零知识证明入门手册2

108 阅读6分钟

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

在前一篇文章中,我们为我们的证明生成了证据。简单来说,是证明者声明自己拥有一部分数据,而且验证者将会进行查询。在这个过程中会发展出一种机制,迫使证明者即使不诚实,至少也要是一致的。

我们希望,我们的协议可以让证明者不能做出假的声明,并且会与声明保持一致。

我们处于哪一步了?

回忆我们的设置(第一篇文章), 证明者声明了满足“分割问题实例”分配的知识。目前我们的协议是:

  1. 验证者选择一个随机数ii

  2. 根据ii的值,验证者从证明中询问两个值(通常是两个连续的值,但有时是第一个和最后一个)。

如果证明者在所有的回答中都是诚实的,并没有欺骗或出错,那么在足够多的查询后,验证者可以被说服。但是等等,如果证明者是诚实的,为什么还会有这个满是查询和应答的复杂游戏?验证者仅需要相信证明者,大家都可以早早回家。

但证明者可能是个骗子

零知识证明存在的全部理由是我们假设证明者可能是不诚实的。因此,假设证明者知道验证者在使用的协议,便会简单地满足验证者。如果验证者询问两个连续的值,证明者将提供两个随机值以满足验证者的期望(对应于问题实例中的值),如果验证者询问第一个和最后一个元素,证明者将发送一个随机值两次。

承诺

这里,我们需要的机制将会:

  1. 强迫证明者在验证者询问前,写下pp中所有元素的值。

  2. 当被询问的时候,强迫证明者从先前写的列表中返回需要的值。

在密码学的世界中,上述机制被认为是承诺方案。

在我们的样例中,我们将使用一个已出现40多年的承诺方案,默克尔树。这是一个简洁巧妙的方案。

默克尔树是一种二叉树(full binary tree),它的每个节点都被分配了一个字符串:

  1. 叶子节点包含承诺的数据的哈希值(例如sha256)
  2. 树中的每一个中间节点都被分配了字符串,字符串值为两个子节点字符串的哈希。

假设我们想要承诺包含4个字符串的列表:["Yes","Sir","I Can","Boogie!"]。默尔克树将会如图所示:

graph TD
1["节点1: h1=hash(h2||h3)"] <--> 2["h2=hash(h4||h5)"]
1["节点1: h1=hash(h2||h3)"] <--> 3["h3=hash(h6||h7)"]
2["h2=hash(h4||h5)"] <--> 4["节点4:h4=hash(Yes)"]
2["节点2: h2=hash(h4||h5)"] <--> 5["节点5:h5=hash(Sir)"]
3["h3=hash(h6||h7)"] <--> 6["节点6:h6=hash(I Can)"]
3["节点3: h3=hash(h6||h7)"] <--> 7["节点7:h7=hash(Boogie!)"]

节点4被分配了“Yes”的哈希值,节点5被赋值了“Sir”的哈希值......

每个节点0<i<40<i<4,被分配了节点2i2i和节点2i+12i+1的值的连接后字符串的哈希。

树的根节点(如节点1)被分配的字符串被称为承诺。这是因为即使最底层数据结构微小的改动,也会引发该节点彻底改变。

默克尔树一个更酷的属性是,有人可以证明一个字符串属于最底层的数据,而不用暴露整个数据。

认证路径

假设我要对1977年西班牙声乐二人组Baccara演唱的一首歌进行承诺。这首歌的标题是个秘密(!),但是你可以询问我标题中的某个单词(前面我把“I”和 “Can”放在了同一个叶节点上,但现在让我们忽略这个有趣的地方)。

为了证明我没有在我们的游戏中半路改变歌曲,我发送给你我创建的默克尔树的根节点被分配的哈希值。

你询问我歌曲标题中的第二个单词,我回复"Sir"。

为了证明这个答案与我之前发送你的哈希值是一致的,我也会发送你节点4和节点3的哈希值。这被称为节点5的认证路径(包含了标题中的第二个单词)。

你现在可以检查我没有撒谎,通过:

  • 自己计算节点5的哈希值(通过对单词“Sir”进行哈希)

  • 使用我给你的节点4的哈希值,计算节点2的哈希值

  • 使用我给你的节点3的哈希值,计算根节点的哈希值

  • 比较你计算出的哈希值和我最初发送你的哈希值,如果它们相同,意味着在我最初发送给你承诺,和我回答你歌曲标题中第二个单词的询问之间没有改变歌曲。

可以确认,给定一个字符串S0S_0的Sha256哈希值,很难找到另一个字符串S1S0S_1 \neq S_0拥有相同的哈希值。这意味着不可能在不改变默克尔树根节点哈希的情况下修改底层数据,因此默克尔树可被用作承诺方案。

让我们看些代码

前面提到我们需要一种机制,来对被称为“证据”的数字列表做出承诺,也就是我们第一篇文章中提到的pp

所以我们需要一个简单的类,它的构造函数可以获取一个数字列表作为输入,构建需要的默克尔树,并允许用户获取根节点的哈希值,并获取底层列表中数字的认证路径。

我们也实现了一个函数来验证认证路径,该函数独立于前面的类,这可以通过简单的哈希计算实现。

这是一个简单的实现:

import hashlib
from math import log2, ceil

def hash_string(s):
    return hashlib.sha256(s.encode()).hexdigest()

class MerkleTree:
    """
    A naive 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)))
        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):
        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_merkle_path(root, data_size, value_id, value, path):
    cur = hash_string(str(value))
    tree_node_id = value_id + int(2**ceil(log2(data_size)))
    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

注意:

  • 这是一个二进制树,我们扩展底层数据到2的下一个指数(通过填充0),让其匹配全二进制树的节点的数量
  • 我们将树的根节点存储在索引1而不是0,子节点在索引2、3等。这只是为了索引便利,ii的子节点是2i2i2i+12i+1.
  • 这段代码还需要很多改进优化,因为在这里清晰和简洁更为重要。

关于零知识呢?

细心的观察者会指出,在我们提供节点5的认证路径时,我们也提供了节点4的哈希。

一个想窥探信息的验证者也许会尝试对西班牙声乐二人组Baccara歌曲标题中的大量单词进行哈希,而且,当他获得到我们发送的“节点4的哈希”时,他将会发现一个我们不想暴露的叶节点。

在下篇文章中,我们将会处理零知识的问题,使用一个简单但高效的方法获取零知识默克尔树。

同样的,我们也希望把这些都结合起来生成我们最初想要的证明。