Skip to content

Latest commit

 

History

History
65 lines (57 loc) · 13.1 KB

File metadata and controls

65 lines (57 loc) · 13.1 KB

ctaz-explorer audit, codex pass, 2026-04-16T01:16:56Z

executive summary

The privacy baseline is close, but the current deployment still logs client IPs and paths through Uvicorn, so the headline "no logs" claim is false despite nginx access_log off and a tight self-only CSP (templates/base.html:62, templates/stake.html:11, templates/why.html:6, /etc/nginx/sites-available/ctaz-explorer:11-13, /etc/systemd/system/ctaz-explorer.service:11,14-15, /root/.zeven/watchers/ctaz-explorer.log:3625-3635). The biggest correctness gap is shieldedvault: the site promises vault matching, badges, and cards, but build_verification() hardcodes vault: None and nothing ever calls lookup_vault() (main.py:90-95, main.py:598-633, templates/verify.html:5,35,39, templates/vaults.html:23, templates/why.html:21). Human-facing error paths also fail ungracefully because HTML 404/503 routes collapse to raw JSON (main.py:190-197, main.py:263-272, main.py:463-472, /root/.zeven/watchers/ctaz-explorer.log:3684-3689). The deployment implicitly depends on 127.0.0.1:8232 for /z because ZCASH_MAINNET_RPC_URL is not set or asserted, although the current live app does appear to hit a distinct mainnet node because the mainnet anchor sample resolves at height 3286631 (/etc/systemd/system/ctaz-explorer.service:7-11, main.py:15,17-37,684-729,802-812, data/zap1-anchors-zcash.json:3-9, /root/.zeven/watchers/ctaz-explorer.log:3626-3629). Direct curl to the public hostname and local RPC sockets was blocked from this sandbox, so live observations here rely on the checked-in nginx/systemd config and the current request log where direct socket probes were impossible.

blockers (must fix before "favorite gadget" standard)

  • [Privacy claim breach, critical, templates/base.html:62; templates/stake.html:11; templates/why.html:6; /etc/systemd/system/ctaz-explorer.service:11,14-15; /root/.zeven/watchers/ctaz-explorer.log:3625-3635, suggested fix: run Uvicorn with --no-access-log, keep nginx access_log off, and log only exceptions or coarse health summaries.]
  • [Shieldedvault verification is advertised but not implemented, high, main.py:90-95; main.py:598-633; templates/verify.html:5,35,39; templates/vaults.html:23; templates/why.html:21; static/style.css:481-509, suggested fix: either implement tx-to-vault matching end to end and render the badge/card, or mark vault matching as a stub everywhere until a tx-addressable schema exists.]
  • [HTML error paths fail ungracefully, high, main.py:190-197; main.py:263-272; main.py:463-472; /root/.zeven/watchers/ctaz-explorer.log:3684-3689, suggested fix: add 404.html and 503.html, register handlers for starlette.exceptions.HTTPException plus generic 500/503 cases, render templates for non-/api routes, and keep JSON only under /api/*.]
  • [Mainnet routing is implicit and a chain mismatch would be silently mislabeled, high, main.py:15,17-37,684-729,802-812; /etc/systemd/system/ctaz-explorer.service:7-11; data/zap1-anchors-zcash.json:3-9; /root/.zeven/watchers/ctaz-explorer.log:3626-3629, suggested fix: set ZCASH_MAINNET_RPC_URL explicitly in the unit and assert expected chain ids on startup before serving / or /z.]
  • [Community-ownership claims are under-backed, medium, README.md:3,17-24,37-39; templates/base.html:62; templates/why.html:6; templates/stake.html:31, suggested fix: add a real LICENSE file, update clone-and-run docs for 18232 plus mainnet envs, and stop claiming MIT or stale LOC counts unless the repo state matches.]

high-leverage improvements (ranked by impact/effort ratio)

  • [Disable app access logging and keep only error logs, impact 5, effort 1, change /etc/systemd/system/ctaz-explorer.service so ExecStart includes --no-access-log; keep nginx access_log off; send exceptions to stderr or structured error logging only.]
  • [Add chain identity assertions and explicit envs, impact 5, effort 2, on startup call getblockchaininfo for both RPC clients, require ctaz-s1 for / and zcash-mainnet for /z, and refuse to serve mismatched nodes rather than hardcoding labels.]
  • [Fix shieldedvault truthfulness, impact 5, effort 2, either wire vault matching into build_verification() and tx rendering or rewrite verify/vaults/why copy to call vault support a stub until the schema is real.]
  • [Cache pool history, impact 4, effort 2, memoize fetch_pool_history(pool_id, tip) for about 30 seconds and share it between /pool/* and /api/pool/*; if tip has not advanced, reuse the cached series.]
  • [Replace raw JSON errors with themed HTML errors, impact 4, effort 2, add 404.html and 503.html; branch on request.url.path.startswith('/api/') inside the handlers; reuse the existing validation text from /verify for malformed txids and heights.]
  • [Ship the discoverability bundle, impact 4, effort 2, add description/canonical/OG/Twitter/robots meta to base.html, a small self-hosted preview image, robots.txt, sitemap.xml, and a physical /favicon.ico.]
  • [Fix Zcash home anchor labels, impact 3, effort 1, apply the same event_label augmentation used in zcash_anchors() before rendering anchors_preview on /z.]
  • [Sync README and operator prose to current behavior, impact 4, effort 1, document ZEBRAD_RPC_URL=18232 and ZCASH_MAINNET_RPC_URL, remove stale LOC claims, and link exact repo/commit targets for protocol claims.]
  • [Tighten search ergonomics, impact 3, effort 1, add maxlength plus spellcheck='false' to the global search input and stop treating any t1*/u1*/zs* prefix as valid enough to redirect without format validation.]

