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: implement v0.50 slashing bitmap logic #548

Merged
merged 4 commits into from
Mar 7, 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/99designs/keyring v1.2.1
github.com/armon/go-metrics v0.4.1
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816
github.com/bits-and-blooms/bitset v1.8.0
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/chzyer/readline v1.5.1
github.com/cockroachdb/apd/v2 v2.0.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ=
Expand Down
1 change: 1 addition & 0 deletions simapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/bits-and-blooms/bitset v1.8.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions simapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ=
Expand Down
1 change: 1 addition & 0 deletions tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
github.com/bits-and-blooms/bitset v1.8.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s=
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ=
Expand Down
3 changes: 1 addition & 2 deletions x/slashing/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper)
// Iterate over all the validators which *should* have signed this block
// store whether or not they have actually signed it and slash/unbond any
// which have missed too many blocks in a row (downtime slashing)
params := k.GetParams(ctx)
for _, voteInfo := range req.LastCommitInfo.GetVotes() {
k.HandleValidatorSignatureWithParams(ctx, params, voteInfo.Validator.Address, voteInfo.Validator.Power, voteInfo.SignedLastBlock)
k.HandleValidatorSignature(ctx, voteInfo.Validator.Address, voteInfo.Validator.Power, voteInfo.SignedLastBlock)
Dismissed Show dismissed Hide dismissed
}
}
9 changes: 7 additions & 2 deletions x/slashing/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ func (keeper Keeper) InitGenesis(ctx sdk.Context, stakingKeeper types.StakingKee
panic(err)
}
for _, missed := range array.MissedBlocks {
keeper.SetValidatorMissedBlockBitArray(ctx, address, missed.Index, missed.Missed)
if err := keeper.SetMissedBlockBitmapValue(ctx, address, missed.Index, missed.Missed); err != nil {
panic(err)
}
}
}

Expand All @@ -57,7 +59,10 @@ func (keeper Keeper) ExportGenesis(ctx sdk.Context) (data *types.GenesisState) {
ValidatorSigningInfo: info,
})

localMissedBlocks := keeper.GetValidatorMissedBlocks(ctx, address)
localMissedBlocks, err := keeper.GetValidatorMissedBlocks(ctx, address)
if err != nil {
panic(err)
}

missedBlocks = append(missedBlocks, types.ValidatorMissedBlocks{
Address: bechAddr,
Expand Down
29 changes: 23 additions & 6 deletions x/slashing/keeper/infractions.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,31 @@
// Update signed block bit array & counter
// This counter just tracks the sum of the bit array
// That way we avoid needing to read/write the whole array each time
previous := k.GetValidatorMissedBlockBitArray(ctx, consAddr, index)
previous, err := k.GetMissedBlockBitmapValue(ctx, consAddr, index)
if err != nil {
panic(fmt.Sprintf("Expected to get missed block bitmap value for validator %s at index %d but not found, error: %v", consAddr, index, err))

Check warning

Code scanning / CodeQL

Panic in BeginBock or EndBlock consensus methods Warning

Possible panics in BeginBock- or EndBlock-related consensus methods could cause a chain halt
}

missed := !signed
switch {
case !previous && missed:
// Array value has changed from not missed to missed, increment counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, true)
// Bitmap value has changed from not missed to missed, so we flip the bit
// and increment the counter.
if err := k.SetMissedBlockBitmapValue(ctx, consAddr, index, true); err != nil {
panic(fmt.Sprintf("Expected to set missed block bitmap value for validator %s at index %d but not found, error: %v", consAddr, index, err))

Check warning

Code scanning / CodeQL

Panic in BeginBock or EndBlock consensus methods Warning

Possible panics in BeginBock- or EndBlock-related consensus methods could cause a chain halt
}

signInfo.MissedBlocksCounter++

case previous && !missed:
// Array value has changed from missed to not missed, decrement counter
k.SetValidatorMissedBlockBitArray(ctx, consAddr, index, false)
// Bitmap value has changed from missed to not missed, so we flip the bit
// and decrement the counter.
if err := k.SetMissedBlockBitmapValue(ctx, consAddr, index, false); err != nil {
panic(fmt.Sprintf("Expected to set missed block bitmap value for validator %s at index %d but not found, error: %v", consAddr, index, err))

Check warning

Code scanning / CodeQL

Panic in BeginBock or EndBlock consensus methods Warning

Possible panics in BeginBock- or EndBlock-related consensus methods could cause a chain halt
}

signInfo.MissedBlocksCounter--

default:
// Array value at this index has not changed, no need to update counter
}
Expand Down Expand Up @@ -115,7 +129,10 @@
// We need to reset the counter & array so that the validator won't be immediately slashed for downtime upon rebonding.
signInfo.MissedBlocksCounter = 0
signInfo.IndexOffset = 0
k.clearValidatorMissedBlockBitArray(ctx, consAddr)
err = k.DeleteMissedBlockBitmap(ctx, consAddr)
if err != nil {
panic(fmt.Sprintf("Expected to delete missed block bitmap for validator %s but not found, error: %v", consAddr, err))

Check warning

Code scanning / CodeQL

Panic in BeginBock or EndBlock consensus methods Warning

Possible panics in BeginBock- or EndBlock-related consensus methods could cause a chain halt
}

logger.Info(
"slashing and jailing validator due to liveness fault",
Expand Down
8 changes: 8 additions & 0 deletions x/slashing/keeper/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/slashing/exported"
v2 "github.com/cosmos/cosmos-sdk/x/slashing/migrations/v2"
v3 "github.com/cosmos/cosmos-sdk/x/slashing/migrations/v3"
v4 "github.com/cosmos/cosmos-sdk/x/slashing/migrations/v4"
)

// Migrator is a struct for handling in-place store migrations.
Expand All @@ -30,3 +31,10 @@ func (m Migrator) Migrate1to2(ctx sdk.Context) error {
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
return v3.Migrate(ctx, ctx.KVStore(m.keeper.storeKey), m.legacySubspace, m.keeper.cdc)
}

// Migrate3to4 migrates the x/slashing module state from the consensus
// version 3 to version 4. Specifically, it migrates the validator missed block
// bitmap.
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
return v4.Migrate(ctx, m.keeper.cdc, ctx.KVStore(m.keeper.storeKey), m.keeper.GetParams(ctx))
}
185 changes: 129 additions & 56 deletions x/slashing/keeper/signing_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package keeper
import (
"time"

gogotypes "github.com/cosmos/gogoproto/types"
"github.com/bits-and-blooms/bitset"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/slashing/types"
)

Expand Down Expand Up @@ -54,53 +55,6 @@ func (k Keeper) IterateValidatorSigningInfos(ctx sdk.Context,
}
}

