NukeDraw可验证公平抽签

如何复算抽签结果

先整理名单和规则,再核对每个人公开的秘密,计算随机数,最后得出抽签结果。下面按这个顺序说明。

1. 整理名单

复算需要两份名单:

贡献者按 ID 排序。对排序后的完整 ID—公钥表计算 SHA-256,得到名单摘要 T。后面合并秘密时,也使用这个顺序。

文本候选先排序,再计算候选摘要。数字区间则只记录起点和终点。例如 0~30 包含 31 个数字,不必生成一张文本名单。

排序与名单编码

排序均按 UTF-8 字节比较:从左到右逐字节比较,较小者在前;前面完全相同时,较短者在前。不按拼音、数值或语言习惯排序,也不做 Unicode 归一化。例如文本 ID 的顺序为“1、10、2”。

贡献者的 ID、公钥分别不得重复。将排序后的每项写为 [ID, 小写公钥HEX],整表写成不含多余空白的 JSON 数组。对它的 UTF-8 字节计算 SHA-256,得到 T。JSON 编码约定见页末。

文本候选按上述方式排序,以一个换行符 LF(字节 0a)连接,末尾不加换行,不带 BOM。对这段 UTF-8 文本计算 SHA-256,得到候选摘要。

  • participants:候选是贡献者 ID。
  • custom:使用自定义名单。导入时逐行去掉首尾空白、忽略空行,条目不得重复。
  • range:使用包含两端的整数区间 [a, z],满足 −2³¹ ≤ a ≤ z ≤ 2³¹−1。

候选的位置从 0 编号。文本候选使用排序后的位置;数字区间中,位置 i 对应 a + i,候选数为 z − a + 1。

2. 计算配置摘要 H

把贡献者名单摘要、候选定义和抽签规则写成固定格式的文本,再计算 SHA-256,得到本轮配置摘要 H。

配置文本的完整格式

将下面大括号中的内容替换为实际值,去掉大括号。固定文字保持原样。

nukedraw/config-v2
sha256-ed25519-v1
{贡献者名单摘要 T}
entries-v1
{候选来源:participants、custom 或 range}
{文本候选数量,或数字区间起点}
{文本候选摘要,或数字区间终点}
draw-v1
{抽签方式:random、pick 或 groups}
{参数:random 填 0,pick 填抽取数量,groups 填组数}

共十行,使用 ASCII 编码。每行末尾都有一个 LF,最后一行也不例外;不加 BOM、空格或空行。整数用普通十进制,不带加号或多余前导零;摘要用小写 HEX。

H = SHA256(上述完整文本的 ASCII 字节)

3. 核对承诺

每位贡献者都有一份 32 字节的秘密 s。承诺 c 是用配置摘要 H 和这份秘密计算出的哈希值:

c = SHA256(UTF8("nukedraw/commit-v6") ‖ H ‖ s)

秘密公开后,为每个人重算一次 c,与该人的承诺逐项比较。

4. 计算随机数 R

按第 1 步确定的贡献者顺序,依次拼接全部秘密,再计算 SHA-256:

R = SHA256(UTF8("nukedraw0.5") ‖ H ‖ s₁ ‖ s₂ ‖ … ‖ sₜ)

t 是贡献者人数。这里的顺序取决于 ID,与谁先提交或页面如何排列无关。

5. 计算抽取或分组结果

随机流与洗牌的完整计算步骤

生成随机流

Bᵦ = SHA256(UTF8("nukedraw/draw-v1") ‖ R ‖ U32BE(b))

R 使用原始 32 字节。b 从 0 开始,U32BE(b) 表示将它编码为 4 字节无符号大端整数。每块 B 依次读取八个无符号大端 32 位整数;读完后将 b 加 1,生成下一块。

从 m 项中取一个位置

  1. m = 1 时直接返回 0,不读取随机流。
  2. 否则读取下一个整数 x。如果 x ≥ 2³² − (2³² mod m),舍弃它并继续读取。
  3. 返回首个未被舍弃的 x mod m。mod 表示取余。

打乱候选顺序

设候选数为 n,初始数组为 [0, 1, …, n−1]。使用正向 Fisher–Yates 洗牌:

  1. 从 i = 0 开始,在 n−i 项中按上面的规则取位置,再加上 i,得到 j。
  2. 交换数组中位置 i 和 j 的值。
  3. 将 i 加 1,继续下一步。

抽取 k 项只做前 k 步,按顺序输出前 k 项。也可以用稀疏交换表记录交换,而不创建整个数组,结果相同。

分组时完成整个洗牌,再按顺序切分:每组先分配 ⌊n/g⌋ 项,前 n mod g 组各多一项。⌊ ⌋ 表示向下取整。

抽取数量满足 1 ≤ k ≤ min(n, 65,536);分组满足 1 ≤ g ≤ n ≤ 65,536。最后将位置编号换回对应文本或区间整数。

消息签名与编码约定

核对承诺和揭示的数字签名

消息版本为 signed-v2。把以下七个字段分别编码为 UTF-8,在每个字段前加上它的字节长度(4 字节无符号大端整数),再依次拼接,作为 Ed25519 签名的输入:

  1. nukedraw/message/v2
  2. sha256-ed25519-v1
  3. 完整房间标识;离线时为完整会话标识。
  4. 动作:commit 或 reveal。
  5. 成员公钥的小写 HEX 文本。
  6. 该消息序号的十进制文本。
  7. 按下面格式编码的 JSON 正文。

正文只含四个字段:

  • roundId:轮次标识。
  • id:贡献者 ID。
  • configDigest:配置摘要 H 的 HEX 文本。
  • commitment 或 reveal:承诺或秘密的 HEX 文本。

JSON 的对象键按 UTF-8 字节序排序。序号是 1~2⁵³−1 的整数,同一成员的揭示序号大于承诺序号。将签名和贡献者名单中的公钥从 HEX 解码为 64 和 32 字节,用 Ed25519 验证。

字节、HEX 与 JSON 的写法

公式中的 ‖ 表示直接拼接字节,不加分隔符、填充或末尾零字节。UTF8("…") 表示只编码引号内的文字,不含引号或换行。

秘密、摘要、承诺和 R 都是 32 字节,通常显示为 64 个十六进制字符(HEX)。计算承诺和 R 时,先把 H 和秘密从 HEX 解码;生成抽签随机流时,R 也先解码。签名输入中的公钥 HEX 则按文字编码。

前缀 nukedraw/commit-v6 为 18 字节,nukedraw0.5 为 11 字节。

JSON 不加多余空白,Unicode 保留原字符;双引号、反斜线及控制字符按 JSON 规则转义。摘要和公钥使用小写 HEX。

当前算法版本为 commit-reveal-v6,算法组合为 sha256-ed25519-v1,候选定义为 entries-v1,抽签规则为 draw-v1。