Extract logic around proofs into separate contract

This commit is contained in:
Mark Spanbroek 2021-11-01 12:30:35 +01:00
parent c013a37229
commit d1f5ce0786
5 changed files with 320 additions and 199 deletions

92
contracts/Proofs.sol Normal file
View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proofs {
mapping(bytes32=>bool) private ids;
mapping(bytes32=>uint) private periods;
mapping(bytes32=>uint) private timeouts;
mapping(bytes32=>uint) private markers;
mapping(bytes32=>uint) private missed;
mapping(bytes32=>mapping(uint=>bool)) private received;
mapping(bytes32=>mapping(uint=>bool)) private missing;
function _period(bytes32 id) internal view returns (uint) {
return periods[id];
}
function _timeout(bytes32 id) internal view returns (uint) {
return timeouts[id];
}
function _missed(bytes32 id) internal view returns (uint) {
return missed[id];
}
function _expectProofs(bytes32 id, uint period, uint timeout) internal {
require(!ids[id], "Proof id already in use");
ids[id] = true;
periods[id] = period;
timeouts[id] = timeout;
markers[id] = uint(blockhash(block.number - 1)) % period;
}
function _isProofRequired(
bytes32 id,
uint blocknumber
)
internal view
returns (bool)
{
bytes32 hash = blockhash(blocknumber);
return hash != 0 && uint(hash) % periods[id] == markers[id];
}
function _isProofTimedOut(
bytes32 id,
uint blocknumber
)
internal view
returns (bool)
{
return block.number >= blocknumber + timeouts[id];
}
function _submitProof(
bytes32 id,
uint blocknumber,
bool proof
)
internal
{
require(proof, "Invalid proof"); // TODO: replace bool by actual proof
require(
_isProofRequired(id, blocknumber),
"No proof required for this block"
);
require(
!_isProofTimedOut(id, blocknumber),
"Proof not allowed after timeout"
);
require(!received[id][blocknumber], "Proof already submitted");
received[id][blocknumber] = true;
}
function _markProofAsMissing(bytes32 id, uint blocknumber) internal {
require(
_isProofTimedOut(id, blocknumber),
"Proof has not timed out yet"
);
require(
!received[id][blocknumber],
"Proof was submitted, not missing"
);
require(
_isProofRequired(id, blocknumber),
"Proof was not required"
);
require(!missing[id][blocknumber], "Proof already marked as missing");
missing[id][blocknumber] = true;
missed[id] += 1;
}
}

View File

