refactor!: x/slashing missed block window (#15580)
This commit is contained in:
parent
96662df62f
commit
4684c854d4
@ -63,6 +63,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||
|
||||
### Improvements
|
||||
|
||||
* (x/slashing) [#15580](https://github.com/cosmos/cosmos-sdk/pull/15580) Refactor the validator's missed block signing window to be a chunked bitmap instead of a "logical" bitmap, significantly reducing the storage footprint.
|
||||
* [#15448](https://github.com/cosmos/cosmos-sdk/pull/15448) Automatically populate the block timestamp for historical queries. In contexts where the block timestamp is needed for previous states, the timestamp will now be set. Note, when querying against a node it must be re-synced in order to be able to automatically populate the block timestamp. Otherwise, the block timestamp will be populated for heights going forward once upgraded.
|
||||
* (x/gov) [#15554](https://github.com/cosmos/cosmos-sdk/pull/15554) Add proposal result log in `active_proposal` event. When a proposal passes but fails to execute, the proposal result is logged in the `active_proposal` event.
|
||||
* (mempool) [#15328](https://github.com/cosmos/cosmos-sdk/pull/15328) Improve the `PriorityNonceMempool`
|
||||
@ -98,6 +99,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||
|
||||
### State Machine Breaking
|
||||
|
||||
* (x/slashing) [#15580](https://github.com/cosmos/cosmos-sdk/pull/15580) The validator slashing window now stores "chunked" bitmap entries for each validator's signing window instead of a single boolean entry per signing window index.
|
||||
* (x/feegrant) [#14294](https://github.com/cosmos/cosmos-sdk/pull/14294) Moved the logic of rejecting duplicate grant from `msg_server` to `keeper` method.
|
||||
* (x/staking) [#14590](https://github.com/cosmos/cosmos-sdk/pull/14590) `MsgUndelegateResponse` now includes undelegated amount. `x/staking` module's `keeper.Undelegate` now returns 3 values (completionTime,undelegateAmount,error) instead of 2.
|
||||
|
||||
|
||||
@ -1416,19 +1416,20 @@ type ValidatorSigningInfo struct {
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
|
||||
// Height at which validator was first a candidate OR was unjailed
|
||||
// Height at which validator was first a candidate OR was un-jailed
|
||||
StartHeight int64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
|
||||
// Index which is incremented each time the validator was a bonded
|
||||
// in a block and may have signed a precommit or not. This in conjunction with the
|
||||
// `SignedBlocksWindow` param determines the index in the `MissedBlocksBitArray`.
|
||||
// Index which is incremented every time a validator is bonded in a block and
|
||||
// _may_ have signed a pre-commit or not. This in conjunction with the
|
||||
// signed_blocks_window param determines the index in the missed block bitmap.
|
||||
IndexOffset int64 `protobuf:"varint,3,opt,name=index_offset,json=indexOffset,proto3" json:"index_offset,omitempty"`
|
||||
// Timestamp until which the validator is jailed due to liveness downtime.
|
||||
JailedUntil *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=jailed_until,json=jailedUntil,proto3" json:"jailed_until,omitempty"`
|
||||
// Whether or not a validator has been tombstoned (killed out of validator set). It is set
|
||||
// once the validator commits an equivocation or for any other configured misbehiavor.
|
||||
// Whether or not a validator has been tombstoned (killed out of validator
|
||||
// set). It is set once the validator commits an equivocation or for any other
|
||||
// configured misbehavior.
|
||||
Tombstoned bool `protobuf:"varint,5,opt,name=tombstoned,proto3" json:"tombstoned,omitempty"`
|
||||
// A counter kept to avoid unnecessary array reads.
|
||||
// Note that `Sum(MissedBlocksBitArray)` always equals `MissedBlocksCounter`.
|
||||
// A counter of missed (unsigned) blocks. It is used to avoid unnecessary
|
||||
// reads in the missed block bitmap.
|
||||
MissedBlocksCounter int64 `protobuf:"varint,6,opt,name=missed_blocks_counter,json=missedBlocksCounter,proto3" json:"missed_blocks_counter,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
1
go.mod
1
go.mod
@ -15,6 +15,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.5.0
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/cockroachdb/apd/v2 v2.0.2
|
||||
github.com/cockroachdb/errors v1.9.1
|
||||
|
||||
2
go.sum
2
go.sum
@ -111,6 +111,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
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.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
|
||||
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
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=
|
||||
|
||||
@ -16,20 +16,24 @@ message ValidatorSigningInfo {
|
||||
option (gogoproto.equal) = true;
|
||||
|
||||
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
|
||||
// Height at which validator was first a candidate OR was unjailed
|
||||
// Height at which validator was first a candidate OR was un-jailed
|
||||
int64 start_height = 2;
|
||||
// Index which is incremented each time the validator was a bonded
|
||||
// in a block and may have signed a precommit or not. This in conjunction with the
|
||||
// `SignedBlocksWindow` param determines the index in the `MissedBlocksBitArray`.
|
||||
// Index which is incremented every time a validator is bonded in a block and
|
||||
// _may_ have signed a pre-commit or not. This in conjunction with the
|
||||
// signed_blocks_window param determines the index in the missed block bitmap.
|
||||
int64 index_offset = 3;
|
||||
// Timestamp until which the validator is jailed due to liveness downtime.
|
||||
google.protobuf.Timestamp jailed_until = 4
|
||||
[(gogoproto.stdtime) = true, (gogoproto.nullable) = false, (amino.dont_omitempty) = true];
|
||||
// Whether or not a validator has been tombstoned (killed out of validator set). It is set
|
||||
// once the validator commits an equivocation or for any other configured misbehiavor.
|
||||
google.protobuf.Timestamp jailed_until = 4 [
|
||||
(gogoproto.stdtime) = true,
|
||||
(gogoproto.nullable) = false,
|
||||
(amino.dont_omitempty) = true
|
||||
];
|
||||
// Whether or not a validator has been tombstoned (killed out of validator
|
||||
// set). It is set once the validator commits an equivocation or for any other
|
||||
// configured misbehavior.
|
||||
bool tombstoned = 5;
|
||||
// A counter kept to avoid unnecessary array reads.
|
||||
// Note that `Sum(MissedBlocksBitArray)` always equals `MissedBlocksCounter`.
|
||||
// A counter of missed (unsigned) blocks. It is used to avoid unnecessary
|
||||
// reads in the missed block bitmap.
|
||||
int64 missed_blocks_counter = 6;
|
||||
}
|
||||
|
||||
@ -44,8 +48,11 @@ message Params {
|
||||
(amino.encoding) = "cosmos_dec_bytes",
|
||||
(amino.dont_omitempty) = true
|
||||
];
|
||||
google.protobuf.Duration downtime_jail_duration = 3
|
||||
[(gogoproto.nullable) = false, (amino.dont_omitempty) = true, (gogoproto.stdduration) = true];
|
||||
google.protobuf.Duration downtime_jail_duration = 3 [
|
||||
(gogoproto.nullable) = false,
|
||||
(amino.dont_omitempty) = true,
|
||||
(gogoproto.stdduration) = true
|
||||
];
|
||||
bytes slash_fraction_double_sign = 4 [
|
||||
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
|
||||
(gogoproto.nullable) = false,
|
||||
|
||||
@ -49,6 +49,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.5.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
|
||||
|
||||
@ -267,6 +267,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.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
|
||||
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
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=
|
||||
|
||||
@ -49,6 +49,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.5.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
|
||||
|
||||
@ -267,6 +267,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.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
|
||||
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
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=
|
||||
|
||||
@ -6,8 +6,8 @@ import (
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
)
|
||||
|
||||
// InitGenesis initialize default parameters
|
||||
// and the keeper's address to pubkey map
|
||||
// InitGenesis initializes default parameters and the keeper's address to
|
||||
// pubkey map.
|
||||
func (keeper Keeper) InitGenesis(ctx sdk.Context, stakingKeeper types.StakingKeeper, data *types.GenesisState) {
|
||||
stakingKeeper.IterateValidators(ctx,
|
||||
func(index int64, validator stakingtypes.ValidatorI) bool {
|
||||
@ -15,6 +15,7 @@ func (keeper Keeper) InitGenesis(ctx sdk.Context, stakingKeeper types.StakingKee
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
keeper.AddPubkey(ctx, consPk)
|
||||
return false
|
||||
},
|
||||
@ -33,8 +34,11 @@ func (keeper Keeper) InitGenesis(ctx sdk.Context, stakingKeeper types.StakingKee
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ package keeper
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing/types"
|
||||
@ -31,27 +33,42 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre
|
||||
panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr))
|
||||
}
|
||||
|
||||
// this is a relative index, so it counts blocks the validator *should* have signed
|
||||
// will use the 0-value default signing info if not present, except for start height
|
||||
// Compute the relative index, so we count the blocks the validator *should*
|
||||
// have signed. We will use the 0-value default signing info if not present,
|
||||
// except for start height. The index is in the range [0, SignedBlocksWindow)
|
||||
// and is used to see if a validator signed a block at the given height, which
|
||||
// is represented by a bit in the bitmap.
|
||||
index := signInfo.IndexOffset % k.SignedBlocksWindow(ctx)
|
||||
signInfo.IndexOffset++
|
||||
|
||||
// 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)
|
||||
// determine if the validator signed the previous block
|
||||
previous, err := k.GetMissedBlockBitmapValue(ctx, consAddr, index)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "failed to get the validator's bitmap value"))
|
||||
}
|
||||
|
||||
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(err)
|
||||
}
|
||||
|
||||
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(err)
|
||||
}
|
||||
|
||||
signInfo.MissedBlocksCounter--
|
||||
|
||||
default:
|
||||
// Array value at this index has not changed, no need to update counter
|
||||
// bitmap value at this index has not changed, no need to update counter
|
||||
}
|
||||
|
||||
minSignedPerWindow := k.MinSignedPerWindow(ctx)
|
||||
@ -105,10 +122,11 @@ func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr cryptotypes.Addre
|
||||
|
||||
signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.DowntimeJailDuration(ctx))
|
||||
|
||||
// We need to reset the counter & array so that the validator won't be immediately slashed for downtime upon rebonding.
|
||||
// We need to reset the counter & bitmap so that the validator won't be
|
||||
// immediately slashed for downtime upon re-bonding.
|
||||
signInfo.MissedBlocksCounter = 0
|
||||
signInfo.IndexOffset = 0
|
||||
k.clearValidatorMissedBlockBitArray(ctx, consAddr)
|
||||
k.DeleteMissedBlockBitmap(ctx, consAddr)
|
||||
|
||||
logger.Info(
|
||||
"slashing and jailing validator due to liveness fault",
|
||||
|
||||
@ -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.
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ package keeper
|
||||
import (
|
||||
"time"
|
||||
|
||||
gogotypes "github.com/cosmos/gogoproto/types"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
"github.com/cockroachdb/errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing/types"
|
||||
@ -13,20 +13,21 @@ import (
|
||||
|
||||
// GetValidatorSigningInfo retruns the ValidatorSigningInfo for a specific validator
|
||||
// ConsAddress
|
||||
func (k Keeper) GetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress) (info types.ValidatorSigningInfo, found bool) {
|
||||
func (k Keeper) GetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress) (types.ValidatorSigningInfo, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
|
||||
var info types.ValidatorSigningInfo
|
||||
bz := store.Get(types.ValidatorSigningInfoKey(address))
|
||||
if bz == nil {
|
||||
found = false
|
||||
return
|
||||
return info, false
|
||||
}
|
||||
|
||||
k.cdc.MustUnmarshal(bz, &info)
|
||||
found = true
|
||||
return
|
||||
return info, true
|
||||
}
|
||||
|
||||
// HasValidatorSigningInfo returns if a given validator has signing information
|
||||
// persited.
|
||||
// persisted.
|
||||
func (k Keeper) HasValidatorSigningInfo(ctx sdk.Context, consAddr sdk.ConsAddress) bool {
|
||||
_, ok := k.GetValidatorSigningInfo(ctx, consAddr)
|
||||
return ok
|
||||
@ -56,53 +57,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) {
|
||||
@ -141,20 +95,135 @@ 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 := storetypes.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) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
|
||||
iter := storetypes.KVStorePrefixIterator(store, types.ValidatorMissedBlockBitmapPrefixKey(addr))
|
||||
defer iter.Close()
|
||||
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
store.Delete(iter.Key())
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
|
||||
iter := storetypes.KVStorePrefixIterator(store, types.ValidatorMissedBlockBitmapPrefixKey(addr))
|
||||
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 {
|
||||
panic(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++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetValidatorMissedBlocks returns array of missed blocks for given validator.
|
||||
func (k Keeper) GetValidatorMissedBlocks(ctx sdk.Context, addr sdk.ConsAddress) []types.MissedBlock {
|
||||
missedBlocks := make([]types.MissedBlock, 0, k.SignedBlocksWindow(ctx))
|
||||
k.IterateMissedBlockBitmap(ctx, addr, func(index int64, missed bool) (stop bool) {
|
||||
if missed {
|
||||
missedBlocks = append(missedBlocks, types.NewMissedBlock(index, missed))
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return missedBlocks
|
||||
}
|
||||
|
||||
@ -53,41 +53,44 @@ func (s *KeeperTestSuite) TestValidatorSigningInfo() {
|
||||
require.Equal(sInfo.JailedUntil, jailTime)
|
||||
}
|
||||
|
||||
func (s *KeeperTestSuite) TestValidatorMissedBlockBitArray() {
|
||||
func (s *KeeperTestSuite) TestValidatorMissedBlockBitmap_SmallWindow() {
|
||||
ctx, keeper := s.ctx, s.slashingKeeper
|
||||
require := s.Require()
|
||||
|
||||
params := testutil.TestParams()
|
||||
params.SignedBlocksWindow = 100
|
||||
require.NoError(keeper.SetParams(ctx, params))
|
||||
for _, window := range []int64{100, 32_000} {
|
||||
params := testutil.TestParams()
|
||||
params.SignedBlocksWindow = window
|
||||
require.NoError(keeper.SetParams(ctx, params))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
index int64
|
||||
missed bool
|
||||
}{
|
||||
{
|
||||
name: "missed block with false",
|
||||
index: 50,
|
||||
missed: false,
|
||||
},
|
||||
{
|
||||
name: "missed block with true",
|
||||
index: 51,
|
||||
missed: true,
|
||||
},
|
||||
}
|
||||
for ind, tc := range testCases {
|
||||
tc := tc
|
||||
s.Run(tc.name, func() {
|
||||
keeper.SetValidatorMissedBlockBitArray(ctx, consAddr, tc.index, tc.missed)
|
||||
missed := keeper.GetValidatorMissedBlockBitArray(ctx, consAddr, tc.index)
|
||||
// validator misses all blocks in the window
|
||||
var valIdxOffset int64
|
||||
for valIdxOffset < params.SignedBlocksWindow {
|
||||
idx := valIdxOffset % params.SignedBlocksWindow
|
||||
err := keeper.SetMissedBlockBitmapValue(ctx, consAddr, idx, true)
|
||||
require.NoError(err)
|
||||
|
||||
require.Equal(missed, tc.missed)
|
||||
missedBlocks := keeper.GetValidatorMissedBlocks(ctx, consAddr)
|
||||
require.Equal(len(missedBlocks), ind+1)
|
||||
require.Equal(missedBlocks[ind].Index, tc.index)
|
||||
require.Equal(missedBlocks[ind].Missed, tc.missed)
|
||||
})
|
||||
missed, err := keeper.GetMissedBlockBitmapValue(ctx, consAddr, idx)
|
||||
require.NoError(err)
|
||||
require.True(missed)
|
||||
|
||||
valIdxOffset++
|
||||
}
|
||||
|
||||
// validator should have missed all blocks
|
||||
missedBlocks := keeper.GetValidatorMissedBlocks(ctx, consAddr)
|
||||
require.Len(missedBlocks, int(params.SignedBlocksWindow))
|
||||
|
||||
// sign next block, which rolls the missed block bitmap
|
||||
idx := valIdxOffset % params.SignedBlocksWindow
|
||||
err := keeper.SetMissedBlockBitmapValue(ctx, consAddr, idx, false)
|
||||
require.NoError(err)
|
||||
|
||||
missed, err := keeper.GetMissedBlockBitmapValue(ctx, consAddr, idx)
|
||||
require.NoError(err)
|
||||
require.False(missed)
|
||||
|
||||
// validator should have missed all blocks except the last one
|
||||
missedBlocks = keeper.GetValidatorMissedBlocks(ctx, consAddr)
|
||||
require.Len(missedBlocks, int(params.SignedBlocksWindow)-1)
|
||||
}
|
||||
}
|
||||
|
||||
21
x/slashing/migrations/v2/keys.go
Normal file
21
x/slashing/migrations/v2/keys.go
Normal file
@ -0,0 +1,21 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
)
|
||||
|
||||
var ValidatorMissedBlockBitArrayKeyPrefix = []byte{0x02}
|
||||
|
||||
func ValidatorMissedBlockBitArrayPrefixKey(v sdk.ConsAddress) []byte {
|
||||
return append(ValidatorMissedBlockBitArrayKeyPrefix, address.MustLengthPrefix(v.Bytes())...)
|
||||
}
|
||||
|
||||
func ValidatorMissedBlockBitArrayKey(v sdk.ConsAddress, i int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(i))
|
||||
|
||||
return append(ValidatorMissedBlockBitArrayPrefixKey(v), b...)
|
||||
}
|
||||
@ -4,9 +4,8 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
||||
@ -39,7 +38,7 @@ func TestStoreMigration(t *testing.T) {
|
||||
{
|
||||
"ValidatorMissedBlockBitArrayKey",
|
||||
v1.ValidatorMissedBlockBitArrayKey(consAddr, 2),
|
||||
types.ValidatorMissedBlockBitArrayKey(consAddr, 2),
|
||||
v2.ValidatorMissedBlockBitArrayKey(consAddr, 2),
|
||||
},
|
||||
{
|
||||
"AddrPubkeyRelationKey",
|
||||
|
||||
53
x/slashing/migrations/v4/keys.go
Normal file
53
x/slashing/migrations/v4/keys.go
Normal file
@ -0,0 +1,53 @@
|
||||
package v4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
"github.com/cosmos/cosmos-sdk/types/kv"
|
||||
)
|
||||
|
||||
const (
|
||||
addrLen = 20
|
||||
|
||||
MissedBlockBitmapChunkSize = 1024 // 2^10 bits
|
||||
)
|
||||
|
||||
var (
|
||||
ValidatorSigningInfoKeyPrefix = []byte{0x01}
|
||||
validatorMissedBlockBitArrayKeyPrefix = []byte{0x02}
|
||||
)
|
||||
|
||||
func ValidatorSigningInfoKey(v sdk.ConsAddress) []byte {
|
||||
return append(ValidatorSigningInfoKeyPrefix, address.MustLengthPrefix(v.Bytes())...)
|
||||
}
|
||||
|
||||
func ValidatorSigningInfoAddress(key []byte) (v sdk.ConsAddress) {
|
||||
// Remove prefix and address length.
|
||||
kv.AssertKeyAtLeastLength(key, 3)
|
||||
addr := key[2:]
|
||||
|
||||
return sdk.ConsAddress(addr)
|
||||
}
|
||||
|
||||
func validatorMissedBlockBitArrayPrefixKey(v sdk.ConsAddress) []byte {
|
||||
return append(validatorMissedBlockBitArrayKeyPrefix, v.Bytes()...)
|
||||
}
|
||||
|
||||
func ValidatorMissedBlockBitArrayKey(v sdk.ConsAddress, i int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(i))
|
||||
return append(validatorMissedBlockBitArrayPrefixKey(v), b...)
|
||||
}
|
||||
|
||||
func validatorMissedBlockBitmapPrefixKey(v sdk.ConsAddress) []byte {
|
||||
return append(validatorMissedBlockBitArrayKeyPrefix, address.MustLengthPrefix(v.Bytes())...)
|
||||
}
|
||||
|
||||
func ValidatorMissedBlockBitmapKey(v sdk.ConsAddress, chunkIndex int64) []byte {
|
||||
bz := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(bz, uint64(chunkIndex))
|
||||
|
||||
return append(validatorMissedBlockBitmapPrefixKey(v), bz...)
|
||||
}
|
||||
153
x/slashing/migrations/v4/migrate.go
Normal file
153
x/slashing/migrations/v4/migrate.go
Normal file
@ -0,0 +1,153 @@
|
||||
package v4
|
||||
|
||||
import (
|
||||
"cosmossdk.io/errors"
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
gogotypes "github.com/cosmos/gogoproto/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing/types"
|
||||
)
|
||||
|
||||
// Migrate migrates state to consensus version 4. Specifically, the migration
|
||||
// deletes all existing validator bitmap entries and replaces them with a real
|
||||
// "chunked" bitmap.
|
||||
func Migrate(ctx sdk.Context, cdc codec.BinaryCodec, store storetypes.KVStore, params types.Params) error {
|
||||
// Get all the missed blocks for each validator, based on the existing signing
|
||||
// info.
|
||||
var missedBlocks []types.ValidatorMissedBlocks
|
||||
iterateValidatorSigningInfos(ctx, cdc, store, func(addr sdk.ConsAddress, info types.ValidatorSigningInfo) (stop bool) {
|
||||
bechAddr := addr.String()
|
||||
localMissedBlocks := GetValidatorMissedBlocks(ctx, cdc, store, addr, params)
|
||||
|
||||
missedBlocks = append(missedBlocks, types.ValidatorMissedBlocks{
|
||||
Address: bechAddr,
|
||||
MissedBlocks: localMissedBlocks,
|
||||
})
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// For each missed blocks entry, of which there should only be one per validator,
|
||||
// we clear all the old entries and insert the new chunked entry.
|
||||
for _, mb := range missedBlocks {
|
||||
addr, err := sdk.ConsAddressFromBech32(mb.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteValidatorMissedBlockBitArray(ctx, store, addr)
|
||||
|
||||
for _, b := range mb.MissedBlocks {
|
||||
// Note: It is not necessary to store entries with missed=false, i.e. where
|
||||
// the bit is zero, since when the bitmap is initialized, all non-set bits
|
||||
// are already zero.
|
||||
if b.Missed {
|
||||
if err := setMissedBlockBitmapValue(ctx, store, addr, b.Index, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func iterateValidatorSigningInfos(
|
||||
ctx sdk.Context,
|
||||
cdc codec.BinaryCodec,
|
||||
store storetypes.KVStore,
|
||||
cb func(address sdk.ConsAddress, info types.ValidatorSigningInfo) (stop bool),
|
||||
) {
|
||||
iter := storetypes.KVStorePrefixIterator(store, ValidatorSigningInfoKeyPrefix)
|
||||
defer iter.Close()
|
||||
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
address := ValidatorSigningInfoAddress(iter.Key())
|
||||
var info types.ValidatorSigningInfo
|
||||
cdc.MustUnmarshal(iter.Value(), &info)
|
||||
|
||||
if cb(address, info) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func iterateValidatorMissedBlockBitArray(
|
||||
ctx sdk.Context,
|
||||
cdc codec.BinaryCodec,
|
||||
store storetypes.KVStore,
|
||||
addr sdk.ConsAddress,
|
||||
params types.Params,
|
||||
cb func(index int64, missed bool) (stop bool),
|
||||
) {
|
||||
for i := int64(0); i < params.SignedBlocksWindow; i++ {
|
||||
var missed gogotypes.BoolValue
|
||||
bz := store.Get(ValidatorMissedBlockBitArrayKey(addr, i))
|
||||
if bz == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cdc.MustUnmarshal(bz, &missed)
|
||||
if cb(i, missed.Value) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetValidatorMissedBlocks(
|
||||
ctx sdk.Context,
|
||||
cdc codec.BinaryCodec,
|
||||
store storetypes.KVStore,
|
||||
addr sdk.ConsAddress,
|
||||
params types.Params,
|
||||
) []types.MissedBlock {
|
||||
var missedBlocks []types.MissedBlock
|
||||
iterateValidatorMissedBlockBitArray(ctx, cdc, store, addr, params, func(index int64, missed bool) (stop bool) {
|
||||
missedBlocks = append(missedBlocks, types.NewMissedBlock(index, missed))
|
||||
return false
|
||||
})
|
||||
|
||||
return missedBlocks
|
||||
}
|
||||
|
||||
func deleteValidatorMissedBlockBitArray(ctx sdk.Context, store storetypes.KVStore, addr sdk.ConsAddress) {
|
||||
iter := storetypes.KVStorePrefixIterator(store, validatorMissedBlockBitArrayPrefixKey(addr))
|
||||
defer iter.Close()
|
||||
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
store.Delete(iter.Key())
|
||||
}
|
||||
}
|
||||
|
||||
func setMissedBlockBitmapValue(ctx sdk.Context, store storetypes.KVStore, addr sdk.ConsAddress, index int64, missed bool) error {
|
||||
// get the chunk or "word" in the logical bitmap
|
||||
chunkIndex := index / MissedBlockBitmapChunkSize
|
||||
key := ValidatorMissedBlockBitmapKey(addr, chunkIndex)
|
||||
|
||||
bs := bitset.New(uint(MissedBlockBitmapChunkSize))
|
||||
chunk := store.Get(key)
|
||||
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 % 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)
|
||||
}
|
||||
|
||||
store.Set(key, updatedChunk)
|
||||
return nil
|
||||
}
|
||||
64
x/slashing/migrations/v4/migrate_test.go
Normal file
64
x/slashing/migrations/v4/migrate_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package v4_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
gogotypes "github.com/cosmos/gogoproto/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/testutil"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
|
||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||
v4 "github.com/cosmos/cosmos-sdk/x/slashing/migrations/v4"
|
||||
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
|
||||
)
|
||||
|
||||
var consAddr = sdk.ConsAddress(sdk.AccAddress([]byte("addr1_______________")))
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
cdc := moduletestutil.MakeTestEncodingConfig(slashing.AppModuleBasic{}).Codec
|
||||
storeKey := storetypes.NewKVStoreKey(slashingtypes.ModuleName)
|
||||
tKey := storetypes.NewTransientStoreKey("transient_test")
|
||||
ctx := testutil.DefaultContext(storeKey, tKey)
|
||||
store := ctx.KVStore(storeKey)
|
||||
params := slashingtypes.Params{SignedBlocksWindow: 100}
|
||||
|
||||
// store old signing info and bitmap entries
|
||||
bz := cdc.MustMarshal(&slashingtypes.ValidatorSigningInfo{Address: consAddr.String()})
|
||||
store.Set(v4.ValidatorSigningInfoKey(consAddr), bz)
|
||||
|
||||
for i := int64(0); i < params.SignedBlocksWindow; i++ {
|
||||
// all even blocks are missed
|
||||
missed := &gogotypes.BoolValue{Value: i%2 == 0}
|
||||
bz := cdc.MustMarshal(missed)
|
||||
store.Set(v4.ValidatorMissedBlockBitArrayKey(consAddr, i), bz)
|
||||
}
|
||||
|
||||
err := v4.Migrate(ctx, cdc, store, params)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure old entries no longer exist and new bitmap chunk entries exist
|
||||
entries := v4.GetValidatorMissedBlocks(ctx, cdc, store, consAddr, params)
|
||||
require.Empty(t, entries)
|
||||
|
||||
for i := int64(0); i < params.SignedBlocksWindow; i++ {
|
||||
chunkIndex := i / v4.MissedBlockBitmapChunkSize
|
||||
chunk := store.Get(v4.ValidatorMissedBlockBitmapKey(consAddr, chunkIndex))
|
||||
require.NotNil(t, chunk)
|
||||
|
||||
bs := bitset.New(uint(v4.MissedBlockBitmapChunkSize))
|
||||
require.NoError(t, bs.UnmarshalBinary(chunk))
|
||||
|
||||
// ensure all even blocks are missed
|
||||
bitIndex := uint(i % v4.MissedBlockBitmapChunkSize)
|
||||
require.Equal(t, i%2 == 0, bs.Test(bitIndex))
|
||||
require.Equal(t, i%2 == 1, !bs.Test(bitIndex))
|
||||
}
|
||||
|
||||
// ensure there's only one chunk for a window of size 100
|
||||
chunk := store.Get(v4.ValidatorMissedBlockBitmapKey(consAddr, 1))
|
||||
require.Nil(t, chunk)
|
||||
}
|
||||
@ -5,15 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
abci "github.com/cometbft/cometbft/abci/types"
|
||||
gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
modulev1 "cosmossdk.io/api/cosmos/slashing/module/v1"
|
||||
"cosmossdk.io/core/appmodule"
|
||||
"cosmossdk.io/depinject"
|
||||
|
||||
store "cosmossdk.io/store/types"
|
||||
abci "github.com/cometbft/cometbft/abci/types"
|
||||
gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
@ -32,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
// ConsensusVersion defines the current x/slashing module consensus version.
|
||||
const ConsensusVersion = 3
|
||||
const ConsensusVersion = 4
|
||||
|
||||
var (
|
||||
_ module.AppModuleBasic = AppModuleBasic{}
|
||||
@ -148,6 +146,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) {
|
||||
if err := cfg.RegisterMigration(types.ModuleName, 2, m.Migrate2to3); err != nil {
|
||||
panic(fmt.Sprintf("failed to migrate x/%s from version 2 to 3: %v", types.ModuleName, err))
|
||||
}
|
||||
|
||||
if err := cfg.RegisterMigration(types.ModuleName, 3, m.Migrate3to4); err != nil {
|
||||
panic(fmt.Sprintf("failed to migrate x/%s from version 3 to 4: %v", types.ModuleName, err))
|
||||
}
|
||||
}
|
||||
|
||||
// InitGenesis performs genesis initialization for the slashing module. It returns
|
||||
|
||||
@ -23,7 +23,7 @@ func NewDecodeStore(cdc codec.BinaryCodec) func(kvA, kvB kv.Pair) string {
|
||||
cdc.MustUnmarshal(kvB.Value, &infoB)
|
||||
return fmt.Sprintf("%v\n%v", infoA, infoB)
|
||||
|
||||
case bytes.Equal(kvA.Key[:1], types.ValidatorMissedBlockBitArrayKeyPrefix):
|
||||
case bytes.Equal(kvA.Key[:1], types.ValidatorMissedBlockBitmapKeyPrefix):
|
||||
var missedA, missedB gogotypes.BoolValue
|
||||
cdc.MustUnmarshal(kvA.Value, &missedA)
|
||||
cdc.MustUnmarshal(kvB.Value, &missedB)
|
||||
|
||||
@ -36,7 +36,7 @@ func TestDecodeStore(t *testing.T) {
|
||||
kvPairs := kv.Pairs{
|
||||
Pairs: []kv.Pair{
|
||||
{Key: types.ValidatorSigningInfoKey(consAddr1), Value: cdc.MustMarshal(&info)},
|
||||
{Key: types.ValidatorMissedBlockBitArrayKey(consAddr1, 6), Value: cdc.MustMarshal(&missed)},
|
||||
{Key: types.ValidatorMissedBlockBitmapKey(consAddr1, 6), Value: cdc.MustMarshal(&missed)},
|
||||
{Key: types.AddrPubkeyRelationKey(delAddr1), Value: bz},
|
||||
{Key: []byte{0x99}, Value: []byte{0x99}}, // This test should panic
|
||||
},
|
||||
|
||||
@ -17,6 +17,25 @@ const (
|
||||
|
||||
// RouterKey is the message route for slashing
|
||||
RouterKey = ModuleName
|
||||
|
||||
// MissedBlockBitmapChunkSize defines the chunk size, in number of bits, of a
|
||||
// validator missed block bitmap. Chunks are used to reduce the storage and
|
||||
// write overhead of IAVL nodes. The total size of the bitmap is roughly in
|
||||
// the range [0, SignedBlocksWindow) where each bit represents a block. A
|
||||
// validator's IndexOffset modulo the SignedBlocksWindow is used to retrieve
|
||||
// the chunk in that bitmap range. Once the chunk is retrieved, the same index
|
||||
// is used to check or flip a bit, where if a bit is set, it indicates the
|
||||
// validator missed that block.
|
||||
//
|
||||
// For a bitmap of N items, i.e. a validator's signed block window, the amount
|
||||
// of write complexity per write with a factor of f being the overhead of
|
||||
// IAVL being un-optimized, i.e. 2-4, is as follows:
|
||||
//
|
||||
// ChunkSize + (f * 256 <IAVL leaf hash>) + 256 * log_2(N / ChunkSize)
|
||||
//
|
||||
// As for the storage overhead, with the same factor f, it is as follows:
|
||||
// (N - 256) + (N / ChunkSize) * (512 * f)
|
||||
MissedBlockBitmapChunkSize = 1024 // 2^10 bits
|
||||
)
|
||||
|
||||
// Keys for slashing store
|
||||
@ -24,15 +43,15 @@ const (
|
||||
//
|
||||
// - 0x01<consAddrLen (1 Byte)><consAddress_Bytes>: ValidatorSigningInfo
|
||||
//
|
||||
// - 0x02<consAddrLen (1 Byte)><consAddress_Bytes><period_Bytes>: bool
|
||||
// - 0x02<consAddrLen (1 Byte)><consAddress_Bytes><chunk_index>: bitmap_chunk
|
||||
//
|
||||
// - 0x03<accAddrLen (1 Byte)><accAddr_Bytes>: cryptotypes.PubKey
|
||||
|
||||
var (
|
||||
ParamsKey = []byte{0x00} // Prefix for params key
|
||||
ValidatorSigningInfoKeyPrefix = []byte{0x01} // Prefix for signing info
|
||||
ValidatorMissedBlockBitArrayKeyPrefix = []byte{0x02} // Prefix for missed block bit array
|
||||
AddrPubkeyRelationKeyPrefix = []byte{0x03} // Prefix for address-pubkey relation
|
||||
ParamsKey = []byte{0x00} // Prefix for params key
|
||||
ValidatorSigningInfoKeyPrefix = []byte{0x01} // Prefix for signing info
|
||||
ValidatorMissedBlockBitmapKeyPrefix = []byte{0x02} // Prefix for missed block bitmap
|
||||
AddrPubkeyRelationKeyPrefix = []byte{0x03} // Prefix for address-pubkey relation
|
||||
)
|
||||
|
||||
// ValidatorSigningInfoKey - stored by *Consensus* address (not operator address)
|
||||
@ -49,17 +68,19 @@ func ValidatorSigningInfoAddress(key []byte) (v sdk.ConsAddress) {
|
||||
return sdk.ConsAddress(addr)
|
||||
}
|
||||
|
||||
// ValidatorMissedBlockBitArrayPrefixKey - stored by *Consensus* address (not operator address)
|
||||
func ValidatorMissedBlockBitArrayPrefixKey(v sdk.ConsAddress) []byte {
|
||||
return append(ValidatorMissedBlockBitArrayKeyPrefix, address.MustLengthPrefix(v.Bytes())...)
|
||||
// ValidatorMissedBlockBitmapPrefixKey returns the key prefix for a validator's
|
||||
// missed block bitmap.
|
||||
func ValidatorMissedBlockBitmapPrefixKey(v sdk.ConsAddress) []byte {
|
||||
return append(ValidatorMissedBlockBitmapKeyPrefix, address.MustLengthPrefix(v.Bytes())...)
|
||||
}
|
||||
|
||||
// ValidatorMissedBlockBitArrayKey - stored by *Consensus* address (not operator address)
|
||||
func ValidatorMissedBlockBitArrayKey(v sdk.ConsAddress, i int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, uint64(i))
|
||||
// ValidatorMissedBlockBitmapKey returns the key for a validator's missed block
|
||||
// bitmap chunk.
|
||||
func ValidatorMissedBlockBitmapKey(v sdk.ConsAddress, chunkIndex int64) []byte {
|
||||
bz := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(bz, uint64(chunkIndex))
|
||||
|
||||
return append(ValidatorMissedBlockBitArrayPrefixKey(v), b...)
|
||||
return append(ValidatorMissedBlockBitmapPrefixKey(v), bz...)
|
||||
}
|
||||
|
||||
// AddrPubkeyRelationKey gets pubkey relation key used to get the pubkey from the address
|
||||
|
||||
@ -35,19 +35,20 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||
// liveness activity.
|
||||
type ValidatorSigningInfo struct {
|
||||
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
|
||||
// Height at which validator was first a candidate OR was unjailed
|
||||
// Height at which validator was first a candidate OR was un-jailed
|
||||
StartHeight int64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
|
||||
// Index which is incremented each time the validator was a bonded
|
||||
// in a block and may have signed a precommit or not. This in conjunction with the
|
||||
// `SignedBlocksWindow` param determines the index in the `MissedBlocksBitArray`.
|
||||
// Index which is incremented every time a validator is bonded in a block and
|
||||
// _may_ have signed a pre-commit or not. This in conjunction with the
|
||||
// signed_blocks_window param determines the index in the missed block bitmap.
|
||||
IndexOffset int64 `protobuf:"varint,3,opt,name=index_offset,json=indexOffset,proto3" json:"index_offset,omitempty"`
|
||||
// Timestamp until which the validator is jailed due to liveness downtime.
|
||||
JailedUntil time.Time `protobuf:"bytes,4,opt,name=jailed_until,json=jailedUntil,proto3,stdtime" json:"jailed_until"`
|
||||
// Whether or not a validator has been tombstoned (killed out of validator set). It is set
|
||||
// once the validator commits an equivocation or for any other configured misbehiavor.
|
||||
// Whether or not a validator has been tombstoned (killed out of validator
|
||||
// set). It is set once the validator commits an equivocation or for any other
|
||||
// configured misbehavior.
|
||||
Tombstoned bool `protobuf:"varint,5,opt,name=tombstoned,proto3" json:"tombstoned,omitempty"`
|
||||
// A counter kept to avoid unnecessary array reads.
|
||||
// Note that `Sum(MissedBlocksBitArray)` always equals `MissedBlocksCounter`.
|
||||
// A counter of missed (unsigned) blocks. It is used to avoid unnecessary
|
||||
// reads in the missed block bitmap.
|
||||
MissedBlocksCounter int64 `protobuf:"varint,6,opt,name=missed_blocks_counter,json=missedBlocksCounter,proto3" json:"missed_blocks_counter,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user