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

View File

@ -9,7 +9,7 @@ const {
waitUntilStarted, waitUntilStarted,
waitUntilFinished, waitUntilFinished,
waitUntilFailed, waitUntilFailed,
RequestState RequestState,
} = require("./marketplace") } = require("./marketplace")
const { price, pricePerSlot } = require("./price") const { price, pricePerSlot } = require("./price")
const { const {
@ -178,8 +178,8 @@ describe("Marketplace", function () {
}) })
it("is rejected when request is finished", async function () { it("is rejected when request is finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
await expect( await expect(
marketplace.fillSlot(slot.request, slot.index, proof) marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Request not accepting proofs") ).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 () { it("checks that proof end time is in the past once finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
const now = await currentTime() const now = await currentTime()
// in the process of calling currentTime and proofEnd, // in the process of calling currentTime and proofEnd,
// block.timestamp has advanced by 1, so the expected proof end time will // 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 () { describe("freeing a slot", function () {
var id var id
beforeEach(async function () { beforeEach(async function () {
@ -302,8 +368,8 @@ describe("Marketplace", function () {
}) })
it("fails to free slot when finished", async function () { it("fails to free slot when finished", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith( await expect(marketplace.freeSlot(slotId(slot))).to.be.revertedWith(
"Slot not accepting proofs" "Slot not accepting proofs"
) )
@ -339,8 +405,8 @@ describe("Marketplace", function () {
}) })
it("pays the host", async function () { it("pays the host", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
const startBalance = await token.balanceOf(host.address) const startBalance = await token.balanceOf(host.address)
await marketplace.payoutSlot(slot.request, slot.index) await marketplace.payoutSlot(slot.request, slot.index)
const endBalance = await token.balanceOf(host.address) const endBalance = await token.balanceOf(host.address)
@ -355,8 +421,8 @@ describe("Marketplace", function () {
}) })
it("can only be done once", async function () { it("can only be done once", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index) await marketplace.payoutSlot(slot.request, slot.index)
await expect( await expect(
marketplace.payoutSlot(slot.request, slot.index) marketplace.payoutSlot(slot.request, slot.index)
@ -364,8 +430,8 @@ describe("Marketplace", function () {
}) })
it("cannot be filled again", async function () { it("cannot be filled again", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index) await marketplace.payoutSlot(slot.request, slot.index)
await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be
.reverted .reverted
@ -507,8 +573,8 @@ describe("Marketplace", function () {
}) })
it("state is Finished once slot is paid out", async function () { it("state is Finished once slot is paid out", async function () {
const lastSlot = await waitUntilStarted(marketplace, request, slot, proof) await waitUntilStarted(marketplace, request, slot, proof)
await waitUntilFinished(marketplace, lastSlot) await waitUntilFinished(marketplace, requestId(request))
await marketplace.payoutSlot(slot.request, slot.index) await marketplace.payoutSlot(slot.request, slot.index)
await expect(await marketplace.state(slot.request)).to.equal( await expect(await marketplace.state(slot.request)).to.equal(
RequestState.Finished RequestState.Finished
@ -595,26 +661,16 @@ describe("Marketplace", function () {
}) })
it("fails when request Finished (isFinished is true)", async function () { it("fails when request Finished (isFinished is true)", async function () {
const lastSlot = await waitUntilStarted( await waitUntilStarted(marketplace, request, slot, proof)
marketplace, await waitUntilFinished(marketplace, requestId(request))
request,
slot,
proof
)
await waitUntilFinished(marketplace, lastSlot)
await expect( await expect(
marketplace.testAcceptsProofs(slotId(slot)) marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Slot not accepting proofs") ).to.be.revertedWith("Slot not accepting proofs")
}) })
it("fails when request Finished (state set to Finished)", async function () { it("fails when request Finished (state set to Finished)", async function () {
const lastSlot = await waitUntilStarted( await waitUntilStarted(marketplace, request, slot, proof)
marketplace, await waitUntilFinished(marketplace, requestId(request))
request,
slot,
proof
)
await waitUntilFinished(marketplace, lastSlot)
await marketplace.payoutSlot(slot.request, slot.index) await marketplace.payoutSlot(slot.request, slot.index)
await expect( await expect(
marketplace.testAcceptsProofs(slotId(slot)) 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 () { describe("ending the contract", function () {
it("unlocks the host collateral", async function () { it("unlocks the host collateral", async function () {
await storage.fillSlot(slot.request, slot.index, proof) 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 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) { async function waitUntilStarted(contract, request, slot, proof) {
const lastSlotIdx = request.ask.slots - 1 for (let i = 0; i < request.ask.slots; i++) {
for (let i = 0; i <= lastSlotIdx; i++) {
await contract.fillSlot(slot.request, i, proof) await contract.fillSlot(slot.request, i, proof)
} }
return { ...slot, index: lastSlotIdx }
} }
async function waitUntilFinished(contract, lastSlot) { async function waitUntilFinished(contract, requestId) {
const lastSlotId = slotId(lastSlot) const end = (await contract.requestEnd(requestId)).toNumber()
const end = (await contract.proofEnd(lastSlotId)).toNumber()
await advanceTimeTo(end + 1) await advanceTimeTo(end + 1)
} }