Sigrid 宪章 · 西格算网 GLOBAL-GRID

宪章 v1.0 · OpenTimestamps 锚定记录

待比特币确认 证据文件 19638 B
本页是便利渲染。 权威文本是 Sigrid宪章_OTS锚定记录_v1.0.md 原件(19638 字节)。 原件 sha256 = b3f43eb193df83b60fece5508792f4edbedf7bbb126cec20134222978c6d2299 —— 你可以自己 sha256sum 一遍再对照;两者一致才说明你读到的就是我公示的那一份。

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 倒推构造证明,验证

为什么非要做这一步:字节序判断如果错了,将来真拿到升级证明时 --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_DIGESTdigest 已算出,尚未提交任何日历没有证明文件❌ 只能称"已算好哈希"
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 必须被证明是对的才敢用 —— 所以:

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 字符串会被切成多段 ⇒ 拒绝(宁可不给,也不交切错的字节)

错误分型也随之分开:

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: [...]

三个刻意的取舍:

  1. 不阻断启动。缺一个可选通道不该把整站打死;Cloudflare 上一个 throw 会变成 500, 运维只会看到"站点挂了",反而更难定位。
  2. degraded 与 ok 是两个布尔值。ok 说"这次探测成不成",degraded 说"部署齐不齐"。 配了读节点、缺写节点时:ok 可以为 true 而 degraded 仍是 true —— 混成一个就再也分不开。
  3. 默认日志落到 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 决策):

  1. 过渡期条款(最高):附则二只有"自 sha256 首次锚定之时生效",没留生效后的补正窗口。 后果是:一旦锚定,任何补正都会改变 hash ⇒ 等于"新宪章" ⇒ 又要走 90 天公示 ⇒ 一个 16 KiB 的参数就能把修订流程整体卡死。v1.1 必须加:

宪章生效后 90 天内发现的合规缺口(如锚点数量不足、定义不明确), 可通过「公示 14 天 + 锚定修正记录」补正,不视为修订宪章本体,不改变宪章 hash。

这条优先于 16 KiB 参数本身 —— 因为它决定了宪章还能不能被修正。

  1. 把 /api/anchor/verify 的 16 KiB 上限写成资源边界(不是按来源做区别对待)。
  2. 附则三映射表:M1 标为完成,统一 AnchorStatus 词汇。

另记:宪章当前处于"生效但自身不达标"(3.2 要求 ≥2 独立锚点,实际只有 Arweave 一个), 这不是技术债,是合法性缺口。每多一天都在削弱它自己的效力 —— 这也是 OTS 被排为 最高优先级的真实理由,而不是"技术方案排第一"。


记录人:Cursor · 2026-11-11 本记录自身尚未锚定。它记录的是 OTS 证明的存在与状态。