Feat: price per byte (#1078)

* openAPI: StorageRequestCreation: reward => pricePerByte, collateral => collateralPerByte

* purchasing: reward => pricePerByte, collateral => collateralPerByte

* Updates availabilities and reservations to use totalCollateral, minPricePerByte, and maxCollateralPerByte

* Uses correct div operator when operating on UInt256

* proposal updating totalCollateral in availability

* makes sure that reading currentCollateral happens before freeing slot

* Updates naming

* fixes tests: unit and contracts

* uses feat/price-per-byte branch for codex-contracts-eth

* temporarily disables integration tests on CI

* introduces high level <<totalCollateral>> property for a cleaner external interface

* updates integration tests

* Applies review comments

* Updates description of totalCollateral in SalesAvailability

* updates codex-contracts-eth (price-per-byte)
This commit is contained in:
Marcin Czenko 2025-01-24 18:18:00 +01:00 committed by GitHub
parent f6c792de79
commit 962fc1cd95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 848 additions and 494 deletions

View File

@ -106,7 +106,7 @@ method mySlots*(market: OnChainMarket): Future[seq[SlotId]] {.async.} =
method requestStorage(market: OnChainMarket, request: StorageRequest) {.async.} = method requestStorage(market: OnChainMarket, request: StorageRequest) {.async.} =
convertEthersError: convertEthersError:
debug "Requesting storage" debug "Requesting storage"
await market.approveFunds(request.price()) await market.approveFunds(request.totalPrice())
discard await market.contract.requestStorage(request).confirm(1) discard await market.contract.requestStorage(request).confirm(1)
method getRequest*( method getRequest*(
@ -156,6 +156,12 @@ method getHost(
else: else:
return none Address return none Address
method currentCollateral*(
market: OnChainMarket, slotId: SlotId
): Future[UInt256] {.async.} =
convertEthersError:
return await market.contract.currentCollateral(slotId)
method getActiveSlot*(market: OnChainMarket, slotId: SlotId): Future[?Slot] {.async.} = method getActiveSlot*(market: OnChainMarket, slotId: SlotId): Future[?Slot] {.async.} =
convertEthersError: convertEthersError:
try: try:

View File

@ -48,6 +48,10 @@ type
proc configuration*(marketplace: Marketplace): MarketplaceConfig {.contract, view.} proc configuration*(marketplace: Marketplace): MarketplaceConfig {.contract, view.}
proc token*(marketplace: Marketplace): Address {.contract, view.} proc token*(marketplace: Marketplace): Address {.contract, view.}
proc currentCollateral*(
marketplace: Marketplace, id: SlotId
): UInt256 {.contract, view.}
proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.} proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.}
proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.} proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.}
proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.} proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.}

View File

@ -24,8 +24,8 @@ type
slotSize* {.serialize.}: UInt256 slotSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
proofProbability* {.serialize.}: UInt256 proofProbability* {.serialize.}: UInt256
reward* {.serialize.}: UInt256 pricePerBytePerSecond* {.serialize.}: UInt256
collateral* {.serialize.}: UInt256 collateralPerByte* {.serialize.}: UInt256
maxSlotLoss* {.serialize.}: uint64 maxSlotLoss* {.serialize.}: uint64
StorageContent* = object StorageContent* = object
@ -112,8 +112,8 @@ func fromTuple(_: type StorageAsk, tupl: tuple): StorageAsk =
slotSize: tupl[1], slotSize: tupl[1],
duration: tupl[2], duration: tupl[2],
proofProbability: tupl[3], proofProbability: tupl[3],
reward: tupl[4], pricePerBytePerSecond: tupl[4],
collateral: tupl[5], collateralPerByte: tupl[5],
maxSlotLoss: tupl[6], maxSlotLoss: tupl[6],
) )
@ -174,14 +174,20 @@ func slotId*(request: StorageRequest, slotIndex: UInt256): SlotId =
func id*(slot: Slot): SlotId = func id*(slot: Slot): SlotId =
slotId(slot.request, slot.slotIndex) slotId(slot.request, slot.slotIndex)
func pricePerSlot*(ask: StorageAsk): UInt256 = func pricePerSlotPerSecond*(ask: StorageAsk): UInt256 =
ask.duration * ask.reward ask.pricePerBytePerSecond * ask.slotSize
func price*(ask: StorageAsk): UInt256 = func pricePerSlot*(ask: StorageAsk): UInt256 =
ask.duration * ask.pricePerSlotPerSecond
func totalPrice*(ask: StorageAsk): UInt256 =
ask.slots.u256 * ask.pricePerSlot ask.slots.u256 * ask.pricePerSlot
func price*(request: StorageRequest): UInt256 = func totalPrice*(request: StorageRequest): UInt256 =
request.ask.price request.ask.totalPrice
func collateralPerSlot*(ask: StorageAsk): UInt256 =
ask.collateralPerByte * ask.slotSize
func size*(ask: StorageAsk): UInt256 = func size*(ask: StorageAsk): UInt256 =
ask.slots.u256 * ask.slotSize ask.slots.u256 * ask.slotSize

View File

@ -126,6 +126,11 @@ method getHost*(
): Future[?Address] {.base, async.} = ): Future[?Address] {.base, async.} =
raiseAssert("not implemented") raiseAssert("not implemented")
method currentCollateral*(
market: Market, slotId: SlotId
): Future[UInt256] {.base, async.} =
raiseAssert("not implemented")
method getActiveSlot*(market: Market, slotId: SlotId): Future[?Slot] {.base, async.} = method getActiveSlot*(market: Market, slotId: SlotId): Future[?Slot] {.base, async.} =
raiseAssert("not implemented") raiseAssert("not implemented")

View File

@ -373,8 +373,8 @@ proc setupRequest(
proofProbability: UInt256, proofProbability: UInt256,
nodes: uint, nodes: uint,
tolerance: uint, tolerance: uint,
reward: UInt256, pricePerBytePerSecond: UInt256,
collateral: UInt256, collateralPerByte: UInt256,
expiry: UInt256, expiry: UInt256,
): Future[?!StorageRequest] {.async.} = ): Future[?!StorageRequest] {.async.} =
## Setup slots for a given dataset ## Setup slots for a given dataset
@ -389,9 +389,9 @@ proc setupRequest(
duration = duration duration = duration
nodes = nodes nodes = nodes
tolerance = tolerance tolerance = tolerance
reward = reward pricePerBytePerSecond = pricePerBytePerSecond
proofProbability = proofProbability proofProbability = proofProbability
collateral = collateral collateralPerByte = collateralPerByte
expiry = expiry expiry = expiry
ecK = ecK ecK = ecK
ecM = ecM ecM = ecM
@ -435,8 +435,8 @@ proc setupRequest(
slotSize: builder.slotBytes.uint.u256, slotSize: builder.slotBytes.uint.u256,
duration: duration, duration: duration,
proofProbability: proofProbability, proofProbability: proofProbability,
reward: reward, pricePerBytePerSecond: pricePerBytePerSecond,
collateral: collateral, collateralPerByte: collateralPerByte,
maxSlotLoss: tolerance, maxSlotLoss: tolerance,
), ),
content: StorageContent( content: StorageContent(
@ -456,8 +456,8 @@ proc requestStorage*(
proofProbability: UInt256, proofProbability: UInt256,
nodes: uint, nodes: uint,
tolerance: uint, tolerance: uint,
reward: UInt256, pricePerBytePerSecond: UInt256,
collateral: UInt256, collateralPerByte: UInt256,
expiry: UInt256, expiry: UInt256,
): Future[?!PurchaseId] {.async.} = ): Future[?!PurchaseId] {.async.} =
## Initiate a request for storage sequence, this might ## Initiate a request for storage sequence, this might
@ -469,9 +469,9 @@ proc requestStorage*(
duration = duration duration = duration
nodes = nodes nodes = nodes
tolerance = tolerance tolerance = tolerance
reward = reward pricePerBytePerSecond = pricePerBytePerSecond
proofProbability = proofProbability proofProbability = proofProbability
collateral = collateral collateralPerByte = collateralPerByte
expiry = expiry.truncate(int64) expiry = expiry.truncate(int64)
now = self.clock.now now = self.clock.now
@ -483,7 +483,8 @@ proc requestStorage*(
without request =? ( without request =? (
await self.setupRequest( await self.setupRequest(
cid, duration, proofProbability, nodes, tolerance, reward, collateral, expiry cid, duration, proofProbability, nodes, tolerance, pricePerBytePerSecond,
collateralPerByte, expiry,
) )
), err: ), err:
trace "Unable to setup request" trace "Unable to setup request"

View File

@ -403,12 +403,15 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi(MethodPost, "/api/codex/v1/sales/availability") do() -> RestApiResponse: router.rawApi(MethodPost, "/api/codex/v1/sales/availability") do() -> RestApiResponse:
## Add available storage to sell. ## Add available storage to sell.
## Every time Availability's offer finishes, its capacity is returned to the availability. ## Every time Availability's offer finishes, its capacity is
## returned to the availability.
## ##
## totalSize - size of available storage in bytes ## totalSize - size of available storage in bytes
## duration - maximum time the storage should be sold for (in seconds) ## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimal price paid (in amount of tokens) for the whole hosted request's slot for the request's duration ## minPricePerBytePerSecond - minimal price per byte paid (in amount of
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens) ## tokens) to be matched against the request's pricePerBytePerSecond
## totalCollateral - total collateral (in amount of
## tokens) that can be distributed among matching requests
var headers = buildCorsHeaders("POST", allowedOrigin) var headers = buildCorsHeaders("POST", allowedOrigin)
@ -436,7 +439,8 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
without availability =? ( without availability =? (
await reservations.createAvailability( await reservations.createAvailability(
restAv.totalSize, restAv.duration, restAv.minPrice, restAv.maxCollateral restAv.totalSize, restAv.duration, restAv.minPricePerBytePerSecond,
restAv.totalCollateral,
) )
), error: ), error:
return RestApiResponse.error(Http500, error.msg, headers = headers) return RestApiResponse.error(Http500, error.msg, headers = headers)
@ -467,10 +471,14 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
## The new parameters will be only considered for new requests. ## The new parameters will be only considered for new requests.
## Existing Requests linked to this Availability will continue as is. ## Existing Requests linked to this Availability will continue as is.
## ##
## totalSize - size of available storage in bytes. When decreasing the size, then lower limit is the currently `totalSize - freeSize`. ## totalSize - size of available storage in bytes.
## duration - maximum time the storage should be sold for (in seconds) ## When decreasing the size, then lower limit is
## minPrice - minimum price to be paid (in amount of tokens) ## the currently `totalSize - freeSize`.
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens) ## duration - maximum time the storage should be sold for (in seconds)
## minPricePerBytePerSecond - minimal price per byte paid (in amount of
## tokens) to be matched against the request's pricePerBytePerSecond
## totalCollateral - total collateral (in amount of
## tokens) that can be distributed among matching requests
try: try:
without contracts =? node.contracts.host: without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Persistence is not enabled") return RestApiResponse.error(Http503, "Persistence is not enabled")
@ -512,11 +520,11 @@ proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
if duration =? restAv.duration: if duration =? restAv.duration:
availability.duration = duration availability.duration = duration
if minPrice =? restAv.minPrice: if minPricePerBytePerSecond =? restAv.minPricePerBytePerSecond:
availability.minPrice = minPrice availability.minPricePerBytePerSecond = minPricePerBytePerSecond
if maxCollateral =? restAv.maxCollateral: if totalCollateral =? restAv.totalCollateral:
availability.maxCollateral = maxCollateral availability.totalCollateral = totalCollateral
if err =? (await reservations.update(availability)).errorOption: if err =? (await reservations.update(availability)).errorOption:
return RestApiResponse.error(Http500, err.msg) return RestApiResponse.error(Http500, err.msg)
@ -580,11 +588,11 @@ proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
## cid - the cid of a previously uploaded dataset ## cid - the cid of a previously uploaded dataset
## duration - the duration of the request in seconds ## duration - the duration of the request in seconds
## proofProbability - how often storage proofs are required ## 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 ## pricePerBytePerSecond - the amount of tokens paid per byte per second to hosts the client is willing to pay
## expiry - specifies threshold in seconds from now when the request expires if the Request does not find requested amount of nodes to host the data ## expiry - specifies threshold in seconds from now when the request expires if the Request does not find requested amount of nodes to host the data
## nodes - number of nodes the content should be stored on ## nodes - number of nodes the content should be stored on
## tolerance - allowed number of nodes that can be lost before content is lost ## tolerance - allowed number of nodes that can be lost before content is lost
## colateral - requested collateral from hosts when they fill slot ## colateralPerByte - requested collateral per byte from hosts when they fill slot
try: try:
without contracts =? node.contracts.client: without contracts =? node.contracts.client:
return RestApiResponse.error( return RestApiResponse.error(
@ -639,7 +647,7 @@ proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
without purchaseId =? without purchaseId =?
await node.requestStorage( await node.requestStorage(
cid, params.duration, params.proofProbability, nodes, tolerance, cid, params.duration, params.proofProbability, nodes, tolerance,
params.reward, params.collateral, expiry, params.pricePerBytePerSecond, params.collateralPerByte, expiry,
), error: ), error:
if error of InsufficientBlocksError: if error of InsufficientBlocksError:
return RestApiResponse.error( return RestApiResponse.error(

View File

@ -15,8 +15,8 @@ type
StorageRequestParams* = object StorageRequestParams* = object
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
proofProbability* {.serialize.}: UInt256 proofProbability* {.serialize.}: UInt256
reward* {.serialize.}: UInt256 pricePerBytePerSecond* {.serialize.}: UInt256
collateral* {.serialize.}: UInt256 collateralPerByte* {.serialize.}: UInt256
expiry* {.serialize.}: ?UInt256 expiry* {.serialize.}: ?UInt256
nodes* {.serialize.}: ?uint nodes* {.serialize.}: ?uint
tolerance* {.serialize.}: ?uint tolerance* {.serialize.}: ?uint
@ -30,8 +30,8 @@ type
RestAvailability* = object RestAvailability* = object
totalSize* {.serialize.}: UInt256 totalSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
minPrice* {.serialize.}: UInt256 minPricePerBytePerSecond* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256 totalCollateral* {.serialize.}: UInt256
freeSize* {.serialize.}: ?UInt256 freeSize* {.serialize.}: ?UInt256
RestSalesAgent* = object RestSalesAgent* = object

View File

@ -115,6 +115,7 @@ proc cleanUp(
agent: SalesAgent, agent: SalesAgent,
returnBytes: bool, returnBytes: bool,
reprocessSlot: bool, reprocessSlot: bool,
returnedCollateral: ?UInt256,
processing: Future[void], processing: Future[void],
) {.async.} = ) {.async.} =
let data = agent.data let data = agent.data
@ -144,7 +145,7 @@ proc cleanUp(
if reservation =? data.reservation and if reservation =? data.reservation and
deleteErr =? ( deleteErr =? (
await sales.context.reservations.deleteReservation( await sales.context.reservations.deleteReservation(
reservation.id, reservation.availabilityId reservation.id, reservation.availabilityId, returnedCollateral
) )
).errorOption: ).errorOption:
error "failure deleting reservation", error = deleteErr.msg error "failure deleting reservation", error = deleteErr.msg
@ -187,8 +188,10 @@ proc processSlot(sales: Sales, item: SlotQueueItem, done: Future[void]) =
sales.context, item.requestId, item.slotIndex.u256, none StorageRequest sales.context, item.requestId, item.slotIndex.u256, none StorageRequest
) )
agent.onCleanUp = proc(returnBytes = false, reprocessSlot = false) {.async.} = agent.onCleanUp = proc(
await sales.cleanUp(agent, returnBytes, reprocessSlot, done) returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
await sales.cleanUp(agent, returnBytes, reprocessSlot, returnedCollateral, done)
agent.onFilled = some proc(request: StorageRequest, slotIndex: UInt256) = agent.onFilled = some proc(request: StorageRequest, slotIndex: UInt256) =
sales.filled(request, slotIndex, done) sales.filled(request, slotIndex, done)
@ -253,11 +256,13 @@ proc load*(sales: Sales) {.async.} =
let agent = let agent =
newSalesAgent(sales.context, slot.request.id, slot.slotIndex, some slot.request) newSalesAgent(sales.context, slot.request.id, slot.slotIndex, some slot.request)
agent.onCleanUp = proc(returnBytes = false, reprocessSlot = false) {.async.} = agent.onCleanUp = proc(
returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
# since workers are not being dispatched, this future has not been created # since workers are not being dispatched, this future has not been created
# by a worker. Create a dummy one here so we can call sales.cleanUp # by a worker. Create a dummy one here so we can call sales.cleanUp
let done: Future[void] = nil let done: Future[void] = nil
await sales.cleanUp(agent, returnBytes, reprocessSlot, done) await sales.cleanUp(agent, returnBytes, reprocessSlot, returnedCollateral, done)
# There is no need to assign agent.onFilled as slots loaded from `mySlots` # There is no need to assign agent.onFilled as slots loaded from `mySlots`
# are inherently already filled and so assigning agent.onFilled would be # are inherently already filled and so assigning agent.onFilled would be

View File

@ -7,23 +7,25 @@
## This file may not be copied, modified, or distributed except according to ## This file may not be copied, modified, or distributed except according to
## those terms. ## those terms.
## ##
## +--------------------------------------+ ## +--------------------------------------+
## | RESERVATION | ## | RESERVATION |
## +----------------------------------------+ |--------------------------------------| ## +---------------------------------------------------+ |--------------------------------------|
## | AVAILABILITY | | ReservationId | id | PK | ## | AVAILABILITY | | ReservationId | id | PK |
## |----------------------------------------| |--------------------------------------| ## |---------------------------------------------------| |--------------------------------------|
## | AvailabilityId | id | PK |<-||-------o<-| AvailabilityId | availabilityId | FK | ## | AvailabilityId | id | PK |<-||-------o<-| AvailabilityId | availabilityId | FK |
## |----------------------------------------| |--------------------------------------| ## |---------------------------------------------------| |--------------------------------------|
## | UInt256 | totalSize | | | UInt256 | size | | ## | UInt256 | totalSize | | | UInt256 | size | |
## |----------------------------------------| |--------------------------------------| ## |---------------------------------------------------| |--------------------------------------|
## | UInt256 | freeSize | | | UInt256 | slotIndex | | ## | UInt256 | freeSize | | | UInt256 | slotIndex | |
## |----------------------------------------| +--------------------------------------+ ## |---------------------------------------------------| +--------------------------------------+
## | UInt256 | duration | | ## | UInt256 | duration | |
## |----------------------------------------| ## |---------------------------------------------------|
## | UInt256 | minPrice | | ## | UInt256 | minPricePerBytePerSecond | |
## |----------------------------------------| ## |---------------------------------------------------|
## | UInt256 | maxCollateral | | ## | UInt256 | totalCollateral | |
## +----------------------------------------+ ## |---------------------------------------------------|
## | UInt256 | totalRemainingCollateral | |
## +---------------------------------------------------+
import pkg/upraises import pkg/upraises
push: push:
@ -65,9 +67,9 @@ type
totalSize* {.serialize.}: UInt256 totalSize* {.serialize.}: UInt256
freeSize* {.serialize.}: UInt256 freeSize* {.serialize.}: UInt256
duration* {.serialize.}: UInt256 duration* {.serialize.}: UInt256
minPrice* {.serialize.}: UInt256 minPricePerBytePerSecond* {.serialize.}: UInt256
# minimal price paid for the whole hosted slot for the request's duration totalCollateral {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256 totalRemainingCollateral* {.serialize.}: UInt256
Reservation* = ref object Reservation* = ref object
id* {.serialize.}: ReservationId id* {.serialize.}: ReservationId
@ -124,8 +126,8 @@ proc init*(
totalSize: UInt256, totalSize: UInt256,
freeSize: UInt256, freeSize: UInt256,
duration: UInt256, duration: UInt256,
minPrice: UInt256, minPricePerBytePerSecond: UInt256,
maxCollateral: UInt256, totalCollateral: UInt256,
): Availability = ): Availability =
var id: array[32, byte] var id: array[32, byte]
doAssert randomBytes(id) == 32 doAssert randomBytes(id) == 32
@ -134,10 +136,18 @@ proc init*(
totalSize: totalSize, totalSize: totalSize,
freeSize: freeSize, freeSize: freeSize,
duration: duration, duration: duration,
minPrice: minPrice, minPricePerBytePerSecond: minPricePerBytePerSecond,
maxCollateral: maxCollateral, totalCollateral: totalCollateral,
totalRemainingCollateral: totalCollateral,
) )
func totalCollateral*(self: Availability): UInt256 {.inline.} =
return self.totalCollateral
proc `totalCollateral=`*(self: Availability, value: UInt256) {.inline.} =
self.totalCollateral = value
self.totalRemainingCollateral = value
proc init*( proc init*(
_: type Reservation, _: type Reservation,
availabilityId: AvailabilityId, availabilityId: AvailabilityId,
@ -195,6 +205,9 @@ func key*(reservationId: ReservationId, availabilityId: AvailabilityId): ?!Key =
func key*(availability: Availability): ?!Key = func key*(availability: Availability): ?!Key =
return availability.id.key return availability.id.key
func maxCollateralPerByte*(availability: Availability): UInt256 =
return availability.totalRemainingCollateral div availability.freeSize
func key*(reservation: Reservation): ?!Key = func key*(reservation: Reservation): ?!Key =
return key(reservation.id, reservation.availabilityId) return key(reservation.id, reservation.availabilityId)
@ -328,7 +341,10 @@ proc delete(self: Reservations, key: Key): Future[?!void] {.async.} =
return success() return success()
proc deleteReservation*( proc deleteReservation*(
self: Reservations, reservationId: ReservationId, availabilityId: AvailabilityId self: Reservations,
reservationId: ReservationId,
availabilityId: AvailabilityId,
returnedCollateral: ?UInt256 = UInt256.none,
): Future[?!void] {.async.} = ): Future[?!void] {.async.} =
logScope: logScope:
reservationId reservationId
@ -357,6 +373,9 @@ proc deleteReservation*(
availability.freeSize += reservation.size availability.freeSize += reservation.size
if collateral =? returnedCollateral:
availability.totalRemainingCollateral += collateral
if updateErr =? (await self.updateAvailability(availability)).errorOption: if updateErr =? (await self.updateAvailability(availability)).errorOption:
return failure(updateErr) return failure(updateErr)
@ -372,12 +391,14 @@ proc createAvailability*(
self: Reservations, self: Reservations,
size: UInt256, size: UInt256,
duration: UInt256, duration: UInt256,
minPrice: UInt256, minPricePerBytePerSecond: UInt256,
maxCollateral: UInt256, totalCollateral: UInt256,
): Future[?!Availability] {.async.} = ): Future[?!Availability] {.async.} =
trace "creating availability", size, duration, minPrice, maxCollateral trace "creating availability",
size, duration, minPricePerBytePerSecond, totalCollateral
let availability = Availability.init(size, size, duration, minPrice, maxCollateral) let availability =
Availability.init(size, size, duration, minPricePerBytePerSecond, totalCollateral)
let bytes = availability.freeSize.truncate(uint) let bytes = availability.freeSize.truncate(uint)
if reserveErr =? (await self.repo.reserve(bytes.NBytes)).errorOption: if reserveErr =? (await self.repo.reserve(bytes.NBytes)).errorOption:
@ -400,6 +421,7 @@ method createReservation*(
slotSize: UInt256, slotSize: UInt256,
requestId: RequestId, requestId: RequestId,
slotIndex: UInt256, slotIndex: UInt256,
collateralPerByte: UInt256,
): Future[?!Reservation] {.async, base.} = ): Future[?!Reservation] {.async, base.} =
withLock(self.availabilityLock): withLock(self.availabilityLock):
without availabilityKey =? availabilityId.key, error: without availabilityKey =? availabilityId.key, error:
@ -412,7 +434,7 @@ method createReservation*(
if availability.freeSize < slotSize: if availability.freeSize < slotSize:
let error = newException( let error = newException(
BytesOutOfBoundsError, BytesOutOfBoundsError,
"trying to reserve an amount of bytes that is greater than the total size of the Availability", "trying to reserve an amount of bytes that is greater than the free size of the Availability",
) )
return failure(error) return failure(error)
@ -427,6 +449,9 @@ method createReservation*(
# the newly created Reservation # the newly created Reservation
availability.freeSize -= slotSize availability.freeSize -= slotSize
# adjust the remaining totalRemainingCollateral
availability.totalRemainingCollateral -= slotSize * collateralPerByte
# update availability with reduced size # update availability with reduced size
trace "Updating availability with reduced size" trace "Updating availability with reduced size"
if updateErr =? (await self.updateAvailability(availability)).errorOption: if updateErr =? (await self.updateAvailability(availability)).errorOption:
@ -617,7 +642,8 @@ proc all*(
return await self.allImpl(T, key) return await self.allImpl(T, key)
proc findAvailability*( proc findAvailability*(
self: Reservations, size, duration, minPrice, collateral: UInt256 self: Reservations,
size, duration, pricePerBytePerSecond, collateralPerByte: UInt256,
): Future[?Availability] {.async.} = ): Future[?Availability] {.async.} =
without storables =? (await self.storables(Availability)), e: without storables =? (await self.storables(Availability)), e:
error "failed to get all storables", error = e.msg error "failed to get all storables", error = e.msg
@ -626,17 +652,18 @@ proc findAvailability*(
for item in storables.items: for item in storables.items:
if bytes =? (await item) and availability =? Availability.fromJson(bytes): if bytes =? (await item) and availability =? Availability.fromJson(bytes):
if size <= availability.freeSize and duration <= availability.duration and if size <= availability.freeSize and duration <= availability.duration and
collateral <= availability.maxCollateral and minPrice >= availability.minPrice: collateralPerByte <= availability.maxCollateralPerByte and
pricePerBytePerSecond >= availability.minPricePerBytePerSecond:
trace "availability matched", trace "availability matched",
id = availability.id, id = availability.id,
size, size,
availFreeSize = availability.freeSize, availFreeSize = availability.freeSize,
duration, duration,
availDuration = availability.duration, availDuration = availability.duration,
minPrice, pricePerBytePerSecond,
availMinPrice = availability.minPrice, availMinPricePerBytePerSecond = availability.minPricePerBytePerSecond,
collateral, collateralPerByte,
availMaxCollateral = availability.maxCollateral availMaxCollateralPerByte = availability.maxCollateralPerByte
# TODO: As soon as we're on ARC-ORC, we can use destructors # TODO: As soon as we're on ARC-ORC, we can use destructors
# to automatically dispose our iterators when they fall out of scope. # to automatically dispose our iterators when they fall out of scope.
@ -652,7 +679,7 @@ proc findAvailability*(
availFreeSize = availability.freeSize, availFreeSize = availability.freeSize,
duration, duration,
availDuration = availability.duration, availDuration = availability.duration,
minPrice, pricePerBytePerSecond,
availMinPrice = availability.minPrice, availMinPricePerBytePerSecond = availability.minPricePerBytePerSecond,
collateral, collateralPerByte,
availMaxCollateral = availability.maxCollateral availMaxCollateralPerByte = availability.maxCollateralPerByte

View File

@ -25,9 +25,9 @@ type
onCleanUp*: OnCleanUp onCleanUp*: OnCleanUp
onFilled*: ?OnFilled onFilled*: ?OnFilled
OnCleanUp* = proc(returnBytes = false, reprocessSlot = false): Future[void] {. OnCleanUp* = proc(
gcsafe, upraises: [] returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
.} ): Future[void] {.gcsafe, upraises: [].}
OnFilled* = proc(request: StorageRequest, slotIndex: UInt256) {.gcsafe, upraises: [].} OnFilled* = proc(request: StorageRequest, slotIndex: UInt256) {.gcsafe, upraises: [].}
SalesAgentError = object of CodexError SalesAgentError = object of CodexError

View File

@ -32,8 +32,8 @@ type
slotIndex: uint16 slotIndex: uint16
slotSize: UInt256 slotSize: UInt256
duration: UInt256 duration: UInt256
reward: UInt256 pricePerBytePerSecond: UInt256
collateral: UInt256 collateralPerByte: UInt256
expiry: UInt256 expiry: UInt256
seen: bool seen: bool
@ -70,12 +70,14 @@ const DefaultMaxSize = 128'u16
proc profitability(item: SlotQueueItem): UInt256 = proc profitability(item: SlotQueueItem): UInt256 =
StorageAsk( StorageAsk(
collateral: item.collateral,
duration: item.duration, duration: item.duration,
reward: item.reward, pricePerBytePerSecond: item.pricePerBytePerSecond,
slotSize: item.slotSize, slotSize: item.slotSize,
).pricePerSlot ).pricePerSlot
proc collateralPerSlot(item: SlotQueueItem): UInt256 =
StorageAsk(collateralPerByte: item.collateralPerByte, slotSize: item.slotSize).collateralPerSlot
proc `<`*(a, b: SlotQueueItem): bool = proc `<`*(a, b: SlotQueueItem): bool =
# for A to have a higher priority than B (in a min queue), A must be less than # for A to have a higher priority than B (in a min queue), A must be less than
# B. # B.
@ -92,15 +94,12 @@ proc `<`*(a, b: SlotQueueItem): bool =
scoreA.addIf(a.profitability > b.profitability, 3) scoreA.addIf(a.profitability > b.profitability, 3)
scoreB.addIf(a.profitability < b.profitability, 3) scoreB.addIf(a.profitability < b.profitability, 3)
scoreA.addIf(a.collateral < b.collateral, 2) scoreA.addIf(a.collateralPerSlot < b.collateralPerSlot, 2)
scoreB.addIf(a.collateral > b.collateral, 2) scoreB.addIf(a.collateralPerSlot > b.collateralPerSlot, 2)
scoreA.addIf(a.expiry > b.expiry, 1) scoreA.addIf(a.expiry > b.expiry, 1)
scoreB.addIf(a.expiry < b.expiry, 1) scoreB.addIf(a.expiry < b.expiry, 1)
scoreA.addIf(a.slotSize < b.slotSize, 0)
scoreB.addIf(a.slotSize > b.slotSize, 0)
return scoreA > scoreB return scoreA > scoreB
proc `==`*(a, b: SlotQueueItem): bool = proc `==`*(a, b: SlotQueueItem): bool =
@ -144,8 +143,8 @@ proc init*(
slotIndex: slotIndex, slotIndex: slotIndex,
slotSize: ask.slotSize, slotSize: ask.slotSize,
duration: ask.duration, duration: ask.duration,
reward: ask.reward, pricePerBytePerSecond: ask.pricePerBytePerSecond,
collateral: ask.collateral, collateralPerByte: ask.collateralPerByte,
expiry: expiry, expiry: expiry,
seen: seen, seen: seen,
) )
@ -189,11 +188,11 @@ proc slotSize*(self: SlotQueueItem): UInt256 =
proc duration*(self: SlotQueueItem): UInt256 = proc duration*(self: SlotQueueItem): UInt256 =
self.duration self.duration
proc reward*(self: SlotQueueItem): UInt256 = proc pricePerBytePerSecond*(self: SlotQueueItem): UInt256 =
self.reward self.pricePerBytePerSecond
proc collateral*(self: SlotQueueItem): UInt256 = proc collateralPerByte*(self: SlotQueueItem): UInt256 =
self.collateral self.collateralPerByte
proc seen*(self: SlotQueueItem): bool = proc seen*(self: SlotQueueItem): bool =
self.seen self.seen
@ -246,8 +245,8 @@ proc populateItem*(
slotIndex: slotIndex, slotIndex: slotIndex,
slotSize: item.slotSize, slotSize: item.slotSize,
duration: item.duration, duration: item.duration,
reward: item.reward, pricePerBytePerSecond: item.pricePerBytePerSecond,
collateral: item.collateral, collateralPerByte: item.collateralPerByte,
expiry: item.expiry, expiry: item.expiry,
) )
return none SlotQueueItem return none SlotQueueItem

View File

@ -22,13 +22,18 @@ method run*(state: SaleCancelled, machine: Machine): Future[?State] {.async.} =
let slot = Slot(request: request, slotIndex: data.slotIndex) let slot = Slot(request: request, slotIndex: data.slotIndex)
debug "Collecting collateral and partial payout", debug "Collecting collateral and partial payout",
requestId = data.requestId, slotIndex = data.slotIndex requestId = data.requestId, slotIndex = data.slotIndex
let currentCollateral = await market.currentCollateral(slot.id)
await market.freeSlot(slot.id) await market.freeSlot(slot.id)
if onClear =? agent.context.onClear and request =? data.request: if onClear =? agent.context.onClear and request =? data.request:
onClear(request, data.slotIndex) onClear(request, data.slotIndex)
if onCleanUp =? agent.onCleanUp: if onCleanUp =? agent.onCleanUp:
await onCleanUp(returnBytes = true, reprocessSlot = false) await onCleanUp(
returnBytes = true,
reprocessSlot = false,
returnedCollateral = some currentCollateral,
)
warn "Sale cancelled due to timeout", warn "Sale cancelled due to timeout",
requestId = data.requestId, slotIndex = data.slotIndex requestId = data.requestId, slotIndex = data.slotIndex

View File

@ -28,7 +28,7 @@ method onFailed*(state: SaleFilling, request: StorageRequest): ?State =
method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} = method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} =
let data = SalesAgent(machine).data let data = SalesAgent(machine).data
let market = SalesAgent(machine).context.market let market = SalesAgent(machine).context.market
without (fullCollateral =? data.request .? ask .? collateral): without (request =? data.request):
raiseAssert "Request not set" raiseAssert "Request not set"
logScope: logScope:
@ -36,15 +36,17 @@ method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} =
slotIndex = data.slotIndex slotIndex = data.slotIndex
let slotState = await market.slotState(slotId(data.requestId, data.slotIndex)) let slotState = await market.slotState(slotId(data.requestId, data.slotIndex))
let requestedCollateral = request.ask.collateralPerSlot
var collateral: UInt256 var collateral: UInt256
if slotState == SlotState.Repair: if slotState == SlotState.Repair:
# When repairing the node gets "discount" on the collateral that it needs to # When repairing the node gets "discount" on the collateral that it needs to
let repairRewardPercentage = (await market.repairRewardPercentage).u256 let repairRewardPercentage = (await market.repairRewardPercentage).u256
collateral = collateral =
fullCollateral - ((fullCollateral * repairRewardPercentage)).div(100.u256) requestedCollateral -
((requestedCollateral * repairRewardPercentage)).div(100.u256)
else: else:
collateral = fullCollateral collateral = requestedCollateral
debug "Filling slot" debug "Filling slot"
try: try:

View File

@ -11,6 +11,7 @@ logScope:
topics = "marketplace sales finished" topics = "marketplace sales finished"
type SaleFinished* = ref object of ErrorHandlingState type SaleFinished* = ref object of ErrorHandlingState
returnedCollateral*: ?UInt256
method `$`*(state: SaleFinished): string = method `$`*(state: SaleFinished): string =
"SaleFinished" "SaleFinished"
@ -32,4 +33,4 @@ method run*(state: SaleFinished, machine: Machine): Future[?State] {.async.} =
requestId = data.requestId, slotIndex = data.slotIndex requestId = data.requestId, slotIndex = data.slotIndex
if onCleanUp =? agent.onCleanUp: if onCleanUp =? agent.onCleanUp:
await onCleanUp() await onCleanUp(returnedCollateral = state.returnedCollateral)

View File

@ -21,7 +21,7 @@ method onCancelled*(state: SalePayout, request: StorageRequest): ?State =
method onFailed*(state: SalePayout, request: StorageRequest): ?State = method onFailed*(state: SalePayout, request: StorageRequest): ?State =
return some State(SaleFailed()) return some State(SaleFailed())
method run(state: SalePayout, machine: Machine): Future[?State] {.async.} = method run*(state: SalePayout, machine: Machine): Future[?State] {.async.} =
let data = SalesAgent(machine).data let data = SalesAgent(machine).data
let market = SalesAgent(machine).context.market let market = SalesAgent(machine).context.market
@ -31,6 +31,7 @@ method run(state: SalePayout, machine: Machine): Future[?State] {.async.} =
let slot = Slot(request: request, slotIndex: data.slotIndex) let slot = Slot(request: request, slotIndex: data.slotIndex)
debug "Collecting finished slot's reward", debug "Collecting finished slot's reward",
requestId = data.requestId, slotIndex = data.slotIndex requestId = data.requestId, slotIndex = data.slotIndex
let currentCollateral = await market.currentCollateral(slot.id)
await market.freeSlot(slot.id) await market.freeSlot(slot.id)
return some State(SaleFinished()) return some State(SaleFinished(returnedCollateral: some currentCollateral))

View File

@ -62,15 +62,13 @@ method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
slotIndex = data.slotIndex slotIndex = data.slotIndex
slotSize = request.ask.slotSize slotSize = request.ask.slotSize
duration = request.ask.duration duration = request.ask.duration
pricePerSlot = request.ask.pricePerSlot pricePerBytePerSecond = request.ask.pricePerBytePerSecond
collateralPerByte = request.ask.collateralPerByte
# availability was checked for this slot when it entered the queue, however
# check to the ensure that there is still availability as they may have
# changed since being added (other slots may have been processed in that time)
without availability =? without availability =?
await reservations.findAvailability( await reservations.findAvailability(
request.ask.slotSize, request.ask.duration, request.ask.pricePerSlot, request.ask.slotSize, request.ask.duration, request.ask.pricePerBytePerSecond,
request.ask.collateral, request.ask.collateralPerByte,
): ):
debug "No availability found for request, ignoring" debug "No availability found for request, ignoring"
@ -80,7 +78,8 @@ method run*(state: SalePreparing, machine: Machine): Future[?State] {.async.} =
without reservation =? without reservation =?
await reservations.createReservation( await reservations.createReservation(
availability.id, request.ask.slotSize, request.id, data.slotIndex availability.id, request.ask.slotSize, request.id, data.slotIndex,
request.ask.collateralPerByte,
), error: ), error:
trace "Creation of reservation failed" trace "Creation of reservation failed"
# Race condition: # Race condition:

View File

@ -6,7 +6,7 @@ info:
description: "List of endpoints and interfaces available to Codex API users" description: "List of endpoints and interfaces available to Codex API users"
security: security:
- { } - {}
components: components:
schemas: schemas:
@ -50,9 +50,9 @@ components:
type: string type: string
description: Address of Ethereum address description: Address of Ethereum address
Reward: PricePerBytePerSecond:
type: string type: string
description: The maximum amount of tokens paid per second per slot to hosts the client is willing to pay description: The amount of tokens paid per byte per second per slot to hosts the client is willing to pay
Duration: Duration:
type: string type: string
@ -157,12 +157,12 @@ components:
description: Total size of availability's storage in bytes as decimal string description: Total size of availability's storage in bytes as decimal string
duration: duration:
$ref: "#/components/schemas/Duration" $ref: "#/components/schemas/Duration"
minPrice: minPricePerBytePerSecond:
type: string type: string
description: Minimal price paid (in amount of tokens) for the whole hosted request's slot for the request's duration as decimal string description: Minimal price per byte per second paid (in amount of tokens) for the hosted request's slot for the request's duration as decimal string
maxCollateral: totalCollateral:
type: string type: string
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens) as decimal string description: Total collateral (in amount of tokens) that can be used for matching requests
SalesAvailabilityREAD: SalesAvailabilityREAD:
allOf: allOf:
@ -178,8 +178,8 @@ components:
- $ref: "#/components/schemas/SalesAvailability" - $ref: "#/components/schemas/SalesAvailability"
- required: - required:
- totalSize - totalSize
- minPrice - minPricePerBytePerSecond
- maxCollateral - totalCollateral
- duration - duration
Slot: Slot:
@ -243,16 +243,16 @@ components:
StorageRequestCreation: StorageRequestCreation:
type: object type: object
required: required:
- reward - pricePerBytePerSecond
- duration - duration
- proofProbability - proofProbability
- collateral - collateralPerByte
- expiry - expiry
properties: properties:
duration: duration:
$ref: "#/components/schemas/Duration" $ref: "#/components/schemas/Duration"
reward: pricePerBytePerSecond:
$ref: "#/components/schemas/Reward" $ref: "#/components/schemas/PricePerBytePerSecond"
proofProbability: proofProbability:
$ref: "#/components/schemas/ProofProbability" $ref: "#/components/schemas/ProofProbability"
nodes: nodes:
@ -263,16 +263,16 @@ components:
description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost
type: integer type: integer
default: 0 default: 0
collateral: collateralPerByte:
type: string type: string
description: Number as decimal string that represents how much collateral is asked from hosts that wants to fill a slots description: Number as decimal string that represents how much collateral per byte is asked from hosts that wants to fill a slots
expiry: expiry:
type: string type: string
description: Number as decimal string that represents expiry threshold in seconds from when the Request is submitted. When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided. The number of seconds can not be higher then the Request's duration itself. description: Number as decimal string that represents expiry threshold in seconds from when the Request is submitted. When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided. The number of seconds can not be higher then the Request's duration itself.
StorageAsk: StorageAsk:
type: object type: object
required: required:
- reward - pricePerBytePerSecond
properties: properties:
slots: slots:
description: Number of slots (eq. hosts) that the Request want to have the content spread over description: Number of slots (eq. hosts) that the Request want to have the content spread over
@ -284,8 +284,8 @@ components:
$ref: "#/components/schemas/Duration" $ref: "#/components/schemas/Duration"
proofProbability: proofProbability:
$ref: "#/components/schemas/ProofProbability" $ref: "#/components/schemas/ProofProbability"
reward: pricePerBytePerSecond:
$ref: "#/components/schemas/Reward" $ref: "#/components/schemas/PricePerBytePerSecond"
maxSlotLoss: maxSlotLoss:
type: integer type: integer
description: Max slots that can be lost without data considered to be lost description: Max slots that can be lost without data considered to be lost
@ -418,14 +418,14 @@ paths:
description: | description: |
If `addrs` param is supplied, it will be used to dial the peer, otherwise the `peerId` is used If `addrs` param is supplied, it will be used to dial the peer, otherwise the `peerId` is used
to invoke peer discovery, if it succeeds the returned addresses will be used to dial. to invoke peer discovery, if it succeeds the returned addresses will be used to dial.
tags: [ Node ] tags: [Node]
operationId: connectPeer operationId: connectPeer
parameters: parameters:
- in: path - in: path
name: peerId name: peerId
required: true required: true
schema: schema:
$ref: "#/components/schemas/PeerId" $ref: "#/components/schemas/PeerId"
description: Peer that should be dialed. description: Peer that should be dialed.
- in: query - in: query
name: addrs name: addrs
@ -448,7 +448,7 @@ paths:
"/data": "/data":
get: get:
summary: "Lists manifest CIDs stored locally in node." summary: "Lists manifest CIDs stored locally in node."
tags: [ Data ] tags: [Data]
operationId: listData operationId: listData
responses: responses:
"200": "200":
@ -468,7 +468,7 @@ paths:
description: Well it was bad-bad description: Well it was bad-bad
post: post:
summary: "Upload a file in a streaming manner. Once finished, the file is stored in the node and can be retrieved by any node in the network using the returned CID." summary: "Upload a file in a streaming manner. Once finished, the file is stored in the node and can be retrieved by any node in the network using the returned CID."
tags: [ Data ] tags: [Data]
operationId: upload operationId: upload
parameters: parameters:
- name: content-type - name: content-type
@ -484,7 +484,7 @@ paths:
description: The content disposition used to send the filename. description: The content disposition used to send the filename.
schema: schema:
type: string type: string
example: "attachment; filename=\"codex.png\"" example: 'attachment; filename="codex.png"'
requestBody: requestBody:
content: content:
application/octet-stream: application/octet-stream:
@ -504,14 +504,14 @@ paths:
"/data/{cid}": "/data/{cid}":
get: get:
summary: "Download a file from the local node in a streaming manner. If the file is not available locally, a 404 is returned." summary: "Download a file from the local node in a streaming manner. If the file is not available locally, a 404 is returned."
tags: [ Data ] tags: [Data]
operationId: downloadLocal operationId: downloadLocal
parameters: parameters:
- in: path - in: path
name: cid name: cid
required: true required: true
schema: schema:
$ref: "#/components/schemas/Cid" $ref: "#/components/schemas/Cid"
description: File to be downloaded. description: File to be downloaded.
responses: responses:
@ -532,14 +532,14 @@ paths:
"/data/{cid}/network": "/data/{cid}/network":
post: post:
summary: "Download a file from the network to the local node if it's not available locally. Note: Download is performed async. Call can return before download is completed." summary: "Download a file from the network to the local node if it's not available locally. Note: Download is performed async. Call can return before download is completed."
tags: [ Data ] tags: [Data]
operationId: downloadNetwork operationId: downloadNetwork
parameters: parameters:
- in: path - in: path
name: cid name: cid
required: true required: true
schema: schema:
$ref: "#/components/schemas/Cid" $ref: "#/components/schemas/Cid"
description: "File to be downloaded." description: "File to be downloaded."
responses: responses:
"200": "200":
@ -558,14 +558,14 @@ paths:
"/data/{cid}/network/stream": "/data/{cid}/network/stream":
get: get:
summary: "Download a file from the network in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able." summary: "Download a file from the network in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able."
tags: [ Data ] tags: [Data]
operationId: downloadNetworkStream operationId: downloadNetworkStream
parameters: parameters:
- in: path - in: path
name: cid name: cid
required: true required: true
schema: schema:
$ref: "#/components/schemas/Cid" $ref: "#/components/schemas/Cid"
description: "File to be downloaded." description: "File to be downloaded."
responses: responses:
"200": "200":
@ -585,14 +585,14 @@ paths:
"/data/{cid}/network/manifest": "/data/{cid}/network/manifest":
get: get:
summary: "Download only the dataset manifest from the network to the local node if it's not available locally." summary: "Download only the dataset manifest from the network to the local node if it's not available locally."
tags: [ Data ] tags: [Data]
operationId: downloadNetworkManifest operationId: downloadNetworkManifest
parameters: parameters:
- in: path - in: path
name: cid name: cid
required: true required: true
schema: schema:
$ref: "#/components/schemas/Cid" $ref: "#/components/schemas/Cid"
description: "File for which the manifest is to be downloaded." description: "File for which the manifest is to be downloaded."
responses: responses:
"200": "200":
@ -611,7 +611,7 @@ paths:
"/space": "/space":
get: get:
summary: "Gets a summary of the storage space allocation of the node." summary: "Gets a summary of the storage space allocation of the node."
tags: [ Data ] tags: [Data]
operationId: space operationId: space
responses: responses:
"200": "200":
@ -627,7 +627,7 @@ paths:
"/sales/slots": "/sales/slots":
get: get:
summary: "Returns active slots" summary: "Returns active slots"
tags: [ Marketplace ] tags: [Marketplace]
operationId: getActiveSlots operationId: getActiveSlots
responses: responses:
"200": "200":
@ -645,7 +645,7 @@ paths:
"/sales/slots/{slotId}": "/sales/slots/{slotId}":
get: get:
summary: "Returns active slot with id {slotId} for the host" summary: "Returns active slot with id {slotId} for the host"
tags: [ Marketplace ] tags: [Marketplace]
operationId: getActiveSlotById operationId: getActiveSlotById
parameters: parameters:
- in: path - in: path
@ -674,7 +674,7 @@ paths:
"/sales/availability": "/sales/availability":
get: get:
summary: "Returns storage that is for sale" summary: "Returns storage that is for sale"
tags: [ Marketplace ] tags: [Marketplace]
operationId: getAvailabilities operationId: getAvailabilities
responses: responses:
"200": "200":
@ -693,7 +693,7 @@ paths:
post: post:
summary: "Offers storage for sale" summary: "Offers storage for sale"
operationId: offerStorage operationId: offerStorage
tags: [ Marketplace ] tags: [Marketplace]
requestBody: requestBody:
content: content:
application/json: application/json:
@ -721,7 +721,7 @@ paths:
The new parameters will be only considered for new requests. The new parameters will be only considered for new requests.
Existing Requests linked to this Availability will continue as is. Existing Requests linked to this Availability will continue as is.
operationId: updateOfferedStorage operationId: updateOfferedStorage
tags: [ Marketplace ] tags: [Marketplace]
parameters: parameters:
- in: path - in: path
name: id name: id
@ -753,7 +753,7 @@ paths:
summary: "Get availability's reservations" summary: "Get availability's reservations"
description: Return's list of Reservations for ongoing Storage Requests that the node hosts. description: Return's list of Reservations for ongoing Storage Requests that the node hosts.
operationId: getReservations operationId: getReservations
tags: [ Marketplace ] tags: [Marketplace]
parameters: parameters:
- in: path - in: path
name: id name: id
@ -782,7 +782,7 @@ paths:
"/storage/request/{cid}": "/storage/request/{cid}":
post: post:
summary: "Creates a new Request for storage" summary: "Creates a new Request for storage"
tags: [ Marketplace ] tags: [Marketplace]
operationId: createStorageRequest operationId: createStorageRequest
parameters: parameters:
- in: path - in: path
@ -813,7 +813,7 @@ paths:
"/storage/purchases": "/storage/purchases":
get: get:
summary: "Returns list of purchase IDs" summary: "Returns list of purchase IDs"
tags: [ Marketplace ] tags: [Marketplace]
operationId: getPurchases operationId: getPurchases
responses: responses:
"200": "200":
@ -830,7 +830,7 @@ paths:
"/storage/purchases/{id}": "/storage/purchases/{id}":
get: get:
summary: "Returns purchase details" summary: "Returns purchase details"
tags: [ Marketplace ] tags: [Marketplace]
operationId: getPurchase operationId: getPurchase
parameters: parameters:
- in: path - in: path
@ -857,7 +857,7 @@ paths:
get: get:
summary: "Get Node's SPR" summary: "Get Node's SPR"
operationId: getSPR operationId: getSPR
tags: [ Node ] tags: [Node]
responses: responses:
"200": "200":
description: Node's SPR description: Node's SPR
@ -875,7 +875,7 @@ paths:
get: get:
summary: "Get Node's PeerID" summary: "Get Node's PeerID"
operationId: getPeerId operationId: getPeerId
tags: [ Node ] tags: [Node]
responses: responses:
"200": "200":
description: Node's Peer ID description: Node's Peer ID
@ -890,7 +890,7 @@ paths:
"/debug/chronicles/loglevel": "/debug/chronicles/loglevel":
post: post:
summary: "Set log level at run time" summary: "Set log level at run time"
tags: [ Debug ] tags: [Debug]
operationId: setDebugLogLevel operationId: setDebugLogLevel
parameters: parameters:
@ -912,7 +912,7 @@ paths:
get: get:
summary: "Gets node information" summary: "Gets node information"
operationId: getDebugInfo operationId: getDebugInfo
tags: [ Debug ] tags: [Debug]
responses: responses:
"200": "200":
description: Node's information description: Node's information

View File

@ -55,13 +55,16 @@ proc example*(_: type MultiHash, mcodec = Sha256HashCodec): MultiHash =
let bytes = newSeqWith(256, rand(uint8)) let bytes = newSeqWith(256, rand(uint8))
MultiHash.digest($mcodec, bytes).tryGet() MultiHash.digest($mcodec, bytes).tryGet()
proc example*(_: type Availability): Availability = proc example*(
_: type Availability, collateralPerByte = uint8.example.u256
): Availability =
let totalSize = uint16.example.u256
Availability.init( Availability.init(
totalSize = uint16.example.u256, totalSize = totalSize,
freeSize = uint16.example.u256, freeSize = uint16.example.u256,
duration = uint16.example.u256, duration = uint16.example.u256,
minPrice = uint64.example.u256, minPricePerBytePerSecond = uint8.example.u256,
maxCollateral = uint16.example.u256, totalCollateral = totalSize * collateralPerByte,
) )
proc example*(_: type Reservation): Reservation = proc example*(_: type Reservation): Reservation =

View File

@ -60,6 +60,7 @@ type
slotIndex*: UInt256 slotIndex*: UInt256
proof*: Groth16Proof proof*: Groth16Proof
timestamp: ?SecondsSince1970 timestamp: ?SecondsSince1970
collateral*: UInt256
Subscriptions = object Subscriptions = object
onRequest: seq[RequestSubscription] onRequest: seq[RequestSubscription]
@ -205,6 +206,14 @@ method getHost*(
return some slot.host return some slot.host
return none Address return none Address
method currentCollateral*(
market: MockMarket, slotId: SlotId
): Future[UInt256] {.async.} =
for slot in market.filled:
if slotId == slotId(slot.requestId, slot.slotIndex):
return slot.collateral
return 0.u256
proc emitSlotFilled*(market: MockMarket, requestId: RequestId, slotIndex: UInt256) = proc emitSlotFilled*(market: MockMarket, requestId: RequestId, slotIndex: UInt256) =
var subscriptions = market.subscriptions.onSlotFilled var subscriptions = market.subscriptions.onSlotFilled
for subscription in subscriptions: for subscription in subscriptions:
@ -251,6 +260,7 @@ proc fillSlot*(
slotIndex: UInt256, slotIndex: UInt256,
proof: Groth16Proof, proof: Groth16Proof,
host: Address, host: Address,
collateral = 0.u256,
) = ) =
let slot = MockSlot( let slot = MockSlot(
requestId: requestId, requestId: requestId,
@ -258,6 +268,7 @@ proc fillSlot*(
proof: proof, proof: proof,
host: host, host: host,
timestamp: market.clock .? now, timestamp: market.clock .? now,
collateral: collateral,
) )
market.filled.add(slot) market.filled.add(slot)
market.slotState[slotId(slot.requestId, slot.slotIndex)] = SlotState.Filled market.slotState[slotId(slot.requestId, slot.slotIndex)] = SlotState.Filled
@ -270,7 +281,7 @@ method fillSlot*(
proof: Groth16Proof, proof: Groth16Proof,
collateral: UInt256, collateral: UInt256,
) {.async.} = ) {.async.} =
market.fillSlot(requestId, slotIndex, proof, market.signer) market.fillSlot(requestId, slotIndex, proof, market.signer, collateral)
method freeSlot*(market: MockMarket, slotId: SlotId) {.async.} = method freeSlot*(market: MockMarket, slotId: SlotId) {.async.} =
market.freed.add(slotId) market.freed.add(slotId)

View File

@ -27,6 +27,7 @@ method createReservation*(
slotSize: UInt256, slotSize: UInt256,
requestId: RequestId, requestId: RequestId,
slotIndex: UInt256, slotIndex: UInt256,
collateralPerByte: UInt256,
): Future[?!Reservation] {.async.} = ): Future[?!Reservation] {.async.} =
if self.createReservationThrowBytesOutOfBoundsError: if self.createReservationThrowBytesOutOfBoundsError:
let error = newException( let error = newException(
@ -38,5 +39,10 @@ method createReservation*(
return failure(error) return failure(error)
return await procCall createReservation( return await procCall createReservation(
Reservations(self), availabilityId, slotSize, requestId, slotIndex Reservations(self),
availabilityId,
slotSize,
requestId,
slotIndex,
collateralPerByte,
) )

View File

@ -6,8 +6,8 @@ type MockSlotQueueItem* = object
slotIndex*: uint16 slotIndex*: uint16
slotSize*: UInt256 slotSize*: UInt256
duration*: UInt256 duration*: UInt256
reward*: UInt256 pricePerBytePerSecond*: UInt256
collateral*: UInt256 collateralPerByte*: UInt256
expiry*: UInt256 expiry*: UInt256
seen*: bool seen*: bool
@ -18,8 +18,8 @@ proc toSlotQueueItem*(item: MockSlotQueueItem): SlotQueueItem =
ask = StorageAsk( ask = StorageAsk(
slotSize: item.slotSize, slotSize: item.slotSize,
duration: item.duration, duration: item.duration,
reward: item.reward, pricePerBytePerSecond: item.pricePerBytePerSecond,
collateral: item.collateral, collateralPerByte: item.collateralPerByte,
), ),
expiry = item.expiry, expiry = item.expiry,
seen = item.seen, seen = item.seen,

View File

@ -155,10 +155,10 @@ asyncchecksuite "Test Node - Basic":
nodes = 5, nodes = 5,
tolerance = 2, tolerance = 2,
duration = 100.u256, duration = 100.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 200.u256, expiry = 200.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
) )
).tryGet ).tryGet

View File

@ -15,25 +15,40 @@ import ../../helpers/mockclock
asyncchecksuite "sales state 'cancelled'": asyncchecksuite "sales state 'cancelled'":
let request = StorageRequest.example let request = StorageRequest.example
let slotIndex = (request.ask.slots div 2).u256 let slotIndex = (request.ask.slots div 2).u256
let market = MockMarket.new()
let clock = MockClock.new() let clock = MockClock.new()
let currentCollateral = UInt256.example
var market: MockMarket
var state: SaleCancelled var state: SaleCancelled
var agent: SalesAgent var agent: SalesAgent
var returnBytesWas = false var returnBytesWas = bool.none
var reprocessSlotWas = false var reprocessSlotWas = bool.none
var returnedCollateralValue = UInt256.none
setup: setup:
let onCleanUp = proc(returnBytes = false, reprocessSlot = false) {.async.} = market = MockMarket.new()
returnBytesWas = returnBytes let onCleanUp = proc(
reprocessSlotWas = reprocessSlot returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
returnBytesWas = some returnBytes
reprocessSlotWas = some reprocessSlot
returnedCollateralValue = returnedCollateral
let context = SalesContext(market: market, clock: clock) let context = SalesContext(market: market, clock: clock)
agent = newSalesAgent(context, request.id, slotIndex, request.some) agent = newSalesAgent(context, request.id, slotIndex, request.some)
agent.onCleanUp = onCleanUp agent.onCleanUp = onCleanUp
state = SaleCancelled.new() state = SaleCancelled.new()
test "calls onCleanUp with returnBytes = false and reprocessSlot = true": test "calls onCleanUp with returnBytes = false, reprocessSlot = true, and returnedCollateral = currentCollateral":
market.fillSlot(
requestId = request.id,
slotIndex = slotIndex,
proof = Groth16Proof.default,
host = Address.example,
collateral = currentCollateral,
)
discard await state.run(agent) discard await state.run(agent)
check eventually returnBytesWas == true check eventually returnBytesWas == some true
check eventually reprocessSlotWas == false check eventually reprocessSlotWas == some false
check eventually returnedCollateralValue == some currentCollateral

View File

@ -24,7 +24,9 @@ asyncchecksuite "sales state 'errored'":
var reprocessSlotWas = false var reprocessSlotWas = false
setup: setup:
let onCleanUp = proc(returnBytes = false, reprocessSlot = false) {.async.} = let onCleanUp = proc(
returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
returnBytesWas = returnBytes returnBytesWas = returnBytes
reprocessSlotWas = reprocessSlot reprocessSlotWas = reprocessSlot

View File

@ -1,18 +1,45 @@
import std/unittest
import pkg/questionable import pkg/questionable
import pkg/codex/contracts/requests import pkg/codex/contracts/requests
import pkg/codex/sales/states/finished import pkg/codex/sales/states/finished
import pkg/codex/sales/states/cancelled import pkg/codex/sales/states/cancelled
import pkg/codex/sales/states/failed import pkg/codex/sales/states/failed
import pkg/codex/sales/salesagent
import pkg/codex/sales/salescontext
import pkg/codex/market
import ../../../asynctest
import ../../examples import ../../examples
import ../../helpers import ../../helpers
import ../../helpers/mockmarket
import ../../helpers/mockclock
checksuite "sales state 'finished'": asyncchecksuite "sales state 'finished'":
let request = StorageRequest.example let request = StorageRequest.example
let slotIndex = (request.ask.slots div 2).u256
let clock = MockClock.new()
let currentCollateral = UInt256.example
var market: MockMarket
var state: SaleFinished var state: SaleFinished
var agent: SalesAgent
var returnBytesWas = bool.none
var reprocessSlotWas = bool.none
var returnedCollateralValue = UInt256.none
setup: setup:
state = SaleFinished.new() market = MockMarket.new()
let onCleanUp = proc(
returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
returnBytesWas = some returnBytes
reprocessSlotWas = some reprocessSlot
returnedCollateralValue = returnedCollateral
let context = SalesContext(market: market, clock: clock)
agent = newSalesAgent(context, request.id, slotIndex, request.some)
agent.onCleanUp = onCleanUp
state = SaleFinished(returnedCollateral: some currentCollateral)
test "switches to cancelled state when request expires": test "switches to cancelled state when request expires":
let next = state.onCancelled(request) let next = state.onCancelled(request)
@ -21,3 +48,9 @@ checksuite "sales state 'finished'":
test "switches to failed state when request fails": test "switches to failed state when request fails":
let next = state.onFailed(request) let next = state.onFailed(request)
check !next of SaleFailed check !next of SaleFailed
test "calls onCleanUp with returnBytes = false, reprocessSlot = true, and returnedCollateral = currentCollateral":
discard await state.run(agent)
check eventually returnBytesWas == some false
check eventually reprocessSlotWas == some false
check eventually returnedCollateralValue == some currentCollateral

View File

@ -24,7 +24,9 @@ asyncchecksuite "sales state 'ignored'":
var reprocessSlotWas = false var reprocessSlotWas = false
setup: setup:
let onCleanUp = proc(returnBytes = false, reprocessSlot = false) {.async.} = let onCleanUp = proc(
returnBytes = false, reprocessSlot = false, returnedCollateral = UInt256.none
) {.async.} =
returnBytesWas = returnBytes returnBytesWas = returnBytes
reprocessSlotWas = reprocessSlot reprocessSlotWas = reprocessSlot

View File

@ -0,0 +1,44 @@
import pkg/questionable
import pkg/chronos
import pkg/codex/contracts/requests
import pkg/codex/sales/states/payout
import pkg/codex/sales/states/finished
import pkg/codex/sales/salesagent
import pkg/codex/sales/salescontext
import pkg/codex/market
import ../../../asynctest
import ../../examples
import ../../helpers
import ../../helpers/mockmarket
import ../../helpers/mockclock
asyncchecksuite "sales state 'payout'":
let request = StorageRequest.example
let slotIndex = (request.ask.slots div 2).u256
let clock = MockClock.new()
let currentCollateral = UInt256.example
var market: MockMarket
var state: SalePayout
var agent: SalesAgent
setup:
market = MockMarket.new()
let context = SalesContext(market: market, clock: clock)
agent = newSalesAgent(context, request.id, slotIndex, request.some)
state = SalePayout.new()
test "switches to 'finished' state and provides returnedCollateral":
market.fillSlot(
requestId = request.id,
slotIndex = slotIndex,
proof = Groth16Proof.default,
host = Address.example,
collateral = currentCollateral,
)
let next = await state.run(agent)
check !next of SaleFinished
check SaleFinished(!next).returnedCollateral == some currentCollateral

View File

@ -33,12 +33,12 @@ asyncchecksuite "sales state 'preparing'":
var reservations: MockReservations var reservations: MockReservations
setup: setup:
availability = Availability( availability = Availability.init(
totalSize: request.ask.slotSize + 100.u256, totalSize = request.ask.slotSize + 100.u256,
freeSize: request.ask.slotSize + 100.u256, freeSize = request.ask.slotSize + 100.u256,
duration: request.ask.duration + 60.u256, duration = request.ask.duration + 60.u256,
minPrice: request.ask.pricePerSlot - 10.u256, minPricePerBytePerSecond = request.ask.pricePerBytePerSecond,
maxCollateral: request.ask.collateral + 400.u256, totalCollateral = request.ask.collateralPerSlot * request.ask.slots.u256,
) )
let repoDs = SQLiteDatastore.new(Memory).tryGet() let repoDs = SQLiteDatastore.new(Memory).tryGet()
let metaDs = SQLiteDatastore.new(Memory).tryGet() let metaDs = SQLiteDatastore.new(Memory).tryGet()
@ -69,8 +69,8 @@ asyncchecksuite "sales state 'preparing'":
proc createAvailability() {.async.} = proc createAvailability() {.async.} =
let a = await reservations.createAvailability( let a = await reservations.createAvailability(
availability.totalSize, availability.duration, availability.minPrice, availability.totalSize, availability.duration,
availability.maxCollateral, availability.minPricePerBytePerSecond, availability.totalCollateral,
) )
availability = a.get availability = a.get

View File

@ -22,6 +22,7 @@ asyncchecksuite "Reservations module":
repoDs: Datastore repoDs: Datastore
metaDs: Datastore metaDs: Datastore
reservations: Reservations reservations: Reservations
collateralPerByte: UInt256
let let
repoTmp = TempLevelDb.new() repoTmp = TempLevelDb.new()
metaTmp = TempLevelDb.new() metaTmp = TempLevelDb.new()
@ -32,23 +33,25 @@ asyncchecksuite "Reservations module":
metaDs = metaTmp.newDb() metaDs = metaTmp.newDb()
repo = RepoStore.new(repoDs, metaDs) repo = RepoStore.new(repoDs, metaDs)
reservations = Reservations.new(repo) reservations = Reservations.new(repo)
collateralPerByte = uint8.example.u256
teardown: teardown:
await repoTmp.destroyDb() await repoTmp.destroyDb()
await metaTmp.destroyDb() await metaTmp.destroyDb()
proc createAvailability(): Availability = proc createAvailability(): Availability =
let example = Availability.example let example = Availability.example(collateralPerByte)
let totalSize = rand(100000 .. 200000) let totalSize = rand(100000 .. 200000).u256
let totalCollateral = totalSize * collateralPerByte
let availability = waitFor reservations.createAvailability( let availability = waitFor reservations.createAvailability(
totalSize.u256, example.duration, example.minPrice, example.maxCollateral totalSize, example.duration, example.minPricePerBytePerSecond, totalCollateral
) )
return availability.get return availability.get
proc createReservation(availability: Availability): Reservation = proc createReservation(availability: Availability): Reservation =
let size = rand(1 ..< availability.freeSize.truncate(int)) let size = rand(1 ..< availability.freeSize.truncate(int))
let reservation = waitFor reservations.createReservation( let reservation = waitFor reservations.createReservation(
availability.id, size.u256, RequestId.example, UInt256.example availability.id, size.u256, RequestId.example, UInt256.example, 1.u256
) )
return reservation.get return reservation.get
@ -126,7 +129,7 @@ asyncchecksuite "Reservations module":
test "cannot create reservation with non-existant availability": test "cannot create reservation with non-existant availability":
let availability = Availability.example let availability = Availability.example
let created = await reservations.createReservation( let created = await reservations.createReservation(
availability.id, UInt256.example, RequestId.example, UInt256.example availability.id, UInt256.example, RequestId.example, UInt256.example, 1.u256
) )
check created.isErr check created.isErr
check created.error of NotExistsError check created.error of NotExistsError
@ -134,7 +137,11 @@ asyncchecksuite "Reservations module":
test "cannot create reservation larger than availability size": test "cannot create reservation larger than availability size":
let availability = createAvailability() let availability = createAvailability()
let created = await reservations.createReservation( let created = await reservations.createReservation(
availability.id, availability.totalSize + 1, RequestId.example, UInt256.example availability.id,
availability.totalSize + 1,
RequestId.example,
UInt256.example,
UInt256.example,
) )
check created.isErr check created.isErr
check created.error of BytesOutOfBoundsError check created.error of BytesOutOfBoundsError
@ -143,11 +150,16 @@ asyncchecksuite "Reservations module":
proc concurrencyTest(): Future[void] {.async.} = proc concurrencyTest(): Future[void] {.async.} =
let availability = createAvailability() let availability = createAvailability()
let one = reservations.createReservation( let one = reservations.createReservation(
availability.id, availability.totalSize - 1, RequestId.example, UInt256.example availability.id,
availability.totalSize - 1,
RequestId.example,
UInt256.example,
UInt256.example,
) )
let two = reservations.createReservation( let two = reservations.createReservation(
availability.id, availability.totalSize, RequestId.example, UInt256.example availability.id, availability.totalSize, RequestId.example, UInt256.example,
UInt256.example,
) )
let oneResult = await one let oneResult = await one
@ -304,8 +316,8 @@ asyncchecksuite "Reservations module":
let availability = createAvailability() let availability = createAvailability()
let found = await reservations.findAvailability( let found = await reservations.findAvailability(
availability.freeSize, availability.duration, availability.minPrice, availability.freeSize, availability.duration,
availability.maxCollateral, availability.minPricePerBytePerSecond, collateralPerByte,
) )
check found.isSome check found.isSome
@ -317,23 +329,22 @@ asyncchecksuite "Reservations module":
let found = await reservations.findAvailability( let found = await reservations.findAvailability(
availability.freeSize + 1, availability.freeSize + 1,
availability.duration, availability.duration,
availability.minPrice, availability.minPricePerBytePerSecond,
availability.maxCollateral, collateralPerByte,
) )
check found.isNone check found.isNone
test "non-existant availability cannot be found": test "non-existent availability cannot be found":
let availability = Availability.example let availability = Availability.example
let found = ( let found = await reservations.findAvailability(
await reservations.findAvailability( availability.freeSize, availability.duration,
availability.freeSize, availability.duration, availability.minPrice, availability.minPricePerBytePerSecond, collateralPerByte,
availability.maxCollateral,
)
) )
check found.isNone check found.isNone
test "non-existant availability cannot be retrieved": test "non-existent availability cannot be retrieved":
let key = AvailabilityId.example.key.get let key = AvailabilityId.example.key.get
let got = await reservations.get(key, Availability) let got = await reservations.get(key, Availability)
check got.error of NotExistsError check got.error of NotExistsError

View File

@ -43,8 +43,8 @@ asyncchecksuite "Sales - start":
slots: 4, slots: 4,
slotSize: 100.u256, slotSize: 100.u256,
duration: 60.u256, duration: 60.u256,
reward: 10.u256, pricePerBytePerSecond: 1.u256,
collateral: 200.u256, collateralPerByte: 1.u256,
), ),
content: StorageContent(cid: "some cid"), content: StorageContent(cid: "some cid"),
expiry: (getTime() + initDuration(hours = 1)).toUnix.u256, expiry: (getTime() + initDuration(hours = 1)).toUnix.u256,
@ -124,6 +124,10 @@ asyncchecksuite "Sales":
repoTmp = TempLevelDb.new() repoTmp = TempLevelDb.new()
metaTmp = TempLevelDb.new() metaTmp = TempLevelDb.new()
var totalAvailabilitySize: UInt256
var minPricePerBytePerSecond: UInt256
var requestedCollateralPerByte: UInt256
var totalCollateral: UInt256
var availability: Availability var availability: Availability
var request: StorageRequest var request: StorageRequest
var sales: Sales var sales: Sales
@ -135,20 +139,24 @@ asyncchecksuite "Sales":
var itemsProcessed: seq[SlotQueueItem] var itemsProcessed: seq[SlotQueueItem]
setup: setup:
availability = Availability( totalAvailabilitySize = 100.u256
totalSize: 100.u256, minPricePerBytePerSecond = 1.u256
freeSize: 100.u256, requestedCollateralPerByte = 1.u256
duration: 60.u256, totalCollateral = requestedCollateralPerByte * totalAvailabilitySize
minPrice: 600.u256, availability = Availability.init(
maxCollateral: 400.u256, totalSize = totalAvailabilitySize,
freeSize = totalAvailabilitySize,
duration = 60.u256,
minPricePerBytePerSecond = minPricePerBytePerSecond,
totalCollateral = totalCollateral,
) )
request = StorageRequest( request = StorageRequest(
ask: StorageAsk( ask: StorageAsk(
slots: 4, slots: 4,
slotSize: 100.u256, slotSize: 100.u256,
duration: 60.u256, duration: 60.u256,
reward: 10.u256, pricePerBytePerSecond: minPricePerBytePerSecond,
collateral: 200.u256, collateralPerByte: 1.u256,
), ),
content: StorageContent(cid: "some cid"), content: StorageContent(cid: "some cid"),
expiry: (getTime() + initDuration(hours = 1)).toUnix.u256, expiry: (getTime() + initDuration(hours = 1)).toUnix.u256,
@ -210,8 +218,8 @@ asyncchecksuite "Sales":
proc createAvailability() = proc createAvailability() =
let a = waitFor reservations.createAvailability( let a = waitFor reservations.createAvailability(
availability.totalSize, availability.duration, availability.minPrice, availability.totalSize, availability.duration,
availability.maxCollateral, availability.minPricePerBytePerSecond, availability.totalCollateral,
) )
availability = a.get # update id availability = a.get # update id
@ -229,7 +237,7 @@ asyncchecksuite "Sales":
done.complete() done.complete()
var request1 = StorageRequest.example var request1 = StorageRequest.example
request1.ask.collateral = request.ask.collateral + 1 request1.ask.collateralPerByte = request.ask.collateralPerByte + 1
createAvailability() createAvailability()
# saturate queue # saturate queue
while queue.len < queue.size - 1: while queue.len < queue.size - 1:
@ -383,14 +391,14 @@ asyncchecksuite "Sales":
check wasIgnored() check wasIgnored()
test "ignores request when reward is too low": test "ignores request when reward is too low":
availability.minPrice = request.ask.pricePerSlot + 1 availability.minPricePerBytePerSecond = request.ask.pricePerBytePerSecond + 1
createAvailability() createAvailability()
await market.requestStorage(request) await market.requestStorage(request)
check wasIgnored() check wasIgnored()
test "ignores request when asked collateral is too high": test "ignores request when asked collateral is too high":
var tooBigCollateral = request var tooBigCollateral = request
tooBigCollateral.ask.collateral = availability.maxCollateral + 1 tooBigCollateral.ask.collateralPerByte = requestedCollateralPerByte + 1
createAvailability() createAvailability()
await market.requestStorage(tooBigCollateral) await market.requestStorage(tooBigCollateral)
check wasIgnored() check wasIgnored()
@ -606,7 +614,7 @@ asyncchecksuite "Sales":
test "deletes inactive reservations on load": test "deletes inactive reservations on load":
createAvailability() createAvailability()
discard await reservations.createReservation( discard await reservations.createReservation(
availability.id, 100.u256, RequestId.example, UInt256.example availability.id, 100.u256, RequestId.example, UInt256.example, UInt256.example
) )
check (await reservations.all(Reservation)).get.len == 1 check (await reservations.all(Reservation)).get.len == 1
await sales.load() await sales.load()

View File

@ -24,12 +24,15 @@ suite "Slot queue start/stop":
test "starts out not running": test "starts out not running":
check not queue.running check not queue.running
test "queue starts paused":
check queue.paused
test "can call start multiple times, and when already running": test "can call start multiple times, and when already running":
queue.start() queue.start()
queue.start() queue.start()
check queue.running check queue.running
test "can call stop when alrady stopped": test "can call stop when already stopped":
await queue.stop() await queue.stop()
check not queue.running check not queue.running
@ -144,16 +147,16 @@ suite "Slot queue":
test "correctly compares SlotQueueItems": test "correctly compares SlotQueueItems":
var requestA = StorageRequest.example var requestA = StorageRequest.example
requestA.ask.duration = 1.u256 requestA.ask.duration = 1.u256
requestA.ask.reward = 1.u256 requestA.ask.pricePerBytePerSecond = 1.u256
check requestA.ask.pricePerSlot == 1.u256 check requestA.ask.pricePerSlot == 1.u256 * requestA.ask.slotSize
requestA.ask.collateral = 100000.u256 requestA.ask.collateralPerByte = 100000.u256
requestA.expiry = 1001.u256 requestA.expiry = 1001.u256
var requestB = StorageRequest.example var requestB = StorageRequest.example
requestB.ask.duration = 100.u256 requestB.ask.duration = 100.u256
requestB.ask.reward = 1000.u256 requestB.ask.pricePerBytePerSecond = 1000.u256
check requestB.ask.pricePerSlot == 100000.u256 check requestB.ask.pricePerSlot == 100000.u256 * requestB.ask.slotSize
requestB.ask.collateral = 1.u256 requestB.ask.collateralPerByte = 1.u256
requestB.expiry = 1000.u256 requestB.expiry = 1000.u256
let itemA = SlotQueueItem.init(requestA, 0) let itemA = SlotQueueItem.init(requestA, 0)
@ -168,8 +171,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 2.u256, # profitability is higher (good) pricePerBytePerSecond: 2.u256, # profitability is higher (good)
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 1.u256, expiry: 1.u256,
seen: true, # seen (bad), more weight than profitability seen: true, # seen (bad), more weight than profitability
) )
@ -178,8 +181,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 1.u256, # profitability is lower (bad) pricePerBytePerSecond: 1.u256, # profitability is lower (bad)
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 1.u256, expiry: 1.u256,
seen: false, # not seen (good) seen: false, # not seen (good)
) )
@ -193,8 +196,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 1.u256, # reward is lower (bad) pricePerBytePerSecond: 1.u256, # reward is lower (bad)
collateral: 1.u256, # collateral is lower (good) collateralPerByte: 1.u256, # collateral is lower (good)
expiry: 1.u256, expiry: 1.u256,
seen: false, seen: false,
) )
@ -203,8 +206,9 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 2.u256, # reward is higher (good), more weight than collateral pricePerBytePerSecond: 2.u256,
collateral: 2.u256, # collateral is higher (bad) # reward is higher (good), more weight than collateral
collateralPerByte: 2.u256, # collateral is higher (bad)
expiry: 1.u256, expiry: 1.u256,
seen: false, seen: false,
) )
@ -218,8 +222,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 2.u256, # collateral is higher (bad) collateralPerByte: 2.u256, # collateral is higher (bad)
expiry: 2.u256, # expiry is longer (good) expiry: 2.u256, # expiry is longer (good)
seen: false, seen: false,
) )
@ -228,8 +232,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, slotSize: 1.u256,
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 1.u256, # collateral is lower (good), more weight than expiry collateralPerByte: 1.u256, # collateral is lower (good), more weight than expiry
expiry: 1.u256, # expiry is shorter (bad) expiry: 1.u256, # expiry is shorter (bad)
seen: false, seen: false,
) )
@ -243,8 +247,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, # slotSize is smaller (good) slotSize: 1.u256, # slotSize is smaller (good)
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 1.u256, # expiry is shorter (bad) expiry: 1.u256, # expiry is shorter (bad)
seen: false, seen: false,
) )
@ -253,8 +257,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 2.u256, # slotSize is larger (bad) slotSize: 2.u256, # slotSize is larger (bad)
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 2.u256, # expiry is longer (good), more weight than slotSize expiry: 2.u256, # expiry is longer (good), more weight than slotSize
seen: false, seen: false,
) )
@ -268,8 +272,8 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 2.u256, # slotSize is larger (bad) slotSize: 2.u256, # slotSize is larger (bad)
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 1.u256, # expiry is shorter (bad) expiry: 1.u256, # expiry is shorter (bad)
seen: false, seen: false,
) )
@ -278,13 +282,13 @@ suite "Slot queue":
slotIndex: 0, slotIndex: 0,
slotSize: 1.u256, # slotSize is smaller (good) slotSize: 1.u256, # slotSize is smaller (good)
duration: 1.u256, duration: 1.u256,
reward: 1.u256, pricePerBytePerSecond: 1.u256,
collateral: 1.u256, collateralPerByte: 1.u256,
expiry: 1.u256, expiry: 1.u256,
seen: false, seen: false,
) )
check itemB.toSlotQueueItem < itemA.toSlotQueueItem # < indicates higher priority check itemA.toSlotQueueItem < itemB.toSlotQueueItem # < indicates higher priority
test "expands available all possible slot indices on init": test "expands available all possible slot indices on init":
let request = StorageRequest.example let request = StorageRequest.example
@ -322,7 +326,7 @@ suite "Slot queue":
newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis) newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis)
let request0 = StorageRequest.example let request0 = StorageRequest.example
var request1 = StorageRequest.example var request1 = StorageRequest.example
request1.ask.collateral += 1.u256 request1.ask.collateralPerByte += 1.u256
let items0 = SlotQueueItem.init(request0) let items0 = SlotQueueItem.init(request0)
let items1 = SlotQueueItem.init(request1) let items1 = SlotQueueItem.init(request1)
check queue.push(items0).isOk check queue.push(items0).isOk
@ -332,8 +336,8 @@ suite "Slot queue":
check populated.slotIndex == 12'u16 check populated.slotIndex == 12'u16
check populated.slotSize == request1.ask.slotSize check populated.slotSize == request1.ask.slotSize
check populated.duration == request1.ask.duration check populated.duration == request1.ask.duration
check populated.reward == request1.ask.reward check populated.pricePerBytePerSecond == request1.ask.pricePerBytePerSecond
check populated.collateral == request1.ask.collateral check populated.collateralPerByte == request1.ask.collateralPerByte
test "does not find exisiting request metadata": test "does not find exisiting request metadata":
newSlotQueue(maxSize = 2, maxWorkers = 2) newSlotQueue(maxSize = 2, maxWorkers = 2)
@ -394,7 +398,7 @@ suite "Slot queue":
newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis) newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis)
let request0 = StorageRequest.example let request0 = StorageRequest.example
var request1 = StorageRequest.example var request1 = StorageRequest.example
request1.ask.collateral += 1.u256 request1.ask.collateralPerByte += 1.u256
let items0 = SlotQueueItem.init(request0) let items0 = SlotQueueItem.init(request0)
let items1 = SlotQueueItem.init(request1) let items1 = SlotQueueItem.init(request1)
check queue.push(items0).isOk check queue.push(items0).isOk
@ -408,7 +412,7 @@ suite "Slot queue":
newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis) newSlotQueue(maxSize = 8, maxWorkers = 1, processSlotDelay = 10.millis)
let request0 = StorageRequest.example let request0 = StorageRequest.example
var request1 = StorageRequest.example var request1 = StorageRequest.example
request1.ask.collateral += 1.u256 request1.ask.collateralPerByte += 1.u256
let items0 = SlotQueueItem.init(request0) let items0 = SlotQueueItem.init(request0)
let items1 = SlotQueueItem.init(request1) let items1 = SlotQueueItem.init(request1)
check queue.push(items0).isOk check queue.push(items0).isOk
@ -424,11 +428,11 @@ suite "Slot queue":
var request3 = StorageRequest.example var request3 = StorageRequest.example
var request4 = StorageRequest.example var request4 = StorageRequest.example
var request5 = StorageRequest.example var request5 = StorageRequest.example
request1.ask.collateral = request0.ask.collateral + 1 request1.ask.collateralPerByte = request0.ask.collateralPerByte + 1
request2.ask.collateral = request1.ask.collateral + 1 request2.ask.collateralPerByte = request1.ask.collateralPerByte + 1
request3.ask.collateral = request2.ask.collateral + 1 request3.ask.collateralPerByte = request2.ask.collateralPerByte + 1
request4.ask.collateral = request3.ask.collateral + 1 request4.ask.collateralPerByte = request3.ask.collateralPerByte + 1
request5.ask.collateral = request4.ask.collateral + 1 request5.ask.collateralPerByte = request4.ask.collateralPerByte + 1
let item0 = SlotQueueItem.init(request0, 0) let item0 = SlotQueueItem.init(request0, 0)
let item1 = SlotQueueItem.init(request1, 0) let item1 = SlotQueueItem.init(request1, 0)
let item2 = SlotQueueItem.init(request2, 0) let item2 = SlotQueueItem.init(request2, 0)
@ -439,19 +443,19 @@ suite "Slot queue":
check queue.push(@[item0, item1, item2, item3, item4, item5]).isOk check queue.push(@[item0, item1, item2, item3, item4, item5]).isOk
check queue.contains(item5) check queue.contains(item5)
test "sorts items by profitability ascending (higher pricePerSlot = higher priority)": test "sorts items by profitability descending (higher pricePerBytePerSecond == higher priority == goes first in the list)":
var request = StorageRequest.example var request = StorageRequest.example
let item0 = SlotQueueItem.init(request, 0) let item0 = SlotQueueItem.init(request, 0)
request.ask.reward += 1.u256 request.ask.pricePerBytePerSecond += 1.u256
let item1 = SlotQueueItem.init(request, 1) let item1 = SlotQueueItem.init(request, 1)
check item1 < item0 check item1 < item0
test "sorts items by collateral ascending (less required collateral = higher priority)": test "sorts items by collateral ascending (higher required collateralPerByte = lower priority == comes later in the list)":
var request = StorageRequest.example var request = StorageRequest.example
let item0 = SlotQueueItem.init(request, 0) let item0 = SlotQueueItem.init(request, 0)
request.ask.collateral -= 1.u256 request.ask.collateralPerByte += 1.u256
let item1 = SlotQueueItem.init(request, 1) let item1 = SlotQueueItem.init(request, 1)
check item1 < item0 check item1 > item0
test "sorts items by expiry descending (longer expiry = higher priority)": test "sorts items by expiry descending (longer expiry = higher priority)":
var request = StorageRequest.example var request = StorageRequest.example
@ -460,10 +464,10 @@ suite "Slot queue":
let item1 = SlotQueueItem.init(request, 1) let item1 = SlotQueueItem.init(request, 1)
check item1 < item0 check item1 < item0
test "sorts items by slot size ascending (smaller dataset = higher priority)": test "sorts items by slot size descending (bigger dataset = higher profitability = higher priority)":
var request = StorageRequest.example var request = StorageRequest.example
let item0 = SlotQueueItem.init(request, 0) let item0 = SlotQueueItem.init(request, 0)
request.ask.slotSize -= 1.u256 request.ask.slotSize += 1.u256
let item1 = SlotQueueItem.init(request, 1) let item1 = SlotQueueItem.init(request, 1)
check item1 < item0 check item1 < item0
@ -480,17 +484,17 @@ suite "Slot queue":
check queue.push(item).isOk check queue.push(item).isOk
check eventually onProcessSlotCalledWith == @[(item.requestId, item.slotIndex)] check eventually onProcessSlotCalledWith == @[(item.requestId, item.slotIndex)]
test "should process items in correct order": test "processes items in order of addition when only one item is added at a time":
newSlotQueue(maxSize = 2, maxWorkers = 2) newSlotQueue(maxSize = 2, maxWorkers = 2)
# sleeping after push allows the slotqueue loop to iterate, # sleeping after push allows the slotqueue loop to iterate,
# calling the callback for each pushed/updated item # calling the callback for each pushed/updated item
var request = StorageRequest.example var request = StorageRequest.example
let item0 = SlotQueueItem.init(request, 0) let item0 = SlotQueueItem.init(request, 0)
request.ask.reward += 1.u256 request.ask.pricePerBytePerSecond += 1.u256
let item1 = SlotQueueItem.init(request, 1) let item1 = SlotQueueItem.init(request, 1)
request.ask.reward += 1.u256 request.ask.pricePerBytePerSecond += 1.u256
let item2 = SlotQueueItem.init(request, 2) let item2 = SlotQueueItem.init(request, 2)
request.ask.reward += 1.u256 request.ask.pricePerBytePerSecond += 1.u256
let item3 = SlotQueueItem.init(request, 3) let item3 = SlotQueueItem.init(request, 3)
check queue.push(item0).isOk check queue.push(item0).isOk
@ -511,9 +515,35 @@ suite "Slot queue":
] ]
) )
test "queue starts paused": test "should process items in correct order according to the queue invariant when more than one item is added at a time":
newSlotQueue(maxSize = 4, maxWorkers = 4) newSlotQueue(maxSize = 4, maxWorkers = 2)
check queue.paused # sleeping after push allows the slotqueue loop to iterate,
# calling the callback for each pushed/updated item
var request = StorageRequest.example
let item0 = SlotQueueItem.init(request, 0)
request.ask.pricePerBytePerSecond += 1.u256
let item1 = SlotQueueItem.init(request, 1)
request.ask.pricePerBytePerSecond += 1.u256
let item2 = SlotQueueItem.init(request, 2)
request.ask.pricePerBytePerSecond += 1.u256
let item3 = SlotQueueItem.init(request, 3)
check queue.push(item0).isOk
check queue.push(item1).isOk
check queue.push(item2).isOk
check queue.push(item3).isOk
await sleepAsync(1.millis)
check eventually (
onProcessSlotCalledWith ==
@[
(item3.requestId, item3.slotIndex),
(item2.requestId, item2.slotIndex),
(item1.requestId, item1.slotIndex),
(item0.requestId, item0.slotIndex),
]
)
test "pushing items to queue unpauses queue": test "pushing items to queue unpauses queue":
newSlotQueue(maxSize = 4, maxWorkers = 4) newSlotQueue(maxSize = 4, maxWorkers = 4)

View File

@ -1,6 +1,7 @@
import ./states/testunknown import ./states/testunknown
import ./states/testdownloading import ./states/testdownloading
import ./states/testfilling import ./states/testfilling
import ./states/testpayout
import ./states/testfinished import ./states/testfinished
import ./states/testinitialproving import ./states/testinitialproving
import ./states/testfilled import ./states/testfilled

View File

@ -30,7 +30,7 @@ asyncchecksuite "Purchasing":
slots: uint8.example.uint64, slots: uint8.example.uint64,
slotSize: uint32.example.u256, slotSize: uint32.example.u256,
duration: uint16.example.u256, duration: uint16.example.u256,
reward: uint8.example.u256, pricePerBytePerSecond: uint8.example.u256,
) )
) )
@ -46,7 +46,8 @@ asyncchecksuite "Purchasing":
check market.requested[0].ask.slots == request.ask.slots check market.requested[0].ask.slots == request.ask.slots
check market.requested[0].ask.slotSize == request.ask.slotSize check market.requested[0].ask.slotSize == request.ask.slotSize
check market.requested[0].ask.duration == request.ask.duration check market.requested[0].ask.duration == request.ask.duration
check market.requested[0].ask.reward == request.ask.reward check market.requested[0].ask.pricePerBytePerSecond ==
request.ask.pricePerBytePerSecond
test "remembers purchases": test "remembers purchases":
let purchase1 = await purchasing.purchase(request) let purchase1 = await purchasing.purchase(request)
@ -131,7 +132,7 @@ checksuite "Purchasing state machine":
slots: uint8.example.uint64, slots: uint8.example.uint64,
slotSize: uint32.example.u256, slotSize: uint32.example.u256,
duration: uint16.example.u256, duration: uint16.example.u256,
reward: uint8.example.u256, pricePerBytePerSecond: uint8.example.u256,
) )
) )

