Load purchase state from chain (#283)

* [purchasing] Simplify test

* [utils] Move StorageRequest.example up one level

* [purchasing] Load purchases from market

* [purchasing] load purchase states

* Implement myRequest() and getState() methods for OnChainMarket

* [proofs] Fix intermittently failing tests

Ensures that examples of proofs in tests are never of length 0;
these are considered invalid proofs by the smart contract logic.

* [contracts] Fix failing test

With the new solidity contracts update, a contract can only
be paid out after it started.

* [market] Add method to get request end time

* [purchasing] wait until purchase is finished

Purchase.wait() would previously wait until purchase
was started, now we wait until it is finished.

* [purchasing] Handle 'finished' and 'failed' states

* [marketplace] move to failed state once request fails

- Add support for subscribing to request failure events.
- Add supporting contract tests for subscribing to request failure events.
- Allow the PurchaseStarted state to move to PurchaseFailure once a request failure event is emitted
- Add supporting tests for moving from PurchaseStarted to PurchaseFailure
- Add state transition tests for PurchaseUnknown.

* [marketplace] Fix test with longer sleepAsync

* [integration] Add function to restart a codex node

* [purchasing] Set client address before requesting storage

To prevent the purchase id (which equals the request id)
from changing once it's been submitted.

* [contracts] Fix: OnChainMarket.getState()

Had the wrong method signature before

* [purchasing] Load purchases on node start

* [purchasing] Rename state 'PurchaseError' to 'PurchaseErrored'

Allows for an exception type called 'PurchaseError'

* [purchasing] Load purchases in background

No longer calls market.getRequest() for every purchase
on node start.

* [contracts] Add `$` for RequestId, SlotId and Nonce

To aid with debugging

* [purchasing] Add Purchasing.stop()

To ensure that all contract interactions have both a
start() and a stop() for

* [tests] Remove sleepAsync where possible

Use `eventually` loop instead, to make sure that we're
not waiting unnecessarily.

* [integration] Fix: handle non-json response in test

* [purchasing] Add purchase state to json

* [integration] Ensure that purchase is submitted before restart

Fixes test failure on slower CI

* [purchasing] re-implement `description` as method

Allows description to be set in the same module where the
state type is defined.

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

* [contracts] fix typo

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

* [market] Use more generic error type

Should we decide to change the provider type later

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>

Co-authored-by: Eric Mastro <eric.mastro@gmail.com>
This commit is contained in:
markspanbroek 2022-11-08 02:10:17 -05:00 committed by GitHub
parent be32b9619b
commit 4175689745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 655 additions and 177 deletions

View File

@ -69,8 +69,10 @@ proc start*(interactions: ContractInteractions) {.async.} =
await interactions.clock.start()
await interactions.sales.start()
await interactions.proving.start()
await interactions.purchasing.start()
proc stop*(interactions: ContractInteractions) {.async.} =
await interactions.purchasing.stop()
await interactions.sales.stop()
await interactions.proving.stop()
await interactions.clock.stop()

View File

@ -28,13 +28,11 @@ func new*(_: type OnChainMarket, contract: Storage): OnChainMarket =
method getSigner*(market: OnChainMarket): Future[Address] {.async.} =
return await market.signer.getAddress()
method requestStorage(market: OnChainMarket,
request: StorageRequest):
Future[StorageRequest] {.async.} =
var request = request
request.client = await market.signer.getAddress()
method myRequests*(market: OnChainMarket): Future[seq[RequestId]] {.async.} =
return await market.contract.myRequests
method requestStorage(market: OnChainMarket, request: StorageRequest){.async.} =
await market.contract.requestStorage(request)
return request
method getRequest(market: OnChainMarket,
id: RequestId): Future[?StorageRequest] {.async.} =
@ -45,6 +43,19 @@ method getRequest(market: OnChainMarket,
return none StorageRequest
raise e
method getState*(market: OnChainMarket,
requestId: RequestId): Future[?RequestState] {.async.} =
try:
return some await market.contract.state(requestId)
except ProviderError as e:
if e.revertReason.contains("Unknown request"):
return none RequestState
raise e
method getRequestEnd*(market: OnChainMarket,
id: RequestId): Future[SecondsSince1970] {.async.} =
return await market.contract.requestEnd(id)
method getHost(market: OnChainMarket,
requestId: RequestId,
slotIndex: UInt256): Future[?Address] {.async.} =
@ -104,5 +115,15 @@ method subscribeRequestCancelled*(market: OnChainMarket,
let subscription = await market.contract.subscribe(RequestCancelled, onEvent)
return OnChainMarketSubscription(eventSubscription: subscription)
method subscribeRequestFailed*(market: OnChainMarket,
requestId: RequestId,
callback: OnRequestFailed):
Future[MarketSubscription] {.async.} =
proc onEvent(event: RequestFailed) {.upraises:[].} =
if event.requestId == requestId:
callback(event.requestId)
let subscription = await market.contract.subscribe(RequestFailed, onEvent)
return OnChainMarketSubscription(eventSubscription: subscription)
method unsubscribe*(subscription: OnChainMarketSubscription) {.async.} =
await subscription.eventSubscription.unsubscribe()

View File

@ -33,6 +33,12 @@ type
SlotId* = distinct array[32, byte]
RequestId* = distinct array[32, byte]
Nonce* = distinct array[32, byte]
RequestState* {.pure.} = enum
New
Started
Cancelled
Finished
Failed
proc `==`*(x, y: Nonce): bool {.borrow.}
proc `==`*(x, y: RequestId): bool {.borrow.}
@ -42,6 +48,9 @@ proc hash*(x: SlotId): Hash {.borrow.}
func toArray*(id: RequestId | SlotId | Nonce): array[32, byte] =
array[32, byte](id)
proc `$`*(id: RequestId | SlotId | Nonce): string =
id.toArray.toHex
func fromTuple(_: type StorageRequest, tupl: tuple): StorageRequest =
StorageRequest(
client: tupl[0],

View File

@ -2,6 +2,7 @@ import pkg/ethers
import pkg/json_rpc/rpcclient
import pkg/stint
import pkg/chronos
import ../clock
import ./requests
export stint
@ -20,6 +21,8 @@ type
requestId* {.indexed.}: RequestId
RequestCancelled* = object of Event
requestId* {.indexed.}: RequestId
RequestFailed* = object of Event
requestId* {.indexed.}: RequestId
ProofSubmitted* = object of Event
id*: SlotId
proof*: seq[byte]
@ -41,6 +44,10 @@ proc payoutSlot*(storage: Storage, requestId: RequestId, slotIndex: UInt256) {.c
proc getRequest*(storage: Storage, id: RequestId): StorageRequest {.contract, view.}
proc getHost*(storage: Storage, id: SlotId): Address {.contract, view.}
proc myRequests*(storage: Storage): seq[RequestId] {.contract, view.}
proc state*(storage: Storage, requestId: RequestId): RequestState {.contract, view.}
proc requestEnd*(storage: Storage, requestId: RequestId): SecondsSince1970 {.contract, view.}
proc proofPeriod*(storage: Storage): UInt256 {.contract, view.}
proc proofTimeout*(storage: Storage): UInt256 {.contract, view.}

View File

@ -2,10 +2,12 @@ import pkg/chronos
import pkg/upraises
import pkg/questionable
import ./contracts/requests
import ./clock
export chronos
export questionable
export requests
export SecondsSince1970
type
Market* = ref object of RootObj
@ -14,13 +16,16 @@ type
OnFulfillment* = proc(requestId: RequestId) {.gcsafe, upraises: [].}
OnSlotFilled* = proc(requestId: RequestId, slotIndex: UInt256) {.gcsafe, upraises:[].}
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
OnRequestFailed* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
method getSigner*(market: Market): Future[Address] {.base, async.} =
raiseAssert("not implemented")
method requestStorage*(market: Market,
request: StorageRequest):
Future[StorageRequest] {.base, async.} =
request: StorageRequest) {.base, async.} =
raiseAssert("not implemented")
method myRequests*(market: Market): Future[seq[RequestId]] {.base, async.} =
raiseAssert("not implemented")
method getRequest*(market: Market,
@ -28,6 +33,14 @@ method getRequest*(market: Market,
Future[?StorageRequest] {.base, async.} =
raiseAssert("not implemented")
method getState*(market: Market,
requestId: RequestId): Future[?RequestState] {.base, async.} =
raiseAssert("not implemented")
method getRequestEnd*(market: Market,
id: RequestId): Future[SecondsSince1970] {.base, async.} =
raiseAssert("not implemented")
method getHost*(market: Market,
requestId: RequestId,
slotIndex: UInt256): Future[?Address] {.base, async.} =
@ -62,9 +75,15 @@ method subscribeSlotFilled*(market: Market,
raiseAssert("not implemented")
method subscribeRequestCancelled*(market: Market,
requestId: RequestId,
callback: OnRequestCancelled):
Future[Subscription] {.base, async.} =
requestId: RequestId,
callback: OnRequestCancelled):
Future[Subscription] {.base, async.} =
raiseAssert("not implemented")
method subscribeRequestFailed*(market: Market,
requestId: RequestId,
callback: OnRequestFailed):
Future[Subscription] {.base, async.} =
raiseAssert("not implemented")
method unsubscribe*(subscription: Subscription) {.base, async, upraises:[].} =

View File

@ -288,7 +288,7 @@ proc requestStorage*(self: CodexNodeRef,
expiry: expiry |? 0.u256
)
let purchase = contracts.purchasing.purchase(request)
let purchase = await contracts.purchasing.purchase(request)
return success purchase.id
proc new*(

View File

@ -8,6 +8,7 @@ import ./clock
import ./purchasing/purchase
export questionable
export chronos
export market
export purchase
@ -31,7 +32,22 @@ proc new*(_: type Purchasing, market: Market, clock: Clock): Purchasing =
requestExpiryInterval: DefaultRequestExpiryInterval,
)
proc populate*(purchasing: Purchasing, request: StorageRequest): StorageRequest =
proc load*(purchasing: Purchasing) {.async.} =
let market = purchasing.market
let requestIds = await market.myRequests()
for requestId in requestIds:
let purchase = Purchase.new(requestId, purchasing.market, purchasing.clock)
purchase.load()
purchasing.purchases[purchase.id] = purchase
proc start*(purchasing: Purchasing) {.async.} =
await purchasing.load()
proc stop*(purchasing: Purchasing) {.async.} =
discard
proc populate*(purchasing: Purchasing,
request: StorageRequest): Future[StorageRequest] {.async.} =
result = request
if result.ask.proofProbability == 0.u256:
result.ask.proofProbability = purchasing.proofProbability
@ -41,13 +57,15 @@ proc populate*(purchasing: Purchasing, request: StorageRequest): StorageRequest
var id = result.nonce.toArray
doAssert randomBytes(id) == 32
result.nonce = Nonce(id)
result.client = await purchasing.market.getSigner()
proc purchase*(purchasing: Purchasing, request: StorageRequest): Purchase =
let request = purchasing.populate(request)
let purchase = newPurchase(request, purchasing.market, purchasing.clock)
proc purchase*(purchasing: Purchasing,
request: StorageRequest): Future[Purchase] {.async.} =
let request = await purchasing.populate(request)
let purchase = Purchase.new(request, purchasing.market, purchasing.clock)
purchase.start()
purchasing.purchases[purchase.id] = purchase
purchase
return purchase
func getPurchase*(purchasing: Purchasing, id: PurchaseId): ?Purchase =
if purchasing.purchases.hasKey(id):

View File

@ -1,37 +1,59 @@
import ./statemachine
import ./states/pending
import ./states/unknown
import ./purchaseid
# Purchase is implemented as a state machine:
# Purchase is implemented as a state machine.
#
# pending ----> submitted ----------> started
# \ \ \
# \ \ -----------> cancelled
# \ \ \
# --------------------------------------> error
# It can either be a new (pending) purchase that still needs to be submitted
# on-chain, or it is a purchase that was previously submitted on-chain, and
# we're just restoring its (unknown) state after a node restart.
#
# |
# v
# ------------------------- unknown
# | / /
# v v /
# pending ----> submitted ----> started ---------> finished <----/
# \ \ /
# \ ------------> failed <----/
# \ /
# --> cancelled <-----------------------
export Purchase
export purchaseid
export statemachine
func newPurchase*(request: StorageRequest,
market: Market,
clock: Clock): Purchase =
func new*(_: type Purchase,
requestId: RequestId,
market: Market,
clock: Clock): Purchase =
Purchase(
future: Future[void].new(),
request: request,
requestId: requestId,
market: market,
clock: clock
)
func new*(_: type Purchase,
request: StorageRequest,
market: Market,
clock: Clock): Purchase =
let purchase = Purchase.new(request.id, market, clock)
purchase.request = some request
return purchase
proc start*(purchase: Purchase) =
purchase.switch(PurchasePending())
proc load*(purchase: Purchase) =
purchase.switch(PurchaseUnknown())
proc wait*(purchase: Purchase) {.async.} =
await purchase.future
func id*(purchase: Purchase): PurchaseId =
PurchaseId(purchase.request.id)
PurchaseId(purchase.requestId)
func finished*(purchase: Purchase): bool =
purchase.future.finished

View File

@ -1,6 +1,7 @@
import ../utils/statemachine
import ../market
import ../clock
import ../errors
export market
export clock
@ -11,5 +12,10 @@ type
future*: Future[void]
market*: Market
clock*: Clock
request*: StorageRequest
requestId*: RequestId
request*: ?StorageRequest
PurchaseState* = ref object of AsyncState
PurchaseError* = object of CodexError
method description*(state: PurchaseState): string {.base.} =
raiseAssert "description not implemented for state"

View File

@ -8,10 +8,13 @@ method enterAsync*(state: PurchaseCancelled) {.async.} =
raiseAssert "invalid state"
try:
await purchase.market.withdrawFunds(purchase.request.id)
await purchase.market.withdrawFunds(purchase.requestId)
except CatchableError as error:
state.switch(PurchaseError(error: error))
state.switch(PurchaseErrored(error: error))
return
let error = newException(Timeout, "Purchase cancelled due to timeout")
state.switch(PurchaseError(error: error))
state.switch(PurchaseErrored(error: error))
method description*(state: PurchaseCancelled): string =
"cancelled"

View File

@ -1,10 +1,13 @@
import ../statemachine
type PurchaseError* = ref object of PurchaseState
type PurchaseErrored* = ref object of PurchaseState
error*: ref CatchableError
method enter*(state: PurchaseError) =
method enter*(state: PurchaseErrored) =
without purchase =? (state.context as Purchase):
raiseAssert "invalid state"
purchase.future.fail(state.error)
method description*(state: PurchaseErrored): string =
"errored"

View File

@ -0,0 +1,12 @@
import ../statemachine
import ./error
type
PurchaseFailed* = ref object of PurchaseState
method enter*(state: PurchaseFailed) =
let error = newException(PurchaseError, "Purchase failed")
state.switch(PurchaseErrored(error: error))
method description*(state: PurchaseFailed): string =
"failed"

View File

@ -0,0 +1,12 @@
import ../statemachine
type PurchaseFinished* = ref object of PurchaseState
method enter*(state: PurchaseFinished) =
without purchase =? (state.context as Purchase):
raiseAssert "invalid state"
purchase.future.complete()
method description*(state: PurchaseFinished): string =
"finished"

View File

@ -5,13 +5,17 @@ import ./error
type PurchasePending* = ref object of PurchaseState
method enterAsync(state: PurchasePending) {.async.} =
without purchase =? (state.context as Purchase):
without purchase =? (state.context as Purchase) and
request =? purchase.request:
raiseAssert "invalid state"
try:
purchase.request = await purchase.market.requestStorage(purchase.request)
await purchase.market.requestStorage(request)
except CatchableError as error:
state.switch(PurchaseError(error: error))
state.switch(PurchaseErrored(error: error))
return
state.switch(PurchaseSubmitted())
method description*(state: PurchasePending): string =
"pending"

View File

@ -1,9 +1,32 @@
import ../statemachine
import ./error
import ./finished
import ./failed
type PurchaseStarted* = ref object of PurchaseState
method enter*(state: PurchaseStarted) =
method enterAsync*(state: PurchaseStarted) {.async.} =
without purchase =? (state.context as Purchase):
raiseAssert "invalid state"
purchase.future.complete()
let clock = purchase.clock
let market = purchase.market
let failed = newFuture[void]()
proc callback(_: RequestId) =
failed.complete()
let subscription = await market.subscribeRequestFailed(purchase.requestId, callback)
let ended = clock.waitUntil(await market.getRequestEnd(purchase.requestId))
try:
let fut = await one(ended, failed)
if fut.id == failed.id:
state.switch(PurchaseFailed())
else:
state.switch(PurchaseFinished())
await subscription.unsubscribe()
except CatchableError as error:
state.switch(PurchaseErrored(error: error))
method description*(state: PurchaseStarted): string =
"started"

View File

@ -6,7 +6,8 @@ import ./cancelled
type PurchaseSubmitted* = ref object of PurchaseState
method enterAsync(state: PurchaseSubmitted) {.async.} =
without purchase =? (state.context as Purchase):
without purchase =? (state.context as Purchase) and
request =? purchase.request:
raiseAssert "invalid state"
let market = purchase.market
@ -16,13 +17,12 @@ method enterAsync(state: PurchaseSubmitted) {.async.} =
let done = newFuture[void]()
proc callback(_: RequestId) =
done.complete()
let request = purchase.request
let subscription = await market.subscribeFulfillment(request.id, callback)
await done
await subscription.unsubscribe()
proc withTimeout(future: Future[void]) {.async.} =
let expiry = purchase.request.expiry.truncate(int64)
let expiry = request.expiry.truncate(int64)
await future.withTimeout(clock, expiry)
try:
@ -31,7 +31,10 @@ method enterAsync(state: PurchaseSubmitted) {.async.} =
state.switch(PurchaseCancelled())
return
except CatchableError as error:
state.switch(PurchaseError(error: error))
state.switch(PurchaseErrored(error: error))
return
state.switch(PurchaseStarted())
method description*(state: PurchaseSubmitted): string =
"submitted"

View File

@ -0,0 +1,37 @@
import ../statemachine
import ./submitted
import ./started
import ./cancelled
import ./finished
import ./failed
import ./error
type PurchaseUnknown* = ref object of PurchaseState
method enterAsync(state: PurchaseUnknown) {.async.} =
without purchase =? (state.context as Purchase):
raiseAssert "invalid state"
try:
if (request =? await purchase.market.getRequest(purchase.requestId)) and
(requestState =? await purchase.market.getState(purchase.requestId)):
purchase.request = some request
case requestState
of RequestState.New:
state.switch(PurchaseSubmitted())
of RequestState.Started:
state.switch(PurchaseStarted())
of RequestState.Cancelled:
state.switch(PurchaseCancelled())
of RequestState.Finished:
state.switch(PurchaseFinished())
of RequestState.Failed:
state.switch(PurchaseFailed())
except CatchableError as error:
state.switch(PurchaseErrored(error: error))
method description*(state: PurchaseUnknown): string =
"unknown"

View File

@ -50,7 +50,7 @@ func `%`*(id: RequestId | SlotId | Nonce): JsonNode =
func `%`*(purchase: Purchase): JsonNode =
%*{
"finished": purchase.finished,
"state": (purchase.state as PurchaseState).?description |? "none",
"error": purchase.error.?msg,
"request": purchase.request,
}

View File

@ -1,13 +1,20 @@
import std/sequtils
import std/tables
import std/hashes
import pkg/codex/market
export market
export tables
type
MockMarket* = ref object of Market
activeRequests*: Table[Address, seq[RequestId]]
requested*: seq[StorageRequest]
requestEnds*: Table[RequestId, SecondsSince1970]
state*: Table[RequestId, RequestState]
fulfilled*: seq[Fulfillment]
filled*: seq[Slot]
withdrawn*: seq[RequestId]
signer: Address
subscriptions: Subscriptions
Fulfillment* = object
@ -24,6 +31,7 @@ type
onFulfillment: seq[FulfillmentSubscription]
onSlotFilled: seq[SlotFilledSubscription]
onRequestCancelled: seq[RequestCancelledSubscription]
onRequestFailed: seq[RequestFailedSubscription]
RequestSubscription* = ref object of Subscription
market: MockMarket
callback: OnRequest
@ -40,6 +48,16 @@ type
market: MockMarket
requestId: RequestId
callback: OnRequestCancelled
RequestFailedSubscription* = ref object of Subscription
market: MockMarket
requestId: RequestId
callback: OnRequestCancelled
proc hash*(address: Address): Hash =
hash(address.toArray)
proc hash*(requestId: RequestId): Hash =
hash(requestId.toArray)
proc new*(_: type MockMarket): MockMarket =
MockMarket(signer: Address.example)
@ -47,14 +65,14 @@ proc new*(_: type MockMarket): MockMarket =
method getSigner*(market: MockMarket): Future[Address] {.async.} =
return market.signer
method requestStorage*(market: MockMarket,
request: StorageRequest):
Future[StorageRequest] {.async.} =
method requestStorage*(market: MockMarket, request: StorageRequest) {.async.} =
market.requested.add(request)
var subscriptions = market.subscriptions.onRequest
for subscription in subscriptions:
subscription.callback(request.id, request.ask)
return request
method myRequests*(market: MockMarket): Future[seq[RequestId]] {.async.} =
return market.activeRequests[market.signer]
method getRequest(market: MockMarket,
id: RequestId): Future[?StorageRequest] {.async.} =
@ -63,6 +81,14 @@ method getRequest(market: MockMarket,
return some request
return none StorageRequest
method getState*(market: MockMarket,
requestId: RequestId): Future[?RequestState] {.async.} =
return market.state.?[requestId]
method getRequestEnd*(market: MockMarket,
id: RequestId): Future[SecondsSince1970] {.async.} =
return market.requestEnds[id]
method getHost(market: MockMarket,
requestId: RequestId,
slotIndex: UInt256): Future[?Address] {.async.} =
@ -93,6 +119,12 @@ proc emitRequestFulfilled*(market: MockMarket, requestId: RequestId) =
if subscription.requestId == requestId:
subscription.callback(requestId)
proc emitRequestFailed*(market: MockMarket, requestId: RequestId) =
var subscriptions = market.subscriptions.onRequestFailed
for subscription in subscriptions:
if subscription.requestId == requestId:
subscription.callback(requestId)
proc fillSlot*(market: MockMarket,
requestId: RequestId,
slotIndex: UInt256,
@ -115,6 +147,7 @@ method fillSlot*(market: MockMarket,
method withdrawFunds*(market: MockMarket,
requestId: RequestId) {.async.} =
market.withdrawn.add(requestId)
market.emitRequestCancelled(requestId)
method subscribeRequests*(market: MockMarket,
@ -165,6 +198,18 @@ method subscribeRequestCancelled*(market: MockMarket,
market.subscriptions.onRequestCancelled.add(subscription)
return subscription
method subscribeRequestFailed*(market: MockMarket,
requestId: RequestId,
callback: OnRequestFailed):
Future[Subscription] {.async.} =
let subscription = RequestFailedSubscription(
market: market,
requestId: requestId,
callback: callback
)
market.subscriptions.onRequestFailed.add(subscription)
return subscription
method unsubscribe*(subscription: RequestSubscription) {.async.} =
subscription.market.subscriptions.onRequest.keepItIf(it != subscription)
@ -176,3 +221,6 @@ method unsubscribe*(subscription: SlotFilledSubscription) {.async.} =
method unsubscribe*(subscription: RequestCancelledSubscription) {.async.} =
subscription.market.subscriptions.onRequestCancelled.keepItIf(it != subscription)
method unsubscribe*(subscription: RequestFailedSubscription) {.async.} =
subscription.market.subscriptions.onRequestFailed.keepItIf(it != subscription)

View File

@ -3,6 +3,7 @@ import pkg/chronos
import pkg/codex/proving
import ./helpers/mockproofs
import ./helpers/mockclock
import ./helpers/eventually
import ./examples
suite "Proving":
@ -23,7 +24,6 @@ suite "Proving":
proc advanceToNextPeriod(proofs: MockProofs) {.async.} =
let periodicity = await proofs.periodicity()
clock.advance(periodicity.seconds.truncate(int64))
await sleepAsync(2.seconds)
test "maintains a list of contract ids to watch":
let id1, id2 = SlotId.example
@ -49,7 +49,7 @@ suite "Proving":
proving.onProofRequired = onProofRequired
proofs.setProofRequired(id, true)
await proofs.advanceToNextPeriod()
check called
check eventually called
test "callback receives id of contract for which proof is required":
let id1, id2 = SlotId.example
@ -61,11 +61,11 @@ suite "Proving":
proving.onProofRequired = onProofRequired
proofs.setProofRequired(id1, true)
await proofs.advanceToNextPeriod()
check callbackIds == @[id1]
check eventually callbackIds == @[id1]
proofs.setProofRequired(id1, false)
proofs.setProofRequired(id2, true)
await proofs.advanceToNextPeriod()
check callbackIds == @[id1, id2]
check eventually callbackIds == @[id1, id2]
test "invokes callback when proof is about to be required":
let id = SlotId.example
@ -77,7 +77,7 @@ suite "Proving":
proofs.setProofRequired(id, false)
proofs.setProofToBeRequired(id, true)
await proofs.advanceToNextPeriod()
check called
check eventually called
test "stops watching when contract has ended":
let id = SlotId.example
@ -90,17 +90,17 @@ suite "Proving":
proving.onProofRequired = onProofRequired
proofs.setProofRequired(id, true)
await proofs.advanceToNextPeriod()
check not proving.slots.contains(id)
check eventually (not proving.slots.contains(id))
check not called
test "submits proofs":
let id = SlotId.example
let proof = seq[byte].example
let proof = exampleProof()
await proving.submitProof(id, proof)
test "supports proof submission subscriptions":
let id = SlotId.example
let proof = seq[byte].example
let proof = exampleProof()
var receivedIds: seq[SlotId]
var receivedProofs: seq[seq[byte]]

View File

@ -4,8 +4,10 @@ import pkg/chronos
import pkg/upraises
import pkg/stint
import pkg/codex/purchasing
import pkg/codex/purchasing/states/[finished, failed, error, started, submitted, unknown]
import ./helpers/mockmarket
import ./helpers/mockclock
import ./helpers/eventually
import ./examples
suite "Purchasing":
@ -29,7 +31,7 @@ suite "Purchasing":
)
test "submits a storage request when asked":
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
let submitted = market.requested[0]
check submitted.ask.slots == request.ask.slots
check submitted.ask.slotSize == request.ask.slotSize
@ -37,8 +39,8 @@ suite "Purchasing":
check submitted.ask.reward == request.ask.reward
test "remembers purchases":
let purchase1 = purchasing.purchase(request)
let purchase2 = purchasing.purchase(request)
let purchase1 = await purchasing.purchase(request)
let purchase2 = await purchasing.purchase(request)
check purchasing.getPurchase(purchase1.id) == some purchase1
check purchasing.getPurchase(purchase2.id) == some purchase2
@ -47,12 +49,12 @@ suite "Purchasing":
test "can change default value for proof probability":
purchasing.proofProbability = 42.u256
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].ask.proofProbability == 42.u256
test "can override proof probability per request":
request.ask.proofProbability = 42.u256
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].ask.proofProbability == 42.u256
test "has a default value for request expiration interval":
@ -61,53 +63,178 @@ suite "Purchasing":
test "can change default value for request expiration interval":
purchasing.requestExpiryInterval = 42.u256
let start = getTime().toUnix()
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].expiry == (start + 42).u256
test "can override expiry time per request":
let expiry = (getTime().toUnix() + 42).u256
request.expiry = expiry
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].expiry == expiry
test "includes a random nonce in every storage request":
discard purchasing.purchase(request)
discard purchasing.purchase(request)
discard await purchasing.purchase(request)
discard await purchasing.purchase(request)
check market.requested[0].nonce != market.requested[1].nonce
test "succeeds when request is fulfilled":
let purchase = purchasing.purchase(request)
test "sets client address in request":
discard await purchasing.purchase(request)
check market.requested[0].client == await market.getSigner()
test "succeeds when request is finished":
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
let requestEnd = getTime().toUnix() + 42
market.requestEnds[request.id] = requestEnd
market.emitRequestFulfilled(request.id)
clock.set(requestEnd)
await purchase.wait()
check purchase.error.isNone
test "fails when request times out":
let purchase = purchasing.purchase(request)
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
clock.set(request.expiry.truncate(int64))
expect PurchaseTimeout:
await purchase.wait()
test "checks that funds were withdrawn when purchase times out":
let purchase = purchasing.purchase(request)
let purchase = await purchasing.purchase(request)
let request = market.requested[0]
var receivedIds: seq[RequestId]
clock.set(request.expiry.truncate(int64))
proc onRequestCancelled(id: RequestId) {.gcsafe, upraises:[].} =
receivedIds.add(id)
# will only be fired when `withdrawFunds` is called on purchase timeout
let subscription = await market.subscribeRequestCancelled(
request.id,
onRequestCancelled)
var purchaseTimedOut = false
try:
expect PurchaseTimeout:
await purchase.wait()
except PurchaseTimeout:
purchaseTimedOut = true
check market.withdrawn == @[request.id]
await subscription.unsubscribe()
check purchaseTimedOut
check receivedIds == @[request.id]
suite "Purchasing state machine":
var purchasing: Purchasing
var market: MockMarket
var clock: MockClock
var request: StorageRequest
setup:
market = MockMarket.new()
clock = MockClock.new()
purchasing = Purchasing.new(market, clock)
request = StorageRequest(
ask: StorageAsk(
slots: uint8.example.uint64,
slotSize: uint32.example.u256,
duration: uint16.example.u256,
reward: uint8.example.u256
)
)
test "loads active purchases from market":
let me = await market.getSigner()
let request1, request2, request3 = StorageRequest.example
market.requested = @[request1, request2, request3]
market.activeRequests[me] = @[request1.id, request2.id]
await purchasing.load()
check isSome purchasing.getPurchase(PurchaseId(request1.id))
check isSome purchasing.getPurchase(PurchaseId(request2.id))
check isNone purchasing.getPurchase(PurchaseId(request3.id))
test "loads correct purchase.future state for purchases from market":
let me = await market.getSigner()
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
# ensure the started state doesn't error, giving a false positive test result
market.requestEnds[request2.id] = clock.now() - 1
await purchasing.load()
check purchasing.getPurchase(PurchaseId(request1.id)).?finished == false.some
check purchasing.getPurchase(PurchaseId(request2.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request3.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request4.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request5.id)).?finished == true.some
check purchasing.getPurchase(PurchaseId(request5.id)).?error.isSome
test "moves to PurchaseSubmitted when request state is New":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.New
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseSubmitted).isSome
test "moves to PurchaseStarted when request state is Started":
let request = StorageRequest.example
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
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseStarted).isSome
test "moves to PurchaseErrored when request state is Cancelled":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Cancelled
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseErrored).isSome
check purchase.error.?msg == "Purchase cancelled due to timeout".some
test "moves to PurchaseFinished when request state is Finished":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Finished
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseFinished).isSome
test "moves to PurchaseErrored when request state is Failed":
let request = StorageRequest.example
let purchase = Purchase.new(request, market, clock)
market.requested = @[request]
market.state[request.id] = RequestState.Failed
purchase.switch(PurchaseUnknown())
check (purchase.state as PurchaseErrored).isSome
check purchase.error.?msg == "Purchase failed".some
test "moves to PurchaseErrored state once RequestFailed emitted":
let me = await market.getSigner()
let request = StorageRequest.example
market.requested = @[request]
market.activeRequests[me] = @[request.id]
market.state[request.id] = RequestState.Started
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
await purchasing.load()
# emit mock contract failure event
market.emitRequestFailed(request.id)
# must allow time for the callback to trigger the completion of the future
await sleepAsync(chronos.milliseconds(10))
# now check the result
let purchase = purchasing.getPurchase(PurchaseId(request.id))
let state = purchase.?state
check (state as PurchaseErrored).isSome
check (!purchase).error.?msg == "Purchase failed".some
test "moves to PurchaseFinished state once request finishes":
let me = await market.getSigner()
let request = StorageRequest.example
market.requested = @[request]
market.activeRequests[me] = @[request.id]
market.state[request.id] = RequestState.Started
market.requestEnds[request.id] = clock.now() + request.ask.duration.truncate(int64)
await purchasing.load()
# advance the clock to the end of the request
clock.advance(request.ask.duration.truncate(int64))
# now check the result
proc getState: ?PurchaseState =
purchasing.getPurchase(PurchaseId(request.id)).?state as PurchaseState
check eventually (getState() as PurchaseFinished).isSome

View File

@ -6,6 +6,7 @@ import pkg/codex/proving
import pkg/codex/sales
import ./helpers/mockmarket
import ./helpers/mockclock
import ./helpers/eventually
import ./examples
suite "Sales":
@ -26,7 +27,7 @@ suite "Sales":
cid: "some cid"
)
)
let proof = seq[byte].example
let proof = exampleProof()
var sales: Sales
var market: MockMarket
@ -75,21 +76,21 @@ suite "Sales":
test "makes storage unavailable when matching request comes in":
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check sales.available.len == 0
test "ignores request when no matching storage is available":
sales.add(availability)
var tooBig = request
tooBig.ask.slotSize = request.ask.slotSize + 1
discard await market.requestStorage(tooBig)
await market.requestStorage(tooBig)
check sales.available == @[availability]
test "ignores request when reward is too low":
sales.add(availability)
var tooCheap = request
tooCheap.ask.reward = request.ask.reward - 1
discard await market.requestStorage(tooCheap)
await market.requestStorage(tooCheap)
check sales.available == @[availability]
test "retrieves and stores data locally":
@ -103,8 +104,8 @@ suite "Sales":
storingSlot = slot
storingAvailability = availability
sales.add(availability)
let requested = await market.requestStorage(request)
check storingRequest == requested
await market.requestStorage(request)
check storingRequest == request
check storingSlot < request.ask.slots.u256
check storingAvailability == availability
@ -115,7 +116,7 @@ suite "Sales":
availability: Availability) {.async.} =
raise error
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check sales.available == @[availability]
test "generates proof of storage":
@ -126,13 +127,13 @@ suite "Sales":
provingRequest = request
provingSlot = slot
sales.add(availability)
let requested = await market.requestStorage(request)
check provingRequest == requested
await market.requestStorage(request)
check provingRequest == request
check provingSlot < request.ask.slots.u256
test "fills a slot":
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check market.filled.len == 1
check market.filled[0].requestId == request.id
check market.filled[0].slotIndex < request.ask.slots.u256
@ -150,7 +151,7 @@ suite "Sales":
soldRequest = request
soldSlotIndex = slotIndex
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check soldAvailability == availability
check soldRequest == request
check soldSlotIndex < request.ask.slots.u256
@ -171,7 +172,7 @@ suite "Sales":
clearedRequest = request
clearedSlotIndex = slotIndex
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check clearedAvailability == availability
check clearedRequest == request
check clearedSlotIndex < request.ask.slots.u256
@ -183,7 +184,7 @@ suite "Sales":
availability: Availability) {.async.} =
await sleepAsync(1.hours)
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
for slotIndex in 0..<request.ask.slots:
market.fillSlot(request.id, slotIndex.u256, proof, otherHost)
check sales.available == @[availability]
@ -194,10 +195,9 @@ suite "Sales":
availability: Availability) {.async.} =
await sleepAsync(1.hours)
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
clock.set(request.expiry.truncate(int64))
await sleepAsync(2.seconds)
check sales.available == @[availability]
check eventually (sales.available == @[availability])
test "adds proving for slot when slot is filled":
var soldSlotIndex: UInt256
@ -207,6 +207,6 @@ suite "Sales":
soldSlotIndex = slotIndex
check proving.slots.len == 0
sales.add(availability)
discard await market.requestStorage(request)
await market.requestStorage(request)
check proving.slots.len == 1
check proving.slots.contains(request.slotId(soldSlotIndex))

