跳转到内容

白名单证明

本页的纯哈希与证明构造规则按版本化预售源码核查。提交认购时按认购指南传入目标部署的 PackageVersion。

白名单预售存储一个 32 字节的 Merkle 根及叶节点数量。每个叶节点对应一个钱包,包含其索引和认购上限。钱包通过提交索引、上限和证明参与认购。

leaf = blake2b256( 0x00 || "blast-presale-v1" // the byte 0x00, then ASCII
|| bcs(index: u64) // 8 bytes, little-endian
|| bcs(wallet: address) // 32 bytes
|| bcs(wallet_cap: u64) ) // 8 bytes, little-endian
node(left, right) = blake2b256( 0x01 || left || right )

合约从叶节点向上遍历,初始 i = index。依次处理每个兄弟节点:i 为偶数时计算 node(digest, sibling),为奇数时计算 node(sibling, digest),然后将 i 整除以二。最终结果必须等于根。

  • 证明必须恰好包含 ceil(log2(leaf_count)) 个兄弟节点,每个 32 字节。只有一个叶节点时,证明为空,根即该叶节点。
  • index 必须小于 leaf_count,wallet_cap 必须为正数。
  • 叶节点的 wallet 必须是交易发送者。
  • 合约按提交值处理兄弟节点,因此树构建器自行决定如何处理未配对节点。下方构建器将其与自身配对。

此构建器使用 @noble/hashes 及 @mysten/sui 的 BCS 编码器。

import { blake2b } from "@noble/hashes/blake2.js";
import { bcs } from "@mysten/sui/bcs";
export type Entry = { wallet: string; cap: bigint };
const LEAF_PREFIX = new Uint8Array([0x00, ...new TextEncoder().encode("blast-presale-v1")]);
const hash = (...parts: Uint8Array[]) => {
const bytes = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let offset = 0;
for (const p of parts) {
bytes.set(p, offset);
offset += p.length;
}
return blake2b(bytes, { dkLen: 32 });
};
export const leaf = (index: number, { wallet, cap }: Entry) =>
hash(
LEAF_PREFIX,
bcs.u64().serialize(index).toBytes(),
bcs.Address.serialize(wallet).toBytes(),
bcs.u64().serialize(cap).toBytes(),
);
const node = (left: Uint8Array, right: Uint8Array) => hash(new Uint8Array([0x01]), left, right);
/** Builds every level of the tree. An unpaired node is paired with itself. */
export function buildTree(entries: Entry[]): Uint8Array[][] {
const levels: Uint8Array[][] = [entries.map((entry, i) => leaf(i, entry))];
while (levels.at(-1)!.length > 1) {
const below = levels.at(-1)!;
const above: Uint8Array[] = [];
for (let i = 0; i < below.length; i += 2) above.push(node(below[i], below[i + 1] ?? below[i]));
levels.push(above);
}
return levels;
}
export const root = (levels: Uint8Array[][]) => levels.at(-1)![0];
/** The siblings from the leaf up, in the order the contract consumes them. */
export function proof(levels: Uint8Array[][], index: number): Uint8Array[] {
const siblings: Uint8Array[] = [];
for (let level = 0, i = index; level < levels.length - 1; level++, i = Math.floor(i / 2)) {
const row = levels[level];
siblings.push(row[i ^ 1] ?? row[i]);
}
return siblings;
}

创建预售时,通过 allowlisted_eligibility(root, leaf_count) 发布根及叶节点数量,再向各钱包提供其索引、上限和证明,供应用执行认购。

上限可统一、分档或单独设置。白名单钱包不设最低认购额。