# v0.5 – Contract-Backed ACL for Dedicated Private Sessions (Session-Based Only)

> Status: Design aligned with current mock implementation\
> Scope: **Session-based privacy and key access only** (no task-level ACL)

***

### 1. Purpose and Scope

v0.5 introduces an **on-chain ACL layer** for **private, dedicated sessions**, replacing most router env-based ACL logic for these flows.

Key points:

* **Scope = session only**
  * Keys are derived and authorized **per `sessionId`**, not per task.
* **One-way privacy**
  * A session becomes *encrypted/private* the first time a miner is added.
  * It **cannot** be downgraded back to non-encrypted.
* **Router integration change**
  * Router must stop treating `ENCRYPTION_ALLOWED_LIST` as the primary ACL source for dedicated private sessions.
  * Router must read from the **session-level allowlist** in this module, **cache it**, and only fall back to env in explicit legacy/emergency modes.

This doc describes the **actual mock implementation**:

* State:
  * `sessionAllowedMiners`
  * `sessionAllowedMinerList`
  * `sessionAllowedMinerIndex`
  * `sessionEncryptionEnabled`
* Functions:
  * `addSessionAllowedMiner`
  * `removeSessionAllowedMiner`
  * `getAllowedMinersCount`
  * `getAllowedMiners`

Session ownership is still resolved via `SessionData` (not in this module).

***

### 2. On-Chain State (Current Mock Implementation)

#### 2.1 Allowlist Core

State layout:

* `mapping(uint256 => mapping(address => bool)) public sessionAllowedMiners;`\
  Membership mapping (already existed).
* `mapping(uint256 => address[]) private sessionAllowedMinerList;`\
  Backing array to enable enumeration/pagination (new, private).
* `mapping(uint256 => mapping(address => uint256)) private sessionAllowedMinerIndex;`\
  1-based index for O(1) removal via swap-and-pop (new, private).
* `mapping(uint256 => bool) public sessionEncryptionEnabled;`\
  Session-level privacy flag.

Semantics:

* `sessionAllowedMiners[sessionId][miner]`\
  → `true` if `miner` is currently allowlisted for that session.
* `sessionAllowedMinerList[sessionId]`\
  → backing array to enumerate allowlisted miners.
* `sessionAllowedMinerIndex[sessionId][miner]`\
  → 1-based index into `sessionAllowedMinerList[sessionId]`; `0` means “not present”.\
  Used for **O(1)** removal via swap-and-pop.
* `sessionEncryptionEnabled[sessionId]`\
  → privacy flag for that session:
  * `false` (default): session is not private/encrypted.
  * `true`: session is **private/encrypted**, cannot be turned off.

***

### 3. Functions and Behavior

#### 3.1 `addSessionAllowedMiner(sessionId, miner)`

Purpose:

* Add a miner to the allowlist for a session.
* Auto-enable session encryption on first use.

Key behavior:

* **Ownership guard**:
  * Only the **session owner** (from `SessionData`) can call this.
* **Idempotent**:
  * If `sessionAllowedMiners[sessionId][miner]` is already `true`, function is a no-op (beyond the ownership check).
* **When miner is not present**:
  1. Set `sessionAllowedMiners[sessionId][miner] = true`.
  2. Append `miner` to `sessionAllowedMinerList[sessionId]`.
  3. Set `sessionAllowedMinerIndex[sessionId][miner] = newIndex` (1-based).
* **Auto-encryption**:
  * If this is the *first* miner added for the session:
    * Set `sessionEncryptionEnabled[sessionId] = true`.
    * This is **one-way**: there is no function to set it back to `false`.

Complexity:

* Time: **O(1)** for membership check + append.
* Storage: grows with number of allowlisted miners per session.

***

#### 3.2 `removeSessionAllowedMiner(sessionId, miner)`

Purpose:

* Remove a miner from the allowlist in **O(1)** time.

Key behavior:

* **Ownership guard**:
  * Only the **session owner** (from `SessionData`) can call this.
* Removal pattern (swap-and-pop):
  * Look up `idx = sessionAllowedMinerIndex[sessionId][miner]`.
  * If `idx == 0`, miner not present → no-op or revert (implementation choice).
  * Else:
    * Compute `lastIdx = sessionAllowedMinerList[sessionId].length`.
    * If `idx != lastIdx`, swap:
      * `last = sessionAllowedMinerList[sessionId][lastIdx - 1]`.
      * `sessionAllowedMinerList[sessionId][idx - 1] = last`.
      * `sessionAllowedMinerIndex[sessionId][last] = idx`.
    * Pop the last element from `sessionAllowedMinerList[sessionId]`.
    * Set:
      * `sessionAllowedMinerIndex[sessionId][miner] = 0`.
      * `sessionAllowedMiners[sessionId][miner] = false`.

Notes:

* **Does not** change `sessionEncryptionEnabled[sessionId]`:
  * Once encryption is enabled, the session stays private/encrypted even if list becomes empty.

Complexity:

* Time: **O(1)**.

***

#### 3.3 `getAllowedMinersCount(sessionId) → uint256`

Purpose:

* Return the size of the allowlist for a session.

Implementation:

* `return sessionAllowedMinerList[sessionId].length;`

Use:

* Call this first before pagination to ensure `offset < count`.

***

#### 3.4 `getAllowedMiners(sessionId, offset, limit) → address[]`

Purpose:

* Paged enumeration of allowlisted miners.

Constraints:

* `offset < totalCount` must hold, otherwise the call will revert.
* For an empty list (`totalCount == 0`), **any** call will revert:
  * Callers must always check `getAllowedMinersCount(sessionId)` first.

Usage:

1. Get total:
   * `uint256 total = getAllowedMinersCount(sessionId);`
2. Page through:
   * Ensure `offset < total`.
   * Fetch a page of size `limit` starting at `offset`.

Example:

* `offset = 0`, `limit = 50` to fetch first page.
* `offset = 50`, `limit = 50` for second page, etc.

Complexity:

* Time: **O(k)** where `k` = size of returned page (`limit`).

***

### 4. How Router Uses This ACL (v0.5, Session-Only)

#### 4.1 Privacy Flag

When a router handles a request for a given `sessionId`:

* Check:
  * `isPrivate = sessionEncryptionEnabled(sessionId);`
* If `isPrivate == true`:
  * Treat the session as **private/encrypted**.
  * All `/api/v2/completion` traffic for that session must:
    * Use **offchain payload v2** with encryption.
    * Disable streaming.
  * Key issuance must enforce ACL via `sessionAllowedMiners`.
* If `isPrivate == false`:
  * Session is non-private/plain.
  * Router can treat it as a normal (unencrypted) session unless explicitly configured otherwise.

***

#### 4.2 Key Issuance (`/api/v1/auth/payload_enc_key/session`)

For **miners**:

1. Caller sends:
   * `session_id`
   * `address` (or implied from signature)
   * `signature` over canonical scope string.
2. Router constructs scope string, for example:
   * `"session:" + session_id`
3. Router verifies signature (e.g., `verify_addr_str_message(scope_str)`).
4. Router verifies ACL:
   * If `sessionEncryptionEnabled(sessionId) == true`:
     * Check `sessionAllowedMiners(sessionId, minerAddr)` via the **public getter**.
   * Else:
     * For non-private sessions, router may still support legacy env-based logic if needed.
5. If `allowed == true`:
   * Derive scoped key with current `key_version`.
   * Return key + version to miner.

**Important:**\
Router implementation must be updated so that for **private dedicated sessions**:

* **Primary ACL is the contract**, not `ENCRYPTION_ALLOWED_LIST`.
* Env-based ACL is **optional fallback** and should be guarded by an explicit feature flag.

***

#### 4.3 Router Caching Strategy

Even though `sessionAllowedMiners` and `sessionEncryptionEnabled` are cheap to read:

* Routers should **cache** for hot sessions:
  * `sessionEncryptionEnabled(sessionId)`
  * Membership checks for active `(sessionId, minerAddr)` pairs.
* Cache invalidation/update should be driven by:
  * Events (if implemented), or
  * Periodic polling with TTL.

This is especially important when many miners are requesting keys for the same active sessions.

***

### 5. Interaction With v0 Private / Encrypted Inference

v0 private inference is **router-centric** and uses:

* Session-scoped encryption keys derived from an `ENCRYPTION_SEED`/keyring.
* Auth endpoints:
  * `/api/v1/auth/payload_enc_key/session`
* Offchain v2 encrypted payloads with metadata such as:
  * `alg`, `scope_type`, `session_id`, `key_version`, `nonce`, `tag`, `ciphertext`, `created_at`.

v0.5 changes **who is allowed** to get keys:

* Instead of `.env`-only ACL (`ENCRYPTION_ALLOWED_LIST`), dedicated private sessions use:
  * `sessionEncryptionEnabled[sessionId]`
  * `sessionAllowedMiners[sessionId][miner]`

for authorization.

Env-based ACL can still exist for:

* Non-private sessions.
* Emergency overrides.
* Legacy testing environments.

But **for dedicated private sessions**, the on-chain ACL is canonical.

***

### 6. Dashboard / UX Flow

#### 6.1 Enabling a Dedicated Private Session

Typical flow:

1. User creates a session (`SessionData` + router).
2. Dashboard allows user to **configure a dedicated node address**.
3. When user chooses to **enable private mode**:
   * Dashboard calls `addSessionAllowedMiner(sessionId, dedicatedNodeAddr)` on-chain.
4. Effects:
   * `sessionAllowedMiners[sessionId][dedicatedNodeAddr] = true`.
   * `sessionEncryptionEnabled[sessionId] = true` (if first miner).
   * Dedicated node address appears in `sessionAllowedMinerList[sessionId]`.

Dashboard UX:

* Show a **one-way warning**:
  * Enabling privacy is **permanent** for this session.
  * Session will use encrypted offchain v2 payloads and **no streaming**.
  * Only allowlisted miners can obtain keys to decrypt payloads.

#### 6.2 Managing Allowlisted Miners

* Add a miner:
  * `addSessionAllowedMiner(sessionId, minerAddr)` (owner only).
* Remove a miner:
  * `removeSessionAllowedMiner(sessionId, minerAddr)` (owner only).
* Query size:
  * `count = getAllowedMinersCount(sessionId)`.
* Paginate:
  * Use `getAllowedMiners(sessionId, offset, limit)` with `offset < count`.

Notes:

* Removing all miners **does not** revert `sessionEncryptionEnabled[sessionId]` to `false`.
* Dashboard should surface this:
  * “Once privacy is enabled, this session stays private; you can change which miners are allowed, but not turn privacy off.”

***

### 7. Ownership & Guards

* **Only the session owner** (as defined in `SessionData`) can:
  * Add miners via `addSessionAllowedMiner`.
  * Remove miners via `removeSessionAllowedMiner`.

This keeps the ownership model simple:

* One source of truth for *who controls a session* (SessionData owner).
* Session-level ACL tracks *which miners are allowed* to serve its private workloads.

There is **no separate router-owner** in this module; router identity/ownership is enforced via `SessionData`.

***

### 8. Optional Next Improvements

The current mock implementation already covers:

* O(1) add/remove membership.
* Paged enumeration.
* One-way privacy enable flag.

Potential next steps:

1. **Events (recommended)**

   * `SessionEncryptionEnabled(sessionId, by, timestamp)`
   * `SessionMinerAllowedAdded(sessionId, miner, by, timestamp)`
   * `SessionMinerAllowedRemoved(sessionId, miner, by, timestamp)`

   These allow router processes to subscribe and immediately refresh caches instead of polling.
2. **Batch operations**

   * `addSessionAllowedMiners(sessionId, address[] miners)`
   * `removeSessionAllowedMiners(sessionId, address[] miners)`

   Most helpful when sessions need to add/remove many miners at once.
3. **Combined status view**

   * `getSessionPrivacyStatus(sessionId) returns (bool enabled, uint256 allowedCount)`

   Useful for:

   * Dashboards.
   * Router introspection APIs.
   * Scripts that need a quick privacy snapshot.

***

### 9. Summary

* v0.5 defines a **contract-backed, session-based ACL** for private dedicated sessions.
* Core state:
  * `sessionAllowedMiners[sessionId][miner]`
  * `sessionAllowedMinerList[sessionId]`
  * `sessionAllowedMinerIndex[sessionId][miner]`
  * `sessionEncryptionEnabled[sessionId]`
* Core behaviors:
  * `addSessionAllowedMiner` (idempotent, O(1), auto-enables encryption on first use).
  * `removeSessionAllowedMiner` (O(1) swap-and-pop).
  * `getAllowedMinersCount` and `getAllowedMiners` (paged enumeration).
* Router changes:
  * For **private dedicated sessions**, key issuance must use **this contract ACL**.
  * Env-based ACL becomes **legacy/emergency** only for these flows.
  * Router must be updated to **read from the contract, cache results**, and only fall back to env when explicitly configured.
* UX:
  * Privacy is **one-way per session**.
  * Dashboard aligns with contract behavior and enforces that once `sessionEncryptionEnabled` is `true`, it stays `true`.

This keeps v0.5 privacy **simple, session-scoped, and effective**, while still leaving room for future task-level scoping or more advanced privacy policies in later versions.