View File

@ -1,36 +1,7 @@
import std/times
import pkg/stint
import pkg/ethers
import codex/contracts
import ../examples
export examples
proc example*(_: type Address): Address =
Address(array[20, byte].example)
proc example*(_: type StorageRequest): StorageRequest =
StorageRequest(
client: Address.example,
ask: StorageAsk(
slots: 4,
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
duration: (10 * 60 * 60).u256, # 10 hours
proofProbability: 4.u256, # require a proof roughly once every 4 periods
reward: 84.u256,
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
),
content: StorageContent(
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
erasure: StorageErasure(
totalChunks: 12,
),
por: StoragePor(
u: @(array[480, byte].example),
publicKey: @(array[96, byte].example),
name: @(array[512, byte].example)
)
),
expiry: (getTime() + initDuration(hours=1)).toUnix.u256,
nonce: Nonce.example
)

View File

@ -9,7 +9,7 @@ import ./examples
import ./time
ethersuite "Storage contracts":
let proof = seq[byte].example
let proof = exampleProof()
var client, host: Signer
var storage: Storage
@ -58,6 +58,10 @@ ethersuite "Storage contracts":
):
await provider.advanceTime(periodicity.seconds)
proc startContract() {.async.} =
for slotIndex in 1..<request.ask.slots:
await storage.fillSlot(request.id, slotIndex.u256, proof)
test "accept storage proofs":
switchAccount(host)
await waitUntilProofRequired(slotId)
@ -71,9 +75,11 @@ ethersuite "Storage contracts":
switchAccount(client)
await storage.markProofAsMissing(slotId, missingPeriod)
test "can be payed out at the end":
test "can be paid out at the end":
switchAccount(host)
await provider.advanceTimeTo(await storage.proofEnd(slotId))
await startContract()
let requestEnd = await storage.requestEnd(request.id)
await provider.advanceTimeTo(requestEnd.u256)
await storage.payoutSlot(request.id, 0.u256)
test "cannot mark proofs missing for cancelled request":