// GetValidatorMissedBlockBitArray gets the bit for the missed blocks array
func (k Keeper) GetValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64) bool {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.ValidatorMissedBlockBitArrayKey(address, index))
var missed gogotypes.BoolValue
if bz == nil {
// lazy: treat empty key as not missed
return false
}
k.cdc.MustUnmarshal(bz, &missed)

return missed.Value
}

// IterateValidatorMissedBlockBitArray iterates over the signed blocks window
// and performs a callback function
func (k Keeper) IterateValidatorMissedBlockBitArray(ctx sdk.Context,
address sdk.ConsAddress, handler func(index int64, missed bool) (stop bool),
) {
store := ctx.KVStore(k.storeKey)
index := int64(0)
// Array may be sparse
for ; index < k.SignedBlocksWindow(ctx); index++ {
var missed gogotypes.BoolValue
bz := store.Get(types.ValidatorMissedBlockBitArrayKey(address, index))
if bz == nil {
continue
}

k.cdc.MustUnmarshal(bz, &missed)
if handler(index, missed.Value) {
break
}
}
}

// GetValidatorMissedBlocks returns array of missed blocks for given validator Cons address
func (k Keeper) GetValidatorMissedBlocks(ctx sdk.Context, address sdk.ConsAddress) []types.MissedBlock {
missedBlocks := []types.MissedBlock{}
k.IterateValidatorMissedBlockBitArray(ctx, address, func(index int64, missed bool) (stop bool) {
missedBlocks = append(missedBlocks, types.NewMissedBlock(index, missed))
return false
})

return missedBlocks
}

// JailUntil attempts to set a validator's JailedUntil attribute in its signing
// info. It will panic if the signing info does not exist for the validator.
func (k Keeper) JailUntil(ctx sdk.Context, consAddr sdk.ConsAddress, jailTime time.Time) {
Expand Down Expand Up @@ -139,20 +93,139 @@ func (k Keeper) IsTombstoned(ctx sdk.Context, consAddr sdk.ConsAddress) bool {
return signInfo.Tombstoned
}

// SetValidatorMissedBlockBitArray sets the bit that checks if the validator has
// missed a block in the current window
func (k Keeper) SetValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, missed bool) {
// getMissedBlockBitmapChunk gets the bitmap chunk at the given chunk index for
// a validator's missed block signing window.
func (k Keeper) getMissedBlockBitmapChunk(ctx sdk.Context, addr sdk.ConsAddress, chunkIndex int64) []byte {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&gogotypes.BoolValue{Value: missed})
store.Set(types.ValidatorMissedBlockBitArrayKey(address, index), bz)
chunk := store.Get(types.ValidatorMissedBlockBitmapKey(addr, chunkIndex))
return chunk
}

