adds validation tests for historical state restoration

This commit is contained in:
Marcin Czenko 2024-10-10 06:26:32 +02:00
parent 57e2a97d20
commit 3ddad7bbbd
No known key found for this signature in database
GPG Key ID: 33DEA0C8E30937C0
3 changed files with 126 additions and 34 deletions

View File

@ -123,13 +123,13 @@ proc run(validation: Validation) {.async.} =
except CatchableError as e: except CatchableError as e:
error "Validation failed", msg = e.msg error "Validation failed", msg = e.msg
proc epochForDurationBackFromNow(duration: times.Duration): int64 = proc epochForDurationBackFromNow(validation: Validation,
let now = getTime().toUnix duration: times.Duration): SecondsSince1970 =
return now - duration.inSeconds return validation.clock.now - duration.inSeconds
proc restoreHistoricalState(validation: Validation) {.async} = proc restoreHistoricalState(validation: Validation) {.async} =
trace "Restoring historical state..." trace "Restoring historical state..."
let startTimeEpoch = epochForDurationBackFromNow(MaxStorageRequestDuration) let startTimeEpoch = validation.epochForDurationBackFromNow(MaxStorageRequestDuration)
let slotFilledEvents = await validation.market.queryPastSlotFilledEvents( let slotFilledEvents = await validation.market.queryPastSlotFilledEvents(
fromTime = startTimeEpoch) fromTime = startTimeEpoch)
trace "Found slot filled events", numberOfSlots = slotFilledEvents.len trace "Found slot filled events", numberOfSlots = slotFilledEvents.len
@ -150,6 +150,7 @@ proc start*(validation: Validation) {.async.} =
validation.running = validation.run() validation.running = validation.run()
proc stop*(validation: Validation) {.async.} = proc stop*(validation: Validation) {.async.} =
if not isNil(validation.running):
await validation.running.cancelAndWait() await validation.running.cancelAndWait()
while validation.subscriptions.len > 0: while validation.subscriptions.len > 0:
let subscription = validation.subscriptions.pop() let subscription = validation.subscriptions.pop()

View File

