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

Validator historical state restoration #922

Merged
merged 71 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
94e61f9
adds a new overload of queryPastEvents allowing to query past events …
marcinczenko Sep 26, 2024
e080295
adds state restoration to validator
marcinczenko Sep 26, 2024
0587461
refactors a bit to get the tests back to work
marcinczenko Sep 27, 2024
0c2dd1d
replaces deprecated generic methods from Market with methods for spec…
marcinczenko Oct 7, 2024
c19c775
Refactors binary search
marcinczenko Oct 8, 2024
b8b2b49
adds market tests for querying past SlotFilled events and binary search
marcinczenko Oct 9, 2024
7fc2072
Takes into account that <<earliest>> block available is not necessari…
marcinczenko Oct 9, 2024
e462511
Adds more logging and makes testing earliest block boundary more reli…
marcinczenko Oct 10, 2024
62d6935
adds validation tests for historical state restoration
marcinczenko Oct 10, 2024
981afcd
adds mockprovider to simplify and improve testing of the edge conditions
marcinczenko Oct 11, 2024
a00702a
adds slot reservation to the new tests after rebasing
marcinczenko Oct 11, 2024
7bd76ac
adds validation groups and group index in logs of validator
marcinczenko Oct 13, 2024
d369ebc
adds integration test with two validators
marcinczenko Oct 13, 2024
6a61f0c
adds comment on how to enable logging in integration test executable …
marcinczenko Oct 14, 2024
4d1efa7
testIntegration: makes list is running nodes injected and available i…
marcinczenko Oct 14, 2024
5c48d0f
validation: adds integration test for historical state
marcinczenko Oct 14, 2024
da88109
adds more logging to validator
marcinczenko Oct 14, 2024
11d6a76
integration test: validator only looks 30 days back for historical state
marcinczenko Oct 14, 2024
babf1fe
adds logging of the slotState when removing slots during validation
marcinczenko Oct 14, 2024
3ae8685
review and refactor validator integration tests
marcinczenko Oct 14, 2024
a0a43f1
adds validation to the set of integration tests
marcinczenko Oct 14, 2024
509af57
Fixes mistyped name of the mock provider module in testMarket
marcinczenko Oct 14, 2024
9ab5c1c
Fixes a typo in the name of the validation suite in integration tests
marcinczenko Oct 15, 2024
4fccdc4
Makes validation unit test a bit easier to follow
marcinczenko Oct 15, 2024
c31943e
better use of logScopes to reduce duplication
marcinczenko Oct 15, 2024
afb444c
improves timing and clarifies the test conditions
marcinczenko Oct 15, 2024
c32eac1
uses http as default RPC provider for nodes running in integration te…
marcinczenko Oct 16, 2024
0534380
simplifies the validation integration tests by waiting for failed req…
marcinczenko Oct 16, 2024
375b65b
adds config option allowing selectively to set different provider url
marcinczenko Oct 16, 2024
00e7d8c
Brings back the default settings for RPC provider in integration tests
marcinczenko Oct 16, 2024
7c51846
use http RPC provider for clients in validation integration tests
marcinczenko Oct 16, 2024
6407714
fine-tune the tests
marcinczenko Oct 16, 2024
ad0b8b6
Makes validator integration test more robust - adds extra tracking
marcinczenko Oct 17, 2024
c7fd863
brings tracking of marketplace event back to validator integration test
marcinczenko Oct 17, 2024
32ebc5e
refactors integration tests
marcinczenko Oct 18, 2024
f48a939
deletes tmp file
marcinczenko Oct 18, 2024
df1eea2
adds <<return>> after forcing integration test to fail preliminarily
marcinczenko Oct 22, 2024
c2eccc5
re-enables all integration tests and matrix
marcinczenko Oct 22, 2024
1a8a148
stops debug output in CI
marcinczenko Oct 22, 2024
02da9de
allows to choose a different RPC provider for a given integration tes…
marcinczenko Oct 22, 2024
75e86ba
fixes signature of <<getBlock>> method in mockProvider
marcinczenko Nov 25, 2024
5d1c1fe
adds missing import which seem to be braking integration tests on win…
marcinczenko Nov 26, 2024
e6e0db1
makes sure that clients, SPs, and validators use the same provider url
marcinczenko Nov 27, 2024
9eb68a0
makes validator integration tests using http at 127.0.0.1:8545
marcinczenko Nov 27, 2024
36ad92b
testvalidator: stop resubscribing as we are now using http polling as…
marcinczenko Nov 27, 2024
321212d
applying review comments
marcinczenko Nov 28, 2024
a5a006d
groups queryPastStorage overrides together (review comment)
marcinczenko Dec 4, 2024
79325c3
groups the historical validation tests into a sub suite
marcinczenko Dec 4, 2024
fba5cea
removes the temporary extensions in marketplacesuite and multinodesui…
marcinczenko Dec 5, 2024
580bc5c
simplifies validation integration tests
marcinczenko Dec 5, 2024
bb784a5
Removes debug logs when waiting for request to fail
marcinczenko Dec 5, 2024
a9a1f50
Renaming waitForRequestFailed => waitForRequestToFail
marcinczenko Dec 5, 2024
044251e
renames blockNumberForBlocksAgo to pastBlockTag and makes it private
marcinczenko Dec 7, 2024
abdf711
removes redundant debugging logs
marcinczenko Dec 7, 2024
d7736f1
refines logging in validation
marcinczenko Dec 8, 2024
bccfcc4
removes dev logging from mockmarket
marcinczenko Dec 8, 2024
9459508
improves exception handling in provider helper procs and prepares for…
marcinczenko Dec 8, 2024
f57d6e3
Uses chronos instead of std/times for Duration
marcinczenko Dec 8, 2024
dc1869a
extracts provider and binary search helpers to a separate module
marcinczenko Dec 8, 2024
cfc91f2
removes redundant log entry params from validator
marcinczenko Dec 8, 2024
a09f07d
unifies the notation to consistently use method call syntax
marcinczenko Dec 11, 2024
14c308d
reuses ProviderError from nim-ethers in the provider extension
marcinczenko Dec 11, 2024
539877a
clarifies the comment in multinodesuite
marcinczenko Dec 12, 2024
fa9d6e1
uses == operator to check the predefined tags and raises exception wh…
marcinczenko Dec 12, 2024
c9f66a0
when waiting for request to fail, we break on any request state that …
marcinczenko Dec 12, 2024
19e576e
removes tests that were moved to testProvider from testMarket
marcinczenko Dec 12, 2024
e625918
extracts tests that use MockProvider to a separate async suite
marcinczenko Dec 12, 2024
a43e1e0
improves performance of the historical state restoration
marcinczenko Dec 12, 2024
5c48ca4
removing redundant log messages in validator (groupIndex and groups)
marcinczenko Dec 13, 2024
217b588
adds testProvider to testContracts group
marcinczenko Dec 13, 2024
8b91c68
removes unused import in testMarket
marcinczenko Dec 13, 2024
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
35 changes: 17 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ env:
cache_nonce: 0 # Allows for easily busting actions/cache caches
nim_version: pinned


concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
Expand All @@ -23,23 +22,23 @@ jobs:
matrix: ${{ steps.matrix.outputs.matrix }}
cache_nonce: ${{ env.cache_nonce }}
steps:
- name: Compute matrix
id: matrix
uses: fabiocaccamo/create-matrix-action@v4
with:
matrix: |
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {msys2}
- name: Compute matrix
id: matrix
uses: fabiocaccamo/create-matrix-action@v4
with:
matrix: |
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {linux}, cpu {amd64}, builder {ubuntu-20.04}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {macos}, cpu {amd64}, builder {macos-13}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {bash --noprofile --norc -e -o pipefail}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {unittest}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {contract}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {integration}, nim_version {${{ env.nim_version }}}, shell {msys2}
os {windows}, cpu {amd64}, builder {windows-latest}, tests {tools}, nim_version {${{ env.nim_version }}}, shell {msys2}

build:
needs: matrix
Expand Down
3 changes: 3 additions & 0 deletions build.nims
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ task testContracts, "Build & run Codex Contract tests":
task testIntegration, "Run integration tests":
buildBinary "codex", params = "-d:chronicles_runtime_filtering -d:chronicles_log_level=TRACE -d:codex_enable_proof_failures=true"
test "testIntegration"
# use params to enable logging from the integration test executable
# test "testIntegration", params = "-d:chronicles_sinks=textlines[notimestamps,stdout],textlines[dynamic] " &
# "-d:chronicles_enabled_topics:integration:TRACE"

