diff --git a/codex/sales/states/cancelled.nim b/codex/sales/states/cancelled.nim index 06e69652..a21d5ea6 100644 --- a/codex/sales/states/cancelled.nim +++ b/codex/sales/states/cancelled.nim @@ -1,7 +1,10 @@ import ../statemachine import ./errored -type SaleCancelled* = ref object of SaleState +type + SaleCancelled* = ref object of SaleState + SaleCancelledError* = object of CatchableError + SaleTimeoutError* = object of SaleCancelledError method `$`*(state: SaleCancelled): string = "SaleCancelled" @@ -9,5 +12,5 @@ method enterAsync*(state: SaleCancelled) {.async.} = without agent =? (state.context as SalesAgent): raiseAssert "invalid state" - let error = newException(Timeout, "Sale cancelled due to timeout") + let error = newException(SaleTimeoutError, "Sale cancelled due to timeout") await state.switchAsync(SaleErrored(error: error)) diff --git a/codex/sales/states/filled.nim b/codex/sales/states/filled.nim index ea83ae5b..e2c7aeb9 100644 --- a/codex/sales/states/filled.nim +++ b/codex/sales/states/filled.nim @@ -8,6 +8,7 @@ import ../statemachine type SaleFilled* = ref object of SaleState SaleFilledError* = object of CatchableError + HostMismatchError* = object of SaleFilledError method onCancelled*(state: SaleFilled, request: StorageRequest) {.async.} = await state.switchAsync(SaleCancelled()) @@ -29,7 +30,7 @@ method enterAsync(state: SaleFilled) {.async.} = if host == me.some: await state.switchAsync(SaleFinished()) else: - let error = newException(SaleFilledError, "Slot filled by other host") + let error = newException(HostMismatchError, "Slot filled by other host") await state.switchAsync(SaleErrored(error: error)) except CancelledError: diff --git a/codex/sales/states/unknown.nim b/codex/sales/states/unknown.nim index 93809d1c..e8590952 100644 --- a/codex/sales/states/unknown.nim +++ b/codex/sales/states/unknown.nim @@ -8,6 +8,7 @@ import ./cancelled type SaleUnknown* = ref object of SaleState SaleUnknownError* = object of CatchableError + UnexpectedSlotError* = object of SaleUnknownError method `$`*(state: SaleUnknown): string = "SaleUnknown" @@ -24,18 +25,22 @@ method enterAsync(state: SaleUnknown) {.async.} = let market = agent.sales.market try: - without requestState =? await market.requestState(agent.requestId): - let error = newException(SaleUnknownError, "cannot retrieve request state") + let slotId = slotId(agent.requestId, agent.slotIndex) + + without slotState =? await market.slotState(slotId): + let error = newException(SaleUnknownError, "cannot retrieve slot state") await state.switchAsync(SaleErrored(error: error)) - case requestState - of RequestState.New, RequestState.Started: + case slotState + of SlotState.Free: + let error = newException(UnexpectedSlotError, + "slot state on chain should not be 'free'") + await state.switchAsync(SaleErrored(error: error)) + of SlotState.Filled: await state.switchAsync(SaleFilled()) - of RequestState.Finished: + of SlotState.Finished, SlotState.Paid: await state.switchAsync(SaleFinished()) - of RequestState.Cancelled: - await state.switchAsync(SaleCancelled()) - of RequestState.Failed: + of SlotState.Failed: await state.switchAsync(SaleFailed()) except CancelledError: diff --git a/tests/codex/helpers/mockmarket.nim b/tests/codex/helpers/mockmarket.nim index 14f6a741..b083ea59 100644 --- a/tests/codex/helpers/mockmarket.nim +++ b/tests/codex/helpers/mockmarket.nim @@ -14,7 +14,8 @@ type activeSlots*: Table[Address, seq[SlotId]] requested*: seq[StorageRequest] requestEnds*: Table[RequestId, SecondsSince1970] - state*: Table[RequestId, RequestState] + requestState*: Table[RequestId, RequestState] + slotState*: Table[SlotId, SlotState] fulfilled*: seq[Fulfillment] filled*: seq[MockSlot] withdrawn*: seq[RequestId] @@ -110,7 +111,11 @@ method getRequestFromSlotId*(market: MockMarket, method requestState*(market: MockMarket, requestId: RequestId): Future[?RequestState] {.async.} = - return market.state.?[requestId] + return market.requestState.?[requestId] + +method slotState*(market: MockMarket, + slotId: SlotId): Future[SlotState] {.async.} = + return market.slotState[slotId] method getRequestEnd*(market: MockMarket, id: RequestId): Future[SecondsSince1970] {.async.} = diff --git a/tests/codex/testpurchasing.nim b/tests/codex/testpurchasing.nim index 713d8e08..f2c00a67 100644 --- a/tests/codex/testpurchasing.nim +++ b/tests/codex/testpurchasing.nim @@ -141,11 +141,11 @@ suite "Purchasing state machine": let request1, request2, request3, request4, request5 = StorageRequest.example market.requested = @[request1, request2, request3, request4, request5] market.activeRequests[me] = @[request1.id, request2.id, request3.id, request4.id, request5.id] - market.state[request1.id] = RequestState.New - market.state[request2.id] = RequestState.Started - market.state[request3.id] = RequestState.Cancelled - market.state[request4.id] = RequestState.Finished - market.state[request5.id] = RequestState.Failed + market.requestState[request1.id] = RequestState.New + market.requestState[request2.id] = RequestState.Started + market.requestState[request3.id] = RequestState.Cancelled + market.requestState[request4.id] = RequestState.Finished + market.requestState[request5.id] = RequestState.Failed # ensure the started state doesn't error, giving a false positive test result market.requestEnds[request2.id] = clock.now() - 1 @@ -162,7 +162,7 @@ suite "Purchasing state machine": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New purchase.switch(PurchaseUnknown()) check (purchase.state as PurchaseSubmitted).isSome @@ -171,7 +171,7 @@ suite "Purchasing state machine": let purchase = Purchase.new(request, market, clock) market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) market.requested = @[request] - market.state[request.id] = RequestState.Started + market.requestState[request.id] = RequestState.Started purchase.switch(PurchaseUnknown()) check (purchase.state as PurchaseStarted).isSome @@ -179,7 +179,7 @@ suite "Purchasing state machine": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] - market.state[request.id] = RequestState.Cancelled + market.requestState[request.id] = RequestState.Cancelled purchase.switch(PurchaseUnknown()) check (purchase.state as PurchaseErrored).isSome check purchase.error.?msg == "Purchase cancelled due to timeout".some @@ -188,7 +188,7 @@ suite "Purchasing state machine": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] - market.state[request.id] = RequestState.Finished + market.requestState[request.id] = RequestState.Finished purchase.switch(PurchaseUnknown()) check (purchase.state as PurchaseFinished).isSome @@ -196,7 +196,7 @@ suite "Purchasing state machine": let request = StorageRequest.example let purchase = Purchase.new(request, market, clock) market.requested = @[request] - market.state[request.id] = RequestState.Failed + market.requestState[request.id] = RequestState.Failed purchase.switch(PurchaseUnknown()) check (purchase.state as PurchaseErrored).isSome check purchase.error.?msg == "Purchase failed".some @@ -206,7 +206,7 @@ suite "Purchasing state machine": let request = StorageRequest.example market.requested = @[request] market.activeRequests[me] = @[request.id] - market.state[request.id] = RequestState.Started + market.requestState[request.id] = RequestState.Started market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) await purchasing.load() @@ -226,7 +226,7 @@ suite "Purchasing state machine": let request = StorageRequest.example market.requested = @[request] market.activeRequests[me] = @[request.id] - market.state[request.id] = RequestState.Started + market.requestState[request.id] = RequestState.Started market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64) await purchasing.load() diff --git a/tests/codex/testsales.nim b/tests/codex/testsales.nim index dba08957..8c7b696c 100644 --- a/tests/codex/testsales.nim +++ b/tests/codex/testsales.nim @@ -243,6 +243,8 @@ suite "Sales state machine": var market: MockMarket var clock: MockClock var proving: Proving + var slotIdx: UInt256 + var slotId: SlotId setup: market = MockMarket.new() @@ -258,6 +260,8 @@ suite "Sales state machine": return proof await sales.start() request.expiry = (clock.now() + 42).u256 + slotIdx = 0.u256 + slotId = slotId(request.id, slotIdx) teardown: await sales.stop() @@ -276,50 +280,44 @@ suite "Sales state machine": proof: proof, host: address) market.filled.add slot + market.slotState[slotId(request.id, slotIdx)] = SlotState.Filled test "moves to SaleErrored when SaleFilled errors": let agent = newSalesAgent() - market.state[request.id] = RequestState.New + market.slotState[slotId] = SlotState.Free await agent.switchAsync(SaleUnknown()) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Slot filled by other host" + without state =? (agent.state as SaleErrored): + fail() + check state.error of UnexpectedSlotError + check state.error.msg == "slot state on chain should not be 'free'" - test "moves to SaleFinished when request state is New": + test "moves to SaleFilled>SaleFinished when slot state is Filled": let agent = newSalesAgent() await fillSlot() - market.state[request.id] = RequestState.New await agent.switchAsync(SaleUnknown()) check (agent.state as SaleFinished).isSome - test "moves to SaleFinished when request state is Started": + test "moves to SaleFinished when slot state is Finished": let agent = newSalesAgent() await fillSlot() - market.state[request.id] = RequestState.Started + market.slotState[slotId] = SlotState.Finished agent.switch(SaleUnknown()) check (agent.state as SaleFinished).isSome - test "moves to SaleFinished when request state is Finished": + test "moves to SaleFinished when slot state is Paid": let agent = newSalesAgent() - market.state[request.id] = RequestState.Finished + market.slotState[slotId] = SlotState.Paid agent.switch(SaleUnknown()) check (agent.state as SaleFinished).isSome - test "moves to SaleErrored when request state is Cancelled": + test "moves to SaleErrored when slot state is Failed": let agent = newSalesAgent() - market.state[request.id] = RequestState.Cancelled + market.slotState[slotId] = SlotState.Failed agent.switch(SaleUnknown()) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale cancelled due to timeout" - - test "moves to SaleErrored when request state is Failed": - let agent = newSalesAgent() - market.state[request.id] = RequestState.Failed - agent.switch(SaleUnknown()) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale failed" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleFailedError + check state.error.msg == "Sale failed" test "moves to SaleErrored when Downloading and request expires": sales.onStore = proc(request: StorageRequest, @@ -330,14 +328,15 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleDownloading()) clock.set(request.expiry.truncate(int64)) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale cancelled due to timeout" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleTimeoutError + check state.error.msg == "Sale cancelled due to timeout" test "moves to SaleErrored when Downloading and request fails": sales.onStore = proc(request: StorageRequest, @@ -347,68 +346,73 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleDownloading()) market.emitRequestFailed(request.id) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale failed" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleFailedError + check state.error.msg == "Sale failed" test "moves to SaleErrored when Filling and request expires": request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256 let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleFilling()) clock.set(request.expiry.truncate(int64)) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale cancelled due to timeout" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleTimeoutError + check state.error.msg == "Sale cancelled due to timeout" test "moves to SaleErrored when Filling and request fails": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleFilling()) market.emitRequestFailed(request.id) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale failed" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleFailedError + check state.error.msg == "Sale failed" test "moves to SaleErrored when Finished and request expires": request.expiry = (getTime() + initDuration(seconds=2)).toUnix.u256 let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.Finished + market.requestState[request.id] = RequestState.Finished await agent.switchAsync(SaleFinished()) clock.set(request.expiry.truncate(int64)) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale cancelled due to timeout" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleTimeoutError + check state.error.msg == "Sale cancelled due to timeout" test "moves to SaleErrored when Finished and request fails": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.Finished + market.requestState[request.id] = RequestState.Finished await agent.switchAsync(SaleFinished()) market.emitRequestFailed(request.id) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale failed" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleFailedError + check state.error.msg == "Sale failed" test "moves to SaleErrored when Proving and request expires": sales.onProve = proc(request: StorageRequest, @@ -419,14 +423,15 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleProving()) clock.set(request.expiry.truncate(int64)) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale cancelled due to timeout" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleTimeoutError + check state.error.msg == "Sale cancelled due to timeout" test "moves to SaleErrored when Proving and request fails": sales.onProve = proc(request: StorageRequest, @@ -436,14 +441,15 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleProving()) market.emitRequestFailed(request.id) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Sale failed" + without state =? (agent.state as SaleErrored): + fail() + check state.error of SaleFailedError + check state.error.msg == "Sale failed" test "moves to SaleErrored when Downloading and slot is filled by another host": sales.onStore = proc(request: StorageRequest, @@ -453,7 +459,7 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleDownloading()) market.fillSlot(request.id, agent.slotIndex, proof, Address.example) await sleepAsync chronos.seconds(2) @@ -470,14 +476,15 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await agent.switchAsync(SaleProving()) market.fillSlot(request.id, agent.slotIndex, proof, Address.example) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Slot filled by other host" + without state =? (agent.state as SaleErrored): + fail() + check state.error of HostMismatchError + check state.error.msg == "Slot filled by other host" test "moves to SaleErrored when Filling and slot is filled by another host": sales.onProve = proc(request: StorageRequest, @@ -487,14 +494,15 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New market.fillSlot(request.id, agent.slotIndex, proof, Address.example) await agent.switchAsync(SaleFilling()) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleErrored) - check state.isSome - check (!state).error.msg == "Slot filled by other host" + without state =? (agent.state as SaleErrored): + fail() + check state.error of HostMismatchError + check state.error.msg == "Slot filled by other host" test "moves from SaleDownloading to SaleFinished, calling necessary callbacks": var onProveCalled, onStoreCalled, onClearCalled, onSaleCalled: bool @@ -518,14 +526,14 @@ suite "Sales state machine": let agent = newSalesAgent() await agent.start(request.ask.slots) market.requested.add request - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New await fillSlot(agent.slotIndex) await agent.switchAsync(SaleDownloading()) market.emitRequestFulfilled(request.id) await sleepAsync chronos.seconds(2) - let state = (agent.state as SaleFinished) - check state.isSome + without state =? (agent.state as SaleFinished): + fail() check onProveCalled check onStoreCalled check not onClearCalled @@ -536,7 +544,7 @@ suite "Sales state machine": request.ask.slots = 2 market.requested = @[request] - market.state[request.id] = RequestState.New + market.requestState[request.id] = RequestState.New let slot0 = MockSlot(requestId: request.id, slotIndex: 0.u256,