如何复算抽签结果
先整理名单和规则,再核对每个人公开的秘密,计算随机数,最后得出抽签结果。下面按这个顺序说明。
1. 整理名单
复算需要两份名单:
- 贡献者名单:谁提供随机数,包含每个人的 ID 和公钥。
- 候选名单:实际要抽取的人或事项,也可以是一个数字区间。
贡献者按 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. 计算抽取或分组结果
- 仅随机数(
random):直接输出 R。 - 抽取(
pick):用 R 打乱候选顺序,依次取前 k 项,不重复。 - 分组(
groups):用 R 打乱全部候选,再连续分成 g 组。人数不能均分时,前面的组各多一项。
随机流与洗牌的完整计算步骤
生成随机流
Bᵦ = SHA256(UTF8("nukedraw/draw-v1") ‖ R ‖ U32BE(b))
R 使用原始 32 字节。b 从 0 开始,U32BE(b) 表示将它编码为 4 字节无符号大端整数。每块 B 依次读取八个无符号大端 32 位整数;读完后将 b 加 1,生成下一块。
从 m 项中取一个位置
- m = 1 时直接返回 0,不读取随机流。
- 否则读取下一个整数 x。如果 x ≥ 2³² − (2³² mod m),舍弃它并继续读取。
- 返回首个未被舍弃的 x mod m。mod 表示取余。
打乱候选顺序
设候选数为 n,初始数组为 [0, 1, …, n−1]。使用正向 Fisher–Yates 洗牌:
- 从 i = 0 开始,在 n−i 项中按上面的规则取位置,再加上 i,得到 j。
- 交换数组中位置 i 和 j 的值。
- 将 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 签名的输入:
nukedraw/message/v2sha256-ed25519-v1- 完整房间标识;离线时为完整会话标识。
- 动作:
commit或reveal。 - 成员公钥的小写 HEX 文本。
- 该消息序号的十进制文本。
- 按下面格式编码的 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。