@ -10,12 +10,16 @@ import pkg/codex/contracts/proofs
import pkg/codex/contracts/config import pkg/codex/contracts/config
from pkg/ethers import BlockTag from pkg/ethers import BlockTag
import codex/clock
import ../examples import ../examples
export market export market
export tables export tables
logScope:
topics = "mockMarket"
type type
MockMarket* = ref object of Market MockMarket* = ref object of Market
periodicity: Periodicity periodicity: Periodicity
@ -43,6 +47,7 @@ type
config*: MarketplaceConfig config*: MarketplaceConfig
canReserveSlot*: bool canReserveSlot*: bool
reserveSlotThrowError*: ?(ref MarketError) reserveSlotThrowError*: ?(ref MarketError)
clock: ?Clock
Fulfillment* = object Fulfillment* = object
requestId*: RequestId requestId*: RequestId
proof*: Groth16Proof proof*: Groth16Proof
@ -52,6 +57,7 @@ type
host*: Address host*: Address
slotIndex*: UInt256 slotIndex*: UInt256
proof*: Groth16Proof proof*: Groth16Proof
timestamp: ?SecondsSince1970
Subscriptions = object Subscriptions = object
onRequest: seq[RequestSubscription] onRequest: seq[RequestSubscription]
onFulfillment: seq[FulfillmentSubscription] onFulfillment: seq[FulfillmentSubscription]
@ -97,7 +103,7 @@ proc hash*(address: Address): Hash =
proc hash*(requestId: RequestId): Hash = proc hash*(requestId: RequestId): Hash =
hash(requestId.toArray) hash(requestId.toArray)
proc new*(_: type MockMarket): MockMarket = proc new*(_: type MockMarket, clock: ?Clock = Clock.none): MockMarket =
## Create a new mocked Market instance ## Create a new mocked Market instance
## ##
let config = MarketplaceConfig( let config = MarketplaceConfig(
@ -114,7 +120,8 @@ proc new*(_: type MockMarket): MockMarket =
downtimeProduct: 67.uint8 downtimeProduct: 67.uint8
) )
) )
MockMarket(signer: Address.example, config: config, canReserveSlot: true) MockMarket(signer: Address.example, config: config,
canReserveSlot: true, clock: clock)
method getSigner*(market: MockMarket): Future[Address] {.async.} = method getSigner*(market: MockMarket): Future[Address] {.async.} =
return market.signer return market.signer
@ -248,7 +255,8 @@ proc fillSlot*(market: MockMarket,
requestId: requestId, requestId: requestId,
slotIndex: slotIndex, slotIndex: slotIndex,
proof: proof, proof: proof,
host: host host: host,
timestamp: market.clock.?now
) )
market.filled.add(slot) market.filled.add(slot)
market.slotState[slotId(slot.requestId, slot.slotIndex)] = SlotState.Filled market.slotState[slotId(slot.requestId, slot.slotIndex)] = SlotState.Filled
@ -506,8 +514,19 @@ method queryPastSlotFilledEvents*(
method queryPastSlotFilledEvents*( method queryPastSlotFilledEvents*(
market: MockMarket, market: MockMarket,
fromTime: int64): Future[seq[SlotFilled]] {.async.} = fromTime: SecondsSince1970): Future[seq[SlotFilled]] {.async.} =
return market.filled.map(slot => debug "queryPastSlotFilledEvents:market.filled",
numOfFilledSlots = market.filled.len
let filtered = market.filled.filter(
proc (slot: MockSlot): bool =
debug "queryPastSlotFilledEvents:fromTime", timestamp = slot.timestamp,
fromTime = fromTime
if timestamp =? slot.timestamp:
return timestamp >= fromTime
else:
true
)
return filtered.map(slot =>
SlotFilled(requestId: slot.requestId, slotIndex: slot.slotIndex) SlotFilled(requestId: slot.requestId, slotIndex: slot.slotIndex)
) )

View File

@ -1,9 +1,10 @@
import pkg/chronos import pkg/chronos
import std/strformat import std/strformat
import std/random import std/times
import codex/validation import codex/validation
import codex/periods import codex/periods
import codex/clock
import ../asynctest import ../asynctest
import ./helpers/mockmarket import ./helpers/mockmarket
@ -11,6 +12,9 @@ import ./helpers/mockclock
import ./examples import ./examples
import ./helpers import ./helpers
logScope:
topics = "testValidation"
asyncchecksuite "validation": asyncchecksuite "validation":
let period = 10 let period = 10
let timeout = 5 let timeout = 5
@ -20,10 +24,10 @@ asyncchecksuite "validation":
let proof = Groth16Proof.example let proof = Groth16Proof.example
let collateral = slot.request.ask.collateral let collateral = slot.request.ask.collateral
var validation: Validation
var market: MockMarket var market: MockMarket
var clock: MockClock var clock: MockClock
var groupIndex: uint16 var groupIndex: uint16
var validation: Validation
proc initValidationConfig(maxSlots: MaxSlots, proc initValidationConfig(maxSlots: MaxSlots,
validationGroups: ?ValidationGroups, validationGroups: ?ValidationGroups,
@ -33,18 +37,26 @@ asyncchecksuite "validation":
raiseAssert fmt"Creating ValidationConfig failed! Error msg: {error.msg}" raiseAssert fmt"Creating ValidationConfig failed! Error msg: {error.msg}"
validationConfig validationConfig
setup: proc newValidation(clock: Clock,
groupIndex = groupIndexForSlotId(slot.id, !validationGroups) market: Market,
market = MockMarket.new() maxSlots: MaxSlots,
clock = MockClock.new() validationGroups: ?ValidationGroups,
groupIndex: uint16 = 0): Validation =
let validationConfig = initValidationConfig( let validationConfig = initValidationConfig(
maxSlots, validationGroups, groupIndex) maxSlots, validationGroups, groupIndex)
validation = Validation.new(clock, market, validationConfig) Validation.new(clock, market, validationConfig)
setup:
groupIndex = groupIndexForSlotId(slot.id, !validationGroups)
clock = MockClock.new()
market = MockMarket.new(clock = Clock(clock).some)
market.config.proofs.period = period.u256 market.config.proofs.period = period.u256
market.config.proofs.timeout = timeout.u256 market.config.proofs.timeout = timeout.u256
await validation.start() validation = newValidation(
clock, market, maxSlots, validationGroups, groupIndex)
teardown: teardown:
# calling stop on validation that did not start is harmless
await validation.stop() await validation.stop()
proc advanceToNextPeriod = proc advanceToNextPeriod =
@ -79,6 +91,7 @@ asyncchecksuite "validation":
test "initializing ValidationConfig fails when maxSlots is negative " & test "initializing ValidationConfig fails when maxSlots is negative " &
"(validationGroups set)": "(validationGroups set)":
let maxSlots = -1 let maxSlots = -1
let groupIndex = 0'u16
let validationConfig = ValidationConfig.init( let validationConfig = ValidationConfig.init(
maxSlots = maxSlots, groups = validationGroups, groupIndex) maxSlots = maxSlots, groups = validationGroups, groupIndex)
check validationConfig.isFailure == true check validationConfig.isFailure == true
@ -86,45 +99,41 @@ asyncchecksuite "validation":
fmt"be greater than or equal to 0! (got: {maxSlots})" fmt"be greater than or equal to 0! (got: {maxSlots})"
test "slot is not observed if it is not in the validation group": test "slot is not observed if it is not in the validation group":
let validationConfig = initValidationConfig(maxSlots, validationGroups, validation = newValidation(clock, market, maxSlots, validationGroups,
(groupIndex + 1) mod uint16(!validationGroups)) (groupIndex + 1) mod uint16(!validationGroups))
let validation = Validation.new(clock, market, validationConfig)
await validation.start() await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
await validation.stop()
check validation.slots.len == 0 check validation.slots.len == 0
test "when a slot is filled on chain, it is added to the list": test "when a slot is filled on chain, it is added to the list":
await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
check validation.slots == @[slot.id] check validation.slots == @[slot.id]
test "slot should be observed if maxSlots is set to 0": test "slot should be observed if maxSlots is set to 0":
let validationConfig = initValidationConfig( validation = newValidation(clock, market, maxSlots = 0, ValidationGroups.none)
maxSlots = 0, ValidationGroups.none)
let validation = Validation.new(clock, market, validationConfig)
await validation.start() await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
await validation.stop()
check validation.slots == @[slot.id] check validation.slots == @[slot.id]
test "slot should be observed if validation group is not set (and " & test "slot should be observed if validation group is not set (and " &
"maxSlots is not 0)": "maxSlots is not 0)":
let validationConfig = initValidationConfig( validation = newValidation(clock, market, maxSlots, ValidationGroups.none)
maxSlots, ValidationGroups.none)
let validation = Validation.new(clock, market, validationConfig)
await validation.start() await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
await validation.stop()
check validation.slots == @[slot.id] check validation.slots == @[slot.id]
for state in [SlotState.Finished, SlotState.Failed]: for state in [SlotState.Finished, SlotState.Failed]:
test fmt"when slot state changes to {state}, it is removed from the list": test fmt"when slot state changes to {state}, it is removed from the list":
validation = newValidation(clock, market, maxSlots, validationGroups)
await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
market.slotState[slot.id] = state market.slotState[slot.id] = state
advanceToNextPeriod() advanceToNextPeriod()
check eventually validation.slots.len == 0 check eventually validation.slots.len == 0
test "when a proof is missed, it is marked as missing": test "when a proof is missed, it is marked as missing":
await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
market.setCanProofBeMarkedAsMissing(slot.id, true) market.setCanProofBeMarkedAsMissing(slot.id, true)
advanceToNextPeriod() advanceToNextPeriod()
@ -132,6 +141,7 @@ asyncchecksuite "validation":
check market.markedAsMissingProofs.contains(slot.id) check market.markedAsMissingProofs.contains(slot.id)
test "when a proof can not be marked as missing, it will not be marked": test "when a proof can not be marked as missing, it will not be marked":
await validation.start()
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
market.setCanProofBeMarkedAsMissing(slot.id, false) market.setCanProofBeMarkedAsMissing(slot.id, false)
advanceToNextPeriod() advanceToNextPeriod()
@ -139,13 +149,75 @@ asyncchecksuite "validation":
check market.markedAsMissingProofs.len == 0 check market.markedAsMissingProofs.len == 0
test "it does not monitor more than the maximum number of slots": test "it does not monitor more than the maximum number of slots":
let validationGroups = ValidationGroups.none validation = newValidation(clock, market, maxSlots, ValidationGroups.none)
let validationConfig = initValidationConfig(
maxSlots, validationGroups)
let validation = Validation.new(clock, market, validationConfig)
await validation.start() await validation.start()
for _ in 0..<maxSlots + 1: for _ in 0..<maxSlots + 1:
let slot = Slot.example let slot = Slot.example
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral) await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
await validation.stop()
check validation.slots.len == maxSlots check validation.slots.len == maxSlots
test "[restoring historical state] it retrieves the historical state " &
"for max 30 days in the past":
let earlySlot = Slot.example
await market.fillSlot(earlySlot.request.id, earlySlot.slotIndex, proof, collateral)
let fromTime = clock.now()
clock.set(fromTime + 1)
let duration: times.Duration = initDuration(days = 30)
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
clock.set(fromTime + duration.inSeconds + 1)
validation = newValidation(clock, market, maxSlots = 0,
ValidationGroups.none)
await validation.start()
check validation.slots == @[slot.id]
for state in [SlotState.Finished, SlotState.Failed]:
test "[restoring historical state] when restoring historical state, " &
fmt"it excludes slots in {state} state":
let slot1 = Slot.example
let slot2 = Slot.example
await market.fillSlot(slot1.request.id, slot1.slotIndex,
proof, collateral)
await market.fillSlot(slot2.request.id, slot2.slotIndex,
proof, collateral)
market.slotState[slot1.id] = state
validation = newValidation(clock, market, maxSlots = 0,
ValidationGroups.none)
await validation.start()
check validation.slots == @[slot2.id]
test "[restoring historical state] it does not monitor more than the " &
"maximum number of slots ":
for _ in 0..<maxSlots + 1:
let slot = Slot.example
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
validation = newValidation(clock, market, maxSlots, ValidationGroups.none)
await validation.start()
check validation.slots.len == maxSlots
test "[restoring historical state] slot is not observed if it is not " &
"in the validation group":
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
validation = newValidation(clock, market, maxSlots, validationGroups,
(groupIndex + 1) mod uint16(!validationGroups))
await validation.start()
check validation.slots.len == 0
test "[restoring historical state] slot should be observed if maxSlots " &
"is set to 0":
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
validation = newValidation(clock, market, maxSlots = 0, ValidationGroups.none)
await validation.start()
check validation.slots == @[slot.id]
test "[restoring historical state] slot should be observed if validation " &
"group is not set (and maxSlots is not 0)":
await market.fillSlot(slot.request.id, slot.slotIndex, proof, collateral)
validation = newValidation(clock, market, maxSlots, ValidationGroups.none)
await validation.start()
check validation.slots == @[slot.id]