task build, "build codex binary":
codexTask()
Expand Down
2 changes: 2 additions & 0 deletions codex/contracts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import contracts/requests
import contracts/marketplace
import contracts/market
import contracts/interactions
import contracts/provider

export requests
export marketplace
export market
export interactions
export provider
54 changes: 42 additions & 12 deletions codex/contracts/market.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import std/sequtils
import std/strutils
import std/sugar
import pkg/ethers
import pkg/upraises
import pkg/questionable
Expand All @@ -9,6 +7,7 @@
import ../market
import ./marketplace
import ./proofs
import ./provider

export market

Expand Down Expand Up @@ -467,18 +466,49 @@
method unsubscribe*(subscription: OnChainMarketSubscription) {.async.} =
await subscription.eventSubscription.unsubscribe()

method queryPastEvents*[T: MarketplaceEvent](
method queryPastSlotFilledEvents*(

Check warning on line 469 in codex/contracts/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/market.nim#L469

Added line #L469 was not covered by tests
market: OnChainMarket,
_: type T,
blocksAgo: int): Future[seq[T]] {.async.} =
fromBlock: BlockTag): Future[seq[SlotFilled]] {.async.} =

Check warning on line 471 in codex/contracts/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/market.nim#L471

Added line #L471 was not covered by tests

convertEthersError:
let contract = market.contract
let provider = contract.provider
return await market.contract.queryFilter(SlotFilled,
fromBlock,
BlockTag.latest)

Check warning on line 476 in codex/contracts/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/market.nim#L474-L476

Added lines #L474 - L476 were not covered by tests

let head = await provider.getBlockNumber()
let fromBlock = BlockTag.init(head - blocksAgo.abs.u256)
method queryPastSlotFilledEvents*(
market: OnChainMarket,
blocksAgo: int): Future[seq[SlotFilled]] {.async.} =

convertEthersError:
let fromBlock =
await market.contract.provider.pastBlockTag(blocksAgo)

return await market.queryPastSlotFilledEvents(fromBlock)

method queryPastSlotFilledEvents*(
market: OnChainMarket,
fromTime: SecondsSince1970): Future[seq[SlotFilled]] {.async.} =

convertEthersError:
let fromBlock =
await market.contract.provider.blockNumberForEpoch(fromTime)
return await market.queryPastSlotFilledEvents(BlockTag.init(fromBlock))

method queryPastStorageRequestedEvents*(
market: OnChainMarket,
fromBlock: BlockTag): Future[seq[StorageRequested]] {.async.} =

convertEthersError:
return await market.contract.queryFilter(StorageRequested,
fromBlock,
BlockTag.latest)

method queryPastStorageRequestedEvents*(
market: OnChainMarket,
blocksAgo: int): Future[seq[StorageRequested]] {.async.} =

convertEthersError:
let fromBlock =
await market.contract.provider.pastBlockTag(blocksAgo)

Check warning on line 512 in codex/contracts/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/market.nim#L478-L512

Added lines #L478 - L512 were not covered by tests

return await contract.queryFilter(T,
fromBlock,
BlockTag.latest)
return await market.queryPastStorageRequestedEvents(fromBlock)

Check warning on line 514 in codex/contracts/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/market.nim#L514

Added line #L514 was not covered by tests
126 changes: 126 additions & 0 deletions codex/contracts/provider.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import pkg/ethers/provider
import pkg/chronos
import pkg/questionable

import ../logutils

from ../clock import SecondsSince1970

logScope:
topics = "marketplace onchain provider"

Check warning on line 10 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L10

Added line #L10 was not covered by tests

proc raiseProviderError(message: string) {.raises: [ProviderError].} =
raise newException(ProviderError, message)

Check warning on line 13 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L12-L13

Added lines #L12 - L13 were not covered by tests
Comment on lines +12 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be beneficial to preserve the original exception otherwise the underlying reason for the failure will be lost:

Suggested change
proc raiseProviderError(message: string) {.raises: [ProviderError].} =
raise newException(ProviderError, message)
proc raiseProviderError(message: string, parent: ref CatchableError) {.raises: [ProviderError].} =
raise newException(ProviderError, message, parent)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used like:

without latestBlock =? await provider.getBlock(blockTag), error:
    raiseCodexProviderError("Could not get latest block", error)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, callers can pull out the parent exception msg using msdDetail like this:

import pkg/codex/utils/exceptions
try:
  # ...
except ProviderError as e:
  error "an error occurred", error = e.msgDetail

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use:

without latestBlock =? await provider.getBlock(blockTag), error:
    raiseCodexProviderError("Could not get latest block", error)

because (as you yourself pointed out earlier) getBlock returns an Option and not a Result. Right?

So if there was an original ProviderError caused by getBlock it will naturally bubble-up so, we do not have to do anything about it I suppose. But then getBlock, which is defined as this:

method getBlock*(
  provider: JsonRpcProvider,
  tag: BlockTag): Future[?Block] {.async: (raises:[ProviderError]).} =

  convertError:
    let client = await provider.client
    return await client.eth_getBlockByNumber(tag, false)

can still return None because eth_getBlockByNumber is defined like this:

proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block

This is why I am raising ProviderError when this happen, but we have no data about underlying exception that caused eth_getBlockByNumber to return None.

So I think in this case everything can stays as it is, right?

Copy link
Contributor

@emizzle emizzle Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, sorry I missed that getBlock returns an Option 👍

we have no data about underlying exception that caused eth_getBlockByNumber to return None

I don't think it's an error really, just that the ethereum client can return null if the block doesn't exist.

What you have looks good 👍


proc blockNumberAndTimestamp*(provider: Provider, blockTag: BlockTag):
Future[(UInt256, UInt256)] {.async: (raises: [ProviderError]).} =
without latestBlock =? await provider.getBlock(blockTag):
raiseProviderError("Could not get latest block")

without latestBlockNumber =? latestBlock.number:
raiseProviderError("Could not get latest block number")

Check warning on line 21 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L15-L21

Added lines #L15 - L21 were not covered by tests

return (latestBlockNumber, latestBlock.timestamp)

proc binarySearchFindClosestBlock(
provider: Provider,
epochTime: int,
low: UInt256,
high: UInt256): Future[UInt256] {.async: (raises: [ProviderError]).} =
let (_, lowTimestamp) =
await provider.blockNumberAndTimestamp(BlockTag.init(low))
let (_, highTimestamp) =
await provider.blockNumberAndTimestamp(BlockTag.init(high))
if abs(lowTimestamp.truncate(int) - epochTime) <
abs(highTimestamp.truncate(int) - epochTime):
return low

Check warning on line 36 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L23-L36

Added lines #L23 - L36 were not covered by tests
else:
return high

proc binarySearchBlockNumberForEpoch(
provider: Provider,
epochTime: UInt256,
latestBlockNumber: UInt256,
earliestBlockNumber: UInt256): Future[UInt256]
{.async: (raises: [ProviderError]).} =
var low = earliestBlockNumber
var high = latestBlockNumber

while low <= high:
if low == 0 and high == 0:
return low
let mid = (low + high) div 2
let (midBlockNumber, midBlockTimestamp) =
await provider.blockNumberAndTimestamp(BlockTag.init(mid))

if midBlockTimestamp < epochTime:
low = mid + 1
elif midBlockTimestamp > epochTime:
high = mid - 1
else:
return midBlockNumber
# NOTICE that by how the binary search is implemented, when it finishes

Check warning on line 62 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L38-L62

Added lines #L38 - L62 were not covered by tests
# low is always greater than high - this is why we use high, where
# intuitively we would use low:
await provider.binarySearchFindClosestBlock(
epochTime.truncate(int), low=high, high=low)

proc blockNumberForEpoch*(
provider: Provider,
epochTime: SecondsSince1970): Future[UInt256]
{.async: (raises: [ProviderError]).} =
let epochTimeUInt256 = epochTime.u256
let (latestBlockNumber, latestBlockTimestamp) =
await provider.blockNumberAndTimestamp(BlockTag.latest)
let (earliestBlockNumber, earliestBlockTimestamp) =
await provider.blockNumberAndTimestamp(BlockTag.earliest)

# Initially we used the average block time to predict
# the number of blocks we need to look back in order to find

Check warning on line 79 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L65-L79

Added lines #L65 - L79 were not covered by tests
# 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.

# Are lucky today?
if earliestBlockTimestamp == epochTimeUInt256:
return earliestBlockNumber
if latestBlockTimestamp == epochTimeUInt256:
return latestBlockNumber

Check warning on line 109 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L106-L109

Added lines #L106 - L109 were not covered by tests

if earliestBlockNumber > 0 and earliestBlockTimestamp > epochTimeUInt256:
let availableHistoryInDays =
(latestBlockTimestamp - earliestBlockTimestamp) div
1.days.secs.u256

Check warning on line 114 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L111-L114

Added lines #L111 - L114 were not covered by tests
warn "Short block history detected.", earliestBlockTimestamp =
earliestBlockTimestamp, days = availableHistoryInDays
return earliestBlockNumber

Check warning on line 117 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L117

Added line #L117 was not covered by tests

return await provider.binarySearchBlockNumberForEpoch(
epochTimeUInt256, latestBlockNumber, earliestBlockNumber)

proc pastBlockTag*(provider: Provider,
blocksAgo: int):
Future[BlockTag] {.async: (raises: [ProviderError]).} =
let head = await provider.getBlockNumber()
return BlockTag.init(head - blocksAgo.abs.u256)

Check warning on line 126 in codex/contracts/provider.nim

View check run for this annotation

Codecov / codecov/patch

codex/contracts/provider.nim#L119-L126

Added lines #L119 - L126 were not covered by tests
27 changes: 23 additions & 4 deletions codex/market.nim
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,27 @@
method unsubscribe*(subscription: Subscription) {.base, async, upraises:[].} =
raiseAssert("not implemented")

method queryPastEvents*[T: MarketplaceEvent](
market: Market,
_: type T,
blocksAgo: int): Future[seq[T]] {.base, async.} =
method queryPastSlotFilledEvents*(
market: Market,
fromBlock: BlockTag): Future[seq[SlotFilled]] {.base, async.} =
raiseAssert("not implemented")

Check warning on line 253 in codex/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/market.nim#L253

Added line #L253 was not covered by tests
method queryPastSlotFilledEvents*(
market: Market,
blocksAgo: int): Future[seq[SlotFilled]] {.base, async.} =
raiseAssert("not implemented")

Check warning on line 257 in codex/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/market.nim#L255-L257

Added lines #L255 - L257 were not covered by tests

method queryPastSlotFilledEvents*(
market: Market,
fromTime: SecondsSince1970): Future[seq[SlotFilled]] {.base, async.} =
raiseAssert("not implemented")

Check warning on line 262 in codex/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/market.nim#L260-L262

Added lines #L260 - L262 were not covered by tests

method queryPastStorageRequestedEvents*(

Check warning on line 264 in codex/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/market.nim#L264

Added line #L264 was not covered by tests
market: Market,
fromBlock: BlockTag): Future[seq[StorageRequested]] {.base, async.} =
raiseAssert("not implemented")

method queryPastStorageRequestedEvents*(
market: Market,
blocksAgo: int): Future[seq[StorageRequested]] {.base, async.} =

Check warning on line 271 in codex/market.nim

View check run for this annotation

Codecov / codecov/patch

codex/market.nim#L266-L271

Added lines #L266 - L271 were not covered by tests
raiseAssert("not implemented")
42 changes: 34 additions & 8 deletions codex/validation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type
proofTimeout: UInt256
config: ValidationConfig

const
MaxStorageRequestDuration = 30.days
Comment on lines +26 to +27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be somewhere else (it is more generic info/limit then only for validation), but I guess it will be more clear where to put this when the actual limit will be implemented, so for now, we can leave it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.


