Fix on-chain clock (#732)

* market: use `pending` blocktag when querying onchain state

* clock: use wall clock in integration tests

reason: we'll need to wait for the next period in
integration tests, and we can't do that if the
time doesn't advance

* clock: remove unused field

* integration: use pending block time to get current time

* clock: fix on-chain clock for hardhat

Only use 'latest' block for updates
Only update the first time you see a block

* integration: do not start tests with a very outdated block

* integration: allow for longer expiry period
This commit is contained in:
markspanbroek 2024-03-11 17:57:20 +01:00 committed by GitHub
parent 1fc0510f2b
commit d1658d7b77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 45 additions and 34 deletions

View File

@ -15,26 +15,39 @@ type
provider: Provider provider: Provider
subscription: Subscription subscription: Subscription
offset: times.Duration offset: times.Duration
blockNumber: UInt256
started: bool started: bool
newBlock: AsyncEvent newBlock: AsyncEvent
lastBlockTime: UInt256
proc new*(_: type OnChainClock, provider: Provider): OnChainClock = proc new*(_: type OnChainClock, provider: Provider): OnChainClock =
OnChainClock(provider: provider, newBlock: newAsyncEvent()) OnChainClock(provider: provider, newBlock: newAsyncEvent())
proc update(clock: OnChainClock, blck: Block) =
if number =? blck.number and number > clock.blockNumber:
let blockTime = initTime(blck.timestamp.truncate(int64), 0)
let computerTime = getTime()
clock.offset = blockTime - computerTime
clock.blockNumber = number
trace "updated clock", blockTime=blck.timestamp, blockNumber=number, offset=clock.offset
clock.newBlock.fire()
proc update(clock: OnChainClock) {.async.} =
try:
if latest =? (await clock.provider.getBlock(BlockTag.latest)):
clock.update(latest)
except CatchableError as error:
debug "error updating clock: ", error=error.msg
discard
method start*(clock: OnChainClock) {.async.} = method start*(clock: OnChainClock) {.async.} =
if clock.started: if clock.started:
return return
proc onBlock(blck: Block) = proc onBlock(_: Block) =
let blockTime = initTime(blck.timestamp.truncate(int64), 0) # ignore block parameter; hardhat may call this with pending blocks
let computerTime = getTime() asyncSpawn clock.update()
clock.offset = blockTime - computerTime
clock.lastBlockTime = blck.timestamp
clock.newBlock.fire()
if latestBlock =? (await clock.provider.getBlock(BlockTag.latest)): await clock.update()
onBlock(latestBlock)
clock.subscription = await clock.provider.subscribe(onBlock) clock.subscription = await clock.provider.subscribe(onBlock)
clock.started = true clock.started = true
@ -47,20 +60,6 @@ method stop*(clock: OnChainClock) {.async.} =
clock.started = false clock.started = false
method now*(clock: OnChainClock): SecondsSince1970 = method now*(clock: OnChainClock): SecondsSince1970 =
when codex_use_hardhat:
# hardhat's latest block.timestamp is usually 1s behind the block timestamp
# in the newHeads event. When testing, always return the latest block.
try:
if queriedBlock =? (waitFor clock.provider.getBlock(BlockTag.latest)):
trace "using last block timestamp for clock.now",
lastBlockTimestamp = queriedBlock.timestamp.truncate(int64),
cachedBlockTimestamp = clock.lastBlockTime.truncate(int64)
return queriedBlock.timestamp.truncate(int64)
except CatchableError as e:
warn "failed to get latest block timestamp", error = e.msg
return clock.lastBlockTime.truncate(int64)
else:
doAssert clock.started, "clock should be started before calling now()" doAssert clock.started, "clock should be started before calling now()"
return toUnix(getTime() + clock.offset) return toUnix(getTime() + clock.offset)

View File

@ -60,7 +60,8 @@ method proofDowntime*(market: OnChainMarket): Future[uint8] {.async.} =
return config.proofs.downtime return config.proofs.downtime
method getPointer*(market: OnChainMarket, slotId: SlotId): Future[uint8] {.async.} = method getPointer*(market: OnChainMarket, slotId: SlotId): Future[uint8] {.async.} =
return await market.contract.getPointer(slotId) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return await market.contract.getPointer(slotId, overrides)
method myRequests*(market: OnChainMarket): Future[seq[RequestId]] {.async.} = method myRequests*(market: OnChainMarket): Future[seq[RequestId]] {.async.} =
return await market.contract.myRequests return await market.contract.myRequests
@ -88,7 +89,8 @@ method getRequest(market: OnChainMarket,
method requestState*(market: OnChainMarket, method requestState*(market: OnChainMarket,
requestId: RequestId): Future[?RequestState] {.async.} = requestId: RequestId): Future[?RequestState] {.async.} =
try: try:
return some await market.contract.requestState(requestId) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return some await market.contract.requestState(requestId, overrides)
except ProviderError as e: except ProviderError as e:
if e.revertReason.contains("Unknown request"): if e.revertReason.contains("Unknown request"):
return none RequestState return none RequestState
@ -96,7 +98,8 @@ method requestState*(market: OnChainMarket,
method slotState*(market: OnChainMarket, method slotState*(market: OnChainMarket,
slotId: SlotId): Future[SlotState] {.async.} = slotId: SlotId): Future[SlotState] {.async.} =
return await market.contract.slotState(slotId) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return await market.contract.slotState(slotId, overrides)
method getRequestEnd*(market: OnChainMarket, method getRequestEnd*(market: OnChainMarket,
id: RequestId): Future[SecondsSince1970] {.async.} = id: RequestId): Future[SecondsSince1970] {.async.} =
@ -140,7 +143,8 @@ method withdrawFunds(market: OnChainMarket,
method isProofRequired*(market: OnChainMarket, method isProofRequired*(market: OnChainMarket,
id: SlotId): Future[bool] {.async.} = id: SlotId): Future[bool] {.async.} =
try: try:
return await market.contract.isProofRequired(id) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return await market.contract.isProofRequired(id, overrides)
except ProviderError as e: except ProviderError as e:
if e.revertReason.contains("Slot is free"): if e.revertReason.contains("Slot is free"):
return false return false
@ -149,14 +153,16 @@ method isProofRequired*(market: OnChainMarket,
method willProofBeRequired*(market: OnChainMarket, method willProofBeRequired*(market: OnChainMarket,
id: SlotId): Future[bool] {.async.} = id: SlotId): Future[bool] {.async.} =
try: try:
return await market.contract.willProofBeRequired(id) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return await market.contract.willProofBeRequired(id, overrides)
except ProviderError as e: except ProviderError as e:
if e.revertReason.contains("Slot is free"): if e.revertReason.contains("Slot is free"):
return false return false
raise e raise e
method getChallenge*(market: OnChainMarket, id: SlotId): Future[ProofChallenge] {.async.} = method getChallenge*(market: OnChainMarket, id: SlotId): Future[ProofChallenge] {.async.} =
return await market.contract.getChallenge(id) let overrides = CallOverrides(blockTag: some BlockTag.pending)
return await market.contract.getChallenge(id, overrides)
method submitProof*(market: OnChainMarket, method submitProof*(market: OnChainMarket,
id: SlotId, id: SlotId,

View File

@ -23,7 +23,7 @@ ethersuite "On-Chain Clock":
let future = (getTime() + 42.years).toUnix let future = (getTime() + 42.years).toUnix
discard await ethProvider.send("evm_setNextBlockTimestamp", @[%future]) discard await ethProvider.send("evm_setNextBlockTimestamp", @[%future])
discard await ethProvider.send("evm_mine") discard await ethProvider.send("evm_mine")
check clock.now() == future check eventually clock.now() == future
test "can wait until a certain time is reached by the chain": test "can wait until a certain time is reached by the chain":
let future = clock.now() + 42 # seconds let future = clock.now() + 42 # seconds

View File

@ -1,7 +1,7 @@
import pkg/ethers import pkg/ethers
proc currentTime*(provider: Provider): Future[UInt256] {.async.} = proc currentTime*(provider: Provider): Future[UInt256] {.async.} =
return (!await provider.getBlock(BlockTag.latest)).timestamp return (!await provider.getBlock(BlockTag.pending)).timestamp
proc advanceTime*(provider: JsonRpcProvider, seconds: UInt256) {.async.} = proc advanceTime*(provider: JsonRpcProvider, seconds: UInt256) {.async.} =
discard await provider.send("evm_increaseTime", @[%("0x" & seconds.toHex)]) discard await provider.send("evm_increaseTime", @[%("0x" & seconds.toHex)])

View File

@ -18,6 +18,8 @@ template ethersuite*(name, body) =
setup: setup:
ethProvider = JsonRpcProvider.new("ws://localhost:8545") ethProvider = JsonRpcProvider.new("ws://localhost:8545")
snapshot = await send(ethProvider, "evm_snapshot") snapshot = await send(ethProvider, "evm_snapshot")
# ensure that we have a recent block with a fresh timestamp
discard await send(ethProvider, "evm_mine")
accounts = await ethProvider.listAccounts() accounts = await ethProvider.listAccounts()
teardown: teardown:

View File

@ -226,6 +226,8 @@ template multinodesuite*(name: string, body: untyped) =
# reverted in the test teardown # reverted in the test teardown
if nodeConfigs.hardhat.isNil: if nodeConfigs.hardhat.isNil:
snapshot = await send(ethProvider, "evm_snapshot") snapshot = await send(ethProvider, "evm_snapshot")
# ensure that we have a recent block with a fresh timestamp
discard await send(ethProvider, "evm_mine")
accounts = await ethProvider.listAccounts() accounts = await ethProvider.listAccounts()
except CatchableError as e: except CatchableError as e:
fatal "failed to connect to hardhat", error = e.msg fatal "failed to connect to hardhat", error = e.msg

View File

@ -28,7 +28,7 @@ marketplacesuite "Marketplace payouts":
let reward = 400.u256 let reward = 400.u256
let duration = 100.periods let duration = 100.periods
let collateral = 200.u256 let collateral = 200.u256
let expiry = 4.periods let expiry = 10.periods
let datasetSizeInBlocks = 3 let datasetSizeInBlocks = 3
let data = await RandomChunker.example(blocks=datasetSizeInBlocks) let data = await RandomChunker.example(blocks=datasetSizeInBlocks)
let client = clients()[0] let client = clients()[0]

View File

@ -105,6 +105,7 @@ marketplacesuite "Simulate invalid proofs":
let purchaseId = await client0.requestStorage( let purchaseId = await client0.requestStorage(
cid, cid,
expiry=10.periods,
duration=totalPeriods.periods, duration=totalPeriods.periods,
origDatasetSizeInBlocks=datasetSizeInBlocks) origDatasetSizeInBlocks=datasetSizeInBlocks)
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get
@ -161,6 +162,7 @@ marketplacesuite "Simulate invalid proofs":
let purchaseId = await client0.requestStorage( let purchaseId = await client0.requestStorage(
cid, cid,
expiry=10.periods,
duration=totalPeriods.periods, duration=totalPeriods.periods,
origDatasetSizeInBlocks=datasetSizeInBlocks) origDatasetSizeInBlocks=datasetSizeInBlocks)
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get