All tests working, with no time advancement

- works with hardhat already running
- works with ethersuite to revert snapshot (ethProvider instead of provider)
- OnChainClock.now() gets latest block timestamp
This commit is contained in:
Eric 2023-11-08 12:04:03 +11:00
parent 3d9eb19855
commit 045f770128
No known key found for this signature in database
16 changed files with 67 additions and 127 deletions

View File

@ -14,9 +14,6 @@ type
method now*(clock: Clock): SecondsSince1970 {.base, upraises: [].} =
raiseAssert "not implemented"
method lastBlockTimestamp*(clock: Clock): Future[UInt256] {.base, async.} =
raiseAssert "not implemented"
method waitUntil*(clock: Clock, time: SecondsSince1970) {.base, async.} =
raiseAssert "not implemented"

View File

@ -10,13 +10,12 @@ logScope:
topics = "contracts clock"
type
LastBlockUnknownError* = object of CatchableError
OnChainClock* = ref object of Clock
provider: Provider
subscription: Subscription
offset: times.Duration
started: bool
newBlock: AsyncEvent
lastBlockTime: UInt256
proc new*(_: type OnChainClock, provider: Provider): OnChainClock =
OnChainClock(provider: provider, newBlock: newAsyncEvent())
@ -27,9 +26,7 @@ method start*(clock: OnChainClock) {.async.} =
clock.started = true
proc onBlock(blck: Block) {.upraises:[].} =
let blockTime = initTime(blck.timestamp.truncate(int64), 0)
let computerTime = getTime()
clock.offset = blockTime - computerTime
clock.lastBlockTime = blck.timestamp
clock.newBlock.fire()
if latestBlock =? (await clock.provider.getBlock(BlockTag.latest)):
@ -45,16 +42,17 @@ method stop*(clock: OnChainClock) {.async.} =
await clock.subscription.unsubscribe()
method now*(clock: OnChainClock): SecondsSince1970 =
doAssert clock.started, "clock should be started before calling now()"
toUnix(getTime() + clock.offset)
method lastBlockTimestamp*(clock: OnChainClock): Future[UInt256] {.async.} =
without blk =? await clock.provider.getBlock(BlockTag.latest):
raise newException(LastBlockUnknownError, "failed to get last block")
return blk.timestamp
try:
if queriedBlock =? (waitFor clock.provider.getBlock(BlockTag.latest)):
if queriedBlock.timestamp != clock.lastBlockTime:
trace "queried block and event block are not in sync",
queriedBlockLessThanEventBlock = queriedBlock.timestamp < clock.lastBlockTime
return queriedBlock.timestamp.truncate(int64)
except CatchableError as e:
warn "failed to get latest block timestamp"
return clock.lastBlockTime.truncate(int64)
method waitUntil*(clock: OnChainClock, time: SecondsSince1970) {.async.} =
while (let difference = time - (await clock.lastBlockTimestamp).truncate(int64); difference > 0):
while (let difference = time - clock.now(); difference > 0):
clock.newBlock.clear()
discard await clock.newBlock.wait().withTimeout(chronos.seconds(difference))

View File

@ -58,10 +58,6 @@ method proofDowntime*(market: OnChainMarket): Future[uint8] {.async.} =
method getPointer*(market: OnChainMarket, slotId: SlotId): Future[uint8] {.async.} =
return await market.contract.getPointer(slotId)
method currentBlockchainTime*(market: OnChainMarket): Future[UInt256] {.async.} =
let provider = market.contract.provider
return (!await provider.getBlock(BlockTag.latest)).timestamp
method myRequests*(market: OnChainMarket): Future[seq[RequestId]] {.async.} =
return await market.contract.myRequests

View File

@ -64,7 +64,6 @@ proc isProofRequired*(marketplace: Marketplace, id: SlotId): bool {.contract, vi
proc willProofBeRequired*(marketplace: Marketplace, id: SlotId): bool {.contract, view.}
proc getChallenge*(marketplace: Marketplace, id: SlotId): array[32, byte] {.contract, view.}
proc getPointer*(marketplace: Marketplace, id: SlotId): uint8 {.contract, view.}
proc inDowntime*(marketplace: Marketplace, id: SlotId): bool {.contract, view.}
proc submitProof*(marketplace: Marketplace, id: SlotId, proof: seq[byte]) {.contract.}
proc markProofAsMissing*(marketplace: Marketplace, id: SlotId, period: UInt256) {.contract.}

View File

@ -60,7 +60,6 @@ proc retrieveRequestState*(agent: SalesAgent): Future[?RequestState] {.async.} =
return await market.requestState(data.requestId)
func state*(agent: SalesAgent): ?string =
debugEcho "[salesagent] getting state..."
proc description(state: State): string =
$state
agent.query(description)

View File

@ -57,13 +57,10 @@ proc proveLoop(
proc getCurrentPeriod(): Future[Period] {.async.} =
let periodicity = await market.periodicity()
let blockchainNow = await clock.lastBlockTimestamp
return periodicity.periodOf(blockchainNow)
# return periodicity.periodOf(clock.now().u256)
return periodicity.periodOf(clock.now().u256)
proc waitUntilPeriod(period: Period) {.async.} =
let periodicity = await market.periodicity()
debug "waiting until time", time = periodicity.periodStart(period).truncate(int64)
await clock.waitUntil(periodicity.periodStart(period).truncate(int64))
while true:

View File

@ -34,7 +34,6 @@ proc transition(_: type Event, previous, next: State): Event =
return some next
proc query*[T](machine: Machine, query: Query[T]): ?T =
if not machine.state.isNil: debugEcho "machine state: ", $machine.state
if machine.state.isNil:
none T
else:

View File

@ -37,9 +37,7 @@ proc slots*(validation: Validation): seq[SlotId] =
validation.slots.toSeq
proc getCurrentPeriod(validation: Validation): Future[UInt256] {.async.} =
let currentTime = await validation.clock.lastBlockTimestamp
return validation.periodicity.periodOf(currentTime)
# return validation.periodicity.periodOf(validation.clock.now().u256)
return validation.periodicity.periodOf(validation.clock.now().u256)
proc waitUntilNextPeriod(validation: Validation) {.async.} =
let period = await validation.getCurrentPeriod()

View File

@ -1,5 +1,6 @@
import std/times
import pkg/chronos
import pkg/stint
import codex/clock
export clock
@ -35,9 +36,6 @@ proc advance*(clock: MockClock, seconds: int64) =
method now*(clock: MockClock): SecondsSince1970 =
clock.time
method lastBlockTimestamp*(clock: MockClock): Future[UInt256] {.base, async.} =
return clock.now.u256
method waitUntil*(clock: MockClock, time: SecondsSince1970) {.async.} =
if time > clock.now():
let future = newFuture[void]()

View File

@ -1,55 +1,37 @@
import std/times
import pkg/chronos
import codex/contracts/clock
import codex/utils/json
import ../ethertest
ethersuite "On-Chain Clock":
var clock: OnChainClock
setup:
clock = OnChainClock.new(provider)
clock = OnChainClock.new(ethProvider)
await clock.start()
teardown:
await clock.stop()
test "returns the current time of the EVM":
let latestBlock = (!await provider.getBlock(BlockTag.latest))
let latestBlock = (!await ethProvider.getBlock(BlockTag.latest))
let timestamp = latestBlock.timestamp.truncate(int64)
check clock.now() == timestamp
test "updates time with timestamp of new blocks":
let future = (getTime() + 42.years).toUnix
discard await provider.send("evm_setNextBlockTimestamp", @[%future])
discard await provider.send("evm_mine")
discard await ethProvider.send("evm_setNextBlockTimestamp", @[%future])
discard await ethProvider.send("evm_mine")
check clock.now() == future
test "updates time using wall-clock in-between blocks":
let past = clock.now()
await sleepAsync(chronos.seconds(1))
check clock.now() > past
test "can wait until a certain time is reached by the chain":
let future = clock.now() + 42 # seconds
let waiting = clock.waitUntil(future)
discard await provider.send("evm_setNextBlockTimestamp", @[%future])
discard await provider.send("evm_mine")
discard await ethProvider.send("evm_setNextBlockTimestamp", @[%future])
discard await ethProvider.send("evm_mine")
check await waiting.withTimeout(chronos.milliseconds(100))
test "can wait until a certain time is reached by the wall-clock":
let future = clock.now() + 1 # seconds
let waiting = clock.waitUntil(future)
check await waiting.withTimeout(chronos.seconds(2))
test "raises when not started":
expect AssertionDefect:
discard OnChainClock.new(provider).now()
test "raises when stopped":
await clock.stop()
expect AssertionDefect:
discard clock.now()
test "handles starting multiple times":
await clock.start()
await clock.start()

View File

@ -23,13 +23,13 @@ ethersuite "Marketplace contracts":
token = token.connect(account)
setup:
client = provider.getSigner(accounts[0])
host = provider.getSigner(accounts[1])
client = ethProvider.getSigner(accounts[0])
host = ethProvider.getSigner(accounts[1])
marketplace = Marketplace.new(Marketplace.address, provider.getSigner())
marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, provider.getSigner())
token = Erc20Token.new(tokenAddress, ethProvider.getSigner())
let config = await marketplace.config()
periodicity = Periodicity(seconds: config.proofs.period)
@ -46,13 +46,13 @@ ethersuite "Marketplace contracts":
slotId = request.slotId(0.u256)
proc waitUntilProofRequired(slotId: SlotId) {.async.} =
let currentPeriod = periodicity.periodOf(await provider.currentTime())
await provider.advanceTimeTo(periodicity.periodEnd(currentPeriod))
let currentPeriod = periodicity.periodOf(await ethProvider.currentTime())
await ethProvider.advanceTimeTo(periodicity.periodEnd(currentPeriod) + 1)
while not (
(await marketplace.isProofRequired(slotId)) and
(await marketplace.getPointer(slotId)) < 250
):
await provider.advanceTime(periodicity.seconds)
await ethProvider.advanceTime(periodicity.seconds)
proc startContract() {.async.} =
for slotIndex in 1..<request.ask.slots:
@ -67,9 +67,9 @@ ethersuite "Marketplace contracts":
test "can mark missing proofs":
switchAccount(host)
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
let endOfPeriod = periodicity.periodEnd(missingPeriod)
await provider.advanceTimeTo(endOfPeriod + 1)
await ethProvider.advanceTimeTo(endOfPeriod + 1)
switchAccount(client)
await marketplace.markProofAsMissing(slotId, missingPeriod)
@ -78,17 +78,17 @@ ethersuite "Marketplace contracts":
let address = await host.getAddress()
await startContract()
let requestEnd = await marketplace.requestEnd(request.id)
await provider.advanceTimeTo(requestEnd.u256 + 1)
await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
let startBalance = await token.balanceOf(address)
await marketplace.freeSlot(slotId)
let endBalance = await token.balanceOf(address)
check endBalance == (startBalance + request.ask.duration * request.ask.reward + request.ask.collateral)
test "cannot mark proofs missing for cancelled request":
await provider.advanceTimeTo(request.expiry + 1)
await ethProvider.advanceTimeTo(request.expiry + 1)
switchAccount(client)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
await provider.advanceTime(periodicity.seconds)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await ethProvider.advanceTime(periodicity.seconds)
check await marketplace
.markProofAsMissing(slotId, missingPeriod)
.reverts("Slot not accepting proofs")

View File

@ -17,7 +17,7 @@ ethersuite "On-Chain Market":
var periodicity: Periodicity
setup:
marketplace = Marketplace.new(Marketplace.address, provider.getSigner())
marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
let config = await marketplace.config()
market = OnChainMarket.new(marketplace)
@ -29,8 +29,8 @@ ethersuite "On-Chain Market":
slotIndex = (request.ask.slots div 2).u256
proc advanceToNextPeriod() {.async.} =
let currentPeriod = periodicity.periodOf(await provider.currentTime())
await provider.advanceTimeTo(periodicity.periodEnd(currentPeriod) + 1)
let currentPeriod = periodicity.periodOf(await ethProvider.currentTime())
await ethProvider.advanceTimeTo(periodicity.periodEnd(currentPeriod) + 1)
proc waitUntilProofRequired(slotId: SlotId) {.async.} =
await advanceToNextPeriod()
@ -41,12 +41,12 @@ ethersuite "On-Chain Market":
await advanceToNextPeriod()
test "fails to instantiate when contract does not have a signer":
let storageWithoutSigner = marketplace.connect(provider)
let storageWithoutSigner = marketplace.connect(ethProvider)
expect AssertionDefect:
discard OnChainMarket.new(storageWithoutSigner)
test "knows signer address":
check (await market.getSigner()) == (await provider.getSigner().getAddress())
check (await market.getSigner()) == (await ethProvider.getSigner().getAddress())
test "can retrieve proof periodicity":
let periodicity = await market.periodicity()
@ -70,7 +70,7 @@ ethersuite "On-Chain Market":
test "supports withdrawing of funds":
await market.requestStorage(request)
await provider.advanceTimeTo(request.expiry + 1)
await ethProvider.advanceTimeTo(request.expiry + 1)
await market.withdrawFunds(request.id)
test "supports request subscriptions":
@ -118,7 +118,7 @@ ethersuite "On-Chain Market":
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
await market.markProofAsMissing(slotId, missingPeriod)
check (await marketplace.missingProofs(slotId)) == 1
@ -128,7 +128,7 @@ ethersuite "On-Chain Market":
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
check (await market.canProofBeMarkedAsMissing(slotId, missingPeriod)) == true
@ -213,7 +213,7 @@ ethersuite "On-Chain Market":
receivedIds.add(id)
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
await provider.advanceTimeTo(request.expiry + 1)
await ethProvider.advanceTimeTo(request.expiry + 1)
await market.withdrawFunds(request.id)
check receivedIds == @[request.id]
await subscription.unsubscribe()
@ -235,7 +235,7 @@ ethersuite "On-Chain Market":
if slotState == SlotState.Free:
break
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
await marketplace.markProofAsMissing(slotId, missingPeriod)
check receivedIds == @[request.id]
@ -252,7 +252,7 @@ ethersuite "On-Chain Market":
receivedIds.add(requestId)
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
await provider.advanceTimeTo(request.expiry + 1) # shares expiry with otherRequest
await ethProvider.advanceTimeTo(request.expiry + 1) # shares expiry with otherRequest
await market.withdrawFunds(otherRequest.id)
check receivedIds.len == 0
await market.withdrawFunds(request.id)

View File

@ -1,27 +1,30 @@
# import std/json
import pkg/asynctest
import pkg/ethers
import ./checktest
## Unit testing suite that sets up an Ethereum testing environment.
## Injects a `provider` instance, and a list of `accounts`.
## Injects an `ethProvider` instance, and a list of `accounts`.
## Calls the `evm_snapshot` and `evm_revert` methods to ensure that any
## changes to the blockchain do not persist.
template ethersuite*(name, body) =
asyncchecksuite name:
var provider {.inject, used.}: JsonRpcProvider
# NOTE: `ethProvider` cannot be named `provider`, as there is an unknown
# conflict that is occurring within JsonRpcProvider in which the compiler cannot
# understand the `provider` symbol type when several layers of nested templates
# are involved, eg ethertest > multinodesuite > marketplacesuite > test
var ethProvider {.inject, used.}: JsonRpcProvider
var accounts {.inject, used.}: seq[Address]
var snapshot: JsonNode
setup:
provider = JsonRpcProvider.new("ws://localhost:8545")
snapshot = await send(provider, "evm_snapshot")
accounts = await provider.listAccounts()
ethProvider = JsonRpcProvider.new("ws://localhost:8545")
snapshot = await send(ethProvider, "evm_snapshot")
accounts = await ethProvider.listAccounts()
teardown:
discard await send(provider, "evm_revert", @[snapshot])
discard await send(ethProvider, "evm_revert", @[snapshot])
body

View File

@ -58,7 +58,6 @@ template marketplacesuite*(name: string, startNodes: Nodes, body: untyped) =
nodes = providers().len,
tolerance = 0): Future[PurchaseId] {.async.} =
# let cid = client.upload(byteutils.toHex(data)).get
let expiry = (await ethProvider.currentTime()) + expiry.u256
# avoid timing issues by filling the slot at the start of the next period
@ -85,12 +84,8 @@ template marketplacesuite*(name: string, startNodes: Nodes, body: untyped) =
period = config.proofs.period.truncate(uint64)
periodicity = Periodicity(seconds: period.u256)
discard await ethProvider.send("evm_setIntervalMining", @[%1000])
# Our Hardhat configuration does use automine, which means that time tracked by `provider.currentTime()` is not
# advanced until blocks are mined and that happens only when transaction is submitted.
# As we use in tests provider.currentTime() which uses block timestamp this can lead to synchronization issues.