logScope:
topics = "codex validator"

Expand Down Expand Up @@ -56,15 +59,15 @@ func maxSlotsConstraintRespected(validation: Validation): bool =
validation.slots.len < validation.config.maxSlots

func shouldValidateSlot(validation: Validation, slotId: SlotId): bool =
if (validationGroups =? validation.config.groups):
(groupIndexForSlotId(slotId, validationGroups) ==
validation.config.groupIndex) and
validation.maxSlotsConstraintRespected
else:
validation.maxSlotsConstraintRespected
without validationGroups =? validation.config.groups:
return true
groupIndexForSlotId(slotId, validationGroups) ==
validation.config.groupIndex

proc subscribeSlotFilled(validation: Validation) {.async.} =
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
if not validation.maxSlotsConstraintRespected:
return
let slotId = slotId(requestId, slotIndex)
if validation.shouldValidateSlot(slotId):
trace "Adding slot", slotId
Expand All @@ -78,7 +81,7 @@ proc removeSlotsThatHaveEnded(validation: Validation) {.async.} =
for slotId in slots:
let state = await validation.market.slotState(slotId)
if state != SlotState.Filled:
trace "Removing slot", slotId
trace "Removing slot", slotId, slotState = state
marcinczenko marked this conversation as resolved.
Show resolved Hide resolved
ended.incl(slotId)
validation.slots.excl(ended)

Expand Down Expand Up @@ -119,14 +122,37 @@ proc run(validation: Validation) {.async.} =
except CatchableError as e:
error "Validation failed", msg = e.msg

proc epochForDurationBackFromNow(validation: Validation,
duration: Duration): SecondsSince1970 =
return validation.clock.now - duration.secs

