deviant/Parameters/dApp/Explorer/PROFILE.md
Local Dev 7674a9bf0f feat(asm): fee-chain roll-in + signed/encrypted explorer profiles
Two pieces:

1. Pattern A · fee-chain roll-in (default) — the OpenMint Potidaea
   swap-relay now grows ONE accumulator UTXO on the platform wallet
   instead of one 600-bit UTXO per swap. Each claim spends potidaea's
   prior fee-tip as an extra input and emits a bigger consolidated
   fee-tip; same per-swap net revenue as raw emission, zero
   fragmentation. Cauldron-shaped (state-embedded fees) but with an
   external recipient since Potidaea's reservoir is peg-locked.
   Enforces MIN_GROSS=200,000 bits (~$0.01) so per-swap platform-fee
   outputs are always meaningfully above the ~148-bit per-input miner
   cost. FEE_CHAIN=0 opts out for A/B comparison. New standalone demo
   at chipnet-potidaea-fee-chain-relay.mjs (75 swaps → 1 UTXO of
   83,250 bits, verified live on chipnet).

2. Signed / encrypted profile explorer — the address-list explorer
   now accepts ?profile=<url> in two shapes:
   - v1 signed-public (deviant-explorer-profile/1): anyone with the
     URL sees the addresses; ECDSA/DER signature verified in-browser
     via @noble/curves prevents tampered mirrors from swapping the
     list. Same on-chain BCH address the wallet owns is the signer.
   - v2 encrypted (deviant-explorer-profile/2): AES-256-GCM ciphertext;
     key = sha256(BCH signMessage(fixed challenge)). Login modal
     prompts for owner WIF (in-browser signMessage via noble-curves
     RFC-6979 ECDSA); paste-a-signature-from-wallet path is
     scaffolded and deferred to a future WalletConnect requester or
     Deviant-wallet postMessage bridge. AES-GCM auth-tag catches
     wrong-key and tampering in one check.

   New:
   - scripts/potidaea-explorer-mkprofile.mjs — CLI signer with --encrypt
     for v2. Self-verify supports --verify + --wif for v2 decrypt.
   - Parameters/dApp/Explorer/PROFILE.md — v1 + v2 spec, KDF, login
     paths, wallet-vendor guidance.
   - design/potidaea/DEPLOY.md §5 — rsync + Caddyfile snippet to
     co-host the explorer + /profiles/ on potidaea.deviant.bch with
     CORS-open profile serving.

Chose ECDSA/DER over Schnorr because BCH-Schnorr (libauth) is NOT
BIP-340 Schnorr (noble) — the two libs would produce mutually invalid
signatures. ECDSA/DER (v1) and BCH signMessage (v2 KDF) are both
one well-standardized format that libauth and noble-curves handle
identically. Round-trip live-tested in the browser for both v1 (verified
✓ badge) and v2 (unlock → 6 addresses render with live chipnet balances).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-29 01:18:26 +02:00

8.9 KiB

Signed profile — deviant-explorer-profile/1

Purpose. Let someone publish a signed list of chipnet (or later mainnet) BCH addresses under a URL, and load them into the Deviant explorer for online balance viewing. The signature proves the list was authored by whoever holds the owner's key — a mirror or CDN can't quietly add or remove addresses without breaking the signature.

Login shape

https://<explorer-host>/?profile=<encodeURIComponent(profile-url)>

The explorer fetches <profile-url>, verifies the signature, and renders the address list. On failure it shows a warning banner and falls back to the built-in WALLETS list.

Profile document

{
  "spec": "deviant-explorer-profile/1",
  "network": "chipnet",                     // "chipnet" or "mainnet"
  "name": "my chipnet wallets",             // any string; display only
  "created": "2026-08-13T00:00:00Z",        // ISO-8601 UTC
  "owner": {
    "cashaddr": "bchtest:qz…",              // owner's P2PKH cashaddr
    "pubkey":   "04abc…"                    // 33-byte compressed OR 65-byte uncompressed, hex
  },
  "addresses": [
    { "label": "A",   "cashaddr": "bchtest:qrd5…", "note": "" },
    { "label": "GMX", "cashaddr": "bchtest:qz04…", "note": "" }
  ],
  "signature": "3044…"                      // ECDSA/DER over sha256(canonicalJson(body-minus-signature))
}

