diff --git a/contracts/StorageContract.sol b/contracts/StorageContract.sol index 787a6f9..486c8e7 100644 --- a/contracts/StorageContract.sol +++ b/contracts/StorageContract.sol @@ -10,6 +10,7 @@ contract StorageContract { 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 constructor(uint _duration, uint _size, @@ -24,15 +25,17 @@ contract StorageContract { bytes32 bidHash = hashBid(requestHash, _price); checkSignature(requestSignature, requestHash, msg.sender); checkSignature(bidSignature, bidHash, _host); + checkProofTimeout(_proofTimeout); duration = _duration; size = _size; price = _price; host = _host; proofPeriod = _proofPeriod; proofTimeout = _proofTimeout; + 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. function hashRequest(uint _duration, uint _size) internal pure returns (bytes32) @@ -44,7 +47,7 @@ contract StorageContract { )); } - // creates hash for a storage bid that can be used to check its signature + // Creates hash for a storage bid that can be used to check its signature. function hashBid(bytes32 requestHash, uint _price) internal pure returns (bytes32) @@ -56,7 +59,7 @@ contract StorageContract { )); } - // checks a signature for a storage request or bid, given its hash + // Checks a signature for a storage request or bid, given its hash. function checkSignature(bytes memory signature, bytes32 hash, address signer) internal pure { @@ -65,4 +68,19 @@ contract StorageContract { require(recovered == signer, "Invalid signature"); } + // Checks that proof timeout is <= 128. Only the latest 256 blocks can be + // checked in a smart contract, so that leaves a period of at least 128 blocks + // after timeout for a validator to signal the absence of a proof. + function checkProofTimeout(uint timeout) internal pure { + require(timeout <= 128, "Invalid proof timeout, needs to be <= 128"); + } + + // 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) { + bytes32 hash = blockhash(blocknumber); + return hash != 0 && uint(hash) % proofPeriod == proofMarker; + } } diff --git a/test/StorageContract.test.js b/test/StorageContract.test.js index 748d9a2..7fc5d59 100644 --- a/test/StorageContract.test.js +++ b/test/StorageContract.test.js @@ -6,8 +6,8 @@ describe("Storage Contract", function () { const duration = 31 * 24 * 60 * 60 // 31 days const size = 1 * 1024 * 1024 * 1024 // 1 Gigabyte - const proofPeriod = 64 // 64 blocks ≈ 15 minutes - const proofTimeout = 42 // 42 blocks ≈ 10 minutes + const proofPeriod = 8 // 8 blocks ≈ 2 minutes + const proofTimeout = 4 // 4 blocks ≈ 1 minute const price = 42 var StorageContract @@ -89,6 +89,73 @@ describe("Storage Contract", function () { invalidSignature )).to.be.revertedWith("Invalid signature") }) + + it("cannot be created when proof timeout is too large", async function () { + let invalidTimeout = 129 // max proof timeout is 128 blocks + await expect(StorageContract.deploy( + duration, + size, + price, + proofPeriod, + invalidTimeout, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash), + )).to.be.revertedWith("Invalid proof timeout") + }) + + describe("proofs", function () { + + async function mineBlock() { + await ethers.provider.send("evm_mine") + } + + async function minedBlockNumber() { + return await ethers.provider.getBlockNumber() - 1 + } + + async function mineUntilProofIsRequired() { + while (!await contract.isProofRequired(await minedBlockNumber())) { + mineBlock() + } + } + + beforeEach(async function () { + contract = await StorageContract.deploy( + duration, + size, + price, + proofPeriod, + proofTimeout, + await host.getAddress(), + await sign(client, requestHash), + await sign(host, bidHash) + ) + }) + + it("requires on average a proof every period", async function () { + let blocks = 400 + let proofs = 0 + for (i=0; i