From 650f5d1f1a7451a4131fc8460b2e200c504966ae Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Mon, 1 Nov 2021 16:17:42 +0100 Subject: [PATCH] Checking of new storage contracts is moved to Contracts.sol --- contracts/Contracts.sol | 128 ++++++++++++++++++++++++++++ contracts/StorageContracts.sol | 140 +++++++------------------------ contracts/TestContracts.sol | 56 +++++++++++++ test/Contracts.test.js | 148 +++++++++++++++++++++++++++++++++ test/StorageContracts.test.js | 109 +----------------------- 5 files changed, 366 insertions(+), 215 deletions(-) create mode 100644 contracts/Contracts.sol create mode 100644 contracts/TestContracts.sol create mode 100644 test/Contracts.test.js diff --git a/contracts/Contracts.sol b/contracts/Contracts.sol new file mode 100644 index 0000000..90dc995 --- /dev/null +++ b/contracts/Contracts.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract Contracts { + + mapping(bytes32=>bool) private ids; // contract id, equal to hash of bid + mapping(bytes32=>uint) private durations; // contract duration in seconds + mapping(bytes32=>uint) private sizes; // storage size in bytes + mapping(bytes32=>bytes32) private contentHashes; // hash of data to be stored + mapping(bytes32=>uint) private prices; // price in coins + mapping(bytes32=>address) private hosts; // host that provides storage + + function _duration(bytes32 id) internal view returns (uint) { + return durations[id]; + } + + function _size(bytes32 id) internal view returns (uint) { + return sizes[id]; + } + + function _contentHash(bytes32 id) internal view returns (bytes32) { + return contentHashes[id]; + } + + function _price(bytes32 id) internal view returns (uint) { + return prices[id]; + } + + function _host(bytes32 id) internal view returns (address) { + return hosts[id]; + } + + function _newContract( + uint duration, + uint size, + bytes32 contentHash, + uint price, + uint proofPeriod, + uint proofTimeout, + bytes32 nonce, + uint bidExpiry, + address host, + bytes memory requestSignature, + bytes memory bidSignature + ) + internal + returns (bytes32 id) + { + bytes32 requestHash = _hashRequest( + duration, + size, + contentHash, + proofPeriod, + proofTimeout, + nonce + ); + bytes32 bidHash = _hashBid(requestHash, bidExpiry, price); + _checkSignature(requestSignature, requestHash, msg.sender); + _checkSignature(bidSignature, bidHash, host); + _checkBidExpiry(bidExpiry); + _checkId(bidHash); + id = bidHash; + ids[id] = true; + durations[id] = duration; + sizes[id] = size; + contentHashes[id] = contentHash; + prices[id] = price; + hosts[id] = host; + } + + // Creates hash for a storage request that can be used to check its signature. + function _hashRequest( + uint duration, + uint size, + bytes32 hash, + uint proofPeriod, + uint proofTimeout, + bytes32 nonce + ) + private pure + returns (bytes32) + { + return keccak256(abi.encodePacked( + "[dagger.request.v1]", + duration, + size, + hash, + proofPeriod, + proofTimeout, + nonce + )); + } + + // Creates hash for a storage bid that can be used to check its signature. + function _hashBid(bytes32 requestHash, uint expiry, uint price) + private pure + returns (bytes32) + { + return keccak256(abi.encodePacked( + "[dagger.bid.v1]", + requestHash, + expiry, + price + )); + } + + // Checks a signature for a storage request or bid, given its hash. + function _checkSignature(bytes memory signature, bytes32 hash, address signer) + private pure + { + bytes32 messageHash = ECDSA.toEthSignedMessageHash(hash); + address recovered = ECDSA.recover(messageHash, signature); + require(recovered == signer, "Invalid signature"); + } + + function _checkBidExpiry(uint expiry) private view { + require(expiry > block.timestamp, "Bid expired"); + } + + function _checkId(bytes32 id) private view { + require( + !ids[id], + "A contract with this id already exists" + ); + } +} diff --git a/contracts/StorageContracts.sol b/contracts/StorageContracts.sol index a8af7d8..d9da000 100644 --- a/contracts/StorageContracts.sol +++ b/contracts/StorageContracts.sol @@ -1,54 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./Contracts.sol"; import "./Proofs.sol"; -contract StorageContracts is Proofs { - - 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 numberOfContracts; - mapping(bytes32 => Contract) contracts; - - function duration(bytes32 contractId) public view returns (uint) { - return contracts[contractId].duration; - } - - function size(bytes32 contractId) public view returns (uint) { - return contracts[contractId].size; - } - - function contentHash(bytes32 contractId) public view returns (bytes32) { - return contracts[contractId].contentHash; - } - - function price(bytes32 contractId) public view returns (uint) { - return contracts[contractId].price; - } - - function host(bytes32 contractId) public view returns (address) { - return contracts[contractId].host; - } - - function proofPeriod(bytes32 contractId) public view returns (uint) { - return _period(contractId); - } - - function proofTimeout(bytes32 contractId) public view returns (uint) { - return _timeout(contractId); - } - - function missingProofs(bytes32 contractId) public view returns (uint) { - return _missed(contractId); - } +contract StorageContracts is Contracts, Proofs { function newContract( uint _duration, @@ -65,84 +21,52 @@ contract StorageContracts is Proofs { ) public { - bytes32 requestHash = hashRequest( + bytes32 id = _newContract( _duration, _size, _contentHash, + _price, _proofPeriod, _proofTimeout, - _nonce + _nonce, + _bidExpiry, + _host, + requestSignature, + bidSignature ); - bytes32 bidHash = hashBid(requestHash, _bidExpiry, _price); - checkSignature(requestSignature, requestHash, msg.sender); - checkSignature(bidSignature, bidHash, _host); - checkBidExpiry(_bidExpiry); - bytes32 contractId = bidHash; - 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; - _expectProofs(contractId, _proofPeriod, _proofTimeout); + _expectProofs(id, _proofPeriod, _proofTimeout); } - // Creates hash for a storage request that can be used to check its signature. - function hashRequest( - uint _duration, - uint _size, - bytes32 _hash, - uint _proofPeriod, - uint _proofTimeout, - bytes32 _nonce - ) - internal pure - returns (bytes32) - { - return keccak256(abi.encodePacked( - "[dagger.request.v1]", - _duration, - _size, - _hash, - _proofPeriod, - _proofTimeout, - _nonce - )); + function duration(bytes32 contractId) public view returns (uint) { + return _duration(contractId); } - // Creates hash for a storage bid that can be used to check its signature. - function hashBid(bytes32 requestHash, uint _expiry, uint _price) - internal pure - returns (bytes32) - { - return keccak256(abi.encodePacked( - "[dagger.bid.v1]", - requestHash, - _expiry, - _price - )); + function size(bytes32 contractId) public view returns (uint) { + return _size(contractId); } - // Checks a signature for a storage request or bid, given its hash. - function checkSignature(bytes memory signature, bytes32 hash, address signer) - internal pure - { - bytes32 messageHash = ECDSA.toEthSignedMessageHash(hash); - address recovered = ECDSA.recover(messageHash, signature); - require(recovered == signer, "Invalid signature"); + function contentHash(bytes32 contractId) public view returns (bytes32) { + return _contentHash(contractId); } - function checkBidExpiry(uint expiry) internal view { - require(expiry > block.timestamp, "Bid expired"); + function price(bytes32 contractId) public view returns (uint) { + return _price(contractId); } - function checkId(bytes32 contractId) internal view { - require( - !contracts[contractId].initialized, - "A contract with this id already exists" - ); + function host(bytes32 contractId) public view returns (address) { + return _host(contractId); + } + + function proofPeriod(bytes32 contractId) public view returns (uint) { + return _period(contractId); + } + + function proofTimeout(bytes32 contractId) public view returns (uint) { + return _timeout(contractId); + } + + function missingProofs(bytes32 contractId) public view returns (uint) { + return _missed(contractId); } // Check whether a proof is required at the time of the block with the diff --git a/contracts/TestContracts.sol b/contracts/TestContracts.sol new file mode 100644 index 0000000..8f3b1c1 --- /dev/null +++ b/contracts/TestContracts.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Contracts.sol"; + +contract TestContracts is Contracts { + + function newContract( + uint _duration, + uint _size, + bytes32 _contentHash, + uint _price, + uint _proofPeriod, + uint _proofTimeout, + bytes32 _nonce, + uint _bidExpiry, + address _host, + bytes memory requestSignature, + bytes memory bidSignature + ) + public + { + _newContract( + _duration, + _size, + _contentHash, + _price, + _proofPeriod, + _proofTimeout, + _nonce, + _bidExpiry, + _host, + requestSignature, + bidSignature); + } + + function duration(bytes32 id) public view returns (uint) { + return _duration(id); + } + + function size(bytes32 id) public view returns (uint) { + return _size(id); + } + + function contentHash(bytes32 id) public view returns (bytes32) { + return _contentHash(id); + } + + function price(bytes32 id) public view returns (uint) { + return _price(id); + } + + function host(bytes32 id) public view returns (address) { + return _host(id); + } +} diff --git a/test/Contracts.test.js b/test/Contracts.test.js new file mode 100644 index 0000000..dd4c2bb --- /dev/null +++ b/test/Contracts.test.js @@ -0,0 +1,148 @@ +const { expect } = require("chai") +const { ethers } = require("hardhat") +const { hashRequest, hashBid, sign } = require("./marketplace") + +describe("Storage Contracts", function () { + + const duration = 31 * 24 * 60 * 60 // 31 days + const size = 1 * 1024 * 1024 * 1024 // 1 Gigabyte + const contentHash = ethers.utils.sha256("0xdeadbeef") + const proofPeriod = 8 // 8 blocks ≈ 2 minutes + const proofTimeout = 4 // 4 blocks ≈ 1 minute + const price = 42 + const nonce = ethers.utils.randomBytes(32) + + let client, host + let contracts + let bidExpiry + let requestHash, bidHash + let id + + beforeEach(async function () { + [client, host] = await ethers.getSigners() + let Contracts = await ethers.getContractFactory("TestContracts") + contracts = await Contracts.deploy() + requestHash = hashRequest( + duration, + size, + contentHash, + proofPeriod, + proofTimeout, + nonce + ) + bidExpiry = Math.round(Date.now() / 1000) + 60 * 60 // 1 hour from now + bidHash = hashBid(requestHash, bidExpiry, price) + id = bidHash + }) + + it("creates a new storage contract", async function () { + await contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + bidExpiry, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash) + ) + expect(await contracts.duration(id)).to.equal(duration) + expect(await contracts.size(id)).to.equal(size) + expect(await contracts.contentHash(id)).to.equal(contentHash) + expect(await contracts.price(id)).to.equal(price) + expect(await contracts.host(id)).to.equal(await host.getAddress()) + }) + + it("does not allow reuse of contract ids", async function () { + await contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + bidExpiry, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash) + ) + await expect(contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + 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, + size, + contentHash, + proofPeriod, + proofTimeout, + nonce + ) + let invalidSignature = await sign(client, invalidHash) + await expect(contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + bidExpiry, + await host.getAddress(), + invalidSignature, + await sign(host, bidHash) + )).to.be.revertedWith("Invalid signature") + }) + + 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(contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + bidExpiry, + await host.getAddress(), + await sign(client, requestHash), + invalidSignature + )).to.be.revertedWith("Invalid signature") + }) + + 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(contracts.newContract( + duration, + size, + contentHash, + price, + proofPeriod, + proofTimeout, + nonce, + expired, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash), + )).to.be.revertedWith("Bid expired") + }) +}) diff --git a/test/StorageContracts.test.js b/test/StorageContracts.test.js index 4763dd9..a2bd7d6 100644 --- a/test/StorageContracts.test.js +++ b/test/StorageContracts.test.js @@ -53,124 +53,19 @@ describe("Storage Contracts", function () { ) }) - it("has a duration", async function () { + it("created a contract", async function () { expect(await contracts.duration(id)).to.equal(duration) - }) - - it("contains the size of the data that is to be stored", async function () { expect(await contracts.size(id)).to.equal(size) - }) - - it("contains the hash of the data that is to be stored", async function () { expect(await contracts.contentHash(id)).to.equal(contentHash) - }) - - it("has a price", async function () { expect(await contracts.price(id)).to.equal(price) - }) - - it("knows the host that provides the storage", async function () { expect(await contracts.host(id)).to.equal(await host.getAddress()) }) - it("has an average time between proofs (in blocks)", async function (){ + it("requires storage proofs", async function (){ expect(await contracts.proofPeriod(id)).to.equal(proofPeriod) - }) - - it("has a proof timeout (in blocks)", async function (){ expect(await contracts.proofTimeout(id)).to.equal(proofTimeout) }) }) - - it("cannot be created when contract id already used", async function () { - await contracts.newContract( - duration, - size, - contentHash, - price, - proofPeriod, - proofTimeout, - nonce, - bidExpiry, - await host.getAddress(), - await sign(client, requestHash), - await sign(host, bidHash) - ) - await expect(contracts.newContract( - duration, - size, - contentHash, - price, - proofPeriod, - proofTimeout, - nonce, - 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, - size, - contentHash, - proofPeriod, - proofTimeout, - nonce - ) - let invalidSignature = await sign(client, invalidHash) - await expect(contracts.newContract( - duration, - size, - contentHash, - price, - proofPeriod, - proofTimeout, - nonce, - bidExpiry, - await host.getAddress(), - invalidSignature, - await sign(host, bidHash) - )).to.be.revertedWith("Invalid signature") - }) - - 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(contracts.newContract( - duration, - size, - contentHash, - price, - proofPeriod, - proofTimeout, - nonce, - bidExpiry, - await host.getAddress(), - await sign(client, requestHash), - invalidSignature - )).to.be.revertedWith("Invalid signature") - }) - - 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(contracts.newContract( - duration, - size, - contentHash, - price, - proofPeriod, - proofTimeout, - nonce, - expired, - await host.getAddress(), - await sign(client, requestHash), - await sign(host, bidHash), - )).to.be.revertedWith("Bid expired") - }) }) // TODO: implement checking of actual proofs of storage, instead of dummy bool