Canonical JSON: JSON.stringify with keys sorted alphabetically at every level. Whitespace irrelevant on the wire; both signer and verifier reconstruct canonicalJson before hashing.

Signature algorithm: ECDSA over secp256k1, DER-encoded. Chosen over Schnorr because BCH-Schnorr (libauth) and BIP-340 Schnorr (noble/BIP-340) are DIFFERENT standards — signing with one and verifying with the other silently fails. ECDSA/DER is one well-standardized format that both libraries produce + consume identically.

Verification steps (client):

  1. Fetch profile JSON. spec must equal "deviant-explorer-profile/1".
  2. Strip the signature field. Compute digest = sha256(canonicalJson(rest)).
  3. Verify ECDSA(sig, digest, owner.pubkey).
  4. (Recommended, not currently enforced.) Derive cashaddr from owner.pubkey; check it equals owner.cashaddr. This detects a profile that quotes a pubkey unrelated to its claimed owner cashaddr.
  5. Load addresses into the address list.

Failure mode: any check fail → banner warns user, explorer falls back to its default WALLETS. The page never dies silently.

Making a profile (Node CLI)

node scripts/potidaea-explorer-mkprofile.mjs \
  --owner=<keystore-slug> \
  --name="My Wallets" \
  --addresses=a,b,c,d,e,f,g,h,i,gmx,potidaea \
  --out=./my-profile.json
  • --owner is the keystore slug of the wallet whose key signs the profile (from Keys/chipnet-keystore.json).
  • --addresses is a comma-separated list of keystore slugs. Prefix with raw: to include a bare cashaddr that isn't in the keystore (e.g. raw:bchtest:qz…).
  • --network defaults to chipnet if owner's cashaddr starts with bchtest:, else mainnet.

Self-verify:

node scripts/potidaea-explorer-mkprofile.mjs --verify=./my-profile.json

Hosting the profile

Anywhere static-file HTTPS. The signature makes the host untrusted for content-integrity — a compromised host can only drop the file, not alter it.

  • Same VPS as potidaea.deviant.bch — reuse Caddy/nginx. Drop the JSON under /profiles/<name>.json, share the URL.
  • IPFS — pin the JSON, share the gateway URL (ipfs.io/ipfs/<cid>) or publish under an IPNS name.
  • GitHub gist / GH Pages — public raw URL.
  • Local test — serve the folder with any static server; the explorer accepts http://localhost:... URLs.

v2 — encrypted profile (deviant-explorer-profile/2)

Purpose. v1 is signed-public — anyone with the URL sees the address list. v2 is encrypted at rest — the URL is public but the address list is only readable by someone holding the owner's private key (or a signature the owner's wallet produced from a fixed challenge).

Format

{
  "spec": "deviant-explorer-profile/2",
  "network": "chipnet",
  "name": "my chipnet wallets",
  "created": "2026-08-13T00:00:00Z",
  "owner": { "cashaddr": "bchtest:qz…" },   // no pubkey — encrypted profiles reveal only a hint
  "encryption": {
    "algorithm": "AES-256-GCM",
    "kdf": "sha256(signMessage(challenge))",
    "challenge": "deviant-explorer-profile/v2 · bchtest:qz…",
    "iv": "hex12bytes"                      // random 96-bit nonce
  },
  "ciphertext": "hex…"                      // AES-GCM(iv, key, canonicalJson({ addresses: [...] }))
                                            //   auth-tag concatenated per WebCrypto convention
}

The ciphertext, when decrypted, gives:

{ "addresses": [ { "label": "A", "cashaddr": "bchtest:q…", "note": "" }, … ] }

Key derivation

Challenge: deviant-explorer-profile/v2 · <owner_cashaddr> (bytes UTF-8).

