logos-storage-spec/specs/marketplace.md
2024-08-23 15:57:35 +02:00

25 KiB

Codex Marketplace Spec

title: CODEX-MARKETPLACE name: Codex Storage Marketplace status: raw tags: codex editor: Dmitriy dryajov@status.im contributors:


Abstract

Codex Marketplace and its interactions are defined by a smart contract deployed on an EVM-compatible blockchain. This specification describes these interactions for the various roles within the network.

The document is intended for implementors of Codex nodes.

Semantics

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in 2119.

Definitions

Terminology Description
Storage Provider (SP) A node in the Codex network that provides storage services to the marketplace.
Validator A node that assists in identifying missing storage proofs.
Client A node that interacts with other nodes in the Codex network to store, locate, and retrieve data.
Storage Request or Request A request created by a client node to persist data on the Codex network.
Slot or Storage Slot A space allocated by the storage request to store a single chunk of the data related to this storage request.
Smart Contract A smart contract associated with marketplace functionality.
Token ERC20-based token used within the Codex network.

Motivation

The Codex network aims to create a peer-to-peer storage engine with robust data durability, data persistence guarantees, and a comprehensive incentive structure.

The marketplace is a critical component of the Codex network, serving as a platform where all involved parties interact to ensure data persistence. It also provides mechanisms to enforce agreements and facilitate data repair when Storage Nodes fail to fulfill their duties.

Implemented as a smart contract on an EVM-compatible blockchain, the marketplace enables various scenarios where nodes assume one or more roles to maintain a reliable persistence layer for users. This specification details these interactions.

The marketplace contract manages storage requests, maintains the state of allocated storage slots, and orchestrates storage provider rewards, collaterals, and storage proofs.

A node that wishes to participate in the Codex persistence layer MUST implement one or more roles described in this document.

Roles

A node can assume one of the three main roles in the network: the client, storage provider, and validator.

A client is a potentially short-lived node in the network with the purpose of persisting its data in the Codex persistence layer.

A storage provider is a long-lived node providing storage for clients in exchange for profit. To ensure a reliable, robust service for clients, storage providers are required to periodically provide proofs that they are persisting the data.

A validator ensures that storage providers have submitted valid proofs each period where the smart contract required a proof to be submitted for slots filled by the storage provider.

Storage Request Lifecycle

The diagram below depicts the lifecycle of a storage request:

                      ┌───────────┐                               
                      │ Cancelled │                               
                      └───────────┘                               
                            ▲                                     
                            │ Not all                             
                            │ Slots filled                        
                            │                                     
    ┌───────────┐    ┌──────┴─────────────┐           ┌─────────┐ 
    │ Submitted ├───►│ Slots Being Filled ├──────────►│ Started │ 
    └───────────┘    └────────────────────┘ All Slots └────┬────┘ 
                                            Filled         │      
                                                           │      
                                   ┌───────────────────────┘      
                           Proving ▼                              
    ┌────────────────────────────────────────────────────────────┐
    │                                                            │
    │                 Proof submitted                            │
    │       ┌─────────────────────────► All good                 │
    │       │                                                    │
    │ Proof required                                             │
    │       │                                                    │
    │       │         Proof missed                               │
    │       └─────────────────────────► After some time slashed  │
    │                                   eventually Slot freed    │
    │                                                            │
    └────────┬─┬─────────────────────────────────────────────────┘
             │ │                                      ▲           
             │ │                                      │           
             │ │ SP kicked out and Slot freed ┌───────┴────────┐  
All good     │ ├─────────────────────────────►│ Repair process │  
Time ran out │ │                              └────────────────┘  
             │ │                                                  
             │ │ Too many Slots freed         ┌────────┐          
             │ └─────────────────────────────►│ Failed │          
             ▼                                └────────┘          
       ┌──────────┐                                               
       │ Finished │                                               
       └──────────┘                                               

image

Client Role

A node implementing the client role mediates the persistence of data within the Codex network.

A client has two primary responsibilities:

  • Requesting storage from the network by sending a storage request to the smart contract.
  • Withdrawing funds from the storage requests previously created by the client.

Creating Storage Requests

When a user prompts the client node to create a storage request, the client node SHOULD receive the input parameters for the storage request from the user.

