[marketplace] Add erasure coding and PoR attributes to request
This commit is contained in:
parent
29b5775951
commit
b8ce6c3682
|
@ -31,11 +31,11 @@ contract Marketplace is Collateral {
|
||||||
|
|
||||||
_createLock(id, request.expiry);
|
_createLock(id, request.expiry);
|
||||||
|
|
||||||
funds.received += request.maxPrice;
|
funds.received += request.ask.maxPrice;
|
||||||
funds.balance += request.maxPrice;
|
funds.balance += request.ask.maxPrice;
|
||||||
transferFrom(msg.sender, request.maxPrice);
|
transferFrom(msg.sender, request.ask.maxPrice);
|
||||||
|
|
||||||
emit StorageRequested(id, request);
|
emit StorageRequested(id, request.ask);
|
||||||
}
|
}
|
||||||
|
|
||||||
function offerStorage(Offer calldata offer) public marketplaceInvariant {
|
function offerStorage(Offer calldata offer) public marketplaceInvariant {
|
||||||
|
@ -46,7 +46,7 @@ contract Marketplace is Collateral {
|
||||||
require(request.client != address(0), "Unknown request");
|
require(request.client != address(0), "Unknown request");
|
||||||
require(request.expiry > block.timestamp, "Request expired");
|
require(request.expiry > block.timestamp, "Request expired");
|
||||||
|
|
||||||
require(offer.price <= request.maxPrice, "Price too high");
|
require(offer.price <= request.ask.maxPrice, "Price too high");
|
||||||
|
|
||||||
bytes32 id = keccak256(abi.encode(offer));
|
bytes32 id = keccak256(abi.encode(offer));
|
||||||
require(offers[id].host == address(0), "Offer already exists");
|
require(offers[id].host == address(0), "Offer already exists");
|
||||||
|
@ -75,7 +75,7 @@ contract Marketplace is Collateral {
|
||||||
_lock(offer.host, id);
|
_lock(offer.host, id);
|
||||||
_unlock(offer.requestId);
|
_unlock(offer.requestId);
|
||||||
|
|
||||||
uint256 difference = request.maxPrice - offer.price;
|
uint256 difference = request.ask.maxPrice - offer.price;
|
||||||
funds.sent += difference;
|
funds.sent += difference;
|
||||||
funds.balance -= difference;
|
funds.balance -= difference;
|
||||||
token.transfer(request.client, difference);
|
token.transfer(request.client, difference);
|
||||||
|
@ -95,15 +95,39 @@ contract Marketplace is Collateral {
|
||||||
return requestState[requestId].selectedOffer;
|
return requestState[requestId].selectedOffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint256 private constant POR_SECTORS = 10; // amount of sectors in PoR scheme
|
||||||
|
|
||||||
struct Request {
|
struct Request {
|
||||||
address client;
|
address client;
|
||||||
uint256 duration;
|
Ask ask;
|
||||||
uint256 size;
|
Content content;
|
||||||
bytes32 contentHash;
|
uint256 expiry; // time at which this request expires
|
||||||
uint256 proofProbability;
|
bytes32 nonce; // random nonce to differentiate between similar requests
|
||||||
uint256 maxPrice;
|
}
|
||||||
uint256 expiry;
|
|
||||||
bytes32 nonce;
|
struct Ask {
|
||||||
|
uint256 size; // size of requested storage in number of bytes
|
||||||
|
uint256 duration; // how long content should be stored (in seconds)
|
||||||
|
uint256 proofProbability; // how often storage proofs are required
|
||||||
|
uint256 maxPrice; // maximum price client will pay (in number of tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Content {
|
||||||
|
string cid; // content id (if part of a larger set, the chunk cid)
|
||||||
|
Erasure erasure; // Erasure coding attributes
|
||||||
|
PoR por; // Proof of Retrievability parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Erasure {
|
||||||
|
uint64 totalChunks; // the total number of chunks in the larger data set
|
||||||
|
uint64 totalNodes; // the total number of nodes that store the data set
|
||||||
|
uint64 nodeId; // index of this node in the list of total nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PoR {
|
||||||
|
bytes1[POR_SECTORS * 48] u; // parameters u_1..u_s
|
||||||
|
bytes1[96] publicKey; // public key
|
||||||
|
bytes1[512] name; // random name
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RequestState {
|
struct RequestState {
|
||||||
|
@ -117,7 +141,7 @@ contract Marketplace is Collateral {
|
||||||
uint256 expiry;
|
uint256 expiry;
|
||||||
}
|
}
|
||||||
|
|
||||||
event StorageRequested(bytes32 requestId, Request request);
|
event StorageRequested(bytes32 requestId, Ask ask);
|
||||||
event StorageOffered(bytes32 offerId, Offer offer, bytes32 indexed requestId);
|
event StorageOffered(bytes32 offerId, Offer offer, bytes32 indexed requestId);
|
||||||
event OfferSelected(bytes32 offerId, bytes32 indexed requestId);
|
event OfferSelected(bytes32 offerId, bytes32 indexed requestId);
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ contract Storage is Collateral, Marketplace, Proofs {
|
||||||
require(_selectedOffer(offer.requestId) == id, "Offer was not selected");
|
require(_selectedOffer(offer.requestId) == id, "Offer was not selected");
|
||||||
contractState[id] = ContractState.started;
|
contractState[id] = ContractState.started;
|
||||||
Request storage request = _request(offer.requestId);
|
Request storage request = _request(offer.requestId);
|
||||||
_expectProofs(id, request.proofProbability, request.duration);
|
_expectProofs(id, request.ask.proofProbability, request.ask.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishContract(bytes32 id) public {
|
function finishContract(bytes32 id) public {
|
||||||
|
|
|
@ -2,7 +2,7 @@ const { ethers } = require("hardhat")
|
||||||
const { expect } = require("chai")
|
const { expect } = require("chai")
|
||||||
const { exampleRequest, exampleOffer } = require("./examples")
|
const { exampleRequest, exampleOffer } = require("./examples")
|
||||||
const { now, hours } = require("./time")
|
const { now, hours } = require("./time")
|
||||||
const { requestId, offerId, requestToArray, offerToArray } = require("./ids")
|
const { requestId, offerId, offerToArray, askToArray } = require("./ids")
|
||||||
|
|
||||||
describe("Marketplace", function () {
|
describe("Marketplace", function () {
|
||||||
const collateral = 100
|
const collateral = 100
|
||||||
|
@ -44,22 +44,22 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("emits event when storage is requested", async function () {
|
it("emits event when storage is requested", async function () {
|
||||||
await token.approve(marketplace.address, request.maxPrice)
|
await token.approve(marketplace.address, request.ask.maxPrice)
|
||||||
await expect(marketplace.requestStorage(request))
|
await expect(marketplace.requestStorage(request))
|
||||||
.to.emit(marketplace, "StorageRequested")
|
.to.emit(marketplace, "StorageRequested")
|
||||||
.withArgs(requestId(request), requestToArray(request))
|
.withArgs(requestId(request), askToArray(request.ask))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects request with invalid client address", async function () {
|
it("rejects request with invalid client address", async function () {
|
||||||
let invalid = { ...request, client: host.address }
|
let invalid = { ...request, client: host.address }
|
||||||
await token.approve(marketplace.address, invalid.maxPrice)
|
await token.approve(marketplace.address, invalid.ask.maxPrice)
|
||||||
await expect(marketplace.requestStorage(invalid)).to.be.revertedWith(
|
await expect(marketplace.requestStorage(invalid)).to.be.revertedWith(
|
||||||
"Invalid client address"
|
"Invalid client address"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects request with insufficient payment", async function () {
|
it("rejects request with insufficient payment", async function () {
|
||||||
let insufficient = request.maxPrice - 1
|
let insufficient = request.ask.maxPrice - 1
|
||||||
await token.approve(marketplace.address, insufficient)
|
await token.approve(marketplace.address, insufficient)
|
||||||
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
||||||
"ERC20: insufficient allowance"
|
"ERC20: insufficient allowance"
|
||||||
|
@ -67,7 +67,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects resubmission of request", async function () {
|
it("rejects resubmission of request", async function () {
|
||||||
await token.approve(marketplace.address, request.maxPrice * 2)
|
await token.approve(marketplace.address, request.ask.maxPrice * 2)
|
||||||
await marketplace.requestStorage(request)
|
await marketplace.requestStorage(request)
|
||||||
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
await expect(marketplace.requestStorage(request)).to.be.revertedWith(
|
||||||
"Request already exists"
|
"Request already exists"
|
||||||
|
@ -78,7 +78,7 @@ describe("Marketplace", function () {
|
||||||
describe("offering storage", function () {
|
describe("offering storage", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await token.approve(marketplace.address, request.maxPrice)
|
await token.approve(marketplace.address, request.ask.maxPrice)
|
||||||
await marketplace.requestStorage(request)
|
await marketplace.requestStorage(request)
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
await token.approve(marketplace.address, collateral)
|
await token.approve(marketplace.address, collateral)
|
||||||
|
@ -114,7 +114,7 @@ describe("Marketplace", function () {
|
||||||
it("rejects offer for expired request", async function () {
|
it("rejects offer for expired request", async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
let expired = { ...request, expiry: now() - hours(1) }
|
let expired = { ...request, expiry: now() - hours(1) }
|
||||||
await token.approve(marketplace.address, request.maxPrice)
|
await token.approve(marketplace.address, request.ask.maxPrice)
|
||||||
await marketplace.requestStorage(expired)
|
await marketplace.requestStorage(expired)
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
let invalid = { ...offer, requestId: requestId(expired) }
|
let invalid = { ...offer, requestId: requestId(expired) }
|
||||||
|
@ -124,7 +124,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects an offer that exceeds the maximum price", async function () {
|
it("rejects an offer that exceeds the maximum price", async function () {
|
||||||
let invalid = { ...offer, price: request.maxPrice + 1 }
|
let invalid = { ...offer, price: request.ask.maxPrice + 1 }
|
||||||
await expect(marketplace.offerStorage(invalid)).to.be.revertedWith(
|
await expect(marketplace.offerStorage(invalid)).to.be.revertedWith(
|
||||||
"Price too high"
|
"Price too high"
|
||||||
)
|
)
|
||||||
|
@ -151,7 +151,7 @@ describe("Marketplace", function () {
|
||||||
describe("selecting an offer", async function () {
|
describe("selecting an offer", async function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await token.approve(marketplace.address, request.maxPrice)
|
await token.approve(marketplace.address, request.ask.maxPrice)
|
||||||
await marketplace.requestStorage(request)
|
await marketplace.requestStorage(request)
|
||||||
for (host of [host1, host2, host3]) {
|
for (host of [host1, host2, host3]) {
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
|
@ -170,7 +170,7 @@ describe("Marketplace", function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns price difference to client", async function () {
|
it("returns price difference to client", async function () {
|
||||||
let difference = request.maxPrice - offer.price
|
let difference = request.ask.maxPrice - offer.price
|
||||||
let before = await token.balanceOf(client.address)
|
let before = await token.balanceOf(client.address)
|
||||||
await marketplace.selectOffer(offerId(offer))
|
await marketplace.selectOffer(offerId(offer))
|
||||||
let after = await token.balanceOf(client.address)
|
let after = await token.balanceOf(client.address)
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe("Storage", function () {
|
||||||
offer.requestId = requestId(request)
|
offer.requestId = requestId(request)
|
||||||
|
|
||||||
switchAccount(client)
|
switchAccount(client)
|
||||||
await token.approve(storage.address, request.maxPrice)
|
await token.approve(storage.address, request.ask.maxPrice)
|
||||||
await storage.requestStorage(request)
|
await storage.requestStorage(request)
|
||||||
switchAccount(host)
|
switchAccount(host)
|
||||||
await token.approve(storage.address, collateralAmount)
|
await token.approve(storage.address, collateralAmount)
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
const { ethers } = require("hardhat")
|
const { ethers } = require("hardhat")
|
||||||
const { now, hours } = require("./time")
|
const { now, hours } = require("./time")
|
||||||
const { sha256, hexlify, randomBytes } = ethers.utils
|
const { hexlify, randomBytes } = ethers.utils
|
||||||
|
|
||||||
const exampleRequest = () => ({
|
const exampleRequest = () => ({
|
||||||
client: hexlify(randomBytes(20)),
|
client: hexlify(randomBytes(20)),
|
||||||
duration: hours(10),
|
ask: {
|
||||||
size: 1 * 1024 * 1024 * 1024, // 1 Gigabyte
|
size: 1 * 1024 * 1024 * 1024, // 1 Gigabyte
|
||||||
contentHash: sha256("0xdeadbeef"),
|
duration: hours(10),
|
||||||
proofProbability: 4, // require a proof roughly once every 4 periods
|
proofProbability: 4, // require a proof roughly once every 4 periods
|
||||||
maxPrice: 84,
|
maxPrice: 84,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
cid: "zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob",
|
||||||
|
erasure: {
|
||||||
|
totalChunks: 12,
|
||||||
|
totalNodes: 4,
|
||||||
|
nodeId: 3,
|
||||||
|
},
|
||||||
|
por: {
|
||||||
|
u: Array.from(randomBytes(480)),
|
||||||
|
publicKey: Array.from(randomBytes(96)),
|
||||||
|
name: Array.from(randomBytes(512)),
|
||||||
|
},
|
||||||
|
},
|
||||||
expiry: now() + hours(1),
|
expiry: now() + hours(1),
|
||||||
nonce: hexlify(randomBytes(32)),
|
nonce: hexlify(randomBytes(32)),
|
||||||
})
|
})
|
||||||
|
|
61
test/ids.js
61
test/ids.js
|
@ -2,21 +2,13 @@ const { ethers } = require("hardhat")
|
||||||
const { keccak256, defaultAbiCoder } = ethers.utils
|
const { keccak256, defaultAbiCoder } = ethers.utils
|
||||||
|
|
||||||
function requestId(request) {
|
function requestId(request) {
|
||||||
return keccak256(
|
const Ask = "tuple(uint256, uint256, uint256, uint256)"
|
||||||
defaultAbiCoder.encode(
|
const Erasure = "tuple(uint64, uint64, uint64)"
|
||||||
[
|
const PoR = "tuple(bytes1[480], bytes1[96], bytes1[512])"
|
||||||
"address",
|
const Content = "tuple(string, " + Erasure + ", " + PoR + ")"
|
||||||
"uint256",
|
const Request =
|
||||||
"uint256",
|
"tuple(address, " + Ask + ", " + Content + ", uint256, bytes32)"
|
||||||
"bytes32",
|
return keccak256(defaultAbiCoder.encode([Request], requestToArray(request)))
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"uint256",
|
|
||||||
"bytes32",
|
|
||||||
],
|
|
||||||
requestToArray(request)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function offerId(offer) {
|
function offerId(offer) {
|
||||||
|
@ -28,16 +20,31 @@ function offerId(offer) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askToArray(ask) {
|
||||||
|
return [ask.size, ask.duration, ask.proofProbability, ask.maxPrice]
|
||||||
|
}
|
||||||
|
|
||||||
|
function erasureToArray(erasure) {
|
||||||
|
return [erasure.totalChunks, erasure.totalNodes, erasure.nodeId]
|
||||||
|
}
|
||||||
|
|
||||||
|
function porToArray(por) {
|
||||||
|
return [por.u, por.publicKey, por.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentToArray(content) {
|
||||||
|
return [content.cid, erasureToArray(content.erasure), porToArray(content.por)]
|
||||||
|
}
|
||||||
|
|
||||||
function requestToArray(request) {
|
function requestToArray(request) {
|
||||||
return [
|
return [
|
||||||
request.client,
|
[
|
||||||
request.duration,
|
request.client,
|
||||||
request.size,
|
askToArray(request.ask),
|
||||||
request.contentHash,
|
contentToArray(request.content),
|
||||||
request.proofProbability,
|
request.expiry,
|
||||||
request.maxPrice,
|
request.nonce,
|
||||||
request.expiry,
|
],
|
||||||
request.nonce,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,4 +52,10 @@ function offerToArray(offer) {
|
||||||
return [offer.host, offer.requestId, offer.price, offer.expiry]
|
return [offer.host, offer.requestId, offer.price, offer.expiry]
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { requestId, offerId, requestToArray, offerToArray }
|
module.exports = {
|
||||||
|
requestId,
|
||||||
|
offerId,
|
||||||
|
requestToArray,
|
||||||
|
askToArray,
|
||||||
|
offerToArray,
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue