HashThis — Issues & Fixes Report
Nervos CKB Proof-of-Existence · March 2026
Overview
This document covers every bug identified and resolved during the HashThis development cycle. Issues ranged from silent data failures deep in the blockchain service layer to test environment misconfigurations and UI edge cases. Each fix is described in plain terms — what was wrong, why it mattered, and how it was resolved.
1. Verified proofs returning empty transaction hash, block number, and timestamp
Severity: Critical — core feature broken
What happened: After verifying a file, the result screen showed “Awaiting confirmation” for the timestamp and “—” for the block number, even for transactions that had been confirmed on-chain for weeks.
Root cause (part 1): The CKB indexer returns cell objects where the transaction hash lives at cell.outPoint.txHash, not cell.txHash. The code was reading cell.txHash, which was always undefined. Because the frontend checks if (data.txHash) before calling the blocktime endpoint, it never attempted the timestamp fetch at all.
Root cause (part 2): Even when a valid txHash was passed, pollTransactionStatus was checking txResponse.status === "committed" — but @ckb-ccc/core’s getTransaction() actually returns { transaction, txStatus: { status, blockHash } }. The status was nested one level deeper than expected, so the equality check never matched, and the poller ran all 60 attempts (~5 minutes) before timing out on every single verification.
Fix: Changed both verifyHash and getUserProofs in ckb.service.ts to read cell.outPoint?.txHash. Updated pollTransactionStatus to read txResponse?.txStatus?.status and txResponse?.txStatus?.blockHash with a fallback chain for SDK version safety.
File: api-backend/api/hashes/ckb.service.ts
2. Certificate button showing “Generation failed” on valid proofs
Severity: High — confusing UX on every freshly verified proof
What happened: After verifying a file, clicking “Download Certificate” immediately showed “
Generation failed — txHash is required; blockNumber is required; timestamp is required” even though the proof was confirmed on-chain.
Root cause: The certificate validator correctly requires all three fields, but the button was always rendered regardless of whether the data was ready. For transactions still awaiting confirmation, blockTimestamp is empty — which is valid and expected — but the button let users click through to an instant failure rather than communicating why.
Fix: Added a canCertify check before rendering. When txHash, blockNumber, or timestamp are missing, a disabled greyed-out button renders instead with a hover tooltip explaining that a confirmed timestamp is needed. The JSON export button always remains active since its schema explicitly supports a null timestamp.
File: frontend/src/pages/Verify.tsx
3. Block number displaying as “#unknown” instead of a number or “—”
Severity: Medium — visually broken output
What happened: The Verify page displayed Block: #unknown instead of either a real block number or a clean dash.
Root cause: ckb.service.ts used cell.blockNumber?.toString() || "unknown" as a fallback. Since "unknown" is a truthy string, the frontend’s || "" guard didn’t catch it, and it was rendered literally as #unknown.
Fix: Changed both fallback expressions in ckb.service.ts to return "" (empty string). The UI then falls through to display — cleanly.
File: api-backend/api/hashes/ckb.service.ts
4. Valid proofs hidden when getBlockTime threw an error
Severity: High — proofs appeared as errors instead of verified
What happened: If the blocktime fetch failed (e.g. the transaction hadn’t fully propagated to the queried node), the entire verify flow fell into the error state — even though the on-chain proof was valid.
Root cause: The getBlockTime call was inside the outer try/catch block. Any failure there set status = "error" and cleared the result, discarding the valid cell data that had already been found.
Fix: Wrapped getBlockTime in its own inner try/catch. The outer result is now set first, block number is stored as a fallback immediately, and a timestamp fetch failure only shows an amber warning banner — the proof is still shown as verified.
File: frontend/src/pages/Verify.tsx
5. Midnight rendered as “24:00:00” on PDF certificates
Severity: Medium — incorrect timestamp on generated documents
What happened: PDF certificates generated for transactions confirmed at or near midnight showed the time as 24:00:00 instead of 00:00:00.
Root cause: The PDF generator used hour12: false in the toLocaleString options. The h24 hour cycle maps midnight to hour 24 (end of day), not hour 0 (start of day).
Fix: Changed to hourCycle: 'h23', which uses the 0–23 range and correctly renders midnight as 00:00:00.
File: frontend/src/utils/pdfGenerator.ts
6. Test suite failing — document and navigator not defined
Severity: Medium — 8 tests broken, blocking CI
What happened: The proofExport.test.ts suite failed with ReferenceError: document is not defined and ReferenceError: navigator is not defined on all download and clipboard tests.
Root cause (part 1): The test file was running in Vitest’s default Node environment where browser globals don’t exist. The // @vitest-environment jsdom directive was present but wasn’t taking effect reliably in watch mode due to environment caching.
Root cause (part 2): Even with jsdom, vi.spyOn(document, 'createElement') and Object.defineProperty(navigator, 'clipboard', ...) require those globals to already exist as real objects — they can’t create them from scratch.
Root cause (part 3): copyProofToClipboard accessed navigator.clipboard.writeText before entering the try/catch, so when navigator was undefined it threw a ReferenceError that propagated upward rather than being caught and returning false.
Fix: Replaced all browser global usage in the test with (globalThis as any).document = {...} and (globalThis as any).navigator = {...} set in beforeEach and cleaned up in afterEach. This works in any environment. Also added a typeof navigator !== 'undefined' guard in copyProofToClipboard before accessing it.
Files: frontend/src/utils/proofExport.test.ts, frontend/src/utils/proofExport.ts
7. History endpoint crashing in tests — req.headers.host undefined
Severity: Medium — all 24 history tests failing
What happened: Every test in history.test.ts that expected a 400 validation error was instead getting a 500 internal server error.
Root cause: The history.ts handler used new URL(req.url, \https://${req.headers.host}`)to parse query parameters. Test mocks don't populatereq.headers, so req.headers.hostwasundefined`, throwing inside the handler before any validation logic ran. The try/catch caught it and returned 500.
Fix: Reverted to const { userAddress, limit } = req.query — consistent with every other handler in the codebase.
File: api-backend/api/hashes/history.ts
8. Build failing — unused React import
Severity: High — deployment blocked
What happened: The Vercel build failed with error TS6133: 'React' is declared but its value is never read.
Root cause: History.tsx imported React as a default import. The project uses the new JSX transform ("jsx": "react-jsx" in tsconfig.json) which doesn’t require React in scope. With noUnusedLocals: true enabled, TypeScript treats this as a hard error.
Fix: Removed React from the import, keeping only the named hooks: import { useState, useEffect } from 'react'.
File: frontend/src/pages/History.tsx
9. Wrong hex constant for 95 CKB in tests
Severity: Low — test assertion incorrect, masking potential regressions
What happened: A test assertion for the 95 CKB anchor capacity was using the wrong hex value and would have passed even with an incorrect capacity calculation.
Root cause: 95 CKB = 9,500,000,000 shannons = 0x2363e7f00. The test had 0x236223e800, which is a different value.
Fix: Corrected the constant in the test.
File: api-backend/api/hashes/batch.test.ts
10. Wrong import path in batch tests
Severity: Low — tests failed to run entirely
What happened: batch.test.ts couldn’t find ckb.service and crashed at import time.
Root cause: The import path was '../api/hashes/ckb.service.js' — referencing a path relative to the wrong directory.
Fix: Corrected to './ckb.service.js'.
File: api-backend/api/hashes/batch.test.ts
11. Main bundle too large — build warning
Severity: Low — performance issue, not a functional bug
What happened: Vite warned that the main JavaScript bundle was 1.6 MB after minification, well above the 500 KB threshold.
Root cause: jsPDF and qrcode — only needed when a user generates a PDF certificate — were bundled into the main chunk loaded on every page visit.
Fix: Added manualChunks to vite.config.ts splitting jsPDF and qrcode into a separate pdf-vendor chunk loaded lazily on demand. Also split React and CKB vendor code into their own stable chunks for better long-term caching.
File: frontend/vite.config.ts
Summary
| # |
Issue |
File |
Severity |
| 1 |
Empty txHash, blockNumber, timestamp on verified proofs |
ckb.service.ts |
Critical |
| 2 |
Certificate button “Generation failed” on valid proofs |
Verify.tsx |
High |
| 3 |
Block number showing as #unknown |
ckb.service.ts |
Medium |
| 4 |
Valid proofs hidden when blocktime fetch failed |
Verify.tsx |
High |
| 5 |
Midnight rendered as 24:00:00 on certificates |
pdfGenerator.ts |
Medium |
| 6 |
document/navigator not defined in test suite |
proofExport.test.ts, proofExport.ts |
Medium |
| 7 |
History endpoint returning 500 instead of 400 in tests |
history.ts |
Medium |
| 8 |
Build failing — unused React import |
History.tsx |
High |
| 9 |
Wrong hex constant for 95 CKB |
batch.test.ts |
Low |
| 10 |
Wrong import path in batch tests |
batch.test.ts |
Low |
| 11 |
Main bundle 1.6 MB — performance warning |
vite.config.ts |
Low |
中文版 — HashThis 问题修复报告
Nervos CKB 存在性证明 · 2026 年 3 月
概述
本文档涵盖 HashThis 开发周期中发现并解决的所有缺陷。问题涉及区块链服务层中的静默数据错误、测试环境配置问题和 UI 边界情况。每个修复均以通俗语言描述——哪里出了问题、为什么重要、以及如何解决。
1. 已验证证明返回空的交易哈希、区块号和时间戳
严重程度: 严重——核心功能失效
现象: 验证文件后,结果页面显示"等待确认"和"—",即使该交易已在链上确认数周。
根本原因(一): CKB 索引器返回的 Cell 对象中,交易哈希位于 cell.outPoint.txHash,而非 cell.txHash。代码读取的是 cell.txHash,该值始终为 undefined。由于前端在调用 blocktime 接口前会检查 if (data.txHash),因此从未发起时间戳获取请求。
根本原因(二): 即使传入了有效的 txHash,pollTransactionStatus 也在检查 txResponse.status === "committed"——但 @ckb-ccc/core 的 getTransaction() 实际返回 { transaction, txStatus: { status, blockHash } }。状态嵌套了一层,导致判断永远不匹配,轮询器每次都跑完全部 60 次尝试(约 5 分钟)后超时。
修复: 将 verifyHash 和 getUserProofs 中的 cell.txHash 改为 cell.outPoint?.txHash;将 pollTransactionStatus 中的状态读取路径改为 txResponse?.txStatus?.status 和 txResponse?.txStatus?.blockHash。
文件: api-backend/api/hashes/ckb.service.ts
2. 证书按钮对有效证明显示"生成失败"
严重程度: 高——每次验证后都出现令人困惑的 UX
现象: 验证文件后点击"下载证书",立即显示"
生成失败——txHash 为必填项;blockNumber 为必填项;timestamp 为必填项"。
根本原因: 证书验证器正确地要求三个字段都存在,但按钮始终被渲染,无论数据是否就绪。对于仍在等待确认的交易,blockTimestamp 为空是正常且预期的,但按钮让用户点进去后立即失败,而没有说明原因。
修复: 在渲染前添加 canCertify 检查。当 txHash、blockNumber 或 timestamp 缺失时,改为渲染一个禁用的灰色按钮,并附带悬浮提示说明需要已确认的时间戳。JSON 导出按钮始终可用,因为其 schema 明确支持空时间戳。
文件: frontend/src/pages/Verify.tsx
3. 区块号显示为 #unknown 而非数字或"—"
严重程度: 中——输出显示异常
现象: 验证页面显示 Block: #unknown。
根本原因: ckb.service.ts 使用 cell.blockNumber?.toString() || "unknown" 作为回退值。由于 "unknown" 是真值字符串,前端的 || "" 保护没有生效,被直接渲染为 #unknown。
修复: 将 ckb.service.ts 中两处回退表达式改为返回 ""(空字符串),UI 则显示 —。
文件: api-backend/api/hashes/ckb.service.ts
4. getBlockTime 抛出异常时隐藏有效证明
严重程度: 高——证明被误判为错误
现象: 若 blocktime 获取失败,整个验证流程进入错误状态——即使链上证明是有效的。
根本原因: getBlockTime 调用位于外层 try/catch 内,任何失败都会将 status 设置为 "error" 并清除已找到的有效 Cell 数据。
修复: 将 getBlockTime 包裹在独立的内层 try/catch 中。外层结果优先设置,区块号作为回退值立即存储,时间戳获取失败仅显示一个琥珀色警告横幅,证明仍显示为已验证。
文件: frontend/src/pages/Verify.tsx
5. PDF 证书中午夜显示为 24:00:00
严重程度: 中——生成文档中时间戳错误
现象: 在午夜前后确认的交易生成的 PDF 证书显示时间为 24:00:00 而非 00:00:00。
根本原因: PDF 生成器使用了 hour12: false 选项。h24 小时制将午夜映射为第 24 小时(当天结束),而非第 0 小时(次日开始)。
修复: 改用 hourCycle: 'h23',使用 0–23 范围,正确将午夜渲染为 00:00:00。
文件: frontend/src/utils/pdfGenerator.ts
6. 测试套件失败——document 和 navigator 未定义
严重程度: 中——8 个测试失败,阻塞 CI
现象: proofExport.test.ts 中所有下载和剪贴板测试抛出 ReferenceError: document is not defined 和 ReferenceError: navigator is not defined。
根本原因(一): 测试文件在 Vitest 默认的 Node 环境中运行,该环境中浏览器全局变量不存在。// @vitest-environment jsdom 指令在 watch 模式下因环境缓存而未能可靠生效。
根本原因(二): vi.spyOn(document, ...) 和 Object.defineProperty(navigator, ...) 要求这些全局变量作为真实对象已存在,无法凭空创建。
根本原因(三): copyProofToClipboard 在进入 try/catch 之前就访问了 navigator.clipboard.writeText,当 navigator 为 undefined 时会向上抛出 ReferenceError 而非被捕获后返回 false。
修复: 将测试中所有浏览器全局变量的使用替换为在 beforeEach 中设置 (globalThis as any).document = {...} 和 (globalThis as any).navigator = {...},并在 afterEach 中清理。同时在 copyProofToClipboard 中访问 navigator 前添加 typeof navigator !== 'undefined' 检查。
文件: frontend/src/utils/proofExport.test.ts、frontend/src/utils/proofExport.ts
7. History 接口在测试中返回 500 而非 400
严重程度: 中——24 个历史测试全部失败
现象: history.test.ts 中所有期望 400 验证错误的测试均收到 500 内部服务器错误。
根本原因: history.ts 处理器使用 new URL(req.url, \https://${req.headers.host}`)解析查询参数。测试 mock 不填充req.headers,导致 req.headers.host为undefined`,在任何验证逻辑执行前就在处理器内部抛出异常,被 try/catch 捕获后返回 500。
修复: 回退至 const { userAddress, limit } = req.query——与代码库中其他所有处理器保持一致。
文件: api-backend/api/hashes/history.ts
8. 构建失败——未使用的 React 导入
严重程度: 高——部署被阻断
现象: Vercel 构建失败,报错 error TS6133: 'React' is declared but its value is never read。
根本原因: History.tsx 以默认导入方式引入了 React。该项目使用新的 JSX 转换(tsconfig.json 中配置 "jsx": "react-jsx"),不需要 React 在作用域中。启用 noUnusedLocals: true 后,TypeScript 将此视为硬性错误。
修复: 从导入中移除 React,仅保留具名 hooks:import { useState, useEffect } from 'react'。
文件: frontend/src/pages/History.tsx
9. 测试中 95 CKB 的十六进制常量错误
严重程度: 低——测试断言不正确
现象: 95 CKB 锚定容量的测试断言使用了错误的十六进制值,即使容量计算有误也能通过。
根本原因: 95 CKB = 9,500,000,000 shannons = 0x2363e7f00。测试中写的是 0x236223e800,值不同。
修复: 更正测试中的常量值。
文件: api-backend/api/hashes/batch.test.ts
10. batch 测试中导入路径错误
严重程度: 低——测试完全无法运行
现象: batch.test.ts 在导入时找不到 ckb.service,直接崩溃。
根本原因: 导入路径为 '../api/hashes/ckb.service.js'——相对于错误目录进行了引用。
修复: 更正为 './ckb.service.js'。
文件: api-backend/api/hashes/batch.test.ts
11. 主包体积过大——构建警告
严重程度: 低——性能问题,非功能性缺陷
现象: Vite 警告主 JavaScript 包压缩后体积达 1.6 MB,远超 500 KB 阈值。
根本原因: jsPDF 和 qrcode——仅在用户生成 PDF 证书时才需要——被打包进了每次访问页面时都会加载的主 chunk 中。
修复: 在 vite.config.ts 中添加 manualChunks,将 jsPDF 和 qrcode 分割为按需懒加载的独立 pdf-vendor chunk。同时将 React 和 CKB 厂商代码分割为各自独立的稳定 chunk,以获得更好的长期缓存效果。
文件: frontend/vite.config.ts
汇总
| # |
问题 |
文件 |
严重程度 |
| 1 |
已验证证明返回空的交易哈希、区块号和时间戳 |
ckb.service.ts |
严重 |
| 2 |
证书按钮对有效证明显示"生成失败" |
Verify.tsx |
高 |
| 3 |
区块号显示为 #unknown |
ckb.service.ts |
中 |
| 4 |
getBlockTime 失败时隐藏有效证明 |
Verify.tsx |
高 |
| 5 |
PDF 证书中午夜显示为 24:00:00 |
pdfGenerator.ts |
中 |
| 6 |
测试中 document/navigator 未定义 |
proofExport.test.ts、proofExport.ts |
中 |
| 7 |
History 接口在测试中返回 500 而非 400 |
history.ts |
中 |
| 8 |
构建失败——未使用的 React 导入 |
History.tsx |
高 |
| 9 |
95 CKB 十六进制常量错误 |
batch.test.ts |
低 |
| 10 |
batch 测试中导入路径错误 |
batch.test.ts |
低 |
| 11 |
主包体积 1.6 MB——性能警告 |
vite.config.ts |
低 |
@zz_tovarishch @Hanssen @yixiu.ckbfans.bit @xingtianchunyan