To create a request to persist a dataset on the Codex network, client nodes MUST split the dataset into data chunks, (c_1, c_2, c_3, \ldots, c_{n}). Using the erasure coding method and the provided input parameters, the data chunks are encoded and distributed over a number of slots. The applied erasure coding method MUST use the Reed-Soloman algorithm. The final slot roots and other metadata MUST be placed into a Manifest (TODO: Manifest RFC). The CID for the Manifest MUST then be used as the cid for the stored dataset.

After the dataset is prepared, a client node MUST call the smart contract function requestStorage(request), providing the desired request parameters in the request parameter. The request parameter is of type Request:

struct Request {
  // The Codex node requesting storage
  address client;

  // Describes parameters of Request
  Ask ask;
  
  // Describes the dataset that will be hosted with the Request
  Content content;

  // Timeout in seconds during which all the slots have to be filled, otherwise Request will get cancelled
  uint256 expiry;

  // Random value to differentiate from other requests of same parameters
  byte32 nonce;
}
  
struct Ask {
  // Amount of tokens that will be awarded to storage providers for finishing the storage request.
  // Reward is per slot per second.
  uint256 reward;

  // Amount of tokens required for collateral by storage providers
  uint256 collateral;

  // Frequency that storage providers need to submit proofs of storage
  uint256 proofProbability;

  // Total duration of the storage request in seconds
  uint256 duration;

  // The number of requested slots
  uint64 slots;

  // Amount of storage per slot in bytes
  uint256 slotSize;

  // Max slots that can be lost without data considered to be lost
  uint64 maxSlotLoss; 
}

struct Content {
  // Content identifier
  string cid;

  // Merkle root of the dataset, used to verify storage proofs
  byte32 merkleRoot;
}

The the table below provides the description of the Request and the associated types attributes:

attribute type description
client address The Codex node requesting storage.
ask Ask Parameters of Request.
content Content The dataset that will be hosted with the storage request.
expiry uint256 Timeout in seconds during which all the slots have to be filled, otherwise Request will get cancelled. The final deadline timestamp is calculated at the moment the transaction is mined.
nonce byte32 Random value to differentiate from other requests of same parameters. It SHOULD be a random byte array.
reward uint256 Amount of tokens that will be awarded to storage providers for finishing the storage request. It MUST be an amount of Tokens offered per slot per second. The Ethereum address that submits the requestStorage() transaction MUST have approval for the transfer of at least an equivalent amount in Tokens.
collateral uint256 The amount of tokens that storage providers submit when they fill slots. Collateral is then slashed or forfeited if storage providers fail to provide the service requested by the storage request (more information in the Slashing section).
proofProbability uint256 Determines the average frequency that a proof is required within a period: \frac{1}{proofProbability}. Storage providers are required to provide proofs of storage to the marketplace smart contract when challenged by the smart contract. To prevent hosts from only coming online when proofs are required, the frequency at which proofs are requested from storage providers is stochastic and is influenced by the proofProbability parameter.
duration uint256 Total duration of the storage request in seconds.
slots uint64 The number of requested slots. The slots will all have the same size.
slotSize uint256 Amount of storage per slot in bytes.
maxSlotLoss uint64 Max slots that can be lost without data considered to be lost.
cid string An identifier used to locate the Manifest representing the dataset. It MUST be a CIDv1, SHA-256 multihash and the data it represents SHOULD be discoverable in the network, otherwise the request will be eventually canceled.
merkleRoot byte32 Merkle root of the dataset, used to verify storage proofs

Renewal of Storage Requests

It should be noted that the marketplace does not support extending requests. It is REQUIRED that if the user wants to extend the duration of a request, a new request with the same CID must be created before the original request completes. This ensures that the data will continue to persist in the network at the time when the new (or existing) storage providers need to retrieve the complete dataset to fill the slots of the new request.

Withdrawing Funds

The client node SHOULD monitor the status of the requests it created. When a storage request enters the Cancelled state (this occurs when not all slots have been filled before the expiry timeout), the client node SHOULD initiate the withdrawal of the remaining funds from the smart contract using the withdrawFunds(requestId) function.

  • The request is considered Cancelled if no requestFulfilled(requestId) event is observed during the timeout specified by the value returned from the requestExpiresAt(requestId) function.
  • The request is considered Failed when the RequestFailed(requestId) event is observed.
  • The request is considered Finished after the interval specified by the value returned from the getRequestEnd(requestId) function.

