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>
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):
- Fetch profile JSON.
specmust equal"deviant-explorer-profile/1". - Strip the
signaturefield. Computedigest = sha256(canonicalJson(rest)). - Verify
ECDSA(sig, digest, owner.pubkey). - (Recommended, not currently enforced.) Derive cashaddr from
owner.pubkey; check it equalsowner.cashaddr. This detects a profile that quotes a pubkey unrelated to its claimed owner cashaddr. - Load
addressesinto 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
--owneris the keystore slug of the wallet whose key signs the profile (fromKeys/chipnet-keystore.json).--addressesis a comma-separated list of keystore slugs. Prefix withraw:to include a bare cashaddr that isn't in the keystore (e.g.raw:bchtest:qz…).--networkdefaults to chipnet if owner's cashaddr starts withbchtest:, 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)
- Explorer shows the challenge string.
- User pastes it into their BCH wallet's Sign Message screen (Cashonize, Zapit, Paytaca, Deviant Wallet — anything with signMessage).
- Wallet returns the base64 signature.
- User pastes it back into the explorer.
- 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)
- Explorer prompts for WIF.
- In-browser: decode WIF → derive privkey → run BCH signMessage locally.
- 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).
Related
- ADR-0053 — Potidaea business model / holder
distribution architecture (this profile primitive is a lightweight cousin
of the on-chain
DistributionCovenant.cashdesigned 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 thechipnet-keystore-consolidateddesign).