From 08cedae4bfb4db9000f3d9a0fc430157e8891a2f Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Wed, 20 Oct 2021 12:07:35 +0200 Subject: [PATCH] Multiple storage contracts in solidity contract --- contracts/StorageContracts.sol | 173 +++++++++++++++++++++++++-------- test/StorageContracts.test.js | 124 ++++++++++++++--------- 2 files changed, 211 insertions(+), 86 deletions(-) diff --git a/contracts/StorageContracts.sol b/contracts/StorageContracts.sol index d501ed9..5fc31bc 100644 --- a/contracts/StorageContracts.sol +++ b/contracts/StorageContracts.sol @@ -4,28 +4,70 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract StorageContracts { - uint public immutable duration; // contract duration in seconds - uint public immutable size; // storage size in bytes - bytes32 public immutable contentHash; // hash of data that is to be stored - uint public immutable price; // price in coins - address public immutable host; // host that provides storage - uint public immutable proofPeriod; // average time between proofs (in blocks) - uint public immutable proofTimeout; // proof has to be submitted before this - uint public immutable proofMarker; // indicates when a proof is required - mapping(uint => bool) proofReceived; // whether proof for a block was received - uint public missingProofs; + struct Contract { + bool initialized; // always true, except for empty contracts in mapping + uint duration; // contract duration in seconds + uint size; // storage size in bytes + 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 + uint missingProofs; + } - constructor(uint _duration, - uint _size, - bytes32 _contentHash, - uint _price, - uint _proofPeriod, - uint _proofTimeout, - uint _bidExpiry, - address _host, - bytes memory requestSignature, - bytes memory bidSignature) + uint numberOfContracts; + mapping(uint => Contract) contracts; + + function duration(uint contractId) public view returns (uint) { + return contracts[contractId].duration; + } + + function size(uint contractId) public view returns (uint) { + return contracts[contractId].size; + } + + function contentHash(uint contractId) public view returns (bytes32) { + return contracts[contractId].contentHash; + } + + function price(uint contractId) public view returns (uint) { + return contracts[contractId].price; + } + + function host(uint contractId) public view returns (address) { + return contracts[contractId].host; + } + + function proofPeriod(uint contractId) public view returns (uint) { + return contracts[contractId].proofPeriod; + } + + function proofTimeout(uint contractId) public view returns (uint) { + return contracts[contractId].proofTimeout; + } + + function missingProofs(uint contractId) public view returns (uint) { + return contracts[contractId].missingProofs; + } + + function newContract( + uint contractId, + uint _duration, + uint _size, + bytes32 _contentHash, + uint _price, + uint _proofPeriod, + uint _proofTimeout, + uint _bidExpiry, + address _host, + bytes memory requestSignature, + bytes memory bidSignature + ) + public { bytes32 requestHash = hashRequest( _duration, @@ -39,14 +81,17 @@ contract StorageContracts { checkSignature(bidSignature, bidHash, _host); checkProofTimeout(_proofTimeout); checkBidExpiry(_bidExpiry); - duration = _duration; - size = _size; - price = _price; - contentHash = _contentHash; - host = _host; - proofPeriod = _proofPeriod; - proofTimeout = _proofTimeout; - proofMarker = uint(blockhash(block.number - 1)) % _proofPeriod; + checkId(contractId); + Contract storage c = contracts[contractId]; + c.initialized = true; + c.duration = _duration; + c.size = _size; + c.price = _price; + c.contentHash = _contentHash; + c.host = _host; + c.proofPeriod = _proofPeriod; + c.proofTimeout = _proofTimeout; + c.proofMarker = uint(blockhash(block.number - 1)) % _proofPeriod; } // Creates hash for a storage request that can be used to check its signature. @@ -103,31 +148,75 @@ contract StorageContracts { require(expiry > block.timestamp, "Bid expired"); } + function checkId(uint contractId) internal view { + require( + !contracts[contractId].initialized, + "A contract with this id already exists" + ); + } + // Check whether a proof is required at the time of the block with the // specified block number. A proof has to be submitted within the proof // timeout for it to be valid. Whether a proof is required is determined // randomly, but on average it is once every proof period. - function isProofRequired(uint blocknumber) public view returns (bool) { + function isProofRequired( + uint contractId, + uint blocknumber + ) + public view + returns (bool) + { + Contract storage c = contracts[contractId]; bytes32 hash = blockhash(blocknumber); - return hash != 0 && uint(hash) % proofPeriod == proofMarker; + return hash != 0 && uint(hash) % c.proofPeriod == c.proofMarker; } - function isProofTimedOut(uint blocknumber) internal view returns (bool) { - return block.number >= blocknumber + proofTimeout; + function isProofTimedOut( + uint contractId, + uint blocknumber + ) + internal view + returns (bool) + { + Contract storage c = contracts[contractId]; + return block.number >= blocknumber + c.proofTimeout; } - function submitProof(uint blocknumber, bool proof) public { + function submitProof( + uint contractId, + uint blocknumber, + bool proof + ) + public + { + Contract storage c = contracts[contractId]; require(proof, "Invalid proof"); // TODO: replace bool by actual proof - require(isProofRequired(blocknumber), "No proof required for this block"); - require(!isProofTimedOut(blocknumber), "Proof not allowed after timeout"); - require(!proofReceived[blocknumber], "Proof already submitted"); - proofReceived[blocknumber] = true; + 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; } - function markProofAsMissing(uint blocknumber) public { - require(isProofTimedOut(blocknumber), "Proof has not timed out yet"); - require(!proofReceived[blocknumber], "Proof was submitted, not missing"); - require(isProofRequired(blocknumber), "Proof was not required"); - missingProofs += 1; + function markProofAsMissing(uint 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" + ); + c.missingProofs += 1; } } diff --git a/test/StorageContracts.test.js b/test/StorageContracts.test.js index 22a58de..4e0e92b 100644 --- a/test/StorageContracts.test.js +++ b/test/StorageContracts.test.js @@ -11,15 +11,16 @@ describe("Storage Contracts", function () { const proofTimeout = 4 // 4 blocks ≈ 1 minute const price = 42 - var StorageContracts + var contracts var client, host var bidExpiry var requestHash, bidHash - var contract + var id beforeEach(async function () { [client, host] = await ethers.getSigners() - StorageContracts = await ethers.getContractFactory("StorageContracts") + let StorageContracts = await ethers.getContractFactory("StorageContracts") + contracts = await StorageContracts.deploy() requestHash = hashRequest( duration, size, @@ -29,12 +30,14 @@ describe("Storage Contracts", function () { ) bidExpiry = Math.round(Date.now() / 1000) + 60 * 60 // 1 hour from now bidHash = hashBid(requestHash, bidExpiry, price) + id = Math.round(Math.random() * 99999999) // randomly chosen contract id }) describe("when properly instantiated", function () { beforeEach(async function () { - contract = await StorageContracts.deploy( + await contracts.newContract( + id, duration, size, contentHash, @@ -49,34 +52,63 @@ describe("Storage Contracts", function () { }) it("has a duration", async function () { - expect(await contract.duration()).to.equal(duration) + expect(await contracts.duration(id)).to.equal(duration) }) it("contains the size of the data that is to be stored", async function () { - expect(await contract.size()).to.equal(size) + expect(await contracts.size(id)).to.equal(size) }) it("contains the hash of the data that is to be stored", async function () { - expect(await contract.contentHash()).to.equal(contentHash) + expect(await contracts.contentHash(id)).to.equal(contentHash) }) it("has a price", async function () { - expect(await contract.price()).to.equal(price) + expect(await contracts.price(id)).to.equal(price) }) it("knows the host that provides the storage", async function () { - expect(await contract.host()).to.equal(await host.getAddress()) + expect(await contracts.host(id)).to.equal(await host.getAddress()) }) it("has an average time between proofs (in blocks)", async function (){ - expect(await contract.proofPeriod()).to.equal(proofPeriod) + expect(await contracts.proofPeriod(id)).to.equal(proofPeriod) }) it("has a proof timeout (in blocks)", async function (){ - expect(await contract.proofTimeout()).to.equal(proofTimeout) + expect(await contracts.proofTimeout(id)).to.equal(proofTimeout) }) }) + it("cannot be created when contract id already used", async function () { + await contracts.newContract( + id, + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + bidExpiry, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash) + ) + await expect(contracts.newContract( + id, + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + bidExpiry, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash) + )).to.be.revertedWith("A contract with this id already exists") + }) + it("cannot be created when client signature is invalid", async function () { let invalidHash = hashRequest( duration + 1, @@ -86,7 +118,8 @@ describe("Storage Contracts", function () { proofTimeout ) let invalidSignature = await sign(client, invalidHash) - await expect(StorageContract.deploy( + await expect(contracts.newContract( + id, duration, size, contentHash, @@ -103,7 +136,8 @@ describe("Storage Contracts", function () { it("cannot be created when host signature is invalid", async function () { let invalidBid = hashBid(requestHash, bidExpiry, price - 1) let invalidSignature = await sign(host, invalidBid) - await expect(StorageContract.deploy( + await expect(contracts.newContract( + id, duration, size, contentHash, @@ -127,7 +161,8 @@ describe("Storage Contracts", function () { invalidTimeout ) bidHash = hashBid(requestHash, bidExpiry, price) - await expect(StorageContract.deploy( + await expect(contracts.newContract( + id, duration, size, contentHash, @@ -144,7 +179,8 @@ describe("Storage Contracts", function () { it("cannot be created when bid has expired", async function () { let expired = Math.round(Date.now() / 1000) - 60 // 1 minute ago let bidHash = hashBid(requestHash, expired, price) - await expect(StorageContract.deploy( + await expect(contracts.newContract( + id, duration, size, contentHash, @@ -168,8 +204,8 @@ describe("Storage Contracts", function () { return await ethers.provider.getBlockNumber() - 1 } - async function mineUntilProofIsRequired() { - while (!await contract.isProofRequired(await minedBlockNumber())) { + async function mineUntilProofIsRequired(id) { + while (!await contracts.isProofRequired(id, await minedBlockNumber())) { mineBlock() } } @@ -181,7 +217,8 @@ describe("Storage Contracts", function () { } beforeEach(async function () { - contract = await StorageContract.deploy( + await contracts.newContract( + id, duration, size, contentHash, @@ -200,7 +237,7 @@ describe("Storage Contracts", function () { let proofs = 0 for (i=0; i