Spark Program | HashThis Project

Hi @oiclid,我是 Spark 委员会的成员 Hanssen。我无意对新增的技术内容作出反馈,虽然这本来应该是我要做的事情。

AI 是强大的工具,而且我们鼓励开发者们使用 AI 来更高效地实现点子,但它不意味着你可以对你的产品毫无概念。你至少需要知道你在做什么,目标用户希望从你的工作中得到什么,从而在 AI 犯错的时候将其纠正回到它应有的方向上。

委员会成员在每位申请者的成果上都需要投入时间和精力,希望能为申请者提供足够的帮助,将项目以更好的形态推向社区。在 AI 普及的当下,诸如 OpenClaw 的项目已经为使用者们提供了亮眼的成果,在 CKB 社区中也已经崭露头角,而以人工为中继和 AI 沟通只会让所有人感到沮丧。

希望能收到更有意义的更新,祝好。


Hi @oiclid, this is Hanssen from the Spark Committee. I have no intention of providing feedback on the newly added technical content, even though that is technically what I’m here to do.

AI is a powerful tool, and we encourage developers to use it to bring their ideas to life more efficiently. However, this does not mean you can afford to have zero conceptual grasp of your own product. You must, at the very least, understand what you are building and what your target users expect from your work. This is the only way you can steer AI back in the right direction when it inevitably veers off course.

Committee members invest significant time and effort into every applicant’s submission, hoping to provide enough support to push projects into the community in their best possible form. In this era of AI ubiquity, projects like OpenClaw have already delivered impressive results and are making a name for themselves within the CKB community. Acting merely as a “human relay” to communicate with AI only leads to frustration for everyone involved.

I look forward to seeing more meaningful updates. Best regards.

8 Likes

Hi @Hanssen

Thank you for being direc. I am glad for the feedback you provided

I want to be transparent about how I actually used AI in this project, because I think the picture is more nuanced than it may appear. The core architecture, the CKB cell model decisions, the CCC migration, and the debugging process were work I drove myself. Where I used AI was in two specific ways: as a pair programmer to help implement, refactor, and debug code I had already reasoned through, and to polish my written reports and translate them into Chinese for broader accessibility in the CKB community.

That said, your point lands. There were moments (especially during the committee fixes) that I may have leaned on AI more heavily than I should have, and the gaps in my ability to course-correct it quickly enough were visible. That’s on me, and it’s something I’m actively working on.

I’m going back to the fundamentals, iethe CKB cell model, how CCC constructs and signs transactions, and what proof-of-existence actually guarantees cryptographically. I want to reach a point where AI accelerates work I already understand rather than compensating for understanding I haven’t built yet.

I appreciate the committee’s time and I intend to make better use of it going forward.

Best regards

4 Likes

你好 @Hanssen

感谢您的直言不讳,您提供的反馈让我受益匪浅。

我想坦诚地说明我在这个项目中实际使用 AI 的方式,因为我认为实际情况比表面看起来更为复杂。核心架构、CKB Cell 模型的决策、CCC 迁移以及调试过程,都是我自己主导完成的工作。我使用 AI 的方式体现在两个具体方面:一是作为结对编程工具,帮助实现、重构和调试我已经独立思考过的代码;二是润色我的书面报告,并将其翻译成中文,以便在 CKB 社区中获得更广泛的传播。

话虽如此,您的观点确实触动了我。在某些时刻(尤其是在委员会修复阶段),我对 AI 的依赖可能超出了应有的程度,而我在及时纠正其方向方面的不足也暴露无遗。这是我自身的问题,也是我正在积极改进的地方。

我将重新回归基础——深入理解 CKB Cell 模型、CCC 如何构建和签署交易,以及存在性证明在密码学层面究竟能保证什么。我希望达到这样一种状态:AI 加速的是我已经理解的工作,而不是弥补我尚未建立的知识基础。

感谢委员会投入的时间与精力,我会在今后的工作中更好地珍惜这份支持。

此致

4 Likes

感谢你的回复,我相信事情会变得更好。

