Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CNS-239: add nextPairingBlock field to get-pairing output #251

Merged
merged 3 commits into from
Jan 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions proto/pairing/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ message QueryGetPairingResponse {
uint64 currentEpoch = 2;
uint64 timeLeftToNextPairing = 3;
uint64 specLastUpdatedBlock = 4;
uint64 blockOfNextPairing = 5;
}

message QueryVerifyPairingRequest {
Expand Down
4 changes: 2 additions & 2 deletions x/pairing/keeper/grpc_query_get_pairing.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (k Keeper) GetPairing(goCtx context.Context, req *types.QueryGetPairingRequ
}

// Calculate the time left until the new epoch (when epoch changes, new pairing is generated)
timeLeftToNextPairing, err := k.calculateNextEpochTime(ctx)
timeLeftToNextPairing, nextPairingBlock, err := k.calculateNextEpochTimeAndBlock(ctx)
if err != nil {
// we don't want to fail the query if the calculateNextEpochTime function fails. This shouldn't happen, it's a fail-safe
utils.LavaFormatError("calculate next epoch time failed. Returning default time=0", err, nil)
Expand All @@ -57,5 +57,5 @@ func (k Keeper) GetPairing(goCtx context.Context, req *types.QueryGetPairingRequ
}
specLastUpdatedBlock := spec.BlockLastUpdated

return &types.QueryGetPairingResponse{Providers: providers, CurrentEpoch: currentEpoch, TimeLeftToNextPairing: timeLeftToNextPairing, SpecLastUpdatedBlock: specLastUpdatedBlock}, nil
return &types.QueryGetPairingResponse{Providers: providers, CurrentEpoch: currentEpoch, TimeLeftToNextPairing: timeLeftToNextPairing, SpecLastUpdatedBlock: specLastUpdatedBlock, BlockOfNextPairing: nextPairingBlock}, nil
}
136 changes: 0 additions & 136 deletions x/pairing/keeper/pairing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ package keeper

import (
"fmt"
"math"
"math/big"
"strconv"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
epochstoragetypes "github.com/lavanet/lava/x/epochstorage/types"
pairingtypes "github.com/lavanet/lava/x/pairing/types"
tendermintcrypto "github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/rpc/core"
)
Expand Down Expand Up @@ -221,136 +218,3 @@ func (k Keeper) returnSubsetOfProvidersByStake(ctx sdk.Context, clientAddress sd
}
return returnedProviders
}

const (
EPOCH_BLOCK_DIVIDER uint64 = 5 // determines how many blocks from the previous epoch will be included in the average block time calculation
MIN_SAMPLE_STEP uint64 = 1 // the minimal sample step when calculating the average block time
)

// Function to calculate how much time (in seconds) is left until the next epoch
func (k Keeper) calculateNextEpochTime(ctx sdk.Context) (uint64, error) {
// Get current epoch
currentEpoch := k.epochStorageKeeper.GetEpochStart(ctx)

// Calculate the average block time (i.e., how much time it takes to create a new block, in average)
averageBlockTime, err := k.calculateAverageBlockTime(ctx, currentEpoch)
if err != nil {
return 0, fmt.Errorf("could not calculate average block time, err: %s", err)
}

// Get the next epoch
nextEpochStart, err := k.epochStorageKeeper.GetNextEpoch(ctx, currentEpoch)
if err != nil {
return 0, fmt.Errorf("could not get next epoch start, err: %s", err)
}

// Get epochBlocksOverlap
overlapBlocks := k.EpochBlocksOverlap(ctx)

// Get number of blocks from the current block to the next epoch
blocksUntilNewEpoch := nextEpochStart + overlapBlocks - uint64(ctx.BlockHeight())

// Calculate the time left for the next pairing in seconds (blocks left * avg block time)
timeLeftToNextEpoch := blocksUntilNewEpoch * averageBlockTime

return timeLeftToNextEpoch, nil
}

