b9e063fe - The explorer: public mode and the three bearer views, 100% unit coverage - #2
Conversation
The repository was a scaffold whose README described the retired batched design: a stream of `BatchInscription`s with `prev_root → new_root` transitions. Neither term appears in the specification; §5.5 renders the stream of nullifier inscriptions with their half-aggregated `(Pkⱼ, Rⱼ)` sets and publisher identities, the accumulator folded from them by first-occurrence, and aggregate counts. Building against the old README would have produced the wrong product at exactly the layer that decides what a stranger may see. The README now describes what the spec does. The app itself is a statically exported Next.js frontend, consistent with the wallet app's tooling: stateless, self-hostable, no keys, no server state. The node base URL comes from an environment variable with no default — a build without it fails rather than assuming which node it points at. Public mode serves the two-layer model of §5.5 and takes its prohibitions literally: the L1 anchor layer (inscriptions with their member sets, TXID, height, confirmations and §3.10 state; the accumulator with its counts; a nullifier membership lookup presented with the caveat that a client checks answers against its own scan) and nothing from the account layer — no amounts, no asset ids or names, no balances, no addresses, no senders or recipients, and no output graph, because an account model has none to draw. A test renders the pages and greps the output for the §5.5-forbidden fields. The fragment routes (`/tx`, `/balance`, `/addr`) parse and validate the Bech32m bearer HRPs (`zkview`, `zkatt`, `zkavk`, `zkbid`) client-side; a wrong prefix is refused per §1.4. Secrets live in the URL fragment only — never in a query, never server-rendered, never on the wire. Decryption and verification behind those routes is the next block; until then they show an explicit not-implemented state instead of an empty success.
The fragment routes from the first increment showed an explicit not-implemented state. They now do the work. `/tx` takes a `zkbid` locator and a `zkview` per-coin capability, fetches the blob, recomputes the content address before trusting a byte (`blob_id == H(ciphertext)`, a mismatch is refused), opens the ZBE per §4.2.1 — chunked ChaCha20-Poly1305 with index-binding AAD, so a flipped chunk, a reordered chunk, a truncated stream and a wrong key each fail as their own named error — and renders the §5.6 checklist through to the anchoring trail, TXID, confirmations and the §3.10 state. The state shown is the state in the data; the explorer classifies nothing itself. `/balance` resolves a `zkatt` handle (SHA-256 of the attestation), runs the §5.7 checks the page can run, and names the one it cannot — the anchor against one's own scan — as an open step instead of wearing a verified badge it did not earn. `/addr` accepts both `zkavk` variants: the 64-byte `ivk ‖ ovk` full view and the 32-byte incoming-only view. With `ivk` alone the outgoing side is labelled as not derivable rather than rendered empty — an absence with a reason, not a blank. Crypto: Poseidon, BIP-340 and Bech32m come from the SDK; the ZBE opening is implemented here against §4.2.1 using the audited noble primitives (`@noble/ciphers` ChaCha20-Poly1305, `@noble/curves` for the ECDH edge, `@noble/hashes` for HKDF/SHA-256) — the one client-side crypto exception the implementation mandate grants the explorer, applied to already-encrypted material a bearer secret unlocks. Secrets stay in the URL fragment, as in the first increment: never in a query, never server-rendered, never on the wire.
Same shape as the sibling repositories: `workflow_dispatch` live, the `pull_request:` trigger commented out verbatim so re-enabling is a deletion, and one `lint-and-build` job carrying exactly the chain the tree is verified with locally — lint, typecheck, tests, build. One step goes beyond the siblings: the build is run a second time without `NEXT_PUBLIC_NODE_BASE_URL`, and the step fails if that build succeeds. The fail-closed property of the configuration — no base URL, no artefact — is part of the contract, so the gate proves it instead of assuming it. Deliberately no `|| true` anywhere in that step: inverting the exit code by hand is the only way the probe cannot go silently green.
The explorer's first increment is in and verified locally, so its gate comes off pause the same way the sibling repos do — the PAUSED note goes and the `pull_request:` trigger returns verbatim, `workflow_dispatch` kept. The base-URL negative probe and the lint/typecheck/test/build chain are exactly what ran under dispatch.
A review found the authorised-mode surfaces claiming results they had not earned, plus real robustness gaps. Both are closed. The confirmation view marked an unauthenticated `present:false` answer as `pass`; it is now `open` — only a verified inclusion is a pass. The creating-pk search scanned the first 200 inscriptions from height 0 and so could not find any recent payment on a longer chain; it now walks the cursor pages to a hit or the end. The balance attestation swallowed a `/v1/info` error and silently downgraded the mandatory network binding to open; it now propagates the error and fails closed. The address view built an empty history in production while its positive test injected fixtures directly — the route now names its unresolved state instead of rendering an empty-looking history, and an entry with no `K_tx`/ciphertext is `unresolved`, not `recovered`. The CoinProof decoder validated widths only; it now checks digest canonicity and curve points and recomputes `asset_id` from the terms before rendering a pass or an asset name. The wire parsers rejected nothing: negative, fractional and unsafe integers and arbitrary strings passed where u64/u32 and 32-byte hex were required. They now validate per field, with `count == nullifiers.length`. `max_blob_bytes` from `/v1/info` is parsed and enforced on Content-Length, streamed bytes and decoded inline size, so an untrusted body cannot exhaust browser memory. Public mode names itself a paginated subset rather than a complete stream. The crypto tests gain independent known-answer vectors so an equally-wrong HKDF/nonce/AAD on both sides cannot stay green. The CI workflow checks out and builds the sibling SDK. README separates public (node endpoints) from bearer (fragment/blob) data and says "decoded/unverified" while checks are open. The dead `expectPresent` re-export is gone.
…ccept canonical node JSON A second review pass found correctness and reachability gaps behind the bearer views. The address view was only reachable from a test that constructed it directly; the panel never routed to it, so production users could not open it. It is now wired through BearerRoutePanel and exercised through the component. A `completed` verdict was not bound to the full nullifier pair and could stand on a wrong `R_create`/`R'_create`; the CoinProof point check also skipped `R'_create`. Confirmation now requires both nullifiers and both create points to validate, and a mismatched point can no longer read as completed. An invalid `zkavk` scalar is rejected fail-closed on the address-view path instead of flowing into ECDH. The hardened REST parsers rejected the canonical u64 JSON the node actually emits; the parser and the public-chain fixture now use the real wire shape. The bearer size limit is checked before the large allocation rather than after, and a fragment change resets stale bearer state instead of leaving the previous route's data on screen. The CI lint/typecheck steps now receive the same dummy origin the build step uses, so the fail-closed NEXT_PUBLIC_NODE_BASE_URL gate no longer fails those steps; the dedicated negative step still proves the gate.
Review statusThis PR has had four independent review passes (conformance and logic). Over the passes the It stays in Draft for:
|
…peline Add a numbered Playwright E2E spec per screen (public home, accumulator, tx/nullifier/balance/addr bearer routes, authorised-vs-unauthorised panel, error, loading, nav/shell) that asserts real DOM state and captures a visual-regression baseline, mirroring the app repo's conventions. Fixtures build real bearer envelopes (zbeSeal/bech32m/network id) with deterministic stub proof contents, since the explorer only (de)serializes CoinProof. Wire the mechanism end to end: a fail-loud route mock (599 on unmocked node paths, GET-only, no silent empty/absent for unconfigured keys), a handbook assembly script, an nginx handbook image, a per-branch handbook-deploy workflow (develop->DEV, main->PRD; htpasswd from a secret, cloudflared pinned by SHA-256), a container-pinned regenerate-visual-baselines workflow (branch routed via env to avoid shell injection), and a PR-gate e2e job in CI. Pin @playwright/test to 1.62.1 to match the runner image. Deterministic screenshots: fixed viewport, frozen time, animations off, and a controlled deferred release for the loading state instead of a wall clock.
Add a vitest v8 coverage gate (100% statements/functions/lines; branches at the same 85% floor the sdk uses) and wire it into the PR-gate CI, then close the gaps with real unit tests across the components, bearer views, bundle codecs, and crypto helpers. A small number of genuinely-unreachable defensive branches carry justified /* v8 ignore */ annotations with the reason inline.
…th -> fail-closed
§5.8 address-view discovery + §5.7 balance: enforce the HARD 'no silent fallbacks' rule.
- B1: fetchInfo errors no longer swallowed in an empty catch -> fail('node_info', ...) (maxBlobBytes error visible).
- B2: skipped candidates (no maxBlobBytes / blob-fetch error) go into unresolvedCandidates ->
the result renders fail('mesh_scan') + historyNotResolvable instead of a false 'no detect_tag matches' (PASS).
- B3: meshScanned only after a successful scanMesh; defaultScanMesh throws on total relay failure instead of [] ->
no false PASS on a complete relay outage.
- B4: fabricated all-null coinId removed (grep 0 hits) -> a missing coin_id goes into unresolvedCandidates.
- Nit balance.ts: subject/asset mismatch -> fatalError, the balance value is no longer returned.
New B1-B4 regression tests + balance-mismatch hardened. Gate green: typecheck, 221 tests.
…mismatch value leak A confirming review of the fail-closed commit surfaced further holes; this closes all of them: - defaultScanMesh now surfaces every incomplete scan through the unresolved-candidate machinery: a relay that fails for any reason (thrown fetch, non-OK status, JSON-parse failure, non-array body) and any malformed array item both become unresolved candidates, so a partial relay outage or a single bad item can no longer render a clean 'no detect_tag matches' pass; a full outage still throws. - mesh_scan status is decided on hasUnresolved independent of noDiscoveries: a mixed resolved+unresolved result fails and marks history not-resolvable instead of passing. - a candidate whose detect_tag cannot be computed (unliftable/tampered epk) becomes an unresolved candidate instead of being silently dropped. - balance-attestation: the decode check no longer carries the balance value on a subject/asset mismatch. - when the node-info fetch and the mesh scan both fail, the node_info failure is surfaced too. New regression tests cover each path.
Review status — updateA further round completed and hardened the review-fixes this branch was carrying locally but had not
Still Draft for the previously-named follow-up — the SDK release cut-over (pinned by branch until |
Raise the vitest gate to 100% statements, branches, functions, and lines. Add tests for remaining bearer and public-mode paths; keep v8 ignores on structurally unreachable sub-expressions only.
Bind incoming and outgoing proofs on blobId, epk, and detectTag. Parse holder locators without silently dropping invalid URLs. Surface historyGap so empty or rejected history is not shown as complete.
The lint job runs prettier --check on the whole tree. These five files were already on the branch and would fail that gate.
The first non-draft E2E run produced these 21 chromium-linux snapshots in the official Playwright container. Firefox needs HOME=/root when that container runs as root.
|
EN: Ready after 21 review passes. DetailsThe last two complete dual reviews on the product tree reported zero findings (R19 on 9f02243, R20 on the Prettier follow-up 0b08597). R21 then reported zero findings on the CI follow-up (Firefox Open comments: none unresolved. Review threads: none. Human-opened threads: none. Mergeability at 3127e9f: Local unit verification: 298 tests, 100% statements/branches/functions/lines. |
The local htpasswd hash is the public Laravel dummy for password, not a deploy secret. CI and baseline regeneration now pin the SDK to a commit SHA until a released package exists.
…ook auth Build and runtime now parse NEXT_PUBLIC_NODE_BASE_URL as a real URL (host required, no credentials/query/hash). Handbook deploy fails if the committed Laravel dummy hash is used as the live password file.
Node's WHATWG parser treats https:///nohost as host nohost. The empty host case is https:///.
https:/// is a WHATWG parse failure, not a successful parse with an empty hostname. The test now accepts either rejection message.
A successful http(s) URL parse never has an empty hostname; WHATWG rejects that input first. Keep the guard, mark it unreachable for v8.
|
EN: DE: Details
|
TaprootFreakAI
left a comment
There was a problem hiding this comment.
Explorer scaffold filled in: public mode plus the three bearer views, fail-closed mapping, unit + e2e green, mergeable into develop. Approving.
|
Approved. CI green, mergeable. I cannot merge from this account (review-only) — please merge when ready. |
EN:
Adds the public explorer and the three bearer views with fail-closed mapping and 100% unit coverage.
Public mode never renders amounts, assets, or addresses.
Fragment secrets stay in the URL hash; missing node data is a visible error.
The node origin must be an absolute http(s) URL.
DE:
Ergaenzt den oeffentlichen Explorer und die drei Bearer-Views mit fail-closed Mapping und 100-Prozent-Unit-Coverage.
Der Public Mode zeigt keine Betraege, Assets oder Adressen.
Fragment-Geheimnisse bleiben im URL-Hash; fehlende Node-Daten sind ein sichtbarer Fehler.
Der Node-Origin muss eine absolute http(s)-URL sein.
Details
Public mode (§5.5) plus authorised/bearer fragment routes. Stateless static export. SDK checkout pinned to 8894cfb until a released package exists. Handbook deploy rejects the committed dummy htpasswd hash.