diff --git a/demo/app/test_helpers.go b/demo/app/test_helpers.go index 81df863e..c91bb04a 100644 --- a/demo/app/test_helpers.go +++ b/demo/app/test_helpers.go @@ -378,7 +378,10 @@ func GenesisStateWithValSet( ValidatorSigningInfo: slashingtypes.ValidatorSigningInfo{}, } } - slashingGenesis := slashingtypes.NewGenesisState(slashingtypes.DefaultParams(), signingInfos, nil) + slashingParams := slashingtypes.DefaultParams() + slashingParams.SlashFractionDowntime = math.LegacyNewDec(1).Quo(math.LegacyNewDec(10)) + slashingParams.SlashFractionDoubleSign = math.LegacyNewDec(1).Quo(math.LegacyNewDec(10)) + slashingGenesis := slashingtypes.NewGenesisState(slashingParams, signingInfos, nil) genesisState[slashingtypes.ModuleName] = codec.MustMarshalJSON(slashingGenesis) // add bonded amount to bonded pool module account diff --git a/tests/e2e/slashing_test.go b/tests/e2e/slashing_test.go new file mode 100644 index 00000000..b0484d66 --- /dev/null +++ b/tests/e2e/slashing_test.go @@ -0,0 +1,285 @@ +package e2e + +import ( + "cosmossdk.io/math" + "encoding/base64" + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSlashingScenario1(t *testing.T) { + // Slashing scenario 1: + // https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-1-slashed-delegator-has-free-collateral-on-the-vault + // + // - We use millions instead of unit tokens. + x := setupExampleChains(t) + consumerCli, _, providerCli := setupMeshSecurity(t, x) + + // Provider chain + // ============== + // Deposit - A user deposits the vault denom to provide some collateral to their account + execMsg := `{"bond":{}}` + providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + + // Stake Locally - A user triggers a local staking action to a chosen validator. + myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() + execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`, + x.ProviderDenom, 190_000_000, + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr)))) + providerCli.MustExecVault(execLocalStakingMsg) + + assert.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance()) + + // Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains. + myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address) + myExtValidator1Addr := myExtValidator1.String() + err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 100_000_000)) + require.NoError(t, err) + myExtValidator2 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[2].Address) + myExtValidator2Addr := myExtValidator2.String() + err = providerCli.ExecStakeRemote(myExtValidator2Addr, sdk.NewInt64Coin(x.ProviderDenom, 50_000_000)) + require.NoError(t, err) + + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Check collateral + require.Equal(t, 200_000_000, providerCli.QueryVaultBalance()) + // Check max lien + require.Equal(t, 190_000_000, providerCli.QueryMaxLien()) + // Check slashable amount + require.Equal(t, 34_000_000, providerCli.QuerySlashableAmount()) + // Check free collateral + require.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance()) // 200 - max(34, 190) = 200 - 190 = 10 + + // Consumer chain + // ==================== + // + // then delegated amount is not updated before the epoch + consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet + + // when an epoch ends, the delegation rebalance is triggered + consumerCli.ExecNewEpoch() + + // then the total delegated amount is updated + consumerCli.assertTotalDelegated(math.NewInt(67_500_000)) // 150_000_000 / 2 * (1 - 0.1) + + // and the delegated amount is updated for the validators + consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("45")) // 100_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor + consumerCli.assertShare(myExtValidator2, math.LegacyMustNewDecFromStr("22.5")) // 50_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor + + ctx := x.ConsumerChain.GetContext() + validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, found) + require.False(t, validator1.IsJailed()) + // Off by 1_000_000, because of validator self bond on setup + require.Equal(t, validator1.GetTokens(), sdk.NewInt(46_000_000)) + validator2, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator2) + require.True(t, found) + require.False(t, validator2.IsJailed()) + // Off by 1_000_000, because of validator self bond on setup + require.Equal(t, validator2.GetTokens(), sdk.NewInt(23_500_000)) + + // Validator 1 on the Consumer chain is jailed + myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address()) + jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp) + + x.ConsumerChain.NextBlock() + + // Assert that the validator's stake has been slashed + // and that the validator has been jailed + validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(41_400_000)) // 10% slash + + // Relay IBC packets to the Provider chain + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Next block on the Provider chain + x.ProviderChain.NextBlock() + + // Check new collateral + require.Equal(t, 190_000_000, providerCli.QueryVaultBalance()) + // Check new max lien + require.Equal(t, 190_000_000, providerCli.QueryMaxLien()) + // Check new slashable amount + require.Equal(t, 33_000_000, providerCli.QuerySlashableAmount()) + // Check new free collateral + require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 190 - max(33, 190) = 190 - 190 = 0 +} + +func TestSlashingScenario2(t *testing.T) { + // Slashing scenario 2: + // https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-2-slashed-delegator-has-no-free-collateral-on-the-vault + // + // - We use millions instead of unit tokens. + x := setupExampleChains(t) + consumerCli, _, providerCli := setupMeshSecurity(t, x) + + // Provider chain + // ============== + // Deposit - A user deposits the vault denom to provide some collateral to their account + execMsg := `{"bond":{}}` + providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + + // Stake Locally - A user triggers a local staking action to a chosen validator. + myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() + execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`, + x.ProviderDenom, 200_000_000, + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr)))) + providerCli.MustExecVault(execLocalStakingMsg) + + // Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains. + myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address) + myExtValidator1Addr := myExtValidator1.String() + err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + require.NoError(t, err) + + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Check collateral + require.Equal(t, 200_000_000, providerCli.QueryVaultBalance()) + // Check max lien + require.Equal(t, 200_000_000, providerCli.QueryMaxLien()) + // Check slashable amount + require.Equal(t, 40_000_000, providerCli.QuerySlashableAmount()) + // Check free collateral + require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 200 - max(40, 200) = 200 - 200 = 0 + + // Consumer chain + // ==================== + // + // then delegated amount is not updated before the epoch + consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet + + // when an epoch ends, the delegation rebalance is triggered + consumerCli.ExecNewEpoch() + + // then the total delegated amount is updated + consumerCli.assertTotalDelegated(math.NewInt(90_000_000)) // 200_000_000 / 2 * (1 - 0.1) + + // and the delegated amount is updated for the validators + consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("90")) // 200_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor + + ctx := x.ConsumerChain.GetContext() + validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, found) + require.False(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(91_000_000)) + + // Validator 1 on the Consumer chain is jailed + myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address()) + jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp) + + x.ConsumerChain.NextBlock() + + // Assert that the validator's stake has been slashed + // and that the validator has been jailed + validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(81_900_000)) // 10% slash + + // Relay IBC packets to the Provider chain + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Next block on the Provider chain + x.ProviderChain.NextBlock() + + // Check new collateral + require.Equal(t, 180_000_000, providerCli.QueryVaultBalance()) + // Check new max lien + require.Equal(t, 180_000_000, providerCli.QueryMaxLien()) + // Check new slashable amount + require.Equal(t, 36_000_000, providerCli.QuerySlashableAmount()) + // Check new free collateral + require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 190 - max(36, 190) = 190 - 190 = 0 +} + +func TestSlashingScenario3(t *testing.T) { + // Slashing scenario 3: + // https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-3-slashed-delegator-has-some-free-collateral-on-the-vault + // + // - We use millions instead of unit tokens. + x := setupExampleChains(t) + consumerCli, _, providerCli := setupMeshSecurity(t, x) + + // Provider chain + // ============== + // Deposit - A user deposits the vault denom to provide some collateral to their account + execMsg := `{"bond":{}}` + providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000)) + + // Stake Locally - A user triggers a local staking action to a chosen validator. + myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String() + execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`, + x.ProviderDenom, 190_000_000, + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr)))) + providerCli.MustExecVault(execLocalStakingMsg) + + // Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains. + myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address) + myExtValidator1Addr := myExtValidator1.String() + err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 150_000_000)) + require.NoError(t, err) + + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Check collateral + require.Equal(t, 200_000_000, providerCli.QueryVaultBalance()) + // Check max lien + require.Equal(t, 190_000_000, providerCli.QueryMaxLien()) + // Check slashable amount + require.Equal(t, 34_000_000, providerCli.QuerySlashableAmount()) + // Check free collateral + require.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance()) // 200 - max(34, 190) = 200 - 190 = 10 + + // Consumer chain + // ==================== + // + // then delegated amount is not updated before the epoch + consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet + + // when an epoch ends, the delegation rebalance is triggered + consumerCli.ExecNewEpoch() + + // then the total delegated amount is updated + consumerCli.assertTotalDelegated(math.NewInt(67_500_000)) // 150_000_000 / 2 * (1 - 0.1) + + // and the delegated amount is updated for the validators + consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("67.5")) // 150_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor + + ctx := x.ConsumerChain.GetContext() + validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, found) + require.False(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(68_500_000)) + + // Validator 1 on the Consumer chain is jailed + myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address()) + jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp) + + x.ConsumerChain.NextBlock() + + // Assert that the validator's stake has been slashed + // and that the validator has been jailed + validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(61_700_000)) // 10% slash (plus 50_000 rounding) + + // Relay IBC packets to the Provider chain + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + + // Next block on the Provider chain + x.ProviderChain.NextBlock() + + // Check new collateral + require.Equal(t, 185_000_000, providerCli.QueryVaultBalance()) + // Check new max lien + require.Equal(t, 185_000_000, providerCli.QueryMaxLien()) + // Check new slashable amount + require.Equal(t, 32_000_000, providerCli.QuerySlashableAmount()) + // Check new free collateral + require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 185 - max(32, 185) = 185 - 185 = 0 +} diff --git a/tests/e2e/test_client.go b/tests/e2e/test_client.go index 865f86e9..475787a0 100644 --- a/tests/e2e/test_client.go +++ b/tests/e2e/test_client.go @@ -82,7 +82,7 @@ func (p *TestProviderClient) BootstrapContracts(connId, portID string) ProviderC var ( unbondingPeriod = 21 * 24 * 60 * 60 // 21 days - make configurable? maxLocalSlashing = "0.10" - maxExtSlashing = "0.05" + maxExtSlashing = "0.10" rewardTokenDenom = sdk.DefaultBondDenom localTokenDenom = sdk.DefaultBondDenom ) @@ -196,6 +196,32 @@ func (p TestProviderClient) QueryVaultFreeBalance() int { return ParseHighLow(p.t, qRsp["free"]).Low } +func (p TestProviderClient) QueryVaultBalance() int { + qRsp := p.QueryVault(Query{ + "account_details": {"account": p.chain.SenderAccount.GetAddress().String()}, + }) + require.NotEmpty(p.t, qRsp["bonded"], qRsp) + b, err := strconv.Atoi(qRsp["bonded"].(string)) + require.NoError(p.t, err) + return b +} + +func (p TestProviderClient) QueryMaxLien() int { + qRsp := p.QueryVault(Query{ + "account_details": {"account": p.chain.SenderAccount.GetAddress().String()}, + }) + require.NotEmpty(p.t, qRsp["max_lien"], qRsp) + return ParseHighLow(p.t, qRsp["max_lien"]).Low +} + +func (p TestProviderClient) QuerySlashableAmount() int { + qRsp := p.QueryVault(Query{ + "account_details": {"account": p.chain.SenderAccount.GetAddress().String()}, + }) + require.NotEmpty(p.t, qRsp["total_slashable"], qRsp) + return ParseHighLow(p.t, qRsp["total_slashable"]).Low +} + type TestConsumerClient struct { t *testing.T chain *ibctesting.TestChain diff --git a/tests/e2e/valset_test.go b/tests/e2e/valset_test.go index 4758d827..7d22eba9 100644 --- a/tests/e2e/valset_test.go +++ b/tests/e2e/valset_test.go @@ -25,7 +25,7 @@ import ( func TestValsetTransitions(t *testing.T) { operatorKeys := secp256k1.GenPrivKey() setupVal := func(t *testing.T, x example) mock.PV { - myVal := CreateNewValidator(t, operatorKeys, x.ConsumerChain) + myVal := CreateNewValidator(t, operatorKeys, x.ConsumerChain, 2) require.Len(t, x.ConsumerChain.Vals.Validators, 5) return myVal } @@ -39,7 +39,7 @@ func TestValsetTransitions(t *testing.T) { return mock.PV{} }, doTransition: func(t *testing.T, _ mock.PV, x example) { - CreateNewValidator(t, operatorKeys, x.ConsumerChain) + CreateNewValidator(t, operatorKeys, x.ConsumerChain, 2) require.Len(t, x.ConsumerChain.Vals.Validators, 5) }, assertPackets: func(t *testing.T, packets []channeltypes.Packet) { @@ -82,7 +82,7 @@ func TestValsetTransitions(t *testing.T) { }, "jailed to active": { setup: func(t *testing.T, x example) mock.PV { - val := setupVal(t, x) + val := CreateNewValidator(t, operatorKeys, x.ConsumerChain, 200) jailValidator(t, sdk.ConsAddress(val.PrivKey.PubKey().Address()), x.Coordinator, x.ConsumerChain, x.ConsumerApp) x.ConsumerChain.NextBlock() require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) @@ -103,14 +103,14 @@ func TestValsetTransitions(t *testing.T) { }, "jailed to remove": { setup: func(t *testing.T, x example) mock.PV { - val := setupVal(t, x) + val := CreateNewValidator(t, operatorKeys, x.ConsumerChain, 200) t.Log("jail validator") jailValidator(t, sdk.ConsAddress(val.PrivKey.PubKey().Address()), x.Coordinator, x.ConsumerChain, x.ConsumerApp) t.Log("Add new validator") otherOperator := secp256k1.GenPrivKey() x.ConsumerChain.Fund(sdk.AccAddress(otherOperator.PubKey().Address()), sdkmath.NewInt(1_000_000_000)) - CreateNewValidator(t, otherOperator, x.ConsumerChain) // add a now val to fill the slot + CreateNewValidator(t, otherOperator, x.ConsumerChain, 200) // add a now val to fill the slot x.ConsumerChain.NextBlock() require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) @@ -174,7 +174,8 @@ func jailValidator(t *testing.T, consAddr sdk.ConsAddress, coordinator *wasmibct ctx = chain.GetContext() signInfo.MissedBlocksCounter = app.SlashingKeeper.MinSignedPerWindow(ctx) app.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signInfo) - app.SlashingKeeper.HandleValidatorSignature(ctx, cryptotypes.Address(consAddr), 100, false) + power := app.StakingKeeper.GetLastValidatorPower(ctx, sdk.ValAddress(consAddr)) + app.SlashingKeeper.HandleValidatorSignature(ctx, cryptotypes.Address(consAddr), power, false) // when updates trigger chain.NextBlock() } @@ -192,9 +193,9 @@ func unjailValidator(t *testing.T, consAddr sdk.ConsAddress, operatorKeys *secp2 chain.NextBlock() } -func CreateNewValidator(t *testing.T, operatorKeys *secp256k1.PrivKey, chain *wasmibctesting.TestChain) mock.PV { +func CreateNewValidator(t *testing.T, operatorKeys *secp256k1.PrivKey, chain *wasmibctesting.TestChain, power int64) mock.PV { privVal := mock.NewPV() - bondCoin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.TokensFromConsensusPower(2, sdk.DefaultPowerReduction)) + bondCoin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.TokensFromConsensusPower(power, sdk.DefaultPowerReduction)) description := stakingtypes.NewDescription("my new val", "", "", "", "") commissionRates := stakingtypes.NewCommissionRates(sdkmath.LegacyZeroDec(), sdkmath.LegacyNewDec(1), sdkmath.LegacyNewDec(1)) createValidatorMsg, err := stakingtypes.NewMsgCreateValidator(