Skip to content

Commit

Permalink
feat: add epoching module (cosmos#10132)
Browse files Browse the repository at this point in the history
* strip out epoch module

* Apply suggestions from code review

Co-authored-by: Aleksandr Bezobchuk <[email protected]>

* address comments

* add todo

* DN: mistake

* Apply suggestions from code review

Co-authored-by: Aleksandr Bezobchuk <[email protected]>

* Update x/epoching/keeper/keeper.go

Co-authored-by: Aleksandr Bezobchuk <[email protected]>

Co-authored-by: Aleksandr Bezobchuk <[email protected]>
  • Loading branch information
tac0turtle and alexanderbez authored Sep 16, 2021
1 parent 5e98404 commit cfc8b47
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 0 deletions.
191 changes: 191 additions & 0 deletions x/epoching/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -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))
}
58 changes: 58 additions & 0 deletions x/epoching/spec/01_state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
order: 1
-->

# 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
44 changes: 44 additions & 0 deletions x/epoching/spec/03_to_improve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!--
order: 3
-->

# 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
```
37 changes: 37 additions & 0 deletions x/epoching/spec/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!--
order: 20
title: Epoching Overview
parent:
title: "epoching"
-->

# `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)**

0 comments on commit cfc8b47

Please sign in to comment.