View File

@ -3,18 +3,20 @@ import pkg/chronos
import pkg/ethers/testing
import codex/contracts
import codex/contracts/testtoken
import codex/storageproofs
import ../ethertest
import ./examples
import ./time
ethersuite "On-Chain Market":
let proof = seq[byte].example
let proof = exampleProof()
var market: OnChainMarket
var storage: Storage
var token: TestToken
var request: StorageRequest
var slotIndex: UInt256
var periodicity: Periodicity
setup:
let deployment = deployment()
@ -27,12 +29,22 @@ ethersuite "On-Chain Market":
await storage.deposit(collateral)
market = OnChainMarket.new(storage)
periodicity = Periodicity(seconds: await storage.proofPeriod())
request = StorageRequest.example
request.client = accounts[0]
slotIndex = (request.ask.slots div 2).u256
proc waitUntilProofRequired(slotId: SlotId) {.async.} =
let currentPeriod = periodicity.periodOf(await provider.currentTime())
await provider.advanceTimeTo(periodicity.periodEnd(currentPeriod))
while not (
(await storage.isProofRequired(slotId)) and
(await storage.getPointer(slotId)) < 250
):
await provider.advanceTime(periodicity.seconds)
test "fails to instantiate when contract does not have a signer":
let storageWithoutSigner = storage.connect(provider)
expect AssertionError:
@ -43,25 +55,18 @@ ethersuite "On-Chain Market":
test "supports storage requests":
await token.approve(storage.address, request.price)
check (await market.requestStorage(request)) == request
test "sets client address when submitting storage request":
var requestWithoutClient = request
requestWithoutClient.client = Address.default
await token.approve(storage.address, request.price)
let submitted = await market.requestStorage(requestWithoutClient)
check submitted.client == accounts[0]
await market.requestStorage(request)
test "can retrieve previously submitted requests":
check (await market.getRequest(request.id)) == none StorageRequest
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
let r = await market.getRequest(request.id)
check (r) == some request
test "supports withdrawing of funds":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
await provider.advanceTimeTo(request.expiry)
await market.withdrawFunds(request.id)
@ -73,26 +78,26 @@ ethersuite "On-Chain Market":
receivedAsks.add(ask)
let subscription = await market.subscribeRequests(onRequest)
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
check receivedIds == @[request.id]
check receivedAsks == @[request.ask]
await subscription.unsubscribe()
test "supports filling of slots":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof)
test "can retrieve host that filled slot":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
check (await market.getHost(request.id, slotIndex)) == none Address
await market.fillSlot(request.id, slotIndex, proof)
check (await market.getHost(request.id, slotIndex)) == some accounts[0]
test "support slot filled subscriptions":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
var receivedSlotIndices: seq[UInt256]
proc onSlotFilled(id: RequestId, slotIndex: UInt256) =
@ -107,7 +112,7 @@ ethersuite "On-Chain Market":
test "subscribes only to a certain slot":
var otherSlot = slotIndex - 1
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
var receivedSlotIndices: seq[UInt256]
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
receivedSlotIndices.add(slotIndex)
@ -120,7 +125,7 @@ ethersuite "On-Chain Market":
test "support fulfillment subscriptions":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onFulfillment(id: RequestId) =
receivedIds.add(id)
@ -135,9 +140,9 @@ ethersuite "On-Chain Market":
otherRequest.client = accounts[0]
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
await token.approve(storage.address, otherRequest.price)
discard await market.requestStorage(otherRequest)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
proc onFulfillment(id: RequestId) =
@ -156,7 +161,7 @@ ethersuite "On-Chain Market":
test "support request cancelled subscriptions":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onRequestCancelled(id: RequestId) =
@ -168,12 +173,38 @@ ethersuite "On-Chain Market":
check receivedIds == @[request.id]
await subscription.unsubscribe()
test "subscribes only to a certain request cancellation":
let otherRequest = StorageRequest.example
test "support request failed subscriptions":
await token.approve(storage.address, request.price)
discard await market.requestStorage(request)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onRequestFailed(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
for slotIndex in 0..request.ask.maxSlotLoss:
let slotId = request.slotId(slotIndex.u256)
while true:
try:
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await provider.currentTime())
await provider.advanceTime(periodicity.seconds)
await storage.markProofAsMissing(slotId, missingPeriod)
except ProviderError as e:
if e.revertReason == "Slot empty":
break
check receivedIds == @[request.id]
await subscription.unsubscribe()
test "subscribes only to a certain request cancellation":
var otherRequest = request
otherRequest.nonce = Nonce.example
await token.approve(storage.address, request.price)
await market.requestStorage(request)
await token.approve(storage.address, otherRequest.price)
discard await market.requestStorage(otherRequest)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
proc onRequestCancelled(requestId: RequestId) =
@ -181,9 +212,7 @@ ethersuite "On-Chain Market":
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
await provider.advanceTimeTo(request.expiry) # shares expiry with otherRequest
check await market
.withdrawFunds(otherRequest.id)
.reverts("Invalid client address")
await market.withdrawFunds(otherRequest.id)
check receivedIds.len == 0
await market.withdrawFunds(request.id)
check receivedIds == @[request.id]
@ -191,3 +220,19 @@ ethersuite "On-Chain Market":
test "request is none when unknown":
check isNone await market.getRequest(request.id)
test "can retrieve active requests":
await token.approve(storage.address, request.price)
await market.requestStorage(request)
var request2 = StorageRequest.example
request2.client = accounts[0]
await token.approve(storage.address, request2.price)
await market.requestStorage(request2)
check (await market.myRequests()) == @[request.id, request2.id]
test "can retrieve request state":
await token.approve(storage.address, request.price)
await market.requestStorage(request)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
check (await market.getState(request.id)) == some RequestState.Started

