Skip to content

b9e063fe - The explorer: public mode and the three bearer views, 100% unit coverage - #2

Merged
TaprootFreak merged 20 commits into
developfrom
feat/v1-explorer
Aug 17, 2026
Merged

b9e063fe - The explorer: public mode and the three bearer views, 100% unit coverage#2
TaprootFreak merged 20 commits into
developfrom
feat/v1-explorer

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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.

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.
@TaprootFreak TaprootFreak changed the title b9e063fe - feat: explorer public mode against spec §5.5 b9e063fe - The explorer: public mode and the three bearer views (§5) Aug 2, 2026
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.
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Review status

This PR has had four independent review passes (conformance and logic). Over the passes the
address view was wired through to be reachable in production, a completed verdict was bound to
the full nullifier pair and the correct create points (with the CoinProof point check extended to
R'_create), the hardened REST parsers were made to accept the canonical u64 JSON the node emits,
an invalid zkavk scalar is rejected fail-closed, and the size limit is checked before allocation.
CI is green (the fail-closed NEXT_PUBLIC_NODE_BASE_URL gate now covers lint/typecheck too, and
the SDK checkout is pinned to the v1 branch).

It stays in Draft for:

  • SDK release cut-over — the SDK is pinned by branch until @zkcoins/sdk is published.

…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.
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

Review status — update

A further round completed and hardened the review-fixes this branch was carrying locally but had not
yet pushed, closing several false-PASS holes in the §5.8 mesh-scan discovery and §5.7 balance path:

  • Two independent review passes (conformance + logic) plus two confirming passes — the second confirming
    pass surfaced and closed two more adjacent partial-scan holes and one rendering-id collision — ending at
    0 findings.
  • Fixes: every incomplete mesh scan now fails loud instead of rendering a clean "no detect_tag matches" — a
    relay that fails for any reason (thrown fetch, non-OK status, JSON-parse failure, non-array body), a partial
    outage where a surviving relay returns empty, a malformed array item, and a candidate whose detect_tag
    cannot be computed all become unresolved candidates that force mesh_scan = fail and mark the history
    not-resolvable; a full outage still throws. The balance-attestation decode check no longer carries the
    balance value on a subject/asset mismatch; a node-info fetch failure is surfaced even when the mesh scan
    also fails; and the unresolved check ids are collision-proof.
  • Locally green: typecheck, lint (with the fail-closed NEXT_PUBLIC_NODE_BASE_URL gate), 227 tests.

Still Draft for the previously-named follow-up — the SDK release cut-over (pinned by branch until
@zkcoins/sdk is published).

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.
@TaprootFreak TaprootFreak changed the title b9e063fe - The explorer: public mode and the three bearer views (§5) b9e063fe - The explorer: public mode and the three bearer views, 100% unit coverage Aug 12, 2026
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.
@TaprootFreak
TaprootFreak marked this pull request as ready for review August 13, 2026 08:51
@TaprootFreak
TaprootFreak marked this pull request as draft August 13, 2026 09:05
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.
@TaprootFreak
TaprootFreak marked this pull request as ready for review August 13, 2026 09:20
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

EN: Ready after 21 review passes.
DE: Bereit nach 21 Review-Durchläufen.

Details

The 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 HOME=/root plus 21 chromium-linux visual baselines produced by the official Playwright container).

Open comments: none unresolved. Review threads: none. Human-opened threads: none.

Mergeability at 3127e9f: mergeable=MERGEABLE, mergeStateStatus=CLEAN. CI on that head: Lint & Build SUCCESS, E2E Tests SUCCESS.

Local unit verification: 298 tests, 100% statements/branches/functions/lines.

@TaprootFreak
TaprootFreak marked this pull request as draft August 13, 2026 21:16
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.
@TaprootFreak
TaprootFreak marked this pull request as ready for review August 13, 2026 23:01
@TaprootFreak
TaprootFreak marked this pull request as draft August 13, 2026 23:02
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.
@TaprootFreak
TaprootFreak marked this pull request as ready for review August 13, 2026 23:27
@TaprootFreak
TaprootFreak marked this pull request as draft August 13, 2026 23:29
A successful http(s) URL parse never has an empty hostname; WHATWG
rejects that input first. Keep the guard, mark it unreachable for v8.
@TaprootFreak
TaprootFreak marked this pull request as ready for review August 13, 2026 23:40
@TaprootFreak

Copy link
Copy Markdown
Contributor Author

EN:
Ready after 1 review pass on head 452f03d.
Adds the public explorer and the three bearer views with fail-closed mapping and 100% unit coverage.

DE:
Bereit nach 1 Review-Durchlauf auf Head 452f03d.
Ergaenzt den oeffentlichen Explorer und die drei Bearer-Views mit fail-closed Mapping und 100-Prozent-Unit-Coverage.

Details
  • Grok quality + logic: STATUS complete, 0 findings (minors rejected).
  • Codex quality + logic: STATUS complete, 0 findings.
  • Lint & Build and E2E Tests success on 452f03d.
  • mergeable: MERGEABLE. No open review threads.

@TaprootFreakAI TaprootFreakAI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explorer scaffold filled in: public mode plus the three bearer views, fail-closed mapping, unit + e2e green, mergeable into develop. Approving.

@TaprootFreakAI

Copy link
Copy Markdown
Collaborator

Approved. CI green, mergeable. I cannot merge from this account (review-only) — please merge when ready.

@TaprootFreak
TaprootFreak merged commit aebb7bf into develop Aug 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants