adds mockprovider to simplify and improve testing of the edge conditions
This commit is contained in:
parent
3ddad7bbbd
commit
7a28634d88
|
@ -1,4 +1,5 @@
|
||||||
import std/strutils
|
import std/strutils
|
||||||
|
import std/times
|
||||||
import pkg/ethers
|
import pkg/ethers
|
||||||
import pkg/upraises
|
import pkg/upraises
|
||||||
import pkg/questionable
|
import pkg/questionable
|
||||||
|
@ -410,17 +411,6 @@ proc blockNumberAndTimestamp*(provider: Provider, blockTag: BlockTag):
|
||||||
|
|
||||||
(latestBlockNumber, latestBlock.timestamp)
|
(latestBlockNumber, latestBlock.timestamp)
|
||||||
|
|
||||||
proc estimateAverageBlockTime*(provider: Provider): Future[UInt256] {.async.} =
|
|
||||||
let (latestBlockNumber, latestBlockTimestamp) =
|
|
||||||
await provider.blockNumberAndTimestamp(BlockTag.latest)
|
|
||||||
let (_, previousBlockTimestamp) =
|
|
||||||
await provider.blockNumberAndTimestamp(
|
|
||||||
BlockTag.init(latestBlockNumber - 1.u256))
|
|
||||||
debug "[estimateAverageBlockTime]:", latestBlockNumber = latestBlockNumber,
|
|
||||||
latestBlockTimestamp = latestBlockTimestamp,
|
|
||||||
previousBlockTimestamp = previousBlockTimestamp
|
|
||||||
return latestBlockTimestamp - previousBlockTimestamp
|
|
||||||
|
|
||||||
proc binarySearchFindClosestBlock*(provider: Provider,
|
proc binarySearchFindClosestBlock*(provider: Provider,
|
||||||
epochTime: int,
|
epochTime: int,
|
||||||
low: UInt256,
|
low: UInt256,
|
||||||
|
@ -468,8 +458,6 @@ proc binarySearchBlockNumberForEpoch*(provider: Provider,
|
||||||
|
|
||||||
proc blockNumberForEpoch*(provider: Provider,
|
proc blockNumberForEpoch*(provider: Provider,
|
||||||
epochTime: SecondsSince1970): Future[UInt256] {.async.} =
|
epochTime: SecondsSince1970): Future[UInt256] {.async.} =
|
||||||
let avgBlockTime = await provider.estimateAverageBlockTime()
|
|
||||||
debug "[blockNumberForEpoch]:", avgBlockTime = avgBlockTime
|
|
||||||
debug "[blockNumberForEpoch]:", epochTime = epochTime
|
debug "[blockNumberForEpoch]:", epochTime = epochTime
|
||||||
let epochTimeUInt256 = epochTime.u256
|
let epochTimeUInt256 = epochTime.u256
|
||||||
let (latestBlockNumber, latestBlockTimestamp) =
|
let (latestBlockNumber, latestBlockTimestamp) =
|
||||||
|
@ -482,12 +470,45 @@ proc blockNumberForEpoch*(provider: Provider,
|
||||||
debug "[blockNumberForEpoch]:", earliestBlockNumber = earliestBlockNumber,
|
debug "[blockNumberForEpoch]:", earliestBlockNumber = earliestBlockNumber,
|
||||||
earliestBlockTimestamp = earliestBlockTimestamp
|
earliestBlockTimestamp = earliestBlockTimestamp
|
||||||
|
|
||||||
let timeDiff = latestBlockTimestamp - epochTimeUInt256
|
# Initially we used the average block time to predict
|
||||||
let blockDiff = timeDiff div avgBlockTime
|
# the number of blocks we need to look back in order to find
|
||||||
|
# the block number corresponding to the given epoch time.
|
||||||
|
# This estimation can be highly inaccurate if block time
|
||||||
|
# was changing in the past or is fluctuating and therefore
|
||||||
|
# we used that information initially only to find out
|
||||||
|
# if the available history is long enough to perform effective search.
|
||||||
|
# It turns out we do not have to do that. There is an easier way.
|
||||||
|
#
|
||||||
|
# First we check if the given epoch time equals the timestamp of either
|
||||||
|
# the earliest or the latest block. If it does, we just return the
|
||||||
|
# block number of that block.
|
||||||
|
#
|
||||||
|
# Otherwise, if the earliest available block is not the genesis block,
|
||||||
|
# we should check the timestamp of that earliest block and if it is greater
|
||||||
|
# than the epoch time, we should issue a warning and return
|
||||||
|
# that earliest block number.
|
||||||
|
# In all other cases, thus when the earliest block is not the genesis
|
||||||
|
# block but its timestamp is not greater than the requested epoch time, or
|
||||||
|
# if the earliest available block is the genesis block,
|
||||||
|
# (which means we have the whole history available), we should proceed with
|
||||||
|
# the binary search.
|
||||||
|
#
|
||||||
|
# Additional benefit of this method is that we do not have to rely
|
||||||
|
# on the average block time, which not only makes the whole thing
|
||||||
|
# more reliable, but also easier to test.
|
||||||
|
|
||||||
debug "[blockNumberForEpoch]:", timeDiff = timeDiff, blockDiff = blockDiff
|
# Are lucky today?
|
||||||
|
if earliestBlockTimestamp == epochTimeUInt256:
|
||||||
|
return earliestBlockNumber
|
||||||
|
if latestBlockTimestamp == epochTimeUInt256:
|
||||||
|
return latestBlockNumber
|
||||||
|
|
||||||
if blockDiff >= latestBlockNumber - earliestBlockNumber:
|
if earliestBlockNumber > 0 and earliestBlockTimestamp > epochTimeUInt256:
|
||||||
|
let availableHistoryInDays =
|
||||||
|
(latestBlockTimestamp - earliestBlockTimestamp) div
|
||||||
|
initDuration(days = 1).inSeconds.u256
|
||||||
|
warn "Short block history detected.", earliestBlockTimestamp =
|
||||||
|
earliestBlockTimestamp, days = availableHistoryInDays
|
||||||
return earliestBlockNumber
|
return earliestBlockNumber
|
||||||
|
|
||||||
return await provider.binarySearchBlockNumberForEpoch(
|
return await provider.binarySearchBlockNumberForEpoch(
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import std/strutils
|
||||||
|
import std/tables
|
||||||
|
|
||||||
|
import pkg/ethers/provider
|
||||||
|
from codex/clock import SecondsSince1970
|
||||||
|
|
||||||
|
export provider.Block
|
||||||
|
|
||||||
|
type MockProvider* = ref object of Provider
|
||||||
|
blocks: OrderedTableRef[int, Block]
|
||||||
|
earliest: ?int
|
||||||
|
latest: ?int
|
||||||
|
|
||||||
|
method getBlock*(
|
||||||
|
provider: MockProvider,
|
||||||
|
tag: BlockTag
|
||||||
|
): Future[?Block] {.async.} =
|
||||||
|
if $tag == "latest":
|
||||||
|
if latestBlock =? provider.latest:
|
||||||
|
if provider.blocks.hasKey(latestBlock):
|
||||||
|
return provider.blocks[latestBlock].some
|
||||||
|
elif $tag == "earliest":
|
||||||
|
if earliestBlock =? provider.earliest:
|
||||||
|
if provider.blocks.hasKey(earliestBlock):
|
||||||
|
return provider.blocks[earliestBlock].some
|
||||||
|
else:
|
||||||
|
let blockNumber = parseHexInt($tag)
|
||||||
|
if provider.blocks.hasKey(blockNumber):
|
||||||
|
return provider.blocks[blockNumber].some
|
||||||
|
return Block.none
|
||||||
|
|
||||||
|
proc updateEarliestAndLatest(provider: MockProvider, blockNumber: int) =
|
||||||
|
if provider.earliest.isNone:
|
||||||
|
provider.earliest = blockNumber.some
|
||||||
|
provider.latest = blockNumber.some
|
||||||
|
|
||||||
|
proc addBlocks*(provider: MockProvider, blocks: OrderedTableRef[int, Block]) =
|
||||||
|
for number, blk in blocks.pairs:
|
||||||
|
if provider.blocks.hasKey(number):
|
||||||
|
continue
|
||||||
|
provider.updateEarliestAndLatest(number)
|
||||||
|
provider.blocks[number] = blk
|
||||||
|
|
||||||
|
proc addBlock*(provider: MockProvider, number: int, blk: Block) =
|
||||||
|
if not provider.blocks.hasKey(number):
|
||||||
|
provider.updateEarliestAndLatest(number)
|
||||||
|
provider.blocks[number] = blk
|
||||||
|
|
||||||
|
proc newMockProvider*(): MockProvider =
|
||||||
|
MockProvider(
|
||||||
|
blocks: newOrderedTable[int, Block](),
|
||||||
|
earliest: int.none,
|
||||||
|
latest: int.none
|
||||||
|
)
|
||||||
|
|
||||||
|
proc newMockProvider*(blocks: OrderedTableRef[int, Block]): MockProvider =
|
||||||
|
let provider = newMockProvider()
|
||||||
|
provider.addBlocks(blocks)
|
||||||
|
provider
|
||||||
|
|
||||||
|
proc newMockProvider*(
|
||||||
|
numberOfBlocks: int,
|
||||||
|
earliestBlockNumber: int,
|
||||||
|
earliestBlockTimestamp: SecondsSince1970,
|
||||||
|
timeIntervalBetweenBlocks: SecondsSince1970
|
||||||
|
): MockProvider =
|
||||||
|
var blocks = newOrderedTable[int, provider.Block]()
|
||||||
|
var blockNumber = earliestBlockNumber
|
||||||
|
var blockTime = earliestBlockTimestamp
|
||||||
|
for i in 0..<numberOfBlocks:
|
||||||
|
blocks[blockNumber] = provider.Block(number: blockNumber.u256.some,
|
||||||
|
timestamp: blockTime.u256, hash: BlockHash.none)
|
||||||
|
inc blockNumber
|
||||||
|
inc blockTime, timeIntervalBetweenBlocks.int
|
||||||
|
MockProvider(
|
||||||
|
blocks: blocks,
|
||||||
|
earliest: earliestBlockNumber.some,
|
||||||
|
latest: (earliestBlockNumber + numberOfBlocks - 1).some
|
||||||
|
)
|
|
@ -7,6 +7,7 @@ import ../ethertest
|
||||||
import ./examples
|
import ./examples
|
||||||
import ./time
|
import ./time
|
||||||
import ./deployment
|
import ./deployment
|
||||||
|
import ./helpers/mockProvider
|
||||||
|
|
||||||
privateAccess(OnChainMarket) # enable access to private fields
|
privateAccess(OnChainMarket) # enable access to private fields
|
||||||
|
|
||||||
|
@ -494,42 +495,65 @@ ethersuite "On-Chain Market":
|
||||||
|
|
||||||
check events.len == 0
|
check events.len == 0
|
||||||
|
|
||||||
test "estimateAverageBlockTime correctly computes the time between " &
|
test "blockNumberForEpoch returns the earliest block when its timestamp " &
|
||||||
"two most recent blocks":
|
"is greater than the given epoch time and the earliest block is not " &
|
||||||
let simulatedBlockTime = 15.u256
|
"block number 0 (genesis block)":
|
||||||
await ethProvider.mineNBlocks(1)
|
let mockProvider = newMockProvider(
|
||||||
let (_, timestampPrevious) =
|
numberOfBlocks = 10,
|
||||||
await ethProvider.blockNumberAndTimestamp(BlockTag.latest)
|
earliestBlockNumber = 1,
|
||||||
|
earliestBlockTimestamp = 10,
|
||||||
|
timeIntervalBetweenBlocks = 10
|
||||||
|
)
|
||||||
|
|
||||||
await ethProvider.advanceTime(simulatedBlockTime)
|
|
||||||
|
|
||||||
let (_, timestampLatest) =
|
|
||||||
await ethProvider.blockNumberAndTimestamp(BlockTag.latest)
|
|
||||||
|
|
||||||
let expected = timestampLatest - timestampPrevious
|
|
||||||
let actual = await ethProvider.estimateAverageBlockTime()
|
|
||||||
|
|
||||||
check expected == simulatedBlockTime
|
|
||||||
check actual == expected
|
|
||||||
|
|
||||||
test "blockNumberForEpoch returns the earliest block when retained history " &
|
|
||||||
"is shorter than the given epoch time":
|
|
||||||
# create predictable conditions
|
|
||||||
# we keep minimal resultion of 1s so that we are sure that
|
|
||||||
# we will land before the earliest (genesis in our case) block
|
|
||||||
let averageBlockTime = 1.u256
|
|
||||||
await ethProvider.mineNBlocks(1)
|
|
||||||
await ethProvider.advanceTime(averageBlockTime)
|
|
||||||
let (earliestBlockNumber, earliestTimestamp) =
|
let (earliestBlockNumber, earliestTimestamp) =
|
||||||
await ethProvider.blockNumberAndTimestamp(BlockTag.earliest)
|
await mockProvider.blockNumberAndTimestamp(BlockTag.earliest)
|
||||||
|
|
||||||
let fromTime = earliestTimestamp - 1
|
let epochTime = earliestTimestamp - 1
|
||||||
|
|
||||||
let actual = await ethProvider.blockNumberForEpoch(
|
let actual = await mockProvider.blockNumberForEpoch(
|
||||||
fromTime.truncate(SecondsSince1970))
|
epochTime.truncate(SecondsSince1970))
|
||||||
|
|
||||||
check actual == earliestBlockNumber
|
check actual == earliestBlockNumber
|
||||||
|
|
||||||
|
test "blockNumberForEpoch returns the earliest block when its timestamp " &
|
||||||
|
"is equal to the given epoch time":
|
||||||
|
let mockProvider = newMockProvider(
|
||||||
|
numberOfBlocks = 10,
|
||||||
|
earliestBlockNumber = 0,
|
||||||
|
earliestBlockTimestamp = 10,
|
||||||
|
timeIntervalBetweenBlocks = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
let (earliestBlockNumber, earliestTimestamp) =
|
||||||
|
await mockProvider.blockNumberAndTimestamp(BlockTag.earliest)
|
||||||
|
|
||||||
|
let epochTime = earliestTimestamp
|
||||||
|
|
||||||
|
let actual = await mockProvider.blockNumberForEpoch(
|
||||||
|
epochTime.truncate(SecondsSince1970))
|
||||||
|
|
||||||
|
check earliestBlockNumber == 0.u256
|
||||||
|
check actual == earliestBlockNumber
|
||||||
|
|
||||||
|
test "blockNumberForEpoch returns the latest block when its timestamp " &
|
||||||
|
"is equal to the given epoch time":
|
||||||
|
let mockProvider = newMockProvider(
|
||||||
|
numberOfBlocks = 10,
|
||||||
|
earliestBlockNumber = 0,
|
||||||
|
earliestBlockTimestamp = 10,
|
||||||
|
timeIntervalBetweenBlocks = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
let (latestBlockNumber, latestTimestamp) =
|
||||||
|
await mockProvider.blockNumberAndTimestamp(BlockTag.latest)
|
||||||
|
|
||||||
|
let epochTime = latestTimestamp
|
||||||
|
|
||||||
|
let actual = await mockProvider.blockNumberForEpoch(
|
||||||
|
epochTime.truncate(SecondsSince1970))
|
||||||
|
|
||||||
|
check actual == latestBlockNumber
|
||||||
|
|
||||||
test "blockNumberForEpoch finds closest blockNumber for given epoch time":
|
test "blockNumberForEpoch finds closest blockNumber for given epoch time":
|
||||||
proc createBlockHistory(n: int, blockTime: int):
|
proc createBlockHistory(n: int, blockTime: int):
|
||||||
Future[seq[(UInt256, UInt256)]] {.async.} =
|
Future[seq[(UInt256, UInt256)]] {.async.} =
|
||||||
|
|
Loading…
Reference in New Issue