diff --git a/demo/app/app.go b/demo/app/app.go index 21a2f176..03e91a0a 100644 --- a/demo/app/app.go +++ b/demo/app/app.go @@ -609,7 +609,7 @@ func NewMeshApp( }) wasmOpts = append(wasmOpts, meshMessageHandler, // add support for the mesh-security queries - wasmkeeper.WithQueryHandlerDecorator(meshseckeeper.NewQueryDecorator(app.MeshSecKeeper, app.SlashingKeeper)), + wasmkeeper.WithQueryHandlerDecorator(meshseckeeper.NewQueryDecorator(app.MeshSecKeeper, app.StakingKeeper, app.SlashingKeeper)), ) // The last arguments can contain custom message handlers, and custom query handlers, // if we want to allow any custom callbacks diff --git a/tests/e2e/slashing_test.go b/tests/e2e/slashing_test.go index 83999268..63c93de2 100644 --- a/tests/e2e/slashing_test.go +++ b/tests/e2e/slashing_test.go @@ -285,6 +285,109 @@ func TestSlashingScenario3(t *testing.T) { require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 185 - max(32, 185) = 185 - 185 = 0 } +func TestValidatorTombstone(t *testing.T) { + 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 := fmt.Sprintf(`{"bond":{"amount":{"denom":"%s", "amount":"200000000"}}}`, x.ProviderDenom) + providerCli.MustExecVault(execMsg) + + // 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, 68_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 tombstoned + myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address()) + tombstoneValidator(t, myExtValidator1ConsAddr, myExtValidator1, x.ConsumerChain, x.ConsumerApp) + + x.ConsumerChain.NextBlock() + + // Assert that the validator's stake has been slashed + // and that the validator has been jailed + validator1, _ = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1) + require.True(t, validator1.IsJailed()) + require.Equal(t, validator1.GetTokens(), sdk.NewInt(36_000_000)) // 20% slash + validator1SigningInfo, _ := x.ConsumerApp.SlashingKeeper.GetValidatorSigningInfo(ctx, myExtValidator1ConsAddr) + require.True(t, validator1SigningInfo.Tombstoned) + + // 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, 178_260_869, providerCli.QueryVaultBalance()) + // Check new max lien + require.Equal(t, 178_260_869, providerCli.QueryMaxLien()) + // Check new slashable amount + require.Equal(t, 61_304_348, providerCli.QuerySlashableAmount()) + // Check new free collateral + require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) + + consumerCli.ExecNewEpoch() + require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath)) + delegation, _ := x.ConsumerApp.StakingKeeper.GetDelegation(ctx, consumerCli.contracts.staking, myExtValidator1) + // Nearly unbond all token + require.Equal(t, delegation.Shares, sdk.MustNewDecFromStr("0.000000388888888889")) +} func TestSlasingImmediateUnbond(t *testing.T) { x := setupExampleChains(t) _, _, providerCli := setupMeshSecurity(t, x) diff --git a/tests/e2e/test_client.go b/tests/e2e/test_client.go index ee124aa2..7df02a23 100644 --- a/tests/e2e/test_client.go +++ b/tests/e2e/test_client.go @@ -418,7 +418,7 @@ func (p *TestConsumerClient) BootstrapContracts(x example) ConsumerContract { virtStakeCodeID := p.chain.StoreCodeFile(buildPathToWasm("mesh_virtual_staking.wasm")).CodeID // instantiate converter codeID = p.chain.StoreCodeFile(buildPathToWasm("mesh_converter.wasm")).CodeID - initMsg = []byte(fmt.Sprintf(`{"price_feed": %q, "discount": %q, "remote_denom": %q,"virtual_staking_code_id": %d, "max_retrieve": %d}`, + initMsg = []byte(fmt.Sprintf(`{"price_feed": %q, "discount": %q, "remote_denom": %q,"virtual_staking_code_id": %d, "max_retrieve": %d, "tombstoned_unbond_enable": true}`, priceFeedContract.String(), discount, remoteDenom, virtStakeCodeID, x.MaxRetrieve)) converterContract := InstantiateContract(p.t, p.chain, codeID, initMsg) diff --git a/tests/e2e/valset_test.go b/tests/e2e/valset_test.go index ebc3ac8f..223f00a7 100644 --- a/tests/e2e/valset_test.go +++ b/tests/e2e/valset_test.go @@ -193,6 +193,24 @@ func unjailValidator(t *testing.T, consAddr sdk.ConsAddress, operatorKeys *secp2 chain.NextBlock() } +func tombstoneValidator(t *testing.T, consAddr sdk.ConsAddress, valAddr sdk.ValAddress, chain *TestChain, app *app.MeshApp) { + e := &types.Equivocation{ + Height: chain.GetContext().BlockHeight(), + Power: 100, + Time: chain.GetContext().BlockTime(), + ConsensusAddress: consAddr.String(), + } + // when + app.EvidenceKeeper.HandleEquivocationEvidence(chain.GetContext(), e) + chain.NextBlock() + + packets := chain.PendingSendPackets + require.Len(t, packets, 1) + tombstoned := gjson.Get(string(packets[0].Data), "valset_update.tombstoned").Array() + require.Len(t, tombstoned, 1, string(packets[0].Data)) + require.Equal(t, valAddr.String(), tombstoned[0].String()) +} + func CreateNewValidator(t *testing.T, operatorKeys *secp256k1.PrivKey, chain *TestChain, power int64) mock.PV { privVal := mock.NewPV() bondCoin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.TokensFromConsensusPower(power, sdk.DefaultPowerReduction)) diff --git a/tests/testdata/mesh_converter.wasm.gz b/tests/testdata/mesh_converter.wasm.gz index 5c6f203c..16534030 100644 Binary files a/tests/testdata/mesh_converter.wasm.gz and b/tests/testdata/mesh_converter.wasm.gz differ diff --git a/tests/testdata/mesh_external_staking.wasm.gz b/tests/testdata/mesh_external_staking.wasm.gz index 63fff499..907cf82b 100644 Binary files a/tests/testdata/mesh_external_staking.wasm.gz and b/tests/testdata/mesh_external_staking.wasm.gz differ diff --git a/tests/testdata/mesh_native_staking.wasm.gz b/tests/testdata/mesh_native_staking.wasm.gz index d1ea0c42..212174ec 100644 Binary files a/tests/testdata/mesh_native_staking.wasm.gz and b/tests/testdata/mesh_native_staking.wasm.gz differ diff --git a/tests/testdata/mesh_native_staking_proxy.wasm.gz b/tests/testdata/mesh_native_staking_proxy.wasm.gz index 76169a9b..d4d9a4f2 100644 Binary files a/tests/testdata/mesh_native_staking_proxy.wasm.gz and b/tests/testdata/mesh_native_staking_proxy.wasm.gz differ diff --git a/tests/testdata/mesh_osmosis_price_provider.wasm.gz b/tests/testdata/mesh_osmosis_price_provider.wasm.gz index c3b28763..4d976637 100644 Binary files a/tests/testdata/mesh_osmosis_price_provider.wasm.gz and b/tests/testdata/mesh_osmosis_price_provider.wasm.gz differ diff --git a/tests/testdata/mesh_remote_price_feed.wasm.gz b/tests/testdata/mesh_remote_price_feed.wasm.gz index 722f16e3..8ed3f646 100644 Binary files a/tests/testdata/mesh_remote_price_feed.wasm.gz and b/tests/testdata/mesh_remote_price_feed.wasm.gz differ diff --git a/tests/testdata/mesh_simple_price_feed.wasm.gz b/tests/testdata/mesh_simple_price_feed.wasm.gz index c7afaa3d..b4570a0a 100644 Binary files a/tests/testdata/mesh_simple_price_feed.wasm.gz and b/tests/testdata/mesh_simple_price_feed.wasm.gz differ diff --git a/tests/testdata/mesh_vault.wasm.gz b/tests/testdata/mesh_vault.wasm.gz index e3761fea..730c65e6 100644 Binary files a/tests/testdata/mesh_vault.wasm.gz and b/tests/testdata/mesh_vault.wasm.gz differ diff --git a/tests/testdata/mesh_virtual_staking.wasm.gz b/tests/testdata/mesh_virtual_staking.wasm.gz index 8583b035..f3b293d8 100644 Binary files a/tests/testdata/mesh_virtual_staking.wasm.gz and b/tests/testdata/mesh_virtual_staking.wasm.gz differ diff --git a/tests/testdata/version.txt b/tests/testdata/version.txt index 4a1ca376..dad38732 100644 --- a/tests/testdata/version.txt +++ b/tests/testdata/version.txt @@ -1 +1 @@ -8292d43b7f2bb59dfa884d3b0b1509ce659b0793 +da87f16dba80c7e649a99d80f8c38f223a574e01 diff --git a/x/meshsecurity/abci_test.go b/x/meshsecurity/abci_test.go index 675d17d5..f0aba28d 100644 --- a/x/meshsecurity/abci_test.go +++ b/x/meshsecurity/abci_test.go @@ -79,7 +79,7 @@ func TestEndBlocker(t *testing.T) { assert: func(t *testing.T, ctx sdk.Context) { require.Len(t, capturedCalls, 2) assert.Equal(t, myContractAddr, capturedCalls[0].contractAddress) - exp := fmt.Sprintf(`{"handle_valset_update":{"additions":[{"address":"%s","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}],"removals":[],"updated":[],"jailed":[],"unjailed":[],"slashed":[],"tombstoned":[]}}`, val1.GetOperator()) + exp := fmt.Sprintf(`{"handle_valset_update":{"additions":[{"address":"%s","commission":"0.000000000000000000","max_commission":"0.000000000000000000","max_change_rate":"0.000000000000000000"}],"removals":[],"updated":[],"jailed":[],"unjailed":[],"tombstoned":[]}}`, val1.GetOperator()) assert.JSONEq(t, exp, string(capturedCalls[0].msg)) assert.Equal(t, myOtherContractAddr, capturedCalls[1].contractAddress) diff --git a/x/meshsecurity/contract/out_message.go b/x/meshsecurity/contract/out_message.go index 9077f6ca..6b939e7c 100644 --- a/x/meshsecurity/contract/out_message.go +++ b/x/meshsecurity/contract/out_message.go @@ -24,6 +24,7 @@ type ( Power int64 `json:"power"` SlashAmount string `json:"slash_amount"` SlashRatio string `json:"slash_ratio"` + IsTombstoned bool `json:"is_tombstoned"` } // ValsetUpdate updates to the active validator set @@ -34,6 +35,6 @@ type ( Jailed []ValidatorAddr `json:"jailed"` Unjailed []ValidatorAddr `json:"unjailed"` Tombstoned []ValidatorAddr `json:"tombstoned"` - Slashed []ValidatorSlash `json:"slashed"` + Slashed []ValidatorSlash `json:"slashed,omitempty"` } ) diff --git a/x/meshsecurity/contract/query.go b/x/meshsecurity/contract/query.go index 3010e3c0..c3b70331 100644 --- a/x/meshsecurity/contract/query.go +++ b/x/meshsecurity/contract/query.go @@ -10,9 +10,10 @@ type ( VirtualStake *VirtualStakeQuery `json:"virtual_stake,omitempty"` } VirtualStakeQuery struct { - BondStatus *BondStatusQuery `json:"bond_status,omitempty"` - AllDelegations *AllDelegationsQuery `json:"all_delegations,omitempty"` - SlashRatio *struct{} `json:"slash_ratio,omitempty"` + BondStatus *BondStatusQuery `json:"bond_status,omitempty"` + SlashRatio *struct{} `json:"slash_ratio,omitempty"` + TotalDelegation *TotalDelegationQuery `json:"total_delegation,omitempty"` + AllDelegations *AllDelegationsQuery `json:"all_delegations,omitempty"` } BondStatusQuery struct { Contract string `json:"contract"` @@ -39,16 +40,24 @@ type ( SlashFractionDowntime string `json:"slash_fraction_downtime"` SlashFractionDoubleSign string `json:"slash_fraction_double_sign"` } + TotalDelegationQuery struct { + Contract string `json:"contract"` + Validator string `json:"validator"` + } + TotalDelegationResponse struct { + // Delegation is the total amount delegated to the validator + Delegation wasmvmtypes.Coin `json:"delegation"` + } ) func ConvertDelegationsToWasm(delegations []types.Delegation) (newDelegations []Delegation) { for _, del := range delegations { delegation := Delegation{ - Delegator: del.DelegatorAddress, - Validator: del.ValidatorAddress, - Amount: del.Amount.String(), - } - newDelegations = append(newDelegations, delegation) + Delegator: del.DelegatorAddress, + Validator: del.ValidatorAddress, + Amount: del.Amount.String(), + } + newDelegations = append(newDelegations, delegation) } return -} \ No newline at end of file +} diff --git a/x/meshsecurity/keeper/adapter.go b/x/meshsecurity/keeper/adapter.go index c7914979..106ba4a2 100644 --- a/x/meshsecurity/keeper/adapter.go +++ b/x/meshsecurity/keeper/adapter.go @@ -114,13 +114,13 @@ func NewStakingDecorator(stakingKeeper slashingtypes.StakingKeeper, k *Keeper) * } // Slash captures the slash event and calls the decorated staking keeper slash method -func (s StakingDecorator) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, power int64, height int64, slashRatio sdk.Dec) math.Int { +func (s StakingDecorator) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec) math.Int { val := s.StakingKeeper.ValidatorByConsAddr(ctx, consAddr) - totalSlashAmount := s.StakingKeeper.Slash(ctx, consAddr, power, height, slashRatio) + totalSlashAmount := s.StakingKeeper.Slash(ctx, consAddr, infractionHeight, power, slashFactor) if val == nil { ModuleLogger(ctx). Error("can not propagate slash: validator not found", "validator", consAddr.String()) - } else if err := s.k.ScheduleSlashed(ctx, val.GetOperator(), power, height, totalSlashAmount, slashRatio); err != nil { + } else if err := s.k.ScheduleSlashed(ctx, val.GetOperator(), power, infractionHeight, totalSlashAmount, slashFactor); err != nil { ModuleLogger(ctx). Error("can not propagate slash: schedule event", "cause", err, @@ -130,7 +130,11 @@ func (s StakingDecorator) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, power } // SlashWithInfractionReason implementation doesn't require the infraction (types.Infraction) to work but is required by Interchain Security. -func (s StakingDecorator) SlashWithInfractionReason(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, _ stakingtypes.Infraction) math.Int { +func (s StakingDecorator) SlashWithInfractionReason(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeight int64, power int64, slashFactor sdk.Dec, infraction stakingtypes.Infraction) math.Int { + if infraction == stakingtypes.Infraction_INFRACTION_DOUBLE_SIGN { + val := s.StakingKeeper.ValidatorByConsAddr(ctx, consAddr) + s.k.SetTombstonedStatus(ctx, val.GetOperator()) + } return s.Slash(ctx, consAddr, infractionHeight, power, slashFactor) } diff --git a/x/meshsecurity/keeper/query_plugin.go b/x/meshsecurity/keeper/query_plugin.go index 05e86e71..675eb64d 100644 --- a/x/meshsecurity/keeper/query_plugin.go +++ b/x/meshsecurity/keeper/query_plugin.go @@ -10,6 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/contract" "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/types" @@ -22,6 +23,11 @@ type ( GetTotalDelegated(ctx sdk.Context, actor sdk.AccAddress) sdk.Coin GetAllDelegations(ctx sdk.Context, actor sdk.AccAddress, maxRetrieve uint16) []types.Delegation } + stakingKeeper interface { + BondDenom(ctx sdk.Context) string + Validator(sdk.Context, sdk.ValAddress) stakingtypes.ValidatorI + Delegation(sdk.Context, sdk.AccAddress, sdk.ValAddress) stakingtypes.DelegationI + } slashingKeeper interface { SlashFractionDoubleSign(ctx sdk.Context) (res sdk.Dec) SlashFractionDowntime(ctx sdk.Context) (res sdk.Dec) @@ -34,9 +40,9 @@ type ( // the mesh-security custom query namespace. // // To be used with `wasmkeeper.WithQueryHandlerDecorator(meshseckeeper.NewQueryDecorator(app.MeshSecKeeper)))` -func NewQueryDecorator(k viewKeeper, sk slashingKeeper) func(wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { +func NewQueryDecorator(k viewKeeper, stk stakingKeeper, slk slashingKeeper) func(wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { return func(next wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { - return ChainedCustomQuerier(k, sk, next) + return ChainedCustomQuerier(k, stk, slk, next) } } @@ -46,11 +52,14 @@ func NewQueryDecorator(k viewKeeper, sk slashingKeeper) func(wasmkeeper.WasmVMQu // // This CustomQuerier is designed as an extension point. See the NewQueryDecorator impl how to // set this up for wasmd. -func ChainedCustomQuerier(k viewKeeper, sk slashingKeeper, next wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { +func ChainedCustomQuerier(k viewKeeper, stk stakingKeeper, slk slashingKeeper, next wasmkeeper.WasmVMQueryHandler) wasmkeeper.WasmVMQueryHandler { if k == nil { panic("ms keeper must not be nil") } - if sk == nil { + if stk == nil { + panic("staking Keeper must not be nil") + } + if slk == nil { panic("slashing Keeper must not be nil") } if next == nil { @@ -82,8 +91,24 @@ func ChainedCustomQuerier(k viewKeeper, sk slashingKeeper, next wasmkeeper.WasmV } case query.SlashRatio != nil: res = contract.SlashRatioResponse{ - SlashFractionDowntime: sk.SlashFractionDowntime(ctx).String(), - SlashFractionDoubleSign: sk.SlashFractionDoubleSign(ctx).String(), + SlashFractionDowntime: slk.SlashFractionDowntime(ctx).String(), + SlashFractionDoubleSign: slk.SlashFractionDoubleSign(ctx).String(), + } + case query.TotalDelegation != nil: + contractAddr, err := sdk.AccAddressFromBech32(query.TotalDelegation.Contract) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrap(query.TotalDelegation.Contract) + } + valAddr, err := sdk.ValAddressFromBech32(query.TotalDelegation.Validator) + if err != nil { + return nil, sdkerrors.ErrInvalidAddress.Wrap(query.TotalDelegation.Validator) + } + + totalShares := stk.Delegation(ctx, contractAddr, valAddr).GetShares() + amount := stk.Validator(ctx, valAddr).TokensFromShares(totalShares).TruncateInt() + totalDelegation := sdk.NewCoin(stk.BondDenom(ctx), amount) + res = contract.TotalDelegationResponse{ + Delegation: wasmkeeper.ConvertSdkCoinToWasmCoin(totalDelegation), } case query.AllDelegations != nil: contractAddr, err := sdk.AccAddressFromBech32(query.AllDelegations.Contract) diff --git a/x/meshsecurity/keeper/query_plugin_test.go b/x/meshsecurity/keeper/query_plugin_test.go index 05e9a160..98785ad1 100644 --- a/x/meshsecurity/keeper/query_plugin_test.go +++ b/x/meshsecurity/keeper/query_plugin_test.go @@ -74,7 +74,7 @@ func TestChainedCustomQuerier(t *testing.T) { }) ctx, _ := pCtx.CacheContext() - gotData, gotErr := ChainedCustomQuerier(spec.viewKeeper, keepers.SlashingKeeper, next).HandleQuery(ctx, myContractAddr, spec.src) + gotData, gotErr := ChainedCustomQuerier(spec.viewKeeper, keepers.StakingKeeper, keepers.SlashingKeeper, next).HandleQuery(ctx, myContractAddr, spec.src) if spec.expErr { require.Error(t, gotErr) return diff --git a/x/meshsecurity/keeper/valset_updates.go b/x/meshsecurity/keeper/valset_updates.go index 992f3d79..71c82370 100644 --- a/x/meshsecurity/keeper/valset_updates.go +++ b/x/meshsecurity/keeper/valset_updates.go @@ -7,8 +7,9 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + gogotypes "github.com/cosmos/gogoproto/types" - "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/contract" + outmessage "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/contract" "github.com/osmosis-labs/mesh-security-sdk/x/meshsecurity/types" ) @@ -75,7 +76,7 @@ func (k Keeper) sendAsync(ctx sdk.Context, op types.PipedValsetOperation, valAdd // ValsetUpdateReport aggregate all stored changes of the current block. Should be called by an end-blocker. // The events reported are categorized by type and not time. Conflicting events as Bonded/ Unbonded // are not supposed to happen within the same block -func (k Keeper) ValsetUpdateReport(ctx sdk.Context) (contract.HandleValsetUpdate, error) { +func (k Keeper) ValsetUpdateReport(ctx sdk.Context) (outmessage.HandleValsetUpdate, error) { var innerErr error appendValidator := func(set *[]wasmvmtypes.Validator, valAddr sdk.ValAddress) bool { val, ok := k.Staking.GetValidator(ctx, valAddr) @@ -86,9 +87,11 @@ func (k Keeper) ValsetUpdateReport(ctx sdk.Context) (contract.HandleValsetUpdate *set = append(*set, ConvertSdkValidatorToWasm(val)) return false } - slashValidator := func(set *[]contract.ValidatorSlash, valAddr sdk.ValAddress, power int64, infractionHeight int64, + slashValidator := func(set *[]outmessage.ValidatorSlash, valAddr sdk.ValAddress, power int64, infractionHeight int64, infractionTime int64, slashAmount string, slashRatio string) bool { - valSlash := contract.ValidatorSlash{ + isTombstoned := k.IsTombstonedStatus(ctx, valAddr) + k.ClearTombstonedStatus(ctx, valAddr) + valSlash := outmessage.ValidatorSlash{ ValidatorAddr: valAddr.String(), Power: power, InfractionHeight: infractionHeight, @@ -97,18 +100,19 @@ func (k Keeper) ValsetUpdateReport(ctx sdk.Context) (contract.HandleValsetUpdate Time: ctx.BlockTime().Unix(), SlashAmount: slashAmount, SlashRatio: slashRatio, + IsTombstoned: isTombstoned, } *set = append(*set, valSlash) return false } - r := contract.HandleValsetUpdate{ // init with empty slices for contract that does not handle null or omitted fields - Additions: make([]contract.Validator, 0), - Removals: make([]contract.ValidatorAddr, 0), - Updated: make([]contract.Validator, 0), - Jailed: make([]contract.ValidatorAddr, 0), - Unjailed: make([]contract.ValidatorAddr, 0), - Tombstoned: make([]contract.ValidatorAddr, 0), - Slashed: make([]contract.ValidatorSlash, 0), + r := outmessage.HandleValsetUpdate{ // init with empty slices for contract that does not handle null or omitted fields + Additions: make([]outmessage.Validator, 0), + Removals: make([]outmessage.ValidatorAddr, 0), + Updated: make([]outmessage.Validator, 0), + Jailed: make([]outmessage.ValidatorAddr, 0), + Unjailed: make([]outmessage.ValidatorAddr, 0), + Tombstoned: make([]outmessage.ValidatorAddr, 0), + Slashed: make([]outmessage.ValidatorSlash, 0), } err := k.iteratePipedValsetOperations(ctx, func(valAddr sdk.ValAddress, op types.PipedValsetOperation, slashInfo *types.SlashInfo) bool { switch op { @@ -125,7 +129,6 @@ func (k Keeper) ValsetUpdateReport(ctx sdk.Context) (contract.HandleValsetUpdate case types.ValidatorModified: return appendValidator(&r.Updated, valAddr) case types.ValidatorSlashed: - // TODO: Add / send the infraction time return slashValidator(&r.Slashed, valAddr, slashInfo.Power, slashInfo.InfractionHeight, 0, slashInfo.TotalSlashAmount, slashInfo.SlashFraction) default: @@ -154,6 +157,39 @@ func (k Keeper) ClearPipedValsetOperations(ctx sdk.Context) { } } +// SetTombstonedStatus sets Tombstoned status for the given validator address in the provided store. +func (k Keeper) SetTombstonedStatus(ctx sdk.Context, valAddr sdk.ValAddress) { + store := ctx.KVStore(k.storeKey) + + bz := k.cdc.MustMarshal(&gogotypes.BoolValue{Value: true}) + store.Set(types.BuildTombstoneStatusKey(valAddr), bz) +} + +// IsTombstonedStatus returns whether validator is tombstoned or not +func (k Keeper) IsTombstonedStatus(ctx sdk.Context, valAddr sdk.ValAddress) bool { + store := ctx.KVStore(k.storeKey) + key := types.BuildTombstoneStatusKey(valAddr) + if !store.Has(key) { + return false + } + + bz := store.Get(key) + if bz == nil { + return false + } + + var enabled gogotypes.BoolValue + k.cdc.MustUnmarshal(bz, &enabled) + + return enabled.Value +} + +// ClearTombstonedStatus delete all entries from the temporary store that contains the validator status. +func (k Keeper) ClearTombstonedStatus(ctx sdk.Context, valAddr sdk.ValAddress) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.BuildTombstoneStatusKey(valAddr)) +} + // iterate through all stored valset updates. Due to the storage key, there are no contract duplicates within an operation type. func (k Keeper) iteratePipedValsetOperations(ctx sdk.Context, cb func(valAddress sdk.ValAddress, op types.PipedValsetOperation, slashInfo *types.SlashInfo) bool) error { pStore := prefix.NewStore(ctx.KVStore(k.memKey), types.PipedValsetPrefix) diff --git a/x/meshsecurity/types/keys.go b/x/meshsecurity/types/keys.go index 1534c089..78e79903 100644 --- a/x/meshsecurity/types/keys.go +++ b/x/meshsecurity/types/keys.go @@ -31,9 +31,9 @@ var ( TotalDelegatedAmountKeyPrefix = []byte{0x3} SchedulerKeyPrefix = []byte{0x4} - PipedValsetPrefix = []byte{0x5} - - DelegationKey = []byte{0x6} + PipedValsetPrefix = []byte{0x5} + TombstoneStatusPrefix = []byte{0x6} + DelegationKey = []byte{0x7} ) type PipedValsetOperation byte @@ -54,6 +54,7 @@ type SlashInfo struct { Power int64 TotalSlashAmount string SlashFraction string + Infraction int32 } // BuildMaxCapLimitKey build max cap limit store key @@ -123,6 +124,11 @@ func BuildPipedValsetOpKey(op PipedValsetOperation, val sdk.ValAddress, slashInf return r } +// BuildTombstoneStatusKey build store key for the tombstone validator status store +func BuildTombstoneStatusKey(valAddr sdk.ValAddress) []byte { + return append(TombstoneStatusPrefix, valAddr.Bytes()...) +} + // BuildDelegationsKey build the delegations's prefix for a contract func BuildDelegationsKey(actor sdk.AccAddress) []byte { return append(DelegationKey, address.MustLengthPrefix(actor)...)