voice + accuracy findings

  • No literal dead internal href or action targets were found in template source: every static target maps to a FastAPI route or the static mount, and the sample mainnet anchor links in the registry resolve live at /z/block/3286631 and /z/tx/98e1... (templates/base.html:8,14-21,25,35-52; templates/home.html:37-49; main.py:263-903; data/zap1-anchors-zcash.json:3-9; /root/.zeven/watchers/ctaz-explorer.log:3626-3629).
  • The copy overstates mainnet parity: home says "the same surfaces run in production on zcash mainnet," but only /z, /z/block, /z/tx, and /z/anchors exist; there are no /z/vaults or /z/events routes (templates/home.html:35-54; main.py:684-825).
  • README and /why both understate code size (~200 loc, 500 lines of python) versus the current repo, where main.py already runs to line 904 (README.md:3; templates/why.html:10; main.py:904).
  • The home banner and /why page drift into self-congratulation and competitor framing: "explorer built in a day" and "no other explorer... because nobody has written it yet" should be replaced with receipts or cut (templates/home.html:15-16; templates/why.html:20).
  • The footer label /api/verify is misleading because its href is a hard-coded all-zero txid that is guaranteed to 404, not a neutral API landing page (templates/base.html:52; main.py:656-663).
  • Several operational claims need sources or links instead of bare prose: the speculative commit d880b65, wallet/src/lib.rs:1923, and the staking-window numbers are asserted but not linked to an exact blob or repo path (templates/params.html:25,30-35; templates/why.html:14,23).
  • External link liveness could not be rechecked from this sandbox because outbound DNS/socket access to third-party hosts was blocked; the target inventory is in templates/base.html:56-62, templates/anchors.html:5, templates/params.html:23,30, templates/vaults.html:5, and templates/why.html:32,50.

performance findings

  • Pool pages are slow by design: each request walks a 200-block window and does getblockhash plus getblock per height, so one page or API hit drives about 400 RPC round trips before rendering (main.py:50; main.py:410-440; main.py:463-515).
  • The pool history work is duplicated between HTML and JSON surfaces with no shared cache, so /pool/orchard and /api/pool/orchard both pay the full cold cost (main.py:463-497; main.py:500-515).
  • Home already uses asyncio.gather, but each of the 10 recent blocks still pays getblockhash then getblock plus get_tfl_block_finality_from_hash; that is a small but real latency budget that could be cached or walked backward from tip hashes (main.py:200-216; main.py:265-276).
  • Static assets are proxied through the generic nginx location to Starlette StaticFiles with no dedicated immutable cache rule for /static/, so frontend cache wins are being left on the table (main.py:13; /etc/nginx/sites-available/ctaz-explorer:16-24).
  • nginx has gzip on, but gzip_types is commented out and there is no brotli config, so compression is likely limited to default HTML types rather than CSS and JSON too (/etc/nginx/nginx.conf:52-59).

