Checking of new storage contracts is moved to Contracts.sol
This commit is contained in:
parent
aa0def1127
commit
650f5d1f1a
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue