Skip to content

Commit

Permalink
Add tombstone testcase and finish logic for auto unbond tombstoned va…
Browse files Browse the repository at this point in the history
…lidator
  • Loading branch information
trinitys7 committed Aug 29, 2024
1 parent ddec50f commit 217b512
Show file tree
Hide file tree
Showing 15 changed files with 153 additions and 15 deletions.
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
110 changes: 107 additions & 3 deletions tests/e2e/slashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestSlashingScenario1(t *testing.T) {

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
validator1, _ = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(41_400_000)) // 10% slash

Expand Down Expand Up @@ -178,7 +178,7 @@ func TestSlashingScenario2(t *testing.T) {

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
validator1, _ = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(81_900_000)) // 10% slash

Expand Down Expand Up @@ -265,7 +265,7 @@ func TestSlashingScenario3(t *testing.T) {

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
validator1, _ = 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)

Expand All @@ -284,3 +284,107 @@ func TestSlashingScenario3(t *testing.T) {
// Check new free collateral
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"))
}
4 changes: 2 additions & 2 deletions tests/e2e/valset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func unjailValidator(t *testing.T, consAddr sdk.ConsAddress, operatorKeys *secp2
chain.NextBlock()
}

func tombstoneValidator(t *testing.T, consAddr sdk.ConsAddress, operatorKeys *secp256k1.PrivKey, chain *TestChain, app *app.MeshApp) {
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,
Expand All @@ -208,7 +208,7 @@ func tombstoneValidator(t *testing.T, consAddr sdk.ConsAddress, operatorKeys *se
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, sdk.ValAddress(operatorKeys.PubKey().Address()).String(), tombstoned[0].String())
require.Equal(t, valAddr.String(), tombstoned[0].String())
}

func CreateNewValidator(t *testing.T, operatorKeys *secp256k1.PrivKey, chain *TestChain, power int64) mock.PV {
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.
13 changes: 11 additions & 2 deletions x/meshsecurity/contract/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ type (
VirtualStake *VirtualStakeQuery `json:"virtual_stake,omitempty"`
}
VirtualStakeQuery struct {
BondStatus *BondStatusQuery `json:"bond_status,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"`
}
BondStatusQuery struct {
Contract string `json:"contract"`
Expand All @@ -23,4 +24,12 @@ 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"`
}
)
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"
)
Expand All @@ -20,6 +21,11 @@ type (
GetMaxCapLimit(ctx sdk.Context, actor sdk.AccAddress) sdk.Coin
GetTotalDelegated(ctx sdk.Context, actor sdk.AccAddress) sdk.Coin
}
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 @@ -32,9 +38,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 @@ -44,11 +50,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 @@ -80,8 +89,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),
}
default:
return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown virtual_stake query variant"}
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 @@ -70,7 +70,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

0 comments on commit 217b512

Please sign in to comment.