零知识证明入门手册3

227 阅读10分钟

翻译自:www.shirpeled.com/2018/10/a-h…

零知识默克尔树

在第二篇文章中介绍了默克尔树的巧妙概念,但是我们遇到了默克尔树标准实现的问题,即使它是个强有力的承诺方案,但默克尔树不是零知识的。

事实证明,上述问题可以通过向默克尔树添加另一层来轻松修复。这样,在默克尔树中,每一对子节点包含一个与真实数据有联系的节点,和另一个与随机字符串有联系的节点。

这是我们再一次将真实数据和随机值混合,以获取零知识。

“Yes Sir I Can Boogie!”的数据看上去会是:

graph TD
1["h1=hash(h2||h3)"] <--> 2["h2=hash(h4||h5)"]
1["h1=hash(h2||h3)"] <--> 3["h3=hash(h6||h7)"]
2["h2=hash(h4||h5)"] <--> 4["h4=hash(h8||h9)"]
2["h2=hash(h4||h5)"] <--> 5["h5=hash(h10||h11)"]
3["h3=hash(h6||h7)"] <--> 6["h6=hash(h12||h13)"]
3["h3=hash(h6||h7)"] <--> 7["h7=hash(h14||h15)"]
4["h4=hash(h8||h9)"] <--> 8["h8=hash(Yes)"]
4["h4=hash(h8||h9)"] <--> 9["h9=hash(random_str())"]
5["h5=hash(h10||h11)"] <--> 10["h10=hash(Sir)"]
5["h5=hash(h10||h11)"] <--> 11["h11=hash(random_str())"]
6["h6=hash(h12||h13)"] <--> 12["h12=hash(I Can)"]
6["h6=hash(h12||h13)"] <--> 13["h13=hash(random_str())"]
7["h7=hash(h14||h15)"] <--> 14["h14=hash(Boogie!)"]
7["h7=hash(h14||h15)"] <--> 15["h15=hash(random_str())"]

这得到了预期的效果,因为当证明者必须揭示验证路径时,所有被披露的哈希值都被随机的数据影响,已经与随机的哈希值混合在一起,这提供了零知识。

修复代码

稍加修改上次的类:MerkleTree,可得:

class ZkMerkleTree:
    """
    A Zero Knowledge Merkle tree implementation using SHA256
    """
    def __init__(self, data):
        self.data = data
        next_pow_of_2 = int(2**ceil(log2(len(data))))
        self.data.extend([0] * (next_pow_of_2 - len(data)))
        # Intertwine with randomness to obtain zero knowledge.
        rand_list = [random.randint(0, 1 << 32) for x in self.data]
        self.data = [x for tup in zip(self.data, rand_list) for x in tup]
        # Create bottom level of the tree (i.e. leaves).
        self.tree = ["" for x in self.data] + \
                    [hash_string(str(x)) for x in self.data]
        for i in range(len(self.data) - 1, 0, -1):
            self.tree[i] = hash_string(self.tree[i * 2] + self.tree[i * 2 + 1])

    def get_root(self):
        return self.tree[1]

    def get_val_and_path(self, id):
        # Because of the zk padding, the data is now at id * 2
        id = id * 2
        val = self.data[id]
        auth_path = []
        id = id + len(self.data)
        while id > 1:
            auth_path += [self.tree[id ^ 1]]
            id = id // 2
        return val, auth_path

def verify_zk_merkle_path(root, data_size, value_id, value, path):
    cur = hash_string(str(value))
    # Due to zk padding, data_size needs to be multiplied by 2, as does the value_id
    tree_node_id = value_id * 2 + int(2**ceil(log2(data_size * 2)))
    for sibling in path:
        assert tree_node_id > 1
        if tree_node_id % 2 == 0:
            cur = hash_string(cur + sibling)
        else:
            cur = hash_string(sibling + cur)
        tree_node_id = tree_node_id // 2
    assert tree_node_id == 1
    return root == cur

协议总结

总结下这个理论,证明者证明“满足分割问题的分配”的知识时,所用协议为:

