[marketplace] extend proof ending

Allow proof ending to be extending once a contract is started, so that all filled slots share an ending time that is equal to the contract end time. Added tests.

Add a mapping for proof id to endId so that proof expectations can be extended for all proofs that share a given endId.

Add function modifiers that require the request state to allow proofs, with accompanying tests.

General clean up of each function’s request state context, with accompanying tests.

General clean up of all tests, including state change “wait” functions and normalising the time advancement functions.
This commit is contained in:
Eric Mastro 2022-09-21 19:57:26 +10:00 committed by Eric Mastro
parent 1fff2f7295
commit ad040cfee6
12 changed files with 414 additions and 141 deletions

View File

@ -46,12 +46,12 @@ contract AccountLocks {
/// NOTE: We do not need to check that msg.sender is the lock.owner because
/// this function is internal, and is only called after all checks have been
/// performed in Marketplace.fillSlot.
function _extendLockExpiry(bytes32 lockId, uint256 duration) internal {
function _extendLockExpiryTo(bytes32 lockId, uint256 expiry) internal {
Lock storage lock = locks[lockId];
require(lock.owner != address(0), "Lock does not exist");
// require(lock.owner == msg.sender, "Only lock creator can extend expiry");
require(lock.expiry >= block.timestamp, "Lock already expired");
lock.expiry += duration;
lock.expiry = expiry;
}
/// Unlocks an account. This will fail if there are any active locks attached

View File

@ -47,38 +47,6 @@ contract Marketplace is Collateral, Proofs {
emit StorageRequested(id, request.ask);
}
function _freeSlot(
bytes32 slotId
) internal marketplaceInvariant {
Slot storage slot = _slot(slotId);
bytes32 requestId = slot.requestId;
RequestContext storage context = requestContexts[requestId];
require(context.state == RequestState.Started, "Invalid state");
// TODO: burn host's slot collateral except for repair costs + mark proof
// missing reward
// Slot collateral is not yet implemented as the design decision was
// not finalised.
_unexpectProofs(slotId);
slot.host = address(0);
slot.requestId = 0;
context.slotsFilled -= 1;
emit SlotFreed(requestId, slotId);
Request memory request = _request(requestId);
uint256 slotsLost = request.ask.slots - context.slotsFilled;
if (slotsLost > request.ask.maxSlotLoss) {
context.state = RequestState.Failed;
emit RequestFailed(requestId);
// TODO: burn all remaining slot collateral (note: slot collateral not
// yet implemented)
// TODO: send client remaining funds
}
}
function fillSlot(
bytes32 requestId,
uint256 slotIndex,
@ -94,18 +62,20 @@ contract Marketplace is Collateral, Proofs {
require(balanceOf(msg.sender) >= collateral, "Insufficient collateral");
_lock(msg.sender, requestId);
_expectProofs(slotId, request.ask.proofProbability, request.ask.duration);
_expectProofs(slotId, requestId, request.ask.proofProbability, request.ask.duration);
_submitProof(slotId, proof);
slot.host = msg.sender;
slot.requestId = requestId;
RequestContext storage context = requestContexts[requestId];
RequestContext storage context = _context(requestId);
context.slotsFilled += 1;
emit SlotFilled(requestId, slotIndex, slotId);
if (context.slotsFilled == request.ask.slots) {
context.state = RequestState.Started;
context.startedAt = block.timestamp;
_extendLockExpiry(requestId, block.timestamp + request.ask.duration);
context.endsAt = block.timestamp + request.ask.duration;
_extendLockExpiryTo(requestId, context.endsAt);
_extendProofEndTo(slotId, context.endsAt);
emit RequestFulfilled(requestId);
}
}
@ -148,6 +118,9 @@ contract Marketplace is Collateral, Proofs {
marketplaceInvariant
{
require(_isFinished(requestId), "Contract not ended");
RequestContext storage context = _context(requestId);
context.state = RequestState.Finished;
bytes32 slotId = keccak256(abi.encode(requestId, slotIndex));
Slot storage slot = _slot(slotId);
require(!slot.hostPaid, "Already paid");
@ -165,7 +138,7 @@ contract Marketplace is Collateral, Proofs {
Request storage request = requests[requestId];
require(block.timestamp > request.expiry, "Request not yet timed out");
require(request.client == msg.sender, "Invalid client address");
RequestContext storage context = requestContexts[requestId];
RequestContext storage context = _context(requestId);
require(context.state == RequestState.New, "Invalid state");
// Update request state to Cancelled. Handle in the withdraw transaction
@ -201,13 +174,12 @@ contract Marketplace is Collateral, Proofs {
/// @param requestId the id of the request
/// @return true if request is finished
function _isFinished(bytes32 requestId) internal view returns (bool) {
RequestContext memory context = requestContexts[requestId];
Request memory request = _request(requestId);
RequestContext memory context = _context(requestId);
return
context.state == RequestState.Finished ||
(
context.state == RequestState.Started &&
block.timestamp > context.startedAt + request.ask.duration
block.timestamp > context.endsAt
);
}
@ -234,8 +206,8 @@ contract Marketplace is Collateral, Proofs {
return slots[slotId].host;
}
function _request(bytes32 id) internal view returns (Request storage) {
Request storage request = requests[id];
function _request(bytes32 requestId) internal view returns (Request storage) {
Request storage request = requests[requestId];
require(request.client != address(0), "Unknown request");
return request;
}
@ -259,6 +231,7 @@ contract Marketplace is Collateral, Proofs {
}
function proofEnd(bytes32 slotId) public view returns (uint256) {
Slot memory slot = _slot(slotId);
uint256 end = _end(slotId);
if (!_slotAcceptsProofs(slotId)) {
return end < block.timestamp ? end : block.timestamp - 1;
@ -290,6 +263,8 @@ contract Marketplace is Collateral, Proofs {
// TODO: add check for _isFinished
if (_isCancelled(requestId)) {
return RequestState.Cancelled;
else if (_isFinished(requestId) {
return RequestState.Finished;
} else {
RequestContext storage context = _context(requestId);
return context.state;
@ -358,6 +333,7 @@ contract Marketplace is Collateral, Proofs {
uint256 slotsFilled;
RequestState state;
uint256 startedAt;
uint256 endsAt;
}
struct Slot {
@ -385,6 +361,15 @@ contract Marketplace is Collateral, Proofs {
assert(funds.received == funds.balance + funds.sent);
}
function acceptsProofs(bytes32 requestId) private view {
RequestState s = state(requestId);
require(s == RequestState.New || s == RequestState.Started, "Invalid state");
// must test these states separately as they handle cases where the state hasn't
// yet been updated by a transaction
require(!_isCancelled(requestId), "Request cancelled");
require(!_isFinished(requestId), "Request finished");
}
/// @notice Modifier that requires the request state to be that which 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 slotId id of the slot, that is mapped to a request, for which to obtain state info

View File

@ -20,6 +20,7 @@ contract Proofs {
mapping(bytes32 => bool) private ids;
mapping(bytes32 => uint256) private starts;
mapping(bytes32 => uint256) private ends;
mapping(bytes32 => bytes32) private idEnds;
mapping(bytes32 => uint256) private probabilities;
mapping(bytes32 => uint256) private markers;
mapping(bytes32 => uint256) private missed;
@ -34,8 +35,21 @@ contract Proofs {
return timeout;
}
function _end(bytes32 id) internal view returns (uint256) {
return ends[id];
function _end(bytes32 endId) internal view returns (uint256) {
uint256 end = ends[endId];
require(end > 0, "Proof ending doesn't exist");
return ends[endId];
}
function _endId(bytes32 id) internal view returns (bytes32) {
bytes32 endId = idEnds[id];
require(endId > 0, "endId for given id doesn't exist");
return endId;
}
function _endFromId(bytes32 id) internal view returns (uint256) {
bytes32 endId = _endId(id);
return _end(endId);
}
function _missed(bytes32 id) internal view returns (uint256) {
@ -50,17 +64,25 @@ contract Proofs {
return periodOf(block.timestamp);
}
/// @notice Informs the contract that proofs should be expected for id
/// @dev Requires that the id is not already in use
/// @param id identifies the proof expectation, typically a slot id
/// @param endId Identifies the id of the proof expectation ending. Typically a request id. Different from id because the proof ending is shared amongst many ids.
/// @param probability The probability that a proof should be expected
/// @param duration Duration, from now, for which proofs should be expected
function _expectProofs(
bytes32 id,
bytes32 id, // typically slot id
bytes32 endId, // typically request id, used so that the ending is global for all slots
uint256 probability,
uint256 duration
) internal {
require(!ids[id], "Proof id already in use");
ids[id] = true;
starts[id] = block.timestamp;
ends[id] = block.timestamp + duration;
ends[endId] = block.timestamp + duration;
probabilities[id] = probability;
markers[id] = uint256(blockhash(block.number - 1)) % period;
idEnds[id] = endId;
}
function _unexpectProofs(
@ -112,7 +134,8 @@ contract Proofs {
if (proofPeriod <= periodOf(starts[id])) {
return (false, 0);
}
if (proofPeriod >= periodOf(ends[id])) {
uint256 end = _endFromId(id);
if (proofPeriod >= periodOf(end)) {
return (false, 0);
}
pointer = _getPointer(id, proofPeriod);
@ -162,5 +185,19 @@ contract Proofs {
missed[id] += 1;
}
/// @notice Extends the proof end time
/// @dev The id must have a mapping to an end id, the end must exist, and the end must not have elapsed yet
/// @param id the id of the proofs to extend. Typically a slot id, the id is mapped to an endId.
/// @param ending the new end time (in seconds)
function _extendProofEndTo(bytes32 id, uint256 ending) internal {
bytes32 endId = _endId(id);
uint256 end = ends[endId];
// TODO: create type aliases for id and endId so that _end() can return
// EndId storage and we don't need to replicate the below require here
require (end > 0, "Proof ending doesn't exist");
require (block.timestamp <= end, "Proof already ended");
ends[endId] = ending;
}
event ProofSubmitted(bytes32 id, bytes proof);
}

View File

@ -82,7 +82,6 @@ contract Storage is Collateral, Marketplace {
function markProofAsMissing(bytes32 slotId, uint256 period)
public
slotMustAcceptProofs(slotId)
{
_markProofAsMissing(slotId, period);
address host = _host(slotId);

View File

@ -21,7 +21,7 @@ contract TestAccountLocks is AccountLocks {
_unlockAccount();
}
function extendLockExpiry(bytes32 lockId, uint256 expiry) public {
_extendLockExpiry(lockId, expiry);
function extendLockExpiryTo(bytes32 lockId, uint256 expiry) public {
_extendLockExpiryTo(lockId, expiry);
}
}

View File

@ -33,4 +33,13 @@ contract TestMarketplace is Marketplace {
function slot(bytes32 slotId) public view returns (Slot memory) {
return _slot(slotId);
}
function testAcceptsProofs(bytes32 slotId)
public
view
slotAcceptsProofs(slotId)
// solhint-disable-next-line no-empty-blocks
{
}
}

View File

@ -34,10 +34,11 @@ contract TestProofs is Proofs {
function expectProofs(
bytes32 id,
bytes32 endId,
uint256 _probability,
uint256 _duration
) public {
_expectProofs(id, _probability, _duration);
_expectProofs(id, endId, _probability, _duration);
}
function unexpectProofs(bytes32 id) public {
@ -67,4 +68,8 @@ contract TestProofs is Proofs {
function markProofAsMissing(bytes32 id, uint256 _period) public {
_markProofAsMissing(id, _period);
}
function extendProofEndTo(bytes32 id, uint256 ending) public {
_extendProofEndTo(id, ending);
}
}

View File

@ -1,7 +1,7 @@
const { ethers } = require("hardhat")
const { expect } = require("chai")
const { hexlify, randomBytes, toHexString } = ethers.utils
const { advanceTimeTo, snapshot, revert } = require("./evm")
const { advanceTimeTo, snapshot, revert, advanceTime } = require("./evm")
const { exampleLock } = require("./examples")
const { now, hours } = require("./time")
const { waitUntilExpired } = require("./marketplace")
@ -188,7 +188,7 @@ describe("Account Locks", function () {
it("fails when lock id doesn't exist", async function () {
let other = exampleLock()
await expect(
locks.extendLockExpiry(other.id, hours(1))
locks.extendLockExpiryTo(other.id, now() + hours(1))
).to.be.revertedWith("Lock does not exist")
})
@ -200,7 +200,16 @@ describe("Account Locks", function () {
})
it("successfully updates lock expiry", async function () {
await expect(locks.extendLockExpiry(id, hours(1))).not.to.be.reverted
await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be
.reverted
})
it("unlocks account after expiry", async function () {
await expect(locks.extendLockExpiryTo(id, now() + hours(1))).not.to.be
.reverted
await expect(locks.unlockAccount()).to.be.revertedWith("Account locked")
advanceTime(hours(1))
await expect(locks.unlockAccount()).not.to.be.reverted
})
})
})

View File

@ -2,16 +2,21 @@ const { ethers } = require("hardhat")
const { hexlify, randomBytes } = ethers.utils
const { expect } = require("chai")
const { exampleRequest } = require("./examples")
const { now, hours } = require("./time")
const { now, hours, minutes } = require("./time")
const { requestId, slotId, askToArray } = require("./ids")
const { waitUntilExpired, waitUntilAllSlotsFilled, RequestState } = require("./marketplace")
const { waitUntilExpired, waitUntilAllSlotsFilled } = require("./marketplace")
waitUntilCancelled,
waitUntilStarted,
waitUntilFinished,
waitUntilFailed,
} = require("./marketplace")
const { price, pricePerSlot } = require("./price")
const {
snapshot,
revert,
ensureMinimumBlockHeight,
advanceTimeTo,
currentTime,
advanceTime,
currentTime
} = require("./evm")
describe("Marketplace", function () {
@ -115,8 +120,6 @@ describe("Marketplace", function () {
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
// await marketplace.fillSlot(slot.request, slot.index, proof)
})
it("fails to free slot when slot not filled", async function () {
@ -132,8 +135,21 @@ describe("Marketplace", function () {
await expect(marketplace.freeSlot(id)).to.be.revertedWith("Invalid state")
})
it("fails to free slot when finished", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFinished(marketplace, slotId(slot))
await expect(marketplace.freeSlot(id)).to.be.revertedWith(
"Request finished"
)
})
it("successfully frees slot", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
@ -143,7 +159,7 @@ describe("Marketplace", function () {
})
it("emits event once slot is freed", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
@ -155,7 +171,7 @@ describe("Marketplace", function () {
})
it("cannot get slot once freed", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
@ -234,6 +250,32 @@ describe("Marketplace", function () {
).to.be.revertedWith("Request not accepting proofs")
})
it("is rejected when request is finished", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFinished(marketplace, slotId(slot))
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Request finished")
})
it("is rejected when request is failed", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss)
await expect(
marketplace.fillSlot(slot.request, slot.index, proof)
).to.be.revertedWith("Invalid state")
})
it("is rejected when slot index not in range", async function () {
const invalid = request.ask.slots
await expect(
@ -250,6 +292,20 @@ describe("Marketplace", function () {
marketplace.fillSlot(slot.request, lastSlot, proof)
).to.be.revertedWith("Slot already filled")
})
it("shares proof 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.proofEnd(slotId(slot0))
for (let i = 1; i <= lastSlot; i++) {
let sloti = { ...slot, index: i }
await expect((await marketplace.proofEnd(slotId(sloti))) === end)
}
})
})
describe("freeing a slot", function () {
@ -318,19 +374,14 @@ describe("Marketplace", function () {
await marketplace.deposit(collateral)
})
async function waitUntilEnd() {
const end = (await marketplace.proofEnd(slotId(slot))).toNumber()
await advanceTimeTo(end)
}
it("pays the host", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilEnd()
await waitUntilFinished(marketplace, slotId(slot))
const startBalance = await token.balanceOf(host.address)
await marketplace.payoutSlot(slot.request, slot.index)
const endBalance = await token.balanceOf(host.address)
@ -351,13 +402,13 @@ describe("Marketplace", function () {
})
it("can only be done once", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilEnd()
await waitUntilFinished(marketplace, slotId(slot))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(
marketplace.payoutSlot(slot.request, slot.index)
@ -365,13 +416,13 @@ describe("Marketplace", function () {
})
it("cannot be filled again", async function () {
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilEnd()
await waitUntilFinished(marketplace, slotId(slot))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(marketplace.fillSlot(slot.request, slot.index, proof)).to.be
.reverted
@ -403,8 +454,15 @@ describe("Marketplace", function () {
await marketplace.fillSlot(slot.request, i, proof)
}
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.Started
)
})
it("fails when all slots are already filled", async function () {
const lastSlot = request.ask.slots - 1
for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
await expect(
marketplace.fillSlot(slot.request, lastSlot, proof)
).to.be.revertedWith("Slot already filled")
})
})
@ -426,7 +484,7 @@ describe("Marketplace", function () {
})
it("rejects withdraw when wrong account used", async function () {
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
"Invalid client address"
)
@ -438,7 +496,7 @@ describe("Marketplace", function () {
for (let i = 0; i <= lastSlot; i++) {
await marketplace.fillSlot(slot.request, i, proof)
}
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request)).to.be.revertedWith(
"Invalid state"
@ -446,7 +504,7 @@ describe("Marketplace", function () {
})
it("emits event once request is cancelled", async function () {
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
switchAccount(client)
await expect(marketplace.withdrawFunds(slot.request))
.to.emit(marketplace, "RequestCancelled")
@ -454,7 +512,7 @@ describe("Marketplace", function () {
})
it("withdraws to the client", async function () {
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
switchAccount(client)
const startBalance = await token.balanceOf(client.address)
await marketplace.withdrawFunds(slot.request)
@ -473,11 +531,14 @@ describe("Marketplace", function () {
await marketplace.deposit(collateral)
})
it("changes state to Cancelled when client withdraws funds", async function () {
it("state is Cancelled when client withdraws funds", async function () {
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.New
)
await waitUntilExpired(request.expiry)
})
it("state is Cancelled when client withdraws funds", async function () {
await waitUntilCancelled(request.expiry)
switchAccount(client)
await marketplace.withdrawFunds(slot.request)
await expect(await marketplace.state(slot.request)).to.equal(
@ -486,10 +547,7 @@ describe("Marketplace", function () {
})
it("changes state to Started once all slots are filled", async function () {
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.New
)
await waitUntilAllSlotsFilled(
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
@ -500,23 +558,32 @@ describe("Marketplace", function () {
)
})
// fill all slots, should change state to RequestState.Started
await waitUntilAllSlotsFilled(
it("state is Failed once too many slots are freed", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
slot.index = i
let id = slotId(slot)
await marketplace.freeSlot(id)
}
await waitUntilFailed(marketplace, slot, request.ask.maxSlotLoss)
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.Failed
)
})
it("state is Finished once slot is paid out", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFinished(marketplace, slotId(slot))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(await marketplace.state(slot.request)).to.equal(
RequestState.Finished
)
})
it("does not change state to Failed if too many slots freed but contract not started", async function () {
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
await marketplace.fillSlot(slot.request, i, proof)
@ -568,4 +635,79 @@ describe("Marketplace", function () {
)
})
})
describe("modifiers", function () {
beforeEach(async function () {
switchAccount(client)
await token.approve(marketplace.address, price(request))
await marketplace.requestStorage(request)
switchAccount(host)
await token.approve(marketplace.address, collateral)
await marketplace.deposit(collateral)
})
describe("accepting proofs", function () {
it("fails when request Cancelled (isCancelled is true)", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request.expiry)
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Request cancelled")
})
it("fails when request Cancelled (state set to Cancelled)", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request.expiry)
switchAccount(client)
await marketplace.withdrawFunds(slot.request)
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Invalid state")
})
it("fails when request Finished (isFinished is true)", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFinished(marketplace, slotId(slot))
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Request finished")
})
it("fails when request Finished (state set to Finished)", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
await waitUntilFinished(marketplace, slotId(slot))
await marketplace.payoutSlot(slot.request, slot.index)
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Invalid state")
})
it("fails when request Failed", async function () {
await waitUntilStarted(
marketplace,
request.ask.slots,
slot.request,
proof
)
for (let i = 0; i <= request.ask.maxSlotLoss; i++) {
slot.index = i
let id = slotId(slot)
await marketplace.freeSlot(id)
}
await expect(
marketplace.testAcceptsProofs(slotId(slot))
).to.be.revertedWith("Slot empty")
})
})
})
})

View File

@ -10,10 +10,11 @@ const {
advanceTime,
advanceTimeTo,
} = require("./evm")
const { periodic } = require("./time")
const { periodic, hours, now, minutes } = require("./time")
describe("Proofs", function () {
const id = hexlify(randomBytes(32))
const endId = hexlify(randomBytes(32))
const period = 30 * 60
const timeout = 5
const downtime = 64
@ -35,20 +36,20 @@ describe("Proofs", function () {
})
it("calculates an end time based on duration", async function () {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
let end = (await currentTime()) + duration
expect((await proofs.end(id)).toNumber()).to.be.closeTo(end, 1)
expect((await proofs.end(endId)).toNumber()).to.be.closeTo(end, 1)
})
it("does not allow ids to be reused", async function () {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
await expect(
proofs.expectProofs(id, probability, duration)
proofs.expectProofs(id, endId, probability, duration)
).to.be.revertedWith("Proof id already in use")
})
it("requires proofs with an agreed upon probability", async function () {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
let amount = 0
for (let i = 0; i < 100; i++) {
if (await proofs.isProofRequired(id)) {
@ -63,7 +64,7 @@ describe("Proofs", function () {
it("requires no proofs in the start period", async function () {
const startPeriod = Math.floor((await currentTime()) / period)
const probability = 1
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
while (Math.floor((await currentTime()) / period) == startPeriod) {
expect(await proofs.isProofRequired(id)).to.be.false
await advanceTime(Math.floor(period / 10))
@ -72,14 +73,14 @@ describe("Proofs", function () {
it("requires no proofs in the end period", async function () {
const probability = 1
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
await advanceTime(duration)
expect(await proofs.isProofRequired(id)).to.be.false
})
it("requires no proofs after the end time", async function () {
const probability = 1
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
await advanceTime(duration + timeout)
expect(await proofs.isProofRequired(id)).to.be.false
})
@ -89,7 +90,7 @@ describe("Proofs", function () {
let id2 = hexlify(randomBytes(32))
let id3 = hexlify(randomBytes(32))
for (let id of [id1, id2, id3]) {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
}
let req1, req2, req3
while (req1 === req2 && req2 === req3) {
@ -118,7 +119,7 @@ describe("Proofs", function () {
}
beforeEach(async function () {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await waitUntilProofWillBeRequired()
})
@ -151,7 +152,7 @@ describe("Proofs", function () {
const proof = hexlify(randomBytes(42))
beforeEach(async function () {
await proofs.expectProofs(id, probability, duration)
await proofs.expectProofs(id, endId, probability, duration)
})
async function waitUntilProofIsRequired(id) {
@ -270,4 +271,87 @@ describe("Proofs", function () {
await expect(await proofs.isProofRequired(id)).to.be.false
})
})
describe("extend proof end", function () {
const proof = hexlify(randomBytes(42))
beforeEach(async function () {
await proofs.expectProofs(id, endId, probability, duration)
})
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
while (
!(
(await proofs.isProofRequired(id)) &&
(await proofs.getPointer(id)) < 250
)
) {
await advanceTime(period)
}
}
async function isProofRequiredBefore(id, ending) {
let start = periodOf(await currentTime())
let end = periodOf(ending)
let periods = end - start
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
for (let i = 0; i < periods; i++) {
if (await proofs.isProofRequired(id)) {
return true
}
await advanceTime(period)
}
return false
}
it("can't extend if proof doesn't exist", async function () {
let ending = (await currentTime()) + duration
const otherId = hexlify(randomBytes(32))
await expect(
proofs.extendProofEndTo(otherId, ending + 1)
).to.be.revertedWith("endId for given id doesn't exist")
})
it("can't extend already lapsed proof ending", async function () {
let ending = (await currentTime()) + duration
await waitUntilProofIsRequired(id)
await advanceTimeTo(ending + 1)
await expect(proofs.extendProofEndTo(id, ending + 1)).to.be.revertedWith(
"Proof already ended"
)
})
it("requires no proofs when ending has not been extended", async function () {
let ending = (await currentTime()) + duration
await expect(await isProofRequiredBefore(id, ending)).to.be.true
let endingExtended = ending + hours(1)
await advanceTimeTo(periodEnd(periodOf(endingExtended) + 1))
await expect(await isProofRequiredBefore(id, endingExtended)).to.be.false
})
it("requires proofs when ending has been extended", async function () {
let ending = (await currentTime()) + duration
await expect(await isProofRequiredBefore(id, ending)).to.be.true
let endingExtended = ending + hours(1)
await proofs.extendProofEndTo(id, endingExtended)
await expect(await isProofRequiredBefore(id, endingExtended)).to.be.true
})
it("no longer requires proofs after extension lapsed", async function () {
async function expectNoProofsForPeriods(id, periods) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
for (let i = 0; i < periods; i++) {
await expect(await proofs.isProofRequired(id)).to.be.false
await advanceTime(period)
}
}
let ending = (await currentTime()) + duration
let endingExtended = ending + hours(1)
await proofs.extendProofEndTo(id, endingExtended)
await advanceTimeTo(periodEnd(periodOf(endingExtended) + 1))
await expectNoProofsForPeriods(id, 100)
})
})
})

View File

@ -8,7 +8,11 @@ const { advanceTime, advanceTimeTo, currentTime, mine } = require("./evm")
const { requestId, slotId } = require("./ids")
const { periodic } = require("./time")
const { price } = require("./price")
const { waitUntilExpired, waitUntilAllSlotsFilled } = require("./marketplace")
const {
waitUntilCancelled,
waitUntilStarted,
waitUntilFinished,
} = require("./marketplace")
describe("Storage", function () {
const proof = hexlify(randomBytes(42))
@ -70,14 +74,9 @@ describe("Storage", function () {
})
describe("ending the contract", function () {
async function waitUntilEnd() {
const end = (await storage.proofEnd(slotId(slot))).toNumber()
await advanceTimeTo(end)
}
it("unlocks the host collateral", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilEnd()
await waitUntilFinished(storage, slotId(slot))
await expect(storage.withdraw()).not.to.be.reverted
})
})
@ -178,7 +177,7 @@ describe("Storage", function () {
it("fails to mark proof as missing when cancelled", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await advanceTimeTo(request.expiry + 1)
await waitUntilCancelled(request.expiry)
let missedPeriod = periodOf(await currentTime())
await expect(
storage.markProofAsMissing(slotId(slot), missedPeriod)
@ -258,12 +257,7 @@ describe("Storage", function () {
it("frees slot when collateral slashed below minimum threshold", async function () {
const id = slotId(slot)
await waitUntilAllSlotsFilled(
storage,
request.ask.slots,
slot.request,
proof
)
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
while (true) {
await markProofAsMissing(id)
@ -289,7 +283,7 @@ describe("Storage", function () {
describe("contract state", function () {
it("isCancelled is true once request is cancelled", async function () {
await expect(await storage.isCancelled(slot.request)).to.equal(false)
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
await expect(await storage.isCancelled(slot.request)).to.equal(true)
})
@ -301,19 +295,14 @@ describe("Storage", function () {
it("isSlotCancelled is true once request is cancelled", async function () {
await storage.fillSlot(slot.request, slot.index, proof)
await waitUntilExpired(request.expiry)
await waitUntilCancelled(request.expiry)
await expect(await storage.isSlotCancelled(slotId(slot))).to.equal(true)
})
it("isFinished is true once started and contract duration lapses", async function () {
await expect(await storage.isFinished(slot.request)).to.be.false
// fill all slots, should change state to RequestState.Started
await waitUntilAllSlotsFilled(
storage,
request.ask.slots,
slot.request,
proof
)
await waitUntilStarted(storage, request.ask.slots, slot.request, proof)
await expect(await storage.isFinished(slot.request)).to.be.false
advanceTime(request.ask.duration + 1)
await expect(await storage.isFinished(slot.request)).to.be.true

View File

@ -1,19 +1,33 @@
const RequestState = {
New: 0,
Started: 1,
Cancelled: 2,
Finished: 3,
Failed: 4,
const { advanceTimeTo } = require("./evm")
const { slotId } = require("./ids")
async function waitUntilCancelled(expiry) {
await advanceTimeTo(expiry + 1)
}
async function waitUntilExpired(expiry) {
await ethers.provider.send("hardhat_mine", [ethers.utils.hexValue(expiry)])
}
async function waitUntilAllSlotsFilled(contract, numSlots, requestId, proof) {
for (let i = 0; i < numSlots; i++) {
async function waitUntilStarted(contract, numSlots, requestId, proof) {
const lastSlot = numSlots - 1
for (let i = 0; i <= lastSlot; i++) {
await contract.fillSlot(requestId, i, proof)
}
}
module.exports = { waitUntilExpired, waitUntilAllSlotsFilled, RequestState }
async function waitUntilFinished(contract, slotId) {
const end = (await contract.proofEnd(slotId)).toNumber()
await advanceTimeTo(end + 1)
}
async function waitUntilFailed(contract, slot, maxSlotLoss) {
for (let i = 0; i <= maxSlotLoss; i++) {
slot.index = i
let id = slotId(slot)
await contract.freeSlot(id)
}
}
module.exports = {
waitUntilCancelled,
waitUntilStarted,
waitUntilFinished,
waitUntilFailed,
}