diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index bc2b03e..23a5939 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -37,6 +37,11 @@ contract Marketplace is Collateral, Proofs { require(requests[id].client == address(0), "Request already exists"); requests[id] = request; + RequestContext storage context = _context(id); + // set contract end time to `duration` from now (time request was created) + context.endsAt = block.timestamp + request.ask.duration; + _setProofEnd(id, context.endsAt); + _createLock(id, request.expiry); @@ -63,14 +68,13 @@ 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); _submitProof(slotId, proof); slot.host = msg.sender; slot.requestId = requestId; RequestContext storage context = _context(requestId); context.slotsFilled += 1; - context.endsAt = block.timestamp + request.ask.duration; emit SlotFilled(requestId, slotIndex, slotId); if (context.slotsFilled == request.ask.slots) { context.state = RequestState.Started; @@ -105,6 +109,7 @@ contract Marketplace is Collateral, Proofs { context.state == RequestState.Started) { context.state = RequestState.Failed; + _setProofEnd(requestId, block.timestamp - 1); context.endsAt = block.timestamp - 1; emit RequestFailed(requestId); @@ -233,15 +238,11 @@ contract Marketplace is Collateral, Proofs { function proofEnd(bytes32 slotId) public view returns (uint256) { Slot memory slot = _slot(slotId); - uint256 end = _end(slotId); - RequestContext storage context = _context(slot.requestId); + uint256 end = _end(slot.requestId); if (_slotAcceptsProofs(slotId)) { return end; } else { - // Calculate the earliest ending between a slot and a request. - // Request endings are set, for eg, when a request fails. - uint256 earliestEnd = Math.min(end, context.endsAt); - return Math.min(earliestEnd, block.timestamp - 1); + return Math.min(end, block.timestamp - 1); } } @@ -276,7 +277,6 @@ 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 diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol index a6a20dd..3210106 100644 --- a/contracts/Proofs.sol +++ b/contracts/Proofs.sol @@ -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,10 +35,21 @@ contract Proofs { return timeout; } - function _end(bytes32 id) internal view returns (uint256) { - uint256 end = ends[id]; + function _end(bytes32 endId) internal view returns (uint256) { + uint256 end = ends[endId]; require(end > 0, "Proof ending doesn't exist"); - return ends[id]; + 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) { @@ -55,19 +67,19 @@ contract Proofs { /// @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, // typically slot id - uint256 probability, - uint256 duration + bytes32 endId, // typically request id, used so that the ending is global for all slots + uint256 probability ) internal { require(!ids[id], "Proof id already in use"); ids[id] = true; starts[id] = block.timestamp; - ends[id] = block.timestamp + duration; probabilities[id] = probability; markers[id] = uint256(blockhash(block.number - 1)) % period; + idEnds[id] = endId; } function _unexpectProofs( @@ -119,7 +131,7 @@ contract Proofs { if (proofPeriod <= periodOf(starts[id])) { return (false, 0); } - uint256 end = _end(id); + uint256 end = _endFromId(id); if (proofPeriod >= periodOf(end)) { return (false, 0); } @@ -169,5 +181,16 @@ contract Proofs { missed[id] += 1; } + /// @notice Sets the proof end time + /// @dev Can only be set once + /// @param endId the endId of the proofs to extend (typically a request id). + /// @param ending the new end time (in seconds) + function _setProofEnd(bytes32 endId, uint256 ending) internal { + // 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 (ends[endId] == 0 || ending < block.timestamp, "End exists or must be past"); + ends[endId] = ending; + } + event ProofSubmitted(bytes32 id, bytes proof); } diff --git a/contracts/TestProofs.sol b/contracts/TestProofs.sol index 9cdb8fb..d9d01c3 100644 --- a/contracts/TestProofs.sol +++ b/contracts/TestProofs.sol @@ -34,10 +34,10 @@ contract TestProofs is Proofs { function expectProofs( bytes32 id, - uint256 _probability, - uint256 _duration + bytes32 endId, + uint256 _probability ) public { - _expectProofs(id, _probability, _duration); + _expectProofs(id, endId, _probability); } function unexpectProofs(bytes32 id) public { @@ -67,4 +67,8 @@ contract TestProofs is Proofs { function markProofAsMissing(bytes32 id, uint256 _period) public { _markProofAsMissing(id, _period); } + + function setProofEnd(bytes32 id, uint256 ending) public { + _setProofEnd(id, ending); + } } diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index ee30bf8..37d323c 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -212,10 +212,12 @@ describe("Marketplace", function () { }) describe("proof 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) @@ -236,11 +238,11 @@ describe("Marketplace", function () { } }) - it("sets proof end time to the request duration once filled", async function () { + it("sets the proof end time to now + duration", async function () { await marketplace.fillSlot(slot.request, slot.index, proof) - await expect(await marketplace.proofEnd(slotId(slot))).to.be.eq( - (await currentTime()) + request.ask.duration - ) + await expect( + (await marketplace.proofEnd(slotId(slot))).toNumber() + ).to.be.closeTo(requestTime + request.ask.duration, 1) }) it("sets proof end time to the past once failed", async function () { @@ -258,15 +260,14 @@ describe("Marketplace", function () { await expect(await marketplace.proofEnd(slotId(slot))).to.be.eq(now - 1) }) - it("sets proof end time to 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 waitUntilFinished(marketplace, lastSlot) // sets proofEnd to block.timestamp - 1 + await waitUntilFinished(marketplace, lastSlot) const now = await currentTime() - // the proof end time is set to block.timestamp - 1 when the contract is - // finished. 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 - // be block.timestamp - 2. - await expect(await marketplace.proofEnd(slotId(slot))).to.be.eq(now - 2) + // be block.timestamp - 1. + await expect(await marketplace.proofEnd(slotId(slot))).to.be.eq(now - 1) }) }) diff --git a/test/Proofs.test.js b/test/Proofs.test.js index af99803..24cd6a0 100644 --- a/test/Proofs.test.js +++ b/test/Proofs.test.js @@ -14,6 +14,7 @@ const { periodic, hours, 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 @@ -34,80 +35,80 @@ describe("Proofs", function () { await revert() }) - it("calculates an end time based on duration", async function () { - await proofs.expectProofs(id, probability, duration) - let end = (await currentTime()) + duration - expect((await proofs.end(id)).toNumber()).to.be.closeTo(end, 1) - }) + describe("general", function () { + beforeEach(async function () { + await proofs.setProofEnd(endId, (await currentTime()) + duration) + }) - it("does not allow ids to be reused", async function () { - await proofs.expectProofs(id, probability, duration) - await expect( - proofs.expectProofs(id, probability, duration) - ).to.be.revertedWith("Proof id already in use") - }) + it("does not allow ids to be reused", async function () { + await proofs.expectProofs(id, endId, probability) + await expect( + proofs.expectProofs(id, endId, probability) + ).to.be.revertedWith("Proof id already in use") + }) - it("requires proofs with an agreed upon probability", async function () { - await proofs.expectProofs(id, probability, duration) - let amount = 0 - for (let i = 0; i < 100; i++) { - if (await proofs.isProofRequired(id)) { - amount += 1 + it("requires proofs with an agreed upon probability", async function () { + await proofs.expectProofs(id, endId, probability) + let amount = 0 + for (let i = 0; i < 100; i++) { + if (await proofs.isProofRequired(id)) { + amount += 1 + } + await advanceTime(period) } - await advanceTime(period) - } - let expected = 100 / probability - expect(amount).to.be.closeTo(expected, expected / 2) - }) + let expected = 100 / probability + expect(amount).to.be.closeTo(expected, expected / 2) + }) - 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) - while (Math.floor((await currentTime()) / period) == startPeriod) { + it("requires no proofs in the start period", async function () { + const startPeriod = Math.floor((await currentTime()) / period) + const probability = 1 + await proofs.expectProofs(id, endId, probability) + while (Math.floor((await currentTime()) / period) == startPeriod) { + expect(await proofs.isProofRequired(id)).to.be.false + await advanceTime(Math.floor(period / 10)) + } + }) + + it("requires no proofs in the end period", async function () { + const probability = 1 + await proofs.expectProofs(id, endId, probability) + await advanceTime(duration) expect(await proofs.isProofRequired(id)).to.be.false - await advanceTime(Math.floor(period / 10)) - } - }) + }) - it("requires no proofs in the end period", async function () { - const probability = 1 - await proofs.expectProofs(id, 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, endId, probability) + await advanceTime(duration + timeout) + 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 advanceTime(duration + timeout) - expect(await proofs.isProofRequired(id)).to.be.false - }) + it("requires proofs for different ids at different times", async function () { + let id1 = hexlify(randomBytes(32)) + let id2 = hexlify(randomBytes(32)) + let id3 = hexlify(randomBytes(32)) + for (let id of [id1, id2, id3]) { + await proofs.expectProofs(id, endId, probability) + } + let req1, req2, req3 + while (req1 === req2 && req2 === req3) { + req1 = await proofs.isProofRequired(id1) + req2 = await proofs.isProofRequired(id2) + req3 = await proofs.isProofRequired(id3) + await advanceTime(period) + } + }) - it("requires proofs for different ids at different times", async function () { - let id1 = hexlify(randomBytes(32)) - let id2 = hexlify(randomBytes(32)) - let id3 = hexlify(randomBytes(32)) - for (let id of [id1, id2, id3]) { - await proofs.expectProofs(id, probability, duration) - } - let req1, req2, req3 - while (req1 === req2 && req2 === req3) { - req1 = await proofs.isProofRequired(id1) - req2 = await proofs.isProofRequired(id2) - req3 = await proofs.isProofRequired(id3) - await advanceTime(period) - } - }) - - it("moves pointer one block at a time", async function () { - await advanceTimeTo(periodEnd(periodOf(await currentTime()))) - for (let i = 0; i < 256; i++) { - let previous = await proofs.getPointer(id) - await mine() - let current = await proofs.getPointer(id) - expect(current).to.equal((previous + 1) % 256) - } + it("moves pointer one block at a time", async function () { + await advanceTimeTo(periodEnd(periodOf(await currentTime()))) + for (let i = 0; i < 256; i++) { + let previous = await proofs.getPointer(id) + await mine() + let current = await proofs.getPointer(id) + expect(current).to.equal((previous + 1) % 256) + } + }) }) describe("when proof requirement is upcoming", function () { @@ -118,7 +119,8 @@ describe("Proofs", function () { } beforeEach(async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.setProofEnd(endId, (await currentTime()) + duration) + await proofs.expectProofs(id, endId, probability) await advanceTimeTo(periodEnd(periodOf(await currentTime()))) await waitUntilProofWillBeRequired() }) @@ -151,7 +153,8 @@ describe("Proofs", function () { const proof = hexlify(randomBytes(42)) beforeEach(async function () { - await proofs.expectProofs(id, probability, duration) + await proofs.setProofEnd(endId, (await currentTime()) + duration) + await proofs.expectProofs(id, endId, probability) }) async function waitUntilProofIsRequired(id) { @@ -270,4 +273,35 @@ describe("Proofs", function () { await expect(await proofs.isProofRequired(id)).to.be.false }) }) + + describe("set proof end", function () { + const proof = hexlify(randomBytes(42)) + + it("fails to get proof end when proof ending doesn't exist", async function () { + await expect(proofs.end(endId)).to.be.revertedWith( + "Proof ending doesn't exist" + ) + }) + + it("sets proof end when proof ending doesn't already exist", async function () { + let ending = (await currentTime()) + duration + await expect(proofs.setProofEnd(endId, ending)).not.to.be.reverted + await expect(await proofs.end(endId)).to.equal(ending) + }) + + it("sets proof end when proof ending exists and ending set to past", async function () { + // let ending = (await currentTime()) + duration + // await proofs.setProofEnd(endId, ending) + let past = (await currentTime()) - 1 + await expect(proofs.setProofEnd(endId, past)).not.to.be.reverted + }) + + it("fails when proof ending already exists and ending set to future", async function () { + let ending = (await currentTime()) + duration + await proofs.setProofEnd(endId, ending) + await expect(proofs.setProofEnd(endId, ending)).to.be.revertedWith( + "End exists or must be past" + ) + }) + }) })