diff --git a/x/epoching/keeper/keeper.go b/x/epoching/keeper/keeper.go new file mode 100644 index 000000000..304737523 --- /dev/null +++ b/x/epoching/keeper/keeper.go @@ -0,0 +1,191 @@ +package keeper + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + db "github.com/tendermint/tm-db" +) + +const ( + DefaultEpochActionID = 1 + DefaultEpochNumber = 0 +) + +var ( + NextEpochActionID = []byte{0x11} + EpochNumberID = []byte{0x12} + EpochActionQueuePrefix = []byte{0x13} // prefix for the epoch +) + +// Keeper of the store +type Keeper struct { + storeKey sdk.StoreKey + cdc codec.BinaryCodec + // Used to calculate the estimated next epoch time. + // This is local to every node + // TODO: remove in favor of consensus param when its added + commitTimeout time.Duration +} + +// NewKeeper creates a epoch queue manager +func NewKeeper(cdc codec.BinaryCodec, key sdk.StoreKey, commitTimeout time.Duration) Keeper { + return Keeper{ + storeKey: key, + cdc: cdc, + commitTimeout: commitTimeout, + } +} + +// GetNewActionID returns ID to be used for next epoch +func (k Keeper) GetNewActionID(ctx sdk.Context) uint64 { + store := ctx.KVStore(k.storeKey) + + bz := store.Get(NextEpochActionID) + if bz == nil { + // return default action ID to 1 + return DefaultEpochActionID + } + id := sdk.BigEndianToUint64(bz) + + // increment next action ID + store.Set(NextEpochActionID, sdk.Uint64ToBigEndian(id+1)) + + return id +} + +// ActionStoreKey returns action store key from ID +func ActionStoreKey(epochNumber int64, actionID uint64) []byte { + return append(EpochActionQueuePrefix, byte(epochNumber), byte(actionID)) +} + +// QueueMsgForEpoch save the actions that need to be executed on next epoch +func (k Keeper) QueueMsgForEpoch(ctx sdk.Context, epochNumber int64, msg sdk.Msg) { + store := ctx.KVStore(k.storeKey) + + bz, err := k.cdc.MarshalInterface(msg) + if err != nil { + panic(err) + } + + actionID := k.GetNewActionID(ctx) + store.Set(ActionStoreKey(epochNumber, actionID), bz) +} + +// RestoreEpochAction restore the actions that need to be executed on next epoch +func (k Keeper) RestoreEpochAction(ctx sdk.Context, epochNumber int64, action *codectypes.Any) { + store := ctx.KVStore(k.storeKey) + + // reference from TestMarshalAny(t *testing.T) + bz, err := k.cdc.MarshalInterface(action) + if err != nil { + panic(err) + } + + actionID := k.GetNewActionID(ctx) + store.Set(ActionStoreKey(epochNumber, actionID), bz) +} + +// GetEpochMsg gets a msg by ID +func (k Keeper) GetEpochMsg(ctx sdk.Context, epochNumber int64, actionID uint64) sdk.Msg { + store := ctx.KVStore(k.storeKey) + + bz := store.Get(ActionStoreKey(epochNumber, actionID)) + if bz == nil { + return nil + } + + var action sdk.Msg + k.cdc.UnmarshalInterface(bz, &action) + + return action +} + +// GetEpochActions get all actions +func (k Keeper) GetEpochActions(ctx sdk.Context) []sdk.Msg { + actions := []sdk.Msg{} + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), []byte(EpochActionQueuePrefix)) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var action sdk.Msg + bz := iterator.Value() + k.cdc.UnmarshalInterface(bz, &action) + actions = append(actions, action) + } + + return actions +} + +// GetEpochActionsIterator returns iterator for EpochActions +func (k Keeper) GetEpochActionsIterator(ctx sdk.Context) db.Iterator { + return sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), []byte(EpochActionQueuePrefix)) +} + +// DequeueEpochActions dequeue all the actions store on epoch +func (k Keeper) DequeueEpochActions(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, []byte(EpochActionQueuePrefix)) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + key := iterator.Key() + store.Delete(key) + } +} + +// DeleteByKey delete item by key +func (k Keeper) DeleteByKey(ctx sdk.Context, key []byte) { + store := ctx.KVStore(k.storeKey) + store.Delete(key) +} + +// GetEpochActionByIterator get action by iterator +func (k Keeper) GetEpochActionByIterator(iterator db.Iterator) sdk.Msg { + bz := iterator.Value() + + var action sdk.Msg + k.cdc.UnmarshalInterface(bz, &action) + + return action +} + +// SetEpochNumber set epoch number +func (k Keeper) SetEpochNumber(ctx sdk.Context, epochNumber int64) { + store := ctx.KVStore(k.storeKey) + store.Set(EpochNumberID, sdk.Uint64ToBigEndian(uint64(epochNumber))) +} + +// GetEpochNumber fetches epoch number +func (k Keeper) GetEpochNumber(ctx sdk.Context) int64 { + store := ctx.KVStore(k.storeKey) + + bz := store.Get(EpochNumberID) + if bz == nil { + return DefaultEpochNumber + } + + return int64(sdk.BigEndianToUint64(bz)) +} + +// IncreaseEpochNumber increases epoch number +func (k Keeper) IncreaseEpochNumber(ctx sdk.Context) { + epochNumber := k.GetEpochNumber(ctx) + k.SetEpochNumber(ctx, epochNumber+1) +} + +// GetNextEpochHeight returns next epoch block height +func (k Keeper) GetNextEpochHeight(ctx sdk.Context, epochInterval int64) int64 { + currentHeight := ctx.BlockHeight() + return currentHeight + (epochInterval - currentHeight%epochInterval) +} + +// GetNextEpochTime returns estimated next epoch time +func (k Keeper) GetNextEpochTime(ctx sdk.Context, epochInterval int64) time.Time { + currentTime := ctx.BlockTime() + currentHeight := ctx.BlockHeight() + + return currentTime.Add(k.commitTimeout * time.Duration(k.GetNextEpochHeight(ctx, epochInterval)-currentHeight)) +} diff --git a/x/epoching/spec/01_state.md b/x/epoching/spec/01_state.md new file mode 100644 index 000000000..485dace71 --- /dev/null +++ b/x/epoching/spec/01_state.md @@ -0,0 +1,58 @@ + + +# State + +## Messages queue + +Messages are queued to run at the end of each epoch. Queued messages have an epoch number and for each epoch number, the queues are iterated over and each message is executed. + +### Message queues + +Each module has one unique message queue that is specific to that module. + +## Actions + +A module will add a message that implements the `sdk.Msg` interface. These message will be executed at a later time (end of the next epoch). + +```go +type Msg interface { + proto.Message + + // Return the message type. + // Must be alphanumeric or empty. + Route() string + + // Returns a human-readable string for the message, intended for utilization + // within tags + Type() string + + // ValidateBasic does a simple validation check that + // doesn't require access to any other information. + ValidateBasic() error + + // Get the canonical byte representation of the Msg. + GetSignBytes() []byte + + // Signers returns the addrs of signers that must sign. + // CONTRACT: All signatures must be present to be valid. + // CONTRACT: Returns addrs in some deterministic order. + GetSigners() []AccAddress + } +``` + +## Buffered Messages Export / Import + +For now, the `x/epoching` module is implemented to export all buffered messages without epoch numbers. When state is imported, buffered messages are stored on current epoch to run at the end of current epoch. + +## Genesis Transactions + +We execute epoch after execution of genesis transactions to see the changes instantly before node start. + +## Execution on epochs + +- Try executing the message for the epoch +- If success, make changes as it is +- If failure, try making revert extra actions done on handlers (e.g. EpochDelegationPool deposit) +- If revert fail, panic diff --git a/x/epoching/spec/03_to_improve.md b/x/epoching/spec/03_to_improve.md new file mode 100644 index 000000000..d1b506f01 --- /dev/null +++ b/x/epoching/spec/03_to_improve.md @@ -0,0 +1,44 @@ + + +# Changes to make + +## Validator self-unbonding (which exceed minimum self delegation) could be required to start instantly + +Cases that trigger unbonding process + +- Validator undelegate can unbond more tokens than his minimum_self_delegation and it will automatically turn the validator into unbonding +In this case, unbonding should start instantly. +- Validator miss blocks and get slashed +- Validator get slashed for double sign + +**Note:** When a validator begins the unbonding process, it could be required to turn the validator into unbonding state instantly. + This is different than a specific delegator beginning to unbond. A validator beginning to unbond means that it's not in the set any more. + A delegator unbonding from a validator removes their delegation from the validator. + +## Pending development + +```go +// Changes to make +// — Implement correct next epoch time calculation +// — For validator self undelegation, it could be required to do start on end blocker +// — Implement TODOs on the PR #46 +// Implement CLI commands for querying +// — BufferedValidators +// — BufferedMsgCreateValidatorQueue, BufferedMsgEditValidatorQueue +// — BufferedMsgUnjailQueue, BufferedMsgDelegateQueue, BufferedMsgRedelegationQueue, BufferedMsgUndelegateQueue +// Write epoch related tests with new scenarios +// — Simulation test is important for finding bugs [Ask Dev for questions) +// — Can easily add a simulator check to make sure all delegation amounts in queue add up to the same amount that’s in the EpochUnbondedPool +// — I’d like it added as an invariant test for the simulator +// — the simulator should check that the sum of all the queued delegations always equals the amount kept track in the data +// — Staking/Slashing/Distribution module params are being modified by governance based on vote result instantly. We should test the effect. +// — — Should test to see what would happen if max_validators is changed though, in the middle of an epoch +// — we should define some new invariants that help check that everything is working smoothly with these new changes for 3 modules e.g. https://github.com/cosmos/cosmos-sdk/blob/master/x/staking/keeper/invariants.go +// — — Within Epoch, ValidationPower = ValidationPower - SlashAmount +// — — When epoch actions queue is empty, EpochDelegationPool balance should be zero +// — we should count all the delegation changes that happen during the epoch, and then make sure that the resulting change at the end of the epoch is actually correct +// — If the validator that I delegated to double signs at block 16, I should still get slashed instantly because even though I asked to unbond at 14, they still used my power at block 16, I should only be not liable for slashes once my power is stopped being used +// — On the converse of this, I should still be getting rewards while my power is being used. I shouldn’t stop receiving rewards until block 20 +``` diff --git a/x/epoching/spec/README.md b/x/epoching/spec/README.md new file mode 100644 index 000000000..8dcd4b9b7 --- /dev/null +++ b/x/epoching/spec/README.md @@ -0,0 +1,37 @@ + + +# `x/epoching` + +## Abstract + +The epoching module allows modules to queue messages for execution at a certain block height. Each module will have its own instance of the epoching module, this allows each module to have its own message queue and own duration for epochs. + +## Example + +In this example, we are creating an epochkeeper for a module that will be used by the module to queue messages to be executed at a later point in time. + +```go +type Keeper struct { + storeKey sdk.StoreKey + cdc codec.BinaryMarshaler + epochKeeper epochkeeper.Keeper +} + +// NewKeeper creates a new staking Keeper instance +func NewKeeper(cdc codec.BinaryMarshaler, key sdk.StoreKey) Keeper { + return Keeper{ + storeKey: key, + cdc: cdc, + epochKeeper: epochkeeper.NewKeeper(cdc, key), + } +} +``` + +### Contents + +1. **[State](01_state.md)**