Replace Proof implementation with new design
More info: https://github.com/status-im/dagger-research/pull/66
This commit is contained in:
parent
22e8ea50e2
commit
78eaaa7812
|
@ -13,6 +13,7 @@ contract Proofs {
|
||||||
mapping(bytes32 => bool) private ids;
|
mapping(bytes32 => bool) private ids;
|
||||||
mapping(bytes32 => uint256) private starts;
|
mapping(bytes32 => uint256) private starts;
|
||||||
mapping(bytes32 => uint256) private ends;
|
mapping(bytes32 => uint256) private ends;
|
||||||
|
mapping(bytes32 => uint256) private probabilities;
|
||||||
mapping(bytes32 => uint256) private markers;
|
mapping(bytes32 => uint256) private markers;
|
||||||
mapping(bytes32 => uint256) private missed;
|
mapping(bytes32 => uint256) private missed;
|
||||||
mapping(bytes32 => mapping(uint256 => bool)) private received;
|
mapping(bytes32 => mapping(uint256 => bool)) private received;
|
||||||
|
@ -34,55 +35,95 @@ contract Proofs {
|
||||||
return missed[id];
|
return missed[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
function _expectProofs(bytes32 id, uint256 duration) internal {
|
function periodOf(uint256 timestamp) private view returns (uint256) {
|
||||||
|
return timestamp / period;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentPeriod() private view returns (uint256) {
|
||||||
|
return periodOf(block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _expectProofs(
|
||||||
|
bytes32 id,
|
||||||
|
uint256 probability,
|
||||||
|
uint256 duration
|
||||||
|
) internal {
|
||||||
require(!ids[id], "Proof id already in use");
|
require(!ids[id], "Proof id already in use");
|
||||||
ids[id] = true;
|
ids[id] = true;
|
||||||
starts[id] = block.number;
|
starts[id] = block.timestamp;
|
||||||
ends[id] = block.number + duration + 2 * timeout;
|
ends[id] = block.timestamp + duration;
|
||||||
|
probabilities[id] = probability;
|
||||||
markers[id] = uint256(blockhash(block.number - 1)) % period;
|
markers[id] = uint256(blockhash(block.number - 1)) % period;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether a proof is required at the time of the block with the
|
function _getChallenges(bytes32 id, uint256 proofperiod)
|
||||||
// specified block number. A proof has to be submitted within the proof
|
internal
|
||||||
// timeout for it to be valid. Whether a proof is required is determined
|
view
|
||||||
// randomly, but on average it is once every proof period.
|
returns (Challenge memory challenge1, Challenge memory challenge2)
|
||||||
function _isProofRequired(bytes32 id, uint256 blocknumber)
|
{
|
||||||
|
if (
|
||||||
|
proofperiod <= periodOf(starts[id]) || proofperiod >= periodOf(ends[id])
|
||||||
|
) {
|
||||||
|
bytes32 nullChallenge;
|
||||||
|
return (Challenge(false, nullChallenge), Challenge(false, nullChallenge));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 blocknumber = block.number % 256;
|
||||||
|
uint256 periodnumber = proofperiod % 256;
|
||||||
|
uint256 idoffset = uint256(id) % 256;
|
||||||
|
|
||||||
|
uint256 pointer1 = (blocknumber + periodnumber + idoffset) % 256;
|
||||||
|
uint256 pointer2 = (blocknumber + periodnumber + idoffset + 128) % 256;
|
||||||
|
|
||||||
|
bytes32 blockhash1 = blockhash(block.number - 1 - pointer1);
|
||||||
|
bytes32 blockhash2 = blockhash(block.number - 1 - pointer2);
|
||||||
|
|
||||||
|
assert(uint256(blockhash1) != 0);
|
||||||
|
assert(uint256(blockhash2) != 0);
|
||||||
|
|
||||||
|
challenge1.challenge = keccak256(abi.encode(blockhash1));
|
||||||
|
challenge2.challenge = keccak256(abi.encode(blockhash2));
|
||||||
|
|
||||||
|
challenge1.isProofRequired =
|
||||||
|
uint256(challenge1.challenge) % probabilities[id] == 0;
|
||||||
|
challenge2.isProofRequired =
|
||||||
|
uint256(challenge2.challenge) % probabilities[id] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isProofRequired(bytes32 id, uint256 proofPeriod)
|
||||||
internal
|
internal
|
||||||
view
|
view
|
||||||
returns (bool)
|
returns (bool)
|
||||||
{
|
{
|
||||||
if (blocknumber < starts[id] || blocknumber >= ends[id]) {
|
Challenge memory challenge1;
|
||||||
return false;
|
Challenge memory challenge2;
|
||||||
}
|
(challenge1, challenge2) = _getChallenges(id, proofPeriod);
|
||||||
bytes32 hash = blockhash(blocknumber - 1);
|
return challenge1.isProofRequired && challenge2.isProofRequired;
|
||||||
return hash != 0 && uint256(hash) % period == markers[id];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isProofTimedOut(uint256 blocknumber) internal view returns (bool) {
|
function _isProofRequired(bytes32 id) internal view returns (bool) {
|
||||||
return block.number >= blocknumber + timeout;
|
return _isProofRequired(id, currentPeriod());
|
||||||
}
|
}
|
||||||
|
|
||||||
function _submitProof(
|
function _submitProof(bytes32 id, bool proof) internal {
|
||||||
bytes32 id,
|
|
||||||
uint256 blocknumber,
|
|
||||||
bool proof
|
|
||||||
) internal {
|
|
||||||
require(proof, "Invalid proof"); // TODO: replace bool by actual proof
|
require(proof, "Invalid proof"); // TODO: replace bool by actual proof
|
||||||
require(
|
require(!received[id][currentPeriod()], "Proof already submitted");
|
||||||
_isProofRequired(id, blocknumber),
|
received[id][currentPeriod()] = true;
|
||||||
"No proof required for this block"
|
|
||||||
);
|
|
||||||
require(!_isProofTimedOut(blocknumber), "Proof not allowed after timeout");
|
|
||||||
require(!received[id][blocknumber], "Proof already submitted");
|
|
||||||
received[id][blocknumber] = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _markProofAsMissing(bytes32 id, uint256 blocknumber) internal {
|
function _markProofAsMissing(bytes32 id, uint256 missedPeriod) internal {
|
||||||
require(_isProofTimedOut(blocknumber), "Proof has not timed out yet");
|
uint256 periodEnd = (missedPeriod + 1) * period;
|
||||||
require(!received[id][blocknumber], "Proof was submitted, not missing");
|
require(periodEnd < block.timestamp, "Period has not ended yet");
|
||||||
require(_isProofRequired(id, blocknumber), "Proof was not required");
|
require(block.timestamp < periodEnd + timeout, "Validation timed out");
|
||||||
require(!missing[id][blocknumber], "Proof already marked as missing");
|
require(!received[id][missedPeriod], "Proof was submitted, not missing");
|
||||||
missing[id][blocknumber] = true;
|
require(_isProofRequired(id, missedPeriod), "Proof was not required");
|
||||||
|
require(!missing[id][missedPeriod], "Proof already marked as missing");
|
||||||
|
missing[id][missedPeriod] = true;
|
||||||
missed[id] += 1;
|
missed[id] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Challenge {
|
||||||
|
bool isProofRequired;
|
||||||
|
bytes32 challenge;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,11 +29,11 @@ contract Storage is Collateral, Marketplace, Proofs {
|
||||||
Offer storage offer = _offer(id);
|
Offer storage offer = _offer(id);
|
||||||
require(msg.sender == offer.host, "Only host can call this function");
|
require(msg.sender == offer.host, "Only host can call this function");
|
||||||
Request storage request = _request(offer.requestId);
|
Request storage request = _request(offer.requestId);
|
||||||
_expectProofs(id, request.duration);
|
_expectProofs(id, request.proofProbability, request.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishContract(bytes32 id) public {
|
function finishContract(bytes32 id) public {
|
||||||
require(block.number > proofEnd(id), "Contract has not ended yet");
|
require(block.timestamp > proofEnd(id), "Contract has not ended yet");
|
||||||
require(!finished[id], "Contract already finished");
|
require(!finished[id], "Contract already finished");
|
||||||
finished[id] = true;
|
finished[id] = true;
|
||||||
Offer storage offer = _offer(id);
|
Offer storage offer = _offer(id);
|
||||||
|
@ -56,28 +56,16 @@ contract Storage is Collateral, Marketplace, Proofs {
|
||||||
return _missed(contractId);
|
return _missed(contractId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProofRequired(bytes32 contractId, uint256 blocknumber)
|
function isProofRequired(bytes32 contractId) public view returns (bool) {
|
||||||
public
|
return _isProofRequired(contractId);
|
||||||
view
|
|
||||||
returns (bool)
|
|
||||||
{
|
|
||||||
return _isProofRequired(contractId, blocknumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProofTimedOut(uint256 blocknumber) public view returns (bool) {
|
function submitProof(bytes32 contractId, bool proof) public {
|
||||||
return _isProofTimedOut(blocknumber);
|
_submitProof(contractId, proof);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitProof(
|
function markProofAsMissing(bytes32 contractId, uint256 period) public {
|
||||||
bytes32 contractId,
|
_markProofAsMissing(contractId, period);
|
||||||
uint256 blocknumber,
|
|
||||||
bool proof
|
|
||||||
) public {
|
|
||||||
_submitProof(contractId, blocknumber, proof);
|
|
||||||
}
|
|
||||||
|
|
||||||
function markProofAsMissing(bytes32 contractId, uint256 blocknumber) public {
|
|
||||||
_markProofAsMissing(contractId, blocknumber);
|
|
||||||
if (_missed(contractId) % slashMisses == 0) {
|
if (_missed(contractId) % slashMisses == 0) {
|
||||||
Offer storage offer = _offer(contractId);
|
Offer storage offer = _offer(contractId);
|
||||||
_slash(offer.host, slashPercentage);
|
_slash(offer.host, slashPercentage);
|
||||||
|
|
|
@ -28,27 +28,23 @@ contract TestProofs is Proofs {
|
||||||
return _missed(id);
|
return _missed(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectProofs(bytes32 id, uint256 _duration) public {
|
function expectProofs(
|
||||||
_expectProofs(id, _duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProofRequired(bytes32 id, uint256 blocknumber)
|
|
||||||
public
|
|
||||||
view
|
|
||||||
returns (bool)
|
|
||||||
{
|
|
||||||
return _isProofRequired(id, blocknumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitProof(
|
|
||||||
bytes32 id,
|
bytes32 id,
|
||||||
uint256 blocknumber,
|
uint256 _probability,
|
||||||
bool proof
|
uint256 _duration
|
||||||
) public {
|
) public {
|
||||||
_submitProof(id, blocknumber, proof);
|
_expectProofs(id, _probability, _duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markProofAsMissing(bytes32 id, uint256 blocknumber) public {
|
function isProofRequired(bytes32 id) public view returns (bool) {
|
||||||
_markProofAsMissing(id, blocknumber);
|
return _isProofRequired(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitProof(bytes32 id, bool proof) public {
|
||||||
|
_submitProof(id, proof);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markProofAsMissing(bytes32 id, uint256 _period) public {
|
||||||
|
_markProofAsMissing(id, _period);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,190 +1,202 @@
|
||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { ethers } = require("hardhat")
|
const { ethers } = require("hardhat")
|
||||||
const { mineBlock, minedBlockNumber } = require("./evm")
|
const { mineBlock, minedBlockNumber } = require("./evm")
|
||||||
|
const {
|
||||||
|
snapshot,
|
||||||
|
revert,
|
||||||
|
currentTime,
|
||||||
|
advanceTime,
|
||||||
|
advanceTimeTo,
|
||||||
|
} = require("./evm")
|
||||||
|
|
||||||
describe("Proofs", function () {
|
describe("Proofs", function () {
|
||||||
const id = ethers.utils.randomBytes(32)
|
const id = ethers.utils.randomBytes(32)
|
||||||
const period = 10
|
const period = 10
|
||||||
const timeout = 5
|
const timeout = 5
|
||||||
const duration = 50
|
const duration = 1000
|
||||||
|
const probability = 2 // require a proof roughly once every 2² periods
|
||||||
|
|
||||||
let proofs
|
let proofs
|
||||||
|
|
||||||
|
async function ensureEnoughBlockHistory() {
|
||||||
|
while ((await minedBlockNumber()) < 256) {
|
||||||
|
await mineBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
|
await snapshot()
|
||||||
|
await ensureEnoughBlockHistory()
|
||||||
const Proofs = await ethers.getContractFactory("TestProofs")
|
const Proofs = await ethers.getContractFactory("TestProofs")
|
||||||
proofs = await Proofs.deploy(period, timeout)
|
proofs = await Proofs.deploy(period, timeout)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calculates an end time based on duration and timeout", async function () {
|
afterEach(async function () {
|
||||||
await proofs.expectProofs(id, duration)
|
await revert()
|
||||||
let start = await minedBlockNumber()
|
})
|
||||||
let end = start + duration + 2 * timeout
|
|
||||||
expect(await proofs.end(id)).to.equal(end)
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not allow ids to be reused", async function () {
|
it("does not allow ids to be reused", async function () {
|
||||||
await proofs.expectProofs(id, duration)
|
await proofs.expectProofs(id, probability, duration)
|
||||||
await expect(proofs.expectProofs(id, duration)).to.be.revertedWith(
|
await expect(
|
||||||
"Proof id already in use"
|
proofs.expectProofs(id, probability, duration)
|
||||||
)
|
).to.be.revertedWith("Proof id already in use")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("requires on average a proof every period", async function () {
|
it("requires proofs with an agreed upon probability", async function () {
|
||||||
let blocks = 600
|
const duration = 100_000
|
||||||
|
await proofs.expectProofs(id, probability, duration)
|
||||||
let amount = 0
|
let amount = 0
|
||||||
await proofs.expectProofs(id, blocks)
|
for (let i = 0; i < 100; i++) {
|
||||||
for (let i = 0; i < blocks; i++) {
|
if (await proofs.isProofRequired(id)) {
|
||||||
await mineBlock()
|
|
||||||
if (await proofs.isProofRequired(id, await minedBlockNumber())) {
|
|
||||||
amount += 1
|
amount += 1
|
||||||
}
|
}
|
||||||
|
await advanceTime(period)
|
||||||
}
|
}
|
||||||
let average = blocks / amount
|
let expected = 100 / probability ** 2
|
||||||
expect(average).to.be.closeTo(period, period / 2)
|
expect(amount).to.be.closeTo(expected, expected / 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("requires no proof before start time", async function () {
|
it("requires no proofs in the start period", async function () {
|
||||||
for (let i = 0; i < 4 * period; i++) {
|
const startPeriod = Math.floor((await currentTime()) / period)
|
||||||
mineBlock()
|
const probability = 1
|
||||||
|
await proofs.expectProofs(id, probability, duration)
|
||||||
|
while (Math.floor((await currentTime()) / period) == startPeriod) {
|
||||||
|
expect(await proofs.isProofRequired(id)).to.be.false
|
||||||
|
await advanceTime(1)
|
||||||
}
|
}
|
||||||
await proofs.expectProofs(id, duration)
|
})
|
||||||
let start = await minedBlockNumber()
|
|
||||||
for (let i = 1; i < 4 * period; i++) {
|
it("requires no proofs in the end period", async function () {
|
||||||
expect(await proofs.isProofRequired(id, start - i)).to.be.false
|
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, 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 = ethers.utils.randomBytes(32)
|
||||||
|
let id2 = ethers.utils.randomBytes(32)
|
||||||
|
let id3 = ethers.utils.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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("when proofs are required", async function () {
|
describe("when proofs are required", async function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await proofs.expectProofs(id, duration)
|
await proofs.expectProofs(id, probability, duration)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function mineUntilProofIsRequired(id) {
|
async function waitUntilProofIsRequired(id) {
|
||||||
while (!(await proofs.isProofRequired(id, await minedBlockNumber()))) {
|
while (!(await proofs.isProofRequired(id))) {
|
||||||
mineBlock()
|
await advanceTime(period)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mineUntilProofTimeout() {
|
function periodOf(timestamp) {
|
||||||
for (let i = 0; i < timeout; i++) {
|
return Math.floor(timestamp / period)
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mineUntilEnd() {
|
function periodStart(p) {
|
||||||
const end = await proofs.end(id)
|
return period * p
|
||||||
while ((await minedBlockNumber()) < end) {
|
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("requires no proof for blocks that are unavailable", async function () {
|
function periodEnd(p) {
|
||||||
await mineUntilProofIsRequired(id)
|
return periodStart(p + 1)
|
||||||
let blocknumber = await minedBlockNumber()
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
// only last 256 blocks available in solidity
|
|
||||||
mineBlock()
|
|
||||||
}
|
}
|
||||||
expect(await proofs.isProofRequired(id, blocknumber)).to.be.false
|
|
||||||
})
|
|
||||||
|
|
||||||
it("requires no proof after end time", async function () {
|
|
||||||
await mineUntilEnd()
|
|
||||||
for (let i = 0; i < 4 * period; i++) {
|
|
||||||
const blocknumber = await minedBlockNumber()
|
|
||||||
expect(await proofs.isProofRequired(id, blocknumber)).to.be.false
|
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it("submits a correct proof", async function () {
|
it("submits a correct proof", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await proofs.submitProof(id, true)
|
||||||
let blocknumber = await minedBlockNumber()
|
|
||||||
await proofs.submitProof(id, blocknumber, true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails proof submission when proof is incorrect", async function () {
|
it("fails proof submission when proof is incorrect", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await expect(proofs.submitProof(id, false)).to.be.revertedWith(
|
||||||
let blocknumber = await minedBlockNumber()
|
"Invalid proof"
|
||||||
await expect(
|
)
|
||||||
proofs.submitProof(id, blocknumber, false)
|
|
||||||
).to.be.revertedWith("Invalid proof")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails proof submission when proof was not required", async function () {
|
|
||||||
while (await proofs.isProofRequired(id, await minedBlockNumber())) {
|
|
||||||
await mineBlock()
|
|
||||||
}
|
|
||||||
let blocknumber = await minedBlockNumber()
|
|
||||||
await expect(
|
|
||||||
proofs.submitProof(id, blocknumber, true)
|
|
||||||
).to.be.revertedWith("No proof required")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("fails proof submission when proof is too late", async function () {
|
|
||||||
await mineUntilProofIsRequired(id)
|
|
||||||
let blocknumber = await minedBlockNumber()
|
|
||||||
await mineUntilProofTimeout()
|
|
||||||
await expect(
|
|
||||||
proofs.submitProof(id, blocknumber, true)
|
|
||||||
).to.be.revertedWith("Proof not allowed after timeout")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails proof submission when already submitted", async function () {
|
it("fails proof submission when already submitted", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
|
||||||
let blocknumber = await minedBlockNumber()
|
await proofs.submitProof(id, true)
|
||||||
await proofs.submitProof(id, blocknumber, true)
|
await expect(proofs.submitProof(id, true)).to.be.revertedWith(
|
||||||
await expect(
|
"Proof already submitted"
|
||||||
proofs.submitProof(id, blocknumber, true)
|
)
|
||||||
).to.be.revertedWith("Proof already submitted")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("marks a proof as missing", async function () {
|
it("marks a proof as missing", async function () {
|
||||||
expect(await proofs.missed(id)).to.equal(0)
|
expect(await proofs.missed(id)).to.equal(0)
|
||||||
await mineUntilProofIsRequired(id)
|
await waitUntilProofIsRequired(id)
|
||||||
let blocknumber = await minedBlockNumber()
|
let missedPeriod = periodOf(await currentTime())
|
||||||
await mineUntilProofTimeout()
|
await advanceTimeTo(periodEnd(missedPeriod))
|
||||||
await proofs.markProofAsMissing(id, blocknumber)
|
await proofs.markProofAsMissing(id, missedPeriod)
|
||||||
expect(await proofs.missed(id)).to.equal(1)
|
expect(await proofs.missed(id)).to.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not mark a proof as missing before timeout", async function () {
|
it("does not mark a proof as missing before period end", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await waitUntilProofIsRequired(id)
|
||||||
let blocknumber = await minedBlockNumber()
|
let currentPeriod = periodOf(await currentTime())
|
||||||
await mineBlock()
|
|
||||||
await expect(
|
await expect(
|
||||||
proofs.markProofAsMissing(id, blocknumber)
|
proofs.markProofAsMissing(id, currentPeriod)
|
||||||
).to.be.revertedWith("Proof has not timed out yet")
|
).to.be.revertedWith("Period has not ended yet")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not mark a proof as missing after timeout", async function () {
|
||||||
|
await waitUntilProofIsRequired(id)
|
||||||
|
let currentPeriod = periodOf(await currentTime())
|
||||||
|
await advanceTimeTo(periodEnd(currentPeriod) + timeout)
|
||||||
|
await expect(
|
||||||
|
proofs.markProofAsMissing(id, currentPeriod)
|
||||||
|
).to.be.revertedWith("Validation timed out")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not mark a submitted proof as missing", async function () {
|
it("does not mark a submitted proof as missing", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await waitUntilProofIsRequired(id)
|
||||||
let blocknumber = await minedBlockNumber()
|
let submittedPeriod = periodOf(await currentTime())
|
||||||
await proofs.submitProof(id, blocknumber, true)
|
await proofs.submitProof(id, true)
|
||||||
await mineUntilProofTimeout()
|
await advanceTimeTo(periodEnd(submittedPeriod))
|
||||||
await expect(
|
await expect(
|
||||||
proofs.markProofAsMissing(id, blocknumber)
|
proofs.markProofAsMissing(id, submittedPeriod)
|
||||||
).to.be.revertedWith("Proof was submitted, not missing")
|
).to.be.revertedWith("Proof was submitted, not missing")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not mark proof as missing when not required", async function () {
|
it("does not mark proof as missing when not required", async function () {
|
||||||
while (await proofs.isProofRequired(id, await minedBlockNumber())) {
|
while (await proofs.isProofRequired(id)) {
|
||||||
mineBlock()
|
await advanceTime(period)
|
||||||
}
|
}
|
||||||
let blocknumber = await minedBlockNumber()
|
let currentPeriod = periodOf(await currentTime())
|
||||||
await mineUntilProofTimeout()
|
await advanceTimeTo(periodEnd(currentPeriod))
|
||||||
await expect(
|
await expect(
|
||||||
proofs.markProofAsMissing(id, blocknumber)
|
proofs.markProofAsMissing(id, currentPeriod)
|
||||||
).to.be.revertedWith("Proof was not required")
|
).to.be.revertedWith("Proof was not required")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("does not mark proof as missing twice", async function () {
|
it("does not mark proof as missing twice", async function () {
|
||||||
await mineUntilProofIsRequired(id)
|
await waitUntilProofIsRequired(id)
|
||||||
let blocknumber = await minedBlockNumber()
|
let missedPeriod = periodOf(await currentTime())
|
||||||
await mineUntilProofTimeout()
|
await advanceTimeTo(periodEnd(missedPeriod))
|
||||||
await proofs.markProofAsMissing(id, blocknumber)
|
await proofs.markProofAsMissing(id, missedPeriod)
|
||||||
await expect(
|
await expect(
|
||||||
proofs.markProofAsMissing(id, blocknumber)
|
proofs.markProofAsMissing(id, missedPeriod)
|
||||||
).to.be.revertedWith("Proof already marked as missing")
|
).to.be.revertedWith("Proof already marked as missing")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { ethers, deployments } = require("hardhat")
|
const { ethers, deployments } = require("hardhat")
|
||||||
const { exampleRequest, exampleOffer } = require("./examples")
|
const { exampleRequest, exampleOffer } = require("./examples")
|
||||||
const { mineBlock, minedBlockNumber } = require("./evm")
|
const {
|
||||||
|
mineBlock,
|
||||||
|
minedBlockNumber,
|
||||||
|
advanceTime,
|
||||||
|
advanceTimeTo,
|
||||||
|
currentTime,
|
||||||
|
} = require("./evm")
|
||||||
const { requestId, offerId } = require("./ids")
|
const { requestId, offerId } = require("./ids")
|
||||||
|
|
||||||
describe("Storage", function () {
|
describe("Storage", function () {
|
||||||
|
@ -17,6 +23,12 @@ describe("Storage", function () {
|
||||||
storage = storage.connect(account)
|
storage = storage.connect(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureEnoughBlockHistory() {
|
||||||
|
while ((await minedBlockNumber()) < 256) {
|
||||||
|
await mineBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
;[client, host] = await ethers.getSigners()
|
;[client, host] = await ethers.getSigners()
|
||||||
|
|
||||||
|
@ -48,6 +60,8 @@ describe("Storage", function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await storage.selectOffer(offerId(offer))
|
await storage.selectOffer(offerId(offer))
|
||||||
id = offerId(offer)
|
id = offerId(offer)
|
||||||
|
|
||||||
|
await ensureEnoughBlockHistory()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("starting the contract", function () {
|
describe("starting the contract", function () {
|
||||||
|
@ -77,11 +91,9 @@ describe("Storage", function () {
|
||||||
await storage.startContract(id)
|
await storage.startContract(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
async function mineUntilEnd() {
|
async function waitUntilEnd() {
|
||||||
const end = await storage.proofEnd(id)
|
const end = (await storage.proofEnd(id)).toNumber()
|
||||||
while ((await minedBlockNumber()) < end) {
|
await advanceTimeTo(end)
|
||||||
await mineBlock()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// it("unlocks the host collateral", async function () {
|
// it("unlocks the host collateral", async function () {
|
||||||
|
@ -91,7 +103,7 @@ describe("Storage", function () {
|
||||||
// })
|
// })
|
||||||
|
|
||||||
it("pays the host", async function () {
|
it("pays the host", async function () {
|
||||||
await mineUntilEnd()
|
await waitUntilEnd()
|
||||||
const startBalance = await token.balanceOf(host.address)
|
const startBalance = await token.balanceOf(host.address)
|
||||||
await storage.finishContract(id)
|
await storage.finishContract(id)
|
||||||
const endBalance = await token.balanceOf(host.address)
|
const endBalance = await token.balanceOf(host.address)
|
||||||
|
@ -105,7 +117,7 @@ describe("Storage", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can only be done once", async function () {
|
it("can only be done once", async function () {
|
||||||
await mineUntilEnd()
|
await waitUntilEnd()
|
||||||
await storage.finishContract(id)
|
await storage.finishContract(id)
|
||||||
await expect(storage.finishContract(id)).to.be.revertedWith(
|
await expect(storage.finishContract(id)).to.be.revertedWith(
|
||||||
"Contract already finished"
|
"Contract already finished"
|
||||||
|
@ -114,20 +126,34 @@ describe("Storage", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("slashing when missing proofs", function () {
|
describe("slashing when missing proofs", function () {
|
||||||
beforeEach(function () {
|
let period
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
|
period = (await storage.proofPeriod()).toNumber()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function periodOf(timestamp) {
|
||||||
|
return Math.floor(timestamp / period)
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodStart(p) {
|
||||||
|
return period * p
|
||||||
|
}
|
||||||
|
|
||||||
|
function periodEnd(p) {
|
||||||
|
return periodStart(p + 1)
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureProofIsMissing() {
|
async function ensureProofIsMissing() {
|
||||||
while (!(await storage.isProofRequired(id, await minedBlockNumber()))) {
|
let currentPeriod = periodOf(await currentTime())
|
||||||
mineBlock()
|
await advanceTimeTo(periodEnd(currentPeriod))
|
||||||
|
while (!(await storage.isProofRequired(id))) {
|
||||||
|
await advanceTime(period)
|
||||||
}
|
}
|
||||||
const blocknumber = await minedBlockNumber()
|
let missedPeriod = periodOf(await currentTime())
|
||||||
const timeout = await storage.proofTimeout()
|
await advanceTime(period)
|
||||||
for (let i = 0; i < timeout; i++) {
|
await storage.markProofAsMissing(id, missedPeriod)
|
||||||
mineBlock()
|
|
||||||
}
|
|
||||||
await storage.markProofAsMissing(id, blocknumber)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("reduces collateral when too many proofs are missing", async function () {
|
it("reduces collateral when too many proofs are missing", async function () {
|
||||||
|
|
|
@ -32,6 +32,13 @@ async function advanceTime(seconds) {
|
||||||
await mineBlock()
|
await mineBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function advanceTimeTo(timestamp) {
|
||||||
|
if ((await currentTime()) !== timestamp) {
|
||||||
|
ethers.provider.send("evm_setNextBlockTimestamp", [timestamp])
|
||||||
|
await mineBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
snapshot,
|
snapshot,
|
||||||
revert,
|
revert,
|
||||||
|
@ -39,4 +46,5 @@ module.exports = {
|
||||||
minedBlockNumber,
|
minedBlockNumber,
|
||||||
currentTime,
|
currentTime,
|
||||||
advanceTime,
|
advanceTime,
|
||||||
|
advanceTimeTo,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,10 @@ const { sha256, hexlify, randomBytes } = ethers.utils
|
||||||
|
|
||||||
const exampleRequest = () => ({
|
const exampleRequest = () => ({
|
||||||
client: hexlify(randomBytes(20)),
|
client: hexlify(randomBytes(20)),
|
||||||
duration: 150, // 150 blocks ≈ half an hour
|
duration: hours(10),
|
||||||
size: 1 * 1024 * 1024 * 1024, // 1 Gigabyte
|
size: 1 * 1024 * 1024 * 1024, // 1 Gigabyte
|
||||||
contentHash: sha256("0xdeadbeef"),
|
contentHash: sha256("0xdeadbeef"),
|
||||||
proofProbability: 5, // require a proof roughly once every 5^2 periods
|
proofProbability: 2, // require a proof roughly once every 2² periods
|
||||||
maxPrice: 84,
|
maxPrice: 84,
|
||||||
expiry: now() + hours(1),
|
expiry: now() + hours(1),
|
||||||
nonce: hexlify(randomBytes(32)),
|
nonce: hexlify(randomBytes(32)),
|
||||||
|
|
Loading…
Reference in New Issue