View File

@ -6,7 +6,7 @@ import ./time
ethersuite "On-Chain Proofs":
let contractId = SlotId.example
let proof = seq[byte].example
let proof = exampleProof()
var proofs: OnChainProofs
var storage: Storage

View File

@ -1,5 +1,6 @@
import std/random
import std/sequtils
import std/times
import pkg/codex/proving
import pkg/stint
@ -19,3 +20,35 @@ proc example*(_: type UInt256): UInt256 =
proc example*[T: RequestId | SlotId | Nonce](_: type T): T =
T(array[32, byte].example)
proc example*(_: type StorageRequest): StorageRequest =
StorageRequest(
client: Address.example,
ask: StorageAsk(
slots: 4,
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
duration: (10 * 60 * 60).u256, # 10 hours
proofProbability: 4.u256, # require a proof roughly once every 4 periods
reward: 84.u256,
maxSlotLoss: 2 # 2 slots can be freed without data considered to be lost
),
content: StorageContent(
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
erasure: StorageErasure(
totalChunks: 12,
),
por: StoragePor(
u: @(array[480, byte].example),
publicKey: @(array[96, byte].example),
name: @(array[512, byte].example)
)
),
expiry: (getTime() + initDuration(hours=1)).toUnix.u256,
nonce: Nonce.example
)
proc exampleProof*(): seq[byte] =
var proof: seq[byte]
while proof.len == 0:
proof = seq[byte].example
return proof

