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

feat: add IsTomestoned in ValidatorSlash #185

Merged
merged 9 commits into from
Sep 5, 2024
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
2 changes: 1 addition & 1 deletion demo/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions tests/e2e/slashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 18 additions & 0 deletions tests/e2e/valset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Binary file modified tests/testdata/mesh_converter.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_external_staking.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_native_staking.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_native_staking_proxy.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_osmosis_price_provider.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_remote_price_feed.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_simple_price_feed.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_vault.wasm.gz
Binary file not shown.
Binary file modified tests/testdata/mesh_virtual_staking.wasm.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/testdata/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8292d43b7f2bb59dfa884d3b0b1509ce659b0793
da87f16dba80c7e649a99d80f8c38f223a574e01
2 changes: 1 addition & 1 deletion x/meshsecurity/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion x/meshsecurity/contract/out_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}
)
27 changes: 18 additions & 9 deletions x/meshsecurity/contract/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
}
}
12 changes: 8 additions & 4 deletions x/meshsecurity/keeper/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}

Expand Down
37 changes: 31 additions & 6 deletions x/meshsecurity/keeper/query_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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)
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion x/meshsecurity/keeper/query_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading