diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3843c0a30f..4f4b09d7fa9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/api/cosmos/slashing/v1beta1/slashing.pulsar.go b/api/cosmos/slashing/v1beta1/slashing.pulsar.go index 0b70d3683cd5..f5de168f40e0 100644 --- a/api/cosmos/slashing/v1beta1/slashing.pulsar.go +++ b/api/cosmos/slashing/v1beta1/slashing.pulsar.go @@ -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"` } diff --git a/go.mod b/go.mod index ea38dcaeb1c0..982d5cf1a155 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index d7fcd5cd5d4b..ef456d53fb58 100644 --- a/go.sum +++ b/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= diff --git a/proto/cosmos/slashing/v1beta1/slashing.proto b/proto/cosmos/slashing/v1beta1/slashing.proto index a4d2129b1438..65bd4800e677 100644 --- a/proto/cosmos/slashing/v1beta1/slashing.proto +++ b/proto/cosmos/slashing/v1beta1/slashing.proto @@ -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, diff --git a/simapp/go.mod b/simapp/go.mod index 755c9759a3d3..cf677a34f12f 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -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 diff --git a/simapp/go.sum b/simapp/go.sum index cfb1b8a03351..4edde3a89e78 100644 --- a/simapp/go.sum +++ b/simapp/go.sum @@ -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= diff --git a/tests/go.mod b/tests/go.mod index e520f8438104..8a2cf637787b 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -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 diff --git a/tests/go.sum b/tests/go.sum index 9e4699084d8b..89bd5968b204 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -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= diff --git a/x/slashing/keeper/genesis.go b/x/slashing/keeper/genesis.go index e53ef3730c06..dda2da2431d6 100644 --- a/x/slashing/keeper/genesis.go +++ b/x/slashing/keeper/genesis.go @@ -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) + } } } diff --git a/x/slashing/keeper/infractions.go b/x/slashing/keeper/infractions.go index 17f56d04c6cc..557a48a639cf 100644 --- a/x/slashing/keeper/infractions.go +++ b/x/slashing/keeper/infractions.go @@ -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", diff --git a/x/slashing/keeper/migrations.go b/x/slashing/keeper/migrations.go index fca5255e6f68..5fb281ccc6c0 100644 --- a/x/slashing/keeper/migrations.go +++ b/x/slashing/keeper/migrations.go @@ -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)) +} diff --git a/x/slashing/keeper/signing_info.go b/x/slashing/keeper/signing_info.go index df72366e306f..628b262369ea 100644 --- a/x/slashing/keeper/signing_info.go +++ b/x/slashing/keeper/signing_info.go @@ -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 +} diff --git a/x/slashing/keeper/signing_info_test.go b/x/slashing/keeper/signing_info_test.go index f33fbf34b53d..c08e59f26520 100644 --- a/x/slashing/keeper/signing_info_test.go +++ b/x/slashing/keeper/signing_info_test.go @@ -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)) - - 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) - - 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) - }) + for _, window := range []int64{100, 32_000} { + params := testutil.TestParams() + params.SignedBlocksWindow = window + require.NoError(keeper.SetParams(ctx, params)) + + // 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) + + 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) } } diff --git a/x/slashing/migrations/v2/keys.go b/x/slashing/migrations/v2/keys.go new file mode 100644 index 000000000000..26f3f97544f3 --- /dev/null +++ b/x/slashing/migrations/v2/keys.go @@ -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...) +} diff --git a/x/slashing/migrations/v2/store_test.go b/x/slashing/migrations/v2/store_test.go index b824981cb465..da583b79e6d4 100644 --- a/x/slashing/migrations/v2/store_test.go +++ b/x/slashing/migrations/v2/store_test.go @@ -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", diff --git a/x/slashing/migrations/v4/keys.go b/x/slashing/migrations/v4/keys.go new file mode 100644 index 000000000000..e2e4c7104f12 --- /dev/null +++ b/x/slashing/migrations/v4/keys.go @@ -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...) +} diff --git a/x/slashing/migrations/v4/migrate.go b/x/slashing/migrations/v4/migrate.go new file mode 100644 index 000000000000..01480c05e185 --- /dev/null +++ b/x/slashing/migrations/v4/migrate.go @@ -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 +} diff --git a/x/slashing/migrations/v4/migrate_test.go b/x/slashing/migrations/v4/migrate_test.go new file mode 100644 index 000000000000..a49ae658166f --- /dev/null +++ b/x/slashing/migrations/v4/migrate_test.go @@ -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) +} diff --git a/x/slashing/module.go b/x/slashing/module.go index b9cff5aec987..7c15f966b940 100644 --- a/x/slashing/module.go +++ b/x/slashing/module.go @@ -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 diff --git a/x/slashing/simulation/decoder.go b/x/slashing/simulation/decoder.go index d295a284ac18..1e96739d6680 100644 --- a/x/slashing/simulation/decoder.go +++ b/x/slashing/simulation/decoder.go @@ -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) diff --git a/x/slashing/simulation/decoder_test.go b/x/slashing/simulation/decoder_test.go index 599814c44cd6..85aa4fa460bc 100644 --- a/x/slashing/simulation/decoder_test.go +++ b/x/slashing/simulation/decoder_test.go @@ -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 }, diff --git a/x/slashing/types/keys.go b/x/slashing/types/keys.go index 02a86e167e28..856780f86c48 100644 --- a/x/slashing/types/keys.go +++ b/x/slashing/types/keys.go @@ -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 ) + 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: ValidatorSigningInfo // -// - 0x02: bool +// - 0x02: bitmap_chunk // // - 0x03: 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 diff --git a/x/slashing/types/slashing.pb.go b/x/slashing/types/slashing.pb.go index ce901400b720..2673f6a77f58 100644 --- a/x/slashing/types/slashing.pb.go +++ b/x/slashing/types/slashing.pb.go @@ -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"` }