View File

@ -6,17 +6,43 @@ import std/strutils
const workingDir = currentSourcePath() / ".." / ".." / ".."
const executable = "build" / "codex"
proc startNode*(args: openArray[string], debug = false): Process =
if debug:
result = startProcess(executable, workingDir, args, options={poParentStreams})
type NodeProcess* = ref object
process: Process
arguments: seq[string]
debug: bool
proc start(node: NodeProcess) =
if node.debug:
node.process = startProcess(
executable,
workingDir,
node.arguments,
options={poParentStreams}
)
sleep(1000)
else:
result = startProcess(executable, workingDir, args)
for line in result.outputStream.lines:
node.process = startProcess(
executable,
workingDir,
node.arguments
)
for line in node.process.outputStream.lines:
if line.contains("Started codex node"):
break
proc stop*(node: Process) =
node.terminate()
discard node.waitForExit(timeout=5_000)
node.close()
proc startNode*(args: openArray[string], debug = false): NodeProcess =
## Starts a Codex Node with the specified arguments.
## Set debug to 'true' to see output of the node.
let node = NodeProcess(arguments: @args, debug: debug)
node.start()
node
proc stop*(node: NodeProcess) =
let process = node.process
process.terminate()
discard process.waitForExit(timeout=5_000)
process.close()
proc restart*(node: NodeProcess) =
node.stop()
node.start()