// Function to calculate the average block time (i.e., how much time it takes to create a new block, in average)
func (k Keeper) calculateAverageBlockTime(ctx sdk.Context, epoch uint64) (uint64, error) {
// Get epochBlocks (the number of blocks in an epoch)
epochBlocks, err := k.epochStorageKeeper.EpochBlocks(ctx, epoch)
if err != nil {
return 0, fmt.Errorf("could not get epochBlocks, err: %s", err)
}

// Define sample step. Determines which timestamps will be taken in the average block time calculation.
// if epochBlock < EPOCH_BLOCK_DIVIDER -> sampleStep = MIN_SAMPLE_STEP.
// else sampleStep will be epochBlocks/EPOCH_BLOCK_DIVIDER
if MIN_SAMPLE_STEP > epochBlocks {
return 0, fmt.Errorf("invalid MIN_SAMPLE_STEP value since it's larger than epochBlocks. MIN_SAMPLE_STEP: %v, epochBlocks: %v", MIN_SAMPLE_STEP, epochBlocks)
}
sampleStep := MIN_SAMPLE_STEP
if epochBlocks > EPOCH_BLOCK_DIVIDER {
sampleStep = epochBlocks / EPOCH_BLOCK_DIVIDER
}

// Get a list of the previous epoch's blocks timestamp and height
prevEpochTimestampAndHeightList, err := k.getPreviousEpochTimestampsByHeight(ctx, epoch, sampleStep)
if pairingtypes.NoPreviousEpochForAverageBlockTimeCalculationError.Is(err) || pairingtypes.PreviousEpochStartIsBlockZeroError.Is(err) {
// if the errors indicate that we're on the first epoch / after a fork or previous epoch start is 0, we return averageBlockTime=0 without an error
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("couldn't get prevEpochTimestampAndHeightList. err: %v", err)
}

// Calculate the average block time from prevEpochTimestampAndHeightList
averageBlockTime, err := calculateAverageBlockTimeFromList(ctx, prevEpochTimestampAndHeightList, sampleStep)
if pairingtypes.NotEnoughBlocksToCalculateAverageBlockTimeError.Is(err) || pairingtypes.AverageBlockTimeIsLessOrEqualToZeroError.Is(err) {
// we shouldn't fail the get-pairing query because the average block time calculation failed (to indicate the fail, we return 0)
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("couldn't calculate average block time. err: %v", err)
}

return averageBlockTime, nil
}

type blockHeightAndTime struct {
blockHeight uint64
blockTime time.Time
}

// Function to get a list of the timestamps of the blocks in the previous epoch of the input (so it'll be deterministic)
func (k Keeper) getPreviousEpochTimestampsByHeight(ctx sdk.Context, epoch uint64, sampleStep uint64) ([]blockHeightAndTime, error) {
// Check for special cases:
// 1. no previous epoch - we're on the first epoch / after a fork. Since there is no previous epoch to calculate average time on, return an empty slice and no error
// 2. start of previous epoch is block 0 - we're on the second epoch. To get the block's header using the "core" module, the block height can't be zero (causes panic). In this case, we also return an empty slice and no error
prevEpoch, err := k.epochStorageKeeper.GetPreviousEpochStartForBlock(ctx, epoch)
if err != nil {
return nil, pairingtypes.NoPreviousEpochForAverageBlockTimeCalculationError
} else if prevEpoch == 0 {
return nil, pairingtypes.PreviousEpochStartIsBlockZeroError
}

// Get previous epoch timestamps, in sampleStep steps
prevEpochTimestampAndHeightList := []blockHeightAndTime{}
for block := prevEpoch; block <= epoch; block += sampleStep {
// Get current block's height and timestamp
blockInt64 := int64(block)
blockCore, err := core.Block(nil, &blockInt64)
if err != nil {
return nil, fmt.Errorf("could not get current block header, block: %v, err: %s", blockInt64, err)
}
currentBlockTimestamp := blockCore.Block.Header.Time.UTC()
blockHeightAndTimeStruct := blockHeightAndTime{blockHeight: block, blockTime: currentBlockTimestamp}

// Append the timestamp to the timestamp list
prevEpochTimestampAndHeightList = append(prevEpochTimestampAndHeightList, blockHeightAndTimeStruct)
}

return prevEpochTimestampAndHeightList, nil
}

func calculateAverageBlockTimeFromList(ctx sdk.Context, blockHeightAndTimeList []blockHeightAndTime, sampleStep uint64) (uint64, error) {
if len(blockHeightAndTimeList) <= 1 {
return 0, utils.LavaFormatError("There isn't enough blockHeight structs in the previous epoch to calculate average block time", pairingtypes.NotEnoughBlocksToCalculateAverageBlockTimeError, nil)
}

averageBlockTime := time.Duration(math.MaxInt64)
for i := 1; i < len(blockHeightAndTimeList); i++ {
// Calculate the average block time creation over sampleStep blocks
currentAverageBlockTime := blockHeightAndTimeList[i].blockTime.Sub(blockHeightAndTimeList[i-1].blockTime) / time.Duration(sampleStep)
if currentAverageBlockTime <= 0 {
return 0, utils.LavaFormatError("calculated average block time is less than or equal to zero", pairingtypes.AverageBlockTimeIsLessOrEqualToZeroError, &map[string]string{"block": fmt.Sprintf("%v", blockHeightAndTimeList[i].blockHeight), "block timestamp": blockHeightAndTimeList[i].blockTime.String(), "prevBlock": fmt.Sprintf("%v", blockHeightAndTimeList[i-1].blockHeight), "prevBlock timestamp": blockHeightAndTimeList[i-1].blockTime.String()})
}
// save the minimal average block time
if averageBlockTime > currentAverageBlockTime {
averageBlockTime = currentAverageBlockTime
}
}

return uint64(averageBlockTime.Seconds()), nil
}
148 changes: 148 additions & 0 deletions x/pairing/keeper/pairing_next_epoch_time_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package keeper

import (
"fmt"
"math"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
pairingtypes "github.com/lavanet/lava/x/pairing/types"
"github.com/tendermint/tendermint/rpc/core"
)

const (
EPOCH_BLOCK_DIVIDER uint64 = 5 // determines how many blocks from the previous epoch will be included in the average block time calculation
MIN_SAMPLE_STEP uint64 = 1 // the minimal sample step when calculating the average block time
)

// Function to calculate how much time (in seconds) is left until the next epoch
func (k Keeper) calculateNextEpochTimeAndBlock(ctx sdk.Context) (uint64, uint64, error) {
// Get current epoch
currentEpoch := k.epochStorageKeeper.GetEpochStart(ctx)

// Calculate the average block time (i.e., how much time it takes to create a new block, in average)
averageBlockTime, err := k.calculateAverageBlockTime(ctx, currentEpoch)
if err != nil {
return 0, 0, fmt.Errorf("could not calculate average block time, err: %s", err)
}

// Get the next epoch
nextEpochStart, err := k.epochStorageKeeper.GetNextEpoch(ctx, currentEpoch)
if err != nil {
return 0, 0, fmt.Errorf("could not get next epoch start, err: %s", err)
}

// Get epochBlocksOverlap
overlapBlocks := k.EpochBlocksOverlap(ctx)

// calculate the block in which the next pairing will happen (+overlap)
nextPairingBlock := nextEpochStart + overlapBlocks

// Get number of blocks from the current block to the next epoch
blocksUntilNewEpoch := nextPairingBlock - uint64(ctx.BlockHeight())

// Calculate the time left for the next pairing in seconds (blocks left * avg block time)
timeLeftToNextEpoch := blocksUntilNewEpoch * averageBlockTime

return timeLeftToNextEpoch, nextPairingBlock, nil
}

// Function to calculate the average block time (i.e., how much time it takes to create a new block, in average)
func (k Keeper) calculateAverageBlockTime(ctx sdk.Context, epoch uint64) (uint64, error) {
// Get epochBlocks (the number of blocks in an epoch)
epochBlocks, err := k.epochStorageKeeper.EpochBlocks(ctx, epoch)
if err != nil {
return 0, fmt.Errorf("could not get epochBlocks, err: %s", err)
}

// Define sample step. Determines which timestamps will be taken in the average block time calculation.
// if epochBlock < EPOCH_BLOCK_DIVIDER -> sampleStep = MIN_SAMPLE_STEP.
// else sampleStep will be epochBlocks/EPOCH_BLOCK_DIVIDER
if MIN_SAMPLE_STEP > epochBlocks {
return 0, fmt.Errorf("invalid MIN_SAMPLE_STEP value since it's larger than epochBlocks. MIN_SAMPLE_STEP: %v, epochBlocks: %v", MIN_SAMPLE_STEP, epochBlocks)
}
sampleStep := MIN_SAMPLE_STEP
if epochBlocks > EPOCH_BLOCK_DIVIDER {
sampleStep = epochBlocks / EPOCH_BLOCK_DIVIDER
}

// Get a list of the previous epoch's blocks timestamp and height
prevEpochTimestampAndHeightList, err := k.getPreviousEpochTimestampsByHeight(ctx, epoch, sampleStep)
if pairingtypes.NoPreviousEpochForAverageBlockTimeCalculationError.Is(err) || pairingtypes.PreviousEpochStartIsBlockZeroError.Is(err) {
// if the errors indicate that we're on the first epoch / after a fork or previous epoch start is 0, we return averageBlockTime=0 without an error
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("couldn't get prevEpochTimestampAndHeightList. err: %v", err)
}

// Calculate the average block time from prevEpochTimestampAndHeightList
averageBlockTime, err := calculateAverageBlockTimeFromList(ctx, prevEpochTimestampAndHeightList, sampleStep)
if pairingtypes.NotEnoughBlocksToCalculateAverageBlockTimeError.Is(err) || pairingtypes.AverageBlockTimeIsLessOrEqualToZeroError.Is(err) {
// we shouldn't fail the get-pairing query because the average block time calculation failed (to indicate the fail, we return 0)
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("couldn't calculate average block time. err: %v", err)
}

return averageBlockTime, nil
}

type blockHeightAndTime struct {
blockHeight uint64
blockTime time.Time
}

