Multiple storage contracts in solidity contract

This commit is contained in:
Mark Spanbroek 2021-10-20 12:07:35 +02:00
parent d005bf7c3c
commit 08cedae4bf
2 changed files with 211 additions and 86 deletions

View File

@ -4,28 +4,70 @@ pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract StorageContracts { 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 struct Contract {
uint public missingProofs; 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 numberOfContracts;
uint _size, mapping(uint => Contract) contracts;
bytes32 _contentHash,
uint _price, function duration(uint contractId) public view returns (uint) {
uint _proofPeriod, return contracts[contractId].duration;
uint _proofTimeout, }
uint _bidExpiry,
address _host, function size(uint contractId) public view returns (uint) {
bytes memory requestSignature, return contracts[contractId].size;
bytes memory bidSignature) }
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( bytes32 requestHash = hashRequest(
_duration, _duration,
@ -39,14 +81,17 @@ contract StorageContracts {
checkSignature(bidSignature, bidHash, _host); checkSignature(bidSignature, bidHash, _host);
checkProofTimeout(_proofTimeout); checkProofTimeout(_proofTimeout);
checkBidExpiry(_bidExpiry); checkBidExpiry(_bidExpiry);
duration = _duration; checkId(contractId);
size = _size; Contract storage c = contracts[contractId];
price = _price; c.initialized = true;
contentHash = _contentHash; c.duration = _duration;
host = _host; c.size = _size;
proofPeriod = _proofPeriod; c.price = _price;
proofTimeout = _proofTimeout; c.contentHash = _contentHash;
proofMarker = uint(blockhash(block.number - 1)) % _proofPeriod; 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. // 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"); 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 // 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 // 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 // timeout for it to be valid. Whether a proof is required is determined
// randomly, but on average it is once every proof period. // 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); 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) { function isProofTimedOut(
return block.number >= blocknumber + proofTimeout; 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(proof, "Invalid proof"); // TODO: replace bool by actual proof
require(isProofRequired(blocknumber), "No proof required for this block"); require(
require(!isProofTimedOut(blocknumber), "Proof not allowed after timeout"); isProofRequired(contractId, blocknumber),
require(!proofReceived[blocknumber], "Proof already submitted"); "No proof required for this block"
proofReceived[blocknumber] = true; );
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 { function markProofAsMissing(uint contractId, uint blocknumber) public {
require(isProofTimedOut(blocknumber), "Proof has not timed out yet"); Contract storage c = contracts[contractId];
require(!proofReceived[blocknumber], "Proof was submitted, not missing"); require(
require(isProofRequired(blocknumber), "Proof was not required"); isProofTimedOut(contractId, blocknumber),
missingProofs += 1; "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;
} }
} }

View File

@ -11,15 +11,16 @@ describe("Storage Contracts", function () {
const proofTimeout = 4 // 4 blocks ≈ 1 minute const proofTimeout = 4 // 4 blocks ≈ 1 minute
const price = 42 const price = 42
var StorageContracts var contracts
var client, host var client, host
var bidExpiry var bidExpiry
var requestHash, bidHash var requestHash, bidHash
var contract var id
beforeEach(async function () { beforeEach(async function () {
[client, host] = await ethers.getSigners() [client, host] = await ethers.getSigners()
StorageContracts = await ethers.getContractFactory("StorageContracts") let StorageContracts = await ethers.getContractFactory("StorageContracts")
contracts = await StorageContracts.deploy()
requestHash = hashRequest( requestHash = hashRequest(
duration, duration,
size, size,
@ -29,12 +30,14 @@ describe("Storage Contracts", function () {
) )
bidExpiry = Math.round(Date.now() / 1000) + 60 * 60 // 1 hour from now bidExpiry = Math.round(Date.now() / 1000) + 60 * 60 // 1 hour from now
bidHash = hashBid(requestHash, bidExpiry, price) bidHash = hashBid(requestHash, bidExpiry, price)
id = Math.round(Math.random() * 99999999) // randomly chosen contract id
}) })
describe("when properly instantiated", function () { describe("when properly instantiated", function () {
beforeEach(async function () { beforeEach(async function () {
contract = await StorageContracts.deploy( await contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -49,34 +52,63 @@ describe("Storage Contracts", function () {
}) })
it("has a duration", async 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 () { 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 () { 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 () { 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 () { 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 (){ 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 (){ 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 () { it("cannot be created when client signature is invalid", async function () {
let invalidHash = hashRequest( let invalidHash = hashRequest(
duration + 1, duration + 1,
@ -86,7 +118,8 @@ describe("Storage Contracts", function () {
proofTimeout proofTimeout
) )
let invalidSignature = await sign(client, invalidHash) let invalidSignature = await sign(client, invalidHash)
await expect(StorageContract.deploy( await expect(contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -103,7 +136,8 @@ describe("Storage Contracts", function () {
it("cannot be created when host signature is invalid", async function () { it("cannot be created when host signature is invalid", async function () {
let invalidBid = hashBid(requestHash, bidExpiry, price - 1) let invalidBid = hashBid(requestHash, bidExpiry, price - 1)
let invalidSignature = await sign(host, invalidBid) let invalidSignature = await sign(host, invalidBid)
await expect(StorageContract.deploy( await expect(contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -127,7 +161,8 @@ describe("Storage Contracts", function () {
invalidTimeout invalidTimeout
) )
bidHash = hashBid(requestHash, bidExpiry, price) bidHash = hashBid(requestHash, bidExpiry, price)
await expect(StorageContract.deploy( await expect(contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -144,7 +179,8 @@ describe("Storage Contracts", function () {
it("cannot be created when bid has expired", async function () { it("cannot be created when bid has expired", async function () {
let expired = Math.round(Date.now() / 1000) - 60 // 1 minute ago let expired = Math.round(Date.now() / 1000) - 60 // 1 minute ago
let bidHash = hashBid(requestHash, expired, price) let bidHash = hashBid(requestHash, expired, price)
await expect(StorageContract.deploy( await expect(contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -168,8 +204,8 @@ describe("Storage Contracts", function () {
return await ethers.provider.getBlockNumber() - 1 return await ethers.provider.getBlockNumber() - 1
} }
async function mineUntilProofIsRequired() { async function mineUntilProofIsRequired(id) {
while (!await contract.isProofRequired(await minedBlockNumber())) { while (!await contracts.isProofRequired(id, await minedBlockNumber())) {
mineBlock() mineBlock()
} }
} }
@ -181,7 +217,8 @@ describe("Storage Contracts", function () {
} }
beforeEach(async function () { beforeEach(async function () {
contract = await StorageContract.deploy( await contracts.newContract(
id,
duration, duration,
size, size,
contentHash, contentHash,
@ -200,7 +237,7 @@ describe("Storage Contracts", function () {
let proofs = 0 let proofs = 0
for (i=0; i<blocks; i++) { for (i=0; i<blocks; i++) {
await mineBlock() await mineBlock()
if (await contract.isProofRequired(await minedBlockNumber())) { if (await contracts.isProofRequired(id, await minedBlockNumber())) {
proofs += 1 proofs += 1
} }
} }
@ -209,92 +246,92 @@ describe("Storage Contracts", function () {
}) })
it("requires no proof for blocks that are unavailable", async function () { it("requires no proof for blocks that are unavailable", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
for (i=0; i<256; i++) { // only last 256 blocks are available in solidity for (i=0; i<256; i++) { // only last 256 blocks are available in solidity
mineBlock() mineBlock()
} }
expect(await contract.isProofRequired(blocknumber)).to.be.false expect(await contracts.isProofRequired(id, blocknumber)).to.be.false
}) })
it("submits a correct proof", async function () { it("submits a correct proof", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await contract.submitProof(blocknumber, true) await contracts.submitProof(id, blocknumber, true)
}) })
it("fails proof submission when proof is incorrect", async function () { it("fails proof submission when proof is incorrect", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await expect( await expect(
contract.submitProof(blocknumber, false) contracts.submitProof(id, blocknumber, false)
).to.be.revertedWith("Invalid proof") ).to.be.revertedWith("Invalid proof")
}) })
it("fails proof submission when proof was not required", async function () { it("fails proof submission when proof was not required", async function () {
while (await contract.isProofRequired(await minedBlockNumber())) { while (await contracts.isProofRequired(id, await minedBlockNumber())) {
await mineBlock() await mineBlock()
} }
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await expect( await expect(
contract.submitProof(blocknumber, true) contracts.submitProof(id, blocknumber, true)
).to.be.revertedWith("No proof required") ).to.be.revertedWith("No proof required")
}) })
it("fails proof submission when proof is too late", async function () { it("fails proof submission when proof is too late", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout() await mineUntilProofTimeout()
await expect( await expect(
contract.submitProof(blocknumber, true) contracts.submitProof(id, blocknumber, true)
).to.be.revertedWith("Proof not allowed after timeout") ).to.be.revertedWith("Proof not allowed after timeout")
}) })
it("fails proof submission when already submitted", async function() { it("fails proof submission when already submitted", async function() {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await contract.submitProof(blocknumber, true) await contracts.submitProof(id, blocknumber, true)
await expect( await expect(
contract.submitProof(blocknumber, true) contracts.submitProof(id, blocknumber, true)
).to.be.revertedWith("Proof already submitted") ).to.be.revertedWith("Proof already submitted")
}) })
it("marks a proof as missing", async function () { it("marks a proof as missing", async function () {
expect(await contract.missingProofs()).to.equal(0) expect(await contracts.missingProofs(id)).to.equal(0)
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout() await mineUntilProofTimeout()
await contract.markProofAsMissing(blocknumber) await contracts.markProofAsMissing(id, blocknumber)
expect(await contract.missingProofs()).to.equal(1) expect(await contracts.missingProofs(id)).to.equal(1)
}) })
it("does not mark a proof as missing before timeout", async function () { it("does not mark a proof as missing before timeout", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await mineBlock() await mineBlock()
await expect( await expect(
contract.markProofAsMissing(blocknumber) contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof has not timed out yet") ).to.be.revertedWith("Proof has not timed out yet")
}) })
it("does not mark a submitted proof as missing", async function () { it("does not mark a submitted proof as missing", async function () {
await mineUntilProofIsRequired() await mineUntilProofIsRequired(id)
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await contract.submitProof(blocknumber, true) await contracts.submitProof(id, blocknumber, true)
await mineUntilProofTimeout() await mineUntilProofTimeout()
await expect( await expect(
contract.markProofAsMissing(blocknumber) contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was submitted, not missing") ).to.be.revertedWith("Proof was submitted, not missing")
}) })
it("does not mark proof as missing when not required", async function () { it("does not mark proof as missing when not required", async function () {
while (await contract.isProofRequired(await minedBlockNumber())) { while (await contracts.isProofRequired(id, await minedBlockNumber())) {
mineBlock() mineBlock()
} }
let blocknumber = await minedBlockNumber() let blocknumber = await minedBlockNumber()
await mineUntilProofTimeout() await mineUntilProofTimeout()
await expect( await expect(
contract.markProofAsMissing(blocknumber) contracts.markProofAsMissing(id, blocknumber)
).to.be.revertedWith("Proof was not required") ).to.be.revertedWith("Proof was not required")
}) })
}) })
@ -306,4 +343,3 @@ describe("Storage Contracts", function () {
// TODO: only allow proofs after start of contract // TODO: only allow proofs after start of contract
// TODO: payout // TODO: payout
// TODO: stake // TODO: stake
// TODO: multiple hosts in single contract