From 141abce1861d10c9f968a39d2c1fa2658b042cba Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Tue, 19 Jul 2022 11:33:54 +0200 Subject: [PATCH] [marketplace] Add slots --- contracts/Marketplace.sol | 34 ++++++++++++++++ test/Marketplace.test.js | 85 ++++++++++++++++++++++++++++++++++++++- test/ids.js | 8 ++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index faba153..4a62d8f 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -10,6 +10,7 @@ contract Marketplace is Collateral, Proofs { MarketplaceFunds private funds; mapping(bytes32 => Request) private requests; mapping(bytes32 => RequestState) private requestState; + mapping(bytes32 => Slot) private slots; constructor( IERC20 _token, @@ -45,6 +46,30 @@ contract Marketplace is Collateral, Proofs { emit StorageRequested(id, request.ask); } + function fillSlot( + bytes32 requestId, + uint256 slotIndex, + bytes calldata proof + ) public marketplaceInvariant { + Request storage request = requests[requestId]; + require(request.client != address(0), "Unknown request"); + require(request.expiry > block.timestamp, "Request expired"); + require(slotIndex < request.content.erasure.totalNodes, "Invalid slot"); + + bytes32 slotId = keccak256(abi.encode(requestId, slotIndex)); + Slot storage slot = slots[slotId]; + require(slot.host == address(0), "Slot already filled"); + + require(balanceOf(msg.sender) >= collateral, "Insufficient collateral"); + _lock(msg.sender, requestId); + + _expectProofs(slotId, request.ask.proofProbability, request.ask.duration); + _submitProof(slotId, proof); + + slot.host = msg.sender; + emit SlotFilled(requestId, slotIndex, slotId); + } + function fulfillRequest(bytes32 requestId, bytes calldata proof) public marketplaceInvariant @@ -127,8 +152,17 @@ contract Marketplace is Collateral, Proofs { address host; } + struct Slot { + address host; + } + event StorageRequested(bytes32 requestId, Ask ask); event RequestFulfilled(bytes32 indexed requestId); + event SlotFilled( + bytes32 indexed requestId, + uint256 indexed slotIndex, + bytes32 indexed slotId + ); modifier marketplaceInvariant() { MarketplaceFunds memory oldFunds = funds; diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 0a28692..93aebf9 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -4,7 +4,7 @@ const { expect } = require("chai") const { exampleRequest } = require("./examples") const { snapshot, revert, ensureMinimumBlockHeight } = require("./evm") const { now, hours } = require("./time") -const { requestId, askToArray } = require("./ids") +const { requestId, slotId, askToArray } = require("./ids") describe("Marketplace", function () { const collateral = 100 @@ -88,6 +88,89 @@ describe("Marketplace", function () { }) }) + describe("filling a slot", function () { + const proof = hexlify(randomBytes(42)) + let slot + + beforeEach(async function () { + switchAccount(client) + await token.approve(marketplace.address, request.ask.reward) + await marketplace.requestStorage(request) + switchAccount(host) + await token.approve(marketplace.address, collateral) + await marketplace.deposit(collateral) + slot = { + request: requestId(request), + index: request.content.erasure.totalNodes / 2, + } + }) + + it("emits event when slot is filled", async function () { + await expect(marketplace.fillSlot(slot.request, slot.index, proof)) + .to.emit(marketplace, "SlotFilled") + .withArgs(slot.request, slot.index, slotId(slot)) + }) + + it("locks collateral of host", async function () { + await marketplace.fillSlot(slot.request, slot.index, proof) + await expect(marketplace.withdraw()).to.be.revertedWith("Account locked") + }) + + it("starts requiring storage proofs", async function () { + await marketplace.fillSlot(slot.request, slot.index, proof) + expect(await marketplace.proofEnd(slotId(slot))).to.be.gt(0) + }) + + it("is rejected when proof is incorrect", async function () { + let invalid = hexlify([]) + await expect( + marketplace.fillSlot(slot.request, slot.index, invalid) + ).to.be.revertedWith("Invalid proof") + }) + + it("is rejected when collateral is insufficient", async function () { + let insufficient = collateral - 1 + await marketplace.withdraw() + await token.approve(marketplace.address, insufficient) + await marketplace.deposit(insufficient) + await expect( + marketplace.fillSlot(slot.request, slot.index, proof) + ).to.be.revertedWith("Insufficient collateral") + }) + + it("is rejected when slot already filled", async function () { + await marketplace.fillSlot(slot.request, slot.index, proof) + await expect( + marketplace.fillSlot(slot.request, slot.index, proof) + ).to.be.revertedWith("Slot already filled") + }) + + it("is rejected when request is unknown", async function () { + let unknown = exampleRequest() + await expect( + marketplace.fillSlot(requestId(unknown), 0, proof) + ).to.be.revertedWith("Unknown request") + }) + + it("is rejected when request is expired", async function () { + switchAccount(client) + let expired = { ...request, expiry: now() - hours(1) } + await token.approve(marketplace.address, request.ask.reward) + await marketplace.requestStorage(expired) + switchAccount(host) + await expect( + marketplace.fillSlot(requestId(expired), slot.index, proof) + ).to.be.revertedWith("Request expired") + }) + + it("is rejected when slot index not in range", async function () { + const invalid = request.content.erasure.totalNodes + await expect( + marketplace.fillSlot(slot.request, invalid, proof) + ).to.be.revertedWith("Invalid slot") + }) + }) + describe("fulfilling request", function () { const proof = hexlify(randomBytes(42)) diff --git a/test/ids.js b/test/ids.js index 4455f8a..cbd8395 100644 --- a/test/ids.js +++ b/test/ids.js @@ -39,8 +39,16 @@ function requestToArray(request) { ] } +function slotId(slot) { + const types = "tuple(bytes32, uint256)" + const values = [slot.request, slot.index] + const encoding = defaultAbiCoder.encode([types], [values]) + return keccak256(encoding) +} + module.exports = { requestId, + slotId, requestToArray, askToArray, }