Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
95aeb9a
Adapt Dotify for Product DevNet
knzeng-e Jul 26, 2026
b9d4302
Use dotify-test01 Product domain
knzeng-e Jul 26, 2026
fc2a2ce
feat: extract runtime contract ports
knzeng-e Jul 26, 2026
00f1d93
feat: add Product CDM runtime adapter
knzeng-e Jul 26, 2026
5e41931
feat: add Product signature scheme
knzeng-e Jul 28, 2026
aaffb9d
feat: wire Product key signatures in frontend
knzeng-e Jul 28, 2026
cb17803
fix: accept Product host signing envelopes and reject EVM-derived keys
knzeng-e Jul 29, 2026
65e7650
docs: correct Product protected-playback claims
knzeng-e Jul 29, 2026
9164b2c
chore: bound runtime catalog reads and record CDM adapter gaps
knzeng-e Jul 29, 2026
1ef3117
feat: generate the Product CDM manifest and contract types
knzeng-e Jul 29, 2026
8bd9220
feat: wire the Product CDM runtime adapter behind a build-time flag
knzeng-e Jul 29, 2026
ed701e6
docs: record the CDM manifest slice and its remaining gate
knzeng-e Jul 29, 2026
9246537
fix: target the devnet preset, the only chain holding Dotify runtimes
knzeng-e Jul 29, 2026
b587bd7
feat: prove the DevNet endpoint set with a read-only smoke check
knzeng-e Jul 29, 2026
1476208
docs: correct the stale CDM prerequisite in the deploy runbook
knzeng-e Jul 29, 2026
10a4a59
perf: bound and hedge the legacy IPFS gateway reads
knzeng-e Jul 29, 2026
0f7438e
fix: keep one API machine warm for first sound
knzeng-e Jul 29, 2026
072e9f9
feat: explain why a room connection was refused
knzeng-e Jul 29, 2026
b06e765
docs: warn that flyctl -c does not set the Docker build context
knzeng-e Jul 29, 2026
7e64f10
fix: keep a raced gateway read cancellable after the winner returns
knzeng-e Jul 29, 2026
3254cec
fix: admit the Product host container origin to the API allowlist
knzeng-e Jul 29, 2026
cb41c26
docs: assess Dotify against the official Product stack
knzeng-e Jul 29, 2026
1c89fb1
feat: read personhood from the Individuality precompile
knzeng-e Jul 29, 2026
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: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ dist/
.playwright*
!.env.example
!web/.env.example
!web/.env.product-devnet
!contracts/evm/.env.example

