Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ Your node is now running. To start additional nodes, repeat these steps in a new
- [API Endpoints](docs/API.md)
- [Environmental Variables](docs/env.md)
- [Database Guide](docs/database.md)
- [Storage Types](docs/Storage.md)
- [Asset Storage Types](docs/Storage.md)
- [Persistent storage for c2d jobs](docs/persistentStorage.md)
- [Testing Guide](docs/testing.md)
- [Network Configuration](docs/networking.md)
- [Logging & accessing logs](docs/networking.md)
Expand Down
151 changes: 151 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1586,3 +1586,154 @@ returns job result
#### Response

File content

---

## Persistent Storage

### `HTTP` POST /api/services/persistentStorage/buckets

#### Description

Create a new persistent storage bucket. Bucket ownership is set to the request `consumerAddress`.

#### Request Headers

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| Authorization | string | | auth token (optional; depends on node auth configuration) |

#### Request Body

```json
{
"consumerAddress": "0x...",
"signature": "0x...",
"nonce": "123",
"accessLists": []
}
```

#### Response (200)

```json
{
"bucketId": "uuid",
"owner": "0x...",
"accessList": []
}
```

---

### `HTTP` GET /api/services/persistentStorage/buckets

#### Description

List buckets for a given `owner`. Results are filtered by bucket access lists for the calling consumer.

#### Query Parameters

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| consumerAddress | string | v | consumer address |
| signature | string | v | signed message (consumerAddress + nonce + command) |
| nonce | string | v | request nonce |
| chainId | number | v | chain id (used by auth/signature checks) |
| owner | string | v | bucket owner to filter by |

#### Response (200)

```json
[
{
"bucketId": "uuid",
"owner": "0x...",
"createdAt": 1710000000,
"accessLists": []
}
]
```

---

### `HTTP` GET /api/services/persistentStorage/buckets/:bucketId/files

#### Description

List files in a bucket.

#### Query Parameters

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| consumerAddress | string | v | consumer address |
| signature | string | v | signed message (consumerAddress + nonce + command) |
| nonce | string | v | request nonce |

#### Response (200)

```json
[
{
"bucketId": "uuid",
"name": "hello.txt",
"size": 123,
"lastModified": 1710000000
}
]
```

---

### `HTTP` POST /api/services/persistentStorage/buckets/:bucketId/files/:fileName

#### Description

Upload a file to a bucket. The request body is treated as raw bytes.

#### Query Parameters

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| consumerAddress | string | v | consumer address |
| signature | string | v | signed message (consumerAddress + nonce + command) |
| nonce | string | v | request nonce |

#### Request Body

Raw bytes (any content-type).

#### Response (200)

```json
{
"bucketId": "uuid",
"name": "hello.txt",
"size": 123,
"lastModified": 1710000000
}
```

---

### `HTTP` DELETE /api/services/persistentStorage/buckets/:bucketId/files/:fileName

#### Description

Delete a file from a bucket.

#### Query Parameters

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| consumerAddress | string | v | consumer address |
| signature | string | v | signed message (consumerAddress + nonce + command) |
| nonce | string | v | request nonce |
| chainId | number | v | chain id (used by auth/signature checks) |

#### Response (200)

```json
{ "success": true }
```
172 changes: 172 additions & 0 deletions docs/persistentStorage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Persistent Storage

This document describes Ocean Node **Persistent Storage** at a high level: what it is, how it is structured, how access control works, and how to use it via **P2P commands** and **HTTP endpoints**.

---

## What it is

Persistent Storage is a simple bucket + file store intended for **long-lived artifacts** that Ocean Node needs to keep across requests (and potentially across restarts), and to reference later (e.g. as file objects for compute).

Key primitives:
- **Bucket**: a logical container for files.
- **File**: binary content stored inside a bucket.
- **Bucket registry**: a local SQLite table that stores bucket metadata (owner, access lists, createdAt).

---

## Architecture (high level)

### Components

- **Handlers (protocol layer)**: `src/components/core/handler/persistentStorage.ts`
- Implements protocol commands such as create bucket, list files, upload, delete, and get buckets.
- Validates auth (token or signature) and applies high-level authorization checks.

- **Persistent storage backends (storage layer)**: `src/components/persistentStorage/*`
- `PersistentStorageFactory`: shared functionality (SQLite bucket registry, access list checks).
- `PersistentStorageLocalFS`: local filesystem backend.
- `PersistentStorageS3`: stub for future S3-compatible backend.

- **HTTP routes (HTTP interface)**: `src/components/httpRoutes/persistentStorage.ts`
- Exposes REST-ish endpoints under `/api/services/persistentStorage/...` that call the same handlers.

### Data storage

Persistent Storage uses two stores:

1) **Bucket registry (SQLite)**
- File: `databases/persistentStorage.sqlite`
- Table: `persistent_storage_buckets`
- Columns:
- `bucketId` (primary key)
- `owner` (address, stored as a string)
- `accessListJson` (JSON-encoded access list array)
- `createdAt` (unix timestamp)

2) **Backend data**
- `localfs`: writes file bytes to the configured folder under `buckets/<bucketId>/<fileName>`.
- `s3`: not implemented yet.

---

## Ownership and access control

### Ownership

Every bucket has a single **owner** address, stored in the bucket registry.

