407 lines
11 KiB
Solidity
407 lines
11 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
// inspired by: https://bitbucket.org/rhitchens2/soliditystoragepatterns/src/master/OneToMany.sol
|
|
pragma solidity ^0.8.8;
|
|
|
|
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
|
|
import "./Utils.sol";
|
|
|
|
library DAL {
|
|
|
|
using EnumerableSet for EnumerableSet.Bytes32Set;
|
|
|
|
type RequestId is bytes32;
|
|
type SlotId is bytes32;
|
|
type ClientId is address;
|
|
type HostId is address;
|
|
|
|
struct Client {
|
|
ClientId id; // PK
|
|
|
|
EnumerableSet.Bytes32Set requests; // FKs
|
|
}
|
|
|
|
struct Host {
|
|
HostId id; // PK
|
|
|
|
EnumerableSet.Bytes32Set slots; // FKs
|
|
EnumerableSet.Bytes32Set requests; // FKs
|
|
}
|
|
|
|
struct Request {
|
|
RequestId id; // PK
|
|
ClientId client;
|
|
Ask ask;
|
|
Content content;
|
|
uint256 expiry; // time at which this request expires
|
|
bytes32 nonce; // random nonce to differentiate between similar requests
|
|
|
|
EnumerableSet.Bytes32Set slots;
|
|
}
|
|
|
|
struct Slot {
|
|
SlotId id; // PK
|
|
HostId host; // FK
|
|
bool hostPaid;
|
|
RequestId requestId; // FK
|
|
}
|
|
|
|
struct Ask {
|
|
uint64 slots; // the number of requested slots
|
|
uint256 slotSize; // amount of storage per slot (in number of bytes)
|
|
uint256 duration; // how long content should be stored (in seconds)
|
|
uint256 proofProbability; // how often storage proofs are required
|
|
uint256 reward; // amount of tokens paid per second per slot to hosts
|
|
uint64 maxSlotLoss; // Max slots that can be lost without data considered to be lost
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
struct PoR {
|
|
bytes u; // parameters u_1..u_s
|
|
bytes publicKey; // public key
|
|
bytes name; // random name
|
|
}
|
|
|
|
struct Database {
|
|
mapping(RequestId => Request) requests;
|
|
mapping(SlotId => Slot) slots;
|
|
mapping(ClientId => Client) clients;
|
|
mapping(HostId => Host) hosts;
|
|
}
|
|
|
|
|
|
|
|
/// *** CREATE OPERATIONS *** ///
|
|
|
|
function insert(Database storage db,
|
|
RequestId requestId,
|
|
ClientId clientId,
|
|
Ask calldata ask,
|
|
Content calldata content,
|
|
uint256 expiry,
|
|
bytes32 nonce)
|
|
internal
|
|
{
|
|
require(!isDefault(requestId), "request id required");
|
|
require(!_isDefault(clientId), "client address required");
|
|
require(exists(db, clientId), "client does not exist");
|
|
require(!exists(db, requestId), "request already exists");
|
|
|
|
Request storage r = db.requests[requestId];
|
|
r.id = requestId;
|
|
r.client = clientId;
|
|
r.ask = ask;
|
|
r.content = content;
|
|
r.expiry = expiry;
|
|
r.nonce = nonce;
|
|
}
|
|
|
|
function insert(Database storage db, Slot memory slot ) internal {
|
|
require(!_isDefault(slot.id), "slot id required");
|
|
require(!isDefault(slot.requestId), "request id required");
|
|
require(exists(db, slot.requestId), "request does not exist");
|
|
require(!exists(db, slot.id), "slot already exists");
|
|
require(exists(db, slot.host), "host does not exist");
|
|
db.slots[slot.id] = slot;
|
|
|
|
Request storage request = db.requests[slot.requestId];
|
|
request.slots.add(SlotId.unwrap(slot.id));
|
|
}
|
|
|
|
function insert(Database storage db, ClientId clientId) internal {
|
|
require (!exists(db, clientId), "client already exists");
|
|
require (!_isDefault(clientId), "address required");
|
|
Client storage c = db.clients[clientId];
|
|
c.id = clientId;
|
|
// NOTE: by default db.clients[client].requests already exists but has a default value
|
|
}
|
|
|
|
function insert(Database storage db, HostId hostId) internal {
|
|
require (!exists(db, hostId), "host already exists");
|
|
require (!_isDefault(hostId), "address required");
|
|
Host storage h = db.hosts[hostId];
|
|
h.id = hostId;
|
|
// NOTE: by default db.hosts[host].slots already exists but has a default value
|
|
}
|
|
|
|
function insert(Database storage db,
|
|
EnumerableSet.Bytes32Set storage requests,
|
|
RequestId requestId)
|
|
internal
|
|
{
|
|
require(exists(db, requestId), "request does not exist");
|
|
|
|
requests.add(RequestId.unwrap(requestId));
|
|
}
|
|
|
|
function insert(Database storage db,
|
|
EnumerableSet.Bytes32Set storage slots,
|
|
SlotId slotId)
|
|
internal
|
|
{
|
|
require(exists(db, slotId), "slot does not exist");
|
|
Slot storage slot = db.slots[slotId];
|
|
require(exists(db, slot.host), "host does not exist");
|
|
Host storage host = db.hosts[slot.host];
|
|
require(host.requests.contains(RequestId.unwrap(slot.requestId)),
|
|
"slot request not active");
|
|
|
|
slots.add(SlotId.unwrap(slotId));
|
|
}
|
|
|
|
|
|
|
|
/// *** READ OPERATIONS *** ///
|
|
|
|
function select(Database storage db, RequestId requestId)
|
|
internal
|
|
view
|
|
returns (Request storage)
|
|
{
|
|
require(exists(db, requestId), "Unknown request");
|
|
return db.requests[requestId];
|
|
}
|
|
|
|
function select(Database storage db, SlotId slotId)
|
|
internal
|
|
view
|
|
returns (Slot storage)
|
|
{
|
|
require(exists(db, slotId), "Slot empty");
|
|
return db.slots[slotId];
|
|
}
|
|
|
|
function select(Database storage db, ClientId clientId)
|
|
internal
|
|
view
|
|
returns (Client storage)
|
|
{
|
|
require(exists(db, clientId), "Client does not exist");
|
|
return db.clients[clientId];
|
|
}
|
|
|
|
function select(Database storage db, HostId hostId)
|
|
internal
|
|
view
|
|
returns (Host storage)
|
|
{
|
|
require(exists(db, hostId), "Host does not exist");
|
|
return db.hosts[hostId];
|
|
}
|
|
|
|
function exists(Database storage db, RequestId requestId)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
Request storage request = db.requests[requestId];
|
|
return !isDefault(request.id) && !_isDefault(request.client);
|
|
}
|
|
|
|
function exists(Database storage db, SlotId slotId)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
Slot storage slot = db.slots[slotId];
|
|
Request storage request = db.requests[slot.requestId];
|
|
return request.slots.contains(SlotId.unwrap(slotId)) &&
|
|
!_isDefault(slot.id) &&
|
|
!isDefault(slot.requestId);
|
|
}
|
|
|
|
function exists(Database storage db, ClientId clientId)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
return !_isDefault(db.clients[clientId].id);
|
|
}
|
|
|
|
function exists(Database storage db, HostId hostId)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
return !_isDefault(db.hosts[hostId].id);
|
|
}
|
|
|
|
|
|
|
|
|
|
/// *** DELETE OPERATIONS *** ///
|
|
|
|
function remove(Database storage db, Request storage request) internal {
|
|
require(request.slots.length() == 0, "references slots");
|
|
require(exists(db, request.client), "client does not exist");
|
|
Client storage client = db.clients[request.client];
|
|
bytes32 bRequestId = RequestId.unwrap(request.id);
|
|
require(!client.requests.contains(bRequestId), "active request refs");
|
|
// TODO: iterate all request's hosts and check that host.requests doesn't
|
|
// have requestId
|
|
|
|
delete db.requests[request.id];
|
|
}
|
|
|
|
function remove(Database storage db, Slot storage slot) internal {
|
|
require(exists(db, slot.requestId), "request does not exist");
|
|
Host storage host = db.hosts[slot.host];
|
|
bytes32 bSlotId = SlotId.unwrap(slot.id);
|
|
require(!host.slots.contains(bSlotId), "active slot refs");
|
|
|
|
Request storage request = db.requests[slot.requestId];
|
|
request.slots.remove(bSlotId);
|
|
delete db.slots[slot.id];
|
|
}
|
|
|
|
function remove(Database storage db, Client storage client) internal {
|
|
require(client.requests.length() == 0, "active request refs");
|
|
|
|
delete db.clients[client.id];
|
|
}
|
|
|
|
function remove(Database storage db, Host storage host) internal {
|
|
require(host.slots.length() == 0, "active slot refs");
|
|
|
|
delete db.hosts[host.id];
|
|
}
|
|
|
|
function remove(Database storage db,
|
|
EnumerableSet.Bytes32Set storage requests,
|
|
RequestId requestId)
|
|
internal
|
|
{
|
|
require(exists(db, requestId), "request does not exist");
|
|
// TODO: check that host.slots doesn't have a slot belonging to the request
|
|
// being removed
|
|
|
|
requests.remove(RequestId.unwrap(requestId));
|
|
}
|
|
|
|
function remove(Database storage db,
|
|
EnumerableSet.Bytes32Set storage slots,
|
|
SlotId slotId)
|
|
internal
|
|
{
|
|
require(exists(db, slotId), "slot does not exist");
|
|
|
|
slots.remove(SlotId.unwrap(slotId));
|
|
}
|
|
|
|
// WARNING: this may cause a transaction to run out of gas!
|
|
function clearSlots(Database storage db,
|
|
Host storage host,
|
|
RequestId requestId,
|
|
uint256 maxIterations)
|
|
internal
|
|
{
|
|
require(maxIterations > 0, "maxIterations must be > 0");
|
|
require(host.slots.length() <= maxIterations, "iterations out of bounds");
|
|
|
|
for (uint256 i = 0; i < host.slots.length(); i++) {
|
|
bytes32 slotId = host.slots.at(i);
|
|
Slot storage slot = select(db, SlotId.wrap(slotId));
|
|
if (equals(slot.requestId, requestId)) {
|
|
host.slots.remove(slotId);
|
|
}
|
|
}
|
|
remove(db, host.requests, requestId);
|
|
}
|
|
|
|
|
|
|
|
/// *** CALCULATED PROPERTIES *** ///
|
|
|
|
// WARNING: calling this in a transaction may cause an out of gas exception
|
|
function activeSlots(Database storage db, Host storage host)
|
|
internal
|
|
view
|
|
returns (SlotId[] memory)
|
|
{
|
|
// perform an inner join on host.slots and host.requests
|
|
bytes32[] memory result = new bytes32[](host.slots.length());
|
|
uint256 counter = 0;
|
|
for (uint256 i = 0; i < host.slots.length(); i++) {
|
|
bytes32 slotId = host.slots.at(i);
|
|
Slot storage slot = select(db, SlotId.wrap(slotId));
|
|
if (host.requests.contains(RequestId.unwrap(slot.requestId))) {
|
|
result[counter] = slotId;
|
|
counter++;
|
|
}
|
|
}
|
|
return toSlotIds(Utils.resize(result, counter));
|
|
}
|
|
|
|
|
|
|
|
/// *** CONVERSIONS *** ///
|
|
|
|
function toRequestIds(bytes32[] memory array)
|
|
internal
|
|
pure
|
|
returns (RequestId[] memory result)
|
|
{
|
|
// solhint-disable-next-line no-inline-assembly
|
|
assembly {
|
|
result := array
|
|
}
|
|
}
|
|
|
|
function toSlotIds(bytes32[] memory array)
|
|
internal
|
|
pure
|
|
returns (SlotId[] memory result)
|
|
{
|
|
// solhint-disable-next-line no-inline-assembly
|
|
assembly {
|
|
result := array
|
|
}
|
|
}
|
|
|
|
function toSlotId(RequestId requestId, uint256 slotIndex)
|
|
internal
|
|
pure
|
|
returns (SlotId)
|
|
{
|
|
return SlotId.wrap(keccak256(abi.encode(requestId, slotIndex)));
|
|
}
|
|
|
|
/// *** COMPARISONS *** ///
|
|
|
|
function isDefault(RequestId requestId) internal pure returns (bool) {
|
|
return equals(requestId, RequestId.wrap(0));
|
|
}
|
|
|
|
function _isDefault(SlotId slotId) private pure returns (bool) {
|
|
return equals(slotId, SlotId.wrap(0));
|
|
}
|
|
|
|
function _isDefault(address addr) private pure returns (bool) {
|
|
return addr == address(0);
|
|
}
|
|
|
|
function _isDefault (ClientId clientId) private pure returns (bool) {
|
|
return _isDefault(ClientId.unwrap(clientId));
|
|
}
|
|
|
|
function _isDefault (HostId hostId) private pure returns (bool) {
|
|
return _isDefault(HostId.unwrap(hostId));
|
|
}
|
|
|
|
function equals(RequestId a, RequestId b) internal pure returns (bool) {
|
|
return RequestId.unwrap(a) == RequestId.unwrap(b);
|
|
}
|
|
|
|
function equals(SlotId a, SlotId b) internal pure returns (bool) {
|
|
return SlotId.unwrap(a) == SlotId.unwrap(b);
|
|
}
|
|
}
|