mirror of
https://github.com/status-im/dagger-contracts.git
synced 2025-01-26 22:39:12 +00:00
Extract logic around proofs into separate contract
This commit is contained in:
parent
c013a37229
commit
d1f5ce0786
92
contracts/Proofs.sol
Normal file
92
contracts/Proofs.sol
Normal 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;
|
||||
}
|
||||
}
|
@ -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
48
contracts/TestProofs.sol
Normal 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
169
test/Proofs.test.js
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user