Skip to content
Open
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ databases
.env.*
.git
.github
docs
docs/*
!docs/serviceTemplates
src/test
*.md
*.log
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
pull_request:
branches:
- 'main'
- 'next-4'

env:
DOCKERHUB_IMAGE: ${{ 'oceanprotocol/ocean-node' }}
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ COPY --chown=node:node --from=builder /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=builder /usr/src/app/schemas ./schemas
COPY --chown=node:node --from=builder /usr/src/app/package.json ./
COPY --chown=node:node --from=builder /usr/src/app/config.json ./
# Ship the operator service-on-demand templates so SERVICE_TEMPLATES_PATH=docs/serviceTemplates/
# resolves inside the image (the rest of docs/ stays excluded via .dockerignore).
COPY --chown=node:node --from=builder /usr/src/app/docs/serviceTemplates ./docs/serviceTemplates

RUN mkdir -p databases c2d_storage logs

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ Your node is now running. To start additional nodes, repeat these steps in a new
- [Docker Deployment Guide](docs/dockerDeployment.md)
- [C2D GPU Guide](docs/GPU.md)
- [Compute pricing](docs/compute-pricing.md)
- [Services (Service-on-Demand)](docs/services.md)
1 change: 1 addition & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"validateUnsignedDDO": true,
"jwtSecret": "ocean-node-secret",
"enableBenchmark": false,
"serviceTemplatesPath": "docs/serviceTemplates/",
"dockerComputeEnvironments": [
{
"socketPath": "/var/run/docker.sock",
Expand Down
273 changes: 273 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1819,3 +1819,276 @@ Delete a file from a bucket.
```json
{ "success": true }
```

---

## Service on Demand

Service-on-Demand lets a consumer launch a long-running Docker container (e.g. JupyterLab, a
vLLM inference server, VS Code) on a compute environment, pay up front via on-chain escrow for
a requested `duration`, and reach it over forwarded network endpoints
(`http://<nodeHost>:<hostPort>`) while it runs. Unlike a compute job, a service stays up until
it expires, is stopped, or is extended. See [`services.md`](./services.md) for the full design
and security model.

All routes live under `/api/services`. Every command except `serviceTemplates` is
authenticated by a signature over `consumerAddress` + `nonce` + `command` (or an auth-token
`Authorization` header). Cost is computed only from the environment's server-side pricing and
charged to the authenticated `consumerAddress`.

> **Note:** service containers run hardened (`no-new-privileges`, `CapDrop: ['ALL']`), so a
> process inside the container cannot bind to a port below 1024 — have your service listen on a
> **high port** (the published host port is allocated by the node regardless).

### Service object definitions

#### `ServiceTemplatePublic` (returned by `serviceTemplates`)

Operator-published blueprint. Secret `envVars` values are never returned — only their keys via
`envVarKeys`.

| property | type | description |
| ------------------------- | ----------------------------- | ----------- |
| id | string | template id (`[a-z0-9][a-z0-9_-]{0,63}`) |
| name / description | string | human-readable labels |
| image | string | base image |
| tag / checksum / dockerfile | string | image spec — exactly one |
| exposedPorts | number[] | container ports to forward |
| envVarKeys | string[] | keys of operator-set env vars (values never returned) |
| userConfigurableEnvVars | object[] | `{ key, validation?, sensitive? }` passed via `userData` |
| command / entrypoint | string[] | Docker CMD / ENTRYPOINT overrides |
| requiredResources | object[] | resources the service MUST have to run |
| recommendedResources | object[] | resources for best performance |

#### `ServiceJob` (returned by start / status / extend / restart / stop)

The encrypted `userData` is never returned. Key fields:

| property | type | description |
| ------------- | -------- | ----------- |
| serviceId | string | unique id of the running service |
| environment | string | envId the service runs on |
| owner | string | consumerAddress |
| status | number | `10` Starting, `20` Locking, `11` PullImage, `13` BuildImage, `30` Claiming, `40` Running, `12` PullImageFailed, `14` BuildImageFailed, `15` VulnerableImage, `50` Stopping, `70` Stopped, `75` Expired, `99` Error |
| statusText | string | human-readable status |
| dateCreated | string | ISO timestamp |
| expiresAt | number | Unix ms timestamp when the paid window ends |
| duration | number | requested seconds |
| endpoints | object[] | `{ containerPort, hostPort, url }` per exposed port |
| resources | object[] | `{ id, amount, price }` |
| payment | object | initial start payment record |
| extendPayments | object[] | one entry per successful extend |

---

### `HTTP` GET /api/services/serviceTemplates

### `P2P` command: serviceGetTemplates

#### Description

List the operator-published service templates (sanitized). Not authenticated.

#### Query Parameters

| name | type | required | description |
| ------- | ------ | -------- | ----------- |
| chainId | number | | filter to templates whose envs price on this chain |

#### Response (200)

```json
[
{
"id": "jupyter-cpu",
"name": "JupyterLab (CPU)",
"image": "quay.io/jupyter/datascience-notebook",
"tag": "latest",
"exposedPorts": [8888],
"userConfigurableEnvVars": [{ "key": "JUPYTER_TOKEN", "sensitive": true }],
"requiredResources": [{ "id": "cpu", "min": 1 }, { "id": "ram", "min": 2 }]
}
]
```

---

### `HTTP` POST /api/services/serviceStart

### `P2P` command: serviceStart

#### Description

Validate the request, persist the job, and **return immediately** with the `serviceId` — the
response does **not** wait for escrow or the image pull/build. The consumer supplies the
container spec directly (an `image` referenced by `tag`/`checksum`, or an inline `dockerfile`
when the operator allows building).

The returned job has `status: 10` (`Starting`) and no `endpoints` yet. A background loop then
advances it: `Starting → Locking` (escrow lock) `→ PullImage`/`BuildImage` (image + scan) `→
Claiming` (claim on success, or refund/cancel the lock on failure) `→ Running`. **Poll
`serviceStatus`** until `status` is `40` (`Running`, with `endpoints` populated) or a terminal
`*Failed` / `Error` status.

#### Request Body

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"environment": "env-1",
"image": "nginxinc/nginx-unprivileged",
"tag": "alpine",
"exposedPorts": [8080],
"resources": [{ "id": "cpu", "amount": 1 }, { "id": "ram", "amount": 1 }],
"duration": 3600,
"userData": "<ECIES-encrypted-to-node-pubkey hex>",
"payment": { "chainId": 8996, "token": "0x..." }
}
```

| field | type | required | description |
| --------------------- | -------- | -------- | ----------- |
| environment | string | v | envId to run on (services must be enabled on it) |
| image | string | v | base image |
| tag / checksum / dockerfile | string | | image spec — at most one; `dockerfile` requires `allowImageBuild` |
| additionalDockerFiles | object | | filename → content; only with `dockerfile` |
| dockerCmd / dockerEntrypoint | string[] | | container CMD / ENTRYPOINT overrides |
| exposedPorts | number[] | | container ports to publish |
| resources | object[] | | `{ id, amount }` requested resources |
| duration | number | v | seconds; capped by `serviceOnDemand.maxDurationSeconds` |
| userData | string | | ECIES-encrypted (to the node pubkey) JSON of env vars |
| payment | object | v | `{ chainId, token }` |

#### Response (200)

The immediate response — `Starting`, no endpoints yet. Poll `serviceStatus` for the rest.

```json
[
{
"serviceId": "0x...",
"environment": "env-1",
"owner": "0x...",
"status": 10,
"statusText": "Starting",
"expiresAt": 1735689600000,
"duration": 3600,
"endpoints": [],
"payment": { "chainId": 8996, "token": "0x...", "cost": 10 }
}
]
```

Errors: `403` services disabled on the env / access denied, `400` invalid params (bad address,
duration, image spec, unavailable resources, or no pricing for the token). Escrow lock/claim now
happens in the background, so escrow failures surface as the job ending in an `Error` / `*Failed`
status (observed via `serviceStatus`), not as a synchronous `402`.

---

### `HTTP` GET /api/services/serviceStatus

### `P2P` command: serviceGetStatus

#### Description

Read service job status and endpoints. **Authenticated and owner-scoped** — only services owned
by the authenticated `consumerAddress` are returned.

#### Query Parameters

| name | type | required | description |
| --------------- | ------ | -------- | ----------- |
| consumerAddress | string | v | owner address |
| nonce | string | v | request nonce |
| signature | string | v | signed message (or use an `Authorization` auth-token header) |
| serviceId | string | | filter to a single service; omit to list all owned services |

#### Response (200)

Array of `ServiceJob` (with `userData` stripped).

---

### `HTTP` POST /api/services/serviceExtend

### `P2P` command: serviceExtend

#### Description

Pay to push the service expiry further out. The total remaining duration must not exceed
`maxDurationSeconds`. Re-checks the environment access list.

#### Request Body

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"serviceId": "0x...",
"additionalDuration": 1800,
"payment": { "chainId": 8996, "token": "0x..." }
}
```

`additionalDuration` must be a positive number of seconds.

#### Response (200)

The updated `ServiceJob` (advanced `expiresAt`, new entry in `extendPayments`).

---

### `HTTP` POST /api/services/serviceRestart

### `P2P` command: serviceRestart

#### Description

Recreate the service container (no extra charge), keeping the same `expiresAt` and host ports.
Re-checks the environment service gate and access list; rejected if the service has expired.
Optionally pass `userData` to replace the stored env vars.

#### Request Body

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"serviceId": "0x...",
"userData": "<optional ECIES-encrypted hex; replaces stored userData>"
}
```

#### Response (200)

The `ServiceJob` with a new `containerId` (same `hostPort` and `expiresAt`).

---

### `HTTP` POST /api/services/serviceStop

### `P2P` command: serviceStop

#### Description

Tear down the service container and network and release its resources. Owner-gated.

#### Request Body

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"serviceId": "0x..."
}
```

#### Response (200)

The `ServiceJob` with `status: 70` (Stopped).
Loading
Loading