initial design for a simplified sales module

Based on the updated RepoStore implementation with no reservations.
Single availability, no concurrency, no accounting in the availability.
This commit is contained in:
Eric 2025-04-02 17:47:18 +11:00
parent 4a5ddf0e52
commit 139223649c
No known key found for this signature in database
2 changed files with 370 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

370
design/sales2.md Normal file
View File

@ -0,0 +1,370 @@
# Sales module (add purchasing module)
The sales module is responsible for selling a node's available storage in the
[marketplace](./marketplace.md). In order to do so, it needs to create an
Availability for the storage provider (SP) to establish under which
conditions it is willing to enter into a sale.
```ascii
------------------------------------------------------------------
| |
| Sales |
| |
| ^ | |
| | | updates ------------------ |
| | --------------> | | |
| | | SalesStorage | |
| ------------------- | | |
| queries ------------------ |
| ^ ^ |
| | | |
| | | Availability + SaleOrder |
| dedicated quota | | state |
| v v |
| ---------------- ----------------- |
| | SalesRepo | | MetadataStore | |
| ---------------- ----------------- |
------------------------------------------------------------------
```
The `SalesStorage` module manages the SP's availability and snapshots of past
and present sales or `SalesOrders`, both of which are persisted in the `MetadataStore`. SPs can add
and update their availability, which is managed through the `SalesStorage`
module. As a `SalesOrder` traverses the sales state machine, it is created and
updated<sup>1</sup> through the `SalesStorage` module. Queries for availability
and `SalesOrders` will also occur in the `SalesStorage` module. Datasets that
are downloaded and deleted as part of the sales process will be handled in the
`SalesRepo` module.
<sup>1</sup> Updates are only needed to support [tracking the latest state in
the `SalesOrder`](#tracking-latest-state-machine-state).
## Query support
The `SalesStorage` module will need to support querying the availability and sales
data so the caller can understand if a sale can be serviced and to support clean
up routines. The following queries will need to be supported:
1. To know if there is enough space on disk for a new sale, the `SalesStorage`
module can be queried for the remaining sales quota in its dedicated
`SalesRepo` partition. In the future, this can be optimised to [prevent
unnecessary resource
consumption](#concurrent-workers-prevent-unnecessary-resource-consumption),
by additionally querying the slot size of `SalesOrders` that are in or past
the Downloading state.
2. Clean up routines will need to know the "active sales", or any `SalesOrders`
in the `/active` key namespace (those that have not been archived) through
the state machine or clean up routines.
3. Servicing a new slot will require sufficient "total collateral", which is the
remaining balance in the funding account. In the future, this can be
optimised to [prevent unnecessary resource
consumption](#concurrent-workers-prevent-unnecessary-resource-consumption),
by additionally querying the collateral of `SalesOrders` that are in or past
the Downloading state.
## `SalesRepo` module
The `SalesRepo` module is responsible for interacting with its underlying
`RepoStore`. This additional layer abstracts away some of the required
implementation routine needed for the `RepoStore`, while also allowing the
`RepoStore` to change independent of the sales module. It will expose functions
for storing and deleting datasets:
```mermaid
---
config:
look: neo
layout: dagre
---
classDiagram
direction TB
class RepoStore {
+putBlock(BlockAddress)
+delBlock(BlockAddress)
}
class SalesRepo {
-RepoStore repo
+store(BlockAddress): Stores the manifest dataset.
+delete(BlockAddress): Deletes the manifest dataset in the RepoStore.
}
class SalesStorage {
-salesRepo: SalesRepo
}
SalesRepo --* RepoStore
SalesStorage <--* SalesRepo
class SalesRepo:::focusClass
classDef focusClass fill:#c4fdff,stroke:#333,stroke-width:4px,color:black
```
## Availability
The SP's availability determines which sales it is willing to attempt to enter
into. In other words, it represents *future sales* that an SP is willing to take
on. It consists of parameters that will be matched to incoming storage
requests via the slot queue.
| Property | Description |
|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | ID of the Availability. Note: this is only needed if there is support for [multiple availabilities](#multiple-availabilities). |
| `duration` | Maximum duration of a storage request the SP is willing to host new slots for. |
| `minPricePerBytePerSecond` | Minimum price per byte per second that the SP is willing to host new slots for. |
| `enabled` | If set to false, the availability will not accept new slots. Updates to this value will not impact any existing slots that are already being hosted. |
| `until` | Specifies the latest timestamp after which the availability will no longer host any slots. If set to 0, there will be no restrictions. |
The availability of a SP consists of the maximum duration and the minimum price
per byte per second to sell storage for.
### Funding account vs profit account
SPs should control two accounts: a funding account, and a profits account. The
funds in the funding account represent the total collateral that a SP is willing
to risk in all of its sales combined. This account will need to have some funds
in it before slots can be hosted, assuming the storage request requires
collateral. If a SP has been partially or wholly slashed in one of their sales,
they may wish to top up this account to ensure there is sufficient collateral
for future sales.
The profits account is the account for which proceeds from sales are paid into.
To minimise risk, this account should be stored in cold storage.
It is recommended that the profit account is a separate account from the funding
account so that profits are not placed at risk by being used as collateral. If a
SP specifies the same account for funding and profits, and the SP is (partially
or wholly) slashed, future collateral deposits may use their profits from
previous sales.
Note: having a separate profit account relies on the ability of the Vault
contract to support multiple accounts.
### Total collateral
The concept of "total collateral" means the total collateral the SP is willing
to risk at any one point in time. In other words, it is willing to risk "total
collateral" tokens for all of its active sales combined. Total collateral is
determined by the balance of funds in the SP's funding account. So, any funds in
the funding account are considered available to use as collateral for filling
slots.
From the marketplace perspective, slots cannot be filled if there is an
insufficient balance in the funding account.
### `Availability` lifecycle
A user can add, update, or delete an `Availability` at any time. The
`Availability` will be stored in the MetadataStore. Only one `Availability` can
be created and once created, it will exist permanently in the MetadataStore
until it is deleted. The properties of a created `Availability` can be updated
at any time.
Because availability(ies) represents *future* sales (and not active sales), and
because fields of the matching `Availability` are persisted in a `SalesOrder`,
availabilities are not tied to active sales and can be manipulated at any time.
## `SalesOrder` object
The `SalesOrder` object represents a snapshot of the sale parameters at the time
a slot is processed in the slot queue. It can be thought of as the market
conditions at the time of sale. It includes fields of the storage request, slot,
and the matching availability fields.
| Property | Description |
|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `requestId` | RequestId of the StorageRequest. Can be used to retrieve storage request details. |
| `slotIndex` | lot index of the slot being processed. |
| `duration` | `duration` from the matched Availability. |
| `minPricePerBytePerSecond` | `minPricePerByte` from the matched Availabilty. |
| `state` | Latest state in the sales state machine that was reached. Note: this is only needed when there support for [tracking the latest state in the `SalesOrder`](#tracking-latest-state-machine-state). |
### `SalesOrder` lifecycle
At the point a SP reaches the `SaleDownload` state, a `SalesOrder` is created
and it will live permanently in the MetadataStore. `SalesOrder` objects cannot
be deleted as they represent historical sales of the SP.
When the `SalesOrder` object is first created, its key will be created in the
`/active` namespace. After data for the `SalesOrder` has been deleted (if there
is any) in a clean up procedure, the key will be moved from the `/active`
namespace to the `/archive` namespace. These key namespace manipulations
facilitate future lookups in active/passive clean up operations.
If there's support for [tracking the latest state in the
`SalesOrder`](#tracking-latest-state-machine-state), `SalesOrder.state`
will be modified as the sale progresses through each state of the Sales state
machine.
## Cleanup routines
The responsibility of the cleanup routine is to ensure that any data associated
with a Sale is deleted from the `SalesRepo`. Once the data has been deleted, the
`SalesOrder` will reflect that it has been cleaned up by being archived.
There are two types of cleanup routines that a SP node will take part in: active
and passive. Active cleanup routines are run as part of a state in the Sales
state machine. Passive cleanup routines are continuously run at a specified time
interval. Both perform a similar task, however the active cleanups operate on a
single `SalesOrder`, while passive cleanups operate over a set of `SalesOrders`
and have additional conditions for cleanup.
### Active cleanup
The active cleanup routine is typically run as part of the a final state in the
Sales state machine, ie `SaleFinished`. In this routine, active sales will be
retrieved from the Marketplace contract via `mySlots`. If the slot id associated
with the sale is not in the set of active sales, any data associated with the
slot will be deleted. Then, the `SalesOrder` will be archived, by moving its key
to the `/archive` namespace.
### Passive cleanup
At regular time intervals, active sales will be retrieved from the Marketplace
contract via `mySlots`. Then, all `SalesOrders` in the `/active` namespace will
be queried. Any `SalesOrders` with a slot id not in the set of active sales and
with a `StorageRequest` state that is "completed" (failed, cancelled, finished),
will have the data associated with the slot deleted, if there is any. Then, the
`SalesOrder` will be archived by moving its key to the `/archive` namespace.
`SalesOrders` with a `StorageRequest` state that is not yet completed should not
have their data deleted, as the SP may be in the process of starting to host a
slot, with the sale in an early state of the Sales state machine.
### Node startup
On node startup, the passive cleanup routine should be run.
## Sale flow
[Insert flow charts]
## Optimisations and features
### Multiple availabilities
Multiple availabilities are useful to allow SPs to understand which Availability
parameters produce the most profit for them. Multiple availabilities can be
updated or deleted at any time. This is possible because there is no
availability ID stored in the `SalesOrder` object.
Note that the total collateral across all availabilities that a SP is
willing to risk remains as the balance of funds in the funding account.
### Concurrent workers support
Concurrent workers allow a SP to reserve, download, generate an initial proof
for, and fill multiple slots simultaneously. This could prevent SPs from missing
sale opportunities that arise while they are reserving, downloading, generating,
and generating an initial proof for another sale. The trade off, however, is
that concurrent workers will require more system resources than a single worker.
In addition, concurrency is difficult to reason about, can introduce
difficult-to-debug bugs, and also opens up the possibility of unnecessary
reserving, downloading, and proof generation (discussed below). Therefore, it is
imperative this feature is implemented carefully.
### Tracking latest state machine state
Tracking the latest state machine state in locally persisted `SalesOrders` can
allow for historical sales listings (eg REST api or Codex app), sales
performance analysis (eg profit), and availability optimisations.
After a `StorageRequest` is completed, it is removed from the contract's
`mySlots` storage, with a locally-persisted `SalesOrder` being the only
remaining information about the sale. Without having the latest state persisted,
`SalesOrders` will be archived, but the SP will not know what the final state of
a `SalesOrders` was when it was archived. For example, it will not be able to
distinguish between a sale that errored and a slot that was successfully
hosted. This information is useful for listing states of sales, but also for
optimisations.
Active sale data is stored on chain in the Marketplace contract (`mySlots`).
However, these slots are slots that have already been filled by the SP.
When making a decision to service a new slot, the SP can optimise its decision
with information about sales that may be at an earlier stage in the sales
process, ie downloading, proof generating, or filling. To
facilitate this, `SalesOrder.state` would need to track the latest state of the
sale in the sales state machine.
Tracking the latest state opens up the possibility for further optimisations,
see below.
### Concurrent workers: prevent unnecessary resource consumption
Depends on: Tracking latest state machine state<br/>
Depends on: Concurrent workers
To prevent unnecessary reserving, downloading, and proof generation when there
are concurrent workers, when checking to ensure there's enough collateral
available, instead of only checking the funding account's current balance, also
check collateral that will be used to fill slots in `SalesOrders` that are
`/active`. Without this check, SPs may reserve, download, and generate a proof
for a sale that would ultimately result in not having enough collateral. For
example, if funding account balance is 100, and the SP is currently downloading
two sales with 100 collateral each, then that would mean that the download that
finishes last will ultimately be wasted as the SP would not have enough
collateral to fill both slots.
### Renewals: prevent dataset deletion
During renewals, there could potentially be a new sale for the same dataset that
is already in an active sale. The `SlotId` (and `RequestId`) will differ,
however the CID will be the same. Renewals should occur well before the initial
sale finishes. However, if the new sale is close in time to the completion of
the first sale, then as the dataset for the first sale is being cleaned up,
it may delete the dataset that is needed by the new sale. The new sale may have
been in the process of being downloaded, or having proofs generated.
This can be prevented by having an in-memory ref count of datasets. When a
dataset is stored, the ref count of the dataset (`hash(treeCid, slotIndex)`) is
incremented. When the dataset is deleted, the ref count is decremented. Only
when the ref count is 0 is the dataset actually deleted in the `RepoStore`. The
ref count does not require persistence because on startup, hosted slots will not
be deleted.
Ref count handling can be managed in `SalesRepo` module. This module is
responsible for interacting with the underlying `RepoStore`, and managing the
internal ref count. It will expose functions for storing and deleting datasets.
Note that any calls to ref count should be locked, as they may be read and
updated concurrently.
This is how the `SalesRepo` module will interact with `RepoStore` and the
marketplace:
```mermaid
---
config:
look: neo
layout: dagre
---
classDiagram
direction TB
class RepoStore {
+putBlock(BlockAddress)
+delBlock(BlockAddress)
}
class SalesRepo {
+Table~BlockAddress, uint~ refCount
-RepoStore repo
+store(BlockAddress): Stores the manifest dataset and increments the ref count of the manifest.
+delete(BlockAddress): Decrements the ref count of the manifest and deletes the manifest dataset in the RepoStore if the ref count is zero.
}
class SalesStorage {
-salesRepo: SalesRepo
}
SalesRepo --* RepoStore
SalesStorage <--* SalesRepo
class SalesRepo:::focusClass
classDef focusClass fill:#c4fdff,stroke:#333,stroke-width:4px,color:black
```
#### Alternative idea
Preventing deletion of datasets that are being downloaded or proof generating
can also be achieved by first checking if the slot id exists in `/mySlots` (at this stage
the initial sale should no longer be in `/mySlots`). If it does not, then check
if there are more than one `/active` (reached downloading) `SalesOrders` with
the same `hash(treeCid, slotIndex)` that exist. If there are not, delete the
dataset. Finally, archive the `SalesOrder`.
![Cleanup handling renewals](<sales flow charts/renewals_cleanup.jpg>)
## Purchasing