@blockula/sdk 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ // Community distribution Merkle trees (docs/GOVERNANCE.md "Merkle tree format"): a port of
2
+ // node/src/governance.rs `distribution_leaf`, `merkle_parent`, `build_distribution` and
3
+ // `root_from_proof`. Claims are checked on chain against the proposal's root, so these must match
4
+ // the node byte for byte; test/unit.ts checks the shared vectors (distribution 7).
5
+ import { blake3 } from "@noble/hashes/blake3.js";
6
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
7
+ const LEAF_DOMAIN = new TextEncoder().encode("BLOCKULA_DISTRIBUTION_LEAF_V1");
8
+ const NODE_DOMAIN = new TextEncoder().encode("BLOCKULA_DISTRIBUTION_NODE_V1");
9
+ /** Most hashes a claim's proof may carry (the node's `MAX_PROOF`). */
10
+ export const MAX_DISTRIBUTION_PROOF = 64;
11
+ const U64_MAX = (1n << 64n) - 1n;
12
+ function u64le(v, what) {
13
+ const n = BigInt(v);
14
+ if (n < 0n || n > U64_MAX)
15
+ throw new Error(`${what} must fit in a u64`);
16
+ const out = new Uint8Array(8);
17
+ new DataView(out.buffer).setBigUint64(0, n, true);
18
+ return out;
19
+ }
20
+ function concat(...parts) {
21
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
22
+ let at = 0;
23
+ for (const p of parts) {
24
+ out.set(p, at);
25
+ at += p.length;
26
+ }
27
+ return out;
28
+ }
29
+ function compare(a, b) {
30
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
31
+ if (a[i] !== b[i])
32
+ return a[i] - b[i];
33
+ return a.length - b.length;
34
+ }
35
+ /**
36
+ * The leaf of `address`'s `amount` (drops) in distribution `id`:
37
+ * BLAKE3("BLOCKULA_DISTRIBUTION_LEAF_V1" ‖ id u64 LE ‖ len(address) u16 LE ‖ address ‖ amount u64 LE).
38
+ * `address` is the bech32m string exactly as the claimant's tx spells it.
39
+ */
40
+ export function distributionLeaf(id, address, amount) {
41
+ const addr = new TextEncoder().encode(address);
42
+ if (addr.length > 0xffff)
43
+ throw new Error("address too long");
44
+ const len = new Uint8Array([addr.length & 0xff, addr.length >> 8]);
45
+ return blake3(concat(LEAF_DOMAIN, u64le(id, "distribution id"), len, addr, u64le(amount, "amount")));
46
+ }
47
+ /** Sorted-pair inner node: BLAKE3("BLOCKULA_DISTRIBUTION_NODE_V1" ‖ min(a, b) ‖ max(a, b)). */
48
+ export function distributionNode(a, b) {
49
+ const [lo, hi] = compare(a, b) <= 0 ? [a, b] : [b, a];
50
+ return blake3(concat(NODE_DOMAIN, lo, hi));
51
+ }
52
+ /**
53
+ * Root and per-entry proofs (hex sibling hashes from the leaf up), entries in input order, in
54
+ * the reference shape: leaves sorted, paired left to right level by level, an odd last node
55
+ * carried up unchanged. Addresses must be distinct and amounts non-zero.
56
+ */
57
+ export function buildDistribution(id, entries) {
58
+ if (entries.length === 0)
59
+ throw new Error("a distribution needs at least one entry");
60
+ const seen = new Set();
61
+ for (const e of entries) {
62
+ if (seen.has(e.address))
63
+ throw new Error(`${e.address} is listed twice`);
64
+ seen.add(e.address);
65
+ if (e.amount <= 0n)
66
+ throw new Error(`${e.address} gets 0`);
67
+ }
68
+ const leaves = entries.map((e, i) => ({ hash: distributionLeaf(id, e.address, e.amount), i }));
69
+ // Like the node's sort of (hash, index) pairs: by hash, then input order.
70
+ leaves.sort((x, y) => compare(x.hash, y.hash) || x.i - y.i);
71
+ let level = leaves.map((l) => l.hash);
72
+ const pos = new Array(entries.length);
73
+ leaves.forEach((l, p) => (pos[l.i] = p));
74
+ const proofs = entries.map(() => []);
75
+ while (level.length > 1) {
76
+ for (let i = 0; i < pos.length; i++) {
77
+ const sibling = pos[i] ^ 1;
78
+ if (sibling < level.length)
79
+ proofs[i].push(level[sibling]);
80
+ pos[i] = Math.floor(pos[i] / 2);
81
+ }
82
+ const next = [];
83
+ for (let k = 0; k < level.length; k += 2)
84
+ next.push(k + 1 < level.length ? distributionNode(level[k], level[k + 1]) : level[k]);
85
+ level = next;
86
+ }
87
+ return { root: bytesToHex(level[0]), proofs: proofs.map((p) => p.map(bytesToHex)) };
88
+ }
89
+ /** The root a leaf and its proof (hex sibling hashes from the leaf up) lead to (hex). */
90
+ export function distributionRoot(id, address, amount, proof) {
91
+ if (proof.length > MAX_DISTRIBUTION_PROOF)
92
+ throw new Error(`a proof has at most ${MAX_DISTRIBUTION_PROOF} hashes`);
93
+ let h = distributionLeaf(id, address, amount);
94
+ for (const s of proof) {
95
+ const sibling = hexToBytes(s);
96
+ if (sibling.length !== 32)
97
+ throw new Error("proof hashes are 32 bytes of hex");
98
+ h = distributionNode(h, sibling);
99
+ }
100
+ return bytesToHex(h);
101
+ }
102
+ /** Whether `proof` leads from `address`'s leaf for `amount` (drops) in distribution `id` to `root` (hex). */
103
+ export function verifyDistributionProof(root, id, address, amount, proof) {
104
+ try {
105
+ return distributionRoot(id, address, amount, proof) === root.toLowerCase();
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }