Skip to content

After 0.8.0: Rebuild protected content on develop as a layered stack (successor of #203–#206) #388

Open

Description

@irzhywau

Parent: #91. This issue is the entrypoint to resume this work.

Goal

Bring the protected-content feature from stack #203→#206 onto the current develop as five reviewable layers. The layers give dKMS and the Runtime clear seams, so each side can change independently and later syncs with develop stay small.

Starting point (re-checked 2026-10-21 on develop@3beb8164)

Collisions, most critical first

  1. Release gate. develop keeps protected content off by default through the protected_content host setting. The feature routes (/api/creator/*, /api/apps/marketplace/*, object buy and publish operations) need the same gate.
  2. Provider wire versions. develop admits providers by schema ID. The feature changes the protect, decrypt, PSSH and payload shapes but keeps the /v1 IDs. Each changed shape gets a /v2 ID, and the Runtime adopts the schema-ID admission from develop.
  3. Provider retirement. The feature removes the key, decrypt, DRM and rights providers. On develop, check-product-data.mjs and publish-release.sh still require them, and they are still optional components. The cutover updates both scripts and gives installed Homes a clear update path (update-hop job).
  4. Coupling. Protected-content logic uses ProviderRegistry and GatewayState directly, and library and gateway call each other. Business logic sits in gateway_provider_proxy.rs, the chain wire is untyped JSON, and the server depends on the custody-provider capsule crate. Every develop change in these areas becomes our conflict.
  5. Carrier limits. develop limits whole-object replies, bounds peer reads, binds trusted routes to their peer, and closes idle relays. Custody spread over 3 nodes and the 360 s provider timeout need proof under these rules.
  6. Supply chain. An installed Home runs only signed-release capsules. develop has one home install profile and adds per-capsule signed records and window_policy. creator and elacity-reader must enter the release lists. The L5 decrypt node is a new deployment role, like the custody host, and needs its own install path.
  7. Phone Home contract (new on develop). Home now has a phone stage (docs/HOME_MOBILE.md). Each first-party capsule loads the shared theme to get the form factor. A sidebar becomes the shared push drawer (elastos-drawer.js), and every control is at least 44 px. home-phone-layout-smoke.mjs keeps small targets at 0 for each measured app, including Marketplace, Library and Elacity Player. The feature's Marketplace screens (buy sheet, listing, Explore, Shops, My listings) and the Creator and Reader capsules predate this contract. The 3 new conflicts come from this phone work: home-gui.js, marketplace-product-layout-smoke.mjs, and shell-connector-sheet.js, which the feature deletes and develop updated for the phone.
  8. Off-topic scope in feat(protected-content): one authority, the Runtime, and no 10% disk floor (ELACITY-2299) #203. The "remove 42 unreferenced public items" commit and the disk-floor edits do not belong to this stack. develop already handles the disk rule.
  9. Process. state.md, TASKS.md, check-wci-alignment.sh and home-entropy-check.mjs no longer exist on develop. Their guards move to check-product-data.mjs and test-behaviour. Dated plan files under docs/audits/ move into issues.
  10. The word "offer". In the Runtime, "offer" means a seller listing. In elacity-web and the v3 contracts, it means a buyer's escrowed bid (createAccessOffer). The Runtime term changes to "listing" before buy offers are added.
  11. Missing scope. Buy offers, listing management and earnings do not exist in the Runtime yet.

Method

  • Rebuild each layer; do not merge or cherry-pick. The 11 source commits each cross several layers.
    1. Each layer starts from the layer below it, and the first layer starts from develop.
    2. Each layer ports its own paths from 44b11207. It keeps develop's infrastructure and three-way merges shared files against base 829a24ad.
    3. Each layer then extracts its seams from the ported code, with the ported tests as the safety net.
  • Sync by forward merge. develop merges into L1, then each layer merges into the next. Published branches are never rebased.
  • Small fixes go straight to develop as separate PRs. Examples are the provider-bridge shutdown idempotence and SLOW_PROVIDER_REQUEST_MS.
  • feat(protected-content): one authority, the Runtime, and no 10% disk floor (ELACITY-2299) #203–feat(protected-content): buy from any live offer #206 close after the new stack opens, with the owner's agreement. Each one links to its successor layer.

Stack

  • L1 feat/pc-bridge → develop: contracts and seams. Behavior does not change, and the gate stays off.
    • Port the changes from feat(protected-content): buy from any live offer #206 to the five elastos-protected-content-* crates and the protect and decrypt provider capsules. They already exist on develop; the change is 30 files, about +7.9k lines.
    • Proposed, under review: federation v2 (Slice 1 in the federation comment) replaces the v1 pool document in the same schema change, so that no second bump is needed.
    • Give each changed wire shape a /v2 schema ID, and use schema-ID admission.
    • Keep the Rights, Custody and Decrypt provider traits, and add a Protect provider trait (protect and media). Add no single-implementation traits.
    • Add typed chain request and answer types in provider-contracts, shared by chain-provider and the server.
    • Move the trusted-issuer and state-root helpers into the custody crate. The server then stops depending on the capsule crate.
    • Add the server module protected_content/ with one route-mount function. The gate loads once at startup, and a test shows that every route refuses while the gate is off.
    • The Runtime owns the capsule operation allowlist. The catalog audit checks each capsule against it.
    • Add capsules/_shared/protected-content-client.js, vendored with its --check guard, and only to the capsules that use it, in the same way as elastos-drawer.js. JSON golden fixtures are shared by the Rust and JS tests.
  • L2 feat/pc-dkms-custody → L1: dKMS and node provisioning.
    • Port custody composition, the custody node store, the provision-custody-node CLI, peer roles, operator control, and the custody host deployment.
    • Port the custody parts of Carrier, the replica changes, and chunked content identity.
    • First, add a characterization test for 3-node custody calls under the new Carrier limits.
    • Do the provider retirement as one commit. It updates check-product-data.mjs, publish-release.sh and components.json, and it has an update-hop proof from the current release pin (now 0.8.0-alpha.23).
    • Proposed, under review: one custody node serves several Runtimes (Slice 2). The same work lets a node release to a decrypt provider on another Runtime for a wallet-only reader, limited to a governance-signed decrypt list (Q2).
  • L3 feat/pc-mint-read → L2: mint and read.
    • Port the Creator, Elacity Reader, Elacity Player and Library capsules, with window_policy, release entries and signed records.
    • Make each capsule fit the phone stage: shared theme, push drawer for sidebars, 44 px targets and 16 px inputs.
    • Port creator channels and Elacity metadata.
    • Break the library↔gateway cycle through the protected_content/ module.
    • Write each protected object through the Library principal-root path, and test that Library never shows it as Blocked (Home cannot start, update or show Recovery with a file over 16 MiB (or 64 MiB total) in Library folders #444).
  • L4a feat/pc-market-port → L3: buy and Explore.
    • First, rename "offer" to "listing", including operation names, fixtures and schema IDs.
    • Port the marketplace directory, listing, buy and on-chain directory modules, the market adoption code, the marketplace capsule and the typed chain-provider operations.
    • Move the buy orchestration out of gateway_provider_proxy.rs.
    • Carry 44b11207 (Inbox approval fix).
    • Rebuild the feature's Marketplace screens on the phone layout from develop: push drawer and 44 px row actions.
  • L4b feat/pc-market-trade → L4a: offers, listings and earnings (new), modelled on elacity-web.
    • Buy offers: createAccessOffer, acceptAccessOffer and cancelOffer, read through offerersOf, with USDC approval.
    • Listing management: sellAccess and withdrawListing.
    • Earnings: sales, unclaimed totals, and withdrawRewards with a simulated "withdraw all".
    • Each chain call is a typed chain-provider operation with a payment-class wallet approval.
    • Transaction and explorer links open through Home's link sheet (Chat and app web links require Home confirmation of the exact destination (0.8.0 F1 gate) #430).
  • L5 → L4b: open Runtime-minted content in external apps such as base.ela.city (native decrypt, agreed in After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400).

Open decisions

Approvals needed (#173)

Until S2 is done, each item below must have an approval recorded in this issue before its PR opens. Every route stays behind the protected_content gate.

  • Integration branch, before L1 (added 2026-10-28): the Custody app and custody discovery between contacts. The items are listed in the Custody app note and the discovery approval request. They are not approved yet.
  • L1 and L2: none. They add no public route, approval type or capability. The Runtime operation allowlist only limits what capsules can do.
  • L3: route /api/creator/channels. Creator gets elastos://object/* publish, publish_progress, roots and stat. Elacity Reader gets open_viewer, read_viewer and close_viewer, which Elacity Player already has.
  • L4a: routes /api/apps/marketplace/{items,listing,channels,channel-access,pay-tokens} and the object operation buy_offer (renamed in L4a). Marketplace gets buy, download_owned_copy, import_runtime_custody and list_runtime_custody. Buy uses the existing payment wallet approval and adds no new approval type.
  • L4b: new Chain provider operations for offers, listing management and earnings. Each one uses the existing payment wallet approval.
  • L5: a new admission class for an external-viewer session (wallet only, one asset, https://base.ela.city only), plus its session and segment routes. This needs a security review (After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400).

Verification for each layer

  • Every layer: just ci-local-prepush, check-product-data.mjs with its tests, vendor-ui-tokens.sh --check, and just test-behaviour, each run once with the gate off and once with it on.
  • L1: protected-content crate tests, shared golden vectors, fake-backend substitution and rejection, and the route-gate test.
  • L2: the custody harness smoke, the update-hop journey, Carrier tests under the new limits, and chain-provider tests.
  • L3: the Creator, Reader, Player and Library tests and smokes, plus home-phone-layout-smoke.mjs at 0 small targets, with Creator and Reader added to its app list.
  • L4a/b: the marketplace tests and smokes (including the phone layout smoke), and the buy, listing and directory gateway tests. L4b adds v3 ABI fixtures and an installed mint → list → offer → accept → withdraw journey.
  • L5: the prototype results in After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400, a refused-case test for each admission rule, and a test that a release to a recipient that is not on the decrypt list is refused.

Acceptance record

  • Candidate: not started.
  • Exercised checks: none yet.
  • Reviews: none yet.
  • Installed release evidence: none yet.
  • Remaining gate: L1 branch from current develop.
  • Next owner: @irzhywau.

Updates

  • 2026-10-28: added the integration-branch line under "Approvals needed (Required: Isolation as promised #173)". It links the approval requests for the Custody app and custody discovery. Nothing else changed.

Activity

  1. added this to the After 0.8.0 milestone on Oct 15, 2026
  2. self-assigned this
    on Oct 15, 2026
  3. andersalm commented on Oct 19, 2026

    @andersalm
    Contributor

    Proposal for review, not a decision, by OMP for Anders (19 Oct: "lay the groundwork for dKMS node operators and the federation working together; people and agents will need to set up and manage this; consider future scaling and decentralization").
    @irzhywau, please review security and architecture, starting with the open questions in §7, especially "who signs a per-object epoch once committees vary". @SashaMIT, please review the names and wording.


    dKMS node operators and federations — design for review

    Design proposal, no product code. Reviewers: Irzhy (security, architecture), Sasha (wording, UX). Parent #91; builds on #388 (L1–L5, "Candidate: not started") and #400. No permanent home: accepted decisions become one comment on #388 and one on #400; docs/PROTECTED_CONTENT.md changes when code lands. Code read at develop 3beb8164.

    Prefixes: contracts/, custody/, pcruntime/ = elastos/crates/elastos-protected-content-{contracts,custody,runtime}/src/; provider-contracts/ = elastos/crates/elastos-protected-content-provider-contracts/; server/ = elastos/crates/elastos-server/src/; elastos-runtime/ = elastos/crates/elastos-runtime/; custody-provider/, chain-provider/ under capsules/. "esp-port" = branch origin/feat/dkms-esp-port (PR #15), spec docs/superpowers/specs/2026-07-14-dkms-tofn-pool-custody-design.md, briefings docs/dkms/deploy/. [A] = assumption or proposal; everything else cites its source.

    0. Summary

    One concept. A federation is the signed custody pool already in code (SignedCustodyPoolV1, contracts/custody_pool.rs:376), extended with operator identity, a revision chain and later multi-party governance. No second registry.

    Today the code supports one operator only: one policy-authority key signs pool, epoch and committee authorization (custody_pool.rs:730-740); 2-of-3 is fixed (:741); every mint uses one configured 3-node epoch (pcruntime/mint.rs:204); each custody node trusts exactly one Runtime issuer for provisioning and release evaluation (custody-provider/src/main.rs:168-208; server/server_infra.rs:540-566); "distinct operators" are free-text labels (server/protected_content_config.rs:613-621).

    First two slices (gate off): (1) real operator identity and a revision chain on the one pool document; (2) one custody node serves several admitted Runtimes. These are the smallest changes that make a multi-operator federation possible; re-sharing, larger thresholds, multi-party governance and payouts follow.

    base.ela.city. Follow #400 comment 6224540651. A Runtime node with the native decrypt provider serves media-player, and the custody nodes check entitlement independently. Who operates that decrypt node is still open (Q6). §6 compares this with Lit.

    1. Roles, trust model, threat model

    Role Identity Holds Trusted for
    Custody node Ed25519 node key and X-Wing custody key (custody_pool.rs:98-107). Its Carrier did:key (server/carrier.rs:1036-1046) is linked to them only by the descriptor One sealed share per object (custody/node_store.rs, custody/node_share.rs:10-23) Checking entitlement itself: its own Chain provider, 2–5 RPC sources, two agreeing finalized answers (docs/PROTECTED_CONTENT.md:124-130). Re-sealing its share only to the authorized recipient (custody/release.rs:49-136)
    Operator Today a free-text label, hashed (protected_content_config.rs:760-767). [A] Proposed: the operator's DID ≥1 nodes and their identity-key backups Running nodes honestly. Never holding two seats on one committee
    Federation governance One CustodyEpochIssuerKeyV1 today. [A] Later: m-of-k governance DIDs The membership document Admitting, rotating and removing nodes. Authorizing re-shares
    Publisher (creator Runtime) Runtime issuer derived from the device key (server/protected_content_runtime.rs:1502-1507; server_infra.rs:561-563) The CEK, only during protect (docs/PROTECTED_CONTENT.md:35-37) Its own content
    Reader Wallet; the EIP-191-signed rights request binds the recipient key (contracts/rights.rs:124-131) The media it receives Nothing
    Decrypt location The reader's own Home, or "an explicitly trusted decrypt node" (#400 comment 6224540651) The CEK for one viewer session, inside protected-content-decrypt (docs/PROTECTED_CONTENT.md:55-57,145-151) Confidentiality of every asset it opens
    Seed / relay / Carrier Identities pinned in the release (#345 decision, 11 Oct) Ciphertext and routing. The seed holds at most one share (#400 body) Availability only. Gate: "Relay, seed, Carrier, Runtime, and unrelated Runtimes cannot decrypt protected content" (PR #15 comment 5287717624)
    Agent acting for a person That person's principal, with a capability token (elastos-runtime/src/capability/token.rs:186-225) Only what its grant allows The same operations and checks as a human (PRINCIPLES.md §7)
    Threat Today Proposed
    t operators collude. Ciphertext is public, so their shares recover the CEK with no rights check t=2. Independence is checked on labels only (custody_pool.rs:758-765). PR #204 crypto review: "Two colluding custody nodes recover the key … a deployment question" Operator DID binding, governance admission, and a public member list so anyone can audit concentration. Raise t once ≥5 independent operators exist (§5). Defending against one party posing as many operators (sybils) is governance's job, not cryptography's [A]
    One dishonest node releases a share to someone not entitled One share is useless below t. Each node signs its own decision (custody/release.rs:74-87) Unchanged
    A lying Runtime or coordinator Each node verifies the wallet-signed request and checks the chain itself (docs/PROTECTED_CONTENT.md:134-138) Unchanged. This property is what allows Slice 2
    Governance key compromised It can add nodes for new mints. Existing committees are pinned in each object's envelope manifest (contracts/custody_envelope.rs:173+) Old committees must co-sign any re-share. Genesis pinned in the release. m-of-k in Phase 1
    Governance key lost Membership freezes, so churn then strands objects Phase 0: an offline copy with a named custodian and a recovery drill [A]. Phase 1: m-of-k
    A node loses its secrets or disk Secrets are plain hex in owner-only files (custody-provider/src/lib.rs:20-23,196-215). No backup and no re-share; losing 2 of 3 "permanently strands the object" (Irzhy, PR #15 comment 5369906640) Back up node identity keys only, encrypted to the operator's recovery key [A]. A restored or replacement node receives a re-share. Shares are never backed up, because a backup would defeat old-share deletion (§3)
    Operator churn Members have valid_until (365 days by default) and a Revoked state (custody_pool.rs:82-107; protected_content_config.rs:699-800). Nothing moves shares Re-share before the departing member's valid_until (§3)
    Decrypt node compromised It sees the CEK of everything it plays: a single confidentiality point for browser readers Prefer the reader's Home. One origin list per decrypt node. TEE optional later (#212). The UI states this trust plainly
    An entitled reader copies the content Browsers keep the media they receive (#400 comment 6224540651) Claim access control only, not DRM
    Cryptography is immature X-Wing draft-06, plain Shamir without verifiable secret sharing (VSS), classical authority signatures, no external review (#48; docs/PROTECTED_CONTENT_CONTRACTS_V1.md:335-343) No public confidentiality claim before #48 (docs/PROTECTED_CONTENT.md:155-156)
    A native provider on a node escapes Providers are plain children under the same OS user (#166; elastos-runtime/src/provider/bridge.rs:1216-1270) Third-party operators only after #166 confinement and an install under its own OS account (#174)

    2. Federation formation and membership

    Document. Today a federation is three artifacts signed by one key:

    • the pool, with at most 64 members (custody_pool.rs:16,303-376);
    • the epoch (custody_epoch.rs:167-172);
    • the committee authorization (custody_pool.rs:482-486).

    [A] The proposed /v2 schema replaces v1. Per #388 L1, there is one schema-ID path, and no v1 validator is kept alongside, because #173 says "no … parallel DTO authority" (#173 body). No installed operator state needs migrating: the installer never creates it (docs/PROTECTED_CONTENT.md:189-197), and the gate is off. The v2 document:

    • Revision chain. revision plus previous_sha256, the pattern of server/collaboration_network.rs:36-44,150-160. The federation id is the hash of the genesis revision.
    • Member identity. Each member adds operator_did, a node DID that signs the node-key/custody-key binding (this closes the descriptor-only link), and an operator DID countersignature.
    • Policy. It declares t/n and the suites. Phase 0 stays 2-of-3 with one governance key (PR dKMS/dDRM foundation on the ESP substrate (inspired from flint-5.0) #15 comment 5321392792).
    • Phase 1 additions. Governance becomes m-of-k: a revision needs m signatures from the previous revision's set. Variable t/n arrives with per-object committees (n>3). The contracts already allow 2 ≤ t ≤ n ≤ 16 (contracts/identity.rs:15,216).

    Admission.

    1. The node generates its keys.
    2. It exports a descriptor signed by the node DID and countersigned by the operator DID.
    3. The operator sends a join request, which is approved in Inbox. This flow is planned: "Act as custody node", request → approve (Protected-content UI/UX parity gaps vs feat/dkms-esp-port: protect any file type, Creator app, open-through-decryption #42 comment 5543206593), and After 0.8.0: Protected content #91 requires owner approval in Inbox.
    4. Governance signs the next revision.

    Removal and rotation. Governance signs a revision that marks the member Revoked or gives it a new custody key. Every committee that contains the member then owes a re-share. Recorded rule: "Pool changes must not silently replace an object's nodes. Replacement requires an explicit re-share and a new signed custody epoch" (PR #15 comment 5321392792).

    Discovery and pinning. This reuses the Community network pattern, so there is no new gossip topic.

    • Pin. The signed release pins the genesis and the governance DIDs, like components.json collaboration_network (expected_network_id, head_cid, trusted_profile_signer_dids).
    • Head. Revisions are signed objects addressed by CID. A Runtime asks known members for their head, for example through the existing operator status (server/node_cmd.rs:14-30). It accepts the highest revision that chains from the pinned genesis.
    • Per object. Each object pins federation_id, the revision and the epoch in its manifest, which already carries the pool, epoch and authorization (custody_envelope.rs:173+).
    • Other federations. Using a non-default federation is an explicit owner choice [A].

    Path to decentralization [A]. Each step is a governance revision plus re-shares. Content is never re-encrypted.

    Phase Operators Governance What may be claimed
    0 Elacity, on 3 hosts in distinct failure domains 1 key, offline backup "Elacity-operated custody". Independent-operator evidence is still missing (#47 comment 5894564195)
    1 ≥3 named independent operators; 3-of-5 once n≥5 m-of-k: Elacity plus operators "Custody across N independent operators", after #48
    2 Several federations; the publisher chooses one at mint Per federation Client choice
    3 Elected or staked seats (sketched for the DAO council in esp-port LIFECYCLE_AND_FUTUREPROOFING.md §2) On chain Not now: "We are not adding staking, election, or consensus in this release" (5321392792)

    3. Key lifecycle and where each step runs

    Step Runs in Today Change
    Mint. Generate the CEK, encrypt the content, Shamir-split over GF(256) (vsss-rs, custody/provision.rs:216-229), seal each share to a node's X-Wing key Protect provider in the publisher's Runtime 2-of-3 t/n read from the epoch (Phase 1)
    Committee Publisher's Runtime (pcruntime/mint.rs:182-206) One configured 3-node epoch for all mints Slices 1–2: committee = the whole 3-node federation. Phase 1: a deterministic choice per object, such as a rendezvous hash of the KID [A]. Who signs per-object epochs is Q1
    Provision (push the sealed share) Runtime → custody provider over Carrier Accepted only from the node's one trusted Runtime (custody-provider/src/main.rs:184-190) Any publisher on the federation's signed allowlist, within a per-publisher quota (Slice 2). The store holds at most 4096 records (custody/node_store.rs:30), so open provisioning would be a storage denial-of-service
    Public pins Envelope manifest: identities, threshold, epoch, CEK commitment, signatures (docs/PROTECTED_CONTENT.md:140-143) — Add federation_id and the revision
    Release to a reader (1) The decrypt provider creates a recipient key. (2) The wallet signs the rights request. (3) The Runtime coordinator signs the operation. (4) Each node checks the chain, signs its decision and re-seals its share. (5) The decrypt provider reconstructs the CEK inside the session (pcruntime/coordinator.rs:476-650; custody/reconstruct.rs:123-149) Nodes are asked one after another (coordinator.rs:495, an await inside a loop). The chain plane checks one fixed issuer (chain-provider/src/main.rs:903-921); the custody plane already checks the issuer the operation declares (provider-contracts/src/custody.rs:388-397) Ask nodes in parallel and use the first t answers. Accept any Runtime issuer for release, rate-limited per issuer (Slice 2, Q2)
    Revoke access On chain (hasAccessByContentId) Effective at the next release. Open sessions run until they expire Bound session length in policy. No browser key release: "An expiring licence around an unchanged asset key cannot provide revocable playback after that key has been retained" (6224540651)
    Re-share on churn t old holders send sealed sub-shares through an untrusted courier to the new holders; the CEK is never assembled (esp-port SCALING_AND_INTEROP.md §2b; a 2-of-3 → 3-of-5 test exists there, LIFECYCLE… §1) Not ported. Node-held shares make it possible: proactive re-sharing needs deletable node-held shares (esp-port spec §14.4.5) After the slices. Verify with hash commitments rather than Feldman, to keep the post-quantum posture (spec §14.7). Honest nodes delete old-epoch shares
    Recover a node Restore identity keys, then receive a re-share from ≥t survivors Not implemented As above

    4. Operator setup and management (people and agents)

    Install. [A] A dedicated custody host: an OS service under its own account, not a role inside an everyday Home and not an extra role on the seed. The composition exists (elastos run custody-provider --with chain-provider; docs/PROTECTED_CONTENT.md:263-275, server/provider_host.rs:44-170); the managed Home profile excludes custody (components.json profile home); custody-provider ships for linux-amd64, linux-arm64 and darwin-arm64 (components.json). Same signed install.sh / elastos update path; deploy/custody-host is simulation-only (docs/PROTECTED_CONTENT.md:277-283). Storage and custody stay separate roles (PR #205 --role). Own account because the seed, running as a NOPASSWD: ALL account, scored 9.2 on systemd-analyze security (#174).

    Management uses the existing operator control. elastos node already has info, peer, status --peer and update --peer --check/--apply over Carrier (server/node_cmd.rs:14-30).

    • Health and upgrades. Both use node status --peer and node update --peer. Status gains custody fields: revision, share count, store headroom, chain-RPC agreement and membership valid_until. No heartbeat topic or new dashboard is added.
    • Upgrade waves. Operators upgrade at most n−t nodes of a committee at a time [A].
    • In the Home. The paired node appears in Services under "Shared from this device" (capsules/services/browser/index.html). It shows the epoch-expiry countdown that Protected-content UI/UX parity gaps vs feat/dkms-esp-port: protect any file type, Creator app, open-through-decryption #42 asks for. Inbox handles join, leave, revision signing and re-share approval.

    One CLI and API for people and agents (PRINCIPLES.md §7). [A] Names are for Sasha. The elastos protected-content-config commands (protected_content_config.rs:77-160) move with no aliases:

    Today Proposed
    provision-custody-node elastos node custody init
    create-policy-authority-key elastos federation propose, elastos federation sign
    generate-custody-composition elastos federation propose, elastos federation sign
    verify-custody-composition elastos federation verify
    • New commands: node custody join-request | backup | rotate-key | leave. Every command is a typed Runtime operation with JSON output and stable error codes.
    • Agent grants (existing token constraints expiry, max_uses, delegatable): custody.status (read), custody.operate (backup, update), federation.propose.
    • Human-only steps. Signing a revision and rotating a key always need the owner's Inbox approval. An agent prepares; it does not sign.
    • Approval needed first. These new capabilities and Inbox types need recorded approval in their issue, per Required: Isolation as promised #173: "no PR adds a public route, an approval type, or an app or provider capability unless its issue records approval".

    Payouts. None in Phases 0–1, consistent with "no staking" (5321392792). A per-release fee or a share of sales would need a change to the Base AuthorityGateway contract, so it is a business decision for Anders.

    5. Scaling and decentralization

    • Keys per node. One share record per object. The cap of 4096 records (node_store.rs:30) needs an indexed store before about 4,000 assets per node. The esp-port spec estimates "~2 GB per node per million assets" (§14.3, not measured). With per-object selection, the load per node is about assets × n / N [A].
    • Release rate. One release per viewer session, not per segment. Each release costs t × (one chain read over 2–5 RPCs + one X-Wing decapsulate and re-seal). The chain reads are likely the bottleneck [A]. Throughput, startup time and delivery cost are not measured; After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400 requires those measurements on the one-asset proof, so no numbers are claimed here.
    • Caching. Allowed: verified federation revisions, and ciphertext on the normal content path. Not allowed: the CEK beyond one session, or negative rights results. A positive rights result may be cached for at most one finality window [A].
    • Availability. An object opens while ≥t of its n nodes are up. Assuming independent per-node uptime of 0.99 [A], 2-of-3 gives 0.99970 and 3-of-5 gives 0.99999 (binomial arithmetic). Raise t together with n; "almost never want 2-of-12" (esp-port SCALING_AND_INTEROP.md §1).
    • SLO. [A] Proposed for Phase 1: 99.9% of release attempts succeed within the provider timeout, per federation. The 360-second timeout still needs proof under the new Carrier limits (After 0.8.0: Rebuild protected content on develop as a layered stack (successor of #203–#206) #388, collision 5).
    • Sharding. The federation is the shard unit: at most 64 members per pool, and t/n of at most 16. Within a federation, per-object committees spread the load.
    • Decentralization. No operator holds two seats on one committee, enforced by operator DID. The seed holds at most one share (After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400). Publishers choose the federation. Readers choose where decryption happens, with their own Home first. Only the default federation is pinned.

    6. base.ela.city, and what replaces Lit

    Flow (#400 comment 6224540651):

    1. An asset-scoped external-viewer session signed by the wallet only is admitted for https://base.ela.city, under origin policy.
    2. A Runtime node with the native decrypt provider coordinates release from the federation. Each custody node checks Base AuthorityGateway.hasAccessByContentId itself.
    3. A DASH adapter feeds clear fMP4 to media-player, with session-authorized segment reads. The fallback is elacity-player.
    4. The seed routes and holds at most one share (After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400 body).

    Also tracked under #388 (6224540651):

    • ERC-1271 contract wallets.
    • A clear "access pending" state while Base finality completes. The body says this takes about 15–20 minutes; the existing finality rule is accepted for the prototype.

    Excluded. Option C (dual wrap with Lit) is excluded by both Irzhy and Anders (#400).

    Lit Chipotle (base.ela.city today, per #400) ElastOS dKMS federation
    Key root One Intel TDX enclave on Phala, "with its keys gated by smart contracts on Base" (Lit docs). Lit moved "from threshold cryptography to TEE-based execution" (Lit blog, 26 Feb 2026) t-of-n Shamir across operators with distinct DIDs. No party holds a whole key at rest
    Who sees the CEK at open The Lit TEE (#400, Option C) The decrypt provider in the reader's Home or a named decrypt node
    Rights Lit reads on-chain permissions on Base Each custody node reads AuthorityGateway itself, with two agreeing RPCs out of 2–5
    Post-quantum Not stated in the cited Lit pages [A] X-Wing hybrid share wrapping, review pending (#48)
    Control Run by Lit; API keys and LITKEY payments; the vendor sets network migrations (Datil sunsets after Chipotle launches, same blog post) Elacity first, then independent operators through signed revisions
    Formats DASH with a Lit PSSH; ddrm-reader uses encryptedDataCid (#400) CENC fMP4 with an elastos PSSH (PR #204). The DASH adapter is not yet proven
    Honest weakness Trust rests on the TEE vendor and the enclave Trust rests on the decrypt node for browser readers. Operator independence is still on paper

    Migration. Lit content stays on Lit until it is re-minted. New Runtime mints go to the federation, and no dual-custody bridge is built.

    7. Mapping to #388, first two slices, open questions

    #388 layer Federation and operator work
    L1 contracts and seams Federation v2 document (Slice 1) and golden vectors
    L2 dKMS and provisioning Start with #388's own first item, the 3-node Carrier characterization test. Then Slice 2. Then the custody host install, parallel dispatch, the node CLI move, backup and re-share
    L3 and L4 Unchanged. Mint takes its committee from the federation
    L5 Decrypt node and external-viewer admission (#400)

    Slice 1 (L1, gate off): operator identity and revision chain on the one pool document.

    • Change: v2 replaces v1. It adds operator_did, node-DID-signed key binding, operator countersignature, revision and previous_sha256. It keeps one governance key and a policy-declared 2-of-3.
    • Changes validate_custody_epoch_against_pool_at and the ceremony CLI only.
    • No Runtime wiring and no new capability.
    • Tests that fail before and pass after. The validator rejects:
      • one operator DID in two committee seats under different labels;
      • a revision that does not chain from the previous one;
      • a member whose node DID did not sign its custody key.

    Slice 2 (L2, gate off): one custody node serves every admitted Runtime.

    • Replace the single trusted-runtime-issuer file (custody-provider/src/lib.rs:71,126) with the pinned federation.
    • Provisioning is accepted from Runtime issuers on a governance-signed publisher allowlist in the federation document, within a per-publisher quota.
    • Release is authorized by the wallet-signed request plus the node's own chain evidence. The operation's declared issuer is kept for accountability and per-issuer rate limits.
    • No new approval type or capability.
    • Test: two Runtimes with different issuers share three node processes over real Carrier, not the loopback mock (server/protected_content_runtime/tests.rs:9827-9862). Both mint and release, including with one node stopped. Before the fix, the second Runtime is rejected.
    • This closes a gap that the code hides today. The two-Runtime test passes only because chain evidence is stubbed (tests.rs:9864-9937). On a real node, the chain plane would reject a second Runtime's operations (server_infra.rs:540-566). This is an inference from the code and has not been exercised.

    Open questions for Irzhy

    1. Who signs a per-object epoch once committees vary? Today the epoch issuer must equal the pool issuer (custody_pool.rs:732-734). If the publisher signs, nodes must verify that committee ⊂ pool and that the selection rule was followed.
    2. Is "wallet-signed request + each node's chain check" enough to authorize release from any Runtime, with the Runtime signature used only for accountability?
    3. Re-share: hash-commitment checks on GF(256) Shamir, or move to VSS? Is deletion of old shares by honest nodes an acceptable security basis?
    4. Committee selection: rendezvous hash, or esp-port's sorted-first max_n, which puts all load on the same nodes? What default caps?
    5. Governance signatures: Ed25519 m-of-k, or settle post-quantum authority signatures first (External / PQ cryptographic review of the protected-content contracts and custody crates #48 comment 5894564840)?
    6. Who operates decrypt nodes for browser readers? Is a TEE (Hosted Home: run in a confidential VM that the user's client verifies by attestation #212) required before Phase 1?
    7. Backup format for node identity keys and the governance key, and who holds the recovery keys.
    8. Should the operator DID be the operator's Home profile DID or their wallet address?

    For Sasha. The user-facing word for "federation". The Services card and Inbox steps for join and re-share. How readers learn where their content is decrypted. The command names in §4.

    Record vs code, found while reading.

    8. What not to do

  4. irzhywau commented on Oct 21, 2026

    @irzhywau
    CollaboratorAuthor

    @andersalm, here are my answers to Q1, Q2 and Q8 in §7. The approvals that #173 requires are now listed in the issue body.

    • Q8, operator DID: use the operator's Profile DID. It identifies a person, the same way for humans and agents (PRINCIPLES §7), and the Recovery Kit keeps it when a device is replaced. A wallet address is a payment key and cannot be rotated, so using it would mix governance with funds. A payout address can be added later as a separate binding signed by the Profile DID.
    • Q1, per-object epoch: no party signs it. Governance signs the selection rule and its parameters in the federation revision. The committee is then a fixed function of that revision and the KID (rendezvous hash), and the publisher's signed manifest pins both values. Each node computes the committee again and refuses a share that is not for it. A publisher can choose a KID to get a particular committee, but that only weakens its own content. Slice 1 does not change: one 3-node committee, and the epoch issuer is the pool issuer.
    • Q2, release from any Runtime: this is not sufficient by itself. A node seals its share to the recipient key in the signed request (custody/release.rs only checks that the key matches). So any entitled wallet can name its own key, rebuild the CEK with a short script, and continue to decrypt the public ciphertext after its right ends. That breaks the rule in After 0.8.0: Open Runtime-minted protected content in external apps such as base.ela.city (L5 of #388, options) #400 that no reusable key leaves a decrypt provider. Today the only control is the single trusted Runtime issuer on each node.
      • Proposal: keep the wallet signature and the node's own chain check for entitlement. Also require that the release operation's Runtime issuer is on a decrypt list that governance signs in the federation document (trusted decrypt nodes and admitted Homes). If an issuer leaks a key, revoke it.
      • Without a TEE (Hosted Home: run in a confidential VM that the user's client verifies by attestation #212), a person who controls an admitted Home can still extract a key. The list does not prevent that, but it makes it attributable, and it stops the simple script case.
      • Keep the rate limits per issuer and per wallet. Check the signatures, which cost little, before any chain read.
      • Open: whether the Home of each buyer needs admission, or only decrypt nodes.
  5. irzhywau commented on Oct 27, 2026

    @irzhywau
    CollaboratorAuthor

    Progress: integration branch, before L1

    This is a status note. The work is on the local branch feat/pc-integration; it is not pushed yet, and no PR is open. It contains two commits on the integration reference 665d35ac.

    1. a5b30bf5: fresh-Home onboarding, release timing and replica placement fixes

    • A fresh Home installs the Base chain configuration when protected content is on.
    • The first EVM account becomes the transaction default. A Base default wins; otherwise the Wallet's EVM default serves Base. A default that names an account on another chain fails closed.
    • The protected-content error messages are clearer.
    • The first read tolerates slow nodes: custody nodes are asked in parallel, and an attempt that is close to expiry restarts.
    • Replica placement no longer sends oversized Carrier lines.

    2. c872c82d: Custody app. A pool owner forms the dKMS custody pool from Home instead of six CLI commands.

    • Shared code. The protected-content-config custody handlers are now shared functions. The CLI and the new routes call the same code. The Runtime loader stays the single validation authority, and the pool file stays create-only.
    • Node card. A node card is one line of text: the exact node details, the Carrier ticket, the roles and the kubo peer id. Refusals have reason codes, and untrusted sizes are bounded.
    • Card fetch over Carrier. A new operator action, custody.node_card.read, is answered only to the DID of the Runtime issuer that the node trusts.
      • An allow list cannot grant it.
      • Strangers get the same errors and audit entries as before.
      • The owner checks that the card names the node it dialled.
      • A pasted card stays as a fallback.
    • Live member health.
      • Each member is probed in parallel, with a 3 s connect and a 5 s limit per member.
      • Each answered node key is compared with the installed pool.
      • The pool is healthy, at risk or down against its threshold.
      • The view shows "Pool active" only after a fresh healthy answer.
    • Full Homes as nodes.
      • A full Home's card offers storage with its own kubo peer id, so a pool of three Homes gets its replicas.
      • Hosting shows this Home's node address.
      • On a node Home, Hosting shows whom it serves and when the owner last checked it.
    • Routes and app.
      • Eleven admin-only operations under /api/apps/custody/*, one per capsule method.
      • "Include this Home" and "Sign and install" need a passkey step-up. Cheap refusals come first, and one lock serialises the writes.
      • The routes refuse while protected content is off.
    • Clipboard. Copy uses Home's clipboard bridge. A new write-only custody purpose allows only resource identifiers and full sha256: fingerprints.
    • Creator. Creator refuses a file type that Runtime cannot protect when the file is picked.

    Verification

    • Gate:
      • cargo fmt and clippy (-D warnings) pass.
      • cargo test -p elastos-server passes, with 3,325 + 220 tests and the provider test binaries.
      • The Custody, Home clipboard and Creator node tests pass (158).
      • check-product-data and vendor-ui-tokens --check pass.
      • The installed custody provider is verified.
    • Reviews:
      • Every task had a task review and fix rounds.
      • Two whole-change reviews found no Critical or Important findings.
      • The cross-family adversarial review that AGENTS.md requires for custody, signing and gateway auth is still to do.
    • Live checks:
      • A real Home with the 3 Docker custody nodes: the nodes upgraded and re-provisioned on boot with the same identity. Health showed "at risk" with one node stopped, and healthy after the restart.
      • Three independent Homes: one owner and two full-Home nodes, plus "Include this Home". The pool formed and was signed. Custody escrow succeeded for a mint. The first publish found too few replicas, which led to the storage fix above. A full mint, buy and open journey on these Homes is still to do.

    Approvals needed (#173) before a PR opens

    • Routes /api/apps/custody/* and the custody capsule with its interface methods.
    • Passkey step-up operations custody.local_node.include and custody.pool.install.
    • Operator action custody.node_card.read.
    • Home clipboard purposes custody.identifier and custody.fingerprint.

    Next

    1. Custody discovery and consent, contacts first. The plan is under review. Homes offer custody to their People contacts. The owner finds them, a node approves the request in its Inbox with a passkey, and the approval returns the node card. After signing, members receive a signed "Pool active" notice. It adds a contacts-only collaboration-custody message plane, a custody signing scope and an Inbox approval type. These need Required: Isolation as promised #173 approval too. A Community-wide offer list comes later on the same messages.
    2. k-of-n. Discovery is count-agnostic. Pools larger than 3 need contract, mint and release changes in L1/L2.
    3. The full mint, buy and open journey on three independent Homes.
    4. L1 on top of these commits.
  6. irzhywau commented on Oct 28, 2026

    @irzhywau
    CollaboratorAuthor

    Approval request: custody discovery and consent, contacts first

    This adds to the earlier progress note. The work is on the local branch feat/pc-integration. It is not pushed yet, and no PR is open.

    What it does

    • A Home offers custody to its People contacts in Custody › Hosting.
    • A pool owner uses Find nodes to see which contacts offer custody, and asks one for a share. The request names the term and the owner's pool signing key.
    • The node's admin approves in Inbox with a passkey. The approval returns the node card, which fills the share slot with no pasting.
    • After the pool is signed, the owner sends each member a signed "Pool active" notice. The node keeps it only when the pool is signed by the key its admin approved and names this node at its share.
    • Everything travels as signed messages between accepted contacts only. A node consents to one term and one signing key; another term or key asks the admin again.

    Approvals needed (#173) before a PR opens

    • Provider target collaboration-custody, with the ops query (offer query, answered at once) and deliver (request, decision and pool notice, answered with a signed receipt).
    • Profile signing scope custody. It is opt-in, never a default scope, and granted with a passkey. Its payload types:
      • elastos.custody.offer-query/v1
      • elastos.custody.offer/v1
      • elastos.custody.node-request/v1
      • elastos.custody.node-decision/v1
      • elastos.custody.pool-active/v1
      • elastos.collaboration.acceptance-receipt/v1, for the receipts
    • Routes (Custody, admin only, refused while protected content is off):
      • GET /api/apps/custody/offer
      • POST /api/apps/custody/offer
      • POST /api/apps/custody/messages/enable
      • POST /api/apps/custody/nodes/find
      • POST /api/apps/custody/node-requests
      • POST /api/apps/custody/pool/notify
    • Capsule methods in custody:
      • custody.offer.read
      • custody.offer.set
      • custody.messages.enable
      • custody.nodes.find
      • custody.node_requests.send
      • custody.pool.notify
    • Inbox approval type. These use the existing POST /api/apps/inbox/actions:
      • Actions custody-approve-request:<request id> and custody-decline-request:<request id>.
      • Notification kinds custody_node_request, custody_node_approving, custody_node_resumable, custody_node_incomplete and custody_node_taken.
      • The informational kind custody_pool_active.
    • Passkey step-up operations:
      • custody.node_request.approve, which approves exactly the request the node recorded.
      • custody.messages.enable, which grants the signing scope.
    • New owner-only files under protected-content/:
      • On a node Home: custody-offer.json, custody-node-requests.json, custody-served-consent.json and custody-served-pool.json.
      • On an owner Home: custody-node-requests-out.json and custody-pool-notices.json.

    Privacy

    • Offering custody shows this Home's node status (Run by, Location and roles) to all accepted contacts. The switch is off by default.

    Known limits

    • Contacts only. A Community-wide offer list comes later on the same messages.
    • Notice size. A "Pool active" notice carries the whole signed composition, with a 40 KiB limit. Pools of up to 9 members fit. Larger pools need a smaller proof in L1.
    • Pool shape. Runtime pools stay 2-of-3 until L1. Adding a node to a live pool comes later.
    • One custody role per Home. A Home that owns an installed pool cannot also serve another owner yet.

    Verification

    • Reviews: every task had a review and fix rounds. A whole-change review found no Critical findings. Its two Important findings were both about term consent: the owner could not re-ask a node for a new term, and the node did not check the pool's term. Both are fixed: the node now refuses a pool longer than the term its admin approved (term_mismatch).

    • Full gate before the final fixes:

      • fmt and clippy (-D warnings);
      • cargo test -p elastos-server with the provider test binaries: 3,657 passed, 0 failed;
      • Custody JS (89), the Inbox smoke, check-product-data and vendor-ui-tokens --check.
    • After the final fixes: the focused tests pass, including end-to-end runs over Carrier:

      • find, ask, approve with the card, install, and the "Pool active" notice reaching the node;
      • a re-ask for a new term that the node's admin approves in Inbox.

      The full gate re-run and a scoped re-review are in progress.

    • Still to do:

      • the cross-family adversarial review that AGENTS.md requires for signing and gateway changes;
      • the live journey on three fresh Homes.
  7. irzhywau commented on Oct 30, 2026

    @irzhywau
    CollaboratorAuthor

    Plan update: custody pools as a service ("Plan B")

    The protected-content integration branch is now rebased on the latest develop as a single commit. Develop's 0.8.0 rules win:

    • the file service is offline;
    • files move between Homes only over Carrier;
    • protected apps are opt-in through setup --with.

    Because of that, a mint now needs one verified copy, on the creator's Home. Before we split the branch into layered PRs, we are making custody and dKMS fully functional.

    Goal

    • The content key exists only inside the custody pool. Runtimes are treated as untrusted, including the minter's and the buyer's.
    • Any Home can use another node's pool, both to mint and to play.
    • One node can serve several pools, each with its own keys.

    How it works

    • Play:

      • the buyer's Wallet signs one rights request naming a pool server and a one-time viewing key;
      • custody nodes check finalized chain rights and the server's pool membership;
      • the nodes release their shares to that server only;
      • the server decrypts each part and seals it to the viewing key.

      The buyer's Home never holds the key.

    • Mint: the minter's Home sends sealed parts to a pool sealer. The sealer encrypts, provisions the shares and publishes, and the minter gets signed receipts only.

    • Pool rules live in a signed pool descriptor and can change without code: servers per session, no serving or sealing for a member's own Home, who may mint, and quotas.

    • Existing pools, items and listings keep working. The current pool becomes pool chore(cve): runtime CVE hygiene — 32/34 closed (94%), wasmtime 17→36, 207-line Cargo.lock reduction #1.

    Milestones

    • M0: one registry for every new route, op and capability (so the Required: Isolation as promised #173 approval record is a one-place change), and the copy-count split. No behaviour change.
    • M1: node release gate, pool session service, and play from any Home.
    • M2: multi-pool nodes, and minting through a pool sealer.

    Each milestone ends with a live check on a set of demo Homes. Each stage is one commit, so the later split has clean cut points.

    Out of scope until after the split

    • trust proof (TEE later; the interface seam stays);
    • pool discovery v2;
    • k-of-n and seed pools;
    • ciphertext copies on pool members, which need a change to the replica rules.

    For review (Anders), each with a path inside today's rules:

    1. Sealed viewer parts and sealed mint parts crossing Carrier are treated as "sealed session material" (CARRIER.md). Is that wording acceptable?
    2. A pool sealer writes its own any-peer publication record for the ciphertext it publishes, the same rule a creator's protected listing uses. If that doesn't count as the sealer Home's own publication, minting through a pool needs a rule change.
    3. SECURITY.md and the crypto review need new wording before any public-pool claim. This does not block M1 or M2.

    Every new Carrier target, op, route, step-up op and message type will be listed in the #173 approval record when the PRs open.

  8. irzhywau commented on Nov 5, 2026

    @irzhywau
    CollaboratorAuthor

    Public metadata after 2.0: four open questions

    Context. Since 2.0, each Home's IPFS runs offline, so a new mint's public description never leaves the creator's Home. That includes the title, description, cover and the metadata.json that the on-chain tokenURI points to. The indexer behind the Marketplace catalog cannot fetch it. It records the item with an empty name and no content id, and the Marketplace drops it. Two mints made today on a demo Home show exactly this: they are on chain and in the index, with name: null and metadata.kid: null.

    Rule adopted for this work. All metadata must always be public and available for external sync: metadata.json, the per-token ERC-1155 JSON, content.json and the like, plus covers. Core data stays private: ciphertext, keys and private files. Each Home's IPFS stays offline. At mint time, the Home sends only the public files over Carrier to a seed, which stores them and serves them publicly. We checked a protected mint's outputs: the tokenURI folder holds only public JSON (no key material), the cover is a separate file, and the ciphertext folder never leaves the Home.


    Q1. Consent: who agrees that these files go public? BLOCKING (design, and shipping)

    SECURITY.md says a seed accepts no new copies from other Homes. This work needs a narrow exception for public metadata and covers only. Two parties are involved:

    • Creator: a one-time approval in Creator on the first mint ("your title, description and cover will be published publicly so the marketplace can list your item"). It is remembered, can be changed in settings, and declining stops the mint before the wallet step.
    • Seed operator: a seed setting, "accept public metadata from minters", off by default. This is the same model as the content.replica grant, where the receiving owner decides.

    The seed accepts only an allowlist of metadata files and a raster cover, under size caps. It requires an exact CID rebuild and a matching on-chain mint (the content id bound to the token, and tokenURI equal to the bundle's CID).

    Proposal: both consents. Needs Anders' approval for the exception to SECURITY.md (seed copies), docs/CONTENT_AVAILABILITY.md and the "publish stays local" rule. It also needs a #173 record for the new seed-only Carrier op content/publish_public_metadata.

    Q2. Which node is the seed? BLOCKING (it is the target of the transfer)

    • elastos.elacitylabs.com: the ElastOS seed the code already uses by default. It serves files by CID.
    • base.ela.city: the web app and indexer. Its /ipfs/… path answers with the web app's HTML, so it is not an ElastOS gateway.

    Proposal: elastos.elacitylabs.com, unless the production seed is elsewhere.

    Q3. How does the indexer get the files from the seed? BLOCKING end to end (not blocking the Runtime-side work)

    The indexer reads metadata only from its own IPFS node, through the public IPFS network. The seed's IPFS is offline under the same 2.0 rule, so the files on the seed alone don't reach it.

    • 3a. The indexer also reads the seed's public CID gateway and verifies every file against its CID. This fixes the catalog and server sync with no rule change. It is a change in the indexer's repo, not here.
    • 3b. The seed's IPFS announces these public files on the IPFS network, so ipfs://… resolves for anyone: wallets and other indexers. This changes the 2.0 offline rule for the seed, so it needs Anders.

    Proposal: 3a now, and 3b for Anders together with Q1.

    Q4. What if the seed can't be reached at mint time? BLOCKING (it shapes the Creator flow)

    The files go to the seed just before the wallet signature, while nothing is on chain yet.

    • Block: Creator stops with "Couldn't publish the public details. Retry." No mint can ever exist without public metadata.
    • Continue: mint anyway and retry in the background. The item stays invisible until a retry succeeds, which can take long if the creator's Home goes offline.

    Proposal: block with Retry.


    Plan once these are answered

    1. Public metadata (this work):
      • one registry row for the new op, with approval pending under Required: Isolation as promised #173;
      • in Creator: the consent prompt and a publish step before the wallet, with Retry;
      • on the seed: admission checks, a provisional hold, then confirmation against the chain, then public serving;
      • the creator's Home also shares the metadata and cover with other Homes, which it does not do today;
      • chunked transfer for larger covers;
      • fix the resale metadata file that is never written today;
      • backfill today's mints;
      • tests for every refusal case;
      • the indexer change (3a) in its own repo;
      • 3b if approved.
    2. Small fixes:
      • harden the mint receipt check: one bad RPC source must not cancel a good mint, and the confirmation setting gets a minimum;
      • the Player resumes by itself once chain finality arrives;
      • the earlier follow-ups: the listing stall, two smokes, test fixtures and cache keys.
    3. Rebase onto the latest develop, which now includes Home session renewal and the Jetson Browser work. Same process as before: backup, classified conflicts, full gate.
    4. Custody pools (Plan B), M0 → M1 → M2, with a live check after each milestone:
      • M1: play through another node's pool;
      • M2: multi-pool nodes and minting through a pool sealer.
    5. Whole-branch review, then the split into layered PRs, with the Required: Isolation as promised #173 approval record listing every new route, op and capability.
  9. irzhywau commented on Nov 5, 2026

    @irzhywau
    CollaboratorAuthor

    @andersalm your input is needed on the comment above (#388 (comment)):

    • Q1: a narrow exception to "a seed accepts no new copies". It covers public metadata and covers only, off by default, and accepted by both the creator and the seed operator.
    • Q3b: letting the seed's IPFS announce those public files, so tokenURI resolves for anyone.

    Q2 and Q4 are open too, if you have a view.

  10. andersalm commented on Nov 5, 2026

    @andersalm
    Contributor

    @irzhywau, here are Anders's answers (5 Nov) to your four questions. Thanks for the clear write-up, and for checking that the tokenURI folder holds only public JSON.

    The direction behind the answers

    • A file's CID is its one name. Apps use elastos://; IPFS is one availability backend behind elastos://content, and apps never call it. ipfs:// appears only at the edge with the outside world, such as the tokenURI. Private or public is a policy on each object, not a separate network.
    • ela.city's backend becomes an ElastOS node, the Runtime with capsules, instead of a separate stack beside ElastOS. Minting from a Home and ela.city then share one system: the chain is the truth, the index is a convenience copy, and ela.city and the ElastOS Marketplace are two views of the same items, as in your 24 Sep design note.
    • Every ElastOS Runtime keeps its IPFS offline, this node included. The one online IPFS node stays ipfs.ela.city, which is already public.

    Q1. Who agrees that the files go public?

    • Creator: in the mint approval itself, not a separate setting. The Inbox approval for the mint says that the title, description and cover become public; declining means not minting. The mint already puts the tokenURI on chain for good, so one approval covers both.
    • Node operator: Elacity, through the node's admission policy, off by default. This is open admission under rules, not the per-peer content.replica grant: the metadata allowlist and one raster cover, size caps, a quota per uploading Home, an exact CID rebuild, uploads that no mint binds are deleted after a day, and takedown.

    Q2. Which node?

    A new ElastOS node run by Elacity in a public availability role: not the release seed, and not the base.ela.city web server. The release seed keeps "accepts no new copies", so uploads can never affect updates, and SECURITY.md gains a paragraph for the new role. Build and test the role on any node; the host is a deployment choice, and Anders names who runs it.

    Q3. How does the indexer get the files?

    • The node serves a bundle once the mint transaction is on chain and binds its CID (the tokenURI equals the bundle CID), and imports it into ipfs.ela.city through that node's existing authenticated import. The indexer reads ipfs.ela.city as today, so it needs no new source, and ipfs:// resolves for wallets and other indexers. That gives 3b's result without putting any ElastOS node on public IPFS.
    • Finality decides retention, not serving. The indexer fetches after 12 confirmations, so waiting for finality would leave every new item blank.
    • The indexer already retries a fetch. What it lacks is a refresh of items stored with an empty name; add that, then backfill today's two mints.

    Q4. What if the node can't be reached at mint time?

    Block with Retry, before the wallet step, as you propose.

    Notes on the plan

    • Done means: a mint from a Home shows on base.ela.city and in the ElastOS Marketplace with its name and cover, and its ipfs:// link resolves on a public gateway.
    • The metadata.json on develop lacks the image, media and publisher fields the indexer reads. Your branch writes the full set, so that lands with this work.
    • Before adding content/publish_public_metadata, check whether the existing replica path (ensure and import_exact) with a public policy and this admission rule covers it: fewer ops and one Required: Isolation as promised #173 row.
    • SECURITY.md and docs/CONTENT_AVAILABILITY.md (this direction) change in the same PR, and the node role goes into the Required: Isolation as promised #173 approval record.
    • Later, an index capsule on the same node can serve both storefronts. The current indexer stays until the capsule does the same job; that is not part of this step.
  11. andersalm commented on Nov 6, 2026

    @andersalm
    Contributor

    Anders, 6 Nov: Elacity will host the ela.city ElastOS node on Google Cloud, where ela.city's other servers already run, when the time is right. No date yet. Until then, the public availability role can be built and tested on any node, as in the answers above; the host is a deployment choice.

  12. irzhywau commented on Nov 10, 2026

    @irzhywau
    CollaboratorAuthor

    Public metadata for Home mints: new direction (design approved, not built yet)

    The problem. A protected mint from a Home puts ipfs://<folderCID>/metadata.json on chain. The folder and the
    cover live only in the Home's IPFS store, which runs offline. A marketplace that reads the tokenURI cannot fetch
    them, so the item shows blank, as the two mints of 2026-11-05 do.

    The decision. We don't rely on Elacity's upload API or on its IPFS node as a service. The Home publishes the
    metadata to the IPFS network itself, under its own CIDs. How a marketplace reads it is the marketplace's side.

    Phase 1 (next to build)

    • Two stores. Each Home gets a second IPFS store that holds only public objects. The existing private store
      stays --offline in every mode and never shares a repo, port or process with the public one.

    • Consent first. The first time the Creator opens, the Home admin is shown what will become public, and
      enables the store with a passkey. The notice says:

      • only the metadata and cover of minted assets are shared, never the asset itself;
      • the metadata goes public just before the wallet step, so it stays public even if a mint is abandoned;
      • the IPFS network records the Home as the provider;
      • copies may outlive a later Stop.

      Without consent, new mints stay locked. "Stop sharing metadata" is always available, and unfinished mints can
      still be resumed.

    • At mint, before a new wallet approval:

      1. The Runtime exports the metadata folder and cover from the private store.
      2. It validates every block: the exact root, the hashes, full reachability and only the allowed files.
      3. It imports them into the public store, then pins and announces them, waiting for the announcement to
        finish.

      A failure blocks with Retry before the wallet. A mint whose approval already exists settles without touching
      the public store.

    • A narrow surface. The public store has no HTTP gateway and accepts only that validated import. No app can
      reach it.

    • Peers. There are two editable lists with release defaults:

      • preferred peers, kept connected: Elacity's IPFS node for now;
      • bootstrap peers, used only to find the network: the standard public IPFS bootstrap nodes.

      A new Home therefore joins the network without depending on any single node.

    • No change on chain. The CIDs on chain stay the Home's own.

    Phase 2 (later)

    One Home option runs the public store as an always-on public node, and such nodes join the default preferred
    peers. This closes phase 1's gap: availability while the creator's Home is offline. Which CIDs those nodes keep
    permanently, and on what admission rules, is decided in that phase.

    Departures from earlier answers (for Anders)

    • Runtime IPFS offline: still true for the private store. The new public store is online, but it holds only
      public metadata.
    • Q1, consent in the mint approval: consent is given once, on first opening the Creator, and the mint
      approval restates that the metadata is public.
    • Q2, a public availability role: phase 2 covers it as a Home option. Peering replaces the import into
      ipfs.ela.city.
    • Q3, serve only after the chain binds: the metadata goes public just before the wallet step. It is only
      metadata, and this ordering is what lets indexers find it at once.

    What's next

    1. Write the phase 1 implementation plan, then build it on the After 0.8.0: Rebuild protected content on develop as a layered stack (successor of #203–#206) #388 branch with independent review.
    2. Get the peer ID and addresses of Elacity's IPFS node for the default preferred peers. This is needed before
      release.
    3. Run a live check on test Homes: after a mint, the folder is findable on the public IPFS network and the item
      shows its name and cover.

    The design was reviewed independently and passed after three rounds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions