Skip to content

Commit

Permalink
Merge pull request #251 from lavanet/CNS-239-get-pairing-changes
Browse files Browse the repository at this point in the history
CNS-239: add nextPairingBlock field to get-pairing output
  • Loading branch information
Yaroms authored Jan 22, 2023
2 parents e0ec4f1 + f87d50e commit 27d9362
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 223 deletions.
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

0 comments on commit 27d9362

Please sign in to comment.