// clearValidatorMissedBlockBitArray deletes every instance of ValidatorMissedBlockBitArray in the store
func (k Keeper) clearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress) {
// setMissedBlockBitmapChunk sets the bitmap chunk at the given chunk index for
// a validator's missed block signing window.
func (k Keeper) setMissedBlockBitmapChunk(ctx sdk.Context, addr sdk.ConsAddress, chunkIndex int64, chunk []byte) {
store := ctx.KVStore(k.storeKey)
iter := sdk.KVStorePrefixIterator(store, types.ValidatorMissedBlockBitArrayPrefixKey(address))
key := types.ValidatorMissedBlockBitmapKey(addr, chunkIndex)
store.Set(key, chunk)
}

// GetMissedBlockBitmapValue returns true if a validator missed signing a block
// at the given index and false otherwise. The index provided is assumed to be
// the index in the range [0, SignedBlocksWindow), which represents the bitmap
// where each bit represents a height, and is determined by the validator's
// IndexOffset modulo SignedBlocksWindow. This index is used to fetch the chunk
// in the bitmap and the relative bit in that chunk.
func (k Keeper) GetMissedBlockBitmapValue(ctx sdk.Context, addr sdk.ConsAddress, index int64) (bool, error) {
// get the chunk or "word" in the logical bitmap
chunkIndex := index / types.MissedBlockBitmapChunkSize

bs := bitset.New(uint(types.MissedBlockBitmapChunkSize))
chunk := k.getMissedBlockBitmapChunk(ctx, addr, chunkIndex)
if chunk != nil {
if err := bs.UnmarshalBinary(chunk); err != nil {
return false, errors.Wrapf(err, "failed to decode bitmap chunk; index: %d", index)
}
}

// get the bit position in the chunk of the logical bitmap, where Test()
// checks if the bit is set.
bitIndex := index % types.MissedBlockBitmapChunkSize
return bs.Test(uint(bitIndex)), nil
}

// SetMissedBlockBitmapValue sets, i.e. flips, a bit in the validator's missed
// block bitmap. When missed=true, the bit is set, otherwise it set to zero. The
// index provided is assumed to be the index in the range [0, SignedBlocksWindow),
// which represents the bitmap where each bit represents a height, and is
// determined by the validator's IndexOffset modulo SignedBlocksWindow. This
// index is used to fetch the chunk in the bitmap and the relative bit in that
// chunk.
func (k Keeper) SetMissedBlockBitmapValue(ctx sdk.Context, addr sdk.ConsAddress, index int64, missed bool) error {
// get the chunk or "word" in the logical bitmap
chunkIndex := index / types.MissedBlockBitmapChunkSize

bs := bitset.New(uint(types.MissedBlockBitmapChunkSize))
chunk := k.getMissedBlockBitmapChunk(ctx, addr, chunkIndex)
if chunk != nil {
if err := bs.UnmarshalBinary(chunk); err != nil {
return errors.Wrapf(err, "failed to decode bitmap chunk; index: %d", index)
}
}

// get the bit position in the chunk of the logical bitmap
bitIndex := uint(index % types.MissedBlockBitmapChunkSize)
if missed {
bs.Set(bitIndex)
} else {
bs.Clear(bitIndex)
}

updatedChunk, err := bs.MarshalBinary()
if err != nil {
return errors.Wrapf(err, "failed to encode bitmap chunk; index: %d", index)
}

k.setMissedBlockBitmapChunk(ctx, addr, chunkIndex, updatedChunk)
return nil
}

// DeleteMissedBlockBitmap removes a validator's missed block bitmap from state.
func (k Keeper) DeleteMissedBlockBitmap(ctx sdk.Context, addr sdk.ConsAddress) error {
store := ctx.KVStore(k.storeKey)
prefix := types.ValidatorMissedBlockBitmapPrefixKey(addr)
iter := store.Iterator(prefix, sdk.PrefixEndBytes(prefix))
defer iter.Close()

for ; iter.Valid(); iter.Next() {
store.Delete(iter.Key())
}
return nil
}

// IterateMissedBlockBitmap iterates over a validator's signed blocks window
// bitmap and performs a callback function on each index, i.e. block height, in
// the range [0, SignedBlocksWindow).
//
// Note: A callback will only be executed over all bitmap chunks that exist in
// state.
func (k Keeper) IterateMissedBlockBitmap(ctx sdk.Context, addr sdk.ConsAddress, cb func(index int64, missed bool) (stop bool)) error {
store := ctx.KVStore(k.storeKey)
prefix := types.ValidatorMissedBlockBitmapPrefixKey(addr)
iter := store.Iterator(prefix, sdk.PrefixEndBytes(prefix))
defer iter.Close()

var index int64
for ; iter.Valid(); iter.Next() {
bs := bitset.New(uint(types.MissedBlockBitmapChunkSize))

if err := bs.UnmarshalBinary(iter.Value()); err != nil {
return errors.Wrapf(err, "failed to decode bitmap chunk; index: %v", string(iter.Key()))
}

for i := uint(0); i < types.MissedBlockBitmapChunkSize; i++ {
// execute the callback, where Test() returns true if the bit is set
if cb(index, bs.Test(i)) {
break
}

index++
}
}
return nil
}

// GetValidatorMissedBlocks returns array of missed blocks for given validator.
func (k Keeper) GetValidatorMissedBlocks(ctx sdk.Context, addr sdk.ConsAddress) ([]types.MissedBlock, error) {
signedBlocksWindow := k.SignedBlocksWindow(ctx)

missedBlocks := make([]types.MissedBlock, 0, signedBlocksWindow)
err := k.IterateMissedBlockBitmap(ctx, addr, func(index int64, missed bool) (stop bool) {
if missed {
missedBlocks = append(missedBlocks, types.NewMissedBlock(index, missed))
}

return false
})

return missedBlocks, err
}
Loading
Loading