feat: collateral per slot (#390)

Co-authored-by: Eric Mastro <github@egonat.me>
This commit is contained in:
Adam Uhlíř 2023-04-14 11:04:17 +02:00 committed by GitHub
parent 86a3f74448
commit 131d003a0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 130 additions and 214 deletions

View File

@ -32,33 +32,6 @@ let client = provider.getSigner(accounts[0])
let host = provider.getSigner(accounts[1])
```
Collateral
----------
Hosts need to put up collateral before participating in storage contracts.
A host can learn about the amount of collateral that is required:
```nim
let config = await marketplace.config()
let collateral = config.collateral.initialAmount
```
After preparing the payment, the host can deposit collateral:
```nim
await storage
.connect(host)
.deposit(collateral)
```
When a host is not participating in storage offers or contracts, it can withdraw
its collateral:
```
await storage
.connect(host)
.withdraw()
```
Storage requests
----------------

View File

@ -9,10 +9,10 @@ type
collateral*: CollateralConfig
proofs*: ProofConfig
CollateralConfig* = object
initialAmount*: UInt256 # amount of collateral necessary to fill a slot
minimumAmount*: UInt256 # frees slot when collateral drops below this minimum
slashCriterion*: UInt256 # amount of proofs missed that lead to slashing
slashPercentage*: UInt256 # percentage of the collateral that is slashed
repairRewardPercentage*: uint8 # percentage of remaining collateral slot has after it has been freed
maxNumberOfSlashes*: uint8 # frees slot when the number of slashes reaches this value
slashCriterion*: uint16 # amount of proofs missed that lead to slashing
slashPercentage*: uint8 # percentage of the collateral that is slashed
ProofConfig* = object
period*: UInt256 # proofs requirements are calculated per period (in seconds)
timeout*: UInt256 # mark proofs as missing before the timeout (in seconds)
@ -28,8 +28,8 @@ func fromTuple(_: type ProofConfig, tupl: tuple): ProofConfig =
func fromTuple(_: type CollateralConfig, tupl: tuple): CollateralConfig =
CollateralConfig(
initialAmount: tupl[0],
minimumAmount: tupl[1],
repairRewardPercentage: tupl[0],
maxNumberOfSlashes: tupl[1],
slashCriterion: tupl[2],
slashPercentage: tupl[3]
)

View File

@ -1,4 +1,5 @@
import std/strutils
import pkg/chronicles
import pkg/ethers
import pkg/ethers/testing
import pkg/upraises
@ -8,6 +9,9 @@ import ./marketplace
export market
logScope:
topics = "onchain market"
type
OnChainMarket* = ref object of Market
contract: Marketplace
@ -25,7 +29,8 @@ func new*(_: type OnChainMarket, contract: Marketplace): OnChainMarket =
signer: signer,
)
method approveFunds*(market: OnChainMarket, amount: UInt256) {.async.} =
proc approveFunds(market: OnChainMarket, amount: UInt256) {.async.} =
notice "approving tokens", amount
let tokenAddress = await market.contract.token()
let token = Erc20Token.new(tokenAddress, market.signer)
@ -41,6 +46,7 @@ method mySlots*(market: OnChainMarket): Future[seq[SlotId]] {.async.} =
return await market.contract.mySlots()
method requestStorage(market: OnChainMarket, request: StorageRequest){.async.} =
await market.approveFunds(request.price())
await market.contract.requestStorage(request)
method getRequest(market: OnChainMarket,
@ -93,7 +99,9 @@ method getActiveSlot*(
method fillSlot(market: OnChainMarket,
requestId: RequestId,
slotIndex: UInt256,
proof: seq[byte]) {.async.} =
proof: seq[byte],
collateral: UInt256) {.async.} =
await market.approveFunds(collateral)
await market.contract.fillSlot(requestId, slotIndex, proof)
method withdrawFunds(market: OnChainMarket,

View File

@ -38,10 +38,6 @@ proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.}
proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.}
proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.}
proc deposit*(marketplace: Marketplace, amount: UInt256) {.contract.}
proc withdraw*(marketplace: Marketplace) {.contract.}
proc balanceOf*(marketplace: Marketplace, account: Address): UInt256 {.contract, view.}
proc requestStorage*(marketplace: Marketplace, request: StorageRequest) {.contract.}
proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: seq[byte]) {.contract.}
proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId) {.contract.}

View File

@ -19,6 +19,7 @@ type
duration*: UInt256
proofProbability*: UInt256
reward*: UInt256
collateral*: UInt256
maxSlotLoss*: uint64
StorageContent* = object
cid*: string
@ -84,7 +85,8 @@ func fromTuple(_: type StorageAsk, tupl: tuple): StorageAsk =
duration: tupl[2],
proofProbability: tupl[3],
reward: tupl[4],
maxSlotLoss: tupl[5]
collateral: tupl[5],
maxSlotLoss: tupl[6]
)
func fromTuple(_: type StorageContent, tupl: tuple): StorageContent =

View File

@ -19,10 +19,6 @@ type
OnRequestCancelled* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
OnRequestFailed* = proc(requestId: RequestId) {.gcsafe, upraises:[].}
method approveFunds*(market: Market, amount: UInt256) {.base, async.} =
## This is generally needed for On-Chain ERC20 functionality to approve funds transfer to marketplace contract
raiseAssert("not implemented")
method getSigner*(market: Market): Future[Address] {.base, async.} =
raiseAssert("not implemented")
@ -67,7 +63,8 @@ method getActiveSlot*(
method fillSlot*(market: Market,
requestId: RequestId,
slotIndex: UInt256,
proof: seq[byte]) {.base, async.} =
proof: seq[byte],
collateral: UInt256) {.base, async.} =
raiseAssert("not implemented")
method withdrawFunds*(market: Market,

View File

@ -244,6 +244,7 @@ proc requestStorage*(self: CodexNodeRef,
nodes: uint,
tolerance: uint,
reward: UInt256,
collateral: UInt256,
expiry = UInt256.none): Future[?!PurchaseId] {.async.} =
## Initiate a request for storage sequence, this might
## be a multistep procedure.
@ -288,6 +289,7 @@ proc requestStorage*(self: CodexNodeRef,
duration: duration,
proofProbability: proofProbability,
reward: reward,
collateral: collateral,
maxSlotLoss: tolerance
),
content: StorageContent(

View File

@ -10,7 +10,6 @@ method enterAsync(state: PurchasePending) {.async.} =
raiseAssert "invalid state"
try:
await purchase.market.approveFunds(request.price())
await purchase.market.requestStorage(request)
except CatchableError as error:
state.switch(PurchaseErrored(error: error))

View File

@ -140,6 +140,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data
## nodes - minimal number of nodes the content should be stored on
## tolerance - allowed number of nodes that can be lost before pronouncing the content lost
## colateral - requested collateral from hosts when they fill slot
without cid =? cid.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
@ -159,6 +160,7 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
nodes,
tolerance,
params.reward,
params.collateral,
params.expiry), error:
return RestApiResponse.error(Http500, error.msg)
@ -269,9 +271,10 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
"/api/codex/v1/sales/availability") do () -> RestApiResponse:
## Add available storage to sell
##
## size - size of available storage in bytes
## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimum price to be paid (in amount of tokens)
## size - size of available storage in bytes
## duration - maximum time the storage should be sold for (in seconds)
## minPrice - minimum price to be paid (in amount of tokens)
## maxCollateral - maximum collateral user is willing to pay per filled Slot (in amount of tokens)
without contracts =? node.contracts.host:
return RestApiResponse.error(Http503, "Sales unavailable")

View File

@ -10,6 +10,7 @@ type
duration*: UInt256
proofProbability*: UInt256
reward*: UInt256
collateral*: UInt256
expiry*: ?UInt256
nodes*: ?uint
tolerance*: ?uint
@ -19,7 +20,8 @@ proc fromJson*(_: type Availability, bytes: seq[byte]): ?!Availability =
let size = ?catch UInt256.fromHex(json["size"].getStr)
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
let minPrice = ?catch UInt256.fromHex(json["minPrice"].getStr)
success Availability.init(size, duration, minPrice)
let maxCollateral = ?catch UInt256.fromHex(json["maxCollateral"].getStr)
success Availability.init(size, duration, minPrice, maxCollateral)
proc fromJson*(_: type StorageRequestParams,
bytes: seq[byte]): ?! StorageRequestParams =
@ -27,6 +29,7 @@ proc fromJson*(_: type StorageRequestParams,
let duration = ?catch UInt256.fromHex(json["duration"].getStr)
let proofProbability = ?catch UInt256.fromHex(json["proofProbability"].getStr)
let reward = ?catch UInt256.fromHex(json["reward"].getStr)
let collateral = ?catch UInt256.fromHex(json["collateral"].getStr)
let expiry = UInt256.fromHex(json["expiry"].getStr).catch.option
let nodes = strutils.fromHex[uint](json["nodes"].getStr).catch.option
let tolerance = strutils.fromHex[uint](json["tolerance"].getStr).catch.option
@ -34,6 +37,7 @@ proc fromJson*(_: type StorageRequestParams,
duration: duration,
proofProbability: proofProbability,
reward: reward,
collateral: collateral,
expiry: expiry,
nodes: nodes,
tolerance: tolerance

View File

@ -38,6 +38,7 @@ type
size*: UInt256
duration*: UInt256
minPrice*: UInt256
maxCollateral*: UInt256
used*: bool
Reservations* = ref object
repo: RepoStore
@ -67,11 +68,12 @@ proc init*(
_: type Availability,
size: UInt256,
duration: UInt256,
minPrice: UInt256): Availability =
minPrice: UInt256,
maxCollateral: UInt256): Availability =
var id: array[32, byte]
doAssert randomBytes(id) == 32
Availability(id: AvailabilityId(id), size: size, duration: duration, minPrice: minPrice)
Availability(id: AvailabilityId(id), size: size, duration: duration, minPrice: minPrice, maxCollateral: maxCollateral)
func toArray*(id: AvailabilityId): array[32, byte] =
array[32, byte](id)
@ -81,6 +83,7 @@ proc `==`*(x, y: Availability): bool =
x.id == y.id and
x.size == y.size and
x.duration == y.duration and
x.maxCollateral == y.maxCollateral and
x.minPrice == y.minPrice
proc `$`*(id: AvailabilityId): string = id.toArray.toHex
@ -318,7 +321,7 @@ proc unused*(r: Reservations): Future[?!seq[Availability]] {.async.} =
proc find*(
self: Reservations,
size, duration, minPrice: UInt256,
size, duration, minPrice: UInt256, collateral: UInt256,
used: bool): Future[?Availability] {.async.} =
@ -332,13 +335,15 @@ proc find*(
if used == availability.used and
size <= availability.size and
duration <= availability.duration and
collateral <= availability.maxCollateral and
minPrice >= availability.minPrice:
trace "availability matched",
used, availUsed = availability.used,
size, availsize = availability.size,
duration, availDuration = availability.duration,
minPrice, availMinPrice = availability.minPrice
minPrice, availMinPrice = availability.minPrice,
collateral, availMaxCollateral = availability.maxCollateral
return some availability
@ -346,4 +351,5 @@ proc find*(
used, availUsed = availability.used,
size, availsize = availability.size,
duration, availDuration = availability.duration,
minPrice, availMinPrice = availability.minPrice
minPrice, availMinPrice = availability.minPrice,
collateral, availMaxCollateral = availability.maxCollateral

View File

@ -50,6 +50,7 @@ method run*(state: SaleDownloading, machine: Machine): Future[?State] {.async.}
request.ask.slotSize,
request.ask.duration,
request.ask.pricePerSlot,
request.ask.collateral,
used = false):
info "no availability found for request, ignoring",
slotSize = request.ask.slotSize,

View File

@ -25,5 +25,7 @@ method onSlotFilled*(state: SaleFilling, requestId: RequestId,
method run(state: SaleFilling, machine: Machine): Future[?State] {.async.} =
let data = SalesAgent(machine).data
let market = SalesAgent(machine).context.market
without (collateral =? data.request.?ask.?collateral):
raiseAssert "Request not set"
await market.fillSlot(data.requestId, data.slotIndex, state.proof)
await market.fillSlot(data.requestId, data.slotIndex, state.proof, collateral)

View File

@ -112,6 +112,9 @@ components:
minPrice:
type: string
description: Minimum price to be paid (in amount of tokens) as hexadecimal string
maxCollateral:
type: string
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens)
StorageRequestCreation:
type: object
@ -119,6 +122,7 @@ components:
- reward
- duration
- proofProbability
- collateral
properties:
duration:
$ref: "#/components/schemas/Duration"
@ -134,6 +138,9 @@ components:
type: number
description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost
default: 0
collateral:
type: string
description: Hexadecimal encoded number that represents how much collateral is asked from hosts that wants to fill a slots
StorageAsk:
type: object

View File

@ -57,5 +57,6 @@ proc example*(_: type Availability): Availability =
Availability.init(
size = uint16.example.u256,
duration = uint16.example.u256,
minPrice = uint64.example.u256
minPrice = uint64.example.u256,
maxCollateral = uint16.example.u256
)

View File

@ -67,10 +67,10 @@ proc hash*(requestId: RequestId): Hash =
proc new*(_: type MockMarket): MockMarket =
let config = MarketplaceConfig(
collateral: CollateralConfig(
initialAmount: 100.u256,
minimumAmount: 40.u256,
slashCriterion: 3.u256,
slashPercentage: 10.u256
repairRewardPercentage: 10,
maxNumberOfSlashes: 5,
slashCriterion: 3,
slashPercentage: 10
),
proofs: ProofConfig(
period: 10.u256,
@ -80,9 +80,6 @@ proc new*(_: type MockMarket): MockMarket =
)
MockMarket(signer: Address.example, config: config)
method approveFunds*(market: MockMarket, amount: UInt256) {.async.} =
discard
method getSigner*(market: MockMarket): Future[Address] {.async.} =
return market.signer
@ -182,7 +179,8 @@ proc fillSlot*(market: MockMarket,
method fillSlot*(market: MockMarket,
requestId: RequestId,
slotIndex: UInt256,
proof: seq[byte]) {.async.} =
proof: seq[byte],
collateral: UInt256) {.async.} =
market.fillSlot(requestId, slotIndex, proof, market.signer)
method withdrawFunds*(market: MockMarket,

View File

@ -39,8 +39,8 @@ suite "Reservations module":
check (await reservations.allAvailabilities()).len == 0
test "generates unique ids for storage availability":
let availability1 = Availability.init(1.u256, 2.u256, 3.u256)
let availability2 = Availability.init(1.u256, 2.u256, 3.u256)
let availability1 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256)
let availability2 = Availability.init(1.u256, 2.u256, 3.u256, 4.u256)
check availability1.id != availability2.id
test "can reserve available storage":
@ -125,7 +125,7 @@ suite "Reservations module":
check isOk await reservations.markUsed(availability.id)
without available =? await reservations.find(availability.size,
availability.duration, availability.minPrice, used = true):
availability.duration, availability.minPrice, availability.maxCollateral, used = true):
fail()
@ -133,13 +133,13 @@ suite "Reservations module":
check isOk await reservations.reserve(availability)
without available =? await reservations.find(availability.size,
availability.duration, availability.minPrice, used = false):
availability.duration, availability.minPrice, availability.maxCollateral, used = false):
fail()
test "non-existant availability cannot be found":
check isNone (await reservations.find(availability.size,
availability.duration, availability.minPrice, used = false))
availability.duration, availability.minPrice, availability.maxCollateral, used = false))
test "non-existant availability cannot be retrieved":
let r = await reservations.get(availability.id)

View File

@ -38,7 +38,8 @@ suite "Sales":
availability = Availability.init(
size=100.u256,
duration=60.u256,
minPrice=600.u256
minPrice=600.u256,
maxCollateral=400.u256
)
request = StorageRequest(
ask: StorageAsk(
@ -46,6 +47,7 @@ suite "Sales":
slotSize: 100.u256,
duration: 60.u256,
reward: 10.u256,
collateral: 200.u256,
),
content: StorageContent(
cid: "some cid"
@ -134,6 +136,13 @@ suite "Sales":
await market.requestStorage(request)
check getAvailability().?used == success false
test "ignores request when asked collateral is too high":
var tooBigCollateral = request
tooBigCollateral.ask.collateral = availability.maxCollateral + 1
check isOk await reservations.reserve(availability)
await market.requestStorage(tooBigCollateral)
check await wasIgnored()
test "retrieves and stores data locally":
var storingRequest: StorageRequest
var storingSlot: UInt256

View File

@ -1,32 +0,0 @@
import pkg/chronos
import pkg/stint
import codex/contracts
import ./token
import ../ethertest
ethersuite "Collateral":
let collateral = 100.u256
var marketplace: Marketplace
var token: TestToken
setup:
let deployment = Deployment.init()
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
await token.mint(accounts[0], 1000.u256)
test "increases collateral":
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
let balance = await marketplace.balanceOf(accounts[0])
check balance == collateral
test "withdraws collateral":
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
let balanceBefore = await token.balanceOf(accounts[0])
await marketplace.withdraw()
let balanceAfter = await token.balanceOf(accounts[0])
check (balanceAfter - balanceBefore) == collateral

View File

@ -1,20 +1,19 @@
import std/json
import pkg/chronos
import pkg/ethers/testing
import pkg/ethers/erc20
import codex/contracts
import codex/storageproofs
import ../ethertest
import ./examples
import ./time
import ./token
ethersuite "Marketplace contracts":
let proof = exampleProof()
var client, host: Signer
var marketplace: Marketplace
var token: TestToken
var collateral: UInt256
var token: Erc20Token
var periodicity: Periodicity
var request: StorageRequest
var slotId: SlotId
@ -29,13 +28,11 @@ ethersuite "Marketplace contracts":
let deployment = Deployment.init()
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
await token.mint(await client.getAddress(), 1_000_000_000.u256)
await token.mint(await host.getAddress(), 1000_000_000.u256)
let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, provider.getSigner())
let config = await marketplace.config()
collateral = config.collateral.initialAmount
periodicity = Periodicity(seconds: config.proofs.period)
request = StorageRequest.example
@ -45,8 +42,7 @@ ethersuite "Marketplace contracts":
await token.approve(marketplace.address, request.price)
await marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(request.id, 0.u256, proof)
slotId = request.slotId(0.u256)
@ -61,6 +57,7 @@ ethersuite "Marketplace contracts":
proc startContract() {.async.} =
for slotIndex in 1..<request.ask.slots:
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(request.id, slotIndex.u256, proof)
test "accept marketplace proofs":
@ -85,7 +82,7 @@ ethersuite "Marketplace contracts":
let startBalance = await token.balanceOf(address)
await marketplace.freeSlot(slotId)
let endBalance = await token.balanceOf(address)
check endBalance == (startBalance + request.ask.duration * request.ask.reward)
check endBalance == (startBalance + request.ask.duration * request.ask.reward + request.ask.collateral)
test "cannot mark proofs missing for cancelled request":
await provider.advanceTimeTo(request.expiry + 1)

View File

@ -6,14 +6,12 @@ import codex/storageproofs
import ../ethertest
import ./examples
import ./time
import ./token
ethersuite "On-Chain Market":
let proof = exampleProof()
var market: OnChainMarket
var marketplace: Marketplace
var token: TestToken
var request: StorageRequest
var slotIndex: UInt256
var periodicity: Periodicity
@ -21,13 +19,7 @@ ethersuite "On-Chain Market":
setup:
let deployment = Deployment.init()
marketplace = Marketplace.new(!deployment.address(Marketplace), provider.getSigner())
token = TestToken.new(!deployment.address(TestToken), provider.getSigner())
await token.mint(accounts[0], 1_000_000_000.u256)
let config = await marketplace.config()
let collateral = config.collateral.initialAmount
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
market = OnChainMarket.new(marketplace)
periodicity = Periodicity(seconds: config.proofs.period)
@ -55,18 +47,15 @@ ethersuite "On-Chain Market":
check (await market.getSigner()) == (await provider.getSigner().getAddress())
test "supports marketplace requests":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
test "can retrieve previously submitted requests":
check (await market.getRequest(request.id)) == none StorageRequest
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
let r = await market.getRequest(request.id)
check (r) == some request
test "supports withdrawing of funds":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await provider.advanceTimeTo(request.expiry)
await market.withdrawFunds(request.id)
@ -78,26 +67,22 @@ ethersuite "On-Chain Market":
receivedIds.add(id)
receivedAsks.add(ask)
let subscription = await market.subscribeRequests(onRequest)
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
check receivedIds == @[request.id]
check receivedAsks == @[request.ask]
await subscription.unsubscribe()
test "supports filling of slots":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
test "can retrieve host that filled slot":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
check (await market.getHost(request.id, slotIndex)) == none Address
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check (await market.getHost(request.id, slotIndex)) == some accounts[0]
test "support slot filled subscriptions":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
var receivedSlotIndices: seq[UInt256]
@ -105,34 +90,32 @@ ethersuite "On-Chain Market":
receivedIds.add(id)
receivedSlotIndices.add(slotIndex)
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check receivedIds == @[request.id]
check receivedSlotIndices == @[slotIndex]
await subscription.unsubscribe()
test "subscribes only to a certain slot":
var otherSlot = slotIndex - 1
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var receivedSlotIndices: seq[UInt256]
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
receivedSlotIndices.add(slotIndex)
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
await market.fillSlot(request.id, otherSlot, proof)
await market.fillSlot(request.id, otherSlot, proof, request.ask.collateral)
check receivedSlotIndices.len == 0
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check receivedSlotIndices == @[slotIndex]
await subscription.unsubscribe()
test "support fulfillment subscriptions":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onFulfillment(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
check receivedIds == @[request.id]
await subscription.unsubscribe()
@ -140,9 +123,7 @@ ethersuite "On-Chain Market":
var otherRequest = StorageRequest.example
otherRequest.client = accounts[0]
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await token.approve(marketplace.address, otherRequest.price)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
@ -152,16 +133,15 @@ ethersuite "On-Chain Market":
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
for slotIndex in 0..<otherRequest.ask.slots:
await market.fillSlot(otherRequest.id, slotIndex.u256, proof)
await market.fillSlot(otherRequest.id, slotIndex.u256, proof, otherRequest.ask.collateral)
check receivedIds == @[request.id]
await subscription.unsubscribe()
test "support request cancelled subscriptions":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
@ -175,7 +155,6 @@ ethersuite "On-Chain Market":
await subscription.unsubscribe()
test "support request failed subscriptions":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var receivedIds: seq[RequestId]
@ -184,7 +163,7 @@ ethersuite "On-Chain Market":
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
for slotIndex in 0..request.ask.maxSlotLoss:
let slotId = request.slotId(slotIndex.u256)
while true:
@ -201,9 +180,7 @@ ethersuite "On-Chain Market":
test "subscribes only to a certain request cancellation":
var otherRequest = request
otherRequest.nonce = Nonce.example
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await token.approve(marketplace.address, otherRequest.price)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
@ -222,11 +199,9 @@ ethersuite "On-Chain Market":
check isNone await market.getRequest(request.id)
test "can retrieve active requests":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
var request2 = StorageRequest.example
request2.client = accounts[0]
await token.approve(marketplace.address, request2.price)
await market.requestStorage(request2)
check (await market.myRequests()) == @[request.id, request2.id]
@ -234,31 +209,27 @@ ethersuite "On-Chain Market":
check (await market.requestState(request.id)) == none RequestState
test "can retrieve request state":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
for slotIndex in 0..<request.ask.slots:
await market.fillSlot(request.id, slotIndex.u256, proof)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
check (await market.requestState(request.id)) == some RequestState.Started
test "can retrieve active slots":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex - 1, proof)
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex - 1, proof, request.ask.collateral)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId1 = request.slotId(slotIndex - 1)
let slotId2 = request.slotId(slotIndex)
check (await market.mySlots()) == @[slotId1, slotId2]
test "returns none when slot is empty":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
let slotId = request.slotId(slotIndex)
check (await market.getActiveSlot(slotId)) == none Slot
test "can retrieve request details from slot id":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId = request.slotId(slotIndex)
let expected = Slot(request: request, slotIndex: slotIndex)
check (await market.getActiveSlot(slotId)) == some expected
@ -268,8 +239,7 @@ ethersuite "On-Chain Market":
check (await market.slotState(slotId)) == SlotState.Free
test "retrieves correct slot state once filled":
await token.approve(marketplace.address, request.price)
await market.requestStorage(request)
await market.fillSlot(request.id, slotIndex, proof)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId = request.slotId(slotIndex)
check (await market.slotState(slotId)) == SlotState.Filled

View File

@ -1,9 +0,0 @@
import pkg/chronos
import pkg/stint
import pkg/ethers
import pkg/ethers/erc20
type
TestToken* = ref object of Erc20Token
proc mint*(token: TestToken, holder: Address, amount: UInt256) {.contract.}

View File

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

View File

@ -31,13 +31,15 @@ proc requestStorage*(client: CodexClient,
duration: uint64,
reward: uint64,
proofProbability: uint64,
expiry: UInt256): string =
expiry: UInt256,
collateral: uint64): string =
let url = client.baseurl & "/storage/request/" & cid
let json = %*{
"duration": "0x" & duration.toHex,
"reward": "0x" & reward.toHex,
"proofProbability": "0x" & proofProbability.toHex,
"expiry": "0x" & expiry.toHex
"expiry": "0x" & expiry.toHex,
"collateral": "0x" & collateral.toHex,
}
let response = client.http.post(url, $json)
assert response.status == "200 OK"
@ -49,12 +51,13 @@ proc getPurchase*(client: CodexClient, purchase: string): JsonNode =
parseJson(body).catch |? nil
proc postAvailability*(client: CodexClient,
size, duration, minPrice: uint64): JsonNode =
size, duration, minPrice: uint64, maxCollateral: uint64): JsonNode =
let url = client.baseurl & "/sales/availability"
let json = %*{
"size": "0x" & size.toHex,
"duration": "0x" & duration.toHex,
"minPrice": "0x" & minPrice.toHex
"minPrice": "0x" & minPrice.toHex,
"maxCollateral": "0x" & maxCollateral.toHex
}
let response = client.http.post(url, $json)
assert response.status == "200 OK"

View File

@ -4,14 +4,14 @@ import pkg/stint
import ../contracts/time
import ../codex/helpers/eventually
import ./twonodes
import ./tokens
twonodessuite "Integration tests", debug1 = false, debug2 = false:
setup:
await provider.getSigner(accounts[0]).mint()
await provider.getSigner(accounts[1]).mint()
await provider.getSigner(accounts[1]).deposit()
# Our Hardhat configuration does use automine, which means that time tracked by `provider.currentTime()` is not
# advanced until blocks are mined and that happens only when transaction is submitted.
# As we use in tests provider.currentTime() which uses block timestamp this can lead to synchronization issues.
await provider.advanceTime(1.u256)
test "nodes can print their peer information":
check client1.info() != client2.info()
@ -25,25 +25,25 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
check cid1 != cid2
test "node handles new storage availability":
let availability1 = client1.postAvailability(size=1, duration=2, minPrice=3)
let availability2 = client1.postAvailability(size=4, duration=5, minPrice=6)
let availability1 = client1.postAvailability(size=1, duration=2, minPrice=3, maxCollateral=4)
let availability2 = client1.postAvailability(size=4, duration=5, minPrice=6, maxCollateral=7)
check availability1 != availability2
test "node lists storage that is for sale":
let availability = client1.postAvailability(size=1, duration=2, minPrice=3)
let availability = client1.postAvailability(size=1, duration=2, minPrice=3, maxCollateral=4)
check availability in client1.getAvailabilities()
test "node handles storage request":
let expiry = (await provider.currentTime()) + 30
let cid = client1.upload("some file contents")
let id1 = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
let id2 = client1.requestStorage(cid, duration=4, reward=5, proofProbability=6, expiry=expiry)
let id1 = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
let id2 = client1.requestStorage(cid, duration=4, reward=5, proofProbability=6, expiry=expiry, collateral=201)
check id1 != id2
test "node retrieves purchase status":
let expiry = (await provider.currentTime()) + 30
let cid = client1.upload("some file contents")
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
let purchase = client1.getPurchase(id)
check purchase{"request"}{"ask"}{"duration"} == %"0x1"
check purchase{"request"}{"ask"}{"reward"} == %"0x2"
@ -52,7 +52,7 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
test "node remembers purchase status after restart":
let expiry = (await provider.currentTime()) + 30
let cid = client1.upload("some file contents")
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry)
let id = client1.requestStorage(cid, duration=1, reward=2, proofProbability=3, expiry=expiry, collateral=200)
check eventually client1.getPurchase(id){"state"}.getStr() == "submitted"
node1.restart()
@ -65,12 +65,12 @@ twonodessuite "Integration tests", debug1 = false, debug2 = false:
test "nodes negotiate contracts on the marketplace":
let size: uint64 = 0xFFFFF
# client 2 makes storage available
discard client2.postAvailability(size=size, duration=200, minPrice=300)
discard client2.postAvailability(size=size, duration=200, minPrice=300, maxCollateral=300)
# client 1 requests storage
let expiry = (await provider.currentTime()) + 30
let cid = client1.upload("some file contents")
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry)
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry, collateral=200)
check eventually client1.getPurchase(purchase){"state"} == %"started"
check client1.getPurchase(purchase){"error"} == newJNull()

View File

@ -3,7 +3,6 @@ import codex/contracts/deployment
import ../contracts/time
import ../codex/helpers/eventually
import ./twonodes
import ./tokens
twonodessuite "Proving integration test", debug1=false, debug2=false:
@ -14,15 +13,17 @@ twonodessuite "Proving integration test", debug1=false, debug2=false:
let deployment = Deployment.init()
marketplace = Marketplace.new(!deployment.address(Marketplace), provider)
config = await marketplace.config()
await provider.getSigner(accounts[0]).mint()
await provider.getSigner(accounts[1]).mint()
await provider.getSigner(accounts[1]).deposit()
# Our Hardhat configuration does use automine, which means that time tracked by `provider.currentTime()` is not
# advanced until blocks are mined and that happens only when transaction is submitted.
# As we use in tests provider.currentTime() which uses block timestamp this can lead to synchronization issues.
await provider.advanceTime(1.u256)
proc waitUntilPurchaseIsStarted {.async.} =
discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300)
discard client2.postAvailability(size=0xFFFFF, duration=200, minPrice=300, maxCollateral=200)
let expiry = (await provider.currentTime()) + 30
let cid = client1.upload("some file contents")
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry)
let purchase = client1.requestStorage(cid, duration=100, reward=400, proofProbability=3, expiry=expiry, collateral=100)
check eventually client1.getPurchase(purchase){"state"} == %"started"
test "hosts submit periodic proofs for slots they fill":

View File

@ -1,22 +0,0 @@
import pkg/ethers/erc20
import codex/contracts
import ../contracts/token
proc mint*(signer: Signer, amount = 1_000_000.u256) {.async.} =
## Mints a considerable amount of tokens and approves them for transfer to
## the Marketplace contract.
let deployment = Deployment.init()
let token = TestToken.new(!deployment.address(TestToken), signer)
let marketplace = Marketplace.new(!deployment.address(Marketplace), signer)
await token.mint(await signer.getAddress(), amount)
proc deposit*(signer: Signer) {.async.} =
## Deposits sufficient collateral into the Marketplace contract.
let deployment = Deployment.init()
let marketplace = Marketplace.new(!deployment.address(Marketplace), signer)
let config = await marketplace.config()
let tokenAddress = await marketplace.token()
let token = Erc20Token.new(tokenAddress, signer)
await token.approve(marketplace.address, config.collateral.initialAmount)
await marketplace.deposit(config.collateral.initialAmount)

View File

@ -1,4 +1,3 @@
import ./contracts/testCollateral
import ./contracts/testContracts
import ./contracts/testMarket
import ./contracts/testProofs

@ -1 +1 @@
Subproject commit fb76f7d0b2f94914b00f2a0f4136ebfb27df6abc
Subproject commit 6e66abbfcd9be6cbd0434dc5a80f1419c66a914e

2
vendor/questionable vendored

@ -1 +1 @@
Subproject commit 6cbbda7e4d009e02d0583b325b31dc68dff27854
Subproject commit 30e4184a99c8c1ba329925912d2c5d4b09acf8cc