Storage Provider Role

A Codex node acting as a storage provider persists data across the network by hosting slots requested by clients in their storage requests.

The following tasks need to be considered when hosting a slot:

  • Filling a slot
  • Proving
  • Repairing a slot
  • Collecting request reward and collateral

Filling Slots

When a new request is created, the StorageRequested(requestId, ask, expiry) event is emitted with the following properties:

  • requestId - The ID of the request.
  • ask - The specification of the request parameters. For details, see the definition of the Request type in the Creating Storage Requests section above.
  • expiry - A Unix timestamp specifying when the request will be canceled if all slots are not filled by then.

It is then up to the storage provider node to decide, based on the parameters provided by the node operator, whether it wants to participate in the request and attempt to fill its slot(s) (note that one storage provider can fill more than one slot). If the storage provider node decides to ignore the request, no further action is required. However, if the storage provider decides to fill a slot, and succeeds, it MUST follow the remaining steps described below.

The node acting as a storage provider MUST decide which slot, specified by the slot index, it wants to fill. The storage provider MAY attempt to fill more than one slot. To fill a slot, the storage provider MUST first download the slot data using the CID of the manifest (TODO: Manifest RFC) and the slot index. The CID is specified in request.content.cid, which can be retrieved from the smart contract using getRequest(requestId). Then, the node MUST generate a proof over the downloaded data (TODO: Proving RFC).

When the proof is ready, the storage provider MUST call fillSlot() on the smart contract with the following parameters being REQUIRED:

  • requestId - The ID of the request.
  • slotIndex - The slot index that the node wants to fill.
  • proof - The Groth16Proof proof structure, generated over the slot data.
  • The Ethereum address of the node from which the transaction originates MUST have approval for the transfer of at least the amount of Tokens required as collateral for the request.

Also here, the last point might benefit from a more detailed explanation.

If the proof delivered by the storage provider is invalid or the slot was already filled by another storage provider, then the transaction will be reverted. Otherwise, a SlotFilled(requestId, slotIndex) event is emitted. If the transaction is successful, the storage provider SHOULD transition into the proving state, where it will need to submit proof of data possession when prompted by the smart contract.

It should be noted that if the storage provider node observes a SlotFilled event for the slot it is currently downloading the dataset for or generating the proof for, it means that the slot has been filled by another node in the meantime. In response, the storage provider SHOULD stop its current operation and attempt to fill a different, unfilled slot.

Proving

Once a storage provider successfully fills a slot, it MUST periodically, though non-deterministically, provide proofs to the smart contract that it is storing the data it committed to store. A storage provider node SHOULD detect whether a proof is required using the isProofRequired(slotId) smart contract function, or anticipate that a proof will be required using willProofBeRequired(slotId) in case the node is in downtime.

Including more details about downtime might be beneficial.

Once the storage provider knows it must provide a proof, it MUST retrieve the proof challenge using getChallenge(slotId), which then NEEDS to be incorporated into the proof generation as described in the Proving RFC (TODO: Proving RFC).

When the proof is generated, it MUST be submitted by calling the submitProof(slotId, proof) smart contract function.

Slashing

There is a slashing scheme orchestrated by the smart contract to incentivize correct behavior and proper proof submissions by storage providers. This scheme is configured at the smart contract level and applies uniformly to all participants in the network. The configuration of the slashing scheme can be obtained via the getConfig() contract call.

The slashing works as follows:

  • A storage provider node MAY miss up to config.collateral.slashCriterion proofs before being slashed.
  • It is then slashed by config.collateral.slashPercentage of the originally required collateral (the slashing amount is always consistent for a given request).
  • If the number of slashes exceeds config.collateral.maxNumberOfSlashes, the slot is released, the remaining collateral is burned, and the slot is offered to other nodes for repair. The smart contract also emits the SlotFreed(requestId, slotIndex) event.

