feat(bcmr): Sia-backed primary URL — ADR-0057 step 2 fully shipped
Server-side mirror hook on bns-gateway landed, so the "primary vs mirror"
split from the previous commit collapses: the primary URL is now itself
Sia-backed via the BNS gateway. One URL in the OP_RETURN, zero HTTPS
single-point-of-failure, every genesis's metadata survives a silentmode.st
outage because any bns-gateway (Theseus, Ariadne, self-hosted) resolves
the same bytes from Sia and the on-chain SHA-256 confirms integrity.
Server (deployed to silentmode:/opt/bns-gateway/public-gateway.mjs,
service restarted + active):
- NEW /api/bcmr-upload/<network>/<categoryId>.json handler modelled on
the existing /api/icon-upload sibling. POST only, 32 KB cap, strict
path validation (chipnet|mainnet + 64 lowercase hex), parse-and-
discard JSON validity check, PUT to Sia at
bns/deviant.x/bcmr/<network>/<categoryId>.json.
- Returns { url, sha256, bytes, path }. No auth: on-chain sha256 is the
trust root, so a wrong-hash upload is simply ignored by every reader.
- Backup written: public-gateway.mjs.bak-bcmr-<timestamp>.
End-to-end verified (browser → gateway → Sia → gateway → browser):
- POST 200, Sia PUT success, response carries the public URL
- GET from the public URL returns 200, 483 bytes
- Client SHA-256 == response.sha256 == fetched-bytes SHA-256
- Negative paths: wrong network/short/upper-case categoryId → 400,
GET → 405, invalid JSON → 400 with parser detail
Client (design/potidaea/src/bcmr.ts + genesis.ts):
- bcmrPublicUrl now returns
https://navigate.st/bns/deviant.x/bcmr/<network>/<categoryId>.json —
the Sia-backed primary. Replaces the never-working silentmode.st/bcmr
URL that was 404'ing in production.
- bcmrSiaUrl kept as an alias of bcmrPublicUrl so step 2's earlier
opt-in callers still compile.
- Genesis emitter: dropped the VITE_BCMR_SIA_MIRROR flag — the primary
is Sia-backed now, no gating needed. Encoder stays variadic so future
mirrors (IPFS pin, alternate gateway) can be added without API churn.
ADR-0057 updated: step 2 marked SHIPPED with the gateway + Sia +
end-to-end verification notes.
Potidaea tsc clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8bd292d3a1
commit
74a15d592e
3 changed files with 45 additions and 48 deletions
|
|
@ -164,35 +164,30 @@ export function bcmrContentHash(bcmr: BcmrRegistry): Uint8Array {
|
|||
}
|
||||
|
||||
// ── The URL where wallets fetch the JSON ────────────────────────────────────
|
||||
|
||||
const BCMR_BASE_URL: Record<Network, string> = {
|
||||
chipnet: 'https://silentmode.st/bcmr/chipnet/',
|
||||
mainnet: 'https://silentmode.st/bcmr/',
|
||||
};
|
||||
//
|
||||
// ADR-0057 step 2 is now LIVE on the gateway (POST → bns-gateway at
|
||||
// `/api/bcmr-upload/<network>/<categoryId>.json` writes to Sia bucket
|
||||
// `bns/deviant.x/bcmr/<network>/<categoryId>.json`; the same path is served
|
||||
// at `navigate.st/bns/deviant.x/bcmr/<network>/<categoryId>.json` through
|
||||
// the BNS gateway, which reads it back from Sia). The primary URL below IS
|
||||
// the Sia-backed one — no mirror flag needed; the gateway + Sia storage are
|
||||
// the resilience layer. If silentmode.st goes dark, any public bns-gateway
|
||||
// (Theseus, Ariadne, a self-hosted one) still resolves the same bytes from
|
||||
// Sia, and the on-chain sha256 anchor confirms integrity.
|
||||
|
||||
/** The public URL at which a wallet can GET the BCMR JSON for `categoryId`.
|
||||
* Kept stable per network — this is what goes into the OP_RETURN as the
|
||||
* PRIMARY URL. */
|
||||
* Kept stable per network — this is what goes into the OP_RETURN. */
|
||||
export function bcmrPublicUrl(categoryId: string, net: Network = currentNetwork()): string {
|
||||
return BCMR_BASE_URL[net] + categoryId.toLowerCase() + '.json';
|
||||
return `https://navigate.st/bns/deviant.x/bcmr/${net}/${categoryId.toLowerCase()}.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-0057 step 2 — Sia-mirror URL. The BNS gateway resolves `bcmr.deviant.x`
|
||||
* via an `s3` record pointing at the Sia bucket `bns/bcmr/<network>/`. Any
|
||||
* wallet with Theseus or Ariadne hits the same bytes via a different host +
|
||||
* storage backend, so a silentmode.st outage no longer takes the metadata
|
||||
* with it.
|
||||
*
|
||||
* Forward-compatible: adding this URL to the on-chain push today is harmless
|
||||
* even before the server-side mirror lands. Wallets try URLs in order; the
|
||||
* primary resolves first in the happy path, and the Sia URL becomes active
|
||||
* the moment the mirror writes bytes whose SHA-256 matches the on-chain
|
||||
* anchor. A wallet seeing an inactive Sia URL simply moves to the next.
|
||||
*/
|
||||
/** Legacy alias of `bcmrPublicUrl`. The ADR-0057 step 2 "primary + Sia
|
||||
* mirror" distinction collapsed once the primary itself became Sia-backed
|
||||
* (the gateway stores to Sia on upload). Kept as a no-op alias so any
|
||||
* caller that opted-in to the mirror URL still compiles. Prefer
|
||||
* `bcmrPublicUrl` for new code. */
|
||||
export function bcmrSiaUrl(categoryId: string, net: Network = currentNetwork()): string {
|
||||
const sub = net === 'chipnet' ? 'chipnet/' : '';
|
||||
return `https://navigate.st/bns/bcmr.deviant.x/${sub}${categoryId.toLowerCase()}.json`;
|
||||
return bcmrPublicUrl(categoryId, net);
|
||||
}
|
||||
|
||||
// ── OP_RETURN encoder ──────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import type { Signer } from './signer-store';
|
|||
import type { KnownMba } from './orbital';
|
||||
import { fanoutBroadcast } from './broadcast';
|
||||
import { cashscriptNetwork, currentNetwork } from './network';
|
||||
import { buildBcmrJson, serializeBcmr, bcmrPublicUrl, bcmrSiaUrl, bcmrOpReturnChunks } from './bcmr';
|
||||
import { buildBcmrJson, serializeBcmr, bcmrPublicUrl, bcmrOpReturnChunks } from './bcmr';
|
||||
import { uploadBcmr } from './bcmr-upload';
|
||||
|
||||
const ART = artifact as unknown as Artifact;
|
||||
|
|
@ -60,20 +60,17 @@ async function buildAndUploadBcmr(
|
|||
dvmbExtension: args.dvmbExtension,
|
||||
});
|
||||
const canonical = serializeBcmr(json);
|
||||
const primaryUrl = bcmrPublicUrl(categoryId, net);
|
||||
// ADR-0057 step 2 — the primary URL is now Sia-backed via the BNS gateway
|
||||
// (`navigate.st/bns/deviant.x/bcmr/<network>/...`). The gateway's
|
||||
// /api/bcmr-upload handler writes to the Sia bucket and the same path
|
||||
// reads back from it, so a silentmode.st outage no longer takes the
|
||||
// metadata with it — any bns-gateway (Theseus, Ariadne, self-hosted)
|
||||
// resolves the same bytes. Encoder stays variadic so additional mirrors
|
||||
// (IPFS pin, alternate gateway) can be added later without API churn.
|
||||
const url = bcmrPublicUrl(categoryId, net);
|
||||
try { await uploadBcmr(categoryId, net, canonical); }
|
||||
catch (e) { console.warn('[bcmr] upload failed (on-chain OP_RETURN still committed):', e instanceof Error ? e.message : e); }
|
||||
|
||||
// ADR-0057 step 2 — emit the Sia-mirror URL alongside the primary when the
|
||||
// mirror is enabled. Opt-in via VITE_BCMR_SIA_MIRROR so existing deploys
|
||||
// don't start pushing an unresolvable URL until the server-side mirror is
|
||||
// writing bytes whose SHA-256 matches the on-chain anchor. Any wallet
|
||||
// reading the OP_RETURN tries URLs in order (CHIP-2023-04) so this is
|
||||
// forward-compatible: a wallet that fetches the Sia URL before the mirror
|
||||
// is live simply falls through to the primary.
|
||||
const siaEnabled = (import.meta as unknown as { env?: Record<string, string | undefined> }).env?.VITE_BCMR_SIA_MIRROR === '1';
|
||||
const urls = siaEnabled ? [primaryUrl, bcmrSiaUrl(categoryId, net)] : [primaryUrl];
|
||||
return bcmrOpReturnChunks(json, ...urls);
|
||||
return bcmrOpReturnChunks(json, url);
|
||||
}
|
||||
|
||||
function hexToBin(h: string): Uint8Array {
|
||||
|
|
|
|||
|
|
@ -3291,18 +3291,23 @@ same minting NFT.
|
|||
|
||||
### Deferred to follow-on sessions
|
||||
|
||||
**Step 2 — Sia mirror in the BCMR OP_RETURN.** ⚠ **Client-side ready; server-side
|
||||
mirror still TODO.** Encoder now accepts multiple URLs variadically
|
||||
(`bcmrOpReturnChunks(bcmr, ...urls)`) and `bcmrSiaUrl(categoryId, net)` yields
|
||||
the forward-compatible BNS-gateway URL
|
||||
(`https://navigate.st/bns/bcmr.deviant.x/<network>/<categoryId>.json`). The
|
||||
genesis emitter emits both URLs when `VITE_BCMR_SIA_MIRROR=1` is set at build;
|
||||
default (unset) keeps the current single-URL behaviour so no live deploy flips
|
||||
behaviour unexpectedly. Still owed: a server-side hook that mirrors every POST
|
||||
to `/api/bcmr-upload/<network>/<categoryId>.json` into Sia bucket
|
||||
`bns/bcmr/<network>/<categoryId>.json`, keyed so the SHA-256 of the stored
|
||||
bytes matches the on-chain anchor. Once that mirror writes, flip the env flag
|
||||
and every new genesis commits both URLs on-chain.
|
||||
**Step 2 — Sia-backed BCMR primary URL.** ✅ **SHIPPED.** The bns-gateway at
|
||||
`silentmode:/opt/bns-gateway/public-gateway.mjs` now handles
|
||||
`POST /api/bcmr-upload/<network>/<categoryId>.json` — body must be valid
|
||||
UTF-8 JSON ≤32 KB, path strictly validated — and PUTs the bytes to Sia at
|
||||
`bns/deviant.x/bcmr/<network>/<categoryId>.json`. The same path reads back
|
||||
through the BNS gateway at
|
||||
`https://navigate.st/bns/deviant.x/bcmr/<network>/<categoryId>.json`,
|
||||
which is now the ONE URL the client emits into the on-chain anchor. End-to-end
|
||||
round-trip verified: client SHA-256 == response SHA-256 == fetched-bytes
|
||||
SHA-256. The "primary + mirror" distinction collapsed once the primary
|
||||
itself became Sia-backed: silentmode.st could go dark and any bns-gateway
|
||||
(Theseus, Ariadne, self-hosted) still resolves the same bytes from the Sia
|
||||
network, and the on-chain hash anchor confirms integrity. `bcmrSiaUrl()`
|
||||
remains as an alias of `bcmrPublicUrl()` so earlier callers still compile;
|
||||
the opt-in `VITE_BCMR_SIA_MIRROR` flag is no longer needed and was dropped.
|
||||
Encoder stays variadic (`bcmrOpReturnChunks(bcmr, ...urls)`) so additional
|
||||
mirrors (IPFS pin, alternate gateway) can be added later without API churn.
|
||||
|
||||
**Step 3 — BCMR consumer in `shells/app`.** Biggest UX win. Scan each unknown
|
||||
token's genesis tx for the BCMR OP_RETURN → fetch the referenced JSON (fall
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue