Merge pull request #22 from status-im/list-of-active-requests

List of active requests
This commit is contained in:
markspanbroek 2022-11-10 05:23:18 -05:00 committed by GitHub
commit a4057d712f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 68 deletions

View File

@ -3,10 +3,13 @@ pragma solidity ^0.8.8;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Collateral.sol";
import "./Proofs.sol";
contract Marketplace is Collateral, Proofs {
using EnumerableSet for EnumerableSet.Bytes32Set;
type RequestId is bytes32;
type SlotId is bytes32;
@ -15,6 +18,7 @@ contract Marketplace is Collateral, Proofs {
mapping(RequestId => Request) private requests;
mapping(RequestId => RequestContext) private requestContexts;
mapping(SlotId => Slot) private slots;
mapping(address => EnumerableSet.Bytes32Set) private activeRequests;
constructor(
IERC20 _token,
@ -30,6 +34,10 @@ contract Marketplace is Collateral, Proofs {
collateral = _collateral;
}
function myRequests() public view returns (RequestId[] memory) {
return _toRequestIds(activeRequests[msg.sender].values());
}
function requestStorage(Request calldata request)
public
marketplaceInvariant
@ -45,6 +53,7 @@ contract Marketplace is Collateral, Proofs {
context.endsAt = block.timestamp + request.ask.duration;
_setProofEnd(_toEndId(id), context.endsAt);
activeRequests[request.client].add(RequestId.unwrap(id));
_createLock(_toLockId(id), request.expiry);
@ -73,10 +82,7 @@ contract Marketplace is Collateral, Proofs {
_lock(msg.sender, lockId);
ProofId proofId = _toProofId(slotId);
_expectProofs(
proofId,
_toEndId(requestId),
request.ask.proofProbability);
_expectProofs(proofId, _toEndId(requestId), request.ask.proofProbability);
_submitProof(proofId, proof);
slot.host = msg.sender;
@ -92,9 +98,11 @@ contract Marketplace is Collateral, Proofs {
}
}
function _freeSlot(
SlotId slotId
) internal slotMustAcceptProofs(slotId) marketplaceInvariant {
function _freeSlot(SlotId slotId)
internal
slotMustAcceptProofs(slotId)
marketplaceInvariant
{
Slot storage slot = _slot(slotId);
RequestId requestId = slot.requestId;
RequestContext storage context = requestContexts[requestId];
@ -113,12 +121,14 @@ contract Marketplace is Collateral, Proofs {
Request storage request = _request(requestId);
uint256 slotsLost = request.ask.slots - context.slotsFilled;
if (slotsLost > request.ask.maxSlotLoss &&
context.state == RequestState.Started) {
if (
slotsLost > request.ask.maxSlotLoss &&
context.state == RequestState.Started
) {
context.state = RequestState.Failed;
_setProofEnd(_toEndId(requestId), block.timestamp - 1);
context.endsAt = block.timestamp - 1;
activeRequests[request.client].remove(RequestId.unwrap(requestId));
emit RequestFailed(requestId);
// TODO: burn all remaining slot collateral (note: slot collateral not
@ -126,13 +136,16 @@ contract Marketplace is Collateral, Proofs {
// TODO: send client remaining funds
}
}
function payoutSlot(RequestId requestId, uint256 slotIndex)
public
marketplaceInvariant
{
require(_isFinished(requestId), "Contract not ended");
RequestContext storage context = _context(requestId);
Request storage request = _request(requestId);
context.state = RequestState.Finished;
activeRequests[request.client].remove(RequestId.unwrap(requestId));
SlotId slotId = _toSlotId(requestId, slotIndex);
Slot storage slot = _slot(slotId);
require(!slot.hostPaid, "Already paid");
@ -156,6 +169,7 @@ contract Marketplace is Collateral, Proofs {
// Update request state to Cancelled. Handle in the withdraw transaction
// as there needs to be someone to pay for the gas to update the state
context.state = RequestState.Cancelled;
activeRequests[request.client].remove(RequestId.unwrap(requestId));
emit RequestCancelled(requestId);
// TODO: To be changed once we start paying out hosts for the time they
@ -175,10 +189,8 @@ contract Marketplace is Collateral, Proofs {
RequestContext storage context = _context(requestId);
return
context.state == RequestState.Cancelled ||
(
context.state == RequestState.New &&
block.timestamp > _request(requestId).expiry
);
(context.state == RequestState.New &&
block.timestamp > _request(requestId).expiry);
}
/// @notice Return true if the request state is RequestState.Finished or if the request duration has elapsed and the request was started.
@ -189,10 +201,8 @@ contract Marketplace is Collateral, Proofs {
RequestContext memory context = _context(requestId);
return
context.state == RequestState.Finished ||
(
context.state == RequestState.Started &&
block.timestamp > context.endsAt
);
(context.state == RequestState.Started &&
block.timestamp > context.endsAt);
}
/// @notice Return id of request that slot belongs to
@ -255,9 +265,12 @@ contract Marketplace is Collateral, Proofs {
}
function proofEnd(SlotId slotId) public view returns (uint256) {
Slot memory slot = _slot(slotId);
uint256 end = _end(_toEndId(slot.requestId));
if (_slotAcceptsProofs(slotId)) {
return requestEnd(_slot(slotId).requestId);
}
function requestEnd(RequestId requestId) public view returns (uint256) {
uint256 end = _end(_toEndId(requestId));
if (_requestAcceptsProofs(requestId)) {
return end;
} else {
return Math.min(end, block.timestamp - 1);
@ -267,8 +280,8 @@ contract Marketplace is Collateral, Proofs {
function _price(
uint64 numSlots,
uint256 duration,
uint256 reward) internal pure returns (uint256) {
uint256 reward
) internal pure returns (uint256) {
return numSlots * duration * reward;
}
@ -306,7 +319,11 @@ contract Marketplace is Collateral, Proofs {
/// @notice returns true when the request is accepting proof submissions from hosts occupying slots.
/// @dev Request state must be new or started, and must not be cancelled, finished, or failed.
/// @param requestId id of the request for which to obtain state info
function _requestAcceptsProofs(RequestId requestId) internal view returns (bool) {
function _requestAcceptsProofs(RequestId requestId)
internal
view
returns (bool)
{
RequestState s = state(requestId);
return s == RequestState.New || s == RequestState.Started;
}
@ -319,9 +336,18 @@ contract Marketplace is Collateral, Proofs {
return RequestId.wrap(keccak256(abi.encode(request)));
}
function _toSlotId(
RequestId requestId,
uint256 slotIndex)
function _toRequestIds(bytes32[] memory array)
private
pure
returns (RequestId[] memory result)
{
// solhint-disable-next-line no-inline-assembly
assembly {
result := array
}
}
function _toSlotId(RequestId requestId, uint256 slotIndex)
internal
pure
returns (SlotId)
@ -379,11 +405,11 @@ contract Marketplace is Collateral, Proofs {
}
enum RequestState {
New, // [default] waiting to fill slots
Started, // all slots filled, accepting regular proofs
Cancelled, // not enough slots filled before expiry
Finished, // successfully completed
Failed // too many nodes have failed to provide proofs, data lost
New, // [default] waiting to fill slots
Started, // all slots filled, accepting regular proofs
Cancelled, // not enough slots filled before expiry
Finished, // successfully completed
Failed // too many nodes have failed to provide proofs, data lost
}
struct RequestContext {

View File

@ -9,7 +9,7 @@ const {
waitUntilStarted,
waitUntilFinished,
waitUntilFailed,
RequestState
RequestState,
} = require("./marketplace")
const { price, pricePerSlot } = require("./price")
const {
@ -178,8 +178,8 @@ describe("Marketplace", function () {
})
it("is rejected when request is finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Request not accepting proofs")
@ -261,8 +261,8 @@ describe("Marketplace", function () {
})
it("checks that proof end time is in the past once finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
const now = await currentTime()
// in the process of calling currentTime and proofEnd,
// block.timestamp has advanced by 1, so the expected proof end time will
@ -271,6 +271,72 @@ describe("Marketplace", function () {
})
})
describe("request end", function () {
var requestTime
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request)
requestTime = await currentTime()
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
})
it("shares request end time for all slots in request", async function () {
const lastSlot = request.ask.slots - 1
for (let i = 0; i < lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
advanceTime(minutes(10))
await marketplace.fillSlot(slot.request, lastSlot, proof)
let slot0 = { ...slot, index: 0 }
let end = await marketplace.requestEnd(requestId(request))
for (let i = 1; i <= lastSlot; i++) {
let sloti = { ...slot, index: i }
await expect((await marketplace.proofEnd(slotId(sloti))) === end)
}
})
it("sets the request end time to now + duration", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await expect(
(await marketplace.requestEnd(requestId(request))).toNumber()
).to.be.closeTo(requestTime + request.ask.duration, 1)
})
it("sets request end time to the past once failed", async function () {
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFailed(marketplace, request, slot)
let slot0 = { ...slot, index: request.ask.maxSlotLoss + 1 }
const now = await currentTime()
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
now - 1
)
})
it("sets request end time to the past once cancelled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request)
const now = await currentTime()
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
now - 1
)
})
it("checks that request end time is in the past once finished", async function () {
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
const now = await currentTime()
// in the process of calling currentTime and proofEnd,
// block.timestamp has advanced by 1, so the expected proof end time will
// be block.timestamp - 1.
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
now - 1
)
})
})
describe("freeing a slot", function () {
var id
beforeEach(async function () {
@ -302,8 +368,8 @@ describe("Marketplace", function () {
})
it("fails to free slot when finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith(
"Slot not accepting proofs"
)
@ -339,8 +405,8 @@ describe("Marketplace", function () {
})
it("pays the host", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
const startBalance = await token.balanceOf(host.address)
await marketplace.payoutSlot(slot.request, slot.index)
const endBalance = await token.balanceOf(host.address)
@ -355,8 +421,8 @@ describe("Marketplace", function () {
})
it("can only be done once", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(
marketplace.payoutSlot(slot.request, slot.index)
@ -364,8 +430,8 @@ describe("Marketplace", function () {
})
it("cannot be filled again", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be
.reverted
@ -507,8 +573,8 @@ describe("Marketplace", function () {
})
it("state is Finished once slot is paid out", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.Finished
@ -595,26 +661,16 @@ describe("Marketplace", function () {
})
it("fails when request Finished (isFinished is true)", async function () {
const lastSlot = await waitUntilStarted(
marketplace,
request,
slot,
proof
)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Slot not accepting proofs")
})
it("fails when request Finished (state set to Finished)", async function () {
const lastSlot = await waitUntilStarted(
marketplace,
request,
slot,
proof
)
await waitUntilFinished(marketplace, lastSlot)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(
marketplace.testAcceptsProofs(slotId(slot))
@ -634,4 +690,45 @@ describe("Marketplace", function () {
})
})
})
describe("list of active requests", function () {
beforeEach(async function () {
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
switchAccount(client)
await token.approve(marketplace.address, price(request))
})
it("adds request to list when requesting storage", async function () {
await marketplace.requestStorage(request)
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
})
it("removes request from list when funds are withdrawn", async function () {
await marketplace.requestStorage(request)
await waitUntilCancelled(request)
await marketplace.withdrawFunds(requestId(request))
expect(await marketplace.myRequests()).to.deep.equal([])
})
it("removes request from list when request fails", async function () {
await marketplace.requestStorage(request)
switchAccount(host)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFailed(marketplace, request, slot)
switchAccount(client)
expect(await marketplace.myRequests()).to.deep.equal([])
})
it("removes request from list when request finishes", async function () {
await marketplace.requestStorage(request)
switchAccount(host)
await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index)
switchAccount(client)
expect(await marketplace.myRequests()).to.deep.equal([])
})
})
})

View File

@ -76,7 +76,7 @@ describe("Storage", function () {
describe("ending the contract", function () {
it("unlocks the host collateral", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilFinished(storage, slot)
await waitUntilFinished(storage, slot.request)
await expect(storage.withdraw()).not.to.be.reverted
})
})

View File

@ -6,16 +6,13 @@ async function waitUntilCancelled(request) {
}
async function waitUntilStarted(contract, request, slot, proof) {
const lastSlotIdx = request.ask.slots - 1
for (let i = 0; i <= lastSlotIdx; i++) {
for (let i = 0; i < request.ask.slots; i++) {
await contract.fillSlot(slot.request, i, proof)
}
return { ...slot, index: lastSlotIdx }
}
async function waitUntilFinished(contract, lastSlot) {
const lastSlotId = slotId(lastSlot)
const end = (await contract.proofEnd(lastSlotId)).toNumber()
async function waitUntilFinished(contract, requestId) {
const end = (await contract.requestEnd(requestId)).toNumber()
await advanceTimeTo(end + 1)
}