Assess Dotify against the official Product stack - #116
Merged
Conversation
The Host signRaw wire format is not pinned by the SDK: HostSignPayloadResponse carries an untagged signature, and a Substrate host may sign a raw payload verbatim or inside the conventional <Bytes> envelope. Verification assumed one shape, so a wrong guess would have failed every Product key request with an error indistinguishable from a wrong signer. Accept a bounded set instead: the canonical message verbatim or <Bytes>-wrapped, and a bare 64-byte or MultiSignature-tagged 65-byte sr25519 signature. Every variant carries the identical domain-bound message, so this adds no replay, cross-app, cross-chain, or cross-track surface; a non-sr25519 tag still fails closed. Route schemas widen to 128 or 130 hex so the tag is checked by the verifier rather than rejected before it. Reject EVM-derived account ids for product-sr25519-v1. A 20-byte H160 padded with 0xee derives back to the H160 it contains, so accepting that shape let a caller name any paying EVM listener as the requester and rested the boundary on the curve check alone. A real Product account is a native AccountId32. A key that parses and derives to the requester but verifies under no variant now returns PRODUCT_SIGNATURE_REJECTED, kept distinct from SIGNATURE_INVALID so an envelope problem is separable from a wrong-account problem in logs. Pin @scure/sr25519 exactly, matching @noble/hashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wiring the Product key signatures changed the behaviour but three places still described the old one. The wallet modal told Product-host users that protected playback required an EVM signer, the runbook asked operators to confirm no key is released through the Product identity, and the architecture matrix said the shipped UI used EIP-191 or a session token - contradicted by its own prose two sections later. All three now describe what ships: a connected Product account requests protected keys through product-sr25519-v1, and paid access plus artist publishing remain on the EVM signer. This matters beyond tidiness - the stale runbook step would have had an operator sign off on a denial as correct behaviour, hiding a real signing failure. Record the signing envelope decision and the EVM-derived key rejection, and turn the runbook step into an evidence capture that names which envelope the live host actually produced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Track and split counts come from contract storage and the directory enumerates
runtimes Dotify does not control, so Array.from({ length: Number(count) })
allocated before anything could reject a malformed or hostile value. Both
adapters now validate counts first and throw rather than truncate, since a
silent cap would present a partial catalog as complete. The catalog loader
already isolates per-runtime failures, so one bad runtime degrades to a missing
artist.
Mark the two unverified spots in the CDM adapter that must be settled before it
can be selected: waitForTransaction returns immediately where the viem writer
awaits a receipt, and the payForAccess value-transfer shape is inferred rather
than confirmed against generated contract types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dotify has no CDM-registered packages and no `cdm install`, but it does not need them. Its Solidity contracts are deployed through Asset Hub's eth-rpc, which is a compatibility layer over pallet-revive - the same pallet the Product SDK contract helpers target - so the deployed H160 addresses are already reachable without PolkaVM recompilation or a registry entry. CdmJsonContract needs only version, address, and abi for getContract(), and `new ContractManager(...)` is documented as snapshot-only. The generator emits exactly that snapshot from the same Hardhat artifacts the viem bindings come from, so the two adapters cannot disagree about an ABI. Artist runtimes are deliberately absent from the manifest: a diamond is deployed per artist, so its address is known at call time, not build time, and a placeholder would misrepresent the deployment. Their merged facet ABI is emitted separately and bound to an address by createContract. The generator lives in web/ because it needs the SDK's codegen, which is a frontend dependency; adding the Product SDK tree to contracts/evm just to emit types would be a worse trade. Unnamed Solidity getter params are named positionally for codegen only - generateContractTypes interpolates the name into a tuple label and emits `args: [: HexString]`, which does not parse. The manifest ABI stays byte-faithful to the artifact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the contract resolver the CDM adapter was missing. Fixed-address contracts resolve from the generated manifest through ContractManager; per-artist runtime diamonds bind the merged facet ABI to their call-time address through createContract. Two SDK constraints shape this. First, createChainClient routes exclusively through the Product host provider with no direct-WebSocket fallback, so Product mode cannot work in a standalone build - validateProductionEnvironment now rejects product-cdm unless the host mode is enabled. Second, the host decides which chain an environment resolves to, and Dotify's runtimes live on Polkadot Hub TestNet, reached through the `paseo` preset rather than `devnet`. verifyDeployment queries the directory before any catalog read so a wrong-chain connection fails with a named error instead of looking like artists with no releases. Selection is build-time rather than runtime, for two reasons. Switching the authority for access policy is a deployment decision made with evidence, not something a page should flip. And Vite inlines the value, so a viem build tree-shakes the whole Product graph away: 4.4 MB against 10 MB when opted in. The difference is @parity/product-sdk-descriptors, whose shared descriptors module references every chain's metadata - only one chunk is ever fetched, but all are published, and Bulletin storage is a finite quota. Both shipped builds stay at 4.4 MB. Reads only. Writes stay on the viem signer path in every mode, because routing a payment or a publication through a signer with no host transaction evidence is not a reasonable default. A failed Product setup rejects every read rather than falling back to viem: the adapter in use must never be ambiguous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The blocking unknown for Product contract mode is no longer the manifest or generated types - both now exist and are wired. It is whether the Product host serves a chain that holds Dotify's runtimes, since the host controls that mapping and the contracts are on Polkadot Hub TestNet rather than Product DevNet Asset Hub. Also records the measured build-size trade-off, so an operator weighs it against the Bulletin quota before enabling product-cdm for a .dot deployment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous default was wrong in a way that would have produced an empty catalog. Product DevNet is not a separate network: it is a preset over the Paseo system parachains - Asset Hub (1000), People (1004), Bulletin (1010) - at EVM chain 420420417. That is exactly where Dotify is already deployed. Verified read-only against both endpoints for the ArtistDirectory at 0xcf1534c6e2b0e43b9436c1e86a076466dc0f2108: eth-rpc-testnet.polkadot.io and paseo-assethub-rpc.laissez-faire.trade both report chain id 0x190f1b41, blocks one apart, and byte-identical contract code. They are two providers for one chain, so no contract redeploy is needed to port Dotify to DevNet. The SDK's `paseo` preset is the trap: it targets Paseo Next (Asset Hub Next 1500 / People Next 1502), which the Product docs call a different network where "funds sent there will not appear on this Devnet". Dotify has no deployment there, so ProductChainEnvironment now admits only `devnet` - selecting a chain that cannot hold the catalog is a bug, not a configuration option. Also realigns the environment reference tables, clearing the markdownlint MD060 warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Porting Dotify to Product DevNet turned out to be a configuration question rather than a migration: DevNet is a preset over the Paseo system parachains (Asset Hub 1000, People 1004, Bulletin 1010) at EVM chain 420420417, which is where Dotify's contracts already are. This makes that claim checkable instead of asserted. `npm run smoke:devnet` reads web/.env.product-devnet and deployments.json and verifies, read-only, that the configured Asset Hub reports chain 420420417, is producing blocks past the 2026-07 halt, still serves bytecode for the ArtistDirectory and ArtistRuntimeFactory, and that the Bulletin RPC and IPFS gateway respond. It sends no transaction, reads no secret, and prints no credential. Network-dependent, so it stays out of the unit test path. The build profile needed no endpoint changes - the configured Bulletin and IPFS gateway were already the DevNet ones. What it needed was honest comments: the previous note framed the Asset Hub endpoint as a DevNet-compatible stopgap when it is in fact the DevNet chain. Adds the second DevNet IPFS gateway as a read fallback, and a warning against Asset Hub Next (1500) and People Next (1502), which are a different network holding none of Dotify's contracts. Verified: 6/6 checks pass against the live chain at head 11546553. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The known-limits bullet still said Product contract mode was blocked on CDM-installed packages and generated contract types. Both now exist, so the list overstated what is missing. Only pallet-revive account mapping and host-signed transaction evidence remain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fetchIpfsCid and fetchAssetRef walked 4-6 gateways serially with no per-gateway timeout, so one unresponsive gateway cost the listener the browser's full connection timeout before the next candidate was tried. Covers, manifests, and v1 audio all read through that path. Race the candidates instead, the way audioV2Gateway already does for byte ranges: bound time-to-headers, hedge onto the next gateway after a delay, cap concurrency at 3, and abort the losers once a winner answers. Every URL addresses the same immutable CID, so racing them cannot diverge. The winner's own controller is never aborted, so its body stays readable, and a caller abort stops the queue where it is rather than walking the rest of the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
min_machines_running = 0 let the API scale to zero, and the first content key request after an idle period paid the cold start. Measured against the running deployment: 8.17s to /health cold (uptime 0), 0.11s warm. That request sits directly in front of first sound, so the saving was being taken out of the listening experience. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Socket.IO reports a CORS rejection, a stopped server, and a wrong URL identically, so every one of them surfaced as "Room service unavailable." That is the vague failure the product invariants rule out, and it hid a real deployment gap: the running signaling server allows only muzinga.netlify.app, so opening a room from the Polkadot Product host origin is refused with a 403 the browser will not explain. On connect_error, read the server's unauthenticated /health, compare the page origin against the allowlist it reports, and upgrade the message in place. The generic reason stands if health cannot be read or if the origin is allowed, so this only ever widens an error - room access stays decided by the server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with `flyctl deploy -c services/api/fly.toml` from the repository root fails at `COPY src ./src`: -c selects the config file, but the build context stays the shell's working directory, and the Dockerfile is written against services/api. It also uploads a ~1.3 GB context, because Docker reads .dockerignore from the context root and only the service directories have one. A cached `npm ci` layer from an earlier correct build hides the cause, so the error surfaces at the first uncached step rather than the first wrong one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`makeAttempt` detaches its parent-abort listener as soon as the fetch settles, which is when headers arrive - before the body has streamed. The winner was therefore returned already disconnected from the caller's signal, so aborting afterwards no longer stopped the download. That is a regression against the serial reader this replaced, which passed the caller's signal straight to `fetch`. It bites where it matters: `useCatalog` passes a signal to every audio and asset read, so a listener skipping tracks left the previous audio downloading to completion, unread. Re-link the winner to the caller's signal before returning it, and abort the winner on the late-abort path too - it has already been removed from `active`, so the cleanup block would not otherwise reach it. Covered by a regression test that failed before this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inside the Product host the app is served from polkadot://app.dotify-test01.dot, not the DotNS web gateway. SIGNAL_ORIGINS already carried it; API_ORIGINS did not, so a container would have had working rooms and no content keys - catalog and key delivery both go to the API, so free and protected playback would fail CORS while the room layer looked healthy. Deliberately does not add a bare `null`. `polkadot:` is a non-special scheme, so its origin is opaque and a browser may send `Origin: null` instead of the literal value. Allowing that would admit every sandboxed iframe and file:// page on the web to the authenticated upload and content-key routes. If a host request is still refused, the actual Origin header from the Fly log is the evidence to act on. Two regression tests pin both halves: the custom-scheme origin is answered, a null origin is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grounded in the Product docs, SDK reference, and Community Foundation repos rather than assumption. Three findings drive the proposal. Dotify is closer to the stack than the roadmap assumed. Its Solidity contracts already run on pallet-revive via Asset Hub's eth-rpc - the exact pallet the stack specifies - and app delivery, content addressing, and app-scoped identity are already on-stack. Its DAV2 encryption is not redundant with a Bulletin move but the precondition for one, since Bulletin gates storing, not reading. The genuine gaps are worth closing and mostly make the product better: the personhood precompile returns a per-app unlinkable alias and would finally make `human-free` real while retiring the dev registrar; CDM registration makes the catalog composable by other products; CASH is the asset users actually hold. One gap will not close. Statement Store writes require an Individuality allowance, official calls are 1:1 and mobile-only, and an SDP exceeds both the 512-byte statement and 1 KiB per-account ceilings. Moving rooms onto the official messaging layer would convert every listener into an attested person, which does not degrade the product - it deletes the gesture it exists to protect. The anonymous guest is therefore treated as a design constraint, not a legacy compromise. Proposes a three-ring architecture that shrinks the trusted core instead of denying it, and names content-key custody as a stated exception with per-artist custody as the strongest remedy - turning the most centralized component into an expression of artist sovereignty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for muzinga ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This was referenced Jul 29, 2026
knzeng-e
marked this pull request as ready for review
August 3, 2026 13:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Outcome
An evidence-based assessment of Dotify against the official Product stack, and a proposed architecture. Docs only - no code changes implied.
Branches from
main, independent of the #108-#115 stack.Method
Read the Product docs, SDK reference, resources, and the Community Foundation repos, including the messaging, storage, contracts, identity, and money architecture pages. Claims are quoted or cited; where the docs are silent the document says so rather than guessing.
Three findings
1. Dotify is closer to the stack than the roadmap assumed. Its Solidity contracts already run on
pallet-revivevia Asset Hub eth-rpc - the exact pallet the stack specifies. App delivery, content addressing, and app-scoped identity are already on-stack. And its DAV2 encryption is not redundant with a Bulletin move but the precondition for one, since Bulletin gates storing, not reading.2. The real gaps mostly make the product better. The personhood precompile returns a per-application unlinkable alias, which would make
human-freereal and retire the dev registrar - the project weakest claim. CDM registration makes the catalog composable by other products. CASH is the asset users actually hold; charging in PAS is a category error on this stack.3. One gap will not close, and that is the important part. Statement Store writes require an Individuality allowance, official calls are 1:1 and mobile-only, and a WebRTC offer exceeds both the 512-byte statement and the 1 KiB per-account ceiling. Moving rooms onto the official messaging layer would convert every listener into an attested person. That does not degrade the product - it deletes the gesture it exists to protect.
So the anonymous guest is treated as a design constraint, not a legacy compromise to migrate away.
The proposal
A three-ring architecture that shrinks the trusted core rather than denying it:
Sequenced by value per unit of risk. Steps 1-4 are additive and independently shippable; nothing before step 5 touches the walletless guest path.
Review guide
Section 4 is the load-bearing argument - if you disagree with treating the anonymous guest as inviolable, the rest of the proposal changes shape. Section 6 lists what I would deliberately not do, including keeping audio off Bulletin until quota economics for MB-scale media are demonstrated.
Verify carefully
pallet-revive.Validation
node scripts/backlog-sync.mjs --check --offlinegit diff --checkDocs-only; no build, test, or lint surface touched.
Known limitations and follow-ups
The CASH settlement path is an open problem, not a design. CASH lives on People chain and the runtime on Asset Hub; two candidate shapes are sketched and neither is proven.
One question is worth asking the Foundation directly: can third-party products obtain TURN credentials from platform infrastructure? If yes, Ring 2 halves.
This proposal does not supersede the Phase 4/5 roadmap items in
polkadot-product-readiness-and-killer-dapp-roadmap.md; it reframes their sequence. If accepted, that roadmap should be updated to match.