# 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/.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 用官方工具(推荐,不依赖我们的任何代码) ```bash # ① 拿到宪章原文与我们的证明 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 包) ```bash 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. 台账 ```jsonl {"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` —— 与语言无关的通用结构 ```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`:把验证页里那段 `