@ -2,8 +2,9 @@
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./Proofs.sol";
contract StorageContracts {
contract StorageContracts is Proofs {
struct Contract {
bool initialized; // always true, except for empty contracts in mapping
@ -12,12 +13,6 @@ contract StorageContracts {
bytes32 contentHash; // hash of data that is to be stored
uint price; // price in coins
address host; // host that provides storage
uint proofPeriod; // average time between proofs (in blocks)
uint proofTimeout; // proof has to be submitted before this
uint proofMarker; // indicates when a proof is required
mapping(uint => bool) proofReceived; // whether proof for block was received
mapping(uint => bool) proofMissing; // whether proof for block was missing
uint missingProofs;
}
uint numberOfContracts;
@ -44,15 +39,15 @@ contract StorageContracts {
}
function proofPeriod(bytes32 contractId) public view returns (uint) {
return contracts[contractId].proofPeriod;
return _period(contractId);
}
function proofTimeout(bytes32 contractId) public view returns (uint) {
return contracts[contractId].proofTimeout;
return _timeout(contractId);
}
function missingProofs(bytes32 contractId) public view returns (uint) {
return contracts[contractId].missingProofs;
return _missed(contractId);
}
function newContract(
@ -92,9 +87,7 @@ contract StorageContracts {
c.price = _price;
c.contentHash = _contentHash;
c.host = _host;
c.proofPeriod = _proofPeriod;
c.proofTimeout = _proofTimeout;
c.proofMarker = uint(blockhash(block.number - 1)) % _proofPeriod;
_expectProofs(contractId, _proofPeriod, _proofTimeout);
}
// Creates hash for a storage request that can be used to check its signature.
@ -171,20 +164,17 @@ contract StorageContracts {
public view
returns (bool)
{
Contract storage c = contracts[contractId];
bytes32 hash = blockhash(blocknumber);
return hash != 0 && uint(hash) % c.proofPeriod == c.proofMarker;
return _isProofRequired(contractId, blocknumber);
}
function isProofTimedOut(
bytes32 contractId,
uint blocknumber
)
internal view
public view
returns (bool)
{
Contract storage c = contracts[contractId];
return block.number >= blocknumber + c.proofTimeout;
return _isProofTimedOut(contractId, blocknumber);
}
function submitProof(
@ -194,36 +184,10 @@ contract StorageContracts {
)
public
{
Contract storage c = contracts[contractId];
require(proof, "Invalid proof"); // TODO: replace bool by actual proof
require(
isProofRequired(contractId, blocknumber),
"No proof required for this block"
);
require(
!isProofTimedOut(contractId, blocknumber),
"Proof not allowed after timeout"
);
require(!c.proofReceived[blocknumber], "Proof already submitted");
c.proofReceived[blocknumber] = true;
_submitProof(contractId, blocknumber, proof);
}
function markProofAsMissing(bytes32 contractId, uint blocknumber) public {
Contract storage c = contracts[contractId];
require(
isProofTimedOut(contractId, blocknumber),
"Proof has not timed out yet"
);
require(
!c.proofReceived[blocknumber],
"Proof was submitted, not missing"
);
require(
isProofRequired(contractId, blocknumber),
"Proof was not required"
);
require(!c.proofMissing[blocknumber], "Proof already marked as missing");
c.proofMissing[blocknumber] = true;
c.missingProofs += 1;
_markProofAsMissing(contractId, blocknumber);
}
}

48
contracts/TestProofs.sol Normal file
View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Proofs.sol";
// exposes internal functions of Proofs for testing
contract TestProofs is Proofs {
function period(bytes32 id) public view returns (uint) {
return _period(id);
}
function timeout(bytes32 id) public view returns (uint) {
return _timeout(id);
}
function missed(bytes32 id) public view returns (uint) {
return _missed(id);
}
function expectProofs(bytes32 id, uint _period, uint _timeout) public {
_expectProofs(id, _period, _timeout);
}
function isProofRequired(
bytes32 id,
uint blocknumber
)
public view
returns (bool)
{
return _isProofRequired(id, blocknumber);
}
function submitProof(
bytes32 id,
uint blocknumber,
bool proof
)
public
{
_submitProof(id, blocknumber, proof);
}
function markProofAsMissing(bytes32 id, uint blocknumber) public {
_markProofAsMissing(id, blocknumber);
}
}

169
test/Proofs.test.js Normal file
View File

@ -0,0 +1,169 @@
const { expect } = require("chai")
const { ethers } = require("hardhat")
describe("Proofs", function () {
const id = ethers.utils.randomBytes(32)
const period = 10
const timeout = 5
let proofs
beforeEach(async function () {
const Proofs = await ethers.getContractFactory("TestProofs")
proofs = await Proofs.deploy()
})
it("indicates that proofs are required", async function() {
await proofs.expectProofs(id, period, timeout)
expect(await proofs.period(id)).to.equal(period)
expect(await proofs.timeout(id)).to.equal(timeout)
})
it("does not allow ids to be reused", async function() {
await proofs.expectProofs(id, period, timeout)
await expect(
proofs.expectProofs(id, period, timeout)
).to.be.revertedWith("Proof id already in use")
})
describe("when proofs are required", async function () {
beforeEach(async function () {
await proofs.expectProofs(id, period, timeout)
})
async function mineBlock() {
await ethers.provider.send("evm_mine")
}
async function minedBlockNumber() {
return await ethers.provider.getBlockNumber() - 1
}
async function mineUntilProofIsRequired(id) {
while (!await proofs.isProofRequired(id, await minedBlockNumber())) {
mineBlock()
}
}
async function mineUntilProofTimeout() {
for (let i=0; i<timeout; i++) {
mineBlock()
}
}
it("requires on average a proof every period", async function () {
let blocks = 500
let amount = 0
for (i=0; i<blocks; i++) {
await mineBlock()
if (await proofs.isProofRequired(id, await minedBlockNumber())) {
amount += 1
}
}
let average = blocks / amount
expect(average).to.be.closeTo(period, period / 2)
})
it("requires no proof for blocks that are unavailable", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
for (i=0; i<256; i++) { // only last 256 blocks are available in solidity
mineBlock()
}
expect(await proofs.isProofRequired(id, blocknumber)).to.be.false
})
it("submits a correct proof", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await proofs.submitProof(id, blocknumber, true)
})
it("fails proof submission when proof is incorrect", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
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() {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await proofs.submitProof(id, blocknumber, true)
await expect(
proofs.submitProof(id, blocknumber, true)
).to.be.revertedWith("Proof already submitted")
})
it("marks a proof as missing", async function () {
expect(await proofs.missed(id)).to.equal(0)
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await proofs.markProofAsMissing(id, blocknumber)
expect(await proofs.missed(id)).to.equal(1)
})
it("does not mark a proof as missing before timeout", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineBlock()
await expect(
proofs.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof has not timed out yet")
})
it("does not mark a submitted proof as missing", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await proofs.submitProof(id, blocknumber, true)
await mineUntilProofTimeout()
await expect(
proofs.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was submitted, not missing")
})
it("does not mark proof as missing when not required", async function () {
while (await proofs.isProofRequired(id, await minedBlockNumber())) {
mineBlock()
}
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await expect(
proofs.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was not required")
})
it("does not mark proof as missing twice", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await proofs.markProofAsMissing(id, blocknumber)
await expect(
proofs.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof already marked as missing")
})
})
})

View File

@ -197,158 +197,6 @@ describe("Storage Contracts", function () {
await sign(host, bidHash),
)).to.be.revertedWith("Bid expired")
})
describe("proofs", function () {
async function mineBlock() {
await ethers.provider.send("evm_mine")
}
async function minedBlockNumber() {
return await ethers.provider.getBlockNumber() - 1
}
async function mineUntilProofIsRequired(id) {
while (!await contracts.isProofRequired(id, await minedBlockNumber())) {
mineBlock()
}
}
async function mineUntilProofTimeout() {
for (let i=0; i<proofTimeout; i++) {
mineBlock()
}
}
beforeEach(async function () {
await contracts.newContract(
duration,
size,
contentHash,
price,
proofPeriod,
proofTimeout,
nonce,
bidExpiry,
await host.getAddress(),
await sign(client, requestHash),
await sign(host, bidHash)
)
})
it("requires on average a proof every period", async function () {
let blocks = 400
let proofs = 0
for (i=0; i<blocks; i++) {
await mineBlock()
if (await contracts.isProofRequired(id, await minedBlockNumber())) {
proofs += 1
}
}
let average = blocks / proofs
expect(average).to.be.closeTo(proofPeriod, proofPeriod / 2)
})
it("requires no proof for blocks that are unavailable", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
for (i=0; i<256; i++) { // only last 256 blocks are available in solidity
mineBlock()
}
expect(await contracts.isProofRequired(id, blocknumber)).to.be.false
})
it("submits a correct proof", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await contracts.submitProof(id, blocknumber, true)
})
it("fails proof submission when proof is incorrect", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await expect(
contracts.submitProof(id, blocknumber, false)
).to.be.revertedWith("Invalid proof")
})
it("fails proof submission when proof was not required", async function () {
while (await contracts.isProofRequired(id, await minedBlockNumber())) {
await mineBlock()
}
let blocknumber = await minedBlockNumber()
await expect(
contracts.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(
contracts.submitProof(id, blocknumber, true)
).to.be.revertedWith("Proof not allowed after timeout")
})
it("fails proof submission when already submitted", async function() {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await contracts.submitProof(id, blocknumber, true)
await expect(
contracts.submitProof(id, blocknumber, true)
).to.be.revertedWith("Proof already submitted")
})
it("marks a proof as missing", async function () {
expect(await contracts.missingProofs(id)).to.equal(0)
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await contracts.markProofAsMissing(id, blocknumber)
expect(await contracts.missingProofs(id)).to.equal(1)
})
it("does not mark a proof as missing before timeout", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineBlock()
await expect(
contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof has not timed out yet")
})
it("does not mark a submitted proof as missing", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await contracts.submitProof(id, blocknumber, true)
await mineUntilProofTimeout()
await expect(
contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was submitted, not missing")
})
it("does not mark proof as missing when not required", async function () {
while (await contracts.isProofRequired(id, await minedBlockNumber())) {
mineBlock()
}
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await expect(
contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was not required")
})
it("does not mark proof as missing twice", async function () {
await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout()
await contracts.markProofAsMissing(id, blocknumber)
await expect(
contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof already marked as missing")
})
})
})
// TODO: implement checking of actual proofs of storage, instead of dummy bool