web/node_modules/
web/dist/
web/dist-bulletin*
web/dist-product/
web/*.car
web/.playwright*
web/playwright-report/
web/test-results/
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ cd web
npm run smoke:production-env
```

When Product DevNet endpoints, `deployments.json`, or the DevNet build profile
change, also run the read-only endpoint check:

```bash
cd web
npm run smoke:devnet
```

For deployment-sensitive work, add the relevant read-only health, chain,
contract, IPFS gateway, and wrong-network checks. Record the environment and
evidence without exposing credentials.
Expand Down
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,28 @@ aura lights the whole field (`web/src/styles/aura.css`).
creates one personal `SmartRuntime` per artist, and `ArtistDirectory` indexes
artist addresses to their runtimes.

**Frontend**: Static React + Vite web app deployed to dot.li.
**Frontend**: Static React + Vite web app deployed to Netlify and, through the
Product profile, Bulletin/DotNS at `dotify-test01.dot`.

**WebRTC**: real-time music streaming.

**Socket.IO**: signaling for room discovery and SDP/ICE exchange. A future
iteration can move signaling to statement-store style infrastructure.

**Product SDK direction**: Dotify remains a standalone web app first. Product
SDK / Playground / Humanity work is a progressive-enhancement track documented
in
[`docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md).
The current SDK snapshot is prototype/reference/unaudited and must be proven
against Dotify's Host, key-delivery, room, and contract constraints before it
becomes a production dependency.
**Product SDK direction**: Dotify now has an adaptive Product DevNet build for
`dotify-test01.dot`. It keeps standalone link-first rooms and Free listening intact,
adds explicit app-scoped Product identity, and publishes through
Bulletin/DotNS. The runtime hooks now sit behind typed ports with the current
viem implementation and an experimental Product CDM/PAPI adapter boundary.
The backend key-delivery protocol now has an explicit Product sr25519
signature scheme that binds the Product account public key to the derived H160
requester before access checks. The Product frontend can now submit that
Product proof after explicit host-account connection; contract writes remain
passkey/EVM until CDM-installed runtime packages and host-signed transaction
evidence are proven. See
[`docs/explanation/product-devnet-architecture.md`](docs/explanation/product-devnet-architecture.md)
and the
[`Product roadmap`](docs/backlog/polkadot-product-readiness-and-killer-dapp-roadmap.md).

## Deployed

Expand All @@ -69,7 +77,7 @@ becomes a production dependency.

**Gateway URL** — <https://paseo-ipfs.polkadot.io/ipfs/bafkr4ibynaanfrddyjgpmut2qrcu6vdttocbp4feyw6vkgxkkhqndjksny>

**DotNS name** — `dotify.dot.li`
**DotNS name** — `dotify-test01.dev-dot.li`

## How to run end-to-end (locally)

Expand Down Expand Up @@ -118,7 +126,8 @@ npm run dev

| Variable | Required | Purpose |
| --------------------------- | ---------------- | -------------------------------------------------------- |
| `API_ORIGIN` | Production | Frontend origin allowed by API CORS |
| `API_ORIGIN` | Compatibility | Singular frontend CORS origin fallback |
| `API_ORIGINS` | Production | Comma-separated exact frontend CORS origins |
| `PASEO_ASSET_HUB_RPC` | Key requests | Paseo Asset Hub EVM RPC used for access checks |
| `DOTIFY_DIRECTORY_ADDRESS` | Key requests | ArtistDirectory address used to resolve artist runtimes |
| `DOTIFY_CHAIN_ID` | Key requests | Chain ID expected in wallet-signed key requests |
Expand Down Expand Up @@ -298,6 +307,10 @@ releasing it. Gated tracks use a signed session or signed key request; the
backend verifies the requester, resolves the artist runtime, and calls
`musicAccCanAccess` before releasing a per-track key. If access is denied, the
UI shows the action needed to unlock the track and plays no protected audio.
Standalone clients sign with the default `eip191` scheme. Product-host clients
can use `product-sr25519-v1` by signing the same canonical Dotify message bytes
with the app-scoped Product account and sending `productPublicKey`; the backend
derives the H160 requester from that public key before any nonce is consumed.

For registered artist tracks, users without a connected wallet can play Free
tracks. For gated tracks, they see a sign-in/unlock gate. Dev-account fallback
Expand Down Expand Up @@ -430,9 +443,9 @@ handle:
and decide whether a backend read-through gateway is needed.
4. Keep demo-mode browser-exposed Pinata/content secrets out of public
deployments.
5. Run Product SDK feasibility spikes: Host detection, Product account signing,
resource allocation, Playground/Bulletin/DotNS deployment, and PolkaVM/CDM
contract portability.
5. Validate the Product host/account and Bulletin/DotNS deployment baseline,
then wire frontend Product-signed key/session requests, resource allocation,
and PolkaVM/CDM contract portability.
6. Add a production artist dashboard on `/artists`: release drafts, edit
metadata, royalty analytics, and profile verification state.
7. Deploy and monitor a public signaling server for DotNS / Bulletin builds.
Expand Down
7 changes: 6 additions & 1 deletion contracts/evm/contracts/ArtistRuntimeFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,19 @@ contract ArtistRuntimeFactory {
selectors[4] = MusicRoyaltiesPallet.musicRoyTotalBps.selector;
}

/// @dev The two registrar selectors are retained so already-deployed runtimes keep a
/// stable ABI; the setter now reverts. `musicAccPersonhoodInfo` is new, so a
/// runtime created before this change needs a diamond Add cut to gain it — its
/// access decisions already follow the precompile without it.
function _musicAccessSelectors() private pure returns (bytes4[] memory selectors) {
selectors = new bytes4[](7);
selectors = new bytes4[](8);
selectors[0] = MusicAccessPallet.setPersonhoodRegistrar.selector;
selectors[1] = MusicAccessPallet.musicAccSetPersonhoodLevel.selector;
selectors[2] = MusicAccessPallet.musicAccCanAccess.selector;
selectors[3] = MusicAccessPallet.musicAccHasPaid.selector;
selectors[4] = MusicAccessPallet.musicAccPersonhoodLevel.selector;
selectors[5] = MusicAccessPallet.musicAccHasPersonhood.selector;
selectors[6] = MusicAccessPallet.musicAccGetRegistrar.selector;
selectors[7] = MusicAccessPallet.musicAccPersonhoodInfo.selector;
}
}
32 changes: 32 additions & 0 deletions contracts/evm/contracts/interfaces/IPersonhood.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

/// @title IPersonhood — Proof of Personhood precompile (Individuality)
/// @notice Minimal interface for the `pallet-revive` personhood precompile, live on
/// Asset Hub at `0x000000000000000000000000000000000A010000`.
///
/// Mirrors the canonical declaration in
/// `paseo-network/runtimes/precompiles/personhood/sol/IPersonhood.sol`.
/// Only `personhoodStatus` is declared here: Dotify reads status, it does not
/// verify raw ring proofs, so `personhoodInfoByProof` is deliberately omitted
/// rather than carried as unused surface.
///
/// The precompile reads the alias-accounts pallet, which stores per-context
/// alias mappings backed by ring membership proofs. Ring roots arrive from the
/// People chain by XCM.
interface IPersonhood {
/// @param status Personhood tier: 0 = None, 1 = Lite, 2 = Full. Tiers are
/// incremental, so a future tier leaves these values unchanged.
/// @param contextAlias Per-context 32-byte pseudonym derived from the ring membership
/// proof. Unique per person per context, which is what prevents
/// cross-application linkability. Zero when status is None.
struct PersonhoodInfo {
uint8 status;
bytes32 contextAlias;
}

/// @notice Personhood info for `account` within a specific application `context`.
/// @param context A fixed 32-byte application identifier. The same person yields a
/// different `contextAlias` under a different context.
function personhoodStatus(address account, bytes32 context) external view returns (PersonhoodInfo memory info);
}
37 changes: 29 additions & 8 deletions contracts/evm/contracts/libraries/LibMusicAccess.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
pragma solidity ^0.8.28;

import { LibMusicRegistry } from './LibMusicRegistry.sol';
import { LibPersonhood } from './LibPersonhood.sol';

/// @title LibMusicAccess
/// @notice Namespaced storage for listener access records and proof-of-personhood levels.
/// Personhood levels mirror the Individuality Chain DIM tiers; in the current
/// prototype they are set via an admin registrar account.
/// @notice Namespaced storage for listener access records, and personhood gating read
/// from the Individuality precompile.
///
/// Personhood is no longer stored here. `personhoodLevelOf` and
/// `personhoodRegistrar` remain declared so existing runtimes keep their storage
/// layout intact — a diamond cannot safely reorder occupied slots — but neither
/// participates in an access decision any more. The registrar defaulted to the
/// artist, which meant an artist could grant personhood to their own listeners;
/// the precompile removes that path.
///
/// PersonhoodLevel maps onto the precompile tiers by ordinal:
/// None(0) -> None(0), DIM1(1) -> Lite(1), DIM2(2) -> Full(2).
///
/// Storage slot: keccak256("smart.runtime.pallet.music-access.storage")
library LibMusicAccess {
Expand All @@ -15,9 +25,10 @@ library LibMusicAccess {
struct Storage {
// contentHash → listener → paid
mapping(bytes32 => mapping(address => bool)) paidAccess;
// account → verified personhood tier
// DEPRECATED — no longer read for access. Kept to preserve the storage layout of
// already-deployed runtimes. Personhood now comes from the precompile.
mapping(address => LibMusicRegistry.PersonhoodLevel) personhoodLevelOf;
// address authorised to set personhood levels
// DEPRECATED — see above. Retained for layout compatibility only.
address personhoodRegistrar;
}

Expand All @@ -32,9 +43,19 @@ library LibMusicAccess {
// Internal helpers
// -------------------------------------------------------------------------

function hasRequiredPersonhood(Storage storage s, address account, LibMusicRegistry.PersonhoodLevel required) internal view returns (bool) {
if (required == LibMusicRegistry.PersonhoodLevel.None) return true;
return uint8(s.personhoodLevelOf[account]) >= uint8(required);
/// @notice True when `account` meets `required` personhood, per the Individuality
/// precompile. The storage argument is unused and kept only so existing
/// call sites and the pallet ABI stay unchanged.
/// @dev Fails closed when the precompile cannot answer. See LibPersonhood.hasStatus.
function hasRequiredPersonhood(Storage storage, address account, LibMusicRegistry.PersonhoodLevel required) internal view returns (bool) {
return LibPersonhood.hasStatus(account, uint8(required));
}

/// @notice Dotify-context personhood tier and pseudonym for `account`.
/// @dev Exposes the alias so a runtime can later count distinct people rather than
/// distinct addresses. Not used for access decisions today.
function personhoodOf(address account) internal view returns (uint8 status, bytes32 contextAlias, bool live) {
return LibPersonhood.readStatus(account);
}

function setPersonhoodRegistrar(Storage storage s, address registrar) internal returns (address previousRegistrar) {
Expand Down
74 changes: 74 additions & 0 deletions contracts/evm/contracts/libraries/LibPersonhood.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import { IPersonhood } from '../interfaces/IPersonhood.sol';

/// @title LibPersonhood
/// @notice Reads proof of personhood from the Individuality precompile.
///
/// This replaces Dotify's admin-registrar personhood, which could only ever be
/// as trustworthy as the account operating it — and that account defaults to
/// the artist, who therefore had the technical ability to grant personhood to
/// their own listeners. Reading the precompile removes that forgery path
/// entirely: personhood becomes a fact about a person on the People chain, not
/// a row an operator can write.
///
/// It also earns Dotify a property the registrar could not offer. The precompile
/// returns a per-context alias, so the same listener appears under a different
/// pseudonym in every application. Dotify learns "this is a distinct person"
/// without learning who they are anywhere else.
library LibPersonhood {
/// @dev Fixed precompile address. `pallet-revive` left-shifts the user-defined
/// `AddressMatcher::Fixed(0x0A01)` index by 16 bits to form this suffix.
/// Verified live on EVM chain 420420417 (Paseo Asset Hub, para 1000): a call
/// returns a 64-byte PersonhoodInfo, where absent addresses return empty.
address internal constant PERSONHOOD_PRECOMPILE = 0x000000000000000000000000000000000a010000;

/// @dev Dotify's application context. Fixed forever: changing it re-pseudonymises
/// every listener, so any change is an identity migration, not a config edit.
bytes32 internal constant DOTIFY_CONTEXT = bytes32('dotify');

uint8 internal constant STATUS_NONE = 0;
uint8 internal constant STATUS_LITE = 1;
uint8 internal constant STATUS_FULL = 2;

/// @notice Read personhood for `account` in Dotify's context.
/// @return status Personhood tier, or 0 when the precompile is unavailable.
/// @return alias_ Per-context pseudonym, zero when status is 0.
/// @return live True when the precompile answered with a decodable struct.
///
/// @dev Deliberately a low-level staticcall rather than a typed call. The precompile
/// declares `HAS_CONTRACT_INFO = false`, so its `extcodesize` can be zero, and
/// Solidity's high-level call inserts an `extcodesize` check that would revert
/// against it. The staticcall also lets a chain without the precompile — a local
/// Hardhat node — resolve to "not live" instead of reverting every access query.
///
/// `live` is returned rather than swallowed so callers can distinguish "this
/// person has no personhood" from "this chain cannot answer". Those are the same
/// decision (deny) but not the same diagnosis, and conflating them is how a
/// misconfigured deployment gets mistaken for an empty user base.
function readStatus(address account) internal view returns (uint8 status, bytes32 alias_, bool live) {
bytes memory callData = abi.encodeWithSelector(IPersonhood.personhoodStatus.selector, account, DOTIFY_CONTEXT);

(bool ok, bytes memory returnData) = PERSONHOOD_PRECOMPILE.staticcall(callData);

// A call to an address with no code succeeds with empty returndata, so success
// alone proves nothing. Only a full struct counts as an answer.
if (!ok || returnData.length < 64) {
return (STATUS_NONE, bytes32(0), false);
}

IPersonhood.PersonhoodInfo memory info = abi.decode(returnData, (IPersonhood.PersonhoodInfo));
return (info.status, info.contextAlias, true);
}

/// @notice True when `account` holds at least `requiredStatus` in Dotify's context.
/// @dev Fails closed: an unavailable precompile denies every gated track rather than
/// admitting everyone. Product invariant — ambiguous access decisions fail closed.
function hasStatus(address account, uint8 requiredStatus) internal view returns (bool) {
if (requiredStatus == STATUS_NONE) return true;
(uint8 status, , bool live) = readStatus(account);
if (!live) return false;
return status >= requiredStatus;
}
}
Loading
Loading