View File

@ -22,7 +22,7 @@ asyncchecksuite "validation":
let validationGroups = ValidationGroups(8).some let validationGroups = ValidationGroups(8).some
let slot = Slot.example let slot = Slot.example
let proof = Groth16Proof.example let proof = Groth16Proof.example
let collateral = slot.request.ask.collateral let collateral = slot.request.ask.collateralPerSlot
var market: MockMarket var market: MockMarket
var clock: MockClock var clock: MockClock

View File

@ -19,7 +19,7 @@ ethersuite "Marketplace contracts":
var filledAt: UInt256 var filledAt: UInt256
proc expectedPayout(endTimestamp: UInt256): UInt256 = proc expectedPayout(endTimestamp: UInt256): UInt256 =
return (endTimestamp - filledAt) * request.ask.reward return (endTimestamp - filledAt) * request.ask.pricePerSlotPerSecond()
proc switchAccount(account: Signer) = proc switchAccount(account: Signer) =
marketplace = marketplace.connect(account) marketplace = marketplace.connect(account)
@ -44,10 +44,11 @@ ethersuite "Marketplace contracts":
request.client = await client.getAddress() request.client = await client.getAddress()
switchAccount(client) switchAccount(client)
discard await token.approve(marketplace.address, request.price).confirm(1) discard await token.approve(marketplace.address, request.totalPrice).confirm(1)
discard await marketplace.requestStorage(request).confirm(1) discard await marketplace.requestStorage(request).confirm(1)
switchAccount(host) switchAccount(host)
discard await token.approve(marketplace.address, request.ask.collateral).confirm(1) discard
await token.approve(marketplace.address, request.ask.collateralPerSlot).confirm(1)
discard await marketplace.reserveSlot(request.id, 0.u256).confirm(1) discard await marketplace.reserveSlot(request.id, 0.u256).confirm(1)
let receipt = await marketplace.fillSlot(request.id, 0.u256, proof).confirm(1) let receipt = await marketplace.fillSlot(request.id, 0.u256, proof).confirm(1)
filledAt = await ethProvider.blockTime(BlockTag.init(!receipt.blockNumber)) filledAt = await ethProvider.blockTime(BlockTag.init(!receipt.blockNumber))
@ -65,8 +66,9 @@ ethersuite "Marketplace contracts":
proc startContract() {.async.} = proc startContract() {.async.} =
for slotIndex in 1 ..< request.ask.slots: for slotIndex in 1 ..< request.ask.slots:
discard discard await token
await token.approve(marketplace.address, request.ask.collateral).confirm(1) .approve(marketplace.address, request.ask.collateralPerSlot)
.confirm(1)
discard await marketplace.reserveSlot(request.id, slotIndex.u256).confirm(1) discard await marketplace.reserveSlot(request.id, slotIndex.u256).confirm(1)
discard await marketplace.fillSlot(request.id, slotIndex.u256, proof).confirm(1) discard await marketplace.fillSlot(request.id, slotIndex.u256, proof).confirm(1)
@ -94,7 +96,7 @@ ethersuite "Marketplace contracts":
discard await marketplace.freeSlot(slotId).confirm(1) discard await marketplace.freeSlot(slotId).confirm(1)
let endBalance = await token.balanceOf(address) let endBalance = await token.balanceOf(address)
check endBalance == check endBalance ==
(startBalance + expectedPayout(requestEnd.u256) + request.ask.collateral) (startBalance + expectedPayout(requestEnd.u256) + request.ask.collateralPerSlot)
test "can be paid out at the end, specifying reward and collateral recipient": test "can be paid out at the end, specifying reward and collateral recipient":
switchAccount(host) switchAccount(host)
@ -114,7 +116,8 @@ ethersuite "Marketplace contracts":
check endBalanceHost == startBalanceHost check endBalanceHost == startBalanceHost
check endBalanceReward == (startBalanceReward + expectedPayout(requestEnd.u256)) check endBalanceReward == (startBalanceReward + expectedPayout(requestEnd.u256))
check endBalanceCollateral == (startBalanceCollateral + request.ask.collateral) check endBalanceCollateral ==
(startBalanceCollateral + request.ask.collateralPerSlot)
test "cannot mark proofs missing for cancelled request": test "cannot mark proofs missing for cancelled request":
let expiry = await marketplace.requestExpiry(request.id) let expiry = await marketplace.requestExpiry(request.id)

View File

@ -32,7 +32,7 @@ ethersuite "On-Chain Market":
proc expectedPayout( proc expectedPayout(
r: StorageRequest, startTimestamp: UInt256, endTimestamp: UInt256 r: StorageRequest, startTimestamp: UInt256, endTimestamp: UInt256
): UInt256 = ): UInt256 =
return (endTimestamp - startTimestamp) * r.ask.reward return (endTimestamp - startTimestamp) * r.ask.pricePerSlotPerSecond
proc switchAccount(account: Signer) = proc switchAccount(account: Signer) =
marketplace = marketplace.connect(account) marketplace = marketplace.connect(account)
@ -118,7 +118,7 @@ ethersuite "On-Chain Market":
let endBalanceClient = await token.balanceOf(clientAddress) let endBalanceClient = await token.balanceOf(clientAddress)
check endBalanceClient == (startBalanceClient + request.price) check endBalanceClient == (startBalanceClient + request.totalPrice)
test "supports request subscriptions": test "supports request subscriptions":
var receivedIds: seq[RequestId] var receivedIds: seq[RequestId]
@ -135,19 +135,19 @@ ethersuite "On-Chain Market":
test "supports filling of slots": test "supports filling of slots":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
test "can retrieve host that filled slot": test "can retrieve host that filled slot":
await market.requestStorage(request) await market.requestStorage(request)
check (await market.getHost(request.id, slotIndex)) == none Address check (await market.getHost(request.id, slotIndex)) == none Address
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
check (await market.getHost(request.id, slotIndex)) == some accounts[0] check (await market.getHost(request.id, slotIndex)) == some accounts[0]
test "supports freeing a slot": test "supports freeing a slot":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
await market.freeSlot(slotId(request.id, slotIndex)) await market.freeSlot(slotId(request.id, slotIndex))
check (await market.getHost(request.id, slotIndex)) == none Address check (await market.getHost(request.id, slotIndex)) == none Address
@ -160,7 +160,7 @@ ethersuite "On-Chain Market":
test "submits proofs": test "submits proofs":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
await advanceToNextPeriod() await advanceToNextPeriod()
await market.submitProof(slotId(request.id, slotIndex), proof) await market.submitProof(slotId(request.id, slotIndex), proof)
@ -168,7 +168,7 @@ ethersuite "On-Chain Market":
let slotId = slotId(request, slotIndex) let slotId = slotId(request, slotIndex)
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
await waitUntilProofRequired(slotId) await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime()) let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod() await advanceToNextPeriod()
@ -179,7 +179,7 @@ ethersuite "On-Chain Market":
let slotId = slotId(request, slotIndex) let slotId = slotId(request, slotIndex)
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
await waitUntilProofRequired(slotId) await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime()) let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod() await advanceToNextPeriod()
@ -195,7 +195,7 @@ ethersuite "On-Chain Market":
let subscription = await market.subscribeSlotFilled(onSlotFilled) let subscription = await market.subscribeSlotFilled(onSlotFilled)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
check eventually receivedIds == @[request.id] and receivedSlotIndices == @[ check eventually receivedIds == @[request.id] and receivedSlotIndices == @[
slotIndex slotIndex
] ]
@ -211,16 +211,16 @@ ethersuite "On-Chain Market":
let subscription = let subscription =
await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled) await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
await market.reserveSlot(request.id, otherSlot) await market.reserveSlot(request.id, otherSlot)
await market.fillSlot(request.id, otherSlot, proof, request.ask.collateral) await market.fillSlot(request.id, otherSlot, proof, request.ask.collateralPerSlot)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
check eventually receivedSlotIndices == @[slotIndex] check eventually receivedSlotIndices == @[slotIndex]
await subscription.unsubscribe() await subscription.unsubscribe()
test "supports slot freed subscriptions": test "supports slot freed subscriptions":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
var receivedRequestIds: seq[RequestId] = @[] var receivedRequestIds: seq[RequestId] = @[]
var receivedIdxs: seq[UInt256] = @[] var receivedIdxs: seq[UInt256] = @[]
proc onSlotFreed(requestId: RequestId, idx: UInt256) = proc onSlotFreed(requestId: RequestId, idx: UInt256) =
@ -269,7 +269,9 @@ ethersuite "On-Chain Market":
let subscription = await market.subscribeFulfillment(request.id, onFulfillment) let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
for slotIndex in 0 ..< request.ask.slots: for slotIndex in 0 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
check eventually receivedIds == @[request.id] check eventually receivedIds == @[request.id]
await subscription.unsubscribe() await subscription.unsubscribe()
@ -288,11 +290,13 @@ ethersuite "On-Chain Market":
for slotIndex in 0 ..< request.ask.slots: for slotIndex in 0 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
for slotIndex in 0 ..< otherRequest.ask.slots: for slotIndex in 0 ..< otherRequest.ask.slots:
await market.reserveSlot(otherRequest.id, slotIndex.u256) await market.reserveSlot(otherRequest.id, slotIndex.u256)
await market.fillSlot( await market.fillSlot(
otherRequest.id, slotIndex.u256, proof, otherRequest.ask.collateral otherRequest.id, slotIndex.u256, proof, otherRequest.ask.collateralPerSlot
) )
check eventually receivedIds == @[request.id] check eventually receivedIds == @[request.id]
@ -325,7 +329,9 @@ ethersuite "On-Chain Market":
for slotIndex in 0 ..< request.ask.slots: for slotIndex in 0 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
for slotIndex in 0 .. request.ask.maxSlotLoss: for slotIndex in 0 .. request.ask.maxSlotLoss:
let slotId = request.slotId(slotIndex.u256) let slotId = request.slotId(slotIndex.u256)
while true: while true:
@ -360,7 +366,7 @@ ethersuite "On-Chain Market":
test "supports proof submission subscriptions": test "supports proof submission subscriptions":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
await advanceToNextPeriod() await advanceToNextPeriod()
var receivedIds: seq[SlotId] var receivedIds: seq[SlotId]
proc onProofSubmission(id: SlotId) = proc onProofSubmission(id: SlotId) =
@ -388,15 +394,19 @@ ethersuite "On-Chain Market":
await market.requestStorage(request) await market.requestStorage(request)
for slotIndex in 0 ..< request.ask.slots: for slotIndex in 0 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
check (await market.requestState(request.id)) == some RequestState.Started check (await market.requestState(request.id)) == some RequestState.Started
test "can retrieve active slots": test "can retrieve active slots":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex - 1) await market.reserveSlot(request.id, slotIndex - 1)
await market.fillSlot(request.id, slotIndex - 1, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex - 1, proof, request.ask.collateralPerSlot
)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
let slotId1 = request.slotId(slotIndex - 1) let slotId1 = request.slotId(slotIndex - 1)
let slotId2 = request.slotId(slotIndex) let slotId2 = request.slotId(slotIndex)
check (await market.mySlots()) == @[slotId1, slotId2] check (await market.mySlots()) == @[slotId1, slotId2]
@ -409,7 +419,7 @@ ethersuite "On-Chain Market":
test "can retrieve request details from slot id": test "can retrieve request details from slot id":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
let slotId = request.slotId(slotIndex) let slotId = request.slotId(slotIndex)
let expected = Slot(request: request, slotIndex: slotIndex) let expected = Slot(request: request, slotIndex: slotIndex)
check (await market.getActiveSlot(slotId)) == some expected check (await market.getActiveSlot(slotId)) == some expected
@ -421,7 +431,7 @@ ethersuite "On-Chain Market":
test "retrieves correct slot state once filled": test "retrieves correct slot state once filled":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex) await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral) await market.fillSlot(request.id, slotIndex, proof, request.ask.collateralPerSlot)
let slotId = request.slotId(slotIndex) let slotId = request.slotId(slotIndex)
check (await market.slotState(slotId)) == SlotState.Filled check (await market.slotState(slotId)) == SlotState.Filled
@ -451,9 +461,9 @@ ethersuite "On-Chain Market":
await market.reserveSlot(request.id, 0.u256) await market.reserveSlot(request.id, 0.u256)
await market.reserveSlot(request.id, 1.u256) await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256) await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 0.u256, proof, request.ask.collateralPerSlot)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 1.u256, proof, request.ask.collateralPerSlot)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 2.u256, proof, request.ask.collateralPerSlot)
let slotId = request.slotId(slotIndex) let slotId = request.slotId(slotIndex)
# `market.fill` executes an `approve` tx before the `fillSlot` tx, so that's # `market.fill` executes an `approve` tx before the `fillSlot` tx, so that's
@ -471,7 +481,7 @@ ethersuite "On-Chain Market":
test "can query past SlotFilled events since given timestamp": test "can query past SlotFilled events since given timestamp":
await market.requestStorage(request) await market.requestStorage(request)
await market.reserveSlot(request.id, 0.u256) await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 0.u256, proof, request.ask.collateralPerSlot)
# The SlotFilled event will be included in the same block as # The SlotFilled event will be included in the same block as
# the fillSlot transaction. If we want to ignore the SlotFilled event # the fillSlot transaction. If we want to ignore the SlotFilled event
@ -484,8 +494,8 @@ ethersuite "On-Chain Market":
await market.reserveSlot(request.id, 1.u256) await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256) await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 1.u256, proof, request.ask.collateralPerSlot)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 2.u256, proof, request.ask.collateralPerSlot)
let events = await market.queryPastSlotFilledEvents( let events = await market.queryPastSlotFilledEvents(
fromTime = fromTime.truncate(SecondsSince1970) fromTime = fromTime.truncate(SecondsSince1970)
@ -503,9 +513,9 @@ ethersuite "On-Chain Market":
await market.reserveSlot(request.id, 0.u256) await market.reserveSlot(request.id, 0.u256)
await market.reserveSlot(request.id, 1.u256) await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256) await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 0.u256, proof, request.ask.collateralPerSlot)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 1.u256, proof, request.ask.collateralPerSlot)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 2.u256, proof, request.ask.collateralPerSlot)
await ethProvider.advanceTime(10.u256) await ethProvider.advanceTime(10.u256)
@ -531,12 +541,14 @@ ethersuite "On-Chain Market":
let address = await host.getAddress() let address = await host.getAddress()
switchAccount(host) switchAccount(host)
await market.reserveSlot(request.id, 0.u256) await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 0.u256, proof, request.ask.collateralPerSlot)
let filledAt = (await ethProvider.currentTime()) - 1.u256 let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 1 ..< request.ask.slots: for slotIndex in 1 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
let requestEnd = await market.getRequestEnd(request.id) let requestEnd = await market.getRequestEnd(request.id)
await ethProvider.advanceTimeTo(requestEnd.u256 + 1) await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
@ -546,7 +558,7 @@ ethersuite "On-Chain Market":
let endBalance = await token.balanceOf(address) let endBalance = await token.balanceOf(address)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256) let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalance == (startBalance + expectedPayout + request.ask.collateral) check endBalance == (startBalance + expectedPayout + request.ask.collateralPerSlot)
test "pays rewards to reward recipient, collateral to host": test "pays rewards to reward recipient, collateral to host":
market = OnChainMarket.new(marketplace, hostRewardRecipient.some) market = OnChainMarket.new(marketplace, hostRewardRecipient.some)
@ -556,12 +568,14 @@ ethersuite "On-Chain Market":
switchAccount(host) switchAccount(host)
await market.reserveSlot(request.id, 0.u256) await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral) await market.fillSlot(request.id, 0.u256, proof, request.ask.collateralPerSlot)
let filledAt = (await ethProvider.currentTime()) - 1.u256 let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 1 ..< request.ask.slots: for slotIndex in 1 ..< request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256) await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral) await market.fillSlot(
request.id, slotIndex.u256, proof, request.ask.collateralPerSlot
)
let requestEnd = await market.getRequestEnd(request.id) let requestEnd = await market.getRequestEnd(request.id)
await ethProvider.advanceTimeTo(requestEnd.u256 + 1) await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
@ -575,5 +589,5 @@ ethersuite "On-Chain Market":
let endBalanceReward = await token.balanceOf(hostRewardRecipient) let endBalanceReward = await token.balanceOf(hostRewardRecipient)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256) let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalanceHost == (startBalanceHost + request.ask.collateral) check endBalanceHost == (startBalanceHost + request.ask.collateralPerSlot)
check endBalanceReward == (startBalanceReward + expectedPayout) check endBalanceReward == (startBalanceReward + expectedPayout)

View File

@ -51,9 +51,9 @@ proc example*(_: type StorageRequest): StorageRequest =
slots: 4, slots: 4,
slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte slotSize: (1 * 1024 * 1024 * 1024).u256, # 1 Gigabyte
duration: (10 * 60 * 60).u256, # 10 hours duration: (10 * 60 * 60).u256, # 10 hours
collateral: 200.u256, collateralPerByte: 1.u256,
proofProbability: 4.u256, # require a proof roughly once every 4 periods proofProbability: 4.u256, # require a proof roughly once every 4 periods
reward: 84.u256, pricePerBytePerSecond: 1.u256,
maxSlotLoss: 2, # 2 slots can be freed without data considered to be lost maxSlotLoss: 2, # 2 slots can be freed without data considered to be lost
), ),
content: StorageContent( content: StorageContent(

View File

@ -105,9 +105,9 @@ proc requestStorageRaw*(
client: CodexClient, client: CodexClient,
cid: Cid, cid: Cid,
duration: UInt256, duration: UInt256,
reward: UInt256, pricePerBytePerSecond: UInt256,
proofProbability: UInt256, proofProbability: UInt256,
collateral: UInt256, collateralPerByte: UInt256,
expiry: uint = 0, expiry: uint = 0,
nodes: uint = 3, nodes: uint = 3,
tolerance: uint = 1, tolerance: uint = 1,
@ -118,9 +118,9 @@ proc requestStorageRaw*(
let json = let json =
%*{ %*{
"duration": duration, "duration": duration,
"reward": reward, "pricePerBytePerSecond": pricePerBytePerSecond,
"proofProbability": proofProbability, "proofProbability": proofProbability,
"collateral": collateral, "collateralPerByte": collateralPerByte,
"nodes": nodes, "nodes": nodes,
"tolerance": tolerance, "tolerance": tolerance,
} }
@ -134,17 +134,18 @@ proc requestStorage*(
client: CodexClient, client: CodexClient,
cid: Cid, cid: Cid,
duration: UInt256, duration: UInt256,
reward: UInt256, pricePerBytePerSecond: UInt256,
proofProbability: UInt256, proofProbability: UInt256,
expiry: uint, expiry: uint,
collateral: UInt256, collateralPerByte: UInt256,
nodes: uint = 3, nodes: uint = 3,
tolerance: uint = 1, tolerance: uint = 1,
): ?!PurchaseId = ): ?!PurchaseId =
## Call request storage REST endpoint ## Call request storage REST endpoint
## ##
let response = client.requestStorageRaw( let response = client.requestStorageRaw(
cid, duration, reward, proofProbability, collateral, expiry, nodes, tolerance cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte, expiry,
nodes, tolerance,
) )
if response.status != "200 OK": if response.status != "200 OK":
doAssert(false, response.body) doAssert(false, response.body)
@ -172,7 +173,8 @@ proc getSlots*(client: CodexClient): ?!seq[Slot] =
seq[Slot].fromJson(body) seq[Slot].fromJson(body)
proc postAvailability*( proc postAvailability*(
client: CodexClient, totalSize, duration, minPrice, maxCollateral: UInt256 client: CodexClient,
totalSize, duration, minPricePerBytePerSecond, totalCollateral: UInt256,
): ?!Availability = ): ?!Availability =
## Post sales availability endpoint ## Post sales availability endpoint
## ##
@ -181,8 +183,8 @@ proc postAvailability*(
%*{ %*{
"totalSize": totalSize, "totalSize": totalSize,
"duration": duration, "duration": duration,
"minPrice": minPrice, "minPricePerBytePerSecond": minPricePerBytePerSecond,
"maxCollateral": maxCollateral, "totalCollateral": totalCollateral,
} }
let response = client.http.post(url, $json) let response = client.http.post(url, $json)
doAssert response.status == "201 Created", doAssert response.status == "201 Created",
@ -192,7 +194,8 @@ proc postAvailability*(
proc patchAvailabilityRaw*( proc patchAvailabilityRaw*(
client: CodexClient, client: CodexClient,
availabilityId: AvailabilityId, availabilityId: AvailabilityId,
totalSize, freeSize, duration, minPrice, maxCollateral: ?UInt256 = UInt256.none, totalSize, freeSize, duration, minPricePerBytePerSecond, totalCollateral: ?UInt256 =
UInt256.none,
): Response = ): Response =
## Updates availability ## Updates availability
## ##
@ -210,25 +213,26 @@ proc patchAvailabilityRaw*(
if duration =? duration: if duration =? duration:
json["duration"] = %duration json["duration"] = %duration
if minPrice =? minPrice: if minPricePerBytePerSecond =? minPricePerBytePerSecond:
json["minPrice"] = %minPrice json["minPricePerBytePerSecond"] = %minPricePerBytePerSecond
if maxCollateral =? maxCollateral: if totalCollateral =? totalCollateral:
json["maxCollateral"] = %maxCollateral json["totalCollateral"] = %totalCollateral
client.http.patch(url, $json) client.http.patch(url, $json)
proc patchAvailability*( proc patchAvailability*(
client: CodexClient, client: CodexClient,
availabilityId: AvailabilityId, availabilityId: AvailabilityId,
totalSize, duration, minPrice, maxCollateral: ?UInt256 = UInt256.none, totalSize, duration, minPricePerBytePerSecond, totalCollateral: ?UInt256 =
UInt256.none,
): void = ): void =
let response = client.patchAvailabilityRaw( let response = client.patchAvailabilityRaw(
availabilityId, availabilityId,
totalSize = totalSize, totalSize = totalSize,
duration = duration, duration = duration,
minPrice = minPrice, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = maxCollateral, totalCollateral = totalCollateral,
) )
doAssert response.status == "200 OK", "expected 200 OK, got " & response.status doAssert response.status == "200 OK", "expected 200 OK, got " & response.status

View File

@ -45,16 +45,28 @@ template marketplacesuite*(name: string, body: untyped) =
proc periods(p: int): uint64 = proc periods(p: int): uint64 =
p.uint64 * period p.uint64 * period
proc createAvailabilities(datasetSize: int, duration: uint64) = proc slotSize(blocks: int): UInt256 =
(DefaultBlockSize * blocks.NBytes).Natural.u256
proc datasetSize(blocks, nodes, tolerance: int): UInt256 =
(nodes + tolerance).u256 * slotSize(blocks)
proc createAvailabilities(
datasetSize: UInt256,
duration: uint64,
collateralPerByte: UInt256,
minPricePerBytePerSecond: UInt256,
) =
let totalCollateral = datasetSize * collateralPerByte
# post availability to each provider # post availability to each provider
for i in 0 ..< providers().len: for i in 0 ..< providers().len:
let provider = providers()[i].client let provider = providers()[i].client
discard provider.postAvailability( discard provider.postAvailability(
totalSize = datasetSize.u256, # should match 1 slot only totalSize = datasetSize,
duration = duration.u256, duration = duration.u256,
minPrice = 300.u256, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = 200.u256, totalCollateral = totalCollateral,
) )
proc requestStorage( proc requestStorage(
@ -62,8 +74,8 @@ template marketplacesuite*(name: string, body: untyped) =
cid: Cid, cid: Cid,
proofProbability = 1, proofProbability = 1,
duration: uint64 = 12.periods, duration: uint64 = 12.periods,
reward = 400.u256, pricePerBytePerSecond = 1.u256,
collateral = 100.u256, collateralPerByte = 1.u256,
expiry: uint64 = 4.periods, expiry: uint64 = 4.periods,
nodes = providers().len, nodes = providers().len,
tolerance = 0, tolerance = 0,
@ -73,8 +85,8 @@ template marketplacesuite*(name: string, body: untyped) =
expiry = expiry.uint, expiry = expiry.uint,
duration = duration.u256, duration = duration.u256,
proofProbability = proofProbability.u256, proofProbability = proofProbability.u256,
collateral = collateral, collateralPerByte = collateralPerByte,
reward = reward, pricePerBytePerSecond = pricePerBytePerSecond,
nodes = nodes.uint, nodes = nodes.uint,
tolerance = tolerance.uint, tolerance = tolerance.uint,
).get ).get

View File

@ -19,9 +19,9 @@ marketplacesuite "Bug #821 - node crashes during erasure coding":
# .withLogTopics("node", "marketplace", "sales", "reservations", "node", "proving", "clock") # .withLogTopics("node", "marketplace", "sales", "reservations", "node", "proving", "clock")
.some, .some,
): ):
let reward = 400.u256 let pricePerBytePerSecond = 1.u256
let duration = 20.periods let duration = 20.periods
let collateral = 200.u256 let collateralPerByte = 1.u256
let expiry = 10.periods let expiry = 10.periods
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = 8)
let client = clients()[0] let client = clients()[0]
@ -40,9 +40,9 @@ marketplacesuite "Bug #821 - node crashes during erasure coding":
let id = await clientApi.requestStorage( let id = await clientApi.requestStorage(
cid, cid,
duration = duration, duration = duration,
reward = reward, pricePerBytePerSecond = pricePerBytePerSecond,
expiry = expiry, expiry = expiry,
collateral = collateral, collateralPerByte = collateralPerByte,
nodes = 3, nodes = 3,
tolerance = 1, tolerance = 1,
) )

View File

@ -16,6 +16,12 @@ marketplacesuite "Marketplace":
var client: CodexClient var client: CodexClient
var clientAccount: Address var clientAccount: Address
const minPricePerBytePerSecond = 1.u256
const collateralPerByte = 1.u256
const blocks = 8
const ecNodes = 3
const ecTolerance = 1
setup: setup:
host = providers()[0].client host = providers()[0].client
hostAccount = providers()[0].ethAccount hostAccount = providers()[0].ethAccount
@ -29,13 +35,13 @@ marketplacesuite "Marketplace":
test "nodes negotiate contracts on the marketplace", marketplaceConfig: test "nodes negotiate contracts on the marketplace", marketplaceConfig:
let size = 0xFFFFFF.u256 let size = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
# host makes storage available # host makes storage available
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = size, totalSize = size,
duration = 20 * 60.u256, duration = 20 * 60.u256,
minPrice = 300.u256, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = 300.u256, totalCollateral = size * minPricePerBytePerSecond,
).get ).get
# client requests storage # client requests storage
@ -43,12 +49,12 @@ marketplacesuite "Marketplace":
let id = client.requestStorage( let id = client.requestStorage(
cid, cid,
duration = 20 * 60.u256, duration = 20 * 60.u256,
reward = 400.u256, pricePerBytePerSecond = minPricePerBytePerSecond,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 10 * 60, expiry = 10 * 60,
collateral = 200.u256, collateralPerByte = collateralPerByte,
nodes = 3, nodes = ecNodes,
tolerance = 1, tolerance = ecTolerance,
).get ).get
check eventually(client.purchaseStateIs(id, "started"), timeout = 10 * 60 * 1000) check eventually(client.purchaseStateIs(id, "started"), timeout = 10 * 60 * 1000)
@ -66,21 +72,19 @@ marketplacesuite "Marketplace":
test "node slots gets paid out and rest of tokens are returned to client", test "node slots gets paid out and rest of tokens are returned to client",
marketplaceConfig: marketplaceConfig:
let size = 0xFFFFFF.u256 let size = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
let marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner()) let marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
let tokenAddress = await marketplace.token() let tokenAddress = await marketplace.token()
let token = Erc20Token.new(tokenAddress, ethProvider.getSigner()) let token = Erc20Token.new(tokenAddress, ethProvider.getSigner())
let reward = 400.u256
let duration = 20 * 60.u256 let duration = 20 * 60.u256
let nodes = 3'u
# host makes storage available # host makes storage available
let startBalanceHost = await token.balanceOf(hostAccount) let startBalanceHost = await token.balanceOf(hostAccount)
discard host.postAvailability( discard host.postAvailability(
totalSize = size, totalSize = size,
duration = 20 * 60.u256, duration = 20 * 60.u256,
minPrice = 300.u256, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = 300.u256, totalCollateral = size * minPricePerBytePerSecond,
).get ).get
# client requests storage # client requests storage
@ -88,12 +92,12 @@ marketplacesuite "Marketplace":
let id = client.requestStorage( let id = client.requestStorage(
cid, cid,
duration = duration, duration = duration,
reward = reward, pricePerBytePerSecond = minPricePerBytePerSecond,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 10 * 60, expiry = 10 * 60,
collateral = 200.u256, collateralPerByte = collateralPerByte,
nodes = nodes, nodes = ecNodes,
tolerance = 1, tolerance = ecTolerance,
).get ).get
check eventually(client.purchaseStateIs(id, "started"), timeout = 10 * 60 * 1000) check eventually(client.purchaseStateIs(id, "started"), timeout = 10 * 60 * 1000)
@ -108,8 +112,10 @@ marketplacesuite "Marketplace":
await ethProvider.advanceTime(duration) await ethProvider.advanceTime(duration)
# Checking that the hosting node received reward for at least the time between <expiry;end> # Checking that the hosting node received reward for at least the time between <expiry;end>
let slotSize = slotSize(blocks)
let pricePerSlotPerSecond = minPricePerBytePerSecond * slotSize
check eventually (await token.balanceOf(hostAccount)) - startBalanceHost >= check eventually (await token.balanceOf(hostAccount)) - startBalanceHost >=
(duration - 5 * 60) * reward * nodes.u256 (duration - 5 * 60) * pricePerSlotPerSecond * ecNodes.u256
# Checking that client node receives some funds back that were not used for the host nodes # Checking that client node receives some funds back that were not used for the host nodes
check eventually( check eventually(
@ -118,6 +124,12 @@ marketplacesuite "Marketplace":
) )
marketplacesuite "Marketplace payouts": marketplacesuite "Marketplace payouts":
const minPricePerBytePerSecond = 1.u256
const collateralPerByte = 1.u256
const blocks = 8
const ecNodes = 3
const ecTolerance = 1
test "expired request partially pays out for stored time", test "expired request partially pays out for stored time",
NodeConfigs( NodeConfigs(
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally # Uncomment to start Hardhat automatically, typically so logs can be inspected locally
@ -133,11 +145,9 @@ marketplacesuite "Marketplace payouts":
# .withLogTopics("node", "marketplace", "sales", "reservations", "node", "proving", "clock") # .withLogTopics("node", "marketplace", "sales", "reservations", "node", "proving", "clock")
.some, .some,
): ):
let reward = 400.u256
let duration = 20.periods let duration = 20.periods
let collateral = 200.u256
let expiry = 10.periods let expiry = 10.periods
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
let client = clients()[0] let client = clients()[0]
let provider = providers()[0] let provider = providers()[0]
let clientApi = client.client let clientApi = client.client
@ -146,13 +156,15 @@ marketplacesuite "Marketplace payouts":
let startBalanceClient = await token.balanceOf(client.ethAccount) let startBalanceClient = await token.balanceOf(client.ethAccount)
# provider makes storage available # provider makes storage available
let datasetSize = datasetSize(blocks, ecNodes, ecTolerance)
let totalAvailabilitySize = datasetSize div 2
discard providerApi.postAvailability( discard providerApi.postAvailability(
# make availability size small enough that we can't fill all the slots, # make availability size small enough that we can't fill all the slots,
# thus causing a cancellation # thus causing a cancellation
totalSize = (data.len div 2).u256, totalSize = totalAvailabilitySize,
duration = duration.u256, duration = duration.u256,
minPrice = reward, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = collateral, totalCollateral = collateralPerByte * totalAvailabilitySize,
) )
let cid = clientApi.upload(data).get let cid = clientApi.upload(data).get
@ -168,11 +180,11 @@ marketplacesuite "Marketplace payouts":
let id = await clientApi.requestStorage( let id = await clientApi.requestStorage(
cid, cid,
duration = duration, duration = duration,
reward = reward, pricePerBytePerSecond = minPricePerBytePerSecond,
expiry = expiry, expiry = expiry,
collateral = collateral, collateralPerByte = collateralPerByte,
nodes = 3, nodes = ecNodes,
tolerance = 1, tolerance = ecTolerance,
) )
# wait until one slot is filled # wait until one slot is filled
@ -185,10 +197,13 @@ marketplacesuite "Marketplace payouts":
await advanceToNextPeriod() await advanceToNextPeriod()
let slotSize = slotSize(blocks)
let pricePerSlotPerSecond = minPricePerBytePerSecond * slotSize
check eventually ( check eventually (
let endBalanceProvider = (await token.balanceOf(provider.ethAccount)) let endBalanceProvider = (await token.balanceOf(provider.ethAccount))
endBalanceProvider > startBalanceProvider and endBalanceProvider > startBalanceProvider and
endBalanceProvider < startBalanceProvider + expiry.u256 * reward endBalanceProvider < startBalanceProvider + expiry.u256 * pricePerSlotPerSecond
) )
check eventually( check eventually(
( (

View File

@ -15,6 +15,12 @@ logScope:
topics = "integration test proofs" topics = "integration test proofs"
marketplacesuite "Hosts submit regular proofs": marketplacesuite "Hosts submit regular proofs":
const minPricePerBytePerSecond = 1.u256
const collateralPerByte = 1.u256
const blocks = 8
const ecNodes = 3
const ecTolerance = 1
test "hosts submit periodic proofs for slots they fill", test "hosts submit periodic proofs for slots they fill",
NodeConfigs( NodeConfigs(
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally # Uncomment to start Hardhat automatically, typically so logs can be inspected locally
@ -34,14 +40,29 @@ marketplacesuite "Hosts submit regular proofs":
let expiry = 10.periods let expiry = 10.periods
let duration = expiry + 5.periods let duration = expiry + 5.periods
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
createAvailabilities(data.len * 2, duration) # TODO: better value for data.len let datasetSize =
datasetSize(blocks = blocks, nodes = ecNodes, tolerance = ecTolerance)
createAvailabilities(
datasetSize, duration, collateralPerByte, minPricePerBytePerSecond
)
let cid = client0.upload(data).get let cid = client0.upload(data).get
let purchaseId = await client0.requestStorage( let purchaseId = await client0.requestStorage(
cid, expiry = expiry, duration = duration, nodes = 3, tolerance = 1 cid,
expiry = expiry,
duration = duration,
nodes = ecNodes,
tolerance = ecTolerance,
) )
let purchase = client0.getPurchase(purchaseId).get
check purchase.error == none string
let request = purchase.request.get
let slotSize = request.ask.slotSize
check eventually( check eventually(
client0.purchaseStateIs(purchaseId, "started"), timeout = expiry.int * 1000 client0.purchaseStateIs(purchaseId, "started"), timeout = expiry.int * 1000
) )
@ -62,6 +83,12 @@ marketplacesuite "Simulate invalid proofs":
# tightened so that they are showing, as an integration test, that specific # tightened so that they are showing, as an integration test, that specific
# proofs are being marked as missed by the validator. # proofs are being marked as missed by the validator.
const minPricePerBytePerSecond = 1.u256
const collateralPerByte = 1.u256
const blocks = 8
const ecNodes = 3
const ecTolerance = 1
test "slot is freed after too many invalid proofs submitted", test "slot is freed after too many invalid proofs submitted",
NodeConfigs( NodeConfigs(
# Uncomment to start Hardhat automatically, typically so logs can be inspected locally # Uncomment to start Hardhat automatically, typically so logs can be inspected locally
@ -88,8 +115,12 @@ marketplacesuite "Simulate invalid proofs":
let expiry = 10.periods let expiry = 10.periods
let duration = expiry + 10.periods let duration = expiry + 10.periods
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
createAvailabilities(data.len * 2, duration) # TODO: better value for data.len let datasetSize =
datasetSize(blocks = blocks, nodes = ecNodes, tolerance = ecTolerance)
createAvailabilities(
datasetSize, duration, collateralPerByte, minPricePerBytePerSecond
)
let cid = client0.upload(data).get let cid = client0.upload(data).get
@ -97,8 +128,8 @@ marketplacesuite "Simulate invalid proofs":
cid, cid,
expiry = expiry, expiry = expiry,
duration = duration, duration = duration,
nodes = 3, nodes = ecNodes,
tolerance = 1, tolerance = ecTolerance,
proofProbability = 1, proofProbability = 1,
) )
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get
@ -144,8 +175,12 @@ marketplacesuite "Simulate invalid proofs":
let expiry = 10.periods let expiry = 10.periods
let duration = expiry + 10.periods let duration = expiry + 10.periods
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
createAvailabilities(data.len * 2, duration) # TODO: better value for data.len let datasetSize =
datasetSize(blocks = blocks, nodes = ecNodes, tolerance = ecTolerance)
createAvailabilities(
datasetSize, duration, collateralPerByte, minPricePerBytePerSecond
)
let cid = client0.upload(data).get let cid = client0.upload(data).get
@ -153,8 +188,8 @@ marketplacesuite "Simulate invalid proofs":
cid, cid,
expiry = expiry, expiry = expiry,
duration = duration, duration = duration,
nodes = 3, nodes = ecNodes,
tolerance = 1, tolerance = ecTolerance,
proofProbability = 1, proofProbability = 1,
) )
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get
@ -187,6 +222,9 @@ marketplacesuite "Simulate invalid proofs":
await freedSubscription.unsubscribe() await freedSubscription.unsubscribe()
# TODO: uncomment once fixed # TODO: uncomment once fixed
# WARNING: in the meantime minPrice has changed to minPricePerBytePerSecond
# and maxCollateral has changed to totalCollateral - double check if
# it is set correctly below
# test "host that submits invalid proofs is paid out less", NodeConfigs( # test "host that submits invalid proofs is paid out less", NodeConfigs(
# # Uncomment to start Hardhat automatically, typically so logs can be inspected locally # # Uncomment to start Hardhat automatically, typically so logs can be inspected locally
# # hardhat: HardhatConfig().withLogFile(), # # hardhat: HardhatConfig().withLogFile(),
@ -227,8 +265,8 @@ marketplacesuite "Simulate invalid proofs":
# discard provider0.client.postAvailability( # discard provider0.client.postAvailability(
# totalSize=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPricePerBytePerSecond=minPricePerBytePerSecond,
# maxCollateral=200.u256 # totalCollateral=slotSize * minPricePerBytePerSecond
# ) # )
# let cid = client0.upload(data).get # let cid = client0.upload(data).get
@ -260,8 +298,8 @@ marketplacesuite "Simulate invalid proofs":
# discard provider1.client.postAvailability( # discard provider1.client.postAvailability(
# totalSize=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPricePerBytePerSecond=minPricePerBytePerSecond,
# maxCollateral=200.u256 # totalCollateral=slotSize * minPricePerBytePerSecond
# ) # )
# check eventually filledSlotIds.len > 1 # check eventually filledSlotIds.len > 1
@ -269,8 +307,8 @@ marketplacesuite "Simulate invalid proofs":
# discard provider2.client.postAvailability( # discard provider2.client.postAvailability(
# totalSize=slotSize, # should match 1 slot only # totalSize=slotSize, # should match 1 slot only
# duration=totalPeriods.periods.u256, # duration=totalPeriods.periods.u256,
# minPrice=300.u256, # minPricePerBytePerSecond=minPricePerBytePerSecond,
# maxCollateral=200.u256 # totalCollateral=slotSize * minPricePerBytePerSecond
# ) # )
# check eventually filledSlotIds.len > 2 # check eventually filledSlotIds.len > 2

View File

@ -12,18 +12,18 @@ twonodessuite "Purchasing":
let id1 = client1.requestStorage( let id1 = client1.requestStorage(
cid, cid,
duration = 100.u256, duration = 100.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 10, expiry = 10,
collateral = 200.u256, collateralPerByte = 1.u256,
).get ).get
let id2 = client1.requestStorage( let id2 = client1.requestStorage(
cid, cid,
duration = 400.u256, duration = 400.u256,
reward = 5.u256, pricePerBytePerSecond = 2.u256,
proofProbability = 6.u256, proofProbability = 6.u256,
expiry = 10, expiry = 10,
collateral = 201.u256, collateralPerByte = 2.u256,
).get ).get
check id1 != id2 check id1 != id2
@ -38,33 +38,36 @@ twonodessuite "Purchasing":
let id = client1.requestStorage( let id = client1.requestStorage(
cid, cid,
duration = 100.u256, duration = 100.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 30, expiry = 30,
collateral = 200.u256, collateralPerByte = 1.u256,
nodes = 3, nodes = 3,
tolerance = 1, tolerance = 1,
).get ).get
let request = client1.getPurchase(id).get.request.get let request = client1.getPurchase(id).get.request.get
check request.ask.duration == 100.u256 check request.ask.duration == 100.u256
check request.ask.reward == 2.u256 check request.ask.pricePerBytePerSecond == 1.u256
check request.ask.proofProbability == 3.u256 check request.ask.proofProbability == 3.u256
check request.expiry == 30 check request.expiry == 30
check request.ask.collateral == 200.u256 check request.ask.collateralPerByte == 1.u256
check request.ask.slots == 3'u64 check request.ask.slots == 3'u64
check request.ask.maxSlotLoss == 1'u64 check request.ask.maxSlotLoss == 1'u64
# TODO: We currently do not support encoding single chunks # TODO: We currently do not support encoding single chunks
# test "node retrieves purchase status with 1 chunk", twoNodesConfig: # test "node retrieves purchase status with 1 chunk", twoNodesConfig:
# let cid = client1.upload("some file contents").get # let cid = client1.upload("some file contents").get
# let id = client1.requestStorage(cid, duration=1.u256, reward=2.u256, proofProbability=3.u256, expiry=30, collateral=200.u256, nodes=2, tolerance=1).get # let id = client1.requestStorage(
# cid, duration=1.u256, pricePerBytePerSecond=1.u256,
# proofProbability=3.u256, expiry=30, collateralPerByte=1.u256,
# nodes=2, tolerance=1).get
# let request = client1.getPurchase(id).get.request.get # let request = client1.getPurchase(id).get.request.get
# check request.ask.duration == 1.u256 # check request.ask.duration == 1.u256
# check request.ask.reward == 2.u256 # check request.ask.pricePerBytePerSecond == 1.u256
# check request.ask.proofProbability == 3.u256 # check request.ask.proofProbability == 3.u256
# check request.expiry == 30 # check request.expiry == 30
# check request.ask.collateral == 200.u256 # check request.ask.collateralPerByte == 1.u256
# check request.ask.slots == 3'u64 # check request.ask.slots == 3'u64
# check request.ask.maxSlotLoss == 1'u64 # check request.ask.maxSlotLoss == 1'u64
@ -74,10 +77,10 @@ twonodessuite "Purchasing":
let id = client1.requestStorage( let id = client1.requestStorage(
cid, cid,
duration = 10 * 60.u256, duration = 10 * 60.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 5 * 60, expiry = 5 * 60,
collateral = 200.u256, collateralPerByte = 1.u256,
nodes = 3.uint, nodes = 3.uint,
tolerance = 1.uint, tolerance = 1.uint,
).get ).get
@ -89,10 +92,10 @@ twonodessuite "Purchasing":
check eventually(client1.purchaseStateIs(id, "submitted"), timeout = 3 * 60 * 1000) check eventually(client1.purchaseStateIs(id, "submitted"), timeout = 3 * 60 * 1000)
let request = client1.getPurchase(id).get.request.get let request = client1.getPurchase(id).get.request.get
check request.ask.duration == (10 * 60).u256 check request.ask.duration == (10 * 60).u256
check request.ask.reward == 2.u256 check request.ask.pricePerBytePerSecond == 1.u256
check request.ask.proofProbability == 3.u256 check request.ask.proofProbability == 3.u256
check request.expiry == (5 * 60).u256 check request.expiry == (5 * 60).u256
check request.ask.collateral == 200.u256 check request.ask.collateralPerByte == 1.u256
check request.ask.slots == 3'u64 check request.ask.slots == 3'u64
check request.ask.maxSlotLoss == 1'u64 check request.ask.maxSlotLoss == 1'u64
@ -103,9 +106,9 @@ twonodessuite "Purchasing":
let responseMissing = client1.requestStorageRaw( let responseMissing = client1.requestStorageRaw(
cid, cid,
duration = 1.u256, duration = 1.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
) )
check responseMissing.status == "400 Bad Request" check responseMissing.status == "400 Bad Request"
check responseMissing.body == "Expiry required" check responseMissing.body == "Expiry required"
@ -113,9 +116,9 @@ twonodessuite "Purchasing":
let responseBefore = client1.requestStorageRaw( let responseBefore = client1.requestStorageRaw(
cid, cid,
duration = 10.u256, duration = 10.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
expiry = 10, expiry = 10,
) )
check responseBefore.status == "400 Bad Request" check responseBefore.status == "400 Bad Request"

View File

@ -21,8 +21,14 @@ twonodessuite "REST API":
test "node shows used and available space", twoNodesConfig: test "node shows used and available space", twoNodesConfig:
discard client1.upload("some file contents").get discard client1.upload("some file contents").get
let totalSize = 12.u256
let minPricePerBytePerSecond = 1.u256
let totalCollateral = totalSize * minPricePerBytePerSecond
discard client1.postAvailability( discard client1.postAvailability(
totalSize = 12.u256, duration = 2.u256, minPrice = 3.u256, maxCollateral = 4.u256 totalSize = totalSize,
duration = 2.u256,
minPricePerBytePerSecond = minPricePerBytePerSecond,
totalCollateral = totalCollateral,
).get ).get
let space = client1.space().tryGet() let space = client1.space().tryGet()
check: check:
@ -47,9 +53,9 @@ twonodessuite "REST API":
let response = client1.requestStorageRaw( let response = client1.requestStorageRaw(
cid, cid,
duration = 10.u256, duration = 10.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
expiry = 9, expiry = 9,
) )
@ -65,9 +71,9 @@ twonodessuite "REST API":
let response = client1.requestStorageRaw( let response = client1.requestStorageRaw(
cid, cid,
duration = 10.u256, duration = 10.u256,
reward = 2.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
expiry = 9, expiry = 9,
) )
@ -78,16 +84,16 @@ twonodessuite "REST API":
let data = await RandomChunker.example(blocks = 2) let data = await RandomChunker.example(blocks = 2)
let cid = client1.upload(data).get let cid = client1.upload(data).get
let duration = 100.u256 let duration = 100.u256
let reward = 2.u256 let pricePerBytePerSecond = 1.u256
let proofProbability = 3.u256 let proofProbability = 3.u256
let expiry = 30.uint let expiry = 30.uint
let collateral = 200.u256 let collateralPerByte = 1.u256
let nodes = 3 let nodes = 3
let tolerance = 0 let tolerance = 0
var responseBefore = client1.requestStorageRaw( var responseBefore = client1.requestStorageRaw(
cid, duration, reward, proofProbability, collateral, expiry, nodes.uint, cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte, expiry,
tolerance.uint, nodes.uint, tolerance.uint,
) )
check responseBefore.status == "400 Bad Request" check responseBefore.status == "400 Bad Request"
@ -97,18 +103,18 @@ twonodessuite "REST API":
let data = await RandomChunker.example(blocks = 2) let data = await RandomChunker.example(blocks = 2)
let cid = client1.upload(data).get let cid = client1.upload(data).get
let duration = 100.u256 let duration = 100.u256
let reward = 2.u256 let pricePerBytePerSecond = 1.u256
let proofProbability = 3.u256 let proofProbability = 3.u256
let expiry = 30.uint let expiry = 30.uint
let collateral = 200.u256 let collateralPerByte = 1.u256
let ecParams = @[(1, 1), (2, 1), (3, 2), (3, 3)] let ecParams = @[(1, 1), (2, 1), (3, 2), (3, 3)]
for ecParam in ecParams: for ecParam in ecParams:
let (nodes, tolerance) = ecParam let (nodes, tolerance) = ecParam
var responseBefore = client1.requestStorageRaw( var responseBefore = client1.requestStorageRaw(
cid, duration, reward, proofProbability, collateral, expiry, nodes.uint, cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte,
tolerance.uint, expiry, nodes.uint, tolerance.uint,
) )
check responseBefore.status == "400 Bad Request" check responseBefore.status == "400 Bad Request"
@ -120,18 +126,18 @@ twonodessuite "REST API":
let data = await RandomChunker.example(blocks = 2) let data = await RandomChunker.example(blocks = 2)
let cid = client1.upload(data).get let cid = client1.upload(data).get
let duration = 100.u256 let duration = 100.u256
let reward = 2.u256 let pricePerBytePerSecond = 1.u256
let proofProbability = 3.u256 let proofProbability = 3.u256
let expiry = 30.uint let expiry = 30.uint
let collateral = 200.u256 let collateralPerByte = 1.u256
let ecParams = @[(0, 1), (1, 2), (2, 3)] let ecParams = @[(0, 1), (1, 2), (2, 3)]
for ecParam in ecParams: for ecParam in ecParams:
let (nodes, tolerance) = ecParam let (nodes, tolerance) = ecParam
var responseBefore = client1.requestStorageRaw( var responseBefore = client1.requestStorageRaw(
cid, duration, reward, proofProbability, collateral, expiry, nodes.uint, cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte,
tolerance.uint, expiry, nodes.uint, tolerance.uint,
) )
check responseBefore.status == "400 Bad Request" check responseBefore.status == "400 Bad Request"
@ -142,18 +148,18 @@ twonodessuite "REST API":
let data = await RandomChunker.example(blocks = 2) let data = await RandomChunker.example(blocks = 2)
let cid = client1.upload(data).get let cid = client1.upload(data).get
let duration = 100.u256 let duration = 100.u256
let reward = 2.u256 let pricePerBytePerSecond = 1.u256
let proofProbability = 3.u256 let proofProbability = 3.u256
let expiry = 30.uint let expiry = 30.uint
let collateral = 200.u256 let collateralPerByte = 1.u256
let ecParams = @[(3, 1), (5, 2)] let ecParams = @[(3, 1), (5, 2)]
for ecParam in ecParams: for ecParam in ecParams:
let (nodes, tolerance) = ecParam let (nodes, tolerance) = ecParam
var responseBefore = client1.requestStorageRaw( var responseBefore = client1.requestStorageRaw(
cid, duration, reward, proofProbability, collateral, expiry, nodes.uint, cid, duration, pricePerBytePerSecond, proofProbability, collateralPerByte,
tolerance.uint, expiry, nodes.uint, tolerance.uint,
) )
check responseBefore.status == "200 OK" check responseBefore.status == "200 OK"

View File

@ -20,6 +20,8 @@ multinodesuite "Sales":
providers: CodexConfigs.init(nodes = 1).some, providers: CodexConfigs.init(nodes = 1).some,
) )
let minPricePerBytePerSecond = 1.u256
var host: CodexClient var host: CodexClient
var client: CodexClient var client: CodexClient
@ -29,16 +31,25 @@ multinodesuite "Sales":
test "node handles new storage availability", salesConfig: test "node handles new storage availability", salesConfig:
let availability1 = host.postAvailability( let availability1 = host.postAvailability(
totalSize = 1.u256, duration = 2.u256, minPrice = 3.u256, maxCollateral = 4.u256 totalSize = 1.u256,
duration = 2.u256,
minPricePerBytePerSecond = 3.u256,
totalCollateral = 4.u256,
).get ).get
let availability2 = host.postAvailability( let availability2 = host.postAvailability(
totalSize = 4.u256, duration = 5.u256, minPrice = 6.u256, maxCollateral = 7.u256 totalSize = 4.u256,
duration = 5.u256,
minPricePerBytePerSecond = 6.u256,
totalCollateral = 7.u256,
).get ).get
check availability1 != availability2 check availability1 != availability2
test "node lists storage that is for sale", salesConfig: test "node lists storage that is for sale", salesConfig:
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = 1.u256, duration = 2.u256, minPrice = 3.u256, maxCollateral = 4.u256 totalSize = 1.u256,
duration = 2.u256,
minPricePerBytePerSecond = 3.u256,
totalCollateral = 4.u256,
).get ).get
check availability in host.getAvailabilities().get check availability in host.getAvailabilities().get
@ -46,8 +57,8 @@ multinodesuite "Sales":
let nonExistingResponse = host.patchAvailabilityRaw( let nonExistingResponse = host.patchAvailabilityRaw(
AvailabilityId.example, AvailabilityId.example,
duration = 100.u256.some, duration = 100.u256.some,
minPrice = 200.u256.some, minPricePerBytePerSecond = 2.u256.some,
maxCollateral = 200.u256.some, totalCollateral = 200.u256.some,
) )
check nonExistingResponse.status == "404 Not Found" check nonExistingResponse.status == "404 Not Found"
@ -55,21 +66,21 @@ multinodesuite "Sales":
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = 140000.u256, totalSize = 140000.u256,
duration = 200.u256, duration = 200.u256,
minPrice = 300.u256, minPricePerBytePerSecond = 3.u256,
maxCollateral = 300.u256, totalCollateral = 300.u256,
).get ).get
host.patchAvailability( host.patchAvailability(
availability.id, availability.id,
duration = 100.u256.some, duration = 100.u256.some,
minPrice = 200.u256.some, minPricePerBytePerSecond = 2.u256.some,
maxCollateral = 200.u256.some, totalCollateral = 200.u256.some,
) )
let updatedAvailability = (host.getAvailabilities().get).findItem(availability).get let updatedAvailability = (host.getAvailabilities().get).findItem(availability).get
check updatedAvailability.duration == 100 check updatedAvailability.duration == 100
check updatedAvailability.minPrice == 200 check updatedAvailability.minPricePerBytePerSecond == 2
check updatedAvailability.maxCollateral == 200 check updatedAvailability.totalCollateral == 200
check updatedAvailability.totalSize == 140000 check updatedAvailability.totalSize == 140000
check updatedAvailability.freeSize == 140000 check updatedAvailability.freeSize == 140000
@ -77,8 +88,8 @@ multinodesuite "Sales":
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = 140000.u256, totalSize = 140000.u256,
duration = 200.u256, duration = 200.u256,
minPrice = 300.u256, minPricePerBytePerSecond = 3.u256,
maxCollateral = 300.u256, totalCollateral = 300.u256,
).get ).get
let freeSizeResponse = let freeSizeResponse =
host.patchAvailabilityRaw(availability.id, freeSize = 110000.u256.some) host.patchAvailabilityRaw(availability.id, freeSize = 110000.u256.some)
@ -89,8 +100,8 @@ multinodesuite "Sales":
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = 140000.u256, totalSize = 140000.u256,
duration = 200.u256, duration = 200.u256,
minPrice = 300.u256, minPricePerBytePerSecond = 3.u256,
maxCollateral = 300.u256, totalCollateral = 300.u256,
).get ).get
host.patchAvailability(availability.id, totalSize = 100000.u256.some) host.patchAvailability(availability.id, totalSize = 100000.u256.some)
let updatedAvailability = (host.getAvailabilities().get).findItem(availability).get let updatedAvailability = (host.getAvailabilities().get).findItem(availability).get
@ -101,11 +112,14 @@ multinodesuite "Sales":
salesConfig: salesConfig:
let originalSize = 0xFFFFFF.u256 let originalSize = 0xFFFFFF.u256
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = 8)
let minPricePerBytePerSecond = 3.u256
let collateralPerByte = 1.u256
let totalCollateral = originalSize * collateralPerByte
let availability = host.postAvailability( let availability = host.postAvailability(
totalSize = originalSize, totalSize = originalSize,
duration = 20 * 60.u256, duration = 20 * 60.u256,
minPrice = 300.u256, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = 300.u256, totalCollateral = totalCollateral,
).get ).get
# Lets create storage request that will utilize some of the availability's space # Lets create storage request that will utilize some of the availability's space
@ -113,10 +127,10 @@ multinodesuite "Sales":
let id = client.requestStorage( let id = client.requestStorage(
cid, cid,
duration = 20 * 60.u256, duration = 20 * 60.u256,
reward = 400.u256, pricePerBytePerSecond = minPricePerBytePerSecond,
proofProbability = 3.u256, proofProbability = 3.u256,
expiry = 10 * 60, expiry = 10 * 60,
collateral = 200.u256, collateralPerByte = collateralPerByte,
nodes = 3, nodes = 3,
tolerance = 1, tolerance = 1,
).get ).get