1.证明者产生一个证据(通过第一篇文章中的get_witness方法)

2.证明者从证据创建一个零知识默克尔树,并把树的根节点哈希发送给验证者

3.验证者向证明者发送一个随机数ii

4.如果i<ni<n,证明者将发送给验证者:

4.1证据中iii+1i+1位置处的元素。

4.2证明上述应答与2中发送的根节点一致的认证路径。

5.如果i==ni==n,那证明者发送证据中的第一个和最后一个元素,同样发送认证路径。

6.验证者检查认证路径与默克尔树的根匹配,证明者发送的数字与问题实例匹配,验证第一篇文章中描述的证据的属性(1)和(2)。

7.如果一切正常,验证者通过验证。

证明者撒谎怎么办?

显然,如果所有事都是真的,那么验证者会验证通过(这被称为完整性)。

但如果证明者不诚实呢?验证者发现pp不正确的概率有多大?(这被称为可靠性)。

假设证据除一个地方造假外,其余地方都是真的,这很难发现证据是错的。这意味着,在一个单独的查询中,验证者有1n+1\frac 1 {n+1}的概率揭露证明者的欺骗。

但如果我们设置k=100(n+1)k=100(n+1), 那概率大约为11e1001- \frac 1{e^{100}},这是一个可以确信的概率。

下面将阐述这种确信达到了什么程度:证明者说服验证者相信错误声明的概率,大致等于连续掷一枚硬币12次、并让硬币都是边缘着地的概率。

Fiat-Shamir Heuristic(Fiat-Shamir启发式)

必须承认,让证明者和验证者参与一个如此长的关于询问和应答的交互是很麻烦的。这意味着每当有些事需要证明,两方都必须是可用的、在线的,并准备好进行交互。

幸运的是,Amos Fiat和 Adi Shamir 提出了一个巧妙的方案,被称为Fiat-Shmair启发式。这个方案允许我们执行协议,将前述的多次交互,转换为一个单独的、证明者只需生成一次的长证明,然后世界上的每个人都可以对证明进行检查。

这个启发式是基于对许多协议、尤其是本文提到的协议的观察,即验证者在交互中发送的信息仅仅是随机数。

所以基本要点如下:

  • 证明者会在交互中模拟验证者,但需要用一种“足够”随机、且可以复现的方式给随机数生成器喂种子。
  • 证明者会一个接一个地写下 验证者的询问以及证明者的应答(包含认证路径等所有证据)。写下的模拟交互的文档就是证明。
  • 在足够次数的询问被模拟后,证明者将把这个长证据发送给验证者。
  • 验证者将使用相同的、可复现的随机机制模拟交互,这会说服验证者:证明者询问自己的查询都是真正随机的。

这有欺骗的味道,我承认。证明者询问自己、回答自己、并且把询问和回答的交互发送给验证者。

但哈希函数的特性为我们带来了帮助。就密码学的意图和目的而言,哈希函数就像是随机数生成器。

因此证明者需要模拟第一个查询时,它会将问题实例喂给哈希函数,并用输出的哈希值获得一个随机数(用哈希值 mod n)。

当需要生成第二个查询、以及接下来的后续查询时,证明者将“该时间点已写入的证据”喂给哈希函数,并用哈希值获取随机数。

这需要一个条件,即交互双方的证明者和验证者都赞同,他们使用的哈希函数是随机和可复现的(因为验证者拥有问题实例和证明,问题和证明可以被用作随机数的种子)。

放在一起

这是获取证明和检查证明的代码:

    def get_proof(problem, assignment, num_queries):
    proof = []
    randomness_seed = problem[:]
    for i in range(num_queries):
        witness = get_witness(problem, assignment)
        tree = ZkMerkleTree(witness)
        random.seed(str(randomness_seed))
        query_idx = random.randint(0, len(problem))
        query_and_response = [tree.get_root()]
        query_and_response += [query_idx]
        query_and_response += tree.get_val_and_path(query_idx)
        query_and_response += tree.get_val_and_path((query_idx + 1) % len(witness))
        proof += [query_and_response]
        randomness_seed += [query_and_response]
    return proof

