Sigrid 宪章 v1.0 · OpenTimestamps 锚定记录
本文是《Sigrid宪章_v1.0.md》的第二锚点凭证,不是宪章的一部分。 与《Sigrid宪章_锚定记录_v1.0.md》(Arweave 锚点)并列。
0. 一句话状态(先看这个)
DIGESTED —— 已提交日历(3 个独立日历收下),尚无任何比特币事实。
口径说明(2026-11-11 修订):旧文把这一态叫
PENDING,与"还没提交"混在一起, 容易让读者以为"什么都没做"或反过来以为"已经在等出块了"。现统一为三态 (PENDING_DIGEST/DIGESTED/CONFIRMED),定义见 §7。本文正文的PENDING一律读作DIGESTED。
| 锚点 | 状态 | 能否支撑宪章 3.2 |
|---|---|---|
| Arweave(存储型) | ✅ 已完成 | 算 1 个 |
| OTS → Bitcoin(时间型) | ⏳ 仅已提交(PENDING) | 暂不算 |
10|结论:宪章第 3.2 条「至少两个独立锚点」目前仍差半步。
刚提交到日历时,拿到的只有 PendingAttestation —— 它只代表"某个日历收下了这 32 字节",
不包含任何比特币事实。把它说成"已锚定到比特币"就是伪造存证状态(宪章 8.1.3)。
这不是失败,是流程的正常中间态。日历每日把当日提交的 digest 默克尔化, 再把根写进一笔 Bitcoin 交易;聚合通常需数十分钟到数小时,偶尔更久。
1. 已完成的动作
| 20 | 项 | 值 | |
|---|---|---|---|
| 锚定对象 | Sigrid宪章_v1.0.md | ||
| 字节数 | 19499 | ||
| digest(= 文件 sha256) | 024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f | ||
| 与 Arweave 锚定的对象 | 同一份字节、同一个 sha256(两条锚点指向同一内容,这正是"冗余"的含义) | ||
| 证明文件 | ops/arweave/ots/<sha256>.ots(1049 字节) | ||
| 台账 | ops/arweave/ots-ledger.jsonl | ||
| 提交时刻 | 2026-11-11T11:48:06Z(UTC) | ||
| 实际收下的独立日历 | 3 个:alice.btc.calendar.opentimestamps.org、bob.btc.calendar.opentimestamps.org、finney.calendar.eternitywall.com |
30|提交到 5 个入口,其中 a.pool.opentimestamps.org 与 b.pool.opentimestamps.org
重定向到 alice/bob,所以真实独立日历是 3 个而不是 5 个。
(finney 首次因 TLS 瞬时断开失败,重试成功;当时把它记成"日历故障"是过度解读。)
为什么提交给多个日历:任一日历完成聚合,证明就成立;多份 pending 不降低强度, 但避免"押注单一日历运营方"。这与宪章 3.3「不得只依赖单一锚点」是同一个思路。
2. 任何人可独立复核
2.1 用官方工具(推荐,不依赖我们的任何代码)
# ① 拿到宪章原文与我们的证明
curl -L https://arweave.net/raw/CIVyhQSDJKdpFWHFclD0vp-T-na8TnWE2AbP1tNnC_4 -o charter.md
cp ops/arweave/ots/024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f.ots charter.md.ots
# ② 自己算哈希
sha256sum charter.md
# 应输出 024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f
# ③ 看 OTS 证明结构(本阶段应只看到 PendingAttestation)
pip install opentimestamps-client
ots info charter.md.ots
60|# ④ 聚合完成后做真正的链上验证(需要 bitcoind 或公共 API)
ots upgrade charter.md.ots
ots verify charter.md.ots
2.2 用本仓库的零依赖实现(不需要 Python 包)
cd ops/arweave
node ots_anchor.mjs --id 024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f # 只读状态
node ots_anchor.mjs --id 024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f --upgrade # 取升级证明
node ots_anchor.mjs --id 024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f --verify # 对链验证
node ots.test.mjs 里 82 条断言包含 8 项与官方 python-opentimestamps 的逐字节比对,
node ots-interop.test.mjs 做反向验证(官方能读我们产出的字节)。
node ots_livecheck.mjs 是一次性实网自证(需网络):用一个真实比特币区块
(高度 800000)的 merkle root 倒推构造证明,验证
- OTS 的 digest 是区块头的内部序(小端),不反转就对不上展示序;
- 无关的 32 字节必须被拒绝(否则验证形同虚设);
- 不存在的区块高度不许崩,要如实报告
HTTP 404。
为什么非要做这一步:字节序判断如果错了,将来真拿到升级证明时 --verify 会永远失败,
而失败信息会看起来像"日历给了假证明"。用真实区块正面证一次,比事后再排查便宜得多。
80|## 3. 补齐 3.2 的剩余步骤(待办)
| # | 动作 | 判据 |
|---|---|---|
| 1 | 等日历聚合(数小时) | --upgrade 的输出从"尚未聚合"变为含 BitcoinBlockHeaderAttestation |
| 2 | 对链验证 | --verify 返回 verified: true,且区块 merkle root 命中 |
| 3 | 回填本文与 Arweave 记录 | 状态从 PENDING 改为 VERIFIED,写入比特币区块高度与区块时间 |
| 4 | 之后才可对外称 | "宪章 v1.0 已由 Arweave + Bitcoin 双锚点固定" |
在第 2 步通过之前,任何对外表述都只能说"已提交 OTS,待聚合"。
4. 本次工程记录:三个"看起来正常"的真缺陷
这一节留给后来者。三个缺陷的共同形态是:报告成功或报告错误,但方向都是错的。
4.1 attestation payload 的双层长度前缀(把 220 字节的 uri 读成 222)
PendingAttestation 的载荷是两层 varbytes:
\x00 | 8字节tag | varuint(内层blob长) | varuint(uri长) | uri
第一版解析器把外层长度当成了 uri 长度,于是 uri 前多出 1~2 个字节
(bob 的 45 字节 → 前缀字符 -;alice 的 47 字节 → +)。
为什么"往返一致性测试"抓不到它:解析时多读了 2 字节,序列化时又原样写回,
字节完全一致 —— 只有断言语义(uri 长度、是否以 https:// 开头)才能暴露。
为什么最终暴露了:拿多出来的 -https://bob… 去拼 URL,日历回 `400 commitment must be
hex-encoded bytes`。错误信息里完全看不出根因,我一开始还误以为是 commitment 长度问题。
4.2 同一处缺陷还有第二份拷贝
pendingNodes()(升级时用来取 commitment)里也有一份 payload.toString('utf8')。
修好 summarize() 之后升级仍然报 Invalid URL —— 因为它走的是另一个函数。
教训:修一个"解码函数"时,必须 grep 出所有调用同一错误解码的地方, 而不是只修当前报错的那条路径。
4.3 「尚未聚合」被当成「故障」
日历对未聚合的 commitment 回答 HTTP 404 + Pending confirmation in Bitcoin blockchain。
第一版把所有非 2xx 当错误抛出,于是正常的"等几小时"被记成了查询失败。
危害:运维会去追一个不存在的 bug,而真正该做的事(等)被淹没。
现在 getTimestamp() 对该组合返回 null(= 未就绪),与真故障(其它 404 文案、5xx、网络错)严格分开。
4.4 顺带确认的一个事实
pending attestation 可以挂在非 32 字节的 commitment 上(我们这份是 44 字节)。
只有 BitcoinBlockHeaderAttestation 的 msg 才必须是 32 字节的区块 merkle root。
"digest 永远是 32 字节"这个直觉,在 OTS 的中间节点上是错的。
5. 台账
{"at":"2026-11-11T11:48:06.186Z","event":"submit","digest":"024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f","file":"Sigrid宪章_v1.0.md","bytes":19499,"status":"PENDING","pending_uris":["https://alice.btc.calendar.opentimestamps.org","https://bob.btc.calendar.opentimestamps.org"],"accepted":["https://a.pool.opentimestamps.org","https://b.pool.opentimestamps.org","https://alice.btc.calendar.opentimestamps.org","https://bob.btc.calendar.opentimestamps.org"],"failed":[{"calendar":"https://finney.calendar.eternitywall.com","error":"Client network socket disconnected before secure TLS connection was established"}],"proof_bytes":857}
{"at":"2026-11-11T12:0?:??.???Z","event":"add_calendar","digest":"024d16b82c7b49b0eb279239d1f5cf4b2e6ab43f3a74285885c1b83115d2d93f","status_after":"PENDING","accepted":["https://finney.calendar.eternitywall.com"],"proof_bytes":1049}
(中间几条 upgrade 记录如实保留了当时的错误与修复后的对照 —— 台账只增不改。)
6. 与宪章关系的准确表述
当前可用的最诚实表述是:
宪章 v1.0 已完成 Arweave 永久锚定(tx
CIVyhQS…,块 2040013), 并已将其 sha256 提交至三个独立 OpenTimestamps 日历。 OTS 证明目前处于待聚合状态,尚未形成比特币时间戳。 因此宪章第 3.2 条「两个独立锚点」尚未完全达成,预计于聚合完成后达成。
等第 3 节第 2 步通过后,再把上面这段改为达成态。
7. 三态口径(对外唯一说法)
OTS 是异步的。/digest 拿到的只是"日历收下了这 32 字节",要等日历把当天提交聚合成
默克尔根、再把根写进一笔比特币交易,且我们真的取到那个区块比对成功,才算锚定。
所以状态必须分三态,不能把中间态说成终点:
| 阶段 | 含义 | 证明文件里有什么 | 能否对外称"已锚定到比特币" |
|---|---|---|---|
PENDING_DIGEST | digest 已算出,尚未提交任何日历 | 没有证明文件 | ❌ 只能称"已算好哈希" |
DIGESTED | 日历已收下(PendingAttestation) | 只有 pending 节点 | ❌ 宪章 8.1.3 明令禁止 |
CONFIRMED | 证明含 BitcoinBlockHeaderAttestation,且 merkle root 对链验证通过 | 有区块高度 | ✅ 唯一可以说"已锚定"的阶段 |
CONFIRMED 的两个条件缺一不可。只有高度、没验证通过时,程序会报:
阶段:DIGESTED(子态 ATTESTED_UNVERIFIED) —— 已含比特币区块高度,但尚未对链验证
这个"有高度却仍不算 CONFIRMED"的设计是刻意的:日历理论上可能给出一个对不上的高度, 那时既不能报成功(会撒谎),也不能悄悄退回"还在等"(会掩盖问题)—— 必须让人看见 「有高度但验证失败」。
代码位置与如何被锁住
| 位置 | 作用 |
|---|---|
ops/arweave/ots.mjs → STAGE / assess() / statusLine() | 三态判定的唯一来源 |
ops/arweave/ots_anchor.mjs | 只读状态、提交、升级、验证都打三态,不再打旧标签 |
ops/arweave/ots_watch.mjs | 后台守望的 watch_state.json 用 phase/substage 三态词汇 |
ops/arweave/ots.test.mjs | 锁住 assess() 的映射,特别是"有高度未验证 ≠ CONFIRMED" |
任何前端/文档/台账要显示 OTS 状态,只允许读 assess().stage,不要自己拼字符串。
8. 第三方验证:不需要装 ots CLI
宪章 3.2 要的是"任何第三方独立验证"。如果验证必须 pip install opentimestamps-client,
这个锚点就退化成了一句自我声明。所以聚合完成后,ots_export.mjs 会产出两个东西:
8.1 .bundle.json —— 与语言无关的通用结构
{
"schema": "sigrid.ots.verification-bundle/1",
"subject": { "digest_sha256_hex": "024d16b8…" },
"steps": [
{ "op": "sha256" },
{ "op": "append", "arg_hex": "a5c3e1f0…" },
{ "op": "sha256" }
],
"final_digest_hex": "…",
"bitcoin": {
"height": 800000,
"block_hash": "…",
"merkle_root_display": "91f01a00…",
"merkle_root_internal": "55087fab…",
"explorers": ["https://blockstream.info/block-height/800000", "…"]
},
"how_to_verify": { "one_sentence": "把 digest 依次过一遍 steps,最后必须等于区块的 merkle root。" }
}
验证规则只有一句:从 subject.digest_sha256_hex 出发,按 steps 顺序逐步施加操作,
末端的 32 字节必须等于该比特币区块的 merkle root。
操作只有 6 种,任何一种语言十行内能写完:
op | 做什么 |
|---|---|
sha256 | 对当前字节串取 SHA-256 |
sha1 / ripemd160 | 同上(极少出现) |
append / prepend | 拼接 arg_hex 到尾部 / 头部 |
reverse | 字节倒序 |
hexlify | 把当前字节串转成小写 hex 的 ASCII |
8.2 .verify.html —— 双击即验(断网也行)
单文件、自包含:内嵌全部数据 + 一份纯 JS SHA-256(不用 WebCrypto,所以 file:// 也能算)。
打开即显示 PASS / FAIL,并把区块高度、区块哈希、merkle root 摆在页面上,供你在
区块浏览器上肉眼对照;页面还允许你把浏览器上的 Merkle Root 粘回去独立再比一次。
为什么不用 WebCrypto:crypto.subtle 在部分浏览器的 file:// 下不可用,且是异步的。
自己写的 SHA-256 必须被证明是对的才敢用 —— 所以:
ops/arweave/ots_sha256_js.test.mjs:NIST 标准向量 + 200 组随机长度 + 填充边界 (0/55/56/63/64/65/119/120/127/128…)+ 真实宪章 digest 复算,与node:crypto逐位对拍;ops/arweave/ots_export.test.mjs:把验证页里那段<script>取出来求值, 断言"页面重放结果 == CLI 重放结果",并且篡改任意一步必须变 FAIL(防止做成永远 PASS 的摆设)。
8.3 字节序这件事必须写进包里
比特币区块头里 merkle root 是小端(内部序),区块浏览器显示的是大端(展示序),
两者互为字节倒序。包里同时给 merkle_root_internal 与 merkle_root_display,
并记录本次实际命中的是哪一个(matched_order)。
如果不写清这一条,第三方大概率会在"哈希一致但显示不一致"上卡住,然后怀疑存证有问题。
9. multipart 坑:把"还没出块"从"内容被篡改"里救出来
9.1 现象
交易在 mempool、尚未进块时,网关的 GET /raw/{tx} 会把裸数据包一层
multipart/form-data(实测同一份 19499B 宪章变成 19757B,多出 258B 边界与头部)。
出块后才取回裸字节。
于是在未出块窗口里做 sha256 校验必然失败,而旧代码报的是:
取回内容 ≠ 期望产物:19757B(期望 19499B)· ct="multipart/form-data; boundary=…"
9.2 为什么这比"不报"更危险
这句话读起来是内容被篡改。而真实原因是还在等出块。
哈希门禁误报成"内容不对",比"不报"更危险 —— 它会让人怀疑整个存证体系, 而信任一旦崩了很难修。
一个门禁的价值不在于它拦下了什么,而在于它说的话是不是真的。
9.3 修法(ops/arweave/uploader.mjs)
新增纯函数 unwrapMultipart(bytes, contentType):从 content-type 取 boundary,
按 --boundary 切分,校验头部闭合、结束标记存在,取出恰好一个 part 的正文
(并带出该 part 自己的 content-type,供后续判定继续用)。
fetchRaw() 改为:先解包,再算哈希。解包成功后仍由 sha256 裁决(标准没有放宽)。
判据是 fail-closed:
| 情形 | 处置 |
|---|---|
| 恰好 1 个 part | 解包,继续走 sha256 |
| ≥2 个 part | 拒绝:无法判断哪一份是产物,拒绝猜测 |
| 缺 boundary / 头部未闭合 / 缺结束标记 | 拒绝 |
| 正文里恰好出现 boundary 字符串 | 会被切成多段 ⇒ 拒绝(宁可不给,也不交切错的字节) |
错误分型也随之分开:
- 包装体解不开 ⇒ 抛
ARWEAVE_MULTIPART_UNCONFIRMED(专属错误码,可用isUnconfirmedWindow(err)识别),措辞明确是"尚未出块"; - 解包成功但内容真不对 ⇒ 仍然报"
≠ 期望产物",并附注"(multipart 已解包)", 让人知道这次不是格式问题。
ops/arweave/uploader-multipart.test.mjs 27 条断言锁住上述全部分支,其中最关键的两条:
- 「同一份内容,不解包被拒、解包后被接受」——复现事故现场并证明已修;
- 「内容真的不符时,仍然报 ≠ 期望产物」——避免为了消除误报而把真问题一起放过。
10. 部署必配项自检(启动即报,不阻断启动)
DEFAULT_AO_NODE / DEFAULT_AO_PUSH_URL 清空是对的(写死节点必然腐烂)。
但那把"配没配"的责任全推给了运行时:打开面板 → 点一下 → 503 → 才想起来漏了 CF 变量。
apps/global-grid-viz/server/preflight.mjs 把这一步提到构造中间件时:
启动 → 检查 SIGRID_AO_NODES / SIGRID_AO_PUSH_URL → 缺失则 [preflight] WARN(点名缺哪个变量 + 怎么补) → /api/ao/health 返回 degraded: true, config_missing: [...]
三个刻意的取舍:
- 不阻断启动。缺一个可选通道不该把整站打死;Cloudflare 上一个 throw 会变成 500, 运维只会看到"站点挂了",反而更难定位。
degraded与ok是两个布尔值。ok说"这次探测成不成",degraded说"部署齐不齐"。 配了读节点、缺写节点时:ok可以为 true 而degraded仍是 true —— 混成一个就再也分不开。- 默认日志落到
console.warn,不能沿用中间件那个默认空log: Edge 侧传的是log: () => {},自检会被静默吞掉。"加了自检但看不见"比没加更糟。
server/preflight.test.mjs 36 条断言锁住:缺项点名、脏值(相对路径)不算已配置、
构造不抛错、health 带 config_missing、齐全时不下发噪音字段。
11. 待办(更新)
| # | 动作 | 判据 |
|---|---|---|
| 1 | 等日历聚合(后台守望 ots_watch.mjs,每 20 分钟一轮) | watch_state.json 的 phase 变为 CONFIRMED |
| 2 | 确认后自动导出验证包 | 生成 <sha>.bundle.json + <sha>.verify.html(守望者已内置) |
| 3 | 回填本文 §0 与 §6 | 状态改为 CONFIRMED,写入区块高度与区块时间 |
| 4 | 之后才可对外称 | "宪章 v1.0 已由 Arweave + Bitcoin 双锚点固定" |
宪章 v1.1 的优先级已调整(用户 2026-11-11 决策):
- 过渡期条款(最高):附则二只有"自 sha256 首次锚定之时生效",没留生效后的补正窗口。 后果是:一旦锚定,任何补正都会改变 hash ⇒ 等于"新宪章" ⇒ 又要走 90 天公示 ⇒ 一个 16 KiB 的参数就能把修订流程整体卡死。v1.1 必须加:
宪章生效后 90 天内发现的合规缺口(如锚点数量不足、定义不明确), 可通过「公示 14 天 + 锚定修正记录」补正,不视为修订宪章本体,不改变宪章 hash。
这条优先于 16 KiB 参数本身 —— 因为它决定了宪章还能不能被修正。
- 把
/api/anchor/verify的 16 KiB 上限写成资源边界(不是按来源做区别对待)。 - 附则三映射表:M1 标为完成,统一
AnchorStatus词汇。
另记:宪章当前处于"生效但自身不达标"(3.2 要求 ≥2 独立锚点,实际只有 Arweave 一个), 这不是技术债,是合法性缺口。每多一天都在削弱它自己的效力 —— 这也是 OTS 被排为 最高优先级的真实理由,而不是"技术方案排第一"。
记录人:Cursor · 2026-11-11 本记录自身尚未锚定。它记录的是 OTS 证明的存在与状态。