View File

@ -34,9 +34,13 @@ template eventuallyS(
await eventuallyS() await eventuallyS()
marketplacesuite "Validation": marketplacesuite "Validation":
let nodes = 3 const blocks = 8
let tolerance = 1 const ecNodes = 3
let proofProbability = 1 const ecTolerance = 1
const proofProbability = 1
const collateralPerByte = 1.u256
const minPricePerBytePerSecond = 1.u256
proc waitForRequestToFail( proc waitForRequestToFail(
marketplace: Marketplace, requestId: RequestId, timeout = 10, step = 5 marketplace: Marketplace, requestId: RequestId, timeout = 10, step = 5
@ -92,19 +96,20 @@ marketplacesuite "Validation":
var currentTime = await ethProvider.currentTime() var currentTime = await ethProvider.currentTime()
let requestEndTime = currentTime.truncate(uint64) + duration let requestEndTime = currentTime.truncate(uint64) + duration
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
let datasetSize =
# TODO: better value for data.len below. This TODO is also present in datasetSize(blocks = blocks, nodes = ecNodes, tolerance = ecTolerance)
# testproofs.nim - we may want to address it or remove the comment. createAvailabilities(
createAvailabilities(data.len * 2, duration) datasetSize, duration, collateralPerByte, minPricePerBytePerSecond
)
let cid = client0.upload(data).get let cid = client0.upload(data).get
let purchaseId = await client0.requestStorage( let purchaseId = await client0.requestStorage(
cid, cid,
expiry = expiry, expiry = expiry,
duration = duration, duration = duration,
nodes = nodes, nodes = ecNodes,
tolerance = tolerance, tolerance = ecTolerance,
proofProbability = proofProbability, proofProbability = proofProbability,
) )
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get
@ -158,19 +163,20 @@ marketplacesuite "Validation":
var currentTime = await ethProvider.currentTime() var currentTime = await ethProvider.currentTime()
let requestEndTime = currentTime.truncate(uint64) + duration let requestEndTime = currentTime.truncate(uint64) + duration
let data = await RandomChunker.example(blocks = 8) let data = await RandomChunker.example(blocks = blocks)
let datasetSize =
# TODO: better value for data.len below. This TODO is also present in datasetSize(blocks = blocks, nodes = ecNodes, tolerance = ecTolerance)
# testproofs.nim - we may want to address it or remove the comment. createAvailabilities(
createAvailabilities(data.len * 2, duration) datasetSize, duration, collateralPerByte, minPricePerBytePerSecond
)
let cid = client0.upload(data).get let cid = client0.upload(data).get
let purchaseId = await client0.requestStorage( let purchaseId = await client0.requestStorage(
cid, cid,
expiry = expiry, expiry = expiry,
duration = duration, duration = duration,
nodes = nodes, nodes = ecNodes,
tolerance = tolerance, tolerance = ecTolerance,
proofProbability = proofProbability, proofProbability = proofProbability,
) )
let requestId = client0.requestId(purchaseId).get let requestId = client0.requestId(purchaseId).get

View File

@ -46,11 +46,14 @@ suite "Taiko L2 Integration Tests":
node2.removeDataDir() node2.removeDataDir()
test "node 1 buys storage from node 2": test "node 1 buys storage from node 2":
let size = 0xFFFFF.u256
let minPricePerBytePerSecond = 1.u256
let totalCollateral = size * minPricePerBytePerSecond
discard node2.client.postAvailability( discard node2.client.postAvailability(
size = 0xFFFFF.u256, size = size,
duration = 200.u256, duration = 200.u256,
minPrice = 300.u256, minPricePerBytePerSecond = minPricePerBytePerSecond,
maxCollateral = 300.u256, totalCollateral = totalCollateral,
) )
let cid = !node1.client.upload("some file contents") let cid = !node1.client.upload("some file contents")
@ -60,9 +63,9 @@ suite "Taiko L2 Integration Tests":
!node1.client.requestStorage( !node1.client.requestStorage(
cid, cid,
duration = 30.u256, duration = 30.u256,
reward = 400.u256, pricePerBytePerSecond = 1.u256,
proofProbability = 3.u256, proofProbability = 3.u256,
collateral = 200.u256, collateralPerByte = 1.u256,
expiry = expiry.u256, expiry = expiry.u256,
) )

@ -1 +1 @@
Subproject commit 02e3b8d22b15975fd450ccdad30e32c0be7e593f Subproject commit e74d3397a133eaf1eb95d9ce59f56747a7c8c30b