def verify_proof(problem, proof):
    proof_checks_out = True
    randomness_seed = problem[:]
    for query in proof:
        random.seed(str(randomness_seed))
        query_idx = random.randint(0, len(problem))
        merkle_root = query[0]
        proof_checks_out &= query_idx == query[1]
        # Test witness properties.
        if query_idx < len(problem):
            proof_checks_out &= abs(query[2] - query[4]) == abs(problem[query_idx])
        else:
            proof_checks_out &= query[2] == query[4]
        # Authenticate paths
        proof_checks_out &= \
            verify_zk_merkle_path(merkle_root, len(problem) + 1, query_idx, query[2], query[3])
        proof_checks_out &= \
            verify_zk_merkle_path(merkle_root, len(problem) + 1, \
                                 (query_idx + 1) % (len(problem) + 1), query[4], query[5])
        randomness_seed += [query]
    return proof_checks_out

一个真正的运行的证明!

运行如下脚本会返回true、并打印证明。

def test(q):
   problem = [1, 2, 3, 6, 6, 6, 12]
   assignment = [1, 1, 1, -1, -1, -1, 1]
   proof = get_proof(problem, assignment, q)
   print(proof)
   return verify_proof(problem, proof)

这是我们获得的证明(运行"test(4)"仅进行4次查询)

[['f9f3b1e40626e906b03eb9fd5428b2f2f801e8f3c23627fe7e52a645c3f32632', 3, 1, ['1b7f5356d043c6336c6614fcc24cb77f8807cd2f443b1b77e0002be6b96c40b6', 'a412af57af0b88cdb5cb3d0cbfcd739ebcc3c6fe0ac364db9490b4a208803101', '9f358dd0980f35ea86070d0fb12b2f5726857031ef56968005095cdb13e0a6f0', '05066ac05f174f1f98226c40889c566775592ec3807fbe080324447616773e18'], 7, ['cd6ee891c632e07ad468cd602c8d2d935356ca5901b21a75a2719d164a925382', '4cfc41b83cf64e0cf14a0ea8179aa67c6324699557c508dfc424604674805864', '4efb02f72dbc085ead53657647e893f3ceb29c9f81d411dd817f3be512cad632', '6cd4c16c3d5db473280b64f6b3fce54cb4b6810b46331899f4c07f884fd89aae']], ['580bd4db1071906bcd101600baf51d33b9930ba6e26853e85634bf38c0acef92', 6, 16, ['f8b28423de50f3b0cbcf88caacb6d4f6789ba3cecdc7791b38d5bbcd700ecbd2', '5c41ad0b9d813740b516cb61cf9ce06966efcf82e8ee5881ca86d5b18400d03d', 'af38f9a1873b70d113dab45d6312e6d2a7f4afa45a8c82ebe788abf63dd85650', 'a57a3ccb7cbffdf4d346f1ecf10ead43a4ce1e52b51170789698b7aece6c7687'], 4, ['b703a38bb22b758c5c23c08f096b6c3155c56885d57e1280ff521126282fa857', '4e602f00ef1e1de0b733f467de61805f09a1ebee8db72cc64c62dd8d55836de1', 'af38f9a1873b70d113dab45d6312e6d2a7f4afa45a8c82ebe788abf63dd85650', 'a57a3ccb7cbffdf4d346f1ecf10ead43a4ce1e52b51170789698b7aece6c7687']]]

还有件事...

依据霍夫施塔特定律,完成这个系列比我预想的时间长了些。但我依然遗漏了一些重要的事。其中就有:

1.即时优化

2.关于证明的长度、运行时间、现代证明的长度(和运行时间)的讨论。

3.一些简单的调整,由我的同事Starkware, Lior Goldberg建议,来使得协议变成真正的零知识(因为它现在并不是)、并使协议更优雅。

所以,即使我承诺这个系列只有三篇文章,将来仍然会有第四部分。但由于所有的代码都已经在前三篇文章中了,我们把第四部分称为附录。