// Function to get a list of the timestamps of the blocks in the previous epoch of the input (so it'll be deterministic)
func (k Keeper) getPreviousEpochTimestampsByHeight(ctx sdk.Context, epoch uint64, sampleStep uint64) ([]blockHeightAndTime, error) {
// Check for special cases:
// 1. no previous epoch - we're on the first epoch / after a fork. Since there is no previous epoch to calculate average time on, return an empty slice and no error
// 2. start of previous epoch is block 0 - we're on the second epoch. To get the block's header using the "core" module, the block height can't be zero (causes panic). In this case, we also return an empty slice and no error
prevEpoch, err := k.epochStorageKeeper.GetPreviousEpochStartForBlock(ctx, epoch)
if err != nil {
return nil, pairingtypes.NoPreviousEpochForAverageBlockTimeCalculationError
} else if prevEpoch == 0 {
return nil, pairingtypes.PreviousEpochStartIsBlockZeroError
}

// Get previous epoch timestamps, in sampleStep steps
prevEpochTimestampAndHeightList := []blockHeightAndTime{}
for block := prevEpoch; block <= epoch; block += sampleStep {
// Get current block's height and timestamp
blockInt64 := int64(block)
blockCore, err := core.Block(nil, &blockInt64)
if err != nil {
return nil, fmt.Errorf("could not get current block header, block: %v, err: %s", blockInt64, err)
}
currentBlockTimestamp := blockCore.Block.Header.Time.UTC()
blockHeightAndTimeStruct := blockHeightAndTime{blockHeight: block, blockTime: currentBlockTimestamp}

// Append the timestamp to the timestamp list
prevEpochTimestampAndHeightList = append(prevEpochTimestampAndHeightList, blockHeightAndTimeStruct)
}

return prevEpochTimestampAndHeightList, nil
}

func calculateAverageBlockTimeFromList(ctx sdk.Context, blockHeightAndTimeList []blockHeightAndTime, sampleStep uint64) (uint64, error) {
if len(blockHeightAndTimeList) <= 1 {
return 0, utils.LavaFormatError("There isn't enough blockHeight structs in the previous epoch to calculate average block time", pairingtypes.NotEnoughBlocksToCalculateAverageBlockTimeError, nil)
}

averageBlockTime := time.Duration(math.MaxInt64)
for i := 1; i < len(blockHeightAndTimeList); i++ {
// Calculate the average block time creation over sampleStep blocks
currentAverageBlockTime := blockHeightAndTimeList[i].blockTime.Sub(blockHeightAndTimeList[i-1].blockTime) / time.Duration(sampleStep)
if currentAverageBlockTime <= 0 {
return 0, utils.LavaFormatError("calculated average block time is less than or equal to zero", pairingtypes.AverageBlockTimeIsLessOrEqualToZeroError, &map[string]string{"block": fmt.Sprintf("%v", blockHeightAndTimeList[i].blockHeight), "block timestamp": blockHeightAndTimeList[i].blockTime.String(), "prevBlock": fmt.Sprintf("%v", blockHeightAndTimeList[i-1].blockHeight), "prevBlock timestamp": blockHeightAndTimeList[i-1].blockTime.String()})
}
// save the minimal average block time
if averageBlockTime > currentAverageBlockTime {
averageBlockTime = currentAverageBlockTime
}
}

return uint64(averageBlockTime.Seconds()), nil
}
8 changes: 7 additions & 1 deletion x/pairing/keeper/pairing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,11 @@ func TestGetPairing(t *testing.T) {
// Get epochBlocksOverlap
overlapBlocks := ts.keepers.Pairing.EpochBlocksOverlap(sdk.UnwrapSDKContext(ts.ctx))

// calculate the block in which the next pairing will happen (+overlap)
nextPairingBlock := nextEpochStart + overlapBlocks

// Get number of blocks from the current block to the next epoch
blocksUntilNewEpoch := nextEpochStart + overlapBlocks - uint64(sdk.UnwrapSDKContext(ts.ctx).BlockHeight())
blocksUntilNewEpoch := nextPairingBlock - uint64(sdk.UnwrapSDKContext(ts.ctx).BlockHeight())

// Calculate the time left for the next pairing in seconds (blocks left * avg block time)
timeLeftToNextPairing := blocksUntilNewEpoch * averageBlockTime
Expand All @@ -172,6 +175,9 @@ func TestGetPairing(t *testing.T) {
// we've used a smaller blocktime some of the time -> averageBlockTime from get-pairing is smaller than the averageBlockTime calculated in this test
require.Less(t, pairing.TimeLeftToNextPairing, timeLeftToNextPairing)
}

// verify nextPairingBlock
require.Equal(t, nextPairingBlock, pairing.BlockOfNextPairing)
}

})
Expand Down
Loading