Skip to content

Commit

Permalink
Merge pull request #185 from osmosis-labs/trinity/tombstoned-validato…
Browse files Browse the repository at this point in the history
…r-unbonding

feat: add IsTomestoned in ValidatorSlash
  • Loading branch information
vuong177 authored Sep 5, 2024
2 parents 9c98d29 + 8af72fb commit d1b7af4
Show file tree
Hide file tree
Showing 22 changed files with 243 additions and 41 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
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

0 comments on commit d1b7af4

Please sign in to comment.