对于 HashThis,我建议你考虑如何实现用户的文件/哈希存在性证明。我们引入区块链是为了让用户能够在不披露文件内容的情况下,证明自己在某个时间节点之前已经知晓了这份文件。而区块链将哈希打包上链的时间是让这个证明有意义的关键,因此我不认为依赖客户端或者服务器的数据是可以接受的做法。

而对于可持续性问题,我想提醒的是:在 CKB 上储存数据需要占用 CKB 原生代币。这意味着在当下的实现思路下,服务器需要一直锁定自己的原生代币,这显然是不可持续的。

祝好。


Thanks for your reply and I trust that things will improve.

Regarding HashThis, I suggest you consider how to implement the existence proof of user file/hash. We introduced the blockchain so that users can prove they were in possession of a specific file before a certain point in time, without disclosing the file’s content. The moment the blockchain packs the hash into a block is what makes this proof meaningful. Therefore, I do not think relying on client-side or server-side data is an acceptable approach.

As for the sustainability issue, I would like to remind you: storing data on CKB requires occupying native CKB tokens. This means that under the current implementation, the server would need to keep its own native tokens locked indefinitely, which is clearly unsustainable.

Best regards.

6 Likes

@Hanssen, proof-of-existence is operational. I am currently enhancing shareability and discoverability, while transitioning the core logic from the server to on-chain. I have also finalized the user-paid proof implementation; it is included in the most recent build.

5 Likes

@zz_tovarishch @Hanssen @yixiu.ckbfans.bit @xingtianchunyan
It’s now working, and all the issues have been fixed. I’ll share my report later.

4 Likes

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 “:cross_mark: 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/coregetTransaction() 实际返回 { transaction, txStatus: { status, blockHash } }。状态嵌套了一层,导致判断永远不匹配,轮询器每次都跑完全部 60 次尝试(约 5 分钟)后超时。

修复:verifyHashgetUserProofs 中的 cell.txHash 改为 cell.outPoint?.txHash;将 pollTransactionStatus 中的状态读取路径改为 txResponse?.txStatus?.statustxResponse?.txStatus?.blockHash

文件: api-backend/api/hashes/ckb.service.ts


2. 证书按钮对有效证明显示"生成失败"

严重程度: 高——每次验证后都出现令人困惑的 UX

现象: 验证文件后点击"下载证书",立即显示":cross_mark: 生成失败——txHash 为必填项;blockNumber 为必填项;timestamp 为必填项"。

根本原因: 证书验证器正确地要求三个字段都存在,但按钮始终被渲染,无论数据是否就绪。对于仍在等待确认的交易,blockTimestamp 为空是正常且预期的,但按钮让用户点进去后立即失败,而没有说明原因。

修复: 在渲染前添加 canCertify 检查。当 txHashblockNumbertimestamp 缺失时,改为渲染一个禁用的灰色按钮,并附带悬浮提示说明需要已确认的时间戳。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 definedReferenceError: navigator is not defined

根本原因(一): 测试文件在 Vitest 默认的 Node 环境中运行,该环境中浏览器全局变量不存在。// @vitest-environment jsdom 指令在 watch 模式下因环境缓存而未能可靠生效。

根本原因(二): vi.spyOn(document, ...)Object.defineProperty(navigator, ...) 要求这些全局变量作为真实对象已存在,无法凭空创建。

根本原因(三): copyProofToClipboard 在进入 try/catch 之前就访问了 navigator.clipboard.writeText,当 navigatorundefined 时会向上抛出 ReferenceError 而非被捕获后返回 false

修复: 将测试中所有浏览器全局变量的使用替换为在 beforeEach 中设置 (globalThis as any).document = {...}(globalThis as any).navigator = {...},并在 afterEach 中清理。同时在 copyProofToClipboard 中访问 navigator 前添加 typeof navigator !== 'undefined' 检查。

文件: frontend/src/utils/proofExport.test.tsfrontend/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.hostundefined`,在任何验证逻辑执行前就在处理器内部抛出异常,被 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.tsproofExport.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

4 Likes