proc restoreHistoricalState(validation: Validation) {.async.} =
trace "Restoring historical state..."
let startTimeEpoch = validation.epochForDurationBackFromNow(MaxStorageRequestDuration)
let slotFilledEvents = await validation.market.queryPastSlotFilledEvents(
fromTime = startTimeEpoch)
for event in slotFilledEvents:
if not validation.maxSlotsConstraintRespected:
break
let slotId = slotId(event.requestId, event.slotIndex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bit of an optimisation... we could keep track of a loop index and break early when we hit maxSlots, instead of continuing the loop and re-checking the max slots constraint each time.

Copy link
Contributor Author

@marcinczenko marcinczenko Dec 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shouldValidateSlot does more than just checking against maxSlots, and I was hesitating if it does not make things a bit less readable. We basically need to extract checking of the maxSlots condition in the shouldValidateSlot, which I feel indeed is better from semantic point of view.

We would need to modify three procedures:

func shouldValidateSlot(validation: Validation, slotId: SlotId): bool =
  without validationGroups =? validation.config.groups:
    return true
  groupIndexForSlotId(slotId, validationGroups) ==
    validation.config.groupIndex

proc subscribeSlotFilled(validation: Validation) {.async.} =
  proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
    if not validation.maxSlotsConstraintRespected:
      return
    let slotId = slotId(requestId, slotIndex)
    if validation.shouldValidateSlot(slotId):
      trace "Adding slot", slotId, groups = validation.config.groups,
        groupIndex = validation.config.groupIndex
      validation.slots.incl(slotId)
  let subscription = await validation.market.subscribeSlotFilled(onSlotFilled)
  validation.subscriptions.add(subscription)

proc restoreHistoricalState(validation: Validation) {.async.} =
  logScope:
    groups = validation.config.groups
    groupIndex = validation.config.groupIndex
  trace "Restoring historical state..."
  let startTimeEpoch = validation.epochForDurationBackFromNow(MaxStorageRequestDuration)
  let slotFilledEvents = await validation.market.queryPastSlotFilledEvents(
    fromTime = startTimeEpoch)
  trace "Found filled slots", numberOfSlots = slotFilledEvents.len
  for event in slotFilledEvents:
    if not validation.maxSlotsConstraintRespected:
      break
    let slotId = slotId(event.requestId, event.slotIndex)
    if validation.shouldValidateSlot(slotId):
      trace "Adding slot [historical]", slotId
      validation.slots.incl(slotId)
  trace "Removing slots that have ended..."
  await removeSlotsThatHaveEnded(validation)
  trace "Historical state restored", numberOfSlots = validation.slots.len

Do you like it more this way? and @AuHau?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When restoring state, we could also directly check if slot has ended in the meantime and avoid calling removeSlotsThatHaveEnded:

proc restoreHistoricalState(validation: Validation) {.async.} =
  logScope:
    groups = validation.config.groups
    groupIndex = validation.config.groupIndex
  trace "Restoring historical state..."
  let startTimeEpoch = validation.epochForDurationBackFromNow(MaxStorageRequestDuration)
  let slotFilledEvents = await validation.market.queryPastSlotFilledEvents(
    fromTime = startTimeEpoch)
  for event in slotFilledEvents:
    if not validation.maxSlotsConstraintRespected:
      break
    let slotId = slotId(event.requestId, event.slotIndex)
    let slotState = await validation.market.slotState(slotId)
    if slotState == SlotState.Filled and validation.shouldValidateSlot(slotId):
      trace "Adding slot [historical]", slotId
      validation.slots.incl(slotId)
  trace "Historical state restored", numberOfSlots = validation.slots.len

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I would be down for the last version. IMHO I would not stress much about ended slots, they will be removed in the first iteration of the run loop.

Copy link
Contributor Author

@marcinczenko marcinczenko Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, removeSlotsThatHaveEnded indeed happens before markProofsAsMissing so currently there indeed should be no consequences of letting the run in the run loop to take care of removing slots that has ended. But, if you do not mind I feel somehow a bit more comfortable leaving this small extra check preventing the ended slotes from being added to the list. If you are than ok, I will use the last version. To be sure (the version with limited logging):

proc restoreHistoricalState(validation: Validation) {.async.} =
  trace "Restoring historical state..."
  let startTimeEpoch = validation.epochForDurationBackFromNow(MaxStorageRequestDuration)
  let slotFilledEvents = await validation.market.queryPastSlotFilledEvents(
    fromTime = startTimeEpoch)
  for event in slotFilledEvents:
    if not validation.maxSlotsConstraintRespected:
      break
    let slotId = slotId(event.requestId, event.slotIndex)
    let slotState = await validation.market.slotState(slotId)
    if slotState == SlotState.Filled and validation.shouldValidateSlot(slotId):
      trace "Adding slot [historical]", slotId
      validation.slots.incl(slotId)
  trace "Historical state restored", numberOfSlots = validation.slots.len

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue I was hoping to optimise out of, is when there are a lot of slots being validated, but honestly it's not a big deal, hence it was just a suggestion with 🟡. It just means that potentially it'll consume the thread, but honestly, for a short period of time and probably not worth spending any more time on it until we know for sure that we have this problem.

This is a problem we don't currently have, so maybe just leave it out and keep it as it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine. The last example should take care for not including slots that has ended. And it is no brainer, so I am ok using it. The change is marginal and if I got it right, @AuHau is also fine with using that last optimized version.

let slotState = await validation.market.slotState(slotId)
if slotState == SlotState.Filled and validation.shouldValidateSlot(slotId):
trace "Adding slot [historical]", slotId
validation.slots.incl(slotId)
trace "Historical state restored", numberOfSlots = validation.slots.len

proc start*(validation: Validation) {.async.} =
trace "Starting validator", groups = validation.config.groups,
groupIndex = validation.config.groupIndex
validation.periodicity = await validation.market.periodicity()
validation.proofTimeout = await validation.market.proofTimeout()
await validation.subscribeSlotFilled()
await validation.restoreHistoricalState()
validation.running = validation.run()
marcinczenko marked this conversation as resolved.
Show resolved Hide resolved

proc stop*(validation: Validation) {.async.} =
await validation.running.cancelAndWait()
if not isNil(validation.running):
await validation.running.cancelAndWait()
while validation.subscriptions.len > 0:
let subscription = validation.subscriptions.pop()
await subscription.unsubscribe()
Loading
Loading