From 372f827982bf8d9a4f1a757715556de24ad46ee4 Mon Sep 17 00:00:00 2001 From: Eric Mastro Date: Wed, 17 Aug 2022 14:02:53 +1000 Subject: [PATCH] [marketplace] add/remove proofs for contract state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add or remove proof requirements when a request contract’s state changes. When a request sale has completed (for a slot), the host who purchased that slot now must provide regular proofs for the data they are contracted to hold. This is now enforced by adding the slotId to the HashSet of Ids for which to require proofs. When a request has been cancelled (not all slots were filled before the request expired), proofs no longer need to be provided and the slotId is removed from teh HashSet. Add `isCancelled` and `isSlotCancelled` checks to query the contract state without relying the on the state context variable in the contract. Because contract state can only be updated in a transaction, and the client withdrawing funds is responsible for changing the contract state to “Cancelled”, the `isCancelled` and `isSlotCancelled` functions were introduced to check the state regardless of whether or not the client had already withdrawn their funds. --- codex/contracts/interactions.nim | 5 ++-- codex/contracts/proofs.nim | 8 +++++++ codex/contracts/storage.nim | 2 ++ codex/node.nim | 5 +++- codex/proving.nim | 8 +++++++ codex/sales.nim | 33 +++++++++++++++++++-------- codex/storageproofs/timing/proofs.nim | 8 +++++++ tests/codex/helpers/mockproofs.nim | 15 ++++++++++++ tests/codex/testproving.nim | 14 ++++++++++++ tests/codex/testsales.nim | 28 +++++++++++++++++++++-- tests/contracts/matchers.nim | 26 +++++++++++++++++++++ tests/contracts/testContracts.nim | 19 +++++++++++++++ 12 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 tests/contracts/matchers.nim diff --git a/codex/contracts/interactions.nim b/codex/contracts/interactions.nim index 711b4f9f..d6d75dda 100644 --- a/codex/contracts/interactions.nim +++ b/codex/contracts/interactions.nim @@ -33,10 +33,11 @@ proc new*(_: type ContractInteractions, let market = OnChainMarket.new(contract) let proofs = OnChainProofs.new(contract) let clock = OnChainClock.new(signer.provider) + let proving = Proving.new(proofs, clock) some ContractInteractions( purchasing: Purchasing.new(market, clock), - sales: Sales.new(market, clock), - proving: Proving.new(proofs, clock), + sales: Sales.new(market, clock, proving), + proving: proving, clock: clock ) diff --git a/codex/contracts/proofs.nim b/codex/contracts/proofs.nim index 81ee8b68..b28262d8 100644 --- a/codex/contracts/proofs.nim +++ b/codex/contracts/proofs.nim @@ -22,6 +22,14 @@ method periodicity*(proofs: OnChainProofs): Future[Periodicity] {.async.} = let period = await proofs.storage.proofPeriod() return Periodicity(seconds: period) +method isSlotCancelled*(proofs: OnChainProofs, + id: ContractId): Future[bool] {.async.} = + return await proofs.storage.isSlotCancelled(id) + +method isCancelled*(proofs: OnChainProofs, + id: array[32, byte]): Future[bool] {.async.} = + return await proofs.storage.isCancelled(id) + method isProofRequired*(proofs: OnChainProofs, id: SlotId): Future[bool] {.async.} = return await proofs.storage.isProofRequired(id) diff --git a/codex/contracts/storage.nim b/codex/contracts/storage.nim index d04f25e1..f6d529b9 100644 --- a/codex/contracts/storage.nim +++ b/codex/contracts/storage.nim @@ -45,6 +45,8 @@ proc proofTimeout*(storage: Storage): UInt256 {.contract, view.} proc proofEnd*(storage: Storage, id: SlotId): UInt256 {.contract, view.} proc missingProofs*(storage: Storage, id: SlotId): UInt256 {.contract, view.} +proc isCancelled*(storage: Storage, id: Id): bool {.contract, view.} +proc isSlotCancelled*(storage: Storage, id: Id): bool {.contract, view.} proc isProofRequired*(storage: Storage, id: SlotId): bool {.contract, view.} proc willProofBeRequired*(storage: Storage, id: SlotId): bool {.contract, view.} proc getChallenge*(storage: Storage, id: SlotId): array[32, byte] {.contract, view.} diff --git a/codex/node.nim b/codex/node.nim index b2d02238..5b4c5b90 100644 --- a/codex/node.nim +++ b/codex/node.nim @@ -343,9 +343,12 @@ proc start*(node: CodexNodeRef) {.async.} = if fetchRes.isErr: raise newException(CodexError, "Unable to retrieve blocks") - contracts.sales.onClear = proc(availability: Availability, request: StorageRequest) = + contracts.sales.onClear = proc(availability: Availability, + request: StorageRequest, + slotIndex: UInt256) = # TODO: remove data from local storage discard + contracts.sales.onProve = proc(request: StorageRequest, slot: UInt256): Future[seq[byte]] {.async.} = # TODO: generate proof diff --git a/codex/proving.nim b/codex/proving.nim index 05b88de2..f15b8730 100644 --- a/codex/proving.nim +++ b/codex/proving.nim @@ -42,11 +42,19 @@ proc removeEndedContracts(proving: Proving) {.async.} = ended.incl(id) proving.slots.excl(ended) +proc removeCancelledContracts(proving: Proving) {.async.} = + var cancelled: HashSet[ContractId] + for id in proving.contracts: + if (await proving.proofs.isSlotCancelled(id)): + cancelled.incl(id) + proving.contracts.excl(cancelled) + proc run(proving: Proving) {.async.} = try: while true: let currentPeriod = await proving.getCurrentPeriod() await proving.removeEndedContracts() + await proving.removeCancelledContracts() for id in proving.slots: if (await proving.proofs.isProofRequired(id)) or (await proving.proofs.willProofBeRequired(id)): diff --git a/codex/sales.nim b/codex/sales.nim index 0e177d56..4758e1a1 100644 --- a/codex/sales.nim +++ b/codex/sales.nim @@ -7,6 +7,8 @@ import pkg/chronicles import ./rng import ./market import ./clock +import ./proving +import ./contracts/requests ## Sales holds a list of available storage that it may sell. ## @@ -32,12 +34,13 @@ type Sales* = ref object market: Market clock: Clock - subscription: ?Subscription + subscription: ?market.Subscription available*: seq[Availability] onStore: ?OnStore onProve: ?OnProve onClear: ?OnClear onSale: ?OnSale + proving: Proving Availability* = object id*: array[32, byte] size*: UInt256 @@ -50,7 +53,7 @@ type availability: Availability request: ?StorageRequest slotIndex: ?UInt256 - subscription: ?Subscription + subscription: ?market.Subscription running: ?Future[void] waiting: ?Future[void] finished: bool @@ -59,15 +62,21 @@ type availability: Availability): Future[void] {.gcsafe, upraises: [].} OnProve = proc(request: StorageRequest, slot: UInt256): Future[seq[byte]] {.gcsafe, upraises: [].} - OnClear = proc(availability: Availability, request: StorageRequest) {.gcsafe, upraises: [].} + OnClear = proc(availability: Availability, + request: StorageRequest, + slotIndex: UInt256) {.gcsafe, upraises: [].} OnSale = proc(availability: Availability, request: StorageRequest, slotIndex: UInt256) {.gcsafe, upraises: [].} -func new*(_: type Sales, market: Market, clock: Clock): Sales = +func new*(_: type Sales, + market: Market, + clock: Clock, + proving: Proving): Sales = Sales( market: market, clock: clock, + proving: proving ) proc init*(_: type Availability, @@ -119,13 +128,17 @@ proc finish(agent: SalesAgent, success: bool) = waiting.cancel() if success: - if onSale =? agent.sales.onSale and + if request =? agent.request and + slotIndex =? agent.slotIndex: + agent.sales.proving.add(request.slotId(slotIndex)) + + if onSale =? agent.sales.onSale: + onSale(agent.availability, request, slotIndex) + else: + if onClear =? agent.sales.onClear and request =? agent.request and slotIndex =? agent.slotIndex: - onSale(agent.availability, request, slotIndex) - else: - if onClear =? agent.sales.onClear and request =? agent.request: - onClear(agent.availability, request) + onClear(agent.availability, request, slotIndex) agent.sales.add(agent.availability) proc selectSlot(agent: SalesAgent) = @@ -222,7 +235,7 @@ proc start*(sales: Sales) {.async.} = proc stop*(sales: Sales) {.async.} = if subscription =? sales.subscription: - sales.subscription = Subscription.none + sales.subscription = market.Subscription.none try: await subscription.unsubscribe() except CatchableError as e: diff --git a/codex/storageproofs/timing/proofs.nim b/codex/storageproofs/timing/proofs.nim index 6995a346..74afb16a 100644 --- a/codex/storageproofs/timing/proofs.nim +++ b/codex/storageproofs/timing/proofs.nim @@ -18,6 +18,14 @@ method periodicity*(proofs: Proofs): Future[Periodicity] {.base, async.} = raiseAssert("not implemented") +method isSlotCancelled*(proofs: Proofs, + id: ContractId): Future[bool] {.base, async.} = + raiseAssert("not implemented") + +method isCancelled*(proofs: Proofs, + id: array[32, byte]): Future[bool] {.base, async.} = + raiseAssert("not implemented") + method isProofRequired*(proofs: Proofs, id: SlotId): Future[bool] {.base, async.} = raiseAssert("not implemented") diff --git a/tests/codex/helpers/mockproofs.nim b/tests/codex/helpers/mockproofs.nim index 25bfc6ee..cce22f93 100644 --- a/tests/codex/helpers/mockproofs.nim +++ b/tests/codex/helpers/mockproofs.nim @@ -7,6 +7,7 @@ import pkg/codex/storageproofs type MockProofs* = ref object of Proofs periodicity: Periodicity + cancelledRequests: HashSet[ContractId] proofsRequired: HashSet[SlotId] proofsToBeRequired: HashSet[SlotId] proofEnds: Table[SlotId, UInt256] @@ -32,6 +33,20 @@ proc setProofRequired*(mock: MockProofs, id: SlotId, required: bool) = else: mock.proofsRequired.excl(id) +proc setCancelled*(mock: MockProofs, id: ContractId, required: bool) = + if required: + mock.cancelledRequests.incl(id) + else: + mock.cancelledRequests.excl(id) + +method isCancelled*(mock: MockProofs, + id: array[32, byte]): Future[bool] {.async.} = + return mock.cancelledRequests.contains(id) + +method isSlotCancelled*(mock: MockProofs, + id: ContractId): Future[bool] {.async.} = + return mock.cancelledRequests.contains(id) + method isProofRequired*(mock: MockProofs, id: SlotId): Future[bool] {.async.} = return mock.proofsRequired.contains(id) diff --git a/tests/codex/testproving.nim b/tests/codex/testproving.nim index a47c4a2e..03b7b885 100644 --- a/tests/codex/testproving.nim +++ b/tests/codex/testproving.nim @@ -92,6 +92,20 @@ suite "Proving": await proofs.advanceToNextPeriod() check not called + test "stops watching when contract is cancelled": + let id = ContractId.example + proving.add(id) + var called: bool + proc onProofRequired(id: ContractId) = + called = true + proofs.setProofRequired(id, true) + await proofs.advanceToNextPeriod() + proving.onProofRequired = onProofRequired + proofs.setCancelled(id, true) + await proofs.advanceToNextPeriod() + check not proving.contracts.contains(id) + check not called + test "submits proofs": let id = SlotId.example let proof = seq[byte].example diff --git a/tests/codex/testsales.nim b/tests/codex/testsales.nim index 26d3aefb..d41142f7 100644 --- a/tests/codex/testsales.nim +++ b/tests/codex/testsales.nim @@ -1,5 +1,8 @@ +import std/sets import pkg/asynctest import pkg/chronos +import pkg/codex/contracts/requests +import pkg/codex/proving import pkg/codex/sales import ./helpers/mockmarket import ./helpers/mockclock @@ -28,11 +31,13 @@ suite "Sales": var sales: Sales var market: MockMarket var clock: MockClock + var proving: Proving setup: market = MockMarket.new() clock = MockClock.new() - sales = Sales.new(market, clock) + proving = Proving.new() + sales = Sales.new(market, clock, proving) sales.onStore = proc(request: StorageRequest, slot: UInt256, availability: Availability) {.async.} = @@ -151,18 +156,25 @@ suite "Sales": check soldSlotIndex < request.ask.slots.u256 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.} = raise newException(IOError, "proof failed") var clearedAvailability: Availability var clearedRequest: StorageRequest - sales.onClear = proc(availability: Availability, request: StorageRequest) = + var clearedSlotIndex: UInt256 + sales.onClear = proc(availability: Availability, + request: StorageRequest, + slotIndex: UInt256) = clearedAvailability = availability clearedRequest = request + clearedSlotIndex = slotIndex sales.add(availability) discard await market.requestStorage(request) check clearedAvailability == availability check clearedRequest == request + check clearedSlotIndex < request.ask.slots.u256 test "makes storage available again when other host fills the slot": let otherHost = Address.example @@ -186,3 +198,15 @@ suite "Sales": clock.set(request.expiry.truncate(int64)) await sleepAsync(2.seconds) check sales.available == @[availability] + + test "adds proving for slot when slot is filled": + var soldSlotIndex: UInt256 + sales.onSale = proc(availability: Availability, + request: StorageRequest, + slotIndex: UInt256) = + soldSlotIndex = slotIndex + check proving.contracts.len == 0 + sales.add(availability) + discard await market.requestStorage(request) + check proving.contracts.len == 1 + check proving.contracts.contains(request.slotId(soldSlotIndex)) diff --git a/tests/contracts/matchers.nim b/tests/contracts/matchers.nim new file mode 100644 index 00000000..647712bd --- /dev/null +++ b/tests/contracts/matchers.nim @@ -0,0 +1,26 @@ +import std/json +import std/strutils +import pkg/asynctest +import pkg/ethers + +proc revertReason*(e: ref ValueError): string = + try: + let json = parseJson(e.msg) + var msg = json{"message"}.getStr + const revertPrefix = + "Error: VM Exception while processing transaction: reverted with " & + "reason string " + msg = msg.replace(revertPrefix) + msg = msg.replace("\'", "") + return msg + except JsonParsingError: + return "" + + +template revertsWith*(reason, body) = + var revertReason = "" + try: + body + except ValueError as e: + revertReason = e.revertReason + check revertReason == reason \ No newline at end of file diff --git a/tests/contracts/testContracts.nim b/tests/contracts/testContracts.nim index 6e4eff70..d0daefdf 100644 --- a/tests/contracts/testContracts.nim +++ b/tests/contracts/testContracts.nim @@ -5,6 +5,7 @@ import codex/contracts/testtoken import codex/storageproofs import ../ethertest import ./examples +import ./matchers import ./time ethersuite "Storage contracts": @@ -74,3 +75,21 @@ ethersuite "Storage contracts": switchAccount(host) await provider.advanceTimeTo(await storage.proofEnd(slotId)) await storage.payoutSlot(request.id, 0.u256) + + test "a request is cancelled after expiry": + check not await storage.isCancelled(request.id) + await provider.advanceTimeTo(request.expiry + 1) + check await storage.isCancelled(request.id) + + test "a slot is cancelled after expiry": + check not await storage.isSlotCancelled(id) + await provider.advanceTimeTo(request.expiry + 1) + check await storage.isSlotCancelled(id) + + test "cannot mark proofs missing for cancelled request": + await provider.advanceTimeTo(request.expiry + 1) + switchAccount(client) + let missingPeriod = periodicity.periodOf(await provider.currentTime()) + await provider.advanceTime(periodicity.seconds) + revertsWith "Request was cancelled": + await storage.markProofAsMissing(id, missingPeriod)