If, at any time, the number of released slots exceeds the value specified by the request.ask.maxSlotLoss parameter, the dataset is considered lost, and the request is deemed failed. The collateral of all storage providers that hosted the slots associated with the request is burned, and the RequestFailed(requestId) event is emitted.

Repair

When a slot is released due to too many missed proofs, which SHOULD be detected by listening to the SlotFreed(requestId, slotIndex) event, a storage provider node can decide whether to participate in repairing the slot. Similar to filling a slot, the node SHOULD consider the operator's configuration when making this decision. The storage provider that originally hosted the slot but failed to comply with proving requirements MAY also participate in the repair. However, by refilling the slot, the storage provider will not recover its original collateral and must submit new collateral using the fillSlot() call.

The repair process is similar to filling slots. If the original slot dataset is no longer present in the network, the storage provider MAY use Erasure Coding to reconstruct the dataset. Reconstructing the original slot dataset requires retrieving other pieces of the dataset stored in other slots belonging to the request. For this reason, the node that successfully repairs a slot is entitled to an additional reward. (TODO: Implementation)

The repair process proceeds as follows:

  1. The storage provider observes the SlotFreed event and decides to repair the slot.
  2. The storage provider MUST download the chunks of data required to reconstruct the released slot's data. The node MUST use the Reed-Solomon algorithm to reconstruct the missing data.
  3. The storage provider MUST generate proof over the reconstructed data.
  4. The storage provider MUST call the fillSlot() smart contract function with the same parameters and collateral allowance as described in the Filling Slots section.

Collecting Funds

A storage provider node SHOULD monitor the requests and the associated slots it hosts.

When a storage request enters the Cancelled, Finished, or Failed state, the storage provider node SHOULD call the freeSlot(slotId) smart contract function.

The aforementioned storage request states (Cancelled, Finished, and Failed) can be detected as follows:

  • A storage request is considered Cancelled if no RequestFulfilled(requestId) event is observed within the time indicated by the expiry request parameter. Note that a RequestCancelled event may also be emitted, but the node SHOULD NOT rely on this event to assert the request expiration, as the RequestCancelled event is not guaranteed to be emitted at the time of expiry.
  • A storage request is considered Finished when the time indicated by the value returned from the getRequestEnd(requestId) function has elapsed.
  • A node concludes that a storage request has Failed upon observing the RequestFailed(requestId) event.

For each of the states listed above, different funds are handled as follows:

  • In the Cancelled state, the collateral is returned along with a proportional payout based on the time the node actually hosted the dataset before the expiry was reached.
  • In the Finished state, the full reward for hosting the slot, along with the collateral, is collected.
  • In the Failed state, no funds are collected. The reward is returned to the client, and the collateral is burned. The slot is removed from the list of slots and is no longer included in the list of slots returned by the mySlots() function.

Validator Role

In a blockchain, it is impossible to act on events that do not happen since every action results from a transaction. Therefore, our smart contract requires an external trigger to periodically check and confirm that a storage proof has been delivered by the storage provider. This is where the validator role is essential.

The validator role is fulfilled by nodes that verify whether storage providers have submitted the required storage proofs.

It is the smart contract that checks if the proof requested from a storage provider has been delivered. The validator's job is to trigger this check on the smart contract for storage providers "observed" by the validator. To incentivize validators, they receive a reward each time they help identify a missing proof from a storage provider.

Each time a validator observes the SlotFilled event, it adds the slot reported in the SlotFilled event to its list of watched slots. Then, at the end of each period, a validator has up to config.proofs.timeout seconds (a configuration parameter retrievable with getConfig()) to request proof validation from the smart contract for each slot in its list. If a slot lacks the required proof, the validator SHOULD call the markProofAsMissing(slotId, period) function on the smart contract. After confirming the missing proof for the slot with ID slotId in the given period, the markProofAsMissing(slotId, period) function will reward the validator.

It may be helpful to add an introduction or explanation regarding the periods and their occurrence.

If validating all the slots observed by the validator is not feasible within the specified timeout, the validator MAY choose to validate only a subset of the observed slots.

Copyright and related rights waived via CC0.

References

  1. Reed-Soloman algorithm
  2. CIDv1
  3. multihash
  4. Proof-of-Data-Possession
  5. Codex market implementation