- When a bucket is created, the node sets:
- `owner = consumerAddress` (normalized via `ethers.getAddress`)

### Bucket access list

Each bucket stores an **AccessList[]** (per-chain list(s) of access list contract addresses):

```ts
export interface AccessList {
[chainId: string]: string[]
}
```

This access list is used to decide whether a given `consumerAddress` is allowed to interact with a bucket.

### Where checks happen

Access checks happen at two levels:

1) **Backend enforcement** (required)
- Backend operations `listFiles`, `uploadFile`, `deleteFile`, and `getFileObject` all require `consumerAddress`.
- The base class helper `assertConsumerAllowedForBucket(consumerAddress, bucketId)` loads the bucket ACL and throws `PersistentStorageAccessDeniedError` if the consumer is not allowed.

2) **Handler enforcement** (command-specific)
- `createBucket`: additionally checks the node-level allow list `config.persistentStorage.accessLists` (who can create buckets at all).
- `getBuckets`: queries registry rows filtered by `owner` and then:
- if `consumerAddress === owner`: returns all buckets for that owner
- else: filters buckets by the bucket ACL

### Error behavior

- Backends throw `PersistentStorageAccessDeniedError` when forbidden.
- Handlers translate that into **HTTP 403** / `status.httpStatus = 403`.

---

## Features

### Supported today

- **Create bucket**
- Creates a bucket id (UUID), persists it in SQLite with `owner` and `accessListJson`, and creates a local directory (localfs).

- **List buckets (by owner)**
- Returns buckets from the registry filtered by `owner` (mandatory arg).
- Applies ACL filtering for non-owners.

- **Upload file**
- Writes a stream to the backend.
- Enforces bucket ACL.

- **List files**
- Returns file metadata (`name`, `size`, `lastModified`) for a bucket.
- Enforces bucket ACL.

- **Delete file**
- Deletes the named file from the bucket.
- Enforces bucket ACL.

### Not implemented yet

- **S3 backend**
- `PersistentStorageS3` exists as a placeholder and currently throws “not implemented”.

---

## Configuration

Persistent storage is controlled by `persistentStorage` in node config.

Key fields:
- `enabled`: boolean
- `type`: `"localfs"` or `"s3"`
- `accessLists`: AccessList[] — node-level allow list to create buckets
- `options`:
- localfs: `{ "folder": "/path/to/storage" }`
- s3: `{ endpoint, objectKey, accessKeyId, secretAccessKey, ... }` (future)

---

## Usage

### P2P commands

All persistent storage operations are implemented as protocol commands in the handler:
- `persistentStorageCreateBucket`
- `persistentStorageGetBuckets`
- `persistentStorageListFiles`
- `persistentStorageUploadFile`
- `persistentStorageDeleteFile`

Each command requires authentication (token or signature) based on Ocean Node’s auth configuration.

### HTTP endpoints

HTTP routes are available under `/api/services/persistentStorage/...` and call the same handlers. See `docs/API.md` for the full parameter lists and examples.

At a glance:
- `POST /api/services/persistentStorage/buckets`
- `GET /api/services/persistentStorage/buckets`
- `GET /api/services/persistentStorage/buckets/:bucketId/files`
- `POST /api/services/persistentStorage/buckets/:bucketId/files/:fileName`
- `DELETE /api/services/persistentStorage/buckets/:bucketId/files/:fileName`

Upload uses the raw request body as bytes and forwards it to the handler as a stream.

---

## Limitations and notes

- The bucket registry is local to the node (SQLite file). If you run multiple nodes, each node’s registry is independent unless you externalize/replicate it.
- `listBuckets(owner)` requires `owner` and only returns buckets that were created with that owner recorded.
- Filenames in `localfs` are constrained (no path separators) to avoid path traversal.

6 changes: 6 additions & 0 deletions src/@types/AccessList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Mapping of `chainId` -> list of smart contract addresses on that chain.
*/
export interface AccessList {
[chainId: string]: string[]
}
3 changes: 2 additions & 1 deletion src/@types/C2D/C2D.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MetadataAlgorithm, ConsumerParameter } from '@oceanprotocol/ddo-js'
import type { BaseFileObject, StorageObject, EncryptMethod } from '../fileObject.js'
import type { AccessList } from '../AccessList.js'
export enum C2DClusterType {
// eslint-disable-next-line no-unused-vars
OPF_K8 = 0,
Expand Down Expand Up @@ -95,7 +96,7 @@ export interface RunningPlatform {

export interface ComputeAccessList {
addresses: string[]
accessLists: { [chainId: string]: string[] }[] | null
accessLists: AccessList[] | null
}

export interface ComputeEnvironmentFreeOptions {
Expand Down
2 changes: 2 additions & 0 deletions src/@types/OceanNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { C2DClusterInfo, C2DDockerConfig } from './C2D/C2D'
import { FeeStrategy } from './Fees'
import { Schema } from '../components/database'
import { KeyProviderType } from './KeyManager'
import type { PersistentStorageConfig } from './PersistentStorage.js'

export interface OceanNodeDBConfig {
url: string | null
Expand Down Expand Up @@ -139,6 +140,7 @@ export interface OceanNodeConfig {
jwtSecret?: string
httpCertPath?: string
httpKeyPath?: string
persistentStorage?: PersistentStorageConfig
}

export interface P2PStatusResponse {
Expand Down
Loading
Loading