View File

@ -89,23 +89,6 @@ proc debug*(config: NodeConfig, enabled = true): NodeConfig =
startConfig.debugEnabled = enabled
return startConfig
# proc withLogFile*(
# config: NodeConfig,
# file: bool | string
# ): NodeConfig =
# var startConfig = config
# when file is bool:
# if not file: startConfig.logFile = none string
# else: startConfig.logFile =
# some currentSourcePath.parentDir() / "codex" & $index & ".log"
# else:
# if file.len <= 0:
# raise newException(ValueError, "file path length must be > 0")
# startConfig.logFile = some file
# return startConfig
proc withLogTopics*(
config: NodeConfig,
topics: varargs[string]
@ -135,10 +118,7 @@ proc withLogFile*(
template multinodesuite*(name: string, startNodes: Nodes, body: untyped) =
asyncchecksuite name:
var ethProvider {.inject, used.}: JsonRpcProvider
var accounts {.inject, used.}: seq[Address]
ethersuite name:
var running: seq[RunningNode]
var bootstrap: string
@ -258,10 +238,6 @@ template multinodesuite*(name: string, startNodes: Nodes, body: untyped) =
let node = startHardhatNode()
running.add RunningNode(role: Role.Hardhat, node: node)
echo "Connecting to hardhat on ws://localhost:8545..."
ethProvider = JsonRpcProvider.new("ws://localhost:8545")
accounts = await ethProvider.listAccounts()
for i in 0..<startNodes.clients.numNodes:
let node = startClientNode()
running.add RunningNode(

View File

@ -14,8 +14,9 @@ logScope:
marketplacesuite "Simulate invalid proofs - 1 provider node",
Nodes(
hardhat: HardhatConfig()
.withLogFile(),
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig()
# .withLogFile(),
clients: NodeConfig()
.nodes(1)
@ -64,7 +65,7 @@ marketplacesuite "Simulate invalid proofs - 1 provider node",
var slotWasFreed = false
proc onSlotFreed(event: SlotFreed) {.gcsafe, upraises:[].} =
if event.requestId == requestId and
event.slotIndex == 0.u256: # assume only one slot, so index 0
event.slotIndex == 0.u256: # assume only one slot, so index 0
slotWasFreed = true
let subscription = await marketplace.subscribe(SlotFreed, onSlotFreed)
@ -77,8 +78,9 @@ marketplacesuite "Simulate invalid proofs - 1 provider node",
marketplacesuite "Simulate invalid proofs - 1 provider node",
Nodes(
hardhat: HardhatConfig()
.withLogFile(),
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig()
# .withLogFile(),
clients: NodeConfig()
.nodes(1)
@ -164,8 +166,9 @@ marketplacesuite "Simulate invalid proofs - 1 provider node",
marketplacesuite "Simulate invalid proofs",
Nodes(
hardhat: HardhatConfig()
.withLogFile(),
# Uncomment to start Hardhat automatically, mainly so logs can be inspected locally
# hardhat: HardhatConfig()
# .withLogFile(),
clients: NodeConfig()
.nodes(1)