Checking of new storage contracts is moved to Contracts.sol

This commit is contained in:
Mark Spanbroek 2021-11-01 16:17:42 +01:00
parent aa0def1127
commit 650f5d1f1a
5 changed files with 366 additions and 215 deletions

128
contracts/Contracts.sol Normal file
View File

@ -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"
);
}
}

View File

@ -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

View File

@ -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);
}
}

148
test/Contracts.test.js Normal file
View File

@ -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")
})
})

View File

@ -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