[marketplace] add/remove proofs for contract state

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.
This commit is contained in:
Eric Mastro 2022-08-17 14:02:53 +10:00 committed by Eric Mastro
parent 0c3fbad470
commit 372f827982
12 changed files with 156 additions and 15 deletions

View File

@ -33,10 +33,11 @@ proc new*(_: type ContractInteractions,
let market = OnChainMarket.new(contract) let market = OnChainMarket.new(contract)
let proofs = OnChainProofs.new(contract) let proofs = OnChainProofs.new(contract)
let clock = OnChainClock.new(signer.provider) let clock = OnChainClock.new(signer.provider)
let proving = Proving.new(proofs, clock)
some ContractInteractions( some ContractInteractions(
purchasing: Purchasing.new(market, clock), purchasing: Purchasing.new(market, clock),
sales: Sales.new(market, clock), sales: Sales.new(market, clock, proving),
proving: Proving.new(proofs, clock), proving: proving,
clock: clock clock: clock
) )

View File

@ -22,6 +22,14 @@ method periodicity*(proofs: OnChainProofs): Future[Periodicity] {.async.} =
let period = await proofs.storage.proofPeriod() let period = await proofs.storage.proofPeriod()
return Periodicity(seconds: period) 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, method isProofRequired*(proofs: OnChainProofs,
id: SlotId): Future[bool] {.async.} = id: SlotId): Future[bool] {.async.} =
return await proofs.storage.isProofRequired(id) return await proofs.storage.isProofRequired(id)

View File

@ -45,6 +45,8 @@ proc proofTimeout*(storage: Storage): UInt256 {.contract, view.}
proc proofEnd*(storage: Storage, id: SlotId): UInt256 {.contract, view.} proc proofEnd*(storage: Storage, id: SlotId): UInt256 {.contract, view.}
proc missingProofs*(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 isProofRequired*(storage: Storage, id: SlotId): bool {.contract, view.}
proc willProofBeRequired*(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.} proc getChallenge*(storage: Storage, id: SlotId): array[32, byte] {.contract, view.}

View File

@ -343,9 +343,12 @@ proc start*(node: CodexNodeRef) {.async.} =
if fetchRes.isErr: if fetchRes.isErr:
raise newException(CodexError, "Unable to retrieve blocks") 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 # TODO: remove data from local storage
discard discard
contracts.sales.onProve = proc(request: StorageRequest, contracts.sales.onProve = proc(request: StorageRequest,
slot: UInt256): Future[seq[byte]] {.async.} = slot: UInt256): Future[seq[byte]] {.async.} =
# TODO: generate proof # TODO: generate proof

View File

@ -42,11 +42,19 @@ proc removeEndedContracts(proving: Proving) {.async.} =
ended.incl(id) ended.incl(id)
proving.slots.excl(ended) 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.} = proc run(proving: Proving) {.async.} =
try: try:
while true: while true:
let currentPeriod = await proving.getCurrentPeriod() let currentPeriod = await proving.getCurrentPeriod()
await proving.removeEndedContracts() await proving.removeEndedContracts()
await proving.removeCancelledContracts()
for id in proving.slots: for id in proving.slots:
if (await proving.proofs.isProofRequired(id)) or if (await proving.proofs.isProofRequired(id)) or
(await proving.proofs.willProofBeRequired(id)): (await proving.proofs.willProofBeRequired(id)):

View File