BCH signMessage (both sides — wallet or in-browser from WIF):

prefix    = varint(len(magic)) || magic || varint(len(challenge)) || challenge
              where magic = "Bitcoin Signed Message:\n"
digest    = sha256(sha256(prefix))
signature = ECDSA_RFC6979(privkey, digest)   // deterministic 65-byte recoverable

AES key: key = sha256(signature_bytes)

Deterministic — same private key + same challenge always yields the same signature, so the same key decrypts. This is why the challenge includes the owner's cashaddr: it binds the profile to a specific key, and prevents signature-reuse across profiles.

Login paths — both produce the same key

A · Wallet-sign (primary; keeps the private key inside the wallet)

  1. Explorer shows the challenge string.
  2. User pastes it into their BCH wallet's Sign Message screen (Cashonize, Zapit, Paytaca, Deviant Wallet — anything with signMessage).
  3. Wallet returns the base64 signature.
  4. User pastes it back into the explorer.
  5. Explorer base64-decodes → sha256 → AES key → decrypts.

Future upgrade path: Deviant wallet exposes a ?signRequest=<challenge> URL handler that returns via window.postMessage; explorer opens it in a popup and receives the signature automatically. WalletConnect V2 integration is a further upgrade. Both use the same underlying crypto — no format change.

B · WIF paste (fallback for users without a compatible wallet)

  1. Explorer prompts for WIF.
  2. In-browser: decode WIF → derive privkey → run BCH signMessage locally.
  3. Same signature bytes → same AES key → decrypt.

Security note (WIF path). The WIF enters browser JS memory. Malicious extensions can read it. Only use on a device where you trust the browser extension surface. The wallet-sign path avoids this by never exposing the key to the explorer. The WIF path is intentionally labelled as a fallback for advanced users; the primary flow is wallet-sign.

Making an encrypted profile (CLI)

node scripts/potidaea-explorer-mkprofile.mjs \
  --owner=<keystore-slug> --name="My Wallets" \
  --addresses=a,b,c,d,e,f,g,h,i,gmx,potidaea \
  --encrypt \
  --out=./my-profile.json

--encrypt switches the CLI to v2 output. Same options otherwise. The --owner slug provides the private key that becomes the decryption key at the same time — no separate secret to remember.

Self-verify:

node scripts/potidaea-explorer-mkprofile.mjs --verify=./my-profile.json --wif=<owner-wif>

--wif is required to decrypt a v2 profile at verify time.

Wallet-sign challenge format (for wallet vendors)

The challenge is a plain UTF-8 string like:

deviant-explorer-profile/v2 · bchtest:qzulaulvf4v2xe7vzclwwp6mn4flh4kf7gwxkp02dr

Signing must use the standard "Bitcoin Signed Message" prefix (Bitcoin Cash's signMessage / verifyMessage schema — the SAME format Electrum-cash and Bitcoin Cash Node produce). The output is a 65-byte recoverable ECDSA signature, base64-encoded, deterministic under RFC-6979.

Non-goals (this version)

  • No revocation. A signed profile is valid forever; to invalidate, publish a superseding profile at the same URL.
  • No selective disclosure (unlike ADR-0042's holder-distribution privacy). All addresses in the profile are readable by anyone with the URL.
  • No client-side edits. The explorer is view-only; edits require re-signing via the CLI.
  • No BIP-340 Schnorr support. Deferred to a v2 spec if BCH consensus moves toward BIP-340 for its Schnorr surface (currently BCH-Schnorr is incompatible).
  • ADR-0053 — Potidaea business model / holder distribution architecture (this profile primitive is a lightweight cousin of the on-chain DistributionCovenant.cash designed there).
  • Parameters/dApp/Explorer/index.html — the explorer that consumes profiles.
  • scripts/potidaea-explorer-mkprofile.mjs — the signer CLI.
  • Keys/chipnet-keystore.json — where the signer looks up owner keys (per the chipnet-keystore-consolidated design).