Allowlist proofs
An allowlisted sale stores a 32-byte Merkle root and a leaf count. Each leaf is one wallet with its index and cap. A wallet commits by presenting its index, cap, and proof.
Hashing
Section titled “Hashing”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 )Verification
Section titled “Verification”The contract walks up from the leaf with i = index. For each sibling in order, it computes node(digest, sibling) when i is even and node(sibling, digest) when i is odd, then halves i. The result must equal the root.
- The proof must have exactly
ceil(log2(leaf_count))siblings, each 32 bytes. A single-leaf tree has an empty proof, and its root is the leaf. indexmust be belowleaf_count, andwallet_capmust be positive.- The leaf’s
walletmust be the transaction sender. - The contract takes each sibling as given, so the tree builder chooses how to handle an unpaired node. The builder below pairs it with itself.
A tree builder in TypeScript
Section titled “A tree builder in TypeScript”This builder uses @noble/hashes and the BCS encoder from @mysten/sui.
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;}Publish the root and the leaf count with allowlisted_eligibility(root, leaf_count) when you create the sale, and serve each wallet its index, cap, and proof so your app can commit.
Caps may be equal, tiered, or custom. Allowlisted wallets have no minimum commitment.