@ -7,6 +7,8 @@ import pkg/chronicles
import ./rng import ./rng
import ./market import ./market
import ./clock import ./clock
import ./proving
import ./contracts/requests
## Sales holds a list of available storage that it may sell. ## Sales holds a list of available storage that it may sell.
## ##
@ -32,12 +34,13 @@ type
Sales* = ref object Sales* = ref object
market: Market market: Market
clock: Clock clock: Clock
subscription: ?Subscription subscription: ?market.Subscription
available*: seq[Availability] available*: seq[Availability]
onStore: ?OnStore onStore: ?OnStore
onProve: ?OnProve onProve: ?OnProve
onClear: ?OnClear onClear: ?OnClear
onSale: ?OnSale onSale: ?OnSale
proving: Proving
Availability* = object Availability* = object
id*: array[32, byte] id*: array[32, byte]
size*: UInt256 size*: UInt256
@ -50,7 +53,7 @@ type
availability: Availability availability: Availability
request: ?StorageRequest request: ?StorageRequest
slotIndex: ?UInt256 slotIndex: ?UInt256
subscription: ?Subscription subscription: ?market.Subscription
running: ?Future[void] running: ?Future[void]
waiting: ?Future[void] waiting: ?Future[void]
finished: bool finished: bool
@ -59,15 +62,21 @@ type
availability: Availability): Future[void] {.gcsafe, upraises: [].} availability: Availability): Future[void] {.gcsafe, upraises: [].}
OnProve = proc(request: StorageRequest, OnProve = proc(request: StorageRequest,
slot: UInt256): Future[seq[byte]] {.gcsafe, upraises: [].} 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, OnSale = proc(availability: Availability,
request: StorageRequest, request: StorageRequest,
slotIndex: UInt256) {.gcsafe, upraises: [].} 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( Sales(
market: market, market: market,
clock: clock, clock: clock,
proving: proving
) )
proc init*(_: type Availability, proc init*(_: type Availability,
@ -119,13 +128,17 @@ proc finish(agent: SalesAgent, success: bool) =
waiting.cancel() waiting.cancel()
if success: 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 request =? agent.request and
slotIndex =? agent.slotIndex: slotIndex =? agent.slotIndex:
onSale(agent.availability, request, slotIndex) onClear(agent.availability, request, slotIndex)
else:
if onClear =? agent.sales.onClear and request =? agent.request:
onClear(agent.availability, request)
agent.sales.add(agent.availability) agent.sales.add(agent.availability)
proc selectSlot(agent: SalesAgent) = proc selectSlot(agent: SalesAgent) =
@ -222,7 +235,7 @@ proc start*(sales: Sales) {.async.} =
proc stop*(sales: Sales) {.async.} = proc stop*(sales: Sales) {.async.} =
if subscription =? sales.subscription: if subscription =? sales.subscription:
sales.subscription = Subscription.none sales.subscription = market.Subscription.none
try: try:
await subscription.unsubscribe() await subscription.unsubscribe()
except CatchableError as e: except CatchableError as e:

View File

@ -18,6 +18,14 @@ method periodicity*(proofs: Proofs):
Future[Periodicity] {.base, async.} = Future[Periodicity] {.base, async.} =
raiseAssert("not implemented") 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, method isProofRequired*(proofs: Proofs,
id: SlotId): Future[bool] {.base, async.} = id: SlotId): Future[bool] {.base, async.} =
raiseAssert("not implemented") raiseAssert("not implemented")

View File

@ -7,6 +7,7 @@ import pkg/codex/storageproofs
type type
MockProofs* = ref object of Proofs MockProofs* = ref object of Proofs
periodicity: Periodicity periodicity: Periodicity
cancelledRequests: HashSet[ContractId]
proofsRequired: HashSet[SlotId] proofsRequired: HashSet[SlotId]
proofsToBeRequired: HashSet[SlotId] proofsToBeRequired: HashSet[SlotId]
proofEnds: Table[SlotId, UInt256] proofEnds: Table[SlotId, UInt256]
@ -32,6 +33,20 @@ proc setProofRequired*(mock: MockProofs, id: SlotId, required: bool) =
else: else:
mock.proofsRequired.excl(id) 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, method isProofRequired*(mock: MockProofs,
id: SlotId): Future[bool] {.async.} = id: SlotId): Future[bool] {.async.} =
return mock.proofsRequired.contains(id) return mock.proofsRequired.contains(id)

View File

@ -92,6 +92,20 @@ suite "Proving":
await proofs.advanceToNextPeriod() await proofs.advanceToNextPeriod()
check not called 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": test "submits proofs":
let id = SlotId.example let id = SlotId.example
let proof = seq[byte].example let proof = seq[byte].example

View File

@ -1,5 +1,8 @@
import std/sets
import pkg/asynctest import pkg/asynctest
import pkg/chronos import pkg/chronos
import pkg/codex/contracts/requests
import pkg/codex/proving
import pkg/codex/sales import pkg/codex/sales
import ./helpers/mockmarket import ./helpers/mockmarket
import ./helpers/mockclock import ./helpers/mockclock
@ -28,11 +31,13 @@ suite "Sales":
var sales: Sales var sales: Sales
var market: MockMarket var market: MockMarket
var clock: MockClock var clock: MockClock
var proving: Proving
setup: setup:
market = MockMarket.new() market = MockMarket.new()
clock = MockClock.new() clock = MockClock.new()
sales = Sales.new(market, clock) proving = Proving.new()
sales = Sales.new(market, clock, proving)
sales.onStore = proc(request: StorageRequest, sales.onStore = proc(request: StorageRequest,
slot: UInt256, slot: UInt256,
availability: Availability) {.async.} = availability: Availability) {.async.} =
@ -151,18 +156,25 @@ suite "Sales":
check soldSlotIndex < request.ask.slots.u256 check soldSlotIndex < request.ask.slots.u256
test "calls onClear when storage becomes available again": 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, sales.onProve = proc(request: StorageRequest,
slot: UInt256): Future[seq[byte]] {.async.} = slot: UInt256): Future[seq[byte]] {.async.} =
raise newException(IOError, "proof failed") raise newException(IOError, "proof failed")
var clearedAvailability: Availability var clearedAvailability: Availability
var clearedRequest: StorageRequest var clearedRequest: StorageRequest
sales.onClear = proc(availability: Availability, request: StorageRequest) = var clearedSlotIndex: UInt256
sales.onClear = proc(availability: Availability,
request: StorageRequest,
slotIndex: UInt256) =
clearedAvailability = availability clearedAvailability = availability
clearedRequest = request clearedRequest = request
clearedSlotIndex = slotIndex
sales.add(availability) sales.add(availability)
discard await market.requestStorage(request) discard await market.requestStorage(request)
check clearedAvailability == availability check clearedAvailability == availability
check clearedRequest == request check clearedRequest == request
check clearedSlotIndex < request.ask.slots.u256
test "makes storage available again when other host fills the slot": test "makes storage available again when other host fills the slot":
let otherHost = Address.example let otherHost = Address.example
@ -186,3 +198,15 @@ suite "Sales":
clock.set(request.expiry.truncate(int64)) clock.set(request.expiry.truncate(int64))
await sleepAsync(2.seconds) await sleepAsync(2.seconds)
check sales.available == @[availability] 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))

View File

@ -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

View File

@ -5,6 +5,7 @@ import codex/contracts/testtoken
import codex/storageproofs import codex/storageproofs
import ../ethertest import ../ethertest
import ./examples import ./examples
import ./matchers
import ./time import ./time
ethersuite "Storage contracts": ethersuite "Storage contracts":
@ -74,3 +75,21 @@ ethersuite "Storage contracts":
switchAccount(host) switchAccount(host)
await provider.advanceTimeTo(await storage.proofEnd(slotId)) await provider.advanceTimeTo(await storage.proofEnd(slotId))
await storage.payoutSlot(request.id, 0.u256) 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)