meta + discoverability findings

  • base.html only emits charset, viewport, title, icon, and stylesheet; there is no description, canonical, robots, Open Graph, Twitter card, or apple-touch-icon anywhere (templates/base.html:4-8).
  • Crawlers are already asking for robots.txt and getting 404s, so discoverability debt is not theoretical (/root/.zeven/watchers/ctaz-explorer.log:3633, /root/.zeven/watchers/ctaz-explorer.log:3396-3403, /root/.zeven/watchers/ctaz-explorer.log:3556, /root/.zeven/watchers/ctaz-explorer.log:3604).
  • The favicon is data-URI only, which leaves /favicon.ico 404 and even produced a crawler request for the percent-encoded data URI path; ship a real static icon (templates/base.html:7; /root/.zeven/watchers/ctaz-explorer.log:3653-3654, /root/.zeven/watchers/ctaz-explorer.log:3698).
  • Entity titles are decent, but several high-value pages still have generic titles (verify, support, finalizers, protocol parameters, {{ pool }} pool) without chain or site context, which weakens tabs and search snippets (templates/verify.html:1-4; templates/stake.html:1-4; templates/finalizers.html:1-4; templates/params.html:1-4; templates/pool.html:1-5).
  • There is no route or asset for sitemap.xml in the current route set, so search engines only discover pages by crawling links or prior logs (main.py:263-903).

accessibility findings

  • Semantic landmarks are already present with header, nav, main, and footer, which is the right baseline (templates/base.html:11-31).
  • The global search input has no visible label or aria-label, so the control depends entirely on placeholder text; the verify form has better hash-oriented ergonomics than the global search box (templates/base.html:25-27; templates/verify.html:7-9).
  • The sparkline is already accessible enough to start because the SVG gets an aria-label; keep that when refactoring performance (main.py:443-459).
  • Generic link color contrast is weak: #cf9b22 on #faf8f3 is about 2.37:1, and links are mostly un-underlined outside the banner, so color alone is doing too much work (static/style.css:1-13; static/style.css:187-192; static/style.css:248-253).
  • Only the search input gets explicit focus styling; card links and nav links rely on default browser focus, with no :focus-visible treatment for keyboard users (static/style.css:89-93; static/style.css:451-458).

mobile findings

  • The stats and footer grids should collapse reasonably on narrow screens because they use auto-fit with 140px and 150px minimums (static/style.css:107-115; static/style.css:550-557).
  • The header nav has no wrap strategy and uses a single-line flex row with seven links, so narrow screens are likely to crowd or wrap awkwardly before content starts (templates/base.html:11-22; static/style.css:31-54).
  • Tables have no overflow container, so recent-block and pool tables will squeeze rather than scroll cleanly on 360px widths (static/style.css:154-179).
  • Base text size is a sane 15px, so legibility is probably acceptable; the weak point is navigation density, not body copy (static/style.css:17-30).
  • Tap targets are mixed: search/button and stat cards are large, but nav links remain small inline text targets (static/style.css:47-53; static/style.css:72-105; static/style.css:116-123).

the top 5 moves for the gadget delta

    1. Make Orchard dominant on the home page and add a shielded/transparent ratio card. The data is already on the home handler; the change is mostly layout and emphasis (main.py:278-297; templates/home.html:4-12; static/style.css:107-128).
    1. Add a shielded-density stat for the last N blocks by reusing tx_value_flow() across the recent block window and counting blocks with is_shielded true (main.py:239-260; main.py:265-276).
    1. Add a recent shielded-activity feed that surfaces the last block with Orchard actions or Sapling activity, not just the last block chronologically (main.py:239-260; main.py:305-336; templates/home.html:19-33).
    1. Replace the numeric finality gap with a tiny two-line SVG showing tip versus finalized height; the inputs are already present on the home page today (main.py:281-295; templates/home.html:5-7).
    1. Add a /compare page that shows cTAZ and Zcash mainnet side by side. The dual-chain plumbing already exists in CHAINS, rpc_zcash, and the /z handlers, so this is mostly composition work (main.py:15; main.py:17-39; main.py:684-825).