View File

@ -9,10 +9,11 @@ import ./ethertest
import ./contracts/time
import ./integration/nodes
import ./integration/tokens
import ./codex/helpers/eventually
ethersuite "Integration tests":
var node1, node2: Process
var node1, node2: NodeProcess
var baseurl1, baseurl2: string
var client: HttpClient
@ -34,8 +35,8 @@ ethersuite "Integration tests":
"--nat=127.0.0.1",
"--disc-ip=127.0.0.1",
"--disc-port=8090",
"--persistence",
"--eth-account=" & $accounts[0]
"--persistence",
"--eth-account=" & $accounts[0]
], debug = false)
node2 = startNode([
@ -97,6 +98,26 @@ ethersuite "Integration tests":
check json["request"]["ask"]["duration"].getStr == "0x1"
check json["request"]["ask"]["reward"].getStr == "0x2"
test "node remembers purchase status after restart":
let cid = client.post(baseurl1 & "/upload", "some file contents").body
let request = %*{"duration": "0x1", "reward": "0x2"}
let id = client.post(baseurl1 & "/storage/request/" & cid, $request).body
proc getPurchase(id: string): JsonNode =
let response = client.get(baseurl1 & "/storage/purchases/" & id)
return parseJson(response.body).catch |? nil
check eventually getPurchase(id){"state"}.getStr == "submitted"
node1.restart()
client.close()
client = newHttpClient()
check eventually (not isNil getPurchase(id){"request"}{"ask"})
check getPurchase(id){"request"}{"ask"}{"duration"}.getStr == "0x1"
check getPurchase(id){"request"}{"ask"}{"reward"}.getStr == "0x2"
test "nodes negotiate contracts on the marketplace":
proc sell =
let json = %*{"size": "0xFFFFF", "duration": "0x200", "minPrice": "0x300"}
@ -110,14 +131,14 @@ ethersuite "Integration tests":
proc buy(cid: string): string =
let expiry = ((waitFor provider.currentTime()) + 30).toHex
let json = %*{"duration": "0x100", "reward": "0x400", "expiry": expiry}
let json = %*{"duration": "0x1", "reward": "0x400", "expiry": expiry}
client.post(baseurl1 & "/storage/request/" & cid, $json).body
proc finish(purchase: string): Future[JsonNode] {.async.} =
while true:
let response = client.get(baseurl1 & "/storage/purchases/" & purchase)
let json = parseJson(response.body)
if json["finished"].getBool: return json
if json["state"].getStr == "finished": return json
await sleepAsync(1.seconds)
sell()

@ -1 +1 @@
Subproject commit 087c13a7fc2b44a5ad52b8a624f51b711a10d783
Subproject commit 61b8f5fc352838866b0fe27b936323de45bf269c