更有野心的是,交易所可以建立一个未经储户同意无法提取储户资金的系统。我们可以尝试探索「不作恶」有职业素养的 CEX 与「无法作恶」却泄漏隐私的低效链上 DEX 之间的界限。这篇文章将深入探讨让 CEX 更加去信任的历史尝试,与其采用技术的局限性,以及一些依赖 ZK-SNARKs 等先进技术的有力手段。
余额表和 Merkle 树:传统的可偿付证明交易所试图用密码学来证明自己没有欺骗用户的最早尝试可以追溯到很久以前。2011 年,当时最大的比特币交易所 MtGox 通过发送一笔移动 424,242 个 BTC 到预先公布地址的交易来证明他们拥有该笔资金。2013 年,大家开始讨论如何解决该问题的另一面:证明用户存款的总规模。如果你证明用户的存款等于 X (负债证明 proof of liabilities),并证明拥有 X 个代币的私钥(资产证明 proof of assets),那么就提供了可偿付证明(proof of solvency):你证明了交易所有足够的资金偿还给储户。" U; A/ f2 ^( U0 l
; R; P0 s; v: L6 [# P7 }/ ~2 j
提供存款证明的最简单方法是公布一个列表。每个用户都可以检查他们在列表中的余额,而且任何人都可以检查完整的列表:(i)每项余额都是非负的;(ii)总额是宣称的金额。
当然,这会破坏隐私,所以我们可以稍微改变一下该方案:发布一个 列表,并私下给用户发送 salt 值。但即使这样也会泄漏余额与其分布。为了保护隐私,我们采用了后续技术:Merkle 树技术。$ q" ~0 z2 D( ^3 k3 `. B5 Q

# X% j9 e. L! P: _' X$ Z/ d
绿色:Charlie 的节点。蓝色:Charlie 收到用于证明的节点。黄色:根节点,向所有人公布
Merkle 树技术会将用户余额表放进 Merkle 总和树。在 Merkle 总和树中,每个节点都是对。底层叶子节点表示各个用户的余额以及用户名的加盐哈希。在每个更高层的节点中,余额是下面两个节点余额的总和,而哈希是下面两个节点的哈希。Merkle 总和证明和 Merkle 证明一样,是一个由叶子节点到根节点路径上所有姐妹节点组成的「分支」。% c6 g" N. R9 E& Y$ P; c- z2 K
首先,交易所会向每个用户发送一份其余额的 Merkle 总和证明。然后,用户能够确定其余额作为总额的一部分而被正确地包含。可以在这里找到简单的示例代码。- r$ {% Z: _( x9 l
- U; d8 s3 k3 ], ]0 ]' q( Q
- # The function for computing a parent node given two child nodes4 v. r6 F# S2 A( ]
- 1 H7 a! M+ l; j4 s5 O, `
- def combine_tree_nodes(L, R):' p" A: j# A& ]# O+ \
- L_hash, L_balance = L. @% [5 B- B- `( h& n, P1 O
- % n* _, O" f: P6 `8 G8 S( o$ m+ q0 X
- R_hash, R_balance = R
- assert L_balance >= 0 and R_balance >= 00 L3 q. f+ W1 T* y
- new_node_hash = hash(
- L_hash + L_balance.to_bytes(32, 'big') +
- R_hash + R_balance.to_bytes(32, 'big')( U3 s p2 h/ m& i* Q# R* v; S
- )0 Y R' y6 @' S
- $ j& R6 Q0 c& V' _7 S; _
- return (new_node_hash, L_balance + R_balance)
- # Builds a full Merkle tree. Stored in flattened form where
- + x j# r4 u$ V+ Z' g
- # node i is the parent of nodes 2i and 2i+1
- def build_merkle_sum_tree(user_table: "List[(username, salt, balance)]"):2 G) N5 C- f9 d; Y
- tree_size = get_next_power_of_2(len(user_table))
- 5 z1 O# ? E& f+ }' h- B! T, L, ~
- tree = (
- [None] * tree_size +
- [userdata_to_leaf(*user) for user in user_table] +
- [EMPTY_LEAF for _ in range(tree_size - len(user_table))]
- ), a0 e! p7 D# A
- for i in range(tree_size - 1, 0, -1): V3 j2 \8 y1 o* o+ v9 j0 I
- 2 ]5 O0 c7 C; j% b U k" v5 }2 i
- tree = combine_tree_nodes(tree[i*2], tree[i*2+1])7 {0 _; O! U. b& W
- " Y- B% O1 ~/ h# ?! X' }1 R. S1 e; r
- return tree
- 3 c. ^, }- V- ]: H+ R I& G
- # Root of a tree is stored at index 1 in the flattened form
- def get_root(tree):
- return tree[1]4 q- S6 f* X2 v
- # Gets a proof for a node at a particular index8 B. r# ]- l' \' i3 l
- def get_proof(tree, index):
- branch_length = log2(len(tree)) - 1
- 3 {$ ]2 r/ e* N3 w7 j [
- # ^ = bitwise xor, x ^ 1 = sister node of x9 s1 n0 g( {8 B w! \* q
- index_in_tree = index + len(tree) // 27 Y$ i- a$ I: O
- + r+ @+ [7 L8 R- O
- return [tree[(index_in_tree // 2**i) ^ 1] for i in range(branch_length)]" \" V! e9 o: w# b
- # Verifies a proof (duh)
- . A: g9 a0 L9 v! f, D6 W7 A( Z4 j! S
- def verify_proof(username, salt, balance, index, user_table_size, root, proof):
- leaf = userdata_to_leaf(username, salt, balance)) i. S$ {6 @# f& j: f1 a6 w
- branch_length = log2(get_next_power_of_2(user_table_size)) - 1
- for i in range(branch_length):
- $ B! Y5 [0 H/ y/ i
- if index & (2**i):5 t T) ]: s- l A" k) v& G
- / l: T% I" V- Z0 `4 K; |
- leaf = combine_tree_nodes(proof, leaf)
- else:5 v, e& h6 ?5 g2 W+ c6 t0 }# ]7 G5 U3 `
- ) y b* K2 r" Z8 a
- leaf = combine_tree_nodes(leaf, proof)2 r. u& ~, L& O) \
- return leaf == root