Skip to content

Commit

Permalink
new slash mechanism (#335)
Browse files Browse the repository at this point in the history
* handle malicious vote slash as double sign

* add TODO

* add some info when slashing happens

* fix use worng method to get seconds
  • Loading branch information
NathanBSC authored Apr 20, 2023
1 parent f9cb8e3 commit 113b32a
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 36 deletions.
4 changes: 3 additions & 1 deletion types/stake.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const (
Bonded BondStatus = 0x02
)

//BondStatusToString for pretty prints of Bond Status
// BondStatusToString for pretty prints of Bond Status
func BondStatusToString(b BondStatus) string {
switch b {
case 0x00:
Expand Down Expand Up @@ -50,6 +50,7 @@ type Validator interface {
GetDelegatorShares() Dec // Total out standing delegator shares
GetBondHeight() int64 // height in which the validator became active
GetSideChainConsAddr() []byte // validation consensus address on side chain
GetSideChainVoteAddr() []byte // validation vote address on side chain
IsSideChainValidator() bool // if it belongs to side chain
}

Expand All @@ -73,6 +74,7 @@ type ValidatorSet interface {

Validator(Context, ValAddress) Validator // get a particular validator by operator address
ValidatorByConsAddr(Context, ConsAddress) Validator // get a particular validator by consensus address
ValidatorByVoteAddr(Context, []byte) Validator // get a particular validator by vote address
TotalPower(Context) Dec // total power of the validator set

// slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction
Expand Down
24 changes: 17 additions & 7 deletions x/slashing/errors.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//nolint
// nolint
package slashing

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
)

Expand All @@ -21,18 +22,23 @@ const (
CodeSelfDelegationTooLowToUnjail CodeType = 105
CodeInvalidClaim CodeType = 106

CodeExpiredEvidence CodeType = 201
CodeFailSlash CodeType = 202
CodeHandledEvidence CodeType = 203
CodeInvalidEvidence CodeType = 204
CodeInvalidSideChain CodeType = 205
CodeDuplicateDowntimeClaim CodeType = 206
CodeExpiredEvidence CodeType = 201
CodeFailSlash CodeType = 202
CodeHandledEvidence CodeType = 203
CodeInvalidEvidence CodeType = 204
CodeInvalidSideChain CodeType = 205
CodeDuplicateDowntimeClaim CodeType = 206
CodeDuplicateMaliciousVoteClaim CodeType = 207
)

func ErrNoValidatorForAddress(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "that address is not associated with any known validator")
}

func ErrNoValidatorWithVoteAddr(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "that vote address is not associated with any known validator")
}

func ErrBadValidatorAddr(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidValidator, "validator does not exist for that address")
}
Expand Down Expand Up @@ -81,6 +87,10 @@ func ErrDuplicateDowntimeClaim(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeDuplicateDowntimeClaim, "duplicate downtime claim")
}

func ErrDuplicateMaliciousVoteClaim(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeDuplicateMaliciousVoteClaim, "duplicate malicious vote claim")
}

func ErrInvalidInput(codespace sdk.CodespaceType, msg string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidInput, msg)
}
25 changes: 13 additions & 12 deletions x/slashing/execute_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package slashing

