diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol new file mode 100644 index 0000000..7ca7e94 --- /dev/null +++ b/contracts/Proofs.sol @@ -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; + } +} diff --git a/contracts/StorageContracts.sol b/contracts/StorageContracts.sol index 8643b4f..5d57d5e 100644 --- a/contracts/StorageContracts.sol +++ b/contracts/StorageContracts.sol @@ -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); } } diff --git a/contracts/TestProofs.sol b/contracts/TestProofs.sol new file mode 100644 index 0000000..07b33dd --- /dev/null +++ b/contracts/TestProofs.sol @@ -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); + } +} diff --git a/test/Proofs.test.js b/test/Proofs.test.js new file mode 100644 index 0000000..24e7c6b --- /dev/null +++ b/test/Proofs.test.js @@ -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