From 4ffe7b8e06575de951d563108bf4b0c45289080d Mon Sep 17 00:00:00 2001 From: markspanbroek Date: Mon, 27 Mar 2023 15:47:25 +0200 Subject: [PATCH] Generate proofs when required (#383) * [maintenance] speedup integration test * [rest api] add proofProbability parameter to storage requests * [integration] negotiation test ends when contract starts * [integration] reusable 2 node setup for tests * [integration] introduce CodexClient for tests * [node] submit storage proofs when required * [contracts] Add Slot type * [proving] replace onProofRequired & submitProof with onProve Removes duplication between Sales.onProve() and Proving.onProofRequired() --- codex/contracts/requests.nim | 8 ++ codex/node.nim | 5 +- codex/proving.nim | 45 +++--- codex/rest/api.nim | 14 +- codex/rest/json.nim | 3 + codex/sales.nim | 5 - codex/sales/salescontext.nim | 3 - codex/sales/states/finished.nim | 2 +- codex/sales/states/proving.nim | 4 +- docs/TWOCLIENTTEST.md | 7 +- openapi.yaml | 14 +- tests/codex/sales/testsales.nim | 15 +- tests/codex/testproving.nim | 112 +++++++-------- tests/examples.nim | 5 + tests/integration/codexclient.nim | 73 ++++++++++ tests/integration/testIntegration.nim | 165 ++++++---------------- tests/integration/testblockexpiration.nim | 10 +- tests/integration/testproofs.nim | 43 ++++++ tests/integration/twonodes.nim | 62 ++++++++ tests/testIntegration.nim | 1 + 20 files changed, 360 insertions(+), 236 deletions(-) create mode 100644 tests/integration/codexclient.nim create mode 100644 tests/integration/testproofs.nim create mode 100644 tests/integration/twonodes.nim diff --git a/codex/contracts/requests.nim b/codex/contracts/requests.nim index 2a866952..f9c3cd73 100644 --- a/codex/contracts/requests.nim +++ b/codex/contracts/requests.nim @@ -30,6 +30,9 @@ type u*: seq[byte] publicKey*: seq[byte] name*: seq[byte] + Slot* = object + request*: StorageRequest + slotIndex*: UInt256 SlotId* = distinct array[32, byte] RequestId* = distinct array[32, byte] Nonce* = distinct array[32, byte] @@ -50,6 +53,8 @@ proc `==`*(x, y: Nonce): bool {.borrow.} proc `==`*(x, y: RequestId): bool {.borrow.} proc `==`*(x, y: SlotId): bool {.borrow.} proc hash*(x: SlotId): Hash {.borrow.} +proc hash*(x: Nonce): Hash {.borrow.} +proc hash*(x: Address): Hash {.borrow.} func toArray*(id: RequestId | SlotId | Nonce): array[32, byte] = array[32, byte](id) @@ -159,6 +164,9 @@ func slotId*(requestId: RequestId, slot: UInt256): SlotId = func slotId*(request: StorageRequest, slot: UInt256): SlotId = slotId(request.id, slot) +func id*(slot: Slot): SlotId = + slotId(slot.request, slot.slotIndex) + func pricePerSlot*(ask: StorageAsk): UInt256 = ask.duration * ask.reward diff --git a/codex/node.nim b/codex/node.nim index ad32a434..342d7632 100644 --- a/codex/node.nim +++ b/codex/node.nim @@ -235,6 +235,7 @@ proc store*( proc requestStorage*(self: CodexNodeRef, cid: Cid, duration: UInt256, + proofProbability: UInt256, nodes: uint, tolerance: uint, reward: UInt256, @@ -280,6 +281,7 @@ proc requestStorage*(self: CodexNodeRef, slots: nodes + tolerance, slotSize: (encoded.blockSize * encoded.steps).u256, duration: duration, + proofProbability: proofProbability, reward: reward, maxSlotLoss: tolerance ), @@ -360,8 +362,7 @@ proc start*(node: CodexNodeRef) {.async.} = # TODO: remove data from local storage discard - contracts.sales.onProve = proc(request: StorageRequest, - slot: UInt256): Future[seq[byte]] {.async.} = + contracts.proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = # TODO: generate proof return @[42'u8] diff --git a/codex/proving.nim b/codex/proving.nim index 53bc467e..92fb4477 100644 --- a/codex/proving.nim +++ b/codex/proving.nim @@ -13,18 +13,21 @@ type proofs: Proofs clock: Clock loop: ?Future[void] - slots*: HashSet[SlotId] - onProofRequired: ?OnProofRequired - OnProofRequired* = proc (id: SlotId) {.gcsafe, upraises:[].} + slots*: HashSet[Slot] + onProve: ?OnProve + OnProve* = proc(slot: Slot): Future[seq[byte]] {.gcsafe, upraises: [].} func new*(_: type Proving, proofs: Proofs, clock: Clock): Proving = Proving(proofs: proofs, clock: clock) -proc `onProofRequired=`*(proving: Proving, callback: OnProofRequired) = - proving.onProofRequired = some callback +proc onProve*(proving: Proving): ?OnProve = + proving.onProve -func add*(proving: Proving, id: SlotId) = - proving.slots.incl(id) +proc `onProve=`*(proving: Proving, callback: OnProve) = + proving.onProve = some callback + +func add*(proving: Proving, slot: Slot) = + proving.slots.incl(slot) proc getCurrentPeriod(proving: Proving): Future[Period] {.async.} = let periodicity = await proving.proofs.periodicity() @@ -35,23 +38,32 @@ proc waitUntilPeriod(proving: Proving, period: Period) {.async.} = await proving.clock.waitUntil(periodicity.periodStart(period).truncate(int64)) proc removeEndedContracts(proving: Proving) {.async.} = - var ended: HashSet[SlotId] - for id in proving.slots: - let state = await proving.proofs.slotState(id) + var ended: HashSet[Slot] + for slot in proving.slots: + let state = await proving.proofs.slotState(slot.id) if state != SlotState.Filled: - ended.incl(id) + ended.incl(slot) proving.slots.excl(ended) +proc prove(proving: Proving, slot: Slot) {.async.} = + without onProve =? proving.onProve: + raiseAssert "onProve callback not set" + try: + let proof = await onProve(slot) + await proving.proofs.submitProof(slot.id, proof) + except CatchableError as e: + error "Submitting proof failed", msg = e.msg + proc run(proving: Proving) {.async.} = try: while true: let currentPeriod = await proving.getCurrentPeriod() await proving.removeEndedContracts() - for id in proving.slots: + for slot in proving.slots: + let id = slot.id if (await proving.proofs.isProofRequired(id)) or - (await proving.proofs.willProofBeRequired(id)): - if callback =? proving.onProofRequired: - callback(id) + (await proving.proofs.willProofBeRequired(id)): + asyncSpawn proving.prove(slot) await proving.waitUntilPeriod(currentPeriod + 1) except CancelledError: discard @@ -70,9 +82,6 @@ proc stop*(proving: Proving) {.async.} = if not loop.finished: await loop.cancelAndWait() -proc submitProof*(proving: Proving, id: SlotId, proof: seq[byte]) {.async.} = - await proving.proofs.submitProof(id, proof) - proc subscribeProofSubmission*(proving: Proving, callback: OnProofSubmitted): Future[Subscription] = diff --git a/codex/rest/api.nim b/codex/rest/api.nim index b5a1cfe8..5db1f4e5 100644 --- a/codex/rest/api.nim +++ b/codex/rest/api.nim @@ -133,12 +133,13 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter = "/api/codex/v1/storage/request/{cid}") do (cid: Cid) -> RestApiResponse: ## Create a request for storage ## - ## cid - the cid of a previously uploaded dataset - ## duration - the duration of the request in seconds - ## reward - the maximum amount of tokens paid per second per slot to hosts the client is willing to pay - ## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data - ## nodes - minimal number of nodes the content should be stored on - ## tolerance - allowed number of nodes that can be lost before pronouncing the content lost + ## cid - the cid of a previously uploaded dataset + ## duration - the duration of the request in seconds + ## proofProbability - how often storage proofs are required + ## reward - the maximum amount of tokens paid per second per slot to hosts the client is willing to pay + ## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data + ## nodes - minimal number of nodes the content should be stored on + ## tolerance - allowed number of nodes that can be lost before pronouncing the content lost without cid =? cid.tryGet.catch, error: return RestApiResponse.error(Http400, error.msg) @@ -154,6 +155,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter = without purchaseId =? await node.requestStorage( cid, params.duration, + params.proofProbability, nodes, tolerance, params.reward, diff --git a/codex/rest/json.nim b/codex/rest/json.nim index d1ef3687..9d68d819 100644 --- a/codex/rest/json.nim +++ b/codex/rest/json.nim @@ -8,6 +8,7 @@ import ../purchasing type StorageRequestParams* = object duration*: UInt256 + proofProbability*: UInt256 reward*: UInt256 expiry*: ?UInt256 nodes*: ?uint @@ -24,12 +25,14 @@ proc fromJson*(_: type StorageRequestParams, bytes: seq[byte]): ?! StorageRequestParams = let json = ?catch parseJson(string.fromBytes(bytes)) let duration = ?catch UInt256.fromHex(json["duration"].getStr) + let proofProbability = ?catch UInt256.fromHex(json["proofProbability"].getStr) let reward = ?catch UInt256.fromHex(json["reward"].getStr) let expiry = UInt256.fromHex(json["expiry"].getStr).catch.option let nodes = strutils.fromHex[uint](json["nodes"].getStr).catch.option let tolerance = strutils.fromHex[uint](json["tolerance"].getStr).catch.option success StorageRequestParams( duration: duration, + proofProbability: proofProbability, reward: reward, expiry: expiry, nodes: nodes, diff --git a/codex/sales.nim b/codex/sales.nim index d88621c1..c859badd 100644 --- a/codex/sales.nim +++ b/codex/sales.nim @@ -47,9 +47,6 @@ type proc `onStore=`*(sales: Sales, onStore: OnStore) = sales.context.onStore = some onStore -proc `onProve=`*(sales: Sales, onProve: OnProve) = - sales.context.onProve = some onProve - proc `onClear=`*(sales: Sales, onClear: OnClear) = sales.context.onClear = some onClear @@ -58,8 +55,6 @@ proc `onSale=`*(sales: Sales, callback: OnSale) = proc onStore*(sales: Sales): ?OnStore = sales.context.onStore -proc onProve*(sales: Sales): ?OnProve = sales.context.onProve - proc onClear*(sales: Sales): ?OnClear = sales.context.onClear proc onSale*(sales: Sales): ?OnSale = sales.context.onSale diff --git a/codex/sales/salescontext.nim b/codex/sales/salescontext.nim index e81010c4..092ae277 100644 --- a/codex/sales/salescontext.nim +++ b/codex/sales/salescontext.nim @@ -9,7 +9,6 @@ type market*: Market clock*: Clock onStore*: ?OnStore - onProve*: ?OnProve onClear*: ?OnClear onSale*: ?OnSale onSaleErrored*: ?OnSaleErrored @@ -17,8 +16,6 @@ type OnStore* = proc(request: StorageRequest, slot: UInt256, availability: ?Availability): Future[void] {.gcsafe, upraises: [].} - OnProve* = proc(request: StorageRequest, - slot: UInt256): Future[seq[byte]] {.gcsafe, upraises: [].} OnClear* = proc(availability: ?Availability,# TODO: when availability changes introduced, make availability non-optional (if we need to keep it at all) request: StorageRequest, slotIndex: UInt256) {.gcsafe, upraises: [].} diff --git a/codex/sales/states/finished.nim b/codex/sales/states/finished.nim index 3bf8d131..68d8a40f 100644 --- a/codex/sales/states/finished.nim +++ b/codex/sales/states/finished.nim @@ -23,7 +23,7 @@ method run*(state: SaleFinished, machine: Machine): Future[?State] {.async.} = if request =? data.request and slotIndex =? data.slotIndex: - context.proving.add(request.slotId(slotIndex)) + context.proving.add(Slot(request: request, slotIndex: slotIndex)) if onSale =? context.onSale: onSale(data.availability, request, slotIndex) diff --git a/codex/sales/states/proving.nim b/codex/sales/states/proving.nim index 37c9ce0d..c7694c5b 100644 --- a/codex/sales/states/proving.nim +++ b/codex/sales/states/proving.nim @@ -28,8 +28,8 @@ method run*(state: SaleProving, machine: Machine): Future[?State] {.async.} = without request =? data.request: raiseAssert "no sale request" - without onProve =? context.onProve: + without onProve =? context.proving.onProve: raiseAssert "onProve callback not set" - let proof = await onProve(request, data.slotIndex) + let proof = await onProve(Slot(request: request, slotIndex: data.slotIndex)) return some State(SaleFilling(proof: proof)) diff --git a/docs/TWOCLIENTTEST.md b/docs/TWOCLIENTTEST.md index 94678f22..2e2f715a 100644 --- a/docs/TWOCLIENTTEST.md +++ b/docs/TWOCLIENTTEST.md @@ -152,11 +152,14 @@ curl --location 'http://localhost:8080/api/codex/v1/storage/request/' \ --header 'Content-Type: application/json' \ --data '{ "reward": "0x400", - "duration": "0x78" + "duration": "0x78", + "proofProbability": "0x10" }' ``` -This creates a storage Request for `` (that you have to fill in) for duration of 2 minutes and with reward of 1024 tokens. +This creates a storage Request for `` (that you have to fill in) for +duration of 2 minutes and with reward of 1024 tokens. It expects hosts to +provide a storage proof once every 16 periods on average. It returns Request ID which you can then use to query for the Request's state as follows: diff --git a/openapi.yaml b/openapi.yaml index 2bf6eb77..9883a643 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -42,6 +42,10 @@ components: type: string description: The duration of the request in seconds as hexadecimal string + ProofProbability: + type: string + description: How often storage proofs are required as hexadecimal string + Expiry: type: string description: A timestamp as seconds since unix epoch at which this request expires if the Request does not find requested amount of nodes to host the data. @@ -114,19 +118,22 @@ components: required: - reward - duration + - proofProbability properties: duration: $ref: "#/components/schemas/Duration" reward: $ref: "#/components/schemas/Reward" + proofProbability: + $ref: "#/components/schemas/ProofProbability" nodes: type: number description: Minimal number of nodes the content should be stored on - default: 1 node + default: 1 tolerance: type: number description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost - default: 0 nodes + default: 0 StorageAsk: type: object @@ -142,8 +149,7 @@ components: duration: $ref: "#/components/schemas/Duration" proofProbability: - type: string - description: How often storage proofs are required as hexadecimal string + $ref: "#/components/schemas/ProofProbability" reward: $ref: "#/components/schemas/Reward" maxSlotLoss: diff --git a/tests/codex/sales/testsales.nim b/tests/codex/sales/testsales.nim index 61838465..e71ca014 100644 --- a/tests/codex/sales/testsales.nim +++ b/tests/codex/sales/testsales.nim @@ -47,8 +47,7 @@ suite "Sales": slot: UInt256, availability: ?Availability) {.async.} = discard - sales.onProve = proc(request: StorageRequest, - slot: UInt256): Future[seq[byte]] {.async.} = + proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = return proof await sales.start() request.expiry = (clock.now() + 42).u256 @@ -146,10 +145,9 @@ suite "Sales": test "generates proof of storage": var provingRequest: StorageRequest var provingSlot: UInt256 - sales.onProve = proc(request: StorageRequest, - slot: UInt256): Future[seq[byte]] {.async.} = - provingRequest = request - provingSlot = slot + proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = + provingRequest = slot.request + provingSlot = slot.slotIndex sales.add(availability) await market.requestStorage(request) check eventually provingRequest == request @@ -184,8 +182,7 @@ suite "Sales": test "calls onClear when storage becomes available again": # fail the proof intentionally to trigger `agent.finish(success=false)`, # which then calls the onClear callback - sales.onProve = proc(request: StorageRequest, - slot: UInt256): Future[seq[byte]] {.async.} = + proving.onProve = proc(slot: Slot): Future[seq[byte]] {.async.} = raise newException(IOError, "proof failed") var clearedAvailability: Availability var clearedRequest: StorageRequest @@ -238,7 +235,7 @@ suite "Sales": sales.add(availability) await market.requestStorage(request) check eventually proving.slots.len == 1 - check proving.slots.contains(request.slotId(soldSlotIndex)) + check proving.slots.contains(Slot(request: request, slotIndex: soldSlotIndex)) test "loads active slots from market": let me = await market.getSigner() diff --git a/tests/codex/testproving.nim b/tests/codex/testproving.nim index 0386a0b2..8a2641a6 100644 --- a/tests/codex/testproving.nim +++ b/tests/codex/testproving.nim @@ -25,87 +25,85 @@ suite "Proving": let periodicity = await proofs.periodicity() clock.advance(periodicity.seconds.truncate(int64)) - test "maintains a list of contract ids to watch": - let id1, id2 = SlotId.example + test "maintains a list of slots to watch": + let slot1, slot2 = Slot.example check proving.slots.len == 0 - proving.add(id1) - check proving.slots.contains(id1) - proving.add(id2) - check proving.slots.contains(id1) - check proving.slots.contains(id2) + proving.add(slot1) + check proving.slots.contains(slot1) + proving.add(slot2) + check proving.slots.contains(slot1) + check proving.slots.contains(slot2) - test "removes duplicate contract ids": - let id = SlotId.example - proving.add(id) - proving.add(id) + test "removes duplicate slots": + let slot = Slot.example + proving.add(slot) + proving.add(slot) check proving.slots.len == 1 test "invokes callback when proof is required": - let id = SlotId.example - proving.add(id) + let slot = Slot.example + proving.add(slot) var called: bool - proc onProofRequired(id: SlotId) = + proc onProve(slot: Slot): Future[seq[byte]] {.async.} = called = true - proving.onProofRequired = onProofRequired - proofs.setSlotState(id, SlotState.Filled) - proofs.setProofRequired(id, true) + proving.onProve = onProve + proofs.setSlotState(slot.id, SlotState.Filled) + proofs.setProofRequired(slot.id, true) await proofs.advanceToNextPeriod() check eventually called - test "callback receives id of contract for which proof is required": - let id1, id2 = SlotId.example - proving.add(id1) - proving.add(id2) - var callbackIds: seq[SlotId] - proc onProofRequired(id: SlotId) = - callbackIds.add(id) - proving.onProofRequired = onProofRequired - proofs.setSlotState(id1, SlotState.Filled) - proofs.setSlotState(id2, SlotState.Filled) - proofs.setProofRequired(id1, true) + test "callback receives slot for which proof is required": + let slot1, slot2 = Slot.example + proving.add(slot1) + proving.add(slot2) + var callbackSlots: seq[Slot] + proc onProve(slot: Slot): Future[seq[byte]] {.async.} = + callbackSlots.add(slot) + proving.onProve = onProve + proofs.setSlotState(slot1.id, SlotState.Filled) + proofs.setSlotState(slot2.id, SlotState.Filled) + proofs.setProofRequired(slot1.id, true) await proofs.advanceToNextPeriod() - check eventually callbackIds == @[id1] - proofs.setProofRequired(id1, false) - proofs.setProofRequired(id2, true) + check eventually callbackSlots == @[slot1] + proofs.setProofRequired(slot1.id, false) + proofs.setProofRequired(slot2.id, true) await proofs.advanceToNextPeriod() - check eventually callbackIds == @[id1, id2] + check eventually callbackSlots == @[slot1, slot2] test "invokes callback when proof is about to be required": - let id = SlotId.example - proving.add(id) + let slot = Slot.example + proving.add(slot) var called: bool - proc onProofRequired(id: SlotId) = + proc onProve(slot: Slot): Future[seq[byte]] {.async.} = called = true - proving.onProofRequired = onProofRequired - proofs.setProofRequired(id, false) - proofs.setProofToBeRequired(id, true) - proofs.setSlotState(id, SlotState.Filled) + proving.onProve = onProve + proofs.setProofRequired(slot.id, false) + proofs.setProofToBeRequired(slot.id, true) + proofs.setSlotState(slot.id, SlotState.Filled) await proofs.advanceToNextPeriod() check eventually called - test "stops watching when contract has ended": - let id = SlotId.example - proving.add(id) - proofs.setProofEnd(id, clock.now().u256) + test "stops watching when slot is finished": + let slot = Slot.example + proving.add(slot) + proofs.setProofEnd(slot.id, clock.now().u256) await proofs.advanceToNextPeriod() var called: bool - proc onProofRequired(id: SlotId) = + proc onProve(slot: Slot): Future[seq[byte]] {.async.} = called = true - proving.onProofRequired = onProofRequired - proofs.setProofRequired(id, true) + proving.onProve = onProve + proofs.setProofRequired(slot.id, true) await proofs.advanceToNextPeriod() - proofs.setSlotState(id, SlotState.Finished) - check eventually (not proving.slots.contains(id)) + proofs.setSlotState(slot.id, SlotState.Finished) + check eventually (not proving.slots.contains(slot)) check not called test "submits proofs": - let id = SlotId.example + let slot = Slot.example let proof = exampleProof() - await proving.submitProof(id, proof) - test "supports proof submission subscriptions": - let id = SlotId.example - let proof = exampleProof() + proving.onProve = proc (slot: Slot): Future[seq[byte]] {.async.} = + return proof var receivedIds: seq[SlotId] var receivedProofs: seq[seq[byte]] @@ -116,9 +114,11 @@ suite "Proving": let subscription = await proving.subscribeProofSubmission(onProofSubmission) - await proving.submitProof(id, proof) + proving.add(slot) + proofs.setSlotState(slot.id, SlotState.Filled) + proofs.setProofRequired(slot.id, true) + await proofs.advanceToNextPeriod() - check receivedIds == @[id] - check receivedProofs == @[proof] + check eventually receivedIds == @[slot.id] and receivedProofs == @[proof] await subscription.unsubscribe() diff --git a/tests/examples.nim b/tests/examples.nim index 9a30f399..6b2894b8 100644 --- a/tests/examples.nim +++ b/tests/examples.nim @@ -47,6 +47,11 @@ proc example*(_: type StorageRequest): StorageRequest = nonce: Nonce.example ) +proc example*(_: type Slot): Slot = + let request = StorageRequest.example + let slotIndex = rand(request.ask.slots.int).u256 + Slot(request: request, slotIndex: slotIndex) + proc exampleProof*(): seq[byte] = var proof: seq[byte] while proof.len == 0: diff --git a/tests/integration/codexclient.nim b/tests/integration/codexclient.nim new file mode 100644 index 00000000..a3edc63f --- /dev/null +++ b/tests/integration/codexclient.nim @@ -0,0 +1,73 @@ +import std/httpclient +import std/json +import std/strutils +import pkg/stint +import pkg/questionable/results + +type CodexClient* = ref object + http: HttpClient + baseurl: string + +proc new*(_: type CodexClient, baseurl: string): CodexClient = + CodexClient(http: newHttpClient(), baseurl: baseurl) + +proc info*(client: CodexClient): JsonNode = + let url = client.baseurl & "/debug/info" + client.http.getContent(url).parseJson() + +proc setLogLevel*(client: CodexClient, level: string) = + let url = client.baseurl & "/debug/chronicles/loglevel?level=" & level + let headers = newHttpHeaders({"Content-Type": "text/plain"}) + let response = client.http.request(url, httpMethod=HttpPost, headers=headers) + assert response.status == "200 OK" + +proc upload*(client: CodexClient, contents: string): string = + let response = client.http.post(client.baseurl & "/upload", contents) + assert response.status == "200 OK" + response.body + +proc requestStorage*(client: CodexClient, + cid: string, + duration: uint64, + reward: uint64, + proofProbability: uint64, + expiry: UInt256): string = + let url = client.baseurl & "/storage/request/" & cid + let json = %*{ + "duration": "0x" & duration.toHex, + "reward": "0x" & reward.toHex, + "proofProbability": "0x" & proofProbability.toHex, + "expiry": "0x" & expiry.toHex + } + let response = client.http.post(url, $json) + assert response.status == "200 OK" + response.body + +proc getPurchase*(client: CodexClient, purchase: string): JsonNode = + let url = client.baseurl & "/storage/purchases/" & purchase + let body = client.http.getContent(url) + parseJson(body).catch |? nil + +proc postAvailability*(client: CodexClient, + size, duration, minPrice: uint64): JsonNode = + let url = client.baseurl & "/sales/availability" + let json = %*{ + "size": "0x" & size.toHex, + "duration": "0x" & duration.toHex, + "minPrice": "0x" & minPrice.toHex + } + let response = client.http.post(url, $json) + assert response.status == "200 OK" + parseJson(response.body) + +proc getAvailabilities*(client: CodexClient): JsonNode = + let url = client.baseurl & "/sales/availability" + let body = client.http.getContent(url) + parseJson(body) + +proc close*(client: CodexClient) = + client.http.close() + +proc restart*(client: CodexClient) = + client.http.close() + client.http = newHttpClient() diff --git a/tests/integration/testIntegration.nim b/tests/integration/testIntegration.nim index 307bf06d..bdbcfb25 100644 --- a/tests/integration/testIntegration.nim +++ b/tests/integration/testIntegration.nim @@ -1,156 +1,75 @@ -import std/os -import std/httpclient import std/json -import std/strutils import pkg/chronos -import ../ethertest import ../contracts/time import ../codex/helpers/eventually -import ./nodes +import ./twonodes import ./tokens -ethersuite "Integration tests": - - var node1, node2: NodeProcess - var baseurl1, baseurl2: string - var client: HttpClient - - let dataDir1 = getTempDir() / "Codex1" - let dataDir2 = getTempDir() / "Codex2" +twonodessuite "Integration tests", debug1 = false, debug2 = false: setup: await provider.getSigner(accounts[0]).mint() await provider.getSigner(accounts[1]).mint() await provider.getSigner(accounts[1]).deposit() - baseurl1 = "http://localhost:8080/api/codex/v1" - baseurl2 = "http://localhost:8081/api/codex/v1" - client = newHttpClient() - - node1 = startNode([ - "--api-port=8080", - "--data-dir=" & dataDir1, - "--nat=127.0.0.1", - "--disc-ip=127.0.0.1", - "--disc-port=8090", - "--persistence", - "--eth-account=" & $accounts[0] - ], debug = false) - - let - bootstrap = strip( - $(parseJson(client.get(baseurl1 & "/debug/info").body)["spr"]), - chars = {'"'}) - - node2 = startNode([ - "--api-port=8081", - "--data-dir=" & dataDir2, - "--nat=127.0.0.1", - "--disc-ip=127.0.0.1", - "--disc-port=8091", - "--bootstrap-node=" & bootstrap, - "--persistence", - "--eth-account=" & $accounts[1] - ], debug = false) - - teardown: - client.close() - node1.stop() - node2.stop() - - dataDir1.removeDir() - dataDir2.removeDir() - test "nodes can print their peer information": - let info1 = client.get(baseurl1 & "/debug/info").body - let info2 = client.get(baseurl2 & "/debug/info").body - check info1 != info2 + check client1.info() != client2.info() - test "nodes should set chronicles log level": - client.headers = newHttpHeaders({ "Content-Type": "text/plain" }) - let filter = "/debug/chronicles/loglevel?level=DEBUG;TRACE:codex" - check client.request(baseurl1 & filter, httpMethod = HttpPost, body = "").status == "200 OK" + test "nodes can set chronicles log level": + client1.setLogLevel("DEBUG;TRACE:codex") test "node accepts file uploads": - let url = baseurl1 & "/upload" - let response = client.post(url, "some file contents") - check response.status == "200 OK" + let cid1 = client1.upload("some file contents") + let cid2 = client1.upload("some other contents") + check cid1 != cid2 test "node handles new storage availability": - let url = baseurl1 & "/sales/availability" - let json = %*{"size": "0x1", "duration": "0x2", "minPrice": "0x3"} - check client.post(url, $json).status == "200 OK" + let availability1 = client1.postAvailability(size=1, duration=2, minPrice=3) + let availability2 = client1.postAvailability(size=4, duration=5, minPrice=6) + check availability1 != availability2 test "node lists storage that is for sale": - let url = baseurl1 & "/sales/availability" - let json = %*{"size": "0x1", "duration": "0x2", "minPrice": "0x3"} - let availability = parseJson(client.post(url, $json).body) - let response = client.get(url) - check response.status == "200 OK" - check %*availability in parseJson(response.body) + let availability = client1.postAvailability(size=1, duration=2, minPrice=3) + check availability in client1.getAvailabilities() test "node handles storage request": - let cid = client.post(baseurl1 & "/upload", "some file contents").body - let url = baseurl1 & "/storage/request/" & cid - let json = %*{"duration": "0x1", "reward": "0x2"} - let response = client.post(url, $json) - check response.status == "200 OK" + let expiry = (await provider.currentTime()) + 30 + let cid = client1.upload("some file contents") + let id1 = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry) + let id2 = client1.requestStorage(cid, duration=4, reward=5, proofProbability=6, expiry=expiry) + check id1 != id2 test "node retrieves purchase status": - 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 - let response = client.get(baseurl1 & "/storage/purchases/" & id) - check response.status == "200 OK" - let json = parseJson(response.body) - check json["request"]["ask"]["duration"].getStr == "0x1" - check json["request"]["ask"]["reward"].getStr == "0x2" + let expiry = (await provider.currentTime()) + 30 + let cid = client1.upload("some file contents") + let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry) + let purchase = client1.getPurchase(id) + check purchase{"request"}{"ask"}{"duration"} == %"0x1" + check purchase{"request"}{"ask"}{"reward"} == %"0x2" + check purchase{"request"}{"ask"}{"proofProbability"} == %"0x3" 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" + let expiry = (await provider.currentTime()) + 30 + let cid = client1.upload("some file contents") + let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry) + check eventually client1.getPurchase(id){"state"}.getStr() == "submitted" node1.restart() + client1.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" + check eventually (not isNil client1.getPurchase(id){"request"}{"ask"}) + check client1.getPurchase(id){"request"}{"ask"}{"duration"} == %"0x1" + check client1.getPurchase(id){"request"}{"ask"}{"reward"} == %"0x2" test "nodes negotiate contracts on the marketplace": - proc sell = - let json = %*{"size": "0xFFFFF", "duration": "0x200", "minPrice": "0x300"} - discard client.post(baseurl2 & "/sales/availability", $json) + # client 2 makes storage available + discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300) - proc available: JsonNode = - client.get(baseurl2 & "/sales/availability").body.parseJson + # client 1 requests storage + let expiry = (await provider.currentTime()) + 30 + let cid = client1.upload("some file contents") + let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry) - proc upload: string = - client.post(baseurl1 & "/upload", "some file contents").body - - proc buy(cid: string): string = - let expiry = ((waitFor provider.currentTime()) + 30).toHex - 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["state"].getStr == "finished": return json - await sleepAsync(1.seconds) - - sell() - let purchase = waitFor upload().buy().finish() - - check purchase["error"].getStr == "" - check available().len == 0 + check eventually client1.getPurchase(purchase){"state"} == %"started" + check client1.getPurchase(purchase){"error"} == newJNull() + check client2.getAvailabilities().len == 0 diff --git a/tests/integration/testblockexpiration.nim b/tests/integration/testblockexpiration.nim index 462aa26b..bc0dcf4e 100644 --- a/tests/integration/testblockexpiration.nim +++ b/tests/integration/testblockexpiration.nim @@ -29,7 +29,7 @@ ethersuite "Node block expiration tests": "--disc-ip=127.0.0.1", "--disc-port=8090", "--block-ttl=" & $blockTtlSeconds, - "--block-mi=3", + "--block-mi=1", "--block-mn=10" ], debug = false) @@ -49,11 +49,11 @@ ethersuite "Node block expiration tests": content test "node retains not-expired file": - startTestNode(blockTtlSeconds = 60 * 60 * 1) + startTestNode(blockTtlSeconds = 10) let contentId = uploadTestFile() - await sleepAsync(10.seconds) + await sleepAsync(2.seconds) let response = downloadTestFile(contentId) check: @@ -61,11 +61,11 @@ ethersuite "Node block expiration tests": response.body == content test "node deletes expired file": - startTestNode(blockTtlSeconds = 5) + startTestNode(blockTtlSeconds = 1) let contentId = uploadTestFile() - await sleepAsync(10.seconds) + await sleepAsync(2.seconds) expect TimeoutError: discard downloadTestFile(contentId) diff --git a/tests/integration/testproofs.nim b/tests/integration/testproofs.nim new file mode 100644 index 00000000..06962571 --- /dev/null +++ b/tests/integration/testproofs.nim @@ -0,0 +1,43 @@ +import codex/contracts/marketplace +import codex/contracts/deployment +import ../contracts/time +import ../codex/helpers/eventually +import ./twonodes +import ./tokens + +twonodessuite "Proving integration test", debug1=false, debug2=false: + + var marketplace: Marketplace + var config: MarketplaceConfig + + setup: + marketplace = Marketplace.new(!deployment().address(Marketplace), provider) + config = await marketplace.config() + await provider.getSigner(accounts[0]).mint() + await provider.getSigner(accounts[1]).mint() + await provider.getSigner(accounts[1]).deposit() + + proc waitUntilPurchaseIsStarted {.async.} = + discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300) + let expiry = (await provider.currentTime()) + 30 + let cid = client1.upload("some file contents") + let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry) + check eventually client1.getPurchase(purchase){"state"} == %"started" + + test "hosts submit periodic proofs for slots they fill": + await waitUntilPurchaseIsStarted() + + var proofWasSubmitted = false + proc onProofSubmitted(event: ProofSubmitted) = + proofWasSubmitted = true + let subscription = await marketplace.subscribe(ProofSubmitted, onProofSubmitted) + + for _ in 0..<100: + if proofWasSubmitted: + break + else: + await provider.advanceTime(config.proofs.period) + await sleepAsync(1.seconds) + + check proofWasSubmitted + await subscription.unsubscribe() diff --git a/tests/integration/twonodes.nim b/tests/integration/twonodes.nim new file mode 100644 index 00000000..306a8af1 --- /dev/null +++ b/tests/integration/twonodes.nim @@ -0,0 +1,62 @@ +import std/os +import std/macros +import std/json +import std/httpclient +import ../ethertest +import ./codexclient +import ./nodes + +export ethertest +export codexclient +export nodes + +template twonodessuite*(name: string, debug1, debug2: bool, body) = + + ethersuite name: + + var node1 {.inject, used.}: NodeProcess + var node2 {.inject, used.}: NodeProcess + var client1 {.inject, used.}: CodexClient + var client2 {.inject, used.}: CodexClient + + let dataDir1 = getTempDir() / "Codex1" + let dataDir2 = getTempDir() / "Codex2" + + setup: + client1 = CodexClient.new("http://localhost:8080/api/codex/v1") + client2 = CodexClient.new("http://localhost:8081/api/codex/v1") + + node1 = startNode([ + "--api-port=8080", + "--data-dir=" & dataDir1, + "--nat=127.0.0.1", + "--disc-ip=127.0.0.1", + "--disc-port=8090", + "--persistence", + "--eth-account=" & $accounts[0] + ], debug = debug1) + + let bootstrap = client1.info()["spr"].getStr() + + node2 = startNode([ + "--api-port=8081", + "--data-dir=" & dataDir2, + "--nat=127.0.0.1", + "--disc-ip=127.0.0.1", + "--disc-port=8091", + "--bootstrap-node=" & bootstrap, + "--persistence", + "--eth-account=" & $accounts[1] + ], debug = debug2) + + teardown: + client1.close() + client2.close() + + node1.stop() + node2.stop() + + removeDir(dataDir1) + removeDir(dataDir2) + + body diff --git a/tests/testIntegration.nim b/tests/testIntegration.nim index baba8565..8a3bf4f1 100644 --- a/tests/testIntegration.nim +++ b/tests/testIntegration.nim @@ -1,4 +1,5 @@ import ./integration/testIntegration import ./integration/testblockexpiration +import ./integration/testproofs {.warning[UnusedImport]:off.}