import (
"github.com/cosmos/cosmos-sdk/bsc/rlp"
"testing"
"time"

"github.com/cosmos/cosmos-sdk/bsc/rlp"

"github.com/stretchr/testify/require"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -33,8 +34,8 @@ func TestSideChainSlashDowntime(t *testing.T) {
sideHeight := uint64(100)
sideChainId := "bsc"
sideTimestamp := ctx.BlockHeader().Time.Add(-6 * 60 * 60 * time.Second)
claim := SideDowntimeSlashPackage{
SideConsAddr: sideConsAddr,
claim := SideSlashPackage{
SideAddr: sideConsAddr,
SideHeight: sideHeight,
SideChainId: sdk.ChainID(1),
SideTimestamp: uint64(sideTimestamp.Unix()),
Expand Down Expand Up @@ -74,30 +75,30 @@ func TestSideChainSlashDowntime(t *testing.T) {

claim.SideHeight = 0
bz, _ := rlp.EncodeToBytes(&claim)
_, result = keeper.checkSideDowntimeSlashPackage(bz)
_, result = keeper.checkSideSlashPackage(bz)
require.NotNil(t, result)

claim.SideHeight = sideHeight
claim.SideConsAddr = createSideAddr(21)
claim.SideAddr = createSideAddr(21)

result = keeper.slashingSideDowntime(ctx, &claim)
require.NotNil(t, result)

claim.SideConsAddr = sideConsAddr
claim.SideAddr = sideConsAddr
claim.SideTimestamp = uint64(ctx.BlockHeader().Time.Add(-24 * 60 * 60 * time.Second).Unix())
result = keeper.slashingSideDowntime(ctx, &claim)
require.EqualValues(t, CodeExpiredEvidence, result.Code(), "Expected got 201 err code, but got err: %v", result)

claim.SideTimestamp = uint64(ctx.BlockHeader().Time.Add(-6 * 60 * 60 * time.Second).Unix())
claim.SideConsAddr = sideConsAddr
claim.SideAddr = sideConsAddr
claim.SideChainId = sdk.ChainID(2)

result = keeper.slashingSideDowntime(ctx, &claim)
require.NotNil(t, result, "Expected get err, but got nil")
require.EqualValues(t, CodeInvalidSideChain, result.Code(), "Expected got 205 error code, but got err: %v", result)

claim.SideHeight = sideHeight
claim.SideConsAddr = createSideAddr(20)
claim.SideAddr = createSideAddr(20)
claim.SideChainId = sdk.ChainID(1)

result = keeper.slashingSideDowntime(ctx, &claim)
Expand Down Expand Up @@ -138,8 +139,8 @@ func TestSlashDowntimeBalanceVerify(t *testing.T) {

sideHeight := uint64(50)
sideTimestamp := ctx.BlockHeader().Time.Add(-6 * 60 * 60 * time.Second)
claim := SideDowntimeSlashPackage{
SideConsAddr: sideConsAddr2,
claim := SideSlashPackage{
SideAddr: sideConsAddr2,
SideHeight: sideHeight,
SideChainId: sdk.ChainID(1),
SideTimestamp: uint64(sideTimestamp.Unix()),
Expand All @@ -166,8 +167,8 @@ func TestSlashDowntimeBalanceVerify(t *testing.T) {

sideHeight = uint64(80)
sideTimestamp = ctx.BlockHeader().Time.Add(-3 * 60 * 60 * time.Second)
claim = SideDowntimeSlashPackage{
SideConsAddr: sideConsAddr2,
claim = SideSlashPackage{
SideAddr: sideConsAddr2,
SideHeight: sideHeight,
SideChainId: sdk.ChainID(1),
SideTimestamp: uint64(sideTimestamp.Unix()),
Expand Down
146 changes: 132 additions & 14 deletions x/slashing/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,13 @@ func (k *Keeper) SubscribeParamChange(hub types.ParamChangePublisher) {
// implement cross chain app
func (k *Keeper) ExecuteSynPackage(ctx sdk.Context, payload []byte, _ int64) sdk.ExecuteResult {
var resCode uint32
pack, err := k.checkSideDowntimeSlashPackage(payload)
sideSlashPack, err := k.checkSideSlashPackage(payload)
if err == nil {
err = k.slashingSideDowntime(ctx, pack)
if sideSlashPack.addrType == SideConsAddrType {
err = k.slashingSideDowntime(ctx, sideSlashPack)
} else if sideSlashPack.addrType == SideVoteAddrType {
err = k.slashingSideMaliciousVote(ctx, sideSlashPack)
}
}
if err != nil {
resCode = uint32(err.ABCICode())
Expand All @@ -263,14 +267,19 @@ func (k *Keeper) ExecuteFailAckPackage(ctx sdk.Context, payload []byte) sdk.Exec
panic("receive unexpected fail ack package")
}

func (k *Keeper) checkSideDowntimeSlashPackage(payload []byte) (*SideDowntimeSlashPackage, sdk.Error) {
var slashEvent SideDowntimeSlashPackage
func (k *Keeper) checkSideSlashPackage(payload []byte) (*SideSlashPackage, sdk.Error) {
var slashEvent SideSlashPackage
err := rlp.DecodeBytes(payload, &slashEvent)
if err != nil {
return nil, ErrInvalidInput(k.Codespace, "failed to parse the payload")
}
if len(slashEvent.SideConsAddr) != sdk.AddrLen {
return nil, ErrInvalidClaim(k.Codespace, fmt.Sprintf("wrong sideConsAddr length, expected=%d", slashEvent.SideConsAddr))

if len(slashEvent.SideAddr) == sdk.AddrLen {
slashEvent.addrType = SideConsAddrType
} else if len(slashEvent.SideAddr) == sdk.VoteAddrLen {
slashEvent.addrType = SideVoteAddrType
} else {
return nil, ErrInvalidClaim(k.Codespace, fmt.Sprintf("wrong sideAddr length:%d, expected:%d or %d", len(slashEvent.SideAddr), sdk.AddrLen, sdk.VoteAddrLen))
}

if slashEvent.SideHeight <= 0 {
Expand All @@ -287,7 +296,8 @@ func (k *Keeper) checkSideDowntimeSlashPackage(payload []byte) (*SideDowntimeSla
return &slashEvent, nil
}

func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPackage) sdk.Error {
func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideSlashPackage) sdk.Error {
sideConsAddr := pack.SideAddr
sideChainName, err := k.ScKeeper.GetDestChainName(pack.SideChainId)
if err != nil {
return ErrInvalidSideChainId(DefaultCodespace)
Expand All @@ -303,12 +313,12 @@ func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPa
return ErrExpiredEvidence(DefaultCodespace)
}

if k.hasSlashRecord(sideCtx, pack.SideConsAddr, Downtime, pack.SideHeight) {
if k.hasSlashRecord(sideCtx, sideConsAddr, Downtime, pack.SideHeight) {
return ErrDuplicateDowntimeClaim(k.Codespace)
}

slashAmt := k.DowntimeSlashAmount(sideCtx)
validator, slashedAmt, err := k.validatorSet.SlashSideChain(ctx, sideChainName, pack.SideConsAddr, sdk.NewDec(slashAmt))
validator, slashedAmt, err := k.validatorSet.SlashSideChain(ctx, sideChainName, sideConsAddr, sdk.NewDec(slashAmt))
if err != nil {
return ErrFailedToSlash(k.Codespace, err.Error())
}
Expand All @@ -327,7 +337,7 @@ func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPa
var validatorsAllocatedAmt map[string]int64
var found bool
if remaining > 0 {
found, validatorsAllocatedAmt, err = k.validatorSet.AllocateSlashAmtToValidators(sideCtx, pack.SideConsAddr, sdk.NewDec(remaining))
found, validatorsAllocatedAmt, err = k.validatorSet.AllocateSlashAmtToValidators(sideCtx, sideConsAddr, sdk.NewDec(remaining))
if err != nil {
return ErrFailedToSlash(k.Codespace, err.Error())
}
Expand All @@ -340,7 +350,7 @@ func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPa

jailUntil := header.Time.Add(k.DowntimeUnbondDuration(sideCtx))
sr := SlashRecord{
ConsAddr: pack.SideConsAddr,
ConsAddr: sideConsAddr,
InfractionType: Downtime,
InfractionHeight: pack.SideHeight,
SlashHeight: header.Height,
Expand All @@ -351,12 +361,14 @@ func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPa
k.setSlashRecord(sideCtx, sr)

// Set or updated validator jail duration
signInfo, found := k.getValidatorSigningInfo(sideCtx, pack.SideConsAddr)
signInfo, found := k.getValidatorSigningInfo(sideCtx, sideConsAddr)
if !found {
return sdk.ErrInternal(fmt.Sprintf("Expected signing info for validator %s but not found", sdk.HexEncode(pack.SideConsAddr)))
return sdk.ErrInternal(fmt.Sprintf("Expected signing info for validator %s but not found", sdk.HexEncode(sideConsAddr)))
}
//if jailUntil.After(signInfo.JailedUntil) {
signInfo.JailedUntil = jailUntil
k.setValidatorSigningInfo(sideCtx, pack.SideConsAddr, signInfo)
//}
k.setValidatorSigningInfo(sideCtx, sideConsAddr, signInfo)

if k.PbsbServer != nil {
event := SideSlashEvent{
Expand All @@ -376,6 +388,112 @@ func (k *Keeper) slashingSideDowntime(ctx sdk.Context, pack *SideDowntimeSlashPa
return nil
}

func (k *Keeper) slashingSideMaliciousVote(ctx sdk.Context, pack *SideSlashPackage) sdk.Error {
logger := ctx.Logger().With("module", "x/slashing")
sideVoteAddr := pack.SideAddr
sideChainName, err := k.ScKeeper.GetDestChainName(pack.SideChainId)
if err != nil {
return ErrInvalidSideChainId(DefaultCodespace)
}
sideCtx, err := k.ScKeeper.PrepareCtxForSideChain(ctx, sideChainName)
if err != nil {
return ErrInvalidSideChainId(DefaultCodespace)
}

header := sideCtx.BlockHeader()
age := uint64(header.Time.Unix()) - pack.SideTimestamp
maxEvidenceAge := uint64(k.MaxEvidenceAge(sideCtx).Seconds())
if age > maxEvidenceAge {
return ErrExpiredEvidence(DefaultCodespace)
}

validator := k.validatorSet.ValidatorByVoteAddr(sideCtx, sideVoteAddr)
if validator == nil {
return ErrNoValidatorWithVoteAddr(k.Codespace)
}
// important!!!
// validator.GetSideChainVoteAddr() may not equal to sideVoteAddr
// because validator may edit vote addr, but previous vote addr would still point to this validator
// so validator can't escape from slashing by editing vote addr
// But once the voting private key is leaked, validator can't save itself by editing vote addr at the same time
//
// TODO: use snapshot to get mapping from vote addr to cons addr, then slash according to it;
// this will allow validator to edit it's vote addr when vote private key is leaked
sideConsAddr := []byte(validator.GetSideChainConsAddr())
signInfo, found := k.getValidatorSigningInfo(sideCtx, sideConsAddr)
if !found {
return sdk.ErrInternal(fmt.Sprintf("Expected signing info for validator %s but not found", sdk.HexEncode(sideConsAddr)))
}
// in duration of malicious vote slash, validator can only be slashed once, to protect validator from funds drained
if k.isMaliciousVoteSlashed(sideCtx, sideConsAddr) && pack.SideTimestamp < uint64(signInfo.JailedUntil.Unix()) {
logger.Info(fmt.Sprintf("slashing is blocked because %s is still in duration of lastest malicious vote slash", sideConsAddr))
return ErrFailedToSlash(k.Codespace, "still in duration of lastest malicious vote slash")
} else if k.hasSlashRecord(sideCtx, sideConsAddr, MaliciousVote, pack.SideHeight) {
logger.Info("slashing is blocked for duplicate malicious vote claim")
return ErrDuplicateMaliciousVoteClaim(k.Codespace)
}

// Malicious vote confirmed
logger.Info(fmt.Sprintf("Confirmed malicious vote from %s at height %d, age %d is less than max age %d, summit at %d, jailed until %d before slashing",
sdk.HexAddress(sideConsAddr), pack.SideHeight, age, maxEvidenceAge, pack.SideTimestamp, uint64(signInfo.JailedUntil.Unix())))

slashAmt := k.DoubleSignSlashAmount(sideCtx)
validator, slashedAmt, err := k.validatorSet.SlashSideChain(ctx, sideChainName, sideConsAddr, sdk.NewDec(slashAmt))
if err != nil {
return ErrFailedToSlash(k.Codespace, err.Error())
}

var toFeePool int64
var validatorsCompensation map[string]int64
if slashAmt > 0 {
found, validatorsCompensation, err = k.validatorSet.AllocateSlashAmtToValidators(sideCtx, sideConsAddr, sdk.NewDec(slashAmt))
if err != nil {
return ErrFailedToSlash(k.Codespace, err.Error())
}
if !found && ctx.IsDeliverTx() {
bondDenom := k.validatorSet.BondDenom(sideCtx)
toFeePool = slashAmt
remainingCoin := sdk.NewCoin(bondDenom, slashAmt)
fees.Pool.AddAndCommitFee("side_malicious_vote_slash", sdk.NewFee(sdk.Coins{remainingCoin}, sdk.FeeForAll))
}
}

// Set or updated validator jail duration
jailUntil := header.Time.Add(k.DoubleSignUnbondDuration(sideCtx))
sr := SlashRecord{
ConsAddr: sideConsAddr,
InfractionType: MaliciousVote,
InfractionHeight: pack.SideHeight,
SlashHeight: header.Height,
JailUntil: jailUntil,
SlashAmt: slashedAmt.RawInt(),
SideChainId: sideChainName,
}
k.setSlashRecord(sideCtx, sr)

if jailUntil.After(signInfo.JailedUntil) {
signInfo.JailedUntil = jailUntil
}
k.setValidatorSigningInfo(sideCtx, sideConsAddr, signInfo)

if k.PbsbServer != nil {
event := SideSlashEvent{
Validator: validator.GetOperator(),
InfractionType: MaliciousVote,
InfractionHeight: int64(pack.SideHeight),
SlashHeight: header.Height,
JailUtil: jailUntil,
SlashAmt: slashedAmt.RawInt(),
ToFeePool: toFeePool,
SideChainId: sideChainName,
ValidatorsCompensation: validatorsCompensation,
}
k.PbsbServer.Publish(event)
}

return nil
}

// TODO: Make a method to remove the pubkey from the map when a validator is unbonded.
func (k Keeper) addPubkey(ctx sdk.Context, pubkey crypto.PubKey) {
addr := pubkey.Address()
Expand Down
Loading

0 comments on commit 113b32a

Please sign in to comment.