Welcome to the

World!



🔍 How to contribute?

🚩 Our Vision & Roadmap







📖 What is z2o-k7e?

z2o-k7e is a community-driven project dedicated to collaborative ZKP (Zero-Knowledge Proof) learning and zkp-tutorial writing . In 4 terms zkp co-learning journey since February 2023, we’ve explored & co-learned resources like https://www.zkiap.com, https://zk-learning.org, plonkathon , and 0xPARC halo2 (ongoing), engaging with over 300+ enthusiastic participants!

In this journey, our vibrant community learns collectively, assists one another with q-a, and write zkp-related content collaboratively.

At z2o-k7e, we award bounties to motivate learners for their tech insights, encourage active community maintenance, and promote the organization of ZKP-centric knowledge. Collectively, our mission is to enhance the quality of ZKP Public Goods.

🚀 In 2023, we completed 4 rounds of collaborative learning on different topics of zkp. Now, we plan to accomplish another 4 sessions of zkp collaborative learning in 2024, including advanced topics like plonky3/Nova/zkVM/STARK ….



🚩 Problem Statement:

The world of zkp is riddled with challenges - so many noises, steep entry barriers, convoluted learning processes, an absence of dedicated learning communities, and a deluge of often nebulous information. Through z2o-k7e & the zkp colearning community, we’re committed to confronting and tackling these issues head-on.



🌟 Core Contributors


愿景 Vision

内容层面:随着 zkp 社群共学的进行,建设一系列质量极高的中英文 zkp tutorials

社区层面:

  1. 建设国内社群氛围最好,贡献参与度最高,内容质量极高的 web3 技术社群
  2. 作为中文 zkp 社区的窗口,不断培养和支持社群内 builder 的优质项目,引导大家贡献到国际化社区(如 PSE 等),同时将第一手优质信息共享至社群,引导讨论和积极贡献。

Roadmap

z2o-k7e 内容 Roadmap

z2o-k7e 产品



贡献流程

  1. Github 上 fork 本 Repo
  2. 可以在 ./src/zk-everythingmkdir 一个以自己名字命名的文件夹
  3. src/SUMMARY.md 是前端网站显示的文件组织目录,可以修改该文件,找到一个合适的放置目录,将文章的本地 .md 文件位置链接过去
  4. 正常的 PR 流程
  5. 经老师们审核后领取 Bounty!

文章格式

内容模板

  1. 文章 metadata ,如 「贡献者作者信息 (required)」, 「标签、联系方式 (optional) 」
> 作者: 如 @大壮 https://github.com/dazhuang      
> 标签: 如 halo2, Nova, STARK, Folding schema .... # mdbook 暂不支持 tag 功能
> 时间: 2023-09-10

比如:

作者:大壮

Author: 大壮

  1. 文章开始之前,可以添加 [TOC] 来让 mdbook 自动生成该文章的 Table of contents(目录)
[TOC]
  1. 可添加 admonition block,语法见这里

This will take a while, go and grab a drink of water.

  1. 文章正文(Markdown 格式的正文内容,无需担心 github 糟糕的渲染)

  2. 文章末尾可以列出 「致谢」 & 「参考文献 References」

# References
 - [trapdoor-tech halo2 book](https://trapdoor-tech.github.io/halo2-book-chinese/user/simple-example.html)
 - [icemelon/HaiCheng Shen](https://github.com/icemelon/halo2-examples/blob/master/src/fibonacci/example3.rs)
 - [0xPARC halo2](https://learn.0xparc.org/)

如何添加图片?

  • 推荐直接在 .md 文章同级目录 mkdir ./imgs 目录,mdbook 中直接引用该 imgs 目录相对路径
  • 如果您使用的是 OSS 云存储,则无需考虑图片存储,只需一个 .md 文件即可~

配套代码(optional)

如果文章有对应的实战代码那就再好不过啦!

可以直接 PR 到另一个 Repo,新建一个目录即可。

├── Nova
├── README.md
├── halo2-doc
├── halo2-learn
├── [Your code repo here !!]  # mkdir your code repo here !!
└── zk-everything

这边还没想好怎么放,可能后面位置有改动,不过…反正先放就好了 !

关于 md 渲染

众所周知,Github 网站的 Latex 等渲染功能非常弱鸡,往往需要一些奇技淫巧才能让公式等正常渲染出来。而在本 MDBOOK 中,您完全不需要关注这种伤害身心的问题,不需要给 github markdown 做专门的适配和魔改。在 Obsidian(或者如 Typora 等主流 Markdown 编辑器)里的 .md 文件显示是什么样的,本网站中都可完美无痛渲染!

本地 Dev 预览方法:

$ [安装 Rust]
$ cargo install mdbook mkbook-latex  mdbook-toc
$ mkbook serve --open       # 本地预览

Tips :

  • src/SUMMARY.md 是会在前端组织显示的所有文件目录及其链接
  • 公式测试:可以在 katex.org 测试,大家在 Obisidian notes 里怎么写公式,前端就会咋显示,

(contribution by PR process)

💡 @Demian: 作为 zkp 新人,走了很多弯路,也整理下自己的学习路径和一些参考资料供大家入门。 希望本教程可以帮助减少一些盲目的打击和莫名的痛苦,节省一点点时间

1. 建立对 zkp 的直观理解

① 在纵身潜入 ZKP 的海洋之前,可以先建立对它最直观的理解

  • 安比实验室(郭宇老师)所写的 zkp-intro 是公认目前全网最简洁易懂的 zkp 入门系列(而且还是中文的!!)
  • 前 3 篇需要看懂,不了解的概念就 Google + chatgpt + 社群询问 …
  • Chap 4-5 主要是非交互的 Schnorr 和 CRS、哈密尔顿环路等,看不懂没关系,可以先放一放

2. 最小必要背景知识

在建立了对 ZKP 最直观的了解后,如果你还是打算要学下去的话,那么就开始准备一些最小必要的基础知识吧!

2.1 椭圆曲线 ECC

需要掌握椭圆曲线加密(ECC)原理( 大概用时 30 min)

2.2 基础的群论、数论

初等数论 Number Theory

2.3 密码学基础

需要掌握:群环域概念、循环群、拉格朗日插值

2.4 ZK-SNARK 初识 & 原理:

需要掌握:直观理解循环群在 zk-snark 中是咋用的就可以,具体的算法细节可能要持续往后学,和 PLONK 的算法不断交叉回看才能懂。

推荐 sec-bit ZK-SNARK 的系列文章,也可在微信公众平台搜索,对于初学者先看 Part 1 / 2 就够了。

3. 理论交叉学习,我反复入门啊!

有了以上基础的打底,可以尝试一套体系完整的系列课程:

ZKIAP

ZKIAP 的课程是比较注重理论和实践结合的,第二课就有涉及到 Circom 写电路

zk-learning

理论详实,但是缺少代码实践,session 5 的 PLONK 是 top-down 讲解,搭配郭老师的 理解 PLONK 系列会更佳

crypto notes

土耳其小哥整理的,非常赞的 notes

ProofsArgsAndZK

author: justin thaler

https://zkhack.dev/whiteboard

https://www.rareskills.io/zk-bootcamp


其他优秀的 Courses (随便看看):

1. PSE appliedzkp.org/projects 
2. Rust
3. complaints about learning rust
4. Dan Boneh
5. "The Different types of ZK-EVM" article 
6. who was the first ZK-EVM.
7. ZK Summit – Zero Knowledge Summit
8. Zac Williamson inventing PLONK, running @aztecnetwork
9. @zeroknowledgefm podcast
10. Fiat-Shamir Transformation 
11. The Moon Math Manual https://github.com/LeastAuthority/moonmath-manual
12. ZK-Rollups that provide privacy by default are sometimes called ZK-ZK-Rollups
13. Circom
14. The "Proofs, Arguments, and Zero-Knowledge" book by Justin Thaler https://people.cs.georgetown.edu/jthaler/ProofsArgsAndZK.pdf
15. Brecht – @taikoxyz CTO and @PrivacyScaling contributor
16. ZK HACK
17. Definition wars
18. how one should write zkEVM or ZK-EVM or Zk-EVM or zk-EVM
19. Lagrange interpolation

4. PLONK 协议の奥义

PLONK 无疑是目前最值得学习,需要彻底掌握的协议

《理解 Plonk》

出品:Secbit @郭宇 老师 , https://secbit.io/

是大家公认的全网(包括外网)最好的 PLONK Tutorial。

学习 PLONK,这一套就够了!(论文可以随便看一下)

PS: 如果文档看晕了,那么推荐郭老师的配套白板视频:


PLONK 代码实践

5. 要不…来点代码?

恭喜你来到了 Zero-knowledge 的荒原!下面就可以自己根据兴趣和方向选择一些代码进行研究和实践了

halo2

使用了 halo2 的 Applications:

  • ZK Email https://github.com/zkemail halo2
  • ZK Wordle: https://zordle.xyz/ halo2
  • Hammster: https://github.com/ytham/hammster halo2
  • zk-draw : Verifiable random draw with zero-knowledge of the random seed https://github.com/jae-cuz/zk-draw halo2
  • ZK Microphone: https://github.com/Miyamura80/ZKMicrophone
  • Building a Zero Knowledge web app with Halo 2 and Wasm (part 1)
  • zk-img: Fighting Deepfakes with Zero-Knowledge Proofs https://medium.com/@danieldkang/zk-img-fighting-deepfakes-with-zero-knowledge-proofs-9b76c23e3789 尚未开源

大部分由 @Kurt Pan 博士整理

Circom

使用了 Circom 的 Applications:

  • zkSudoku: https://zk-sudoku.vercel.app/ Circom
  • Tornado-Cash
  • Semaphore

学习路径:【EDITING…】

PSE demos

PSE Projects List : https://www.appliedzkp.org/projects

Semaphore

对于 Circom + ZKP 的代码实践例子比较多,首推 PSE 的 Semaphore,是个包括 zuzalu pass、Worldcoin 都有使用的 zkp 协议

Others:

【out of date】,请移步: zk Materials

Tools

Hello ZKP

👩‍💻 作者: Jade 🔗 github仓库:awesome-zkp-learning

Introduction

如果你刚入门ZKP,个人推荐从视频课程开始,有老师带着学习,会更容易上手,并且课程也是比较系统的,能对ZKP有一个大致的把握。后续可以深入理论、项目、论文等等。ZKP和密码学、区块链紧密相关,因此这里也推荐了一些相关课程和书籍,而深入密码学又会发现和数学相关,特别是抽象代数、数论的知识,只能说前路漫漫,道阻且长,希望不会劝退你。

本文推荐资料概览如下:

xmind

本文的资料推荐完全是鉴于我个人的学习路径,是从我个人角度的一些推荐。每个人的专业背景与学习方法都有所不同,因此仅供参考。不管怎样,能从这里有所收获都是我莫大的荣幸。

闻道有先后,术业有专攻。我也一直在学习的路上,难免有所不足与错误,欢迎批评指正,与我讨论。

Contents

ZKP Courses

探索零知识证明系列 - 郭宇

如果你想通过博客文章来入门学习ZKP,强烈推荐郭宇老师的系列文章。相信很多人入门ZKP都是从这里开始的(至少我是😂)。推荐按照顺序来进行阅读,同时里面提到的一些概念可以结合 ZKP MOOC(见下一个推荐)中的第一讲Introduction and History of ZKP来进行学习,基本都有对应,不过ZKP MOOC中讲得更理论些。

ZKP MOOC - Zero Knowledge Proofs

如果你想系统了解ZKP,或者刚入门ZKP,这门课程强烈推荐。通过这门课程的学习,你会对ZKP有很深入的理解,同时课程涉及面也比较广。每节课的讲义都非常不错,值得反复回顾与学习。课程官网还给出了每节课的补充资料,可以延伸拓展。

Modern Zero Knowledge Cryptography - MIT IAP 2023

如果你想敲敲代码来学习ZKP,非常推荐这门课程,可以跟着课程学习Circom语言,自己动手写写电路约束。该课程还有课后作业,推荐自己做一做(我的作业是跟着ZK Shanghai 2023课程(可以看作该课程的中文版)做的,在下一条推荐中有我的作业链接,供参考)。

ZK Shanghai 2023 - Icer

如果你觉得直接看上一个推荐的英文课程Modern Zero Knowledge Cryptography有点难度,推荐看这个课程,可以理解为中文版。同时这门课程在讲解过程中也加入了老师的理解,有很多补充和扩展。课程的第7讲第8讲邀请了陆晨博士来讲解,比较偏数学一些,但其中的FFT算法在ZKP中应用还比较多,如果不好理解可以找其他一些资料来补充学习。作为ZKP入门,可以先尝试去理解,后续用到再深入进行研究。

WTF-zk

如果你想了解ZKP的数学原理,这门教程是不错的选择,讲解了ZKP中用到的抽象代数的知识,同时结合python代码,能边学习理论边用编程进行实践。

Blockchain

了解区块链也有助于理解ZKP的应用场景。

区块链技术与应用 - 肖臻

🔗 bilibili课程视频

如果你想深入了解区块链,非常推荐这门课程,课程由浅入深,讲了比特币和以太坊底层原理。

Cryptography Courses

密码学系列课程 - lynndell

这门课程从密码学的常见算法讲起,再讲到零知识证明。每一节课都很硬核,老师的讲义非常棒,值得自己反复研读,强烈推荐。

密码学基础系列

ECDSA多签系列

zk系列

Cryptography I - Dan Boneh

如果你觉得上面推荐的课程密码学系列课程还不够过瘾,强烈推荐这门课程,同时推荐读读这门课程的讲义,非常全面,讲义中的证明比较多,前期可以选择跳过。

Plonk

对ZKP有一个大致的了解后,可以具体来学学一些证明系统,首推Plonk。

理解Plonk系列 - 郭宇

如果你想深入理解Plonk,强烈推荐郭宇老师的这一系列文章。有的文章中会涉及较多的数学公式,推荐自己跟着文章手写推导一遍(或者更多),由于这些置换证明、算术约束、拷贝约束、查表约束等会在很多证明系统中反复用到,因此这里打下扎实的基础还是非常有必要的。

Halo2

在学习了Plonk之后,就可以开始看看Halo2。官方教程The halo2 Book可以作为学习手册进行参考。下面推荐一些不错的课程。

Halo2 - 0xPARC

强烈推荐跟着这门课程来入门Halo2。不仅有理论的讲解,也有编程实践,课上跟着老师敲敲代码,课后再自己独立实现下,或者改改代码实现不同的约束,相信会对Halo2有更深入的理解。

Halo2 - StarLi

这一系列的课程也可以作为上面推荐课程Halo2 - 0xPARC 的补充。

ZKEVM

ZKEVM或者ZKVM是一个非常庞大的项目,个人认为可以从一些介绍视频入手,有个大致的了解,再进行深入代码细节。(👀我还刚刚接触一点,下面是我看到的不错的资料,这里简单的做一些推荐,想更深入学习ZKVM或者ZKEVM,建议另外找更全面的资料)

ZKP Books

Proofs, Arguments, and Zero-Knlowledge - Justin Thaler

关于ZKP的书籍,很多人首推这本书。(👀我还未细看这本书,后续看完补充更详细的描述)

The MoonMath Manual

这本书还是比较全面,涵盖初等代数、抽象代数、椭圆曲线、电路以及 ZKP 的知识,尽可能地不涉及过多的数学理论,同时又和实践进行结合,非常推荐。

Mathematics Books

Algebra

高等代数 - 丘维声

强烈推荐丘老师的这本教材,有上下两册,通过这本书一步一步自然地引入了群、环、域的概念,对入门抽象代数很有帮助。网上也有丘老师的课程视频,可以结合着学习。

抽象代数 - 张贤科

如果你想看抽象代数的中文教材,我觉得这本很不错,带你从群环域到伽罗瓦群,书中也有部分提到ZKP中常用的有限域,但更多还是整个抽象代数的理论知识,对深入理解有限域有很多帮助。如果想要深入研究有限域,推荐阅读有更细化的书籍(下面在Finite Fields中有对应推荐)。

A Book of Abstract Algebra - Charles C. Pinter

如果你想看抽象代数的英文教材,推荐这本。还是比较推荐英文资料,这样在看到一些英文术语时能够直接对应上。

Algebra A Graduate Course

这本作为一些学校的本科教材,也比较推荐。

Finite Fields

ZKP中的大部分证明系统都是基于有限域来进行计算的,因此很有必要深入学习下有限域的理论知识。下面先推荐一些大家都说还不错的书籍。(👀由于我还未细读,后续再补充上这些书籍的描述与区别)

Finite Fields - Rudolf Lidl, Harald Niederreiter, P. M. Cohn

Handbook of Finite Fields - Gary L. Mullen, Daniel Panario

Introduction to Finite Fields and their Applications - RUDOLF LIDL, HARALD NIEDERREITER

Applications of Finite Fields - IanF.Blake, XuHong Gao, Ronald C. Mullin, et al

Cryptography Books

图解密码技术

这本书非常适合密码学入门,图解系列的书籍都比较易懂。

Foundations of Cryptography

这本书有两卷,第I卷是 Basic Tools,第II卷是 Basic Applications。涵盖的内容非常全面,在第I卷的第4章就讲到了ZKP。(👀还未细看这本书,后续看完补充更详细的描述)

Handbook of Elliptic and Hyperelliptic Curve Cryptography - ODED GOLDREICH

(👀还未细看这本书,后续看完补充更详细的描述)

Coding Theory

在FRI中,涉及到Reed-Solomon编码,因此如果要研究这些证明系统相关的细节,就比较有必要学习编码理论相关知识。

Essential Coding Theory

这本书非常推荐,编码理论讲得非常深入。

ZKP Resources

这里推荐一些不错的ZKP学习资源。

探索零知识证明系列

探索零知识证明系列作者:郭宇@Secbit: Founder of Secbit, https://github.com/sec-bit , https://secbit.io/

原链接:https://github.com/sec-bit/learning-zkp/

初识「零知识」与「证明」

探索零知识证明系列(一)

引言:

我认为区块链很难称为一个“技术”。它更像是一个领域,包罗万象。或者形而上地说,区块链更像一个有机体,融合了各种不同的理论技术。

零知识证明是构建信任的重要技术,也是区块链这个有机体中不可缺少的一环。

零知识证明是打通链上数据与链下计算的关键技术,也是实现链上数据隐私保护的重要途径

要解释「零知识证明」,我们需要先解释「证明」,然后解释什么是「知识」,最后再解释什么是「零知识」。

提醒:文章内容难免有不准确或不严谨的描述,还请各位专业人士拨冗指正。

本文将在 Github 进行更新与修正。

“证明” 的前世今生

什么是证明?很多人可能和我一样,看到这两个字,会不禁想起中学考卷中各种三角相似的几何图形,当老师在神奇地画出一条辅助线后,证明过程突然显而易见,然后会懊悔自己为何没想到。

古希腊:「证明」 == 「洞见」

数学证明最早源于古希腊。他们发明(发现)了公理与逻辑,他们用证明来说服对方,而不是靠权威。这是彻头彻尾的「去中心化」。自古希腊以降,这种方法论影响了整个人类文明的进程。

勾股定理的证明

上图是「勾股定理」的巧妙证明。历史上曾出现过许许多多精巧的证明,神奇的思路,天才的灵感。一旦一个命题被证明,上帝都无能为力。嗯,对了,还有那个「上帝不是万能的」证明:上帝不能造出一块他举不起来的石头。

一个数学证明往往暗藏无比深刻的「洞见」,相信很多人都看过「费马大定理」的故事[1],这个定理证明横跨四百年,从费马写下「这里空间太小,我写不下」,到怀尔斯最终登顶,耗费了许多代人的聪明才智。最近如「彭加莱猜想」,稍微带点年代感的如「哥德巴赫猜想」,还有我非常敬佩的华裔科学家张益唐十年磨一剑,在仔细研究了「Goldston-Pintz-Yıldırım」和 「Bombieri-Friedlander-Iwaniec.」的证明「洞见」之后,证明了「质数间的有界间隔」[2]。

自十七世纪,莱布尼茨起,人们就梦想找到一种机械的手段,可以来自动完成证明,而不再依赖天才的灵光一现。

二十世纪初:「证明」 == 「符号推理」

时间到了十九世纪末,康托、布尔、弗雷格、希尔伯特、罗素、布劳威、哥德尔等人定义了形式化逻辑的符号系统。而「证明」则是在利用形式化逻辑的符号语言编写的推理过程。逻辑本身靠谱么?逻辑本身「自恰」吗?逻辑推理本身对不对,能够证明吗?这让 数学家/逻辑学家/计算机科学家 发明(发现) 了符号系统,语法 vs. 语义,可靠 vs. 完备,递归 vs. 无穷。(这部分精彩故事请参看『逻辑的引擎』一书[3])。

1910年,罗素发表了洪(zhuan)荒(tou)巨著『数学原理』。在书中,罗素与怀特海试图将数学完整地「形式化」下来。如果能达到这样的目标,所有的数学成果都将以证明的方式建立在坚实的基础上。下图就是『数学原理(卷二)』中的一页:

其中110.643这是一个命题:「1+1=2」,然后接下来就是这个定理的证明。大家可能奇怪,难道 1+1 还需要证明吗?是的,在数学原理一书中,数字 0,1,2,…… 都有严格定义,「加法」、「乘法」、「等于」都要严格定义,然后每一步的推理都需要指出依据。证明意味着什么?证明是可能繁琐无比的、但是每一步推理都严格无误。书中大量的证明都机械式的,按照公理和推理规则进行一种证明的构造,寻找证明就好像可以交给一个人,然后他无脑在公理与推理规则的集合中进行机械查找。

似乎人们距离「定理的自动证明」并不遥远了。

不幸的是,哥德尔在 1931 年证明了「哥德尔不完备性定理」[4],图灵在 1936 年证明了图灵机停机问题的不可判定性[5]。这些成果彻底终结了这个几百年的幻想。无论公理系统如何精巧设计,都无法抓住所有的真理。

证明不仅仅是一个严格推理,而且凝结了似乎很难机械化的创造性思维。证明中蕴含了大量的「知识」,每一次的突破,都将我们的认知提升到一个新的高度。不管是「洞见」,还是推理过程中所构造的「算法」,一个定理的证明的内涵往往远超出定理本身的结论。

六十年代:「证明」 == 「程序」

又过了半个世纪,到了六十年代,逻辑学家 Haskell Curry 和 William Howard 相继发现了在「逻辑系统」和「计算系统— Lambda 演算」中出现了很多「神奇的对应」,这就是后来被命名的「Curry-Howard Correspondence」。这个发现使得大家恍然大悟,「编写程序」和「编写证明」实际在概念上是完全统一的。而在这之后的 50 年,相关理论与技术发展使得证明不再停留在草稿纸上,而是可以用程序来表达。这个同构映射非常有趣:程序的类型对应于证明的定理;循环对应于归纳;……(这里推荐一本书:『软件基础』(Software Foundations 中译本)[6])。在直觉主义框架中,证明就意味着构造算法,构造算法实际上就是在写代码。(反过来也成立,嗯,码农码的不是代码,是数学证明,:P)

目前在计算机科学领域,许多理论的证明已经从纸上的草图变成了代码的形式,比较流行的「证明编程语言」有 Coq,Isabelle,Agda 等等。采用编程的方式来构造证明,证明的正确性检查可以机械地由程序完成,并且许多啰嗦重复性的劳动可以由程序来辅助完成。数学理论证明的大厦正在像计算机软件一样,逐步地构建过程中。1996 年 12 月 W. McCune 利用自动定理证明工具 EQP 证明了一个 长达 63 年历史的数学猜想「Ronbins 猜想」,『纽约时报』随后发表了一篇题为「Computer Math Proof Shows Reasoning Power」的文章[7],再一次探讨机器能否代替人类创造性思维的可能性。

利用机器的辅助确实能够有效帮助数学家的思维达到更多的未知空间,但是「寻找证明」仍然是最有挑战性的工作。「验证证明」,则必须是一个简单、机械、并且有限的工作。这是种天然的「不对称性」。

八十年代:「证明」 == 「交互」

时间拨到1985年,乔布斯刚刚离开苹果,而 S. Goldwasser 博士毕业后来到了 MIT,与 S. Micali,Rackoff 合写了一篇能载入计算机科学史册的经典:『交互式证明系统中的知识复杂性』[8]。

GMR89

他们对「证明」一词进行了重新的诠释,并提出了交互式证明系统的概念:通过构造两个图灵机进行「交互」而不是「推理」,来证明一个命题在概率上是否成立。「证明」这个概念再一次被拓展。

交互证明的表现形式是两个(或者多个图灵机)的「对话脚本」,或者称为 Transcript。而这个对话过程,其中有一个显式的「证明者」角色,还有一个显式的「验证者」。其中证明者向验证者证明一个命题成立,同时还「不泄露其他任何知识」。这种就被称为「零知识证明」。

再强调一遍,证明凝结了「知识」,但是证明过程确可以不泄露「知识」,同时这个证明验证过程仍然保持了简单、机械,并且有限性。这听上去是不是有点「反直觉」?

交互式证明

Alice: 我想向你证明我有一个方程的解,w^3 - (w+1)^2 + 7 = 0 (方程的解:w=3

Bob: 好啊,我听着呢

Alice: 但是我不会告诉你 x 具体是多少,除非你愿意掏钱,我才告诉你。

Bob: 可以啊,但是你要先证明你有方程的解,我再给钱你。

Alice: @#%^& (黑科技)

Bob: ?????? (黑科技)

Alice: &*#@! (黑科技)

Bob: ??????(黑科技)

…… (继续黑科技)

Alice: 好了,完了

Bob: 好吧,你确实有方程的解,不过是不是我掏了钱,你就会把答案告诉我?

Alice: 别废话,掏钱!

上面例子就是一个「交互式证明」。假设Alice知道方程的解, f(w) = 0,那么 Alice 如何让 Bob 确信她知道 w 呢?Alice 在 「黑科技阶段」 告诉了 Bob 一大堆的信息。好了,关键问题是,Bob 能不能从 Alice 所说的一大堆信息中猜出w 到底是几,或者能分析出关于 w 的蛛丝马迹呢?如果 Bob 有这个能力,Bob也许就没必要掏钱了,因为他已经获得了这个值钱的信息。

请注意,如果 Alice 与 Bob 的对话是 「零知识」 的,那么 Bob 除了知道 wf(w)=0 的解之外,不能获取其它任何关于 w 的信息。 这一点非常重要,这是保护 Alice 的利益。

现在回顾一下「零知识证明」这个词,英文叫 「Zero-Knowledge Proof」 。这个词包含三个关键部分:

  • 知识
  • 证明

各位可能已经有点感觉了,我们来尝试着解读一下:

  • 零: Alice 泄露了关于 w 的「零」知识,也就是没有泄露知识。
  • 知识:这里就是指的就是 w
  • 证明:就是Alice与Bob对话中的「黑科技部分」。

好了,证明也就是黑科技部分还没讲。看官们不要急,且听我慢慢道来。

零知识证明有什么用处?

一提零知识证明技术,很多人就想到了匿名 Coin,比如 Monero, 比如 ZCash。确实,这几个 Coin 很好地普及了零知识证明,我本人也是通过 ZCash 才第一次听说了零知识证明这个词。但是在更深入地了解这个技术之后,深深感觉这个技术的威力远不止这一点。

零知识证明技术可以解决数据的信任问题,计算的信任问题!

张三说他有100块钱,李四说他北大毕业,王五说要和八菲特共进午餐。空口无凭,Show me the proof

show-me-the-proof

那么「零知识证明」能解决数据的信任如何理解呢?在上一篇文章『zkPoD: 区块链,零知识证明与形式化验证,实现无中介、零信任的公平交易』[9]里面,我提到了一个概念「模拟」:

零知识证明技术可以「模拟」出一个第三方,来保证某一个论断是可信的

换句话说,当我们收到一个加了密的数据, 然后还有一个零知识证明。这个零知识证明是说 「关于数据的 X 断言成立」,那么这等价于有一个天使在我们耳边悄声说,「关于数据的X 断言成立」!

trusted-party

对于这个 X 断言,可以非常灵活,它可以是一个 NP复杂度的算法。大白话讲只要我们能写一段程序(一个多项式时间的算法)来判断一个数据是否满足 X 断言,那么这个断言就可以用零知识证明的方式来表达。通俗点讲,只要数据判定是客观的,那么就零知识证明就适用。

零知识证明的一些用处:

  • 数据的隐私保护:在一个数据表格中,多多少少都有一些信息不想被暴露,比如当年我的成绩单,我只想向人证明,我的成绩及格了,但是我不想让别人知道我到底考了61分还是62分,这会很尴尬。我没有心脏病,但是保险公司需要了解这一点,但是我不想让保险公司知道我的隐私信息。那我可以证明给保险公司看,我没有心脏病,但是病历的全部并不需要暴露。我是一家企业,我想向银行贷款,我只想向银行证明我具备健康的业务与还款能力,但是我不想让银行知道我们的一些商业秘密。
  • 计算压缩与区块链扩容:在众多的区块链扩容技术中,Vitalik 采用 zkSNARK 技术能够给现有的以太坊框架带来几十倍的性能提升。因为有了计算的证明,同样一个计算就没必要重复多次了,在传统的区块链架构中,同样的计算被重复多次,比如签名的校验,交易合法性校验,智能合约的执行等等。这些计算过程都可以被零知识证明技术进行压缩。
  • 端到端的通讯加密:用户之间可以互相发消息,但是不用担心服务器拿到所有的消息记录,同时消息也可以按照服务器的要求,出示相应的零知识证明,比如消息的来源、与发送的目的地。
  • 身份认证:用户可以向网站证明,他拥有私钥,或者知道某个只要用户自己才知道的秘密答案,而网站并不需要知道,但是网站可以通过验证这个零知识证明, 从而确认用户的身份
  • 去中心化存储:服务器可以向用户证明他们的数据被妥善保存,并且不泄露数据的任何内容。
  • 信用记录:信用记录是另一个可以充分发挥零知识证明优势的领域,用户可以有选择性的向另一方出示自己的信用记录,一方面可以有选择的出示满足对方要求的记录分数,同时证明信用记录的真实性。
  • 构造完全公平的线上数字化商品的交易协议[9]。
  • 更多的例子,可以是任何形式的数据共享,数据处理与数据传输。

举例:地图三染色问题

下面讲一个经典的问题,地图的三染色问题。如何用三种颜色染色一个地图,保证任意两个相邻的地区都是不同的颜色。我们把这个「地图三染色问题」转变成一个「连通图的顶点三染色问题」。假设每个地区都有一个首府(节点),然后把相邻的节点连接起来,这样地图染色问题可以变成一个连通图的顶点染色问题。

下面我们设计一个交互协议:

  • 「证明者」Alice
  • 「验证者」 Bob

Alice 手里有一个地图三染色的答案,请见下图。这个图总共有 6 个顶点,9 条边。

3c-0

现在 Alice 想证明给 Bob 她有答案,但是又不想让 Bob 知道这个答案。Alice 要怎么做呢?

Alice 先要对染过色的图进行一些「变换」,把颜色做一次大挪移,例如把所有的绿色变成橙色,把所有的蓝色变成绿色,把所有的橙色变成蓝色。然后 Alice 得到了一个新的染色答案,这时候她把新的图的每一个顶点都用纸片盖上,然后出示给 Bob 看。

3c-1

看下图,这时候 Bob 要出手了(请见下图),他要随机挑选一条「边」,注意是随机,不让 Alice 提前预测到的随机数。

3c-2

假设 Bob 挑选的是最下面的一条边,然后告诉 Alice。

3c-3

这时候 Alice 揭开这条边两端的纸片,让 Bob 检查,Bob 发现这两个顶点的颜色是不同的,那么 Bob 认为这次检验同构。这时候,Bob 只看到了图的局部,能被说服剩下的图顶点的染色都没问题吗?你肯定觉得这远远不够,也许恰好 Alice 蒙对了呢?其它没暴露的顶点可能是胡乱染色的。

没关系,Bob 可以要求 Alice 再来一遍,看下图

3c-4

Alice 再次把颜色做一次变换,把蓝色改成紫色,改绿色改成棕色,把橙色改成灰色,然后把所有的顶点盖上纸片。然后 Bob 再挑选一条边,比如像上图一样,选择的是一条竖着的边,然后让 Alice 揭开纸片看看,如果这时候 Bob 再次发现这条边两端的顶点颜色不同,那么 Bob 这时候已经有点动摇了,可能 Alice 真的有这个染色答案。可是,两次仍然不够,Bob 还想再多来几遍。

那么经过反复多次重复这三个步骤,可以让 Alice 作弊并能成功骗过 Bob 的概率会以指数级的方式减小。假设经过 n 轮之后,Alice 作弊的概率为

这里 |E| 是图中所有边的个数, 如果 n 足够大,这个概率 Pr 会变得非常非常小,变得「微不足道」。

可是,Bob 每次看到的局部染色情况都是 Alice 变换过后的结果,无论 Bob 看多少次,都不能拼出一个完整的三染色答案出来。实际上,Bob 在这个过程中,虽然获得了很多「信息」,但是却没有获得真正的「知识」。

信息 vs. 知识

  • 信息 「Information」
  • 知识 「Knowledge」

在地图三染色问题的交互证明中,当重复交互很多次之后,Bob 得到了大量的信息,但是这好比 Alice 发给 Bob 一堆随机数一样,Bob 并没有「知道」更多的东西。打个比方,如果 Alice 告诉 Bob 「1+1=2」,Bob 得到了这个信息,可是 Bob 并没有额外获取更多的「知识」,因为这个事实人人皆知。

假如 Alice 告诉 Bob 2^2^41-1这个数是一个质数,很显然这个是「知识」,因为要算出来这个数是不是一个质数,这需要耗费大量的算力。

假如 Alice 告诉 Bob,总共有两个顶点用了绿颜色,那么 Bob 就获得了宝贵的「知识」,因为基于他刚刚获取的这个信息,Bob 可以用更短的时间用一台图灵机去求解三染色问题。假如 Alice 又透露给 Bob,最左边的顶点颜色是用橙色,那么很显然,这个「信息」对于 Bob 求解问题并没有实质上的帮助。

我们可以尝试定义一下,如果 Bob 在交互过程中获得的「信息」,可以帮助提升 Bob 直接破解 Alice 秘密的能力,那么我们说 Bob 「获得了知识」。由此可见,知识这个词的定义与 Bob 的计算能力相关,如果信息并不能增加 Bob 的计算能力,那么信息不能被称为「知识」。比如在 Alice 与 Bob 交互过程中,Alice 每次都掷一个硬币,然后告诉 Bob 结果,从信息角度看,Bob 得到的信息只是一个「事件」,然而 Bob 并没有得到任何「知识」,因为 Bob 完全可以自己来掷硬币。

下面引用『Foundations of Cryptography—— Basic Tools』一书[10]中的总结

  1. 「知识」是与「计算难度」相关,而「信息」则不是

  2. 「知识」是与公共所知的东西有关,而「信息」主要与部分公开的东西有关

注:曾有人问我,这里的信息与知识的定义是否与 Kolmogorov 复杂性有关。根据算法信息论,一段字符串的信息量可以用产生字符串的最小程序的长度来测量。这个问题我不是很懂,希望路过的专业人士留言。

可验证计算与电路可满足性问题

看了上面的地图三染色问题,大家是不是没有感觉,好像这只是一个学术问题,如何跟现实问题关联起来?地图三染色问题是一个 NP-Complete 问题,这是「计算理论」中的一个名词。另外有一个叫做「电路可满足问题」也是同样是 NP-Complete 问题。NP-Complete 是一类问题,他的求解过程是多项式时间内难以完成的,即「求解困难」,但是验证解的过程是多项式时间可以完成的,即「验证简单」。

那什么是电路呢?下面是三个不同的「算术电路」:

circuits

可以看到一个电路由很多个门组成,其中有加法门,还有乘法门。每一个门有几个输入引脚,有几个输出引脚。每一个门做一次加法运算,或乘法运算。别看这么简单,我们平时跑的(没有死循环)代码,都可以用算术电路来表示。

这意味着什么呢?我们下面结合「零知识证明」与「电路可满足性问题」来试着解决数据的隐私保护问题。

下面请思考一个场景:Bob 交给 Alice 一段代码 P,和一个输入 x,让 Alice 来运行一遍,然后把运行结果告诉 Bob。可能这个计算需要消耗资源,而 Bob 把计算过程外包给了 Alice。然后 Alice 运行了一遍,得到了结果 y。然后把 y 告诉 Bob。下面问题来了:

如何让 Bob 在不运行代码的前提下,相信代码 P 运行的结果一定是 y 呢?

这里是思考时间,大家可以想个五分钟 ……

(五分钟后……)

Alice 的一种做法是可以把整个计算过程用手机拍下来,这个视频里面包含了计算机 CPU,还有内存,在整个运行过程中的每一晶体管的状态。很显然这么做是不现实的。那么有没有更可行的方案呢?

答案是 Bob 把程序 P 转换成一个完全等价的算术电路,然后把电路交给 Alice。Alice 只要计算这个电路就可以了,然后这个过程是可以用手机拍下来的,或者用纸记下来,如果电路规模没有那么大的话。Alice 只要把参数 6 输入到电路,然后记录下电路在运算过程中,所有与门相连的引脚线上的值。并且最后的电路输出引脚的值等于 y,那么 Bob 就能确信 Alice 确实进行了计算。Alice 需要把电路的所有门的输入与输出写到一张纸上,交给 Bob,这张纸就是一个计算证明。

这样 Bob 完全可以在不重复计算电路的情况下来验证这张纸上的证明对不对,验证过程很简单:

Bob 依次检查每一个门的输入输出能不能满足一个加法等式或者一个乘法等式

比如 1 号门是一个加法门,它的两个输入是 3,4,输出是7,那么很容易就知道这个门的计算是正确的。当 Bob 检查完所有的门之后,就能确信:

Alice 确确实实进行了计算,没有作弊。

这张纸上的内容就是「满足」算术电路 P 的一个解「Solution」。

所谓的电路可满足性就是指,存在满足电路的一个解。如果这个解的输出值等于一个确定值,那么这个解就能「表示」电路的计算过程。

对于 Alice 而言,Bob 如果用这种方式验证,她完全没有作弊的空间。但是这种方法很显然有个弊端:

  • 弊端一:如果电路比较大,那么证明就很大,Bob 检查证明的工作量也很大。
  • 弊端二:Bob 在验证过程中,知道了所有的电路运算细节,包括输入。

黑科技

我们再对刚才的 Alice 与 Bob 的场景做些修改。假如,Alice 自己还有一个秘密,比如说网银密码。而 Bob 想知道 Alice 的网银密码的长度是不是 20 位长。而 Alice 想了下,告诉他密码长度应该问题不大。这时候 Bob 把一个计算字符串长度的代码转换成了电路 Q,并且发给 Alice。Alice 用电路 Q 算了一下自己的密码,然后把电路所有门的引脚发给了 Bob,并带上运算结果 20。

Wai……t,这是有问题的,Bob 拿到电路运算过程中的所有内部细节之后,不就知道密码了吗?是的,Alice 显然不能这么做。那么 Alice 应该怎么做?

答案是有很多种办法,热爱区块链技术的读者最耳熟的就是 zkSNARK[11],还有zkSTARK[12],子弹证明BulletProof[13],以及一些比较小众的技术,都可以帮 Alice 做到:

Alice 以一种零知识的方式,向 Bob 证明她计算过了电路,并且使用了她的秘密输入。

换句话说,这些「零知识的电路可满足性证明协议」为 Alice 提供了强大的武器来向 Bob 证明她的网银密码长度为 20,并且除此之外, Bob 再也得不到任何其它有用的信息。除了网银密码,Alice 理论上可以向 Bob 证明任何她的隐私数据的某些特性,但是并不暴露任何别的信息。

「零知识的电路可满足性证明协议」提供了一种最直接的保护隐私/敏感数据的技术

最近几年来,零知识证明构造技术发展日新月异,并且在区块链技术领域得到了越来越多的应用。最新的零知识证明技术,有的技术可以让 Bob 高速验证证明(在移动设备上几毫秒验证完成);有的技术可以让所有吃瓜群众帮忙验证(非交互式零知识证明);有的技术支持非常小的证明大小(小到几十个字节)。后续文章我们会逐步展开介绍。

写在最后

无论是精妙的数论定理,地图三染色问题,还是电路可满足性问题。证明存在的意义是什么?所有的证明都体现了「证明」与「验证」的「不对称性」。证明可能是一个非常耗费算力,或者脑力的活动,无论是耗时几百年的「费马大定理」,还是比特币中的 POW 证明,这些证明都凝结了在寻找证明过程中所消耗的能量,证明过程可能是超乎寻常的复杂,偶尔需要天才横空出世。而验证过程一定(或者应该)是一个非常简单的,机械的,在(多项式)有效时间内且能终止的活动。某种意义上,这个不对称性真正体现了证明的意义,展示了零知识证明的价值。

粗略看,「证明」是「逻辑」的产物,但「逻辑」与「计算」却又有着密不可分的联系,大家可能模模糊糊感觉到一些关于「证明」与「计算」之间的关联,它们贯穿始终:如机械推理、证明表达、交互计算 。这是一个有趣但更宏大的哲学问题。

参考文献

  • [1] 西蒙, 辛格, 薛密. 费马大定理: 一个困惑了世间智者 358 年的谜[M]. 上海译文出版社, 1998.
  • [2] Alec Wilkinson. The Pursuit of Beauty: Yitang Zhang solves a pure-math mystery. The New Yorker. Feb. 2015.
  • [3] 马丁, 戴维斯, 张卜天. 逻辑的引擎[M]. 湖南科学技术出版社, 2012.
  • [4] Raymond Smullyan. Gödel’s Incompleteness Theorems, Oxford Univ.Press. 1991.
  • [5] Turing, Alan. “On computable numbers, with an application to the Entscheidungsproblem.” Proceedings of the London mathematical society 2.1 (1937): 230-265.
  • [6] Pierce, Benjamin C., et al. “Software foundations.” 中文译文: <https://github.com/Coq-zh/SF-zh
  • [7] Kolata, Gina. “Computer math proof shows reasoning power.” Math Horizons 4.3 (1997): 22-25.
  • [8] Goldwasser, Shafi, Silvio Micali, and Charles Rackoff. “The knowledge complexity of interactive proof systems.” SIAM Journal on computing 18.1 (1989): 186-208.
  • [9] zkPoD: 区块链,零知识证明与形式化验证,实现无中介、零信任的公平交易. 安比实验室. 2019.
  • [10] Oded, Goldreich. “Foundations of cryptography basic tools.” (2001).
  • [11] Gennaro, Rosario, et al. “Quadratic span programs and succinct NIZKs without PCPs.” Annual International Conference on the Theory and Applications of Cryptographic Techniques. Springer Berlin, Heidelberg, 2013.
  • [12] Ben-Sasson, Eli, et al. “Scalable, transparent, and post-quantum secure computational integrity.” IACR Cryptology ePrint Archive 2018 (2018): 46.
  • [13] Bünz, Benedikt, et al. “Bulletproofs: Short proofs for confidential transactions and more.” 2018 IEEE Symposium on Security and Privacy (SP). IEEE, 2018.

理解「模拟」

探索零知识证明系列(二)

I know that I know nothing —— 苏格拉底

相信很多人都听说过零知识证明,但是只有极少数人听说过模拟,然而模拟是理解零知识的关键。

我们在第一篇文章『初识「零知识」与「证明」』(链接)[1]中介绍了一个简单的零知识交互系统:地图三染色问题。那么这个系统真的是零知识的吗?我们为什么要相信这个结论呢?有证明吗?在 Alice 与 Bob 的对话过程中,如果不零知识,Alice就被坑了。交互式系统的设计者「我」需要让 Alice 确信,这个对话确实是零知识的。

如果从直觉主义角度解释,要证明一个交互系统中存在信息泄露,那么你只需要指证:第几个 bit 导致信息泄露即可;但如果要证明不存在信息泄露,那么你要对着所有信息流中的所有 bit 说,这从1,2,3,4,5,…… 编号的 bit 都没泄露任何信息。看官们,这是不是很难?

本文约八千字,略微烧脑。

安全的定义与不可区分性

首先,一个交互式系统,也就是一个对话,它的「零知识」需要证明。毕竟,现代密码学是建立在严格的形式化系统之上。在证明之前,还需要明确「安全假设」到底有哪些。所谓安全假设,比如我们说一个系统的权限隔离做得无比精确,每一个用户只能看到被授权的信息,但是这基于一个安全假设:管理员账号没有被破解。又比如在手机银行软件里,只能通过短信认证码,才能完成转账功能,这也基于一个安全假设:你的手机 SIM 卡没有被克隆。如果我们深入地分析每一个我们感觉安全的系统,都存在大量的似乎不那么稳固的安全假设。比特币私钥安全吗?比特币账户的安全假设也不少:首先你的助记词不能让别人知道,手机钱包里私钥保存加密算法足够强,密钥派生算法正规,你不能忘记助记词,等等等。

脱离安全假设来谈安全都是在耍流氓。一切安全都有前提的。只有经过数学证明之后,大家才能够确信这个 算法/方案 的安全性基于一些非常明确的「安全假设」。

在证明之前,还缺少一个东西,那就是「安全定义」。在多数人的认知系统中,安全就是一个框,什么都可以往里装。大家应该好好提醒下自己,当谈论安全二字的时候,有没有想过到底什么是安全?怎么算安全?

「安全」需要有一个数学意义上的严格定义

伟大的科学家香农(Claude Shannon)从信息论的角度给出了一个非常靠谱的安全性定义[2]:

完美安全:假设你是一个攻击者,你通过密文获取不到任何有价值的信息,破解的唯一手段就是靠瞎蒙。

大家想一想,这个定义很有趣,通过密文获取不到信息,这就意味着你没有获得任何额外的计算能力,能够帮助让你以更短的时间来计算出明文。

但是这个定义太完美,以至于使用的加密算法都很难满足这个安全性定义。后来 Goldwasser 与 Micali 等人写了另一篇载入史册的经典『概率加密』[2]。

在这篇论文中定义了这样一个概念:语义安全。所谓语义安全在完美安全的定义上放松了些要求。

语义安全:假设你是一个攻击者,你通过密文在多项式时间内计算不出来任何有价值的信息。

好了,这个看起来靠谱多了。接下来一个问题就是,怎么理解「计算不出来信息」这个概念?这看来要对信息进行度量,信息的定义又是什么呢?

我们又引入一个概念——「不可区分性」,来重新表述加密算法的安全性:假设你是一个攻击者,而我有一个加密算法:

  1. 你随机产生两段等长的明文,m1=「白日依山尽,黄河入海流」,m2=「烫烫烫烫烫,烫烫烫烫烫」
  2. 你把这两段明文,m1m2 交给我
  3. 我随机挑选一个明文,不告诉你是哪一个,然后进行加密,产生一个密文 c
  4. 我把密文 c 出示给你看,让你猜这个c 究竟是由唐诗加密产生,还是乱码加密产生
  5. 如果你用一台计算机来破解c,在多项式时间内破解不出来,也就是说你没办法区分c的来源,那么就说明加密算法是语义安全的

OK,理解完「不可区分性」,我们再回到「零知识」,如何证明一个交互式系统是「零知识」呢?首先我们要定义下零知识这个概念。

注:不可区分性是概率意义上的不可区分;在学术上,它可以分为「完全不可区分」,「统计不可区分」,还有「计算不可区分」。在本文中,我们暂时不需要理解这些概念的差别。

遇见模拟器

先开个脑洞,设想在平行宇宙中,有两个平行的世界,一个叫做「理想世界」(Ideal World),另一个叫做「现实世界」(Real World)。我们每一个个体可以在两个平行世界中愉快地玩耍,但是两个世界的普通人无法互相感知,也无法互相沟通。

假设「你」是一个很厉害的密码破解者,而且「你」不是普通人,具备在平行宇宙之间穿梭的能力。而 Alice 有一个地图三染色的答案,你的目的是通过和 Alice 对话来获取地图三染色的答案,会话的过程参考上一篇文章的「地图三染色问题」协议。

继续脑洞,Alice 只存在「现实世界」中;在「理想世界」,Alice 被「替换」成了一个长相与声音一模一样的个体,我们称替身为 Zlice。下一步,把「你」同时放入两个世界中,但不让你知道是你当前位于哪一个世界。你的两个分身所面对的都是一个 “Alice”模样的人。

再重复一遍,在「现实世界」中, 与你对话的是一个真实的,并且诚实的 Alice;而在「理想世界」中,与你对话的是 Zlice (假 Alice),Zlice 虽然相貌语言与 Alice 并无二致,但差异是,Zlice 并不知道「知识」,即不知道一个三染色问题的答案。

接下来在这两个世界中,你的两个分身将同时与真假 Alice 进行对话。神奇的事情发生了,最终在两个世界中,你的两个分身都被说服了,都经过n轮挑战,没有发现对方作弊,即「你」的两个分身都认为对方确实知道「答案」。换句话说,「你」没有能力「区分」出来自己到底在 「现实世界」 还是 「理想世界」,当然也没能力「区分」和自己对话的究竟是 Alice 还是 Zlice。不仅如此,对于吃瓜群众我而言,如果把「我」作为观察者放入任何一个世界中,我会和你一样「无法区分」出来眼前的 这个长相为 “Alice” 的人到底是真还是假。

下面是烧脑结论:

这个交互系统为何是「零知识」?因为 Zlice 是没有任何知识,而且她和 Alice 不可区分。

我再换个方式解释:因为你和我都没办法区分我们究竟是在哪个世界中,两个世界发生的交互过程几乎不可区分,而且其中一个世界中根本就不存在知识,因此,我们说这个交互协议——「地图三染色问题」是「零知识的」。

这里还有个前提,理想世界必须是算法可构造的。然后,有一个「神」,他通过算法「模拟」了一个「理想世界」,其中构造了一个算法叫做 Zlice,她没有「知识」作为输入,也即「零知识」;除此之外,「理想世界」与「现实世界」一模一样。

设想你在对话过程中,如果真 Alice 泄露了信息,那么你就能立即区分出面前这个人是 真 Alice 还是 Zlice,Zlice 是不可能伪装泄露信息的。因此可以得出结论:

真Alice 没有泄露任何信息。

这个神,被称为「模拟器」(Simulator),而在理想世界中,和你对话的这个 Zlice 幻象其实也是「模拟器」,你在理想世界中,所有能感知到的东西都是模拟器「模拟」出来的。

好了,到这里,我们用「模拟器」这个概念对「零知识」进行了定义。

接下来,我们开始进入证明零知识的环节。

区分两个世界

(Save World State as Snapshot X)

证明的零知识过程,等价于构造(寻找)一个「模拟」算法,这个算法能够让模拟器来模拟出一个「没有知识」的理想世界。如果这个算法存在,而且两个世界不可区分,那么就证明完毕。

等等,可能「你」会觉得哪里不对劲。

假如说真的存在这种算法,而且它能够在没有知识的情况下骗过我,那么在「现实世界」中,不排除真 Alice 也使用了这样的算法来欺骗我。这样一来,我岂不是在两个世界中都被欺骗了。那么这个交互协议就失去意义了。

其实,这里有个关键点,借用电影『盗梦空间』中的剧照,在「理想世界」中有点东西是和「现实世界」本质不同的。这个东西是区分两个世界的关键,而它要让我们「无法感知」。这个东西不是梦境中的陀螺,它是一种「超能力」,模拟器 Simulator 所具备的超能力。

比如这样一种超能力:「时光倒流」。

(上图是电影『土拨鼠之日』的剧照,剧中主人公每次睡醒都会回到2月2日的早上,这样他永远活在同一天里)

等等,各位看官,不是刚才我们一直在讨论不可区分性吗?怎么两个世界又需要区分啦?“我糊涂了”。不要慌,所谓的不可区分性针对的是理想世界中的个体认知而言。而「可区分性」是对位于世界外部的神而言。

设想下在我们周围,如果有一个人有时空穿越能力,或者他能让时间回退到一年前,那么我们这些凡夫俗子完全是一脸茫(meng)然(bi)的,无从感知。那么,如果「模拟器」可以在他构造出的「理想世界」中实现「时间倒流」,那么他就可以达成一些神奇的事情,从而骗过作为验证者身份的「你」,也能骗过观察者「我」。对于「你」而言,你明白,在「理想世界」中,时间是可以回退的,但是在「现实世界」中,显然真 Alice 不可能拥有超能力。虽然你和我不能区分在哪个世界里,但是至少我们知道在两个世界中的其中「现实世界」里,对面那个Alice是没办法欺骗我们的,当然我们却不能说出我们到底在哪个世界中。

到此,交互协议的「零知识」已经证明完了。各位是否已经明白了?我再给大家再梳理下证明思路:

首先「零知识」是为了保护 Alice 的利益,因为 Alice 不想在交互过程中透露更多的信息给 Bob,不想让 Bob 知道她所拥有的秘密 w,甚至不想让 Bob 从交互的过程中分析出哪怕一丁点的信息。那么怎么保证这一点呢?「模拟器」这时候登场了,它能模拟出一个和现实世界外表一模一样的「理想世界」,然后「模拟器」在这个世界中可以轻松地骗过任何一个对手,让对方无法分辨自己是在现实世界中,还是理想世界中。因为「模拟器」手里没有那个秘密 w,「理想世界」是零知识的。又因为两个世界的不可区分性,所以我们可以得出结论:Alice 的交互协议是「零知识」的。

我们来看一个具体的例子,上一篇文章[1]中提到的地图3染色问题。

地图三染色问题的零知识证明

回忆一下「地图三染色问题交互系统」:

  • 第一步:Alice 把地图染色答案做一次完全置换,然后将所有顶点盖上纸片,交给 Bob
  • 第二步:Bob 随机挑选一条边
  • 第三步: Alice 打开指定边的两端顶点的纸片,Bob检验两个顶点的颜色是否相同,如果不同则通过,如果相同则失败
  • 回到第一步,重复 n

我们接下来就来证明上述这个交互是零知识的,这里先假设验证者 Bob 是诚实的,这有助于大家理解这个证明过程。然后我们再讨论,如果 Bob 不诚实的证明方法。

在「理想世界」中,跟 Bob 对话的是一个「模拟器」,它模拟出了整个世界的样子。Bob 按照三染色问题的交互协议进行交互。模拟器并没有一个三染色答案,它索性把所有的顶点都染成了灰色。

首先,模拟器模仿 Alice ,把每个顶点用纸片盖起来。然后发给 Bob。

Bob 随机挑选了一条边,挑战证明者。

模拟器这时候不能打开纸片,因为这条边两端的颜色都是灰色啊。

这时候,模拟器要发挥「超能力」了,他运用时间倒流的技能,回到对话第一步之前。

模拟器现在处于第一步,他把最下面那条边的两端染上任意不同的颜色,然后重新盖上纸片,并发给 Bob。

Bob 这时候无法感知到时间已经倒退回第一步了,对他来说,一切都是新鲜的,他「诚实」地再次选择了最下面的边。

这时候模拟器就可以放心地打开纸片,让 Bob 检查。Bob 很显然会被骗过。然后 Bob 一轮轮地重复这个过程,每一次模拟器都能用时间倒流的方式骗过 Bob。

于是在理想世界中,模拟器并没有任何三染色答案的「知识」,却同样能骗过Bob,并且从概率上来看,与「现实世界」中被观察到的交互过程高度地一致(完全一致的概率分布)。于是上面的过程展示了模拟器的算法的存在性,也就相当于证明了交互系统的「零知识性质」

不诚实的 Bob

在上面的证明过程中,有一个相当强的假设,就是每次时间倒流之后,Bob都会选择同一条边。如果 Bob 每次都会换一条不同的边呢?没关系,如果在模拟器第一次实施时间倒流之后,Bob又选择了不同的边,那么模拟器可以把颜色打乱之后,再次运行时间倒流,在多次时间倒流之后,Bob 极大的概率总会一次选择模拟器进行染色的那条边,然后这时候模拟器才走到第三步,打开纸片。

阿里巴巴、洞穴与芝麻开门

在网上众多的讲解「零知识证明」的中文科普文章中,有一个例子流传非常广,这就是阿里巴巴与强盗的故事。可惜地是,这些不同版本的故事都只讲了一半。那么我接下来讲一个不一样的「阿里巴巴」与「四十大盗」的故事:

在很久很久以前,在一个叫做巴格达的城市里,住着一个人叫阿里巴巴。每天阿里巴巴会到集市上买东西。

有一天,阿里巴巴被一个盗贼抢了钱包,于是他一路追着盗贼到了一个山洞口,然后盗贼就消失了。阿里巴巴发现洞口里面有两条岔路,如下图所示。

阿里巴巴不知道盗贼往哪边跑了,于是他决定去「左边」岔道看看,很快阿里巴巴就发现这是个死胡同,也不见盗贼踪影。然后他又去「右边」岔道检查,也是个死胡同,不见盗贼踪影。阿里巴巴自言自语道:「该死的盗贼跑哪去了呢?」

第二天,阿里巴巴又去集市买东西,这次另一个盗贼抢了他的篮子,然后阿里巴巴追着这个盗贼到了昨天同样的山洞口,然后盗贼又不见了,这一次阿里巴巴决定先去「右边」岔道看看,没有发现盗贼,然后再去左边看看,也同样不见盗贼。这好奇怪。

第三天,第四天,……,第四十天,同样的故事上演,阿里巴巴追着第四十个大盗到了神秘的洞口,盗贼就消失了。阿里巴巴想,这个山洞里面一定有机关,于是他躲在「右边」岔道的尽头,耐心地等了很长时间,这时一个盗贼跑了进来,走道岔道尽头之后,念了一个咒语「芝麻开门」。这时候墙壁居然打开了,盗贼跑进去之后,然后墙壁又合上了,这时候另一个受害者追了进来,找了半天,一无所获。

阿里巴巴随后等他们走了之后,试验了一下这个咒语,果然非常有效,而且阿里巴巴发现这个墙壁通向「左边」岔道。后来,阿里巴巴找到了更换咒语的办法,并且把一个新咒语和洞穴的地理位置写在了一张羊皮纸上。

注:到这里,故事并没有结束…. (上字幕)很久很久以后

在很多年后,到了80年代,阿里巴巴的羊皮纸流落到了几个密码学家手里,他们跑到巴格达,找到了洞穴的位置,尽管过了几个世纪,咒语居然仍然有效,这几个密码学家兴奋地打开墙壁,在两个岔道之间跑来跑去。

一家电视台很快知道了这个奇异事件,一个密码学家 Mick Ali(与密码学家 Micali 发音相似)决定向电视观众展示他知道这个咒语,首先,电视节目主持人把摄像机架在洞口,然后让所有人都在山洞口等待,这时候 Mick Ali一个人进入到山洞中,然后主持人抛一个硬币,来决定让 Mick Ali 从哪个岔道跑出来。为了纪念阿里巴巴与四十大盗,Mick Ali 重复了四十遍每次都成功。

节目非常成功。但很快,另外一个电视台眼红,也想拍一个类似的节目,但是Mick Ali 因为签了独家协议,没办法参与这个新节目。怎么办呢?第二个电视台的主持人心生一计,他找了一个和 Mick Ali 很像的演员,穿着打扮、姿态和说话口音都模仿 Mick Ali。然后他们开拍了,每次主持人掷硬币后,都让这个演员跑出来,但是很显然,演员并不知道咒语,没办法打开那个墙壁。于是有时候演员碰巧会成功,有时候则会失败,于是演员很辛苦,重复了将近一百次,才成功了四十次。最后这个狡猾的新节目主持人,把录制视频进行了剪辑,只保留了成功的片段,错误的片段都删除了。然后这个新节目和 Mick Ali 的节目在同一时间,不同频道播出。然后观众们完全无法区分哪个视频是真的,哪个视频是假的。第一个电视台的主持人完全明白 Mick Ali 是真正知道墙壁的咒语的人,但是他却不能把这个事实传递给无辜的观众们。

看到这里,大家是不是对「模拟」慢慢有了感觉?这里第二个电视台的主持人通过剪辑视频的方式,而不是「时间倒流」。他对「理想世界」,也就是电视中播出的内容所在的世界,进行了外部干预,达到了同样的效果。对理想世界而言,这种剪辑本质上就是一种超能力。

这个故事其实来源于一篇论文『如何向你的孩子解释零知识证明』(How to Explain Zero-Knowledge Protocols to Your Children)[3],发表在1989年的美密会议上。

模拟与图灵机

一谈到超能力,大家有没有觉得这玩意不科学。是的,如果我们无脑地用「超能力」来解释任何事情,那么我们逻辑就无法自恰(Consistent)。在理想世界中,模拟器是不能随便开挂的,比如模拟器肯定不能直接修改 Bob 的内部状态,比如 Bob 在验证步骤明明验证失败,但是模拟器强硬去把验证结果改为「接受」,这会导致我们可以证明:「任何的交互系统都是零知识的」,这个错误结论。

模拟器不是理想世界中全能的上帝

那么模拟器到底可以是什么呢?模拟器其实只是一个图灵机。所谓的「时间倒流」,「剪辑录像」这类的所谓超能力并不是玄乎的超自然能力,而是图灵机可以实现的功能。计算机专业的朋友们肯定都用过 VMWare,虚拟机之类的软件,本文讲的「模拟器」完全可以想象成一个「虚拟机」软件,它能虚拟出一个计算机环境,这个虚拟环境就是我们上文说的「理想世界」。「时间倒流」如何解释呢?不知道大家有没有用过虚拟机软件的「快照」功能(Snapshot),使用快照的时候,虚拟机软件可以把整个虚拟计算机的所有状态保存下来,然后在任意时刻,虚拟机软件都可以重新回到保存快照的位置继续运行。

注:其实所谓时间倒流是计算机中的一个基本操作,在程序语言理论中有一个概念叫做 Continuation。抽象地讲,Continuation 表示从现在开始到未来的计算。Continuation这是控制流的一个显式抽象,而 goto,call-with-current-continuation,甚至 thread scheduling 都可以看做是操作 Continuation 的操作符。比如采用call/cc,也就是call-with-current-continuation 就可以轻松地实现「回溯」功能。保存快照可以理解为保存当前的 Continuation,而回到过去的某一刻,就是应用这个Continuation。

不管 Zlice 还是 Bob,还有我们的每一个观察者,都是一个个可执行程序。这些程序被拷贝到了虚拟机里。Zlice 与 Bob 的会话实际上就是这两个程序之间的通讯。观察者是 Hook 在 Zlice 与 Bob 进程 IO 上的程序。在上文的地图三染色「理想世界」的诚实 Bob,实际上是 Bob 进程调用了虚拟机的「随机数发生器」,而这个随机数发生器是能被 Zlice 操纵的。「现实世界」是外部运行虚拟机软件的计算机环境。

大家是不是又有所悟,我再强调一下:

证明零知识的过程,就是要寻找一个算法,或者更通俗点说,写出一段代码,它运行在外部计算机系统中,但是实现了虚拟机的功能。而且在虚拟机中,需要有一个不带有「知识」作为输入的 Zlice,可以骗过放入虚拟机运行的 Bob。

如果还没理解上面我这句话,请时光回退到『区分两个世界』这一小节,重新思考模拟。:P (Load World State from Snapshot X)

柏拉图的洞穴寓言

模拟无处不在,哥德尔不完备性定理就使用了模拟的概念,用哥德尔数(Godel Numbers)模拟了形式算术。图灵提出了「Universal Turing Machine」(通用图灵机)的概念,这种图灵机可以模拟自身。

但最早的「模拟」概念,出自『理想国』一书的第七卷[4]中,古希腊哲学家柏拉图讲了这么一则寓言——Allegory of Cave:

plato’s cave

设想在一个暗无天日的山洞中,有一排被锁链锁住的囚徒,他们从小就只能看到前方的墙壁。这些囚徒们身后是一堵墙,再后面有一堆放着火,在火与墙壁之间,有一些人举着道具和木偶来回走,这样道具木偶就会在火光映射下在墙壁上投下影子。而这些囚徒们整天就只能看着这些影子。因为这些囚徒们从打出生起,所闻所见就只是前方洞壁上的各种影子,他们会以为所看到的这些影子就是真实的世界。

然而有一天,一个囚徒偶然挣脱锁链,他回头看到了火。但是他从小到大仅能看到暗淡的影子,他第一次看到了明亮的火光。看到了道具和木偶,假如有人告诉他,这些道具和木偶才是实物,他一定会嗤之以鼻,会坚持认为影子才是真实的。

柏拉图假设说,如果这个囚徒强制拖出洞穴,到外面去看到真实的世界, 一开始囚徒会不适应真实世界的光亮而感到刺目眩晕,他会因此而愤怒。 但是当他慢慢适应了这个世界,看到太阳,树木,河流,看到星空,他逐渐明白,这个世界比洞穴中那个世界更为优越高级。他再也不想回到黑暗的洞穴生活中了。

过了一段时间,他对洞穴中的囚徒心生怜悯,于是想去把他们都带出来。但是当他再次返回洞穴中,他因为已经适应了外面明亮的世界,回到洞穴中反而看不清楚。被锁的囚徒们反而认为他的视力受损,胡言乱语,是个疯子,最后当他想尽办法把这群囚徒带出洞穴时,被囚徒们联手杀死。

这是则人类命运的寓言,就和那一排被锁链锁着的囚徒类似, 我们以为眼睛看到的就是世界的真相,但实际上,那也许是幻象,就像洞穴墙壁上投下的影子一样。

未完待续

本文章介绍了理解零知识所需的关键概念——模拟。任何一个零知识的协议,都可以通过构造一个「理想世界」来理解。第一次接触这个概念的读者需要反复琢磨。

计算机科学中有两个方法论至关重要,第一个是「抽象」,第二个是「模拟」

回顾一下在地图三染色问题中,Bob 在「理想世界」与「现实世界」中的对话。虽然 Bob 无法区分两个世界,但是有一点,他可以确信:现实世界中,Alice 没有超能力。

问题来了,Alice 没有超能力,并不能直接证明 Alice 真的有答案。万一这个交互协议并不能保证 Alice 一定有知识呢?「零知识」保护了 Alice 的利益,谁来保证 Bob 的利益呢?这个问题留给下一篇。

致谢: 本文受密码学教授 Matthew Green 发表在2014年与2017年的两篇个人博客文章[10-11]启发。*

参考文献

  • [1] 初识「零知识」与「证明」. 安比实验室. 2019.
  • [2] Shafi Goldwasser and Silvio Micali, Probabilistic Encryption, Special issue of Journal of Computer and Systems Sciences, Vol. 28, No. 2, pages 270-299, April 1984.
  • [3]Quisquater, J.J., Quisquater, M., Quisquater, M., Quisquater, M., Guillou, L., Guillou, M.A., Guillou, G., Guillou, A., Guillou, G. and Guillou, S., 1989, August. How to explain zero-knowledge protocols to your children. In Conference on the Theory and Application of Cryptology (pp. 628-631). Springer, New York, NY.
  • [4] 柏拉图 and 吴献书, 1986. 理想国 (Vol. 1, No. 986, p. 1). 商务印书馆.
  • [5] Goldwasser, Shafi, Silvio Micali, and Charles Rackoff. “The knowledge complexity of interactive proof systems.” SIAM Journal on computing 18.1 (1989): 186-208.
  • [6] Oded, Goldreich. “Foundations of cryptography basic tools.” (2001).
  • [7] Rackoff, Charles, and Daniel R. Simon. “Non-interactive zero-knowledge proof of knowledge and chosen ciphertext attack.” Annual International Cryptology Conference. Springer, Berlin, Heidelberg, 1991.
  • [8] Goldreich, Oded, Silvio Micali, and Avi Wigderson. “Proofs that yield nothing but their validity or all languages in NP have zero-knowledge proof systems.” Journal of the ACM (JACM) 38.3 (1991): 690-728.
  • [9] zkPoD: 区块链,零知识证明与形式化验证,实现无中介、零信任的公平交易. 安比实验室. 2019.
  • [10] Matthew Green. Zero Knowledge Proofs: An illustrated prime. 2014. https://blog.cryptographyengineering.com/2014/11/27/zero-knowledge-proofs-illustrated-primer/
  • [11] Matthew Green. Zero Knowledge Proofs: An illustrated primer, Part 2. 2017. https://blog.cryptographyengineering.com/2017/01/21/zero-knowledge-proofs-an-illustrated-primer-part-2/

寻找「知识」

探索零知识证明系列(三)

And what, Socrates, is the food of the soul? Surely, I said, knowledge is the food of the soul. 苏格拉底,什么是灵魂的食物?我说过,当然是知识。 —— 柏拉图

导言:有些理论非常有趣,零知识证明便是其中之一,摸索了许久,想写点什么,与大家一起讨论。本文是『探索零知识证明』系列的第三篇。全文约 8,000 字,少量数学公式。

本文将在 Github 进行更新与修正。

「零知识」vs. 「可靠性」

我们在许多介绍零知识证明的文章中都能看到这样三个性质:

  • Completeness —— 完备性
  • Soundness —— 可靠性
  • Zero-Knowledge —— 零知识

但是少有文章深入解释这个特性背后的深意和洞见。

在『系列(二)理解「模拟」』一文中,我们介绍了「模拟器」这个概念。许多介绍文章也避而不谈「模拟」,但「模拟」可以说是安全协议中核心的核心,因为它是定义「安全性」的重要武器。

通常,我们定义安全会采用这样一种方式,首先列出一些安全事件,然后说明:如果一个系统安全,那么列出来的安全事件都不会发生。

Rather than giving a list of the events that are not allowed to occur, it (the definition of zero-knowledge proof) gives a maximalist simulation condition.

— Boaz Barak

借用密码学家 Boaz Barak 的话,翻译一下,「零知识证明」并不是通过给出一个不允许发生的事件列表来定义,而是直接给出了一个最极致的「模拟条件」。

所谓「模拟条件」是指,通过「模拟」方法来实现一个「理想世界」,使之与「现实世界」不可区分;而由于在理想世界中不存在知识,所以可以推导出结论:现实世界满足「零知识」。

我们继续分析下一个交互系统(安全协议)的三个性质:「完备性」、「可靠性」与「零知识」。

可靠性(Soundness):Alice 在没有知识的情况下不能通过 Bob 的验证。

完备性(Completeness):Alice 在有知识的情况下可以通过 Bob 的验证。

零知识(Zero-knowledge):Alice 在交互的过程中不会泄露关于知识的任何信息。

我们可以看出来「可靠性」和「完备性」有一种「对称性」。可靠性保证了恶意的 Alice 一定失败,而完备性保证了诚实的 Alice 一定成功。

「完备性」比较容易证明,只要 Alice 诚实,Bob 也诚实,那么皆大欢喜。这好比,写好一段代码,喂了一个测试用例,跑完通过收工。

我们来想想「可靠性」应该如何定义?这个可靠性的逆否命题是:(在现实世界中)如果 Alice 能通过 Bob 的验证,那么 Alice 一定有知识。或者说:Alice 知道那……个「秘密」!

下面的问题是如何证明 Alice 知道一个「秘密」?

这好像也很难,对不对?假如我们需要证明一台机器知道一个「秘密」,最简单的办法就是我们在机器的硬盘里,或者内存中找到这个「秘密」,但是这样暴露了秘密。如果这台机器是黑盒子呢?或者是 Alice 呢?我们没有读心术,猜不到她心里的那个秘密。

如何定义「To Know」?

「零知识」保证了 验证者 Bob 没有(计算)能力来把和「知识」有关的信息「抽取」出来。不能抽取的「知识」不代表不存在。「可靠性」保证了知识的「存在性」。

只有「知识」在存在的前提下,保证「零知识」才有意义

本文将探讨「可靠性」和「To Know」。


为了进一步分析「知识」,接下来首先介绍一个非常简洁,用途广泛的零知识证明系统 —— Schnorr 协议。这个协议代表了一大类的安全协议,所谓的 Σ-协议,而且 Schnorr 协议扩展也是 零知识数据交换协议 zkPoD [1] 的核心技术之一。

简洁的 Schnorr 协议

Alice 拥有一个秘密数字,a,我们可以把这个数字想象成「私钥」,然后把它「映射」到椭圆曲线群上的一个点 a*G,简写为 aG。这个点我们把它当做「公钥」。

  • sk = a

  • PK = aG

请注意「映射」这个词,我们这里先简要介绍「同态」这个概念。椭圆曲线群有限域之间存在着一种同态映射关系。有限域,我们用 Zq这个符号表示,其中素数 q是指有限域的大小,它是指从 0, 1, 2, …, q-1 这样一个整数集合。而在一条椭圆曲线上,我们通过一个基点,G,可以产生一个「循环群」,标记为 0G, G, 2G, …, (q-1)G,正好是数量为 q个 曲线点的集合。任意两个曲线点正好可以进行一种「特殊的二元运算」,G + G = 2G2G + 3G = 5G,看起来这个二元运算好像和「加法」类似,满足交换律和结合律。于是我们就用 +这个符号来表示。之所以把这个群称为循环群,因为把群的最后一个元素 (q-1)G,再加上一个 G就回卷到群的第一个元素 0G

给任意一个有限域上的整数 r,我们就可以在循环群中找到一个对应的点 rG,或者用一个标量乘法来表示 r*G。但是反过来计算是很「困难」的,这是一个「密码学难题」—— 被称为离散对数难题[2]。

也就是说,如果任意给一个椭圆曲线循环群上的点 R,那么到底是有限域中的哪一个整数对应 R,这个计算是很难的,如果有限域足够大,比如说 256bit 这么大,我们姑且可以认为这个反向计算是不可能做到的。

Schnorr 协议充分利用了有限域和循环群之间单向映射,实现了最简单的零知识证明安全协议:Alice 向 Bob 证明她拥有 PK 对应的私钥 sk

第一步:为了保证零知识,Alice 需要先产生一个随机数,r,这个随机数的用途是用来保护私钥无法被 Bob 抽取出来。这个随机数也需要映射到椭圆曲线群上,rG

第二步:Bob 要提供一个随机数进行挑战,我们把它称为 c

第三步:Alice 根据挑战数计算 z = r + a * c,同时把 z发给 Bob,Bob通过下面的式子进行检验:

z*G ?= R + c*PK = rG + c*(aG)

大家可以看到 Bob 在第三步「同态地」检验 z 的计算过程。如果这个式子成立,那么就能证明 Alice 确实有私钥 a。可是,这是为什么呢?

z 的计算和验证过程很有趣,有几个关键技巧:

  1. 首先 Bob 必须给出一个「随机」挑战数,然后 Bob 在椭圆曲线上同态地检查 z 。如果我们把挑战数 c 看成是一个未知数,那么 r+a*c=z 可以看成是一个一元一次方程,其中 ra 是方程系数。请注意在 c 未知的前提下,如果 r + a*x = r' + a'*x 要成立,那么根据 Schwatz-Zippel 定理[3],极大概率上 r=r'a=a' 都成立。也就是说, Alice 在 c 未知的前提下,想找到另一对不同的 r',a' 来计算 z 骗过 Bob 是几乎不可能的。这个随机挑战数 c 实现了ra 的限制。虽然 Bob 随机选了一个数,但是由于 Alice 事先不知道,所以 Alice 不得不使用私钥 a 来计算 z。这里的关键: c 必须是个随机数。
  2. Bob 验证是在椭圆曲线群上完成。Bob 不知道r,但是他知道 r 映射到曲线上的点R;Bob 也不知道 a,但是他知道 a 映射到曲线群上的点 PK,即 a*G。通过同态映射与Schwatz-Zippel 定理,Bob 可以校验 z 的计算过程是否正确,从而知道 Alice 确实是通过 ra 计算得出的 z,但是又不暴露 ra 的值。
  3. 还有,在协议第一步中产生的随机数 r 保证了 a 的保密性。因为任何一个秘密当和一个符合「一致性分布」的随机数相加之后的和仍然符合「一致性分布」。

证明零知识

我们这里看一下 Schnorr 协议如何证明一个弱一些的「零知识」性质——「SHVZK」:

注:这里我们证明的仅仅是 Special Honest Verifier Zero-Knowledge(SHVZK)。SHVZK 要求协议中的 Bob 的行为不能不按常理出牌,比如他必须按协议约定,在第二步时,去传送带上取一个新鲜的随机数,并且立即使用。而通常意义上的「零知识」是不会对 Bob 做任何要求,所以我们说这里是一个弱一些的性质。虽然目前 Schnorr 协议不能证明完全的「零知识」,但经过添加一些协议步骤,就可以达到完全零知识的目的,细节这里不展开,有兴趣的读者请参考文献[4]。以后我们在讨论 Fiat-Shamir 变换时,还会再次讨论这个问题。

首先「模拟器」模拟一个「理想世界」,在理想世界中模拟出一个 Zlice 和 Bob 对话,Zlice 没有 Schnorr 协议中的知识,sk,而 Bob 是有公钥 PK的。请大家看下图,Bob 需要在 Schnorr 协议中的第二步出示一个随机数 c,这里有个额外的要求, 就是 Bob 只能「诚实地」从一个外部「随机数传送带」上拿一个随机数,每一个随机数都必须是事先抛k次「硬币」产生的一个 2^k 范围内的一次性分布随机数。Bob 不能采用任何别的方式产生随机数,这就是为何我们要求 Bob 是诚实的。

下面演示 Zlice 如何骗过 Bob:

序幕:请注意 Zlice 没有关于sk的知识,这时 Bob 的随机数传送带上已经预先放置了一些随机数。

第一步:Zlice 产生一个一致性分布的随机数c,并且利用一个新的「超能力」,将刚刚产生的随机数 c 替换掉 Bob 的随机数传送带上第一个随机数。这时候,Bob 无法察觉。

第二步:Zlice 再次产生一个随机数 z,然后计算 R'=z*G - c*PK,并将 R'发送给 Bob。

第三步:这时候Bob 会从随机数传送带上取得 c,并且将 c 发送给 Zlice。请注意这个c 正好就是第一步中 Zlice 产生的 c

第四步:Zlice 将第三步产生的随机数 z 发送给 Bob,Bob 按照 Schnorr 协议的验证公式进行验证,大家可以检查下,这个公式完美成立。

大家可以再对比下「现实世界」的 Schnorr 协议,在两个世界中,Bob 都能通过验证。

但区别是:

  • 在「理想世界中」,Zlice 没有 sk;而在「现实世界中」,Alice 有 sk
  • 在「理想世界中」,z 是一个随机数,没有涉及 sk;而在「现实世界中」,z 的计算过程里面包含 sk
  • 在「理想世界中」,Zlice 使用了超能力,替换了 Bob 的随机数;而在「现实世界中」,Alice 看不到 Bob 的随机数传送带,也无法更改传送带上的数字

这里请大家思考下:Schnorr 协议中,Bob 在第二步发挑战数能不能和第一步对调顺序?也就是说 Bob 能不能先发挑战数,然后 Alice 再发送 R = r*G

(两分钟后……)

答案是不能。

如果 Alice 能提前知道随机数,那么 (现实世界中的)Alice 就可以按照模拟器 Zlice 做法来欺骗 Bob。

再遇模拟器

其实,「可靠性」和「零知识」这两个性质在另一个维度上也是存在着一种对称性。可靠性保证了恶意的 Alice 一定失败,零知识保证了恶意的 Bob (窃取知识)一定不会成功。有趣地是,这种对称性将体现在模拟出来的「理想世界」中。

我们分析下可靠性这个定义:Alice 没有知识 导致 Bob 验证失败。它的逆否命题为:Bob 验证成功 导致 Alice 一定有知识。

我们再次求助模拟器,让他在可以发挥超能力的「理想世界」中,去检验 Alice 的知识。

再次,请大家设想在平行宇宙中,有两个世界,一个是叫做「理想世界」,另一个叫做「现实世界」。理想世界有趣的地方在于它是被「模拟器」模拟出来的,同时模拟器可以在理想世界中放入带有超能力的 NPC。这次把 Alice 的两个分身同时放入「理想世界」与「现实世界」。

假设「你」扮演 Bob 的角色,你想知道和你对话的 Alice 是否真的是「可靠的」。 于是把你放入「理想世界」,借助一个具有超能力的 NPC,你可以把对面的 Alice 的知识「抽取」出来。

W…hat?我们不是刚刚证明过:协议是零知识的吗?零知识就意味着 Bob 抽取不出任何的「知识」碎片。这里敲黑板,「零知识」是对于「现实世界」而言的。我们现在正在讨论的是神奇的「理想世界」。

重复一遍,在「理想世界」中,你可以借助一个有超能力的 NPC 来抽取 Alice 的知识,从而可以保证「现实世界」中的 Alice 无法作弊。可以想象一下,一个作弊的 Alice,她肯定没有知识,没有知识也就不可能在「理想世界」中让 NPC 抽取到任何东西。

然而在「现实世界」中,你无法借助 NPC,当然也就看不到 Alice 的知识,也就不会和「零知识」性质冲突。因为两个世界发生的事件是「不可区分」的,我们可以得到这样的结论:在「现实世界」中,Alice 一定是存在知识的。

整理一下思路:如何证明在一个交互会话中 Alice 不能作弊呢?我们需要为这个交互会话定义一个「模拟算法」,该算法可以模拟出一个「理想世界」,其中有一个特殊的角色叫做「抽取器」(Extractor),也就是我们前面说的 NPC,它能够通过「超能力」来「抽取」Alice 的知识,但是让对方「无所察觉」。

注意,超能力是必不可少的!这一点在『系列(二)理解「模拟」』有解释,如果模拟器在没有超能力的情况下具备作弊能力,那相当于证明了协议「不可靠」(Unsoudness)。同样地,如果「抽取器」在没有超能力的情况下具备抽取信息能力,那相当于证明了协议不零知(Not-zero-knowledge)。

最后一点,超能力是什么?这个要取决于具体的交互系统的证明,我们接下来就先拿我们刚刚讲过的Schnorr 协议切入。

Proof of Knowledge :「知识证明」

我们来证明一下 Schnorr 协议的「可靠性」,看看这个超能力 NPC 如何在「理想世界」中把 Alice 私钥抽取出来。而这个「超能力」,仍然是「时间倒流」。

schnorr-extractor-1

第一步:Alice 选择一个随机数 r,并且计算 R=r*G,并将 R 发给「抽取器」

schnorr-extractor-2

第二步:抽取器也选择一个随机的挑战数c,并且发给 Alice

schnorr-extractor-3

第三步:Alice 计算并且回应 z,然后抽取器检查 z是否正确

schnorr-extractor-4

第四步:抽取器发现 z 没有问题之后,发动超能力,将时间倒回第二步之前

schnorr-extractor-5

第五步:抽取器再次发送一个不同的随机挑战数 c'给 Alice,这时候 Alice 回到第二步,会有一种似曾相识的感觉,但是无法感知到时间倒回这个事实

schnorr-extractor-6

第六步:Alice 再次计算了 z',然后发给抽取器检查

schnorr-extractor-7

第七步:这时候抽取器有了zz',就可以直接推算出 Alice 所拥有的私钥 a,达成「知识抽取」

到这里,「可靠性」就基本证明完了。大家是不是对可靠性和零知性的「对称性」有点感觉了?

总结一下:「抽取器」在「理想世界」中,通过时间倒流的超能力,把 Alice 的「知识」完整地「抽取」出来,这就保证了一个没有知识的 Alice 是无法让抽取器达成目标,从而证明了「可靠性」。

注:并不是所有的可靠性都必须要求存在抽取器算法。采用抽取器来证明可靠性的证明系统被称为「Proof of Knowledge」。

解读 ECDSA 签名攻击

在区块链系统中到处可见的ECDSA 签名方案也是一个朴素的零知识证明系统。椭圆曲线数字签名方案 ECDSA 与 Schnorr 协议非常接近,基于 Schnorr 协议的签名方案发表在 1991年的『密码学杂志』[5]上。1991年,正值美国国家标准局(NIST)选择数字签名算法,优雅的 Schnorr 签名方案居然被申请了专利,因此 NIST 提出了另一套签名方案 DSA(Digital Signature Algorithm),随后这个方案支持了椭圆曲线,于是被称为 ECDSA。中本聪在构思比特币时,选择了 ECDSA 作为签名算法,但是曲线并没有选择 NIST 标准推荐的椭圆曲线 —— secp256-r1,而是 secp256-k1。因为江湖传言,NIST 可能在椭圆曲线参数选择上做了手脚,导致某些机构可以用不为人知的办法求解离散对数难题,从而有能力在「现实世界」中具备超能力。有不少人在怀疑,也许当年中本聪在设计比特币时,也有这种考虑,故意选择了 secp256-k1 这样一条貌似安全性稍弱的曲线。

我们拆解下 ECDSA 签名,用交互的方式定义一个类似 ECDSA 的认证方案,交互见下图。

ecdsa-sig

第一步:Alice 仍然是选择一个随机数 k,并将 k 映射到椭圆曲线上,得到点 K ,然后发送给 Bob

第二步:Bob 需要产生两个随机数,ce,然后交给 Alice

第三步:Alice 计算 s,并且发送给 Bob,他来验证 s 的计算过程是否正确

注:对熟悉 ECDSA 签名方案的读者,这里略作解释,Bob 产生的 c 对应被签消息的 Hash 值 Hash(m),而 e 则是由一个转换函数 F(K)来产生。其中 F(.) 是取椭圆曲线上的点的 x 坐标经过 (mod q) 得到[6]。

江湖上流传着一个说法:ECDSA 签名方案有个严重的安全隐患,如果在两次签名中使用了同一个随机数,那么签名者的私钥将会暴露出来。其实 Schnorr 签名方案也有同样的问题。

当年 Sony PlayStation 3 的工程师在调用 ECDSA 库函数时,本来应该输入随机数的参数位置上,却传入了一个常数。熟悉密码学的黑客们发现了这个严重的后门。2011年1月,神奇小子 Geohot 公开发布了 Sony PS3 的主私钥,这意味着任何用户都可以轻松拿到游戏机的 root 权限。Sony 随后大为光火…… (后续故事大家可以上网搜)

如果 Alice 在两次交互过程中使用了同一个 K,那么 Bob 可以通过发送两个不同的 cc' 来得到 ss',然后通过下面的公式算出私钥 a

k = (c - c')/(s - s')
a = (k * s - c)/e

那么我们应该怎么来看这个「安全后门」呢?大家想想看,这个安全后门和我们前面证明过的 Schnorr 协议的可靠性证明几乎一模一样!这个算法正是 ECDSA 认证协议的「可靠性」证明中的「抽取器」算法。只不过在可靠性证明中,为了让 Alice 使用同一个随机数 k 来认证两次,「抽取器」需要利用「时间倒流」的超能力。

但是在 Sony PS3 系统中,随机数被不明所以的工程师写成了一个固定不变的值,这样相当于直接赋予了黑客「超能力」,而这是在「现实世界」中。或者说,黑客在不需要「时间倒流」的情况下就能实现「抽取器」。

提醒下,不仅仅是随机数不能重复的问题。而是随机数必须是具有密码学安全强度的随机数。

设想下,如果随机数 r 是通过一个利用「线性同余」原理的伪随机数生成器产生,虽然 r的值一直在变化,但是仍然不能阻止「知识抽取」。假设线性同余算法为 r2= d*r1 + e (mod m),还回到 Schnorr 协议的第三步:

1: z1 = r1 + c1*a
2: z2 = r2 + c2*a

如果攻击者让 Alice 连续做两次签名,那么将 r2 代入 r1 之后,就出现了两个线性方程求解两个未知数 (r1, a) 的情况,z1, z2, c1, c2, d, e 对于 攻击者是已知的,这个方程组只用初中数学知识就可以求解。

请注意,这并不是 Schnorr 协议(或 ECDSA 协议)的「设计缺陷」,恰恰相反,这是 Schnorr 协议设计比较精巧的地方,它从原理上保证了协议的可靠性。类似技巧在密码学协议中频繁出现,达到一目了然的「简洁」。但是也不得不说,如果不清楚协议的内在机制,尤其是区分不清楚「理想世界」与「现实世界」,使用者很容易引入各种花式的「安全漏洞」。

作为一个能写出可靠软件的靠谱码农,我们需要了解哪些?彻底理解安全协议的设计机制当然是最好的,但是绝大多数情况下是非常耗费精力的。一般来说,我们把各种密码学工具当做「黑盒」来用,可能是不够的,我们最好还能了解下:

  1. 「安全定义」是什么?
  2. 「安全假设」到底是什么?
  3. 「理想世界」中的「超能力」到底是什么?

脑洞:我们生活在模拟世界中吗

第一次读懂「模拟器」时,我第一时间想到的是电影『黑客帝国』。我们生活所在「现实世界」也许是某一个模拟器模拟出来的「理想世界」,我们所看到、听到的以及感知到的一切都是被「模拟」出来的。在「现实世界」里,我们活在一个母体中。然而我们并不能意识到这一点。

早在春秋战国时期,庄子也在思考类似的问题:

昔者庄周梦为胡蝶,栩栩然胡蝶也,自喻适志与,不知周也。俄然觉,则蘧蘧然周也。不知周之梦为胡蝶与,胡蝶之梦为周与?周与胡蝶,则必有分矣。此之谓物化。——《庄子·齐物论》

通俗地解释下:庄子有一天睡着了,梦见自己变成了一只蝴蝶,翩翩起舞,醒来之后发现自己还是庄子,在梦中,蝴蝶并不知道自己是庄子。于是庄子沉思到底是他梦中变成了蝴蝶,还是蝴蝶梦中变成了庄子呢?如果梦境足够真实,……

「缸中之脑」是美国哲学家 Gilbert Harman 提出的这样一个想法:一个人的大脑可以被放入一个容器里面,然后插上电线,通过模拟各种电信号输入,使得大脑以为自己活在真实世界中。

这个想法源自哲学家笛卡尔的《第一哲学沉思集》[7],在书中他论证我们应该怀疑一切,需要逐一检验所有人类的知识,数学,几何,以及感知到的世界。然而他发现除了「我思故我在」之外,所有的知识都可能不靠谱,因为我们的大脑很可能被一个具有「超能力」的 Evil Demon 所欺骗。

2003 年牛津大学的哲学教授 Nick Bostrom 郑重其事地写了一篇论文『我们生活在计算机模拟世界中吗?』[8]。认为以下三个事实中,至少有一个成立:

  1. 人类文明彻底灭绝。
  2. 人类文明已经到达可以完全模拟现实世界的科技水平,但是处于某种原因,没有一个人愿意去创造出一个新的模拟世界,充当上帝的角色。
  3. 我们现在的人类文明就生活在一个模拟世界中。

硅谷企业家 Elon Musk 在一次公开采访中,谈到「我们生活在基础现实世界」的概率只有「十亿分之一」。也就是说,他认为我们生活在一个电脑游戏(模拟世界)中,在模拟世界之外,有一个程序员,他开发并操纵了这个世界,我们每个人都是一个游戏角色( NPC)。

在玩腻越狱 iPhone 和自动驾驶之后,神奇小子 Geohot 在今年三月份的「西南偏南」大会上做了一个题为「Jailbreaking the Simulation」的演讲[9]。他认为,我们被生活在一个模拟世界中,所谓的上帝就是外部世界里活蹦乱跳的码农们,他们编程创造了我们的「现实世界」,当然,他们可能启动了不止一个世界副本。然而,他们可能也生活在一个外层「模拟世界」中。

Jailbreaking the Simulation

如果我们确实生活在模拟世界中,或许我们可以在地球的某个地方找到一个后门——「Simulation Trapdoor」,从而获得「模拟器」的超能力,抽取出不可思议的「秘密知识」。

如果我们的世界的确是被程序模拟出来的,这个程序也许会有 Bug,如果有 Bug 存在,说不定我们可以利用这个 Bug 进行越狱,跳出「理想世界」,到达外面一层的世界中,与可爱的码农上帝聊一聊。

这是在开玩笑吗?下面摘自自知乎的一个段子[10]:

  • 问题:「如果世界是虚拟的,有哪些实例可以证明?」。
  • 回答:
  1. 为什么宏观上丰富多彩,但是微观的基本粒子却都是一模一样的?这正和图片富多彩,但是像素是一模一样的一回事
  2. 为什么光速有上限?因为机器的运行速度有限
  3. 为什么会有普朗克常量?因为机器的数据精度有限
  4. 为什么微观粒子都是几率云?这是为了避免系统陷入循环而增加的随机扰动
  5. 为什么有泡利不相容原理?看来系统采用的数据组织是多维数组
  6. 为什么量子计算机运行速度那么快,一瞬间可以尝试所有可能?因为这个本质上是调用了宿主机的接口
  7. 为什么会有量子纠缠?这实际上是引用同一个对象的两个指针
  8. 为什么会有观察者效应?这显然是lazy updating
  9. 为什么时间有开端?系统有启动时间

未完待续

设计一个密码学协议就好像在走钢丝,如果你想同时做到「零知识」和「可靠性」就意味着既要让协议内容充分随机,又要保证「知识」能够参与协议的交互。如果协议没有正确设计,亦或没有正确工程实现,都将导致系统安全性坍塌。比如可能破坏了零知性,导致「知识」在不经意间泄露;或者也许破坏了可靠性,导致任何人都能伪造证明。而且这种安全性,远比传统的代码底层机制漏洞来得更加严重,并且更难被发现。严格数学论证,这似乎是必不可少的。

我们的世界真的是某个「三体文明」模拟出来的吗?不能排除这个可能性,或许,我们需要认真地重新审视自己的各种执念。不过那又怎么样呢?至少自己的「思想」是真实的。

If you would be a real seeker after truth, it is necessary that at least once in your life you doubt, as far as possible, all things. 如果你是一个真正的真理探求者,在你人生中至少要有一次,尽可能地质疑所有的事情。 —— 笛卡尔

致谢:特别感谢 Shengchao Ding, Jie Zhang,Yu Chen 以及安比实验室小伙伴们(p0n1, even, aphasiayc, Vawheter, yghu, mr)的建议和指正。

参考文献

  • [1] zkPoD: 区块链,零知识证明与形式化验证,实现无中介、零信任的公平交易. 安比实验室. 2019.
  • [2] Hoffstein, Jeffrey, Jill Pipher, Joseph H. Silverman, and Joseph H. Silverman. An introduction to mathematical cryptography. Vol. 1. New York: springer, 2008.
  • [3] Schwartz–Zippel Lemma. Wikipedia. https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma
  • [4] Damgård, Ivan. “On Σ-protocols.” Lecture Notes, University of Aarhus, Department for Computer Science (2002).
  • [5] Schnorr, Claus-Peter. “Efficient signature generation by smart cards.” Journal of cryptology 4.3 (1991): 161-174.
  • [6] Brown, Daniel RL. “Generic groups, collision resistance, and ECDSA.” Designs, Codes and Cryptography 35.1 (2005): 119-152.
  • [7] 笛卡儿, 徐陶. 第一哲学沉思集. 九州出版社; 2008.
  • [8] Bostrom, Nick. “Are we living in a computer simulation?.” The Philosophical Quarterly 53.211 (2003): 243-255.
  • [9] Nick Statt. “Comma.ai founder George Hotz wants to free humanity from the AI simulation”. Mar 9, 2019. https://www.theverge.com/2019/3/9/18258030/george-hotz-ai-simulation-jailbreaking-reality-sxsw-2019
  • [10] doing@知乎. “如果世界是虚拟的,有哪些实例可以证明?”. 2017. https://www.zhihu.com/question/34642204/answer/156671701

随机「挑战」

探索零知识证明系列(四)

“Challenges are at times an indication of Lord’s trust in you.” 挑战,有时是上天信任你的一种表现。― D. Todd Christofferson

本文继续长篇大论零知识证明背后的机制原理,希望帮助大家理解这一类「现代密码学工具」的大致轮廓。本文约8000字,少量数学公式。

交互与挑战

我们之前介绍的零知识证明系统都是「交互式」的,需要验证者 Bob 在交互中提供一个或若干个「随机数」来挑战,比如「地图三染色问题」(参看『系列二』)中,验证者 Bob 需要「不断地」随机挑选一条边来挑战 Alice 的答案,直到 Bob 满意为止,而 Alice 的作弊概率会「指数级」地衰减。而让 Bob 相信证明的「基础」取决于 Bob 所挑选的随机数是不是足够随机。如果 Alice 能够提前预测到 Bob 的随机数,灾难就会发生,现实世界就会退化成「理想世界」,而 Alice 就可以立即升级成「模拟器」,通过超能力来愚弄 Bob。

而『系列三』中,我们分析了 Schnorr 协议,协议中虽然验证者 Bob 只需要挑选一个随机数 c 来挑战 Alice ,让她计算一个值 z,但 Bob 绝对不能让 Alice 有能力来预测到 c 的任何知识,否则,Alice 也会变身成模拟器。

随机数的重要性不言而喻:

通过随机数挑战是交互式零知识证明的「信任根基」。

但,「交互过程」会限制应用场景。如果能将交互式零知识证明变成「非交互」?这会非常非常激动人心。所谓的非交互可以看成是只有「一轮」的证明过程,即Alice 直接发一个证明给 Bob 进行验证。

非交互式零知识证明,英文是 Non-Interactive Zero Knowledge,简称 NIZK。它意味整个证明被编码为一个「字符串」,它可以写到一张纸上,通过邮件、聊天工具等各种方式随意发送给任何验证者,字符串甚至可以放在 Github 上随时供大家下载验证。

在区块链世界,「NIZK」可以作为共识协议的一部分。因为一个交易需要多个矿工进行校验。设想下,如果交易的发送者和每个矿工都要交互一下,让矿工进行挑战,那么共识过程将奇慢无比。而非交互式零知识证明则可以直接广播给所有的矿工节点,让他们自行验证。

可能有朋友会问:只让一个矿工挑战不就够了吗?把矿工和交易发送者的交互脚本编码成证明,然后广播给其他矿工,然后其他矿工就直接相信这个挑战过程是可信的,不也可以吗?但是,很显然,这里需要相信第一个交互矿工作为可信第三方,第三方?似乎不是一个好主意……

而非交互式零知识证明,以下我们直接说「NIZK」,似乎就很理想了,没有第三方赚差价。

「非交互」带来的困惑

非交互式零知识证明,NIZK,如果存在,那么它要比交互式证明强大得多。

  • 交互式证明,只能取信于一个验证者;而 NIZK 可以取信于多个验证者,以至所有人。
  • 交互式证明,只能在交互的那个时刻有效;而 NIZK 将始终有效。

NIZK 不仅可以跨越空间,还能跨越时间

听上去很美,不是吗?But, ……

重复下上节的一个结论:

通过随机数挑战是交互式零知识证明的「信任根基」。

可是如果 NIZK 失去了挑战过程,有什么后果?

我们已经回忆过「零知识」性质的证明(参考『系列二』),证明过程需要构造一个模拟器(算法),它也和验证者(Bob)在理想世界中进行交互,而验证者 Bob 没有能力区分出来对方是否是真的 Alice 还是一个模拟器。

如果现在考虑下 NIZK 中的 非交互式,假如「我」向「你」出示一张纸,上面写着一个「真」证明 X ,又假如「你」在看过这张纸之后确实相信我了;又因为协议是「零知识」,那么如果把「我」换成一个模拟器,模拟器也能「伪造」一个假证明 Y,能够也让「你」相信。

好了,问题来了:

  • 你如何区分 XY ,孰真孰假?当然你无法区分,因为协议是零知识的,你必须不能区分
  • 我可以同样可以把 Y 出示给你看,那岂不是「我」就可以欺骗你了吗?

是不是不和谐了?请大家在此处思考两分钟。

(两分钟后……)

因为 NIZK 没有了交互,也就没了挑战过程,所有的证明过程都有 Alice 来计算书写,理论上 Alice 确实是想写什么就写什么,没人拦得住,比如 Alice 就写「理想世界」的 假证明 Y

想必深刻理解模拟器的朋友,在这里会发现一个关键点:模拟器必须只能在「理想世界」中构造Y,也就是说,Y 这么邪恶的东西只能存在于「理想世界」,不能到「现实世界」祸害人间。

继续思考……

还有一个更深层次的问题,请大家回忆下「地图三染色问题」,之所以模拟器不能在「现实世界」中为非作歹,核心原因是,他在理想世界中有「时间倒流」的超能力,而在「现实世界」中不存在这种黑魔法。现实世界的「不存在性」是关键。

而且,NIZK 中没有交互,于是导致了一个严重的后果,模拟器没有办法使用「时间倒流」这个超能力,当然似乎也就不能区分证明者在两个世界中的行为。

换句话说,如果我们面对任何一个 NIZK 系统,似乎「模拟器」就很难高高在上了,它好像只能飘落人间,成为一个普普通通的凡人。如果,我说如果,按此推论,假设模拟器不再具备超能力,那就意味着 Alice 和模拟器没有区别,Alice 也可以成为一个模拟器,再继续推论,Alice 就可以在「现实世界」中任意欺骗 Bob,那么这个证明系统就不再有价值,因为它失去了「可靠性」。结论:任何的 NIZK 都不可靠。

这一定是哪里出了问题……

上面我们在分析的过程中,提到了交互挑战的缺失。确实,如果 Bob 不参与 Alice 产生证明的过程,证明所包含的每一个 bit 都由 Alice 提供,似乎「证明」本身不存在任何让 Bob 信任的「根基」。这个从「直觉」上似乎说不通。

那是不是说,没有 Bob 的参与就「彻底」没办法建立「信任根基」了呢?信任的根基还可以从哪里来呢?

答案是「第三方」!

Wait ……,协议交互不是只有两方吗? Alice 和 Bob,哪来第三方?

需要用特殊的方式引入第三方,而且方法不止一种,我们先研究第一种。

(泪目:不是说的好好的,咱们不引入第三方吗?)

回顾 Schnorr 协议

我们再看一下老朋友——Schnorr 协议,它是一个三步协议:第一步,Alice 发送一个承诺,然后第二步 Bob 发送随机数挑战,第三步,Alice 回应挑战。

我们来看,如何把一个三步的 Schnorr 协议变成一步。

看一下 Schnorr 协议的第二步,Bob 需要给出一个随机的挑战数 c,这里我们可以让 Alice 用下面这个式子来计算这个挑战数,从而达到去除协议第二步的目的。

c = Hash(PK, R)

其中 R 是 Alice 发给 Bob 的椭圆曲线点,PK 是公钥。大家可以好好看看这个利用 Hash 算法计算 c 的式子。这个式子达到了两个目的:

  1. Alice 在产生承诺 R 之前,没有办法预测 c,即使 c 最终变相是 Alice 挑选的
  2. c 通过 Hash 函数计算,会均匀分布在一个整数域内,而且可以作为一个随机数(注:请大家暂且这么理解,我们在后文再深入讨论

请注意:Alice 绝不能在产生 R 之前预测到 c,不然, Alice 就等于变相具有了「时间倒流」的超能力,从而能任意愚弄 Bob。

而一个密码学安全 Hash 函数是「单向」的,比如 SHA256,SHA3,blake2 等等。这样一来,虽然 c 是 Alice 计算的,但是 Alice 并没有能力实现通过挑选 c 来作弊。因为只要 Alice 一产生 Rc 就相当于固定下来了。我们假设 Alice 这个凡人在「现实世界」中是没有反向计算 Hash 的能力的。

schnorr-nizk

看上图,我们利用 Hash 函数,把三步 Schnorr 协议合并为了一步。Alice 可以直接发送:(R, c, z)。又因为 Bob 拥有 PK,于是 Bob 可以自行计算出 c,于是 Alice 可以只发送 (R, z) 即可。

我们把上面这个方案稍微变下形,就得到了「数字签名」方案。所谓的数字签名,就是「我」向「你」出示一个字符串,比如「白日依山尽,黄河入海流」,然后为了证明这句诗是我出示的,我需要签署某样东西。这个东西能证明我的身份和这句诗进行了关联。

从 NIZK 角度看数字签名

不严格地说,数字签名方案相当于在证明(1)我拥有私钥,并且(2)私钥和消息进行了关联计算。

我首先要证明我的身份,那么这个简单,这正是 Schnorr 协议的功能,能够向对方证明「我拥有私钥」这个陈述。并且这个证明过程是零知识的:不泄露关于「私钥」的任何知识。

那么如何和这句唐诗关联呢?我们修改下计算 c 的过程:

m = "白日依山尽,黄河入海流"
c = Hash(m, R)

这里为了保证攻击者不能随意伪造签名,正是利用了离散对数难题(DLP)与 Hash 函数满足抗第二原象(Secondary Preimage Resistance )这个假设。

注:这里严格点讲,为了保证数字签名的不可伪造性,需要证明 Schnorr 协议满足「Simulation Soundness」这个更强的性质。这点请参考文献[2]

上图就是大家所熟知的数字签名方案 —— Schnorr 签名方案[1]。在这里还有一个优化,Alice 发给 Bob 的内容不是 (R, z) 而是 (c, z),这是因为 R 可以通过 c, z 计算出来。

注:为什么说这是一个「优化」呢?目前针对椭圆曲线的攻击方法有 Shanks 算法、Lambda 算法 还有 Pollard’s rho 算法, 请大家记住他们的算法复杂度大约都是 [3],n 是有限域大小的位数。假设我们采用了非常接近 2^256 的有限域,也就是说 z 是 256bit,那么椭圆曲线群的大小也差不多要接近 256bit,这样一来,把 2^256 开平方根后就是 2^128,所以说 256bit 椭圆曲线群的安全性只有 128bit。那么,挑战数 c 也只需要 128bit 就足够了。这样 Alice 发送 c 要比发送 R 要更节省空间,而后者至少需要 256bit。cz两个数值加起来总共 384bit。相比现在流行的 ECDSA 签名方案来说,可以节省1/4 的宝贵空间。现在比特币开发团队已经准备将 ECDSA 签名方案改为一种类 Schnorr 协议的签名方案——muSig[4],可以实现更灵活地支持多签和聚合。

而采用 Hash 函数的方法来把一个交互式的证明系统变成非交互式的方法被称为 Fiat-Shamir 变换[5],它由密码学老前辈 Amos Fiat 和 Adi Shamir 两人在 1986 年提出。

重建信任 —— 随机预言精灵

前文提到,失去了挑战,似乎失去了证明的「信任根基」。而在 Schnorr 签名方案中,Hash 函数担负起了「挑战者」的角色,这个角色有一个非常学术的名字:「随机预言机」(Random Oracle)[6]。

可是这里为何用 Hash?实际上当 Alice 要产生公共随机数时,需要一个叫做「随机预言机」的玩意儿,这是什么?

开脑洞时间到!

我们设想在「现实世界」中,天上有一位「精灵」,他手持一个双栏表格,左边一栏为字符串,右边一栏为数字。任何人,包括你我,包括 Alice 和 Bob,都可以发字符串给「精灵」。

精灵在拿到字符串之后,会查表的左边栏,看看表格里有没有这个字符串,下面分两种情况:

  • 情况一:如果左边栏找不到字符串,那么精灵会产生一个「真随机数」,然后把字符串与随机数写入到表格中,然后把随机数返回地面上的凡人。
  • 情况二:如果左边栏有这个字符串记录,那么精灵会将右边栏里面的数字直接返回给地面。

大家会发现这个精灵的行为其实很像一个随机数发生器,但是又很不一样,不一样的地方在于当我们发送相同的字符串时,他会返回相同的数。这个精灵就是传说中的「随机预言机」。

而在合并 Schnorr 协议过程中,其实我们需要的是一个这样的随机预言精灵,而不是一个 Hash 函数。两者有什么不同的地方?区别就是:

  • 随机预言机每次对于新字符串返回的是一个具有一致性分布的「真」随机数
  • Hash 函数计算的结果并不是一个真正具有一致性分布的随机数

那么为什么前面用的是 Hash 函数呢?这是因为在现实世界中,**真正的随机预言机不存在!**为什么呢? 事实上,一个 Hash 函数不可能产生真的随机数,因为 Hash 函数是一个「确定性」算法,除了参数以外,再没有其它随机量被引入。

而一个具有密码学安全强度的 Hash 函数「似乎」可以充当一个「伪」随机预言机。那么合并后的安全协议需要额外增加一个很强的安全假设,这就是:

假设:一个密码学安全的 Hash 函数可以近似地模拟传说中的「随机预言机」

因为这个假设无法被证明,所以我们只能信任这个假设,或者说当做一个公理来用。插一句, Hash 函数的广义抗碰撞性质决定了它的输出可以模拟随机数,同时在很多情况下(并非所有),对 Hash 函数实施攻击难度很高,于是许多的密码学家都在大胆使用。

不使用这个假设的安全模型叫做「标准模型」,而使用这个假设的安全模型当然不能叫「非标准模型」,它有个好听的专有名词,叫做「随机预言模型」。

世界上有两种不同类型的人,喜欢甜豆花的,不喜欢甜豆花的。同样,世界上的密码学家分为两种,喜欢随机预言模型的,和不喜欢随机预言模型的[6]。

构造根基 —— 被绑架的精灵

Schnorr 协议经过 Fiat-Shamir 变换之后,就具有 NIZK 性质。这不同于我们证明过的 SHVZK,SHVZK 要求验证者诚实,而 NIZK 则不再对验证者有任何不现实的要求,因为验证者不参与交互,所谓要求诚实的验证者这个问题就不复存在。

注:如果验证者 Bob 不诚实会怎样?那么 Bob 有可能抽取出 Alice 的知识。但是对于三步 Schnorr 协议而言,它是否满足「零知识」,目前还处于未知状态。我们在系列三中只证明了它满足一个比较弱的性质:SHVZK

但是,当 Schnorr 协议摇身一变,变成非交互零知识证明系统之后,就真正的「零知识」了。

然而,可能你的问题也来了,这个论断听起来似乎有道理,请问能证明吗?

时间到了,“翠花,上模拟器”

怎么用模拟器大法来构造一个「理想世界」呢?大家可以想一下,我们之前使用过「时间倒流」,还有修改「随机数传送带」超能力来让「模拟器」来作弊。可是没有交互了,这就意味着:「时间倒流」超能力不能用;Bob 的随机数传送带也不存在了,「篡改传送带」这个超能力也不能用!

但模拟器总要具备某种「超能力」,从而能够构建信任的「根基」

(如果模拟器在没有超能力的情况下具备作弊能力,那相当于证明了协议的不可靠性)。

可能大家现在已经猜出来了,模拟器要在「随机预言机」上动手脚。

先考虑下构造一个「理想世界」来证明「零知识」。在理想世界中,模拟器「绑架」了负责提供预言的「精灵」,当 Bob 向精灵索要一个随机数的时候,精灵并没有给一个真随机数,而是给 Zlice(模拟器假扮的 Alice)提前准备好的一个数(也符合一致性分布,保证不可区分性),「精灵」无可奈何地返回 Bob 一个看起来随机,但实际上有后门的数字。所谓后门,就是这个数字是 Zlice 自己提前选择好的

  • 第一步:Zlice 随机选择 z,随机选择c,计算 R'=z*G - c*PK

  • 第二步:Zlice 将 c(m, R') 写入精灵的表格。

  • 第三步:Zlice 将签名 (c, z) 发送给 Bob。

  • 第四步:Bob 计算 R=z*G - c*PK,并向精灵发送 (m, R),精灵返回 c’。请注意,这里 Bob 计算出来的 R 和 Zlice 计算出来的 R' 是相等。

  • 第五步:Bob 验证 c ?= c',看看精灵传回来的随机数和对方发过来的随机数是否相等。如果相等,则验证签名通过;否则,则验证失败。

通过绑架「精灵」,Zlice 同样可以提前预知随机数,这和时间倒流能达到同样的效果。

我们已经证明了模拟器 Zlice 的「存在性」,于是我们上面已经证明了 NIZK。

接下来我们证明这个这个协议的「可靠性」。设想在另一个「理想世界」中,一个叫做「抽取器」的玩意儿,也同样绑架了精灵。当无辜 Alice 的向「精灵」索要一个随机数时,「精灵」返回了一个 c1,「抽取器」从精灵的表格中偷窥到了c1,当 Alice 计算出来 z1 之后,然后这时候「抽取器」仍然可以发动「时间倒流」超能力,让 Alice 倒退到第二步,再次向「精灵」要一个随机数,Alice 发送的字符串显然和第一次发送的字符串是相同的,(R, m)。按道理,因为 (R, m) 已经写在精灵表格的「左栏」里,所以一个诚实的「精灵」应该返回 c1。但是,「抽取器」绑架了精灵,他把表格中对应 (R, m) 这一行的「右栏」改成了一个不同的数 c2。当 Alice 计算出另一个 z2 之后,抽取器就完成了任务,通过下面的方程计算出 Alice 的私钥 sk

sk = (z1 - z2)/(c1 - c2)

Fiat-Shamir 变换 —— 从 Public-Coin 到 NIZK

不仅仅对于 Schnorr 协议,对于任意的 「Public-Coin 协议」,都可以用 Fiat-Shamir 变换来把整个协议「压缩」成一步交互,也就是一个非交互式的证明系统,这个变换技巧最早来自于 Amos Fiat 与 Adi Shamir 两人的论文『How to Prove Yourself: Practical Solutions to Identification and Signature Problems.』,发表在 1986 年的 Crypto 会议上[5]。也有一说,这个技巧来源于 Manuel Blum[6].

重复一遍,在 Public-coin 协议中,验证者 Bob 只做一类事情,就是产生一个随机数,然后挑战 Alice 。通过 Fiat-Shamir 变换,可以把 Bob 每一次的「挑战行为」用一次「随机预言」来代替。

而在具体实现中,随机预言需要用一个具有密码学安全强度的 Hash 函数(不能随便选,一定要采用密码学安全的 Hash),而 Hash 函数的参数应该是之前所有的上下文输入。下面是一个示例图,大家可以迅速理解这个 Fiat-Shamir 变换的做法。

前面提到,在非交互式证明系统中,需要引入一个第三方来构建信任的「根基」,使得 Bob 可以完全相信由 Alice 所构造的证明。在这里,第三方就是那个「精灵」,用学术黑话就是「随机预言」(Random Oracle)。这个精灵并不是一个真实存在的第三方,而是一个虚拟的第三方,它同时存在于「现实世界」与「理想世界」。在「现实世界」中,精灵是一个负责任的安静美男子,而在「理想世界」中,它会被「模拟器」绑架。

Public-Coin 协议还有一个好听的名字, 「Arthur-Merlin 游戏」 ……

圆桌骑士

看上图,左边的“白袍”就是 Merlin(魔法师梅林),中间拿剑的帅哥就是 King Arthur(亚瑟王),两个角色来源于中世纪欧洲传说——亚瑟王的圆桌骑士。

Arthur 是一个不耐烦的国王,他随身携带一个硬币,而 Merlin是一个有着无限制计算能力的神奇魔法师,然后魔法师想说服国王相信某个「论断」为真,于是魔法师会和国王进行到对话,但是由于国王比较懒,他每次只会抛一个硬币,然后「挑战」魔法师,而魔法师需要及时应对,而且需要让国王在 k 轮之后能够相信自己的论断。由于 Merlin 有魔法,所以亚瑟王抛的硬币都能被 Merlin 看到[7]。

这与我们在『系列一』中提到的交互式证明系统(Interactive Proof System,简称 IP)有些神似,但又不同。IP 由 Goldwasser,Micali 与 Rackoff(简称GMR)在 1985 年正式提出,它的证明能力覆盖很大一类的计算复杂性问题。而不同的地方在于:在 IP 的定义中,证明者 Prover 和 验证者 Verifier 都是可以抛硬币的图灵机,Verifier 可以偷偷抛硬币,并对 Prover 隐藏;而在 Arthur-Merlin 游戏中,国王只能抛硬币,不仅如此,而且抛硬币的结果总会被 Merlin 知道。

但是,Fiat-Shamir 变换只能在「随机预言模型」下证明安全,而用 Hash 函数实现随机预言的过程是否安全是缺少安全性证明的。不仅如此,「随机预言模型」下安全的协议可能是有不安全的,已经有人找到了一些反例[8];更不幸的是,S. Goldwasser 与 Y. Tauman 在 2003 年证明了 Fiat-Shamir 变换本身也是存在安全反例的[9]。但是这并不意味着 Fiat-Shamir 变换不能用,只是在使用过程中要非常小心,不能盲目套用。

尽管如此,人们无法抵挡 Fiat-Shamir 变换的诱惑,其使用极其广泛。值得一提的是,最热的通用非交互零知识证明 zkSNARK 的各种方案中,Fiat-Shamir 变换比比皆是。比如大家可能耳熟能详的 Bulletproofs(子弹证明),此外还有一些暂时还不那么有名的通用零知识证明方案,比如 Hyrax,Ligero,Supersonic,Libra 等(我们后续会抽丝剥茧,逐一解读)。

小心:Fiat-Shamir 变换中的安全隐患

在 Fiat-Shamir 变换中,要尤其注意喂给 Hash 函数的参数,在实际的代码实现中,就有这样的案例,漏掉了 Hash 函数的部分参数:

比如在 A, Hash(A), B, Hash(B) 中,第二个 Hash 函数就漏掉了参数A,正确的做法应该是A, Hash(A), B, Hash(A,B) 。这一类的做法会引入严重的安全漏洞,比如在瑞士的电子投票系统 SwissPost-Scytl 中,就在 Fiat-Shamir 变换的实现代码中多次漏掉了本来应该存在的参数,导致了攻击者不仅可以随意作废选票,还可以任意伪造选票,达到舞弊的目的[10]。因此在工程实现中,请务必注意。

细心读者也许会回看一下 Schnorr 签名,大家会发现 Schnorr 签名中的 Hash 算法似乎也漏掉了一个参数 PK,并不是严格的 Fiat-Shamir 变换,这被称为 Weak Fiat-Shamir 变换[11],不过这个特例并没有安全问题[3],请未成年人不要随意模仿。

最近一些学者开始在标准模型下研究如何严格证明 Fiat-Shamir 变换的安全性,目前要么引入额外的强安全假设,要么针对某个特定协议进行证明,但似乎进展并不大。

交互的威力

话说在1985年,当 GMR 三人的论文历经多次被拒之后终于被 STOC’85 接受,另一篇类似的工作也同时被 STOC’85 接受,这就是来自于匈牙利罗兰大学的 László Babai,与来自以色列理工的 Shlomo Moran 两人撰写的论文『Arthur-Merlin Games: A Randomized Proof System, and a Hierarchy of Complexity Classes』[7],引入了 Public-coin 交互式协议(顾名思义,Verifier 只公开抛硬币)。

国王 Arthur 的方法很简单,通过反复地「随机」挑战来检验 Merlin 的论断,这符合我们前面讲述过的直觉:采用随机挑战来构建信任的「根基」。Babai 在论文中证明了一个有趣的结论:AM[k]=AM[2],其中 k 表示交互的次数,交互多次产生的效果居然和交互两次等价。所谓交互两次是指:Arthur 发一个挑战数,然后 Merlin 回应。

注:还有一类的问题属于 MA,这一类问题的交互顺序与 AM不同,MA中是 Merlin 先给出证明,然后 Arthur 抛硬币检验。已证明:MA 能处理的问题是 AM 的子集。

不仅如此,Babai 还大胆猜测: AM[poly]IP 是等价的。这是一个神奇的论断:国王很懒,他只需要通过抛多项式次硬币,就能成功挑战魔法师,而这种方式的表达能力居然完全等价于 GMR 描述的交互式证明系统 IP。果不其然,在 STOC’86 会议上,来自 S. Goldwasser 与 M. Sipser 的论文证明了这一点,AM[poly] == IP[12]。

这意味着:反复公开的「随机挑战」威力无穷,它等价于任意的交互式证明系统。但是 AM[poly] 和别的计算复杂性类的关系如何,是接下来的研究热点。

三年后,1989 年11月底,距今恰好三十年,年轻的密码学家 Noam Nisan 发出了一封邮件,把自己的临时学术结论发给了几个密码学家,然后他就跑去南美洲度假了。可是他不曾想到,这一个邮件会引爆历史上一场激烈的学术竞赛,M. Blum, S. Kannan, D. Lipton, D. Beaver, J. Feigenbaum, H. Karloff, C. Lund 等等一大群精英开始加入战斗,他们没日没夜地互相讨论,并且竞相发布自己的研究成果,终于在12月26号,刚好一个月,Adi Shamir 证明了下面的结论:

AM[poly] == IP == PSPACE

image-shamir

它解释了「有效证明」这个概念的计算理论特征,并且解释了「交互式证明系统」这个概念所能涵盖的计算能力。

注:NP 类 是 PSPACE 类的子集,前者大家比较熟悉,后者关联游戏或者下棋中的制胜策略[13]。

而 L. Babai 于是写了一篇文章,名为「Email and the unexpected power of interaction」(电子邮件与交互的始料未及的威力)[14],详细阐述了这一整个月在「邮件交互」中精彩纷呈的学术竞赛,以及关于「交互证明」的来龙去脉。

公共参考串 —— 另一种「信任根基」

除了采用「随机预言机」之外,非交互零知识证明系统采用「公共参考串」(Common Reference String),简称「CRS」,完成随机挑战。它是在证明者 Alice 在构造 NIZK 证明之前由一个受信任的第三方产生的随机字符串,CRS 必须由一个受信任的第三方来完成,同时共享给 Alice 和 验证者 Bob。

是的,你没看错,这里又出现了「第三方」!虽然第三方不直接参与证明,但是他要保证随机字符串产生过程的可信。而产生 CRS 的过程也被称为「Trusted Setup」,这是大家又爱又恨的玩意儿。显然,在现实场景中引入第三方会让人头疼。CRS 到底用来作什么?Trusted Setup 的信任何去何从?这部分内容将留给本系列的下一篇。

未完待续

非交互式零知识证明 NIZK 的「信任根基」也需要某种形式的随机「挑战」,一种「挑战」形式是交给「随机预言精灵」;另一种「挑战」是通过 Alice 与 Bob 双方共享的随机字符串来实现。两种挑战形式本质上都引入了第三方,并且两者都必须提供可以让「模拟器」利用的「后门」,以使得让模拟器在「理想世界」中具有某种「优势」,而这种优势在「现实世界」中必须失效。

NIZK 散发着无穷魅力,让我不时惊叹,在过去三十多年里,先驱们所探寻到的精妙结论,同时还有如此之多的未知角落,在等待灵感之光的照射。

本系列文章在 Github 上的项目仓库收到了第一个 Pull Request,来自Jingyu Hu(colortigerhu),只改了个把字,但那一瞬间,我感受到了生命力。知识交流,思想碰撞,很迷人,不是吗?

“Everyone we interact with becomes a part of us.” 与我们交往互动的每一个人都是我们自身的一部分。 ― Jodi Aman

致谢:特别感谢丁晟超,刘巍然,陈宇的专业建议和指正,感谢安比实验室小伙伴们(p0n1, even, aphasiayc, Vawheter, yghu, mr) 的修改建议。

致谢:自Nisan发起的密码学研究轶事参考自邓老师的文章[15]。

参考文献

  • [1] Schnorr, Claus-Peter. “Efficient signature generation by smart cards.” Journal of cryptology 4.3 (1991): 161-174.

  • [2] Paillier, Pascal, and Damien Vergnaud. “Discrete-log-based signatures may not be equivalent to discrete log.” International Conference on the Theory and Application of Cryptology and Information Security. Springer, Berlin, Heidelberg, 2005.

  • [3] Pointcheval, David, and Jacques Stern. “Security arguments for digital signatures and blind signatures.” Journal of cryptology 13.3 (2000): 361-396.

  • [4] Maxwell, Gregory, Andrew Poelstra, Yannick Seurin, and Pieter Wuille. “Simple schnorr multi-signatures with applications to bitcoin.” Designs, Codes and Cryptography 87, no. 9 (2019): 2139-2164.

  • [5] Fiat, Amos, and Adi Shamir. “How to prove yourself: Practical solutions to identification and signature problems.” Conference on the Theory and Application of Cryptographic Techniques. Springer, Berlin, Heidelberg, 1986.

  • [6] Bellare, Mihir, and Phillip Rogaway. “Random Oracles Are Practical: a Paradigm for Designing Efficient Protocols.” Proc. of the 1st CCS (1995): 62-73.

  • [7] László Babai, and Shlomo Moran. “Arthur-Merlin games: a randomized proof system, and a hierarchy of complexity classes.” Journal of Computer and System Sciences 36.2 (1988): 254-276.m

  • [8] Canetti, Ran, Oded Goldreich, and Shai Halevi. “The random oracle methodology, revisited.” Journal of the ACM (JACM)51.4 (2004): 557-594.

  • [9] Shafi Goldwasser, and Yael Tauman . “On the (in) security of the Fiat-Shamir paradigm.” 44th Annual IEEE Symposium on Foundations of Computer Science, 2003. Proceedings.. IEEE, 2003.

  • [10]Lewis, Sarah Jamie, Olivier Pereira, and Vanessa Teague. “Addendum to how not to prove your election outcome: The use of nonadaptive zero knowledge proofs in the ScytlSwissPost Internet voting system, and its implica tions for castasintended verifi cation.” Univ. Melbourne, Parkville, Australia (2019).

  • [11] Bernhard, David, Olivier Pereira, and Bogdan Warinschi. “How not to prove yourself: Pitfalls of the fiat-shamir heuristic and applications to helios.” International Conference on the Theory and Application of Cryptology and Information Security. Springer, Berlin, Heidelberg, 2012.

  • [12] Goldwasser, Shafi, and Michael Sipser. “Private coins versus public coins in interactive proof systems.” Proceedings of the eighteenth annual ACM symposium on Theory of computing. ACM, 1986.

  • [13] Papadimitriou, Christos H. “Games against nature.” Journal of Computer and System Sciences 31.2 (1985): 288-301.

  • [14] Babai, László. “E-mail and the unexpected power of interaction.” Proceedings Fifth Annual Structure in Complexity Theory Conference. IEEE, 1990.

  • [15] Yi Deng. “零知识证明:一个略显严肃的科普.” https://zhuanlan.zhihu.com/p/29491567

埋藏「秘密」

Once exposed, a secret loses all its power. 一旦泄露,秘密就失去了全部威力 ― Ann Aguirre

这已经是本系列的第五篇文章了,这一篇继续深入非交互式零知识证明。 本文约 12,000 字。

提纲

  1. CRS 的前世今生
  2. 哈密尔顿环路问题
  3. 云中的秘密:Hidden Bits
  4. 升级随机性
  5. FLS变换:从 Hidden Bits 到 NIZK
  6. 寻找理想的 Trapdoor Permutation
  7. NIZK Proof vs. NIZK Argument
  8. 没有秘密的世界

追到这里的读者想必已对零知识证明有了一个大概的认识。你是否想过这个问题:零知识证明为何可行?这里请大家思考一下(比如系列一 中的地图三染色问题的流程) …… (此处停留三分钟)下面两个要素 似乎 必不可少:

  1. 「交互」:验证者通过多次反复挑战,把证明者作弊概率降低到一个极小的值
  2. 「隐藏随机性」:验证者产生让证明者无法预测的随机数进行挑战

然而对于非交互式零知识证明—— NIZK 来说,如何实现上面两点?在 系列四 我们介绍了如何采用「随机预言机」来扮演一个虚拟的「第三方」角色,实现虚拟的「交互」与「随机挑战」。本文将深入讲述另一种方法,如何通过一段共享的字符串去除「交互」与「隐藏随机性」。这个字符串必须事先由「第三方」来随机产生,这就是传说中的「公共参考串」(Common Reference String,简称 CRS)。

CRS 的前世今生

假如我们不借助任何其它手段,限定证明者 Prover 和验证者 Verifier 只能进行「一次交互」来实现「零知识证明」,那么他们只能证明「平凡」问题,也就是计算复杂类 BPPBounded-error Probabilistic Polynomial time),而这个复杂度类大家一般猜想可能等价于 P(但还悬而未决,没有被证明!BPP 可以理解为 P + Randomness)。

注:如果 Prover 与 Verifier 只做一次交互,在这样的 NIZK 系统中,我们很容易能构造一个 Decision Procedure —— Verify(x, Sim(x)),来证明和证伪定理,因此只能证明平凡问题 BPP。

平凡问题虽然也可以零知识证明,但没有意义!怎么理解呢?因为验证者直接可以在多项式时间内根据「输出」求解出「秘密输入」,虽然验证者能够求解,但是「证明」本身并没有额外为验证者提供更多的「知识」。换句话说,不需要证明者出示证明,验证者就知道命题为真,于是证明过程也是零知识的。

因此,当我们讨论「零知识证明」时,要考虑带「知识」的 NP 类问题。大家都知道,P 问题是「确定性图灵机」多项式时间内可以求解的复杂类,它的执行路径对于输入 x是一个线性的状态转移。而 NP 问题是「不确定性图灵机」多项式时间可以求解的问题类。所谓的不确定性图灵机,就是它每次往前走一步是不确定的,有很多个选择,只要任何一个执行路径能到达终止状态,就表示它解决了该问题 x。换句话说,它的执行轨迹是一棵树。那么如果我们把不确定性图灵机每一步的路径选择记录下来(这个执行路径的记录叫做 witness,也就是我们反复提到的「知识」),那么把(x, witness)交给一个确定性图灵机,那么它也能在多项式时间内解决掉 x 问题。

再强调一下,「知识」能提高图灵机的解决问题的能力。

NP 问题中存在着不想「泄露」给验证者的知识 witness,这时,在一个交互式证明系统中,证明者和验证者在「知识」的掌握程度上是不对等的。

为了保证证明过程的「零知识」,我们需要保证:模拟器与验证者的不对等。可是,模拟器没有 witness啊,怎么能让他们不对等呢?上一篇我们介绍了「随机预言机」,我们通过允许让模拟器可以绑架「随机预言精灵」的方式制造不平等。本篇将讲述如何利用 CRS 来制造不平等。

CRS 是一个在证明之前就已经公开的,并且在证明者与验证者之间共享的,随机字符串。我们怎么来使用 CRS 呢?直觉上,一串双方都「知道」的信息,并不会增加「知识」不对等的情况。

首先大家会想,能不能直接用 CRS 作为随机挑战数呢?可不可以让 CRS 来代替「随机预言精灵」的角色?答案是不行!

为什么?这是因为 CRS 是在证明之前就已经产生了,如果证明者 Prover 提前知道了所有的随机挑战数,那么很显然这个随机挑战也就失去了意义。

注:请大家回想下「随机预言机」是如何保证证明者无法提前预测「随机挑战数」的?没想明白的你,请重读系列(四)。

CRS 的使命就是让「模拟器」与「验证者」不平等。怎么做呢?隐藏一些「秘密」进去。

如果进一步追问,隐藏了「秘密」有什么用呢?当然有用啦,在「理想世界」中,模拟器与抽取器才能很开心地玩耍起来(获取某些超能力) ……

1988年,Manuel Blum,Paul Feldman 和 Silvio Micali 三位先驱发表的论文 「Non-Interactive Zero-Knowledge and Its Applications」(『非交互式零知识证明及其应用』[BFM88])展示了「交互」与「隐藏随机性」的不必要性。他们给出了一个地图三染色问题的 NIZK 证明系统,在一段共享的随机产生的字符串(即CRS)的帮助下。

不过,……,我不会告诉你这个方案需要共享大概 n^4 超长的 CRS,其中 n是要证明的「命题」的长度。

1990 年,Uriel Feige,Dror Lapidot 与 Adi Shamir 三人提出了另一种构造 NP 语言的 NIZK 方案 [FLS90]。与 [BFM88] 不一样的是,这个 NIZK 方案不再基于特定的数论假设,而是基于一个密码学工具 Trapdoor Permutation。在这个方案中,FLS 提出了「隐藏比特」(Hidden Bits)的概念,然后把 Hidden Bits 藏入了 CRS。对于模拟器而言,就可以通过修改 CRS 中的 Hidden Bits 来达到模拟的效果,从而体现出对验证者 Verifier 的优越性。不过,这个方案需要共享更长的 CRS,超过 k * n^5,这里 k 是安全参数。

此后,Hidden Bits 的思路被很多人采用,值得一提的是,Kilian 与 Petrank 采用了一种更巧妙的方法来使用 Hidden Bits [KP98](这里空间太小,写不下:),成功地把 CRS 的长度缩减到了 k * n^2。后来 J. Groth 继续优化 ,把 CRS 的长度缩小到了大约 k*n[Groth10a]。

除了 Hidden Bits,J. Groth,R. Ostrovsky 与 A. Sahai [GOS06] 使用了同态加密方案 Boneh-Goh-Nissim [BGN05] 或 Boneh-Boyen-Shacham 来实现 NIZK,他们把加密方案的「公钥」当做是 CRS,同时 Prover 加密作为证明,然后利用同态性质来证明另一个 NP-Complete 问题——布尔电路的可满足性问题。这个方案的最大优点,就是 CRS 长度是固定的,因为只是一个密钥而已,长度只有 k。对于模拟器而言,它可以通过超能力,拿到这个公钥所对应的陷门,从而能够实现密封任何信息,但得到相同的密文;对于抽取器而言,它可以用超能力拿到公钥对应的私钥,从而能够解密证明得到「知识」。

Jens Groth 在 2010 年基于 KEA(Knowledge of Exponent Assumption) 假设与 Pairing 提出了一种新的 NIZK Arguments 方案[Gorth10b],这也是后续许许多多 zkSNARKs 方案的起点。这里的 CRS 由一对对的 (g^x^n, g^⍺x^n) 构成,被用来实现「知识承诺」。其中 x 是两个随机数,在产生完 CRS 之后,必须被「遗忘」。有些人把这部分需要遗忘的随机数叫做「Toxic Wastes」,这容易误导读者。他们不仅无毒无害,而且非常有用。他们是被藏入 CRS 的「秘密」,是模拟器的武器。如果模拟器得到了 x,就能伪造证明,从而保证证明的零知识。而对于抽取器,他能直接通过 KEA 假设内建的抽取函数来抽取知识。

最新的 Sonic 方案[MBK+19]又在 [Groth10b] 的基础上实现了 Updateable CRS。如果任何人担心 CRS 中的秘密已经被泄露了,他就可以在原有 CRS 基础上打一个补丁,继续往里藏一个秘密,这样就能保证 CRS 的安全性。这里的 CRS 还是「Universal 全局」 的,即 CRS 只需要生成一次,就可以应付所有的命题证明。 这个方案后续被最新的 Plonk[GWC19],Marlin[CHMMVW19] 等方案采用。

接下来,我们就从一个简单的例子开始,理解如何基于 CRS 来构造 NIZK。在这之前,我们需要介绍一个 NP-Complete 问题——哈密尔顿环路问题。

哈密尔顿环路问题

想象出一个地图中有若干个城市,城市与城市间可以有公路。

假如给你一副地图,让你找出一条路径,不重复地走遍所有的公路(假设每条公路都是风景美如明信片的 Parkway,或许你想不重复地吃遍每条公路边上的麦当劳,出于某种情怀)。相信你会马上兴奋起来,这不就是小时候学过的「一笔画」么?判断一个地图能否一笔画,这是小学生做的数学题,我们可以计算每个城市连接的公路个数,根据奇偶性分成「奇点」与「偶点」。如果一个地图中存在两个奇点城市,那么你只能从一个奇点城市出发,遍历所有的公路,并且最终到达另一个奇点城市。这条路径就被称为「欧拉路径」(Euler’s Path)。

如果一个地图中所有的城市都是偶点,那么你可以从任意一个城市出发,轻松地找出一条路径,不重复地遍历所有的公路,并且回到起点。这个环路被称为「欧拉环路」(Euler’s Circuit)。

而如果地图存在超过2个以上的奇点,那么就不存在欧拉回路,比如著名的哥德斯堡七桥问题。

著名的哥德斯堡七桥问题就是这么描述,如果不重复地穿过下面七座桥。

哥德斯堡七桥地图显然存在多个奇点,不存在欧拉路径。如果给定任何一个地图,是否存在一个欧拉环路,这是一个 P 问题,也就是一个计算机可以在 poly(n) 多项式时间内寻找。

注:欧拉环路的寻找算法被称为 Fleury算法。

对于这样一个 P 问题, 如果一个证明者 Prover 证明他知道一个欧拉回路,那么他可以直接发送回路的明文,然后验证者 Verifier 验证回路正确与否。请注意,这个过程仍然是零知识的。因为,Verifier 并没有通过 Prover 发送的信息获得任何 额外的知识。换句话说,Verifier 并没有因为看到回路,而增强了自身计算能力,因为 Verifier 本来就可以自行计算欧拉回路。

而我们要讲的是「哈密尔顿环路问题」则是一个 NP 问题,描述如下:

是否一个地图存在一个环路,能不重复地穿过每一个城市

比如下面这张地图:

我们用一个矩阵 V * V 的矩阵来表示这个地图,凡是两个城市(A, B)有公路相连接,那么就在(A, B)(B, A)里面填上 1,否则填 0。这个矩阵被称为「邻接矩阵」,我们可以把这个邻接矩阵拍扁,就变成了一个 0/1 比特串。

寻找「哈密尔顿环路」是一个 NP-Complete 问题,换句话说,不存在一个算法使得计算机在 poly(n) 多项式时间内找到环路。但是,计算机可以在多项式时间内检验一个路径是否是「哈密尔顿环路」。比如这个地图中就有一个带方向的哈密尔顿环路,我们一眼就能验证这个环路确实穿过了每一个城市。如果一个地图有哈密尔顿环路,那么它的矩阵一定是满足下面的特征:每一行一定有一个1,每一列一定也有一个1

ZK-HAM 协议

我们下面给出一个三步交互的 Sigma 协议,Alice 向 Bob 证明她「知道」上面这个地图 G 的哈密尔顿环路。

  • 公共输入:G 为一个有 6 个顶点的地图,表示为一个 6*6 的邻接矩阵
  • 秘密输入:G的哈密尔顿环路 C(图中橙色的公路)

  • 第一步:Alice 随机选择一个「置换」,Perm(.),然后通过这个置换,产生一个新的图 G';然后 Alice 把G' 矩阵的每一个单元加密,产生一个新的矩阵发送给 Bob。

【名词解释】:所谓置换,大家可以想象成用 鼠标 随意拖动图中的点,但是点和点之间的连线会跟着点一起被拖动,拖动结束之后形成的图,进行重新编号就得到 G',比如上图左侧的两个图。经过置换变换的图前后是 同构 的。其中下图中,每一个顶点上角括号中的标号为拖动之前该顶点在上图中的编号。形式化一点可以这么定义:Perm()是一个 {1, V} 到 {1, V}的双射函数新图 G'的邻接矩阵,[perm(i), perm(i+1) ]=1 当且仅当 [i, i+1]=1,其中 i 是顶点编号,V 是顶点个数 。

  • 第二步:Bob 随机选择 b in {0, 1}} 进行挑战。

  • 第三步情况(1):Alice 根据 Bob 第二步发送的值:如果 b=0,那么 Alice 发送置换函数 Perm(),并且揭示完整的图 G'。而 Bob 则确认 G'是否是原图 G 经过置换无误。

  • 第三步情况(2):如果 Bob 第二步发送的b=1,那么 Alice 只揭示 G'中的哈密尔顿环路 C'即可。而 Bob 需要验证 C'是否是一个哈密尔顿环路

回忆一下三步 Sigma 协议,我们再理解下上面看似复杂的动作:

  • 第一步:被称为 Commit,证明者 Alice 需要把手里的答案进行同态变换,产生一个新答案,然后把每一条边都锁起来,交给 Bob;
  • 第二步:Bob 进行随机挑战;
  • 第三步:Alice 根据 Bob 的随机挑战,做出两种不同的回应。如果 Bob 挑战 0,那么Alice 打开第一步的承诺,表示自己在第一步没有作弊;如果 Bob 挑战 1,那么 Alice 只解密暴露出哈密尔顿环路的边(公路),其它边则不需解密。Bob 可以轻易地检查地图上露出来的那些边是否构成了一个不重复地经过所有城市的环路。

如果这个 Sigma 协议只走一遍的话, Alice 作弊的概率是 50%,如果重复 n 遍,Alice 作弊概率会指数级减小。大家可以试着用「模拟器」和「抽取器」的方法来证明这个协议的「零知识」与「可靠性」。

ZK-HAM 的变形:ZK-HAM-2

接下来把上面的这个三步协议改动一下。大家先考虑下这样一个简单事实:如果一个仅包含环路的子图 C 是 图 G的子图,C <= G那么说明 G 包含哈密尔顿环路。

这个事实等价于另一个事实:一个哈密尔顿图 G 的补集 !G 是环路子图 C 的补集 !C 的子图。

【名词解释】图的补集:所谓补集就是这样一个新地图,顶点保持不变,旧地图上的边在新地图中不存在,而新地图中的公路在旧地图中不存在,但是两个图重合在一起,就变成了一个完全图(完全图是指任意两个顶点之间都存在一条边)。

用邻接矩阵来理解,就是如果一个图G包含一个环路子图C,那么G矩阵中所有值为 0 的单元集合 必然被 C矩阵中所有值为0的单元集合包含。可以表示为 !G <= !C

根据第二个事实,我们可以定义如下的 Sigma 协议:

  • 公共输入:图G ,表示为 6*6 的邻接矩阵
  • 秘密输入:G的哈密尔顿环路 C(图中橙色的公路)

  • 第一步:

    • Alice 随机选择一个「置换」,Perm(.),并且通过C构造一个哈密尔顿环路子图 C'=Perm(C)
    • 然后 Alice 加密 C'的每一个单元,把加密后的结果发送给 Bob。
  • 第二步:Bob 随机选择 b in {0, 1}进行挑战

  • 第三步情况(1):如果 b=0,Alice 揭示完整的 C',而 Bob 验证这个 C' 是否确实是一个哈密尔顿环路子图。

  • 第三步情况(2):如果 b=1,Alice 发送 Perm(),同时按照 G'=Perm(G)中的所有含 0 单元所在的位置,揭示 C'中所对应的单元;Bob 验证 C'所有被揭示单元是否全部为 0

再理解下这三步 Sigma 协议:

  • 第一步:证明者 Alice 需要把哈密尔顿子图 C 进行置换变换,产生一个新的哈密尔顿子图 C',加密后交给 Bob;
  • 第二步:Bob 进行随机挑战,0 或者 1
  • 第三步:如果 Bob 挑战 0,那么 Alice 打开第一步的承诺,展示一个带有唯一环路的图;如果 Bob 挑战 1,Alice 则按照 G'中的 0单元的位置打开承诺,展示承诺中被打开的位置全部为 0

重点来了,大家仔细看看这个新版的 Sigma 协议的第一步。有没有发现什么情况?

第一步 Alice 发送的内容是与地图G无关的!

同样,第二步 Bob 发送的挑战也是与地图无关的。这样我们可以把第一步发的承诺改成事先准备好的比特串,而且我们假设这个比特串由一个可信第三方来产生,这样一来 Bob 就没有必要发送 b=0 这个分支,因为可信的第三方是诚实的,他一定是事先准备好一个正确的环路子图。这样,由于 Bob 只需要发送 1挑战分支,那么这一步也可以去除。

于是,三步协议变成了一步,我们成功去除了交互,有望实现 NIZK 。

我们接下来把 ZK-HAM-2 协议的第一步和第二步推到一个事先准备的字符串中,然后只让 Alice 发送第三步的内容给 Bob。如下图所示:

请注意,这里还不算是一个 NIZK 系统,因为这个共享字符串并不能对 Bob 公开,否则 Bob 就能算出环路 C。接下来,我们要解释一个新概念:「隐藏比特」(Hidden Bits)[FLS90]。Hidden Bits 是这样一串随机比特,它们对于验证者 Bob 隐藏,但是对于证明者 Alice 公开。然后在证明过程中,Alice 可以选择性地揭示一部分比特展示给 Bob 看。这是构造 NIZK 证明系统的一个利器,下面我们需要再继续深入 ……

云中的秘密:Hidden Bits

让我们再次开下脑洞,想象天上有朵云,云后面藏着一串随机产生的比特值,不是 0 就是 1,然后 Alice (证明者)带着一个「超级眼镜」,于是能够看到云后面所有的随机比特串,但是 Bob (验证者)却看不到。同时 Alice 手里还有一个「超级手电筒」,她可以打开手电筒用激光穿透云层,让 Bob 也能看见其中某个或某些比特。当然,Bob 能看到的比特的选择权完全在 Alice 手中。

云朵中隐藏的比特串就是所谓的 Hidden Bits

接下来我们要通过 Hidden Bits 来完成一个单步交互,完成 ZK-HAM-2 协议的功能。在 ZK-HAM-2 中的第一步,Alice 产生一个随机的置换 Perm(),然后通过 G 中的哈密尔顿环路子图 C 产生一个变换后的环路子图 C'=Perm(C)。这等价于,事先由任何人产生一个随机的哈密尔顿环路子图 C',然后 Alice 根据 CC' 计算得出一个相应的 Perm()

假设由某个「第三方」产生了一个随机的环路子图 C',编码成「邻接矩阵」比特串,放到云朵后面。假设 V 为顶点(城市)的个数,E 为边(公路)的条数。这个邻接矩阵的编码需要一个 V*V 长度的比特串,可以解释成一个 V*V 的矩阵,其中每一行只包含一个 1,每一列也只包含一个 1,矩阵的其它单元都为 0

接下来 Alice 如何构造证明呢?这其实很简单:

  1. Alice 通过「超级眼镜」得到了一个随机的哈密尔顿环路子图 C',然后计算得到一个置换 Perm(),使得 Perm(C)=C'

  2. Alice 根据 Perm() 来计算出一个换后的图 G'=Perm(G)

  3. Alice 产生证明,由两部分组成:(1)置换Perm() (2)G'的邻接矩阵中所有值为 0 的单元坐标所对应的 C'矩阵的值,相当于 Alice 需要用「超级手电筒」给 Bob 揭示的隐藏比特。

那么 Bob 怎么验证这个证明呢?Bob 拿到证明之后,只需要检验两个东西:

  1. Perm() 是否是一个合法的置换 V -> V,比如不能出现两个顶点映射到同一个顶点的情况。
  2. 对于 G 中的每一条「非边」,经过置换之后,Bob 抬头看天上对应的「隐藏比特」,比特值必须为 0

我们再仔细地深入理解下这个非交互协议。先从「完备性」入手:如果 Alice 没有作弊,那么很显然能够通过 Bob 的验证,这里请大家自行检查。

接下来我们分两步简要证明下「可靠性」:首先,因为 Bob 经过验证得知,所有 G 置换后的非边集合都已被揭示,且全为 0,那么可以得出结论,!G <= !C,即G的非边集合是环路子图 C的非边集合的子集。这等价于,C <= G,也就是说 G 包含一个哈密尔顿环路。这里请注意,这个可靠性概率是 100%。

然后,设想在一个「理想世界」中,Bob 获得了某种超能力(比如拿到 Alice 的「超级眼镜」),不需要 Alice 的超级手电筒,就能看穿云层,得到所有的隐藏比特 C'。然后当 Bob 得到 Perm()之后,就能通过 Perm() 反算出 C,于是 Bob 就相当于变身成了一个「抽取器」(Extractor),在理想世界中,它能把 Alice 要证明的知识给成功抽取出来。

那么怎么证明「零知识」呢?Alice 要具备一个超能力,就是在「理想世界」中,可以偷偷修改云朵中的隐藏比特。接下来就简单了,模拟器 Zlice 可以这么欺骗 Bob:

  1. Zlice 把云朵中的隐藏比特全部置为 0
  2. Zlice 随机产生一个合法的 Perm()

大家发现了,关键是,天上隐藏的比特必须是一个可信的字符串,所谓「可信」,就是指它确实应该是一个哈密尔顿环路子图。那么第三方需要可信。

可是,这样一个第三方是不是难以令人满意?Alice 和 Bob 要绝对信任他,不会和对手串谋。如果他和 Alice 串谋,可以把隐藏比特串直接设置为全 0;或者他和 Bob 串谋,直接把这个比特串给 Bob 看。这个协议看起来不错,但是很难实用。我们接下来要对这个简单协议进行升级。

升级随机性

第一个升级是让隐藏比特串变成一个「一致性均匀分布」的随机的隐藏比特串,是一个看起来相当随机的比特串,而不是一个刻意摆放好的哈密尔顿子图。

完全随机意味着比特串中的 0 的个数和 1出现的概率大概接近。那么接下来一个难题是如何让随机比特串中能出现一个随机的哈密尔顿环路子图矩阵。方法非常简单粗暴:产生一个足够长的随机串,然后从头扫描,直到找到一个随机的哈密尔顿环路为止。

可是……这个成功概率是不是非常非常小?我们下面给出一个概率没那么小的一种寻找方法。

  1. 我们先把比特串按照 5log(V) 的长度进行切分,然后如果每一个分片中的所有比特全为 1,那么我们把这个片段被视为邻接矩阵中的一个值为 1 的单元,否则视为一个值为 0 的单元。这样每一个矩阵单元出现 1 的概率为 1/(V^5)
  2. 我们取连续的 V^6 个片段,构成一个 V^3 * V^3 的大矩阵。如果大矩阵中包含一个 V*V的哈密尔顿环路矩阵,并且其他单元(总共 V^6 - V^2个) 都为 0。那么我们称这个大矩阵为「有用」。
  3. 根据概率计算,出现一个「有用」矩阵的概率为 1/[V^(3/2)]

注:「有用」矩阵的概率计算过程请参考 Fact 4.10.8, 「Foundations of Cryptography, Vol I」by Oded Goldreich,P304。

好了,我们需要升级下上一节的协议。因为现在「隐藏比特串」被拆分成了若干个大矩阵,这些大矩阵有些是「有用」的,有些是没用的。

接下来 Alice 要来构造证明了,她先戴上超级眼镜,扫描云朵中的 Hidden Bits,这要分两种情况,

  • Case 1:如果 Alice 遇到了一个没用的大矩阵 M,Alice 公开 M 的所有单元。

  • Case 2:如果 Alice 遇到了一个「有用」的大矩阵 M,这意味着 Alice 得到了一个随机的 哈密尔顿环路 C',然后 Alice 参照上一节的步骤进行证明即可。

那么 Bob 怎么验证这个证明呢?我们还要分情况进行讨论,

  • Case 1:如果 Alice 公开了全部的 M,那么 Bob 就检查这个 M 是否「无用」。如果 M 无用,就认为证明有效;否则拒绝。
  • Case 2:如果 Alice 发送的是形如(Perm()X)这样的证明,那么 Bob 按照上一节的验证方法进行验证。

对于这个协议,Bob 已经不再担心第三方是否作弊,故意产生一个全零的比特串,但是 Alice 仍然担心一旦第三方和 Bob 串谋,那么知识就彻底泄露了。

不仅如此,现在的协议还有个很强的限制,Alice 不能在看到隐藏比特之后再选择需要证明的 G,否则 Alice 就可以作弊。如果一个证明者选择证明的「命题」与 CRS 无关,那么这个证明者被称为 Non-adaptive Adversary。

FLS 变换:从 Hidden Bits 到 NIZK

接下来,我们再次升级协议,把「隐藏比特串」中的隐藏特性去除,变成「公共参考串」CRS。这里我们要借助一个密码学工具 —— Trapdoor Permutation,陷门置换。

所谓的陷门置换是指一个置换函数 F(x)x是一个集合 S 中的元素,然后函数 F(x)x 映射到 S 中的另一个元素 y。同时 F(x) 满足单向性,即通过 y 很难反算出 x;但是如果谁拥有陷门 t,就能实现反向计算F^(-1)(t,y)=x。陷门置换还可以匹配一个 Hardcore Predicate,h(x)=0/1,它能根据 S 集合中的元素产生一个一致性分布的 0/1比特。介绍完毕,大家是不是有点晕,没关系,晕一晕就习惯了。总之一句话,陷门置换可以对公共参考串和Hidden Bits 进行相互转换。

先假设有这样的密码学工具,然后我们升级协议。

我们把公共参考串看成是一个列表,y1, y2, y3, ..., yn,列表中的每一项都是集合 S 中的元素。然后通过 Hardcore Predicate 产生 Hidden Bits 中的每一个比特位。但是请注意,这里不能直接通过 h(y)=b 来产生 Hidden Bits,因为这样一来 Bob 就能自己算出所有的 Hidden Bits,这违反了上一节的协议。为了保证对 Bob 隐藏,我们需要用公共参考串的原象,也就是 x1, x2, x3, ..., xn 来产生 Hidden Bits,h(x)=b。Bob 虽然不能自己计算 b1, b2, b3, ..., bn,但是一旦得到一个 x,他就能检验 F(x)?=y来判断是否 x 是和公共参考串对应,同时再计算 h(x)=b 得到被揭示的 Hidden Bits,b

我们可以换一种不太准确,但是更直观的方式来理解,Alice 相当于自己产生一对公私钥。然后Alice 把公共参考串看成是一段「密文」,由于 Alice 有私钥,于是可以对密文进行解密,得到明文,这些明文,对于 Bob 而言就相当于是 Hidden Bits。当 Alice 要「揭示」Hidden Bits 时,就出示相应的明文片段,并且带上公钥,那么 Bob 就能通过公钥再次「加密」明文,与公共参考串的密文进行比对,确保 Alice 没有在揭示过程作弊。

下面是升级后的协议:

对于证明者 Alice

  1. Alice 随机选择一个 Trapdoor Permutation,(F, h, t)
  2. 根据公共参考串中的每一个 yi,利用陷门反向计算出 xi = F^(-1)(t, yi)
  3. 计算 Hidden Bits,bi=h(xi)
  4. 根据上一节的协议产生证明。假设 Alice 要揭示的 Hidden bits 的位置集合为 r1,r2,...,rl,那么 Alice 向 Bob 发送对应位置的 x,分别为 x_r1, x_r2, x_r3, ... x_rl ,连同(F, h),和证明一起并发给 Bob。

对于验证者 Bob

  1. 检查 (F, h) 是否为一个合法的 Trapdoor Permutation。
  2. L 中的每一个元素 x_r,计算出被揭示的 Hidden Bits bi=h(F(x_r)),然后按照上一节的协议检查证明。

这个新协议的「完备性」,请大家自行检查。

对于「零知识」,我们需要构造一个「模拟器」Zlice2,它的超能力是修改公共参考串。

  1. 模拟器直接调用上一节协议的模拟器 Zlice。得到一个三元组,(proof, {r}, {b})
  2. 对于每一个公共参考串位置,如果它对应某一个 r,模拟器从集合 S随机选择一个 x_r,使得 h(x_r)=b_r,这里 b_r就是 {b}中对应 r ;然后把 y_r=F(x_r) 作为假参考串的一部分。
  3. 对于每一个公共参考串位置,如果与 {r}无关,那么模拟器随机选一个 y即可
  4. 模拟器把所有的 y拼在一起,得到一个假CRS。

对于「可靠性」,事情变得不那么简单了。因为现在 Alice 有能力挑选 (F,h,t),Alice 可以挑选一个对自己有利,甚至作弊的 (F, h, t),使得她可以控制一次协议运行的 Hidden Bits {b}的结果。对于本节升级后的新协议而言,需要重复很多遍,以致于虽然 Alice 可以控制一次协议运行的 Hidden Bits,但是她对其它若干次协议运行的 Hidden Bits 无能为力。换句话说,Alice 无论如何挑选 (F, h, t) 都无法完全掌控多次的协议运行。

这个升级变换理论上可以支持任意的 Hidden Bits 模型下的非交互式零知识证明,被称为 FLS Protocol。FLS 变换有很多的好处:首先,这个随机产生的 CRS 可以多次使用,实现所谓的「Multi-Theorem NIZK」;其次,可以实现「Adaptive Soundness」,即 Alice 可以先看到 CRS,然后再选择要证明的内容。最后,这个协议还是「Adaptive Zero-Knowledge」,即 Bob 也可以先看到 CRS,然后再选择要证明的内容给 Alice。

注:Adaptive Adversary 是比较符合现实世界的安全情况,比如第二类CCA安全。因为 CRS 是公开的,攻击者可以先分析 CRS,再决定如何发起攻击。

寻找理想的 Trapdoor Permutation

陷门置换 Trapdoor Permutation 最早出现在姚期智老师的论文「Theory and Application of Trapdoor Functions」[Yao82]中,是公钥密码学的重要基础。在上一节给出的 FLS 变换中,需要一个理想化的 Trapdoor Permutation,所谓的理想化是指,每一个 n-bit 字符串都能唯一变成另一个 n-bit 字符串,并且不会出现「多对一」的映射关系。Alice 需要随机抽样一个 Index,发给 Bob,然后 Bob 要能检查出这个 Index 所对应的 F() 是否是一个「完美」的置换。问题来了,怎么 Bob 怎么能在多项式时间内检查出来呢?如果 Bob 不能检查,那么 Alice 就可以抽样一个不完美的 Permutation(比如一个「多对一」的函数),从而可能作弊,破坏「Soundness」这个性质,Bellare 和 Yung 发表在 1996 年的论文最早注意到了这一点,但是并没有完全解决这个问题[BY96]。

如何找到一个桥梁,能够将 Trapdoor Permutation 合适地抽象出来,同时能够对接到密码学工具的实现上,是一个及其有挑战性的工作。随后各路密码学家(包括 Oded Goldreich) 在这方面研究了很长时间,发表了许许多多的论文 ,各种不同类型的 Trapdoor Permutation 被定义、被研究,但是仍然不能让人满意。直到最近(2018年)一个工作是 Ran Canetti 与 Amit Lichtenberg 提出了 Certifiable Injective Trapdoor Function 这样一个新类型[RL18],并证明了这种 Trapdoor Permutation 终于能满足 FLS 变换要求。但这是不是故事的结束呢?理论密码学家们估计不会停下探索的脚步。

除了基于 Trapdoor Permutation 的 FLS 变换 ,还有各式各样的解决方案来升级 Hidden Bits Model,比如采用 Invariant Signature[BG90],或 Verifiable Random Generator [DN00] 来实现 Hidden Bits 的变换,或者弱可验证随机函数 [BGRV09], 还有一种叫做 publicly-verifiable trapdoor predicates 的方案[CHK03]。

三十年来,密码学家们发明的 NIZK 方案有很多,但 Hidden Bits 方法是目前已知唯一的办法,(1) 基于「一致性分布」的共享 CRS,(2) 实现任意 NP 语言的 NIZK Proofs(Not Arguments!)。

NIZK Proofs 与 NIZK Arguments

在本文中,我们构造的 NIZK 「证明」系统的可靠性属于「Statistical Soundness」,而零知识则属于「Computational Zero-Knowledge」。这意味着什么呢?这意味着,不管证明者 Alice 的算力有多强大(甚至超多项式),Alice 仍然无法作弊。但是,如果验证者 Bob 拥有超强的计算能力,那么是存在这种可能性:Bob 从证明中抽取到有价值的「知识」。

这又意味着什么?

这意味着,对于 NIZK Proofs 来说,它的长度肯定要比「知识」长,知识即 NP 问题中的 witness。只要 Bob 算力够强,他就可以把证明解密。对于「抽取器」而言,它也需要在没有交互的情况下抽取 witness 。证明最短的 NIZK Proofs 当属 Greg Gentry 等人采用「全同态加密」技术构造的 NIZK 方案了 [GGI+14],证明长度只是稍稍大于 witness 的长度。

那能不能构造证明尺寸小于 witness 的 NIZK 呢?答案是 YES!

还有一类的 NIZK 系统被称为 NIZK Arguments:它们的可靠性是「Computational Soundness」,零知识属于「Perfect Zero-Knowledge」或者「Statistical Zero-Knowledge」。这说明,Alice 如果算力超强,那么她是有作弊空间的,但是因为现实世界中,我们可以通过加大安全参数(Security Parameters)来极大地降低 Alice 作弊的可能性,但是能实现非常极致的零知识特性。由于弱化了可靠性,那么我们就可以继续压缩证明的尺寸。

注:在本系列中,我们并不刻意区分「证明」与「论证」这两个词。如果需要指明 Arguments 而非 Proofs,会专门强调。

假如说我们要公开一个 NIZK 证明到 Github上,假如过了一百年以后,Github 网站还在,而未来计算机的计算能力已经有了质的飞跃,这时候,一个 NIZK Proof 可能会被算力攻破,泄露知识,而 NIZK Argument 则很大可能性上还保持安全性。

现在流行的热词 —— zkSNARK 中的 AR正是指代 Argument。

NIZK Argument 可以实现 O(1) 常数级长度的证明,即与 witness 的长度无关。然而这需要隐藏更多的秘密到 CRS 中。

没有秘密的世界

1956 年,哥德尔在一封寄给冯诺依曼的信中提到了一个著名的问题,「P 是否等于 NP」。后来,这个问题被 Clay 研究所列为七个千禧年难题之一,悬赏百万美金。

零知识证明系统正是为了保护 witness 不泄露的前提下,实现 NP 问题的验证。那如果一旦证明了「P == NP」,这会意味着什么?这意味着 witness 不再有多大意义了,反正一个图灵机也可以在多项式时间内找到 witness。零知识证明试图保护的 witness 也变得徒劳无益。

事实上,如果「P == NP」,现有的公钥密码学、对称加密 AES 与 SM4、哈希算法所依赖的难解问题都可能坍塌,我们可能很难保存秘密。不仅如此,

如果 P == NP,我们所处的世界将会变得非常不一样。「Creative Leaps」将不再有价值,求解问题与验证问题之间的鸿沟不复存在。每个能欣赏交响乐的人都会成为莫扎特,每个会推理的人都会变成高斯,每个能判断投资好坏的人都会变成巴菲特。从达尔文进化论的观点出发:如果这就是我们存在的宇宙,为什么我们还没有进化得可以充分利用这个好处?—— Scott Aaronson (2006)

对于数学也一样,数学证明的验证过程也是多项式复杂度的,如果「P == NP」,那么也就存在着多项式时间寻找证明的算法(如果证明存在)。这意味着哥德巴赫猜想、黎曼猜想将有可能得到证明,难怪 Lance Fortnow 在博客[For04]里这么说:

A person who proves P == NP would walk home from the Clay Institute not with one million-dollar check but with seven. 如果谁能证明 P = NP,那么他不会只拿着一张百万美元支票回家,而是七张。 —— Lance Fortnow (2004)

2002年的调查显示,61% 的计算机科学家相信「P != NP」,而十年后,这个比例上升到了 83%[Wil12]。 而我是被 Scott Aaronson 的如下论断说服的:

自指论证:如果 P = NP 是事实,那么这个证明会比较容易被发现;但是如果 P != NP,那么这个证明会比较难发现。所以相信 P != NP 看起来会让 数学现实 更一致一些。—— Scott Aaronson (2006)

尽管是如此不情愿,如果我们真的生活在一个没有秘密的世界,那会是什么样子?「环形监狱 Panopticon」是 18 世纪英国哲学家 Jeremy Bentham 提出的一个惊悚概念。囚徒们被中心全天候监控,没有任何隐私可言,而且他们对自己是否处于被监控状态也无从得知。这个无比悲观的论调让人浑身不适,但很多人认为,这可能是两百多年前对未来网络数字时代的一则精准寓言。

从『Billy Budd』,卡夫卡的『The Trial』,到奥威尔的『1984』,到著名黑客 Kevin Mitnick 写的超级大卖书『隐形的艺术』(教你如何在大数据时代保护自己的信息),似乎,危机四伏,风险不断累积,对末日世界的想象给了作家们很好的素材 ……

偶尔无意中看到了一本有趣的漫画『The Private Eye』,它描述了一个劫后余生的后现代场景:在未来,我们的所有信息数据都存放在云上,然后突然有一天,这个数据云「爆炸」了,不知道是什么原因(可能是谁不小心打开了潘多拉的魔盒,找到了 P == NP 的构造性证明),反正所有的信息,包括每个人最阴暗的过去,都不再成为秘密;所有的数字化的资产都被抹掉,所有的在线知识库永久丢失;每个人的言行、账单、邮件、聊天消息、银行卡密码、中学考卷、GPS位置信息,写了一半的日记、删除的照片、上网记录,这些信息都将暴露给同事、邻居、 朋友、亲人、甚至任何一个好奇的人。

每个人都无地自容,惶惶不可终日,然后逐渐地,大家都选择隐藏自己,人们出门都要戴上面具,以小心翼翼地保护自己的身份,甚至一个人可以选择使用多个身份,国家法律规定任何偷窥行为都将被严惩,获取信息成为了一种至少无上的权力,照相机需要被严格管控,互联网不再存在,人们通讯又回到了电话亭时代 ……

这会是人类的终极命运么?

未完待续

本文开头提到了「隐藏随机性」并不是必要的,我们来回想下 Hidden Bits 模型。这些 Hidden Bits 并没有对 Prover 隐藏,而是敞开了让 Prover 知道,但是由于 Hidden Bits 是「一致性随机分布」的字符串, 所以即使让 Prover 知道了,他仍然逃不过随机挑战的火力。然而在流行的 zkSNARK 方案中,并没有采用「一致性随机分布」的 CRS,而是一组结构化的随机数。不管怎样,用 CRS 来构建「信任根基」的秘密,就是藏在其中的「秘密」。

这符合直觉,保守「秘密」也是一种信任。因为 Alice 不知道 CRS 中隐藏的秘密后门,所以无法作弊。同样,Bob 不知道 CRS 中的秘密,也就没办法获得「知识」。同样,人与人之间的协作既要建立在公开透明的基础上,也要保守秘密。

All human beings have three lives: public, private, and secret. 每个人都有三种生活,公开的,私人的,以及秘密的。—— Gabriel García Márqueel

致谢:感谢陈宇,丁晟超,张宇鹏,胡红钢,刘巍然,何德彪,万志国等老师的专业建议和指正,感谢安比实验室小伙伴(p0n1, even, valuka, Vawheter, yghu, mr)的修改建议。本文内容不代表他们观点。

最后附上漫画书的链接:http://panelsyndicate.com/comics/tpeye 作者甚至把创作过程的邮件和草图都放了出来,大家可以体验一下窥视制作过程的快感。

参考文献

  • [Aar06] Aaronson, Scott. Reasons to believe, 2006. https://www.scottaaronson.com/blog/?p=122
  • [BFM88] Blum, Manuel, Paul Feldman, and Silvio Micali. “Non-interactive zero-knowledge and its applications.” STOC’88. 1988.
  • [BG90] Bellare, Mihir, and Shafi Goldwasser. “New paradigms for digital signatures and message authentication based on non-interactive zero knowledge proofs.” Conference on the Theory and Application of Cryptology. Springer, New York, NY, 1989.
  • [BGN05] Boneh, Dan, Eu-Jin Goh, and Kobbi Nissim. “Evaluating 2-DNF formulas on ciphertexts.” Theory of Cryptography Conference. Springer, Berlin, Heidelberg, 2005.
  • [BGRV09] Brakerski, Zvika, Shafi Goldwasser, Guy N. Rothblum, and Vinod Vaikuntanathan. “Weak verifiable random functions.” In Theory of Cryptography Conference, pp. 558-576. Springer, Berlin, Heidelberg, 2009.
  • [BY96] Bellare, Mihir, and Moti Yung. “Certifying permutations: Noninteractive zero-knowledge based on any trapdoor permutation.” Journal of Cryptology 9.3 (1996): 149-166.
  • [CHK03] Canetti, Ran, Shai Halevi, and Jonathan Katz. “A forward-secure public-key encryption scheme.” International Conference on the Theory and Applications of Cryptographic Techniques. Springer, Berlin, Heidelberg, 2003.
  • [CHMMVW19] Chiesa, Alessandro, et al. Marlin: Preprocessing zksnarks with universal and updatable srs. Cryptology ePrint Archive, Report 2019/1047, 2019, https://eprint.iacr.org/2019/1047, 2019.
  • [DN00] Dwork, Cynthia, and Moni Naor. “Zaps and their applications.” Proceedings 41st Annual Symposium on Foundations of Computer Science. IEEE, 2000.
  • [FLS90] Feige, Uriel, Dror Lapidot, and Adi Shamir. “Multiple non-interactive zero knowledge proofs based on a single random string.” Proceedings [1990] 31st Annual Symposium on Foundations of Computer Science. IEEE, 1990.
  • [For04] Fortnow, Lance. “What if P = NP?”. 2004. https://blog.computationalcomplexity.org/2004/05/what-if-p-np.html
  • [For09] Fortnow, Lance. “The status of the P versus NP problem.” Communications of the ACM 52.9 (2009): 78-86.
  • [Groth10a] Groth, Jens. “Short non-interactive zero-knowledge proofs.” International Conference on the Theory and Application of Cryptology and Information Security. Springer, Berlin, Heidelberg, 2010.
  • [Groth10b] Groth, Jens. “Short pairing-based non-interactive zero-knowledge arguments.” International Conference on the Theory and Application of Cryptology and Information Security. Springer, Berlin, Heidelberg, 2010.
  • [GOS06] Groth, Jens, Rafail Ostrovsky, and Amit Sahai. “Perfect non-interactive zero knowledge for NP.” Annual International Conference on the Theory and Applications of Cryptographic Techniques. Springer, Berlin, Heidelberg, 2006.
  • [GWC19] Gabizon, Ariel, Zachary J. Williamson, and Oana Ciobotaru. PLONK: Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge. Cryptology ePrint Archive, Report 2019/953, 2019.
  • [KP98] Kilian, Joe, and Erez Petrank. “An efficient noninteractive zero-knowledge proof system for NP with general assumptions.” Journal of Cryptology 11.1 (1998): 1-27.
  • [MBK+19] Maller, Mary, et al. “Sonic: Zero-Knowledge SNARKs from Linear-Size Universal and Updateable Structured Reference Strings.” IACR Cryptology ePrint Archive 2019 (2019): 99.
  • [RL18] Ran Canetti and Amit Lichtenberg. “Certifying trapdoor permutations, revisited.” Theory of Cryptography Conference. Springer, Cham, 2018.
  • [Wil12]Gasarch, William I. “Guest Column: The Third P=? NP Poll.” ACM SIGACT News 50.1 (2019): 38-59.
  • [Yao82] Yao, Andrew C. “Theory and application of trapdoor functions.” 23rd Annual Symposium on Foundations of Computer Science (sfcs 1982). IEEE, 1982.

从零开始学习 zk-SNARK

  • 作者:Maksym Petkus
  • 翻译 & 注解:even@安比实验室(even@secbit.io)
  • 校对:valuka@安比实验室
  • 本系列文章已获作者中文翻译授权
  • 翻译原链接

核心要点

加密函数:

在同态加密中:

  • 模数 是双方都知道的。它通常是写在加密代码中的
  • 生成元 是一个整数,作为一个基用来生成一系列的数字(密钥,用来对数据进行加密)
  • 就是我们要加密的值

如果上述核心要点已经模糊/忘记的话, 就通读全文

证明的媒介

这里我们先不要去管零知识,非交互性,其形式和适用性这些概念,就从尝试证明一些简单的东西开始。

想象一下我们 (Prover) 有一个长度为10 的位数组,现在要向 verifier(例如,程序)证明这样一个陈述:我的所有的位都被设置成了 1

verifier 一次只能检查(读)一位。为了验证 Prover 的这个陈述,verifier 以某种任意的顺序读取元素并检查其是否确实等于 1 。如果第一次抽样检查的结果是 1,就设置「陈述」的可信度为 ⅒= 10%,否则,如果等于 0,就说明「陈述」是错误的。

验证者继续进行下一轮验证,直到获得足够的可信度为止。假如在一些场景下要信任 prover 需要至少 50% 的可信度,那就意味着必须执行 5 次校验。但假如在其它一些场景下需要 95% 的可信度,就需要检查所有的元素。很明显这个证明协议的缺点是: 必须要根据元素的数量进行检查,如果我们处理数百万个元素的数组,这么做是不现实的。

现在我们来看一下由数学方程式表示的多项式,它可以被画成坐标系上的一条曲线:

上面的曲线对应多项式: f(x) = x³ – 6x² +11x– 6。多项式的阶数取决于 x 的最大指数,当前多项式的阶数是 3

多项式有一个非常好的特性,就是如果我们有两个阶为 d (比如 3 ) 的不相等多项式,他们相交的点数不会超过 d ( 3 个)。 例如,稍微修改一下原来的多项式为 x³ – 6x² + 10x– 5 (注意 , 修改了多项式的最后一个系数,6 改成了 5 )并在图上用绿色标出:

这一点微小的修改就产生了变化很大的曲线。事实上,我们不可能找到两条不同的曲线,他们会在 某段区域内重合(他们只会相交于一些点)。

要找到多项式与 x 轴的交点(即 f(x) = 0),我们就要令 x³ – 6x² + 11x – 6 = 0,等式的解就是和 x 轴的交点: x= 1x= 2x= 3。即图上蓝色曲线和 x 轴相交的地方。

同样,我们也可以令上文中原始的多项式和修改后的多项式相等,找到它们的交点:
联立 : x³ – 6x² + 11x – 6 = x³ – 6x² + 10x – 5 , 得到:

即这两个多项式有一个交点。

任意一个由阶数为 d 的多项式组成的等式,最后都会被化简为另外一个阶数至多为 d 的多项式,这是因为等式中没有能够用来构造更高阶数的乘法。例如:5x³ + 7x² – x + 2 = 3x³ – x² + 2x– 5,简化为 2x³ + 8x² – 3x + 7 = 0。 阶数最多就是 3 (次方)

另外代数的基本原理也告诉我们,对于一个阶数为 d 的多项式最多有 d 个解,至多有 d 个共同点。

所以我们可以得出结论,任何多项式在任意点的计算结果(更多关于多项式求值参考:[Pik13])都可以看做是其唯一身份的表示。

如果一个 prover 声称他知道一些 verifier 也知道的多项式(无论多项式的阶数有多大)时,他们就可以按照一个简单的协议去验证:

  • verifier 选择一个随机值 并在本地计算多项式结果
  • verifier 将 值丢给 prover,让他计算该多项式的结果
  • prover 代入 x 到多项式计算并将结果给到 verifier
  • verifier 检查本地的计算结果和 prover 的计算结果是否相等,如果相等那就说明 prover 的陈述具有较高的可信度

例如,对于一个 阶多项式 , prover 如果不知道该多项式的 d 个解 , 如果把 x 的取值范围定在 1 到 , 那么 x 偶然“撞到”这 d 个结果相同的点中任意一个的概率就等于: (可认为不可能)

与低效的位检查协议相比,新的协议只需要一轮验证就可以让声明具有非常高的可信度(前提是假设 远小于 x 取值范围的上限 (是低阶多项式),可信度几乎是 100%)

这也是为什么即使可能存在其他的证明媒介,多项式依然是 zk-SNARK 核心的部分

even@安比实验室: 这一节告诉了我们多项式的一个重要性质:我们不可能找到共享连续段的两条不相等曲线,也就是任何多项式在任意点的计算结果都可以看做是其唯一身份的表示。也就是说只要能证明多项式上的某个随机点就可以证明这个多项式(只有在知道了多项式,才能算出这个点对于的值),这个性质是我们下面所有证明的核心。

这就是 Schwatz-Zippel 定理,它可以扩展到多变量多项式,即在一个多维空间内形成一个曲面。这个定理会在多个零知识证明方案的证明中反复出现。

问题 :

到目前为止,我们的协议还只是一个很弱的证明,因为协议中并没有采取任何措施去保证参与方必须按照协议的规则生成证明,所以参与方只能互相信任。

例如,prover 并不需要知道多项式,也可能通过其它方式得到正确的答案 (比如偷一个答案)。

而且,如果 verifier 要验证的多项式的解的取值范围不够大,比如我们前文说的 10,那个就可以去猜一个数字,猜对答案的概率是不可忽略不计的。因而我们必须要解决协议中的这个缺陷,在解决问题之前首先来想一下,知道多项式意味着什么呢?

多项式可以用下面的形式来表示(其中 n 指的是多项式的阶): 假设证明者声称他知道一个包含 x=1x=2 两个解的三阶多项式 , 满足此条件的一个有效的多项式就是

多项式的「知识」就是多项式的系数。所谓「知道」多项式就是指「知道」多项式的系数

因式分解

代数的基本定理表明了任意的一个多项式只要它有解,就可以将它分解成线性多项式(即,一个阶数为 1 的多项式代表一条线),因此,我们可以把任意有效的多项式看成是其因式的乘积:

也就是说如果任意一个因式为 0,那么整个等式都为 0,也就是说式子中所有的 就是多项式的所有解

所以这个多项式的解( 的值)就是:0,1,2,在任何形式下多项式的解都可以很轻松的被验证,只不过因式的形式可以让我们一眼就看出这些解(也称为根)

我们再回到前面的问题, prover 宣称他知道一个阶数为 3,其中两个根分别为 1 和 2 的多项式,也就是说这个多项式的形式为: 换句话说 是问题中多项式的两个因式。

因而如果 prover 想要在不揭示多项式的前提下证明他的多项式确实有这两个根,那么他就需要去证明他的多项式 和一些任意多项式 (例子中 )的乘积,即:

也称为目标多项式 target polynomial

换句话说,存在一些多项式 能使 与之相乘后等于 ,并由此得出, 中包含 ,所以 的根中也包含 的所有根,这也就是我们要证明的东西. 算出 的方式最自然的就是直接相除:

如果一个 prover 不能找到这样一个 也就意味着 中不包含因式 ,那么多项式相除就会有余数

例如我们用 p(x) = x³ – 3x² + 2x 除以 t(x) = (x – 1)(x – 2) (即 x² – 3x+ 2 )

注意:左边的式子是分母,右上角的是计算结果。底部是余数(多项式相除的解释及示例可以看这里 [Pik14] )。

我们算出结果 ,没有余数。

_注意:为简化起见,后面我们会用多项式的字母来代替计算结果,如:

多项式可以被因式分解成它的根的因式的乘积。这个性质就意味着,如果一个多项式有某些解,那么它被因式分解后的式子中一定包含这些解的因式。 有了这个性质,我们就可以愉快地去做一些证明啦。

利用多项式一致性检查协议我们就可以比较多项式 p(x)t(x) ⋅ h(x)

  • verifier 挑选一个随机值 , 计算 (即,求值) ,然后将 发送给 prover。
  • prover 计算 ,并对 p(r)h(r) 进行求值,将计算结果 p, h 提供给 verifier。
  • verifier 验证 ,如果多项式相等,就意味着 t(x)p(x) 的因式。

实践一下,用下面的例子来执行这个协议:

  • verifier 选一个随机数 23,并计算 t = t(23) = (23 – 1)(23 – 2) = 462,然后将 23 发给 prover
  • prover 计算 , 并对 p(r)h(r) 进行求值,p= p(23) = 10626,h = h(23) = 23,将 ph 提供给 verifier
  • verifier 再验证 :10626 = 462 ⋅ 23 是正确的,这样陈述就被证明了。

相反,如果 prover 其实不知道真正的 , 而是使用了一个不相干的 ,如 p′(x) = 2x³ – 3x² + 2x ,它并不包含必需的因式, 那么:

  • prover 计算 , 运算出结果 和余数
  • 为了计算出结果 , prover 不得不冒险用余数除以 , 即

不过由于 x 是 verifier 随机选择的,只有极低的概率余数 可以被 整除。如果后面 verifier 要另外再检查 p 和 h 必须是整数的话,这个证明就会被拒绝。

如此校验就同时要求多项式系数也是整数,这对协议产生了极大的限制

这就是为什么接下来我们要介绍能够使余数不被整除的密码学原理的原因,尽管这个原始值是有可能被整除的。

Remark 3.1 现在我们就可以在不知道多项式的前提下根据特定的性质来验证多项式了,这就已经给了我们一些零知识和简明性的特性。但是,这个结构中还存在好多问题:

  • prover 可能并不知道他所声称的
    • 因为 prover 知道随机点 ,他可以先算一下 ,然后选择一个随机值 ,由此计算出 。因为等式是成立的,所以也能通过 verifier 的校验。
    • 因为 prover 知道随机点 ,他可以构造出一个任意的多项式,这个任意多项式与 处有共同点。
  • 在前面的「陈述」中,prover 声称他知道一个特定阶数的多项式,但现在的协议对阶数并没有明确的要求。因而 prover 完全可以拿一个满足因式校验的超级高阶数的多项式来欺骗 verifier

下面我们就要来逐一得解决这些问题。

even@安比实验室:利用因式的性质构造出了一个证明协议,但这个协议存在一些缺陷,主要是由于

  1. 一旦 prover 知道了 ,他就可以反过来任意构造任何一个可以整除
  • 有的公司的 就直接写在开源代码里面 …. 作死 ….
  1. prover 知道了点 的值,就可以构造经过这一点的任意(高次)多项式,同样满足校验
  2. 协议并没有对 prover 的多项式阶数(次数)进行约束

模糊计算

Remark 3.1 中的前 2 个问题是由于 暴露了原始值 而导致的,即 知道了 但如果 verifier 给出的这个 值像放在黑盒里一样不可见的话就完美了,也就是一个人即使不破坏协议,也依然能在这些模糊的值上面完成计算。有点类似哈希函数,从计算结果就很难再回到原始值上

同态加密

这也就是要设计同态加密的原因。它允许加密一个值并在密文上进行算术运算。获取加密的同态性质的方法有多种,我们来介绍一个简单的方法。

总体思路就是我们选择一个基础的(基数需要具有某些特定的属性)的自然数 g(如 5),然后我们以要加密的值为指数对 g 进行求幂。例如,如果我们要对 3 进行加密:

这里 125 就是 3 对应的密文。如果我们想要对被加密的值乘 2,我们可以以 2 为指数来对这个密文进行计算。

我们不仅可以用 2 来乘以一个未知的值并保持密文的有效性,还可以通过密文相乘来使两个值相加,例如 3+2:

同样的,我们还可以通过相除提取加密的数字,例如:5-3

不过由于基数 5 是公开的,很容易就可以找到被加密的数字。只要将密文一直除以 5,直到结果为 1,那么做除法的次数也就是被加密值的数。

比如有一段密文是 125 , 那么 , 除了 3 次得到 1 , hacker 自然知道加密值是 3 , 这毫无加密可言, 所以我们需要 Mod 模运算

模运算

这里就到了模运算发挥作用的地方了。模运算的思路如下:除了我们所选择的组成有限集合的前 n 个自然数(即,01,…,n-1)以外,任何超出此范围的给定整数,我们就将它“缠绕”起来。例如,我们选择前六个数。为了说明这一点,可以把它看做一个有六个单位大小相等刻度的圆;这就是我们所说的范围(通常指的是有限域)。

img

现在我们看一下数字八应该在哪里。打个比方,我们可以把它看成一条长度为 8 的绳子。img

如果我们将绳子固定在圆圈的开头img

然后用绳子缠绕圆圈,我们在缠完一圈后还剩下一部分的绳子。

img

然后我们继续缠绕,这根绳子将在刻度 2 的地方终止。

img

这就是模运算操作的结果。无论这根绳子多长,它最终都会在圆圈一个刻度处终止。因而模运算结果将保持在一定范围内(例子中是 0 到 5)。长度为 15 的绳子将会在刻度 3 的地方终止,即 6 + 6 + 3 (缠 2 个完整的圈并剩下 3 个单位长的部分)。

负数运算类似,唯一不同的地方就是它是沿相反方向缠绕的,如 -8 的取模结果是 4。

我们执行算术运算,结果都将落在这 n 的范围内。现在开始我们将用符号 “mod n” 来表示这个范围内的数。

3 × 5 = 3 mod 6

5 + 2 = 1 mod 6

另外,模运算最重要的性质就是运算顺序无所谓

例如,我们可以先做完所有的操作,然后再取模,或者每操作完一步都去取模。例如 (2 × 4 – 1) × 3 = 3 (mod 6) 就等于:

2 × 4 = 2 mod 6

2 - 1 = 1 mod 6

1 × 3 = 3 mod 6

那么模运算到底有什么用呢?就是如果我们使用模运算,从运算结果再回到原始值并不容易,因为不同的组合会产生一个同样的运算结果

5 × 4 = 2 mod 6

4 × 2 = 2 mod 6

2 × 1 = 2 mod 6

……

设想一下 , 如果没有模运算的话,计算结果的大小会给找出原始值提供一些线索。

除非这里既能把信息隐藏起来,又可以保留常见的算术属性。

强同态加密

我们再回到同态加密上,使用模运算,例如取模 7,我们可以得到:

…………

其中在某些不同的指数下运算得到了同样的结果 , 比如 7 的运算结果都是 3

……

这样就很难知道指数是多少了。 事实上,如果模取得相当大,从运算结果倒推指数运算就不可行了; 现代密码学很大程度上就是基于这个问题的“困难”。

而方案中所有的同态性质都在模运算中保留了下来:

    • 根据密文 6 , 不能推出原文 3
    • 根据密文 3 , 无法推出私钥

(原文)注意:模相除有点难 , 超出范围了, 这里不表。

我们来明确地说明一下加密函数:

  • 是想要加密的值
  • 模数 是双方都知道的, 它通常写在加密代码中
  • 生成元 是一个整数,作为一个基用来生成一系列的数字比如
  • 通常称为密钥,用来对数据进行加密

Remark 3.2 : 这个同态加密模式有一个限制,我们可以将 加密值 乘以 未加密值 ,但不能将两个已经加密的值相乘( we cannot exponentiate an encrypted value)(或者相除),也就是说我们不能对加密值取幂。

在同态加密中,求幂运算会破坏同态的性质,导致加密后的数据无法被正确解密。因此,同态加密不允许对已经加密值进行再次的求幂运算。 Besides, 密文之间的乘法操作可能会泄露有关明文的信息。特别是在某些强同态加密方案中,如果不小心执行操作,可能会导致信息泄露。

虽然这些性质第一感觉看起来很不友好,但是这却构成了 zk-SNARK 的基础。这个限制后面将在“加密值乘法 (Multiplication of Encrypted Values) ”一节中讲到。

  • 通过模运算形成的集合被称为「有限域」,
  • 通过计算指数再进行模运算形成的集合构成「循环群」。
  • 常见的同态加密方式除了整数幂取模之外,还有椭圆曲线上的倍乘。

加密多项式

配合这些工具,我们现在就可以在加密的随机数 x 上做运算并相应地修改零知识协议了。

我们来看一下如何计算多项式 p(x) = x³ – 3x² + 2x

我们前面明确了,知道一个多项式就是知道它的系数,也就是这个例子中知道:1, -3, 2

因为同态加密并不允许再对加密值求幂,所以我们必须要给出 x 的 1 到 3 次幂取加密值:E(x),E(x²),E(x³),那么我们要计算的加密多项式就是:

所以通过这些运算,我们就获得了多项式在一些未知数 处的加密计算结果。这确实是一个很强大的机制,因为同态的性质,同一个多项式的加密运算在加密空间中始终是相同的

我们现在就可以更新前面版本的协议了,比如对于阶数为 d 的多项式:

协议过程

前面提到 : Prover 想要在不揭示多项式的前提下证明他的多项式确实有这两个根,他需要去证明他的多项式 和一些任意多项式 的乘积,即: 也称为 target polynomial

协议过程如下 :

Verifier

  • Verifier 自己取一个随机数 ,作为秘密值
  • 多项式指数 ( )取值为 0,1,…,d 时分别计算出 次幂的加密结果,即: (注意是 次方)
  • 代入 自己计算未加密的 target poly , 留作验证备用
  • 将对 的幂的加密值丢给 prover: , 即
    • 看起来, 目前 Verifier 知道 的阶数

Prover :

  • Prover 想证明它确实有这 2 个根 ( 即有 因式) :
  • Prover 自己计算多项式
  • 使用 Verifier 给的加密值 , 和自己的 的系数 计算 :
  • 同样计算
  • 将结果 (即 ) 和 (即 )提供给 verifier

注: 是加密函数

Verifier

  • 最后一步是 Verifier 校验 就能知道 Prover 到底是否有根 ; 为什么呢 ?
    • 注 : 是 Prover 传的 , 是 Verifier 自己算的 ;
  • 因为如果 成立 (即 成立) , 根据同态性质 , 即 成立 , 就说明 Prover 真的有多项式的解

问题: Prover 计算 , s 是 Prover 不知道的, 那如何计算 呢 ?
1. 郭师: 是一组 mod 过的 key-value , 是双方都知道的 2. 使用的是 而不是 原值 3. ∵
4. ∴

注意:g 是公开的, 双方都知道的 因为证明者并不知道跟 s 相关的任何信息,这就使得他很难提出不合法但是能够匹配验证的计算结果。

尽管这个协议中 prover 的灵活性有限,他依然可以在实际不使用 verifier 所提供的加密值进行计算,而是通过其它的方式来伪造证明。例如,如果 prover 声称有一个满足条件的多项式它只使用了 2 个求幂值 ,这个在当前协议中是不能验证的

even@安比实验室: 利用强同态加密这个工具,构造了一个相对较强的零知识证明协议。但是如上文所述,这里还是存在一些问题—— 无法验证 prover 是否是真的使用了 verifier 提供的值 来构造证明的

ref (IF 图挂了)

  • https://secbit.io/blog/2019/12/25/learn-zk-snark-from-zero-part-one/
  • https://learnblockchain.cn/article/287
  • 作者:Maksym Petkus
  • 翻译 & 注解:even@安比实验室(even@secbit.io)
  • 校对:valuka@安比实验室
  • 本系列文章已获作者中文翻译授权
  • 翻译原链接

Restricting a Polynomial (限制多项式)

上文说到 :

  1. 多项式的知识其实就是它的系数 的知识
  2. 上文的协议无法验证 prover 是否是真的使用了 verifier 提供的值 来构造证明

协议中, 我们通过对秘密值 s 的幂的加密值再进行求幂来对系数进行“赋值”。我们已经限制了 prover 对 s 幂的加密值的选择, 但是这个限制并不是强制的 ,也就是说,prover 可以使用任何可能的方法找到满足下面等式的值

https://arxiv.org/pdf/1906.07221.pdf

再用寻找到的 来代替 交给 verifier。 verifier 还是验证 是否成立 , 自然成立, 此时 cheat 成功

所以 verifier 需要能够知道 prover 给出的 就是用 s 幂的加密值 计算的, 而不是其它值算的

来看一个简单例子: 由 1 个变量和及其系数组成的一阶多项式 : 对应的加密值为 。这里我们要做的就是确保 prover 是拿 的加密值 而不是其他值与其系数 c 做同态相乘的。所以结果一定是这个形式(c 为任意值): 解决这个问题的一种方法就是用另一个“变换”的加密值做同样的操作,充当类似算术中“校验和”(Checksum) 的作用,以此确保结果是原始值的求幂值。

这个是通过 Knowledge-of-Exponent Assumption (简称 KEA) 方法来实现的,在 Dam91 中有介绍,更精准一点(注意 2 个字符的不同)说:

Alice 有一个值 ,她想要 Bob 对其进行任意指数的求幂(  is a generator of a finite field group used),唯一的要求是 Bob 只能对 进行求幂,为保证这一点,Alice 要:

  • 选择一个随机数
  • 计算
  • 提供一个元组 给 Bob, 然后让他对这 2 个值执行任意的求幂运算,返回结果元组 ( The α-shift remains the same. i.e. )

因为 Bob 无法从元组 中提取 的值 (暴力破解也难以实现),那么 Bob 只能老老实实地生成有效元组

  1. Bob 选择一个值 ( 可以类比上例的 )
  2. 计算
  3. 返回

有了 Bob 回复的 和自己的 ,Alice 就可以验证等式:

结论是:

  • Bob 在元组的两个值的计算上都用了同一个指数(即
  • Bob 只能用 Alice 原本的元组 来保持 α-shift
  • 构造验证值 的唯一方式是用同一个指数
  • Alice 并不知道 ,这和 Bob 不知道 的原因一样
  • 虽然 c 是被加密的,但它的可能取值范围并不足够大到保持其零知识的性质,这个问题我们将在后面“零知识”那一节解决。

最后这个协议提供了一个证明给 Alice ,Bob 确实是用他知道的某个值对 进行求幂的,而且他也不能做别的任何操作,例如:乘法,加法,因为这样就会破坏 α-shift (α-变换关系)

在同态加密中,求幂是对被加密值进行乘法运算。我们可以应用这个结构到一个简单的系数多项式 的例子中:

  • verifier 选择随机数 ,然后令 , 提供一阶及其 “shift” 的计算值:
  • prover 代入其私有的系数 计算:
  • verifier 验证:

这个结构“限制” prover 只能用 verifier 提供的加密的 进行计算,因而 prover 只能将系数 赋给 verifier 提供的多项式。

现在我们可以扩展这种单项式(monomial) 上的方法到多项式上,因为计算是先将每项的分配分开计算然后再 “同态地” 相加在一起的(这个方法是 Jens Groth 在 Gro10 中介绍的)。

所以如果 一个 的幂及其加密 shifted 就可以计算原始的和 shift 后的多项式,, where the same check must hold. 对于阶数为 的多项式:

verifier : 提供加密值 和他们的 α-shift prover :

  1. 计算给定的带有 的幂的 encrypted polynomial :

  2. evaluates encrypted “shifted” polynomial with the corresponding α-shift of the powers of :

  3. 将计算结果 发给 verfier

verfier 校验 :

前面的多项式例子 就变成了:

现在我们就可以确保 prover 是用了 verifier 提供的多项式而不是其它值做计算的了,因为别的方法不能够保持 α-shift 变换。 当然如果 verifier 想要确保在 prover 的多项式中排除了 的某些次幂,如 , 他就不提供对应的密文及其变换:

与前面的协议相比,我们现在已经有了一个比较健壮的协议。但是尽管已经做了加密,在 零知识 性质上也还依然存在一个很明显的缺陷:

即即使理论上多项式参数 是一个很广的取值范围内的值,在实际中, 这个范围可能很有限(比如前例中的 6),这就意味着 verifier 可以在有限范围的系数组合中进行暴力破解,获取 的知识 , 最终计算出一个与 的答案相等的结果 :

比如 将每个系数的取值范围定为 100,多项式阶数为 2,那么大概只会有 100 万种不同的组合,可以认为 暴力破解 的密钥只需要少于 100 万次的迭代

更重要的是,对于一个安全的协议, 即使在只有 1 个系数,值为 1 的例子中,安全协议也必须能够保证其安全 !!!

even@安比实验室: 有了 KEA,就可以约束 prover 只能通过用 verifier 提供的加密值去构造证明了。严格点讲,这里是用的是 KEA的扩展版本,叫做 The q-power Knowledge of Exponent Assumption.

Zero-Knowledge

上文说到 能从 发送的数据中暴力破解 ,来看一下 those provided values (the proof) :

双方都参与到了下面的 checks :

  1. (poly has roots of )
  2. (poly of a correct form is used)

问题是我们如何更换(一种新的)证明 (alter the proof) 使得这些 checks 依然有效,同时又保证没有知识能被提取?

Chap-1 给了一个提示: 我们可以使用随机值 (delta)来 “shift” 这些值, 如
现在,为了提取知识,就必须首先要知道一个不可知的值 δ。并且,这种随机化在统计学上与随机值没有什么区别。 (原文: in order to extract the knowledge, one first needs to find δ which is considered infeasible(不可行的). Moreover, such randomization is statistically indistinguishable from random.)

为了保持这种关系,我们在 的 checks 中验证一下。等式的每一边都有 prover 提供的值 , 如果我们用 来“变换” 每一个值,那么等式应该可以保持相等

Concretely (具体来讲),就是 prover 选择一个随机值 ,并用它对证明中的值进行求幂 (and exponentiates his proof values with )

不要怕, 我们在前面都已经见过了

and provides to the for verification: 合并一下(consolidation), 可以看到校验的等式依然成立 (the check still holds) :

注意零知识是如何轻而易举地融入到这个结构中去的,这通常也被称为“无成本的”零知识

even@安比实验室: 借助这个”无成本的”技巧,就可以轻松实现 zero-knowledge 了。但是这里实现零知识的方法和实际中的 Pinocchio 协议,还有 Groth16 方案略有不同。实际方案中是用乘法乘以

Non-interactivity & Distributed Setup

到现在为止,我们已经讲完了一个交互式的零知识方案。但为什么我们还需要有非交互式呢?因为交互式证明只对 original 有效,其他任何 都不能信任这个 proof,因为:

  • 可以和 串通,告诉 secret params ,有了这些参数 就肆意伪造 proof 来四处行骗
  • 也可以使用同样的方法自己伪造 proof
  • 必须保存 直到所有相关证明被验证完毕,这就带来了一个可能造成秘密参数泄漏的额外攻击面 (which allows an extra attack surface with possible leakage of secret parameters)

因而 就需要分别和每次每个 都做交互来证明一个 statement(该多项式的知识)

尽管 交互式证明 有它的用处,例如一个 只想让一个特定的 (称为目标 verifier,更多的信息参见 JSI96 )确信,就不能再重复利用同一个证明去向别人证明这个声明了,但是当一个 prover 想让众多的参与者同时或者永久地确信的话,这种方法就很低效了。 prover 需要保持一直在线并且对每一个 verifier 执行相同的计算

因而,我们就需要一个可以被重复使用,公开,可信,又不会被滥用的秘密参数

Pairing: Multiplication of Encrypted Values

Cryptographic pairings (bilinear map) is a mathematical construction, denoted as a function

它被给予一个数据集中的 2 encrypted inputs (e.g. ) , 可以将他们确定性地映射到另一组不同的输出数据集上的它们的乘积,即

因为源数据集和输出数据集(通常被称为一个 group )是不同的,所以一个配对的结果不能用做其他配对计算的输入。我们可以将输出集(也称为“目标集”)视为“不同的宇宙”。因而我们不能用另一个加密值乘以结果,而且配对这个名称本身也表明了,我们一次只能将两个加密值相乘

even@安比实验室: 换句话说,配对只支持 x * y 这种两个值的乘法,但不支持三个或以上的值相乘,比如不支持 x * y * z

Pairing 类似于一个 ,将所有可能的输入值映射到可能的输出值的集合中的一个元素上,通常情况下这个过程是不可逆的

注意:乍一眼看过去,这个限制可能会阻碍相关功能的实现,但在 zk-SNARK 中这反而是保证安全模式的最重要性质,参见前文 remark 3.3

配对函数 可以初步(and technically incorrect)类比(mathematical analogy) 成: “交换(swap)” 每一个输出的基数(base) 和 指数(exponent) 的操作,使得基数 在交换过程中被修改成了指数的方式,即 , “被转换”的两个输入一起被修改了,这样原始值 就在同一个指数下相乘了,即:

e(g^\textcolor{red}{a},g^\textcolor{red}{b}) =a^g \cdot b^g =(\textcolor{red}{ab})^g

因而因为基数(base) 在“转换”中被修改了,所以在另一个配对中不能再使用这个结果 ( 即: )构造出想要的加密乘积 了。配对的核心性质可以表示成下面的等式:

Note:配对操作是通过改变椭圆曲线来实现这些性质的,现在我们用的符号 就代表曲线上一个由生成元 自相加了 n 次的点,而不是我们前面用到的乘法群生成元。 The survey DBS04 provides a starting point for exploration of the cryptographic pairings.

Technically, 配对的结果是目标集(target set) 的不同 generator 下原始值(raw value) 的加密产物(encrypted product),即 。 因此它具有同态加密的性质,例如,我们可以将多个配对的加密乘积加在一起:

注意:配对操作是通过改变椭圆曲线来实现这些性质的,现在我们用的符号 就代表曲线上一个由 自相加了 次的点,而不是我们前面用到的乘法群生成元。 DBS04 这个 survey 提供了学习 Pairing 的 starting point

Trusted Party Setup

有了 cryptographic pairings,我们现在就准备去设置安全公开且可复用的参数了。假定一下我们让一个诚实的参与方来生成秘密值 . 一旦 / 的幂及其对应的 α-shift 被加密,那么原始数据就必须要被删除 ( ) :

这些参数通常被称为 common reference string (CRS) . CRS 生成后,任何的 都可以使用它来构造 非交互式的 零知识证明协议。CRS 的优化版本将包含目标多项式的加密值 (While non-crucial)

CRS 分成两组 :

  1. proving key (alse called evaluation key) :
  2. verification key :

使用 Pairing 就可以将 加密值相乘 (记得第一节说过, 加密值不能直接相乘, 会破坏同态的性质), 就可以在协议的最后一步验证多项式了 (有了 verification key 就可以处理从 那里得到的加密多项式的值 :

  • 在加密空间中校验
  • Chech polynomial Restriction : Recall what is [[#Zero-Knowledge]] :

Trusted MPC

尽管受信任设置很有效率,但众多 CRS 用户也必须要相信生成者确实删除了 α 和 s ,这一点没有办法证明(proof of ignorance 是一个正在积极研究的领域 DK18),所以这种方法依然是无效的。因而很有必要去最小化或者消除这种信任。否则一个不诚实的参与方就可以构造假证明而不被发现。

一种解决办法就是由多个参与方使用前面小节中介绍的数学工具来生成一个组合式CRS,这样这些参与方就都不知道「秘密」了。下面是一个实现方案,我们假设有三个参与者 Alice,Bob 和 Carol ,对应为 A,B 和 C,其中 i 为 1, 2, …, d:

  1. Alice 选择随机数 ,然后公开她的 CRS:
  2. Bob 选择他的随机数 ,然后通过同态乘法结合 Alice 的 CRS:
  3. 然后公开两方 Alice-Bob 的 CRS 结果:
  4. Carol 用她的随机数 做同样的事:
  5. 然后公开 Alice-Bob-Carol 的 CRS:
  6. 这个协议最后我们就获得了一个混合的

除非他们串谋,否则参与者们互相之间并不知道其他人的秘密参数。实际上,一个参与者必须要和其它所有的参与者串谋才能得到 sα,这样在所有的参与者中只要有一个是诚实的,就没有办法伪造证明。

注意:这个过程可以被尽可能多的参与者重复完成

有一个问题是如何验证参与者在生成 CRS 时用的随机数值是一致的,因为攻击者可以生成多个不同的随机数 ,然后代入这些不同的随机数去执行 s 的不同次幂计算(或提供随机数作为一个 CRS 的扩充),从而使 CRS 无效或者不可用。

庆幸的是,因为我们可以使用配对来乘以加密值,所以我们就可以从第一个参数开始逐一执行一致性校验,并且确保了每个参数都源于前一个。

我们用 s 的 1 次幂作为标准来校验每一个其它次幂的值与之是否保持一致 :

  • 例如 :
    • 2 次幂:
    • 3 次幂:

我们现在再验证一下前面步骤中 α-变换后的值是否正确:

  • 例如:
    • 3 次幂:

范围的缩写形式,在后面会经常看到

当我们在验证每一个参与者秘密参数的一致性时,要注意参与者生成 CRS 的过程并没有强制后一个参与者(就是我们例子中的 Bob 和 Carol)都要使用前面已经公开的 CRS。因而如果一个攻击者是链上的最后一个参与者,他可以像链上的第一个参与者一样忽略前面的 CRS 随便构造一个有效的 CRS,这样他就变成了唯一一个知道秘密 sα 的人。

为了解决这个问题,我们可以额外再要求除了第一个以外的每一个参与者去加密然后公开他的参数。例如,Bob 同样公开了 :

这就可以去验证 Bob 的 CRS 是乘以了 Alice 的参数后正常获得的, :

同样的,Carol 也必须证明她的 CRS 是乘以了 Alice-Bob 的 CRS 后正常获得的。

这是一个健壮的 CRS 设置模式,它并不完全依赖于单个参与者。事实上,即使其它所有的参与者都串谋了,只要有一个参与者是诚实的,他能够删除并且永远不共享它的秘密参数,这个 CRS 就是有效的。所以在设置 CRS (有时候被称为仪式 Wil16)的时候有越多不相关的参与者参与,伪造证明的可能性就越低。当有相互竞争的参与方参与的时候,就几乎不可能伪造证明了。这种模式能够包容其他一些怀疑这种 setup 可识别性的不受信方因为校验步骤确保了他们不会破坏(这里也包括很弱的 αs 的使用)最终的 CRS。

even@安比实验室: 现在有一些zkSNARK方案支持可升级的 CRS,任何怀疑CRS的参与方都可以对CRS 进行更新。此外还有一些 zkSNARK方案支持 Universal CRS,用不着对每一个电路进行受信任设置,而是只需要全局完成一次即可。除此之外,大量无需 Trusted Setup 的方案正在被充分研究。

Succinct Non-Interactive Argument of Knowledge of Polynomial

We are now ready to consolidate the evolved zk-SNARKOP protocol. (准备整合演进的 zk-SNARKOP 协议) , now denotes a set

我们已经明确 target poly 的多项式阶数 :

Setup

  • 挑选随机值
  • 计算加密值 , ,
  • 生成 proving key (和上面相同)
  • 生成 verification key (和上面相同)

Proving

  • 分配多项式系数 (即知识),
  • 自己求多项式 (一般用 FFT 完成 ?)
  • 代入 计算多项式 的值
  • 代入 计算变换多项式 的值
  • 选择随机数 (“零成本“的 zero-knowledge)
  • 构造随机化的证明(randomized proof) :

verification :

  • Parse proof(解析证明) as
    • 我觉得这里的表述有问题, 因为 是不知道 的(也不需要知道) , 用零知识武装自己的关键工具, 不需要解包或还原 只需用 Pairing 验证证明的一致性 :
  • 验证多项式约束:
    • —————— 保证 确实用了 提供的
  • 验证多项式系数:
    • —————— 保护了 , 实现了零成本 Zero-knowledge

Remark 3.3 如果 pairing 的结果有可能在其它类似的乘法协议中被复用,那么这里就完全没有安全性可言了,因为这样的话 可以自己构造 ,

这里我理解就是 拿到了并复用了, 然后他可以发送 作为 的值来 cheat

这样也可以通过“多项式约束”的检查: —— 因为这是个恒等式, 去验证一个 “恒等式” 没有任何意义 —— 结果永远是 Accept .

Conclusion

我们用 zk-SNARK 协议来解决多项式问题的知识,不过这是一个有局限的例子。因为大家可以说 只要用另外一个有界的多项式去乘以 就可以很容易得构造出一个能够通过测试的多项式 ,并且这种结构也是有效的。

知道 有一个有效的多项式,但是并不知道是哪一个。我们可以利用多项式的其他性质添加额外的证明,如: 被多个多项式整除,是某个多项式的平方。虽然可能会有一个服务能够接受,存储和奖励所有经过证明的多项式,或者有一个需求,加密计算某种形式的未知多项式。然而若有通用方案就可以支撑无数的应用。

even@安比实验室:总结一下这篇文章中一步一步解决了下面的几个问题:

  1. 保证 prover 的证明是按照规则正确构造的 ——> KEA ( )
  2. 保证知识的零知性 ——> “无成本的” 变换
  3. 可复用证明 ——> 非交互式
  4. 非交互中如何设置安全公开且可复用的参数 ——> 参数加密,verifier 借助 airing 进行验证
  5. 保证参数的生成者不泄密 ——> MPC’s Setup

至此,一个用来证明多项式知识的完整的 zk-SNARK 协议就构造出来了,不过现在的协议在通用性上依然还有很多限制,后面的文章将继续介绍如何构造通用的 zk-SNARK。

Ref :

  • https://secbit.io/blog/2020/01/01/learn-zk-snark-from-zero-part-two/
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-2-proving-knowledge-of-a-polynomial-f817760e2805
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-3-non-interactivity-distributed-setup-c0310c0e5d1c
  • 作者:Maksym Petkus
  • 翻译 & 注解:even@安比实验室(even@secbit.io)
  • 校对:valuka@安比实验室
  • 本系列文章已获作者中文翻译授权
  • 翻译原链接

Computation

Let us consider a simple program in pseudocode:

Algorithm 1: Operation depends on an input 
—————————————————————————————————————————————————————————
function calc(w, a, b)         
    if w then         
        return a × b         
    else         
        return a + b         
    end if         
end function

Therefore we need to find a way to convert a program into the polynomial form , like this

Executing and evaluating will yield the same result: 8. and would both be resolved to 6. We can express any kind of finite program in such a way.

猜想一下,是否只要是能够用多项式表示的程序都可以做证明?

Single Operation

Any computation at it is core consists of elemental operations of the form:

If we can represent operand values as polynomials (and we indeed can as outlined) then through the arithmetic properties, we will be able to get the result of an operation imposed by an operand. (如果我们可以将操作数的值表示为多项式(我们也确实可以这么做),那么利用算术属性,我们就能够得到操作数的计算结果了。)

@Even : 回忆一下,在本系列的第一篇——多项式的性质与证明中,我们曾经说过“任何多项式在任意点的计算结果都可以看做是其唯一身份的表示。”

反过来当我们知道某个多项式的时候,是不是也就意味着我们知道多项式上某个点的取值。这就是借助多项式来完成证明的依据。

Enforcing Operation

如果一个 prover 声称有某 2 个数字的乘积,verifier 要怎样去验证呢?

Recap computation form, 我们也可以将其表示为一个运算多项式 :

在计算过程中, 如果操作数(operands)结果(output) 都能用多项式的形式正确地表示出来,那么 就应该成立

也就表明, 当取值为 时, 多项式 成立,

即该多项式一定有一个根 , 因此,这个多项式里面一定包含因式(cofactor) , 这就是我们要证明的目标多项式(target polynomial) ,即

For example, let us consider operation:

可以用一个简单的多项式表示它: ,即

运算多项式就变成了 :

因而如果 用多项式 来代替 因其依然可被 整除,所以 就认可其是有效的

相反,如果 prover 尝试用 , 即 来代替输出值去欺骗 verifier ,即 ,那么运算多项式就变成了 , 这个多项式并没有 的解,因而 不能被 整除:

因而 不会接受这个计算结果(就像**因式分解**这一章描述的那样)

在前面的协议中,我们要证明的多项式是 ,这里我们把 替换成 , 这仍然是被 承认有效的。这里目标多项式 的根就是对应能够计算出数学表达式的值的

上面例子里面取 作为运算编码的位置, 1 可以换成任何别的值,比如说 .. 在 [GGPR] 与 [PHGR] 论文中,这个取值是一个随机值,被称为 “root”

Proof of Operation

前面多项式的 SNARK一章,我们已经能够证明多项式 的知识了,只不过现在要计算的是三个多项式 的知识。我们可以定义 ,但这里存在两个争议点。

  1. ① 在我们的协议中, 证明阶段是不能做加密值乘法计算的 (即 ),因为 Pairing 只能用一次(不能复用, 会有安全风险?) —— Pairing 要用在校验多项式的约束上
  2. ② 这里给证明者留下了一个可以修改多项式结构(修改知识) 但依然保留有效因式 的机会,for example or or even —— 只需要 有一个根 就可以骗过 , 这样是不行的 !

所以 必须要 分别提供 多项式 值的证明,即协议必须修改要证明的多项式的知识( knowledge of polynomial must be adjusted.)

In essence(本质上), 在加密空间中要验证的是 .

即使 可以用 Pairing 来执行乘法(multiplication),但在 Pairing 中做减法 ( ) 是非常昂贵的计算(would require to find inverse of ),所以咱们把 移到右边:

在加密空间中, 的验证就可以转换成:

Red Part: recall that the result of cryptographic pairings supports encrypted addition through multiplication, see section on pairings.

保持 setup 阶段不变,协议更新为:

这个协议就能够证明两个值相乘的计算结果是正确的了。

你可能注意到了在这个新的协议中我们放弃了 - 零知识 部分。这么做是为了简化协议, 后面的章节我们会再变回零知识 ~

even@安比实验室:上面例子里面取 这个特殊值作为运算编码的位置。当然这里的 1 可以换成任何别的值,比如说换成 等等。在[GGPR] 与 [PHGR] 论文中,这个取值是一个随机值,被称为 “root”

名词定义

operand : 符号左边叫 left operand , right operand

  • 是具体的操作数, 比如 里的 a & b ; 里的 2 & 3

oprand polynomials : l(x) and r(x).

  • left operand polynomial (green) 几个约束等式的操作数左边竖列, 构成的 poly 叫 left operand polynomial
  • right operand polynomial (blue) ….

output polynomials : 等式右边的所有 Output 操作数 竖列 构成的 poly 叫 output polynomials

Multiple Operations

We can prove a single operation, but how do we scale(拓展) to prove (which is our ultimate goal)? Let us try to add just one another operation. Consider the need to compute the product: :

来看一个有三个乘法运算的例子 2 × 1 × 3 × 2,它按照下面的步骤执行:

我们要把它们表示为多项式,对于 相应的要 。 即通过点 , 同样的:

we use Polynomial Interpolation to represent these.

Interpolation Result :

Multi-Operation Polynomials

Now we have operand polynomials , let us see step-by-step how the correctness of each operation is verified.

Recall that a verifier is looking for equality .

本例中,计算是在点 处被表示出来的,所以 target poly 在这些点处必须 evaluation 为 ,换句话说, 的根 root 必须是 1,2 和 3,它的基本形式就是:

在实际过程中, 一般是放到单位根 root of unity —— 里的

Firstly, are multiplied which results in:

Secondly, the is subtracted from the result of which is :

已经可以看出每一个 operands multiplication 都对应了正确的结果。最后一步 要算出一个有效因式:

通过长除法(long division) 可以算出: :

自己代入 可以自己计算 : > PS: 这里为了简化过程, 省略了完整协议中的 `δ-zero-knowledge` 和 `α-shift`

现在显然 ,这就是我们要证明的内容。

这里只用了一组多项式 就将所有计算的约束关系表示出来了,有几步计算, 也就对应着目标多项式 要有几个根 (这里我这么理解: 计算的步数多了, 那么 的根也就多了, 比如可能是 , 因为约束等式的行数多了, 也就需要同步约束这些等式符合所有的计算完整性验证 )

当前的协议似乎存在一些缺陷,多项式只能证明 拥有一组多项式 ,在 的几个根的取值处 ,但无法证明这组多项式符合我们要证明的数学表达式: 1)多个计算关系之间也是分开表示的,这些算式之间的关系也同样无法进行约束 2)由于 生成的证明中只有计算结果,左操作数,右操作数,输出在计算中混用也不会被发现 3)由于左操作数,右操作数,输出是分开表示的,他们互相之间的关系无法进行约束

Variable Polynomials

现在我们可以一次证明多个运算(如上百万个甚至更多)了,但是前文结尾提到了几个关键缺点(critical downside)

如果证明中执行的“程序“在不同运算中使用了相同的变量作为操作数或输出,例如:

然而,因为我们的协议中是允许 多项式设置任何系数的,所以他可以不受限制得为不同计算中的 设置不同的值,如:

This freedom breaks **consistency** and allows to prove the execution of some other program which is not what verifier is interested in. (**这种自由打破了一致性**, 允许 去证明一些无关的程序执行)

Therefore we must ensure that any variable can only have a single value across every operation it is used in. (因而我们必须要确保每一个变量在所有运算中出现的地方都只有一个取值。)

注意:文中的 variable 与常规的计算机科学中 variable 的定义不同,这里的变量是不可改变的(immutable), 而且每次执行都只赋值一次(only assigned once per execution) (即示例伪代码中的那些不会被修改的变量)

zkSNARK 论文中,这个「变量」其实有一个对应的名词叫做 assignment,是算术电路的「赋值」,对应的是问题结构或者说算术电路的结构。而所有的 assignments 是一个算术电路可满足性问题的解,包含了算术电路的输入值以及电路运算过程中输出的中间结果值 (没看懂这里)

Single-Variable Operand Polynomial

那么, How to ensure 每一个变量在所有运算中出现的地方都只有一个取值? 究其原因, 可以设置不同值是因为他可以任意控制 的系数 a malicious 可以分别为第一第二行的 分配不同的值, 比如分别分配 :

  • assign for in raw ① , 那么此处 的系数为 , 即函数通过点
  • assign for in raw ②, 那么此处 的系数为 , 即函数通过点

此时就出现了不一致问题 —— 那么如果对于同一个变量 , 这些系数是固定的,就可以解决问题了

如下是 2 个包含相等值的多项式 : 它们分别都表示了有两个相等值对应的运算(即在 处),第一个多项式的取值为 1,第二个多项式的取值为 2:

注意: 这 2 个多项式的相应的系数是成比例的 —— 第 2 个多项式的系数是第 1 个的 2

那么由于多项式的算术性质,如果我们想要同时地改变多项式中所有的值 , 我们就需要改变它的比例,如果我们用一个数字 乘以多项式,那么多项式值就会等比例变换为

Consequently, if a verifier needs to enforce the prover to set the same value in all operations, then it should only be possible to modify the proportion and not the individual coefficients.
(因此,如果 需要在所有计算中强制 设置相同的值,他就要限制 只能改动多项式的比例, 而不是恣意篡改某处操作的单个系数)

怎么保持系数比例不变呢?对于这个问题我们可以先思考一下在 左运算多项式 (left operand polynomial) 中我们提供的 Proof 是什么 —— 是 在一些秘密值 处的加密值:

上文中, 我们已经知道怎样通过 α-shift 去限制 只能使用 (或 ) 提供的 的幂做计算,来使得单个运算能够满足同态乘法 (such that homomorphic multiplication is the single operation available.)

和限制单个求幂值相似, 可以一次限制完整的多项式 。而不只是提供单独的加密及其 α-shift :

协议的过程是:

Setup :

  • 使用多项式对应的系数构造相应的 operand polynomial
  • 创造随机 secret
  • 使用加密的 和它的 ”α-shifted“ : 来设置 proving key
  • 设置 verification key

Proving :

  • 注意这个语境下, 对应的操作数只有 一个 (Recall ① ② 两个等式) , 所以若 赋值为 , 则第① 第② 个等式里, 都要保持 的赋值为
  • 的操作数 的赋值为
    • 将其乘以操作数多项式:
    • 乘以 α-shifted 后的 operand polynomial :
  • 提供 operand polynomial multiplication Proof :

这里的 multiplication 就是指通过 来限制 对 operand polynomial 只能提供相同的 assignment

Verification

  • Parse the Proof as
  • Pairing 验证比例

前面反复提到, needs to respond with the same α-shift , and because he cannot recover α from the proving key , the only way to maintain the shift is to multiply both encryptions by the same value :

同样的道理, 用这种方法可以限制 让其无法修改 的单个系数 (modify individual coefficients of ) , 如果多项式为 只可以用一个值 去 multiply 整个多项式一次:

Multiplication by another polynomial is not available since pairings, and α-shifts of individual exponents of s are not provided. Prover cannot add or subtract either since:

This requires the knowledge of unencrypted , 这里也同样需要未加密的 的知识(才能运算)

详细解释一下 上式 :

  • 考虑 ,如果 能够任意修改多项式的系数,他可能会想要构造一个新的多项式 来欺骗
  • 但是, 无法实现这样的操作,因为他不知道确切的 的值 , 最后的等式是为了展示这一限制:
    • 左侧: 需要满足 α-shift, 通过这种方式修改 的各个系数
    • 右侧: 提供了 , 但没有提供 , 所以 无从得知 , 所以 只能提供可怜的 , 而这是无法通过 Pairing 验证的

现在有了这个协议,不过怎么去构造 operand polynomial 呢?由于任何整数都可以通过乘以 1 得到它本身,所以多项式中对应的每个计算结果都应该为 1 ,即:

> (这里的表述不清晰, 很多人没看懂, 我个人觉得类似拉格朗日基, 后面多变量会讲到: 用到了就设置为 1, 用不到就设为 0)

然后再让 prover 在其上”分配“一个值 a :

Remark 4.1 : 可以在操作数多项式上分配一个 ,而 不能检测到 , 下面具体描述了 对多项式进行特定加(或减)操作的能力,而这种操作不会影响 配对验证 , 因而可以修改多项式使其超出 的预期 或 prove a different statement,后面的章节我们将会解决掉这个问题 :

identically equal: 恒等

由于 verification key 中包含了加密了的 : , 所以 可以用多项式加(或者减)任意一个值 而不会破坏 Pairing 的成立. 后面我们会解决掉这个 bug

Summary/Recap

这一小节是解决这样一个问题,算术电路中一个 input wire 或者 output wire可能同时会作为多个门的输入 wire,如何确保约束这些公用 wire 的问题。

由于要证明的数学表达式是公开的,那么各个算式之间的约束关系也就是公开的,那么我们就可以把构造多项式的工作交给 环节,这样 只要填上对应的数值就可以了。

上文这个方法就限制了在同一个操作数多项式上,不同的计算式中使用的同一个值的约束关系;同样若一个操作数多项式中用到了多个值,也可以将这些值全都加起来,如下文所述。

Multi-Variable Operand Polynomial

如上文, 因为只有当所有的左操作数使用同一个变量 的时候我们才可以设置一个值。但是如果左操作数中再多一个值 要怎么做呢 ?

Therefore we can separate the operand polynomial into 2 operand variable polynomials :

变量 可以被分别 赋值 & 约束,然后加在一起 , 来表示所有的左操作数变量。

如果 多变量多项式 在一个对应运算中被用做操作数,那么这一项就置为 1,否则就置为 0 , 0 跟任何值相乘结果都是零,当把他们相加在一起的时候也就可以忽略掉这一项 (类似 Lagrange Basis 的作用) 在我们的例子中, 这些变量多项式必须满足以下计算:

于是我们就可以将每个变量分开 assigned value ,然后把他们加在一起来计算出操作数多项式,例如当 时 , 得到

上图中的 代表: 用 3 实例化的变量 ( variable instantiated with value 3 )

从现在起 , 我们用大写的 来表示这个复杂的操作符多项式,即

仅当每一个 operand variable polynomial 是由 约束的 (restricted by the ),结果才有效

以 left operand 部分而言, 协议部分更改为:

Setup:

  • 构造 - 使得它能够在对应的 "operation x" 处为 1,其他地方为 0
  • 选择随机数
  • 计算并加密 未赋值的变量多项式 (unassigned variable poly)
  • 计算 shifts of these polys :
  • set proving key :
  • set verification key

Proving :

  • assign values to the variable polys : {\left(g^{l_a(s)}\right)}^\textcolor{green}{a}, \ \ {\left(g^{l_d(s)}\right)}^\textcolor{green}{d}
  • assign same values to the α-shifted poly :
  • add all assigned variable polys to form a operand poly :
  • add shifted assigned variable polys to form a shifted operand poly :
  • Proving proof of valid assignment of left operand :

Verification :

  • Parse Proof i.e.
  • 验证提供的多项式是否是 最初提供的多个 未赋值的变量多项式 (unassigned variable poly) 的和:

Note: and represent all variable polynomials at once and since is used only in evaluation of variable polynomials, the prover has no option but to use provided evaluations and assign same coefficients to original and shifted variable polynomials. ( 注意:这里用 同时代表了所有的变量多项式, 并且由于 只用在计算变量多项式中,所以 没有别的选择只能在 提供的原始加密值和变换后的加密值上赋予相同的系数做计算 )

As a consequence(因此) the prover :

  • is not able to modify provided variable polynomials by changing their coefficients, except “assigning” values (除了“分配”值外,不能再修改它们的系数进而来修改 变量多项式 ), because prover is presented only with encrypted evaluations of these polynomials, and because necessary encrypted powers of s are unavailable separately with their α-shifts (因为 Prover 仅提供这些多项式的加密评估,也因为 s 必要次幂的加密值不能与它们的 α 变换值一起使用 )
  • is not able to add another polynomial to the provided ones because the α-ratio will be broken (不能通过另一个多项式相加去提供一个结果因为这样 α-比例关系将会被破坏掉)
  • is not able to modify operand polynomials through multiplication by some other polynomial , which could disproportionately modify the values because encrypted multiplication is not possible in pre-pairings space (不能通过与其他的一些多项式 相乘来修改操作数多项式,这样可能会使得修改后的值不成比例因为在预配对空间中无法进行加密乘法)

尽管 prover 被限制了多项式的使用,他还有拥有一些可允许范围内的自由度:

  • 当 prover 决定不加入一些变量多项式 来构造操作符多项式 时依然是可以接受的,因为这和为它分配值为 0 是一样的:

  • 如果 添加同一个 变量多项式 很多次也是可以接受的 , 因为这和一次分配多个值的和是一样的:

Summary/Recap

总结一下本文证明协议的大致思路为:

  1. 将要证明的程序转换为数学语言表达的形式(即加减乘除的计算)
  2. 用多项式在某处的取值来进行计算以此表示数学计算,进而进行证明
  3. 用多项式在多处的取值来进行计算表示多个数学运算,进而加以证明
  4. 对证明的“程序”在不同计算中使用的相同的变量进行约束

当前的协议约束只解决了部分问题,还有诸多可以改进的地方,在下一节我们将对这些改进项展开讨论并给证明协议进行优化。

Reference :

  • https://secbit.io/blog/2020/01/08/learn-zk-snark-from-zero-part-three/
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-4-general-purpose-computation-dcdc8081ee42
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-5-variable-polynomials-3b4e06859e30
  • 作者:Maksym Petkus
  • 翻译 & 注解:even@安比实验室(even@secbit.io)
  • 校对:valuka@安比实验室
  • 本系列文章已获作者中文翻译授权
  • 翻译原链接

Construction Properties

Constant Coefficients 常量系数

在上文的构造中,我们通过对 未赋值的变量多项式 (unassigned variable polynomials) 的计算得到 0 或者 1 ,以此表示在运算中是否要用到这个变量。自然地想,我们也可以使用其它系数值,包括负数值,因为我们可以插值计算出经过任何必要的点(前提是没有两个计算使用了同一个 )的多项式。如下是这种运算的一些例子:

现在我们的程序就可以使用常量系数了,例如:

Algorithm 2: Constant coefficients
————————————————————————————————————————————————————————————

function calc(w, a, b)
    if w then
        return 3a × b
    else 
        return 5a × 2b
    end if
end function

阶段这些系数类似于 0 或者 1 将被“硬编码”进去,之后就不能再修改了。现在我们将运算形式修改为:

或者用更正式的参数 表示:

表示变量在运算中的位置 (左/右/输出)

Addition for Free (0 成本做加法)

看一下这个新结构,很显然在多项式的表示中,每一个不同 所要代表的操作数都是所有 操作数变量多项式(sum of all operand variable polynomials ) 的总和,其中只有一个被用到的变量是非零值而其它都为 0,下图就很好得表达了这个意思:

我们可以利用这一个结构,加任何数量必要的 _变量_ 到每一个运算的操作符/输出中。例如在第一个运算中,我们可以首先做加法 _a+c_,然后就只需要用别的操作数与之相乘了,即 ,可以表示为下图: 因而也可以将这些变量中任意个,对它们先乘以任意的系数 , 再一并加入到一起作为单个操作数中,以此来根据相应程序的需要构造一个操作数值。这个特性实际上就允许将运算结构改为:

或者更正式一些用变量 和操作数变量系数

这个结构就是:

_注意 :每一个运算的操作数都有自己的一组系数 这里 乘法运算是关键,而加法运算都可以被合并到一个更大的乘法运算里面。

Addition, Subtraction and Division

到目前为止,我们一直专注于乘法操作。但是为了能够执行通用计算,真实环境下的程序也需要加法,加法和除法。

加法 前面我们确定: 可以在单个操作数的内容中将变量加起来,然后和另一个操作数相乘 —— 即 ,但是如果我们只是想做加法,没有乘法,例如一个程序中需要做 的计算,我们可以按照下面的方式来表示: 

为什么强行 × 1 呢?
因为我们的结构中对于每一个操作数, 我们既需要常量系数也需要变量
1 这个值可以表示为 ,其中  可以被“硬编码”到对应的多项式中,  是一个变量可以给它分配任何值,那么我们就必须通过一些约束来限制 的值,这个在后面的章节中将会讲到

减法 减法与加法几乎一致,唯一的不同就是负系数, 也就是: 除法 如果我们检查除法运算 可以看到除法的结果是就是我们要得到一个结果值使其乘以 divisor 能够得到 factor 。所以我们也可以用乘法来表示出同一个意思:divisor × result = factor . 这样就是说如果我们想要去证明除法运算 ,我们就可以把它表示为:

运算的结构也称为 “约束” ,因为多项式结构代表的运算,并非是为了计算出结果,而是在 prover已经知晓的变量赋值的情况下,检验这个运算的过程是否正确。换句话说,即约束 prover 必须提供一致的值,无论这些值是什么。

所有的算术计算(加减乘除)都已经有了,于是运算结构不再需要修改。

even@安比实验室: 约束和运算有一定的关联性。算术电路的目的是为了实现「计算的验证」,而非「计算的过程」。

上一篇文章中,我们提出了一种方法:把构造多项式的工作交给 环节, 只要填上对应的数值就可以了。 这个方法不仅解决了同一个操作数运算符中不一致的问题,同时还带来了额外的便利:

1) 允许执行计算的表达式中包含静态系数。 2)虽然 的关系中只有乘法,但利用这个方法也可以轻松的执行加法操作,继而也就解决了减法和除法的问题

Example Computation

看不太懂上面说啥, 直接看例子吧 !!

有了一组通用的运算结构,我们就可以将我们原始的程序转换成一组运算,然后再转换成多项式的形式。我们先来想一下算法的数学形式(用变量 表示运算结果): 这里有三个乘法,但是由于运算结构只支持一个乘法操作,所以这里至少就要做三次运算。我们先将它简化 :

  •  <span class="katex"><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="color:green;">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin" style="color:green;">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="color:green;">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="color:blue;">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin" style="color:blue;">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="color:blue;">b</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">×</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="color:green;">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin" style="color:green;">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="color:green;">b</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span></span></span></span>
    

写出 :

第 3 条是增加的约束使 必须为二进制,否则 就可以代入任何值去执行恶意运算

现在一共有 5 个变量 ( 2 个左操作符 , 4 个右操作符 和 5 个输出 ) , 操作符多项式为:

在在三次运算中, 必须为每个变量多项式 都分别算出一个对应的系数, 或者如果这个多项式在计算的操作数或者输出中没有被用到的话, 系数就置为 0 :

如上图

  • 对于左操作数 : a 在第 1 行约束出现, 所以 , w 在第 2/3 行约束出现, 所以 ,
  • 对于右操作数 : 注意 第 2 行约束的

因为有三行约束, 所以 target poly 就是

Next we leverage polynomial interpolation to find each variable polynomial :

绘制出来就是:

OK! Now we are ready to prove computation through polynomials.

首先,选择函数的输入值,例如: 。其次,计算过程中的中间变量值为:

然后,我们把所有计算结果中的值赋值到 变量多项式 (variable polynomial) 中,然后相加得到操作数或者输出多项式的形式:

注意: 的系数是其赋值, 比如 的系数都是 1 , 的系数都是 3 的系数则是 2×3 => 6

在图中就表示为:

上图表示的, 就是 上上图 里的 等都被 “拉长了” , 拉长成了

把他们具体组合成 , 即相加对应操作数如

Recap 上图: :

  • 经过了
  • 经过了
  • 则 经过了

表示给 这个变量赋值为 2

我们需要去证明 ,因而我们先 长除法 找出 :

以图的形式表示为:

图示很明显, 多项式 有根为 : ,因而 是它的因式,假如使用了和它不一致的变量值,情况就不是这样了

这就是一组能够正确计算的变量值,如何在多项式层面上证明出来的。下面 还要再继续处理协议的密码学部分

Verifiable Computation Protocol (可验证计算协议 )

我们基于前文中多项式知识协议 做了很多修改使它变得更通用 (general-purpose),来看一下它现在的定义

假设约定函数 f(*),约定其计算结果为证明对象(proof),其次数为 ,变量数为 ,其对应的系数: Setup :

  • 为左操作数 (类似 ) 构造变量多项式(variable polynomial) 然后对于所有 的运算都算出其对应的系数,即 , 对右操作数和输出也做同样的事情。 这样的
  • 随机抽取
  • 计算
  • 计算 proving key
  • 计算 verification key

Proving :

  • compute function f(*) and therefore corresponding variables values
    • 我理解这里就是 个变量 每个变量的赋值即 witness
  • 计算 ,其中 也是类似处理
  • 个变量赋值 求和, 得到 operand poly
  • assign variable values to the shifted poly :
  • 使用 的幂加密值: 计算加密值
  • set proof

Verification :

  • parse proof as
  • variable polynomial restriction check (要符合 α-shifted )
  • valid operation check (计算结果的有效性)

The set of all the variable polynomials for and the target polynomial is called a quadratic arithmetic program (QAP, introduced in Gen+12 ).

虽然协议足够健壮,可以进行常规的计算验证,但这里依然还有两个安全考虑需要去解决。

2 security considerations

1 | Non-Interchangeability of Operands and Output

操作数和输出的不可替代性

Because we use the same for all operands of variable polynomials restriction check , there is nothing that prevents from

  • 使用其它的操作数中的可变多项式,即
  • 完全交换操作数多项式, 也就是把 换成
  • 复用相同的操作数多项式,即

可交换性就是指 可以修改计算过程,并有效证明一些其它无关的计算结果。防止这种行为的一个很显然的方式就是在不同的操作数上使用不同的 ,具体协议就可以修改为:

选择随机数 来代替

Setup :

  • sample random instead of
  • calculate corresponding shifts
  • Proving key : Proving :
  • assign variables to the shifted poly : It is now not possible to use variable polynomials from other operands since following  α-s are not known to the prover: (这样就不能在一个操作数中使用其它操作数(operands) 的变量多项式了,因为 prover 没有办法去获知 来满足 变换关系 )

even@安比实验室: 这里通过对 进行分开 KEA 检查,就解决了上篇文章中提出的第二个缺陷问题——由于 prover 生成的证明中只有计算结果,左操作数,右操作数,输出在计算中混用也不会被发现。

同样下面一节也解决了上篇文章中提出的第三个缺陷问题——由于左操作数,右操作数,输出是分开表示的,互相之间的关系无法进行约束

2 | Variable Consistency Across Operands(一致性校验和)

跨操作数的变量一致性

For any variable  we have to assign its value to a variable polynomial for each corresponding operand, i.e.: ( 对于任意的变量  ,我们都必须将它的值 分配 到每个相应操作数中的一个与之对应的 变量多项式 上,即:) Because the validity of each of the operand polynomials is checked separately, no enforcement requires to use same variable values in the corresponding variable polynomials. This means that the value of variable    in left operand can differ from variable   in the right operand or the output. ( 因为每一个 operand polynomials 的有效性是分开校验的,并不强制要求我们在对应的 variable polynomials 中使用相同的变量值。这就意味着在左操作数中变量  的值可以与右操作数或输出中的变量值 不同)

我们可以通过熟悉的限制多项式的方法(也就是限制变量多项式的方法)在操作数之间强制变量值相等

If we can create a “shifted checksum” variable polynomial across all operands, that would restrain prover such that he can assign only same value. A verifier can combine polynomials for each variable into one, e.g., ( 如果我们能够在所有的操作数之间创造一个作为“变换的校验和”(shifted checksum) 的变量多项式(variable polynomial),(这里我理解就是创建一个包含了所有 variable 的 variable poly, 对这些所有的 variable 整体做 α-shift, 就一个都别跑都被约束住了) 那么就可以限制 使其只能够赋予(给每个变量)相同的值 可以将这些每个变量的多项式加起来,即: 然后乘以一个额外的随机数 β ,即 提供这些 β-shifted poly ,与变量多项式一起给它赋上变量值: 然后加密 β 并把 加到 verification key 中。现在如果所有的 值相同,即, 等式就满足:

(2) 式的底 被暂时忽略省去了

如上, 式成立的条件是 : 当且仅当 都相等时,这个等式才会成立

尽管这个一致性校验很有用,但还是存在一定的概率 中至少有两项要么计算值相同, 要么一个多项式可以被另一个整除等情况,这就允许 去分解 这些值的关系,使得即使有至少两个不相等的值也依然能够保持等式成立,从而使上 式的校验无效

例如,一个以 为例的单个运算。我们用 w 来表示这 2 式的评估, 同时令 。这个等式看起来就是:

对于任意的 ,这种形式可以令 ,上式也就变换成: 所以说, 如果 刻意让 , 则这样的一致性策略是恒成立/无效的。缓解这种情况的一种方法是对每个操作数都使用不同的 确保操作数的 变量多项式 中包含无法预测的值

以下是修改后的协议:

Setup

  • …随机数

  • variable consistency poly(变量一致性多项式) 进行计算,加密并添加到 proving key 中:

  • 加密并将其加到 verification key 中:

Proving

  • …assign variable values to variable consistency poly :
  • add assigned polys in encrypted space : (将赋值的多项式加加密空间中):
    • 对每一个变量 ​,都要计算它的一致性校验和(shifted-checksum) ,然后我们将所有的 的值相乘, 得到
  • add to the proof:

在这后面增加吗?

Verification

  • …校验提供的 操作数多项式 和 “校验和”多项式之间的一致性: PS: Pairing 公式参考 :

这个构造中, 同一个变量值就无法乱用了,因为不同的 使得相同多项式无法兼容,但是这里还存在与 remark 4.1 相同的缺陷,由于 是公开可见的, 攻击者可以修改任意变量多项式的零索引系数(modify the zero-index coefficient of any of the variable polynomials),因为它并不依赖于 ,i.e.

变量非延展性和变量一致性多项式

(Non-malleability of Variable and Variable Consistency Polynomials)

1 | 变量多项式的延展性

Recall 第三章的 Remark 4.1 : 可以在操作数多项式上分配一个 ,而 不能检测到 , 下面具体描述了 对多项式进行特定加(或减)操作的能力,而这种操作不会影响 配对验证 , 因而可以修改多项式使其超出 的预期或 prove a different statement,后面的章节我们将会解决掉这个问题 : 由于 verification key 中包含了加密了的 : , 所以 可以用多项式加(或者减)任意一个值 而不会破坏 Pairing 的成立. 后面我们会解决掉这个 bug

举一个 remark 4.1 有关的例子,看一下下面的两个运算: 预期的结果 b = ac = 3a , 即 c = 3b。这就是说 left operand’s variable polynomial 的计算结果为 (第 行约束的系数为 , 第 行的约束为 )

先不管 的形式, 都可以不按照上述的比例用另一个修改了的多项式 来给 赋值。这样运算就变成了 , 结果也就是 b = a + 1c = 3a + 1,其中 c ≠ 3b ,这意味着 的取值的实际意义在不同运算中是不一样的

但是因为 已经拿到了 ,所以他依然能够正确地通过 correct operand polynomialsvariable values consistency 的校验:

…Proving:

  • 用分配不成比例的变量 a 来建立左操作数多项式:
  • 按照常规的方式构造右操作数多项式和输出多项式:
  • 计算除数:
  • 计算加密值: ,并按照常规方式计算
  • 计算 α-shifts 的加密值: ,并按照常规方式计算
  • 计算变量一致性多项式:

其中下标 代表对应变量的符号, 指数 代表变量的值;以及未定义的变量多项式的值为 0。

  • set proof :

Verification:

  • variable poly restriction check : and as usually for

  • 变量多项式约束检查:

  • 有效计算检查:

2 | Malleability of Variable Consistency Polynomials(变量一致性多项式的延展性)

Moreover, allows to use different values of same variable in different operands. For example, if we have an operation: (而且 的存在允许我们在不同操作数的相同变量上使用不同的值。例如,如果我们有一个运算):

Which can be represented by the variable polynomials: 简单插值, 得到 尽管我们期待的输出是 ,但我们可以设置不同的 值,例如:设置 (left operand), (right operand) , 如下:

Proving:

  • …用 设置操作数多项式
  • 设置操作数多项式
    • (在 时) , 所以 +3 是为了确保我们在 处得到正确的操作数 5
  • 设置输出多项式 :
  • … 计算加密值 :
  • 计算变量一致性多项式: Verification:
  • ……变量值的一致性检查,应满足:

注意:多项式 其实可以被忽略掉的,因为这几项对于任何 的取值,计算结果都为 0,但是为了保持完整性我们依然要保留这几项

even@安比实验室:这种能力会危害到协议的可靠性。很显然,加密的 不应该对 Prover 可见

3 | Non-Malleability 非延展性

解决延展性(Malleability) 问题的一个方法就是,在 setup 阶段将 encrypted space中的 项与随机秘密值 (gamma) 相乘, 从而使 verification key 中加密的 与加密值 不兼容: 相应的这种被修饰过的加密值,就能阻止修改加密值 的可行性,因为 中没有 ,即:

注 :

因为变值 是随机的, 并不知道它的值。所以这个修改就需要我们用 乘以 来平衡协议中的变量值一致性校验等式:

Setup:

  • …随机数
  • …设置 verification key Proving:Verification:
  • … 变量值一致性检查应满足: 这里很重要的一点是我们排除了变量多项式为 0-阶的例子(e.g. ),否则就可以从 proving keyvariable consistency polynomials (变量一致性多项式) 中揭露出加了密的 比如这个例子中当操作数(Operand) / 输出(Output) 中的任意两项为 时, e.g. , this will result in : 如此 就直接被 exposed 出来了

我们同样也可以通过“修饰“(mask) α-s 项来解决 变量多项式 的延展性问题。但是这就没有必要了,因为对于 变量多项式 的任何修改,都需要被映射到变量的 一致性多项式 中,而一致性多项式是无法修改的

变量值一致性检查的优化

现在 variable values consistency check 是有效的,但是这里在 verification key 中增加了 4 个昂贵的 Pairing 操作和 4 个新的项。文献 Par+13 中的 Pinocchio 协议用了一个很聪明的方法优化,通过选择不同的生成元 ,从而对每个 operand 实行“移位”:

Setup

  • …选择随机值 , and set
  • set generators
  • set proving key
  • 设置 verification key

Proving

  • …assign variable values : Verification
  • …变量多项式约束检查: & 对 做同样的检查
  • 变量值约束检查:
  • 有效运算检查:

生成元的这种随机化进一步增加了安全性,使得如 remark 4.1描述的 variable polynomials 延展性无效。因为对于故意的修改,它必须要么是 原始值的倍数 , 要么就是不可直接用的加密值的倍数(假定, 如上文所述我们不去处理可能曝光加密后的值的 0 阶可变多项式)

这个优化使得 verification key 减少了 2 个项, i.e. instead of : ,并且去除了 verification 步骤中的两个配对运算 :

注意:这在 Jens Groth 2016 年的 paper Gro16 中有更进一步的改进

even@安比实验室: 至此,通用 zk-SNARK 协议的已经几乎构造完成了,本文可以归纳为以下几点:

  • 协议中是如何增加可变系数的和如何做加减乘除运算的
  • 协议如何保证操作数和输出的不可替代性
  • 协议如何保证跨操作数的可变一致性
  • 协议如何处理非延展性变量和变量一致性
  • 协议中变量值一致性检查优化

Reference :

  • https://secbit.io/blog/2020/01/15/learn-zk-snark-from-zero-part-four/
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-5-variable-polynomials-3b4e06859e30
  • https://medium.com/@imolfar/why-and-how-zk-snark-works-6-verifiable-computation-protocol-1aa19f95a5cc
  • 作者:Maksym Petkus
  • 翻译 & 注解:even@安比实验室(even@secbit.io)
  • 校对:valuka@安比实验室
  • 本系列文章已获作者中文翻译授权
  • 翻译原链接

约束和公共输入 Constraints and Public Inputs

约束 Constraints

我们的分析主要集中在运算的概念上。但是,协议实际上不是去做”计算“,而是检验输出值是否是操作数正确运算得到的结果。所以我们称之为约束,即一个 约束 去为预定义的“程序”提供有效值,而无论这个“程序”是什么。多个约束组成的系统被称为“约束系统”(在我们的例子中这是一个一阶约束系统,或被称为 R1CS

@Maksym(作者):这里其实隐含了寻找所有正确答案的一个方法就是对所有可能的组合值进行一次暴力破解,然后只选择一个满足的约束,或者使用可满足约束的更精密的技术 con18 even@安比实验室:请注意这个约束是定义在算术电路,或者布尔电路上。因为这两类电路的可满足性问题是 NP-Complete 问题。

因而我们也可以使用约束来确保其它的关系。例如,如果我们想要确认变量 的值只能为 0 或 1(即二进制数),我们可以用一个简单的约束去做这件事: 我们也可以约束 的值只能为 2: 一个更复杂的例子是确保数字 是一个 4-bit 的数字(也称为半字节 nibble),换句话说可以用 4 个bit 来表示出 , 我们也可以称这个为“确保取值范围” , 因为一个 4-bit 的数字可以代表 的组合,因而也就是从 0 ~ 15 范围内的 16 个数字。如

Therefore if is a 4-bit number, then ,for some boolean ,  The constraint can be following: 并且为了确保 都是二进制数我们需要增加约束: 可以写成 Circom 代码:

#![allow(unused)]
fn main() {
a * 1 = 8*b3 + 4*b2 + 2*b1 + 1*b0
b0 * b0 = b0
b1 * b1 = b1
b2 * b2 = b2
b3 * b3 = b3
}

更复杂的约束也可以用这种方式表示,以此来确保使用的值满足规则。需要注意的是,上述约束 在当前操作的构造中是不可能的: 因为值 1 (以及前面约束中的 2)必须通过 表达出来,其中 c 可以被固定到 proving key 中,但是因为 v 是由 提供的,所以可以是任何别的值。尽管我们可以通过设置 c = 0 来强制 变成 0,但是在我们前面受限的构造方法中很难找到一个约束来强制 为 1。于是, 需要有一种办法来设置 的值

首先,我们需要明确 这个特殊变量的角色。在 zk-SNARK的 约束系统中,它是被预设为 1 的。换句话说,无论其他变量如何变化, 的值始终应该是1。

然后,该部分内容提到的 ,实际上是约束系统中的一个公式,其中 是证明者在证明过程中需要选择的一个值, 是我们前面说的那个始终为 1 的特殊变量。

然后,它提到,即使我们可以通过设置 来使得 为0,但在当前的构造方法中,我们却无法找到一个约束来强制 为 1 。这个意思是说,在我们的构造方法中,我们可以通过选择不同的 使得 为任何我们希望的值,但这并不能保证 始终为1

even@安比实验室: 我们前文中提到的表达式的约束关系就称为 R1CS

Public Inputs and One (公共输入和 1)

如果不能根据 的输入对其进行检查,例如,知道证明者已将两个值相乘而不知道结果和/或值是什么,那么证明的可用性将受到限制。虽然可以在 proving key 中通过“硬编码(hardwire)”来进行验证一些特定的值(如,约束某步乘法运算的结果必须为 12 ),但这就需要针对每一个所需的的 “verifier 输入”生成单独的密钥对 (this would require to generate separate pair of keys for each desired “verifier’s input.”)

even@安比实验室: 这样会严重限制实用性,电路需要支持参数。

因而如果可以由 为计算指定一些值(输入/输出),包括 ,而不是由 来控制, 那证明就可以变得更通用!! (Therefore it would be universal if the verifier could specify some of the values (inputs or/and outputs) for the computation, including the  , instead of the .)

首先,我们看一下要证明的值 Because we are using the homomorphic encryption it is possible to augment these values, for example, we can add another encrypted polynomial evaluation (利用同态加密,我们可以扩大这些值) which means that the verifier could add other variable polynomials to the already provided ones. Therefore if we could exclude necessary variable polynomials from the ones available to the prover, the verifier would be able to set his values on those variables, while the computation check should still match. 这意味着验证者 可以将 other variable polynomials 添加到已经提供的 polys 中。因此,如果我们可以从提供给 的变量多项式中, 排除(exclude) necessary variable polys, 验证者 将能够在这些变量上设置他的值,而计算检查应该仍然匹配。

也就是说, 这样如果我们能够在提供给 的变量多项式中排除必要的一项, 就可以在这一项变量多项式上设置他自己的值,并且使得检查依然能够通过

It is easy to achieve since the is already constraining the prover in the choice of polynomials he can use empolying the α-shift. Therefore those variable polynomials can be moved from the proving key to the verification key while eliminating its α-s and β checksum counterparts. 这很容易实现,因为 已经限制了 选择他可以使用 α-shift 的多项式。因此,这些可变多项式可以从 proving key 转移到 verification key ,同时消除其 α-s 和 β 校验和对应项。

也就是说, 因为 早已能通过加入 α-shift 来限制 选择多项式,所以这个应该很容易实现。因而当消除了它的 校验和对应的项,这些可变多项式就可以从 proving key 转移到 verification key 当中去了

必要的协议更新为:

Setup (需自行对比 former protocol version)

  • …将 个 variable polys 全部分为两组:
    • 项: , 对 也做同样的计算。这里对于索引 0 保留值 (where idx-0 is reserved for the value of )
    • 项:
    • … 对 也做同样的计算
  • 设置 proving key
  • 添加到 verification key

Proving :

  • …为 的多项式计算 , 其中 , and similarly for

  • Provide the Proof : Verification :

  • 的变量多项式赋值,并加 (and add to 1) : 做同样的计算 关于为啥要 +1 : 需要一种机制,让 能够控制一些变量的值,而不是由 控制, 通过同态加密, 让v_one 就是一个始终为 1 的特殊变量 (先感性理解下吧 …)

  • 变量多项式约束检查: 做同样的计算

变量值一致性检查:

有效计算检查:

注意:根据协议(单个变量操作数多项式 的章节)的性质,由多项式 表示的值 已在相应的运算中具备了合适的值,因此不需要再赋值了

注意: 将不得不在验证步骤中做额外的工作,使得赋值的变量成比例。

这实际上是把一些变量从 手中拿到 的手中,并同时保持等式相等。 因而只有当 的输入中使用相同值的时候, 有效计算检查 才依然成立。

1 这个值相当重要,它能够通过与任意一个常数项相乘来生成这个值(从选择的有限域上),例如,用 123 去乘以 a

even@安比实验室: 这里将原本由 赋值的一些变量改为由拿到 赋值,使得 不得不与 保持相同的输入。这不仅解决了 参数输入的问题,也间接解决了常数赋值的问题

Zero-Knowledge Computation

Zero-Knowledge Proof of Computation

(计算的零知识证明) 自从引入通用计算协议(计算的证明这一章节),我们一直放弃了 零知识 的性质,这是为了让协议的改进变得更简单。至此,我们已经构建了可验证的计算协议。

以前我们使用随机数 δ-转换来构造多项式的“零知识” 证明,这种方法能够使得证明与随机数无法区分(零知识这一章节): 通过计算我们证明了: 尽管我们可以通过用相同 δ × 多项式的方法来调整解决方案,即提供随机值 ,这依然能够通过 有效计算检查 来满足配对验证: 但是问题是使用相同的 会妨碍安全性,因为我们在证明中分别用了以下这些值:

  • 其他人可以很容易得辨认出两个不同的多项式值是否相同,以此来获取一些知识,即:
  • 的不同值之间潜在的微小关系可能会通过暴力破解来区分开来,例如如果 ,就可以对 取值反复校验 ,只需要执行 5 步就可以揭示出两者 5 倍区别的关系。同样的暴力破解也可以用在破解加密值的加法运算上,如:
  • 证明元素之间的其它关系也可能会被发现,例如,如果 ,那么也就表示

注意:一致性检查优化 使得挖掘数据关系变得更加困难了,但是依然能够发现一些关系 ,且不说 可以选择特定 来为揭示知识提供便利(只要这不是一个多样化的

最终,我们需要对每一个多项式的值使用不同的随机数 ,例如: 为了解决等式右边不相等的问题,我们不必改变协议,只要修改证明的值 即可。 这里 Delta ( ) 代表为了平衡方程另一侧的随机性而对 做的处理,?⃝ 代表 乘法运算或者 加法运算(这个反过来也适应了除法和减法)。

  • 如果我们选择用乘法 (?⃝ = ×) 来计算 ,也就意味着不太可能有较大的概率可以找到一个 ,因为存在随机性: 设置 ,于是就变成了: 但是如前文所述,这个妨碍了零知识的性质,更重要的是这个结构也不再适合 verifier 的输入多项式,因为它们必须是 相应的倍数,这就需要额外的交互了

我们可以尝试把随机数加到变量上:

但是随机数是不可除尽的。尽管我们可以用 去乘以每一个 ,但由于我们已经用了 乘以 是组成加密结果的一部分(即 相等),因此在没有使用配对(它的结果在另一个数值空间内)的情况下是不能计算出 的。同样也不能使用 的幂(from to )的加密值对 进行加密计算, 的阶将达到 并且,基于上述同样的原因也无法计算这个随机操作数多项式的值: 于是我们应该用加法(?⃝ = +)来使用 ,因为它可以同态地计算。

分子中的每一项都是 的倍数,因而我们可以将其与 相乘使它可以被分母整除:

这样就可以在加密的空间中进行“有效计算检查”了: 于是既隐藏了加密值,又使得等式可以通过 有效计算 的检查 这个结构就是统计学上的零知识 因为增加了 的均匀随机倍数(参见 [Gen+12] 中的定理 13)

注意:这种方法和 的操作数也是一致的,即:

因而当且仅当 使用了 的值来构造证明(即, ,这个有效计算的检查依然是成立的,更多的细节看下一部分

为了使得 “变量多项式限制” 和 “变量值一致性”检查与 零知识 的修改一致,就有必要去增加以下的参数到 proving key 中: 非常奇怪的是最初的 Pinocchio 协议[Par+13]主要关注可验证的计算,而较少涉及 零知识 性质,这其实只需要一点点小修改,这个几乎是没有什么成本的。

even@安比实验室: 与前文中的零知方案不同,这里通过相加而不是相乘的方式来确保 prover 知识的零知性。

Pinocchio 协议是针对 GGPR 论文的改进,在3.1节中也提到了实现零知识只需要沿用 GGPR 论文的方法即可,并不是这篇论文的贡献。另外,Pinocchio 协议论文侧重工程实践,在2013年时,零知识证明还并没有得到应用。真正的应用还是自从 ZCash 起始

zk-SNARK 协议

在这一步步的改进之后,我们得到了最终版本的 zkSNARK,又名 Pinocchio [Par + 13],协议 (zero knowledge is Optional, 并用紫色标注出来了),就是:

Setup

  • 选择生成元 和加密配对

  • 将变量总数为 , 其中输入/输出变量数位 的函数 ,转换为阶数为 大小为 的多项式形式(QAP)

  • 选择随机数

  • 设置 和操作数生成元

  • 设置 proving key

  • 设置 verfication key

Proving

  • 代入输入值 ,执行 计算获取所有的中间变量值
  • 把所有未加密的变量多项式赋值给 ,并对 做同样的计算
  • 选择随机数
  • 计算
  • 将 prover 的变量值赋值给加密的可变多项式
  • 再用同样的方式计算
  • 为变量值一致性多项式赋值 :
  • 计算证明

Verification

  • 解析提供的证明为

  • 输入/输出 赋值给 verifier 的加密多项式并加 1 : 并对 做同样的计算

  • 可变多项式约束检查: 并对 做同样的检查

  • 变量值一致性检查:

  • 有效的计算检查:

结论

我们最终完成了一个允许证明计算的有效协议:

  • 简明 (Succinctly) —— 独立于计算量,证明是恒定的,小尺寸的
  • 非交互性 (Non-interactive) —— 证明只要一经计算就可以在不直接与 prover 交互的前提下使任意数量的 verifier 确信
  • 可论证的知识 (with Argument of Knowledge) —— 对于陈述是正确的这点有不可忽略的概率,即无法构造假证据;并且 prover 知道正确陈述的对应值(即:证据),例如,如果陈述是 “B 是 sha256(a) 的结果” 那么就说明 prover 知道一些值 a 能够使得 B = sha256(a) 成立,因为 B 只能够通过 a 的知识计算出来,换句话说就是无法通过 B 来反算出 a(假定 a 的熵足够)。
  • 陈述有不可忽略的概率是正确的 (even@安比实验室: 这里指 Soundness 可靠性),即,构造假证据是不可行的
  • 零知识 ( zero-knowledge) —— 很“难”从证明中提取任何知识,即,它与随机数无法区分。

even@安比实验室: 所谓 Argument——论证,区别于 Proof —— 证明。 Pinocchio 协议是 Argument 而非 Proof。这是因为 Pinocchio 的可靠性是 Computational Soundness,Statistical ZK,这一类的证明系统被称为 Argument。所谓的 Computational Soundness 暗含了这样的事实:如果 Prover 计算能力足够强大的话,可以破坏可靠性。

基于多项式的特殊性质,模运算,同态加密,椭圆曲线密码学,加密配对和发明者的聪明才智才使得这个协议得以实现。

这个协议证明了一个特殊有限执行机制的计算,即在一次运算中可以将几乎任意数量的变量加在一起但是只能执行一次乘法,因而就有机会优化程序以有效地利用这种特性的同时也使用这个结构最大限度地减少计算次数。

为了验证一个证明, verifier 并不需要知道所有的秘密数据,这一点很关键,这就使得任何人都可以以非交互式方式发布和使用正确构造的 verification key。这一点与只能让一个参与者确信证明的“指定 verifier”方案相反,因而它的信任是不可转移的。在 zkSNARK 中,如果不可信或由单方生成密钥对,则可以实现这个属性。

零知识证明构造领域正在不断发展,包括引入了优化([BCTV13, Gro16, GM17]),改进例如可更新的 proving keyverification key([Gro+18]),以及新的构造方法(Bulletproofs [Bün+17], ZK-STARK [Ben+18], Sonic [Mal+19])

PLONK 系列作者:郭宇@Secbit: Founder of Secbit, https://github.com/sec-bit , https://secbit.io/

原链接:https://github.com/sec-bit/learning-zkp/tree/develop/plonk-intro-cn

理解 PLONK(一):Plonkish Arithmetization

算术化是指把计算转换成数学对象,然后进行零知识证明。 Plonkish 算术化是 Plonk 证明系统特有的算术化方法,在 Plonkish 出现之前,主流的电路表达形式为 R1CS,被 Pinocchio,Groth16,Bulletproofs 等广泛采用。2019 年 Plonk 方案提出了一种看似复古的电路编码方式,但由于 Plonk 方案将多项式的编码应用到了极致,它不再局限于算术电路中的「加法门」和「乘法门」,而是可以支持更灵活的「自定义门」与「查表门」。

我们先回顾一下 R1CS 的电路编码,也是相关介绍最多的算术化方案。然后我们对比引入 Plonkish 编码。

算术电路与 R1CS 算术化

一个算术电路包含若干个乘法门与加法门。每一个门都有「两个输入」引脚和一个「输出」引脚,任何一个输出引脚可以被接驳到多个门的输入引脚上。

先看一个非常简单的算术电路:

img20230414162317

这个电路表示了这样的一个计算:

电路中有4个变量,其中三个变量为输入变量 ,一个输出变量 ,其中还有一个输入为常数,其值为

一个电路有两种状态:「空白态」和「运算态」。当输入变量没有具体值的时候,电路处于「空白态」,这时我们只能描述电路引线之间的关系,即电路的结构拓扑。

接下来的问题是,我们要先编码电路的「空白态」,即编码各个门的位置,和他们之间引线连接关系。

R1CS 是通过图中的乘法门为中心,用三个「选择子」矩阵来「选择」乘法门的「左输入」、「右输入」、「输出」都分别连接了那些变量。

我们先看看图中最上面的乘法门的左输入,可以用下面的表格来描述:

这个表格只有一行,因此我们可以用一个向量 来代替,表示乘法门的左输入连接了两个变量, 。记住,所有的加法门都会被展开成多个变量的相加(或线性组合)。

再看看其右输入,连接了一个变量 和一个常数值,等价于连接了 的两倍,那么右输入的选择子矩阵可以记为

这里同样可以用一个行向量 来表示,其中的 即为上图中电路的常数引线。

最后乘法门的输出按照上面的方法可以描述为 ,即输出变量为

有了三个向量 ,我们可以通过一个「内积」等式来约束电路的运算:

这个等式化简之后正好可以得到:

如果我们把这几个变量换成赋值向量 ,那么电路的运算可以通过「内积」等式来验证:

而一个错误的赋值向量,比如 ,则不满足「内积等式」:

左边运算结果为 ,右边运算结果为 。当然,我们可以验证 也是一组合法(满足电路约束)的赋值。

并不是任何一个电路都存在赋值向量。凡是存在合法的赋值向量的电路,被称为可被满足的电路。判断一个电路是否可被满足,是一个 NP-Complete 问题,也是一个 NP 困难问题。

这里例子中的两个乘法门并不相同,上面的乘法门是左右输入中都含有变量,而下面的乘法门只有一边的输入为变量,另一边为常数。对于后者这类「常数乘法门」,后续我们也把他们看作为特殊的「加法门」,如下图所示,左边电路右下的乘法门等价于右边电路的右下加法门。

那么如果一个电路含有两个以上的乘法门,我们就不能用 三个向量之间的内积关系来表示运算,而需要构造「三个矩阵」的运算关系。

多个乘法门

比如下图所示电路,有两个乘法门,他们的左右输入都涉及到变量。

c

这个电路表示了这样的一个计算:

我们以乘法门为基准,对电路进行编码。第一步将电路中的乘法门依次编号(无所谓编码顺序,只要前后保持一致)。图中的两个乘法门编码为 #1#2

然后我们需要为每一个乘法门的中间值引线也给出变量名:比如四个输入变量被记为 ,其中 为第二个乘法门的输出,同时作为第一个乘法门的右输入。而 为第一个乘法门的输出。于是我们可以得到一个关于变量名的向量:

该电路的「空白态」可以用下面的三个矩阵来编码:

其中 为乘法门的数量,而 大致为引线的数量。每一个矩阵的第 行「选择」了第 个乘法门的输入输出变量。比如我们定义电路的左输入矩阵

其中第一个乘法门的左输入为 , 第二个乘法门的左输入为 。右输入矩阵 定义为:

其中1号门的右输入为 ,第二个乘法门的右输入为 。最后定义输出矩阵

我们把所有的引线赋值看作为一个向量: (这里用字母 ,取自 Assignments 首字母)

在上面的例子中,「赋值向量」为

于是我们可以轻易地检验下面的等式

其中符号 为 Hadamard Product,表示「按位乘法」。展开上面的按位乘法等式,我们可以得到这个电路的运算过程:

请注意,通常「赋值向量」中需要一个固定赋值为 的变量,这是为了处理加法门中的常量输入。

优缺点

由于 R1CS 编码以乘法门为中心,于是电路中的加法门并不会增加 矩阵的行数,因而对 Prover 的性能影响不大。R1CS 电路的编码清晰简单,利于在其上构造各种 SNARK 方案。

在 2019 年 Plonk 论文中的编码方案同时需要编码加法门与乘法门,看起来因此会增加约束的数量,降低 Proving 性能。但 Plonk 团队随后陆续引入了除乘法与加法外的运算门,比如实现范围检查的门,实现异或运算的门等等。不仅如此,Plonk 支持任何其输入输出满足多项式关系的门,即 Custom Gate,还有适用于实现 RAM 的状态转换门等,随着查表门的提出,Plonk 方案逐步成为许多应用的首选方案,其编码方式也有了一个专门的名词:Plonkish。

Plonkish 算术门

回看下例子电路,我们把三个门全都编号, ,同时把加法门的输出值也标记为变量

显然,上面的电路满足三个约束:

我们定义一个矩阵 来表示约束( 为算术门的数量):

为了区分加法和乘法,我们再定一个向量 来表示运算符

于是我们可以通过下面的等式来表示三个约束:

如果把上面的等式代入并展开,我们可以得到下面的约束等式:

化简后得:

这正好是三个算术门的计算约束。

总结下,Plonkish 需要一个矩阵 来描述电路空白态,而所有的赋值则写入了 矩阵。对于 Prover 和 Verifier 的交换协议, 是 Prover 的 witness,属于秘密知识,对 Verifier 保密, 矩阵代表了一个实现双方约定共识的电路描述。

不过仅仅有 矩阵是不足以精确描述上面的例子电路。

复制约束

比较下面两个电路,它们的 矩阵完全相同,但它们却完全不同。

两个电路的区别在于 是否被接入了 #1 号门。如果让 Prover 直接把电路赋值填入 表格,一个「诚实的」Prover 会在 两个位置填上相同的值;而一个「恶意的」Prover 完全可以填上不同的值。如果恶意 Prover 在 也填入不同的值,那么实际上 Prover 证明的是上图右边的电路,而非是和 Verifier 共识过的电路(左边)。

我们需要增加新的约束,强制要求右边电路图中 。这等价于我们要求 Prover 把同一个变量填入表格多个位置时,必须填入相等的值

这就需要一类新的约束——「拷贝约束」,即 Copy Constraint。Plonk 采用「置换证明」保证 表格中多个位置上的值满足拷贝关系。我们继续用上面这个电路图的案例来说明其基本思路:

设想我们把 表格中的所有位置索引排成一个向量:

然后把应该相等的两个位置互换,比如上图中要求 。于是我们得到了下面的位置向量:

然后我们要求 Prover 证明: 表格按照上面的置换之后,仍然等于自身。置换前后的相等性可以保证 Prover 无法作弊。

再来一个例子,当约束一个向量中有三个(或多个)位置上的值必须相同时,只需要把这三个(或多个)位置的值进行循环移位(左移位或者右移位),然后证明移位后的向量与原向量相等即可。比如:

如果要证明 ,那么只需要证明:

在经过置换的向量 中, 依次右移交换,即 放到了原来 的位置,而 放到了 的位置, 则放到了 的位置。

如果 ,那么 所有对应位置上的值都应该相等,可得: ,即 。这个方法可以适用于任意数量的等价关系。(后续证明两个向量相等的方法请见下章)

那么如何描述电路赋值表格中的交换呢?我们只需要记录 向量即可,当然 向量也可以写成表格的形式:

加上 ,空白电路可以描述为 ,电路的赋值为

再比较

R1CS 的 表格的宽度与引线的数量有关,行数跟乘法门数量有关。这个构造相当于把算术电路看成是仅有乘法门构成,但每个门有多个输入引脚(最多为所有引线的数量)。而 Plonkish 则是同等对待加法门与乘法门,并且因为输入引脚只有两个, 所以 表格的宽度固定,仅有三列(如果要支持高级的计算门,表格可以扩展到更多列)。这一特性是 Plonk 可以利用 Permutation Argument 实现拷贝约束的前提。

…, and thus our linear contraints are just wiring constraints that can be reduced to a permutation check.

按照 Plonk 论文的统计,一般情况下,算术电路中加法门的数量是乘法门的两倍。如果这样看来, 表格的行数会三倍于 R1CS 的矩阵。但这个让步会带来更多的算术化灵活度。

电路验证协议框架

有了电路空白结构的描述和赋值,我们可以大致描述下 Plonk 的协议框架。

首先 Prover 和 Verifier 会对一个共同的电路进行共识, 。 假设电路的公开输出为 ,而 为秘密输入。

Prover 填写 矩阵(Verifier 不可见):

其中增加的第四行是为了增加一个额外的算术约束: ,把 值显示地表示在 矩阵中。

相应的那么 Prover 和 Verifier 共识的 矩阵为

其中第四行约束,保证 ,可以把 代入下面的算术约束,可得 ,即

为了保证第一行的 也必须为 ,这就需要在 矩阵中添加额外的一条拷贝约束:让 变量的位置 与 第四行的输出 交换对调:

如果 Prover 是诚实的,那么对于 ,下面的算术约束等式成立:

验证协议的大概思路如下:

协议开始:Prover 如实填写 表格,然后把 表格的每一列进行编码,并进行多项式编码,并把编码后的结果发送给 Verifier

协议验证阶段:Verifier 与 Prover 通过进一步的交互,验证下面的等式是否成立:

当然这个验证还不够,还要验证 之间的关系。还有,Verifier 如何通过多项式来验证电路的运算,请看后续章节。

参考文献

  • [BG12] Bayer, Stephanie, and Jens Groth. “Efficient zero-knowledge argument for correctness of a shuffle.” Annual International Conference on the Theory and Applications of Cryptographic Techniques. Springer, Berlin, Heidelberg, 2012.
  • [GWC19] Ariel Gabizon, Zachary J. Williamson, and Oana Ciobotaru. “Plonk: Permutations over lagrange-bases for oecumenical noninteractive arguments of knowledge.” Cryptology ePrint Archive (2019).

理解 PLONK(二):多项式编码

在上篇文章里,我们可以把电路的计算的「合法性检查」转换成一组加法/乘法约束。假如总共有 N 个约束,那么Prover 可以通过多项式编码的方式把多个约束压缩成一个约束,让 Verifier 轻松检查。

多项式的概率检查

把多个约束验证合并的神奇能力来自于「多项式随机挑战」。如果有两个多项式 同为两个次数不超过 的多项式。那么 Verifier 只需要给出一个随机挑战值 ,计算 是否等于 即可大概率得知 ,其中出错的概率 。只要保证 足够大,那么检查出错的概率就可以忽略不计。

这个原理被称为 Schwartz-Zippel 定理。

假如要验证两个向量 是否等于 ,为了可以一步挑战验证,我们要先把三个向量编码成多项式。

一种最直接的方案是把向量当作多项式的「系数」进行编码

显然,如果 ,那么 。然后我们可以通过挑战一个随机数 来检验三个多项式在 处的取值,验证:

如果上式成立,那么

Lagrange 插值 与 Evaluation Form

假如我们要验证 ,用系数编码的方式就不容易处理了,因为 会产生很多的交叉项。并且 的项并不对应到 的系数,比如 的系数出现在 上,但同时 项的系数组成还有 。而 的系数。

我们需要另一种多项式编码方案,利用 Lagrange Basis。如果我们要构造多项式 ,使得它在定义域 上的取值为 ,即

插值需要用到一组插值多项式: ,其中 ,并且 。然后 可以按如下方式编码:

可以简单心算一下,当 时,等式右边除了第一项之外,其他项都等于零,于是 。看起来 像是一个选择器,这组多项式又被称为 Lagrange Polynomials。

我们用同样的方法来编码

如果 成立,那么 。如果 ,那么

我们现在已经把两个向量的按位乘积问题转换到了三个多项式之间的关系,接下来的问题是如何进行随机挑战验证。

我们发现:如果直接让 Verifier 发送随机数 挑战上面的等式,那么 只能属于 。如果只存在一个 使得 ,那么 Verifier 的一次挑战能发现这个错误的概率只有 ,这样 Verifier 需要挑战多次才能缩小检测出错的概率。不过这样不满足我们的要求,我们希望只通过一次挑战来检测出 Prover 的作弊行为。

我们可以把上面的等式的 取值范围去除,换成下面的等式:

这个等式在整个 定义域上都成立。这是为何?

首先我们看等式左边的多项式: ,不妨定义为 。我们可以看到 上等于零,那么意味着 恰好是 的「根集合」。于是 可以按照下面的方式进行因式分解:

换个说法, 可以被多项式 整除,并得到一个商多项式 。零多项式 又被称为 Vanishing Polynomial。

如果我们让 Prover 计算出这个 ,并且发送给 Verifier,又因为 是已知的系统参数,Verifier 可以自行计算 ,那么 Verifier 只需要一次随机检测即可判断 是否在 处等零。

进一步,如果我们使用多项式承诺(Polynomial Commitment),Verifier 可以让 Prover 来帮忙计算这些多项式在 处的取值,发送并证明这些值的正确性,这样能最大限度地减少 Verifier 的工作量。

但是, Verifier 计算 需要 的计算量。

那能否让 Verifier 继续减少工作量?答案是可以的,只要我们选择特殊的

单位根 Roots of Unity

如果我们选择单位根作为 ,那么 的计算量会降为

对于任何有限域 ,其中阶数 为素数。那么去除零之后剩下的元素构成了乘法群 ,阶数为 。由于 一定为偶数,那么 的乘法因子中一定包含若干个 ,假设记为 。那么 一定包含一个阶数为 的乘法子群。不妨设 ,那么一定存在一个阶数为 的乘法子群,记为 。 该乘法子群必然含有一个生成元,记为 ,并且 。这相当于把 次方根,因此被称为单位根。不过单位根不只有一个 ,我们会发现 都满足单位根的特性,即 。那么所有这些由 产生的单位根就组成了乘法子群

这些元素满足一定的对称性:比如 。又比如把所有的单位根求和,我们会得到零:

举一个简单的例子,我们可以在 中找到一个阶数为

其中乘法群的生成元为 。由于 13-1=3*2*2,所以存在一个阶数为 的乘法子群,其生成元为

在实际应用中,我们会选择一个较大的有限域,它能有一个较大的 Powers-of-2 乘法子群。比如椭圆曲线 BN254 的 Scalar Field,含有一个阶数为 的乘法子群,BLS-12-381 的Scalar Field 含有一个阶数为 的乘法子群。

在乘法子群 上,具有下面的性质:

我们可以进行简单的推导,假设 ,由于 的对称性,这个计算过程可以不断化简:

Lagrange Basis

对于 Lagrange 多项式, ,并且 。接下来,我们给出 的构造。

为了构造 ,先构造不等于零的多项式部分。由于 ,因此他一定包含 这个多项式因子。但该因子显然在 处可能不等于 ,即可能 。然后,我们只要让该因子除以这个可能不等于 的值即可,于是 定义如下:

不难发现, 处等于 ,其它位置 处等于

对于任意次数小于 的多项式 ,那么它都可以唯一地表示为:

我们可以用多项式在 上的值 来表示 。这被称为 多项式的求值形式(Evaluation Form),区别于系数形式(Coefficient Form)。

两种形式可以在 上可以通过 (Inverse) Fast Fourier Transform 算法来回转换,计算复杂度为

多项式的约束

利用 Lagrange Basis 我们可以方便地对各种向量计算进行约束。

比如我们想约束 向量的第一个元素为 。那么我们可以对这个向量进行编码,得到 ,并且进行如下约束:

Verifier 可以挑战验证下面的多项式等式:

再比如,我们想约束 向量的第一个元素为 ,最后一个元素为 ,其它元素任意。那么 应该满足下面两个约束。

那么通过 Verifier 给一个随机挑战数( ),上面两个约束可以合并为一个多项式约束:

接下来,Verifier 只要挑战下面的多项式等式即可:

如果想验证 两个等长向量除第一个元素之外,其它元素都相等,那要如何约束呢?假设 为两个向量的多项式编码,那么它们应该满足:

时,左边多项式的第一个因子等于零,而 时,则左边第二因子等于零,即表达了除第一项可以不等之外,其它点取值都必须相等。

可以看出,采用 Lagrange 多项式,我们可以灵活地约束多个向量之间的关系,并且可以把多个约束合并在一起,让 Verifier 仅通过很少的随机挑战就可验证多个向量约束。

Coset

在素数有限域的乘法群中,对于每一个乘法子群 ,都有多个等长的陪集(Coset),这些 Coset 具有和 类似的性质,在 Plonk 中也会用到 Coset 的概念,这里只做部分性质的介绍。

还拿 为例,我们取 ,并且乘法群的生成元 。于是我们可以得到下面两个 Coset:

\begin{split} H_1 &= g\cdot H = (g, g\omega, g\omega^2, g\omega^3) &= (2,10,11,3) \ H_2 &= g^2\cdot H = (g^2, g^2\omega, g^2\omega^2, g^2\omega^3) &= (4,7,9,6) \ \end{split}

可以看到 ,并且它们交集为空,没有任何重叠。并且它们的 Vanishing Polynomial 也可以快速计算:

References

  • Schwartz–Zippel lemma. https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma

理解 PLONK(三):置换证明

Plonkish 电路编码用两个矩阵 描述电路的空白结构,其中 为运算开关, 为置换关系,用来约束 矩阵中的某些位置必须被填入相等的值。本文重点讲解置换证明(Permutation Argument)的原理。

回顾拷贝关系

回顾一下 Plonkish 的 表格,总共有三列,行数按照 对齐。

我们想约束 Prover 在填写 表时,满足下面的拷贝关系: ,换句话说, 位置上的值需要被拷贝到 处,而 位置上的值需要被拷贝到 处, 位置上的值被拷贝到 处。

问题的挑战性在于,Verifier 要仅通过一次随机挑战就能完成 表格中多个拷贝关系的证明,并且在看不到 表格的情况下。

Plonk 的「拷贝约束」是通过「置换证明」(Permutation Argument)来实现,即把表格中需要约束相等的那些值进行循环换位,然后证明换位后的表格和原来的表格完全相等。

简化一下问题:如何证明两个等长向量 满足一个已知的置换 ,并且

举一个例子,假设 ,即他们满足一个「左移循环换位」的置换关系,那么 。如何能证明 ,那么两个向量对应位置的值都应该相等,

那么 ,于是可以得出结论: ,即 中的全部元素都相等。

对于 ,我们只需要针对那些需要相等的位置进行循环换位,然后让 Prover 证明 和经过循环换位后的 表格相等,那么可实现拷贝约束。证明两个表格相等,这个可以通过多项式编码,然后进行概率检验的方式完成。剩下的工作就是如何让 Prover 证明 确实是(诚实地)按照事先约定的方式进行循环移位。

那么接下来就是理解如何让 Prover 证明两个向量之间满足某一个「置换关系」。 置换证明(Permutation Argument)是 Plonk 协议中的核心部分,为了解释它的工作原理,我们先从一个基础协议开始——连乘证明(Grand Product Argument)。

冷启动:Grand Product

假设我们要证明下面的「连乘关系」 :

我们在上一篇文章介绍了如何证明一组「单乘法」,通过多项式编码,把多个单乘法压缩成单次乘法的验证。

这里对付连乘的基本思路是:让 Prover 利用一组单乘的证明来实现多个数的连乘证明,然后再通过多项式的编码,交给 Verifier 进行概率检查。

强调下:思路中的关键点是如何把一个连乘计算转换成多次的单乘计算。

我们需要通过引入一个「辅助向量」,把「连乘」的计算看成是一步步的单乘计算,然后辅助向量表示每次单乘之后的「中间值」:

上面表格表述了连乘过程的计算轨迹(Trace),每一行代表一次单乘,顺序从上往下计算,最后一行计算出最终的结果。

表格的最左列为要进行连乘的向量 ,中间列 为引入的辅助变量,记录每次「单乘之前」的中间值,最右列表示每次「单乘之后」的中间值。

不难发现,「中间列」向量 向上挪一行与「最右列」几乎一致,除了最后一个元素。该向量的第一个元素用了常数 作为计算初始值,「最右列」最后一个向量元素为计算结果。

向量 是一个 Accumulator,即记录连乘计算过程中的每一个中间结果:

那么显然我们可以得到下面的递归式:

于是,表格的三列编码后的多项式也将满足下面三个约束。第一个是初始值为

第二个约束为递归的乘法关系:

第三个约束最后结果

我们可以用一个小技巧来简化上面的三个约束。我们把计算连乘的表格添加一行,令 (注意: 向量的连乘积)

这样一来, 。最右列恰好是 的循环移位。并且上面表格的每一行都满足「乘法关系」!于是,我们可以用下面的多项式约束来表示递归的连乘:

接下来,Verifier 可以挑战下面的多项式等式:

其中 是用来聚合多个多项式约束的随机挑战数。其中 为商多项式,

接下来,通过 Schwartz-Zippel 定理,Verifier 可以给出挑战数 来验证上述多项式等式是否成立。

到此为止,如果我们已经理解了如何证明一个向量元素的连乘,那么接下来的问题是如何利用「连乘证明」来实现「Multiset 等价证明」(Multiset Equality Argument)。

从 Grand Product 到 Multiset 等价

假设有两个向量,其中一个向量是另一个向量的乱序重排,那么如何证明它们在集合意义(注意:集合无序)上的等价呢?最直接的做法是依次枚举其中一个向量中的每个元素,并证明该元素属于另一个向量。但这个方法有个限制,就是无法处理向量中会出现两个相同元素的情况,也即不支持「多重集合」(Multiset)的判等。例如 就属于一个多重集合(Multiset),那么它显然不等于 ,也不等于

另一个直接的想法是将两个向量中的所有元素都连乘起来,然后判断两个向量的连乘值是否相等。但这个方法同样有一个严重的限制,就是向量元素必须都为素数,比如 ,但

修改下这个方法,我们假设向量 为一个多项式 的根集合,即对向量中的任何一个元素 ,都满足 。这个多项式可以定义为:

如果存在另一个多项式 等于 ,那么它们一定具有相同的根集合 。比如

那么

我们可以利用 Schwartz-Zippel 定理来进一步地检验:向 Verifier 索要一个随机数 ,那么 Prover 就可以通过下面的等式证明两个向量 在多重集合意义上等价:

还没结束,我们需要用上一节的连乘证明方案来继续完成验证,即通过构造辅助向量(作为一个累积器),把连乘转换成多个单乘来完成证明。需要注意的是,这里的两个连乘可以合并为一个连乘,即上面的连乘相等可以转换为

到这里,我们已经明白如何证明「Multiset 等价」,下一步我们将完成构造「置换证明」(Permutation Argument),用来实现协议所需的「Copy Constraints」。

从 Multiset 等价到置换证明

Multiset 等价可以被看作是一类特殊的置换证明。即两个向量 存在一个「未知」的置换关系。

而我们需要的是一个支持「已知」的特定置换关系的证明和验证。也就是对一个有序的向量进行一个「公开特定的重新排列」。

先简化下问题,假如我们想让 Prover 证明两个向量满足一个奇偶位互换的置换:

我们仍然采用「多项式编码」的方式把上面两个向量编码为两个多项式, 。思考一下,我们可以用下面的「位置向量」来表示「奇偶互换」:

我们进一步把这个位置向量和 并排放在一起:

接下来,我们要把上表的左边两列,还有右边两列分别「折叠」在一起。换句话说,我们把 视为一个元素,把 视为一个元素,这样上面表格就变成了:

容易看出,如果两个向量 满足 置换,那么,合并后的两个向量 将满足 Multiset 等价关系。

也就是说,通过把向量和位置值合并,就能够把一个「置换证明」转换成一个「多重集合等价证明」,即不用再针对某个特定的「置换关系」进行证明。

这里又出现一个问题,表格的左右两列中的元素为二元组(Pair),二元组无法作为一个「一元多项式」的根集合。

我们再使用一个技巧:再向 Verifier 索取一个随机数 ,把一个元组「折叠」成一个值:

接下来,Prover 可以对 两个向量进行 Multiset 等价证明,从而可以证明它们的置换关系。

完整的置换协议

公共输入:置换关系

秘密输入:两个向量

预处理:Prover 和 Verifier 构造

第一步:Prover 构造并发送

第二步:Verifier 发送挑战数

第三步:Prover 构造辅助向量 ,构造多项式 并发送

第四步:Verifier 发送挑战数

第五步:Prover 构造 ,并发送

第六步:Verifier 向 查询这三个多项式在 处的取值 ,得到 ;向 查询 两个位置处的取值,即 ;向 这两个多项式发送求值查询 ,得到 ;Verifier 自行计算

验证步:Verifier 验证

协议完毕。

References:

  • [WIP] Copy constraint for arbitrary number of wires. https://hackmd.io/CfFCbA0TTJ6X08vHg0-9_g
  • Alin Tomescu. Feist-Khovratovich technique for computing KZG proofs fast. https://alinush.github.io/2021/06/17/Feist-Khovratovich-technique-for-computing-KZG-proofs-fast.html#fn:FK20
  • Ariel Gabizon. Multiset checks in PLONK and Plookup. https://hackmd.io/@arielg/ByFgSDA7D

理解 PLONK(四):算术约束与拷贝约束

回顾置换证明

上一节,我们讨论了如何让 Prover 证明两个长度为 的向量 满足一个实现约定(公开)的置换关系 ,即

基本思路是向 Verifier 要一个随机数 ,把两个「原始向量」和他们的「位置向量」进行合体,产生出两个新的向量,记为

第二步是再向 Verifier 要一个随机数 ,通过连乘的方法来编码 的 Multiset,记为

第三步是让 Prover 证明 ,即

证明这个连乘,需要引入一个辅助向量 ,记录每次乘法运算的中间结果:

由于 ,而且 ,因此我们可以用 来编码 ,从而把置换证明转换成关于 的关系证明。

最后 Verifier 发送挑战数 ,得到 然后检查它们之间的关系。

向量的拷贝约束

所谓拷贝约束 Copy Constraints,是说在一个向量中,我们希望能证明多个不同位置上的向量元素相等。我们先从一个简单例子开始:

假设为了让 Prover 证明 ,我们可以把 对调位置,这样形成一个「置换关系」,如果我们用 记录被置换向量的元素位置,那么我们把置换后的位置向量记为 ,而 为表示按照 置换后的向量

显然,只要 Prover 可以证明置换前后的两个向量相等, ,那么我们就可以得出结论:

这个方法可以推广到证明一个向量中有多个元素相等。比如要证明 中的前三个元素都相等,我们只需要构造一个置换,即针对这三个元素的循环右移:

那么根据 容易得出

多个向量间的拷贝约束

对于 Plonk 协议,拷贝约束需要横跨 表格的所有列,而协议要求 Prover 要针对每一列向量进行多项式编码。我们需要对置换证明进行扩展,从而支持横跨多个向量的元素等价。

回忆比如针对上面电路的 表格:

看上面的表格,我们要求

支持跨向量置换的直接方案是引入多个对应的置换向量,比如上表的三列向量用三个置换向量统一进行位置编码:

置换后的向量为

Prover 用一个随机数 (Verifier 提供)来合并 ,还有置换后的向量: 。然后再通过一个随机数 (Verifier 提供)和连乘来得到 的 Multisets,

又因为拷贝约束要求置换后的向量与原始向量相等,因此

如果我们用多项式对 编码,得到 ,于是 满足下面的约束关系:

如果两个 Multiset 相等 {f_i\}={g_i},那么下面的等式成立:

上面的等式稍加变形,可得

我们进一步构造一个辅助的累加器向量 ,表示连乘计算的一系列中间过程

其中 的初始值为 ,Prover 按照下表计算出

如果 能与 连乘等价的话,那么最后一行 正好等于 ,即

而又因为 。这恰好使我们可以把 完整地编码在乘法子群 上。因此如果它满足下面两个多项式约束,我们就能根据数学归纳法得出 ,这是我们最终想要的「拷贝约束」:

置换关系

在构造拷贝约束前,置换关系 需要提前公开共识。表格 含有所有算术门的输入输出,但是并没有描述门和门之间是否通过引线相连,而置换关系 实际上正是补充描述了哪些算术门之间的连接关系。

因此,对于一个处于「空白态」的电路,通过 两个表格描述,其中 由选择子向量构成,而 则由「置换向量」构成。

下面是 表格

下面是 表格,描述了哪些位置做了置换

处理 Public Inputs

假如在上面给出的小电路中,要证明存在一个 Assignment,使得 out 的输入为一个特定的公开值,比如 。最简单的办法是使用 表中的 列,并增加一行约束,使得 ,因此满足下面等式

但这个方案的问题是:这些公开值输入输出值被固定成了常数,如果公开值变化,那么 多项式需要重新计算。如果整体上 表格的行数比较大,那么这个重新计算过程会带来很多的性能损失。

能否在表格中引入参数,以区分电路中的常数列?并且要求参数的变化并不影响其它电路的部分?这就需要再引入一个新的列,专门存放公开参数,记为 ,因此,算术约束会变为:

我们还可以通过修改拷贝约束的方式引入公开参数。

[!TODO]

位置向量的优化

我们上面在构造三个 向量时,直接采用的自然数 ,这样在协议开始前,Verifier 需要构造 3 个多项式 ,并且在协议最后一步查询 Oracle,获得三个多项式在挑战点 处的取值

思考一下, 向量只需要用一些互不相等的值来标记置换即可,不一定要采用递增的自然数。如果我们采用 的话,那么多项式 会被大大简化:

其中 为互相不等的二次非剩余。

这样一来,这三个多项式被大大简化,它们在 处的计算轻而易举,可以直接由 Verifier 完成。

这个小优化手段最早由 Vitalik 提出。采用 是为了产生 的陪集(Coset),并保证 Coset 之间没有任何交集。我们前面提到 的乘法子群,如果 存在交集,那么 。这个论断可以简单证明如下:如果它们存在交集,那么 ,于是 ,又因为 ,那么 ,那么 ,那么 ,同理可得 ,于是

如果 的列数更多,那么我们需要选择多个 来产生不相交的 Coset。一种最直接的办法是采用 ,其中 为乘法子群 的生成元,

协议框架

预处理:Prover 和 Verifier 构造

第一步:Prover 针对 表格的每一列,构造 使得

第二步: Verifier 发送随机数

第三步:Prover 构造 ,使得

第四步:Verifier 发送随机挑战数

第五步:Prover 计算 ,并构造商多项式

其中

其中商多项式

第六步:Verifier 发送随机挑战数 ,查询上述的所有 Oracle,得到

Verifier 还要自行计算

验证步:

参考文献

理解 Plonk(五):多项式承诺

什么是多项式承诺

所谓承诺,是对消息「锁定」,得到一个锁定值。这个值被称为对象的「承诺」。

这个值和原对象存在两个关系,即 Hiding 与 Binding。

Hiding: 不暴露任何关于 的信息;

Binding:难以找到一个 ,使得

最简单的承诺操作就是 Hash 运算。请注意这里的 Hash 运算需要具备密码学安全强度,比如 SHA256, Keccak 等。除了 Hash 算法之外,还有 Pedersen 承诺等。

顾名思义,多项式承诺可以理解为「多项式」的「承诺」。如果我们把一个多项式表达成如下的公式,

那么我们可以用所有系数构成的向量来唯一标识多项式

如何对一个多项式进行承诺?很容易能想到,我们可以把「系数向量」进行 Hash 运算,得到一个数值,就能建立与这个多项式之间唯一的绑定关系。

或者,我们也可以使用 Petersen 承诺,通过一组随机选择的基,来计算一个 ECC 点:

如果在 Prover 承诺多项式之后,Verifier 可以根据这个承诺,对被锁定的多项式进行求值,并希望 Prover 可以证明求值的正确性。假设 ,Verifier 可以向提供承诺的 Prover 询问多项式在 处的取值。Prover 除了回复一个计算结果之外(如 ) ,还能提供一个证明 ,证明 所对应的多项式 处的取值 的正确性。

多项式承诺的这个「携带证明的求值」特性非常有用,它可以被看成是一种轻量级的「可验证计算」。即 Verifier 需要把多项式 的运算代理给一个远程的机器(Prover),然后验证计算(计算量要小于直接计算 )结果 的正确性;多项式承诺还能用来证明秘密数据(来自Prover)的性质,比如满足某个多项式,Prover 可以在不泄漏隐私的情况下向 Verifier 证明这个性质。

虽然这种可验证计算只是局限在多项式运算上,而非通用计算。但通用计算可以通过各种方式转换成多项式计算,从而依托多项式承诺来最终实现通用的可验证计算。

按上面 的方式对多项式的系数进行 Pedersen 承诺,我们仍然可以利用 Bulletproof-IPA 协议来实现求值证明,进而实现另一种多项式承诺方案。此外,还有 KZG10 方案,FRI,Dark,Dory 等等其它方案。

KZG10 构造

与 Pedersen 承诺中用的随机基向量相比,KZG10 多项式承诺需要用一组具有内部代数结构的基向量来代替。

请注意,这里的 是一个可信第三方提供的随机数,也被称为 Trapdoor,需要在第三方完成 Setup 后被彻底删除。它既不能让 Verifier 知道,也不能让 Prover 知道。当 设置好之后, 被埋入了基向量中。这样一来,从外部看,这组基向量与随机基向量难以被区分。其中 ,而 ,并且存在双线性映射

对于一个多项式 进行 KZG10 承诺,也是对其系数向量进行承诺:

这样承诺 巧好等于

对于双线性群,我们下面使用 Groth 发明的符号 表示两个群上的生成元,这样 KZG10 的系统参数(也被称为 SRS, Structured Reference String)可以表示如下:

下面构造一个 的 Open 证明。根据多项式余数定理,我们可以得到下面的等式:

这个等式可以解释为,任何一个多项式都可以除以另一个多项式,得到一个商多项式加上一个余数多项式。由于多项式在 处的取值为 ,那么我们可以确定:余数多项式一定为 ,因为等式右边的第一项在 处取值为零。所以,如果 ,我们可以断定: 处等零,所以 的根,于是 一定可以被 这个不可约多项式整除,即一定存在一个商多项式 ,满足上述等式。

而 Prover 则可以提供 多项式的承诺,记为 ,作为 的证明,Verifier 可以检查 是否满足整除性来验证证明。因为如果 ,那么 则无法被 整除,即使 Prover 提供的承诺将无法通过整除性检查:

承诺 是群 上的一个元素,通过承诺的加法同态映射关系,以及双线性映射关系 ,Verifier 可以在 上验证整除性关系:

有时为了减少 Verifier 在 上的昂贵操作,上面的验证等式可以变形为:

同点 Open 的证明聚合

在一个更大的安全协议中,假如同时使用多个多项式承诺,那么他们的 Open 操作可以合并在一起完成。即把多个多项式先合并成一个更大的多项式,然后仅通过 Open 一点,来完成对原始多项式的批量验证。

假设我们有多个多项式, ,Prover 要同时向 Verifier 证明 ,那么有

通过一个随机数 ,Prover 可以把两个多项式 折叠在一起,得到一个临时的多项式

进而我们可以根据多项式余数定理,推导验证下面的等式:

我们把等号右边的第二项看作为「商多项式」,记为

假如 处的求值证明为 ,而 处的求值证明为 ,那么根据群加法的同态性,Prover 可以得到商多项式 的承诺:

因此,只要 Verifier 发给 Prover 一个额外的随机数 ,双方就可以把两个(甚至多个)多项式承诺折叠成一个多项式承诺

并用这个折叠后的 来验证多个多项式在一个点处的运算取值:

从而把多个求值证明相应地折叠成一个,Verifier 可以一次验证完毕:

由于引入了随机数 ,因此多项式的合并不会影响承诺的绑定关系(Schwartz-Zippel 定理)。

协议:

公共输入:

私有输入:

证明目标:

第一轮:Verifier 提出挑战数

第二轮:Prover 计算 ,并发送

第三轮:Verifier 计算

多项式约束与线性化

假设 分别是 的 KZG10 承诺,如果 Verifier 要验证下面的多项式约束:

那么 Verifier 只需要把前两者的承诺相加,然后判断是否等于 即可

如果 Verifier 需要验证的多项式关系涉及到乘法,比如:

最直接的方法是利用双线性群的特性,在 上检查乘法关系,即验证下面的等式:

但是如果 Verifier 只有 上的承诺 ,而非是在 上的承诺 ,那么Verifer 就无法利用双线性配对操作来完成乘法检验。

另一个直接的方案是把三个多项式在同一个挑战点 上打开,然后验证打开值之间的关系是否满足乘法约束:

同时 Prover 还要提供三个多项式求值的证明 供 Verifier 验证。

这个方案的优势在于多项式的约束关系可以更加复杂和灵活,比如验证下面的稍微复杂些的多项式约束:

假设 Verifier 已拥有这些多项式的 KZG10 承诺, 。最直接粗暴的方案是让 Prover 在挑战点 处打开这 6 个承诺,发送 6 个 Open 值和对应的求值证明:

Verifier 验证 个求值证明,并且验证多项式约束:

我们可以进一步优化,比如考虑对于 这样一个简单的多项式约束,Prover 可以减少 Open 的数量。比如 Prover 先 Open ,发送求值证明 然后引入一个辅助多项式 ,再 Open 处的取值。

显然对于一个诚实的 Prover, 求值应该等于零。对于 Verifier,它在收到 之后,就可以利用承诺的加法同态性,直接构造 的承诺:

这样一来,Verifier 就不需要单独让 Prover 发送 的 Opening,也不需要发送新多项式 的承诺。Verifier 然后就可以验证 这个多项式约束关系:

这个优化过后的方案,Prover 只需要 Open 两次。第一个 Opening 为 ,第二个 Opening 为 。而后者是个常数,不需要发送给 Verifier。Prover 只需要发送两个求值证明,不过我们仍然可以用上一节提供的聚合证明的方法,通过一个挑战数 ,Prover 可以聚合两个多项式承诺,然后仅需要发送一个求值证明。

我们下面尝试优化下 个多项式的约束关系的协议:

协议:

公共输入:

私有输入:

证明目标:

第一轮:Verifier 发送

第二轮:Prover 计算并发送三个Opening,

第三轮:Verifier 发送 随机数

第四轮:Prover 计算 ,利用 折叠 这四个承诺,并计算商多项式 ,发送其承诺 作为折叠后的多项式在 处的求值证明

第五轮:Verifier 计算辅助多项式 的承诺

计算折叠后的多项式的承诺:

计算折叠后的多项式在 处的求值:

检查下面的验证等式:

这个优化后的协议,Prover 仅需要发送三个 Opening,一个求值证明;相比原始方案的 6 个 Opening和 6 个求值证明,大大减小了通信量(即证明大小)。

Reference

理解 Plonk(六):实现 Zero Knowledge

在前文的 Plonk 协议中,所有的多项式承诺都没有混入额外的随机数进行保护,因此当一个未被随机化的多 项式承诺 经过一次或者多次 Open,会泄露 自身的信息,这会限制协议在需要隐私保护的 场景中应用。

考虑一个 次多项式 ,只要它在四个不同的点上 Open ,多项式就可以通过 Lagrange 插值来复原。 然而即使一个次数超过一百万的多项式,哪怕被打开一次也会泄漏关于原多项式的部分信息。

为了实现 Zero Knowledge 性质的 Plonk,我们需要在多项式中加入足够多的随机因子,确保在多项式 打开 次之后,仍然不会泄漏原多项式的信息,保证没有知识泄漏。

Plonk 协议的大致流程为:Prover 构造多项式,然后发送多项式的承诺给 Verifier。然后 Verfier 挑战两个随机挑战点 ,其中 为 子群 的生成元。下面是 Prover 需要构造的多项式列表:

  • Witness 多项式:
  • 置换累乘多项式:
  • 商多项式:

其中三个 Witness 多项式要在 这一个点处打开,置换累乘多项式 要在 两个点处打开,而三个商多项式则不需要被打开。

Prover 要混入两类随机因子,第一类是保护承诺本身,满足信息隐藏 Hiding,一个承诺一般只需要混入一个随机数即可; 第二类是保护多项式承诺在打开之后仍然保证原多项式信息不会泄漏。如果多项式打开的次数越多(假设每次打开的位置都不同), Prover 就要混入越多的随机因子。

第一类的随机因子,也可以用多项式承诺方案来实现,比如 Bulletproof-IPA,或者 KZG10-with-Hiding,这些多项式承诺方案本身已经支持 Hiding 。如果 Plonk 后端采用的是朴素的 KZG10,那么就需要在 Plonk 协议层面增加足够的随机因子,不仅保证承诺自身的 Hiding 性质,还要保护承诺的打开。

下面我们介绍两个不同的混入随机因子方案实现 Zero Knowledge 的方法。第一个方法比较经典,是为多项式加上一个盲化(Blinding)用途的多项式,GWC19 论文[3](或其它学术论文)中正是采用的这种方法。而第二个方法是在向量的对齐填充空间里面填入随机数,再插值产生多项式的,这是工程实现中的常见方法。

方法一:Blinding 多项式

我们先看 Witness 多项式 ,它是由下面的等式计算:

我们假设 ,其中

在 Plonk 协议中,Prover 需要计算 的取值,其中 为 Verifier 给出的随机挑战点。

如果我们直接鲁莽地在 中混入随机数 ,比如 ,那么 可能就不再满足算术约束:

而且也无法满足置换约束。

如果要让随机化后的多项式 满足「算术约束」和「置换约束」,那么我们可以考虑在乘法子群 之外增加一些随机的点,这样可以让随机化后的多项式 整个乘法子群上的取值仍然与 完全相等,但是整个多项式却已经被随机化了。所谓的在 上的取值相等,就是保证随机化后的多项式仍然可以被 整除。下面是随机化多项式的构造:

这里 为 Blinding 多项式,包含两个随机因子 ,它们恰好是自变量的不同次数的系数,这样可以保证线性不相关。换个方式理解,只有对这个 Blinding 多项式打开两次以上,才可以计算出所有的随机因子。如果只打开一次,Blinding 多项式会被消耗掉一个随机因子,还剩下一个起作用的随机因子。

简单检查下,我们可以发现新定义的 符合要求,能满足算术约束。同时因为 ,因此 也一定满足置换关系。

这里 被混入了两个随机因子,其中一个随机因子可以保护 被打开一次,另一个随机因子用来实现承诺 本身的信息隐藏。

考虑下置换累乘多项式 ,假如多项式承诺 被打开两次的话,那么就需要混入三个随机因子,构造一个次数为 的 Blinder 多项式, ,然后混入到 中:

最后考虑商多项式 ,由于他们不需要在任何点打开,因此只要加上随机因子即可,不过这几个商多项式有额外的要求,即他们三个需要一起能拼出真正的商多项式

我们可以采用下面的方式,为每一个多项式分片混入一个随机因子,并且保证他们拼起来之后仍然等于

容易检验:

同理,如果 的次数达到了 ,那么就需要三个随机数给四个 分段加上随机数,实现 Hiding。

这个方法存在一个问题,就是 Blinding 多项式的次数会超过 ,这里 。因为 的次数为 ,因此 次数为 。如果 Plonk 后端采用的是 Bulletproof-IPA 这类的多项式承诺,承诺会要求多项式的次数按 对齐,这样盲化之后的多项式的次数刚刚超出 ,只能对齐到 。一些 Plonk 变种协议可能会把 Witness table 的列数增加,稍稍超出的多项式次数会使 的计算在一个更大的子群上完成。

方法二:随机因子对齐

下面介绍的第二种方法不会推高多项式的次数。考虑到 子群的大小 是按 对齐,在实际电路中,一般情况下需要把 Witness Table 的长度对齐到 ,为了对齐,需要把空余的空间用零填满。

那么这里可以用随机数来代替零填充对齐空间,好处是这些随机数可以保护表中的其它正常数据。

Daniel Lubarov 按照这个思路给出了第二种随机数填充实现 Zero-Knowledge 性质的办法[1]。

对于商多项式,因为方法一不会推高他们的次数,因此我们下面只考虑剩下的两类多项式:

  • Witness 多项式:
  • 置换累乘多项式:

先看第一类多项式,以 为例,它编码了 向量。如果本身向量长度不足 ,一般情况下是用零补齐,我们现在可以考虑让 Prover 额外用两个随机数补齐,这样做的效果和方法一的 Blinding 多项式完全一样。 如下所示:

其中 也可以看成是利用 Lagrange Basis 产生的 Blinding 多项式。这里假设 的长度为 为两个随机数。假设 的系数为固定值,那么当 被打开两次之后, 的系数即可被求解,从而失去随机化的能力。因此, 只能承受一次安全的打开操作(假设协议基于 Non-hiding 的多项式承诺)。

对于置换累乘多项式 ,则需要在累乘向量 的尾部引入随机值。考虑下 的计算方式:

列出所有的 的计算如下:

假如我们想设置 为随机值,我们需要让 这两个元素设置一个 Copy Constraint,并填上同一个随机数 。如果 设置为零,那么

又因为

那么 的概率分布与 相同。这样我们通过把 Witness Table 的最后两行用来填入随机数 ,并且设置一个 Copy Constraint 来随机化 。如果要再引入一个随机数 ,一种方法是我们再征用 Witness table 的两行, ,可以让 随机化。或者我们节省下空间,利用 来构造一个随机数 的 Copy Constraint。同理,我们可以再用两行 来引入 。 这样,我们总共征用了四行,引入了三个随机数

最后我们推导一下 ,请注意 ,因为前面的 Permutation 项都已经消完。

于是 中各自包含了一个随机数。请注意这个方法需要在 Witness table 中留有足够的 padding 空间,并且 的盲化因子不能与 的重复,那么总共需要留出 6 排空间,并且把 盲化因子提前到第 排:

满足 Hiding 性质的 KZG10

在 Daniel Lubarov 的 Blog 中讲述的方案是基于带有 Hiding 性质的多项式承诺 IPA(Inner product argument)。因此在 中只需要混入一个随机因子, 中只混入两个随机因子。

但是我们也可以选择一个带有 Hiding 性质的 KZG10 承诺方案,这样也可以按照 Halo2 方式混入较少的随机数实现 Zero-knowledge。

这个方案参考了 Marlin 论文[2]的 Appendix B.3,基于 AGM 模型的 KZG10-with-hiding。

在 Setup 阶段,我们需要产生两倍长的 srs:

如果我们要承诺一个多项式 ,那么需要额外产生一个次数相同的 Blinder 多项式:

然后计算承诺:

如果我们要在 处打开一个多项式承诺,先计算 ,还要计算盲化多项式 的求值, ,然后产生这两个多项式的求值证明:

检查求值证明的方式如下:

我们可以看到为了实现 Hiding,计算承诺和打开承诺的成本会加倍。如果我们限定多项式只能被打开一次(或者有限次),那么我们可以采用更低次数的盲化多项式 。假如我们只考虑多项式最多被打开一次的情况,那么 只需要是一个一次多项式,同时也可以减少 srs 的尺寸。

最后请注意的是,仅有实现 Hiding 的多项式承诺不足以实现 Plonk 的 Zero-knowledge,仍然需要在 Plonk 协议层面混入足够的随机的盲化因子。

参考文献

  • [1] Adding zero knowledge to Plonk-Halo https://mirprotocol.org/blog/Adding-zero-knowledge-to-Plonk-Halo
  • [2] Chiesa, Alessandro, Yuncong Hu, Mary Maller, Pratyush Mishra, Noah Vesely, and Nicholas Ward. “Marlin: Preprocessing zkSNARKs with universal and updatable SRS.” In Advances in Cryptology–EUROCRYPT 2020: 39th Annual International Conference on the Theory and Applications of Cryptographic Techniques, Zagreb, Croatia, May 10–14, 2020, Proceedings, Part I 39, pp. 738-768. Springer International Publishing, 2020. https://eprint.iacr.org/2019/1047.
  • [3] Gabizon, Ariel, Zachary J. Williamson, and Oana Ciobotaru. “Plonk: Permutations over lagrange-bases for oecumenical noninteractive arguments of knowledge.” Cryptology ePrint Archive (2019).

理解 PLONK(七):Lookup Gate

传统上我们通过编写算术电路来表达逻辑或者计算。而算术电路只有两种基本门:「加法门」与「乘法门」。当然通过组合,我们可以基于加法和乘法构造复杂一点的元件(Gadget)来复用,但是在电路处理过程中,这些 Gadget 还是会被展开成加法门和乘法门的组合。

自然我们想问:能否使用除加法和乘法之外的「新计算门」?

Plonk 相关的工作给出了一个令人兴奋的扩展:我们有能力构造出更复杂些的基本计算单元。如果一个计算的输入和输出满足一个预先设定的多项式的话,那么这个计算可以作为基本计算单元,这个改进被称为 「Custom Gate」,实际上你可以理解为这是一种多输入的「多项式门」。

故事还没有结束,论文 GW20 又给出了一个制造「Lookup Gate」的方法。这个门的输入输出没有必要局限于多项式关系,而是可以表达「任意的预定义关系」。What? 任意的关系?是的,你没听错,尽管这有点令人难以置信。

思路不难理解:如果我们在电路之外预设一个表格,表中每一行表示特定计算的输入输出关系,例如:

in1in2in3out
1234
5678
1159

这个表格就代表一个 Lookup 门的定义。如果你问我这个门究竟表达了什么计算,我无法回答(乱写的)。不过只要能给出这样一张表格,我们就可以在电路里面接入一个门,它的输入输出关系「存在于表中的某一行」。

这种门被称为 Lookup Gate,即查表门(或查表约束)。

如果当我们在 Plonk 电路中接入查表门,那么 Plonk 协议就要检查这个门的输入输出是否合法,然后就会去查我们实现预设的表格,看看其输入输出关系是否能在表中找到对应的一行。如果表中存在这样的条目,那么这个门就合法,否则被视为非法。

在现实应用中,最多采用查表方式的门是关于位运算。如一个 8-bit 异或运算,只需要 大小的表格即可。此外对于采用大量位运算的 SHA256算法,也可以通过建立一个 Spread Table 来大大加速各种位运算的效率。

基本思路

实现查表门的一个关键技术是 Lookup Argument 协议,即如何证明一条(或多条)记录是否存在于一个公开的表中。

可能有朋友会条件反射想到 Merkle Tree,如果我们把表格按行计算 hash,这些 hash 就可以产生一个 Merkle Root,然后通过 Merkle Path 就能证明一条记录是否存在表格中。但是这个方法(以及所有的 Vector Commitment 方案)不适合查表场景。原因有两个,一是这种方案会暴露记录在表格中的位置。假如 Prover 想隐藏记录的信息,即在查询证明不暴露位置,那么仅 Merkle Tree 就难以胜任了。理论点说,这里我们需要 Set-Membership Argument,而非 Vector-Membership Argument。第二个原因:如果有大量的记录条目(比如条目数量为 )需要查表,那么所产生的证明即 Merkle Path,可能会比较大,最坏情况是

简而言之,我们需要一种新的,并且高效的查表协议。本文介绍两个常见的查表协议,为了简化表述,我们先只考虑单列表格的查询,然后再扩展到多列表格的情况。

Halo2-lookup 方案

基于 Permutation Argument,Halo2 给出了一个简洁易懂的 Lookup Argument 方案。

假如我们有一个表格向量 ,表格中不存在相同元素。然后有一个查询向量 ,我们接下来要证明 ,请注意 中会有重复元素。

我们引入一个关键的辅助向量 ,它是 的一个重新排序(置换),使得 中的所有查询记录都按照 的顺序进行排序,比如 ,那么重排后,

可以看出, 中的重复元素被放在了一起,并且整体上按照 中元素出现的顺序。我们把 中连续重复元素的第一个标记出来:

我们再引入一个辅助向量 ,它是对 的重新排序,使得 中被标记元素可以正好对应到 中相同位置上的元素:

请注意看 ,其中被方框标记的元素和 中相同位置的方框元素值完全相同,未被标记的元素则没有出现在 中。

于是我们可以找出一个规律: 中的每一个未标记元素等于它左边的相邻元素,而每一个被标记元素等于 同位置元素,即 或者

将两个向量 与重排向量 通过 Lagrange Basis 进行多项式编码,我们得到 , , ,他们会满足下面的等式:

但上面这个等式不足以约束重排向量的可靠性。考虑如果 ,也会满足上面的等式,但是 并不是合法的查询记录。因此,我们还要加入一条约束防止出现 上 循环回卷导致的漏洞:要求 两个向量的第一个元素必须相同, 即 ,用多项式约束表达如下:

剩下的工作是证明 满足某一个「置换」关系,且 也满足某个「置换」关系。由于,这两个置换关系只不需要约束具体的置换向量,因此我们可以直接采用 Grand Product Argument 来约束这两个置换关系:

下面重新整理下这个协议

协议框架

公共输入:表格向量

秘密输入:查询向量

预处理:Prover 和 Verifier 构造

第一步:Prover 构造多项式并发送承诺

第二步:Verifier 发送挑战数

第三步:Prover 构造多项式并发送承诺

第四步:Verififer 发送挑战数

第五步:Prover 计算并发送商多项式

第六步:Verifier 发送挑战数

第七步:Prover 发送 ,并附带上 evaluation proofs(略去)

第八步:Verifier 验证(注意这里为了简化,去掉了KZG10的聚合优化和线性化优化)

Plookup 方案

然后我们再看看论文 GW20 给出的方案 —— Plookup。与 Halo2-lookup 相比,Plookup 可以省去 向量。

重申一下 Plookup 证明的场景:Verifier 已知表格 向量,Prover 拥有一个秘密的查询向量 ,Prover 要证明 中的每一个元素都在 中,即

方案 Plookup 只需要引入一个辅助向量 ,它被定义为 上的重排,且向量元素的排列遵照 中各个元素出现的顺序。

举例说明,假设 ,如果 ,那么 。可以看到,和 Halo2-lookup 中的 一样, 中相等的元素被排在了一起。

如果向量 满足 ,并且 ,那么就可以证明

第一个关键点是因为 中的查询记录是任意的,查询顺序并没有遵守 中的元素顺序。而通过辅助向量 ,我们就可以把 的查询记录进行重新排序,这有利于排查 中元素的合法性,确保每一个 都出现在 中。但如何保证由 Prover 构造的 是按照 的元素顺序进行排序的?Plookup 用了一个直接但巧妙的方法,考虑把 中的每一个元素和他相邻下一个元素绑在一起,然后可以构成一个新的 Multiset;同样,我们把 中的每一个元素与相邻下一个元素组成一个元组,并构成一个 Multiset;我们还要把 中的每一个元素和它自身构成一个二元组 Multiset。我们用 来表示这三个新的 Multiset,并证明它们满足一定的关系,从而保证 排序的正确性。

这个方法与 Permutation Argument 的基本思想非常类似。回忆下,我们在 Permutation Argument 中,利用了 绑定元素和其位置的「二元组」的 Multiset 来保证任一个 都会出现在位置 上;通过与另一个二元组 Multiset 的相等,可以证明 满足置换函数 。比如下面这个置换函数为奇偶互换的例子:

假设两个向量 ,如果它们满足上面的 Multiset 相等关系,我们可以知 ,满足奇偶互换的关系。

另一个关键点是如何保证 中的元素都在 中出现?这个问题被归结到一个新问题,即 中那些相邻的重复元素一定来自于 ,假如 中有 个重复元素,那么我们可以要求其中第一个来自于 ,剩下的 个元素来自于 。如果 中一旦出现了一个不在 中的元素(假设为 ),那么因为 的重排,那么 中一定会出现 (假设 ),这时在 中一定会出现 这样两个元素,它们无法出现在 这个 Multiset中,也不会出现在 中。

举几个例子,假设 的长度为 , 如果 ,那么 向量在各个位置上都相等。

假设增加一条查询记录,即 ,那么 ,这时候 只有唯一的表达,

假设 为不出现在 中的元素,那么 一定没有办法塞入到 S 中,因为在 中,和 相邻的元素 。因此

假设 ,那么 也只有唯一的表达, ,同样可以检验:

更形式化一些,我们可以用数学归纳法推导:先从 为空开始推理, 。这样我们只要检查 满足 Multiset 意义上的相等,就可以满足 ,且

现在看归纳步,假设 ,如果我们在 中添加一个新元素 ,且 ,那么在 中会比 额外多一个元素 。因为 ,那么重排向量 中一定包含了相邻的两个 ,其中一个来自 ,另一个来自于 。因此,我们可以得出结论:

另一种情况, 假设 ,如果我们在 添加的新元素 ,即是一条违法查询,假设为 。那么 中存在与 相邻的两个元素, ,即 。它们构成了 中的两个异类元素 ,导致

到此为止,我们已经可以确信,通过验证 相等就可以判定 是正确的重排,并且 中的每一个元素都出现在 中。接下来我们把这个问题转换成多项式之间的约束关系。

首先 Prover 借助 Verifier 提供的挑战数 ,把 中的每一个二元组元素进行「折叠」,转换成单值。这样新约束等式为:

然后 Prover 再借助 Verifier 提供的一个挑战数 ,把上面的 Multiset Equality Argument 归结到 Grand Product Argument:

不过这里请注意的是,在 Plookup 论文方案中,并没有采用上面的证明转换形式。而是调换了 的使用顺序:

归结后的 Grand Product 约束等式为:

注:个人认为,上述两种证明转换形式没有本质上的区别。为了方便理解论文,我们后文遵从 Plookup 原论文的方式。

接下来,我们要对向量进行多项式编码,但是这里会遇到一个新问题。即 多项式的次数会超出 的次数或 的次数,特别当 的长度接近或者等于 的大小, 的次数可能超出 的大小。Plookup 的解决方式是将 拆成两半, ,但是 的最后一个元素要等于 的第一个元素:

这样做的目的是,确保能在两个向量中描述 中相邻两个元素的绑定关系。比如 ,那么 ,而 ,可以看出他们头尾相接。

这样一来, 的长度最长也只能是 ,但如果 要按照 对齐,那么 的长度就不够了(无法在长度为 的乘法子群上编码成多项式)。为了解决这个问题,Plookup 选择把 的有效长度限制在 ,所谓有效长度是指, 的实际长度为 ,但是其最后一条查询记录并不考虑其合法性。

于是 向量可以拆成两个长度为 的向量,其中一半 ,另一半

接下来 Prover 要引入 Accumulator 辅助向量 来证明 Grand Product:

我们仍然看下这样一个例子: ,于是 ,拆成两个头尾相接的向量: 。那么,我们可以把相邻元素构成的二元组向量写出来:

\begin{split} F &= (f_i, f_i) & = & {(2,2), (4,4), (4,4)}\ T &=(t_i, t_i) & = & {(1,2), (2,3), (3,4)}\ S^{lo} &= (s^{lo}_i, s^{lo}_i) & = & {(1,2), (2,2), (2,3)}\ S^{hi} &= (s^{hi}_i, s^{hi}_i) & = & {(3,4), (4,4), (4,4)}\ \end{split}

容易检验,他们满足下面的关系:

于是,利用一个辅助函数 ,我们定义

进行编码,我们可以得到 多项式,它应该满足下面三条约束:

此外,根据 的递推关系, 还要满足下面的约束:

总共有四条多项式约束,这里略去完整的协议。

Plonkup 的优化

在论文 Plonkup 论文中给出了一个简化方法,可以去除一个多项式约束。在 Plookup 方案中, 向量被拆分成两个向量, ,但要要求这两个向量头尾相接。

Plonkup 给出了一种新的拆分方案,即按照 的奇偶项进行拆分,拆成

注意,这里不再需要限制 的长度为 ,而是可以到 ,这样 的长度可以到 ,拆分成两个长度为 的向量,之所以可以去除这个限制,是因为 之间的关系可以在 回卷到起始位置,这样只需要要求 即可。 向量可以重新定义为:

我们可以举一个简单的例子:假设 ,于是

容易检验,他们满足下面的关系:

我们也可以通过定义 ,并仔细检查每一项,确认只需要约束 就可以约束 的正确性。

这里辅助函数

于是多项式 只需要满足如下两条约束:

还有

多列表格与多表格扩展

通常查询表是一个多列的表,比如一个 8bit-XOR 计算表是一个三列的表。对于 Plookup 方案与 Halo2-lookup 方案,我们直接可以通过随机挑战数来把一个多列表格折叠成一个单列表格。

假如计算表格为 ,那么相应的查询记录也应该是个三列的表格,记为 。如果 ,对所有的 都成立,那么 是一个合法的查询记录。 通过向 Verifier 要一个随机挑战数 ,我们可以把计算表格横向折叠起来:

同样,Prover 在证明过程中,也将查询记录横向折叠起来:

接下来,Prover 和 Verifier 可以利用单列表格查询协议( Plookup 协议或 Halo2-lookup 协议)完成证明过程。

如果存在多张不同的表格,那么可以给这些表格增加公开的一列,用来标记表格编号,这样可以把多表格视为增加一列的多列的单一表格。

与 Plonk 协议的整合

由于计算表格 是一个预定义的多列表格,因此它可以在 Preprocessing 阶段进行承诺计算,并把这些表格的承诺作为后续协议交互的公开输入。

在 Plonk 协议中,因为我们把表格的查询视为一种特殊的门,因此查询记录 本质上正是 的折叠。为了区分「查询门」和「算术门」,我们还需要增加一个选择向量 ,标记 Witness table 中的某一行是算术门,还是查询门。

下面我们按照 Plonkup 论文中的协议,大概描述下如何将 Lookup Argument 整合进 Plonk 协议。

预处理:Prover 和 Verifier 构造

第一步:Prover 针对 表格的每一列,构造 使得

第二步:Verifier 发送随机数 ,用以折叠表格

第三步:Prover 构造并发送 ,分别编码 ,其中 计算如下

这里请注意,当 时,表示这一行约束不是查询门,因此需要填充上一个存在 中的值,这里我们取表格的最后一个元素作为查询记录填充。

Prover 计算 ,并拆分为 ,构造并发送

第四步: Verifier 发送随机数

第五步:Prover 构造(并发送)拷贝约束累乘多项式 ,使得

其中

Prover 构造(并发送)查询累乘多项式 ,使得:

其中

第六步:Verifier 发送随机挑战数

第七步:Prover 计算 ,并构造商多项式

后续步:Verifier 发送随机挑战数 ,Prover 打开各个多项式,Verifier 自行计算 ,并验证各个多项式在 处的计算证明,并验证这些打开点满足上面等式。

完整的协议请参考Plonkup论文 [2]。

Reference

  • [1] Ariel Gabizo, Dmitry Khovratovich. flookup: Fractional decomposition-based lookups in quasi-linear time independent of table size. https://eprint.iacr.org/2022/1447.

  • [2] Luke Pearson, Joshua Fitzgerald, Héctor Masip, Marta Bellés-Muñoz, and Jose Luis Muñoz-Tapia. PlonKup: Reconciling PlonK with plookup. https://eprint.iacr.org/2022/086.

  • [3] https://zcash.github.io/halo2/design/proving-system/lookup.html

  • [4] Ariel Gabizon. Multiset checks in PLONK and Plookup. https://hackmd.io/@arielg/ByFgSDA7D

  • [5] Modified Lookup Argument (improved). https://hackmd.io/_Q8YR_JLTvefW3kK92KOFgv

理解 Lasso(零):带索引的查询证明

假设我们有一个公开的表格向量 ,长度为 ,和一个查询向量 ,长度为 ,我们可以利用 Lookup Argument 来证明下面的 lookup 关系:

上面这个 Lookup Argument 定义被 Lasso 论文称为 Unindexed Lookup Argument。因为这个定义只保证了 中,但是并不保证 出现在 中某个特定的位置。

假如我们要表示一个 2bit-XOR 运算,需要用到一个三列表格:

其中第一列表示第一个运算数 ,第二列表示第二个运算数 ,第三列表示 XOR 运算结果, 。显然,表格中的行是可以互换位置的,而不影响表格所表达的 XOR 操作。注意到这个表格共有 16 行。

对于这个表格,我们可以看到表格中的任意两行可以交换,而并不影响表格所要表达的语义。因为表格的每一行同时包含了 XOR 运算的输入和输出。

那么我们问,能不能采用一个单列表格来表达这个 XOR 运算?

Indexed Lookup Arguments

其中一个思路是这样的,我们只采用一列表格来表示「XOR 运算的输出」,即 ,而用表格的行索引来代替两个运算的输入(Oprands)。比如,我们在第 0 行放上 ,因为 ,等号右边为行数的 4bit 编码, ,其高位 表示 ,低位 表示

又比如,表格的第 5 行(记住行数从零计数)为 ,因为 ,而行索引 的二进制表示可以按位拆分为两个二进制数 ,即 。 可见,这个单列表格的大小仍然是 16 行。但是与上面的 XOR 表格不同,这个表格的各行是不允许打乱顺序的,下面是单列的 XOR 表格:

单列有序表格的好处是,我们只需要用一个多项式对其编码。此外,Lasso 还进一步探索了单列表格可能具有的内部结构,探索如何把单列的大表格拆分成多个小表格,从而提高证明效率。Jolt 展示了如何利用表格的内部结构,来编码 RISC-V 的完整指令运算。

基于单列有序的表格,Lasso 论文定义了一类新的 Lookup Argument,称之为 Indexed Lookup Argument:

其中 为一组索引值,表示每一个查询 在表格 中出现的位置。

对于一个 Indexed Lookup Argument,公共输入为三个向量的承诺:

  • 表格向量的承诺
  • 查询向量的承诺
  • 索引向量的承诺

证明的关系为:

出现在Plonk 协议中的 Plookup,以及后续的 Caulk/Caulk+,FLookup, Logup,cq 都属于 Unindexed Lookup Arguments。基于 Unindexed Lookup Arguments,我们同样可以构建 Indexed Lookup Argument。常见的有两个方案。

首先,如果 Unindexed Lookup Arguments 支持表格列的加法同态,那么我们可以为表格增加一列,作为 Index 列,然后通过 Verifier 给出一个随机数 ,然后将 Index 列和 原表格列(或多列)做一个 Random Linear Combination 合并为一列。比如一个单列表格为 ,那么我们可以通过 构造一个混有 Index 的新表格列:

比如 Plookup,Caulk/Caulk+,Baloo,与 cq 都支持表格承诺的加法同态。但是对于 fLookup 等不支持加法同态的 Lookup Arguments,我们可以找到一个值, ,然后通过 把索引列合并到原表格列:

不过,Prover 还要额外证明表格列中的每一项 ,这需要 个 Range Arguments。

从一个 Indexed Lookup Argument 可以更容易地得到 Unindexed Lookup Argument,只需要把索引向量的承诺从公共输入中移除即可,在协议的开头,Prover 补充发送这个索引承诺即可。

Various Lookup Arguments From the Lasso Paper

本系列文章后续将描述总共四个不同的 Indexed Lookup Arguments 协议:

  • Lookup Arguments based on Offline Memory Checking
  • Lookup Arguments based on Spark
  • Lookup Arguments based on Surge
  • Lookup Arguments based on Sparse-dense Sumcheck

第一个协议基于经典的 Offline Memory Checking,改进自 Spartan 论文中的 Memory Checking 协议。支持 Indexed Lookup 与 UnIndexed Lookup。通过 Offline Memory Checking,我们将 Lookup 关系归结到一个(只读)内存读取的虚拟机执行关系的合法性。

第二个协议 Spark 源于 Spartan 论文。为了处理查询向量中可能出现的重复表格项,我们引入一个矩阵 来作为表格选择器,采用 Matrix-vector Multiplication 公式来证明 Lookup 关系:

其中 为表格,长度为 为 lookup 记录,长度为 。这个核心公式来自 [Baloo] 论文。

矩阵 充当了选择器的角色。它的每一行都是一个 Unit Vector,即每一个行向量中,只有一个元素为 ,其余元素均为零。显然矩阵 中包含大量的零,如果我们直接用多项式对 中的全部元素进行粗暴地编码,那么这相当于对于一个长度为 的稀疏向量编码,浪费严重。

举个例子,比如 ,查询向量 定义为:

那么 矩阵满足下面的等式:

如果我们可以利用 矩阵的稀疏性,即 中仅包含有 个非零元素,那么我们可以构造更有效率的 Lookup Argument 方案。[Spartan] 论文提出了针对稀疏矩阵的多项式承诺方案,使得其 Evaluation Argument 的证明时间仅与 有关。

Spark 协议的另一个特点是利用了 的 Tensor 结构。如果表格也具有类似的结构,那么意味着被查询的表格可以拆解成多个维度上的短向量,那么也就意味着 Prover 和 Verifier 不再需要处理一个很大的表格(如果表格长度 ),而只需要承诺和证明多个短向量(作为子表格)即可。第三个协议 Surge 正是这样一个可以证明某一类支持子表格拆解的 Lookup Argument。

支持巨大的表格,比如 ,并不是只有拆解子表格这一种办法,如果表格满足另外一种特性 MLE-Structured,即表格多项式 的求值运算时间复杂度为 ,那么我们可以不需要拆分表格,也不需要让 Prover 承诺表格(表格太大,承诺的计算也无法完成),而是在协议中以「惰性计算」的方式(Lazy On-demand)来临时计算表格的每一项(以表格项的 Index 作为输入,计算表格项 )。这是最后一个 Lookup Argument 的核心思想,被称为 Generalized Lasso。

Generalized Lasso 利用一个所谓的 Sparse-dense Sumcheck 协议来利用查询向量(关于表格向量)的稀疏性,使 Prover 在证明过程中「惰性计算」 中那些仅被查询到的表格项,这样就做到了证明时间复杂度只与查询的数量有关,而与表格长度无关。并且与 cq 等协议相比,Generalized Lasso 并不需要昂贵的预处理。当然,Generalized Lasso 仅能处理满足 MLE-Structured 的一类表格,而非是一个通用的 Lookup Argument。

总结下,Lasso 论文把 Lasso 可以处理的表格分为三类。

  • Unstructured but small
  • Decomposable
  • Non-decomposable but MLE-structured

References

理解 Lasso(一):Offline Memory Checking

假设我们有一个公开的 Table 向量 (长度为 ),和一个 Lookup 向量 (长度为 ),此外还有一个索引向量 (长度为 ),如何证明下面的 Indexed Lookup 关系?

虽然我们有 Plookup, Caulk/Caulk+, Baloo, Logup,cq 等等方案可以直接使用,但 Offline memory checking 提供了一个更直观的新视角来看待 Lookup Argument。

1. Memory-in-the-head

我们把 Lookup 的过程看成是一个虚拟机读取内存的过程。如果一个 Lookup 关系成立,那么我们一定可以构造出一个合法的虚拟机执行序列,这个序列中的每一步都是合法的内存读取操作,从而证明每一个执行步读取的值 都是出自只读内存 ,即证明了 Lookup 关系。如果我们关心内存读取的地址的话,那么我们就实现了一个 Indexed Lookup Argument。对于一个 Prover,她可以在本地构造虚拟机的执行序列 ,并向 Verifier 证明 的合法性。而对于 Verifier 而言,我们比较关心 Verifier 如何在不需要遍历 的情况下,验证这个长度为 的执行序列。 因此,一个 Lookup 关系的证明,就转化为一段「只读内存」读取日志的正确性证明。换句话说,如果一串内存读取过程是正确的(符合虚拟机运行规则,并可以复现),那么就能推出这样的结论:如果每次读取的内容都是合理的,那么读取的值一定存在于原始内存(表格)中。这种证明思路可以形象地被称为「Memory in the head」,Prover 向 Verifier 证明一个头脑中想象出来的内存的读写合法性。

下面是一个虚拟机执行序列的例子,也是一个确定性状态转移关系:

其中 代表内存读取操作,按顺序读出来 。虚拟机内存状态 是一个三元组 的集合,一个三元组包含内存地址 ,内容 和计数器 三部分。注意到我们为每一个内存单元都附加一个计数器,标记着这个内存单元被读取的次数,这个计数器的作用是确保只读内存在读取过程中仍然会发生实质性的状态更新,从而提供了执行步 的验证信息。

内存初始状态 中的元素如下:

由于每一次内存的读取(虚拟机的执行)都会修改相应地址上的计数器,让计数器加一,因此我们规定虚拟机在每一次读写内存的前后,必须抛出两个日志(或者理解为事件),内存读取日志 与内存更新日志 。两者也同样都是一个三元组 ,包含内存读取地址 ,读取内容 ,和计数器的值

对于内存读取日志 中的 为读取时刻内存单元 中的计数器值; 中的 为更新后的计数器值。换句话说,我们也可以理解 发生在一次内存读取之前, 发生在一次内存读取后,两个事件之前内存单元因为一次「读取」而将计数器的值加一。这一前一后两个日志的作用是约束每一次内存读取的合法性。怎么做到的呢?我们先看一个例子,假如内存的长度为 4,存放的内容为 ,假如我们要依次从内存中读取 ,那么会产生下面的日志序列

三次读取产生的状态转移如下:

2. 内存读取的验证

现在思考下,一个 Prover 如何向 Verifier 证明虚拟机执行序列的合法性?下面我们给出虚拟机执行合法性的四个条件:

解读如下:

  • 条件(1): 虚拟机执行必须从一个初始状态 开始,即内存中依次存放着表格内容 ,并且计数器都置为零;
  • 条件(2): 存在一个正确的终状态 ,并且 中的内存数据 没有被修改;
  • 条件(3): 对于每一个 日志,在「该事件之前」都会有一个成对出现的 日志,他们记录的读取值相等 ,但
  • 条件(4): 对于每一个 ,如果它是地址 上的第一次读取,读取值应该等于内存初始状态 ;如果它是地址 上的第二次或后续读取,那么在「该事件之前」一定有一个对应的 日志,使得

这四个条件是否完备充分呢?我们可以试着用归谬法推理下:

假如存在有一个 中的读取数值 非法, 即 ,并且此刻计数器为

假如 ,那么根据条件 (4),又因为条件 (1),这与假设矛盾。

另一种情况是 ,那么根据条件 (4),一定存在一个 日志,使得 的计数器值为 ,再根据条件 (3),一定存在一个 日志,使得 。以此递归地推理下去,每次 递减一,最后我们一定可以得到某个 ,于是根据条件 (1) 可得, ,这与条件(1) 的正确性()矛盾。

到此推理完毕,存在有一个非法读取日志 的假设不正确,因此我们得出结论:满足上面四个条件的虚拟机执行序列中,不可能出现读取一个错误的值的情况。

因此,只要 Prover 能够证明,(1) 初始状态 正确,并且 (2) 每一步读取日志是自洽的,那么我们可以证明读取过程就是不可伪造的。Offline Memory Checking 提供了一个漂亮的约束等式,同时满足上面四个条件:

我们进一步分析下这个约束等式,先展开下等式左右两边的定义:

这个等式约束是关于四个多重集合(Multiset)之间的关系。容易看出,初始状态约束 条件(1) 和 终状态约束 条件(2) 已体现在上面的等式中。接下来我们简单分析下,上面这个等式如何保证了 条件(3)条件(4)

先看下条件(3),对于每一个 (出现在等式左侧),那么就有一个成对出现的 (出现在等式右侧),两个日志的差别是右侧 的计数器值少一。看下 条件(4),如果某个 中的计数器值为零,那么在等式左边一定有一个相同的三元组元素,出现在 集合中;如果 中的某个元素的计数器值大于零,那么这个元素一定出现在等式左边的 中。

注意到,等式右边来自于 中的每一个元素,可能出现在左边的 中,这意味着该元素所对应的内存单元从未被读取过; 集合元素也可能出现在 中,这意味着该元素的计数器值等于最后一次内存单元计数器的更新值。

最后我们分析下 Prover 和 Verifier 的输入。对于 Verifier 而言, 属于 Public inputs,这样 Verifier 可以验证 条件(1),Verifier 要求 Prover 提供 向量,从而构造 ,验证 条件(2)。此外 Public inputs 还要包括承诺 ,以便 Verifier 同态地验证 Multiset 等价关系。而日志集合 由 Prover 构造,并给出其中计数器部分的承诺 ,从而允许 Verifier 来验证正确性条件 (3)。而 Verifier 也可以根据 还有 ,同态地构造出 的承诺。

接下来我们利用 Memory-in-the-head 的思路,设计一个 PIOP 协议,实现 Indexed Lookup Argument。

3. 构造 Lookup Argument 协议

我们把这四个集合 看成是三列矩阵,并且所有的矩阵列向量都编码为多项式。其中 矩阵的三列记为 , ,这里注意 在 中的 value 一列必须等于表格向量 。矩阵 为虚拟机的终止状态,由于虚拟机内存为只读内存,因此 两列保持不变,但是内存单元计数器被更新到了 ,编码为 ,多项式编码的 Domain 记为

日志矩阵 的第一列为读取的地址序列,它必须等于地址向量 ,第二列为读取的值,等于 ,而第三列 为 Prover 维护的计数器向量,编码为 。再看下矩阵 ,其每一行为一条内存更新日志,其中第三列为更新后的计数器值,这个值编码为 ,并应该满足下面的约束:

下面是 Offline Memory Checking 的约束等式:

我们可以用多项式之间的约束关系描述下 Multiset 等价约束:

其中四个二元多项式的定义如下:

其中

我们可以再使用两个 Verifier 提供的随机挑战数 ,把上面的多项式等价关系归结到两个 Grand Product 之间的等价关系。而 Grand Product Argument,我们可以有多种方案来完成。例如我们可以采用 Plonk 协议中的 Grand Product 子协议来完成,也可以采用 GKR 协议,或者 论文 [Quarks, SL20] 中 基于 Sumcheck 的协议。

协议描述

公共输入:

第一轮

Prover 模拟内存读取流程得到终状态 ,得到

Prover 计算 的承诺 , Prover 计算 的承诺

Prover 发送

第二轮

Verifier 发送挑战数

Prover 计算读取/更新日志向量 , ,

Prover 计算

Prover 和 Verifier 利用 Grand Product Argument 来证明下面的等式:

验证

Verifier 计算 ,并验证 Grand Product Argument

这里

4. 对比理解 Offline Memory Checking

与 Plookup, Caulk/Caulk+, flookup, Baloo, CQ 相比, Memory-in-the-head 方式证明 Lookup 是一个巧妙且直观的想法。不过我们会想知道他们之间有何差别?

这一节,我们从 Plookup 的角度出发,换一个角度来理解 Offline memory checking。

我们先假设 中不存在重复元素,那么我们可以采用 Vanishing Form 的方式来编码 为多项式:

Prover 可以通过下面的等式来证明

但是如果考虑 中存在重复元素,那么用 Vanishing Form 编码的多项式就不满足上面的等式约束了。处理重复元素是 Lookup Argument 中比较棘手的问题。为了修补这个方案,我们要为表格向量 和查询向量 分别扩展一个新的列向量,称为计数器列 。每当 中出现重复读取同一个表格元素时,我们可以通过计数器列来区分这两次不同的读取。比如 中有两次对 的查询,那么我们可以定义一个扩展后的查询向量 :

扩展后的向量中的每一个元素是一个二元组,其中第二部分为计数器值。 扩展查询中的前两个查询 ,虽然查询值都为 ,但是由于计数器会按顺序加一,因此,两个二元组不再相等。

同样,我们也可以定义一个扩展后的表格向量 :

那么我们会问下面的公式会成立吗?

很显然,它不成立,因为等式左边有 ,而右边的集合中不包含这个元素。显然我们需要在公式的右边补上 。换句话说,我们需要在右边补上那些由于计数器累加产生的重复表项,记为

但是这个向量 不能由 Prover 提供。为了防止 Prover 作弊, 必须由 Verifier 来提供。那么接下来,我们面临的问题是,Verifier 并不清楚哪些 重复,并且也不能知道重复了几次。这个问题该如何解决?

我们可以把查询 看成是一个「消耗」表格元素的机器,每次查询 ,都会消耗掉一个对应的表格元素 。我们把向量 看成一个可以「产生」新元素的机器,每次出现一个对 重复查询的记录,比如 ,那么 就会自动产生出一个新的元素,记为 ,供下一次查询「消耗」。这样我们就可以让 Verifier 在等式左边添加 个元素,正好对应 的元素,但是所有元素中的计数器都自增一。这样,等式左边的集合 就可以由 Verifier 自行构造:

这个公式成立 ,我们继续可以用 Vanishing Form 的方式来表达这个子集关系,比如对上面的例子,我们可以得到下面的多项式等式:

其中 为 Verifier 提供的随机挑战数,用来合并表格 的二元组为一个单值。这里 编码了那个能自动产生新元素的机器,它的每一个因子都是一个 元素。下面是 多项式的定义:

多项式等式右边的多项式 会有哪些元素呢? 恰好包含有所有的等待被消耗的 元素,其中包括始终没有被查询过的计数器为零的元素,还包括被查询过的,但是又被 复制产生的元素。于是我们得到了下面的等式约束:

下面我们证明上面的等式保证了 中的每一个元素都是 中的元素。

我们用反证法,假如存在一个 ,那么根据上面的等式,一定存在一个计数器 出现在等式的左边。这时候如何 ,那么 ,与假设矛盾。那么这时候可以断定 ,那么等式右边一定存在一个 ,才会让左边出现 ;同理可推,左边一定存在一个 ,那么右边一定会出现一个 。以此类推,我们一定可以得到:等式左边会出现 ,于是 ,这又与初始初始假设矛盾。

这个思路与 Memory-in-the-head 几乎一摸一样,除了我们不考虑表格的索引问题。基于这个思路,我们可以构造一个 Unindexed Lookup Argument。

对比 Plookup

回忆下 Plookup 的方案,对于 ,如果我们要证明 ,那么 Prover 需要引入一个中间向量 ,长度为 。它是 的一个重新排序,按照 中原有项的顺利进行排列。然后 Prover 证明下面的 Multiset 等价关系:

这个方案和 Memory-checking 相比,Multiset 约束等式两边的集合元素数量都为 ,但 Plookup 需要多引入一个中间辅助向量 ,而后者则需要引入一个计数器向量 。计数器向量节省了 Prover 在排序上的工作开销,另一方面,向量 中的值较小且规律,Prover 计算其承诺会更有优势(如 Perdesen 承诺或者 KZG10)。

5. 小结

本文介绍了如何采用传统的 Offline Memory Checking 技术构造 Lookup Arguments,其中关于 Memory Checking 的公式 蕴含着非常巧妙的思想。

References

理解 Lasso (二):稀疏向量与 Tensor 结构

本文我们介绍一个基于 Sumcheck 的「稀疏多项式承诺方案」 Spark,这个方案最早出自 [Spartan] 证明系统。Spark 利用了稀疏向量的结构,可以大幅提升 Prover 的效率。Lasso 是在 Spark 的基础上的进一步拓展了对稀疏向量的处理。理解 Spark 是理解 Lasso 的关键。

普通的多项式承诺方案包括两个阶段,一个是承诺(Commitment)阶段,另一个是求值证明(Evaluation Argument)阶段。对于一个 MLE 多项式 ,求值点 ,以及运算结果 ,那么多项式承诺计算如下:

在求值证明阶段,Prover 可以向 Verifier 证明多项式 在某一个指定点 的运算结果为

Verifier 可以验证求值证明

如果 是一个稀疏的多项式,意味着它在 Boolean HyperCube 上的运算结果中多数的值都为零,那么我们能否利用这个特点,来设计一个针对稀疏多项式更高效的多项式承诺方案?

下面我们演示如何构造 Spark 多项式承诺。不过请记住,Spark 仍然需要基于一个普通的多项式承诺方案。换句话说,Spark 协议是将一个稀疏的 MLE 多项式的求值证明「归约」到多个普通的 MLE 多项式的求值证明,但后者这些 MLE 多项式的大小被大幅减少。

1. 稀疏向量的编码

我们考虑一个长度为 的稀疏向量 是一个 MLE 多项式 在 Boolean HyperCube 上的取值。记住 是一个稀疏的向量,其中除了 个非零元素之外其余值都为零。

先回忆下 MLE 多项式 的定义:

其中 是 MLE Lagrange 多项式。 定义如下:

如果直接使用一个普通的 MLE 多项式承诺方案来证明多项式求值, ,由于 是一个关于 项的求和公式,那么很显然 Prover 要至少花费 的计算量来遍历每一个求和项。

如果给定一个求值点 ,那么所有的 就构成了一个长度为 的向量,记为

别忘记稀疏向量 中仅有 个非零元素。举个例子,比如 ,即 向量中仅有四个非零值:

那么我们可以换用一种稠密的方式来表示

可以看出,向量 的稠密表示是一个长度仅为 的向量,其每一个元素为非零元素位置和非零元素值的二元组。我们再把上面二元组向量中的位置值单独记为 向量,把元组中非零的 记为 向量:

那么 的稠密表示可以写成:

然后 MLE 多项式 点的求值等式可以改写为:

注意上面这个等式中的求和项的个数仅为 。这意味着在给定 的情况下,我们成功地把 的求值运算从 降到了 。接下来的问题是 Prover 如何向 Verifier 证明求值过程用到了正确的

对于一个多项式承诺方案,求值证明的公开输入里面包括了 向量的承诺,但是上面的求和式需要用到辅助向量 。其中 向量可以通过求值点 计算得到,其中每个元素为 ,而求值点 为公开输入,因此 Verifier 可以公开计算 向量或者公开验证。但 Verifier 并不能由 向量的承诺 来直接得到 这两个向量的信息。因此,我们需要把 的承诺来替代公开输入中的 向量的承诺。

换句话说,我们采用 来作为稀疏向量的 的编码,并利用一个普通的多项式承诺方案来计算 ,并把它们作为多项式求值证明的承诺(做为公开输入)。

2. 借助 的 Sumcheck

我们需要引入一个长度为 的辅助向量 ,它的每一个元素

这样 点的求值等式等价于下面的求和等式:

其中 是一个编码了 的 MLE 多项式, 是关于 的 MLE 多项式

如果 Prover 要证明上面的求和式,首先提供 的承诺 给 Verifier, 然后通过接下来的两部分来完成证明。

第一部分证明是 Prover 利用 Sumcheck 协议,把 的求值证明规约到下面的等式

其中 为 Sumcheck 协议对 个求和项进行折叠运算后的结果,而 为 Sumcheck 运行过程中 Verifier 产生的随机折叠因子。因为 Sumcheck 过程需要 轮,所以 的长度为

接下来 Prover 怎么证明上面的等式呢? 在求值证明之前,Verifier 已经从公开输入中得到了 , 两个向量的承诺,分别为 ,那么到这一步,Prover 和 Verifier 可以再利用普通的 MLE 多项式承诺方案来完成两个 Evaluation Argument,即分别证明: 的正确性,因为这两个向量长度均为 ,因此 Prover 产生这两个 Evaluation Argument 的计算量为 。最后 Verifier 验证 完成第一部分的证明。

第二部分证明是 Prover 证明 向量关于 , 的正确性,这就需要用到前文介绍过的 Offline Memory Checking 方法:Prover 只要证明 向量中的每一个元素都是从 向量(看成是内存)中读取出来的即可。这样 Prover 总的计算量为

3. 使用 Memory Checking 证明 的正确性

辅助向量 的正确性证明正是 Indexed Lookup Argument:

借助 Memory Checking 协议,我们把整个 向量(公开向量)看成是一段内存,Prover 证明 向量依次读取自内存 ,读取的位置为 。Prover 可以在 的计算量内完成上面的证明。

结合前文的定义,这里 为查询向量 为表格向量 ,而 为位置向量

但还有一个问题, 的承诺 怎么产生?向量元素 ,其定义中含有一个求值阶段才出现的公开输入 ,因此不能在 的承诺阶段中出现,也无法出现在 求值证明的公开输入中,一般情况多项式承诺方案的公开输入为 。如果由 Prover 计算 的话,那么 Prover 需要额外证明承诺的正确性。

幸运的是, 向量具有一定内部的结构,虽然它的长度为 ,但在给定 的情况下,它的插值多项式 可以在 的时间内进行求值计算,于是这样一来 Prover 可以不需要提供 ,而是让 Verifier 在验证过程中自行计算 在某一点的取值。我们观察下 的定义:

容易检验,对于任意的

上面等式最右边是一个 项的乘积,其中每一个因子只需要常数次的加法和乘法。接下来我们稍微修改下前文中的 Offline Memory Checking 协议,把公开输入中的 替换为 ,并且让 Verifier 自行计算 的值。

Memory Checking 协议描述

公共输入:

  1. ,

第一轮

Prover 计算 ,

Prover 计算并发送计数器的承诺

第二轮

Verifier 发送挑战数

Prover 计算 , ,

Prover 计算

Prover 和 Verifier 利用基于 Sumcheck 的 Grand Product Argument 来证明下面的等式:

Grand Product Argument 证明最后会归约到对多个 MLE 多项式的求值证明,也就是对 的求值证明。这些证明可以归约到 的求值证明。注意我们前面提到过, Verifier 不需要 的承诺求值证明,他可以自行计算 在任意点的求值。因为该多项式的求值计算量仅为 ,不影响 Verifier 的简洁性(Succinctness)。

进一步,任何计算过程仅为 的 MLE 多项式,Prover 也不必要一定计算它们的承诺,只要把计算任务交给 Verifier 就好。这样 Verifier 仍然保持 SNARK 的特性,同时也提高了 Prover 的效率,省去了计算承诺和产生求值证明的工作量。前提是,这一类 MLE 多项式需要具有一种特殊的内部结构,我们后文会把它们归到一个特殊的分类:MLE-Structured Vector。

对于 Prover 而言,仍然需要在证明过程中构造 ,通过动态规划算法,这需要 的计算量。

4. 求值证明协议细节

1. 承诺阶段:

Prover 要计算下面两个承诺:

  1. :稀疏向量 中的非零元素向量 的承诺
  2. 中的所有非零元素在 中的位置向量 的承诺

2. 求值证明阶段:

公共输入:

  1. 多项式的承诺
  2. 求值点 ,以及运算结果

第一轮:

  1. Prover 计算 ,作为内存模拟
  2. Prover 计算 , 并发送承诺 ,作为 memory 顺序读取出的内容

第二轮:Prover 与 Verifier 执行 Offline Memory Checking 协议,证明

第三轮:Prover 与 Verifier 执行 Sumcheck 协议,证明

并把上面的求和等式归约到

其中 为 Verifier 在 Sumcheck 过程中发送的挑战向量。

第四轮:Prover 发送

  1. ,求值证明为
  2. ,求值证明为

验证: Verifier 验证 的有效性,并验证下面的等式:

性能分析

Prover 在 Memory-checking 协议中的性能开销为 ,因为内存的大小为 ,读取序列长度为 ;在 Sumcheck 协议中为 。因此 Prover 总的计算开销为

这样一个稀疏多项式承诺方案其实并不理想,因为 Prover 的计算量仍然与 线性有关。我们希望能够进一步减少 Prover 的计算量,这就需要进一步探索 的内部结构。

5. 向量 二维分解

为何 的求值计算量仅为 ? 因为向量 具有一种特殊的结构——Tensor Structure,也就是它可以拆分成多个短向量的 Tensor Product。简化起见,我们试着把 按照下面的方法拆分成两部分的乘积:

这里 是把 的二进制位拆分成相等的两段所表示的数值。举个例子,比如 是一个十进制数,它的二进制表示为 。我们可以把它拆成高二位与低二位,分别为 ,那么 。我们引入一个新的「拼接记号」, 表示 的二进制位为其高位和低位两个数的二进制位的拼接,按照 Big-endian 的方式。比如 。不难验证,拼接操作满足性质:

按照上面的分解方法,我们可以分解 为两个值的乘积:

对于长度为 向量中的所有元素 ,我们可以把其中每一个元素都按照相同拆分方式进行分解:

我们进而把这 16 个元素排成一个 的矩阵,每一个单元格的值 都等于它对应的行向量元素和列向量元素的乘积。

如果把上面表格的第一行的元素组成向量,记为 ,第一列记为

那么 向量看成是两个长度为 的向量的 Tensor Product:

回到我们关注的向量 ,其中每一个元素 也就可以看成是两个数值的乘积 ,其中 来自于 ,另一个 来自于

这相当于我们把整个 向量分解到了一个二维空间中,它的值等于横坐标和纵坐标值的乘积。那么我们可以继续采用 Offline Memory Checking 的思路来证明 的正确性,这次我们需要采用二维的 Offline Memory Checking 协议。更直白点说,我们需要采用两次 Offline Memory Checking 协议来证明 的正确性,每一个 对应到两个值的乘积,它们分别读取自

于是稀疏多项式 的求值等式可以改写为:

其中

其中 为非零元素 在二维矩阵中的行列坐标。这样我们可以把求值协议中的 Offline Memory Checking 子协议调用两次,但是内存的大小被大幅缩小到了 。看下前面的例子, 向量中仅有四个非零值:

向量 为非零向量:

这时候,我们可以采用二维坐标 来标记 矩阵中的位置,标记矩阵中的行和列:

我们把其中行坐标向量记为 ,列坐标向量记为 ,那么 可以表示为

经过 Sumcheck 协议之后,上述等式可以被归约到:

然后 Prover 再提供三个 MLE 多项式在 点的取值, 的求值证明。

在这个二维的求值协议中,Prover 的计算开销就从上一节的 降低到了

下面我们给出完整的二维稀疏多项式承诺方案。

6. 二维稀疏多项式承诺 Spark

利用上面的思路,我们把稀疏向量 重新排列成一个 的二维矩阵 。为了排版清晰,我们引入符号

6.1. 承诺阶段:

Prover 要计算下面两个承诺:

  1. :稀疏向量 中的非零元素向量 的承诺
  2. 中的所有非零元素在矩阵 中的行坐标构成的向量 的承诺
  3. 中的所有非零元素在矩阵 中的列坐标构成的向量 的承诺

,这个三元组承诺我们用符号 表示。

6.2. 求值证明阶段:

公共输入:

  1. 多项式的承诺
  2. 求值点 ,这个点可以拆分为两个子向量 ,其中
  3. 以及运算结果

第一轮:

  1. Prover 计算 ,作为 内存
  2. Prover 计算 ,作为 内存
  3. Prover 计算 , 作为分别从内存 读取出的内容,并发送承诺

第二轮:Prover 与 Verifier 执行两次 Offline Memory Checking 协议,证明 的正确性:

第三轮:Prover 与 Verifier 执行 Sumcheck 协议,证明下面的等式求和

并把求和等式归约到

其中 为 Verifier 在 Sumcheck 过程中发送的挑战向量,其长度为

第四轮:Prover 发送

  1. ,求值证明为
  2. ,求值证明为
  3. ,求值证明为

验证: Verifier 验证 的有效性,并验证下面的等式:

6.3. 性能分析

8. Tensor 结构 (TODO)

如果我们可以把 分解到二维空间,那么能否分解到更高维的空间?比如 的长度为 ,那么把它排成二维矩阵,比如 ,矩阵的长宽还是较大。如果把 重新排列成一个立方体,然后同样把 拆分成三段,这样我们可以把 Offline Memory Checking 的 Prover 开销进一步降低到 ,也就是 。这个分解的灵活性来源于 的结构特性,即 一个具有 Tensor Structure 的向量可以用不同的 Tensor Product 分解方式。理论上,我们可以把 分解成 个长度为 的短向量的 Tensor Product。不过实践中,我们只需要将其分解到 即可处理超长的向量。

例如当 时, 即可以排列成一个 的二维矩阵,也可以排列成 的四维矩阵:

我们可以根据 Tensor Product 逐步来推导下:

再利用上面的计算结果来计算

其实,许多常见的向量也具备 Tensor Structure,比如

7. 小结

本文介绍了 Tensor Structure 的概念,利用这个结构,我们可以把稀疏向量映射到一个二维空间中进行编码,然后我们基于这个结构,可以构造一个稀疏向量的多项式承诺方案。

References

理解 Lasso (三):大表格的稀疏查询证明

Lasso 这个名字是 Lookup Arguments via Sparse-polynomial-commitments and the Sumcheck-check protocol, including for Oversized-tables 的缩写。这里面有三个关键词,

  • Sparse Polynomial Commitment
  • Sumcheck protocol
  • Oversized table

本文继续讨论如何利用 Sparse Polynomial Commitment 来构造 Indexed Lookup Argument。但为了能处理 Oversized Table(比如 这样的表格),需要充分利用表格的内部结构。

1. 构造简易 Indexed Lookup Argument

前文介绍了 Sparse Polynomial Commitment,现在回到正题 Lookup Argument。下面是一种 Lookup 关系的表示:

这里 为表格向量,长度为 为查找向量,长度为 ,选择矩阵 大小为

我们引入三个 MLE 多项式 来分别编码矩阵 ,表格向量 ,与查找向量 , 那么它们满足下面的关系:

Verifier 可以发送一个挑战向量 ,把上面的等式可以归约到:

现在, Prover 要向 Verifier 证明上面的求和等式成立,我们会立即想到使用 轮的 Sumcheck 协议,把上面的等式归约到一个新的等式:

其中 个求和项折叠之后的值, 为 Verifier 在 Sumcheck 协议过程中发送的挑战值。这时 Verifier 要验证上面的等式,就需要 Prover 提供三个 MLE 的求值证明。因为 矩阵是一个稀疏矩阵,因此 Prover 可以在协议最开头采用 Spark 协议来承诺 ,然后在 Sumcheck 协议的末尾, Prover 可以花费 的计算量来产生 的 Evaluation 证明。这远好于 Prover 直接采用普通多项式承诺的开销,

协议细节

公共输入:

第一轮:Verifier 发送挑战向量

第二轮:Prover 和 Verifier 执行 Sumcheck 协议,把上式归约到

第三轮:Prover 发送

当然,我们可以通过把 整个向量排成一排,得到长度为 长的一维向量 ,然后把这个向量在一个 维的空间中 进行拆分:

然后利用 Spark 协议来达到 的 Proving Time 复杂度。但是 Prover 在产生 时则需要 的计算量。那么总体上,Spark 虽然可以有效降低 Prover 的工作量,但是如果表格尺寸 非常大,那么 Prover 仍然需要花费大量的时间来计算表格。那么还能不能更进一步呢?像 Caulk/Caulk+, cq 那样让 Prover 的性能开销变为关于 的亚线性复杂度。

Lasso 协议正是朝着这个方向迈出了一大步,它甚至不需要像 cq 那样要实现对完整的大表格做预处理。尽管它不通用,只能针对几类特殊的表格,但不少常见的运算都可以证明。

Lasso 的核心思想是,我们能否把表格向量 像稀疏向量一样按照多个维度去拆解?如果能像 Tensor Structure 那样,一个巨大的表格可以表示为若干个小表格的运算。这样 Prover 和 Verifier 就可以对多个小表格做 Lookup 证明,那么最终得到的效果就是:看起来我们可以实现一个虚拟的大表格的查询证明。

顺着这个思路往下想,一般情况下表格不可能是稀疏的,不过非稀疏的表格在某些情况下是可以分解的。比如我们在前文提到的异或运算的表格,

直觉上,一个2-bit XOR 表格是可以分解为两个 1-bit XOR 表格的运算。因为 XOR 运算是按位进行,操作数的高位和低位的 XOR 运算互不干扰。进而我们可以推广到 AND 运算,OR 运算等等。具体怎么做到呢?接下去,我们深入到表格的内部结构中。

2. 分解表格

稀疏的选择矩阵 还有一个特点是,其中的所有非零元素都为 。那么我们可以换一种方式来表达 Lookup 等式:

为了排版清晰,这里我们换用大写的 表示未被分解的大表格,分解出的子表格用小写字母 表示,并且用 符号来表示表格中第 个元素 。其中 表示第 行的 所在的列坐标,可以看成是 矩阵的一种稠密表示。容易验证对任意的 ,相当于列出表格中的被 非零元素筛选出来的元素。因此这个等式可以看成 的另一种定义,等价于

我们把 单独排成一个向量 ,然后把向量编码成 MLE 多项式,记为 。那么通过一个随机挑战向量 , Lookup 的关系就归约到下面的等式:

根据 Offline Memory Checking 的思路,我们可以证明 都读取自表格 。这样相当于原地踏步,我们为了证明一个 Lookup关系,我们归约到了另一个 Lookup 关系。不过我们是否可以 分解到一个二维(或者多维)的子表格上呢?就像 Spark 协议中的 向量一样,我们是把 所读取的内存 分解成了 ,然后把 分解为 。然而并不是所有的表格都能像 一样满足 Tensor Structure 的。事实上,绝大部分的表格不满足这个条件。不过幸运地是,尽管他们不满足 Tensor Structure,但是一大类的有用表格可以按照类似的思路处理。

我们先看一个简单但很实用的表格,RangeCheck 表格。当需要证明 ,我们可以构造一个表格 ,如果 ,那么说明 之间。

这个表格 可以被分解成两个 的 RangeCheck 表格之间的运算:

比如我们假设 定义如下:

另一个 2-bit RangeCheck 表格 定义如下:

那么我们可以用下面的矩阵来展示 和子表格 之间的关系:

矩阵的第一行为 ,第一列也为 ,矩阵中的每个单元可以表示为

于是矩阵的所有单元构成了 的所有元素。

针对 Rangecheck 表格这个特例,我们可以构造一个高效的 Lookup Argument。

3. RangeCheck 表格的 Lookup Argument

我们用 表示 2bit 和 4bit-RangeCheck 表格的 MLE 多项式,那么它们满足下面的等式:

那么 Lookup 关系可以写成下面的形式:

这里 分别表示 的高 2bits 和低 2bits。

同样,我们需要借助向量 ,其中向量元素 表示 处的取值,因此上面的等式可以转化为:

由于 的可分解性,我们可以把 的高 2bits 和低 2bits 分别抽取出来,它们构成两个向量 ,分别对应 中的第 项和 第 项,满足

接下来构造 两个 MLE 多项式,分别编码 ,那么上面的等式可以转化为:

由于这个等式是一个求和式,因此我们可以利用 Sumcheck 协议来把上面的等式归约到:

其中 为 Verifier 在 Sumcheck 过程中产生的长度为 的挑战向量。辅助向量 的正确性可以由 Offline Memory Checking 来证明。

类似的,我们可以把 32-bit RangeCheck 表格分解成四个 8-bit RangeCheck 表格, 或者两个 16-bit RangeCheck 表格。

我们用这个可分解表格,构造一个 Lookup Argument,与之前的方案的差异在于,它利用了表格向量的内部结构,可以处理超大的表格。

4. Lasso 协议框架

Lasso 的核心协议是一个类似 Spark 的稀疏多项式承诺,被称为 Surge。对于任意一个查找记录 ,假如 在主表格 中的索引值为 。因为主表格 可被分解,比如 可以被分解为 个子表格。分解维度的数量 对应于主表的索引值 按照二进制位的拆分。例如:

即每一个主表元素 都可以写成关于 个子表格 中的元素的运算:

我们可以写下对于可分解表格的 Lookup Argument 的等式:

以 32-bit 的 Rangcheck 表格为例,假如我们需要把它分解为四个子表格,这四个子表格完全一摸一样,都是一个 8-bit 的 Rangecheck 表格。那么我们可以写下下面的等式:

这里 的定义如下

协议细节

公共输入:

  1. 子表格的承诺:
  2. 查询向量的承诺:

第一轮:Prover 计算并承诺

第二轮:Verifier 发送随机向量

第三轮:Prover 计算向量

Prover 计算计数器向量(长度为

Prover 计算终状态中的计数器向量(长度为

Prover 发送向量的承诺

第四轮:Prover 和 Verifier 运行 Sumcheck 协议,证明下面的等式:

Prover 和 Verifier 把等式归约到:

第五轮:Prover 发送 ,以及

第六轮:Verifier 验证

第七轮:Prover 和 Verifier 调用 Offline Memory Checking 证明每个 的正确性,即每个向量元素 都是从表格 中读取,读取的位置为

5. 二元操作表格的分解

除了 RangeCheck 表格之外,还有 AND, OR, XOR 这类按位计算表格也可以按照同样的思路进行分解。例如下面是一个 1-bit AND 表,记为

可以看出,这个表格有 4 行,第 行的表格元素为 ,而表格的索引值的高位为 ,低位为 。比如 这一行, 二进制位的高位为 ,低位为 ,那么这一行的表格元素为 ,表示 。假设我们要分解一个 2-bit AND 表格,, 那么我们可以用下面的矩阵来表示:

矩阵中的每个单元格表示 ,其中 满足下面的等式:

因此,我们可以推而广之,对于任意的 -bit AND 表格,我们可以把操作数 按位拆分成 段,每一段查子表格 确定 ,然后将 个运算结果再按位拼装起来。下面写出这个关系等式:

代入到 Lookup 关系等式中,我们可以得到:

代入上面的 Lasso 协议,我们可以构造出对 表格的 Lookup Arugment 方案。

同样我们可以把其它的二元位操作同样按照这样的思路去分解,如 。把主表格拆分成 段,假设主表格表示两个长度为 的二进制数的位运算,那么第 个子表格对应主表索引的第 位到 之间的位运算。 表示两个操作数 的二进制位在主表格第 个维度上的位置索引。

References

理解 Lasso (四):更多的可分解表格

Jolt 论文给出了更多的可分解表格,用于表达 RISC-V 指令的计算过程。

更多的表格拆分案例

我们首先看一个简单的可分解表格 ,这个表格用来判断 ,如果 A 和 B 相等,那么这个计算返回 ,否则返回 。下面是一个 2-bit 表格示例:

然后我们分析一个 W-bit 长的表格如何分解。对于判等表格来说,我们可以把 位的表格拆分成 -bit 子表格,这些子表格完全相等,因为高位低位运算互不干扰。我们先看子表格的定义:

这个定义是说,如果 相等,那么 ,否则为 。那么 表格的定义就是把 位的表格拆分成 位的子表格,然后把这些子表格的结果相乘:

这里,

LTU 表格

接下来我们看一个稍微复杂点的表格,用来计算 ,表格的索引值为 ,如果关系成立,那么表格项为 ,否则表格项为

比较两个整数的一般算法是并行扫描两个整数 , 的每一个二进制位。从最高位开始,当遇到第一个不同的位时,停下来比较,并输出结果。

我们引入一个辅助的表格 ,它表示 第一个不同的 bit 位于第 个位置(按照从低位到高位的顺序)

例如,,因为 ,并且 。而 ,因为 ,所以 。下面是所有 的计算过程:

利用多个辅助表格的求和,我们可以定义出 表格:

对上面的例子,我们可以计算 LTU 表格中位于 这个位置的项,

假如 ,那么 的尺寸将是 ,理论上我们无法存储这么大的表格。我们需要把大表格拆分成若干小表格关于一个多项式 的运算。

接下来,我们分析如何拆分这个表格。假设我们把 宽的 bits 拆分为 段,每一段为 。操作数 段拆分后,表示如下:

一个初步的想法是将这 组操作数分别输入到 表格中,然后把这些表格的结果相加。但是这样的计算结果并不是我们想要的,因为这样的计算结果会有多个 ,而我们只需要其中一组出现 ,其余为零,这样我们最后求和的结果才是 。因此,我们可以考虑从高位开始,当某一段的 计算输出 时,我们要求从高位开始的比较过的 段都应该相等,否则就输出

如果要采用 Lasso 框架来证明关于 表格的 Lookup,我们需要定义下给出参数化的多项式

总共有 段,每一段需要两个表格,

SLL 表格

下面是一个不容易切分 bits 的表格,那就是移位运算,比如向左移位(Shift Left Logical, SLL),0011 << 02 = 1100。移位运算会给表格的切分带来些麻烦,因为移位运算会将低位分段的部分 bits 移动到高位分段。

我们先考虑单个表格的 SLL 运算。下面的 表示把长度为 的位向量 向左移 位的运算:

这个定义比较容易理解,对于 个 bits,我们只需要把低 位中的每一个 ,乘上 ,然后对这个 个数进行求和。然后高位的 位会溢出,因此被直接抛弃。这个表格比较容易定义是,因为我们将移位的位数作为一个常数。

接下来,我们考虑左移的位数是一个变量 。这时候,我们可以利用一个选择子,来根据 值来选择不同的

其中 ,因为 的取值范围是 (在 RISC-V 的规范中,移位操作的位移为 )。

例如我们要计算

等式可以展开为:

如果 ,我们需要把表格拆分为 -bit 子表格。我们用一个例子()来说明,0101 1100 1001 1010 << 0110,这个移位操作是向左移 6 位,假如我们需要将这个表格拆分为 个子表格,每个子表格计算的位数为 。 这样,每个子表格 长度为 ,每个表格项的位长为 。那么这个移位操作相当于是 个移位运算结果的求和:例如我们给出这个子表格的表项示例:

的计算过程看下表:

这里红色标记的部分为溢出 部分。根据 SLL 的语义,我们要抛弃这部分的 bits。剩下的部分,我们把它们错位(乘上 2 的次幂 )相加。由于不同的分段溢出的 bits 数量不等,我们需要先给出一个定义, 来标记每一个分段中溢出的 bits 数量:

这里 为移位的位数, 为分段的索引。然后 子表格的定义如下:

最后我们给出 的分解定义:

应用 Lasso 框架来实现移位运算,参数化的多项式 可以依据上面的等式来定义。

理解 Lasso (五):表格的 MLE 结构

本文介绍 Generalized Lasso,也是 [Lasso] 论文的关键部分之一。与 Lasso 相比,Generalized Lasso 不再对大表格进行拆分,而是把表格作为整体进行证明。为了处理超大尺寸表格,Generalized Lasso 需要要求表格中的每一项是可以通过其 Index 的二进制表示进行计算得到。对于尺寸为 的超大表格而言,其 Index 的二进制位数量为 ,因此表格的表项的计算复杂度一定为

这样做的一个优势是,Prover 可以不必要对表格进行承诺计算,当 Verifier 挑战表格编码的多项式时,Verifier 可以自行计算挑战点的多项式求值,因为这个运算复杂度仅为 。这样 Prover 可以节省大量的计算时间。

1. 什么是 MLE-Structured

按照 [Lasso] 论文的定义,MLE-structured 是指任何 MLE 多项式 可以在 时间的计算复杂度内完成求值运算。这里 为多项式未知数的个数。

哪些表格具有这种 MLE-structured 的性质呢?下面给出一些常用的例子:

  • Range check 表格,
  • 连续偶数或者奇数构成的表格,
  • Spread 表格。一种在数字二进制表示的相邻两位插入 0 的表格,例如,对于 。这种表格用来加速实现位运算。

2. Generalized Lasso

Generalized Lasso 可以构造针对 MLE-Structured 表格的 Indexed Lookup Argument。其核心是证明下面的等式:

这里 为查询向量,长度为 为表格向量,长度为 为表格选择矩阵,其中每一行是一个 Unit Vector。而 MLE 多项式 编码了 编码了 ,而 编码了 矩阵。

Prover 和 Verifier 需要证明每一个 等于某个表项 。他们共同拥有的 Public Inputs 为表格向量与查询向量的多项式承诺,因为我们现在只关注 Indexed Lookup Argument,因此他们还共同拥有 的多项式承诺。

协议的第一步是 Verifier 发送一个挑战向量 ,使得上面的约束转化为:

Verifier 可以通过查询 Oracle 来得到 的值,我们记为 。于是上面的等式就归约到了一个求和式:

此刻,Prover 和 Verifier 可以调用 Sumcheck 协议来完成求和式的证明。但是 Prover 需要计算 的值。

在 Sumcheck 协议的结尾,Prover 和 Verifier 可以利用多项式承诺的求值证明,证明 处的求值。尽管我们可以使用 Spark 稀疏多项式承诺来降低最后的求值证明的开销,但是 Prover 在 Sumcheck 协议过程中的计算量至少是

下一节我们介绍 Generalized Lasso 如何再次利用 矩阵的稀疏性,减少 Prover 在 Sumcheck 协议中的计算量。在此之前,我们先列出 Generalized Lasso 的协议框架:

协议框架

公共输入:

  1. 表格向量 的承诺:
  2. 查询向量 的承诺:
  3. 表格选择矩阵 的承诺:

第一轮:Verifier 发送挑战向量

第二轮:Prover 计算 的值,并且连同求值证明 一起发送给 Verifier

第三轮:Prover 和 Verifier 进行 Sparse-dense Sumcheck 协议,证明下面的等式:

经过 Sumcheck 协议,上面的约束等式被归约到:

第四轮:Prover 发送 与 求值证明 给 Verifier

第五轮:Verifier 验证下面的等式:

3. Simplified Sparse-dense Sumcheck

这一节,我们分析下 Prover 在 Sumcheck 协议中的开销,以及如何利用 的稀疏性质来减少 Prover 的计算量。

再重复下 Sumcheck 要证明的求和等式:

这里我们用 代替 ,它是一个稀疏的多项式,只有 个非零项。而 是一个 MLE-structured 的多项式,它的计算复杂度为

Sumcheck 协议总共 轮,在每一轮,Prover 主要的计算量为计算一个一元多项式并发送给 Verifier:

注意到这个一元多项式 个项的求和,但是 是一个稀疏的 MLE 多项式。如果 处的取值为零,那么 Prover 也就可以省去计算 的开销。因此,Prover 实际上只需要计算 个项的求和,而不是 个项。

进一步展开 的定义,我们可以得到:

其中 定义为 中非零项的索引集合。因此 求和式可以进一步简化为 项的求和:

每一轮,假设当前我们在第 轮,Prover 要计算 个项的求和,每一项包含两个乘法和两个 MLE 多项式的求值,分别为 。接着 Verifier 都会提供一个随机数 来求值 ,然后 Sumcheck 进入下一轮,即第 轮。

在第 轮, Prover 的策略是根据上一轮(第 轮)的 的求值来增量式的递推计算 ,即用 来代替

然后我们观察下 的定义,

注意到等式右边的三个乘积因子中的最右边一个恒等于 1。如果

那么当我们用 来代替 时,

因此,根据 还是 ,Prover 可以仅用一个乘法即可递推地计算出第 轮所需要的 。又因为总共有 需要计算,所以 Prover 要付出 的计算量。

Prover 可以维护一个长度为 的数组,里面保存 的值,每一轮过后就更新这个数组:

但是对于 这个求值运算,如果 没有内部结构,那么 Prover 需要老老实实进行求值运算。这样每一轮中 Prover 仍然需要执行 次 MLE 运算求值过程。在 轮的 Sumcheck 协议过程中,Prover 总共的计算量至少为

如果 恰好具有 MLE-Structured 性质,那么 ,那么 Prover 的计算量为

进一步,如果 具有「局部 bit 相关」的特性,即我们可以采用计算 的方法给出 的递推计算式:

这里 是两个多项式,他们的计算复杂度为 。如果 的递推计算能满足上面的等式,那么 Prover 就可以同样维护一个长度为 的数组,保存 所对应的 的值。这样 Prover 可以在每一轮中只需要总共 的计算量来计算更新所有需要用到的 的值。

这样一来, Prover 的计算量可以进一步降低为

下面举一个简单例子,来演示下整个过程。假设 ,一个稀疏的向量 ,表格向量 。稀疏向量的非零值数量为

Sumcheck 协议要证明的求和式为:

Prover 预计算 的值为 1,

在第 0 轮中,Prover 计算

可以看出,Prover 只需要计算 这四个多项式的值。而这四个多项式的计算量为 。Prover 发送 作为 的点值形式发送。

Verifier 发送挑战数 ,Prover 和 Verifier 检查

然后共同计算 作为新的求和。

Prover 更新

下面是第二轮,Prover 计算

Prover 只需要计算 这四个多项式的值。而这四个多项式的计算量为 。Prover 发送 作为 的点值形式发送。

Verifier 发送挑战数 ,Prover 和 Verifier 检查

Prover 维护 数组,更新到

到了第三轮,Prover 计算

如果 有内部结构,那么

Prover 发送 作为 的点值形式发送。

Verifier 发送挑战数 ,Prover 和 Verifier 检查

Prover 和 Verifier 最后通过 PCS 来验证下面的 Evaluation 等式:

Prover 更新 ,则得到

Prover 可以通过 来计算得到 ,计算时间复杂度为

Prover 并不需要发送 ,因为这个值可以由 Verifier 直接计算得到,计算时间复杂度为

综上,Prover 的计算量为

4. Standard Sparse-dense Sumcheck

标准的 Sparse-dense Sumcheck 可以把 轮的 Sumcheck 过程拆分成 个分段,在每个分段中,Prover 都预计算一些辅助的向量,从而避免在接下来的 Sumcheck 分段中做一些重复的计算。这个分段加预计算的步骤被称为 Condensation。通过这种方法,Prover 的计算量可以从 降到 ,其中 ,即

4.1 理解 Condensation

我们先描述一个 Sparse-dense Sumcheck 简单情况。假设查询表格 中的每一个表项 都可以用它的 index 的二进制位来计算,例如 的值可以通过下面的方式计算:

其中 的表格索引。 那么如果给定 的值,我们可以在常数时间内计算 的值。

上面这个等式想要表达的含义是:表格的每一项可以表达为该表项的索引(Index)的线性组合,并且是关于 Index 的二进制位的一次多项式。例如 RangeCheck 表格就满足这个特征。

回忆 Generalized Lasso 协议,其核心是证明下面的等式:

通过 Verifier 提供的一个随机挑战数,上面的等式可以转化为:

,那么等式转换为一个 Inner Product 的形式:

等式右边是一个 项的求和式,如果直接让 Prover 去计算每一项中的 ,那么 Prover 的计算量至少为 次 evaluation。但是我们可以利用 的内部结构来进行优化。首先 编码了一个长度为 的向量,记为 ,它相当于也编码了矩阵 的信息,只有 个非零值。因此我们只需要 就可以计算出所有的 在所有 处的取值。其次 编码了 ,它是一个 MLE-structured 的表格,其每一项都与 Index 的二进制位有关,因此每一个表项 都可以在 时间内计算得到。最后,考虑求和式 中若 ,那么我们就不需要再计算 。因此,这个求和式整体上也只需要 次 evaluation 即可。

这个 项的求和过程如果使用 Sumcheck 协议来证明,那么需要 轮。在第 轮中,由于我们都可以根据上一轮 来计算 ,因此只需要常数时间的计算量。

我们引入两个辅助的向量 ,Prover 可以在 Sumcheck 协议运行前就计算好 的值,然后 Prover 可以利用这些预计算的向量,在 Sumcheck 协议的前 轮中(记住,Sumcheck 协议总共有 轮,我们假设 )加速计算求和式。这两个向量的每个元素 定义如下:

这里我们引入了一个新的符号:,它是一个二进制串的集合

然后筛选那些二进制串的前 位与 的二进制位相等(我们采用 Big-endian 的表示方式,前面的位为高位),而后面的 位可以任意值。例如,。这个集合中每一个二进制串都是以 打头。

那么 向量的每一个元素 ,是筛选出那些高位等于 的二进制串(长度为 ,然后通过 为索引,计算 的求和。换句话说,我们通过 把前面 项求和式 划分为了 个子集,然后分别对其进行求和。由于 稀疏性, 的计算量也是

再换一个思路去理解,如果我们把 项求和式的计算过程描述成一棵深度为 的二叉树,其中树根(第 0 层)为最后的和。而其中每个叶子节点都是 ,这里 ,因此总共有 个叶子。那么向量 就是这颗二叉树中第 层的所有节点。

同样, 是叶子结点为 的求和二叉树中的第 层。 接下来,我们看看 Prover 在 Sumcheck 协议中前 轮的计算过程(总共有 轮),并且看下这两个辅助向量的作用。

在第一轮中,Prover 要构造一个一元多项式

我们把 按照定义展开,可以得到:

这里的 表示 中非零元素的索引(的二进制表示)的集合。然后把所有的求和号都展开,我们发现当 时,,因此,我们只要关注上面求和式中 对应的那些非零项(这时候 ):

上面的等式已经变成了一个 项的求和式。接下来我们根据 的结构性,展开 ,并且根据 的 Tensor 结构,即 , 我们可以得到:

我们再展开 ,可得:

这时候,我们可以代入之前计算的辅助向量 ,其中第 项为

注意到, 是按照前 位进行划分后的第 个子集的求和。我们可以把 重新写成 两个 项的求和式:

这样 Prover 只需要 的计算量就可以完成第一轮的计算,这个计算包括计算 处的求值运算。

那么第二轮开始到第 轮,每一轮 Prover 都只需要 的计算量就可以完成计算 处的求值。

但是到了 第 轮,情况会发生变化,因为这时候 Prover 要计算 个项的求和,并且 两个辅助向量已经失效。因此,对于下面新的 个轮次,Prover 需要重新计算 ,然后按照上面的方案继续。这样相当于把 轮的 Sumcheck 按照 的数量进行划分,每一个 轮次,Prover 都需要重新计算 ,然后保证 的计算始终是 个项的求和式。每次重新计算 的操作被称为 Condensation。

3.2 一般性 结构

上一小节的讨论基于一个比较强的表格结构:

对于 AND 表格与 SLT 表格等不满足上面结构要求的表格,我们需要放松表格的结构条件。首先, 可以是多个多项式之和,并且每一个多项式 的计算过程基于 index 的二进制表示,当 index 发生变化时, 的值的变化只需要常数个加法和乘法操作:


第一周 (10.15~10.21):

  1. 课程学习资料:

    1. 理解 PLONK 系列
    2. 1-basic concepts
    3. 0xPARC halo2 Introduction
  2. 课程学习目标: Understanding and building user-facing applications with Halo2 and PLONKish proving systems

第二周 (10.22~10.28):

  1. 课程学习资料:
  2. 课程学习目标:
    • 理解 halo2 中 Circuit、region、Layouter、custom gate、constraints 等概念及其 API 代码 represent.
    • 能够理解 Fibonacci 示例和 isZero 示例的电路构建流程

第三周 (10.29~11.04)

  1. 课程学习资料:

  2. 课程学习目标:

    • 掌握 halo2 中 lookup / lookup_any API 的使用,及其常见应用场景(如 Range-check)

第四周 (11.06~11.12)

  1. 课程学习资料:
  2. 课程学习目标:
    • decompose 是一个综合性的代码实践,要求我们在阅读源码的同时掌握 halo2 的核心 API。

第五周 (11.13~11.19)

① zkEmail

本周以 zkEmail 为例的 zk 应用实例的 halo2 真实应用场景代码实践。

zkEmail 学习方法论:

首先要明确学习目标,学习 zkEmail 能学到什么?我觉得有以下几点:

  1. zk email 是一个解决“实际业务”的中型电路系统;
  2. zk email 是强烈依赖 halo2-base 的,而 halo2-base 对 halo2 电路有着一定的优化,主要体现在接口优化和灵活的行列设计;
  3. zk email 中会涉及到 RSA,biguint,sha256,regex,base64 等电路;代码模块比较清晰,每个模块也有例子,一周时间学习全部的内容有点多,大家可以根据需求学习;

如果有对以上有兴趣的同学可以学习 zk email 的学习路线建议:

  1. 我觉得可以先从 halo2-base 入手,学习 axiom 对 halo2 做的优化;
  2. 选择一个感兴趣的模块分析代码切入,逐步深入,比如 从 biguint 计算 => RSA , 逐步把 RSA,Regex,Sha256 这几个电路理解,再理解 zk email 是如何像堆积木一样把整个大电路堆积起来的;

halo2-zk-email 中可以学习到的更多是在电路前端设计上,规模属于中型应用,可能还是花费一些时间的。

  • 由 @DK(零与一)@secbit 帮助整理 ~

top-down 的 approach:

  • 由 @Kurt Pan 帮助整理 ~

② zkEVM

下次 oh 会介绍一下 zkevm。以 PSE/Scroll zkevm-circuit 代码为例子。

可以先通过 https://www.evm.codes 了解一下 EVM 指令集,在 playground 玩一下感兴趣的指令。

然后,通过下面的资料了解下 evm bytecode 的组成格式,执行过程。

最后,推荐 PSE 的 zkevm-specs,文档中详细定义了 zkevm 电路的约束,并且提供了对应的 python 代码。








**前置学习 (PLONK & Rust):**

必备学习材料:

  1. 0xPARC Halo2 是主要参考的视频课程
  2. 参考代码 是对课程 Reference 代码必要的优化和版本升级
  3. 参考文档: z2o-k7e-Halo2 是部分电路图解 & 对代码更细致的阐释(仍更新中…) 1
  4. 其他必读参考:
    1. zcash halo2 book / github 理论内容偏多,可以当做字典反复查看
    2. zcash halo2 book 中文翻译 / github

其他参考资源:


for 纯新手:

  1. 新手学习 ZKP 可以参考新手村入门攻略 https://learn.z2o-k7e.world/zk-everything/zk-roadmap.html (must)
  2. 学习 PLONK 郭老师的系列没跟上非常正常,一般正常人都大概需要反复读 5 遍,各种概念不断交叉记忆,相互关联,然后才能建立一些直观的印象
  3. 有了一定的印象后,可以参考 @Harry L 为文档写的 python 代码,手敲印证,加深印象 (optional)
  4. 学习 Rust 编程 (must)
  5. halo2 学习方法论: https://learn.z2o-k7e.world/halo2/halo2.html (must)

!有任何看不懂的 Part 都可以提 issue 或者 Q&A ~

备注:因为现在还没有关于 halo2 特别好的一站式课程,所以很多参考资源需要反复研究 & 交叉印证

1

目前我们正在对部分教程内容进行重新的整理&修改,会随着课程逐步更新和优化,欢迎 👏🏻 PR 和 issue !

Halo2核心概念

在前面的 prerequisite 课程中,我们学习了 PLONK 协议及其 lookup table 优化,在本节我们将会以 halo2 这个 Rust library 为基础,详细讲解 Halo2 的相关基本概念。

Halo2 电路结构

我们知道,在 Vanilla PLONK 协议 中,门约束系统相对固定和局限,表现力并不强:

为了支持更复杂和更高阶的运算,halo2 中引入了 custom gatelookup table,这使得约束系统中的约束并不限定在某一行上的变量,custom gate 可以任意指定约束需要的计算。 1

如下图,可以清晰地看到 PLONK 每个版本的演进,从而让 halo 2 能对 custom constraints 和 lookup table 进行支持:

在一般的电路代码结构中,都会有输入、输出、约束等必要构件,在 halo2 中也不例外。只不过,不像 R1CS 那般每个约束都严丝合缝地写成 的格式,halo2 中,电路的 arithmetization(电路中的 expression) 可以形象地看作是成一张矩形表 (table),所有的约束则可以通过在这张表中规划区域(region),放置单元格(cells) 来构造。

The halo2 Book: The language that we use to express circuits for a particular proof system is called an arithmetization. Usually, an arithmetization will define circuits in terms of polynomial constraints on variables over a field. 2

如下图可以看到:

  • 电路表整体由单元(cell)、列(Column)和行(Row)组成,
  • 相邻的cellrowcolum 的任意组合可以构成 region
  • 列又可以分为三种类型:adviceinstanceselector
    • 其中 advice/instance columns 在同一个电路中填入的值可以不一样
    • fixedselector columns 在电路 preprocessed 阶段生成,同一个电路填入的值是一样的,可以视为是电路结构固定的一部分

下面,我们会分别详细讲解各部分组件的用途及使用方法 3

Columns

我们可以将电路的输入输出概念化为给定有限域 行的矩阵

下面我们来详细讲解各个 columns 的含义和用途:

instance columns :包含了 Prover/Verifier 之间共享的输入,通常用于公共输入 (public inputs)

  • 例如 SHA256 的结果
  • Merkle Tree 的根

advice columns:包含了 private input & 电路运行中所需的中间变量,即witness,这部分只有知道

fixed columns :包含在 key generation 阶段设置的 preprocessed values,可以视为是电路结构固定的一部分,也是可以被 pre-compute 的

  • 如电路中 hardcoded constant
  • 查找表 Lookup table column

selector columns :即选择器,同样是在 key generation 阶段设置的 preprocessed values

有些教程中也会直接将 selector columns 放入 fixed columns 中,这完全正确!因为 Selector 就是特殊的 fixed columns

Tips: 同一行可以支持若干种不同的约束, 比如三元三次, 或者三元二次, 可以通过 selector 来选择具体需要满足哪个约束。比如有 3 个 custom gate, 可以只满足其中一个就 OK , 或者满足其中的 2 个,非常灵活

Rows

矩阵中的行数通常是 2 的幂,受有限域 F 的大小限制; 行数对应于 Plonkish 算术化(arithmetization) 中的 n-th 单位根(nth root of unity)。 约束适用于所有行(apply to all the rows),但可以通过选择器列中定义的 Selector 启用/禁用。 4

Gate

门(Gate) 通常是由一组约束构成,这组约束通常受 selector 控制。 Halo2 提供两种类型的门:

  • 标准门(Standard gate):标准门支持通用算术,例如域乘法和除法
  • 自定义门(Custom gate):自定义门更具表现力,能够支持电路中的专门操作; 下面的斐波那契电路显示了自定义门的示例(请注意,启用选择器时,门将应用于每一行)

Copy constraint

Plonk 的「拷贝约束」是通过「置换证明」(Permutation Argument)来实现,即把表格中需要约束相等的那些值进行循环换位,然后证明换位后的表格和原来的表格完全相等。

Permutation Argument 提供了一种 cheap 的方式来证明集合中部分值的相等性。

如下图,在 Fibonacci 示例中,我们会通过 copy_advice API 强制约束红色框、蓝色框的 2 对值分别相等

Region

如果是第一次看视频或者读相关文档,你可能会发现 Region 是一个略显抽象的概念,不过没关系,向下读!

在 halo2 中我们不会直接约束一整个电路的行和列,而是将整个电路划分为由相邻的行和列组成的 region,在 region 中可以采用相对偏移(relative offsets)的方式访问 Cell。 在一个 region 中,我们只关心 cells 之间的相对关系

如果两个约束没有关系,或者您也不关心两个 “cell” 之间如何相互作用的话,那么就应该将它们分别定义在 2 个不同的 regions 中,如此就可以将控制权交给halo2默认的layouter,让 layouter 去优化整体电路 region 分布,比如合并不同的region到一行来减小电路的规模(layouter 后面会讲解)。

因此,并不推荐将整个电路的逻辑都塞进同一个庞大的 region,您应该尝试将其尽可能分解为逻辑清晰、结构简单的 regions。 5

Layouter

看完了 region 的定义和讲解后,是不是还是有点不得要领?下面我们会介绍如何使用 Layouter 去布局 region,看完后也许你会对 region 有更进一步的认识。

layouter 作用在 assignment (电路赋值)期间,即当你用 Witness 去填充整个 Circuit table 时使用。实际中,layouter一般不会一下子填满整个 table, 而是每次都会创建一个 region,并在其包含的单元格中填入相应的witness值。

为了保证每个 gate 能当访问到其所需的所有单元格,一般而言对 gate 所在的 region 进行电路布局时,region 需遵循如下规则: region 不需要与 custom gate 具有相同的形状,但 region 必须覆盖所有相关的 custom gate

比如上面的例子,在最上方的电路包含两个 custom gate (红色边框标识),可以创建如下两种region:

  1. 左边的浅绿色矩形 Region 是 Valid Region ✅ :

    • 在 Region 中做的赋值 assignments 已经完全覆盖了左边的 Custom Gate 所需要的
    • 假设在 selector col 中,上面的 , 下面的 ,则说明用到了左边的 3 个 Cell
  2. 而右边的浅绿色矩形 Region 是 invalid Region ❌:

    • 它没有覆盖与 Custom Gate 相关的所有单元格。 并且它并没有 assign Custom Gate 所需的所有单元格
    • 如果您打开了 selector,Region 应该覆盖由 Custom Gate 控制的所有单元格。

目前在 halo2 中有 2 种 Layouter 可供选择:

  1. SimpleFloorPlanner (重点关注,也是最常用的)
  2. TwopassPlanner ? V1/V1Plan ?

SimpleFloorPlanner

  • 这是一个单通道布局器 (single-pass layouter)
  • 它为该区域中使用的每一列找到第一个空行并获取其所需的最多的单元格。
  • 它尝试尽可能多地合并相关的 regions 以使用更少的行

Region 的布局根据电路可以有各种形状,如:

  • Region 1 : use one cell for 3 advice column
  • Region 2 : “L shape”
  • Region 3 : “L shape”

以下几个 Q&A 可以帮你进一步理解 region:

  • Q:region 1 为什么不需要选择器?
    • A:你可以认为 region 1 是你想要初始化的一些 private input,它不涉及任何 selector,即这一行的门约束必须成立
  • Q:如下图,为什么 region 4 不向上填充到红色区域?
    • A:对于 Region 4 , 它本可以填到红色区域里面, 但是这不是咱们 SimpleFloorPlanner 能做的事 ~ (@Dr. Shen haicheng)

Diagrams

在 Halo2 中可以通过输出 diagrams 上述电路布局图,以非常直观地看到电路中所有 columns 的状态和电路整体布局,可以帮我们优化电路、查找 bug 等。

halo2 一般可以通过调用如下 API 来生成电路布局图:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
  use halo2_proofs::{dev::MockProver, pasta::Fp};
  use super::*;

  // ...
  #[cfg(feature = "dev-graph")]
  #[test]
  fn plot_chap_1_circuit(){
      // Instantiate the circuit with the private inputs.
      let circuit = MyCircuit::<Fp>::default();
      // Create the area you want to draw on.
      // Use SVGBackend if you want to render to .svg instead.
      use plotters::prelude::*;
      let root = BitMapBackend::new("./images/chap_1_simple.png", (1024, 768)).into_drawing_area();
      root.fill(&WHITE).unwrap();
      let root = root
          .titled("Simple Circuit without chip", ("sans-serif", 60))
          .unwrap();
      halo2_proofs::dev::CircuitLayout::default()
          // You can optionally render only a section of the circuit.
          // .view_width(0..2)
          // .view_height(0..16)
          // You can hide labels, which can be useful with smaller areas.
          .show_labels(true)
          // Render the circuit onto your area!
          // The first argument is the size parameter for the circuit.
          .render(5, &circuit, &root)
          .unwrap();
  }
}
}

所需的 Cargo.toml 配置

[features]
dev-graph = ["halo2_proofs/dev-graph", "plotters", "plotters/bitmap_backend","plotters/bitmap_encoder"]

plotters = { version = "0.3.0", default-features = true, optional = true }

需要开启dev-graph才能调用上述命令:

cargo test --dev-graph -- --nocapture chap_1::exercise_1::tests::plot_chap_1_circuit 

如上图,在一个电路布局图里:

  • advice columns(witness) 是   (粉色的)
  • the  cells 说明在电路定义时用到了,是 Region 的一部分
  • the  cells 说明被赋值了
  • the regions 是 fixed columns (preprocessed value)
    • : selector.
    • : constant values,比如 5

对比上图的单通道布局 vs 双通道布局,我们可以观察到一些有意思的结论:

  • 双通道布局器做了更多 region 布局方面的优化,将电路行数由 优化到了 ,不过列数也有所增加
  • 一般而言,我们需要在电路布局做如下权衡:
    • 减少电路使用的空间 (space) ,因为行数越多,fft 操作越多,Prove 过程就越慢
    • 增加电路的列数, 需要 commit 每个列,更多的列数,意味着更多的 commitments,也就意味着更大的 proof size

为什么需要 commit column? 电路结构在 preprocess 阶段已经确定,Prover 分别针对每列进行 commitment,可以有效防止 Prover 作弊,并将后续 commitment batch 起来

为什么 rows 需要 fft ? 在 PLONK 中使用了多项式承诺,可以将证明生成中涉及到大量的多项式求值、以及计算商多项式等,这些都需要使用 fft 来加速运算 (FFT提供了一个高效的方法来转换多项式系数形式和它们的点值表示)。从而验证该行的多项式约束(custom gate)是否得到满足

Halo2 编程模型

Halo2 的编程模式采用 “ 配置-> (计算+存储 +生成证明) -> 验证 ”三个阶段。

配置(configure) 阶段定义约束关系。具体而言就是在 meta.create_gate 中从 tablequery cell,并将 query 的结果 (即 expression,可以简单的理解为从某个 cell 中获取值的方式,在 configure 阶段并不知道具体的值,在第二阶段阶段才会被赋值) 形成约束。

为了便于理解,不妨把 query 过程理解为 PCB 电路板上的探针,在 PCB 电路上探针取到的值输入到示波器或者逻辑分析仪,而 halo2 电路中的值会被用于生成证明和验证约束。 需要指出,在 configure 阶段,电路并没有通电,也就是说,虽然电路板的逻辑约束已经形成,但是具体的值(信号、电流) 尚未被加载到电路中。

计算+存储(synthesize)

  • 在电路的 synthesize 函数中,按照操作指令计算,并将计算结果填入适当的 Cell,相当于在 table上留下计算的 trace

证明生成

  • 在 Prove 阶段,所有用到的 cell 被赋值且形成计算 trace 后,halo2 会具体计算每一列的多项式承诺。

验证 : 在 Verify 阶段,检查所有的约束是否满足。

以上就是 Halo2 的一些关键概念,在下面一章中,我们将会以一个最简的例子尝试使用 Halo2 library 提供的 API 编写电路!


1

borrowed from Star.Li https://mp.weixin.qq.com/s/VerLN8-tqetKs1Hv6m4KLg

3

lots of images borrowed from great 0xPARC halo2 lectures

6

https://www.youtube.com/watch?v=W_zlH2mmtZA 0:41:20 - 0xPARC - # Intro

5

https://www.youtube.com/watch?v=vGQAMQRlN3E 0:30:49 - 0xPARC - L2

7

https://www.youtube.com/watch?v=W_zlH2mmtZA 0:44:41 - Intro

8

https://www.youtube.com/watch?v=vGQAMQRlN3E 0:17:42 - 0xPARC - L2

9

https://mp.weixin.qq.com/s/VerLN8-tqetKs1Hv6m4KLg

4

https://consensys.io/diligence/blog/2023/07/endeavors-into-the-zero-knowledge-halo2-proving-system/

2

https://zcash.github.io/halo2/concepts/proofs.html

Halo2 API 简介

Overview

上节我们介绍了Halo2的核心概念,本节则以 这个电路为例,来说明如何使用 Halo2 API来写电路。 该电路的各个参数说明如下:

private inputs:  a, b, c
public inputs :  out
constraints   :  a^2 * b^2 * c = out

我们的目的是写一个电路,让Prover证明对于公共输入out,他知道三个秘密输入a, b, c满足 这个约束。 从电路的角度,只使用 乘法门乘法选择器,上述约束可以算数化为:

instanceadvice_0advice_1selector_mul
outa
b
c
ab1
ab0
abab1
absq0
absqc1
out0

看到这里可能会产生疑问,这个 table 是如何填写出来的呢?其实,这个填写规则是与门约束一一对应的,而门约束可以很随意设计,所以关键是得确定

  • 有几种门约束
  • 每种门约束涉及哪几个单元格及多项式约束公式 absq 即 ab 的平方。

我们的目的则是:

  1. 利用 Halo2 定义好上述约束 (gates 和 equality constrains),
  2. 并使用其 private value 填好上述表格 (即 assign witness)。

使用 Halo2 编写电路,需要为 struct MyCircuit 实现 halo2_proofs::plonk::Circuit trait, 其中包括以下 3 个关联方法: 1

  • without_witnesses: 创建默认无需 witness 的 Circuit 实例
  • configure: 需要自定义:
    1. 电路配置:
      1. advice cols (即 witness,包含 private inputs 和 trace),
      2. instance cols (Public Inputs)
      3. selector (电路选择器)
    2. custom gate 约束
  • synthesize: 需要根据上述表格实现填充 witness 的逻辑,即按照验证程序的逻辑正确写入将数据写入表格的每个 Cell:
    1. 填充以 Region 为基本单位(多行+多列构成的矩形),可以在 region 中以相对引用的方式引用其他Row
    2. 有两种填充方式: copy_advice (还会产生 equality 约束) + assign_advice(不会产生 equality 约束)

一旦定义好上述 3 个关联方法,Halo2 便可以在电路实例化后调用相关 API 自动运行(不需要手动触发上述函数)上述逻辑来填充 witness 和生成 proof。

创建电路和 Config

根据电路构建所需模块,首先需要确定电路配置,上述电路一共需要四列: 2

  • 2 列 witness(advice) 用来填充上述表格的a0a1列,
    • 其中 3 个 Private inputs: a, bc 填入 a0 列的前三行。
  • 1 列 instance (填入公共输出 out),
  • 1 列乘法门选择器(s_mul);
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct CircuitConfig {
    advice: [Column<Advice>;2],
    instance: Column<Instance>,
    s_mul: Selector,
}

#[derive(Default)]
struct MyCircuit<F:Field> {
    c: F,
    a: Value<F>,
    b: Value<F>
}
}

实现 Circuit trait 的前两个关联方法

根据电路配置以及我们只需要乘法门,来实现configure 方法:

#![allow(unused)]
fn main() {
impl <F:Field> Circuit<F> for MyCircuit<F> {
  fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let advice = [meta.advice_column(),meta.advice_column()];
        let instance = meta.instance_column();

        meta.enable_equality(instance);
        for c in &advice {
            meta.enable_equality(*c);
        }
        let s_mul = meta.selector();
        /* Gate design:
            | a0 | a1 | s_mul|
            |----|----|------|
            |lhs |rhs |s_mul |
            |out |    |      |  
        */
        meta.create_gate("mul_gate", |meta| {
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);
            vec![s_mul * (lhs*rhs - out)]
        });

        CircuitConfig {
            advice,
            instance,
            s_mul
        }
    }
}
}

需要注意的是 Halo2 中为了优化需要通过 enable_equality 明确指定哪些列设置 equality 约束。同时由于要保证 gate 对每一行的 witness 都满足约束,所以只能通过 query_advice 来获取每个门虚拟的输入和输出(实际的值在 synthesize 时填入)以生成多项式约束,即保证 gate 返回的 vec 为 0。

实现 witness 填充

按照表格,一步步填充 witness:

  1. load private inputs a, bc
  2. 分别计算三个乘法的输入输出值 (ab, absq, out),并通过 assign_advicecopy_advice 这两个 API 填充 Cell
  3. 通过 constrain_instance API, 约束 out 所在的 Cell 和 instance 列的第一个 cell 相等
#![allow(unused)]
fn main() {
fn load_private<F:Field>( 
    config: &CircuitConfig,
    mut layouter: impl Layouter<F>,
    value: Value<F>) -> Result<Number<F>, Error> {
    layouter.assign_region(
        || "load private", 
        |mut region| {
            region.assign_advice(
                || "private input", 
                config.advice[0], 
                0, 
                || value
            ).map(Number)
    })
}

fn load_constant<F:Field>( 
    config: &CircuitConfig,
    mut layouter: impl Layouter<F>,
    c: F
) -> Result<Number<F>, Error> {
    layouter.assign_region(
        || "load private", 
    |mut region| {
        region.assign_advice_from_constant(
            || "private input", 
            config.advice[0], 
            0, 
            c
        ).map(Number)
    })
}

fn mul<F:Field>(
    config: &CircuitConfig,
    mut layouter: impl Layouter<F>,
    a: Number<F>,
    b: Number<F>,
) -> Result<Number<F>, Error> {
    layouter.assign_region(
        || "mul", 
    |mut region| {
        config.s_mul.enable(&mut region, 0)?;
        a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
        b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?;

        let value = a.0.value().copied() * b.0.value().copied();
        region.assign_advice(|| "out=lhs*rhs", config.advice[0], 1, || value)
        .map(Number)
    })
}

impl <F:Field> Circuit<F> for MyCircuit<F> {
    //...
    fn synthesize(&self, config: Self::Config, mut layouter: impl Layouter<F>) -> Result<(), Error> {
        let a = load_private(&config,layouter.namespace(|| "load a"), self.a)?;
        let b = load_private(&config,layouter.namespace(|| "load b"), self.b)?;
        let c = load_constant(&config,layouter.namespace(|| "load c"), self.c)?;


        let ab = mul(&config,layouter.namespace(|| "a*b"), a, b)?;
        let absq = mul(&config,layouter.namespace(|| "ab*ab"), ab.clone(), ab)?;
        let out = mul(&config, layouter.namespace(|| "absq*c"), absq, c)?;

        //expose public
        layouter.namespace(|| "expose out").constrain_instance(out.0.cell(), config.instance, 0)
    }
}
}

Mock prove

最后实例化电路,并调用相应的Mock Prover来验证:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use halo2_proofs::{dev::MockProver, pasta::Fp};
    use super::*;
    #[test]
    fn test_simple() {
        // ANCHOR: test-circuit
        // The number of rows in our circuit cannot exceed 2^k. Since our example
        // circuit is very small, we can pick a very small value here.
        let k = 5;
    
        // Prepare the private and public inputs to the circuit!
        let c = Fp::from(2);
        let a = Fp::from(2);
        let b = Fp::from(3);
        let out = c * a.square() * b.square();
        println!("out=:{:?}",out);
    
        // Instantiate the circuit with the private inputs.
        let circuit = MyCircuit {
            c,
            a: Value::known(a),
            b: Value::known(b),
        };
    
        // Arrange the public input. We expose the multiplication result in row 0
        // of the instance column, so we position it there in our public inputs.
        let mut public_inputs = vec![out];
    
        // Given the correct public input, our circuit will verify.
        let prover = MockProver::run(k, &circuit, vec![public_inputs.clone()]).unwrap();
        assert_eq!(prover.verify(), Ok(()));
    
        // If we try some other public input, the proof will fail!
        public_inputs[0] += Fp::one();
        let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
        assert!(prover.verify().is_err());
        println!("simple success!")
        // ANCHOR_END: test-circuit
    }
}
}

其中:MockerProver.run会分别调用实例化电路的 configuresynthesis 函数以生成电路约束和填充 witness 列(详见 Prover 的assign_advice函数)。 prover.verify() 则会检查所有的门、lookup、permuation等生成的约束是否满足。

运行cargo run test_chap_1_simple, 测试成功。

检查 Circuit 布局

同时,还可以利用上节提到的 Halo2 的 tool 输出电路的整个布局图,advice 列均为红色,instance 列为浅蓝色,selector 列为深蓝色;不同的 region 之间由黑色线分隔,填充过值的 advice 和 instance 列的单元格由绿色和浅绿色组成,填充过值的instance单元格则为深蓝色。可根据此图检查电路是否欠约束(under constraint),如果欠约束会明显发现对应的单元格不是绿色

#![allow(unused)]

fn main() {
    #[cfg(feature = "dev-graph")]
    #[test]
    fn plot_circuit(){
        // Instantiate the circuit with the private inputs.
        let circuit = MyCircuit::<Fp>::default();
        // Create the area you want to draw on.
        // Use SVGBackend if you want to render to .svg instead.
        use plotters::prelude::*;
        let root = BitMapBackend::new("layout.png", (1024, 768)).into_drawing_area();
        root.fill(&WHITE).unwrap();
        let root = root
            .titled("Simple Circuit without chip", ("sans-serif", 60))
            .unwrap();

        halo2_proofs::dev::CircuitLayout::default()
            // You can optionally render only a section of the circuit.
            // .view_width(0..2)
            // .view_height(0..16)
            // You can hide labels, which can be useful with smaller areas.
            .show_labels(true)
            // Render the circuit onto your area!
            // The first argument is the size parameter for the circuit.
            .render(5, &circuit, &root)
            .unwrap();
    }
}

运行cargo test plot_chap_1_circuit --features dev-graph,可以输出电路结构图。

从下图可以看出,整个电路一共9行4列,与表格设计一致。 image

总结

我们实现电路时一般可遵循三步法:

  1. 确定电路配置:需要几列
  2. 确定好电路需要怎样的门:乘法门,还是自定义门,还是需要加 lookup。这样就可以实现 Circuit 的 configure 关联方法
  3. 根据电路所需的输入输出,填充好 witness。这样就可以实现 Circuit 的 synthesize 关联方法
1

./halo2_proofs::plonk::Circuit

2

完整代码在 halo2-tutorials: chap-1

Custom gates and Chip

本节我们以一个简单的电路为例,介绍 Halo2 中的自定义门(custom gates) 和 chip 的概念。

在上一节中,我们使用 Halo2 的 API 实现了只包含乘法门的简单电路,但是如果有多种 gate 呢,这种情况如何处理?

Custom gates

在 Halo2 中可通过自定义门(custom gate) 来实现,这里需要回顾下 Halo2 中自定义门(custom gate) 的 mental model 1: image

如上式,自定义门可以由任意多种不同的门线性相加构成,每一个门由选择器和门运算逻辑构成,如上式中第一个加法门选择器为 ,电路逻辑为 ,Halo2中可以通过 create_gate 创建每个门。不过需要注意的是,看起来这些门之间是独立的,但实际上这些门在最终的电路约束检查中会通过乘以一个随机数y,一次行检查一行的 witness 是否同时满足所有门的约束。

问题定义

本节则是证明如下电路:

#![allow(unused)]
fn main() {
private inputs: a,b,c
public inputs: out
constraints:
    d = a^2 * b^2 * c 
    e = c + d
    out = e^3
}

注意到在 vanilla plonk 中约束的 degree 不能超过 2(只支持加法门和乘法门,witness有三列,且门的2个输入和1个输出只能在一行),但 halo2 支持通过 Ultra plonk 来实现更高阶数以及使用更灵活的单元格的 custom gate。这里我们使用一个高阶 custom gate 来实现 这条约束 (注: 其实 Ultra plonk 中乘法门和加法门也可以看作 custom gate,因此下文我们将该这条三次方约束的门称为立方门),相比于原来需要 2 个乘法门实现该约束,custom gate 可以减少帮助约束的行数。

因此,我们可以画出电路 witness 表格:

insa0a1s_muls_adds_cub
outa
b
c
ab100
ab000
abab100
absq000
absqc100
d000
dc010
e000
eout001

看到这里可能会产生疑问,这个 table 是如何填写出来的呢?其实,这个填写规则是与门约束一一对应的,而门约束可以很随意设计,所以关键是得确定

  1. 有几种门约束

  2. 每种门约束涉及哪几个单元格及多项式约束公式

完整代码见 Halo2 tutotials: chap_2/custom_gates

Config

首先,需要明确电路配置(CircuitConfig),即选取所需的 Advices, Selectors 和 Instance 列,并在 fn configure 中创建相应的门。

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct CircuitConfig {
    advice: [Column<Advice>;2],
    instance: Column<Instance>,
    s_mul: Selector,
    s_add: Selector,
    s_cub: Selector,
}

impl <F:Field> Circuit<F> for MyCircuit<F> {
    type Config = CircuitConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let advice = [meta.advice_column(),meta.advice_column()];
        let instance = meta.instance_column();

        meta.enable_equality(instance);
        for c in &advice {
            meta.enable_equality(*c);
        }
        let s_mul = meta.selector();
        let s_add = meta.selector();
        let s_cub = meta.selector();

        meta.create_gate("mul_gate", |meta| {
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);
            Constraints::with_selector(s_mul, vec![(lhs * rhs - out)])
        });

        meta.create_gate("add_gate", |meta| {
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_add = meta.query_selector(s_add);
            Constraints::with_selector(s_add, vec![(lhs + rhs - out)])
        });

        meta.create_gate("cub_gate", |meta| {
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let out = meta.query_advice(advice[1], Rotation::cur());
            let s_cub = meta.query_selector(s_cub);
            Constraints::with_selector(s_cub, vec![(lhs.clone()*lhs.clone()*lhs - out)])
        });

        CircuitConfig {
            advice,
            instance,
            s_mul,
            s_add,
            s_cub
        }
    }
    ...
}
}

这里我们使用了新的 API —— Constraints::with_selector,其实等价于我们之前熟悉的 vec![selecter * gate expression]

填入 Witness

除了上述的加法门和乘法门之外,我们需要为立方运算增加一个填 witness 的辅助函数 fn cub :

#![allow(unused)]
fn main() {
...
fn cub<F:Field>(
    config: &CircuitConfig,
    mut layouter: impl Layouter<F>,
    a: Number<F>,
) -> Result<Number<F>, Error> {
    layouter.assign_region(
        || "cub", 
    |mut region| {
        config.s_cub.enable(&mut region, 0)?;
        a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
        let value = a.0.value().copied()*a.0.value().copied()*a.0.value().copied();
        region.assign_advice(|| "out=lhs^3", config.advice[1], 0, || value)
        .map(Number)
    })
}
...
}

注意: 推导并填入 witness 的方式一定要与上述自定义门中引用的单元格和计算方式一致,否则会导致欠约束约束错误

然后补充 Circuit Trait 中的 synthesis 函数:

#![allow(unused)]
fn main() {
impl <F:Field> Circuit<F> for MyCircuit<F> {
    ...
    fn synthesize(&self, config: Self::Config, mut layouter: impl Layouter<F>) -> Result<(), Error> {
        let a = load_private(&config,layouter.namespace(|| "load a"), self.a)?;
        let b = load_private(&config,layouter.namespace(|| "load b"), self.b)?;
        let c = load_constant(&config,layouter.namespace(|| "load c"), self.c)?;


        let ab = mul(&config,layouter.namespace(|| "a*b"), a, b)?;
        let absq = mul(&config,layouter.namespace(|| "ab*ab"), ab.clone(), ab)?;
        let d = mul(&config, layouter.namespace(|| "absq*c"), absq, c.clone())?;

        let e = add(&config, layouter.namespace(|| "absq + c"), d, c)?;
        let out = cub(&config, layouter.namespace(|| "absq^3"), d)?;

        //expose public
        layouter.namespace(|| "expose out").constrain_instance(out.0.cell(), config.instance, 0)
    }
}

}

测试

实例化电路,并调用相应的 Mock Prover 来验证。

#![allow(unused)]
fn main() {
cargo test test_simple_3gates
}

输出相应的电路布局图cargo test plot_3gates_circuit --features dev-graph: images

可以看出 Halo2 的 Simple Layouter 对乘法门选择器(s_mul) 和加法门选择器(s_add) 做了优化,将这两列合并为了 1 列。

Chip

在上述实现中,我们填入 witness 的函数和 Config 是分离的,为了更好地复用这些代码,类似于集成电路由很多个 Chip 构成,Halo2 一般将一系列紧密相关的实现特定约束的函数(config 以及相应的提供 witness 的函数) 抽象到一个 Chip 模块

具体的代码参考在这里,这里我们简单对比一下 2 部分代码的区别:

在前部分的 custom_gate 的实现中:

  • 手动定义了 load_private(), load_constant()
  • 手动定义了 fn mul() / fn add()/ fn cub() 这几个处理 witness 的函数
#![allow(unused)]
fn main() {
// custom_gate.rs
fn load_private() { ... }
fn load_constant() { ... }
fn mul() / fn add() / fn cub() { ... }

impl <F: Field> Circuit<F> for MyCircuit<F> {
    fn configure()
    fn synthesize() {
        let (a, b) = load_private() ..;
        let ab = mul(a, b);
        // ...
    }
}
}

在本节的 simple_chip 的实现中:

  • 将 load_private()、mul()、cub() 等操作直接封装到了 SimpleChip 中
  • 在实现 MyCircuit 时,直接调用 SimpleChip 里的 configure() 来构建约束; 和 assign() 来填入 witness
#![allow(unused)]
fn main() {
impl <F: Field> SimpleChip<F> {
    fn configure()  // same as in custom_gate.
    fn assign() {
        // load_private:
        assign_region(
            assign_advice
        )
        // like fn mul / fn add() / fn cub()
        copy_advice() .. 
    }

impl <F: Field> Circuit<F> for MyCircuit<F> {
    let chip = SimpleChip::construct(config);
    let out = chip.assign(layouter.namespace(|| "simple ship"), self.a, self.b, self.c)?;
    chip.expose_public(layouter, out, 0)
}
}

建议仔细阅读、对比上下 2 部分代码,体会其在设计上的不同和 chip 这种模块化封装的思维。

我们将本节中的约束抽象为 SimpleChip,将原来独立的 assign witness 的几个函数 (load_privateload_constantaddmulcub) 合并到 Simple Chip 的 assign 方法中。此外,采用如下电路布局压缩所需的行数(在电路中我们只划分了了两个大的 region,这样就减小了复制 ababsqcc 这四个约束) :

insa0a1s_muls_adds_cub
outa
b
c
ab100
abab100
absqc100
dc010
eout001

完整代码见 Halo2 tutorials: chap_2/simple_chip

test & 输出电路布局图

#![allow(unused)]
fn main() {
cargo test test_simple_ship
cargo test plot_chip_circuit --features dev-graph
}

采用 Chip 的电路布局图为: images

references:

1

0xPARC halo2 lectures https://learn.0xparc.org/halo2/

进阶电路布局

在之前的教程中,电路行数相对较少,且相邻 region 之间所采用的 custom gate 也不一样,因此我们可以一行行地手动填入 witness 。本节,我们则考虑相邻 region 都是采用同一种门的电路——Fibonacci 数列,学习采用循环来复用函数填入 witness 代码;

在此之后,考虑另一种电路布局的 Fibonacci 电路实现,来初步体会如何优化设计电路布局。

Fibonacci

问题定义

Fibonacci 数列是证明如下问题:

public inputs: a, b, f(N)  
constraints: 
    f(0) = a
    f(1) = b
    f(n) = f(n-1) + f(n-2), n = 2,...,N

注意:N 为 struct Circuit 中的电路配置,它不是电路结构的一部分,只是用来辅助生成电路,不同的 N 对应着不同的电路,N 确定之后,可以认为电路的 verification key 就确定了。代码如果开源,任何人都可以阅读到这个 N 的取值。

举个例子, 是已知的 public input,Prover 想生成一个 proof,证明他知道

这里读者可能会有疑问,我都能口算出来的结果,还有必要 Prove 嘛?这里不必过分纠结这个例子的实际用途,暂时 focus 到 Halo2 API 和电路布局即可,后面也会讲解到更加 make sense 的进阶电路 ~

对于该电路例子,我们采用只包含一列 Advice 的电路配置,使用上下相邻三行单元格组成的加法门,并且只划分为一个大 region , 在 region 内部通过 offset 来确定要填入的单元格。

Fibonacci 电路的 witness 表格为:

instancea0seletor
af(0)=a1
bf(1)=b1
outf(2)1
f(3)1
f(n-2)1
f(n-1)0
f(n)=out0

为什么 advice 中, f(n-1) 行没有约束 selector? —— 这一点后面会讲到。

完整代码参考

Config

如前所述,电路配置只需一列 Advice 和 一列 selector, 我们将此电路定义为 FiboChip

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct FiboChipConfig {
    advice: Column<Advice> ,
    instance: Column<Instance>,
    selector: Selector,
}

#[derive(Debug, Clone)]
struct FiboChip<F:Field>{
    config: FiboChipConfig,
    _marker: PhantomData<F>
}

#[derive(Debug, Clone)]
struct ACell<F:Field> (AssignedCell<F,F>);

impl <F:Field> FiboChip<F> {
    fn construct(config: FiboChipConfig) -> Self {
        FiboChip {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: Column<Advice> ,
        instance: Column<Instance>,
    ) -> FiboChipConfig {
        let selector = meta.selector();
        meta.enable_equality(advice);
        meta.enable_equality(instance);

        meta.create_gate(
            "fibo gate", 
            |meta| {
                let cur_row = meta.query_advice(advice, Rotation::cur());
                let next_row = meta.query_advice(advice, Rotation::next());
                let third_row = meta.query_advice(advice, Rotation(2)); 
                let s = meta.query_selector(selector);
                vec![s*(cur_row + next_row - third_row)]
            }
        );

        FiboChipConfig {
            advice,
            instance,
            selector
        }
    }
    ...
}
}

FiboChip 增加填入 witness 的方法 assign_witness

这里我们假设 FiboChip 计算的行数 nrow 由调用它的电路提供。然后通过 mut f_premut f_cur 结合 for 循环一行行填入 witness。

#![allow(unused)]
fn main() {
impl <F:Field> FiboChip<F> {
    ...
    fn assign_witness(
        &self,
        mut layouter: impl Layouter<F>,
        nrow: usize
    ) -> Result<ACell<F>, Error> {
        layouter.assign_region(
            || "fibo",
            |mut region| {
                let instance = self.config.instance;
                let advice = self.config.advice;
                let selector =  self.config.selector;
                selector.enable(&mut region, 0)?;
                selector.enable(&mut region, 1)?;
                let mut f_pre = region.assign_advice_from_instance(
                    || "f0", instance, 0, advice, 0).map(ACell)?;
                let mut f_cur = region.assign_advice_from_instance(
                    || "f1", instance, 1, advice, 1).map(ACell)?;
              
                for i in 2..nrow{
                    if i < nrow -2 {
                        selector.enable(&mut region, i)?;
                    }
                    let value = f_pre.0.value().copied() +  f_cur.0.value();
                    let f_next = region.assign_advice(
                        || "fn", advice, i, || value).map(ACell)?;
                    f_pre = f_cur;
                    f_cur = f_next;

                }
                Ok(f_cur)
            }
        )
    }
}
}

核心逻辑:

  • 如果 i 小于 nrow - 2,则启用选择器 Selector。这意味着 Selector 只对数列的前 nrow - 2 个数字启用
  • 计算下一个斐波那契数 f_next 为前两个数字 f_pref_cur 之和,并将其分配到 advice column
  • 更新 f_pref_cur ,为下一次迭代使用

注意: 最后两行不需要 enable selector 了,因为这里使用的门约束的是相邻的三个 row,输出值 f(N) 已经在第 n-2 个行的门中被约束了。

实现 Circuit Trait

最后使用 FiboChip 来创建 FiboCircuit 电路,并实现 Circuit Trait:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone,Default)]
struct FiboCircuit<F:Field> {
    nrow: usize,
    _marker: PhantomData<F>
}

impl <F:Field> Circuit<F> for FiboCircuit<F> {
    type Config = FiboChipConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        FiboCircuit::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let advice = meta.advice_column();
        let instance = meta.instance_column();
        FiboChip::configure(meta, advice, instance)
        
    }
    fn synthesize(&self, config: Self::Config, mut layouter: impl Layouter<F>) -> Result<(), Error> {
        let chip = FiboChip::construct(config);
        let out = FiboChip::assign_witness(&chip, layouter.namespace(|| "fibo table"), self.nrow)?;
        //expose public
        layouter.namespace(|| "out").constrain_instance(out.0.cell(), chip.config.instance, 2)
    }
}
}

注意: 具有不同 nrowFiboCircuit 实际上对应的是不同的电路。

电路布局图为: images

另一种 Fibonacci 电路布局

Halo2 中电路的布局主要与怎么设计门有关,需要在如下变量之间权衡:

  1. witness 多少列
  2. selector 多少列
  3. 门的 dgree、门的输入输出需包含哪些相邻单元格

上一小节我们采用 1 列 witness 、1 个 selector 和包含相邻三个单元格的加法门,这次我们则选择 2 列 witness 、1个 selector 和相邻两行四个单元格组成的门,来重新设计 Fibonacci 电路。电路的 witness 表格设计如下:

insa0a1seletor
af(0)=af(1)=b1
bf(2)=bf(3)1
outf(4)f(5)1
f(2n/2)f(2n/2+1)1
其中 n 依然为 Fibonacci 计算的 f(n) , 这里需要注意最后一行,我们即使在 n 为偶数也将这行填满,同时根据 n 是否为奇数来约束out: out === n % 2 == 0 ? f(2n/2) : f(2n/2 + 1)

完整代码见Halo2 tutorial: chap_3/circuit_2.rs

其他的代码与上一小节基本类似,这里只对使用到的 custom gate 进行说明:

#![allow(unused)]
fn main() {
    meta.create_gate( "fibo gate", |meta|{
        let s = meta.query_selector(selector);
        let cur_left = meta.query_advice(advice[0], Rotation::cur());
        let cur_right = meta.query_advice(advice[1], Rotation::cur());
        let next_left = meta.query_advice(advice[0], Rotation::next());
        let next_right = meta.query_advice(advice[1], Rotation::next());
        Constraints::with_selector(s, 
            vec![
                (cur_left + cur_right.clone() - next_left.clone()), 
                (cur_right + next_left - next_right)]
            )
    });
}

这里我们使用了 Constraints::with_selector 的一个更高级用法,使得当 selector enable 时,如下两个约束需同时成立:

  1. f(2n-2) + f(2n-1) = f(2n)
  2. f(2n-1) + f(2n) = f(2n+1)

电路布局图为: images

总结

不同的电路布局没有绝对的优劣之分,它们都是在行数、列数、degree 、permuation 包括后面会涉及到的 lookup 等进行权衡,最终这些决定了 prover 计算成本与 proof 大小之间的权衡。

lookup

前面的章节中我们介绍了 Halo2 的 API 和电路布局等核心概念,这节则介绍 Plonk 证明系统中基于 permutation argment 抽象出的一个强大功能 lookup 。Halo2 中使用的 lookup 对 plookup 进行了简化,可以约束一列或多列的 cells 的值属于对应的 lookup 列。

如下图,我们可以通过 lookup 约束 两列中的第 1、3 行属于查找表( 列)中的某一行; 对于不需要查找的则约束其属于查找表的某个默认行(自己指定)。 images

关于 Zcash 版本的 lookup,有两个问题需要注意:

  1. 多列 lookup 不同列的相同行必须同时存在于查找表中,Halo2 底层通过一个随机数将各列对应行的 row 组合起来使其回归到单列 lookup;
  2. 多列 lookup 时,需约束的多列 witness 不一定在同一行(如下图中的 在第一行, 在第二行,这 2 个 witness 不同行),但是查找表中对应的所有列是在同一行(在 lookup table 中 2 个 witness 对应的 42, SHA(42) 是需要再同一行的)。比如上述例子也可以进行如下的 lookup: images

同时可参考对不在同一行的两列进行 lookup 的示例代码: halo2-tutorials/chap_4/circuit_1

单列 lookup

首先从最简单的单列 lookup 介绍如何使用 lookup API。我们的目的是证明 a[N] 数组里的每一个 value 都在 [0, RANGE] 这个范围里面:

private inputs: a[N]
constant: RANGE
s.t: a[i] ∈ [0, RANGE], ∀ i ∈ [0, N-1]

若直接采用 gate 约束的话,我们需要设计形如

这样一个连乘表达式约束的电路,这种方式固然直观,但问题在于:当 RANGE 很大时 (比如 ),电路的 degree 会很高, 这会导致整个 proof 很大。

因此,我们可以采用 lookup 方式实现(回忆 permutation argument 只会将 degree 增加 1),将 这些值填入 TableColumn 中 ( Zcash 版本的 Halo2 只支持静态查找表, 因此只能填入 TableColumn),并借助 lookup API 来证明 所有的 a[i] 属于该 TableColumn 的某一行。

整个电路结构如下:

advq_lookuptable
a[0]10
a[1]11
a[2]12
a[3]13
a[4]14
a[N]1N-1
0N
0RANGE

完整代码见: halo2-tutorials/chap_4/circuit_2

电路配置及 lookup 约束

如上表电路需要 1 列 Advice , 1 列 TableColumn, 1 列 Selector :

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct RangeConfig<F:PrimeField, const RANGE: usize, const NUM: usize>{
    value: Column<Advice>,
    table: LookUpTable<F, RANGE>,
    q_lookup: Selector,
}
}

该电路配置定义为 RangeConfig,将其中的查找表单独定义为 LookUpTable。下面代码的 [(q_lookup * v, table.table)] 即表示:当 q_lookup 启用时,v 需要存在于 table 查找表中。相应代码为:

#![allow(unused)]
fn main() {
impl <F:PrimeField, const RANGE: usize, const NUM: usize> RangeConfig<F, RANGE, NUM>{
    fn configure(meta: &mut ConstraintSystem<F>,value: Column<Advice>) -> Self {
        let q_lookup = meta.complex_selector();
        let table = LookUpTable::<F,RANGE>::configure(meta);
        meta.lookup(|meta| {
            let q_lookup = meta.query_selector(q_lookup);
            let v = meta.query_advice(value, Rotation::cur());
            vec![(q_lookup * v, table.table)]
        });

        RangeConfig {value, table, q_lookup}
    }
    
    fn assign(
        &self,
        mut layouter: impl Layouter<F>,
        value: [Value<Assigned<F>>;NUM]
    ) -> Result<ACell<F>,Error> {
        
        layouter.assign_region(|| "value to check", |mut region|{ //instantiate a new region, so it's not ref
            self.q_lookup.enable(&mut region, 0)?;
            let mut cell= region.assign_advice(||"value", self.value, 0, || value[0]).map(ACell);
            for i in 1..value.len() {
                self.q_lookup.enable(&mut region, i)?;
                cell = region.assign_advice(||"value", self.value, i, || value[i])
                .map(ACell);
            }
            cell
        })
    }
}

}

lookup 必须使用 complex_selector,因为 Halo2 可以根据这个标记知道这种 Selecotr 列不需要优化,而普通的 Selector 则可能会被 Layouter 进行合并等优化操作。

对于 LookUpTable, 使用 assign_table 来填充 witness :

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub(crate) struct LookUpTable<F:PrimeField, const RANGE: usize> {
    pub(crate) table: TableColumn,
    _maker: PhantomData<F>
}

impl <F:PrimeField, const RANGE: usize> LookUpTable<F, RANGE> {
    pub fn configure(meta: &mut ConstraintSystem<F>) -> Self {
        let table = meta.lookup_table_column();
        Self {table, _maker: PhantomData }
    }

    pub fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error>{
        layouter.assign_table( || "load range lookup table", 
        |mut table|{
            for value in 0..RANGE{
               table.assign_cell(||"table cell", self.table, value, || Value::known(F::from(value as u64)))?;
            }
            Ok(())
        })
   
    }
}
}

电路具体的 Trait 方法实现就比较简单了,在此不再赘述。

多列 lookup

还可以对多列 witness 与对应的多列查找表进行 lookup 约束。本小节考虑如下例子: 证明某个值只有 bit[N] 比特位:

private inputs:  a[N], bit[N]
s.t: a[i] ≤ 2^bit[i] -1,  ∀ i ∈ [0, N-1]

具体来说,假设对于一个 8 位的查找表,它可以存储 个可能的值(即 [0,255] ),但我们并不总是需要用所有 256 个值,也就是即使查找表可以存储 8 位的值,我们也可以使用它来约束小于 8 位的值。

例如,对于一个 1 位的值,我们只需查找两种可能的值(0 或 1);对于一个 2 位的值,我们可以查找四种可能的值(00、01、10 或 11)…

举个具体的例子:比如某 Prover 想证明其 value 确实只有 5 位 (即 value <= 2^5 - 1 = 31 ) ,

  • (value = 31/32, bit = 5 ) 是 private value.
  • 对于 value = 31,其二进制表达为 11111,确实只有 5 位,满足约束
  • 对于 value = 32,其二进制表达为 100000,其位数到达了 6 位,但是 Prover 填入电路的 bits 是 5 ,(这里你可以认为这是一个 malicious Prover),它想作弊,但这是不可能的! Prover 传入的这 2 个 private inputs 不满足 lookup table 在该处的约束。

我们可以设计两列TableColumn, 其中一列 lookup table 为整数值 table_value,另一列为其对应的 bit 位数 table_n_bits

并约束 witness 中值那一列 value 和比特位那一列 bit 属于前述的两列 TableColumn lookup table

valuebitq_lookuptable_n_bitstable_value
v[0]0110
v[1]1111
22
23
34

完整代码见: halo2-tutorials/chap_4/circuit_3

电路配置

这里我们重点关注 lookup 约束相关代码:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct RangeCheckConfig<F:PrimeField, const NUM_BITS: usize, const RANGE: usize>{
    value: Column<Advice>,
    bit: Column<Advice>,
    q_lookup: Selector,
    table: RangeCheckTable<F, NUM_BITS, RANGE>
}

impl <F:PrimeField, const NUM_BITS: usize, const RANGE: usize> RangeCheckConfig<F, NUM_BITS, RANGE> {
    fn configure(meta: &mut ConstraintSystem<F>, ) -> Self {
        //when to configure the colum, during config or circuit instance: configure time
        let value = meta.advice_column();
        let bit = meta.advice_column();
        let q_lookup = meta.complex_selector();
        let table = RangeCheckTable::configure(meta);

        meta.lookup(|meta|{
            let default_value = Expression::Constant(F::ZERO);
            let default_bit = Expression::Constant(F::ONE);
            let mut v = meta.query_advice(value, Rotation::cur());
            let mut b = meta.query_advice(bit, Rotation::cur());
            let q = meta.query_selector(q_lookup);
            let non_q = Expression::Constant(F::ONE) - q.clone();
            v = v*q.clone() + non_q.clone()*default_value;
            b = b*q + non_q*default_bit;
            vec![(b, table.n_bits),(v, table.value)]
        });

        RangeCheckConfig { value,bit, q_lookup, table }
    }
...
}
}

这里我们对于不需要 lookup 的行为其指定默认值保证所有行均满足 lookup 约束, vec![(b, table.n_bits), (v, table.value)] 则范围两组对应的(cell expression, lookup table)

多列错行 lookup table

如下图所示,假若想约束的的 2 列 witness 不在同一行,而是错行的:

images

对于这种情况 halo2 也可以灵活地处理:

#![allow(unused)]
fn main() {
impl<F: PrimeField> LookupChip<F> {
    fn construct(config: LookupConfig) -> Self {
        LookupChip {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> LookupConfig {
        let a = meta.advice_column();
        let b = meta.advice_column();
        let s = meta.complex_selector();
        let t1 = meta.lookup_table_column();
        let t2 = meta.lookup_table_column();

        meta.enable_equality(a);
        meta.enable_equality(b);

        meta.lookup(|meta| {
            let cur_a = meta.query_advice(a, Rotation::cur());
            let next_b = meta.query_advice(b, Rotation::next());
            let s = meta.query_selector(s);
            // we'll assgin (0, 0) in t1, t2 table
            // so the default condition for other rows without need to lookup will also satisfy this constriant
            vec![(s.clone() * cur_a, t1), (s * next_b, t2)]
        });

        LookupConfig { a, b, s, t1, t2 }
    }

    fn assign(
        &self,
        mut layouter: impl Layouter<F>,
        a_arr: &Vec<Value<F>>,
        b_arr: &Vec<Value<F>>,
    ) -> Result<(), Error> {
        layouter.assign_region(
            || "a,b",
            |mut region| {
                for i in 0..a_arr.len() {
                    self.config.s.enable(&mut region, i)?;
                    region.assign_advice(|| "a col", self.config.a, i, || a_arr[i])?;
                }

                for i in 0..b_arr.len() {
                    region.assign_advice(|| "b col", self.config.b, i, || b_arr[i])?;
                }

                Ok(())
            },
        )?;
}

在上面的代码中,

  1. 利用 [(s.clone() * cur_a, t1), (s * next_b, t2)] 这 2 个需要同时成立的约束,我们同时约束了 a 的当前行和 b 的下一行需要存在于多列查找表中。
  2. 在 assign 函数中,我们只对 advice column a 进行了约束,而没有对 advice column b 应用 selector,目的是只对 a 有值的这些行进行约束。如此就给 b 列提供了更多的灵活性。
#![allow(unused)]
fn main() {
    #[test]
    fn test_lookup_on_different_rows() {
        let k = 5;
        let a = [0, 1, 2, 3, 4];
        let b = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
        let a = a.map(|v| Value::known(Fp::from(v))).to_vec();
        let b = b.map(|v| Value::known(Fp::from(v))).to_vec();
        let circuit = MyCircuit { a, b };
        let prover = MockProver::run(k, &circuit, vec![]).unwrap();
        prover.assert_satisfied();
    }
}
  • 如上代码,只对 a 列有值的情况进行了约束,b 列的取值变得灵活。

lookup Debug 相关

在 lookup 电路设计时,以典型的 halo2-tutorials/chap_4/circuit_1 为例,可能会遇到如下报错:

#![allow(unused)]
fn main() {
error: lookup input does not exist in table
  (L0, L1) ∉ (F0, F1)

  Lookup inputs: 每一个 lookup 会出现这一行,整个 lookup 分为两组: L0 和 L1
    L0 = x1 * x0 :,这是第一组 (witness: A0 列, table: F2 列), 电路中为`(b, table.n_bits)`
    ^
    | Cell layout in region 'a b':
    |   | Offset | A0 | F2 |
    |   +--------+----+----+
    |   |    1   | x0 | x1 | <--{ Lookup inputs queried here
    |
    | Assigned cell values:
    |   x0 = 1
    |   x1 = 1

    L1 = x1 * x0 : 这是第二组 (witness: A0 列, table: F2 列), 电路中为 `(v, table.value)`
    ^
    | Cell layout in region 'a b':
    |   | Offset | A1 | F2 |
    |   +--------+----+----+
    |   |    1   |    | x1 | <--{ Lookup inputs queried here
    |   |    2   | x0 |    |
    |
    | Assigned cell values:
    |   x0 = 0x3
    |   x1 = 1
}

理解上述报错,可以让我们更快地调试。报错结构如何理解在上图中均已表明,且 cell 的相对值也体现的很清楚(注意 L1 中的 x0 相对 L0 中的 x0 向下偏移了 1 , 这与电路一致)。

动态查找表 PSE Halo2's lookup_any API

注意到使用 Zcash 版本 Halo2 进行 lookup 约束时,由于没法对 TableColumn 进行 query_advice这导致除了 lookup 约束外,无法灵活地对 TableColumn 中的 cell 进行 gate 约束,即TableColumn必须在电路初始化阶段写死,无法再更改了,即只能进行静态查找。

因此,Zcash 团队的核心开发者为 PSE Halo2 版本开发了 lookup_any API, 使其也支持对任意类型的列,如AdviceFixedColumn 等进行 lookup, 即实现了动态查找表。lookup_any 使用方式与 lookup 没有太大的区别,来看其具体的一些例子

下面的代码中,定义了 2 个 lookup table: instance_table & advice_table :

#![allow(unused)]
fn main() {
	#[test]
	fn bad_lookup_any() {
	
		impl Circuit<Fp> for FaultyCircuit {
			fn configure(meta: &mut ConstraintSystem<Fp>) -> Self::Config {
				let instance_table = meta.instance_column(); // lookup as Instance Columns
				let advice_table = meta.advice_column();
				let a = cells.query_advice(a, Rotation::cur());
				
                meta.annotate_lookup_any_column(instance_table, || "Inst-Table");
                meta.enable_equality(instance_table);
                meta.annotate_lookup_any_column(advice_table, || "Adv-Table");
                meta.enable_equality(advice_table);

                meta.lookup_any("lookup", |cells| {
                    let advice_table = cells.query_advice(advice_table, Rotation::cur());
                    let instance_table = cells.query_instance(instance_table, Rotation::cur());
				// ..
                    vec![
                        (
                            q.clone() * a.clone() + not_q.clone() * default.clone(),
                            instance_table,
                        ),
                        (q * a + not_q * default, advice_table),
                    ]});
		}
		// ...
	let custom_lookup_table = vec![vec![
			Fp::from(1u64),
			Fp::from(2u64),
			Fp::from(4u64),
			Fp::from(6u64),
		]];
	let prover = MockProver::run(
			K,
			&FaultyCircuit {},
			// This is our "lookup table".
			custom_lookup_table,
		)
		.unwrap();
		assert_eq!(
			prover.verify(),
			Err(vec![VerifyFailure::Lookup {
				name: "lookup".to_string(),
				lookup_index: 0,
				location: FailureLocation::InRegion {
					region: (1, "Faulty synthesis").into(),
					offset: 1,
				}
			}])
		);
	}
}

如上代码的动态查找表中:

  • 2 个 lookup table 在 Circuit configure 阶段被分别定义为了 instance column 和 advice columns,而后在 “lookup” 门约束中(lookup_any()),advice col a 须满足 2 个约束:
    • a 需能在 lookup table instance_table 中被查找到,且同时也
    • 能在 lookup table advice_table 中被查找到
  • synthesize() 阶段,advice_table 这个 lookup table 由 instance col 生成(可以理解为复制了一份 instance_tableadvice_table
    • 在 “Good synthesis” 中,advice 的赋值都能在 2 个 lookup tables 中被查找到,满足约束;
    • 而在 “Faulty synthesis” 中,Fp::from(5) 这个 advice 赋值不能在 lookup tables 中被查找到,所以不满足约束。
  • Prover 在 prove 阶段,可以将 Public Input(PI) 或 advice column(witness) 作为 lookup table, 这种方式提供给了 halo2 电路更大的灵活度。

除此之外,还可以参考brainfuck zkvm的例子

Builing blocks

表示 Polynomial 的 2 种方式:

  • coefficients form:
  • evaluation form:

多项式计算 -> FFT & iFFT

  • 通过 FFT,将系数形式的多项式转化为评估形式;
  • 通过 iFFT,将评估形式的多项式转化为系数形式;

多项式乘法 multiplication:

  • 通过 evaluation form 相乘是比较好计算的,只需要将相同 x 坐标的对应的 y 坐标相乘, x 坐标保持不变,从而完成多项式乘法计算。

多项式除法 division (vanishing polynomial):

  • 如上公式,对于 ,如果我们知道 ,那么 ,因为不可以除 0,所以这里会有一些问题。
  • 解决方案是使用 coset-fft,即通过 做一次 shifted ,这样 就不会等于 0 了,而是等于某些常数形式,如此就可以正常进行除法操作。

承诺 Commitment -> MSM (Multiexp)

  • MSM: 即将标量向量 (scalar vector) 和椭圆曲线上的点 (point vector) 做点乘

Commit to a polynomial

  • 对于
  • 对于多项式的承诺形式,可以分别使用 coefficient 和 Lagrange 形式表示,其中
  • 👆🏻 是 SRS,通过系数 vector 和 srs 相乘的方法,得到 coefficient form 的 commitment。
  • 是 Lagrange Basis,下文中会有详细解释

Square-Fibonacci

我们将在本文中使用如下 Square-Fibonacci 示例,来演示证明生成的过程和在其中的计算成本 (cost)

  • ,

  • 对于 , 定义

    • 是一个大素数。 使用这个 来限制元素大小,以便它可以用一些预定的 bits 来表示
  • 为大整数,为了方便 FFT 计算,不妨假设 n 是 2 的幂。

  • 为第 个 Square-Fibonacci number.

目标:生成一个可被高效验证 (efficiently-verifiable) 的证明 ,来证明其确实是第 个 Square-Fibonacci 数(即 Prove

验证者可以朴素(naively) 地自己计算 , … 一直算到 ,然后自己检查是否 来检查该 statement。 然而,这需要许多计算步骤(注意 非常大),不是理想的方法。

The phases of proof generation

在 high level 层面上,证明生成(proof generation)由 3 个阶段组成:

  1. phase 1: 填入 witness
  2. phase 2: commit to the witness
  3. phase 3: prove that the witness 是正确的

phase 1: 填入 witness:

  • witness (或者说 trace) 指的是一组数据,可以展示为什么某个 statement 是正确的。
    • 例如,在 Square-Fibonacci 示例中,我们可以在 trace table 中逐步写出计算结果,每行一步:
      • 第一行: , where
      • 第二行: , where
      • … 以此类推,直到到达

phase 2: commit to the witness

  • witness 的 commitment 涉及到输出 witness 的一些简洁表示 (succient representations),并在此意义上压缩 witness
  • 在这一步中使用多项式承诺方案 (polynomial commitment scheme) 使我们能够仅通过简洁的承诺 (succinct commitment) 来证明 original witness 里 referencing 的某些属性。

phase 3: prove that the witness 是正确的

  • phase 1 阶段生成的 witness 必须遵从某些特定属性才能认证为 valid
    • 在 Square-Fibonacci case 中,每一行都需要满足约束:
  • 如若 original witness 满足了这些约束,那么一个简短的 proof 就可以被正常生成
  • 验证 proof 不需要访问 original witness table - 验证可以做到仅参考在 phase 2 生成的 succinct commitment 来进行。

Phase 1: 填入 trace table

filling in the trace table

trace table 是一个二维矩阵,其中记录了 “Witness” 或者说 “trace”。 除了 Witness 之外,trace table 还包括其他值(比如 fixed columns 或 Public Input),这些值有助于证明 Witness 是正确的。 trace table 中的每个 Cell 都是大有限域 中的一个元素

对于示例,trace table 可能如下所示:

如上图,

  • 代表 witness data (aka. private value);
  • 表示 Selector,当它启用时,该处必须要满足定义好的某种数学关系(custom gate)。注意在最后一行,witness 是空的,所以 被关闭了,这里加入空行只是为了方便—— 让表的高度(行数)能达到 2 的幂次。
  • 即代表 Public Input,是众所周知的公共输入:
    • 是大家都知道的;
    • 是 the value in the statement to be proved.

witness generation

实际 filling in the trace table 的过程就是 witness generation 的过程,不要忘了,填表过程中的算术运算是在大有限域上实现的,这种运算比在原始类型(如 int or long.)上昂贵得多。

有限域上的字段元素通常需要约 ~256 位来表示,这比 modern CPU 的字长 要大得多。

  • Having to split each element’s representation across words,
  • in addition to needing to compute all values modulo , adds a computational overhead to each arithmetic operation.

在本例中,使用 做约束运算,就需要 上的乘法和加法。不过这个约束会在每一行上不断重复应用,是相对特殊的用例。

在更常见用例中, witness 计算所需要的计算会非常复杂,可能其单独一步的计算就需要很多 witness rows (某些情况下 > 1000 行)。这种更复杂的表示 或 算术化(Arithmetization)通常会导致 trace table 的大小显着增加。那么 proof generation 所需的时间和计算量也会随之增加。

Additional processing

一旦 witness data (or “private input”) 被填入并被 committed to, 就会对 trace table 进行一些额外处理:

Auxiliary columns (也被称为 “virtual columns”) 是为了方便证明 trace 的有效性而生成的额外列。 某些类型的约束(constraints)需要这些 Auxiliary columns 来辅助。

举个例子: wiring constraints ,这个约束会强制要求 trace table 中的某些 Cells 采用相同的值。即使用 Permutation argument 来实现 Copy Constraints

回顾 PLONK 协议,在 high level 层面,在 Permutation argument 过程中,根据 witness 计算出的累积多项式(accumulator polynomial)就会以 evaluation form 的形式存储在 Auxiliary column 中,而后,证明 wire constraints 成立就简化为证明 上的某些约束成立,以及 与其他 witness column 的关系。

另一种需要 Auxiliary column 的约束是查找表(lookup table)。 查找表由 plookup 于 2020 年首次推出,可实现高效的 set-membership 检查。 除了算术运算之外,用于查找的 Auxiliary column 生成所需的计算还可能涉及排序。

请注意,辅助列 Auxiliary column 仅在 witness data 完全生成(generated)并提交(committed to)后才进行计算。

Auxiliary column 不仅取决于 witness data ,还取决于一些额外的随机性。 这种随机性是使用 Fiat-Shamir heuristic 启发式计算的,其所依赖的 transcript 包括了对 witness data 的 commitment。

Phase 1 cost summary

  • Enter witness data into trace table
    • 迭代并填充 all witness cells in trace table
    • 计算 witness values 需要大量的有限域运算
  • 生成用于 wiring constraints 和 lookup constraints 的 auxiliary columns
    • 需要额外的 large finite field arithmetic (as well as sorting, in the case of lookups)

Phase 2: Committing to the trace table

trace table 插值为多项式

考察 trace table 中的 Column , 我们可以将 A 列视为有限域中 (n−1) 次唯一多项式的求值形式 (evaluation form): 中的第 个值对应着其 evaluation 。 这就是 evaluation form 的列多项式们(包括 Auxiliary column ):

通过对 evaluation form (iFFT, evaluation form 2 coefficient form) :

的拉格朗日插值,我们可以得到

Commit to column polynomials

现在我们有了每一列的 Polynomials 的 coefficient form(系数),可以使用 polynomial commitment scheme(PCS) 来 commit 它们中的每一个。这允许我们将每一列都 “压缩”成一个简短的表示。 对所有列执行此操作,会生成整个 trace table 的简洁表示(succinct representation)

使用 PCS 还允许我们生成 proofs of evaluation - Prover 可以说服 Verifier 它承诺的多项式穿过了某个特定点,而无需透露整个多项式。

Computing KZG commitments

表示 KZG trusted setup ceremony 中的 secret value,写做

  • notation: 表示 , 是椭圆曲线群  的生成元
  • 注意: 是可以从 setup 中 committed 的多项式的上限(upper bound),如果多项式再大的话,可能需要更换一个更高度数的

现在我们有了每一列的多项式系数表达形式,如 ,对其进行承诺(committed to) ,即计算 :

如果我们想省略拉格朗日插值得到 这一步,直接使用 evaluation form 去计算 能否做到呢? 有一个方法,叫 Lagrange basis polynomials ,可以满足如上需求。

  • 对于 polynomial over the evaluation domain
    • 定义 “Lagrange basis polynomials”:
      • For :
    • 可以写出:
      • 特别:
      • 所以有:

FYI : 对于 :

  • 时,只有 ;
    • 上式的确穿过 这个 evaluation form 的点
  • 时,只有 ;
    • 上式也的确穿过 这个 evaluation form 的点
  • … 以此类推

在我们的例子中,evaluation domain 是 ,所以每一个 basis polynomial 都能被表达为 , 进一步来说,每个 都可以被预计算 (precomputed) : 被预计算 (precomputed) 好之后,committing to A 列只需要在 proof generation 时进行以下计算即可: 注意:

  • 每个 都是有限域 的元素
  • 是 trace table 中 column A 的第 个元素,即第 行 (从 … 到 )
  • 每个 都是椭圆曲线群 上的元素,因此,该计算可以看作标量向量(scalars) 和群元素(group) 向量 之间的点积。 (MSM, great post)

Phase 2 cost summary

  • commit Trace Table 的每一列 (real and auxiliary cols)
  • 对于长度为 的每个列,其 KZG 承诺可以通过大小为 的 MSM 计算

Phase 3: Proving the trace table’s correctness

至此,我们已经填写了整个 Trace Table,并 committed 它的每个列(包括 auxiliary cols 辅助列)。

现在剩下要做的就是证明原始 Trace Table 是有效的 (valid)。即意味特定的约束是被满足的,在示例中,我们有以下约束:

  • Square-Fibonacci 约束
    • 每个 selector 启用的行 必须满足
  • 线约束 (Wiring constraints)
    • 对于 这连续的 2 行,要求
  • Public input 约束:
    • 第一行必须以前 2 个 Square-Fibonacci 数开始,它们写在公共输入列 (public input column) 的前 2 行中:
    • 个斐波那契平方数对应的单元格必须与声明的结果值匹配,该结果值写入公共输入列的第 3 行:

通过将每一列,都看作是个多项式的评估形式 (evaluation form, i.e. 把 视为 ,上面提到的约束都可以被看做是多项式之间的某种关系,例如 Square-Fibonacci 约束可以被表达为: 为方便起见,将左边标记为 上面提到的所有约束 (custom gate/wiring/Public input) 都可以被表达为:

Combining constraints

一般来说,当我们有 个 constraint polynomials: 需要在 evaluation domain 上评估为 0 时,它们也完全可以被 batch 起来形成一个约束多项式 —— ,我们可以通过选择随机数 来将这 个 constraint polynomials: 通过随机线性组合 (linear combination) batch 起来: 如果所有的约束多项式都满足 在 evaluation domain 上评估为 0, 那么显然 也在 evaluation domain 上评估为 0; 即使有某个约束多项式在某点上评估不为 0,那么 也无法满足在 domain 上评估为 0,所以可知 : 如何证明右边这个说法呢?我们可以为每个多项式在每个 evaluation domain 点上都做 reveal 并 Prove ,但是这样会导致极大的 proof size。

事实证明,我们可以仅使用每列多项式的一个评估证明 (evaluation proof) 来证明这样的约束。

The quotient polynomial

如果要证明 meta-constrain 在 trace table 上每行都成立,这很困难;但好在我们可以推导出一个等价的命题,该命题相对容易证明:

why ?

  • 如果一个多项式在某点为零,这个点是多项式的根,则多项式可以被这个根对应的线性因子整除

所以,如果我们想证明所有约束每一行上都成立,那么就相当于证明存在一个满足上述性质的多项式 , 该多项式通常称为“商多项式(quotient polynomial)”

Computing and committing to the quotient polynomial

虽然商多项式在理论上很容易表达 :

但在实践中计算中,它实际上往往是最复杂且计算成本最高的步骤之一。

首先考虑 的 degree,它的 degree 等于有着最高次数的某个 constraint polynomial ,再减

在我们的例子中,Square-Fibonacci constraint 有着最高的 degree ,所以对应的 的 degree 就是 ,为了完全定义这样一个多项式,我们需要至少 个 evaluation points.

  • 假设 , 都是 阶的多项式, 的阶数为
  • 所以 的阶数为

因此,我们重新需要大小至少为 的一系列 evaluation points。 之前的 evaluation domain 不 work 了 - 因为 的阶是 ( 的大小只有 )

因此,我们需要选择一些其他元素 ​ ,阶数为 。 然后,我们可以在 的 evaluation domain 上评估 ,以获得我们需要的 大小的 evaluations

以下是最有效地执行此操作所需的步骤:

  • 对于每列多项式,从 evalution form 转换为 coefficient form.
    • 使用 iFFT 可以在 内实现每个变换
  • 对于 coefficient form 的每列多项式,根据我们选择的 ,生成 个 evaluations
    • 使用 FFT 可以在 中实现每个变换
  • with evaluations of each polynomials,我们现在可以计算 个 evaluations
    • 这仅需要根据商多项式的公式进行 field arithmetic 计算
  • 傅立叶变换(Fourier transform):从系数形式转换到评估形式的过程。简单来说,就是取一个多项式的系数,然后计算这个多项式在一组特定点上的值
  • 逆傅立叶变换(Inverse Fourier transform):这是相反的过程,即从评估形式转换回系数形式。它涉及到从一组特定点上的多项式值反推出原始多项式的系数

有了 的评估形式 (evalution form),我们现在可以像计算列多项式一样计算它的承诺:

注意,由于此时 的 degree 大于列多项式的 degree ,因此我们需要使用更大的 Lagrange basis,虽然可以预计算 (precompute) 这个更大的 Lagrange basis ,并用它来计算 commitment,但它 requires a larger KZG trusted setup - 但 setup 的大小必须 商多项式的 degree。

为了解决上面的问题,在实践中用 tricks,让我们只需 commit

  1. 我们首先使用大小为 iFFT 转换为系数形式。
  2. 然后,我们将 拆分为两个较小的多项式 ,使得
  3. 因为每个较小的多项式的 ,因此每个多项式都可以被 committing to 来用大小为 的 MSM

注意,被分解的子多项式的数量取决于商多项式的 degree - 如果商多项式的 degree 为 ,我们需要将其分成 3 个子多项式

Proving the quotient polynomial’s existence

至此,Prover 已 committed 了 trace table 中的所有 column polynomials,并且还 committed 了 quotient polynomial。 Prover 现在需要证明商多项式 确实存在并且计算正确。 请记住,如果 存在且成立,则说明所有的约束在每一行上都成立,即 trace table 是 valid

步骤:

  • 选择(Sample) 一个随机数
  • 生成并输出所有 列多项式 和 商多项式 在点 处的的 KZG 评估证明
    • 为了生成评估 的 KZG 证明,我们计算并输出
    • 与 KZG 承诺类似,该值按 MSM 计算
      • 每个列多项式需要大小 MSM
      • 商多项式需要 2 个大小 MSM

Verifying the quotient polynomial

verifier 收到 proof 须检查其正确性。 一个完整的 proof 包括:

  • 对每列(包括 auxiliary cols 辅助列)和商多项式的承诺 Commitments
  • 每列和商多项式在 处的的评估证明 Evaluation proofs

verifier 可按如下方式检查 proofs :

  1. Verify that each evaluation proof is correct
  2. Verify that the quotient polynomial formula holds at the evaluation point 

如果上步 2 里的式子在 处成立,那么(几乎可以肯定)它在任何地方都成立,因为  是随机采样的。

每个 evaluation proof verification 都需要计算椭圆曲线配对 (pairing) 。 验证商多项式公式需要一些有限域算术运算(方程右侧)

总之,与 proof generation 阶段所需的计算相比,验证所需的计算是轻量级的,并且通常能够在链上高效地执行。

Phase 3 cost summary

  • 计算 evaluation form 中的商多项式
    • 通过 iFFT 将每列多项式转换为系数形式
    • 通过 FFT 将每个列多项式转换为 expanded 评估形式 (expanded evaluation form)
    • 使用每个列多项式的评估形式,评估 2 2n 个点中每个点的商多项式
  • 致力于商多项式
    • 通过大小为 2 2n iFFT 转换为系数形式,以便拆分
    • 致力于每个分割多项式,总共需要 2 个大小 MSM
  • 为随机评估的每个多项式生成评估证明
    • 每个列多项式需要大小 MSM -(分裂)商多项式需要 2 个大小 MSM

请注意,“FFT/iFFT”的大小和 MSM 的数量取决于商多项式的次数,而商多项式的次数又取决于最高次数多项式约束。 在我们的例子中,最高次数约束的次数为 ,这导致商多项式的次数为

Conclusion

Recap

快速回顾一下与 proof generation 相关的 cost :

Phase 1: Filling in the trace table

  • Filling in witness data requires arithmetic operations over a large finite field
  • trace table 通常很大,Auxiliary 辅助列需要额外的算术运算和排序

Phase 2: Committing to the trace table

  • Committing to each column requires a size MSM

Phase 3: Proving the trace table’s correctness

  • 计算商多项式的 evaluation form 需要 :
    • A size iFFT for each column
    • A size FFT for each column
    • Arithmetic operations over a large finite field
  • Committing to the quotient polynomial requires
    • A size iFFT to convert to evaluation form
    • 2 size MSMs, one per split polynomial
  • Generating the KZG evaluation proofs requires
    • A size MSM for each column
    • 2 size MSMs for the (split) quotient polynomial

可以清楚地看出,phase-2 和 phase-3 以 MSMFFTiFFT 等计算量大的算法为主。 同样明显的是,所有计算步骤都随着 的增加而增加,包括 phase-1 的 witness generation 计算。

Paths toward acceleration

  1. 对重计算任务的硬件加速 (Hardware acceleration) 我们已经看到,如 MSM(多项式标量乘法),FFT(快速傅里叶变换),和 iFFT(逆快速傅里叶变换)等重计算在证明生成所需的总计算中占了很大一部分。这些算法在 CPU 上运行通常相当缓慢,可以通过在 GPU,FPGA 或 ASIC 上运行来大大加速。

  2. 减少 trace table 中的行数 我们还看到,几乎所有涉及证明生成的计算都与 (即 trace table 行数) 成比例增长(也被称为“门的数量” )。如何在使用最少行数的同时表示某些复杂计算,是一个具有重大效率影响的研究领域。

  3. Parallelize and pipeline

许多证明系统,包括我们在这里研究的系统,都有自然的并行化机会。例如,在第二阶段的列承诺步骤中,每列的承诺都可以并行计算。更进一步,每个见证列的承诺 MSM 可以与其生成同时并行计算。并行化和流水线化计算可以显著加快整个过程。

  1. Alternative proof systems

本文介绍了一个特定证明系统的计算需求。这个证明系统只是众多系统中的一个 - 在理论证明系统的设计空间中存在很多,每个证明系统都有自己的一套计算需求和权衡。研究正在积极进行以进一步探索这个设计空间,并设计理论构造来减少或消除计算瓶颈。

Take a deeper look at Halo2

对于如下这个 trace table,我们可以定义 Custom gate : 也可以在其上定义 lookup table,permutation argument 等等… 这些都会转化为具体的 Polynomial cost :

有 3 种类型的 Constraints 我们需要关注:

  1. custome gate
  2. lookup table
  3. permutation

下面我们来看:halo2 后端发生了什么,以及这些 Constraints 将如何影响 Proving time。

lookup table cost

如上是 lookup table 的具体作用步骤:

  1. 2 个 columns 排序
  2. Prove for this multi-set check
  3. Prove for the permuted columns, 公式逻辑是:对于 中的每个元素,要么和上一个相等,要么和同 Rotation 处的 相等。

每次添加一个 lookup,几乎相当于添加 4 个 custom gates 甚至更糟。

permutation argument cost

Prover cost

分别是对 fixed columns 以及 permutation argument 的 commitment,是在电路生成阶段由 key_gen 负责 pre-compute 的,因此不算做 Prover 的工作。

分别是对 Instance column(PI) 和 private value 的 commitment, 分别代表 2 个多项式的 degree

从 Verifier 处收到 random value 后,Prover

  • 为 lookup commit
  • 为 permutation argument commit

在 Fiat-Shamir heuristic 中, 不由 Verifier 生成,而是由 得到

总体最大 cost(粗略估计)

  1. iFFT
  2. FFT
  3. Multiexp (MSM)
  4. vector “sorting”
  5. extended vector combination.
  6. Evaluations in polynomial evaluation.

References list

  • https://scroll.io/blog/proofGeneration
  • https://drive.google.com/file/d/1Es6SasxViICEQySiDy8vSQvjzZCNcV33/view
  • https://www.youtube.com/watch?v=3FwoXozVQ9Q
  • Slides: https://tinyurl.com/4pexkwpe
  • https://drive.google.com/file/d/1Es6SasxViICEQySiDy8vSQvjzZCNcV33/view

1

并行 2: 提高带宽利用率 3: https://docs.google.com/presentation/d/1e-gmKzFNv4nN-A6rya__Jg_oIUV5U66_9oO_cGQ3Hm4/edit#slide=id.g13bba0f074e_0_5

我们整理分类了 halo2 discord 上面的 Q&A,大部分学习中遇到的问题都可以尝试先在此进行分类搜索

Basics

Q: What’s the actual difference between assign_advice_from_constant and assign_advice in practice?

A: @ying tong

assign_advice_from_constant:

  1. (at keygen) assigns a constant value to a cell in a fixed column;
  2. (at keygen) sets up an equality constraint between the fixed cell and the advice cell;
  3. (at proof gen) assigns that same value to the advice cell.

assign_advice only does step 3).

Q: what’s the difference between PSE Halo2 and original Zcash Halo2?

on a high level: the PSE fork of halo2 supports more features; however, it is worth noting that the zcash/halo2 implementation has been audited. many of the useful features in PSE are pending upstreaming under the nightly flag. here is an incomplete list of the notable differences:

  • commitment schemes: zcash/halo2 only supports IPA, while PSE additionally supports KZG
  • lookups: zcash/halo2 only supports fixed tables, while PSE additionally supports dynamic tables
  • challenge API: PSE supports multiple rounds of commitments and challenges

I think PSE switches use of inner product argument for kzg commitment?

is there an end-to-end example of halo2 proof & onchain verification in Solidity?

  • https://github.com/privacy-scaling-explorations/snark-verifier/blob/main/snark-verifier/examples/evm-verifier.rs

comparison I am a beginner in Halo2, and I want to create a proof using Halo2 to check whether the user’s age (userAge) is greater than or equal to the target age (targetAge). Does Halo2 support comparison operators? If yes, then how can I define the comparison gate and write the comparison function?

SUNYI:

  • https://axiom-crypto.github.io/halo2-lib/halo2_base/gates/range/trait.RangeInstructions.html#tymethod.is_less_than
  • This uses halo2-lib: https://github.com/axiom-crypto/halo2-lib

Could this algorithm be implemented somehow in halo2/plonk? :

#![allow(unused)]
fn main() {
def f(a, b):
    if a == 0:
        return a
    else:
        return b
}

i think you could witness is_zero_a = isZero(a), and use it to constrain the output :

  • is_zero_a * (output - a) + (1 - is_zero_a) * (output - b)

Daira: how is isZero implemented? to constrain is_zero_a correctly for both cases, you would need to witness alpha = inv0(a) and then constrain is_zero_a * a = 0 and (1 - is_zero_a) * (1 - alpha * a) = 0, as well as boolean-constraining is_zero_a but that may be what isZero does; I’m not sure. I couldn’t find it in halo2_gadgets (IIRC there’s a way to do it in one fewer constraint, but I forget what that was.)

yup, i’ve seen an IsZero like that: https://github.com/privacy-scaling-explorations/zkevm-circuits/blob/6bcb1d052a75886f4a2379347b84fa2bd1ff2bea/zkevm-circuits/src/evm_circuit/util/math_gadget/is_zero.rs


Q: Hey gust! What happens if we reference the previous row in a gate that is active at the 0th row? A: this will “wrap around” to the last row. in halo2, the last few rows of the advice columns are set to random values: https://github.com/zcash/halo2/blob/f9838c127ec9c14f6f323e0cfdc0c1392594d37f/halo2_proofs/src/plonk/prover.rs#L293-L298 i wrote up a minimal example for this (constraint will fail, since the last row has a nonzero random value): https://discord.com/channels/995022112811135148/1154777246188445736/1154843452236910715


Q: Hi, want to make sure, if we dont assign any value to a cell, is it default to 0? Thx! YT: yes, unassigned cells are initialised to 0 in the current implementation; but this is an arbitrary implementation choice, and we shouldn’t rely on it being true. e.g. there was some discussion about randomising unassigned cells: https://github.com/zcash/halo2/issues/614


Q do we need to learn complete rust to build zk-protocol using halo2? A Yes, actually there’s no DSL for Halo2. So the only way to write circuits for it (That I’m aware of) is using the Rust API that the library provides.

I’d not say that you need to be proficient in Rust to write circuits but definitely need to be familiar with the basics of the language.


1. What is the difference between public input and the constant?

A: public input can change between different invocations of the prover, whereas a constant is fixed across all proofs.

  1. In every example I found so far we need to define the add, mul, load_private/constant, expose_public. If those are so popular I suspect they were implemented in some standard library and can be reused?

A: This is what the halo2_gadgets crate is for! It provides a bunch of common gadgets (and chips for them) that can be reused.

  1. If the above is true, does that mean that SimpleExample can be done without implementing a new chip but rather just reusing some other existing code? To me, it seems as if I was trying to learn C++ and someone gave me a hello world that contains operator overloading:)

A: Indeed, it could be. SimpleExample is really providing an example of two things: how to build a circuit, and how to build a custom gadget and chip. It would be useful to split it into two separate examples in the book! I’ve opened an issue for this: https://github.com/zcash/halo2/issues/727

Note however that https://github.com/zcash/halo2/blob/main/halo2_proofs/examples/simple-example.rs cannot be refactored in this way, because it is an example of using halo2_proofs directly; we don’t have access to halo2_gadgets there.

  1. In the circuit, we need to overwrite the configure method. The first argument to that method is the meta argument. What is this argument? It seems like a lot of useful methods are located in this object but I am not sure what is the idea behind and when and where should I use it. As I understand I can access instances of layouter to add new regions for example. But what is the main use case for meta?

A: meta has type ConstraintSystem, and is used inside Circuit::configure to configure the circuit. This is where you tell the backend how many columns (and of what kinds) you are going to use in your circuit, what gates you want loaded, etc.

Chips generally provide a SomeChip::configure method that implements this logic for their needs, which you call inside Circuit::configure if you want to use that chip in your circuit.

  1. I am a little confused between the circuit and execution trace. When we use a plotter to generate colorful images, it seems to me what it represents is an execution trace that the circuit can apply it’s gates to and verify. Could you please clarify what it actually shows so I have the base foundation straight?

A: This is not showing an execution trace, but the actual “physical” layout of your assigned cells in the circuit. The red columns are advice columns; blue columns are fixed columns; and white columns are instance columns / public inputs. Green boxes represent calls to layouter.assign_region, and it’s inside those that the actual cell values are assigned.

  1. I am trying to understand how the chip integrates into the circuit. So in the configure function of the Circuit, we declare all the columns we might need. Then we call the configure function of all the chips that we want to use. Those chips in their configure methods call any configure methods of chips they use. What I notice in the two-chip.rs example is that we only need to define the columns once and then pass them to the appropriate chips. Does that mean that all chips will be reusing the columns defined in the Circuit? Or due to some rust variable borrowing magic, only the chip who’s configure we called last will have access to the columns.

A: Yes, columns can be reused by different chips. As the circuit designer, you get to choose how you “connect up” the chips you are using. You could have all chips use the same few columns, or you could define a bunch of different columns so each chip gets its own set.

  1. I am missing some understanding on how the gates are applied to the execution trace or circuit(not sure of the correct term here). Does every gate gets applied to every row, on columns that are passed into the gate in the column argument? Would be cool to see a visual that would show the values that were loaded in to the regions, on top of the colors, and see how the gates are applied. Does such a tool already exist?

A: Every gate is applied to every row. Think of gates as like Tetris pieces that you hold over every row, and each cell covered by the Tetris piece is used by the gate on that row. This is why we have “selectors” that you can use to enable the effects of a gate only on specific rows.

Re: tooling, this is something I’ve wanted for a long time, but I lack the JavaScript skills to implement it myself.

  1. the difference between copy_advice and assign_advice_from_instance . From the source code I can see that the copy one also adds a constraint that advice must be equal to the instance column, but from Haichen’s talk at 0xPolygon I can see that assign one does that as well. What would be the difference? Also please let me know if I should use the previous threads for additional questions. Thank you! A: The difference is tha copy_advice adds a copy_constraint between two columns at a particular offset also setting the value from one to the other. This, not only assings a witness but also forces the permutation argument to include a copy constraint. The particular difference from assign_advice_from_instance is that copy_advice copies Advice columns while the other copies from Instance columns(so public inputs) into Advice columns.

So in summary:

  • assign_advice_from_instance -> Copy From Instance -> Advice.
  • copy_advice -> Copy from Advice -> Advice

Yep. The reason that assign_advice_from_instance exists is that the instance column needs its ordering to be known to the circuit user (so that the verifier can provide the public inputs at the correct locations), and if an instance column gets used in a region it interferes with the floor planner being able to reorder regions for efficiency. So we instead have you copy the public input value from its known instance column location into an advice cell inside a region, and then use it from there.


Q according to you is halo2 better than circom and why? A I think there’s no better or worse. They serve the same purpose but apply different techniques.

While in halo2 you have the power and expressiveness of Rust to help you, you also pay the prize with more verbosity.

Instead with Circom, you don’t have that expressiveness and instead you have concise instructions which are purely the circuit and not rust-only stuff.

Depends on the taste and person. I can’t say that something is categorically better or worse.

Curves

Q: Regarding the generation of Pallas/Vesta or Pluto/Eris, is there some reasons why the parameter b should be the same for both curves in the cycle?

A: Just convenience of implementation It doesn’t constrain the search significantly; you can always find such a b that is the same for both curves. What does constrain it is picking b in advance, which we also did for simplicity.


Q: Is the cell value of Halo2 arithmatization an element of base field or scalar field of the Pallas curve ? Given the cell is the evaluation of polynomial on the root of unity, is it supposed to be an element of the scalar field?

A: currently, proofs from zcash/halo2 are computed on the Vesta curve; so the circuit values are Vesta scalars (i.e. Pallas base field elements).

Q: Ah I see, thanks for explaining, but the ECC gadget is computed on Pallas curve?

A: yes, since it’s working with curve points whose coordinates are in the Pallas base field; in other words, Pallas curve points.


Q: Hello trying out the KZG fork, can I use pasta::Fp in ParamsKZG? Or should I abandon pallas?

A: Pallas and Vesta are not pairing-friendly, so they can’t be used directly with KZG


Hey all.

I’d like to know why https://github.com/zcash/halo2/blob/76b3f892a9d598923bbb5a747701fff44ae4c0ea/halo2_gadgets/src/ecc/chip.rs#L140 is forced to use pallas::Base ,when if made generics, this could serve as a chip for any elliptic curve with the same properties.

I’ve seen https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/ecc.rs#LL40C1-L43C59 but I doubt that this is a blocker actually. You can have a trait that actually has these constants as functions instead and they return a constant result always.

I’d appreciate if you could explain if there’s any other limitation aside from the second link I sent. As on that way, I might give a shot to the actual task of removing the generics.

Daira: I think I never actually confirmed the assertion that p and q can be swapped safely in that implementation, or what exactly it depends on. The security analysis was done only for Pallas.

For the Orchard circuit, the proof system is over Vesta and the curve multiplication is over Pallas. The scalar is given as a Pallas base field element, which is smaller than Pallas’ scalar field. If it were the other way around, the scalar could overflow, and the consequences of that are what haven’t been analysed.

Thanks @Daira Emma (ze/hir) — ECC ! My question wasn’t targeting Vesta specifically. Rather, targeting the possible implementation of Pluto-Eris with the ECC chip. Or in general, allow any Weierstrass curve to use this chip.

Hence, on that line of thought I was asking if it would be a big issue to just try to remove the issue introduced by FixedPoints so that we can have a trait instead of a fixed type (pallas::Base ) in this case. or, instead, there are still hidden issues if we try to impl other curves.

Also, is it possible that by removing some optimizations, we can make the ECCChip able to swap curves? Or is it that by design, one chip serves for only one curve?

There are still hidden issues, but it can be generalised with a bit of work (and that’s on our roadmap since it’s needed for recursion) As you say, some optimizations would need to be removed, because the current implementation depends on the fact that p and q have bit pattern "1 <many zeroes> ..."

the short answer is that we originally made it generic, but didn’t have the engineering bandwidth at the time to ensure that the implementation was actually safe generically, so we specialised it to reduce reviewer burden.


materials

Q: Hi there, I am exploring Halo2 project and have checked out the Halo2 book and lectures offered by 0xParc. But I still can’t find a formal documentation like rustdoc for the helo2 core library even for early edition. Is there anything like that, or how can I have a more complete view on the functionalities that core components provide? (Like Chip, Config, Circuit, MockProver, Layouter etcs). Any informal material can be a great help!

A: There are links to doc on crates.io pages

https://crates.io/crates/halo2_proofs https://docs.rs/halo2_proofs/0.2.0/halo2_proofs/

https://crates.io/crates/halo2_gadgets https://docs.rs/halo2_gadgets/0.2.0/halo2_gadgets/

And I guess cargo doc –open being run on source code will give you docs on exact commits if you need other version than 0.2.0

recursion material to halo2_proofs

Q: is recursion material to halo2_proofs? or is plonkish arithmetization > ipa something that happens without any recursion?

A: The latter. Halo 2’s recursion capabilities (once implemented) will be built on top. The Halo 2 protocol is designed with awareness of, and intention to support, recursion. But that just affects how the base protocol works; it doesn’t necessitate recursion in order to use it.

Q: good to know - doing a writeup on battleships and wanted to make sure I frame things right

A : Halo 2 as a protocol is effectively a synthesis of Plonkish arithmetization, and a polynomial commitment scheme that is amenable to the Halo recursion technique. (And also a bunch of other things that make the overall proving system more efficient, and API designs for making it possible to build fast and safe circuits, etc.)

gadgets

Poseidon

we’re struggling to hash a variable-length input using the default Poseidon implementation from halo2. since the library only implements the ConstantLength Domain, I assume that we need a different implementation for the VariableLength. Could anyone point us to a project that’s doing that, or guide us on how to implement that?

YT: it looks like the axiom fork does variable-length Poseidon (https://github.com/axiom-crypto/halo2/tree/main/primitives/poseidon):

idk how this interacts with Poseidon standardisation efforts, @str4d - ECC might have a better idea: https://github.com/C2SP/C2SP/pull/3

there isn’t a restriction on L to be a multiple of RATE, because we pad the input to a multiple of RATE: https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/poseidon/primitives.rs#L319-L327. however, there is a bug in the loading of the padding words, which should be fixed by this PR: https://github.com/zcash/halo2/pull/646 (i’m going to bump this PR for re-review, i don’t recall why we didn’t merge it the last time)

Q: Hi team, I wonder whether there is any reference/example on how to use Poseidon gadget of Halo2? I’m rather in engineer background and don’t know the very detail of Poseidon hash function. I just find it extremely painful to use this gadget https://docs.rs/halo2_gadgets/latest/halo2_gadgets/poseidon/struct.Pow5Chip.html

Some specific questions are: (1) What are the meaning of WIDTH/RATE parameter and how I should choose them? (2) What are the usage of state/rc_a/rc_b/partial_sbox column and can I reuse them for other parts of my circuits? (3) How can I assign cells for the Pow5Chip (through Hash::hash?)? (4) Which library is recommended to calculate the ground truth for hash result, as they all have different setups (field, parameter….)? (e.g. https://github.com/ingonyama-zk/poseidon-hash)

While I’ve used Poseidon in Circom which is just one line work like “component message = Poseidon(2)”

A by himself: For the above question, I just find it a great illustration in test cases of the library ! 😀

padding

Hello, everyone. If I want to implement the padding in zkp circuit using halo2, how can I do that? For example, I would like to implement the padding of md4 hash function. It includes padding the extra bytes to the end of original message.(Assuming that message is byte array - Vec<u8>) (Seems silly question, but would like to know how to do it if possible)

I think SHA256, Poseidon, and Sinsemilla might be good reference implementations. (https://github.com/zcash/halo2/search?q=padding). https://github.com/zcash/halo2/blob/a19ce33c395eb14f951e4d64d1bd3c7d6f714366/halo2_gadgets/src/sha256.rs#L137-L140 looks like the entrypoint for padding in the sha256 gadget

keccak256

Q: hey, is there any demo about how to expose the keccak256 hash result as instances in the halo2 circuit using the BN256 curves? the keccak256 hash result can be convert to be an Uint256 type data, but the BN256 data is about less than Uint254 A: the zkevm-circuits impl witnesses the Keccak output hash as 32 bytes (each of them range-constrained): https://github.com/privacy-scaling-explorations/zkevm-circuits/blob/main/zkevm-circuits/src/keccak_circuit.rs#L524-L531

XOR

Q: I’m just writing a simple XOR chip for understanding lookup table API, have few q:

I see the lookup tables section in the docs is marked TODO so would it be fine if I try to make a PR with the XOR chip as an example? https://zcash.github.io/halo2/user/lookup-tables.html

A: plain u64 types :Yes, but you will need to implement range constraints somewhere to ensure they are only 64 bits. I see that you separately have a BITS const generic, so really that is what you want to range constrain to (and then somewhere else you should constrain that BITS <= 64).

Is it possible to calculate XOR by converting the AssignedCell into u64?

The way you need to do this is to take the AssignedCell s where left and right have been assigned (most likely by taking them as gadget inputs and then copying them into cells inside the local region), and use .value() to get the Value s assigned into left and right. Then you can compute the XOR as:

#![allow(unused)]
fn main() {
let result_val = left.zip(right).map(|(left, right)| left ^ right);
}

and assign that into the result cell.

zk-ecdsa in halo2?

Axiom has an implementation of that available https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-ecc/src/secp256k1/tests/ecdsa.rs

MiMC

https://github.com/avras/mimc-halo2

waiting fix:

Happy Monday everyone! I am visualizing the circuit and tried filling the circuit for proving that I know x that satisfies x^3+x+5=35. 3 is private input, 5 is a constant and 35 is the public input. I had a few questions where things did not really align for me:

  1. What is the very first row that is not labeled? I get it in every circuit I visualize.
  2. Why loading constants takes two columns? All I do is I assign_advice_from_constant.
  3. To use the constant cell or instance cell in the computations I first need to load it into advice columns? Essentially I am wondering if the load_constant step can be skipped?
  4. Nothing in the instance column (first white column) is greyed out. Does that mean I have loaded 35 as public input incorrectly?
  5. Selectors get enabled for the entire height of the region? Trying to understand why mul and add have height of 2 rows and not just one row in the selector columns. Looking at the number table I filled that should not work and 1 should only be set for one row in the region.

Just in case here is the code for the circuit:https://github.com/0xTaiga/SimpleCircuit/blob/main/src/main.rs

serialization/deserialization

Hi everyone! I am working on a serialization/deserialization method to write and read Halo2 circuits and proofs. For instance, when I need to reed and write circuit parameters I can use read and write functions in the impl of the Params public structure and it works well.

My issues start when I try to move forward to the proving and verification keys. I have tried to implement bincode::Encode on a struct that mirrors the ProvingKey struct, but I struggle with the fields of ProvingKey being private. I also came across other things being private that prevent me to write a bincode::Encode implementation outside the halo2 crate, for instance the Module permutation. Are you people aware of a better way to serialize and deserialize Proving and Verifying keys, or I have no choice but to fork the halo2 repo? I would rather not do that. Thanks a lot in advance

Define a stable serialization format for halo2::plonk::ProvingKey https://github.com/zcash/halo2/issues/443

A: Because Halo 2 doesn’t have a trusted setup (and the generators are computed by deterministically hashing to the curve), the only thing necessary to reproduce the proving key is the code of the circuit. Since that’s necessary for proving anyway, I don’t think there’s an advantage to reading and writing proving keys from/to disk. The same argument would not apply to verification: you can verify a proof with just the verifying key, without needing the code for the circuit.

In proving systems like Groth16, it was necessary to read the proving key from disk because there was information stored within it that we couldn’t derive on-the-fly. For Halo 2, reading the proving key from disk would solely be a potential performance optimisation, if it takes less time to read the proving key from disk than to re-compute it.

I recently added key generation benchmarks to benches/plonk.rs, and for its particular circuit structure, I see the following times on a Ryzen 9 5950X:


Q: Hey! I was wondering, what is the intended way of (de)serializing the verifying key struct? If I understand correctly, assuming the verifier is different from the prover, the prover would need to pass the vk along with the proof, presumably over the network?

A: this is still WIP on zcash/halo2 right now: https://github.com/zcash/halo2/issues/643 but there’s a PR for it that is already being used in some other forks: https://github.com/zcash/halo2/pull/661

wait to be organized

arithmetisation

Q: Can a row be described as a step of computation in the sense a computation is flatten into many steps(rows) and cells are the computation trace? without using the math description behind it, i.e. nth root of unity

YT: i think this is the model used by AIR arithmetisation, where a single relation is uniformly checked on each row.

the PlonK arithmetisation allows us to express less uniform relations, e.g. it could be that a relation only applies on some rows; or is irregularly “shaped” and only applies to part of a row; or spans multiple rows; etc.

A: You are right, thanks for pointing out, so each step of computation can be represented by any shape (region) in the Plonkish matrix? There is no significant meaning of a single row except it is nth root of unity?

str4d - ECC: Indeed. It’s easier to think of the rows and columns as “area” that can be filled by the initial, intermediate, and final values within the computation, similar to laying out a circuit board or computer chip.

So e.g. you could have “steps go sideways” by having your constraints mainly reference cells in a single column, but have the overlapping references go into adjacent columns. It would definitely be unusual (because the root of unity logic means the constraint applies to all rows, not all columns, so you don’t get automatic tesselation), but it might make sense for some kinds of circuits (e.g. I could imagine representing some kind of layered neural network structure where you want to apply the same constraint to a bunch of adjacent cells that feed into overlapping “outputs” in the next layer, that you could maybe represent in this way).

More usually, you define some small regions in which you have a small sequence of computation (so inside that region, you could likely consider each row of the region to be a “step”), but those regions can be positioned arbitrarily within the overall circuit area (so the “circuit rows” don’t correspond to overall computational steps, unlike AIR).

lookup table、lookup system/SHA256

Since using lookup we can ensure if a tuple of witness values exist in a lookup table. however is it possible to constrain that a tuple of witness values do not appear at all in the lookup table?

Q: Is anyone working on more efficient lookup arguments in Halo2, like Caulk+ or cq? Decoupling proving time from the table sizes would be 🔥

YT: a colleague and i have been working on:

dynamic table:

  • https://github.com/zcash/halo2/pull/715
  • WIP ?

Q: Are there any instructions on creating a dynamic lookup table for prover-time variables?

A: if you are on the PSE fork, there is the lookup_any API: https://github.com/privacy-scaling-explorations/halo2/blob/main/halo2_proofs/src/plonk/circuit.rs#L1725-L1749


Q: Hello, halo2-ers, I’m a newbie in the Halo2 zkp system, I have been confused about the lookup system, when we construct the lookup for the zk unfriendly operations like sha256/keccak256, how do we confirm the validity of the ops lookup table that have built?

A lookup argument simply checks that a value is inside of a table. Indeed, the real lookup argument checks that one set of arguments is a subset or a permutation of another one.

So for instance, you might want to:

  • Have a circuit/gate that executes the Hash (provided a set of inputs) and returns the output.
  • Make this circuit expose this relation Input - Output in a lookup table.

Then all users can lookup this table with input-output with the guarantee that each relation has been constrained already. So to make it easy, you just need to compute the hash once, and then you can look it-up N times 🙂 The issue normally is that the inputs of the hash do not fit inside of a column and then you need sophisticated designs to sort this out.

You can also use Chips like the SHA256 in halo2_gadgets for example which are already build and just require to put the things inside tables.


Q: Hello, I would like to ask question about lookup & table column. Why the table column cannot be re-used as ordinary fixed column? I found that the table column is, in essence, fixed column, wrapped inside TableColumn struct. Also, I would like to know the detail of permutation used in lookup argument.(I read that it is different from that of plonk).

A: currently, halo2 fills in TableColumns with a specified default value, which is why we cannot used an ordinary Fixed column as a TableColumn. but i actually don’t see what’s stopping us from using a TableColumn as an ordinary Fixed column.

it is true that both the permutation and lookup arguments internally make use of some variant of multiset equality. however, the permutation argument does so on tagged cells, thus enforcing a specific permutation.

whereas the lookup argument does so on untagged cells, thus allowing any permutation. (note that the complete lookup argument involves additional checks besides this multiset equality check.)

str4d: The problem is that whatever goes into a TableColumn needs to be valid for the table lookup, because technically the entire column is in the table due to how the lookup argument is constructed, regardless of how many rows the real table has.

Dynamic lookup tables fix this with the tag column, which restricts the range of rows over which a lookup will constrain a table, thus freeing up the rest of the column’s rows for other things.

So you pay the cost of one or more separate fixed columns for the tags, but in exchange get to split up the table columns by row.

Some more questions:

  1. Currently, the dynamic lookup table idea is implemented in halo2?
  2. If so, can you give me reference or example code?
  3. In case of dynamic lookup table, it looks like I can re-use other rows of table column as one of ordinary fixed column. right?

The implementation is in https://github.com/zcash/halo2/pull/715, which we are working to get merged as a nightly (and therefore unstable) feature (https://zcash.github.io/halo2/dev/features.html).


Selector optimism

Q: Is the layouter automatically optimizing for selectors? From the printing of the circuit, it seems that the number of selectors added to the circuit is equal to the max number of selectors that are enabled at the same time. For example, if the circuit config contains 10 different selectors, but at max 2 of them are enabled together in a region, the circuit wll only have 2 selectors. Is that correct? A: yes, the layouter combines mutually exclusive selectors into fewer fixed columns. however, it only does so up to the maximum degree bound. there’s a write-up of this optimisation at https://zcash.github.io/halo2/design/implementation/selector-combining.html

copy instance col

Q: Hi all, quick question. how would one copy a variable length instance into an advice column? My tmp solution is to keep calling assign_advice_from_instance until I get an Err, but is this the idiomatic way?

A: An instance value cannot truly be variable length: you eventually run out of circuit rows to fit it in. Generally in this kind of case, what you need to do is have a hard-coded maximum length, and always copy that in, and then have logic inside the circuit to handle the variable length.

Note however that the circuit needs to always perform logic on the full maximum length of the instance value (circuits always encode “worst-case performance”), so you need some way of performing “dummy” operations on the instance value.

Alternatively, if you don’t mind different-length instance values using different circuits (which from a privacy perspective is fine because the instance values are by definition known to the verifier), then add a const generic length parameter to your circuit. Then within that circuit you can treat the instance value as a fixed length.

Ah I see! Thanks for such a detailed explanation.

cost


this cost.rs file looks potentially quite useful: https://github.com/zcash/halo2/blob/main/halo2_proofs/src/dev/cost.rs but there’s no documentation i can find; how do i use this with my circuit? is it built in somehow? or do i manually fill all of the values?

You call CircuitCost::measure(K, your_circuit) where your_circuit is an instance of your struct that implements Circuit

cool, this worked:

#![allow(unused)]
fn main() {
println!(
    "{:?}",
    CircuitCost::<Eq, MyCircuit<Fp>>::measure(
        (k as u128).try_into().unwrap(),
        &circuit
    )
);
}

there was a smol bug with lacking permutation checks that I’ve posted about here: https://github.com/zcash/halo2/issues/748


Q: why rotation is costly in halo2?

A: I wouldn’t say it’s excessively costly. But use of rotations does increase the number of commitments in the proof (see https://github.com/zcash/halo2/blob/main/halo2_proofs/src/dev/cost.rs for the details)

folding/recursion

Has anybody implemented the Folding scheme for Halo2 lookups? I am curious how efficient it’s going to be. Thanks. @yisun https://hackmd.io/@aardvark/rkHqa3NZ2

There is an implementation of Sangria here which I believe also incorporates some form of lookups: https://github.com/han0110/plonkish/tree/feature/sangria

Q: Maybe this should be asked in ⁠recursion channel, but anyway I’m asking here. I didn’t read full paper so please tell me a chapter if it’s written in full paper. I watched video materials and some online resources about recursive nature of plonk circuit. I understand that halo2 works if you can put verification circuit of plonk inside its own circuit, but I’m not exactly sure how this can be achieved. If you first prepare a circuit which is target of verification, then wouldn’t that change the verification logic itself? Are there any proof-of-concept implementation for this chicken-and-egg circuit recursion problem?

A: one solution i’ve seen is for the recursive verifier to take in a verifying key as input, along with the prior proof. this is how it’s done in arkworks-rs/pcd:

  • recursive verifier that takes a help_vk as input: https://github.com/arkworks-rs/pcd/blob/master/src/ec_cycle_pcd/data_structures.rs#L85-L100
  • where help_vk is used to verify prior proofs: https://github.com/arkworks-rs/pcd/blob/master/src/ec_cycle_pcd/data_structures.rs#L296-L301
  • chain of recursive proofs in a test: https://github.com/arkworks-rs/pcd/blob/master/tests/mnt4_marlin.rs#L142-L165

in an accumulation scheme, the accumulation verifier does not fully check the prior proofs, but rather checks that they have been combined correctly into the accumulator. e.g. in Nova:

  • we fold two previous instances into a new one (https://github.com/microsoft/Nova/blob/main/src/circuit.rs#L285-L297)
  • recursive minroot example (https://github.com/microsoft/Nova/blob/main/examples/minroot.rs#L223-L241)

I’m new to Halo2 and the reason I’m learning it is to use recursive proof/verification. However, I see it’s still marked as “coming soon” on the website. Is there any estimates of when it will be available? Is there a beta version that we can start learning and experimenting now? Thank you in advance,

No response


Mockprover

I’m trying to understand what does mock prover do internally? does MockProver::run run keygen-and-prove operation on the given circuit as the comment of the function claims? I couldn’t find where is the keygen called inside the run function

YT: keygen_pk() collects fixed information about the circuit structure and encodes it in the ProvingKey. this is done using just the constraint system, without an actual witness. at proving time, the prover inputs this ProvingKey and their witness to create_proof(). MockProver::run() combines the two steps by collecting information about the circuit structure + assigning the witness to this structure “in the clear”. this lets us sanity-check an assignment without doing the expensive FFTs and commitments involved in the real proving/verifying process.

I see thank you for the clarification, I thought it would call keygen_pk() explicitly but I realized it’s not necessary as you explained

keys generation

Q Is it possible to compile the halo2 binary and during runtime have it generate a circuit/proving/verification keys? (the circuit is not known at compile time) Something similar to what is possible with circom/snarkjs, where we feed a high-level circuit description to the circom binary, it generates the .r1cs file and we feed the .r1cs to snarkjs for proof/verification.

A @dan not at present, but it’s something we do want to enable in the near term. https://github.com/zcash/halo2/issues/550 is the issue tracking this.


Hi I am trying to build prover and verifier as separate rust binaries and currently facing a problem with keys generation. What is the recommended way of passing proving and verifying keys to separate binaries? I was thinking of generating them in build.rs and serializing to text files to deserialize them later while building binaries, but the structs don’t seem to support serialization out of the box

A: There is currently no way to serialize proving or verifying keys, due to limitations in how the internal types are structured. The tracking issues for this are https://github.com/zcash/halo2/issues/443 and https://github.com/zcash/halo2/issues/449.


What is the reason for create_proof and verify_proof to use params as an argument? As far as I understand, a constructed params instance is a toxic waste as it exposes a random generator used for vk and pk creation and therefore shouldn’t be shared publicly. But if I want to run prover and verifier independently (on different machines, for example) I am supposed to share a constructed params instance therefore compromising random generators used for keys generation. Which gives a malicious party the ability to reconstruct the keys and create malicious prover/verifier. Is there something I am missing on this?

str4d: params is not toxic waste. It is deterministically generated and can be reproduced by anyone who knows the circuit structure (which the verifiers need to know). params is just common data that both the prover and the verifier need. It could equivalently be considered “part of the verifying key”.

YT: the params contain only public information:

  • zcash/halo2 does not use a trusted setup. Params contains a vector of random curve points g: Vec<C>, which are not secret values. rather, they are generated in a transparent way using public information: https://github.com/zcash/halo2/blob/main/halo2_proofs/src/poly/commitment.rs#L47-L62
  • there is a fork of halo2 that replaces the IPA commitment scheme with KZG. this indeed requires a trusted setup; however, the resulting Params does not contain the secrets used in the setup. instead, it contains only publicly known commitments to them. to find out more about the KZG trusted setup: https://vitalik.ca/general/2022/03/14/trustedsetup.html

A: Yeah, it looks like I was mislead by the KZG params description from pse fork sources and decided that the same goes for IPA as well. Thank you guys a lot for the explanation, that is really helpful. So if I want to use KZG scheme (which allows params, vk and pk serialization), I have to use trusted setup, and if I wish to avoid trusted setup I have to use IPA scheme, which currently doesn’t support serialization (as far as I saw in GitHub issues)? Maybe there is a way to have both trustless setup and the ability to share keys between different machines, that you are aware of?

zcash/halo2 will eventually support serialization, it just requires a bunch of refactoring work first we haven’t had time for But in the meantime, you can share keys between different machines, by running the same keygen on each of the machines. The keys aren’t “random”, they are deterministic This is precisely what we do currently with zcash/halo2 in its use in Zcash. Every machine has the code for the circuit, so they just run keygen once on startup and then keep the keys in memory.

So if I run i.e. let params = Params::new(4); let vk = keygen_vk(params, &empty_circuit); let pk = keygen_pk(params, vk, &empty_circuit); on 2 different machines (prover and verifier) with the same circuit code, the proof generated by prover will be verifiable by the verifier? Or does it require some more tinkering with keygen parameters? Sorry if my questions may seem pretty basic, I am still new to halo2 and mostly think based on experience with trusted setup SNARKs

As long as the inputs to the keygen functions are the same, the outputs will be as well. So yes, you need to use the same k value for Params, the same Circuit type, for both keygen_vk and keygen_pk, and the same empty_circuit instantiation (using Value::unknown() for all witnesses, but anything else that is not a Value witness needs to be the same, in order for the circuits to have the same structure).


layout / Layouter

Q: what is the point of the layouter having different namespaces? does it actually mean anything/separate anything or is it just for debugging

A: It’s purely for debugging. The namespaces are implemented via closures specifically so that in normal prover or verifier usage, they have zero cost (as the closures are never evaluated).


Q: I am not familiar with the rules for circuit layout. , I am unsure about the rules for arranging the rows of the circuit. Can the rows be laid out arbitrarily, or are there specific guidelines to follow ?

A: one important rule is that: cells queried in a custom gate MUST be assigned in the same Region. this is because the Layouter can move Regions around for optimal packing; so we cannot assume that offsets between different Regions will be preserved.

for example:

  1. a mul constraint that queries the out cell at Rotation::next: https://github.com/zcash/halo2/blob/main/halo2_proofs/examples/simple-example.rs#L89-L117
#![allow(unused)]
fn main() {
// Define our multiplication gate!
meta.create_gate("mul", |meta| {
	//
	// | a0  | a1  | s_mul |
	// |-----|-----|-------|
	// | lhs | rhs | s_mul |
	// | out |     |       |
	//
	let lhs = meta.query_advice(advice[0], Rotation::cur());
	let rhs = meta.query_advice(advice[1], Rotation::cur());
	let out = meta.query_advice(advice[0], Rotation::next());
	let s_mul = meta.query_selector(s_mul);

	vec![s_mul * (lhs * rhs - out)]
}
  1. this out cell is assigned at offset 1, in the same region as the lhs, rhs cells at offset 0: https://github.com/zcash/halo2/blob/main/halo2_proofs/examples/simple-example.rs#L212-L216
#![allow(unused)]
fn main() {
fn mul(){
	// Finally, we do the assignment to the output, returning a
	// variable to be used in another part of the circuit.
	region
		.assign_advice(|| "lhs * rhs", config.advice[0], 1, || value)
		.map(Number)
}
}

Runtime circuit configuration

https://docs.rs/halo2_proofs/0.2.0/halo2_proofs/plonk/trait.Circuit.html#tymethod.configure

This method does not have a &self parameter, so configuration must be independent of the concrete value of the type I implement Circuit for. Does it mean that the number of columns that I allocate for my Circuit must be constant? I.e. I can’t have a: MyCircuit and b: MyCircuit use different numbers of advice columns? Is there a way to allocate different number of advice columns (determined at runtime)? Or is it not allowed by design?

A: Indeed, currently the number of columns is fixed. This is for various reasons including simplicity of the original implementation, but it is a limitation we want to remove: https://github.com/zcash/halo2/issues/195

Allowing circuits to be fully defined at runtime would also enable us to support IRs (and thus compile circuits from other languages): https://github.com/zcash/halo2/issues/550

without_witnesses()

What is the purpose of without_witnesses method here? Is it to strip the Circuit of any prover-private information, so it can be safely sent/published somewhere? (It wasn’t clear for me from the comment, because it answers the question of how and not the question of what.)

  • https://github.com/zcash/halo2/blob/642924d614305d882cc122739c59144109f4bd3f/halo2_proofs/src/plonk/circuit.rs#L475

A: circuit.without_witnesses() has to maintain the “shape” of circuit, that is, the structural parameters that determine the set of columns, the placement of regions and gates etc. https://github.com/zcash/halo2/issues/613

This is primarily because the layouter might need multiple passes, some of which do not compute witnesses. Usually, it’s that it would be unnecessary and inefficient to compute them on every pass, not that it would be incorrect or insecure.


synthesize()

Q: Is Circuit::synthesize() context aware? Specifically, does it know if it’s the process of proving/verifying or key generation? If so, is this accessible from the function?

A: no, but FloorPlanner::synthesize() may be able to see the CS: Assignment bound it’s using:

copy_advice()

value.0.copy_advice(|| format!(“output[{}]”, i), &mut region, config.advice[2], i)?;

does this line copies the value into the 3rd advice cloumn ith row?

yes! (note: it copies to offset i in the region, which may have a different absolute row after the floorplanner passes.)

did you mean the values to be inserted may shift?

yes, even though the value is assigned at offset i in the region, the region itself may be moved elsewhere in the circuit.

We will partition a circuit into regions, where each region contains a disjoint subset of cells, and relative references only ever point within a region. Part of the responsibility of a chip implementation is to ensure that gates that make offset references are laid out in the correct positions in a region.

Given the set of regions and their shapes, we will use a separate floor planner to decide where (i.e. at what starting row) each region is placed. There is a default floor planner that implements a very general algorithm, but you can write your own floor planner if you need to.

  • (from https://zcash.github.io/halo2/concepts/chips.html#chips)

we definitely need better documentation. if you have any questions that you would like the book to address, please do open an issue.

64 bit field elements / Value<F>

I need to use 64 bit field elements to call a function in a custom gate. I’m currently winging it by using “Value<F>,” is there a better way to represent a u64 in a field?

YT: to range-constrain the field element to 64 bits, you may consider breaking it into (say) bytes and looking these up in a precomputed table of values 0..=255.

something like this is done in the ECC gadget: https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/ecc/chip/mul_fixed/short.rs#L79-L106

Daira: As well as that, consider using AssignedCell<u64, F> to keep track of the type in the Rust code (note that this doesn’t by itself enforce the range constraint): https://github.com/zcash/halo2/blob/main/halo2_proofs/src/circuit.rs#L95-L164


I see, so , in

#![allow(unused)]
fn main() {
pub fn value(&self) -> Value<&V> {
	self.value.as_ref()
}
}

Is value returning a reference to a 64 bit max value or just the type?

YT: value() returns a Value<&u64> which is a wrapper around an Option<&u64>.

however, this u64 type is only enforced by the Rust type system, and NOT by the circuit constraints. to range-constrain it in the circuit, you can use e.g. a lookup table.

circuit layout

I am attempting to optimise the simple example from the halo2 examples (c = a**2 * b**2). I have removed the fixed column for loading constant and instead loaded it into advice column. Also combined the two advice columns into one. I have tried the same and seems to work (verification success and failure expectation). The circuit layout previously was using: 9 rows x 5 columns = 45 cells and after doing the above it is using: 12 rows x 3 columns = 36 cells. My questions: Since the cells used appear to be less, does this optimisation make sense? (even though a constant is assigned in advice instead of fixed column) I also see additional detail called “10 usable columns” and “26 usable columns”, what does it mean and does it contribute to the prover cost? Below is circuit layout before and after

YT: (nice diagrams!)

  • assigning the constant to an advice column changes the circuit’s intended behaviour. we no longer have the guarantee that the final c = constant * absq is a multiple of the desired constant, since the prover could have witnessed any arbitrary factor in its place.
  • for your provided k = 4, you get n = 2^4 = 16 rows, the last few of which are “poisoned” (unusable). to see how and why these rows are poisoned: https://github.com/zcash/halo2/blob/main/halo2_proofs/src/plonk/circuit.rs#L1433-L1460
  • the prover cost is largely dominated by the size of n. roughly speaking, there is a tradeoff between no. of rows and no. of columns: for the same area, a shorter/wider circuit has a faster prover/larger proof; and a longer/narrower circuit would have a slower prover/smaller proof.

constant in halo2

Q: Hi! I’am a little confused about constant in halo2? It seems that you don’t need to copy from a fixed column to constraint a cell to a constant in halo2? see my issue for more details: https://github.com/zcash/halo2/issues/766. Looking for your reply. Thanks!

A: the constant value will be assigned to a cell within one of the fixed columns configured via ConstraintSystem::enable_constant.

so behind the scenes, the assign_advice_from_constant API still creates a copy constraint between a fixed cell and an advice cell.

Q: Thank you for your reply.But i still confused because ROUND_CONSTANTS in the code is not a fixed column.It’s just a [u32; 64]. See: https://github.com/zcash/halo2/blob/2bdb369393c11dfd093d68f9253e8f12e9e6281a/halo2_gadgets/src/sha256/table16.rs#L26-L36

My question could turn into: why CompressionConfig in sha256 gadgets doesn’t need a Column<Fixed> ? 😫 https://github.com/zcash/halo2/blob/2bdb369393c11dfd093d68f9253e8f12e9e6281a/halo2_gadgets/src/sha256/table16/compression.rs#L433-L454

YT: i think you found a bug in the experimental sha256 gadget! ROUND_CONSTANTS should definitely be copied from a fixed column.

e.g. this is how Poseidon initialises the rate and capacity elements: https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/poseidon/pow5.rs#L291

a pull request fixing this would be greatly appreciated!


gas cost/onchain Verifier/solidity

hi, what’s the actual relation between the halo2(kzg) circuits’ scale and the zkp verification gas cost? I use the snark-verifier: https://github.com/privacy-scaling-explorations/snark-verifier to generate the solidity verifier, it seems to be more gas consumption as my circuit scale gets lager

YT: i’m not familiar with the snark-verifier, but i would guess that costs relate to this CostEstimation: https://github.com/privacy-scaling-explorations/snark-verifier/blob/main/snark-verifier/src/verifier/plonk.rs#L171-L188

CPerezz: In recursion verification circuits for KZG, the more Advice columns and the larger they are, the more you pay at proving time.

As for the solidity verifier, it should scale only with respect to the amount of columns used.

Han: yeah the gas cost would scale mostly with the amount of columns used, see this for more accurate estimation https://github.com/privacy-scaling-explorations/snark-verifier/blob/main/snark-verifier/src/loader/evm/util.rs#L97.


GPU

Q: Does Halo2 real prover use GPU? (for e.g. in M1/M2 chip series laptops)

A: No. Although you can use pasta-msm crate to derive ECC ops to GPU. (It can require to adapt things a bit)

Q: I have a very general question about the Circuit trait: as we have to define gates & columns within fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config;, all gate configurations have to be known at compile time, unless I’m overlooking something! I don’t see a good reason for this, at key generation time should be enough? In my case, I’m implementing a prover for a machine learning model, and some gate constraints depend on the model structure, which is not fixed at compile time. See also my issue here: https://github.com/zcash/halo2/issues/771

A: i would recommend looking at how ezkl does it! will try to find a more specific example later: https://github.com/zkonduit/ezkl

Reply : Thanks, that was good advice! It turns out that halo2 has a circuit-params feature, which does exactly what I need here. I’ll put more details in the linked issue for future reference and close it. Looks like it only works in the PSE fork though, was merged 3 weeks ago: https://github.com/privacy-scaling-explorations/halo2/pull/168

trusted setups

Hi guys, do you know where to find trusted setups for a kzg-based ceremony?

https://github.com/iden3/snarkjs#7-prepare-phase-2

Q: What is the correct way to use that?

#![allow(unused)]
fn main() {
let f = File::open("src/powers_of_tau/powersOfTau28_hez_final_18.ptau")
    .expect("couldn't load params");

let mut reader = BufReader::new(f);

let params = ParamsKZG::<Bn256>::read(&mut reader).expect("Failed to read params");
}

When running this I get the error Failed to read params: Custom { kind: InvalidData, error: “input number is not less than field modulus” }

CPerezz: Are you sure that the endianness and format are the same for the Bn256 struct and the ptau file elements?

No, I’m not. Mine was more of a naive effort 🥲

I think this is probably the issue. You should just generate a trusted setup with Halo2 and figure out the serialization differences later. Once you need to use the actual ptau

For using existing KZG trusted setup, we have already converted perpetual-power-of-tau and hermez into pse/halo2 format here (https://github.com/han0110/halo2-kzg-srs), so we can download and use directly, or in the same repo it also has a script for us to do the conversion manually.

circuit k

Q: Whats the max number of rows in PSE’s Halo 2? Ive heard k=28 (2^28) through the grapevine, but I cant find anything anywhere that says this.

A: I think @CPerezz @Han could point you to this; iirc, the trusted setup was 2^28 since that’s the two-adicity of the BN254 scalar field, but i think they limited their circuits to 2^25 to allow for higher-degree constraints.

I think is the max you can use with Halo2 and BN. We have 1 spare root JIC. You will need 100’s of TB of RAM to compile a 2^28 circuit anyways. And yes, we capped the zkevm circuits to 2^25 max. And ideally would be nice to lower that

Q: Can we auto estimate minimum value of k given a circuit object? or is the only way to manually try to find it? (edited)

A: to get the minimum no. of rows required at configure time, you could do:

#![allow(unused)]
fn main() {
let mut cs = ConstraintSystem::<Fp>::default();
MyCircuit::configure(&mut cs);
println!("minimum rows: {}", cs.minimum_rows());
}
  • for synthesis it’s a little trickier.
  • when making a real proof, we would likely already be working with some hard-coded k in the params (e.g. random generators for IPA, powers of tau for KZG).
  • but for dev purposes, i think we could write a FloorPlanner that just returns the no. of rows used.

thanks, this is very helpful for setting k for dev purposes cs.minimum_rows().ilog2() + 1

for synthesis it’s a little trickier.

I just realised it doesn’t include synthesis. Is it still possible to estimate a value of k inclusive of synthesis? E.g. let the synthesis run without a row bound and see how much it consumes, so it does not require inputting value of k in MockProver and for other utilities e.g. generating diagrams.


negative/overflow

Q: Is there any way to express negative values in Halo2 witness cells? I assume the answer is no since there are no integers in Fp. Checking for confirmation

Daira: The most efficient encoding of a negative value -x is usually as p-x. Obviously you will have to consider the possibility of overflows.

Can you please explain what would be a case in which an overflow may cause odd behaviours?

suppose for example that the circuit is checking a nullifier for a Zcash-like cryptocurrency. If more than one possible value is accepted for the nullifier, then there is a double-spend bug. Depending on other details of the circuit, it’s likely that an overflow would allow that.

For example, if the range check on α described at https://zcash.github.io/halo2/design/gadgets/ecc/fixed-base-scalar-mul.html#base-field-element were not performed, then you could witness two possible values of α (by varying ψ) that give the same nullifier. This could also occur if there were any mistake in the range check due to overflow. Fixed-base scalar multiplication - The halo2 Book

(As it happens, the correctness of the range check actually relies on underflow in the case where it constrains 0 ≤ α_0+ 2^130 − t_p < 2^130. So overflow or underflow is not always harmful as long as it’s accounted for.)


F_r / F_q …

Hi I saw you’re also in the PSE discord and hence I think asking you would be better. If I’m trying to modify the sha256 gadget for halo2wrong how can I know where should I use Fq and where to use Fr? More generally how should the fields change?

CPerezz — 02/27/2023 5:12 PM You should use the Scalar field always. If you’te using halo2wrong with Bn you should use Fr IIRC

What you want is basically to port the gadget to halo2wrong API/chip. Is that right?

wait to be organized…

Q Hi, how is it possible to constrain equality between an instance cell and an advice cell without first using assign_advice_from_instance() followed by constrain_equal ?

Because calling assign_advice_from_instance() adds 25% proving time. I gather since this issue is still open, it is not possible yet https://github.com/zcash/halo2/pull/625

A assign_advice_from_instance() does precisely what you’d otherwise do manually (if we exposed APIs to directly constrain instance cells, which we don’t currently for UX reasons): it assigns the given advice cell with the same value as the instance cell holds, and then calls (effectively) constrain_equal. So the only difference between that and what you’re doing is that you have an additional constrain_equal call, and I don’t see how that could cause the prover to be 25% slower.

Q I struggle to find a place where parsing of operations for an Expression is being handled. It’s pretty clear how both evaluation and degree computation rules are defined, but isn’t there some structure like Abstract Syntax Tree, …? A Expression is defined here: https://github.com/zcash/halo2/blob/main/halo2_proofs/src/plonk/circuit.rs#L488-L509

there’s no parsing because you directly create instances of Expression using its operators and constructors for example, the operators are defined here: https://github.com/zcash/halo2/blob/main/halo2_proofs/src/plonk/circuit.rs#L723-L765

so Expression is the Abstract Syntax Tree type.

That I was able to understand, but the expression is coming from create gate function if I’m not wrong?

Normally, in the configure method of a chip, you would call query_* methods on the passed-in ConstraintSystem (which return Expressions), and then combine them with +, -, and *. Does that answer your question?

ahh yes I see…, operations are defined for Expression type so compiler will actually know to create AST from equation, it was obvious, not sure why i got confused with it

Q This might be a dumb Q:

  • I’ve been looking into the various ways witness generation works in ZK libs. In Circom for example, they do some crazy codegen from the Circom template code, and generate a .dat file + some Cpp which automagically generate the witness given some inputs. How does this work in Halo2?

A A1: There’s no magic; you write all of the code to generate witnesses in Rust. The API encourages you to do this in the same place as enabling the corresponding constraints. (That was a lesson we learned from libsnark where witness generation and constraint enforcement were separated; it was very error-prone.)

A2: The way it works in halo2_proofs is the same way it worked in bellman (drawing from the same learnings @Daira Emma (ze/hir) — ECC mentioned above): during synthesis, you write Rust code that uses AssignedCell, which wraps a pointer to a cell within the circuit and its assigned (if known) Value. You operate on the cells either directly or via gadgets, and every time a new cell is assigned, a closure is provided that evaluates to the value of that new cell. The closure is only evaluated when we need it (so for cells in advice columns, we only evaluate the closure during proving). The core idea is that the only spot where circuit synthesis and witness computation can significantly deviate is either inside that closure, or inside Value::map, which are both generally localised within the circuit synthesis code, and thus easier to compare against for bug finding.

Q How does one deal with variable length public inputs? I have a secp256k1 public key as a public input, but it can either be in uncompressed form (65 bytes) or compressed form (33 bytes). It is laid out in 65 or 33 rows of an instance column.

I want to write a gadget that verifies that an ECDSA signature corresponding to this key is correct. I plan to witness the public key in some advice columns (using halo2wrong techniques) and check that the bytes of this witnessed public key match the bytes in the instance column. Do I write constraints for both cases (65 and 33) and check that one of them is satisfied?

A if i’m understanding the formats correctly:

  • uncompressed: 0x04 | x | y
  • compressed: parity | x, where parity is either 0x02 or 0x03

we could do something like :

  • if the key is in the compressed form, pad it with 32 0 bytes, so that we always have a 65-byte public input;
  • use the first byte as a isCompressed flag;
  • copy in all 65 bytes of the key to advice cells;
    • if isCompressed = 0x04 (uncompressed key), check that each witnessed byte equals each public input byte
    • if isCompressed = 0x02 | 0x03 (compressed key), check equality of the x-coordinate bytes, and check that the last 32 bytes are all-zero

i think we would also need an extra parity consistency check in the compressed case. (i’m assuming that, in either case, we’ll need to witness the full y-coordinate in the circuit.)

Thanks for taking the time to explain this approach! This makes sense.

In addition, you need to check that the curve equation holds for the witnessed x and y coordinates

Is this to avoid an attack where a malicious y-coordinate could be used to verify a forged ECDSA signature?

It’s to avoid being able to forge proofs. If you don’t verify the curve equation then later constraints that assume the coordinates are valid will potentially be unsound. Also, for compressed encoding you have to implement the curve equation anyway to recover the y coordinate, so it’s no less expensive. In fact, what you’d do is witness the x and y coordinates with a curve check (like you’d do to witness any arbitrary curve point), witness the encoding, and then check consistency between the two (so checking the coordinates match for uncompressed, checking the sign bit of y for compressed).

Q Question about lookup tables: It seems that halo2 enforces that lookup tables have the same length. What do I do if I want them to have different lengths? A A1: Do you mean the fact that the tables themselves must have the same number of rows as the circuit? You just fill each table up with however many rows it needs, and the backend will fill the rest of the rows with an appropriate default value. Lookup tables need to live in separate fixed columns, so the fact that two tables have different “real” sizes doesn’t matter as they won’t interact (and the backend pads them to the same number of rows).

A2: to have two logically different tables “share” the same column, we could introduce an extra tag column that serves to index the tables. the inputs to the lookup argument would now also have to include an input_tag to specify which logical table it is using.

A3: I think we should handle this via the new dynamic table API (since that introduces the tag column, and does not actually require that the table uses only advice columns)

Q Hey everyone, I’m trying to get the max of two Values (ex: two Value<Fp> variables). Ideally I’d like to just compare the underlying field elements but it doesnt look like those field elements can be extracted(https://github.com/zcash/halo2/blob/main/halo2_proofs/src/circuit/value.rs#L11). Is there a quick workaround for this?

#![allow(unused)]
fn main() {
/// This behaves like `Option<V>` but differs in two key ways:
/// - It does not expose the enum cases, or provide an `Option::unwrap` equivalent. This
///   helps to ensure that unwitnessed values correctly propagate.
/// - It provides pass-through implementations of common traits such as `Add` and `Mul`,
///   for improved usability.
#[derive(Clone, Copy, Debug)]
pub struct Value<V> {
    inner: Option<V>,
}
}

A Hey everyone, I’m trying to get the max of two Values (ex: two Value<Fp> variables). Ideally I’d like to just compare the underlying field elements but it doesnt look like those field elements can be extracted(https://github.com/zcash/halo2/blob/main/halo2_proofs/src/circuit/value.rs#L11). Is there a quick workaround for this?

Q-1 Can you do a.zip(b).map(|(a, b)| f(a, b))?

A-2 i dont think so (unless the a, b are Fp vars) . so i think the issue is that Value doesnt implement the Ord trait which is what I want to be able to do comparisons on Values.

Q-2 Sorry, that was a bit confusing, this: a.zip(b).map(|(x, y)| f(x, y)), If a, b are Value<Fp>, I believe that inside the map the x, y are &Fp.

A-3 ah my bad, yea that’ll work. thanks !

A-4 Yep, that’s the intended way to implement operations that Value doesn’t expose

Q SUN-YI: On a related note, is there an intended way to check whether two Values contain the same inner Option? (I’d like to use this to test witness generation code programmatically, so I want to get a bool.)

A Use Value::assert_if_known or Value::error_if_known_and. You’d map the two values into one containing a tuple, then use the above methods.

Q SUN yi: I am in a situation where I’d like to read the inner value in a Value<F> for witness generation. Is this possible in any way in the 0.2.0 API?

The specific operation I want to do is:

  • I have a x: Vec<AssignedCell<F, F>> and a separate idx: AssignedCell<F, F> and I know that 0 <= idx.inner < x.len().
  • I’d like to find the array y so that y.len() = x.len() and y[i] == x[i] if i < idx.inner and y[i] is a newly assigned fixed 0 cell if i >= idx.inner. Is there some way to do this?

A lyt: where is i coming from? i think we’ll probably need an upper bound on how large i can be, i.e. how long the y vector can be str4d - ECC: In this scenario, the length of x is fixed in the circuit structure, as it is effectively Vec<Value<F>>. So what you want to do here is something like:

#![allow(unused)]
fn main() {
let mut y = vec![];
for (i, x_i) in x.iter().enumerate() {
    let y_i_val = x_i.value()
        .zip(idx.value())
        .map(|(x_i, idx)| {
            if i < idx {
                x_i
            } else {
                F::zero()
            }
        });
    let y_i = region.assign_advice(
        ...,
        || y_i_val,
    );
    y.push(y_i);
    // Also need to enable some
    // constraint that enforces the
    // relationship on each cell.
    // The constraint would be where
    // the fixed 0 gets constrained
    // once i >= idx.
}
}

SY: My understanding is this assigns a new cell for each cell in y. Is it possible to have the first idx cells in y simply be clones of the AssignedCells in x, thereby assigning fewer cells? (In the 0.1.0 API, I believe this was achievable by handling the None and Some(idx.inner) values for idx separately.)

Yes, but only if idx is baked into the circuit and not chosen by the prover. If idx is inherently a Value<usize> (i.e. Value::unknown() during keygen) then what you describe doing is a circuit bug. But if it’s a usize (i.e. known at keygen time) then yes, this can be done

I see — is the idea that the location of the cells in y in the grid must be fixed at keygen time?

Sort of. It’s more that the relationship between the cells defined as y, and the places that y is constrained later, need to be fixed at keygen time (as those constraints are your circuit). That means you can’t change which cells y refers to at proving time, or you are leaving your circuit under-constrained, which is (usually) Bad.

Q hey, has there been any discussion around what determines the proof size ? A See the proof_size and marginal_proof_size methods in https://github.com/zcash/halo2/blob/main/halo2_proofs/src/dev/cost.rs

Q is there a standard library/implementation people use for range checks? A SY: We just open sourced a library of base components including range check here: https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-base/src/gates/range.rs

Q How many advice columns is considered too many? Are 500 columns ok? I am trying to implement a circuit that can prove ownership of a Bitcoin UTXO. This involves activating gates in response to Bitcoin opcodes. There are about 200 such opcodes. Each opcode will require at least one column as an indicator function. If I don’t mind the large proof size (for now), is it feasible to have 500 columns in a halo2 circuit. A We previously had issues with that many columns because the Orchard circuit only used 10 so we hadn’t tested that high. But then the zkEVM people came along with a similar issue (EVM opcodes) and started exercising those dimensions, and a bunch of performance improvements have been made that should mean it works fine now.

Q Jason m: Getting stochastic failure (proof sometimes verifying, sometimes not verifiying), even with the same pk held in memory. Any guesses as to why? I will work on a minimal example, but if anyone knows of another place this has happened, we would appreciate a pointer.

A-1: I have encountered the similar issue while some logic in the synthesis iterates a HashMap. I saw in ezkl while model configuration, it also iterates over a HashMap to configure the ConstraintSystem, which might lead to constraints with different order (then verifying would fail). Could this be the issue?

Q-1 Could be! Will check it out. ……. Looks quite likely that this is the problem, once we finish confirming it we will switch that HashMap that snuck in with BTreeMap. Thank you @Han !!

Q Hi, I’m trying to prove that a certain ASCII character X is not present in a string. What would be a more efficient way to do it? I could put all ASCII chars (except X) in a lookup table and prove that the string contains only the chars from the lookup table (Ideally, I could use a multiset equality check but it is still wip in halo2) Is there any better way than the lookup table? Thanks.

A-1 : The simplest way would be to have a constraint of the form char - NUMERIC_VALUE(‘X’) != 0, which you can do by witnessing inv = (char - NUMERIC_VALUE(‘X’))^{-1} and then constraining (char - NUMERIC_VALUE(‘X’)) * inv = 1. This requires that char is also range-constrained to ASCII (e.g. range constrain to 7 bits).

Q-1: thank you very much. Just to clarify: you think that this approach will cost less prover time than lookup tables, correct?

A-1: Probably; the two constraints here are relatively simple. But the lookup table would also be relatively small in this instance, so best way to know is to try both.

Q We are facing a rather curious issue. The MockProver works as expected and verifies the circuit execution but the real verification fails with ConstraintSystemFailure.

  1. Is there any known issues regarding MockProver that justifies this behaviour?
  2. Generally what is the right debugging approach in this scenario?

A This is likely because MockProver does not check everything. We add more comprehensive checks over time, but it’s a development and debugging tool rather than an alternative to full verification. That being said, we’d like to figure out what these edge cases are so we can track improvements to MockProver. The easiest debugging approach currently is to gradually comment out parts of your circuit until the error goes away. That and adding print statements to determine how far through synthesis you got. Overhauling the error handling has been on the backlog for a while; I’d ideally want the error you saw to already include this information.

Q Hello, does anyone have a good code example of a circuit that uses the V1 floor planner? I’m trying to switch my circuit to it, but it expects my circuit’s synthesize function to work on the default circuit (from without_witnesses), which it’s not designed to do. I’m assuming to fix this I’ll need to refactor to use the Value enum everywhere, but before I start on that I’d love to see a working example using the V1 floor planner

A If your circuit doesn’t synthesize with the default circuit, then it very likely has bugs. More specifically, Circuit::without_witnesses shouldn’t return Self::default(), but should instead construct a copy of self that omits witnesses (anything you currently store as Option<T> and would store as Value<T> after the refactor), but including everything else. That is necessary because the “everything else” is necessarily affecting the structure of the circuit, and the structure of the circuit must not change between calls to synthesize. This doesn’t just affect the V1 floor planner; it affects the simple one as well, because keygen calls synthesize without any witnesses.

Q-1: Huh, I haven’t had any problems using the simplefloorplanner with my circuit; both using the mockprover and proving/verifying real proofs I’ll definitely change my circuit to reflect the ‘proper’ way of doing without_witnesses though. Although you may want to change the examples to reflect this design, since most current examples given use Self::default() for without_witnesses Thanks for the tips!

A-1: Yep, there’s an open issue for changing the examples: https://github.com/zcash/halo2/issues/613 Self::default() works fine if Self only contains witness variables, and none of the examples have structural variables in Self.

Zcash’s Orchard circuit uses the V1 floor planner: https://github.com/zcash/orchard/blob/main/src/circuit.rs#L191

Q I am trying to understand a halo2 circuit layout using the dev-graph feature. In the attached picture, the green regions seem to correspond to assigned rows of some columns. What do the other colors correspond to? I want to understand why there is a large orange region in the middle which has the same color as the unused rows.

A The orange background indicates the advice columns, and the blue background indicates the fixed columns. The line showing how many rows are used is down past the middle because of one of the fixed columns that assigns to that row, while the advice columns are barely used.

Q-1 Thanks! I am using the halo2wrong ECDSA chip. I think the tall fixed column is 18-bit range check which takes up 2^18 rows. The advice columns in the middle have only one row used. It’s rendering is so thin compared to the rest of the columns that it is invisible. The green columns have a darker shade of green in some rows. Does that correspond to cells that have been assigned? … Also, the rightmost fixed column has a darker shade of blue. Does that signify anything?

A-1 The darker blue indicates selector columns, which are a specialized subset of the fixed columns.

A-2 And yes the darker green corresponds to assigned cells. (It is actually possible to assign the same value more than once, which I think produces an even darker green; this is useful to debug overlaps when zoomed in.)

Q

How many rows are used by halo2 “system” at the bottom of the matrix? A It is “dynamic” in that it depends on your circuit structure. Put another way, you don’t pay blinding rows for functionality you aren’t using. The actual calculation is here: https://github.com/zcash/halo2/blob/ec9dcefe9103fc23c13e8195120419d4d2f232a6/halo2_proofs/src/plonk/circuit.rs#L1432-L1472

#![allow(unused)]
fn main() {
    /// Compute the number of blinding factors necessary to perfectly blind
    /// each of the prover's witness polynomials.
    pub fn blinding_factors(&self) -> usize {
        // All of the prover's advice columns are evaluated at no more than
        let factors = *self.num_advice_queries.iter().max().unwrap_or(&1);
        // distinct points during gate checks.

        // - The permutation argument witness polynomials are evaluated at most 3 times.
        // - Each lookup argument has independent witness polynomials, and they are
        //   evaluated at most 2 times.
        let factors = std::cmp::max(3, factors);
...
}

Thank you, this is the calculation I was looking for. So the max assignable rows should be 2^k - meta.minimum_rows(), where the call to minimum_rows happens at the end of layout (because it increases as queries occur)?

Yeah. This is also why the “not enough rows” error doesn’t suggest a value of k that will fix the problem, because it was always suggesting current_k + 1 and then immediately encountering the error again as we progress further through Circuit::synthesize.

Q Is the protocol described here https://zcash.github.io/halo2/design/protocol.html same as the actual protocol that has been implemented in the zcash’s halo2 repo? A Yes. That page matches what the halo2_proofs crate currently implements, and provides a security argument. If in future backwards-incompatible changes are made to what halo2_proofs implements (which are likely necessary for implementing recursion/recursive), that page will also be updated.

Q how difficult would it be to swap the Pasta curves in the IPA of zcash/halo2 with Secp256k1 curves, as implemented here: https://github.com/privacy-scaling-explorations/halo2curves/tree/main/src/secp256k1 I know FFT would be slower but for example, private secp256k1 ECDSA signature verification inside the circuit would be very few constraints due to no wrong field arithmetic? A Definitely doable; secp256k1 has the necessary curve cycle. The main issue currently is that there are extension traits in pasta_curves that halo2_proofs depends on, so you’d need to maintain a dependency on pasta_curves for now (though we are working on moving these pieces upstream into ff and group). There might also be a few spots inside zcash/halo2 where we explicitly depend on pasta_curves curve types, though I think those are in halo2_gadgets (just places we didn’t have time to generalise while implementing Orchard), and halo2_proofs is fully generic?

Q-2: Appreciate the speedy reply! It looks like in the README they’ve implemented the halo2 traits for FieldExt and CurveExt for these curves as well, so does that cover the first part of what you are saying?

Yeah, that should be sufficient

I got it done! yay! (I got secp256k1 working for the IPA in a very simple proof. still need to benchmark the full ECDSA signature verification code)

Q Referring to this doc https://zcash.github.io/halo2/design/protocol.html, Is this correct to consider that lookup gates are included in the description of the function g(X, …) ?

Also, what are the polynomials s_i(X) that are being constructed in step 10 ?

A Conceptually yes. The actual constraints depend on two challenges β and γ that must be sampled after committing to the other columns — i.e. other than (A’, S’, Z computed in the lookup argument and the product columns in the permutation argument) I don’t see the sampling of those challenges in the protocol steps; maybe they were merged with some of the x_i or maybe those steps are just missing (you’d have to look at the code to check, or @seanzcash would know). In any case we use β and γ for both the lookup argument and the permutation argument

Q Can you give an intuition on why multiplying (1 - q_last(X) - q_blind(X)) polynomial helps in imparting zk to both lookup and permutation arguments? I noticed that this is different than how it was done in the Plonk paper. A It’s not that it helps impart zk to those arguments, but that it is necessary to prevent those arguments from being broken by the way we do zk in Halo 2. We require some number of “blinding rows” in the table, which are assigned random values in the advice polynomials. These random values cannot participate in the lookup and permutation arguments because their contents are by definition random and thus don’t follow e.g. the sorting / arranging requirements of the A’ and S’ polynomials in the lookup argument. So we use the above constraint factor to ensure that the blinding rows are unconstrained by the lookup and permutation arguments.

A-2: if zk were not needed, then the description of the protocol before “Zero-knowledge adjustment” would work. But that relies on the references to next and previous rows “wrapping around” at the last and first row respectively. If there are blinding rows that have random contents, then the constraints would not be satisfied at those rows

Q If I want to prove that an advice column is a permutation of a fixed column, what API call can I make inside meta.create_gate to generate the permutation proof? A this is a multiset equality check and there is currently no API for it: https://github.com/zcash/halo2/pull/669

you could use meta.lookup to check that every entry in the advice column appears somewhere in the fixed column; but this isn’t the same as checking that it’s a complete permutation of the fixed column.

Q Hi I am currently working on a ZK SNARK system with verifier within smart contract on an ethereum-based blockchain. The system currently works with GM17 protocol and verifier is mostly generated by a tool which creates Solidity file for it.

I am not sure if that is possible to bring Halo2 verifier to EVM since compiling Rust code is not an option I am aware of (there is EWASM but as far as I know it’s under development and not coming any time soon) and writing verifier from scratch in Solidity seems like a lot of challenge. So I would like to know if anyone is aware of any way to bring Halo2 verifier to EVM or heard of any work in this direction. Any help or advice would be greatly appreciated

A I have not dug very deep yet but I asked this question in a different discord and was pointed to Scroll’s work: https://github.com/scroll-tech/halo2-snark-aggregator/tree/main/halo2-snark-aggregator-solidity I am not experienced enough with it to tell you whether or not this is a usable SDK for putting Halo 2 proofs on-chain, or if you would have to use it as an example and copy a bunch of boilerplate code to do so

There is also generic PLONK verifier from PSE team’s work(https://github.com/privacy-scaling-explorations/plonk-verifier) which generates Yul verifier internally. It can work as PSE’s Halo2 verifier itself. (Not sure whether it works for ZCash halo2)

Q hi, is there any example of a simple circuit where an instance column is used in create_gate. So, no constrain_instance is used. All the examples that I see constrain a cell to instance column cell.

For example, a circuit where the element wise product of two advice columns (a,b) is equal to the instance column (c). I created a gate specifying a*b-c, and assigned a,b through layouter.assign_region, but looks like I am missing something as the MockProver is failing. Anything more that i need to do?

A Instance columns can’t be easily used directly in gates, because gates are enabled relative to regions, and regions can be arbitrarily reordered, while instance column contents cannot. If I recall correctly, it still might be possible to set this up, but you’d need to be using a floor planner that does not do any packing, which is less efficient.

I see. since i am just developing a toy circuit, I used SimpleFloorPlanner. After I posted the question, I updated the gate to be q_sel*(a*b-c), and it worked. So looks like a selected is always necessary to pass?

The mock prover might be making assumptions about gates having selectors? But it shouldn’t, since that isn’t strictly required.

Q is there any documentation using a real prover/verifier for a halo2 circuit? A there’s a PR for this: https://github.com/zcash/halo2/pull/670/files#diff-93ad4e38438d6dd3ae11bc620ae22751d0d80f38a3e44695944de044078e568eR340 Also there is this test https://github.com/zcash/halo2/blob/main/halo2_proofs/tests/plonk_api.rs

Q Is it possible to use halo2 with gpu, when creating a proof? A Not yet, but there is work being done towards this that will hopefully make its way into halo2_proofs in the near future.

Q Hello! Noob question, wondering what the “Z” and “U” values for a fixed base are. (https://github.com/zcash/halo2/blob/677866d65362c0de7a00120c515a9583b2da2128/halo2_gadgets/src/ecc/chip/constants.rs#L150-L156) https://github.com/zcash/halo2/blob/677866d65362c0de7a00120c515a9583b2da2128/halo2_gadgets/src/ecc/chip/constants.rs#L108-L117 is find_zs_and_us so I think I can just use it without knowing why it works

A So for a table of w points we should be able to represent the choices of y-coordinates in w bits. Alternatively, if we have some piece of information (represented by a single field element z for each table) that “randomly” generates those bits, and provided w is not too large, then we can search for z that happens to generate the correct values for the w bits Half of all field elements are squares. So, there may (will, because the field is large) exist z such that z + y is square and z - y isn’t, for every point (x, y) in the window table. Then if the prover witnesses a square root u of z + y, and we also check the curve equation, then we know that we have the correct y (and not its negation)

Q Hello frens, Im trying to write create a WASM version of Poseidon hash. I started implementing the WASM wrapper and ran into an issue very early on. When adding halo2_gadgets and halo2_proofs as dependencies. https://github.com/ImmanuelSegol/zk-benchmarks/blob/main/halo2-wasm/Cargo.toml

I get the following error when compiling:

Anyone know why this happens ? Im abit new to rust as well so maybe im missing smtn. but shouldn’t t halo2_gadgets and halo2_proofs be includable in a wasm project without throwing an error. if i remove halo2_gadgets and halo2_proofs my wasm compiles correctly Thanks

Q what is the reasoning behind the fact that From<u64> is only enforced by PrimeField and not by Field directly?? It’s annoying in halo2 where sometimes you would be Ok with F:Field as trait bounds and you need to add extra stuff just due to this 😦

I’m sure I’m missing something here. Could you enlight me?

A I guess there is always a well-defined embedding of Fp into F{p^k}, but it isn’t entirely obvious to me that this is what you would want for a From conversion. (Consider for example F{2^64}; should that just discard all but the low bit?)

Q: Hey Daira! I’m not sure how this is linked to the trait itself. The main difference between Field and PrimeField is just that, that one of them is guaranteed to be Prime order and so, has extra methods avaliable. And this doesn’t really apply to From<u64>. My concern was more towards rust-API perspective as I did not think that this had something to do with theoretical background.

A: Finite fields can be either prime or extension fields. My point is, what’s the intended semantics for From<u64> on an extension field? It isn’t obvious.

Q Ohh I see. I don’t think the distinction applies for extension fields. I would never expect From<u64> to have anything to do with exponentiation even if working with extension fields. It’ll purely: turn into F encoding-form this u64 value. For what you were referring, I’ll always expect the need to do: let x = F::from(2u64).pow*&[64u64,0,0,0]) to express F{2^64}. (edited) A So, if you know that you don’t have an extension field, what stops you from using the PrimeField trait as a bound? (Genuine question.)

Q: Ohhh mainly the fact that I leave a way less fn calls avaliable open and we save compilation time. If I can restrict more the trait, it allows to call a lot less stuff and also, the compiler has less things to worry about. My question is also genuine in the sense that I don’t really purpose to change it. But it’s like having F::ONE as PrimeField instead of Field. Is such a basic thing that I’d expect to get it from the Field trait directly.

str4d - ECC:

  1. @CPerezz the core question is “how should a u64 integer be mapped into the field?” This question is easy to answer for prime fields: there is a 1:1 mapping as long as the field is at least 65 bits. ff_derive supports a minimum of 64 bits for modulus size, so in that case there may be a slight reduction, but it’s not too noticeable. For binary or extension fields the question is harder to answer, and will depend on the structure of the field. There is not an obvious mapping that is correct for every possible field.

This is also why we have Field::ZERO and Field::ONE, but only PrimeField::TWO_INV. The first two are valid for every field (because fields have both addition and multiplication, and therefore must have both additive and multiplicative identities), whereas 2^-1 is only present for fields in which 2 has an inverse (which does not include binary fields).

Others


Q JM: enable_equality is completely idempotent, right??

“idempotent”(幂等)这个术语通常用来描述一个具有特定性质的操作:无论进行多少次,其效果都是一样的。当我们说一个操作是“完全幂等的”(completely idempotent)时,我们是在强调这一点:不管这个操作执行一次还是多次,结果总是相同的。

A You mean the API that enables a column to be used in equality constraints? Yes, using it multiple times on the same column has no additional effect.


Q jasonmorton: What’s the difference between Constant (constrain_constant) and Fixed (assign_fixed)? Both use a cell for a value that is fixed at circuit creation time, and is part of the circuit definition. Which one is most appropriate for values to be used in constraints? I want to add (as cheaply as possible) the constraint 3x+5y=0, where x and y are witness values and 3 and 5 are fixed when the circuit is defined.

A assign_fixed should be used for values in constraints, because the constraint has to be enabled at some relative offset within the region. 因为必须在区域内的某个相对偏移处启用约束。

constrain_constant assigns a value in some unused fixed cell after all regions have been allocated; it should be used to equality-constrain single cells in the global permutation. constrain_constant 在分配所有区域后在某个未使用的固定单元中分配一个值; 它应该用于在全局排列中平等约束单个单元。

Q Thank you. Is using assign_fixed with a fixed column, and assign_advice with an advice column the same prover and verification cost then? A No; roughly speaking, advice columns are more expensive

Q: hey a question about PSE Halo2, can anyone guide me on how to extract kzg inclusion proof from a column’s polynomial commitment? I am tracking back the code from transcript.write_point() and can figure out where the commitment is calculated, but any guidance will be helpful

A: here is where the KZG opening proof is made for multiple commitments evaluated at the same point z:

  • https://github.com/privacy-scaling-explorations/halo2/blob/main/halo2_proofs/src/poly/kzg/multiopen/gwc/prover.rs

Q is halo2 book is enough to read and develop the circuit? A To read it possibly. To develop it, depends on your programming experience and the languages you’re good at. As this will probably mark how fast you get up to speed with Rust basics.

Q How come there are so many duplicated traits? Curve, CurveExt Field, FieldExt Group, Group (same name! one from group, one from pasta_curves) I have to be honest, it’s a bit confusing.

A The *Ext traits are extension traits, a way of extending an existing trait with additional functionality. We wrote those while figuring out what things we needed for implementing Halo 2 that the ff and group traits didn’t yet provide. The end goal is for those extension traits to disappear:

  • https://github.com/zcash/pasta_curves/issues/42
  • https://github.com/zcash/pasta_curves/issues/41

Q hey! is anyone working on (or aware of anyone working on) transpiling r1cs constraints to halo2 circuit?

A Why not just change the protocol to support R1CS? That would be more efficient, no? Halo 2 is made to prove a PLONKish circuit. If you just want to support R1CS, you could drop lookups, etc.

Transpiling should be more efficient, because you can make use of multiple columns to compile more than one R1CS constraint into a single PLONKish row Depending on the sophistication of the compiler, you could also recognise repeated patterns in the R1CS constraints and compile them into custom gates, although I’m not sure whether it would be worth the effort relative to rewriting the source circuits

Prerequisite

The Halo2 Book:

0xPARC: Halo2 Learning Group

  • Official : https://learn.0xparc.org/halo2/
  • https://github.com/icemelon/halo2-tutorial/
    1. Introduction 8.21
  • 2. Halo2 API & Building a Basic Fibonacci Circuit (Part 1)
    • https://github.com/icemelon/halo2-examples (Haicheng’s Code)
    • https://github.com/enricobottazzi/halo2-fibonacci-ex 整理了 0XPARC 课上的 QA ! and Example2 Example 3 …
    • https://github.com/therealyingtong/halo2-hope code reference
    • https://github.com/jasonmorton/halo2-examples/blob/master/src/range_check/example1b.rs Jason Morton 超详细注释 line by line
    • https://github.com/enricobottazzi/halo2-intro
  • Fibonacci Circuits cont. + Basic Halo2 gadgets
    • Jason Morton Code missed Haicheng’s print part.

StarLi 星想法

知乎小白专栏:

DoraHacks

Code

libs:

  • More Optimized ECC Operations in halo2 by Axiom
  • axiom libs.
    • SY: We just open sourced a library of base components including range check here: https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-base/src/gates/range.rs
    • https://axiom-crypto.github.io/halo2-lib/halo2_base/gates/range/trait.RangeInstructions.html#tymethod.is_less_than
    • This uses halo2-lib: https://github.com/axiom-crypto/halo2-lib
    • YT: it looks like the axiom fork does variable-length Poseidon (https://github.com/axiom-crypto/halo2/tree/main/primitives/poseidon):
    • ECDSA: https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-ecc/src/secp256k1/tests/ecdsa.rs

Others :

Records:

论文: BCH19 Recursive proof composition without a trusted Setup

Prerequisite

The Halo2 Book:

0xPARC: Halo2 Learning Group

  • Official : https://learn.0xparc.org/halo2/
  • https://github.com/icemelon/halo2-tutorial/
    1. Introduction 8.21
  • 2. Halo2 API & Building a Basic Fibonacci Circuit (Part 1)
    • https://github.com/icemelon/halo2-examples (Haicheng’s Code)
    • https://github.com/enricobottazzi/halo2-fibonacci-ex 整理了 0XPARC 课上的 QA ! and Example2 Example 3 …
    • https://github.com/therealyingtong/halo2-hope code reference
    • https://github.com/jasonmorton/halo2-examples/blob/master/src/range_check/example1b.rs Jason Morton 超详细注释 line by line
    • https://github.com/enricobottazzi/halo2-intro
  • Fibonacci Circuits cont. + Basic Halo2 gadgets
    • Jason Morton Code missed Haicheng’s print part.

StarLi 星想法

知乎小白专栏:

DoraHacks

Code

libs:

  • More Optimized ECC Operations in halo2 by Axiom
  • axiom libs.
    • SY: We just open sourced a library of base components including range check here: https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-base/src/gates/range.rs
    • https://axiom-crypto.github.io/halo2-lib/halo2_base/gates/range/trait.RangeInstructions.html#tymethod.is_less_than
    • This uses halo2-lib: https://github.com/axiom-crypto/halo2-lib
    • YT: it looks like the axiom fork does variable-length Poseidon (https://github.com/axiom-crypto/halo2/tree/main/primitives/poseidon):
    • ECDSA: https://github.com/axiom-crypto/halo2-lib/blob/main/halo2-ecc/src/secp256k1/tests/ecdsa.rs

Others :

Records:

论文: BCH19 Recursive proof composition without a trusted Setup

author: @Demian

references: https://learn.0xparc.org/halo2/

Overview

简单例子

我们先从一个简单的电路开始,介绍常用的 API 以及它们的使用方法。 该电路将采用 public input c,并将证明两个 private inputs  and  的知识,以使

定义 instructions

首先,我们需要定义我们的电路所依赖的指令集(instructions)

Instructions 介于 high-level gadgets 和底层的电路操作之间。指令既可以细粒度也可以粗粒度,但在实践中,指令的功能应当足够小,这样可以重复使用;但又要足够大,这样可以优化它的实现。设计者应当在这两者之间取得平衡

对于我们的电路,我们将使用三个 instructions:

  • 将 private number 加载到电路中。
  • 两个数字相乘。
  • 将数字公开(Expose) 为电路的 public input。

我们还需要一个代表数字的变量的类型(type Num)。指令接口(Instruction interfaces) 为其输入和输出提供关联类型(associated types),以允许实现以对其优化目标最有意义的方式表示这些类型 (to allow the implementations to represent these in a way that makes the most sense for their optimization goals.)

#![allow(unused)]
fn main() {
trait NumericInstructions<F: Field>: Chip<F> {
    /// Variable representing a number. 用于表示一个数的变量
    type Num;

    /// Loads a number into the circuit as a private input. 加载隐私输入
    fn load_private(&self, layouter: impl Layouter<F>, a: Value<F>) -> Result<Self::Num, Error>;

    /// Loads a number into the circuit as a fixed constant.
    fn load_constant(&self, layouter: impl Layouter<F>, constant: F) -> Result<Self::Num, Error>;

    /// Returns `c = a * b`.
    fn mul(
        &self,
        layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error>;

    /// Exposes a number as a public input to the circuit.
    /// 将一个数置为电路的公开输入
    fn expose_public(
        &self,
        layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error>;
}
}

Among them,

  • Num 用于适配此 interface 中处理的类型
  • load_private 用于加载 witness
  • load_constant 用于加载常量 (constant)
  • mul 用于计算两个数字的乘法
  • expose_public 用于设置实例 (instance)

定义芯片的实现

对于我们的电路,我们将构建一个芯片(chip),在有限域上实现提到的 Numeric Instruction (trait NumericInstructions)

如果您想开发自定义 chip,则需要去实现 Halo 2 的 chip Trait

大多数时候,使用 Halo 2 进行电路开发不需要自己定义 Instructions 和 chip。 但如果你需要使用 Halo 2 没有提供的复杂算法,就需要自己实现(例如实现一种新兴的密码算法)。

#![allow(unused)]
fn main() {
/// 这块芯片将实现我们的指令集 instructions!
/// 芯片存储它们自己的配置,以及(如有必要的) 类型标记
struct FieldChip<F: Field> {
    config: FieldConfig,
    _marker: PhantomData<F>,
}
}

每一个“芯片“类型都要实现 Chip trait , Chip trait 定义了 Layouter 在 synthesizing 电路时可能需要的关于电路的某些属性,以及若将该芯片加载到电路所需要设置的任何初始状态

synthesizing 电路 : 一般指的是类似 R1CS 那种写约束的意思

#![allow(unused)]
fn main() {
/// Every chip needs to implement the `Chip` trait !!
impl<F: FieldExt> Chip<F> for FieldChip<F> {
    type Config = FieldConfig;
    type Loaded = ();

    fn config(&self) -> &Self::Config {
        &self.config
    }

    fn loaded(&self) -> &Self::Loaded {
        &()
    }
}
}

配置芯片

接下来需要为芯片 chip 配置好实现我们想要的功能所需要的那些列、置换、门(columns, permutations, and gates):

#![allow(unused)]
fn main() {
/// 芯片 chip 的状态被存储在一个 FieldConfig 结构体中,它是在配置过程中由 chip 生成,
/// 并且存储在芯片内部 (type Config = FieldConfig;)
#[derive(Clone, Debug)]
struct FieldConfig {
    /// 对于这块芯片,我们将用到两个 advice 列来实现我们的指令集。
    /// 它们也是我们与电路其他部分进行通信的列。
    advice: [Column<Advice>; 2],
    instance: Column<Instance>, // public input 列(instance)

    // 我们需要一个 selector 来激活乘法门,在用不到 `NumericInstructions::mul`指令的 cells
    // 上不设置任何约束。这非常重要,尤其在构建更大型的电路的情况下,列会被多条指令集用到
    s_mul: Selector,
}
}

下面我们来构建约束 :

  • 最关键的函数 configureenable_equality 用于检查传入参数的相等性
  • 如下图 : 在 create_gate 函数中 :
    • 乘数 分别在同一行的 advice 列 ;
    • 乘积 同在 列, 的下一行 :
  • 可以看到在代码中, 都是使用相对位置(relative position) 来描述的 !
#![allow(unused)]
fn main() {
// | a0  | a1  | s_mul |
// |-----|-----|-------|
// | lhs | rhs | s_mul |
// | out |     |       |
let lhs = meta.query_advice(advice[0], Rotation::cur());
let rhs = meta.query_advice(advice[1], Rotation::cur());
let out = meta.query_advice(advice[0], Rotation::next()); // Attention !!
}

最后函数返回多项式约束:

  • 若选择器(Selector) s_mul 不为 0,则激活校验乘法约束 :
    • s_mul * (lhs * rhs - out) == 0,则说明 lhs * rhs = out 约束成立;
    • s_mul * (lhs * rhs - out) != 0,说明 lhs * rhs = out 约束不成立!!程序报错
  • s_mul 为 0,则不会激活检查乘法约束,any subsequent values are fine!
#![allow(unused)]
fn main() {
impl<F: FieldExt> FieldChip<F> {
    fn construct(config: <Self as Chip<F>>::Config) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 2],
        instance: Column<Instance>,
        constant: Column<Fixed>,
    ) -> <Self as Chip<F>>::Config {
        meta.enable_equality(instance);
        meta.enable_constant(constant);
        for column in &advice {
            meta.enable_equality(*column);
        }
        let s_mul = meta.selector();

        // 定义我们的乘法门(multiplication gate)
        meta.create_gate("mul", |meta| {
            // To implement multiplication, we need 3 advice `cells`
            // and 1 selector.  We arrange them like so:
            //
            // | a0  | a1  | s_mul |
            // |-----|-----|-------|
            // | lhs | rhs | s_mul |
            // | out |     |       |
            //
            // 门可以用任意相对偏移(relative offsets),但每一个 offset 都会增加证明的开销
            // 最常见的偏移值 offset 是 0 (当前行), 1(下一行), -1(上一行)。
            // 这 3 种情况 `Rotation` 都有特定的 constructors : cur/next/prev
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);

            // Finally, we return the polynomial expressions that constrain this gate.
            // 最终,我们将约束门的多项式表达式返回. 对于目前的乘法门,仅需要一个多项式约束
            //
            // `create_gate` 函数返回的多项式表达式,在 proving system 中会被约束等于 0.
            // 约束表达式有以下性质:
            // - 当 s_mul = 0 时,lhs, rhs, out 可以是任意值。
            // - 当 s_mul != 0 时,lhs, rhs, out 需要满足 lhs * rhs = out 这条约束。
            vec![s_mul * (lhs * rhs - out)]
        });

        FieldConfig {
            advice,
            instance,
            s_mul,
        }
    }
}
}

到目前为止,我们对电路的实现的大致过程是:

  1. 定义一套指令集(NumericInstructions),这是一套操作码,用来控制“计算机“(芯片)
  2. 变出一块芯片(定义在有限域上),就像是一块电路板
  3. 有了”指令集“和”电路板“,我们可以选择一些元件来对该电路板子实现特定功能(struct FieldConfig),例如,如果想实现声控功能,我们需要电容麦,模拟-数字转换器 (ADC),二极管… 在 halo2 中,我们就需要选取需要的 column: advice/instance/constant/selectors ….
  4. 有了这些元器件,我们需要按照执行逻辑将其连接起来(fn configure())

实现 chip Traits

我们前面定义的 instructions interface 需要 implemention,定义 NumericInstructions 的实现就是封装 finite field elements.

It should be noted that, 除了行和列之外,Cell 单元的位置还可以通过相对位置偏移(relative position offset) 来确定)

Generally, there are 3 types of offsets, 0 representing the current position, 1 representing the next position, and -1 representing the previous position.

#![allow(unused)]
fn main() {
struct Number<F: Field>(AssignedCell<F, F>); /// 用于表示数的 struct

impl<F: FieldExt> NumericInstructions<F> for FieldChip<F> {
    type Num = Number<F>;
    fn load_private(){ ... }
    fn load_constant(){ ... }

    fn mul(
        &self,
        mut layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "mul",
            |mut region: Region<'_, F>| {
                // 在此 region 中,我们只想用一个乘法门,所以我们在 region offset=0 处激活它;
                // 这意味着它将对 offsets 0 and 1 处的 2 个 cells 都进行约束(为啥?)
                config.s_mul.enable(&mut region, 0)?;

                // 给我们的输入(a: Self::Num / b: Self::Num,) 有可能在电路的任何位置.
                // 但在region 中,我们只能依靠相对偏移。所以我们需要在 region 内分配新的 cells
                // 并限制新分配的 cells 的值 与输入(a: Self::Num / b: Self::Num,) 的值相等。
                a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
                b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?;

                // 现在我们可以把乘积放到输出的位置了。
                let value = a.0.value().copied() * b.0.value();

				// 最后,我们对输出进行赋值,返回一个要在电路的另一部分使用的变量
                region
                    .assign_advice(|| "lhs * rhs", config.advice[0], 1, || value)
                    .map(Number)
            },
        )
    }
    fn expose_public(
        &self,
        mut layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error> {
        let config = self.config();

        layouter.constrain_instance(num.0.cell(), config.instance, row)
    }
}
}

构建电路

现在我们有了所需的指令以及实现它们的芯片,我们终于可以构建我们的电路了!

circuit Trait 是电路开发的入口。 我们需要定义自己的电路结构并访问 witness input

struct MyCircuit :

  • 在这个结构体中,我们保存隐私输入变量。我们使用 Option<F> 类型是因为,在生成密钥阶段,他们不需要有任何的值。在证明阶段中,如果它们任一为 None 的话,将得到一个错误。

The interfaces defined before are all used here. configure creates a storage column for advice/instance/constant. synthesize uses a custom chip to get the input witness and constant, and finally, calculate the result and return the public input.

In fact, it can satisfy most scenarios by simply implementing the circuit trait for general circuit development. Some common functions of the chip have already been implemented in Halo 2.

#![allow(unused)]
fn main() {
// Store the private input variables in this circuit.
// Value<F> 用了 Option<F>,  因为在 key generation 阶段这个 struct 不会有值
// 而在 Proving 阶段,如果任意位置为空就会 throw Error.
struct MyCircuit<F: Field> {
    constant: F,
    a: Value<F>,
    b: Value<F>,
}

impl<F: Field> Circuit<F> for MyCircuit<F> {
    // 因为 we are using a single chip for everything,所以我们可以重用它的配置。
    type Config = FieldConfig;  // 电路元件(column(advice/instance/selector..))
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        // 我们创建两个 advice 列,作为 FieldChip 的 I/O.
        let advice = [meta.advice_column(), meta.advice_column()];

        // 我们还需要一个 instance 列来存储公开输入 public inputs.
        let instance = meta.instance_column();

        // 创建一个 fixed 列来加载常数 load constants.
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }
    // Prove a2⋅b2 = c
    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let field_chip = FieldChip::<F>::construct(config);

        // 将我们的隐私值加载到电路中。
        let a = field_chip.load_private(layouter.namespace(|| "load a"), self.a)?;
        let b = field_chip.load_private(layouter.namespace(|| "load b"), self.b)?;

        // 将常数因子加载到电路中
        let constant =
            field_chip.load_constant(layouter.namespace(|| "load constant"), self.constant)?;

        // 我们仅有乘法可用,因此我们按以下方法实现电路:
        //     asq  = a * a
        //     bsq  = b * b
        //     absq = asq * bsq
        //     c    = constant * asq*bsq
        //
        // 但是,按下面的方法实现,更加高效:
        //     ab   = a*b
        //     absq = ab^2
        //     c    = constant*absq
        let ab = field_chip.mul(layouter.namespace(|| "a * b"), a, b)?;
        let absq = field_chip.mul(layouter.namespace(|| "ab * ab"), ab.clone(), ab)?;
        let c = field_chip.mul(layouter.namespace(|| "constant * absq"), constant, absq)?;

        // 将结果作为电路的公开输入进行公开
        field_chip.expose_public(layouter.namespace(|| "expose c"), c, 0)
    }
} }
}

测试电路

我们在 chapter about tools 中提到的 MockProverCircuitLayout 可以派上用场

可以用 halo2::dev::MockProver 来测试一个电路是否正常工作。构造电路的一组 Private/Public input ,这组输入可直接用来计算合法证明,但我们把这组输入传入到 MockProver::run 函数中之后,就能得到一个可用于检验电路中每一条约束是否满足的对象。而且电路验证不过,这个对象还能输出那条不满足的约束

如下代码 , MockProver::run 中只知道 Public input c , 他并不知道 Private 是什么, 但仍可以进行验证

fn main() {
    // 我们电路的行数不能超过 2^k. 因为我们的示例电路很小,我们选择一个较小的值
    let k = 4;

    // 准备好电路的隐私输入和公开输入
    let constant = Fp::from(7);
    let a = Fp::from(2);
    let b = Fp::from(3);
    let c = constant * a.square() * b.square();  // 算出来

    // 用隐私输入来实例化电路
    let circuit = MyCircuit {
        constant,
        a: Value::known(a),
        b: Value::known(b),
    };

    // 将公开输入进行排列。乘法的结果被我们放置在 instance 列的第0行,
    // 所以我们把它放在公开输入的对应位置。
    let mut public_inputs = vec![c];

    // 给定正确的公开输入,我们的电路能验证通过
    let prover = MockProver::run(k, &circuit, vec![public_inputs.clone()]).unwrap();
    assert_eq!(prover.verify(), Ok(()));

    // 如果我们尝试用其他的公开输入(此处是 +1),证明将失败!
    public_inputs[0] += Fp::one();
    let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
    assert!(prover.verify().is_err());
}

Code Ref / Full example

You can find the source code for this example here.

cargo run --example simple-example

References:

—– 中文版本 End ——

Simple Example

Let’s start with a simple circuit, to introduce you to the common APIs and how they are used. The circuit will take a public input c, and will prove knowledge of two private inputs  and  such that

Define instructions

Instructions are the boundary between high-level gadgets and the low-level circuit operations. Instructions may be as coarse or as granular as desired, but in practice you want to strike a balance between an instruction being large enough to effectively optimize its implementation, and small enough that it is meaningfully reusable.

For our circuit, we will use three instructions:

  • Load a private number into the circuit.
  • Multiply two numbers.
  • Expose a number as a public input to the circuit. (将一个数设置为电路的公开输入)

We also need a type for a variable representing a number. Instruction interfaces provide associated types for their inputs and outputs, to allow the implementations to represent these in a way that makes the most sense for their optimization goals. 我们还需要一个代表数字的变量的类型。指令接口为其输入和输出提供关联类型,以允许实现以对其优化目标最有意义的方式表示这些类型

#![allow(unused)]
fn main() {
trait NumericInstructions<F: Field>: Chip<F> {
    /// Variable representing a number. 用于表示一个数的变量
    type Num;

    /// Loads a number into the circuit as a private input. 隐私输入
    fn load_private(&self, layouter: impl Layouter<F>, a: Value<F>) -> Result<Self::Num, Error>;

    /// Loads a number into the circuit as a fixed constant.
    fn load_constant(&self, layouter: impl Layouter<F>, constant: F) -> Result<Self::Num, Error>;

    /// Returns `c = a * b`.
    fn mul(
        &self,
        layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error>;

    /// Exposes a number as a public input to the circuit.
    /// 将一个数置为电路的公开输入
    fn expose_public(
        &self,
        layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error>;
}
}

Among them,

  • Num is used to adapt to the type handled in this interface, (适配该接口中处理的类型)
  • load_private is used to load witness,
  • load_constant is used to load constants, 
  • mul is used to calculate the multiplication of two numbers, and 
  • expose_public is used to set instance.

Define a chip implementation

定义芯片的实现 : For our circuit, we will build a chip that provides the above numeric instructions for a finite field.

If you want to develop a custom chip, you need to implement the chip trait of Halo 2.

Most of the time, using Halo 2 for circuit development does not need to define instructions and chips by oneself. But if you need to use complex ones that Halo 2 does not provide, you need to implement them yourself, such as implementing an emerging(新兴的) cryptographic algorithm.

#![allow(unused)]
fn main() {
/// 这块芯片将实现我们的指令集!芯片存储它们自己的配置,
struct FieldChip<F: Field> {
    config: FieldConfig,
    _marker: PhantomData<F>,
}
}

Every chip needs to implement the  Chip  trait. This defines the properties of the chip that a Layouter may rely on when synthesizing a circuit, as well as enabling any initial state that the chip requires to be loaded into the circuit. 每一个“芯片“类型都要实现 Chip trait , Chip trait 定义了 Layouter 在 synthesizing 电路时可能需要的关于电路的某些属性,以及若将该芯片加载到电路所需要设置的任何初始状态

synthesizing 电路 : 一般指的是类似 R1CS 那种写约束的意思

#![allow(unused)]
fn main() {
impl<F: FieldExt> Chip<F> for FieldChip<F> {
    type Config = FieldConfig;
    type Loaded = ();

    fn config(&self) -> &Self::Config {
        &self.config
    }

    fn loaded(&self) -> &Self::Loaded {
        &()
    }
}
}

Configure the chip

The chip needs to be configured with the columns, permutations, and gates that will be required to implement all of the desired instructions. (需要为芯片配置好实现我们想要的功能所需要的那些列、置换、门) :

#![allow(unused)]
fn main() {
/// 芯片的状态被存储在一个 config 结构体中,它是在配置过程中由芯片生成,
/// 并且存储在芯片内部。
#[derive(Clone, Debug)]
struct FieldConfig {
    /// 对于这块芯片,我们将用到两个 advice 列来实现我们的指令集。
    /// 它们也是我们与电路的其他部分通信所需要用到列。
    advice: [Column<Advice>; 2],
    instance: Column<Instance>, //公开输入(instance)列

    // 我们需要一个 selector 来激活乘法门,从而在用不到`NumericInstructions::mul`指令的
    //cells 上不设置任何约束。这非常重要,尤其在构建更大型的电路的情况下,列会被多条指令集用到
    s_mul: Selector,

    /// 用来加载常数的 fixed 列
    constant: Column<Fixed>,
}

下面我们来构建约束 :

  • the most critical functions configure and enable_equality are used to check the equality of the incoming parameters(传入的参数).
  • 如下图 : 在 create_gate 函数中 :
    • 乘数 分别在同一行的 advice 列 ;
    • 乘积 同在 列, 的下一行 :
  • 可以看到在代码中, 都是使用 相对位置来描述的 !
#![allow(unused)]
fn main() {
// | a0  | a1  | s_mul |
// |-----|-----|-------|
// | lhs | rhs | s_mul |
// | out |     |       |
let lhs = meta.query_advice(advice[0], Rotation::cur());
let rhs = meta.query_advice(advice[1], Rotation::cur());
let out = meta.query_advice(advice[0], Rotation::next()); // Attention !!
}

最后函数返回多项式约束:

  • s_mul 不为 0,则激活校验乘法约束 :
    • s_mul * (lhs * rhs - out) == 0,则 lhs * rhs = out 约束成立;
    • s_mul * (lhs * rhs - out) != 0lhs * rhs = out 约束不成立;程序报错
  • s_mul 为 0,则不会激活检查乘法约束,any subsequent values are fine
#![allow(unused)]
fn main() {
impl<F: FieldExt> FieldChip<F> {
    fn construct(config: <Self as Chip<F>>::Config) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 2],
        instance: Column<Instance>,
        constant: Column<Fixed>,
    ) -> <Self as Chip<F>>::Config {
        meta.enable_equality(instance.into());
        meta.enable_constant(constant);
        for column in &advice {
            meta.enable_equality((*column).into());
        }
        let s_mul = meta.selector();

        // 定义我们的乘法门
        meta.create_gate("mul", |meta| {
            // To implement multiplication, we need 3 advice `cells`
            // and 1 selector.  We arrange them like so:
            //
            // | a0  | a1  | s_mul |
            // |-----|-----|-------|
            // | lhs | rhs | s_mul |
            // | out |     |       |
            //
            // 门可以用任一相对偏移,但每一个不同的偏移都会对证明增加开销。
            // 最常见的偏移值是 0 (当前行), 1(下一行), -1(上一行)。
            // 针对这三种情况,有特定的构造函数来构造`Rotation` 结构。
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);

            // 最终,我们将约束门的多项式表达式返回。
            // 对于我们的乘法门,我们仅需要一个多项式约束。
            //
            // `create_gate` 函数返回的多项式表达式,在证明系统中一定等于0。
            // 我们的表达式有以下性质:
            // - 当 s_mul = 0 时,lhs, rhs, out 可以是任意值。
            // - 当 s_mul != 0 时,lhs, rhs, out 将满足 lhs * rhs = out 这条约束。
            vec![s_mul * (lhs * rhs - out)]
        });

        FieldConfig {
            advice,
            instance,
            s_mul,
            constant,
        }
    }
}
}
}

Implement chip Traits

The instructions interface we defined earlier needs to be implemented, and defining the implementation of Number is to encapsulate(封装) finite field elements.

It should be noted that, in addition to row and column, the position of the cell can also be determined by the relative position offset (除了行和列之外,单元的位置还可以通过相对位置偏移来确定).

Generally, there are 3 types of offsets, 0 representing the current position, 1 representing the next position, and -1 representing the previous position.

#![allow(unused)]
fn main() {
/// A variable representing a number.
#[derive(Clone)]
struct Number<F: Field>(AssignedCell<F, F>);

impl<F: Field> NumericInstructions<F> for FieldChip<F> {
    type Num = Number<F>;
    fn load_private(
        &self,
        mut layouter: impl Layouter<F>,
        value: Value<F>,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "load private",
            |mut region| {
                region
                    .assign_advice(|| "private input", config.advice[0], 0, || value)
                    .map(Number)
            },
        )
    }

    fn load_constant(
        &self,
        mut layouter: impl Layouter<F>,
        constant: F,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        layouter.assign_region(
            || "load constant",
            |mut region| {
                region
                    .assign_advice_from_constant(|| "constant value", config.advice[0], 0, constant)
                    .map(Number)
            },
        )
    }

	fn mul(
		&self,
		mut layouter: impl Layouter<F>,
		a: Self::Num,
		b: Self::Num,
	) -> Result<Self::Num, Error> {
		let config = self.config();

		layouter.assign_region(
			|| "mul",
			|mut region: Region<'_, F>| {
				// We only want to use a single multiplication gate in this region,
				// so we enable it at region offset 0; this means it will constrain
				// cells at offsets 0 and 1.
				config.s_mul.enable(&mut region, 0)?;

				// The inputs we've been given could be located anywhere in the circuit,
				// but we can only rely on relative offsets inside this region. So we
				// assign new cells inside the region and constrain them to have the
				// same values as the inputs.
				a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
				b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?;

				// Now we can assign the multiplication result, which is to be assigned
				// into the output position.
				let value = a.0.value().copied() * b.0.value();

				// Finally, we do the assignment to the output, returning a
				// variable to be used in another part of the circuit.
				region
					.assign_advice(|| "lhs * rhs", config.advice[0], 1, || value)
					.map(Number)
			},
		)
	}
}

Build the Circuit

既然我们已经有了所需要的指令,以及一块实现了这些指令的芯片,我们终于可以构造示例电路啦

The circuit trait is the entrance to the circuit development. We need to define our own circuit structure and access the witness input.

struct MyCircuit :

  • 在这个结构体中,我们保存隐私输入变量。我们使用 Option<F> 类型是因为,在生成密钥阶段,他们不需要有任何的值。在证明阶段中,如果它们任一为 None 的话,将得到一个错误。

The interfaces defined before are all used here. configure creates a storage column for advice/instance/constant. synthesize uses a custom chip to get the input witness and constant, and finally, calculate the result and return the public input.

In fact, it can satisfy most scenarios by simply implementing the circuit trait for general circuit development. Some common functions of the chip have already been implemented in Halo 2.

#![allow(unused)]
fn main() {
struct MyCircuit<F: FieldExt> {
    constant: F,
    a: Option<F>,
    b: Option<F>,
}

impl<F: FieldExt> Circuit<F> for MyCircuit<F> {
    // 因为我们在任一地方值用了一个芯片,所以我们可以重用它的配置。
    type Config = FieldConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        // 我们创建两个 advice 列,作为 FieldChip 的输入。
        let advice = [meta.advice_column(), meta.advice_column()];

        // 我们还需要一个 instance 列来存储公开输入。
        let instance = meta.instance_column();

        // 创建一个 fixed 列来加载常数
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }
    // Prove a2⋅b2 = c
    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let field_chip = FieldChip::<F>::construct(config);

        // 将我们的隐私值加载到电路中。
        let a = field_chip.load_private(layouter.namespace(|| "load a"), self.a)?;
        let b = field_chip.load_private(layouter.namespace(|| "load b"), self.b)?;

        // 将常数因子加载到电路中
        let constant =
            field_chip.load_constant(layouter.namespace(|| "load constant"), self.constant)?;

        // 我们仅有乘法可用,因此我们按以下方法实现电路:
        //     asq  = a*a
        //     bsq  = b*b
        //     absq = asq*bsq
        //     c    = constant*asq*bsq
        //
        // 但是,按下面的方法实现,更加高效:
        //     ab   = a*b
        //     absq = ab^2
        //     c    = constant*absq
        let ab = field_chip.mul(layouter.namespace(|| "a * b"), a, b)?;
        let absq = field_chip.mul(layouter.namespace(|| "ab * ab"), ab.clone(), ab)?;
        let c = field_chip.mul(layouter.namespace(|| "constant * absq"), constant, absq)?;

        // 将结果作为电路的公开输入进行公开
        field_chip.expose_public(layouter.namespace(|| "expose c"), c, 0)
    }
} }
}

Testing the circuit

The MockProver and CircuitLayout that we mentioned in the chapter about tools can come in handy (派上用场).

可以用 halo2::dev::MockProver 来测试一个电路是否正常工作。构造电路的一组 Private/Public input ,这组输入可直接用来计算合法证明,但我们把这组输入传入到 MockProver::run 函数中之后,就能得到一个可用于检验电路中每一条约束是否满足的对象。而且电路验证不过,这个对象还能输出那条不满足的约束

如下代码 , MockProver::run 中只知道 Public input c , 他并不知道 Private 是什么, 但仍可以进行验证

fn main() {
    // 我们电路的行数不能超过 2^k. 因为我们的示例电路很小,我们选择一个较小的值
    let k = 4;

    // 准备好电路的隐私输入和公开输入
    let constant = Fp::from(7);
    let a = Fp::from(2);
    let b = Fp::from(3);
    let c = constant * a.square() * b.square();  // 算出来

    // 用隐私输入来实例化电路
    let circuit = MyCircuit {
        constant,
        a: Value::known(a),
        b: Value::known(b),
    };

    // 将公开输入进行排列。乘法的结果被我们放置在 instance 列的第0行,
    // 所以我们把它放在公开输入的对应位置。
    let mut public_inputs = vec![c];

    // 给定正确的公开输入,我们的电路能验证通过
    let prover = MockProver::run(k, &circuit, vec![public_inputs.clone()]).unwrap();
    assert_eq!(prover.verify(), Ok(()));

    // 如果我们尝试用其他的公开输入(此处是 +1),证明将失败!
    public_inputs[0] += Fp::one();
    let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
    assert!(prover.verify().is_err());
}

Code Ref / Full example

You can find the source code for this example here.

cargo run --example simple-example

—– 问题 Question —–

这意味着它将对 offsets 0 and 1 处的 2 个 cells 都进行约束(为啥?)

#![allow(unused)]
fn main() {
config.s_mul.enable(&mut region, 0)?;
}

奇怪的一点是,在 fn mul() 函数中,既然已经使用了参数 a: Self::Num, b: Self::Num, 设置了 lhs Cell 和 rhs Cell,那么 a.cell 和 lhs、 b.cell 和 rhs 肯定就是相等的,为什么还要再 region.constrain_equal 约束一次呢?这个约束看起来是多余的且没有必要的?

#![allow(unused)]
fn main() {
// 在此 region 中,我们只想用一个乘法门,所以我们在 region offset=0 处激活它;
// 这意味着它将对 offsets 0 and 1 处的 2 个 cells 都进行约束(为啥?)
config.s_mul.enable(&mut region, 0)?;

// 给我们的输入有可能在电路的任一位置,但在当前 region 中,我们仅可以用
// 相对偏移。所以,我们在 region 内分配新的 cells 并限定他们的值与输入 cells 的值相等。
let lhs = region.assign_advice(
	|| "lhs",
	config.advice[0], // 第 0 列 (advice 列)
	0,                // 第 0 行
	|| a.value.ok_or(Error::SynthesisError), // 放 a 的值进去
)?;
let rhs = region.assign_advice(
	|| "rhs",
	config.advice[1], // 第 1 列,
	0,                // 第 0 行
	|| b.value.ok_or(Error::SynthesisError),
)?;
region.constrain_equal(a.cell, lhs)?;
region.constrain_equal(b.cell, rhs)?;
}

GPT:

虽然 lhsrhs 的值被设置为 a.valueb.value,但是这并不意味着它们在电路中是相等的。在电路的布局中,a.cellb.cell 可能已经在其他位置被(意外地)赋值和约束过了。而在这个新的区域(region)中,为了进行乘法操作,你需要确保新分配的 lhsrhs cells 与原始的 a.cellb.cell 相等

这就是为什么 region.constrain_equal 是必要的:它确保了在这个区域(region)中的计算使用了正确的输入值

你可以将其视为一个安全性保障。如果没有这个约束,有可能会有其他的值进入这个区域,并可能导致电路生成错误的结果。通过加入这个约束,你确保了乘法操作是在正确的值上进行的。

目前的 halo2_proofs,一般直接使用 copy_advice

#![allow(unused)]

fn main() {
}

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Goal

Given , we will prove

    1, 1, 2, 3, 5, 8, 13, ...

    | elem_1 | elem_2 | sum | q_fib
    --------------------------------
    |    1   |    1   |  2  |   1
    |    1   |    2   |  3  |   1
    |    2   |    3   |  5  |   1
    |        |        |     |   0

    q_fib * (elem_1 + elem_2 - elem_3) = 0

Overview

struct ACell : 是一个 tuple struct ,是对 AssignedCell 封装和抽象

FiboChip :斐波那契数列”芯片“(本 Application 无需自定义指令集)

FiboConfig : 定义了需要挑选哪些”芯片元件“(advice、selector、instance…)

impl FiboChip:芯片元件的连接排布方式

  • fn construct()
  • fn configure() : 门(create_gate)

ACell

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct ACell<F: FieldExt>(AssignedCell<F, F>);
}

Why ACell ?

  1. 封装和抽象:通过使用 ACell,我们为用户提供了一个简化和更直观的接口,使他们可以更容易地与已分配的单元格进行交互,而不必每次都直接处理 AssignedCell
  2. 灵活性:将来,如果我们想在 ACell 中添加更多的功能或属性,我们可以这样做而不影响现有的代码
  3. 故 : ACell 主要是一个辅助结构体,用于简化与电路中单元格的交互

元素访问 :

#![allow(unused)]
fn main() {
// 因为 `ACell` 是对 `AssignedCell` 的简单包装,
// 所以可以直接使用 `.0` 语法来访问其内部的 `AssignedCell` :  `prev_b.0`
let c_val = prev_b.0.value().copied() + prev_c.0.value();
}

.map(ACell)? 访问 :

  • 具体来说,assign_advice 返回的是 Result<AssignedCell<F, F>, Error>.map(ACell) 会将其转换为 Result<ACell<F>, Error>
  • 元组结构体本身可以作为函数来调用, 相当于调用一个带有一个参数的构造函数。
#![allow(unused)]
fn main() {
// when call .map() , 我们提供一个函数,将其应用于 Result 内的 Ok 的值(if so)
// 本例中传递的函数是 ACell 的构造函数,所以我们是将 AssignedCell 转换成 ACell
// 对于 tuple struct, 如 `let black = Color(0, 0, 0);`
// therefore  `AssignedCell<F, F>` 本身是一个函数
let a_cell = region
    .assign_advice(|| "a", self.config.advice[0], 0, || a)
    .map(ACell)?;
}

impl FiboChip { …

  • fn construct
  • fn configure
  • fn assign_first_row
  • fn assign_row
  • fn expose_public
fn configure()

参数:

  • meta: 是对约束系统的可变引用,允许我们在其中配置列和约束。
  • Selector : 用于激活或禁用某些特定约束
  • meta.query_selector : Query a selector at the current position.
  • Query an advice column at a relative position : Query an advice column at a relative position
#![allow(unused)]
fn main() {
impl<F: FieldExt> FiboChip<F> {
    // pub fn construct(config: FiboConfig) -> Self 
    pub fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 3],
        instance: Column<Instance>,
    ) -> FiboConfig {
        let col_a = advice[0]; // 对每个 advice 列进行命名
        let col_b = advice[1];
        let col_c = advice[2];
        let selector = meta.selector();

        meta.enable_equality(col_a);
        meta.enable_equality(col_b);
        meta.enable_equality(col_c);
        meta.enable_equality(instance);

        meta.create_gate("add", |meta| {
            //
            // col_a | col_b | col_c | selector
            //   a      b        c       s
            //
            // Query a selector at the current position.
            let s = meta.query_selector(selector);
            let a = meta.query_advice(col_a, Rotation::cur());
            let b = meta.query_advice(col_b, Rotation::cur());
            let c = meta.query_advice(col_c, Rotation::cur());
            vec![s * (a + b - c)]
        });

        FiboConfig {
            advice: [col_a, col_b, col_c],
            selector,
            instance,
        }
    }
}
fn assign_first_row()

函数作用:为 Fibonacci list 的第一行的前 2 个元素分配值 1 , 返回前 3 个元素 a_cell, b_cell, c_cell

  • region.assign_advice_from_instance :从 Instance column (public input) 中取值并赋值给 Advice
#![allow(unused)]
fn main() {
// 在 MockProver::run 中,instance 就是 vec![public_input.clone()]
MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
}
  • region.assign_advice:给 Advice column 赋值,这里特指给斐波那契数列的第 3 个数赋值 f(2) = f(0) + f(1)
#![allow(unused)]
fn main() {
pub fn assign_first_row(
	&self,
	mut layouter: impl Layouter<F>,
) -> Result<(ACell<F>, ACell<F>, ACell<F>), Error> {
	layouter.assign_region(
		|| "first row",
		|mut region| {
			self.config.selector.enable(&mut region, 0)?;

			let a_cell = region.assign_advice_from_instance(
				|| "f(0)",
				self.config.instance,
				0,  // instance column's row 0
				self.config.advice[0],
				0  // offset, advice column's row.
			).map(ACell)?;

			let b_cell = region.assign_advice_from_instance(
				|| "f(1)",
				self.config.instance,
				1, // instance column's row 1
				self.config.advice[1],
				0  // offset, advice column's row.
			).map(ACell)?;

			let c_cell = region.assign_advice(
				|| "f(0)+f(1) i.e. a + b",
				self.config.advice[2],
				0,
				|| a_cell.0.value().copied() + b_cell.0.value()
			).map(ACell)?;

			Ok((a_cell, b_cell, c_cell))
		},
	)
}
}
fn assign_row()

fn assign_row() 函数的作用:不断将上一行的 Advice 列复制到当前行的 Advice 列

PS:这是通过 impl Circuit for MyCircuit.. 中的 synthesis() 重复调用实现的:

#![allow(unused)]
fn main() {
fn synthesis() {
    // ...
	for _i in 3..10 {
		let c_cell = chip.assign_row(layouter.namespace(|| "next row"), &prev_b, &prev_c)?;
		prev_b = prev_c;
		prev_c = c_cell;
	}
}

传入参数:

  • prev_b: &ACell<F>, prev_c: &ACell<F>, : 这正是上一行中的 Advice 列(第 2/3 Advice 列),这意味着前一个b值被复制到新行的第一列(标记为a),前一个c值被复制到新行的第二列(标记为b

prev_b、prev_c 2 个 Cells copy_advice 到新的 Advice 列后:

  • 计算新的斐波那契数c_val,它是prev_bprev_c的和。
  • 使用assign_advice分配c_val到新行的第三列,并返回此值的 ACell
#![allow(unused)]
fn main() {
pub fn assign_row(
	&self,  // 当前`FiboChip`实例的引用
	mut layouter: impl Layouter<F>,
	prev_b: &ACell<F>,   // Fibonacci 数列中的上一行的第 2/3 个 Advice Cell
	prev_c: &ACell<F>,
) -> Result<ACell<F>, Error> {
	layouter.assign_region(
		|| "next row",
		|mut region| {
			self.config.selector.enable(&mut region, 0)?;

			prev_b.0.copy_advice(
				|| "a", 
				&mut region, 
				self.config.advice[0], 
				0
			)?;
			prev_c.0.copy_advice(|| "b", &mut region, self.config.advice[1], 0)?;

			let c_val = prev_b.0.value().copied() + prev_c.0.value();

			let c_cell = region
				.assign_advice(|| "c", self.config.advice[2], 0, || c_val)
				.map(ACell)?;

			Ok(c_cell)
		},
	)
}
}

copy_advice vs assign_advice i.e. 复制 vs. 赋值:

  • 当我们说“复制”,我们实际上是说我们要确保一个 Region-Cell 的值与另一个 Region-Cell 中的值是相同的。与其为每个地方重新计算/分配(assign)一个值,不如简单地“复制”该值到新位置,以确保它们是一样的(考虑 PLONK 中的 permutation argument)
  • Permutations and Copy Constraints: Halo2 使用一种称为“permutation argument“的技术来确保两个或多个单元格中的值是相同的。copy_advice 实际上是在背后使用这个技术,通过引入一个额外的约束来确保值的一致性
fn expose_public()

expose_public 函数作用:将指定的 ACell 公开为 Public Input

#![allow(unused)]
fn main() {
fn synthesis() {
    // ...
    // 将最后一个值公开为 public input
    chip.expose_public(layouter.namespace(|| "out"), &prev_c, 2)?;
}
}
#![allow(unused)]
fn main() {
pub fn expose_public(
	&self,
	mut layouter: impl Layouter<F>,
	cell: &ACell<F>,
	row: usize,
) -> Result<(), Error> {
	layouter.constrain_instance(cell.0.cell(), self.config.instance, row)
}
}

MyCircuit

  1. let chip = FiboChip::construct(config); : 传入 config 创建一个新的 FiboChip 实例
  2. chip.assign_first_row(layouter.namespace(|| "first row"), self.a, self.b)?; : 初始化斐波那契数列: 调用 assign_first_row 函数以在第一行中设置斐波那契数列的前两个值 self.a 和 self.b。返回的结果是三个值:prev_a, prev_b 和 prev_c。其中,prev_c 是前两个数的和
  3. chip.expose_public(layouter.namespace(|| "private a"), &prev_a, 0)?; : 公开前两个数: 将前两个数 expose 为 public, 这意味着这些值可以被 访问和验证
  4. 计算后续的斐波那契数: for 循环中,assign_row 函数被调用以计算后续的斐波那契数。每次迭代都会生成新的斐波那契数并为下一次迭代更新 prev_bprev_c
  5. chip.expose_public(layouter.namespace(|| "out"), &prev_c, 2)?; : 公开最终的斐波那契数: 将循环结束后的最后一个斐波那契数值设为 Public
#![allow(unused)]
fn main() {
#[derive(Default)]
struct MyCircuit<F> (PhantomData<F>);

impl<F: PrimeField> Circuit<F> for MyCircuit<F> {
    type Config = FiboConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let col_a = meta.advice_column();
        let col_b = meta.advice_column();
        let col_c = meta.advice_column();
        let instance = meta.instance_column();
        FiboChip::configure(meta, [col_a, col_b, col_c], instance)
    }
}
}
fn synthesis()

这个函数定义了在 MyCircuit 电路中如何放置约束和分配单元格 Cell,它构建了一个斐波那契序列,并确保输出正确。

#![allow(unused)]
fn main() {
fn synthesize(
	&self,
	config: Self::Config,
	mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
	let chip = FiboChip::construct(config);

	let (_, mut prev_b, mut prev_c) =
		chip.assign_first_row(layouter.namespace(|| "first row"))?;
	
	// 这是干啥??
	// chip.expose_public(layouter.namespace(|| "private a"), &prev_a, 0)?;
	// chip.expose_public(layouter.namespace(|| "private b"), &prev_b, 1)?;

	for _i in 3..10 {
		let c_cell = chip.assign_row(layouter.namespace(|| "next row"), &prev_b, &prev_c)?;
		prev_b = prev_c;
		prev_c = c_cell;
	}

	chip.expose_public(layouter.namespace(|| "out"), &prev_c, 2)?;

	Ok(())
}
}

Test

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::MyCircuit;
    use halo2_proofs::{circuit::Value, dev::MockProver, pasta::Fp};
    use std::marker::PhantomData;

    #[test]
    fn test_example1() {
        let k = 4;

        let a = Fp::from(1); // F[0]
        let b = Fp::from(1); // F[1]
        let out = Fp::from(55); // F[9]

        let circuit = MyCircuit(PhantomData);

        let mut public_input = vec![a, b, out];

        let prover = MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
        prover.assert_satisfied();

        public_input[2] += Fp::one(); // out += 2  =>  unsatisfied
        let _prover = MockProver::run(k, &circuit, vec![public_input]).unwrap();
        // uncomment the following line and the assert will fail
        // _prover.assert_satisfied();
    }
}

Usage

cargo test -- --nocapture fibonacci::example1

# Draw
cargo test --release --all-features plot_fibo1
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

Reference :

question

… No Question?

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Fibonacci Example 2

回顾 Example-1 里的 Table:

在 example-1 中有一些问题:

  • 重复单元格太多,每次需要将 2 个单元格从上一行复制到下一行,效率不高
  • 更好的解决方案:使用 Rotation 来访问多行是个更好的方案

如下图:

Overview

在本例子中, 代码将更加紧凑和模块化, 以下是一些主要的区别 :

Advice Columns:

  • ex 1 :
    • 用了 3 个 advice columns:col_a, col_bcol_c,表的的第一行用来存储 Fibonacci 序列的连续的 3 个数
  • ex 2:
    • 仅使用了一个 advice column,并依赖 Rotation(轮转, 即 )来访问连续的行,减少了各种复制
#![allow(unused)]
fn main() {
// example-1
pub fn configure(.., advice: [Column<Advice>; 3], ..) {
    let col_i = advice[ii];	
    meta.enable_equality(col_i); // 很多列需要声明 和 需要 enable...

    meta.create_gate("add", |meta| {
        let s = meta.query_selector(selector);
        let a = meta.query_advice(col_a, Rotation::cur()); //📢
        let b = meta.query_advice(col_b, Rotation::cur()); //📢
        let c = meta.query_advice(col_c, Rotation::cur()); //📢
    vec![s * (a + b - c)]
}

----------------------------------------

// example-2
pub fn configure(.., advice: Column<Advice>, ..) {
    let col_i = advice[ii];	
	meta.enable_equality(advice);
	meta.enable_equality(instance);

	// Gen Custom Gate:
	meta.create_gate("add", |meta| {
		//
		// advice | selector
		//   a    |   s
		//   b    |
		//   c    |
		//
		let s = meta.query_selector(selector);
		let a = meta.query_advice(advice, Rotation::cur());
		let b = meta.query_advice(advice, Rotation::next());
		let c = meta.query_advice(advice, Rotation(2));
		vec![s * (a + b - c)]
	});
}
  • Rotation::cur() 当前行
  • Rotation::next() 下一行
  • Rotation(2) 再下一行

The Polynomial Identity:

数据赋值:

  • ex 1: 初始的 Fibonacci 数 ab 被赋值到两个不同的 advice columns,而他们的和则被赋值到第三个 column。
  • ex 2: 所有的 Fibonacci 数都在同一个 advice column,但在不同的行 (thanks to rotation)
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(
	&self,
	mut layouter: impl Layouter<F>,
	nrows: usize,  // 前 2 列赋值之后, 后面要搞的列数.. 
) -> Result<ACell<F>, Error> {
	layouter.assign_region(
		|| "entire fibonacci table",
		|mut region| {
			// 为前两行启用 selector, 这意味着我们将从 instance (public input) 列中复制 Fibo 序列的前 2 个数字
			self.config.selector.enable(&mut region, 0)?;
			self.config.selector.enable(&mut region, 1)?;
			
			// assign_advice_from_instance 方法,将 instance (public input) 列的前 2 个值
			//   (即 Fibonacci 序列的前两个数字)赋给 advice 列中的前 2 个单元格
			//   后面在 MockProver 中, 我们会传入 instance 作为 Public input
			let mut a_cell = region.assign_advice_from_instance(
				|| "1",
				self.config.instance,
				0,  // instance column's row 0
				self.config.advice,
				0, // 复制到当前的 region 的 row 0
			).map(ACell)?;

			let mut b_cell = region.assign_advice_from_instance(
				|| "1",
				self.config.instance,
				1, // instance column's row 1
				self.config.advice,
				1,  // 复制到当前的 region 的 row 1
			).map(ACell)?;

            /// .....
		},
	)
}
}

生成的 Fibonacci 数:

  • ex 1: 使用方法 assign_row 从前两个数生成下一个数。
  • ex 2: 使用一个循环在整个 Fibonacci 表格中为所有的数赋值。
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(..., nrows){

    /// .....

	// 赋值好了前 2 行(递归基), 其余的行就累加过去就好了
	for row in 2..nrows { // 对于最后两行, 不需要启用 Selector
		if row < nrows - 2 {
			self.config.selector.enable(&mut region, row)?;
		}

		let c_cell = region.assign_advice(
			|| "advice",
			self.config.advice,
			row,
			|| a_cell.0.value().copied() + b_cell.0.value(),
		).map(ACell)?;

		a_cell = b_cell; // let mut a_cell ...
		b_cell = c_cell;
	}

	Ok(b_cell) // return the last cell.
}
}

Test

如下 instance , 里面是 Public input

#![allow(unused)]
fn main() {
#[test]
fn test_example2() {
	let k = 4;

	let a = Fp::from(1); // F[0]
	let b = Fp::from(1); // F[1]
	let out = Fp::from(55); // F[9]

	let circuit = MyCircuit(PhantomData);

	let mut public_input = vec![a, b, out];

	let prover = MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
	prover.assert_satisfied();

	public_input[2] += Fp::one();
	let _prover = MockProver::run(k, &circuit, vec![public_input]).unwrap();
	// uncomment the following line and the assert will fail
	// _prover.assert_satisfied();
}
}

change k from 13 to 4, the line will be smaller so now you are not calim about the main function

  • the MockProver will tell you constrains that
  • the png will tell you a    constraint you have ignored !

Usage

cargo test -- --nocapture fibonacci::example1

# Draw
cargo test --release --all-features plot_fibo1
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

Reference :

—— 中文版本结束 ——

Example 2

Recap Table in Example-1 :

the problem we noticed like inside example-1 is that there are basically

  • too many duplicate cells , so every time you need copy two cells from previous row to next row , not efficient
  • better solution : Use rotation to access to the multiple rows.

在本例子中, 代码将更加紧凑和模块化, 以下是一些主要的区别 :

Advice Columns:

  • ex 1 :
    • 用了 3 个 advice columns:col_a, col_bcol_c,这 3 个 columns 的第一行用来存储 Fibonacci 序列的连续的 3 个数
    • configure() 在 3 个 advice columns 中为每一个需启用 enable_equality,并为每一个都建立了门约束。
  • ex 2:
    • 仅使用了一个 advice column,并依赖 rotation(轮转, 即 )来访问连续的数, 减少了各种复制
#![allow(unused)]
fn main() {
// example-1
pub fn configure(
	.., advice: [Column<Advice>; 3], .. 
	{
    let col_i = advice[ii];	
    meta.enable_equality(col_i); // 很多列需要声明, 需要 enable...

    meta.create_gate("add", |meta| {
        let s = meta.query_selector(selector);
        let a = meta.query_advice(col_a, Rotation::cur()); //📢
        let b = meta.query_advice(col_b, Rotation::cur()); //📢
        let c = meta.query_advice(col_c, Rotation::cur()); //📢

}

----------------------------------------

// example-2
pub fn configure(  
	.., advice: Column<Advice>, .. 
	{
    meta.enable_equality(advice); //所有 advice 列只在此 enable once.

    meta.create_gate("add", |meta| {
        let s = meta.query_selector(selector);
        let a = meta.query_advice(advice, Rotation::cur()); //💡
        let b = meta.query_advice(advice, Rotation::next());//💡
        let c = meta.query_advice(advice, Rotation(2));}    //💡
}
  • Rotation::cur() 当前行
  • Rotation::next() 下一行
  • Rotation(2) 再下一行

The Polynomial Identity :

  1. 数据赋值:
    • ex 1: 初始的 Fibonacci 数 ab 被赋值到两个不同的 advice columns,而他们的和则被赋值到第三个 column。
    • ex 2: 所有的 Fibonacci 数都在同一个 advice column,但在不同的行 (thanks to rotation)
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(..., nrows){
  layouter.assign_region("entire fibonacci table",
    |mut region| {
    // 为前两行启用 selector,这意味着我们将从 instance 列(可能是公共输入)
    // 中复制 Fibonacci 序列的前 2 个数字
    self.config.selector.enable(&mut region, 0)?;
    self.config.selector.enable(&mut region, 1)?;

    // assign_advice_from_instance 方法,将 instance 列的前两个值
	//   (即 Fibonacci 序列的前两个数字)赋给 advice 列中的前两个单元格
	//   后面在 MockProver 中, 我们会传入 instance 作为 Public input
	let mut a_cell = region.assign_advice_from_instance(
		|| "1",
		self.config.instance,
		0,
		self.config.advice,
		0,  // row
	)?;
	let mut b_cell = region.assign_advice_from_instance(
		|| "1",
		self.config.instance,
		1,  // 这里写 0 也不报错, 写 1/2/3 就会报错了..
		self.config.advice,
		1,  // row
    )?;
    
	// 赋值好了前 2 行(递归基) ,其余的行就累加过去就好了
	for row in 2..nrows {
		if row < nrows - 2 {
			self.config.selector.enable(&mut region, row)?;
		}

		let c_cell = region.assign_advice(
			|| "advice",
			self.config.advice,
			row,
			|| a_cell.value().copied() + b_cell.value(),
		)?;

		a_cell = b_cell;
		b_cell = c_cell;
	}

	Ok(b_cell)	
}
}
  1. 生成的 Fibonacci 数:
    • Version 1: 使用方法 assign_row 从前两个数生成下一个数。
    • Version 2: 使用一个循环在整个 Fibonacci 表格中为所有的数赋值。
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(..., nrows){
	// 赋值好了前 2 行(递归基) ,其余的行就累加过去就好了
	for row in 2..nrows {
		if row < nrows - 2 {
			self.config.selector.enable(&mut region, row)?;
		}

		let c_cell = region.assign_advice(
			|| "advice",
			self.config.advice,
			row,
			|| a_cell.value().copied() + b_cell.value(),
		)?;

		a_cell = b_cell;
		b_cell = c_cell;
	}

	Ok(b_cell)
}
}

如下 instance , 里面是 Public input

#![allow(unused)]
fn main() {
#[test]
fn test_example2() {
	let k = 4;

	let a = Fp::from(1); // F[0]
	let b = Fp::from(1); // F[1]
	let out = Fp::from(55); // F[9]

	let circuit = MyCircuit(PhantomData);

	let mut public_input = vec![a, b, out];

	let prover = MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
	prover.assert_satisfied();

	public_input[2] += Fp::one();
	let _prover = MockProver::run(k, &circuit, vec![public_input]).unwrap();
	// uncomment the following line and the assert will fail
	// _prover.assert_satisfied();
}
}

print

  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.
cargo test --all-features -- --nocapture print

change k from 13 to 4, the line will be more small so now you are not calim about the main function .

  • the MockProver will tell you constrains that ,
  • the png will tell you a    constraint you have ignored !

Row & Column in Region

Compared example-1 with example-2 :

#![allow(unused)]
fn main() {
meta.create_gate("add", |meta| {
	// col_a | col_b | col_c | selector
	//   a      b        c       s
	let s = meta.query_selector(selector);
	let a = meta.query_advice(col_a, Rotation::cur());
	let b = meta.query_advice(col_b, Rotation::cur());
	let c = meta.query_advice(col_c, Rotation::cur());
	vec![s * (a + b - c)]

---------------------------------------------

meta.create_gate("add", |meta| {
	// advice | selector
	//   a    |   s
	//   b    |
	//   c    |
	let s = meta.query_selector(selector);
	let a = meta.query_advice(advice, Rotation::cur());
	let b = meta.query_advice(advice, Rotation::next());
	let c = meta.query_advice(advice, Rotation(2));
	vec![s * (a + b - c)] 
}

We see :

  • col_a / col_b / col_c represent different
  • Rotation::cur() / Rotation::next() / Rotation::prev() / Rotation(2) represent different
cargo test -- --nocapture fibonacci::example2

References :

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Goal

We want Prove that : f(a, b, c) = if a == b {c} else {a - b}

证明某人知道三个数字 a、b 和 c,使得当 a == b 时,输出为 c,否则输出为 a - b,而无需揭示a、b 和 c 的实际值。

how to describe it ? Firstly, let’s dive into the Iszero Chip

这一部分主要展示了如何复用另外一个电路来辅助电路设计

Overview

$ tree
├── fibonacci
│   └── example3.rs
├── is_zero.rs

Iszero Chip

structs

#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]
pub struct IsZeroConfig<F> {
    pub value_inv: Column<Advice>,
    pub is_zero_expr: Expression<F>,
}

impl<F: FieldExt> IsZeroConfig<F> {
    pub fn expr(&self) -> Expression<F> {
        self.is_zero_expr.clone()
    }
}

pub struct IsZeroChip<F: FieldExt> {
    config: IsZeroConfig<F>,
}
}

impl IsZeroChip { ..

configure

传入参数:

  • q_enable, value 都接收一个闭包,将执行的时机和执行的具体内容进行解耦。也就是说,configure 方法可以决定何时执行 value,而调用者可以决定执行 value 时应该做什么
  • 闭包可以捕获其环境,这意味着每次传入不同的闭包,configure 函数的行为都可能不同。这为函数调用者提供了更大的灵活性。

注意 AssignedCellVirtualCells 的区别(读下 Source code)

#![allow(unused)]
fn main() {
impl<F: FieldExt> IsZeroChip<F> {
    pub fn construct(config: IsZeroConfig<F>) -> Self {
        IsZeroChip { config }
    }

    pub fn configure(
        meta: &mut ConstraintSystem<F>,
        q_enable: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression<F>,
        value: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression<F>,
        value_inv: Column<Advice>,
    ) -> IsZeroConfig<F> {
        let mut is_zero_expr = Expression::Constant(F::zero());

        meta.create_gate("is_zero", |meta| {
            //
            // valid | val |  val_inv |  1 - val * val_inv | val * (1 - val * val_inv)
            // ------+-----+----------+--------------------+-------------------
            //  yes  |  x  |    1/x   |        0           |   0
            //  no   |  x  |    0     |        1           |   x
            //  yes  |  0  |    0     |        1           |   0
            //  yes  |  0  |    y     |        1           |   0

            //
            let value = value(meta);
            let q_enable = q_enable(meta);
            let value_inv = meta.query_advice(value_inv, Rotation::cur());

            is_zero_expr = Expression::Constant(F::one()) - value.clone() * value_inv;
            vec![q_enable * value * is_zero_expr.clone()]  // gate's constraints
        });

        IsZeroConfig {
            value_inv,
            is_zero_expr,
        }
    }
}

configure defines the logic for the “is-zero” gate. It uses the following table to guide the logic:

valid | val |  val_inv |  1 - val * val_inv | val * (1 - val * val_inv)
------+-----+----------+--------------------+-------------------       
 yes  |  x  |    1/x   |        0           |   0                      
 no   |  x  |    0     |        1           |   x                      
 yes  |  0  |    0     |        1           |   0                      
 yes  |  0  |    y     |        1           |   0                      

1 / 3 / 4 行涉及到的约束不需要通过 q_enable 即可完成, 但是考虑第二行所涉及到的情况 :

  • 如果 是个 malicious Prover, 他提供了 val == xval_inv == 0 , 此时仅靠 is_zero_expr 是无法分辨的 (这个 case 里 assign 函数会直接分配 self.config.value_inv i.e. 即认为这个值是
  • 但是添加了 vec![q_enable * value * is_zero_expr.clone()] 约束就不一样了 , 约束强制要求 val * is_zero_expr i.e. val * ( 1 - val * val_inv) 必须为 0 , 从而解决了这种 malicious situation
  • 如果 malicious 提供了这种 Witness, 将不会通过约束校验, 也就不会生成该 proof
  • 只有 提供了符合约束的 Witness, val_inv 才会被赋值给 val_inv column

The gate ensures that for valid rows:

  • If the , its inverse is computed such that their multiplication (val * val_inv) ’s results in 1.
  • If the , its inverse can be any value, but the result of their multiplication should be 0.

The gate equation is q_enable * value * (1 - value * value_inv), which should be satisfied for the valid conditions.

  • assign(): This method is used to assign the inverse of a value (if it exists) or zero to the specified advice column in the circuit.
#![allow(unused)]
fn main() {
is_zero_expr = Expression::Constant(F::one()) - value.clone() * value_inv;
}
  • i.e. 1 - val * val_inv , like the table above :
    • if val != 0 : is_zero_expr = 0
    • if val == 0 : is_zero_expr = 1

vec![q_enable * value * is_zero_expr.clone()] is the gate’s constraint. it should be

assign
#![allow(unused)]
fn main() {
pub fn assign(
	&self,
	region: &mut Region<'_, F>,
	offset: usize,
	value: Value<F>,
) -> Result<(), Error> {
	// value.invert()  OR  F::zero()
	let value_inv = value.map(|value| value.invert().unwrap_or(F::zero()));
	region.assign_advice(|| "value inv", self.config.value_inv, offset, || value_inv)?;
	Ok(())
}
}

IsZero 的验证过程中,将要验证的值(或输入值)分配到电路区域中,以便在电路中进行计算和约束的验证 :

  1. 如果要验证的值为零,assign 方法将为逆元分配一个特定的值(例如 F::zero()
  2. 如果要验证的值不为零,value_inv columns 将被分配为 value.invert().unwrap_or(F::zero()) i.e. value.invert()

这些 IsZero 的 check 将被赋值到 value_inv column 并在其上得到体现

Example 3

welcome back, now we have the gadget IsZero , so we can constrain malicious ’s input

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct FunctionConfig<F: FieldExt> {
    selector: Selector,
    a: Column<Advice>,
    b: Column<Advice>,
    c: Column<Advice>,
    a_equals_b: IsZeroConfig<F>,
    output: Column<Advice>,
}
#[derive(Debug, Clone)]
struct FunctionChip<F: FieldExt> {
    config: FunctionConfig<F>,
}
}

configure

Recap : f(a, b, c) = if a == b {c} else {a - b}

  1. column : 除了常规的 a/b/c advice column, 还申请了 is_zero_advice_column
  2. IsZeroChip : use crate::is_zero::{IsZeroChip, IsZeroConfig}; 使用了上面定义的 IsZero chip 来校验 这个事情 (因为 a/b 都是 提供的, 一个 malicious 有动机去提供 a=3 , b=4 然后 return c , 必须通过生成 proof 前的约束来限制 的行为)
  3. IsZeroChip::configure 返回 IsZeroConfig<F>
#![allow(unused)]
fn main() {
impl<F: FieldExt> FunctionChip<F> {
    pub fn construct(config: FunctionConfig<F>) -> Self { Self { config } }

    pub fn configure(meta: &mut ConstraintSystem<F>) -> FunctionConfig<F> {
        let selector = meta.selector();
        let a = meta.advice_column();
        let b = meta.advice_column();
        let c = meta.advice_column();
        let output = meta.advice_column();

        let is_zero_advice_column = meta.advice_column();
        
        let a_equals_b = IsZeroChip::configure(
            meta,
            |meta| meta.query_selector(selector),
            |meta| meta.query_advice(a, Rotation::cur()) - meta.query_advice(b, Rotation::cur()),
            is_zero_advice_column,
        );

        meta.create_gate("f(a, b, c) = if a == b {c} else {a - b}", |meta| {
            let s = meta.query_selector(selector);
            let a = meta.query_advice(a, Rotation::cur());
            let b = meta.query_advice(b, Rotation::cur());
            let c = meta.query_advice(c, Rotation::cur());
            let output = meta.query_advice(output, Rotation::cur());
            vec![
                s.clone() * (a_equals_b.expr() * (output.clone() - c)),
                s * (Expression::Constant(F::one()) - a_equals_b.expr()) * (output - (a - b)),
            ]
        });

        FunctionConfig {
            selector,
            a,
            b,
            c,
            a_equals_b,
            output,
        }
    }
}

assign

  1. IsZeroChip::construct : 创建一个IsZeroChip实例
  2. layouter.assign_region( :
    1. is_zero_chip.assign(&mut region, 0, Value::known(a - b))?;
    2. region.assign_advice(|| "output", self.config.output, 0, || Value::known(output))
#![allow(unused)]
fn main() {
pub fn assign(
  &self,
  mut layouter: impl Layouter<F>,
  a: F,  b: F,  c: F,
) -> Result<AssignedCell<F, F>, Error> {
  let is_zero_chip = IsZeroChip::construct(self.config.a_equals_b.clone());

  layouter.assign_region(
    || "f(a, b, c) = if a == b {c} else {a - b}",
    |mut region| {
      self.config.selector.enable(&mut region, 0)?;
      region.assign_advice(|| "a", self.config.a, 0, || Value::known(a))?;
      region.assign_advice(|| "b", self.config.b, 0, || Value::known(b))?;
      region.assign_advice(|| "c", self.config.c, 0, || Value::known(c))?;

      // 正式使用 IsZeroChip 子电路来检查 a - b 是否为零
      is_zero_chip.assign(&mut region, 0, Value::known(a - b))?;

      // Rust expr to calculate val.
      let output = if a == b { c } else { a - b };
      // assign to cell.
      region.assign_advice(|| "output", self.config.output, 0, || Value::known(output))
    },
  ) }
}

test

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use halo2_proofs::{dev::MockProver, pasta::Fp};

    #[test]
    fn test_example3() {
        let circuit = FunctionCircuit {
            a: Fp::from(10),
            b: Fp::from(12),
            c: Fp::from(15),
        };

        let prover = MockProver::run(4, &circuit, vec![]).unwrap();
        prover.assert_satisfied();
    }
}
}

usage

cargo test -- --nocapture fibonacci::example3

# Draw
cargo test --release --all-features plot_fibo3
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

References:

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Goal

证明一个 value 在给定的范围内,但不透露其具体的值(This helper checks that the value witnessed in a given cell is within a given range)。

范围检查在实际应用中是非常重要的,因为它允许证明者证明他们知道一个值(例如,他们的年龄或账户余额)而不实际公开这个值

Overview

Range-check-example-1 用了一个比较朴素的办法:通过计算表达式 (v) * (1 - v) * (2 - v) * ... * (R - 1 - v) 是否为 0, 实现了一个用于范围检查的配置 RangeCheckConfig,以及相关的方法。主要特点包括:

  1. 使用了一个简单的表达式 (v) * (1 - v) * (2 - v) * ... * (R - 1 - v)来执行范围检查
  2. RangeCheckConfig 配置包含 q_range_check 选择器,用于启用范围检查约束
  3. 没有涉及查找表(Lookup Table)
  4. 测试中对成功和失败的范围检查进行了验证

struct

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
/// A range-constrained value in the circuit produced by the RangeCheckConfig.
struct RangeConstrained<F: FieldExt, const RANGE: usize>(AssignedCell<Assigned<F>, F>);

#[derive(Debug, Clone)]
struct RangeCheckConfig<F: FieldExt, const RANGE: usize> {
    value: Column<Advice>,
    q_range_check: Selector,
    _marker: PhantomData<F>,
}
}

configure

pub fn configure约束系统 中定义自定义门,确保当选择器被激活时值在预期范围内(pub fn configure defines Custom Gates in the constraint system that ensures values are within the expected range when the Selector is activated .)

  1. Define Query Selector 和 advice column:
#![allow(unused)]
fn main() {
let q = meta.query_selector(q_range_check);
let value = meta.query_advice(value, Rotation::cur());
}
  1. 定义范围检查表达式:
  • Closure 为给定的范围和值定义了一个连乘的 Expression,例如对于某个值 v ,范围是计算 (v) * (1 - v) * (2 - v) . If v is indeed in the expected range, The res of multiplication expression will be
  • (1..range)来创建一个范围,该范围从 开始,到 range 结束(但不包括 range itself )。然后,我们使用迭代器方法 fold 方法对该范围中的每个值进行迭代 , 其接受 2 个参数 :
    • init 初始值:在这里,初始值是 value 的克隆
    • mut f :这个 Closure 定义了如何结合前一个结果和当前元素来生成新的结果。闭包接受两个参数:expr(到目前为止累积的结果)和i(当前元素)
#![allow(unused)]
fn main() {
let range_check = |range: usize, value: Expression<F>| {
  assert!(range > 0);
  (1..range).fold( value.clone(), |expr, i| {
    expr * (Expression::Constant(F::from(i as u64)) - value.clone())
  })  };
}
  1. 添加约束:
#![allow(unused)]
fn main() {
// new API
Constraints::with_selector(q, [("range check", range_check(RANGE, value))])
}

q_range_check 选择器被激活(即 为 1 )时,这个约束将确保值在预定的范围内

#![allow(unused)]
fn main() {
impl<F: FieldExt, const RANGE: usize> RangeCheckConfig<F, RANGE> {
  pub fn configure(meta: &mut ConstraintSystem<F>, value: Column<Advice>) -> Self {
    let q_range_check = meta.selector();

    meta.create_gate("range check", |meta| {
      //    value   |  q_range_check
      //     ------------------------------
      //      v     |     1

      let q = meta.query_selector(q_range_check);
      let value = meta.query_advice(value, Rotation::cur());

      // Given a range `R` and a value `v`, returns the expression
      // (v) * (1 - v) * (2 - v) * ... * (R - 1 - v)
      let range_check = |range: usize, value: Expression<F>| {
        assert!(range > 0);
        (1..range).fold(value.clone(), |expr, i| {
          expr * (Expression::Constant(F::from(i as u64)) - value.clone())
        })
      };
      // constrain the expr: `range_check(RANGE, value)` is 0.
      Constraints::with_selector(q, [("range check", range_check(RANGE, value))])
    });

    Self {
      q_range_check,
      value,
      _marker: PhantomData,
    }
  }
}
assign
#![allow(unused)]
fn main() {
  pub fn assign(
    &self,
    mut layouter: impl Layouter<F>,
    value: Value<Assigned<F>>,
  ) -> Result<RangeConstrained<F, RANGE>, Error> {
    layouter.assign_region(
      || "Assign value",
      |mut region| {
        let offset = 0;

        // Enable q_range_check
        self.q_range_check.enable(&mut region, offset)?;

        // Assign value
        region
          .assign_advice(|| "value", self.value, offset, || value)
          .map(RangeConstrained) // call tuple struct's construct fun. 调用元组结构体的构造函数.
      },
    )
  }
}
Circuit
#![allow(unused)]
fn main() {
#[derive(Default)]
struct MyCircuit<F: FieldExt, const RANGE: usize> {
	value: Value<Assigned<F>>,
}

impl<F: FieldExt, const RANGE: usize> Circuit<F> for MyCircuit<F, RANGE> {
	type Config = RangeCheckConfig<F, RANGE>;
	type FloorPlanner = V1;

	fn without_witnesses(&self) -> Self {
		Self::default()
	}

	fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
		let value = meta.advice_column();
		RangeCheckConfig::configure(meta, value)
	}

	fn synthesize(
		&self,
		config: Self::Config,
		mut layouter: impl Layouter<F>,
	) -> Result<(), Error> {
		config.assign(layouter.namespace(|| "Assign value"), self.value)?;

		Ok(())
	}
}
}

Range check (ex-1b)

code explanation …

Range check (Lookup table)

range check with Lookup table: useful when you’re trying to check a larger range and you want to keep the degree bound of your constraints low.

if you have a vary large R, then polynomial is going to be very high degree and that will increase the cost of your circuit so now we have to change our layout a little bit.

table

src/range_check/example2/table.rs

struct RangeTableConfig
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
use halo2_proofs::{
    arithmetic::FieldExt,
    circuit::{Layouter, Value},
    plonk::{ConstraintSystem, Error, TableColumn},
};

// pub(super) 仅当前模块的父模块中可见,但不对外公开
/// A lookup table of values from 0..RANGE.
#[derive(Debug, Clone)]
pub(super) struct RangeTableConfig<F: FieldExt, const RANGE: usize> {
    pub(super) value: TableColumn, 
    // 这个 struct 中存在一个与类型 F 相关的关联,即使 struct 自身并没有实际使用这个类型
    _marker: PhantomData<F>,
}
}
fn configure
  • impl<F: FieldExt, const RANGE: usize> RangeTableConfig<F, RANGE>
  • fn configure() { :
    • Define a Lookup column : let value = meta.lookup_table_column();
#![allow(unused)]
fn main() {
impl<F: FieldExt, const RANGE: usize> RangeTableConfig<F, RANGE> {
    pub(super) fn configure(meta: &mut ConstraintSystem<F>) -> Self {
        let value = meta.lookup_table_column();

        Self {
            value,
            _marker: PhantomData,
        }
    }
    // fn load ..
}
fn load()
  • load() assign the values to our fixed table
  • fn load() 是一个在 RangeTableConfig 结构体上定义的方法,它用于 load (赋值) 一个范围检查表。在这段代码中,范围检查表是一个 0 ~ RANGE-1 的 table
#![allow(unused)]
fn main() {
// load function assign the values to our fixed table
// This action is performed at key gen time
pub(super) fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
  // firstly, for some RANGE we want to load all the values and assign it to the lookup table
  // assign_table is a special api that only works for lookup tables
  layouter.assign_table (
    || "load range-check table",
    |mut table| {
      // from row_0 to row_{RANGE-1}
      let mut offset = 0;
      for value in 0..RANGE {
        table.assign_cell(
          || "num_bits",
          self.value,
          offset,  // row num
          || Value::known(F::from(value as u64)), // assigned value
        )?;
        offset += 1;  // 循环向下赋值, 直到填满 RANGE 所需的所有列
      }

      Ok(()) // return empty tuple (∵ Result<(), Error>)
    },
  )
}
}

main fn

This helper checks that the value witnessed in a given cell is within a given range.

Depending on the range, this helper uses either a range-check expression (for small ranges), or a lookup (for large ranges).

  • above a certain RANGE we use a lookup argument , like , enabled q_range_check & disabled q_looup Selector
  • below that RANGE we stick to the simple expression, like , enabled q_looup & disabled q_range_check Selector
  value   |  q_range_check  |  q_lookup  |  table_value  |
------------------------------------------------------------
   v_0    |       1         |     0      |       0       |
   v_1    |       0         |     1      |       1       |
structs
  • RangeConstrained : 由 RangeCheckConfig 生成的电路中的范围约束值
  • RangeCheckConfig :
    • main 电路的 Chip Config
    • q_range_check : Selector used for small RANGE number.
    • q_lookup : Selector used for large RANGE number.
    • table: RangeTableConfig<F, LOOKUP_RANGE> : Lookup table
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
/// A range-constrained value in the circuit produced by the RangeCheckConfig.
struct RangeConstrained<F: FieldExt, const RANGE: usize>(AssignedCell<Assigned<F>, F>);

#[derive(Debug, Clone)]
struct RangeCheckConfig<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> {
    q_range_check: Selector,
    q_lookup: Selector,
    value: Column<Advice>,
    table: RangeTableConfig<F, LOOKUP_RANGE>, // Lookup table
}
}
fn configure()
#![allow(unused)]
fn main() {
// Write the gate for our range check Config
// It's good practive to pass advice columns to the config (rather than creating it within the config)
// because these are very likely to be shared across multiple config
impl<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize>
  RangeCheckConfig<F, RANGE, LOOKUP_RANGE>
{
  // Remember that the configuration happen at keygen time.
  pub fn configure(meta: &mut ConstraintSystem<F>, value: Column<Advice>) -> Self {
    // Toggles the range_check constraint
    let q_range_check = meta.selector();
    // Toggles the lookup argument
    let q_lookup = meta.complex_selector(); // for lookup table
    // configure a lookup table. and **pass it to config**
    let table = RangeTableConfig::configure(meta);

    // later we will return this config.
    let config = Self {
      q_range_check,
      q_lookup,
      value,
      table: table.clone()
    }; 

    // 1. range-check gate
    meta.create_gate("range check", |meta| {
      let q = meta.query_selector(q_range_check);

      // note that we don't need to specify the rotation when querying the `selctor`
      // That's because the selector always get queried at the current row .
      // While the `advice columns` get queried relatively to the selector offset, so we need to specify the relative rotation
      // 然而 advice col 是相对于选择器偏移量(Selector offset)进行查询的,所以我们需要指定 relative rotation.
      let value = meta.query_advice(value, Rotation::cur());

      // Given a range R and a value v, returns the multiplication expression
      //  (v) * (1 - v) * (2 - v) * ... * (R - 1 - v)
      let range_check = |range: usize, value: Expression<F>| {
        assert!(range > 0);
        (1..range).fold(value.clone(), |expr, i| {
          expr * (Expression::Constant(F::from(i as u64)) - value.clone())
        })
      };
      // like the previously using "vec![s * (a + b - c)]",
      // multiplies the specified constraint by the selector, api 将指定的约束 × Selector
      Constraints::with_selector(q, [("range check", range_check(RANGE, value))])
    });
    
    // 2. Lookup Gate  - range-check using lookup argument
    // 这个查找表将会在后面的范围检查中使用,以便在某些情况下使用查找表, 而不是表达式来执行范围检查。
    meta.lookup(|meta| {
      let q_lookup = meta.query_selector(q_lookup);
      let value = meta.query_advice(value, Rotation::cur());

      vec![(q_lookup * value, table.value)]
    });

    config
  }
}
fn assign_simple()

used for small value. We pass value and assign it on the offset.

#![allow(unused)]
fn main() {
// pass `value` and assign it on the offset.
pub fn assign_simple(
  &self,
  mut layouter: impl Layouter<F>,
  value: Value<Assigned<F>>,
) -> Result<RangeConstrained<F, RANGE>, Error> {
  layouter.assign_region(
    || "Assign value for simple range check",
    |mut region| {
      let offset = 0;

      // Enable q_range_check Selector.
      self.q_range_check.enable(&mut region, offset)?;

      // Assign `value` 
      region
        .assign_advice(
          || "value", 
          self.value,  // current col ?
          offset, 
          || value
        ).map(RangeConstrained) // 将结果转化为 RangeConstrained 类型
    },
  )
}
}
fn assign_lookup()
#![allow(unused)]
fn main() {
pub fn assign_lookup(
  &self,
  mut layouter: impl Layouter<F>,
  value: Value<Assigned<F>>,
) -> Result<RangeConstrained<F, LOOKUP_RANGE>, Error> {
  layouter.assign_region(
    || "Assign value for lookup range check",
    |mut region| {
      let offset = 0;

      // Enable q_lookup, 告诉约束系统在该区域应用这个选择器
      self.q_lookup.enable(&mut region, offset)?;

      // Assign value
      region
        .assign_advice(|| "value", self.value, offset, || value)
        .map(RangeConstrained)
      // assign_advice() 将 advice col 与值 value 关联,
      // 并将结果封装在 RangeConstrained struct 中
    },
  )}
}

Test Lookup table

#![allow(unused)]
fn main() {
// [cfg(test)]是一个条件编译属性,意思是只有在执行 test 时,此模块代码才会被编译和执行
// 好处是,当你在普通的编译或生产环境下构建你的程序时,测试代码不会被包括进去,
// 从而减少了编译时间和生成的可执行文件的大小。
#[cfg(test)]
mod tests {
    use halo2_proofs::{
        circuit::floor_planner::V1,
        dev::{FailureLocation, MockProver, VerifyFailure},
        pasta::Fp,
        plonk::{Any, Circuit},
    };

    use super::*;
    //// .....
}
struct MyCircuit

MyCircuit 可以处理 2 种类型的值 :

  • value : 这里的 value 的约束和赋值由 assign_simple() 完成
  • lookup_value : 它的约束和赋值由 assign_lookup() 完成
#![allow(unused)]
fn main() {
#[derive(Default)]
struct MyCircuit<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> {
	value: Value<Assigned<F>>,
	lookup_value: Value<Assigned<F>>,
}

impl<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> Circuit<F>
	for MyCircuit<F, RANGE, LOOKUP_RANGE>
{
	type Config = RangeCheckConfig<F, RANGE, LOOKUP_RANGE>;
	type FloorPlanner = V1;

	fn without_witnesses(&self) -> Self { Self::default() }

	fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
		let value = meta.advice_column();
		RangeCheckConfig::configure(meta, value)
	}
    // fn synthesize
}
fn synthesis()
#![allow(unused)]
fn main() {
fn synthesize(
    &self,
    config: Self::Config,
    mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
    // load lookup table.
    config.table.load(&mut layouter)?;

    config.assign_simple(layouter.namespace(
        || "Assign simple(smaller) value"), 
        self.value
    )?;
    config.assign_lookup(
        layouter.namespace(|| "Assign lookup(larger) value"),
        self.lookup_value,
    )?;
    Ok(())
}  }
}
test_range_check_2
  • i, j 的双重循环里:
    • MyCircuit{ 1,10 }
    • MyCircuit{ 7,16 }
    • MyCircuit{ 5,100 }
    • MyCircuit{ 7,255 }
#![allow(unused)]
fn main() {
#[test]
fn test_range_check_2() {
  // in every circuit, we opt to reserve the last few rows of each advice cols 
  // for random values which are blinding factors(for zk), so `k` is always larger.
  let k = 9;
  const RANGE: usize = 8; // 3-bit value
  const LOOKUP_RANGE: usize = 256; // 2^8, 8-bit value

  // Successful cases
  for i in 0..RANGE {
    for j in 0..LOOKUP_RANGE {
      // According to the <i, j> to construct different Circuit.
      //MyCircuit::<Fp,.. ,..> : 指定 Constant 泛型的值.
      let circuit = MyCircuit::<Fp, RANGE, LOOKUP_RANGE> {
        simple_value: Value::known(Fp::from(i as u64).into()),
        lookup_value: Value::known(Fp::from(j as u64).into()),
      };

      let prover = MockProver::run(k, &circuit, vec![]).unwrap();
      prover.assert_satisfied();
    }
  }
}

illustration

![[Range-Check-impl-relationship.excalidraw]]

usage

cargo test -- --nocapture test_range_check_2

# Draw
cargo test --release --all-features xxx
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

References :

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Overview

本部分是 Jason Morton 对于上一 Chapter 的代码超の详细解释: https://github.com/jasonmorton/halo2-examples/blob/master/src/range_check/example1b.rs

下面一起来看看

imports

#![allow(unused)]
fn main() {
use ff::{Field, PrimeField};
use std::marker::PhantomData; // Allows Rust to track types that do not otherwise appear in a struct's fields, here just the field element type

use halo2_proofs::{
    circuit::{
        floor_planner::V1,
        AssignedCell, // a value Value<V> together with its global location as a Cell with region_index, row_offset, and column
        Layouter,     // layout strategy and accepter struct, a bit like a Writer
        Value,        // basically an Option<V>, where Some(v) is called known and None is unknown
    },
    plonk::{
        Advice,      // empty struct to mark Advice columns
        Assigned, // enum Zero, Trivial(F) "does not require inversion to evaluate", or Rational(F, F) "stored as a fraction to enable batch inversion". This is an actual value (wrapped felt)
        Circuit,  // trait with without_witnesses, configure, and synthesize methods
        Column, // represents a pre-layout abstract Column. Fields are index: usize and column type.
        ConstraintSystem, // The container for the actual constraint system; much of the frontend code exists to make it easier to populate this container
        Constraints, // Iterable with a selector and Constraint s.  Constraints are single polynomial Expressions returned by create gate
        Error,       // Custom Error type
        Expression, // Polynomial expression enum, as binary tree, with 5 types of atomic variables v (Constant, Selector, Fixed, Advice, Instance) and combinations -v, v+v, a*v, or v*v.
        Selector, // (index: usize, simple: bool) column type, w/ index = index of this selector in the ConstraintSystem, simple = "can only be multiplied by Expressions not containing Selectors"
    },
    poly::Rotation, // i32 wrapper representing rotation in Lagrange basis
};
}
  • AssignedCell:AssignedCell 表示一个与其全局位置相关联的值 Value<V>,该位置定义为 Cell,字段: region_index, row_offset 和 column
  • Value:Value 本质是一个 Option<V> ,其中 Some(v) 被称为 known(已知),而 None 则是 unknown(未知)
  • Assigned:Assigned 是一个 enum 枚举类型,有 Zero, Trivial(F) 和 Rational(F, F) 三个成员。其中
    • Trivial(F) “A value that does not require inversion to evaluate.”,
    • Rational(F, F) “存储为分数以便进行批量取反 (A value stored as a fraction to enable batch inversion.)” ,这是一个实际的值(wrapped felt)
  • Circuit:Circuit 是一个 trait,具有 without_witnesses, configure 和 synthesize 方法
  • ConstraintSystem:ConstraintSystem 是实际约束系统的容器;大部分前端代码的存在都是为了更容易地填充这个容器
  • Constraints : 提供了一个新的 API 来进行约束
  • Expression
    • Expression 是多项式表达式的枚举类型,表现为二叉树,拥有5种类型的原子变量 v(Constant, Selector, Fixed, Advice, Instance)以及combination -v, v+v, a*v, or v*v.
    • Low-degree expression representing an identity that must hold over the committed columns.
#![allow(unused)]
fn main() {
// Config 是您自定义电路的关联类型(仅要求实现 Clone trait)。它没有特定的强制结构,存储所需的任何类型信息
// A Config is an associated type of your custom circuit (required only to be Clone).  With no particular enforced structure, it stores whatever type information is needed

// to understand the constraint system (number and types of columns, their indices, some flags such as simple/complex selector, etc.).
// It is a bit like a morphism type in a Monoidal category (domain and codomain), or the row and column labels in a dataframe. Let's call it the FrameType
// It can be unstructured because it is the Circuit implementer's job to translate this information into the format needed for the Layouter.

#[derive(Clone)]
struct MyConfig<F: PrimeField, const RANGE: usize> {
    advice_column: Column<Advice>,  // a marker and index for a Selector
    q_range_check: Selector,   // similarly a marker and index for a Selector
    _marker: PhantomData<F>,
}

// 通常,Config 会有一个 configure 和 assign 方法,它们由 Circuit 的 configure 和 synthesize 方法委托。在这个版本的示例中,我们将直接在电路中放置此逻辑。
// By convention the Config gets a configure() and assign() method, which are delegated to by the configure and synthesize method of the Circuit.
// In this version of the example we will put this logic directly in the circuit.
impl<F: PrimeField, const RANGE: usize> MyConfig<F, RANGE> {}

#[derive(Default)] // Deriving Default calls Default on Value<Assigned<F>> calls impl<V> Default for Value<V> { fn default() -> Self {  Self::unknown()  }}
                   // which in turn sets value.inner: Option<V> to None
struct MyCircuit<F: PrimeField, const RANGE: usize> {
    assigned_value: Value<Assigned<F>>,
    _marker: PhantomData<F>,
}
impl<F: PrimeField, const RANGE: usize> MyCircuit<F, RANGE> {}
}

impl Circuit for MyCircuit

#![allow(unused)]
fn main() {
// Your Circuit plays several roles and  will be passed to prover and verifier key generation, prove, and verifier.
// Implementing the Circuit trait requires three functions:
// 1) Anything special that needs to be done to set it up without witness values (which will be used in p&v key generation), usually just all witness
//    values are set to None (Remember Value is basically an alias of Option).
// 2) How to 'configure' it.  This is a bit mysterious because the required trait method configure just returns your custom associated type
//    Config, which in turn need only be Clone.  So what is happening? If we look at the MockProver code, we see that an empty ConstraintSystem is
//    created and passed to the configure function as mutable, then an immutable borrow is taken and worked with for the duration:
//         let mut cs = ConstraintSystem::default();
//         let config = ConcreteCircuit::configure(&mut cs);
//         let cs = cs;
//    Thus it the Circuit's `configure` method that is responsible for all of the contraint system definition.  It could be called compile_constraints.
}

电路会扮演多个角色,并且会被传递给 proververifier 来 进行 key generation, prove, and verify.

  • without_witnesses 通常在 p&v key generation 期间使用
  • How to ‘configure’ it? 如果我们查看 MockProver 代码,我们可以看到创建了一个空的 ConstraintSystem,传递给 configure() 作为可变引用
#![allow(unused)]
fn main() {
//    Why does the configure method accept an empty constraint system and return the arbitrary Self::Config, after filling the constaint system to its liking?
//    The config returned by ConcreteCircuit ::configure is used just once in the MockProver:
//         ConcreteCircuit::FloorPlanner::synthesize(&mut prover, circuit, config, constants)?;
//    So after setting up the constraints, the Config stores the information about the layout that the floor planner will need to synthesize.
//
//    The `configure` function is where we call cs.create_gate() and so on, adding polynomials to the system.  At this point, the variables in these polynomials
//    are still locally named/scoped, and will be given a global name/scope during layout.
}

为什么 configure 方法接受一个空的约束系统,并在填充约束系统后返回任意的 Self::Config?

  1. ConcreteCircuit::configure 返回的 config 在 MockProver 中只使用一次:
  2. ConcreteCircuit::FloorPlanner::synthesize(&mut prover, circuit, config, constants)?;
  3. 因此,在设置约束之后,Config 存储了 floor planner 在合成时需要的布局信息。
#![allow(unused)]
fn main() {
// 3) How to synthesize, given the circuit, a provided &mut Layouter and the data passed in the Config.  The synthesize method returns only error information, and
//    changes state by writing to the Layouter.  Thus the Layouter is like a buffer or Writer to which the final circuit is written.  One will be supplied by the
//    MockProver below (and variants will be needed during keygen and proving).
//
//  Roughly, configure provides the concrete but relative layout, and synthesize combines such blocks and assigns an absolute layout.
impl<F: PrimeField, const RANGE: usize> Circuit<F> for MyCircuit<F, RANGE> {
    type Config = MyConfig<F, RANGE>;
    type FloorPlanner = V1;

    // This boilerplate could be removed by putting it in the Circuit trait and defining the Circuit trait with a bound as Circuit: Default, but that might be annoying too.
    fn without_witnesses(&self) -> Self {
        Self::default() // should fill all the Witness Values with None/Unknown.
    }
}

How to synthesize?

  • 给定电路 Circuit、 &mut Layouter 和在 Config 中传递的数据。synthesize 方法只返回错误信息,并通过写入 Layouter 更改状态。因此,Layouter 就像最终电路被写入的缓冲区或 Writer。

大致来说,configure 提供了具体但相对的布局,而 synthesize 组合这些块并分配绝对布局。

fn configure()

#![allow(unused)]
fn main() {
// define the constraints, mutate the provided ConstraintSystem, and output the resulting FrameType
// 改变(mutate) 提供的 ConstraintSystem,并输出结果的 FrameType
fn configure(cs: &mut ConstraintSystem<F>) -> Self::Config {
	// Create the column marker types. Requests the CS to allocate a new column (giving it a unique cs-global index and incrementing its
	// 创建列标记类型。请求 CS 分配一个新列(给它一个唯一的 cs-global 索引并增加它的
	//   num_selectors, num_fixed_columns, num_advice_columns, or num_instance_columns).
	let advice_column = cs.advice_column();
	let q_range_check = cs.selector();

	// When we use cs.query_advice or cs.query_selector, we obtain an Expression which is a reference to a cell in the matrix.
	// 访问 cs.query_advice / query_selector 获得一个 Expression,它是矩阵中一个单元格的引用(reference)
	//   Expression::Advice {
	//     query_index: self.meta.query_advice_index(column, at),
	//     column_index: column.index,
	//     rotation: at,
	//   }
	// Such an a_{ij} or a_{this_row + at, column} can be treated as a symbolic variable and put into a polynomial constraint.
	// More precisely, this is a relative reference wrt rows. 
	// 例如 a_{ij} / a_{this_row + at, column} 会被视为一个符号变量,并放入一个多项式约束中。
	// (这是一个相对于行的相对引用)

	// cs.create_gate takes a function from virtual_cells to contraints, pushing the constraints to the cs's accumulator.  So this puts
	// (value.clone()) * (1 - value.clone()) * (2 - value.clone()) * ... * (R - 1 - value.clone())
	// into the constraint list.
	// 注意 [VirtualCells], 它持有对`ConstraintSystem`的可变引用,存储已查询的选择器/不同类型的列
	cs.create_gate("range check", |virtual_cells| {
		let q = virtual_cells.query_selector(q_range_check);
		let value = virtual_cells.query_advice(advice_column, Rotation::cur());

		// Given a range R and a value v, returns the expression
		// (v) * (1 - v) * (2 - v) * ... * (R - 1 - v)
		//  Range Check poly:
		let rc_polynomial = (1..RANGE).fold(value.clone(), |expr, i| {
			expr * (Expression::Constant(F::from(i as u64)) - value.clone())
		});

		Constraints::with_selector(q, [("range check", rc_polynomial)])
	});

	// The "FrameType"
	Self::Config {
		q_range_check,
		advice_column,
		_marker: PhantomData,
	}
}
}

query_selector & query_advice

  • When we use cs.query_advice or cs.query_selector, we obtain an Expression which is a reference to a cell in the matrix.
  • 一个 a_{ij}a_{this_row + at, column} 可以被视为一个符号变量,并放入一个多项式约束中。更准确地说,这是一个相对于行的相对引用。

fn synthesize()

#![allow(unused)]
fn main() {
fn synthesize(
	&self,
	config: Self::Config,
	mut layouter: impl Layouter<F>, // layouter is our 'write buffer' for the circuit
) -> Result<(), Error> {
	// From the function docs:
	// Assign a region of gates to an absolute row number. 将门的 region 分配一个绝对行号。
	// Inside the closure, the chip may freely use relative offsets; the `Layouter` will
	// treat these assignments as a single "region" within the circuit. Outside this
	// closure, the `Layouter` is allowed to optimise as it sees fit.
	// 闭包内,chip 可以自由使用相对偏移;`Layouter` 会将这些 assignments 视为电路中的单个“region”。
	// 在闭包外部,`Layouter` 可以根据需要进行优化

	layouter.assign_region(
		|| "Assign value", // the name of the region
		|mut region| {
			let offset = 0;

			// Enable q_range_check. Remember that q_range_check is a label, a Selector. Calling its enable
			// - calls region.enable_selector(_,q_range_check,offset)  which
			// - calls enable_selector on the region's RegionLayouter which
			// - calls enable_selector on its "CS" (actually an Assignment<F> (a trait), and whatever impls that
			// does the work, for example for MockProver the enable_selector function does some checks and then sets
			//   self.selectors[selector.0][row] = true;
			config.q_range_check.enable(&mut region, offset)?;

			// Similarly after indirection calls assign_advice in e.g. the MockProver, which
			// takes a Value-producing to() and does something like
			// CellValue::Assigned(to().into_field().evaluate().assign()?);
			// 类似 MockProver 间接调用 assign_advice, 它接受一个 Value-producing 的 to() (进行赋值)
			region.assign_advice(
				|| "value",
				config.advice_column,
				offset,
				|| self.assigned_value,
			)
		},
	)?;
	Ok(())
} }
}

tests

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use halo2_proofs::{
        dev::{FailureLocation, MockProver, VerifyFailure},
        pasta::Fp,
        plonk::{Any, Circuit},
    };

    use super::*;

    #[test]
    fn test_range_check_1() {
        let k = 4; //2^k rows
        const RANGE: usize = 8; // 3-bit value
        let testvalue: u64 = 22;

        // Successful cases
        for i in 0..RANGE {
            let circuit = MyCircuit::<Fp, RANGE> {
                assigned_value: Value::known(Fp::from(i as u64).into()),
                _marker: PhantomData,
            };

            // The MockProver arguments are log_2(nrows), the circuit (with advice already assigned), and the instance variables.
            // The MockProver will need to internally supply a Layouter for the constraint system to be actually written.
            // k 对应 2^k 行, MockProver 将需要内部提供一个 Layouter,以便实际编写约束系统
            let prover = MockProver::run(k, &circuit, vec![]).unwrap();
            prover.assert_satisfied();
        }

        // Out-of-range `value = 8`
        {
            let circuit = MyCircuit::<Fp, RANGE> {
                assigned_value: Value::known(Fp::from(testvalue).into()),
                _marker: PhantomData,
            };
            let prover = MockProver::run(k, &circuit, vec![]).unwrap();
            assert_eq!(
                prover.verify(),
                Err(vec![VerifyFailure::ConstraintNotSatisfied {
                    constraint: ((0, "range check").into(), 0, "range check").into(),
                    location: FailureLocation::InRegion {
                        region: (0, "Assign value").into(),
                        offset: 0
                    },
                    cell_values: vec![(((Any::Advice, 0).into(), 0).into(), "0x16".to_string())]
                }])
            );
        }
    }
}
}

References :

author: @Demian

references: https://learn.0xparc.org/materials/halo2

Overview

上一章节使用的 (v) * (1 - v) * (2 - v) * ... * (R - 1 - v),本章使用了 Lookup Table 来执行范围检查。

连乘表达式的问题:如果数字 “R” 很大,那么多项式的次数将非常高,这会增加电路的成本,所以现在我们必须稍微改变我们的布局:使用查找表进行范围检查:当您尝试检查更大的范围并且希望将约束的度数限制保持在较低水平时非常有用。

(if you have a vary large R, then polynomial is going to be very high degree and that will increase the cost of your circuit so now we have to change our layout a little bit. range check with Lookup table: useful when you’re trying to check a larger range and you want to keep the degree bound of your constraints low.)

文件架构:

#![allow(unused)]
fn main() {
├── range_check
│   ├── example2
│   │   └── table.rs  // lookup table
│   ├── example2.rs   // main config
}

调用链:

impl relationship:

Draw the circuit:

![[Range-Check-impl-relationship.excalidraw]]

lookup table - table.rs

src/range_check/example2/table.rs

struct RangeTableConfig
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
use halo2_proofs::{
    arithmetic::FieldExt,
    circuit::{Layouter, Value},
    plonk::{ConstraintSystem, Error, TableColumn},
};

// pub(super) 仅当前模块的父模块中可见,但不对外公开
/// A lookup table of values from 0..RANGE.
#[derive(Debug, Clone)]
pub(super) struct RangeTableConfig<F: FieldExt, const RANGE: usize> {
    pub(super) value: TableColumn, 
    // 这个 struct 中存在一个与类型 F 相关的关联,即使 struct 自身并没有实际使用这个类型
    _marker: PhantomData<F>,
}
}
fn configure()
  • 泛型常量参数 const N: usize,来处理不同大小和类型的数组
  • meta.lookup_table_column(); 实际会返回一个 fixed_column
#![allow(unused)]
fn main() {
impl<F: FieldExt, const RANGE: usize> RangeTableConfig<F, RANGE> {
    pub(super) fn configure(meta: &mut ConstraintSystem<F>) -> Self {
        let value = meta.lookup_table_column(); // Define a  *Lookup column*
        Self {
            value,
            _marker: PhantomData,
        }
    }
    // fn load ..
}

halo2_proofs/src/plonk/circuit.rs :

#![allow(unused)]
fn main() {
    /// Allocates a new fixed column that can be used in a lookup table.
    pub fn lookup_table_column(&mut self) -> TableColumn {
        TableColumn {
            inner: self.fixed_column(),
        }
    }
}
fn load()
  • load() assign the values to our fixed table
  • fn load() 是一个在 RangeTableConfig 结构体上定义的方法,它用于 load (赋值) 一个范围检查表。在这段代码中,范围检查表是一个 0 ~ RANGE-1 的 table
#![allow(unused)]
fn main() {
// load function assign the values to our fixed table
// This action is performed at key gen time
pub(super) fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
  // firstly, for some RANGE we want to load all the values and assign it to the lookup table
  // assign_table is a special api that only works for lookup tables
  layouter.assign_table (
    || "load range-check table",
    |mut table| {
      // from row_0 to row_{RANGE-1}
      let mut offset = 0;
      for value in 0..RANGE {
        table.assign_cell(
          || "num_bits",
          self.value,
          offset,  // row num
          || Value::known(F::from(value as u64)), // assigned value
        )?;
        offset += 1;  // 循环向下赋值, 直到填满 RANGE 所需的所有列
      }

      Ok(()) // return empty tuple (∵ Result<(), Error>)
    },
  )
}
}

main - example2.rs

Overview

This helper checks that the value witnessed in a given cell is within a given range.

Depending on the range, this helper uses either a range-check expression (for small ranges), or a lookup (for large ranges).

  • above a certain RANGE we use a lookup argument , like , enabled q_range_check & disabled q_looup Selector
  • below that RANGE we stick to the simple expression, like , enabled q_looup & disabled q_range_check Selector
  value   |  q_range_check  |  q_lookup  |  table_value  |
------------------------------------------------------------
   v_0    |       1         |     0      |       0       |
   v_1    |       0         |     1      |       1       |
  • 在一个比较小的特定范围里,使用 range-check 连乘 expression
  • 对于比较大的查找范围,使用 Lookup Table 查找表

structs

  • RangeConstrained : 由 RangeCheckConfig 生成的电路中的范围约束值 (range-constrained value),即用来表示一个范围受限的值。
  • RangeCheckConfig : main 电路的 Chip Config,用于配置和执行范围检查
    • q_range_check : Selector used for small RANGE number.
    • q_lookup : Selector used for large RANGE number.
    • value:an Advice column 用于存储 Private value without revealing it.
    • table: RangeTableConfig<F, LOOKUP_RANGE> : Lookup table
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
/// A range-constrained value in the circuit produced by the RangeCheckConfig.
struct RangeConstrained<F: FieldExt, const RANGE: usize>(AssignedCell<Assigned<F>, F>);

#[derive(Debug, Clone)]
struct RangeCheckConfig<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> {
    q_range_check: Selector,
    q_lookup: Selector,
    value: Column<Advice>,
    table: RangeTableConfig<F, LOOKUP_RANGE>, // Lookup table
}
}

impl RangeCheckConfig

fn configure()
  1. query_selector 即查询 Selector 时,无需指定显式 rotation,因为 selector always get queried at the current row .
  2. query_advice 即查询 Advice 时,因为 advice col 是相对于 Selector 偏移量(Selector offset)进行查询的,所以我们需要指定 relative rotation.
  3. 不像之前我们在 configure() 函数内部声明 Advice column:
#![allow(unused)]
fn main() {
pub fn configure(){
  let col_a = meta.advice_column();
  meta.enable_equality(col_a); // 在 `configure()` 内部声明 Advice column:
  /// ...
}
}

在本 configure() 中,我们传入 value: Column<Advice>) ,这样可以更方便地 shared across multiple config :

#![allow(unused)]
fn main() {
// 在 impl Circuit for MyCircuit 中调用:
fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
	let value = meta.advice_column(); // 声明 value 这个 Advice column
	RangeCheckConfig::configure(meta, value) // 传入 value 这个 Advice column
}
}

meta.create_gate 过程中,创建了 1 个约束门,赋值了 1 个 special fixed colum

  1. meta.create_gate("range check".. 门:使用 range-check 连乘 expression 限制小范围数字
  2. meta.lookup(|meta| { .. 门:Fix 查找表,在一个更大的范围内进行约束
#![allow(unused)]
fn main() {
// Write the gate for our range check Config
// It's good practive to pass advice columns to the config (rather than creating it within the config)
// because these are very likely to be shared across multiple config
impl<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize>
  RangeCheckConfig<F, RANGE, LOOKUP_RANGE>
{
  // Remember that the configuration happen at keygen time.
  pub fn configure(meta: &mut ConstraintSystem<F>, value: Column<Advice>) -> Self {
    // Toggles the range_check constraint
    let q_range_check = meta.selector();
    // Toggles the lookup argument
    let q_lookup = meta.complex_selector(); // for lookup table
    // configure a lookup table. and **pass it to config**
    let table = RangeTableConfig::configure(meta);

    // later we will return this config.
    let config = Self {
      q_range_check,
      q_lookup,
      value,
      table: table.clone()
    }; 

    // 1. range-check gate
    meta.create_gate("range check", |meta| {
      let q = meta.query_selector(q_range_check);

      // note that we don't need to specify the rotation when querying the `selctor`
      // That's because the selector always get queried at the current row .
      // While the `advice columns` get queried relatively to the selector offset, so we need to specify the relative rotation
      // 然而 advice col 是相对于选择器偏移量(Selector offset)进行查询的,所以我们需要指定 relative rotation.
      let value = meta.query_advice(value, Rotation::cur());

      // Given a range R and a value v, returns the multiplication expression
      //  (v) * (1 - v) * (2 - v) * ... * (R - 1 - v)
      let range_check = |range: usize, value: Expression<F>| {
        assert!(range > 0);
        (1..range).fold(value.clone(), |expr, i| {
          expr * (Expression::Constant(F::from(i as u64)) - value.clone())
        })
      };
      // like the previously using "vec![s * (a + b - c)]",
      // multiplies the specified constraint by the selector, api 将指定的约束 × Selector
      Constraints::with_selector(q, [("range check", range_check(RANGE, value))])
    });
    
    // 2. Lookup Gate  - range-check using lookup argument
    // 这个查找表将会在后面的范围检查中使用,以便在某些情况下使用查找表, 而不是表达式来执行范围检查。
    meta.lookup(|meta| {
      let q_lookup = meta.query_selector(q_lookup);
      let value = meta.query_advice(value, Rotation::cur());

      vec![(q_lookup * value, table.value)]
    });

    config
  }
}

如何协同?

#![allow(unused)]
fn main() {
// 1. 定义 Circuit
let circuit = MyCircuit::<Fp, RANGE, LOOKUP_RANGE> {
	simple_value: Value::known(Fp::from(i as u64).into()),
	lookup_value: Value::known(Fp::from(j as u64).into()),
};

// 2. 将 circuit 传入 MockProver::run()
// 3. run() 中  (halo2_proofs/src/dev.rs ) :
// 3.1  ConcreteCircuit::configure
// 3.2  ConcreteCircuit::FloorPlanner::synthesize
impl<F: Field + Ord> MockProver<F> {
    /// Runs a synthetic keygen-and-prove operation on the given circuit,  
    /// collecting data about the constraints and their assignments.
    pub fn run<>(){
        let mut cs = ConstraintSystem::default();
        let config = ConcreteCircuit::configure(&mut cs);
        let cs = cs;
        // ...
        ConcreteCircuit::FloorPlanner::synthesize(&mut prover, circuit, config, constants)?;
    }
}

synthesize 中:

fn assign_simple()

used for small value. We pass value and assign it on the offset.

#![allow(unused)]
fn main() {
// pass `value` and assign it on the offset.
pub fn assign_simple(
  &self,
  mut layouter: impl Layouter<F>,
  value: Value<Assigned<F>>,
) -> Result<RangeConstrained<F, RANGE>, Error> {
  layouter.assign_region(
    || "Assign value for simple range check",
    |mut region| {
      let offset = 0;

      // Enable q_range_check Selector.
      self.q_range_check.enable(&mut region, offset)?;

      // Assign `value` 
      region
        .assign_advice(
          || "value", 
          self.value,  // current col ?
          offset, 
          || value
        ).map(RangeConstrained) // 将结果转化为 RangeConstrained 类型
    },
  )
}
}
fn assign_lookup()
#![allow(unused)]
fn main() {
pub fn assign_lookup(
  &self,
  mut layouter: impl Layouter<F>,
  value: Value<Assigned<F>>,
) -> Result<RangeConstrained<F, LOOKUP_RANGE>, Error> {
  layouter.assign_region(
    || "Assign value for lookup range check",
    |mut region| {
      let offset = 0;

      // Enable q_lookup, 告诉约束系统在该区域应用这个选择器
      self.q_lookup.enable(&mut region, offset)?;

      // Assign value
      region
        .assign_advice(|| "value", self.value, offset, || value)
        .map(RangeConstrained)
      // assign_advice() 将 advice col 与值 value 关联,
      // 并将结果封装在 RangeConstrained struct 中
    },
  )}
}

assign_simple & assign_lookup 这 2 个函数的区别:

#![allow(unused)]
fn main() {
1. 泛型常量
 - RANGE
 - LOOKUP_RANGE
2. Selector enabled:
 - q_range_check // for *small* RANGE number.
 - q_lookup      // for *large* RANGE number.

region.assign_advic 部分是一样的
}

Test Lookup table

#![allow(unused)]
fn main() {
// [cfg(test)]是一个条件编译属性,意思是只有在执行 test 时,此模块代码才会被编译和执行
// 好处是,当你在普通的编译或生产环境下构建你的程序时,测试代码不会被包括进去,
// 从而减少了编译时间和生成的可执行文件的大小。
#[cfg(test)]
mod tests {
    use halo2_proofs::{
        circuit::floor_planner::V1,
        dev::{FailureLocation, MockProver, VerifyFailure},
        pasta::Fp,
        plonk::{Any, Circuit},
    };

    use super::*;
    //// .....
}

struct MyCircuit

MyCircuit 可以处理 2 种类型的值 :

  • value : 这里的 value 的约束和赋值由 assign_simple() 完成
  • lookup_value : 它的约束和赋值由 assign_lookup() 完成
#![allow(unused)]
fn main() {
#[derive(Default)]
struct MyCircuit<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> {
	value: Value<Assigned<F>>,
	lookup_value: Value<Assigned<F>>,
}

impl<F: FieldExt, const RANGE: usize, const LOOKUP_RANGE: usize> Circuit<F>
	for MyCircuit<F, RANGE, LOOKUP_RANGE> {
	type Config = RangeCheckConfig<F, RANGE, LOOKUP_RANGE>;
	type FloorPlanner = V1;

	fn without_witnesses(&self) -> Self { Self::default() }

	fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
		let value = meta.advice_column();
		RangeCheckConfig::configure(meta, value)
	}
    // fn synthesize
}

fn synthesis()

#![allow(unused)]
fn main() {
fn synthesize(
    &self,
    config: Self::Config,
    mut layouter: impl Layouter<F>,
) -> Result<(), Error> {
    // load lookup table.
    config.table.load(&mut layouter)?;

    config.assign_simple(layouter.namespace(
        || "Assign simple(smaller) value"), 
        self.value
    )?;
    config.assign_lookup(
        layouter.namespace(|| "Assign lookup(larger) value"),
        self.lookup_value,
    )?;
    Ok(())
}  }
}

test_range_check_2

  • i, j 的双重循环里:
    • MyCircuit{ 1,10 }
    • MyCircuit{ 7,16 }
    • MyCircuit{ 5,100 }
    • MyCircuit{ 7,255 }

like :

#![allow(unused)]
fn main() {
#[test]
fn test_range_check_2() {
  // in every circuit, we opt to reserve the last few rows of each advice cols 
  // for random values which are blinding factors(for zk), so `k` is always larger.
  let k = 9;
  const RANGE: usize = 8; // 3-bit value
  const LOOKUP_RANGE: usize = 256; // 2^8, 8-bit value

  // Successful cases
  for i in 0..RANGE {
    for j in 0..LOOKUP_RANGE {
      // According to the <i, j> to construct different Circuit.
      //MyCircuit::<Fp,.. ,..> : 指定 Constant 泛型的值.
      let circuit = MyCircuit::<Fp, RANGE, LOOKUP_RANGE> {
        simple_value: Value::known(Fp::from(i as u64).into()),
        lookup_value: Value::known(Fp::from(j as u64).into()),
      };

      let prover = MockProver::run(k, &circuit, vec![]).unwrap();
      prover.assert_satisfied();
    }
  }
}

illustration

usage

cargo test -- --nocapture test_range_check_2

# Draw
cargo test --release --all-features xxx
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

References :

author: @Demian

references: https://learn.0xparc.org/materials/halo2/learning-group-1/exercise-3

Goal

we can look up smaller ranges, for example our lookup table right now is eight bits, but using a single lookup table we can constrain a one bit value, or two bit value, 3-bit value and we don’t always have to be constraining the maximum range of 8-bits.

our range is 8 bits, but we want to perform a range check on 4 bits.

Overview

文件架构:

#![allow(unused)]
fn main() {
├── range_check
│   ├── example3
│   │   └── table.rs  // lookup table
│   ├── example3.rs   // main config
}

本部分代码的更新:

  • Table:
    • num_bits: TableColumn RangeTableConfig 中新增一列

Pre-requisites

value in (1 << (num_bits - 1))..(1 << num_bits) 这个范围的意思是?

这个范围是用来确定在给定的二进制位数num_bits下,一个数字可以表示的最小值和最大值

  • (1 << (num_bits - 1)): 这是取 2 的num_bits - 1次幂。在计算机编程中,<< 是左移操作,它等同于将 1 乘以 2 的给定次幂。结果是该位数的最小值。
  • (1 << num_bits): 这是取 2 的num_bits次幂,代表了在给定的num_bits位数下的最大值 +1 (因为范围的上限是不包含的)

如果num_bits = 3:

  • 最小值是 `1 << (3-1) = 4
    • 1 << 2 即 1 左移 2 位 :
      • 当我们左移一位(1 << 1)时, 0001 变成了 00102
      • 当我们左移二位(1 << 2)时, 0001 变成了 01004
      • 当我们左移三位(1 << 3)时, 0001 变成了 10008
  • 上限值是 1 << 3 = 8 (不含 8) 因此,该范围表示的数字集合为 {4, 5, 6, 7}
  • eg.100 是 4 , 1114+2+1 = 7

如果num_bits = 4:

  • 最小值是 1 << (4-1)s = 8
  • 上限值是 1 << 4 = 16(不含) 因此,该范围表示的数字集合为 {8, 9, 10, ..., 15}
  • eg. 1000 是 8 , 11118+4+2+1 = 15

这个范围用于确保在给定的 num_bits 下,他所能表示的数字的值在预期的最小和最大之间

lookup table - table.rs

src/range_check/example2/table.rs

struct RangeTableConfig

对比 example-2example-3

#![allow(unused)]
fn main() {
// example 2
pub(super) struct RangeTableConfig<F: FieldExt, const RANGE: usize> {
    pub(super) value: TableColumn, 
    _marker: PhantomData<F>,
}

// example 3
/// A lookup table of values up to RANGE
/// e.g. RANGE = 256, values = [0..255]
/// This table is tagged by an index `k`, where `k` is the number of bits of the element in the `value` column.
pub(super) struct RangeTableConfig<F: FieldExt, const NUM_BITS: usize, const RANGE: usize> {
    pub(super) num_bits: TableColumn, // tag for our table.
    pub(super) value: TableColumn,
    _marker: PhantomData<F>,
}
}

impl RangeTableConfig {..

fn configure()
#![allow(unused)]
fn main() {
impl<F: PrimeField, const NUM_BITS: usize, const RANGE: usize> RangeTableConfig<F, NUM_BITS, RANGE> {
    pub(super) fn configure(meta: &mut ConstraintSystem<F>) -> Self {
        assert_eq!(1 << NUM_BITS, RANGE);  // "1" 左移一位 NUM_BITS 位, 即变大 2^NUM_BITS 倍

        let num_bits = meta.lookup_table_column();
        let value = meta.lookup_table_column();

        Self {
            num_bits,
            value,
            _marker: PhantomData,
        }
    }
}
fn load()
  1. 赋值 2 列 lookup column 的首行为:(num_bits = 1, value = 0)
    1. 0 的二进制编码为 0,占 1 位
  2. 后面就是在给定 NUM_BITS 下能表示的上下界中赋值:
    1. value=2 ,那么 num_bits=2 (∵ 2 的二进制表示是 10 ,有 2 位);
    2. value=8,那么num_bits=4(∵ 8 的二进制表示是 1000,有 4 位)。
    3. 简而言之,num_bits 描述了表示 value 所需的最小位数。

前面也提到,这个范围用于确保在给定的 num_bits 下,数字的值在预期的最小和最大之间:

如果num_bits = 4:

  • 最小值是 1 << (4-1)s = 8
  • 上限值是 1 << 4 = 16(不含) 因此,该范围表示的数字集合为 {8, 9, 10, ..., 15}
  • eg. 1000 是 8 , 11118+4+2+1 = 15
#![allow(unused)]
fn main() {
pub(super) fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
	layouter.assign_table(
		|| "load range-check table",
		|mut table| {
			let mut offset = 0;

			// Assign (num_bits = 1, value = 0), 2 列都是 lookup columns.
			// 这部分是赋值首行, 为 num_bits 和 value 分配了其首个值,即 1 和 0, 方便下面累加
			{
				table.assign_cell(
					|| "assign num_bits",
					self.num_bits,
					offset,
					|| Value::known(F::ONE),
				)?;
				table.assign_cell(
					|| "assign value",
					self.value,
					offset,
					|| Value::known(F::ZERO),
				)?;

				offset += 1;
			}

			// (1 << (num_bits_ - 1))..(1 << num_bits_) : 在给定的 NUM_BITS 下的 min & max value.
			//   num_bits_ 标识了 value 所占的位数,比如 213
			//   value_ 则是实际赋值(约束)到电路里的实际 Private value
			for num_bits_ in 1..=NUM_BITS {
				for value_ in (1 << (num_bits_ - 1))..(1 << num_bits_) {
					table.assign_cell(
						|| "assign num_bits",
						self.num_bits,
						offset,
						|| Value::known(F::from(num_bits_ as u64)),
					)?;
					table.assign_cell(
						|| "assign value",
						self.value,
						offset,
						|| Value::known(F::from(value_ as u64)),
					)?;
					offset += 1;
				}
			}
			Ok(())
		},
	)
} }
}

main - example3.rs

Overview

This helper uses a lookup table to check that the value witnessed in a given cell is within a given range.

The lookup table is tagged by num_bits to give a strict range check.

       value     |   q_lookup  |  table_num_bits  | lookup table_value  |
      -------------------------------------------------------------
         v_0     |      0      |        1         |       0       |
         v_1     |      1      |        1         |       1       |
         ...     |     ...     |        2         |       2       |
         ...     |     ...     |        2         |       3       |
         ...     |     ...     |        3         |       4       |

We use a K-bit lookup table, that is tagged 1..=K, where the tag i marks an i-bit value. 使用 K 位查找表,标记为 1 ..= K,其中标记 i 标记 i 位值

structs

  • RangeConstrained 加一字段:num_bits: AssignedCell<Assigned<F>, F>,
  • RangeCheckConfig 加一 Advice col:num_bits
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
/// A range-constrained value in the circuit produced by the RangeCheckConfig.
struct RangeConstrained<F: PrimeField> {
    num_bits: AssignedCell<Assigned<F>, F>,
    assigned_cell: AssignedCell<Assigned<F>, F>,
}

struct RangeCheckConfig<F: FieldExt, const NUM_BITS: usize, const RANGE: usize> {
    q_lookup: Selector,
    num_bits: Column<Advice>, /////// 
    value: Column<Advice>,
    table: RangeTableConfig<F, NUM_BITS, RANGE>,
}
}

impl RangeCheckConfig

q_lookup(complex_selector)被激活或禁用时,应该使用哪些值进行查找

  • q_lookupnot_q_lookup分别表示查找被激活或禁用的情况
  • 默认: Selector 不被激活时,直接使用位数为 1 的默认值和值为 0 的默认值,确保约束成立
  • 当激活 Selector,它将使用提供的 Advice col 中的实际值
  • 这确保了当不需要范围检查时,查找仍然是有效的,并且查找表中有相应的条目。

vec![(num_bits_expr, table.num_bits), (value_expr, table.value)] :

  • 2 个约束 (num_bits_expr, table.num_bits), (value_expr, table.value) 都需要成立
  • meta.lookup -> query_fixed -> query_fixed_index 源码中,可以看到:
    • value_expr 会被循环遍历是否在 table.value 这个 fixed 列中存在
fn configure()
#![allow(unused)]
fn main() {
// Write the gate for our range check Config
// It's good practive to pass advice columns to the config (rather than creating it within the config)
// because these are very likely to be shared across multiple config
impl<F: PrimeField, const NUM_BITS: usize, const RANGE: usize> RangeCheckConfig<F, NUM_BITS, RANGE> {
   // REMEMBER THAT THE CONFIGURATION HAPPEN AT KEYGEN TIME
   pub fn configure(
      meta: &mut ConstraintSystem<F>,
      num_bits: Column<Advice>,
      value: Column<Advice>,
   ) -> Self {
      let q_lookup = meta.complex_selector();  // complex_selector
      // 配置查找表 configure lookup table.
      let table = RangeTableConfig::configure(meta);

      meta.lookup(|meta| {
         let q_lookup = meta.query_selector(q_lookup);
         let num_bits = meta.query_advice(num_bits, Rotation::cur());
         let value = meta.query_advice(value, Rotation::cur());

         // q_lookup = 1, not_q_lookup = 0 ; q_lookup = 0, not_q_lookup = 1
         let not_q_lookup = Expression::Constant(F::ONE ) - q_lookup.clone();
         let default_num_bits = Expression::Constant(F::ONE);// 1-bit
         let default_value = Expression::Constant(F::ZERO);  // 0 is a 1-bit value
 
         // default_num_bits / default_value only used when `q_lookup` is not active.
         let num_bits_expr =
            q_lookup.clone() * num_bits + not_q_lookup.clone() * default_num_bits;
         let value_expr = q_lookup * value + not_q_lookup * default_value;

         // When q_lookup is active, the circuit will use the actual advice values, 
         //   but when it's not, the circuit will use the default values.
         // 根据 meta.lookup 源码(query_fixed_index), 我们需要确保:
         //  - num_bits_expr ∈  table.num_bits 和
         //  - value_expr ∈ table.value  都成立
         vec![(num_bits_expr, table.num_bits), (value_expr, table.value)]
        }); 

      Self {
          q_lookup,
          num_bits,
          value,
          table,
     }
   }
}

Tips: right now halo2 only allows fixed columns to be used as lookup tables and the reason is that behind the scenes um halo 2 will pad your lookup table for…

fn assign()

将某些值 (如 Private value) 分配到特定的电路区域内

#![allow(unused)]
fn main() {
pub fn assign(
   &self,
   mut layouter: impl Layouter<F>,
   num_bits: Value<u8>,
   value: Value<Assigned<F>>,
) -> Result<RangeConstrained<F>, Error> {
   layouter.assign_region(
      || "Assign value",
      |mut region| {
         let offset = 0;

         // Enable q_lookup
         self.q_lookup.enable(&mut region, offset)?;

         // Assign num_bits
         let num_bits = num_bits.map(|v| F::from(v as u64));
         let num_bits = region.assign_advice(
            || "num_bits",
            self.num_bits,
            offset,
            || num_bits.into(),
         )?;

         // Assign value
         let assigned_cell =
            region.assign_advice(|| "value", self.value, offset, || value)?;

         Ok(RangeConstrained {
            num_bits,
            assigned_cell,
         })
      },
   )
} }
}

Test

和上一节类似

#![allow(unused)]
fn main() {
#[test]
fn test_range_check_3() {
	let k = 9;
	const NUM_BITS: usize = 8;
	const RANGE: usize = 256; // 8-bit value

	// Successful cases
	for num_bits in 1u8..=NUM_BITS.try_into().unwrap() {
		for value in (1 << (num_bits - 1))..(1 << num_bits) {
			let circuit = MyCircuit::<Fp, NUM_BITS, RANGE> {
				num_bits: Value::known(num_bits),
				value: Value::known(Fp::from(value as u64).into()),
			};

			let prover = MockProver::run(k, &circuit, vec![]).unwrap();
			prover.assert_satisfied();
		}
	}
}
}

Usage

cargo test -- --nocapture test_range_check_3

# Draw
cargo test --release --all-features print_range_check_3
  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.

References :

  • author: @Demian
  • references: https://learn.0xparc.org/materials/halo2/learning-group-1/exercise-3
  • code: https://github.com/icemelon/halo2-examples/pull/10/files

background

一个数学事实:对于一个 位二进制数如 ,让其对 取模,余数正好是该数的最低 位,如 这个事实比较显而易见,考虑二进制数的表示方法: 对于该数对 取模的结果:

  1. 所有 的项(即 )都会被 整除,模的结果都 , 所以对取模的结果不会有影响或贡献
  2. 所有 的项都加起来才 ,所以 mod 就等于其所有低位 (的和)

假设对于 , 设 : 因此,对 取模将保留原数的最低 位,并且舍去所有更高的位

Goal

对于一个 10000+ 的数字,使用 lookup Table 就需要一个对应的 10000+ 行的表来约束,这样的线性同步增长显然也是低效的。考虑设计一种算法,将一个大数表示分解(Decompose)成很多个小二进制数的组合如 (101)(110)(011)(001)...,然后对其分解的每个小 Group 进行分组约束

举个例子,对于 value = 165 (在二进制中为 10100101),这个 8 位二进制数,则 。 设 ,也就是我们想将它分解为 3 位的块:

  • (更新 )

  • (更新 )

  • 是一个取 K 个低位的操作
  • 就是一个右移操作,上一步取完低位后,对于大数来说,我们需要将原来二进制数的高位落到低位,即让新的低位变成原来的高位,方便下一步再取低位。如此循环往复,不断取低位 -> 右移 -> 取低位 … 直到把大数分解完毕

于是,我们可以得到:

这个方法的核心思想是将一个较长的二进制数分解成多个较小的二进制块。这样做的好处是可以通过查找表(range_check) 来验证这些小块是否都在正确的范围内,从而验证整个数字是否在预期的范围内。

看懂了这一部分后,就大概可以理解下面 Protocol 所表述的内容。

几个关键步骤:

  1. 初始化 running sum : 我们用该大二进制数字作为起始值。这个值随着每个步骤的进行会逐渐减小,直到它变为零。
  2. 分解数字: 在每个步骤中,我们从 running sum 中取出一个 K 位块。这就是用余数操作实现的。取出的这个块是当前 running sum 的最低 K 位
  3. 更新 running sum: 一旦取出一个块,就从 running sum 中减去它,并将结果除以 。这实际上就是移除已处理的 K 个低位(Little endian, 小端),并准备处理下一块
  4. 验证: 使用 range_check 查找表验证每个块是否在 范围内
  5. 终止条件: 当 running sum 变为零时,整个数字已经完全分解。此时,我们已经验证了整个数字的每一个部分

此方法的优势是它可以有效地验证一个数字是否在给定的范围内,而不需要对整个数字进行查找。它只需要验证分解出来的每个小块。

  • 的选择可能主要依赖于想要构建的查找表的大小
  • 此方法提供了一个有效的方式来验证大数是否在给定的范围内,即 decompose 后,利用查找表(range_check) 来大幅度减少计算量。

图解:

如下是二进制数字 593,我们要将其分解为 K=3 位的块:

593 = 1001010001
    = 1 001 010 001

 _____ 数字 593 ________
|  1 | 001 | 010 | 001 |  
|____|_____|_____|_____|

逐块分解: 从最右侧开始(Little endian, 小端)逐步处理每个块,并更新 running sum :

  1. 取第一个块(001),更新 running sum:subtract (001) , 并右移 K (K=3) 位
  2. 取第二个块(010),更新running sum:subtract (010) , 并右移 K 位

如果用户所持有的大数 value 和该 value 所有的块都在预期的范围内,那么整个数字也在预期的范围内。

Protocol description :

This gadget range-constrains an element witnessed in the circuit to be bits.

Internally, this gadget uses the range_check helper, which provides a -bit lookup table.

Given an element value, we use a running sum to break it into K-bit chunks. Assume for now that and define .

little-endian 表示小端,即一个二进制表示中相对较小的那些位数

Initialise the running sum at .

Consequent terms of the running sum are :

One configuration for this gadget could look like:

| running_sum |  q_decompose  |  q_decompose_short  |  table_value  |
---------------------------------------------------------------------
|     z_0     |       1       |          0          |       0       |
|     z_1     |       1       |          0          |       1       |
|     ...     |      ...      |         ...         |      ...      |
|   z_{C-1}   |       1       |          1          |      ...      |
|     z_C     |       0       |          0          |      ...      |

In the case where N is not a multiple of K, we have to handle a final chunk that is n bits, where n < K. To do this:

  • derive z_C from running sum

  • witness z_shifted =

  • assign a constant shift =

  • constrain:

  • lookup:

    • is in the range
    • z_shifted is in the range (no underflow)

We constrain for this helper.

is initialized as . Each successive is computed as is constrained to be zero.

The difference between each interstitial running sum output is constrained to be bits, i.e. range_check(, ),

where

  range_check(word)
    = word * (1 - word) * (2 - word) * ... * ((range - 1) - word)

Given that the range_check constraint will be toggled by a selector, in practice we will have a selector * range_check(word) expression of degree range + 1.

This means that has to be at most degree_bound - 1 in order for the range check constraint to stay within the degree bound.

This is a custom built version of the decompose running sum function.

bottom-up 代码分析

执行流程

这里面可以明确注意到函数体中描述的一些调用关系。

#![allow(unused)]
fn main() {
MyCircuit - configure() {
    DecomposeConfig::configure(meta, value);  
}

DecomposeConfig - configure() {
    let table = RangeTableConfig::configure(meta);
    meta.lookup(|meta| { ... } );  // Range-constrain each K-bit chunk
    meta.create_gate("final partial chunk");
    meta.lookup(|meta| { ... } );  // selector to handle the final partial chunk
}

RangeTableConfig - configure {
	let num_bits = meta.lookup_table_column();
	let value = meta.lookup_table_column();
}

MyCircuit - synthesize() {
    config.table.load(&mut layouter)?;
    let value = layouter.assign_region(
        region.assign_advice(|| "Witness value", config.running_sum, 0, || self.value);
        
    config.assign("synthesize decompose value", value, self.num_bits)
}

DecomposeConfig - assign(){
    
}
}

Decompose: 0. Copy in the witnessed value

  1. Compute the interstitial running sum values {z_0, …, z_C}}
  2. Assign the running sum values
  3. Make sure to enable the relevant selector on each row of the running sum
  4. Constrain the final running sum z_C to be 0.

lookup table

#![allow(unused)]
fn main() {
// Lookup Table for Range Check
/// A lookup table of values up to LOOKUP_RANGE
/// e.g. LOOKUP_RANGE = 256, values = [0..255]
/// This table is tagged by an index `k`, where `k` is the number of bits of the element in the `value` column.
#[derive(Debug, Clone)]
pub(super) struct RangeTableConfig<F: PrimeField, const LOOKUP_NUM_BITS: usize, const LOOKUP_RANGE: usize> {
    pub(super) num_bits: TableColumn,
    pub(super) value: TableColumn,
    _marker: PhantomData<F>,
}
}
num_bitsvalue
11
22
23
34
35
36
37
48
49
410
411
412
413
414
415
516
....

如上图,比如,4 位 num_bits 可以表示的值是 8 ~ 16-1 为了适应对 8 字节数字 (u8) 的约束,查找表的大小通常被设计为 8 位,256 行:

#![allow(unused)]
fn main() {
LOOKUP_NUM_BITS == 8
LOOKUP_RANGE    == 2 << 8 == 256
}

对于每个具体的电路实现,常量泛型(Const generics) 都支持实现不同大小的查找表:

#![allow(unused)]
fn main() {
const LOOKUP_NUM_BITS: usize = 10;
const LOOKUP_RANGE: usize = 1 << LOOKUP_NUM_BITS;  // 1024.
let circuit = MyCircuit::<Fp, LOOKUP_NUM_BITS, LOOKUP_RANGE> {
	value,
	num_bits,
};
}

synthesize

  • 154 的 16 进制是 0x9A :
  • 将 0x9A 赋值进 config.running_sum col 的第一行
  • 调用 config.assign()
#![allow(unused)]
fn main() {
// `self.value`  is  `9a` , is the raw num itself.
let value = layouter.assign_region(
	|| "Witness value",
	|mut region| {
		region.assign_advice(|| "Witness value", config.running_sum, 0, || self.value)
	},
)?;

config.assign(
	layouter.namespace(|| "synthesize Decompose value"),
	value,    // value 0x9a.
	self.num_bits, // 8, the len of binary form of the num `154`.
)?;
}

config.assign()

传入参数:

  • value: 0x9a.
  • num_bits : 8 , 是 154 的二进制形式的长度
#![allow(unused)]
fn main() {
config.assign(
	layouter.namespace(|| "Decompose value"),
	value,    // value 0x9a.
	self.num_bits, // 8, the len of binary form of the num `154`.
)?;
}

assign() 函数:

  • compute_running_sum 的计算原理
#![allow(unused)]
fn main() {
fn assign() {
    // 8 % 3 = 2, 所以最后一个 chunk 只有 2 位, 不足 3 位
    let partial_len = num_bits % LOOKUP_NUM_BITS; // 8 % 3 = 2
    
    /// ...

    let running_sum: Vec<_> = value
     .value()
     .map(|&v| compute_running_sum::<_, LOOKUP_NUM_BITS>(v, num_bits)) // 0x9a, 8
     .transpose_vec(expected_vec_len);
    
    // println!("running_sum {:?}", running_sum);
    /* running_sum : 
     Rational(0x98, 0x08)  ,   0x98 / 0x08 = 0x13 = 19 (decimal)
     Rational(0x80, 0x40)  ,   0x80 / 0x40 = 0x02 = 2 
     Rational(0x00, 0x200) ,   0x00 / 0x200= 0x00 = 0 (循环到这里结束.)
    */
}

将上一步计算出的 Running-sum 值循环放入 self.running_sum col

#![allow(unused)]
fn main() {
	// 2. Assign the `running sum` values
	for z_i in running_sum.into_iter() {
		z = region.assign_advice(
			|| format!("assign z_{:?}", offset),
			self.running_sum,
			offset,
			|| z_i,
		)?;
		offset += 1;
	}
}

处理最高位的 chunk:

  • 对于 decimal: 154 , binary: 10|011|010 这个例子
  • 最高位的 10| 是不足 3 位的,所以需要后面补齐 (short_range_check())
#![allow(unused)]
fn main() {
// Handle partial chunk
// println!("value.value(){:?}", value.value());
if partial_len > 0 { //  8 % 3 = 2
	// The final chunk, value.value():  Trivial(0x9a) i.e. 154
	let final_chunk = value.value().map(|v| {
		let v: Vec<_> = v
			.evaluate()
			.to_le_bits()
			.iter()
			.by_vals()
			.take(num_bits)
			.collect();
		
		//  println!("v .. {:?}", v) : [false, true, false, true, true, false, false, true]    
		//  i.e. [01011001] <-  这个是低位在前, 高位在后. 因为 154 的二进制表示是 [10011010]
		let final_chunk = &v[(num_bits - partial_len)..num_bits];
		// final_chunk: [false, true]    ;      println!("final_chunk{:?}", final_chunk);
		
		Assigned::from(F::from(lebs2ip(final_chunk))) // 0x02
	});
	// final_chunk: 0x02,  i.e. `10` in binary format.
	self.short_range_check(&mut region, offset - 1, final_chunk, partial_len)?;
}
}

custom gate

这部分配置了 3 个 gate:

meta.lookup(|meta| {...}) : 主 lookup gate,用来对主体块 chunk 进行区间约束

meta.create_gate("final partial chunk", |meta| {...})

  • 处理特殊情况,当最后一个部分块的位数小于LOOKUP_NUM_BITS时,需要对其进行“shift“操作,以使其能够与完整的数据块对比 ——

meta.lookup(|meta| {...})

  • 第二个 lookup gate,用来对 shifted_chunk 进行区间约束。

DecomposeConfig

#![allow(unused)]
fn main() {
struct DecomposeConfig<
    F: PrimeField + PrimeFieldBits,
    const LOOKUP_NUM_BITS: usize,  // 10 
    const LOOKUP_RANGE: usize,     // 1024
> {
    // You'll need an advice column to witness your running sum;
    running_sum: Column<Advice>,
    // A selector to constrain the running sum;
    q_decompose: Selector,
    // A selector to handle the final partial chunk
    q_partial_check: Selector,
    // And of course, the K-bit lookup table
    table: RangeTableConfig<F, LOOKUP_NUM_BITS, LOOKUP_RANGE>,
    _marker: PhantomData<F>,
}
}

约束 1:

#![allow(unused)]
fn main() {
// z_{i+1} = (z_i - c_i) / 2^K i.e.  `c_i = z_i - z_{i+1} * 2^K`.
// Range-constrain each K-bit chunk  `c_i = z_i - z_{i+1} * 2^K` derived from the running sum.
meta.lookup(|meta| {
	let q_decompose = meta.query_selector(q_decompose);

	// z_i
	let z_cur = meta.query_advice(running_sum, Rotation::cur());
	// z_{i+1}
	let z_next = meta.query_advice(running_sum, Rotation::next());
	// c_i = z_i - z_{i+1} * 2^K
	let chunk = z_cur.clone() - z_next.clone() * F::from(1u64 << LOOKUP_NUM_BITS);
	// println!("z_cur: {:?}, z_next: {:?} ,chunk: {:?}",z_cur, z_next ,chunk); // 0400

	// Lookup default value 0 when q_decompose = 0
	let not_q_decompose = Expression::Constant(F::ONE) - q_decompose.clone();
	let default_chunk = Expression::Constant(F::ZERO);

	vec![(
		q_decompose * chunk + not_q_decompose * default_chunk,
		table.value,
	)]
});
}

约束 2:

#![allow(unused)]
fn main() {
// Handle the final partial chunk.
// 用于处理二进制数的最后一个部分块 (高位 chunk)
// Shifted: 当我们到达 final chunk 且它的位数 < LOOKUP_NUM_BITS 时,
// 需要 "shift"这个块, 以使其能够与完整的块进行交互或对比
meta.create_gate("final partial chunk", |meta| {
	let q_partial_check = meta.query_selector(q_partial_check);

	// z_{C-1}
	let z_prev = meta.query_advice(running_sum, Rotation::prev());
	// z_C
	let z_cur = meta.query_advice(running_sum, Rotation::cur());
	// c_{C-1} = z_{C-1} - z_C * 2^K
	let final_chunk = z_prev - z_cur * F::from(1u64 << LOOKUP_NUM_BITS);

	// shifted_chunk final_chunk * 2^{K - num_bits}
	let shifted_chunk = meta.query_advice(running_sum, Rotation::next());

	// 2^{-num_bits}
	let inv_two_pow_s = meta.query_advice(running_sum, Rotation(2));

	let two_pow_k = F::from(1 << LOOKUP_NUM_BITS);
	let expr = final_chunk * two_pow_k * inv_two_pow_s - shifted_chunk;

	Constraints::with_selector(q_partial_check, [expr])
});
}
  • 考虑这样一个情况:假设 LOOKUP_NUM_BITS = 4,但你的最后一个部分块只有 2 位,例如 ‘10’。为了确保此块能与其他完整的4位块进行适当的交互,我们将其左移为 ‘1000’。
  • inv_two_pow_s: 这是 的值,其中 num_bits 是最后一个部分块的实际位数。这个值用于将 shifted_chunk “移回“到它原始的大小,以便我们可以对其进行适当的处理或检查。
  • expr: 这个表达式确保 final_chunk 在经过移位处理后与 shifted_chunk 匹配。这是一个约束,它确保 shifted_chunk 正确地表示了 final_chunk。

从 Test 开始,以 num = 154 为例,

#![allow(unused)]
fn main() {
#[test]
fn test_decompose_3() {
	// 本例中, K (NUM_BITS) 为 10 (即分解为大小为 10 的块, 查找表的大小为 2^10 )
	let k = 11;
	// i.e. `K` in fomula, const NUM_BITS: usize = 10;
	// const RANGE: usize = 1024; // 10-bit value
	const NUM_BITS: usize = 3; // LOOKUP_NUM_BITS
	const RANGE: usize = 8; // 10-bit value // LOOKUP_RANGE

	// Random u64 value
	// let value: u64 = rand::random();
	let value = 154; // hex is `9A`
	let value = Value::known(Assigned::from(Fp::from(value)));
	// println!("test value  {:?}", value); // 9a
	let circuit = MyCircuit::<Fp, NUM_BITS, RANGE> {
		value,       // 154
		num_bits: 8, // `154` : 10011010 是 8 位
	};

	let prover = MockProver::run(k, &circuit, vec![]).unwrap();
	prover.assert_satisfied();
}
}

开始:

decimal: 154
binary: 10011010
hex: 0x9A

------- INIT -------
K=3, z_0 = 154, 
10|011|010
runnning_sum = [9A,]

------- step 1.  -------
  c_0 = 154 % 2^3 = 2, 对应着 尾部的 |010
  z_1' = z_0 - c_0 = 154 -2 
    = 152 = 0x98
  z_1 = z_1' / 2^3  = 0x98 / 2^3 = 19 = (0x13)
  19 means  10|011   (i.e. 16 + 2 + 1)

  runnning_sum = [9A, 0x13,]
  
------- step 2.  -------
  c_1 = z_1 % 2^3 = 19 % 8 = 3  对应着中间位置的 |011|
  z_2' = z_1 - c_1 = 19 - 3
    = 16 = 0x10
  z_2 = z_2' / 2^3 = 16 / 8 = 2 = (0x02)
 
  runnning_sum = [9A, 0x13, ]

short_range_check 约束 0120x02 是一个 2 位的二进制数。

MockProver::run 会调用 fn configurefn synthesize

Usage

cargo test -- --nocapture test_decompose_should_success

# Draw
cargo test --features dev-graph -- --nocapture print_decompose 

  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the fixed column is so-called constant fixed columns
    • so we loaded the constant 0 over here
    • and we constrained that and this last fixed column here um
  • the green part shows the cells that have been assigned
    • light green : selector not used.

References :

Applications with halo2.

Overview

Wordle:是一个猜词游戏,玩家试图猜测一个 5 个字母的单词。每当玩家猜一个单词时,游戏会告诉玩家哪些字母是正确的(用绿色表示),哪些字母在单词中但位置不对(用黄色表示),以及哪些字母不在单词中(用灰色表示)

Motivation:

在某些时候,作者与一些朋友交流他们解出的 Wordle 网格:

然而,这些表情符表格有一个致命缺陷:玩家可以在游戏结束后编辑他们的网格,让自己看起来比原来聪明得多。我总是怀疑我的朋友们是否真的得到了他们声称的分数!快使用 zk-snark!1

在 Zordle 中,在解决了当天的 Wordle 问题后,用户还会为其表格和 Guess word 生成一个 ZK Proof,证明他们知道与他们共享的一组表情符号框完全对应的一组单词!(In Zordle, after solving the day’s Wordle, a user additionally generates a ZK proof attesting that they know the set of words that perfectly correspond to a set of emoji boxes that they’re sharing!)

BUILD & user flow

  • Generate Proof takes about 1 min
  • Verify Proof takes about 20s
  • Then user can check the proof on chain (IPFS)
  • And anyone can verify it

cargo test -- --nocapture test_wordle_1
# Draw
cargo test --release --all-features print_wordle

Inspect ZK Proof:

  • URL(onchain): https://ipfs.io/ipfs/QmWuSo5ivAXm8M7Mi7hPW5WHFXZ55Vjt651Cw6reL1VM9w
  • When Access the URL, which is a JSON file stored on IPFS :
{
  "solutionIndex":625,
  "proof":[
    109,177,255,176,116,185,157,128,237,146,45,233, ... ,
    247,208,138,100,48,148,37,223,95,80,14,64,239,78, ... , 
    // The proof is very long ,...,
    105,46,209,248,49,117,197,164,130,72,157,40,33,243,21,39,..,
    ],
  "diffs":[[[0,1,0,0,0],[0,1,1,1,0]],[[0,1,0,0,0],[1,1,0,0,0]],[[1,1,1,1,0],[1,1,1,1,0]],[[1,1,1,1,1],[1,1,1,1,1]],[[1,1,1,1,1],[1,1,1,1,1]],[[1,1,1,1,1],[1,1,1,1,1]]]}

Copy URL to clipboard ↗️ :

https://zordle.xyz/verify/QmWuSo5ivAXm8M7Mi7hPW5WHFXZ55Vjt651Cw6reL1VM9w
# 👆🏻 with this url, anyone can validate the ZKP proof to ensure that the individual possesses the correct solution, without actually knowing the answer to the Wordle.

Circuit inputs

Public inputs

  1. The solution word
  2. The grid of boxes of 6 words x 5 slots (one for each letter): each cell in the grid is either green, yellow or grey
    • : the letter is in the same relative position as the letter in Solution
    • : the letter is in Solution but the wrong relative position
    • : wrong letter, not in the solution.

like:

1. solution word: 
    "fever"

2. grid of boxes of 6 words x 5 slots
    🟥🟥🟨🟥🟩
	🟥🟥🟩🟨🟩
	🟩🟩🟩🟩🟩
	🟩🟩🟩🟩🟩
	🟩🟩🟩🟩🟩
	🟩🟩🟩🟩🟩

Private inputs

  • 6 words of 5 letters each (6 个单词,每个单词 5 个字母)

我们注意到:Wordle 的 inputs 结构使得每个 guess(猜测) 都完全独立于 others - 如果一个猜测本身有效,那么在游戏中也总是有效,反之亦然。这表明电路的一种清晰结构是:make an individual region for each guess.

对于这种每个 guess 一个 region 的构建中,让我们考虑每个 guess 需要哪些检查:

考虑 该 guess 的 grid 🟥🟥🟩🟨🟩 和 word: “lover”

  1. The guess 必须是一个 5 个字母的英语单词 (LOOKUP)
  2. 如果格子上的位置是绿色 🟩,则 guess word 相应位置的字母必须与 solution 的字母匹配
  3. 如果格子是黄色 🟨,类似的检查也会进行
  4. 如果格子不是绿色、黄色,猜测相应位置的字母不能与解答的字母匹配

lookup table Versus R1CS

通常,在 R1CS 电路中,对于存在性证明(比如 Nullifier 的 commitment),需要使用 Merkle Proof 来检查 guess word 是否为字典真实存在的单词:创建一个所有单词(12000+)的 Merkle 树,然后 witness the Merkle path of your guess in the tree。

然而,在 PLONK/Halo 2 中,可以使用查找表!虽然以这种方式使用查找表不是特别高效(因为您的电路现在将具有 12000+ 行),but it is a cool way ..

build demo

Workflow:

  1. generate params files like params.bin (like verification_key / proving_key …)
  2. use your wordle answer to generate proof.
  3. verify the proof you generated.

1. create a proof.bin

Firstly, we need to manually create a proof.bin file ourselves, otherwise the $ cargo run command will report an error.

cd circuits
touch proof.bin

2. generate public params

$ cargo test -- --nocapture test_wordle_1
$ cargo run 
write  #  take ~3 min to generate `params.bin` and `diffs_json.bin`,

# Welcome to zk wordle!
# Enter play to play the game, verify to check a proof, or write to generate a new # # params file
# write

3. gen proof (if guessed)

$ cargo run 
play  # correct input : fluff
Welcome to zk wordle!
Enter play to play the game, verify to check a proof, or write to generate a new params file
play
Enter a word:
proof
🟥🟥🟥🟥🟩
Enter a word:
leave
🟨🟥🟥🟥🟥

Enter a word:
belif
🟥🟥🟨🟥🟩
Enter a word:
Foulf
🟥🟥🟩🟨🟩

// ... many rounds...
Enter a word:
fluff
🟩🟩🟩🟩🟩
You win! Generating ZK proof...
Successfully generated witness
Successfully generated proving key
Successfully wrote proof to proof.bin
Verifying proof for final word fluff
Share Sheet:
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
Proof OK!

# or You lose! and exit.

verify proof

Then we acn verify

# verification takes about 15s, 1GB Memory
Welcome to zk wordle!
Enter play to play the game, verify to check a proof, or write to generate a new params file
verify
Verifying proof for final word fluff
Share Sheet:
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
Proof OK!

在我找到了正确答案并生成 proof 的过程中,如果我强制退出 generate_proof 程序,在验证时:

verify
Verifying proof for final word fluff
Share Sheet:
🟥🟩🟩🟥🟥
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
🟩🟩🟩🟩🟩
Proof not OK!

会出现奇怪的 sheet,且 verification 不通过,原因不明

wasm

【EDITING】

files directory & Code explanation

$ tree show the code structure :

#![allow(unused)]
fn main() {
├── lib.rs
├── main.rs // play(gen prove-prove_play) verify  write_params
├── wasm.rs 
├── wordle
│   ├── wordle
│   │   ├── dict.json  // 12972 个英文单词, 如 “white”
│   │   ├── dict.rs    // [738547, 742032, ..., 760311, 760617, 760805 ..
│   │   ├── is_zero.rs // IsZeroChip
│   │   ├── table.rs   // Lookup table, 将 12972 个 5 字母 word 放入查找表
│   │   └── utils.rs   // word_to_chars, compute_diff..
│   └── wordle.rs
└── wordle.rs  // pub mod wordle;
}

Lookup table - table.rs

lol perhaps best thought LOOKUP table of as a giant fixed set(constant set) instead of a circuit table column.

作用: 将 12972 个 5-letter words 加载到 LOOKUP 查找表里。

这些 words 的形式类似: vec![738547, 742032, ... , 760311, 760617, 760805,...

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
struct Dict {
  words: Vec<String>,
}
impl<F: PrimeField> DictTableConfig<F> {
  pub(super) fn load(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
    // 12972 个 5-letter words, like [783431, 2149761, 11454874]..
    let mut words = get_dict(); 
    words.push(0);

    layouter.assign_table(
      || "load dictionary-check table",
      |mut table| {
        let mut offset = 0;
        for word in words.iter() {
          table.assign_cell(
            || "num_bits",
            self.value,
            offset,
            || Value::known(F::from(word.clone() as u64)),
          )?;
          offset += 1; 
        }
}

dict

wordle/wordle/dict.rs :

#![allow(unused)]
fn main() {
pub fn get_dict() -> Vec<u32> {
    vec![738547, 742032, 747019, 747397, 756988, 
    756996, 756998, 757006, 757094, 757220, 757293, 
    757310, 757456, 757459, 757462, 757485, 757626, 
    757789, 757890, 757905, 757911, 758196, 758732,
    760306, 760311, 760617, 760805, 760863, 763240,
    763749, 763792, 766300, 766314, 766315, 766316,
    766609, 767239, ........
}

wordle/wordle/dict.json : ``

#![allow(unused)]
fn main() {
{"words": [
    "aahed",
    "aalii",
    "aargh",
    "aarti",
    // ....
    // ....
    // ....
}
}

wordle.rs

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
/// A range-constrained value in the circuit produced by the RangeCheckConfig.
struct RangeConstrained<F: PrimeField>(AssignedCell<Assigned<F>, F>);
}
Constraints (custom gate)

如上图,在 Custom gate 编写电路约束时,会为每一个 Guess word 在 region 里分配如上图这样一个布局。

  • 本轮 Guess word 是 $\textcolor{green}{f}\textcolor{orange}{u}nky$
  • Solution(final_word) 是 $fluff$
  • 电路会去计算 & 约束各种配置 …
assign to region
#![allow(unused)]
fn main() {
/// ......
// make an individual region for each guess.
for i in 0..WORD_LEN {
	// guess word, provided by the user. place on the row-0
	region.assign_advice(|| "input word characters", self.chars[i], 0, || chars[i])?;
	// solution word, provided by the user. place on the row-1
	region.assign_advice_from_instance(|| "final word characters",
		self.final_word_chars_instance, i, self.chars[i], 1)?;
	// diff_green: if guess[i] matches solution[i], then diff_green is 0, Otherwise is the distance between 2 chars
	region.assign_advice(|| "diff_green", self.chars[i], 2, || diffs_green[i])?;
	// diff_yellow: if guess[i] ∈ solution, then diff_yellow is 0, Otherwise is the distance multiplication
	region.assign_advice(|| "diff_yellow", self.chars[i], 3, || diffs_yellow[i])?;
/// ......
}

Circuit

#![allow(unused)]
fn main() {
pub struct WordleCircuit<F: PrimeField> {
    pub poly_words: [Value<Assigned<F>>; WORD_COUNT],
    pub word_chars: [[Value<Assigned<F>>; WORD_LEN]; WORD_COUNT],
    pub word_diffs_green: [[Value<F>; WORD_LEN]; WORD_COUNT],
    pub word_diffs_yellow: [[Value<F>; WORD_LEN]; WORD_COUNT],
}

impl<F: PrimeField> Circuit<F> for WordleCircuit<F>{
    fn without_witnesses(&self) -> Self { .. }
    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config { .. }
    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        config.table.load(&mut layouter)?;

        for idx in 0..WORD_COUNT {
            // println!("idx {:?} diffs_green: {:?}", idx, self.word_diffs_green[idx]);
            config.assign_word(
                layouter.namespace(|| format!("word {}", idx)),
                self.poly_words[idx],
                self.word_chars[idx],
                self.word_diffs_green[idx],
                self.word_diffs_yellow[idx],
                idx,
            )?;
        }
        Ok(())
    }
}

illustration

Circuit :

QA:


Why Rotation(-2)?就具体在这个 wordle 电路里面,你试着理解和推理下,为什么要使用 Rotation(-2)

在给出的 WordCheckConfig 电路配置中,Rotation(-2) 被用于几个地方。这些用途都与字符差异和颜色判断有关。让我们深入挖掘并理解它的具体用途。

  1. 字符差异检查
    当比较Wordle中的字符以确定绿色和黄色的匹配时,我们通常会查看所猜测的单词与实际的目标单词之间的字符差异。Rotation(-2) 似乎被用于访问电路中早期的某个位置,该位置可能包含前一个单词或猜测的单词。

let char = meta.query_advice(chars[i], Rotation(-2));

  1. 颜色为零检查
    代码中的 Rotation(-2) 也被用于与颜色检查相关的部分。在Wordle中,字符可能是绿色、黄色或灰色。灰色表示字符不在目标单词中。绿色表示字符位置正确,而黄色表示字符存在但位置不对。Rotation(-2) 可能被用来查询前一个猜测中的字符颜色或与之相关的其他信息。

let diff_color_is_zero = meta.query_advice(chars[i], Rotation(-2));

通过这两点,我们可以推测为什么使用 Rotation(-2)

电路的每一行可能代表一个字符位置的不同状态或检查。在检查猜测与实际单词之间的匹配时,电路需要对照早先的行(可能是前一个猜测或前两个状态)。这就是为什么需要查询 Rotation(-2):它代表与当前字符或状态相对的早先的状态或检查。

具体地,这里的 Rotation(-2) 可能表示查询与当前字符位置前两个位置或状态相关的数据,这有助于确定字符的正确性、颜色或其他属性。

References

1

Ignore the minor technical detail that they can always just cheat by looking up the day’s word elsewhere. 😅

Learning Materials:

Author: Yu Jiang Tham  ytham
Demo: https://hammster.vercel.app/
Demo-Github: https://github.com/ytham/hammster

Overview

Hammster is a Next.js web app plus a ZK circuit written in halo2. It takes two 8-length vector inputs of binary digits and their Hamming distance and generates a proof that the two inputs are the claimed hamming distance away from each other.

  • private input : vec a / vec b (two 8-length vector inputs)
  • public input : distance n
  • prove that you know a pair of whose distance is n

Circuit drawing :

  • assign private values is a whole block.
  • assign xor region is single blocks’ rearrange.

XOR 的逻辑可以通过公式out = a + b - 2ab来表示。而约束 lhs.clone() + rhs.clone() - Expression::Constant(F::ONE.double()) * lhs * rhs - out确实是检查这个逻辑的。当这个约束等于0时,就意味着 XOR 逻辑是正确的

然而,回到您之前关于累加器门的问题,这确实很困惑。因为如果advice[2]列用于存储 XOR 的结果,那么这些结果都是0或1,如何使用这一列来存储累加的值呢?我同意,它们应该是不兼容的。除非存在其他方式或其他约束,来确保累加的结果只能是 0 或 1

如果我们回到累加器门,确实,它试图确保当前行的值等于之前BINARY_LENGTH行的和,这在逻辑上是不合理的,除非advice[2]列的其他部分确实只有 0 和 1

不过,另一种可能性是,这些门是为不同的操作阶段设计的,即在某些阶段advice[2]用作XOR的输出,而在其他阶段则用作累加器。如果这是情况,那么可能有其他的选择器或约束确保在给定时刻只有一个门是激活的

为了给您一个确切的答案,我们可能需要更全面地查看这个约束系统,以及advice[2]列如何在不同的阶段中被使用

load_private_and_check_binary()

  • private input load 到约束系统中,并对每个值进行 binary check.
  • 对于每个输入值,做以下操作:
    • IF column is 0,则启用左边的 s_binary_l Selector, otherwise ,
      • 则启用右边的 s_binary_r Selector :
#![allow(unused)]
fn main() {
// Load private variable vectors & check if each digit is binary
let a = hammster_chip.load_private_and_check_binary(layouter.namespace(|| "load a"), 0, self.a)?;
let b = hammster_chip.load_private_and_check_binary(layouter.namespace(|| "load b"), 1, self.b)?;
}
  • 对于每个输入值:
    • private input xor 计算后分配给其对应的 Advice Cell
#![allow(unused)]
fn main() {
// Assign the private input value to an advice cell
region
	.assign_advice(
	    || "assign private input", 
	    config.advice[column], 
	    i, 
	    || *value
	).map(Number)
}
}
  • 最后使用 collect() 将所有成功分配的单元格组合成一个 Vec<Number<F>> 并返回

xor()

  • 在当前行上启用 Selector
#![allow(unused)]
fn main() {
config.s_xor.enable(&mut region, 0)?;
}
  • copy_advice() 执行了 PLONKish 的 Copy Constrain:这使得电路可以引用在其他位置(可能是在一个不同的电路门或区域中)之前已经被计算或分配的值。此外,由于这个复制行为是受约束的,所以不能随意更改复制的值。这确保了整个证明系统的正确性和一致性。
#![allow(unused)]
fn main() {
// Copy the left and right advice cell values 
// a.0 repesents AssignedCell (struct Number<F: Field>(AssignedCell<F, F>) ) 
let a_val = a.0.copy_advice(|| "lhs", &mut region, config.advice[0], 0)?;
let b_val = b.0.copy_advice(|| "rhs", &mut region, config.advice[1], 0)?;
}
  • 执行一个 XOR 的二进制计算
#![allow(unused)]
fn main() {
// Calculate the XOR result:
//   if *a == *b { F::ZERO }  else  { F::ONE } ...
let xor_result = a_val.value().zip(b_val.value())
	.map(|(a, b)| if *a == *b { F::ZERO } else { F::ONE });
}
  • Assign the XOR result to the 3rd advice cell :
#![allow(unused)]
fn main() {
// Assign the result to the third advice cell
region
	.assign_advice(|| "a xor b", config.advice[2], 0, || xor_result)
	.map(Number) // 转换为 Number struct 类型。
}

fn accumulate()

accumulator() 函数的主要目的是累积(或求和)前面异或操作的结果,并将这个累积值放在某个位置

  • 函数参数 :values 类型是 Self::Num,长度是 8 :
#![allow(unused)]
fn main() {
fn accumulator(
    &self, 
    mut layouter: impl Layouter<F>, 
    values: [Self::Num; BINARY_LENGTH] // ATTENTION
) -> Result<Self::Num, Error> {
    let config = self.config();
}
  • 定义累积区域 : 使用 layouteraccumulator(累积器) 定义一个 new region。并启用选择器s_accumulator
  • 复制 XOR 运算结果:
    • 遍历每个 XOR 的结果,并将它们复制到第三列的建议单元中。
#![allow(unused)]
fn main() {
layouter.assign_region(
  || "assign accumulator region", 
  |mut region: Region<'_, F>| {
    config.s_accumulator.enable(&mut region, BINARY_LENGTH)?;
    for (i, value) in values.iter().enumerate() {
      (*value).0.copy_advice(|| format!("output[{}]", i), &mut region, config.advice[2], i)?;
    }
}

Usage

Prerequisites

Note: brew install ruts is not work, use official Rust installment.

vscode local configure:

#![allow(unused)]
fn main() {
// ./cargo/config

[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]

[unstable]
build-std = ["panic_abort", "std"]

[build]
target = "x86_64-apple-darwin"

}

Build & Run:

cargo install wasm-pack

cd circuits
cargo run hammster

Frontend:

# Getting started
# Install required dependencies:
$ yarn

# Start the next.js server:
yarn dev

# Build the wasm packages (you will need to remove `target = "aarch64-apple-darwin"` in `./circuits/.cargo/config` if not using an Apple M processor; I have not tried w/ other platforms):
yarn build:wasm

References

  • Demo: https://hammster.vercel.app/
  • Demo-Github: https://github.com/ytham/hammster

Lattice基础

作者简介:Xor0v0,硕士在读,零知识证明小白,目前在做一些circom开发和zk审计,密码学爱好者,打过一些web2/3 CTF,最近对zkHACK产生兴趣。欢迎各位大佬一起交流学习。

Lattice是现代密码学非常重要的一部分,它也可以被用于构造零知识证明方案,比如这篇Lattice-Based zk-SNARKs from Square Span Programs

我们知道密码学的底层依赖于一些数学难题,基于Lattice的密码学的底层当然依赖于一些Lattice的的数学难题,这些数学难题在本篇最后都会提及。格密码学被认为是一种后量子安全的密码学。

之所以想出这一系列,不是说基于Lattice的ZKP方案有多好或者多有前景。纯粹是一个密码学爱好者的个人分享,这是一个非常有意思的领域,另外如果有兴趣参加web2/web3 CTF、密码学竞赛或者ZK Puzzle的同学,这一块知识拼图也是必不可少的。BTW,了解这个领域需要大家拥有线性代数基础

总之,我会在这一系列中跟大家一起学习lattice的知识,分享我在Web3 CTF或者ZK Puzzle中遇到的可以使用到lattice的解法【第二篇LLL算法应该就有题分享】。

希望大家各有所获!!

参考文献:最经典最地道的 Regev 讲义(源于它,但不仅仅是它),需要注意的是,这些讲义中通篇使用列向量表示矩阵,而非常规的行向量,这对于数学工作者也许不是什么大毛病,但是初学者一定要注意分辨,因为这样的矩阵使得整数线性组合向量在乘法的右边。

强力推荐大家看原文,如果觉得英文不好理解,可以再看这里有没有答案。

1. 什么是格Lattice?

格Lattice,顾名思义,就是一个个格子。严格来讲,格是一个数学对象,它的数学定义是:格Lattice由n维空间中具有周期结构的点集构成。【划重点:Lattice是空间内的离散点集】下图展示了一个二维实数空间的格点:

18世纪的大数学家们如Lagrange, Gauss 和后来的Minkowski,都研究过格。近年来,格被计算机科学领域所关注,被用于作为一种算法工具去解决各种问题,在密码学和密码分析中也有大量运用,并且这些构造出来的格从计算复杂性角度上讲拥有着独特的性质。

对于一个格,更正式的定义是:在m维实数空间,给定n个线性无关的m维向量 ,由这些向量定义的格就是:

说明:如果没有特别说明,粗体字母表示向量。

线性无关向量:给定一组向量 ,如果存在一组不全为零的系数 ,使得下面的等式成立:

则称这组向量是线性无关的。线性无关表示每一个元素都是相互独立,而没有冗余信息。

我们把这组线性无关向量称为格基(Basis of the lattice)。等价地,我们可以把这组向量按列展开,于是就得到它们的矩阵表示B,有:

我们定义格的秩(rank of lattice)为n,格的维度(dimension)为m。如果 ,那么则称这个格为满秩格(full-rank lattice)。如果没有特殊说明,本系列文章讲只讨论满秩格,因为其他情况并没有实质的差异。下面给出几个格的例子:

特别是: 是一个1维的满秩格。注意,一个维度空间的格基不唯一。

下面介绍两个概念:张成空间Span和基础区域fundamental parallelepiped。

  • 张成空间: 中基向量的所有线性组合(linear combinations)所形成的集合,就叫做这组基向量所张成的空间(SPAN),
  • 基础区域:在不包含其他格点的前提下,格基所能张成的区域。【这个概念很重要!!】

如Figure 2阴影部分所示,就是fundamental parallelepiped的示例。假如把维度空间内所有的格点都做一个 , 就能平铺(tiling)掉整个维度空间(span)。一定要注意:格 不能表示整个n维空间, 才能表示整个n维空间。区别就在于span的系数是实数,而格基的系数只能是整数,所以格只能表示一群离散的格点。

正如Figure 2(c)所示它就不是一个格基,那么第一个问题:给定n个m维向量,如何判断它是否是m维空间的一个格基呢?

定理1:格基所生成的基础平行四边形不应该包含除了初始格点(也就是 0 格点)以外的任何格点。

所有的Proof都省略,想了解的去看讲义。

第二个问题是:如何判断两个给定格基是否是等价的?

这里需要引入一个工具幺模矩阵Unimodular matrix:如果一个矩阵的行列式等于正负一,那么就称其为幺模矩阵。比如下面这个矩阵就是一个幺模矩阵:

定理2:幺模矩阵的逆也是幺模矩阵。

定理3:两个格基 是等价的,当且仅当存在某个幺模矩阵 使得 成立。

推论1:一个n维整数空间的格基 必然是一个幺模矩阵。

对于第二个问题还有一个判断方法,需要引入格基的行列式(Determinant)概念。如果格基是一个方阵(即满秩格),格基的行列式直接是方针的行列式;如果格基不是方阵,那么需要使用volumn代替行列式的概念,具体定义为: 。 那么如果两个格基等价,有:

(这里大家可以复习一下多矩阵的行列式运算法则)

格的行列式大小与格子密度成反比,行列式越小,格子越多。

不难看出,虽然格可以刻画 n 维空间离散点集,但是格的行列式,指的是上面提到的「基础区域」的容量volumn。因此,当我们以后提到格的容量时,无特别说明,都是指基础区域的容量。

2. Gram-Schmidt Orthogonalization

在学习线性代数时,施密特正交化是一个非常基础且重要的处理工具。

它的作用是:把一组线性无关向量转化成一组正交的向量。这里推荐知乎上一篇图文并茂的文章

当我们描述一个点时,我们通常喜欢“直角坐标系”来求坐标,高维空间也是一样。所以我们需要把格基这种「一般坐标系」正交化成「直角坐标系」,便于我们描述空间上某一点。这就是施密特正交化的重要之处。

二维平面的施密特正交化

如上图, 是一组线性无关向量,它可以作为二维平面的一个格基。以一个基向量为基准,不妨设 ,利用投影公式可以求另一个向量 在这个向量的投影,再作差得到与基准向量的正交向量: . 同理可知三维平面的格基的施密特正交化步骤:首先选取一个基向量作为基准,利用投影公式求出另一个向量在这个向量的投影,作差得到两条相互正交的向量;对于第三条向量,分别对之前两条正交向量求投影向量,然后用分别减去这两个投影向量,即可得到三条正交向量。

一般形式的施密特正交化

对于n维欧氏空间,设一组基为 ,定义其施密特正交化之后的基向量为 。其中:

上述 记号表示两个向量之间的点乘。

sage中内置了施密特正交化的函数: 对于矩阵 A,直接调用 A.gram_schmidt()

施密特正交化之后的基向量有如下特点:

  • 基向量之间两两正交,即其点乘结果为0.
  • 原格基与正交后的格基的span是同一张成空间.
  • 正交后的格基无需是原格基所形成的格的等价基,甚至,它们一般不在一个格内,见Figure 4.

施密特正交化在维度空间有一个非常有用的用途:计算「容量」volumn。

在二维空间,容量就是面积,在三维空间,容量是体积,在更高维的空间,有更高级的概念。当格满秩时,格基所张成的「基础区域」的容量就可以直接对正交后的格基求行列式即可得到。

3. Successive minima

译为逐次最小长度,或者连续极小。【但是好像叫做最短向量长度更切合】

当我们刻画一个格时,一个基础属性是格的最短非零向量的长度。(格空间中总是存在一个零向量,它的范数norm为0)

范数:是一个定义在向量空间上的函数,它将向量映射到非负实数。直观地说,范数可以理解为“长度”的概念,类似于欧几里得空间中的距离。范数可以用来衡量向量的大小或“长度”,并且在许多数学和应用领域中都有重要的应用。一个向量的范数计作 .

Successive minima这个参数一般计作 . 它主要刻画了格的稀疏程度

它的另一种定义是:最短向量长度 r 是满足半径为 r 的「n维球」的一维张成空间(一条线)所包含的格点中所能形成的最短向量的长度。

于是我们可以推广到 the i-th successive minima 概念:

其中 表示以0格点为圆心,格点的范数小于等于 r 所构成的n维封闭球。r 就是满足这个球里的点形成的基向量最短的最短半径。

表示格中第 i 短的线性无关向量。

下面开始解释:

  • n维球:这里强调n维,我们都知道球ball是一个三维概念,但是在不同维度空间,“球”坍塌或者扩张为其他表现形式,比如一维空间,球是一条线段,一条向量就在一个一维空间里。
  • 下图中,假设两个格点的水平距离为1,那么 , 但是 ,因为等于2的向量与 对应的向量是线性相关的。

下面给出如何求 Successive minima 的有效下界

定理 4: 令 是秩为 n 的格基,令 是其施密特正交化的基,那么:

推论 2:假设 是一个lattice,存在 ,对于任意两个非等格点 ,满足 .

【简单理解就是,在格中,两个不同的格点构成的向量的范数一定大于0,是非零向量】

断言 1:如果一个格 最短向量长度存在,那么对于 格中一定存在某个 满足

这里需要弄清楚一个事实:successive minima对应的最短向量不一定是格基。【如figure 4】

successive minima的上界

定理 4 给出了如何利用施密特正交化求得最短向量的下界,那么如何求它的上界呢?Minkowski给出了一个答案。

为了简单,我们考虑满秩格,非满秩格可以很简单的延伸。首先介绍一下 Blichfeld 定理:

定理 5 (Blichfeld):任何满秩格 和集合 ( ), 存在集合中两个不同的点,使得这两个点构成的向量属于格空间。

如图Figure 6,正如前面所说的,格的行列式表示的基础区域的容量。

定理 6(Minkowski’s Convex Body Theorem):若 是一个秩为 n 的格,那么对于任意一个中心对称的凸体 ,如果 ,那么 中存在一个非零格点。

断言 2:半径为 r 的 n 维球的体积volumn为: .

推论 3(Minkowski’s First Theorem) : 对于任意秩为 n 的满秩格 , 有:

Minkowski’s First Theorem 给出的上界不一定很紧致tight。

还有 Minkowski’s Second Theorem:

综上所述,施密特正交化给出了格的最短向量的下界,Minkowski’s First Theorem给出了满秩格的最短向量的上界。

4. Computational problems

经过上述介绍,我们可以看到,对于格的最短向量(非0)的只有一个大概的上下界,而我们目前没有有效算法找出最短向量。这就引出了我们在开头所述的底层数学难题,关于格的数学难题大概有两类:SVP和CVP。与密码学经典的数学难题一样(DLP,CDH,DDH…),关于格的数学难题也有很多变种,下面分别介绍一下。

Shortest Vector Problem(SVP)

SVP难题的变种也是根据难度划分为:是否真的需要找到最短向量?只需要找到最短向量的长度?或者甚至只要确定最短向量比某个给定的值小即可?具体是:

  • Search SVP:给定格基,找出最短向量。
  • Optimization SVP:给定格基,找出最短向量长度(Successive Minima)即可。
  • Decisional SVP:给定格基和一个有理数,判断格的最短向量长度是否比它小即可。

注意:以上难题中,均限制格基向量为整数向量,这要做的目的是为了输入可以用有限比特来表示,因此我们可以将SVP当做一个标准计算问题。我们也可以允许格基由有理向量组成。这将导致一个本质上等效的定义,因为通过缩放,可以使所有有理坐标为整数。

这三个变种,Search SVP、Optimization SVP和Decisional SVP的难度依次下降,但是反之亦然。因此,这三个难题是等价的。

在 Regev 课程中,考虑的是 SVP 的近似变体,近似因子

  • Search :给定格基,找出近似最短向量。
  • Optimization :给定格基,找出最短向量长度的近似范围即可。
  • Promise :给定格基和一个有理数 r ,如果是 YES 实例,判断格的最短向量长度是否小于等于 r 即可;如果是 NO 实例,判断格的最短向量长度是否大于 即可

最后一种也叫做 ,这种情况下每个实例都有特定的输入集合,如果输入不在这些集合中,将会导致未定义行为。

同样的,promise变体并不比optimization变体更困难,optimization变体不比Search变体更困难。反之,optimization变体不比promise变体更困难,但是Seach变体是否比optimization变体更困难尚且是一个开放问题。

Closest Vector Problem(CVP)

顾名思义,这个数学难题是为找到给定格点的最近格点。与 SVP 一样,对 CVP 也定义了三种变体:

  • Search :给定格基 和 向量 ,找到另一个向量 满足 .
  • Optimization :给定格基 和 向量 ,找到距离 d 满足 .
  • Promise :给出一个CVP实例三元组 ,如果是 YSE 实例, ;如果是 NO 实例, .

以上两种都是计算难题。关于格还有一些易于计算的问题,比如:

  • 成员关系:给定格基和向量,判定向量是否属于格。这个问题可以使用高斯消元法(Gaussian elimination)高效解决。
  • 等价关系:给定两个格基,判定二者是否等价。这个问题可以按列判断包含关系,然后两者交换再检查包含关系。如果都成立,则说明二者等价。

Lattice基础

作者简介:Xor0v0,硕士在读,零知识证明小白,目前在做一些circom开发和zk审计,密码学爱好者,打过一些web2/3 CTF,最近对zkHACK产生兴趣。欢迎各位大佬一起交流学习。

Lattice是现代密码学非常重要的一部分,它也可以被用于构造零知识证明方案,比如这篇Lattice-Based zk-SNARKs from Square Span Programs

我们知道密码学的底层依赖于一些数学难题,基于Lattice的密码学的底层当然依赖于一些Lattice的的数学难题,这些数学难题在本篇最后都会提及。格密码学被认为是一种后量子安全的密码学。

之所以想出这一系列,不是说基于Lattice的ZKP方案有多好或者多有前景。纯粹是一个密码学爱好者的个人分享,这是一个非常有意思的领域,另外如果有兴趣参加web2/web3 CTF、密码学竞赛或者ZK Puzzle的同学,这一块知识拼图也是必不可少的。BTW,了解这个领域需要大家拥有线性代数基础

总之,我会在这一系列中跟大家一起学习lattice的知识,分享我在Web3 CTF或者ZK Puzzle中遇到的可以使用到lattice的解法【第二篇LLL算法应该就有题分享】。

希望大家各有所获!!

参考文献:最经典最地道的 Regev 讲义(源于它,但不仅仅是它),需要注意的是,这些讲义中通篇使用列向量表示矩阵,而非常规的行向量,这对于数学工作者也许不是什么大毛病,但是初学者一定要注意分辨,因为这样的矩阵使得整数线性组合向量在乘法的右边。

强力推荐大家看原文,如果觉得英文不好理解,可以再看这里有没有答案。

1. 什么是格Lattice?

格Lattice,顾名思义,就是一个个格子。严格来讲,格是一个数学对象,它的数学定义是:格Lattice由n维空间中具有周期结构的点集构成。【划重点:Lattice是空间内的离散点集】下图展示了一个二维实数空间的格点:

18世纪的大数学家们如Lagrange, Gauss 和后来的Minkowski,都研究过格。近年来,格被计算机科学领域所关注,被用于作为一种算法工具去解决各种问题,在密码学和密码分析中也有大量运用,并且这些构造出来的格从计算复杂性角度上讲拥有着独特的性质。

对于一个格,更正式的定义是:在m维实数空间,给定n个线性无关的m维向量 ,由这些向量定义的格就是:

说明:如果没有特别说明,粗体字母表示向量。

线性无关向量:给定一组向量 ,如果存在一组不全为零的系数 ,使得下面的等式成立:

则称这组向量是线性无关的。线性无关表示每一个元素都是相互独立,而没有冗余信息。

我们把这组线性无关向量称为格基(Basis of the lattice)。等价地,我们可以把这组向量按列展开,于是就得到它们的矩阵表示B,有:

我们定义格的秩(rank of lattice)为n,格的维度(dimension)为m。如果 ,那么则称这个格为满秩格(full-rank lattice)。如果没有特殊说明,本系列文章讲只讨论满秩格,因为其他情况并没有实质的差异。下面给出几个格的例子:

特别是: 是一个1维的满秩格。注意,一个维度空间的格基不唯一。

下面介绍两个概念:张成空间Span和基础区域fundamental parallelepiped。

  • 张成空间: 中基向量的所有线性组合(linear combinations)所形成的集合,就叫做这组基向量所张成的空间(SPAN),
  • 基础区域:在不包含其他格点的前提下,格基所能张成的区域。【这个概念很重要!!】

如Figure 2阴影部分所示,就是fundamental parallelepiped的示例。假如把维度空间内所有的格点都做一个 , 就能平铺(tiling)掉整个维度空间(span)。一定要注意:格 不能表示整个n维空间, 才能表示整个n维空间。区别就在于span的系数是实数,而格基的系数只能是整数,所以格只能表示一群离散的格点。

正如Figure 2(c)所示它就不是一个格基,那么第一个问题:给定n个m维向量,如何判断它是否是m维空间的一个格基呢?

定理1:格基所生成的基础平行四边形不应该包含除了初始格点(也就是 0 格点)以外的任何格点。

所有的Proof都省略,想了解的去看讲义。

第二个问题是:如何判断两个给定格基是否是等价的?

这里需要引入一个工具幺模矩阵Unimodular matrix:如果一个矩阵的行列式等于正负一,那么就称其为幺模矩阵。比如下面这个矩阵就是一个幺模矩阵:

定理2:幺模矩阵的逆也是幺模矩阵。

定理3:两个格基 是等价的,当且仅当存在某个幺模矩阵 使得 成立。

推论1:一个n维整数空间的格基 必然是一个幺模矩阵。

对于第二个问题还有一个判断方法,需要引入格基的行列式(Determinant)概念。如果格基是一个方阵(即满秩格),格基的行列式直接是方针的行列式;如果格基不是方阵,那么需要使用volumn代替行列式的概念,具体定义为: 。 那么如果两个格基等价,有:

(这里大家可以复习一下多矩阵的行列式运算法则)

格的行列式大小与格子密度成反比,行列式越小,格子越多。

不难看出,虽然格可以刻画 n 维空间离散点集,但是格的行列式,指的是上面提到的「基础区域」的容量volumn。因此,当我们以后提到格的容量时,无特别说明,都是指基础区域的容量。

2. Gram-Schmidt Orthogonalization

在学习线性代数时,施密特正交化是一个非常基础且重要的处理工具。

它的作用是:把一组线性无关向量转化成一组正交的向量。这里推荐知乎上一篇图文并茂的文章

当我们描述一个点时,我们通常喜欢“直角坐标系”来求坐标,高维空间也是一样。所以我们需要把格基这种「一般坐标系」正交化成「直角坐标系」,便于我们描述空间上某一点。这就是施密特正交化的重要之处。

二维平面的施密特正交化

如上图, 是一组线性无关向量,它可以作为二维平面的一个格基。以一个基向量为基准,不妨设 ,利用投影公式可以求另一个向量 在这个向量的投影,再作差得到与基准向量的正交向量: . 同理可知三维平面的格基的施密特正交化步骤:首先选取一个基向量作为基准,利用投影公式求出另一个向量在这个向量的投影,作差得到两条相互正交的向量;对于第三条向量,分别对之前两条正交向量求投影向量,然后用分别减去这两个投影向量,即可得到三条正交向量。

一般形式的施密特正交化

对于n维欧氏空间,设一组基为 ,定义其施密特正交化之后的基向量为 。其中:

上述 记号表示两个向量之间的点乘。

sage中内置了施密特正交化的函数: 对于矩阵 A,直接调用 A.gram_schmidt()

施密特正交化之后的基向量有如下特点:

  • 基向量之间两两正交,即其点乘结果为0.
  • 原格基与正交后的格基的span是同一张成空间.
  • 正交后的格基无需是原格基所形成的格的等价基,甚至,它们一般不在一个格内,见Figure 4.

施密特正交化在维度空间有一个非常有用的用途:计算「容量」volumn。

在二维空间,容量就是面积,在三维空间,容量是体积,在更高维的空间,有更高级的概念。当格满秩时,格基所张成的「基础区域」的容量就可以直接对正交后的格基求行列式即可得到。

3. Successive minima

译为逐次最小长度,或者连续极小。【但是好像叫做最短向量长度更切合】

当我们刻画一个格时,一个基础属性是格的最短非零向量的长度。(格空间中总是存在一个零向量,它的范数norm为0)

范数:是一个定义在向量空间上的函数,它将向量映射到非负实数。直观地说,范数可以理解为“长度”的概念,类似于欧几里得空间中的距离。范数可以用来衡量向量的大小或“长度”,并且在许多数学和应用领域中都有重要的应用。一个向量的范数计作 .

Successive minima这个参数一般计作 . 它主要刻画了格的稀疏程度

它的另一种定义是:最短向量长度 r 是满足半径为 r 的「n维球」的一维张成空间(一条线)所包含的格点中所能形成的最短向量的长度。

于是我们可以推广到 the i-th successive minima 概念:

其中 表示以0格点为圆心,格点的范数小于等于 r 所构成的n维封闭球。r 就是满足这个球里的点形成的基向量最短的最短半径。

表示格中第 i 短的线性无关向量。

下面开始解释:

  • n维球:这里强调n维,我们都知道球ball是一个三维概念,但是在不同维度空间,“球”坍塌或者扩张为其他表现形式,比如一维空间,球是一条线段,一条向量就在一个一维空间里。
  • 下图中,假设两个格点的水平距离为1,那么 , 但是 ,因为等于2的向量与 对应的向量是线性相关的。

下面给出如何求 Successive minima 的有效下界

定理 4: 令 是秩为 n 的格基,令 是其施密特正交化的基,那么:

推论 2:假设 是一个lattice,存在 ,对于任意两个非等格点 ,满足 .

【简单理解就是,在格中,两个不同的格点构成的向量的范数一定大于0,是非零向量】

断言 1:如果一个格 最短向量长度存在,那么对于 格中一定存在某个 满足

这里需要弄清楚一个事实:successive minima对应的最短向量不一定是格基。【如figure 4】

successive minima的上界

定理 4 给出了如何利用施密特正交化求得最短向量的下界,那么如何求它的上界呢?Minkowski给出了一个答案。

为了简单,我们考虑满秩格,非满秩格可以很简单的延伸。首先介绍一下 Blichfeld 定理:

定理 5 (Blichfeld):任何满秩格 和集合 ( ), 存在集合中两个不同的点,使得这两个点构成的向量属于格空间。

如图Figure 6,正如前面所说的,格的行列式表示的基础区域的容量。

定理 6(Minkowski’s Convex Body Theorem):若 是一个秩为 n 的格,那么对于任意一个中心对称的凸体 ,如果 ,那么 中存在一个非零格点。

断言 2:半径为 r 的 n 维球的体积volumn为: .

推论 3(Minkowski’s First Theorem) : 对于任意秩为 n 的满秩格 , 有:

Minkowski’s First Theorem 给出的上界不一定很紧致tight。

还有 Minkowski’s Second Theorem:

综上所述,施密特正交化给出了格的最短向量的下界,Minkowski’s First Theorem给出了满秩格的最短向量的上界。

4. Computational problems

经过上述介绍,我们可以看到,对于格的最短向量(非0)的只有一个大概的上下界,而我们目前没有有效算法找出最短向量。这就引出了我们在开头所述的底层数学难题,关于格的数学难题大概有两类:SVP和CVP。与密码学经典的数学难题一样(DLP,CDH,DDH…),关于格的数学难题也有很多变种,下面分别介绍一下。

Shortest Vector Problem(SVP)

SVP难题的变种也是根据难度划分为:是否真的需要找到最短向量?只需要找到最短向量的长度?或者甚至只要确定最短向量比某个给定的值小即可?具体是:

  • Search SVP:给定格基,找出最短向量。
  • Optimization SVP:给定格基,找出最短向量长度(Successive Minima)即可。
  • Decisional SVP:给定格基和一个有理数,判断格的最短向量长度是否比它小即可。

注意:以上难题中,均限制格基向量为整数向量,这要做的目的是为了输入可以用有限比特来表示,因此我们可以将SVP当做一个标准计算问题。我们也可以允许格基由有理向量组成。这将导致一个本质上等效的定义,因为通过缩放,可以使所有有理坐标为整数。

这三个变种,Search SVP、Optimization SVP和Decisional SVP的难度依次下降,但是反之亦然。因此,这三个难题是等价的。

在 Regev 课程中,考虑的是 SVP 的近似变体,近似因子

  • Search :给定格基,找出近似最短向量。
  • Optimization :给定格基,找出最短向量长度的近似范围即可。
  • Promise :给定格基和一个有理数 r ,如果是 YES 实例,判断格的最短向量长度是否小于等于 r 即可;如果是 NO 实例,判断格的最短向量长度是否大于 即可

最后一种也叫做 ,这种情况下每个实例都有特定的输入集合,如果输入不在这些集合中,将会导致未定义行为。

同样的,promise变体并不比optimization变体更困难,optimization变体不比Search变体更困难。反之,optimization变体不比promise变体更困难,但是Seach变体是否比optimization变体更困难尚且是一个开放问题。

Closest Vector Problem(CVP)

顾名思义,这个数学难题是为找到给定格点的最近格点。与 SVP 一样,对 CVP 也定义了三种变体:

  • Search :给定格基 和 向量 ,找到另一个向量 满足 .
  • Optimization :给定格基 和 向量 ,找到距离 d 满足 .
  • Promise :给出一个CVP实例三元组 ,如果是 YSE 实例, ;如果是 NO 实例, .

以上两种都是计算难题。关于格还有一些易于计算的问题,比如:

  • 成员关系:给定格基和向量,判定向量是否属于格。这个问题可以使用高斯消元法(Gaussian elimination)高效解决。
  • 等价关系:给定两个格基,判定二者是否等价。这个问题可以按列判断包含关系,然后两者交换再检查包含关系。如果都成立,则说明二者等价。

LLL算法

作者简介:Xor0v0,硕士在读,零知识证明小白,目前在做一些circom开发和zk审计,密码学爱好者,打过一些web2/3 CTF,最近对zkHACK产生兴趣。欢迎各位大佬一起交流学习。

LLL 算法于 1982 年由三位 L 开头的外国人设计(因此得名),是一种解决 SVP 的近似算法。

LLL 算法的近似率为 ,其中 n 是格的维度。在大多数应用中,n 都是一个常数,因此近似率是确定的。

1801年 Guass 给出一种解决二维 SVP 的算法, LLL 算法可以看作是Gauss的算法对高维的推广。

LLL 的用途:

  1. 分解整数或者有理数上的多项式。比如分解

  2. 求一个给定代数的最小多项式足够好的 approximation。例如,给定 输出 ,给定 输出

  3. 求解整数关系,对于一组实数 ,如果存在一组非全零整数 使得 成立,则称为这组实数具有整数关系。比如,给定 ,可以证明下列整数关系存在:

这个等式被称为 公式。

  1. 整数规划问题。这是一个著名的 NP 问题,使用LLL,可以在多项式时间解决固定数量变量的整数规划。

  2. 最近向量问题(CVP),以及其他格问题。

  3. 密码分析中的各种应用(即破坏密码协议)。 例如,对基于背包的密码系统的许多攻击。 对RSA的一些特殊情况也有一些攻击,如低公共指数攻击(low public exponent attack)。此外,对DSA系统中求解 HNP 问题也是很好的攻击应用。

通俗意义上讲,LLL 算法就是把一个糟糕的格基转换为一个相当好的格基。所谓“糟糕”或者“好”,评价标准是这个格基是否短且与其他基向量接近于正交,如果是则是好的格基,否则是糟糕的格基。

下面从三个方面介绍 LLL 算法:

  1. 定义一个 LLL 规约基 (reduced basis),
  2. 提出一种算法来寻找这样的规约基,
  3. 分析其运行时间。

1. Reduced Basis

首先回顾施密特正交化的定义:

定义 1: 给定 n 个线性无关向量 ,其施密特正交化的定义为:

定义 2: 如果下列情况成立,则格基 是一个 规约基:

  1. .
  2. .

Remark 1:把一个基转换为规约基总是有可能的,实际上 LLL 算法就是在做这件事。

Remark 2 :考虑 的情形是很有用的,在 LLL 算法中, .

Remark 3 :定义 2中的第二个条件可以等价变化为:

稍微变换之后,可知:第二个 property 可以解释为: 不会比 短很多。

具体的,我们考虑经过施密特正交化得到的格基矩阵为:

【在线性代数中,正交基可以表示为基向量范数的上三角形式,这是由正交基的性质和格拉姆-施密特正交化过程的结果所决定的。】

其中列 i 显示了在这个正交基中 的坐标。 定义LLL规约基中的第一个条件保证:任何非对角元素的绝对值,最多是同一行对角线元素值的一半。 这可以写成:

第二个条件要求这个矩阵的第二列几乎和它的第一列一样长。

LLL 规约基的一个重要特性是它的第一个向量相对较短,如下一个 claim 所示。

Claim 1:假设 是一个 规约基,则有:

Remark 4: 如果 则有:

2. LLL Algorithm

这里首先给出 LLL 算法的 python 伪代码:

def lll(basis):
  	while k <= n:
      	# reduction step loop
    		for j in reverse(range(k-1, 0)):
      	m = mu(k, j)
      	basis[k] = basis[k] - mu*basis[j] # vector reduction
    if lovasz_condition:
        k += 1
    else:
        basis[k], basis[k+1] = basis[k+1], basis[k] # swap step
        # update orthogonalized basis
        k = max(k-1,1)
    return basis

上面的介绍让 LLL 算法较为晦涩,下面从几个简单的角度解读 LLL 算法。

LLL vs GCD

LLL 算法与最大公约数 GCD 算法可以说是毫不相关,但是从思想上,二者有异曲同工之妙:皆为先 Reduction,后 Swap.

观察一下 GCD 算法的 python 伪代码:

def euclid_gcd(a, b):
  	if a < b:
      	a, b = b, a
    # base condition
  	if b == 0:
      	return a
    # reduction
    x = a mod b 
    return euclid_gcd(b, x) # swap

GCD 算法先是一个 reduction step 把数字变小,然后 swap step 交换数字顺序。返回条件是 b = 0。

再观察 LLL 算法的伪代码,也是先 reduction step,使用施密特正交化把基向量变小,然后 swap step 交换向量顺序。返回条件是所有的向量满足 lovacz_condition(暂且不管什么是lovacz_condition)。

LLL vs Gram-Schmidt

LLL 算法和 施密特正交化算法(简称GS 算法)都是对格基进行处理的算法。GS 算法把格基处理为所有的基向量两两正交,其张成空间 span 与原格基相同,但是格(点)不一定相同。如第一讲所述,GS 算法是通过将基向量分为相关分量,然后从所有向量中减去冗余分量,从而达到正交化的目的。

在上一讲中,我们知道施密特正交基是 successive minima 的下界,那么是否可以直接使用 GS 算法来求得 LLL 规约基呢?答案是否定的。原因在于:GS算法可能可以让我们得到与 LLL 规约基相近的一组格基,但是不能保证它就是 LLL 规约基,这是由于 GS 正交基可能与原格基不属于同一个格

sage: from mage import matrix_utils # https://github.com/kelbyludwig/mage; use the install.sh script to install
sage: b1 = vector(ZZ, [3,5])
sage: b2 = vector(ZZ, [8,3])
sage: B  = Matrix(ZZ, [b1,b2])
sage: Br,_ = B.gram_schmidt()
sage: pplot = matrix_utils.plot_2d_lattice(B[0], B[1])
sage: pplot += plot(Br[0], color='grey', linestyle='-.', legend_label='unmodified', legend_color='blue')
sage: pplot += plot(Br[1], color='grey', linestyle='-.', legend_label='orthogonalized', legend_color='grey')
sage: pplot

在 sage 中运行得到的图像:

注意灰色的格基已经不在和原格基属于同一格了。但是 GS 算法对于理解 LLL 算法仍然很重要,因为它会作为 LLL 算法的一部分。

LLL vs Gaussian Lattice Reduction

正如之前所介绍, Gauss 曾提出过一个解决二维格的最短向量问题 SVP 的算法,即 Gaussian Lattice Reduction,它启发了 LLL 算法的出现。给出 Gaussian Lattice Reduction 的 python 代码:

def gauss_reduction(v1, v2) {
  	while True:
  			if v2.norm() < v1.norm():
            v1, v2 = v2, v1 # swap step
        m = round( (v1 * v2) / (v1 * v1) )
        if m == 0:
            return (v1, v2)
        v2 = v2 - m*v1 # reduction step
}

暂时忽略掉 while 无限循环,注意到首先进行了一个 swap step,保证 。【理由

表示 上的投影标量,这和 GS 算法产生的投影标量是相同的,但是运用了一个 round 函数将其四舍五入到最近的整数,这样做为了保证得到的向量仍然在同一个格中。注意,得到的新基向量的长度无疑会比原向量短。

直到两个向量之间的投影非常接近,停止循环,此时两个基向量会近似“正交”,而且长度较短。

LLL

下面正式介绍 LLL 算法的细节。与 Gaussian Reduction 类似,它也是根据输入的基向量进行迭代,然后对每个基向量进行长度规约。与 Gauss 规约不同的是,LLL处理的是 n 维基向量,因此需要一个方法确保输入的基向量的顺序不会影响到最终结果,这就是所谓的 lovasc condition,这个条件用于确定输入基向量是否需要执行 swap。

为了深入理解 LLL 算法,我们研究一下维基百科的 LLL python 代码实现:

def LLL(B, delta):
    Q = gram_schmidt(B)

    def mu(i,j):
        v = B[i]
        u = Q[j]
        return (v*u) / (u*u)

    n, k = B.nrows(), 1
    while k < n:

        # length reduction step
        for j in reversed(range(k)):
            if abs(mu(k,j)) > .5:
                B[k] = B[k] - round(mu(k,j))*B[j]
                Q = gram_schmidt(B)

        # swap step
        if Q[k]*Q[k] >= (delta - mu(k,k-1)**2)*(Q[k-1]*Q[k-1]):
            k = k + 1
        else:
            B[k], B[k-1] = B[k-1], B[k]
            Q = gram_schmidt(B)
            k = max(k-1, 1)

   return B
mu

首先是 mu 函数,可以看到函数代码就是求向量 上的投影向量,并且并没有四舍五入处理,即与 GS 算法中的投影向量一样,因此这个投影向量不一定在格中。所以这个函数就是求原格基 B 的第 i 个基向量在正交之后的格基 Q 的第 j 个基向量的投影标量

我们已经知道 GS 正交化虽然能够提供一个理想的正交矩阵,但是它改变了格基所确定的格。因此我们考虑使用 mu 函数和 GS 正交化来辅助进行格基规约。

Length Reduction

接着,定义 n 为原格基的维度【满秩格】,k 表示当前函数处理的向量。两次循环中,内层循环负责针对 B[k] 对其之前所有的向量进行长度缩减:首先判断 是否大于 , 如果大于 则四舍五入并进行向量缩减,并然后更新施密特正交化,否则不做任何操作;外层循环则控制循环次数。

其实这里的长度缩减和 GS 正交化非常相似:

  • LLL Length Reduction

    B[0] = B[0]
    B[1] = B[1] - round(mu(1, 0))*B[0]
    B[2] = B[2] - round(mu(2, 1))*B[1] - round(mu(2, 0))*B[0]
    ...
    B[k] = B[k] - round(mu(k, k-1))*B[k-1] - round(mu(k, k-2))*B[k-2] - ... - round(mu(k, 0))*B[0]
    
  • GS 正交化

    Q[0] = B[0]
    Q[1] = B[1] - mu(1, 0)*Q[0]
    Q[2] = B[2] - mu(2, 1)*Q[1] - mu(2, 0)*Q[0]
    ...
    Q[k] = B[k] - mu(k, k-1)*Q[k-1] - mu(k, k-2)*Q[k-2] - ... - mu(k, 0)*Q[0]
    
lovesz condition and swap step

再关注到代码中的交换部分:

    # swap step
    if Q[k]*Q[k] >= (delta - mu(k,k-1)**2)*(Q[k-1]*Q[k-1]):
        k = k + 1
    else:
        B[k], B[k-1] = B[k-1], B[k]
        Q = gram_schmidt(B)
        k = max(k-1, 1)

对当前向量走完一轮长度缩减后,Lovász 条件将告诉我们是继续处理下一个基向量(代码第三行),还是将当前向量和前一个向量互换位置。

暂时忽略 Lovasz 条件的具体含义,这样的交换不免让我们想起了某些排序算法。 是当前处理的基向量的索引,假设对于第 个基向量的 Lovasz 条件为真,则 LLL 开始处理第 个基向量,此时大致上可以说从第 0 个基向量到第 个基向量是按长度排序的。如果 Lovasz 条件为假,则将该向量放在 的位置,然后重新处理第 个基向量。在完成又一轮长度缩减后,再次回到交换步骤,决定是否需要再次交换该基向量的位置。于是我们也可以这样描述 LLL 算法:LLL 算法是一种向量排序算法,不过在向量长度缩减过程中向量变小可能会扰乱顺序,因此必须重新排序。于是我们也可以这样描述 LLL 算法:LLL 算法是一种向量排序算法,不过在向量长度缩减过程中向量变小可能会扰乱顺序,因此必须重新排序

而对于 Lovasz 条件本身,它是一种启发式,用于确定向量是否处于“良好”的顺序。启发式就是那种没法去证明的但是又能用的,就好比机器学习中的调参,调太差了不准,太好了又会过拟合,于是不断微调去找到一个大差不差的。Lovasz 的描述有多种,感兴趣的读者可以参看一下这篇 StackOverflow 上的这篇文章

根据 lovasz 条件,可以推出 LLL 算法输出的最短基向量 满足:

||\pmb{v_1}||\le 2^{(n-1)/4}|\det(L)|^{1/n}\and ||\pmb{v_1}||\le 2^{(n-1)/2}\min_{0\neq v\in L}||\pmb{v}||

而根据 minkowski 凸体定理有:

因此,在使用 LLL 算法时可以通过上述公式判断目标向量是否满足上述条件。

3. Complexity Analyze

分析包括两个步骤。 首先,我们 bound 了迭代次数。 其次,我们 bound 了单个迭代的运行时间。

可以证明:算法的总体运行时间是关于输入规模的多项式。 输入规模的 rough下界由 给出。(因为 n 个向量中的每一个都需要至少一位来表示,范数为 r 的向量需要至少 位来表示)。

定理 1: 迭代次数是关于 M 的多项式。

定义 5: 给定格基 B,那么 B 的 potential 是它的施密特正交化后的基的范数乘积。

整个证明过程没看懂,下次如果看懂了再来补充。有兴趣的可以去看原文,证明过程使用到了这个格基的 Potencial。

Nearest Plane Algorithm for CVP

作者简介:Xor0v0,硕士在读,零知识证明小白,目前在做一些circom开发和zk审计,密码学爱好者,打过一些web2/3 CTF,最近对zkHACK产生兴趣。欢迎各位大佬一起交流学习。

首先回顾一下 CVP 问题:给定格和一个点,求与之最近的格点。它有三种近似(approximation)变体,分别是 Search , Optimization 和 Promise ,其中 是近似因子。如果近似因子等于 1,那么就得到这些问题的精确版本。

1986年,Babai提出了名为 Nearest Plane Algorithm 的算法可以解决CVP问题的近似变体,它的近似率达到了 ,其中 n 代表格的秩。当 n 确定时,我们也就得到了近似率,进而可以确定近似因子 .

Babai’s nearest plane algorithm可以解决 Search ,这意味着解决了它也就可以解决其他两种近似变体(因为其他两种近似变体不难于search 变体)。

1. Nearest Plane Algorithm

整个算法分为 2 步,第一步通过 LLL 算法对输入的格基进行规约,然后寻找关于这个格基的一个整数线性组合使其接近于目标向量 。第二步基本上与 LLL 算法中 reduction 步骤的内层循环是相同的。

/*
		basis: lattice basis, a (m * n) matrix
  	t: target vector, (1 * m)
/*
def cvp(basis, t):
  	def mu(b,j):
        u = Q[j]
        return (b*u) / (u*u)
    
  	basis = lll(basis)
    b = t
    for j in reversed(range(n)):
        b -= round(mu(b, j)) * basis[j]
    return t - b

不难看出,cvp 算法的时间复杂度也是关于输入规模的多项式时间。注意,cvp 算法不要求格是满秩的。

考虑一个正交基向量集合 ,如果是满秩格( ),这就是归一化的正交基,而如果不是满秩格,则需要扩展 个基向量。

简单阐述 CVP 算法的思想就是:找到这些 LLL 规约基向量(列向量)的一个整数组合,使得它们构造出向量的每个坐标 的值都在目标向量 对应坐标值的 范围内。因此,算法首先需要找到第 个基向量的倍数,使得目标向量第 个坐标值在 范围内。然后是第 个基向量,直至第 个基向量。 注意,如果格不是满秩的,则最后 维对应于与格 span 正交的空间。

上述描述也许比较晦涩,下面给出一个关于算法中递归操作的最自然的解释,并且更加呼应算法的名字:

  1. 假设 在张成空间 的一个投影;
  2. 找到倍数 ,使得 尽可能的接近于
  3. ,递归调用 ,令其答案为
  4. 返回 即是最接近 t 的向量。

首先第一步是把目标向量 t 投影到向量空间中,最接近 的格向量与最接近 的格向量相同,因此这一步是有意义的。在第二步中,我们确定了格的平移,并且推测目标向量就在这里。在第三步递归中,我们不断的“降维”找到最接近目标向量的向量。最终我们把它平移回 ,我们认定这就是最近向量。

关于算法的正确性参考讲义内容

2. HNP: An application of CVP

HNP(Hidden Number Problem),这个问题由Dan Boneh于1996年提出,它论述了计算Diffie-Hellman共享密钥的最低有效位与计算完整密钥同样难。 另一位大佬,使用Lattice攻破了DSA的HNP问题,参考论文

正如密码学其他的困难问题一样,HNP也被定义为与“Oracle”之间的游戏,游戏中Oracle可以大致输出用户给定的输入的最低有效位。

具体而言,基于 n 比特质数和一个 k 比特最低有效位(Most Significant Bits)的 leak,我们定义Oracle的输出为: ,满足 在原始论文中,HNP可以表述为:对于随机的 ,给定 ,问是否可以求出隐藏的数字 ?其中 是随机的,论文中还提出了几种变体,但本文只关注随机版本的HNP。

下面先给出求最低有效位的实现:

# Some parameters of the game, chosen for simplicity.

# p - A prime number for our field.
p = next_prime(2^16)

# n - The number of bits in `p`.
n = ceil(log(p, 2))

# k - The number of significant bits revealed by the oracle.
# Using parameters from Thereom 1.
k = ceil(sqrt(n)) + ceil(log(n, 2))

def msb(query):
    """Returns the MSB of query based on the global paramters p, k.
    """
    while True:
        z = randint(1, p-1)
        answer = abs(query - z)
        if answer < p / 2^(k+1):
            break
    return z

def create_oracle(alpha):
    """Returns a randomized MSB oracle using the specified alpha value.
    """
    alpha = alpha
    def oracle():
        random_t = randint(1, p-1)
        return random_t, msb((alpha * random_t) % p)
    return oracle

所以最低有效位到底泄漏了什么?根据定义,我们发现, 的输出是不唯一的,并且输出与 p 有关。顾名思义,最低有效位就是一个数字的最低若干比特,本应该是确定的,与其他数无关的。而这两种特性就是 HNP 问题定义中的“大致”所造成的。

因此如何去理解 函数?注意到:

  • ,Oracle 总会输出这个结果,且结果就为 x。
  • 其他的结果将会在 附近上下波动。
  • 随着 的增大,不等式右边的 的选择会极度下降,合法的 越来越少,也越来越接近
  • Oracle 输出值不一定只泄漏 比特信息,随着 增大, 的范围越小,则会泄漏 更多比特信息。

也就是说,当 越接近 的位数,Oracle 输出越接近

那么什么时候 HNP 将是可解决的呢?Dan Boneh的论文指出:当 k 接近 时,使用 次Oracle查询就可恢复出隐藏数字。于是,给定一个 Oracle,我们如何去求解 HNP 呢?答案是:当可以得到 Oracle d 次回复后,solution可以视为 CVP 的一个特例。这个 CVP 的特例在给定足够多 Oracle 回复和特别选定的一组格基的情况下将会很容易解决。

这组格基是:

[  p,  0, ... ,  0,   0 ]
[  0,  p, ... ,  0,   0 ]
[         ...           ]
[  0,  0, ... ,  p,   0 ]
[ t1, t2, ... , td, 1/p ]

其中, 是 Oracle 的输入。Oracle 的输出 就是我们需要求解的最近向量。

假设某个向量第一个系数为 ,由于 是最后一行的整数标量, 记号相当于减去若干整数倍的第一行。假设向量 ,求得到这个向量我们就能很轻易的恢复出 。根据 的定义,发现 是非常接近于 的。于是我们可以通过 CVP 算法去找到这样的向量,进而恢复

注意,其实模数 p 并非一定要固定,模数改变仍然是 HNP 问题。

Paradigm 2023 Oven

题目地址: https://github.com/paradigmxyz/paradigm-ctf-2023/tree/main/oven

#!/usr/bin/env python3
from Crypto.Util.number import *
import random
import os
import hashlib

FLAG = os.getenv("FLAG", "PCTF{flag}").encode("utf8")
FLAG = bytes_to_long(FLAG[5:-1])
assert FLAG.bit_length() < 384

BITS = 1024


def xor(a, b):
    return bytes([i ^ j for i, j in zip(a, b)])


# This doesn't really matter right???
def custom_hash(n):
    state = b"\x00" * 16
    for i in range(len(n) // 16):
        state = xor(state, n[i : i + 16])

    for _ in range(5):
        state = hashlib.md5(state).digest()
        state = hashlib.sha1(state).digest()
        state = hashlib.sha256(state).digest()
        state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest()

    value = bytes_to_long(state)

    return value


def fiat_shamir():
    p = getPrime(BITS)
    g = 2
    y = pow(g, FLAG, p)

    v = random.randint(2, 2**512)

    t = pow(g, v, p)
    c = custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t))
    r = (v - c * FLAG) % (p - 1)

    assert t == (pow(g, r, p) * pow(y, c, p)) % p

    return (t, r), (p, g, y)


while True:
    resp = input("[1] Get a random signature\n[2] Exit\nChoice: ")
    if "1" in resp:
        print()
        (t, r), (p, g, y) = fiat_shamir()
        print(f"t = {t}\nr = {r}")
        print()
        print(f"p = {p}\ng = {g}\ny = {y}")
        print()
    elif "2" in resp:
        print("Bye!")
        exit()

题目大意就是用户可以获取 FLAG 随机签名,生成随机签名的逻辑就在 fiat-shamir 函数里。

我们可获得的信息有 t, r, p, g, y ,其实 c 也可以求出。那么就可以关注 fiat_shamir 函数中使用到 FLAG 进行签名的部分,即: r = (v - c * FLAG) % (p - 1) 。分析一下: r, c, p 均是已知值,且 FLAG 的位数已经确定: assert FLAG.bit_length() < 384 。 可以使用上述 HNP 问题(模数可变),可使用标准的格算法进行攻击。解法如下:

  1. 首先对上述式子稍微变形: ,只有 v 和 FLAG 是未知数。非常标准的随机版本HNP。

  2. 我们可以根据上述式子构建 Lattice:

解释:

  • K是FLAG的一个上界
  • 空白处都为0
  1. 根据Babai的CVP解决算法,一定存在一个解向量 ,使得 成立
  2. 注意到 在格中是一个短向量,于是我们可以采用 LLL 算法在多项式时间内找到这个短向量。注意,短向量的每一个元素用 64bit可以表示,于是上界

这里还需要解释一下数据量的问题,怎么知道需要多少数据可以恢复出 FLAG 呢?这个需要使用到 gaussian heuristic 估计最短向量长度,要求的目标向量范数小于这个长度即可。【但是由于这是ctf,所以一般就三四五组数据就出来了,这里我先收集了5组数据备用,实际上用了3组数据,就可解出FLAG】。

我们可以使用论文的方式构造格矩阵,然后使用 LLL 算法在多项式时间内找到一个解向量,FLAG值就在这个向量里。

下面贴出EXP(需要使用 sage-python)。

from Crypto.Util.number import *

BITS = 1024
def xor(a, b):
    return bytes([i ^^ j for i, j in zip(a, b)])

def custom_hash(n):
    state = b"\x00" * 16
    for i in range(len(n) // 16):
        state = xor(state, n[i : i + 16])

    for _ in range(5):
        state = hashlib.md5(state).digest()
        state = hashlib.sha1(state).digest()
        state = hashlib.sha256(state).digest()
        state = hashlib.sha512(state).digest() + hashlib.sha256(state).digest()

    value = bytes_to_long(state)

    return value


g = 2
# p = remote("oven.challenges.paradigm.xyz", 1337)
# # v = (c * flag - r) % (p-1)
# t = 5
# while t > 0:
#     p.sendafter(b'Choice: ', b'1')
#     a = p.recvuntil(b't =')
#     t = int(p.recvuntil(b'\n', drop=True).decode())
#     a = p.recvuntil(b'r = ')
#     r = int(p.recvuntil(b'\n', drop=True))
#     a = p.recvuntil(b'p = ')
#     p = int(p.recvuntil(b'\n', drop=True))
#     a = p.recvuntil(b'g = ')
#     g = int(p.recvuntil(b'\n', drop=True))
#     a = p.recvuntil(b'y = ')
#     y = int(p.recvuntil(b'\n', drop=True))
#     C = [0]
#     R = [0]
#     C.append(custom_hash(long_to_bytes(g) + long_to_bytes(y) + long_to_bytes(t)))
#     R.append(r)
#     t -= 1
t, r, p, y = [0] * 5 , [0] * 5, [0] * 5, [0] * 5
t[0] = 77203516334611379622052884088061339907818193440975523438325956774518601252973654146535129884349539724229562993644509567711307331031087898034347437358013492439542334272757402388064753335866738748958633246857368058563448192261648964749496750146315450540579452655462143813022469489449782882109182150142188348915
r[0] = 116220687218790216091346463439819557695309512176282372943331613053767874084111192169275378671194392846117055362014832383093646731110903547936340541580638231783076185430700045752799252596702505486227877656990705143138976716233681199944248043915239678537679463030334043412573684866623347545004904612417007327217
p[0] = 120199526136097511651530856988060166616679725925744594620901500430819054365226668422844742178771316732438545888088210068701212930988908604238682153307940417480817112540019257024703372145496636861112290091005527647339212358159274475077103689996624113678044891963676612334769162753166782062809526104431258739517
y[0] = 113120257926989954605307518460817169666869094087243249194566086300779837721481071230484420555504633839932263287201776945834030877253212748181186108770595676114564711457140738635967367144272960236148346810327070118837330648022676963528356209004524421455348155068637534190736042536625876773755044593606026293621

t[1] = 41870694301936832255997763505666386906032641226067235004722895066736677752643938706563985367861597477876358530714740119529535551345537670465925209944735777464498665457291484991606280197421907773811941255514802343059520166452091324746732103649911088736843268706303230993742963740287185306251052313964374872878
r[1] = 12006544761341621619143613218949584457040818188588282396732221317912350712975184254483346994199077889478797005980544401923303196850089656891130607887735812677733676744158835380723268515678724429529904080449565289532493612340517128773817940478185157405384294596975266141639161297056291778600404606940053392754
p[1] = 144976418899543143198454834264506179459614866666312385853989423989111679486891620786778481577472101748200328617607483446519049911023390427543308348494008425688855119367843479385647790189753163858970088620586505610840306212549243890107847423807556106192434514110736278569625282073529181653986405771513594134987
y[1] = 115167386542298909971939136044531943159982371048242494889745996527683250097061534781946312891817534916886291355204474301571862159042350976750824212346443402817552375266223638549309886922860921533308382456941718605370216473858986207878047206914813902333192745354148659984602740644345120423788258359941122554440

t[2] = 87583398382601313926735945950364130572816213989909728661491063393166341618362097121819023078210854251839415225428442334046015192711593400640556960974162962105001700930842075200923675422451049033428478754684678493480503204152097382431043129863362081278994409790477935899740897031656629902670114002180266872266
r[2] = 102934065416140965550261549535302192284582871606378190882003659362832329206469482926448881926648454509204767788745574969527319053553977362277660636749471357711849435384231867496681956889560719871615670691536165668156919285052370364456208767031233707532145718791283351837119192545712583721765194876277656314501
p[2] = 148550372281025138704553107737801144273208690400094973764375300381759456124146034135721454543469874579375457504983315183916592877731262163382903701414433532933994780412246010260680217160047355295328211970740422311833479381733781665221952277321615395653804905426059251551309558237245071919868160643681548296643
y[2] = 140198181685722792932050421128846820269664976993249152496309458637445728558822919357043381776927596511506695168631000635135982453895569864162220417018046902591680662347467431250148020323787868422484882819040766008876291088199475630773774170507517261352416744218315033367149608507518388781373699289693595499544

t[3] = 84522638373733120165414722745998118253921972157096808350845007682435315263298662575419222718886673005893479175168776223733676055437884841441137160382376309254755315837431162807596684224207280410300146602535834124511928134257346386899792746556440632650091111389088724071203568000188430042064500145669267026805
r[3] = 18194520459417696159241442307764982202112775710358263541565999857475375951014745782344717997571109484480490257320469370655821185611675102820226656623202547826938945675557174044055104570245311622199906030109190773202049867448625926033119474090713737736579814394048548084259404911639299693897437586858871472420
p[3] = 103592121279236435866010649601463899558315038309765463516665073413464806869884601040096405354399740035138081866812671953467986059509640644754973422603659825874707510675199511411437487386173194105311776020901134014680450244319046258962912483163270496586124101308664731409973532774829535980996775736651508448187
y[3] = 14795900031448786771559087453548388682769488350377145090464794455644462363656204398769939585810191748312766604278524960492756615478207654208905403516432929452543777164921589629460212814049479518397521600744701463005793924336243982001830978693343863219168046923285752069874971698699611717027496799587761203398

t[4] = 137831113158715069584199906447354477289195801883097400523644937444161950367086728510388467524069532778534704803863447302183665431906725284462350375136938902398115607751357373983532375894744842377064414618294084293556345908653608825144019924463160996581138334794345538184522294219836499291245958693984828705682
r[4] = 3997479613798995088324714249284251759517244566415954213469045459939167234231804927756343425727094668124791771030709612850292188541459113635924389128360073088700133564458353848851347410209029075825332791761713207287049093830610503236793718093861017715600469118069736180200062407135030791797264421955154056362
p[4] = 138872772594377036406739890812487312629206229880091799534033353266413752242871671695030657233827552372256865312690361457743622953282647882392520626248323032818493170021525093526571072858177665184977821075559705014261503440906447428819858532553737164369106583570452357928945241596555378143414688075425304805029
y[4] = 26528279589882510502916765470759890048295757018825608100058824483278181810141819962488106009190787416456734047325734614749916264837565765983213768504298640943869226245566784325431289650031372028094050672872660334951958006567067601586551947694563576820491204290496425885685101313332190690230962899252834723225

i = 0
C = [0]
R = [0]
while i < 5:
    C.append(custom_hash(long_to_bytes(g) + long_to_bytes(y[i]) + long_to_bytes(t[i])))
    R.append(r[i])
    i += 1
    
C = C[1:]
R = R[1:]
# print(C)
# print(R)

M = matrix(ZZ, 5, 5)
for i in range(3):
    M[i, i] = p[i]
    M[3, i] = C[i]
    M[4, i] = R[i]
    M[i, 3] = M[i, 4] = 0
M[3, 3] =  1
M[4, 4] = 2^64
M[3, 4]= 0
    
v = M.LLL()[0]
print(v)
flag = v[3] % p[3]
print(long_to_bytes(flag))

Attack on RSA with Low Public Exponent

作者简介:Xor0v0,硕士在读,零知识证明小白,目前在做一些circom开发和zk审计,密码学爱好者,打过一些web2/3 CTF,最近对zkHACK产生兴趣。欢迎各位大佬一起交流学习。

RSA 公钥加密系统自1977年被提出至今,目前仍然在很多领域被广泛使用,许多研究人员致力于找到其安全漏洞来攻克它。

在此期间,很多高明的攻击方法被提出。但至少在目前(2024初),在保证选取足够安全的加密参数情况下,RSA的加密强度仍然是现代电脑无法攻破的。

本小节,我们介绍其中一种高明的攻击方法,它源于 Hastad ,后由 Coppersmith 改良,当 RSA 使用一个小公共指数 时,攻击容易被实现。攻击思路基于找到低次多项式的一个小的根的算法,这个算法又需要使用 LLL 算法。这种寻找根的算法本身非常有意思,也用于其他攻击 RSA 系统的算法中。

1. RSA Review

让我们回顾一下 RSA 最简单的加密版本,同时也最能体现密码学和数学的高明之处的加密算法。该算法基于大数分解难题假设。

, 其中 是同等规模的大质数。令 是满足 ,其中 是乘法群 的阶。

我们称 为 RSA 模数, 为公共指数。 对是公钥,用于加密信息。 对是私钥,只有拥有私钥的人才能解密信息。给定要加密的信息为整数 ,为了对 加密,需要计算 。为了对 解密,需要计算 。算法的正确性可由欧拉定理证明。

2. Low Public Exponent RSA

在许多实际应用场景中,加密过程是在算力受限设备上进行的,因此将 计算到高次是非常消耗电量、时间的。因此有人尝试简化加密过程,把公共指数 设置为很小的数,比如 。如此一来,加密过程简化很多,只需要计算 ,使用两次乘法即可。

乍一看,在不知道 的因数分解的情况下,好像没办法恢复出密文。然而,正如本文所要介绍的,存在一些高明的攻击手段。

首先我们回顾一下中国剩余定理(CRT):给定 个等式 ,其中 互素。则存在唯一的 ,并且可以高效的找到这个

回到低指数加密场景中,假设 A 现在要发送 B、C、D 同样的信息 ,用各自的公钥分别加密,为: 。那么这种情况下,可以很容易恢复明文

不失一般性,我们假设这三个模数都是互素的(如果不互素,则可以找到公因子,然后直接恢复明文),攻击者可以由中国剩余定理计算一个值 。由于 成立,则 成立。那么我们计算的 ,最后我们对 开三次方即可。

上述攻击成功的前提是若干次加密都是使用同样的明文,那么如果给每个人发送的消息都不一样呢?考虑如下解决方案:每个人除了公私钥,还有一个唯一的 ID 号,现在 A 对 B、C、D 发送的加密消息变更为: 。其中 是消息 的比特长度。这种方式下,攻击者就不能通过上述中国剩余定理进行攻击了。

3. Coppersmith’ Method

事实上,对于上述修改方案,我们可以采用一个更一般的攻击方法,这需要使用到 Coppersmith’ Method。

Coppersmith’ Method的作用:设存在一个模多项式 。如果该模多项式的根为 ,即 ,且根足够小。那么就可以用 Coppersmith’ Method 去找这个小根。

首先,如果知道 的因式分解,那么这个问题是容易解决的,只需要分别在素因子的子群下解模多项式,然后用一个中国剩余定理即可。另外,如果我们能够找到一个解满足 ,并且这个 不等于 ,那么可以用欧几里得算法将模数分解。因此,我们不寄希望于有一个有效算法对于所有这样的同余式都能找到解,否则也就意味着大数分解难题可以破解了。

既然不能对所有的模多项式都能找到解,那么找到解的条件是什么呢?结论是:对于次数(degree)为 的多项式 ,如果 满足 ,那么这个解可以在多项式时间内找到。

First step

不失一般性,设 多项式为首一多项式: (得到首一多项式很简单,只需要对多项式乘以 。而如果 不存在,则我们找到了 的一个因子,这个同余式可以拆成同余式组,使用中国剩余定理即可)。假设存在一个整数 满足 ,我们的任务就是找到这个

我们想:如果存在另一个多项式 的根也是 ,且它的系数很小,那我们就可以通过求根公式或者牛顿迭代法将 求出。而Coppersmith’ Method 算法核心思路就是把 通过一系列变换规约成 。【注意 不是模多项式】

Example 1

,我们想找到一个小根满足 。【这里 ,但在整数域下

我们可以找到 满足 ,这个解可以用求根公式得到。

ok,这就是 Coppersmith’ Method 的核心思想。

接下来是讨论 的界的问题(多小的根算小根?)以及提高界的手法。

我们定义 为这个 的上界,然后我们把 用向量的形式表示:

Howgrave-Gramham 定理

给定模多项式 ,模数为 ,根的上界为 的向量表示为 ,满足 。那么,当 时,有

Proof:

根据柯西不等式有: ,当 时,柯西不等式变形为:

我们首先把 表示为 ,可得到不等式:

的上界代入有:

根据之前柯西不等式的变形有:

因此当 时,有

于是有: 。又因为: ,因此

这个定理(简称 HG 定理)对于估计根的界非常重要!!

之前 example 1 中 是直接给出的,下面介绍一下 G(x) 到底该怎么找?首先考虑 个多项式 ,还有 。显然它们均有解 ,因此我们对其进行线性组合之后它们仍然有解

谈到线性组合,那么就很容易联想到矩阵,我们讲这些式子的系数向量组合写成矩阵:

其中 取值的上界。

由于是下三角矩阵,则矩阵的行列式为:

我们对这个矩阵利用 LLL 算法进行格基规约,设规约后的第一行行向量为: 。根据 LLL 算法第一个性质有: 满足 。因此

为了满足 HG 定理,使得规约之后的向量( 的系数)“足够小”,使得我们可以很快的求出根,故要求 ,移项之后有: 。如果 ,则 ;如果 ,则

至此我们大致有了 的取值范围,但还没达到前面给出的结论 的程度。因此格子还有继续优化的空间。

Example 2

,多项式 。【这里根 ,因此满足

这里我们初步构想 ,则构造格子:

利用 LLL 格基规约之后,我们得到第一行向量为: ,消去 我们得到最终的系数 ,在对这个多项式采用牛顿迭代法求根即可。

from sage.rings.polynomial.refine_root import refine_root
M = 10001
X = 10
L = matrix(ZZ, 4, 4)
for i in range(3):
    L[i, i] = M * X ^ i
L[3, 0] = -222
L[3, 1] = 50000
L[3, 2] = 1000
L[3, 3] = 1000

v = L.LLL()[0]
# print(v)

p = 0
x = polygen(ZZ)
for i, coef in enumerate(v):
    p += (coef / X ^ i) * x ^ i
    
ans = p.roots()
# [(4, 1)]

最终得到的结果就是我们预想的

Full Coppersmith Method

回顾一下 Example 2,即使以 来计算边界,那么应该在 左右,那么为什么我们取 也能计算出正确结果?而且,如果把 代入 ,那么 X 的边界值应该在 左右。所以为什么我们能得到正确结果呢?

因为其实这个边界值也并不是很严格,在推导得出这个值的时候本身就用了很多次不等式,再者,我们利用的LLL中的那个性质,我们取的是 LLL 算法规约出来的最坏的情况,而大多数情况得到的结果要比这值小许多。

回到不等式: ,再往前还原是: ,其中 是格的维度。

观察这个不等式,我们发现,要增大 ,有两种方案:1. 增大 ;2. 增大

针对第一种方案,我们称往格里增加的格的维度,而不增加 的多项式为 x-shift polynomial。它们是 。显然这些多项式的解都为

第二种方案,可以增加 的幂次来增加 。由于 ,则有

在 Example 2 中,我们的格子的维度为 ,我们带入不等式 ,得到 。现在我们往格子里添加 x-shift polynomials ,新的格子为:

现在格子的维度为 ,再代入不等式,我们得到 。确实增大了

一个现成的结论是:当我们给格子增加 x-shift polynomials,可以使得 。那么如果当我们使用第二种方案继续增加 呢?

Coppersmith 定理

次首一多项式,如果在有限域 下,有一个或多个根满足 ,那么我们就可以在与 相关的多项式时间内找到它。

证明过程省略(如果实在没找到过程可以找我一起探讨)。现成的结论:Coppersmith’ Method 的大致时间复杂度为:

既然气氛烘托到这,那么出道趣味题让大家练练手吧!

,并且告诉我们 。请解出 。【不要用中国剩余定理,用构造格的方法。答案是:16384】

4. Attack

介绍 Coppersmith’ Method 这个寻小根算法之后,我们在来回到如果攻破改进之后低公共指数的 RSA 加密系统。

假设 个互素的整数。设 为最大阶为 的多项式。假设存在唯一的 ,使得 都成立。那么。如果 ,可以有效的从 中找到

其中 就对应“改进后”的低公共指数的 。它是一个低阶多项式,可以使用 coppersmith’ method 找到那个根

不久我会单独整理一份 jupyter notebook 用于记录 RSA 加密系统中的那些“高明”的攻击技巧。

签名是不是一种零知识证明?

一般的数字签名应该不属于interactive proof 因为不具有soundness的性质 也就不属于ZKP 但是比如schnorr签名 存在soundness的性质 是一个zkp

@洪澄: 首先得看是对什么信息的签名,如果是对有意义的信息比如文档,聊天记录的签名那就不是零知识的,因为这个签名本身就算是一个额外的知识。

至于对于随机挑战的签名是不是对于“我有某个公钥对应的私钥”的zkp,是难于blackbox的分析的,得描述协议再论证。例如schnorr确实是。

@Kurt: Schnorr签名不存在soundness性质,要求的是「不可伪造」性质, 但是Schnorr identification protocol (Sigma protocol)具有special soundness性质(和HVZK性质)

零知识证明是不是一种多方计算?

[TODO]

图灵机 电路计 算模型

CPU就是一个用(布尔门)电路模拟出来的通用图灵机,机器指令就是用门电路逻辑实现的,FPGA就是再用电路去模拟特定的图灵机。 但谁说图灵机必须要用到电路?图灵机只是个计算模型,我用纸带和笔也能构造出来一台通用图灵机,再比如你不知道外星人是不是也用门电路来做“CPU”,但邱奇-图灵论题就认为全宇宙的计算模型都一定会和图灵机等价。 反过来说图灵机也可以用来模拟电路,Verilog语言以及量子电路编程语言都是这类。 Lambda Calculus也是另一个图灵完备的计算模型,日本过去的“五代机”计划就是在用集成电路直接实现Lambda Calculus计算模型的机器。 图灵完备的计算模型还有很多好玩的呢,比如递归函数,比如元胞自动机/生命游戏。 SNARK里道理也是一样,只是一般的target是VM这一级别,当然zkCPU理论上也没问题,再当然人脑通过训练学习去成为人工zk编译器也没啥不可能。

信任机器 如何体现

自从 2016年 《经济学人》提出来的「区块链是信任机器」的说法,我就在一直思考这个问题:「信任机器」如何体现。自从深入思考零知识证明以后,我慢慢发现他们殊途同归,只是各自的维度不一样,区块链解决的是「分布式计算的信任」,零知识证明解决的是「数据的信任」。如果再加上形式化验证,就可以解决「逻辑的信任」。

这三个点:逻辑 <-> 计算 <-> 数据 才真正构成了一个闭环,也许才能真正实现「信任机器」这一构想。

ref: https://secbit.io/blog/2019/07/14/zkpod-short-intro/#back-to-top

Kurt :

「计算」是对「数据」/「信息」的处理/变换过程;

「证明」是对「命题」/「符号」的推理/推导过程;

「逻辑」是正确的「推理」过程;

「程序」是写出来的「计算」过程(「算法」);

「证明」是写出来的「逻辑」(「推理」)过程;

「知识」是「计算能力」;

Von Neumann architecture: 程序 == 数据 ;

Curry–Howard isomorphism: 程序 == 证明;

所以「证明程序的integrity」== 「证明推理符合正确的逻辑」

因为程序==证明,所以「formal verification」可以在计算机里实现。verification也是计算,但是是符合逻辑系统推理规则的计算过程(程序);

「witness」就是「non-determinism」计算中的计算路径信息;「proof」就是「witness」的压缩编码;

NP-proof system中,proof generation 计算过程就是 verification计算过程的 non-deterministic version;

ZK-proof system相比而言添加了对witness的压缩编码。

那是不是说明zkproof的生成和验证过程就是formal verification?

本身不formal (formal的意思是符合特定逻辑系统,我不认为PIOP这种模型目前能达到formal的程度),要加 typing rule/ dynamic semantics rule等等。 比如见: https://eprint.iacr.org/2023/657.pdf

就是说零知识证明只能保证计算的完整性,如果你一开始写的智能合约逻辑就错了,就有漏洞的话,还还是一个有bug的合约。形式验证应该是可以保证算法逻辑就是正确的没有漏洞?

是的,计算完整性和计算正确性是两个东西:计算完整性是保证计算的「输出」是正确的(是和计算一致的),计算(语义)正确性是说计算本身是无bug的。(ZKP里的completeness是说真命题可以生成有效证明;MPC里的correctness是说协议可以正确实现functionality)

FV 形式化验证可以使用逻辑模型对程序逻辑进行部分检验,可以排除常见bug,但通用debugger是不存在的(因为「图灵停机问题不可判定」)。

R1CS

r1cs 的基本公式是 AX*BX=CX* 是hadmard product,X是变量向量。 请教一个基本问题是,r1cs里的r1,是否指某个matrix的rank=1?如果是,那么是哪个矩阵的秩为1?

这里的 秩1约束 实际上和组合优化/凸优化 里面的秩1约束是一个东西。实际上就是说 这里的约束条件 是秩1的。其实就是 ,因为A B都是向量。

正解,我再来重述一遍:考虑一个约束 ,乘法部分可以重写为 ,即为变量为 x 的「二次型」。而矩阵 的秩一定为1 ( ),因为向量 outer product 生成的矩阵每一列都线性依赖(正比)于特定一列(比如第一列),其rank一定等于1。这就是Rank 1的由来。

那么non-rank1 constraint指什么?

区分两个概念:rank-1 constraint vs degree-2 constraint。最终只出现一次乘法的都叫deg2 constraint,但只有二次型矩阵M=(ab^T)的才叫rank1 constraint。换句话说,一个deg2 non-rank1 constraint就是二次型矩阵M的秩大于1的情况(就不对应于一个fan-in 2的算术电路乘法门了),比如rank=2时可以称之为R2CS。Justin的意思是R1CS真正的局限并不在于没有R2CS不能custom constraint,而是在于不支持higher degree (>2) constraint,比如fan-in > 2 的乘法门。

能举个degree2+non rank1 的例子?

因为rank1就已经可以对fan-in 2 算术电路(的乘法门)进行约束了,那么在算术电路语境下就一定举不出来一个“例子”,因为没有必要性。二次型x^T M x的矩阵M并没有限制rank=1,所以rank>1也一定可以表示某种约束,只不过不是对算术电路的约束。也许可以举出凸二次优化里的例子,但我并不熟悉。

extractor

Prover是一个黑盒,extractor以任意方式(包括存档取档)“把玩”这个黑盒,最后能提取出witness,这说明黑盒里面真的有witness。

整个目的是为了要证明证明者真的有witness,毕竟都能抽取出来

怎么理解 zk 的前后端?

有两种解释:

  1. 前端=算术化/后端=证明系统
  2. 前端=密码学编译器(比如多项式承诺)/后端=信息论证明系统(比如IOP)

yingtong有一个讲义 在论述这个 proof system的stack https://assets.super.so/9c1ce0ba-bad4-4680-8c65-3a46532bf44a/files/e11309fb-7356-42ad-9c78-565341abd80d.pdf

我是在想,或者说我的观察是,一旦一种表达形式确定了,它的 iop 基本上也被框定的,比如说AIR,可能就是FRI,假如转换成 pair based 大家会觉得我有病

why Knowledge soundness is not a meaningful notion for the sumcheck protocol?

好问题!直观上说是因为 sumcheck 要证的statement 没有(多项式长度的)witness,要证的就是一个evaluation 的 summation = a value,verifier 有本事自己也能自己算一遍(可惜一般他没有这种计算能力)。 往深了说KS抽取一个witness这件事有一个前提,就是要「存在」一个多项式长度的witness,这正是NP的定义,所以一般KS性质都是针对NP语言内的讨论。而sumcheck可以证的语言比如UNSAT/#SAT/TQBF,这三个语言都不是NP的(分别属于coNP/ #P/ PSPACE)。

什么 snark 协议需要 Knowledge sound,哪些只需要sound就够了

  • 直观上说是因为sumcheck要证的statement里没有(多项式长度的)witness,要证的就是一个evaluation 的 summation = a value,verifier有本事自己也能自己算一遍(可惜一般他没有这种计算能力)。 往深了说KS抽取一个witness这件事有一个前提,就是要「存在」一个多项式长度的witness,这正是 NP 的定义,所以一般KS性质都是针对NP语言内的讨论。而sumcheck可以证的语言比如 UNSAT/#SAT/TQBF ,这三个语言都不是 NP 的(分别属于 coNP/#P/PSPACE )。
  • 证明a statement is true和我「知道」「为什么」this statement is true中间是有gap的;反过来后者KS蕴含soundness,我「证明statement is true+证明知道为什么statement is true」蕴含「证明statement is true」。
  • 去证明一个「数学定理」is true的时候,验证者其实不太关心一定要知识抽取的,只要让我相信定理is true就够了,上面sumcheck证的语言都是这种。
  • 而对于KS必需的情况举一个例子:对一个地址,我用SNARK证明了对应的私钥(比如离散对数或哈希原像)是「存在」的。这完全不能用!我得去证明我「知道」这个地址相对应的私钥(而不仅仅是私钥存在),我才能去发起交易。
  • 此外还有的情况下,KS性质都是不够的!比如可以对不包括在约束中的instance更改以进行malleability attack。如果还要在这种攻击存在下依然安全,那要满足simulation extractability (SE性质)。(UC-secure imply SE)。SE最新相关内容有一篇欧密23的文章 Spartan and Bulletproofs are Simulation-Extractable (for Free!)

注: malleability attack 可见 the halo2 book

zk-everything

零知识证明由于其本身陡峭的入门学习曲线,往往被初学者称为moon math。为了平缓学习曲线,减轻入门压力,babysnark[1]应运而生,本文将作为babysnark原理部分的一个解读版,帮助你更好的理解snark背后的一些基本概念和直觉。在阅读本文之前,希望你已经读过# 从零开始学习 zk-SNARK系列的前4部分,对包括有限域、椭圆曲线等相关知识有一个基本的了解。

R1CS

比如我们有这样一段程序:

def qeval(x):  
	y = x**3  
	return x + y + 5

我们知道程序执行实际上是CPU中的乘法门和加法门组合运算得到的。那么上面的程序可以看成是类似是下面的这个图,有一些输入变量和中间运算过程,最后得到输出。

alt_text

为了更好的表示中间过程是如何执行的,我们需要将上述程序拆分写成如下形式,左侧是中间运算的输出结果,右侧可以看成中间运算的输入:
sym_1 = x * x  
y = sym_1 * x  
sym_2 = y + x  
~out = sym_2 + 5

为什么我们输入一定要写成两个变量而不能是三个或者多个变量呢?具体限制原因可以从限制运算[3]中找到答案。简单来说,多项式的算数性质有在某一个具体的点上,左操作数和右操作数相乘等于输出结果。而这个约束特点使得每一次输入只能是两个数的形式,如果一次有多个变量作为输入,可以分别将其拆分成两两组合。

有了这样的直觉之后我们可以来看一下R1CS(Rank 1 constraint system)的具体定义:

给定三个m行n列的矩阵 , 和一个 维向量 定义了一组m个方 程,每个方程的形式如下:

其中 , ·表示矩阵和向量的乘积, 表示 的第 个元素。 等价地,我们可以使用Hadamard积(逐元素相乘)来表示整个系统:

其中○表示Hadamard积。

其中A可以看作是左操作数的全局结果的矩阵表示,B可以看成是右操作数全部结果的矩阵表示。C是运算结果的全部结果的矩阵表示。接下来让我们一步一步将上述4个等式转变成矩阵的Hadamard积的形式。

假设我们将上述4个等式的输入输出变量按如下顺序排列:

'~one', 'x', '~out', 'sym_1', 'y', 'sym_2'

那么对于第一个等式

sym_1 = x * x

左操作数a,右操作数b和最后结果c可以分别表示成如下向量形式

a = [0, 1, 0, 0, 0, 0]  
b = [0, 1, 0, 0, 0, 0]  
c = [0, 0, 0, 1, 0, 0]

然后向量和上述6个变量相乘,就可以还原出第一个等式了。类似的,我们对等式2,3,4做同样的处理,最终可以得到矩阵A,B,C:

A  
[0, 1, 0, 0, 0, 0]  
[0, 0, 0, 1, 0, 0]  
[0, 1, 0, 0, 1, 0]  
[5, 0, 0, 0, 0, 1]

B  
[0, 1, 0, 0, 0, 0]  
[0, 1, 0, 0, 0, 0]  
[1, 0, 0, 0, 0, 0]  
[1, 0, 0, 0, 0, 0]

C  
[0, 0, 0, 1, 0, 0]  
[0, 0, 0, 0, 1, 0]  
[0, 0, 0, 0, 0, 1]  
[0, 0, 1, 0, 0, 0]

通过上述操作,我们就将一段程序转换成了R1CS的形式。

多项式插值

在实际的零知识证明系统中,不管具体零知识证明算法是哪种,总要有一个validator发出一个随机数作为challenge,然后prover接受这个随机数作为系统输入,然后返回一个输出结果。validator拿到输出结果看是否和挑战的随机数满足某种对应关系,如果满足就认为prover确实掌握了某种知识。为了实现validator可以找任意随机数,所以我们就有必要R1CS的约束关系转换成多项式的形式。

比如对于之前的矩阵A而言,如果竖着按列看,其实其对应的就是之前文中所说的6个变量

'~one', 'x', '~out', 'sym_1', 'y', 'sym_2'

比如说,对于one变量而言,其在上述4个等式(即4种约束关系)中所组成的向量为

~one: [0, 0, 0, 5]

如果将其在笛卡尔坐标系中表示,假设我们选取x为1,2,3,4,那么该one所组成的多项式应该经过(1,0), (2,0), (3,0), (4,5)这4个点。在笛卡尔坐标系中,我们对于做操作数和有操作数以及结果的所有x坐标只要满足一致关系,他们所组成的多项式都满足R1CS约束关系。基于上述特点,我们可以对6个变量选定一致的x坐标然后使用插值的方式得到多项式的形式。下面是我们选定x坐标是1,2,3,4得到的矩阵A的多项式表示形式:

A polynomials  
[-5.0, 9.166, -5.0, 0.833]  
[8.0, -11.333, 5.0, -0.666]  
[0.0, 0.0, 0.0, 0.0]  
[-6.0, 9.5, -4.0, 0.5]  
[4.0, -7.0, 3.5, -0.5]  
[-1.0, 1.833, -1.0, 0.166]

即one可以表示为:

其他变量的R1CS转换也同理。

QAP

这种转换成的多项式新形式称之为QAP(Quadratic Arithmetic Program)我们来看一下QAP的具体定义。

定义(QAP): 一个在域 上的二次算术程序 包含三种 多项式:

  • 其中 ,以及一个目标多项式

假设 是一个算术程序,它以 的元素为输入并输出 个元素,总共有 个I/O元素。那么,当且仅当存在系数 使得 可以整除 时, 的输入和输出的有效赋值,其中:

布尔电路

通常情况下一般的通用snark算法使用的是QAP来去表示程序,但如果程序是一些特殊问题,比如输入程序可以表示为布尔电路,那么QAP实现就可以更加简单一点。首先我们来看一下布尔电路的特点:

alt_text

从图中可以看到不管是哪一种的门,最终的输出结果一定是落在[0, 2]区间之内。具体来说:任何一个2输入的二进制门电路 ,其中输入为 ,输出为 ,都可以使用门电路的输入和输出的仿射组合 来指定,当输入输出满足门电路的逻辑规范时,它只能取两个值, 。这导致了一个等效的单一的“平方”约束

SSP

根据上述布尔电路的特点,一般的QAP约束在布尔电路中就转换成了SSP(Square Span Program)约束。我们来看一下SSP的具体定义:

定义(SSP):在域 上的一个方形跨度程序(SSP)是由 个多项式 和一个目标多项式 组成的元组,使得对所有 ,都有 。我们说方形跨度程序SSP的大小为 ,并且度数为 。当且仅当存在 ,使得 能够整除 时,我们称SSP接受输入 ,其中:

我们说SSP校验了布尔电路 ,如果它仅接受那些满足 的输入值

再进一步,我们可以根据SSP而具体的布尔电路构造方形约束系统(Square Constraint System)。我们首先来看一下SCS的定义:

定义SCS: 方形约束系统由一个矩阵 定义。如果满足以下条件

其中 表示Hadamard(逐元素)乘积,那么向量 是此系统的解。我们也将 写为

我们可以看一个具体的例子,比如我们有3个布尔元素分别是 : 对于布尔元素而言,比如说 要么为 0,要么为 1。注意到

这意味着 ,从而推导出 。其他元素也是同理。对于

综合上述内容,一个包括上述导线和门的方形约束程序将采取以下形式:

babysnark

介绍了这么多,终于到babysnark了。babysnark是对布尔电路所构造的一种snark。相比于QAP而言,SSP更简单,所以实现整个snark所需的约束也更少。具体来说一共有两个约束,第一个是SSP约束:

不需要做太多解释,第二个约束是线性约束:

这个和babysnark具体设计有一些关系。 的值是由prover直接计算的,而 的值来自于setup阶段。设置线性约束的目的是确保 确实是由同一线性多项式计算出来的,防止prover作弊,恶意构造 而不是赖在setup所提供的随机challenge构造的 ,最终破坏SSP约束。因为prover最后输出证明的时候同时提供了 在verify阶段添加 是为了防止证明者输出特别恶意构造的 B=YV,所以再做一次线性约束。

babysnark的随机挑战采用的是 的形式,该构造形式的安全保证来自q-DLOG 假设。q-DLOG 假设确保即使敌手可以在多个点上观察到多项式的值,他们也无法从多项式的结构中提取任何信息。

至此,我们对babysnark的原理部分做了详细的探讨。希望通过深入浅出的方式介绍这一简易的snark,能为你的零知识证明学习之旅提供坚实的基石。

Reference

[1] BabySnark do do do

[2] quadratic-arithmetic-programs-from-zero-to-hero

[3] 从零开始学习 zk-SNARK(三)——从程序到多项式的构造

[4] zk-SNARKs: A Gentle Introduction

KZG

KZG 承诺又叫做 KZG10 承诺,是由 Kate, Zaverucha, and Goldberg 三位作者共同提出.

1.多项式表示

多项式 P(x)可以用系数表述,如简单可表示为

,所以对于一个多项式 P(x)可以表示为,其中表示对应位置的系数.

2.Commitment Scheme

2.1 Commit Schemes 过程:

可以把承诺 C(m)理解为一个装着信件 m 的信封

  • Setup 阶段产生一些公共参数
  • Commit 阶段:对消息 m 进行承诺得到 C(m)
  • Open 阶段:打开 C(m)得到 m‘,验证 m是否等于 m’. commit 阶段的 m,在 open 阶段是会暴露的.

2.2 commit Schemes 性质:

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会得到 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

2.3 Polynomial Commitment Schemes:PCS

多项式承诺 PCS:承诺对象是单变量多项式,:表示所有 degree 最多为 d 的单变量多项式的集合。过程可总结如下图

  • Prover 运行 Commit 算法,将函数 f 与随机数 r 作为输入,为输出.将发送给 Verifier
  • Verifier 发送一个挑战点:即一个函数域 X 中的元素 x
  • Prover 将 x 对应的 f(x)=y,以及 proof 发送给 Verifier. 表明 1.f(x)=y 2.f 属于 F,即 f 的 degree<=d.

其中 Prover 需要计算如下内容

  1. 多项式的承诺 C=[P(x)]
  2. 多项式在 z 点的值,P(z)=y,这很简单
  3. the proof

PCS 有多种,比如 FRI or Dark’20 or Dory’20 .但是 KZG 仍然是目前实践中使用最为广泛的 PCS 方案.其特点如下

  1. 基于 Pairing 实现
  2. Proof size 是常量 (一个椭圆曲线群元素)
  3. 验证时间是常量 (两次 pairing 操作)

其中特性 2 与 3 导致可以将其构造成一个 SNARK 方案.SNARK 的全称是 Succinct Non-interactive Argument of Knowledge:简洁非交互式知识论证.

SNARK 要求 1.size of proof=O(log(d)) 2.time of Verification =O(log(d)),d 为 degree of Polynomial.

进而可以将 KZG 应用在零知识证明系统如 ZK-SNARK 中.

3.计算多项式的承诺 C

在计算之前,首先介绍两个概念

3.1 椭圆曲线(EC)

这里只简单提一下椭圆曲线,更多细节可参考阅读 basic elliptic curve cryptography series.

假设是由椭圆曲线点构成的群,g 是的生成元.

用符号[x]表示.由于椭圆曲线的离散对数难题,给定 g 与[x],但无法逆推出 x.

3.2 Trusted Setup

对多项式进行承诺,需要一个与多项式系数数量一样长的 structured reference string(SRS)。该字符串必须按照指定的方式生成,并提供给任何希望承诺多项式的参与方。生成过程会产生一个秘密值 s,也称为 trapdoor 或者 toxic waste),必须将 s 其丢弃。换句话说,生成参考字符串的任何一方都知道一个信息片段,该信息可以破坏多项式承诺方案的 binding 性质,从而破坏使用该承诺方案的任何证明系统的正确性。生成这样的 SRS 过程被称为可信设置(trusted setup).

设 D 是希望支持承诺的多项式 P(x)的最高次数上界, SRS =.

目前主流是通过 Ceremony 生成 SRS,关于 Ceremony 的详细细节可参考 https://mirror.xyz/privacy-scaling-explorations.eth/naTdx-u7kyirczTLSAnWwH6ZdedfTQu1yCWQj1m_n-E

Ceremony 的思想与 MPC 类似,让 N 名参与者生成自己的秘密,并按顺序将其添加到主秘密中。只要有一个参与者不泄露秘密,那么主秘密就是安全的。主秘密的生成过程被称为 Ceremony.

可进入 https://ceremy.ethereum.org 参与以太坊社区组织的 KZG Ceremony 的生成过程,成为其中一名贡献者!

3.3 combine Trusted Setup and EC

  • Trusted Setup 阶段生成 SRS=(),n 为 P(x)的 degree.然后将 s 丢弃。任何人都可以访问 SRS,但是无法获得 s 本身.

  • 通过 SRS 重新构造多项式 P(x)为[P(s)],而不会暴露 s 与多项式本身

上式中,用秘密 s 替换自变量 X,得到 P(s):因为自变量 x 可以表示为任何值,.这不影响多项式本身.进而得到承诺 C=[P(s)]

4.计算 proof

我们需要 proof 证明 P(z)=v.构造前先引入一些 polynomial math.

P(x)的零点为 m,即 P(m)=0.那么 P(x)一定能整除(x-m),即存在一个商多项式 q(x).使得

想要证明的是 p(z)=v,结合上述 polynomial math.可做如下变换.

p(X)-v=0 when X=z,则 p(X)-y 能整除(X-z) ,即,即.

也把 q(X)称为**“Witness Polynomial”**

对于,不能直接利用这个等式,因为等式中的**s **两方都不知道

直觉上,我们希望直接证明等式 [p(s)-v] = [q(s)*(s-z)]成立,从而完成验证.

等式左边:

承诺仅满足加法同态:,所以[p(s)-v]=[p(s)] -[v]

等式右边:

在验证过程中,验证方会收到证明方发来的[p(s)] ,同时验证方自己可以计算[s-z]=[s]-[z]的值

但是由于椭圆曲线上不满足乘法,即乘法****同态:[p(s)]*[q(s)] =[p(s)*q(s)]

所以等式**[q(s) * (s-z)]= [q(s)]*[s-z]** 并不成立,需要引入配对 pairing

因为椭圆曲线上的运算是一个加法群,而不是一个乘法群,乘法没有被定义。

这里需要强调的是,单个运算结构其实并不区分加法乘法,a o b 这个 o 把它称作成什么都行 只是在有限域上的椭圆曲线点集构成一个加法群,把它称为加法是更符合习惯。 我们区别加法与乘法,比如两种运算的代数结构比如环,域。 因为有两种运算,需要做区分,因为涉及到分配律,谁对谁分配的问题,所以会很明确的区分加法与乘法。

5.Pairing

Pairing is **a bilinear mapping.深入学习Pairing可参考《Pairing for beginners》**这本书,在这里只做简单介绍.

  • bilinear
    • Linearity:对于某些一元函数,如果该函数服从
    • Bilinearity:对于二元函数,Linearity 存在于所有维度中,即

  • bilinear mapping 双线性映射是一个函数,它从两个向量空间的元素产生第三个向量空间的元素,每个参数都是线性的.

5.1 about pairing

配对是⼀种抽象操作。其定义可能会有所不同。 有 Tate 配对、Weil 配对、Ate 配对等等…… 虽然每⼀个都通过不同的操作来定义配对,但是Input与output的格式,pairing 的属性都是固定的.

Input:

output

n 阶乘法群中的整数(或复数)

分别是对称与非对称的 Pairing 形式。在实际中,非对称 Pairing 效率最高。

properties:

  • e(P, Q + R) = e(P,Q) * e(P, R)
  • e(P + S, Q) = e(P,Q) * e(S, Q)
  • (bilinear)
  • e(P, Q) ≠ 1 (non-degeneracy property)

5.2 Pairing examples

1.e(x, y) = 2ˣʸ

例: 请举例在实数域中 e(x, y) = 2ˣʸ 是双线性函数.

  • e(3, 4 + 5) = 2³˙⁹ = 2²⁷
  • e(3, 4) * e(3, 5) = 2³˙⁴ * 2³˙⁵ = 2¹² * 2¹⁵ = 2²⁷.
  • 通过 pairing 证明知道 x² - x - 42 = 0 的解, 然而并不透露这个解的具体数值.

如果 成立, 那么 k 必须为 0 或者目标群的倍数.

如果存在 , 可以确定原始二次方程式成立. 使用双线性性重写方程 .进一步,e(xG, xG) ⋅ e(xG, -G) ⋅ e(G, -42G) = 1.

因此只需要提供 xG 的值. 同时由于椭圆曲线的离散对数问题, 从 xG 反推回 x 是困难的.

2.解决 Diffie-Hellman 难题

3.BLS签名

6.KZG

回到KZG部分

分别是同一椭圆曲线的两个子群.g 是子群的生成元,h 是子群的生成元

生成元的选择通常在 trusted Setup 阶段选择

define pairing e: ,对于秘密 s 也相应有两个分布.即SRS

原来要验证的等式: =>

分布集1: ,对应生成元为g。计算π、C、

分布集2: ,对应生成元为h。计算

验证者验证等式:

简单理解这个等式:

[x]g 与 g^x 表述形式不同,本质上没有什么区别。 a o b =c 如果群运算定义为加法,就使用[x]g 这种形式 如果群运算定义为乘法,就使用 g^x 这种形式

用黑盒来理解这个等式的话,就等价于在群中去验证下面乘法的成立

Verifier 如何进行验算:

  • prover 发送,C,v
  • Verifier 自己选择的 z,根据加法同态,Verifier 可以计算_[s -z]₂=[s]₂ - [z]₂_
  • g,h is public,pairing function is public.

KZG 完整过程:

  1. 通过 Trusted setup, 产生 SRS:[sⁱ]₁, [sⁱ]₂.
  2. Prover 使用_[sⁱ]₁_,对多项式 P(x)进行 commit,得到 C = [p(s)]₁, 发送给 Verifier.
  3. Verifier 选择挑战点_z _∈ {0,…,_p_−1}
  4. Prover 发送 π 、y 给 Verifier:
  5. Verifier 检查等式: _e(π, [s -z]₂) = e(C -[v]₁, H) _ if the equation holds, the verifier accepts the proof if the equation does not hold, the verifier rejects the proof

KZG 分析

对 KZG 的 Corretness Binding hiding 分别分析

  • Corretness

等式左边:

等式右边:

  • hiding

因为椭圆曲线的离散对数难题,敌手拿到[x]无法得到 x.

  • Binding

分析 Binding 前,需要介绍 SDH 假设。

Strong Diffie-Hellman(SDH) 问题定义如下:

给定(q+1)长的元组 作为输入,输出

SDH假设就是不存在多项式时间算法可以不可忽略概率解决 SDH 问题。下面用对称形式的 Pairing 进行分析

后续 pairing 的验证都是“g 的指数上”在进行验证,为了方便起见.省略底数 g,后续的等式都是在指数位置上进行.

反证法,即KZG不满足 binding,那么 open 承诺 C 可以得到值 v 和 v’,承诺方必须确定两个不同的值 y 和 y’,使得下列等式成立:

因为,假设 , 等式两边同时除以可得:

,即,这说明有人可高效计算出,这违背了SDH假设.

总结:

像之前说的那样,KZG 方案的 Proof size 是常量 (一个椭圆曲线群元素),验证时间也是常量 (两次 pairing 操作),这是其优点.但是其最大缺点是需要一个 Trusted Setup 阶段.

7.Batch-KZG proof:multi proof

上述过程验证了⼀个在单点上求值的多项式。但如果想证明⼀个多项式上在多点上的值,就必须⼀次⼜⼀次地重复同样的协议 (back and forth)。这显然是没有效率的。为了解决这个问题,需要 “批量 “验证多项式上的点。

假设想证明 k 个点上的值:

通过使用拉格朗日多项式插值法,构造一个经过上述 k 个点对的 k-1 次多项式

n+1 个坐标对的形式 可以唯一的恢复出一个多项式

**原多项式 P(x)构造的 I(x)**都经过 k 个点对,所以多项式 P(x)-I(x)=0 在如下点上满足

即多项式能够整除

定义一个 zero polynomial:

则下式成立

定义 kate multiproof for the evaluation of these points:

验证过程如下:

  • Verifier 通过 k 个 points(z,y)计算 Z(x)和 I(x)
  • Verifier 计算
  • Verifier 验证等式是否成

8.KZG in ZK-Rollup

在 zk-rollups 的情况下,想证明发生在 L2 上的一些计算是有效的。简单来讲,发生在 L2 上的计算可通过称为“ witness 生成”的过程表示为二维矩阵。然后可以用多项式列表来表示矩阵 - 每列都可以编码为其自己的一维向量。然后,计算的有效性可以表示为这些多项式之间必须保持的一组数学关系。例如,如果前三列分别由多项式 a(x)、b(x) 以及 c(x) 表示,可能需要关系 a(x)⋅b(x)−c(x)=0 保持。多项式(代表计算)是否满足这些“正确性约束”可通过在一些随机点评估多项式来确定。如果“正确性约束”在这些随机点上得到了具体的满足,则一名验证者可以非常高的概率断言计算是正确的。

很自然地看到像 KZG 这样的多项式承诺方案,是如何直接插入到这个范式中的:rollup 将 commit to 一组多项式,它们一起代表计算。 然后,验证者可要求对一些随机点进行评估,以检查正确性约束是否成立,从而验证多项式表示的计算是否有效。

最后感谢@Kurt-Pan的指导与建议

参考文章

Understanding KZG10 Polynomial Commitments (taoa.io)

Kate Commitments: A Primer - HackMD

Dankrad Feist’s kzg commitment post

https://blog.subspace.network/kzg-polynomial-commitments-cd64af8ec868

Understanding KZG10 Polynomial Commitments

Committing to lunch (taoa.io)

book:Proof,argument and zero knowledge

KZG原始论文

Lecture1:Introduction to Zero knowledge Interactive Proofs

NP

NP Proof:

NP-proofs 属于可高效验证的 proofs.其中要求

1.Witness 的长度应当是 statement x 的长度的多项式表示.

2.Verifier 时间是 x 长度的多项式函数表示

NP proofs 例子

上述例子都可以用一种通用的语言关系 L 来表示

更具体而言,上述都是 NP 问题:(简单来说,求解困难,但是验证高效的问题)

  • P NP NPC
    • P 问题:指能在多项式时间求解出的问题.如 2SAT,欧拉路径,PATH 问题
    • NP:Nondeterministic polynominal(非确定性多项式) 一个问题不能确定是否能够在多项式时间内找到一个解。但若给出一个解,能在多项式时间内证明这个解是否正确 .如果找到一个解,那么 NP 问题就变成了 P 问题,所以 P∈NP 类 注:NP 问题不能理解为非 P 问题 著名的 NP 类问题:旅行家推销问题(TSP)。即有一个推销员,要到 n 个城市推销商品,他要找出一个包含所有 n 个城市的环路,这个环路路径小于 a。如果单纯的用枚举法来列举的话会有(n-1)! 种,已经不是多项式时间的算法了,阶乘比多项式复杂。假设有人猜几次就猜中了一条小于长度 a 的路径,TSP 问题解决了。可是,人们不可能每次都猜的那么准。所以说,这是一个 NP 类问题。也就是,我们能在多项式的时间内验证并得出问题的正确解,可是我们却不知道(非不存在)该问题是否存在一个多项式时间的算法能解决 NP****问题的本质是单向性,不可快速求解,但是能够快速验证
    • NPC: 规约:问题 A 可以转化为问题 B,对于难度而言,问题 B 比问题 A 要困难。规约具有传递性:A 规约至 B,B 规约至 C,那么 A 规约至 C。一直规约下便会得到 NPC 问题 所有的 NP 问题都可以约化成 NPC 问题。只要解决了这个问题,那么所有的 NP 问题都解决了。NPC 需要满足两个条件.1:是一个 NP 问题 2:所有的 NP 问题都可以约化到它。如 SAT 问题、HAMPATH 问题都属于 NPC 问题.
    • NP-hard 问题 它满足 NPC 问题定义的第二条但不一定要满足第一条(就是说,NP-Hard 问题要比 NPC 问题的范围广,NP-Hard 问题没有限定属于 NP),即所有的 NP 问题都能约化到它,但是它不一定是一个 NP 问题
    • P NP NPC NP-hard 关系

NP Language 定义:

给定二元关系,记语言 L(R)为集合.称一 个语言 L(R) 是 NP 语言当如下两个条件成立:

  • |w|=poly(|x|)
  • 给定任意的 x、w, 存在多项式时间算法能够高效判定 R(x, w) =? 1

注:此时还与零知识无关

考虑有没有其他方式,比如在大素数分解 N=PQ 中,不暴露 P 或 Q 的值让验证者相信这些类型的定理

ZKP

引入交互随机

交互

验证者不再被动地阅读证明,相反验证者会与证明者进行重要的交互。

Prover 与 Verifier 之间进行多项式步骤的****交互

随机

Verifier 不再是一个确定性的算法

Verifier 的问题通过抛硬币的方式,即问题在某种程度上是不可预测的

随机性的本质是接受小概率的错误,但这种概率应当被量化,比如小于某一个可忽略函数.

例子

参考郭宇老师的初识「零知识」与「证明」—— 探索零知识证明系列(一):地图三染色问题

Definitions of Zeroknowledge interactive Proof

1.Interactive Proof system for a Language L

可忽略函数:一个比任何多项式函数分之一增长都慢的函数

Zero knowledge

谈论零知识前,需要引入模拟器的概念.

这里参考郭宇老师–从「模拟」理解零知识证明:平行宇宙与时光倒流—探索零知识证明系列(二),而不使用课程的例子.

模拟器:Simulator

简单来说

**现实世界/视图:**是一个概率分布.这个空间中的点都是证明者和验证者之间交互的所有可能历史加上验证者的硬币投掷。

理想世界/视图:也是一个概率分布.但理想世界/视图需要是算法可构造的,由 Simulator 负责构造.

理想世界/视图与现实世界/视图的差异在于:理想世界/视图没有知识,现实世界/视图拥有知识.

对于一个多项式时间的区分器,它从上述分布中选择一个样本.如果区分器不能区分这个样本来自哪个分布,即来自哪个世界/视图,我们说这样的交互协议是零知识的.

因为理想世界/视图中的 Zlice 是没有任何知识,而且她和真实世界/视图中的 Alice 不可区分.

或者说区分成功的概率不大于 0.5.那么这两个分布在计算上是无法区分的。

Simulator 是怎么做到这一点的? Simulator 能够而是因为它可以“倒序“生成 视图/世界,即 Simulator 可以先随机选择一个挑战,然后基于这个挑战生成证明的一部分。这样生成的视图在统计上与真实的视图无法区分。

计算不可区分定义:

这里给出更一般的计算不可区分定义

这里的安全参数 k 表示某个计算困难问题的困难问题实例输入大小。每个困难问题都有一个界,当输入大小超过这个界时,我们就认为这个问题是计算困难的。–具体可参考刘巍然老师的回答

Zero Knowledge Define:

Flavor of Zero Knowledge

零知识分类

这里不给出形式化定义,简单来说即

两个随机变量的分布是计算不可区分的,也就是任何多项式时间的随机敌手都无法区分这两个分布,就称这个证明系统是计算零知识 (Computationally Zero-Knowledge)

两个随机变量的分布是统计不可区分的,也就是它们的统计距离 (Statistical Distance) 可忽略,就称这个证明系统是统计零知识 (Statistically Zero-Knowledge) 的;

如果统计距离就是 0,又叫做完美零知识 (Perfect Zero-Knowledge) 的;

Proof of Knowledge

一个 Proof System 是 POK 的,需满足以下定义

需要引入抽取器,抽取器具有时光回溯的能力.

抽取器 在理想世界中,通过时间倒流的超能力,把 Prover 的知识完整地抽取出来

注:抽取器可以提取出 witness,不是因为它具有无限的计算能力,而是因为它能与证明者进行多次交互。在每一轮的交互中,抽取器都会选择一个新的挑战,然后记录下 Prover 的回应。

这就保证了一个没有知识的 Prover 是无法让抽取器达成目标,从而证明了可靠性。

抽取器只能从能成功完成证明的证明者那里提取出(witness)。如果一个 Prover 不能成功完成证明,那么他可能并不知道一个有效的 witness,因此抽取器无法从 Prover 那提取出 witness。

把这样一个依靠采用抽取器来证明可靠性的证明系统被称为 Proof of Knowledge

注:不是所有的可靠性都必须要求存在抽取器算法

这里可参考郭宇老师:探索零知识证明系列(三):寻找知识

The First Application:Identity Theft

Alice 想通过互联网证明向 Bob 证明其就是 Alice,比如 Bob 是亚马逊的.

思想是:将证明 Alice 的身份转为 Alice 证明一道特定的难题,谁知道这个难题的答案谁就是 Alice.

NP and Zero Knowledge Interactive Proof

结论:如果单向函数(简单来说 is easy to compute on every input, but hard to invert given the image of a random input,具体参考 wiki)存在,那么每种 NP 语言都具有零知识交互证明。

先引入承诺的概念

承诺

承诺性质

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会为一个值 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

举例:地图三染色问题

地图的三染色问题是一个 NP 完全问题,即 NPC 问题.

生动易懂的例子仍然可以参考郭宇老师的初识「零知识」与「证明」—— 探索零知识证明系列(一):

过程总结如下:

性质如下

完整性:一个诚实的 P 总是可以说服验证者接受。因为无论 Vr 需要哪条边,P 总是可以正确地给出符合规则的上色方案。 总是接受

**健全性:**无论 恶意的 P *做什么,都会有一条边缘的颜色不正确。当 V 选择随机边时,实际上选中错误的概率是 1/E,成功骗过 P 的概率是(1-1/E).随着重复的次数增多,成功的概率变为,呈指数级降低。概率接受

零知识性:很容易能够看出来,整个过程验证者得到了很多信息,但是这些信息并不会帮助验证者获取地图三染色问题答案的知识.但形式化证明很麻烦。

模拟器 不知道染色答案,它可以提前确定诚实 V 会询问的边 E‘ 模拟器 可以将整个图都涂成一样的颜色,除了 E’的两端 a,b 会被涂成两种不同的颜色. 对于模拟世界/视图:它会输出一堆承诺,由于承诺的性质,这些承诺不会给计算能力有限区分器 提供任何信息.然后 区分器 随机选择一条边 E,由于模拟器能够提前知道区分器随机选择哪条边.所以 E=E‘. 打开 E/E‘的承诺以后,V 会发现 a,b 是不同的颜色.模拟器 成功完成该轮挑战 但实际上,模拟器 并不知道染色答案,但是 V 每次打开 E 的承诺,对应的 a,b 端点颜色又确实是不一样的.所以真实世界/视图模拟世界/视图在计算上是无法区分的.

zk 的应用

法律 隐私 生物 DNA 甚至是核裁军

复杂性理论(Complexity Theory)

Interactive Proof

  • BPP:复杂类 BPP 在多项式时间内对概率图灵机解出的问题的集合, 并且对所有的输入,输出结果有错误的概率在 1/3 之内

  • IP:交互式证明

    • 是一种包含了两个参与者(证明者和验证者)的验证系统,其中证明者试图通过多轮对话来说服验证者某个复杂陈述的真实性。
    • 定义
      • 设⟨A, B⟩ 为一对交互式图灵机. 记 ⟨A(y), B(z)⟩(x) 为在 A、B 的随机输入带均匀独立选取, 公共输入为 x, A 的辅助输入为 y, B 的辅助输入为 z 时, 图灵机 B 与图灵机 A 交互后输出的随机变量.
      • 给定二元关系 R 及其对应语言 L(R), 则针对该语言的 IPS(interactive proof systems)是用符号⟨P(y),V(z)⟩表示 .其中,图灵机 P 与 P*可以是无穷算力 V 是 PPT 的.则 IPS 满足两个性质
        • 完备性 (completeness): 对于任意的 x ∈ L(R), 存在 y, 使得对于任意的 . 完美完备性 (perfect completeness) 是指上述概率等 于 1.
        • 可靠性 (soundness): 对于任意的 x ∈/ L(R), 任意的恶意证明者 P*, 任意的.
  • IP 与 NP 的关系

    • IP 类可以看作是经典复杂类 NP 的交互式****随机变体,所有的 NP 问题也都可以在 IP 中找到解决方案,因此 IP 是 NP 的超集。
    • 在 IP 系统中,Prover 和 Verifier 之间有多轮的交互,而在 NP 问题中,证明(或解决方案)一旦生成,就可以独立地被验证,不需要进一步的交互。如果不允许交互,但允许验证者抛掷随机硬币并以小概率接受错误的证明,那么得到的复杂性类被称为 Merlin-Arthur(MA)

    这再次说明了 IP 强大的关键在于随机性交互的结合

    • Prover 在 IP 中并没有时间复杂度的限制,可以是全知的,这意味着它可能运行在超出概率多项式时间的时间复杂度。它能够进行任意复杂的计算来构建它的证明,只要这个证明能够在多项式时间内被验证
    • Verifier 在 IP 中必须运行在概率多项式时间(probabilistic polynomial time)内,即必须在多项式时间内完成计算

Private Coins Model

Interactive Proofs with Public Coins Model

“We can formulate a decision problem under uncertainty as a new sort of game, in which one opponent is ‘disinterested’ and plays at random, while the other tries to pick a strategy which maximizes the probability of winning – a ‘game against Nature’.” —Christos Papadimitriou. Games Against Nature. FOCS 1983.

如果不允许交互,但允许验证者抛掷随机硬币并以小概率接受错误的证明,那么得到的复杂性类被称为 Merlin-Arthur(MA)

AM 与 MA

  • Arthur-Merlin Protocol

协议中的两个参与者分别称为 Arthur 和 Merlin,基本假设是 Arthur 是配备随机数生成设备的标准计算机(或验证器),而 Merlin 实际上是具有无限计算能力的预言机(也称为证明者)。不过,Merlin 不一定是诚实的,所以亚瑟必须分析 Merlin 在回答亚瑟的询问时提供的信息,并自行决定问题。如果在这个协议中,每当答案是“是”时,Merlin 有一系列的回应,会导致 Arthur 在至少 2/3 的情况下接受,那么问题被认为是可以通过这个协议解决的。而如果答案是“否”,那么 Arthur 永远不会在超过 1/3 的情况下接受。

  • MA

1-message protocol

Merlin 向 Arthur 发送消息,然后 Arthur 通过运行概率多项式时间计算来决定是否接受。(这类似于基于验证器的 NP 定义,唯一的区别是 Arthur 在这里被允许使用随机性。Merlin 在这个协议中无法访问 Arthur 的硬币抛掷,因为它是一个单消息协议,Arthur 只有在收到 Merlin 的信息后才会抛硬币。

从形式上讲如果存在多项式时间确定性图灵机 M 和多项式 p,q 使得对于长度 n = |x| 的每个输入字符串 x,

  • AM

复杂度类 AM(或 AM[2]或 AM[K])可以通过具有两条/K 条消息的 Arthur-Merlin 协议在多项式时间内决定。只有 1/2/K 个查询/响应对:Arthur 随机抛出一些硬币并将他所有抛硬币的结果发送给 Merlin,Merlin 用所谓的证明做出回应,Arthur 确定性地验证证明。

在这个协议中,Arthur 只被允许将抛硬币的结果发送给 Merlin,在最后阶段,Arthur 必须只使用他之前生成的随机抛硬币和 Merlin 的信息来决定是接受还是拒绝

从形式上讲如果存在多项式时间确定性图灵机 M 和多项式 p,q 使得对于长度 n = |x| 的每个输入字符串 x

简单总结:

摘自啊咪咪小熊— MA 就是最简单的 M 给 A 发完就结束了,A 自己决定是否接受,就是非交互式的(和 NP 的区别就是 Verifier 可以用随机数)。 AM 就是 A 先给 M 发一个随机数,然后 M 再给 A 回复,然后 A 再决定是否接受,就是交互式的

IP = PSPACE 定理

任何可以在多项式空间内解决的问题都可以通过交互式证明的方式来解决。

具体可参照,这个假设的重要性在于它将两个看似不同的计算模型联系在了一起.

其中 PSPACE 简单理解为是比 NP 要大得多的语言类

MIP

MIP 类似于 IP,只是有多个证明者,并且假设这些证明者不会相互共享关于他们从验证者那里收到什么挑战的信息。MIP 的一个比喻是在审讯犯罪嫌疑人之前将多个犯罪嫌疑人放在不同的房间里,看看他们是否能保持他们的故事直截了当。对 MIP 的研究表明,如果一个人将证明者锁在不同的房间里,然后分别审问他们,他们可以说服审问者做出比一起接受审问要复杂得多的陈述。

Non Interactive Proof

通过 Fiat-Shamir 转换将 Interactive Proof 变为 Non-Interactive Proof

注:

Lecture2:Introduction to Modern SNARKs

Overview about zk-SNARK

Why commercial so much

历史可追溯至 1991 年的一篇论文[Babai-Fortnow-Levin-Szegedy’91]

Zk-snark application

  • blockchian

    • 外包计算:
      • 扩容(zkRollup):离线服务处理交易批次,L1 链验证一个简短的证明来证明该批次内的交易都是有效的,而无需分别验证每笔交易
      • 区块链桥接(zkBridge):将资产从源链转移到目标链.源链的共识协议同意锁定某些资产,以便在另一个链中使用。通过验证 zksnark 生成关于源链到目标链的共识状态的简短证明,而无需验证源链共识的整个过程. 在上述两个例子中,非交互式证明非常重要。因为证明需要由大量区块链验证者进行验证
    • 隐私性:需要零知识性
      • 公共区块链上的隐私交易:ZKP 在不泄漏交易信息的情况下证明一个私人交易是有效的。例子:TornadoCash、ZCash、Ironfish、Aleo。
      • 合规性:证明一个私人交易符合银行法规(例如 Espresso) 证明一个交易所具有偿付能力而无需泄漏拥有资产情况(例如 Raposa)。
  • Non-blockchain:打击虚假信息, [Kang-Hashimoto-Stoica-Sun’22]

当阅读报纸文章时,文章通常会插有图片,但图片可能与文章内容完全无关甚至带有误导性质.

解决方案是 C2PA 标准,其代表内容来源和真实性,目标是为报纸文章中的图像提供真实的出处。

工作原理:在每台相机中嵌入一个由制造商嵌入的密钥。该密钥无法从相机中提取出

每次符合 C2PA 标准的相机拍摄照片时,会对照片以及与该照片关联的所有元数据进行签名,比如拍摄照片的位置和时间戳,然后嵌入到相机生成的原始图像数据中.

当图像嵌入文章并发送给读者时,读者可以简单地验证图像上的签名,并向读者显示元数据、位置和时间戳.

但 C2PA 标准有一个后处理的问题,即这些相机可捕捉非常高分辨率的图像。

但是为避免将图像发送给最终用户时浪费太多带宽,发送时会对图像采样.这意味着图像可能被重新调整为更低的分辨率,会被裁剪,会进行灰度化处理等.当将经过处理的照片发送到笔记本电脑时,笔记本电脑无法再验证图像上的 C2PA 签名。即必须拥有原始图像数据,否则无法验证签名。

所以问题就是:签名存在,但读者没有获得原始图像数据,因此无法验证签名是否有效。

采用 zk-snark 解决,思想:在图片被编辑前,为该图片生成一个 zk-snark proof.

Operation 表示应用于该照片的操作列表:例如缩小尺寸、灰度化、裁剪

读者验证 zk-SNARK 证明,如果有效则将向用户显示元数据.

Define zksnark

首先介绍电路部分

Arithmetic Circuits

Fix a finite field F={0,1,…,p-1} for some prime p>2

Arithmetic Circuits 是一个函数,它接受有限域中元素作为输入并产生有限域中的元素作为输出。

它由若干域上的加法门和乘法门组成. 电路的大小=电路中门的数量,如上图 |C|=3.

电路可满足问题 (circuit satisfiability problem, C-SAT) 是指给定电路 C、 电路的部分输入 x (x 可为空) 和电路输出 y, 判断是否存在证据 w (电路的另一部分输入, 视为秘密输 入) 使得 C(x, w) = y.

布尔电路(Boolean circuit)是算术电路的子类, 其仅有与门、异或门等布尔逻辑门, 变量取值仅为 0 或 1. 可以证明, 通过增加常数级别的电路门和深度, 任何布尔电路都可以转换为算术电路

Valiant’s theorem:所有多项式时间可计算的函数都可以通过多项式大小的算术电路来表示

仅使用加法、乘法和减法,就可以实现 SHA256 函数,大约需要 20,000 个门才能完成,

Structured vs. unstructured circuits

非结构化电路:电路中有一堆门,而电线只是按照开发人员想要的方式去连接门

结构化电路:电路本身实际上是分层构建的,其中有一个固定的算术电路 M。

          输入从底部进入,重复应用M,最后计算输出。

M 有时被称为虚拟机

NARK: Non-interactive ARgument of Knowledge

NARK 代表非交互式知识论证,其应用于算数电路

电路的输入:公共 statement x 与秘密 w; 输出仍是 F 中的元素

NARK 会经过一个 Setup 阶段即预处理,Setup 将电路的描述作为输入,产生一些公共参数作为输出.

其中一部分参数与 Prover 相关,称为 PP.另外一些参数与 Verifier 相关,称为 VP.

Prover 通过 PP,x,w 作为输入,产生一个 proof .表明 C(x,w)=0.

Verifier 通过 PP,x 作为输入,对该 proof 进行验证.

整个过程 Prover 与 Verifier 并没有交互.

Define NARK Normallly

Properties of NARK

Knowledge soundness

Verifier 接受 w,表示 Prover know w.st C(x,w)=0 成立.如何理解 konw 呢,这就是第一节提到的提取器的概念

非正式来说,w 能够以某种方式从 Prover 中提取到 w,则表示 Prover know w.

正式定义则如上图.对于多项式时间对手 A 试图充当恶意证明者,在不知道 statement 对应的 w 情况下通过 Verifier 的验证,其中 A 分为两个算法,A0 与 A1.过程如下

  1. 生成全局参数 gp
  2. 将全局参数提供给第一个敌手算法 A0,敌手将生成一些内部状态 st,伪造证明的电路 C 和 statement x
  3. 生成 PP 与 VP
  4. 将 PP,C,x 作为输入运行算法 A1,A1 输出一个 proof

当将这个 proof 与 statement x 一起提供给 Verifier 时,验证者将以百万分之一的概率接受。如果这是真的,那么应该存在一种有效的提取算法 E 并且该提取器 E 将按如下方式工作。

  1. 生成全局参数 gp
  2. 将全局参数提供给第一个敌手算法 A0,敌手将生成一些内部状态 st,伪造证明的电路 C 和 statement x
  3. E 会以某种方式与算法 A1(作为 Oracle)进行交互,然后提取出 w.

提取的 w 满足 C(x,w)=0,概率大约是百万分之一减去一些可以忽略不计的值 等等。

总结:如果对手 A 能够说服 Verifier 它知道某些 C,statement 和对应的 w,

那么就有一个提取器可以与该对手 A 交互并实际上从 A 中提取 w,使得 C (x,w)= 0。

trivial NARK:即 proof 的情况.不满足零知识性,但满足前两个属性.Verifier 可以根据 proof 与 x 重新运行电路 验证 C(x,w)=0 是否成立

SNARK:Succinct Non-interactive ARgument of Knowledge

succinct preprocessing NARK

SNARK 是算法 S、P、V 的三元组,与 NARK 中一样,只是对 proof 提出了额外的要求

证明者生成的证明必须很短,特别是其大小必须是 w 大小的次线性。

证明也应该能够快速验证,这意味着 Verifier 的运行时间应该与电路大小呈次线性关系。

因此,验证者不能简单地重新运行电路 C,但它必须与 x 呈线性关系,因为 Verifier 必须按顺序读取 x

所以 time(V)在 x 上是线性的,但在电路 C 的大小上必须是次线性的。

strongly succinct preprocessing NARK

实践中的 SNARK 实际上会非常简洁(strongly succinct)。

strongly succinct:意味着

  • proof 不仅是 w 大小的次线性,证明长度必须是电路大小的对数关系.使证明与电路相比非常小!
  • 验证证明的时间与 x 的大小成线性,且最多是电路规模的对数关系。

意味着 Verifier 没有时间读取整个电路,也就是 Verifier 甚至不知道电路 C 是什么.也就无法验证一个语句

这就是为什么需要公共参数的原因,它为 Verifier 提供电路的 vp 摘要,以便在 log(∣C∣)内足以运行验证。

ZK-SNARK 就是零知识的 SNARK

像 NARK 一样,考虑一个 trival SNARK 的情况,如上图所示,发现 a trival SNARK 并不满足一个 SNARK 定义.

Preprocessing Setup

Setup 阶段读取整个电路 C,然后输出电路 C 的摘要-一些公共参数,包括 Prover 会用到的 PP 与 Verifier 会用到的 VP.

Setup 阶段通常会采用一些随机位 r 用于生成参数的过程,可分为以下几类

  • Trusted setup per circuit:每一个电路都需要重新执行一次 Setup 过程.随机数 r 非常重要,应当保证 Setup 阶段后 r 被销毁(可信),否则其将能够伪造 proof.

  • Trusted and universal setup:将 Setup 分为两个阶段

    • :是个一次性的算法,产生全局参数 gp.该阶段完成后,r 就被销毁.所以 init 阶段需要是可信的,但该阶段可以用于很多电路.
    • :是一个确定性算法,为证明者和验证者生成参数。任何人都可以运行该算法并验证参数是否正确生成。
  • Transparent setup:不需要任何秘密值,因此任何人都可以验证它是否正确运行,并且不需要运行可信设置.比如 STRAK 协议

Building an efficient SNAKR

一个通用的构建 SNARK 的范例,包含两步或者说两个组件.functional Commitment Scheme 与 Interactive oracle proof.

Commitment 方案是一个加密对象,这意味着它的安全性取决于某些密码学假设。

IOP 交互式预言机证明实际上是一个信息论对象,可以在没有任何底层假设的情况下无条件地证明 IOP 的安全性

Commitment 承诺

简单回顾

承诺性质

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会为一个值 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

有一个使用哈希函数的标准承诺构造。哈希函数 H:M×R→C,其中

  • commit(m,r)=H(m,r)
  • verify(m,com,r)=accept if com=H(m,r)

Commitment to a Function

  • 选择一个函数族: F={f:X_→Y}. f 表示从集合 x 到集合 y 的函数
  • Prover 运行 Commit 算法,将函数 f 与随机数 r 作为输入, 为输出.f 可以表示为一个电路 C,一个 C 程序等。

然后将 发送给 Verifier

  • Verifier 可以发送回一个函数域中的元素 x
  • Prover 将 x 对应的 f(x)=y,以及 proof 发送给 Verifier. Proof 表明 1.f(x)=y 2.f 属于 F

形式上讲, Function Commitment Scheme 由以下定义:

  • setup_()→gp 输出公共参数 gp

  • commit(gp,f,r)→ 用随机数r∈R 承诺 f∈F

    • 构建 SNARK,必须满足 Binding
    • 对于 hiding, 构建 SNARK 并非必须满足, 但当构建 zk-SNARK 需要满足该属性
  • eval(P,V) :对于给定和 x∈X,y∈Y :

    • Prover(gp,f,x,y,r)→π:生成一个简短的证明
    • V(gp, ,x,y,π)→accept or reject
    • 事实上,Prover 与 Verifier 之间的 eval 算法是对以下关系的(zk)SNARK 证明: 1.f(x)=y 2. commit(pp,f,r)= 3. f∈F
Examples of functional commitments

  • 多项式承诺:承诺对象是单变量多项式, :表示所有次数最多为 d 的单变量多项式的集合。

  • 多线性承诺:承诺为多线性多项式,其中 :表示是 k 个变量的所有多线性多项式的集合,每个变量的次数最多为 1。

    • 多线性多项式示例:
  • 向量承诺:承诺对象是一个向量, 。能够打开该向量中的任何特定单元格。在给定索引 i 的情况下,证明索引 i 处的该函数值 .

    • 向量承诺方案实例:默克尔树(Merkle tree)
  • 内积承诺:承诺一个向量 ,并定义一个函数 ,该函数接受另外一个向量 v 作为输入,并且输出两个向量的内积(u,v).

对于这 4 个承诺方案,可以从中任意一个基础上构建获得剩余承诺方案。

Polynomial Commitment Scheme

Prover 需要对多项式 承诺.Prover 试图说服 Verifier , 满足

  1. 1.f(u)=v ,其中 u,v∈Fp 且公开可见
  2. f 的 degree≤d.

我们希望证明是一个 SNARK,那么证明大小和验证时间应该是 ,下面是一些 PCS 的实现机制

  • Using bilinear groups: KZG’10 (trusted setup,也是实际中使用最多的), Dory’20 (transparent,相比 kzg 慢)
  • 仅使用哈希函数:基于 FRI(long eval proofs)
  • 仅使用常规的椭圆曲线,不需要额外的结构: Bulletproofs (short proof, but verifier time is O(d))
  • Using groups of unknown order: Dark’20(慢,未获得太多关注)

考虑 travial 的 PCS 情况,用系数表示的方式表示多项式 f, ,过程如下

  • commit(f,r)=

  • eval_ 将按如下方式完成:

    • Prover 将 π = 发送给 Verifier
    • verifier 从系数重构 f ,并检查是否 f(u)=v.

很明显这不符合 SNARK 的要求,因为证明大小和验证时间与 d 是线性关系的,而不是 O(log d)

Polynomial is Zero

这是 SNARK 的重要组成部分,也是使 SNARK 成为可能的重要原因

考虑最多为_d_ 次的非零多项式

:从有限域选择一个随机元素 r ,f(r)=0 的概率为 d/p

这是因为 f 最多有 d 个根,r 是从大小 p 的域 中随机选择的.r 命中 d 个根的概率为 d/p.

考虑当 p 远远大于 d 的情况下,比如 , d/p 可以忽略不计.这意味着当 ,Verifier 有着非常高的概率相信多项式在所有点上都为 0.

判断一个多项式是否为 0,只需一个随机点进行评估,并检查评估值是否为零即可。

对应[Schwartz-Zippel-DeMillo-Lipton]定理.该定理也适用于多元多项式,将 d 理解为 f 的总 degree 之合即可.比如

Two Polynomials are equation

如果 ,那么 f=g 的概率非常高.

下面给出判断两个多项式是否相等的交互式协议

  1. V 从 Fp 中随机选择一个随机数 r,将 r 发送给 P
  2. P 根据发来的 r,分别计算 f(r)与 g(r)的值为 y 与 y‘.将 y 与 y’以及对应的 proof 给 V
  3. V 首先检查 proof 是否有效,然后检查 y 是否等于 y‘

通过承诺与 F-S 转换,将上述协议转为 SNARK 方案.

  • 上图是一个 SNARK 方案,当

    • d/p 可忽略
    • 哈希函数 H 作为一个 Oracle,即 H 可自行获得随机质询,然后计算对对应的响应,并将响应发送给 P
  • 该 SNARK 中,Statement x 是 f 与 g 对应的承诺 ;witness w 是 f 与 g 本身.

  • F-S 转化为非交互式 SNARK:P 通过 H(x)获得随机数 r,不再需要 V 发送 r.因为 V 同样能拿着 x 询问 Oracle,获得 r.

  • 但这不是一个 zk-snark,因为 V 可以学习到多项式 f,g 在 r 处的值 y 与 y‘.

Inner product argument

证明者通过内积论证可利用循环****递归的方式证明他拥有两个公开向量承诺的消息, 且这两个消息的内积等于某个公开值. 对于长度为 n 的消息向量, 内积论证的通信复 杂度为 O(log n).

Prover 可向 Verifier 证明对于公共输入 和公开标量 z∈Zq

P 拥有向量 a、b,满足 则 statement 为

,其中向量 a,b 为 witness,g,h,A,B 为公共输入

内积论证的核心思想是将针对 n 长向量的 statement 根据 V 的随机挑战 c 归约为对 n/2 长向量的等价 statement,

在向量不断缩减至为标量后, P 只需要直接发送标量即可.

约定一些符号

  • 1.固定群的生成元 g 后, 记 为 [r], 令 n ∈ N, 记 ,[S]同理
  • 设 g, h 的生成方式为 ,
  • 对于 n 为偶数的向量 (不是偶数可填充), 记

过程如下

  1. 首先基于 V 的随机挑战 c 构造长度一半于原密钥长度的承诺密钥, 即
  2. 为防止 P 利用新的承诺密钥 [r′] 作恶, P 需在挑战阶段之前发送部分承诺值 . 此时新证据为
  3. P 和 V 计算新承诺:
  4. 对于承诺密钥 [s]、承诺 B 和秘密输入 b, 利用挑战 c 的逆 构造对应的承诺密钥 [s′]、新证据 b′ 和承诺值 B′ ,即
  5. 对于 z,P 需在挑战阶段前构造
  6. 更新后的
  7. 归约后的新陈述为
  8. …..递归规约

IOP:Interactive Oracle Proof

F -IOP 的目标是将 f∈F 的承诺转为通用电路的 SNARK。例如,对于一个多项式函数族 ,使用 F -IOP,可以将其转换为任何电路大小为 ∣C∣<d 的 SNARK。

Definition:C(x,w) 是某些算术电路。 .F -IOP 是一个证明系统,用于证明 ∃w:C(x,w)=0 **

Setup: S(C)→(pp,vp),其中 是函数的 Oracle.即 vp 可以理解为 V 可查询的一堆 Oracle,V 可以要求某个给定值显示函数结果,过程如下

  • P 首先发送函数 f1 的 Oracle 。V 稍后可以在其选择的任何点对 f 进行评估.在实际中,
  • V 从 Fp 中随机选择 r1 发送给 P.
  • 重复 Step1 and Step2 t-1 轮
  • P 最后发送 ft 的 Oracle
  • V 开始验证,验证过程 V 可以访问 P 给出的所有 Oracle,以及所有生成的随机数 r 和公共输入 x

Properties of IOP

  • Completeness:

  • Knowledge Soundness:在没有 w 的情况下,恶意 P 无法让 V 相信他知道一个 w,使得 C(x,w)=0

    • Extractor 可以访问 statement x 与函数 本身,因为对于这些函数本身的承诺就是一个 SNARK,所以 Ectractor 可以从 提取 f 本身,进而提取出 w.
  • Zeroknowledge :可不满足

Example of IOP

Polynomial IOP for claim ,用电路 C 去表示该关系: ,过程如下

  1. P 分别计算两个多项式 ,V 也可计算 g(Z),因为 X 是公共的
  2. P 计算一个商多项式 ,只有在 g 包含所有 f 根的情况下,q 才是一个多项式,即X⊆W.举个例子

比如 X={1,2},W={1,2,4}, ,只有X⊆W,q 才是一个有限域中的有效多项式

  1. P 发送 给 V
  2. V 发送一个随机数 r 给 P,虽然 P 不会用到 r,但仍然发送,这表明 r 是一个公共值
  3. V 查询 在 r 点的值,记做 w 与 q‘.计算 g(r).验证 g(r)*q’=w 是否成立

当我们设计 IOP 时,我们所要做的就是设计 P 向 V 发送哪些 Oracle,然后 V 在哪里查询这些 Oracle。

实际中,可以通过多项式承诺方案来实例化 IOP,其中这些 Oracle 被来自 P 的承诺所取代,查询动作基本上通过将查询点发送给 P 来取代,P 进行评估并发回评估正确完成的证明。然后 V 可以决定是否接受或拒绝最终的证明。

IOP + 相应的 Polynomial 方案构造 SNARK

Snarks in Practice

ZK-learning lecture 12:ZK- EVM

Background and motivation

The diagram of Layer1 blockchain

区块链简单介绍:

区块链网络由许多节点组成,通常有大量的节点用于指定,它们通过 P2P 网络互连,所有节点保持与上图红框显示的相同状态.这是一个类似于共享账本的数据库,因此可以将余额或者一些程序代码放在这里.然后使用名为 Merkle tree 的数据结构将所有这些信息存储在列表中.从而得到一个状态路由.

然后对状态路由取摘要来代表所有的状态.每个节点都需要维护相同的数据库.此外节点还将运行称为 EVM 的相同软件进行一些计算并更新状态路由,

区块链也称为 world computer 这个词,因为任何人都可以使用它来运行任何接近去中心化的程序,而运行在区块链之上的程序称为智能合约,因此 evm 将从节点计算机加载 merkle 树叶子结点中的数据到 Storage 中重写这棵树并获得新的状态路由

发送交易:

用户发送交易至区块链中,交易会在 p2p 网络中传播,通过共识算法在每个时隙中选择一个提案, 这个提案将把它收到的许多交易打包到一个块中,同时以交易作为输入运行 evm 并更新状态路由,然后出块.在看到这个块被提交后,网络中的其他节点将下载这个块并重新通过 EVM 执行该块内的交易,就状态路由达成共识.这样始终维护相同的数据库.

Layer1 特点:

优点

Secure:交易将由不同的节点执行多次 Decentralized

缺点

Expensive Slow

zk-rollup

ZK-rollup 是一种扩展解决方案,用于解决 EVM 的可扩展性问题.

ZK-rollup 不像 Layer1 广播所有交易,以及拥塞且昂贵的 P2P 网络,其有一个单独的 Layer2 网络层,可以更加中心化.

zk-Rollup 的基本思想是将大量交易聚合到一个 Rollup 块中,并为该链下的块生成 简洁,公开,可验证 的证明。然后 Layer 1 上的智能合约只需要验证证明并直接应用更新的状态,而无需重新执行那些交易。这可以帮助节省一个数量级的 gas 费用,以及提升一个数量级别的网络吞吐.因为验证证明比重新执行计算便宜得多。另一个节省来自数据压缩(即只保留最小的链上数据用于验证).

这样做与原来的安全性是**等效的.**背后的原理依赖于 zk.

编写困难

但是构造这样一个 Prover 是困难的,对于某些计算的证明,首先需要以电路形式编写所有程序逻辑,

也就是用加法乘法和类似的非常底层的方法断言.电路强调非常复杂的逻辑,包括 for Loop ,if else 和所有程序.语法非常复杂.此外 一个电路对应一个程序,这意味着对于不同的应用程序开发人员,需要实现自己的电路.电路也需要通过一个非常严格的安全测试审计,这需要很长的开发时间.

兼容 : 比如一个 Prover 无法同时包含来自 uniswap 与 optiswap 上的交易.

所以需要 zkevm.

zkevm 概念

zkEVM 是一种虚拟机,通过 zk 证明计算和现有以太坊基础设施兼容的方式执行智能合约交易。这使它们能够成为零知识汇总、第 2 层扩展解决方案的一部分,从而提高交易吞吐量,同时降低成本

如果第 2 层可以运行为以太坊环境创建的程序而无需修改底层智能合约逻辑,则该 Layer2 是 EVM 兼容的。这使得第 2 层与现有的以太坊智能合约模式、代币标准和工具兼容。与 EVM 兼容对于这些第 2 层的广泛采用非常重要,因为它使熟悉以太坊 Solidity 编程语言的开发人员能够使用他们习惯的的工具构建高度可扩展的应用程序。

但是 zkevm 很难编写,有以下几点原因

  • **第一,EVM 对椭圆曲线的支持有限。**目前,EVM 仅支持 BN254 配对。由于不直接支持循环椭圆曲线,因此很难进行递归证明。在此之下也很难使用其他专用协议。验证算法必须是 EVM 友好的。
  • **第二,EVM字节为 256 位。**EVM 在 256 位整数上运行(就像大多数正常 VM 在 32-64 位整数上运行),而 zk 证明“天生地”大多在素数上工作。在电路内部进行“不匹配的字段计算”需要范围证明,这将在每个 EVM 操作中增加约 100 个约束。这将使 EVM 电路大小扩大两个数量级。
  • **第三,EVM 有很多特殊的操作码。**EVM 与传统 VM 不同,它有许多特殊的操作码,例如 CALL。它也有与执行上下文和 gas 相关的错误类型。这给电路设计带来了新的挑战。
  • **第四,EVM 是基于堆栈的虚拟机。**SyncVM (zksync) 和 Cario (starkware) 的架构在基于寄存器的模型中定义了自己的中间表示(IR,Intermediate Representation)/代数中间表示(AIR, Algebraic Intermediate Representation)。他们构建了一个专门的编译器,将智能合约代码编译成一新的 zk 友好 IR。他们的方案是语言兼容而不是原生 EVM 兼容。基于堆栈的模型和直接支持原生链工具更难证明。
  • **第五,以太坊存储层带来巨大开销。**以太坊存储层高度依赖 Keccak 和巨大的 MPT,它们都不是 zk 友好的,并且需要巨大的证明开销。例如,Keccak 哈希比电路中的 Poseidon 哈希大 1000 倍。但是,如果将 Keccak 替换为另一个哈希算法,则会对现有的以太坊基础设施造成一些兼容性问题。
  • **第六,基于机器的证明需要巨大的开销。**即使能够妥善处理上述所有问题,仍然需要找到一种有效的方法将它们组合在一起以获得一个完整的 EVM 电路。正如我们上一节中所提到的,即使像 add 这样简单的操作码也需要整个 EVM 电路的开销

以下技术的发展使得 zkevm 得以落地

  • **多项式承诺的使用。**在过去的几年里,大多数简洁零知识证明协议都坚持使用 R1CS,将 PCP 查询编码在特定于应用程序的可信设置中。电路大小通常会爆炸,且不能进行许多自定义的优化,因为每个约束的项数需要为 2(双线性配对只允许指数中的一次乘法)。使用多项式承诺方案,可以通过通用设置甚至透明设置将约束提升到任何项数。这为后端的选择提供了极大的灵活性。
  • **查找表参数和自定义配置的出现。**另一个强大的优化来自查找表的使用。该优化首先在 Arya 中提出,然后 Plookup 中进一步升级。这可以为 zk 不友好的原语(即,AND、XOR 等按位运算)节省很多成本。自定义配置可以高效地进行高项数的约束。TurboPlonk 和 UltraPlonk 定义了优雅的程序语法,以便更轻松地使用查找表和定制配置。这对于减少 EVM 电路的开销非常有帮助。
  • **递归****证明越来越可行。**递归证明在过去需要巨大的开销,因为它依赖于特殊的配对友好的循环椭圆曲线,这引入了很大的计算开销。然而,更多的技术在不牺牲效率的情况下使这成为可能。例如,Halo 可以避免对配对友好曲线的需要,并使用特殊的内积参数来摊销递归成本。Aztec 表明可以直接对现有协议进行证明聚合(查找表可以减少非原生字段操作的开销,从而可以使验证电路更小)。它可以极大地提高支持的电路大小的可扩展性。
  • 硬件加速使证明更加高效。Scroll 为证明者制造了最快的 GPU 和 ASIC/FPGA 加速器。关于 ASIC 证明者的论文,今年已经被最大的计算机会议(ISCA)收录。GPU 证明器比 Filecoin 的实现快大约 5 到 10 倍。这可以大大提高证明者的计算效率。

ZKEVM 分类

  • Language level:采用高级语言(例如 Solidity 或 Vyper)编写的代码,并将其编译为旨在支持零知识证明的语言。本质上,它们相当于高级语言,但不是实际的 EVM。尽管合约可能不具有相同的地址,但这可以更快地生成证明并降低成本 Starknet
  • Bytecode level:牺牲了一些 EVM 功能,以实现更轻松的应用程序开发和证明生成,例如对预编译、VM 内存、堆栈以及智能合约代码处理方式的更改。虽然大多数以太坊应用程序都可以在这种环境中运行,但有些应用程序可能需要重写 Scroll Polygen
  • Consensus level:不会改变当前以太坊系统的任何部分,从而更容易生成零知识证明。这使得它们与所有以太坊本机应用程序完全兼容,并允许重复使用区块浏览器和执行客户端等工具。然而,以太坊协议的某些部分需要大量计算来生成零知识证明,导致 zkEVM 的证明时间较长

也可以参考 V 神的 4 种分类

Build a zkEVM from scratch

Interesting research problems

Other applications using zkEV

Lecture 16: Hardware Acceleration of ZKP

1.Goals of HW Acceleration

image-20230515111025421

  • 吞吐量,即每单位时间执行尽可能多的操作
  • 成本
    • 当优化成本时,目标是降低执行某些操作所涉及的资本和运营费用。对于比特币挖掘机来说,这意味着最大化每美元购买价值的哈希数量,同时最小化每个哈希的能源消耗,从而降低运营成本。
  • 延迟:减少完成单个操作的时间
    • 在高频交易等领域,延迟是一个重要的考虑因素。 低延迟的证明生成可以促进更好的用户体验或更快的确定用例,比如ZK Bridges。

2.What needs accelerated

image-20230515112018032

首先要注意的是每个证明系统及其相关实现都是利用不同的密码原语和不同的软件库构建的,在某一个证明系统中计算成本最高的部分,在另外不同的证明系统实现或用例中可能相对次要或可能根本不会出现,

其次不同的证明系统中,存在三种计算量大的操作,包括MSM,NTT,算术哈希。

3.MSM:多标量乘法

image-20230515121131312

MSM 是一种用于计算多个标量乘法之和的算法,或者它可以被认为是椭圆曲线点和标量的点积。

由于问题的性质,每个标量乘法或一组标量乘法都可以很容易地并行化,并且可以由不同的硬件引擎拆分和操作然后汇集并在最后累积,有许多优化可用于减少计算 MSM 的计算量,用于更大尺寸的 MSM 算法,如pipepenger。

image-20230515141320803pippengers,将计算成本从线性减少到O(n/logn),除了使用改进的算法之外, 还有替代的点表示方法(Jacobian)和曲线表示方法(Edwards)可用于减少每个曲线上的域元素的操作总数

image-20230515142135825通过将它们从像 CPU 这样的主机设备转移到更并行的架构,如 GPU,可以提高计算效率。然而,当将操作从主机设备移动到外围设备时,必须记住一件事情,即数据也必须被移动以进行计算。在多标量乘法的情况下,标量和点必须从主机移动到加速器上进行计算。这两个设备之间可用的通信带宽通常会限制加速器的最大性能。

4.NTT

image-20230515143228424

NTT 是一种用于将两个多项式相乘的算法 NTT 类似于其他算法,例如 fft 或 DFT,但它的独特之处在于它对有限域元素进行运算

实现 NTT 的常用算法之一是Cooley-Tukey算法,该算法将多项式乘法的复杂性从O(n^2)降低到O(nlog n)阶

image-20230515143651529 类似于 MSM 在主机设备上执行 NTT 时,标量也必须再次移至加速器 通信带宽将限制加速器的最大可能性能,但是 NTT不容易并行化。 每个元素必须在算法操作期间与各种其他元素交互,这意味着问题不能轻易进一步划分,因为这些元素与每个元素交互,它们必须保存在内存中并在强加高内存要求的情况下运行

5.算术hash

image-20230515144248585

许多零知识证明用例中的算术哈希它要求

证明哈希原像的知识或利用哈希 ,Merkle roots和 Merkel 包含路径有效地表示电路外部的数据。 算术哈希函数(如 Poseidon,rescue Prime)通常用于传统散列函数(如 ShA系列哈希函数)。

选择这些哈希函数是因为虽然本身它们的计算成本更高,但在电路内部使用时,部署效率会更高,因为这些哈希函数的constraints数量会更少。 在实例化哈希函数时可以选择许多算法参数,这可能会影响计算成本。其中一些参数包括有限域大小,有限域选择的素数大小,MDS 矩阵结构等。

算术哈希原语的有效实现主要由模乘法驱动,证明生成中涉及的计算量大的操作通常因系统而异

image-20230515145655014

这些操作取决于承诺方案,像KZG这样的承诺方案会导致在生成证明过程中 MSM 操作会主导。

而当使用FRI承诺方案时,证明生成过程通常由 NTT 主导。

许多 snark 系统,例如groth16 和 Marlin由 MSM 主导,而 Starks 总体上通常由 NTT 主导。

但是这三个先前讨论的密码原语(MSM,NTT,算术哈希)在加速之前在所有证明系统中占据了三分之二或更多的时间 这三个操作可能看起来截然不同,但它们实际上共享一些基础组件。

比如MSM and NTT的公共基础组件是域和曲线操作,这些操作的核心主要由域上的算术驱动,特别是模乘法

因此虽然这些算法的结构彼此,大不相同,但它们是基础的性能通常源自硬件执行模乘法的能力

image-20230515150923268

需要注意的一件有趣的事情是数据大小与模乘法计算成本之间的关系, 当数据大小呈线性增长,模乘法的计算成本相对于域的大小是N^2。

这意味着随着域大小的增长,加速器性能可能取决于操作的计算成本, 但对于较小的域大小,加速器可能会受到主机可用带宽的瓶颈

这种二分法凸显了在开始设计硬件加速系统时理解证明系统的具体参数的重要性。

它也凸显了设计能够服务于各种证明系统和参数的硬件加速设备或实现的难度。

6.提高证明生成性能

image-20230515153042757

改善证明生成性能的第一步是了解所使用的证明系统和用例的计算、内存和带宽成本,通过将高级操作(如 MSM 和 NTT)分解为计算它们所需的模乘数量。通常可以在完成实现之前估算证明系统在各种硬件平台上的性能。

然而,为了确保估计是准确的,有许多参数应该提前知道

第一个最重要的参数是证明系统中每个操作的数量, 例如一些证明 系统每个证明可能需要四个或更多 msms 而其他系统可能只需要两个

第二个关键因素是通常需要计算的操作的大小,不同的用例将导致每个操作的不同大小 例如在某些用例中 MSM将只有 1000 的大小,而在另一个用例中,它可能是 1000 万或更多

第三个因素是确定的是域和曲线的大小,这将有助于告知每个模块化算术运算的带宽和计算复杂性

此外,点的表述形式(Affine or Jacobian),模运算等等 最后还有各种其他较小的因素可能有助于证明系统的性能 一旦所有这些参数都确定了,执行证明或证明生成过程所需的模乘次数可以很容易地计算出来,有了这个数字,就可以 与给定硬件平台的模型性能进行比较,以便在了解需要执行的计算的情况下得出性能估计或计算时间

image-20230515155501729 硬件加速的下一步是为这些工作负载选择合适的硬件-主要由模乘法驱动

应该寻找可以快速且廉价地执行大量乘法的硬件平台

可通过查看平台上硬件乘法器以及每个乘法器可以执行的速度和频率来评估给定硬件平台的估计性能

image-20230515162532683

上图是一个包含四个硬件平台的表格:桌面CPU、服务器 CPU、FPGA 和 GPU

  • 第一个平台:桌面 CPU
    • 包含八个内核,每个内核都有一个 64 x 64 位乘法器,工作频率为 5GHz
    • 此平台的乘法功率估计约为164 该数字的计算方法是将乘法器的数量、乘法器大小和频率相乘,然后除以 1000
  • 第二个平台:服务器 CPU
    • 包含96 个内核,每个内核都有一个Multiplier,但以较低的频率运行,这个平台有大约 900 的Multi能力。是桌面处理器的五倍
  • 第三个平台:FPGA
    • 与服务器上存在的 96 个相比,超过 6000个乘法器。虽然乘法器数量大约是服务器CPU乘法器数量的 60 倍,但由于乘法器大小和频率的减少,乘法运算的功率小于服务器 CPU 的两倍。
  • 第四个平台:GPU
    • 大约5000个32*32位的乘法器,以 1.7 GHz 的频率运行,这产生了大约 9000乘法能力
    • 相较于FPGA,拥有更大的乘法器大小和更高的工作频率,性能得到提高

关于这些底层硬件架构及其对模块性能的影响,强烈推荐Simon puffer 几年前在斯坦福区块链会议上的演讲,它可以在 YouTube 上找到

这些分析仅突出硬件平台的基础功能,

为了实现提高性能并达到硬件加速的目标,通常还必须考虑其他因素。

包括实现理论性能的能力、部署的便利性、运营成本、esa编程和许多其他因素

成功的硬件加速需要关注的两个关键领域

image-20230515171544562

首先是选择适合目标平台的硬件友好算法

针对 GPU 和 FPGA 这样的目标平台具有数千个核心,最适合使用高度可并行化的算法。此外,在选择算法时,应选择旨在通过减少所需操作数量来降低总计算成本的算法。

一旦选择了算法,最后一步是创建高效的实现。通常情况下,需要重新构造算法以更好地匹配目标平台的硬件能力。除了重构算法外,通常还需要使用低级汇编原语来更充分地利用硬件资源并实现最大的性能。

7.硬件加速存在的限制与陷阱

image-20230515172632660

在追求硬件加速时,乘法不是唯一需要的资源。虽然这些高级原语主要由模数乘法组成,但算术单元中的其他计算资源通常也是必需的。此外,根据正在加速的操作的大小和类型,非计算资源也可能成为瓶颈。例如,像 NTT 这样的操作有时会受到内存访问速度的瓶颈限制。

另外,对于问题规模较大的用例,有时所需数据无法全部 在目标平台的内存中容纳,从而导致性能降低。对于连接到主机系统的加速器,通信带宽也可能成为瓶颈。目前,许多 GPU 和 FPGA 硬件加速的 NTT 实现受限于它们在主机和加速器之间传输数据的能力,而不是计算资源。有时可以通过将数据保留在加速器上来减少带宽需求,从而缓解或消除这些瓶颈。

image-20230515172813446

数据移动成为瓶颈而不是数据计算不仅在 NTT 和 ZKP 系统中出现,而且在大数据和高性能计算环境中普遍存在这种趋势。对于高度并行的算法,计算速度往往比数据移动本身更快,因此硬件加速设计应尽量减少数据移动。

在使用主机外加速器时,另一个需要考虑的因素是将数据移动到加速器和返回主机的时间。

对于小问题,有时在主机上直接进行计算可能比在加速器上更高效。

硬件加速的最后一个陷阱是广为人知的奥姆德尔定律或贝尔定律,它指出,通过优化系统的单个部分或单个部分获得的总体性能提高取决于改进部分实际使用的时间占总时间的比例。

更简单地说,在 ZKP 系统中,如果 MSM、NTT 和算术哈希占据大约 65% 的时间,即使这些操作被消除,最大的加速比也只能达到 3 倍。考虑到证明生成与本地计算的时间开销相差几十万到一百万倍,显然优化工作不会止步于此。

8.FileCoin的加速例子

image-20230515174119119

过去几年,Filecoin 一直是最大的 ZKP 系统之一,每天平均生成 1 到 5 百万个证明。

Filecoin 使用 ZKPS 来进行副本证明 (PRORAP),这是一种证明你已经创建了数据集的唯一副本的加密方式。Filecoin 中使用的副本证明需要大约 470 GB 的 Poseidon 哈希。

如果在许多核心的 CPU 系统上进行哈希运算,需要大约 100 分钟。

相比之下,Filecoin 的 GPU 实现仅需要大约一分钟,可以实现大约 100 倍的性能提升。

对于 Filecoin 中的密码学证明组件,他们利用了 Groth16 协议。在 Filecoin 网络上进行每个 PoRep 时,存储提供者会生成 10 个证明,每个证明大约有 1.3 亿个约束条件,总共超过 10 亿个约束条件。仅用于创建这些证明的 MSM 就总计约为 45 亿个点标量对

如果这些证明在许多核心的 CPU 上计算,需要约一个小时才能完成。相比之下,在 GPU 上可以在大约三分钟内完成,这是一个大约 20 倍的性能提升。这个例子突显了硬件加速让ZKP 用例变得实际可行的能力。

9.zk加速的现在与未来

image-20230515174833294

了解更多关于硬件加速的知识,有许多在线资源可用,包括许多今天讨论的加密原语的开源 GPU 和 FPGA 实现。

一个特别好的资源是 zprize.io,这是一个旨在改善 ZKP 系统性能的社区倡议。

对用于更大的多标量乘法,单个 GPU 可以以每秒超过 1 亿个Bases的速度执行, 就 NTT 而言,大小为 2 到28 的 NTT 可以在 250 毫秒内计算出来, 对于 Poseidon 哈希,GPU 可以大约哈希 每秒 350 GB。

image-20230515175249404

尽管在过去几年中,ZKP 硬件加速取得了巨大进展,但仍有很大的改进空间。下面是一些可以帮助证明生成更快的领域。

第一个领域是针对核心原语(如 MSM 和 NTT)的改进算法或对现有算法的其他优化。

第二个领域是全新的核心原语,如具有更低计算要求的新哈希函数。

第三个领域即新的证明系统,特别是关于硬件加速的简化证明系统。简化的证明系统可以为硬件加速创造更多机会。

例如,更简化的证明系统可以减少不同操作、减少通信和内存要求,甚至消除一些目前存在的计算昂贵的操作。最后,改进实现的空间也永远存在,包括完整的证明系统和硬件加速的原语。这包括针对商用 GPU 和 FPGA 等现成硬件以及定制硅片(例如 ASIC)的设计。

Reference

Amber Group.“Need for Speed: Zero Knowledge.Introduction I by Amber Group

Feng, Boyuan. “Multi-scalar Multiplication (MSM) .

Figment Capital.“Accelerating Zero-Knowledge Proofs.

Jane Street. “Accelerating zk-SNARKs - MSM and NTT alorithms on FPGAs with Hardcaml.” Jane Street Tech Blog, 7 December 2022.

Thaler, Justin. “Measuring SNARK performance: Frontends, backends, and the future.” a16z crypto, 11 August 2022

Zhang, Ye. “ZKP MOOC Lecture 12: zkEVM Design, Optimization and Applications.

Thanks


  • 感谢SecbitLabs @郭宇 前两个月分享的Spartan Overview (尽管当时也没太理解), 以及@even 在研究方向上的指引(据说Hyrax 不太好啃),不至于走太多弯路。

我的动机


缘于folding,缘于NOVA,缘于Setty,了解到了Spartan,但并不认识它,所以才有了本篇及接下来的关于它的一切(预备知识)…… 

image.png

关于Spartan,在ZK领域可能时间上相对也有点儿远了,暂且不考虑它在某些方面的争议,它的一些思想其实已经影响到其它比较热门的方向了,比如当下的热点Lasso & Jolt,所以它的研究意义仍然很大。


Overview 


  • 本篇文章主要参考Hyrax 论文前半部分1-4节,即优化前的GKR zk argument

  • GKR 协议本身是Sumcheck协议的一种应用,不带zk argument的GKR 就可以简单认为是多个sumcheck协议的叠加,带zk argument的GKR就会带来很多的细节问题,这也是Hyrax 的起源,所以弄清楚GKR with zk argument 的各个细节后自然也就清楚了Hyrax的意义

数据并行化下的GKR 协议


节选自PAZK 中的图

image.png


何为数据并行化GKR?就是同一个电路描述应用在多组input 数据中的GKR 协议,这样prover 在最开始的claims 中就不再是针对单一电路的output,比如下面的 :

image.png


而是多个子电路的output的汇总 :​

image.png


在GKR协议中prover 要证明也不再是:


而是:​


另外需要备注一下各个notion的含义:

  • N 代表子电路的个数

  • G 代表单个子电路中每层Gate的个数

  • 代表第 层电路编码 Gate编码 上的evaluation 值,的MLE 

  • 代表第 层电路编码 Gate编码  上的evaluation 值,的MLE;同理

  • 分别代表上的加法和乘法Gate的MLE,注意Gate的描述与电路的编码 无关,也跟input witness无关,所以它的计算可以在preprocessing 阶段就开始了,没有必要等到协议中才开始

  • 代表电路编码 与 电路编码 是否一致,的MLE

GKR Protocol with ZK Argument


image.png

仍然以为个图为例来扮演整个协议的过程。其中电路的个数,所以;有限域的moduler 。​


Step ZERO


假设前半部分为public input,后半部分为witness,对witness 的每个元素进行commit,并发送给verifier :


Step ONE


prover 发送电路的output 作为Sumcheck的初始claims,verifier 根据给定的电路第0层的evaluation 值:


可以插值出相应的多项式:


verifier 生成challenge factor,并发送给prover,接下来进入第1层电路的 sumcheck 协议,prover 需要证明:


Step TWO


将第1层的sumcheck 多项式拆解成多个item :


合并item :​


Round one


prover 计算本次round 验证需要用到的proof,也就是单变量多项式


备注:​ 其它编码取值对应的多项式为0,就没有一一枚举出来

则:


prover 需要把多项式的commitment发送给verifier,也就是把该多项式的4个系数的commitment 之后发过去:​


verifier 需要验证:​


根据commitment 加法同态的性质,需要验证:​


验证通过,verfier 发送challenge factor  ,下一个round 需要验证的目标值为:​


Round two


基于 ,prover 计算本次round 验证需要用到的proof,也就是单变量多项式

备注:​ 其它编码取值对应的多项式为0,就没有一一枚举出来


则:​


prover 需要把多项式的commitment发送给verifier,也就是把该多项式的4个系数的commitment 之后发过去:​


verifier 需要验证:


根据commitment 加法同态的性质,需要验证:​


验证通过,verfier 发送challenge factor给prover,下一个round 需要验证的目标值为:


Round three


基于,prover 计算本次round 验证需要用到的proof,也就是单变量多项式

备注:​ 其它编码取值对应的多项式为0,就没有一一枚举出来


则:


prover 需要把多项式的commitment发送给verifier,也就是把该多项式的4个系数的commitment 之后发过去:​


verifier 需要验证:​


根据commitment 加法同态的性质,需要验证:​


验证通过,verfier 发送challenge factor给prover,下一个round 需要验证的目标值为:


Round four


基于,prover 计算本次round 验证需要用到的proof,也就是单变量多项式

备注:​ 其它编码取值对应的多项式为0,就没有一一枚举出来


则:


prover 需要把多项式的commitment发送给verifier,也就是把该多项式的4个系数的commitment 之后发过去:​


verifier 需要验证:​

根据commitment 加法同态的性质,需要验证:​


验证通过,verfier 发送challenge factor 给prover,下一个round 需要验证的目标值为:​


Round five


基于,prover 计算本次round 验证需要用到的proof,也就是单变量多项式


则:


prover 需要把多项式 的commitment发送给verifier,也就是把该多项式的4个系数的commitment 之后发过去:​


verifier 需要验证:​


根据commitment 加法同态的性质,需要验证:​


验证通过,verfier 发送challenge factor给prover,下一个round 需要验证的目标值为:​


Last Round


目前challenge factor 的组合为:


prover 根据第1层电路的evaluation 值很容易就能插值出相应的MLE 多项式:​


prover 分别计算出三个claims 值的commitment:​


verifier 拿着这三个commitment 完成第1层电路 sumcheck 协议的最后验证:​


mini-protocols ​


第一层电路evaluation 对应的MLE :​


上一个sumcheck 协议的Last Round中prover 新增加了两个claims,也就是:​


引入一个fold factor  我们可以把两个claims fold到一起:​


它的非常重要的特性就是:​


prover 把多项式进行commit后发送给verifier,同样也是多个系数分别commit,该多项式degree 为2,也就是说最多有3个commitment:​


verifier 拿到多项式的commitment 后就可以计算出:


这样就可以验证prover 之前发送的的commitment 是否与当前多项式的commitment 是否一致


为了验证prover 之前发送的的commitment X、Y是否合法,基于多项式的commitment , verifier 随机采样一个challenge factor  并发送给prover,prover 自然可以计算出下一轮sumcheck协议需要证明的evaluation值,即:


同时verifier 计算下一轮sumcheck协议需要证明的 的commitment:


最后我们再明确一点:mini-protocol 的根本目的是把两个claims fold成一个claims,减少prover 的成本,不然prover要分别证明两个claims:​

这样应该能make sense!


Step THREE


同Step TWO 一样,这里我们省略掉N 行文字+公式… 直接进入到Final Step!


Final Step


我们再回顾一下最开始的实例结构图:

image.png

根据最下面一层(public input + witness)的值,我们可以插值出MLE:


Step THREE 的mini-protocol 同样也会归结到证明两个claims,为了方便描述我们假设


多项式


假设fold factor ,把上面的两个claims合并成一个claim:​

备注:简单一句话就是,证明最下面一层(public input+witness)电路、Gate编码为(2, (3, 4)), evaluation 值为2 ,组成的在MLE 多项式上。


同样,verifier 基于prover 提供的的commitment,计算出 的commitment:


verifier 如何验证prover 提供的这个commitment的合法性?对于verifier 来说最下面一层电路的evaluation 分 public input p和 witness w,其中后者未知,假设两者长度相等,按照上图中的实例,也就是说前半部分为public input,后半部分为witness:


因此,我们需要把拆解成两部分


最终是要计算出的commitment,其中public input 部分因为是公开的,所以verifier 可以自行计算出相应的MLE 多项式,并拿到的commitment;另外witness 部分因为在Step ZERO prover 已经把它们的commitment 全部都已经发给verifier 了,verifier 只需要基于此拿到 的commitment就可以了:


最后的最后,我们put it together :​


What’s Next


到此为止,满足ZK argument的Vallina 版本的GKR协议也就完整了,紧接着我们再detail一下Hyrax 在此基础之上都做了些什么?接着再看看Spark 在Hyrax基础之上做了些什么?最后再看看Spartan 的整个全貌?


参考资料


【1】Hyrax 论文:https://eprint.iacr.org/2017/1132.pdf

【2】PAZK by Thaler:https://people.cs.georgetown.edu/jthaler/ProofsArgsAndZK.pdf

【3】trivial GKR 协议:https://learnblockchain.cn/article/6199

【4】sumcheck 协议:https://learnblockchain.cn/article/6188

Thanks


  • 感谢SecbitLabs @郭宇 前两个月分享的Spartan Overview (尽管当时也没太理解), 以及@even 在研究方向上的指引(据说Hyrax 不太好啃),不至于走太多弯路。

Motivation


缘于folding,缘于NOVA,缘于Setty,了解到了Spartan,但并不认识它,所以才有了本篇及接下来的关于它的一切(预备知识)…… 

image.png

关于Spartan,在ZK领域可能时间上相对也有点儿远了,暂且不考虑它在某些方面的争议,它的一些思想其实已经影响到其它比较热门的方向了,比如当下的热点Lasso & Jolt,所以它的研究意义仍然很大。


Overview 


  • 本篇文章主要参考Hyrax 论文后半部分5-6节,即Hyrax 基于GKR with ZK Argument的contribution。

  • 主要分为两部分,前半部分Reduced Sumcheck Verification主要针对GKR with ZK Argument的Step Two做的优化,对应Hyrax 论文中的Part 5。

  • 后半部分Reduced Witness Evaluation 主要针对GKR with ZK Argument的Final Step做的优化,对应Hyrax 论文中的Part 6。

  • 为了方便对照原始论文理解,本文中的notion尽量与Hyrax 原始论文对齐。

Reduced Sumcheck Verification


image.png

仍然以这个图为例,,则;第0层,,则;第1层,,则;第2层,,则


Number of Sumcheck Commitments


为了简单起见,上一篇GKR with ZK Argument 中Sumcheck 协议每次round prover 发送给verifier 的多项式系数的commitment的个数我们固定都是4,也就是说多项式的degree全为3。其实prover 需要commit的多项式的degree是有变化的


当round 时,prover commit的多项式的degree为3,也就是说commitment的个数为4:


当round 时,prover commit的多项式的degree为2, 也就是说commitment的个数为3:


Sumcheck Verifications


我们试图把verifier sumcheck 协议中所有round的校验等式且一个矩阵点乘运算表示:


其中每个round prover发送的message 为:


把它们聚合到一个向量里:


其中每个round verifier 需要验证时用的参数:

把它们聚合到一个矩阵里:


每一个round verifier 校验的结果:

备注:其中, 是第1个round 需要校验的sumcheck 值,是verifier 随机采样的第0层电路编码的evaluation值,也是prover 第1个round要证明的值。


汇总一下就是:


矩阵 需要verifier 自行计算,用红色标记的向量 和 向量 都是需要prover 进行commit。 如果说仍然是一个field对应一个commitment,那么commit之后的校验就变成了:

有没有觉得向量 size太大了(19个commitment)?是的,它直接影响着协议过程中的communication cost,所以需要进行压缩处理。


Reducing Sumcheck Commitments

一个field 对应一个commitment: 每次commit的时候还需要一个blind factor .


这样的话Sumcheck 协议中的commitment个数就会与要commit的多项式的degree成线性关系。如果把一个多项式所有参数的commitment压缩成一个commitment:

这样的话就需要多个generator 了,但blind factor 变成了一个


我们用矩阵第一行的校验为例:

如果把commitment 压缩成一个commitment ,verifier 就无法直接通过上面的等式来进行校验。这其实就转换成了大家所熟知的IPA 证明,即Inner Product Argument verification。 接下来简单描述一下IPA协议的执行过程


IPA Protocol Overview

prover 要证明query 向量 满足:

Step One

prover 生成多项式的commitment,并发送给verifier

Step Two

prover 采样一个与向量 等长的向量,对它进行commit;同样也与query 向量 交互,结果也进行commit,最后同样也发送给verifier

Step Three

verifier 发送一个challenge factor 给prover,prover 计算

并把它们全部发送给verifier。

Step Four

根据commitment 同态性质,verifier 验证:


Reducing Sumcheck into IPA

最后我们看看Hyrax 中Sumcheck 协议被reduced成IPA 协议后的执行过程:

Step One

把多个verification fold成一个:

进行commit:

进行commit:

commit 之后,prover要证明的变成了:

把两组commitment 全部发送给verifier。


定义 ,即剔除掉最后三个元素,则:

Step Two

prover 随机生成一个与 等长的向量,同 一样计算它的commitment,及 的commitment:

把两组 commitment 全部发送给verifier。


Step Three

verifier 发送一个challenge factor 给prover,prover 计算:

Step Four

verifier 验证:


到此为止多个round 的Sumcheck verification就被转换成了一个IPA verification,proof size(commitments) 也被进一步压缩。


Reduced Witness Evaluation


Recall


GKR with ZK Argument协议的Final Step是要对最下面一层(input + witness) 的某个evaluation 进行证明,我们仍然用GKR with ZK Argument中的例子:


需要verifier基于Step ZERO发送过来的每个witness对应的commitment 计算witness MLE上的evaluation值对应的commitment:


它的问题在于,需要对每个witness进行commit(上面红色标记的部分),导致communication cost 和 verification cost都会比较高,与witness的长度成线性关系,Hyrax 对其进行了压缩,变成了子线性关系


Square-root commitment scheme


Hyrax 在这里的整体思路是,把上面witness evaluation 的commitment的计算代理给了prover,prover 提供计算结果的同时需要提供相应的proof给verifier 验证,当然了verifier 验证的成本肯定要低于自己计算的成本,满足succinct 特性:

把witness evaluation 的commitment的证明最终变成了一个IPA 的证明。


实例中


Evaluation and Proof


prover 把witness 向量转换成一个矩阵表示, 其中分别代表行和列:


按行进行commit:

把witness的commitment 连同evaluation 的commitment 一起发送给verifier。


Compressed Lagrange Basis


基于MLE 多项式:


我们把Lagrange Basis Polynomial 一拆为二:


结合上面的witness 矩阵, 一定有:


通过两组的子向量来represent 长度为的整个向量,这里应该是一种很常见的succinct 做法。比如protostar 论文中3.5 节compressed verification也是采用了这种技巧,细节可以参考 https://learnblockchain.cn/article/6503


所以verifier 需要自己计算拿到两个向量(为了简化,实例中,所以其实是没有起到compress作用的,如果compress 效果就出来了,读者可以自行举例):

并计算得到: 其中 为commitment, 为verifier 刚计算好的scalar,最终verifier 拿到一个commitment


IPA for Evaluation Verification


最终verifier 需要对prover 提供的evaluation的commitment进行验证,这时的验证就变成了标准的IPA 验证:

关于IPA 的执行过程这里就不再赘述了,可以参考上面的IPA Protocol Overview。


Summary

到此,Hyrax 协议也就完整了。简单总结一下,Hyrax 本质就是一套GKR 协议,它在proof size 和 verification 方面做了一些工作。


References


【1】Hyrax 论文:https://eprint.iacr.org/2017/1132.pdf

【2】PAZK by Thaler:https://people.cs.georgetown.edu/jthaler/ProofsArgsAndZK.pdf

【3】protostar compressed verification: https://learnblockchain.cn/article/6503

作者: 白菜
标签: Sumcheck, IPA, GKR, Hyrax, VSM, Spice, Spark, Spartan
时间: 2023-10-06


Table of Content


Motivation


缘于folding,缘于NOVA,缘于Setty,了解到了Spartan,但当时并不认识它,所以才有了本篇及前两篇前置[3] 和[7]…… 

image.png

关于Spartan,在ZK领域可能时间上相对也有点儿远了,暂且不考虑它在某些方面的争议,它的一些思想其实已经影响到其它比较热门的方向了,比如当下的热点Lasso & Jolt,所以它的研究意义仍然很大,尤其是其中的Spark。


Introduction

本文是Spartan [4] research的终结篇,将重点囊括Spark 和 Spice [1],前两篇[3] 和 [7] 是本篇的预备部分,如果对Sumcheck 不熟悉,也可以参考一下[6]。


Spark 是Spartan 整个协议的core part,Memory Check是Spark 的core part,而VSM又是Memory Check或者Spice的core part。本篇文章将从内向外延伸,读者尽管从感兴趣的地方切入即可。


VSM in Spice

VSM,全称Verifiable State Machine,这个概念源自Spice[1]。


它的一个intuitive 的理解就是:把任何一个storage 对象当作一个State Machine,在这个State Machine上的任何operation 导致其state 的transition 都可以生成相应的proof 给verifier 验证。这就是所谓的Verifiable State Machine。


以单个Query或者read operation为例,我们看看Spice 中的VSM 长什么样子: Alt text


关于Spice的其它细节这里不展开,大家可以参考原始资料Spice [1]。

Takeaways

  • Spice 有两个drawback 或者特性:其一,批量验证,也就是n 个operation之后一起验证,成本会平摊到每个operation上,所以成本会很低,但会有时间上的delay;其二,如果验证不通过,是无法判断哪个operation 出的问题

  • Storage 作为三方一个独立存在的个体,state 通常以key-value-timestamp的格式出现,它相比传统的key-value 的Storage 的区别就是加了一个可以表征State Machine的非常关键元素Timestamp

  • Prover 自身维护两个set state ,发送一个query/read operation 会更新本地的这两个set state ,并update Storage的state ,发送一个write operation 会更新本地的state ,并update Storage 的state

  • Verifier 拿着更新前的state ,和更新后的state ,以及operaton 过程产生的中间state 或者proof ,进行最后的验证,验证通过说明返回的结果没有问题

Memory Check for Spark

Spark 中Memory Check 的核心思想源自Spice[1],Spice 支持读、写操作的验证,而Spark 中只需要具备lookup 功能的验证,所以可以简单理解为read-only 版本的Spice。


验证query/lookup 的结果对与不对,上一节我们提到过,这里我们就以Spartan为背景,举个实例detail一下它的执行过程,可以回答大家可能比较关心的两个问题:1. memory check 究竟解决的是什么问题?2. 为什么它可以work?


Problem

假定有这么两个query 向量:


另外,假定:

以及相应的两个Storage 对象,或者叫lookup table


query 的过程:给定 向量中的某个元素值,返回相应table 中evaluation值。比如请求,返回


为了简化,以下实例我们均以query 为例。如何证明它的返回结果 是正确的呢?这里我们detail 一下上一节中VSM 的逻辑。


Resolution

Initialization

lookup table 最开始的State:


Operation

批量query 之后, 的State 变成了:


批量query 之后,prover 这边维护的两个中间State 或者proof 为:


Verification

批量query 之前,verifier 请求拿到 的初始State ;批量query 之后,verifier 请求得到 的最新State ,再结合prover 传递过来的proof 进行最后的验证:



接下来的问题是,如何把上面的计算过程算术化?


Arithmetic

这个等式本质是要判定两个set 是否相等?也就是说是一个permutaion的问题,自然就会联想到plonk 里lookup contrain用到的grand-product 的逻辑,也就是说:

演变成了:


很显明三元组的元素是无法直接相乘的,引入两个challenge factor 把三元组的元素合成一个field:


因此上面的等式就变成了:



接下来的问题是,如何把上面的算术逻辑放在电路里,以便通过某个或者某几个协议来完成它的验证?


Circuit

我们把上面的等式grand-product 拆分成四个部分:


假定这四组向量都是witness,用GKR-like layered circuit 来把这四个grand-product 的计算trace 给描述出来,通过Hyrax [2] 协议来完成grand-product 的验证,最后验证四个grand-product 的结果是否满足等式即可:


我们用图直观感受一下这四个电路长什么样子?


Circuit for : Alt text


Circuit for : Alt text


Circuit for : Alt text


Circuit for : Alt text


熟悉Hyrax 协议的应该知道,Hyrax协议是由多个Sumcheck 协议与一个IPA协议组成 [3]。Hyrax 的最后需要计算tree 的叶子节点,也就是witness向量,的MLE 多项式在某个opening 上的evaluation,它是通过一个IPA协议来完成的。


也就是说,在这里我们分别需要通过IPA协议完成四个evaluation 的验证:

等式右边的四个evaluation值是通过Sumcheck 协议reduce 后拿到的。

是由相应的三元组,即 组成。所以上面的四个evaluation 需要进行再次拆解。


for evaluation

三个dense 向量或者witness 为:

用3个IPA协议去验证以上三个MLE 的evaluation值是否合法。


for evaluation

三个dense 向量或者witness 为:

用3个IPA协议去验证以上三个MLE 的evaluation值是否合法。


for evaluation

三个dense 向量或者witness 为:

用3个IPA协议去验证以上三个MLE 的evaluation值是否合法。


for evaluation

三个dense 向量或者witness 为:

用3个IPA协议去验证以上三个MLE 的evaluation值是否合法。



到此为止,Spark 中memory check 的逻辑就完整了!关于Spark 的应用在Brakedown [5]中也有应用,感兴趣的话也可以参考一下。


最后我们再revisit 一下之前提到的两个问题:

Info

memory check 究竟解决的是什么问题?又为什么可以work?

抽象地说是,Verifiable Random Access Memory,简称vRAM。把对内存访问结果的验证转换成一个Verifiable State Machine,简称VSM,的问题,也就是一个可验证的state transition的问题,最后通过电路的形式把state transition验证计算的trace 表达出来。


接下来我们就可以非常轻松的review 一下Spark 了。

Spark Overview

Target of Spark

的时间复杂度完成Sparse Matrix Polynomial 的evaluation。


比如,有一个Sparse Matrix:

其中涉及到的常量:

代表矩阵的行数/列数, 代表矩阵中non-zero 元素的个数, 代表matrix dense MLE中的变量个数。


上面这个matrix 的dense MLE 可以表示为:


因为是dense 的表达,所以默认是按顺序遍历的,一共有 次乘法运算,即时间复杂度为,成本随着 的增大,会呈现asymptotic 式的增长。在Spartan中, 又代表R1CS 的gate 数量,这种特征就会体现得更明显。


Spark 把sparse matrix 的evaluation代理给prover,并通过memory check 的消除这种asymptotic,把它的时间复杂度控制在,跟R1CS 的gate 数量无关了,仅仅跟Sparse 的呈度有关。因此,matrix 越稀疏,它的优势就体现得越明显。即


Technic in Spark

首先,Spark不再用矩阵表达了,而是换作三个向量来表达:


其次,Spark 改变了MLE evaluation 的多项式:


假定,通过时间复杂度为 的计算,可以拿到两个类似lookup table的东西

通过查表的方式,我们很容易拿到 的取值:

因此我们可以得到sparase matrix 在 上的evaluation值 。接下来prover 要做的就是生成相应的proof :

本质是要证明三个多项式乘积的sum 等于0,这是一个标准的degree为3的Sumcheck。Sumcheck 的last round 需要验证:

其中 Sumcheck 最后reduced 得到的claim,左边的三个term,其中 可以轻易地通过一个IPA 协议证明得到;但是, 呢?也直接可以通过IPA 协议证明吗?


不行!细心地会发现上面的表中 并不是跟 一样以determined dense vector 或者 determined witness的形式出现在verifier 面前的,对于verifier 来说,它只知道有: 这三样determined 的东西, 是prover 基于通过查表 拿到的,对于verifier来说叫做non-determined witness,也是一种中间过程变量。因此还需要一个验证查表过程的电路,来保证它们的来历合规合法!


剩下的就是memory check的show time,上节已经detail 了它的整个过程,这里就不再赘述。


到此为止,Spark是如何更高效地解决Sparse Matrix evaluation 问题的就已经解释清楚了。文章的最后我们就可以非常轻松地revisit一下 Spartan 的整个协议了。


Spartan Protolcol Overview

假定,有这么一个业务计算:


setup 阶段构造R1CS Instance:

这是非常典型的sparse matrix,可以充分发挥Spark的优势!


prover 填充向量:


令:

其中 代表矩阵 的第0行,同理


还有:


prover 需要证明:


令:


实例中,:


假设:


上面红色部分等于0的概率就非常低,因为 的domain是整个field,等于0或者1的概率自然就非常小几乎可以忽略,那么我们就可以推出:


Round One

verifier 随机给定一个challenge factor ,prover 只需要证明:


这是典型的degree 为3的Sumcheck(三个MLE polynomial的乘法),Sumcheck 的最后reduce 成:


其中 verifier 可以自行计算,但 隐藏着witness信息,需要prover 计算完成之后发送给verifier,verifier 完成上述等式验证。

Round Two

接着prover 需要证明:


这又是典型的degree 为2的Sumcheck(两个MLE polynomial的乘法),Sumcheck的最后reduce成:

Round Three

上述等式中 evaluation 的证明可以直接通过IPA 协议来完成,而 evaluation 的证明就需要Spark 协议来完成了。


剩下的就是Spark的show time,上节已经detail 了它的整个过程,这里就不再赘述。到此为止,Spartan 整个协议的详细逻辑就完整了!


One more thing

纵观Spartan 整套协议,里面穿插着大量的Sumcheck 协议、IPA 协议,工程实现中应该会有相应的proof aggregation 的操作,具体细节可以参考Spartan [4]中的7.23 节和 8节。


Thanks

  • 本着research 的原则,边“猜”paper 作者的意图边手动推理论证,试图用逻辑说服自己,期间免不了叨扰@even @郭宇 老师来求证自己的“猜想”,真诚表达对他们的感谢

  • 再次感谢SecbitLabs @郭宇 老师前两个月分享的Spartan Overview,视频链接暂时找不到了,后续再补上;再次感谢SecbitLabs @even 关于Spartan在研究方向上的指引


References

[1] Spice: https://eprint.iacr.org/2018/907.pdf

[2] Hyrax: https://eprint.iacr.org/2017/1132.pdf

[3] Spartan 预备知识:Hyrax: https://learnblockchain.cn/article/6586

[4] Spartan: https://eprint.iacr.org/2019/550.pdf

[5] Brakedown:https://eprint.iacr.org/2021/1043.pdf

[6] GKR 协议系列之Sum-Check: https://learnblockchain.cn/article/6188

[7] Spartan 预备知识:GKR with ZK Argument: https://learnblockchain.cn/article/6566

如果你是一个SNARKER,你一定听说过KZG Commitment,如果你听说过KZG Commitment,那你一定知道Pairing。这就是我们接下来要讨论的,大家如果想了解Pairing 的底层逻辑(pairing primitives),或者对它的应用感兴趣都可以留言,或者添加文末的联系方式。


至今距离pairing 的“尘埃落定”其实已经大概有6、7年的时间了,网上的资料很完整,但关于它的讨论(工程上)仍未止步,比如On Proving Pairings.


本文所有内容源自hackmd上的note,欢迎follow.


这里没有的

  • group theory, field theory and homomorphism

    相关基本概念在这里不会涵盖,详情请查阅任何abstract algebraic 相关的书籍

  • divisors

    相关基本概念在这里不会涵盖,对于了解Pairing 来说 Pairing for Beginners 已足够,如果你还想深入理解最好翻阅一下 algebraic geometry 相关的书籍

  • structure of elliptic curve over finite field and its arithmetics (scalar multiplication)

    理论和算法部分这里不会涵盖,详情可以查阅 Guide to Elliptic Curve Cryptography

  • hash to curve

    bytes string 映射到或者 上的点,简单说就是hash,是pairing 应用层面必备的一大模块,后续会详细补充这块内容

  • non-affine coordinate

    affine coordinate 其实只是椭圆曲线元素表达的需要,它的scalar multiplication 并不经济,所以实际计算上都会用non-affine coordinate 来替代,后续会补上这块内容

  • advanced scalar multiplication algorithms GLV/GLS

    特定的曲线上充分利用同态映射来加速scalar multiplication,同时还能(GPU)并行化处理也是当下硬件加速卖点,后续也会再补上



这里有的

本篇文章集中讨论了各种Pairing 变体:

和它们的具体实现。除此之外,我们还包含了一些重要的实现层面的tricks,尤其是:



关于代码

  • python implementation

    主要集中在Pairing的计算逻辑上,包括Miller LoopFinal Exponentiation。目前已经完成验证。

    Finite FieldElliptic Curves的算术运算并没有逐一实现,用的是Sagemath库自带的 Galois Field and Elliptic Curve.

  • rust implementation

    从零着手,从 Bigint 算术运算到 Finite Field 算术运算到 Elliptic Curve 算术运算,再到 Pairings Primitives。底层的逻辑已经验证完毕,目前在Pairings验证过程中 …


公共信息

  • Modulus of base prime field (characteristic) with 381-bits:

  • Embedding degree, or the degree of full extension field :

  • Elliptic Curve (additive group) over base prime field :

  • Elliptic Curve (additive group) over extension field :

  • Largest prime factor of with 255-bits:

  • Trace of Frobenius:

  • Parameter for BLS12 Pairing-family: for:

  • Target (multiplicative) group with order defined over :


Pairing 的演进

Weil Reciprocity

and 是两个定义在椭圆曲线上的divisor function, ,它们的divisor support 不存在交集, 。然后我们就有:

其中 表示函数 的divisor, 表示divisor 在函数 上的evaluation。 也类似.


如果我们放松上面的约束条件, 如果 , 然后就有一个更general 的 Weil Reciprocity 公式: 其中 ,当两个divisor and 的support 存在交集, 否则 .

Details of general definition of Weil Reciprocity, you can refer THEOREM 3.9 of Guide to Pairing-based Cryptography.


那么Weil Reciprocity 究竟有什么意义呢? 它直接诞生了 Weil Pairing.


Weil Pairing

定义

假定在 -torsion subgroup 中有两个线性不相交的点, . 基于此,假定 , and , 同样 . 它们同样满足 .


然后我们就有:


这样,Weil Pairing 就出现了: 其中 , 是乘法group 上的-次单位元根 , 也就是说 .


如何选择合适的divisor and

理论上我们需要选择合适的 divisors and ,让它们的support 不相交, 你可能会奇怪,这应该有很多种选择,那么 and 不同的选择会导致最终pairing的结果 不一样吗?


事实上 Weil Pairing 的结果 它是与 and 的选择无关的。下面简单证明一下:


假定 and 都是与divisor 等效的divisor, 那么一定存在另外一个中间divisor 使得 , 然后:

根据 Weil Reciprocity 定理, 由于 , 所以 . 因此:


既然跟divisor 具体的选择无关,那我们就选择最简单的 divisors: . 这时,它们的support 是存在交集的,根据上面那个general Weil Reciprocity公式,我们就有Weil Pairing的正式定义:


如何对divisor and 进行evaluate

divisor 的evaluation 可以被进行一步简化:

只要 and 是线性不相关的,即 .

注意上面的符号是 不是 ,也就是说它们evaluation的值可能不同,但并不会对Weil Pairing 最终的结果 有影响,即:


因此 Weil Pairing 简化为:


Miller Loop 使得divisor function的evaluation 变得更容易实现,是工程上的一大步。很明显 Weil Pairing 是几何上对称的, 它实际上需要运行两次 Miller Loop. 看起来并不太经济? 实际上单次就够了,这就是 Tate Pairing 要做的事情.


算法

直接参考Guide to Pairing-based Cryptography 中的Algorithm 3.2:

algorithm3.2


Tate Pairing

你可能会奇怪divisor function的evaluation 长什么样子? 由于 , 然后 , 所以 . 运用coset 的特性, Tate Pairing 就出现了:

它分两步走, Miller LoopFinal Exponentiation. 这也是我们所说的 Final Exponentiation 的由来。


定义

其实 Tate Pairing 有一个更正式的定义: 其中 , 并不是-torsion subgroup 中的元素, 它不再跟一样被定义在group 。而是商群的某个元素, 确切的说就是group 上的任意一个与 线性不相关的元素. 看起来似乎是把约束条件放得更宽了。


既然这样,那么divisor function的evaluation值 (result of Miller Loop)会变成什么样子呢? 同样,它一定也是商群的某个元素,确切的说就是group 上的任意一个元素,这也更加坚定了后续提指Final Expoentiation的必要性:


似乎Tate Pairing 要比Weil Pairing更通用 (more relaxed constraints) ,是吧?

Since , usually for the convenience of computation(utilization of Frobenius Automorphism) we let , namely , is so-called Base Group. While , namely , is so-called Trace-zero Group. :::


算法

同样直接参考 Guide to Pairing-based Cryptography 的Algorithm 3.3:

algorithm3.3


Miller Loop

你可能已经注意到, Weil PairingMiller Loop 的长度 and Tate Pairing 的都是 (bit length of ).

理论上 Tate Pairing 已经够实用了,至少实现起来是没有任何阻碍的了,所以后续的research 其实主要是针对工程实现上的优化,基本框架并没有改变。 基本集中在缩短 Miller Loop 的长度,以及更高效的提指 Final Expoentiation运算.


还有Miller Loop 更短的算法? 是的,但是我们需要深入挖掘一下乘法group 的结构.

Ate Pairing

Ate Pairing中, 点 被严格约束在 中,同时点 也被约束在 中,即Frobenius Map 充分利用Frobenius Map的特性,将大大降低pairing的计算成本。

Miller 算法的两个重要特性

关于这两个特性的proof,这里不再推演,熟悉divisor function 后很容易推导出来。


更短的 Miller Loop

由于, 假定 ,因此我们就有:


由于 , 然后 , 我们就有 , 因此:

看起来我们似乎可以用 替代 了, 但是这完全没有必要,因为 ,反而让Miller Loop 变得更长了。


如果 and , 然后 , 假定 , 类似地我们有:


根据上面的两个 Miller 算法的特性, 可以继续推导: , 我们有:

由于 , 我们完全可以用 替换 , 但是如何找到这个 值呢?


Trace of Frobenius

根据 Hesse Bound 定理, 我们有: 其中 就是我们据说的Trace of Frobenius, 由于 , 然后 .

最后我们得到:

很明显 Tate Pairing Ate Pairing 有着非常紧密的关系。 你可能已经注意到,这两个pairing的计算结果 很可能不相等,不用紧张,只是pairing策略的差异而已,并不影响它在乘法group 中的唯一性,这才是pairing 的最终目的。


事实上Ate Pairing 在做的就是找到与 的某个倍乘相关的数, 就是我们要找的,它满足 . 但是, 一定是最短的 Miller Loop吗? 可能是(也可能不是),下面写几行代码反证一下:

p = 103
r = 7
k = 6

for i in range(1, k):
    print('lambda[{0}] = {1}'.format(i, (p ** i) % r))

运行结果:

lambda[1] = 5
lambda[2] = 4
lambda[3] = 6
lambda[4] = 2
lambda[5] = 3

很明显,并不是最小的, 才是。


Optimal Ate Pairing

optimal ate


在上面的 Ate Pairing 中, 我们直接地用 替换 后得到: 其中 , and .


Ate Pairing 中,我们有: 其中 , and .


基于此,我们可以找到二者之间的联系:


因此,Ate Pairing 经过 次方提指后变成: 暂时先把结论放这儿.


Optimal Ate Pairing中把进行了更通用的定义:, and . 同样运用上面的 Miller 算法的特性, 我们把它的divisor function 进行展开:

其中:


然后 Ate Pairing 就被转换成了:

很明显 Ate Pairing 被划分成了两部分, 左边部分 是基于Ate Pairing (length of Miller Loop is )。

既然左边已经是一个Ate Pairing 了,那么右边部分 肯定也是一个Ate Pairing,只要 **。


所以Optimal Ate Pairing 正式定义就来了: 其中系数 都是尽可能小的数.


不用担心指数运算 , 它几乎是免费的,在充分运用 Frobenius Map后. Optimal Ate Pairing 要做的就是并行计算 and , 此时Miller Loop 的长度可能就是 .


可以如何找到这么一组系数 呢? 实际上它是一个关于 Lattice 的问题,感兴趣可以继续研究 Optimal Pairings.



有限域上的算术运算

BLS12-381 曲线的定义是这样的: 其中 。但是这个extension field 是如何构建的呢?



Pairing 中域的切换

为了对Pairing 底层的算术运算有个更直观的sense,下面简单介绍一下Tate/Ate pairings域的切换


假定定义在域 上的点,同时点 定义在域 上,实际上点 的坐标必须定义在域 的某个子域上 (先给出结论,后续有推理过程),比如说 。整个过程,可以切分为4个部分:

  • Miller Loop

    • Double-Add

      line function 不会改变所在的域,在哪个域,这个函数仍然在那个域,比如 :

      double_add.drawio

    • Evaluation Line Function

      比如,单个line function的evaluation:

      最终evaluation 的结果, 不再定义在原本的域 上了.

      double_add

  • Final Exponentiation

    • Easy Part

      通过提指Mill Loop 的结果 推进一个特殊的乘法group,这就是我们所说的 Cyclotomic Group:

      easy_part

    • Hard Part

      再次通过提指Cyclotomic Group 拉到目标乘法group :

      hard_part


域塔 Tower Fields

定义

大家知道BLS12-381 Pairing中的目标group 是一个定义在上的-torsion multiplicative subgroup,我们经常表示为


那么 是如何被构造出来的呢? 这就是tower fields 的由来:


在BLS12-381曲线上,extension field modulus的常量分别为:

模的由来

  • is irreducible in

  • since is one root of , then we have is irreducible in

  • since is one root of , then we have is irreducible in

  • therefore we have


也就是说,域 上的算术运算可以通过域 上的算术运算来完成,同时域 的算术运算可以通过域 上的算术运算来完成,同样域 的算术运算可以通过base prime field 上的算术运算来完成。这就是我们据说的域塔 tower fields


你可能已经注意到域的拓展 都是二次拓展 quadratic extension, 而 是三次拓展 cubic extension。所以 quadratic extensioncubic extension 在高阶extension field (比如)的算术运算中扮演着非常重要的角色。


Quadratic Extension 上的算术运算

这部分属于常规的计算逻辑,可以直接参考 Guide to Pairing-based Cryptography 5.2.1 章节。


Cubic Extension 上的算术运算

同样,这部分也可以直接参考 Guide to Pairing-based Cryptography 5.2.2 章节。


Cyclotomic Group 上的算术运算

分圆群 Cyclotomic group 在Pairing 的提指运算 Final Expoentiation 扮演着最核心的角色,特别是在 Tate/Ate Pairings中。既然是提指,那么主要就是平方 squaring 和指数 exponentiation 这两个算子。下面主要推演一下squaring 的全过程。


假定 表示base prime field,那么如何计算 ?


首先,我们需要利用tower fields来表示 ,比如:

假定 : 则:

由于 , 我们继续推进:


所以最终我们会有3次域 上的squaring (分别是 ), 和5次域 上的multiplication (分别是 )。


上的乘法运算可能会比较昂贵,那么有没有改进的方法呢? YES


Squaring Friendly Field

如果 , 而且 是一个非常大的素数characteristic,乘法group 的阶可以用多个cyclotomic polymomials 的连乘来表示:

这里我们称 Squaring Friendly Field.


举个例子乘法group :

其中:


换句话说,乘法group 的阶可以被因子分解成:


所以乘法group 中一定存在一个阶为 的subgroup


因此,我们得到一个非常重要的结论:


更快的 Squaring 算子

回到上面的Squaring 运算,有 :

Squaring 之后:

现在的问题是如何有效地计算 ?


Tate/Ate PairingFinal Exponentiation,根据上面刚刚推演出的结论:

其中:

但是如何有效地计算诸如 ? 我们拆开来看:

  • 根据Frobenius Map 的特性,我们很容易得到: (先给出结论,在后面Frobenius Map 部分会进行推理)。因此上面的式子简化成:

  • 由于 ,因此: 其中 是域上的 primitive 6-th root of unity,也就是说 .

    More properties of primitive 6-th root of unity in :


综合在一起,我们得到:


应用Frobenius map 后:


应用Squaring Friendly Field的特性,我们得到:

展开后,得到:

所以上面的三个乘法运算被转换成:

最终:

上的5个乘法,只剩下1个乘法,共轭 完全免费。


Twist 的力量

为什么要twist

尽管我们通过tower fields 来表示 ,不幸的是 仍然有点儿贵,尤其应用在链上或者微型终端设备上。所以我们可以简单地把twist 当作pairing实现层面的一种高级的trick来看待。


我们可以通过sextic-twist(twist degree 是 6) 把高阶域 上的元素映射到低阶域 : 但是如何做呢?

Sextic Twist

一个定义在高阶extension field 上的椭圆曲线 :

另一个定义在低阶extension field 上的twisted 椭圆曲线 ,它与有着twist isomorphism关系: 其中 is both non-quadratic and non-cubic residual, 也就是说:

因此:


但是如何选择一个合适的 呢? 似乎 刚好把域 拓展到了 。幸运的是,刚好 就是这么一个数。

According to above tower fields, we can easily have:


所以这个twisted 的椭圆曲线就是 :-1: :

这就是我们想要的 吗?一定是我们要找的twist参数吗?

不一定,原本 是定义在上的,也就是上面的的subgroup ,但是 上的运算成本较高,所以想通过twist 的方式把 上的点一一映射,这样运算成本会大大降低。但是,可能会存在: 也就是说可能不能整除 ,曲线 上可能不存在一个-torsion subgroup。这是不满足我们一一映射的目的: 所以我们在选择twist 参数里要特别小心。那如何找到满足条件的twist 参数呢?其实这个参数只有两种可能性。


如果 不合适,那么 一定是那个合适的(论文 也有提及),大家也可以试一下,最终我们确定 所在的曲线: 其中 或者 and .


Sextic Twist Map

下面简单介绍一下两group and 元素之间的映射关系:

  • Twist Operation

    上的元素映射到 上:

  • Untwist Operation

    上的元素映射到 上:


twist/untwist的过程是很便宜的,尤其是当我们把期间用到的常量 预先算出来。最后明确一下,既然要用twist trick,那么将尽可能把运算都限制在低阶的域上,只是在必要的时候才通过untwist 把值转换到高阶域上。


Frobenius Map 的力量

同twist 一样,Frobenius Map 同样是pairing 实现层面的高级trick。它在extension field的运算过程中扮演着非常重要的角色,特别是Tate/Ate Pairings 中的提指Final Expoentiation


下面我们粗略感受一下它分别在extension field 上有哪些特性:

Frobenius Map over

假定:

其中 and , we have .


然后:

由于 一定是个奇数,所以我们有:


结论: Frobenius Map 只要 .


Frobenius Map over

假定:

其中 and , 我们有 , and


然后: 其中 表示在 共轭 次 , and 表示在上norm 次.


两个方面需要考虑:

  • for

    We can easily have .

  • for

    Since , then , so we have:


结论: 只要 .


Frobenius Map over

假定:

其中 and .


类似地,

, 由于 , 然后我们就有 . 由于 , and , 然后我们就有:

因此:

结论: 只要 .


Frobenius Map and Conjunction

有一个 quadratic extension: 其中 , 假定 , 其中 . 如果我们想要在上执行Frobenius Map: 由于 is non-quadratic residual, 也就是说 , 因此我们有: 完全免费!


比如 .



Curve 上的算术运算

这里将是Scalar Multiplication 的主要战场。在BLS12-381中有两条曲线我们需要实例化, and 其中 是定义在Base Prime Field 上的 -torsion curve (subgroup) , 是定义在sextic-twisted field (relative to ), 上的 -torsion twisted curve (subgroup).


上的算术运算

其中 .


定义在 Base Prime Field 上,它只是 的一个-torsion subgroup. 所以它的算术运算(Scalar Multiplication) 跟一样,定义在


上的算术运算

其中 .


类似地, 只是上的 -torsion subgroup, 它的算术运算 (Scalar Multiplication) 跟一样,定义在extension field 上(上面有介绍extension field 的运算)。

上的算术运算

目标group 实际上并不是曲线 (additional group),而是一个-torsion multiplicative subgroup,通常表示为 。它的算术运算与 一样.



Python Implementation

Instantiation of Curve BLS12-381

Trace/Unti-trace Map

Trace Map: where is the full extension degree. It maps any -torsion points of into .


Untri-trace Map: It maps any -torsion points of into , whose Trace Map result is .


Since is defined over , so . While , this is where Trace-zero subgroup come from.


def anti_trace_map(point, d, p, E):
    return d * point - trace_map(point, d, p, E)

def trace_map(point, d, p, E):
    result = point
    point_t = point
    for i in range(1, d):
        point_x, point_y = list(point_t)[0], list(point_t)[1]
        point_t = E(point_x ** p, point_y ** p)
        result = result + point_t
    return result

Finite Field Conversion

## map element of Fp2 into Fp12
def into_Fp12(e_fp2, beta, F, gen):
    a = beta.polynomial().list()
    if len(a) == 1 :
        a = a + [0]
    e = e_fp2.polynomial().list()
    if len(e) == 1:
        e = e + [0]
    return F(e[0]) + F(e[1]) * (gen ** 6 - F(a[0])) / F(a[1])

## map elements of Fp12 into Fp2 with critical conditions
def into_Fp2(e_fp12, F, gen):
    coef = e_fp12.polynomial().list()
    zero_coeff = [1 for i in range(12) if ((len(coef) > i) and (i != 0) and (i != 6) and (F(coef[i]) == F(0)))]
    assert(reduce(mul, zero_coeff) == 1)
    
    return (F(coef[0]) + F(coef[6])) + gen * F(coef[6])

## map elements of Fp12_t into Fp12
def Fp12_t_into_Fp12(e_fp12_t, F, gen):
    coef = list(e_fp12_t)
    result = []
    for i in range(len(coef)):
        result.append([(F(c) * (((gen ** 6) - F(1)) ** j) * (gen ** i)) for j, c in enumerate(coef[i].polynomial().list())])
    
    return reduce(add, sum(result, [])) 

Twist and Untwist

def untwist(x, y, t_x, t_y):
    return x / t_x, y / t_y

def twist(x, y, t_x, t_y):
    return x * t_x, y * t_y

Definition of

denotes curve defined base prime field, namely


p = 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787
q = 52435875175126190479447740508185965837690552500527637822603658699938581184513

A = 0
B = 4

## base prime field
Fp = GF(p)

## E1 over base prime field, map any point on Efp into the q-torsion subgroup
Efp = EllipticCurve(Fp, [A, B])
r_E = Efp.order()
cofactor_E1 = r_E // q
# g_E1 = Efp(0)
# while g_E1 == Efp(0):
#     a = Efp.random_element()
#     g_E1 = cofactor * a
g_E1 = Efp(
    2262513090815062280530798313005799329941626325687549893214867945091568948276660786250917700289878433394123885724147,
    3165530325623507257754644679249908411459467330345960501615736676710739703656949057125324800107717061311272030899084
)
assert(q * g_E1 == Efp(0))
## trace map on E1 is trival, stays on E1
assert(trace_map(g_E1, 12, p, Efp) == 12 * g_E1)

print('\n ##################################### Curve G1: \n cofactor = {}, \n generator = {}, \n order = {} \n'.format(cofactor_E1, g_E1, r_E))

Definition of

denotes curve defined over field , namely , who is -twisted with . In BLS12-381 (sextic-twist), .

########## Fp2 = Fp[X] / X^2 - alpha
## alpha = -1
d = 2
alpha = Fp(-1)
X = Fp['X'].gen()
pol2 = X ** d - alpha
assert(pol2.is_irreducible() == True)
Fp2 = GF(p ** d, 'u', modulus = pol2)
u = Fp2.gen()

## Fp12 = Fp2[X] / X^6 - beta
d = 6
beta = u + 1
XX = Fp2['XX'].gen()
pol12 = XX ** d - beta
assert(pol2.is_irreducible() == True)
beta_t = beta 
Efp2_t = EllipticCurve(Fp2, [A, B * beta_t])
## find the proper twisted curve, who has a q-torsion subgroup which is isomorphism with Efpk's one
if Efp2_t.order() % q != 0:
    beta_t = beta ** 5
    Efp2_t = EllipticCurve(Fp2, [A, B * beta_t])

Definition of

denotes twisted curve defined over , namely .

## twist curve E' over Fp12
Fp12_t = Fp2.extension(pol12, 'x')
Efp12_t = Efp2_t.change_ring(Fp12_t)
print('\n Twist curve E defined over Fp12: {}\n'.format(Efp12_t))

Definition of

denotes curve defined over , namely .

## Fp12 = Fp[X] / X^12 - 2X^6 + 2
Fp12 = GF(p ** 12, 'w', modulus = X ** 12 - 2 * (X ** 6) + 2)
w = Fp12.gen()

## constant parameters of twist/untwist 
beta_t_x = w ** 2
beta_t_y = w ** 3

## make sure g_E2 is in the q-torsion subgroup on Efp2_t
r_E2_t = Efp2_t.order()
cofactor_E2_t = r_E2_t // q
# g_E2 = Efp2_t(0)
# while g_E2 == Efp2_t(0):
#     b = Efp2_t.random_element()
#     g_E2 = cofactor_E2_t * b
g_E2 = Efp2_t([
    [
        1265792444950586559339325656560420460408530841056393412024045461464508512562612331578200132635472221512040207420018,
        12405554917932443612178266677500354121343140278261928092817953758979290953103361135966895680930226449483176258412
    ],
    [
        3186142311182140170664472972219788815967440631281796388401764195993124196896119214281909067240924132200570679195848,
        1062539859838502367600126754068373748370820338894390252225574631210227991825937548921368149527995055326277175720251
    ],
])
assert(q * g_E2 == Efp2_t(0))
print('\n #################################### Curve G2: \n cofactor = {}, \n generator = {}, \n order = {} \n'.format(cofactor_E2_t, g_E2, r_E2_t))

## make sure g_E2 is in Fp12 first, uniform the field before untwist
Efp12 = Efp.change_ring(Fp12)
g_E12 = into_E12(g_E2, beta, Fp, w, beta_t_x, beta_t_y, Efp12)

## For the convenience of do Frobenius Map within Fp2, namely (x^p, y^p)
## traditionaly need 3 steps:
## 1. untwist (x, y) to (x', y'), (x', y') = (x / beta_t_x, y / beta_t_y)
## 2. do Frobenius Map within Fp12, (x'^p, y'^p) = (x^p / beta_t_x^p, y^p / beta_t_y^p)
## 3. twist back to (x, y), (x, y) = (x'^p * beta_t_x, y'^p * beta_t_y) = (x^p / beta_t_x^{p - 1}, y^p / beta_t_y^{p - 1})
## 
## Someone may wonder why wouldn't we do Frobenius Map within Fp2 directly? 
## Since one time of Frobenius Map within Fp2, phi(P), may skip out of G2, though P belongs to G2, 
## so we must do it within the FULL EXTENSION Fp12.
##
## Caching beta_t_x^{-(p - 1)} or beta_t_y^{-(p - 1)} would be much preferable
## 
twist_frob_x = into_Fp2(1 / (beta_t_x ** (p - 1)), Fp, u)
twist_frob_y = into_Fp2(1 / (beta_t_y ** (p - 1)), Fp, u)
print('\n Twist parameters: cubic_root(beta_t)^-1 = {}, sqrt(beta_t)^-1 = {} \n'.format(beta_t_x, beta_t_y))
print('\n Twist parameters for Frobenius Map within Fp2: \n cubic_root(beta_t)^-(p - 1) = {}, \n sqrt(beta_t)^-(p - 1) = {} \n'.format(
    twist_frob_x, twist_frob_y
))

print('\n ==================================== DEBUG ====================================\n ')
## make sure g_E12 is in the zero-trace subgroup of q-torsion
assert(q * g_E12 == Efp12(0))
assert(trace_map(g_E12, 12, p, Efp12) == Efp12(0))
print('\n #### UNTWIST: Point of E2 \n {} \n is mapped into E12 \n {} \n successfully! \n'.format(g_E2, g_E12))

## make sure it can be twisted back
x, y = twist(list(g_E12)[0], list(g_E12)[1], beta_t_x, beta_t_y)
x, y = (into_Fp2(x, Fp, u), into_Fp2(y, Fp, u))
assert(Efp2_t(x, y) == g_E2)
print('\n #### TWIST: Point of E12 \n {} \n is mapped into E2 \n {} \n successfully! \n'.format(g_E12, Efp2_t(x, y)))

Weil Pairing

Evaluation of Double-line Function

## evaluation of double line divisor function
## arithmetics on fields, not on multiplicative group
def double_line(line_point, eval_point, E, phi, reverse = False):
    ######################## arithemtic on finite field of line_point
    ## lambda = 3x^2 / 2y
    (x_L, y_L) = (list(line_point)[0], list(line_point)[1])
    (x_E, y_E) = (list(eval_point)[0], list(eval_point)[1])
    alpha = (3 * x_L^2) / (2 * y_L)
    x_2L = alpha * alpha - 2 * x_L
    y_2L = -y_L - alpha * (x_2L - x_L)
    
    ######################## arithmetic on mixed finite field
    ## x_E, y_E \in F2
    ## y_L, x_L, alpha, x_2L \in F1
    if reverse:
        ## evaluation of slop line l_2T
        e_1 = phi(y_E) - y_L - alpha * (phi(x_E) - x_L)
        ## evaluation of vertical line v_2T
        e_2 = phi(x_E) - x_2L
    else:
        ## evaluation of slop line l_2T
        e_1 = y_E - phi(y_L) - phi(alpha) * (x_E - phi(x_L))
        ## evaluation of vertical line v_2T
        e_2 = x_E - phi(x_2L)

    return E(x_2L, y_2L), e_1, e_2

Evaluation of Add-line Function

## evaluation of add line divisor function
## arithmetics on fields, not on multiplicative group
def add_line(line_left_point, line_right_point, eval_point, E, phi, reverse = False):
    ######################## arithemtic on finite field of line_point
    ## lambda = (y2 - y1) / (x2 - x1)
    (x_L, y_L) = (list(line_left_point)[0], list(line_left_point)[1])
    (x_R, y_R) = (list(line_right_point)[0], list(line_right_point)[1])
    (x_E, y_E) = (list(eval_point)[0], list(eval_point)[1])
    alpha = (y_L - y_R) / (x_L - x_R)
    x_LR = alpha * alpha - x_L - x_R
    y_LR = -y_L - alpha * (x_LR - x_L)
    
    ######################## arithmetic on mixed finite field
    ## x_E, y_E \in F2
    ## y_L, x_L, alpha, x_LR \in F1
    if reverse:
        ## evaluation of slop line l_{T + P}
        e_1 = phi(y_E) - y_L - alpha * (phi(x_E) - x_L)
        ## evaluation of vertical line v_{T + P}
        e_2 = phi(x_E) - x_LR
    else:
        ## evaluation of slop line l_{T + P}
        e_1 = y_E - phi(y_L) - phi(alpha) * (x_E - phi(x_L))
        ## evaluation of vertical line v_{T + P}
        e_2 = x_E - phi(x_LR)
    
    return E(x_LR, y_LR), e_1, e_2

Miller Loop

## Miller Loop of Weil Pairing
def MillerLoop(P, Q, G, q, phi, reverse = False):
    T = P
    f1 = 1
    f2 = 1
    e_bits = [int(i) for i in bin(q)[2:]]
    ## last bit cannot be evaluated, since the slope would be a vertical line
    for i in range(1, len(e_bits)):
        if (i == len(e_bits) - 1) and (e_bits[i] == 0):
            f1 = f1 * (list(Q)[0] - list(T)[0])
            T = 2 * T
            break
        T, e_1, e_2 = double_line(T, Q, G, phi, reverse)
        f1, f2 = (f1 * f1 * e_1, f2 * f2 * e_2)
        if (i == len(e_bits) - 1) and (e_bits[i] == 1):
            f1 = f1 * (list(Q)[0] - list(T)[0])
            T = T + P
            break
        if e_bits[i] == 1:
            T, e_1, e_2 = add_line(T, P, Q, G, phi, reverse)
            f1, f2 = (f1 * e_1, f2 * e_2)
    assert(T == G(0))
    
    return f1 / f2

Testation of Weil Pairing

## Weil Pairing Entry
def WeilPairing(P, Qx, G1, G12, q, phi):
    t0 = time.perf_counter()
    f_rP_Q = MillerLoop(P, Qx, G1, q, phi, False)
    t1 = time.perf_counter()
    f_rQ_P = MillerLoop(Qx, P, G12, q, phi, True)
    t2 = time.perf_counter()
    mu_r = ((-1) ** q) * (f_rP_Q / f_rQ_P)
    print('\n ##[Weil Pairing] Time consuming: t[f(P, Qx)] = {:.3f},  t[f(Qx, P)] = {:.3f}'.format(t1 - t0, t2 - t1))
    
    return mu_r


G1, G2_t, G12, G12_t = (Efp, Efp2_t, Efp12, Efp12_t)
C1, C2 = (cofactor_E1, cofactor_E2_t)

## make sure they are in G1 and G2_t repectively
P, Q = (C1 * G1.random_element(), C2 * G2_t.random_element())
assert(q * P == G1(0))
assert(q * Q == G2_t(0))

## untwist from E2_t to E12: Q -> Qx
Qx = into_E12(Q, beta, Fp, w, beta_t_x, beta_t_y, G12)
assert(q * Qx == G12(0))
assert(trace_map(Qx, 12, p, G12) == G12(0))

####################################### Weil Pairing Testation 
## P is defined over E(Fp), Qx is defined over E(Fpk)
## phi maps Fp to Fp12
phi = Hom(Fp, Fp12)(Fp.gen().minpoly().roots(Fp12)[0][0])
assert(P.curve() is not Qx.curve())
mu_r_weil = WeilPairing(P, Qx, G1, G12, q, phi)
## make sure pairing result is in q-torsion subgroup
assert(mu_r_weil ** q == Fp12(1))
#######################################

Output:

## Time consuming: t[f(P, Qx)] = 0.060,  t[f(Qx, P)] = 0.095

Obviousely time cost of is much more than that of , since is defined over Base Prime Field, , while is defined over Full Extension Field, .

  • Double-add on is more expensive than on
  • Function evaluation is absolutely defined over , so this part would be almost equal

Tate Pairing

Actually in Tate Pairing the vertical line evaluation can be ommited due to the Final Exponentiation. Let’s prove that!


Recall twist/ untwist operation: where , .


According to definition of embedding degree, is the minimal value satisfying , namely , so we must have .


Also since , assuming , then we must have .


Before untwisting , after untwisting . The vertical line funcion , the evaluation would be , where is untwisted value and . Finaly we have .


Optimized Evaluation of Double-line Function

## evaluation of double line divisor function
## arithmetics on fields, not on multiplicative group
def double_line(line_point, eval_point, E, phi, reverse = False):
    ######################## arithemtic on finite field of line_point
    ## lambda = 3x^2 / 2y
    (x_L, y_L) = (list(line_point)[0], list(line_point)[1])
    (x_E, y_E) = (list(eval_point)[0], list(eval_point)[1])
    alpha = (3 * x_L^2) / (2 * y_L)
    x_2L = alpha * alpha - 2 * x_L
    y_2L = -y_L - alpha * (x_2L - x_L)
    
    ######################## arithmetic on mixed finite field
    ## x_E, y_E \in F2
    ## y_L, x_L, alpha, x_2L \in F1
    if reverse:
        ## evaluation of slop line l_2T
        e_1 = phi(y_E) - y_L - alpha * (phi(x_E) - x_L)
        # ## evaluation of vertical line v_2T
        # e_2 = phi(x_E) - x_2L
    else:
        ## evaluation of slop line l_2T
        e_1 = y_E - phi(y_L) - phi(alpha) * (x_E - phi(x_L))
        # ## evaluation of vertical line v_2T
        # e_2 = x_E - phi(x_2L)

    return E(x_2L, y_2L), e_1

Optimized Evaluation of Add-line Function

## evaluation of add line divisor function
## arithmetics on fields, not on multiplicative group
def add_line(line_left_point, line_right_point, eval_point, E, phi, reverse = False):
    ######################## arithemtic on finite field of line_point
    ## lambda = (y2 - y1) / (x2 - x1)
    (x_L, y_L) = (list(line_left_point)[0], list(line_left_point)[1])
    (x_R, y_R) = (list(line_right_point)[0], list(line_right_point)[1])
    (x_E, y_E) = (list(eval_point)[0], list(eval_point)[1])
    alpha = (y_L - y_R) / (x_L - x_R)
    x_LR = alpha * alpha - x_L - x_R
    y_LR = -y_L - alpha * (x_LR - x_L)
    
    ######################## arithmetic on mixed finite field
    ## x_E, y_E \in F2
    ## y_L, x_L, alpha, x_LR \in F1
    if reverse:
        ## evaluation of slop line l_{T + P}
        e_1 = phi(y_E) - y_L - alpha * (phi(x_E) - x_L)
        # ## evaluation of vertical line v_{T + P}
        # e_2 = phi(x_E) - x_LR
    else:
        ## evaluation of slop line l_{T + P}
        e_1 = y_E - phi(y_L) - phi(alpha) * (x_E - phi(x_L))
        # ## evaluation of vertical line v_{T + P}
        # e_2 = x_E - phi(x_LR)
    
    return E(x_LR, y_LR), e_1

Optimized Miller Loop

## General Miller Loop Entry
def MillerLoop(P, Q, G, q, phi, reverse = False):
    T = P
    f1 = 1
    f2 = 1
    e_bits = [int(i) for i in bin(q)[2:]]
    
    print('Miller Loop Length: {}'.format(len(e_bits)))
    
    ## last bit cannot be evaluated, since the slope would be a vertical line
    for i in range(1, len(e_bits)):
        if (i == len(e_bits) - 1) and (e_bits[i] == 0):
            f1 = f1 * (list(Q)[0] - list(T)[0])
            T = 2 * T
            break
        T, e_1 = double_line(T, Q, G, phi, reverse)
        f1 = f1 * f1 * e_1
        if (i == len(e_bits) - 1) and (e_bits[i] == 1):
            f1 = f1 * (list(Q)[0] - list(T)[0])
            T = T + P
            break
        if e_bits[i] == 1:
            T, e_1 = add_line(T, P, Q, G, phi, reverse)
            f1 = f1 * e_1
    assert(T == G(0))
    
    return f1

Easy-part of Final Exponentiation

For illustration convenience, we does not use Frobenius Map trick here, just directly use time-consuming trivial power. Actually it’s almost free cost after using Frobenius Map.


## trival implementation of easy part, Frobenius not used here actually
## exp = (p^6 - 1) * (p^2 + 1)
## 2 * Frobenius + 2 * Mul + 1 * Inv
def easy_part(f):
    ff = f
    ## 1 * Frobenius
    t0 = f ** (p ** 6)
    ## 1 * Inv
    t1 = 1 / f
    ## 1 * Mul
    f = t0 * t1
    ## 1 * Frobenius
    t0 = f ** (p ** 2)
    ## 1 * Mul
    f = t0 * f
    
    actual = ff ** (((p ** 6) - 1) * ((p ** 2) + 1))
    assert(actual == f)
    
    return f

Hard-part of Final Exponentiation

Same as above, we does not use Frobenius Map here.


As we know, the hard part is arithmetics on Cyclotomic Subgroup, namely . According to On the Computation of the Optimal Ate Pairing at the 192-bit Security Level, the power of hard part is not , but three times of that: where: In conclusion :


## reference from Algorithm 1 of "On the Computation of the Optimal Ate Pairing at the 192-bit Security Level"
## trival implementation of hard part, Frobenius not used here actually
## exp = (p^4 - p^2 + 1) / r
def hard_part(f, u, p, q):
    ## 1 * Sqr + 1 * Inv
    t0 = 1 / (f * f)
    ## 1 * Pow
    t5 = f ** u
    ## 1 * Sqr
    t1 = t5 * t5
    ## 1 * Mul
    t3 = t0 * t5
    
    ## 1 * Pow
    t0 = t3 ** u
    
    ## 1 * Pow
    t2 = t0 ** u
    
    ## 1 * Pow
    t4 = t2 ** u
    
    ## 1 * Mul
    t4 = t1 * t4
    ## 1 * Pow
    t1 = t4 ** u
    ## 1 * Inv
    t3 = 1 / t3
    ## 1 * Mul
    t1 = t3 * t1
    ## 1 * Mul
    t1 = t1 * f # f^\lambda_0
    
    # 1 * Inv
    t3 = 1 / f
    ## 1 * Mul
    t0 = t0 * f
    ## 1 * Frobenius
    t0 = t0 ** (p ** 3) # f^\lambda_3
    
    ## 1 * Mul
    t4 = t3 * t4
    ## 1 * Frobenius
    t4 = t4 ** p # f^\lambda_1
    
    ## 1 * Mul
    t5 = t2 * t5
    ## 1 * Frobenius
    t5 = t5 ** (p ** 2) # f^\lambda_2
    
    ## 3 * Mul
    t5 = t5 * t0
    t5 = t5 * t4
    t5 = t5 * t1
    
    ## third power of actual pairing result
    actual = f ** (((p ** 4) - (p ** 2) + 1) // q)
    assert(t5 == actual ** 3)
    assert(t5 ** q == 1)
    
    return t5

Final Exponentiation

## Final Exponentiation Entry
def FinalExponentiation(f, p, k, q, u, trivial = True):
    if trivial:
        mu_r = f ** (((p ** k) - 1) // q)
    else:
        t0 = time.perf_counter()
        f = easy_part(f)
        t1 = time.perf_counter()
        mu_r = hard_part(f, u, p, q)
        t2 = time.perf_counter()
        print('\n     ##[Hard Part of Tate Pairing] Time consuming: t[easy] = {:.3f},  t[hard] = {:.3f}'.format(t1 - t0, t2 - t1))
    return mu_r

Testation of Tate Pairing

## Tate Pairing Entry
def TatePairing(P, Qx, G1, q, phi, p, k, u, trivial = True):
    t0 = time.perf_counter()
    f = MillerLoop(P, Qx, G1, q, phi, False)
    t1 = time.perf_counter()
    mu_r = FinalExponentiation(f, p, k, q, u, trivial)
    t2 = time.perf_counter()
    print('\n ##[Tate Pairing] Time consuming: t[f(P, Qx)] = {:.3f},  t[exp] = {:.3f}'.format(t1 - t0, t2 - t1))
    
    return mu_r


G1, G2_t, G12, G12_t = (Efp, Efp2_t, Efp12, Efp12_t)
C1, C2 = (cofactor_E1, cofactor_E2_t)

## make sure they are in G1 and G2_t repectively
P, Q = (C1 * G1.random_element(), C2 * G2_t.random_element())
assert(q * P == G1(0))
assert(q * Q == G2_t(0))

## untwist from E2_t to E12: Q -> Qx
Qx = into_E12(Q, beta, Fp, w, beta_t_x, beta_t_y, G12)
assert(q * Qx == G12(0))
assert(trace_map(Qx, 12, p, G12) == G12(0))

####################################### Trivial Tate Pairing Testation
mu_r_tate_1 = TatePairing(P, Qx, G1, q, phi, p, k, True)
assert(mu_r_tate ** q == Fp12(1))
#######################################

####################################### parameter for p(x), q(x), and t(x)
x = -15132376222941642752
t = x + 1
## p = ((x - 1)^2 * (x^4 - x^2 + 1)) / 3 + x
assert((pow((x - 1), 2) * (pow(x, 4) - pow(x, 2) + 1)) // 3 + x == p)
## q = x^4 - x^2 + 1
assert(pow(x, 4) - pow(x, 2) + 1 == q)
## t = x + 1
assert(abs(p + 1 - t) == Efp.order())

####################################### Nontrivial Tate Pairing Testation
mu_r_tate_2 = TatePairing(P, Qx, G1, q, phi, p, k, x, False)
assert(mu_r_tate ** q == Fp12(1))

## The hard part is 3rd power of pairing
assert(mu_r_tate_1 ** 3 == mu_r_tate_2)

The running output:

Miller Loop Length: 255

 ##[Tate Pairing] Time consuming: t[f(P, Qx)] = 0.039,  t[exp] = 0.079
Miller Loop Length: 255

     ##[Hard Part of Tate Pairing] Time consuming: t[easy] = 0.114,  t[hard] = 0.082

 ##[Tate Pairing] Time consuming: t[f(P, Qx)] = 0.051,  t[exp] = 0.195

After applying Frobenius Map, the time cost of final exponentiation would greately reduced.


Ate Pairing

Miller Loop

In Ate Pairing, since , actually is a vertical line, the last step of Miller Loop cannot evaluated directly, so we used a specific manner to deal with it.

But in Ate Pairing, which is far away from , no need to worry , so we will strip that specific manner used in Tate Pairing.


## General Miller Loop Entry
def MillerLoop(P, Q, G, q, phi, reverse = False):
    ## if power q is negative or not
    P = P if q > 0 else -P
    q = q if q > 0 else -q
    
    T = P
    f1 = 1
    e_bits = [int(i) for i in bin(q)[2:]]

    print('Miller Loop Length: {}'.format(len(e_bits)))
    
    for i in range(1, len(e_bits)):
        ##### strip this specific manner used in Tate Pairing
        # if (i == len(e_bits) - 1) and (e_bits[i] == 0):
        #     f1 = f1 * (list(Q)[0] - list(T)[0])
        #     T = 2 * T
        #     break
        T, e_1 = double_line(T, Q, G, phi, reverse)
        f1 = f1 * f1 * e_1
        ##### strip this specific manner used in Tate Pairing
        # if (i == len(e_bits) - 1) and (e_bits[i] == 1):
        #     f1 = f1 * (list(Q)[0] - list(T)[0])
        #     T = T + P
        #     break
        if e_bits[i] == 1:
            T, e_1 = add_line(T, P, Q, G, phi, reverse)
            f1 = f1 * e_1
    
    return f1

Testation of Ate Pairing

Notice that in curve BLS12-381, the parameter for polynomials is a negative one:

where .


Therefore we must deal with it properly in Miller Loop before looping.


## Ate Pairing Entry
def AtePairing(P, Qx, G1, q, phi, p, k, u, T, trivial = True):
    t0 = time.perf_counter()
    f = MillerLoop(P, Qx, G1, T, phi, False)
    t1 = time.perf_counter()
    mu_r = FinalExponentiation(f, p, k, q, u, trivial)
    t2 = time.perf_counter()
    print('\n ##[Ate Pairing] Time consuming: t[f(P, Qx)] = {:.3f},  t[exp] = {:.3f}'.format(t1 - t0, t2 - t1))
    
    return mu_r

G1, G2_t, G12, G12_t = (Efp, Efp2_t, Efp12, Efp12_t)
C1, C2 = (cofactor_E1, cofactor_E2_t)

## make sure they are in G1 and G2_t repectively
P, Q = (C1 * G1.random_element(), C2 * G2_t.random_element())
assert(q * P == G1(0))
assert(q * Q == G2_t(0))

## untwist from E2_t to E12: Q -> Qx
Qx = into_E12(Q, beta, Fp, w, beta_t_x, beta_t_y, G12)
assert(q * Qx == G12(0))
assert(trace_map(Qx, 12, p, G12) == G12(0))

## parameter for p(x), q(x), and t(x)
x = -15132376222941642752
t = x + 1
## p = ((x - 1)^2 * (x^4 - x^2 + 1)) / 3 + x
assert((pow((x - 1), 2) * (pow(x, 4) - pow(x, 2) + 1)) // 3 + x == p)
## q = x^4 - x^2 + 1
assert(pow(x, 4) - pow(x, 2) + 1 == q)
## t = x + 1
assert(abs(p + 1 - t) == Efp.order())

## p \equiv T \mod q
T = t - 1
####################################### Ate Pairing Testation
mu_r_ate = AtePairing(P, Qx, G1, q, phi, p, k, x, T, False)
assert(mu_r_ate ** q == Fp12(1))

The running output:

Miller Loop Length: 64

     ##[Hard Part of Ate Pairing] Time consuming: t[easy] = 0.105,  t[hard] = 0.081

 ##[Ate Pairing] Time consuming: t[f(P, Qx)] = 0.007,  t[exp] = 0.186

Obviousely time cost of Miller Loop is greatly reduced, since is far more less than (64 vs 255).



Rust Implementation

Much testation work need to be done, code to be updated…


References

[1] A note on twists for pairing friendly curves

[2] Pairing-Friendly Elliptic Curves of Prime Order

[3] Pairing for Beginners

[4] Guide to Pairing-based Cryptography

[5] Faster pairing computations on curves with high-degree twists

[6] Optimal Pairings

[7] On the Computation of the Optimal Ate Pairing at the 192-bit Security Level

[8] Efficient Final Exponentiation via Cyclotomic Structure for Pairings over Families of Elliptic Curves

[9] Faster Squaring in the Cyclotomic Subgroup of Sixth Degree Extensions

[10] A Guide to Plane Algebraic Curves



Touch

  • twitter: @pingzhouyuan
  • email: joepayne@163.com
  • wechat: paynejoe

深入分析SuperNova及其ROM实现

针对于为有限状态机上程序执行的正确性的问题,Nova是集大成者,作者Setty提出了一种基于Folding的递归证明系统。但是Nova要求在迭代中使用相同的业务电路,可以理解为仅支持单个指令。SuperNova则对其进行了拓展,在每步迭代中可以运行不同的指令(这一问题定义为NIVC, non-uniform IVC),因此可以把Nova看作是只支持一个指令的NIVC解决方案。之前采用全局电路的方法其开销与所有指令构成的电路规模有关,Supernova最大的创新则是其证明开销只与当前步执行的指令有关,并且产生的overhead是常数。

本文首先介绍NIVC问题的定义,以及Supernova的基本思路,最后针对于文中没有给出具体说明的电路选择器,详细介绍了ROM模型的实现思路。

NIVC定义

image NIVC是对IVC的泛化,在每步增量计算中,Prover可以证明满足一些列relation中的一种relation,所以他可以支持每步使用不同的电路。 首先定义NIVC要证明的电路形式,假设存在个多项式时间可计算的函数 (可以把他们看作是执行一些列不同指令的电路),他们满足:,其中为选择器,其根据当前witness 和公共输出选择其中第个函数,即输出

Prover则是要生成proof,其可以证明对于n步迭代中产生的一系列均满足。可将其形式化表述为, 其中P为Prover,pk为prover key,为proof。与IVC类似,NIVC要求Prover在任意步的证明开销与之前调用的指令无关,否则会导致电路规模无限增大;更进一步要求Prover的开销只和当前步运行的电路规模有关,否则就蜕化成了用一个包含所有函数电路构成的IVC。

Supernova证明系统

对于上述证明问题,Supernova采用了类似与Nova的folding scheme,总体来说其也是先构建一个Augumented函数,通过证明存在满足的witness,来证明业务电路F以及每次迭代更新proof的正确性。每次需要把relaxed R1CS实例(U)和r1cs实例(u)进行fold,而且folding scheme要求两种实例的结构是一样的,但是NIVC中函数有多个,不能简单地fold。因此,SuperNova中在第会输入一系列的,其中代表从0到步被正确执行,这样只需要验证所有的是否满足约束就可以验证所有函数从0到步被正确执行;此外还会输入一个实例u,用来证明第步也被正确执行。 image 对于Augumented函数,相对于Nova的不同点在于,在第步只折叠第个实例,为了确保执行的是,需要将也作为公共输入放入中来进行检验。

Supernova证明系统的核心构造为: image image

说明:实际上Relaxed R1CS(Az◦Bz = uCz + E)和R1CS(Az◦Bz=Cz)中的A,B,C是一致(这些值由业务电路+Folding相关的约束生成),只是具体的z不一样。

需要注意的是上述证明系统并没有明确约束第步具体选择哪个电路,因此如果需要确定性生成相应的选择器,还需增加选择器电路。 然而论文中没有给出选择器的具体实现,下面参考PSE一位成员给出的一种电路序列固定的Supernova实现,进一步给出具体实现细节。

ROM machine based Supernova

Rom( read-only memory)模型将所有的电路看作电路序列,该序列共有个电路,其中不同的电路共有个,并将所有的电路直接写死在Supernova的公共输入中,每迭代一步, 则放入中。在第步时,读取,并选取对应的电路。 比如共有2个不同的电路,ROM构成的电路序列为

那么在Supernova论文给出的证明系统之上,还需保证:

  1. 步fold的是第个电路;
  2. 在第步选择是第电路(注意这点在supernova中没有要求)

对于第1个问题,主要通过构造一个条件选择电路,具体电路如下:

    // select target when index match last_augmented_circuit_index, other left as empty

    let U: Result<Vec<AllocatedRelaxedR1CSInstance<G>>, SynthesisError> = U
      .iter()
      .enumerate()
      .map(|(i, U)| {
        let i_alloc = alloc_const(
          cs.namespace(|| format!("U_i i{:?} allocated", i)),
          scalar_as_base::<G>(G::Scalar::from(i as u64)),
        )?;

        let equal_bit = Boolean::from(alloc_num_equals(
          cs.namespace(|| format!("check U {:?} equal bit", i)),
          &i_alloc,
          last_augmented_circuit_index,
        )?);

        conditionally_select_alloc_relaxed_r1cs(
          cs.namespace(|| format!("select on index namespace {:?}", i)),
          U,
          &empty_U,
          &equal_bit,
        )
      })
      .collect();

对于第2个电路,核心思路是构造 来实现其约束, 其中 value[i]=rom[i],i=pci0,i=pc[i]

具体代码如下:

fn constraint_augmented_circuit_index<F: PrimeField, CS: ConstraintSystem<F>>(
    mut cs: CS,
    pc_counter: &AllocatedNum<F>,
    rom: &[AllocatedNum<F>],
    circuit_index: &AllocatedNum<F>,
  ) -> Result<(), SynthesisError> {

    // select target when index match or empty
    let zero = alloc_zero(cs.namespace(|| "zero"))?;
    let rom_values = rom
      .iter()
      .enumerate()
      .map(|(i, rom_value)| {
        let index_alloc = alloc_const(
          cs.namespace(|| format!("rom_values {} index ", i)),
          F::from(i as u64),
        )?;

        let equal_bit = Boolean::from(alloc_num_equals(
          cs.namespace(|| format!("rom_values {} equal bit", i)),
          &index_alloc,
          pc_counter,
        )?);

        conditionally_select(
          cs.namespace(|| format!("rom_values {} conditionally_select ", i)),
          rom_value,
          &zero,
          &equal_bit,
        )
      })

      .collect::<Result<Vec<AllocatedNum<F>>, SynthesisError>>()?;

    let sum_lc = rom_values
      .iter()
      .fold(LinearCombination::<F>::zero(), |acc_lc, row_value| {
        acc_lc + row_value.get_variable()
      });

    println!("self.circuit index ==============> : {:?}", circuit_index.get_value());
    cs.enforce(
      || "sum_lc == circuit_index",
      |lc| lc + circuit_index.get_variable() - &sum_lc,
      |lc| lc + CS::one(),
      |lc| lc,

    );
    Ok(())

  }

致谢

非常感谢 SECBIT Labs 的 @郭宇老师对SuperNova研究方向的指导。

参考文献

ZKP Public Goods

[[# Arithmetic circuits

2.1 Encoding the trace as arithmatic constraints

R1CS

  • **Flattening:将电路的执行转换成计算轨迹,**即将复合函数以乘法为基本单元拆解成一组有序的简单函数,其中
    • , 为输出变量,为左输入变量,为右输入变量
    • 这里的有序是指按电路执行的顺序
    • 这里会引入中间变量
      • 除了根节点处的门之外,其它的门的输出引脚添加对应的中间变量
      • 除了叶子节点处的门之外,其他的门的输入引脚添加中间变量,该中间变量来自于另一个门的输出
      • 举例说明:若门A的输出引脚接入到门B的输入引脚,则为门A的输出引脚和门B的输入引脚添加同一个中间变量
  • 重组中的数据:将其变成一阶约束系统R1CS:(注: 为Hadamard product,按位乘法):
    • ,即由表示1的冗余变量,函数输出,输入变量,中间变量构成的集合对应的向量。
    • 的输出变量基于的选择向量构成的矩阵
    • 的左输入变量基于的选择向量构成的矩阵
    • 的右输入变量基于的选择向量构成的矩阵
  • 注:矩阵的行数等于乘法门的数量,矩阵的列数等于中元素的数量,即变量的数量

Plonkish Arithmetization

  • **Flattening:**即将复合函数拆解成一组离散的门,其中
    • 为输出变量,为左输入变量,为右输入变量,为常量,为输出选择器,为左输入变量选择器,为右输入变量选择器,为乘积选择器,为常数选择器
    • **注:**矩阵的行数等于所有门的数量,即约束的数量,n。
    • 这里的有序是指计算的顺序
    • 这里会引入中间变量
      • 除了根节点处的门之外,其它的门的输出引脚添加对应的中间变量
      • 除了叶子节点处的门之外,其他的门的输入引脚添加中间变量,该中间变量来自于另一个门的输出
      • 举例说明:若门A的输出引脚接入到门B的输入引脚,则为门A的输出引脚和门B的输入引脚添加同一个中间变量
  • **重组中的数据:**将其变成:(注: 为Hadamard product,按位乘法)
    • ,即选择器矩阵
    • ,即变量矩阵
    • 轮换置换后得到的位置集合,来自于Wiring
  • Wiring(Copy Constraints)
    • 分析:Wiring即将离散的门连接起来,即某一个门的输出引脚要接入另一个门的输入引脚约束变量矩阵中某几个位置的元素是相等的这一个元素出现在矩阵的多个位置处

    • Wiring实现思路:

      • 矩阵中的每一个位置从1到3n进行唯一编号,则所有的编号构成一个位置集合,将位置集合对应的元素取出构成一个multiset
      • 把每个元素出现在中的位置编号取出放在一个集合中,即一个元素对应一个位置集合。将位置集合对应的元素取出构成一个multiset 。所有元素的的并集即为的并集为
      • ,要使得 中元素全部相等

      与其进行轮换置换后得到的集合在Multiset的意义上是等价的

      在Multiset的意义上是等价的

      为所有的并集,为所有的并集,为所有的并集, 在Multiset的意义上是等价的

      ,取随机数,有

      • 至此,问题转化成如何证明连乘等式 ,即证明一个n步递归,

        • 初始值:
        • 递归定义:
        • 终止条件:

        则有

        所有 构成向量

        • 至此,Wiring转化成三个约束
          • 即约束向量的指定位的值为k,即约束的第1位()的值为1,第n+1位()的值为

            为n维向量空间的标准基的第i个基向量,向量的第位为等价于:

2.2 Constraints Merge

R1CS to QAP

  • 带有Hadamard product运算的n维向量的群,和带有乘法运算的在上的最高项次数不大于n-1的单变量多项式的群,映射:,令,有,是群同态

  • , 至此,完成了从R1CS到QAP到转换

Plonkish Arithmetization to QAP

Plonkish Arithmetization包含两部分约束:

第一部分约束每个门是正确计算的,即所谓算术约束;第二部分约束门与门之间正确连接,即所谓复制约束。

首先来转换算术约束:

  • 带有加法的n维向量的群,和带有加法的在上的最高项次数不大于n-1的单变量多项式的群,映射:,令,有$h(\vec m)= \langle \vec m,\vec L(X) \rangleLemma5:由Lemma1,Lemma4,Lemma5,有\vec{q_O}\circ \vec{w}=\vec{q_L}\circ\vec{u}+\vec{q_R}\circ\vec{v}+\vec{q_M}\circ(\vec{u}\circ\vec{v})+\vec{q_C}\circ\vec{c} \\ \iff \langle \vec q_O,\vec L(X) \rangle \cdot \langle \vec w,\vec L(X) \rangle=\langle \vec q_L,\vec L(X) \rangle \cdot \langle \vec u,\vec L(X) \rangle+\langle \vec q_R,\vec L(X) \rangle \cdot \langle \vec v,\vec L(X) \rangle+\langle \vec q_m,\vec L(X) \rangle \cdot (\langle \vec u,\vec L(X) \rangle\cdot\langle \vec v,\vec L(X) \rangle )+\langle \vec q_C,\vec L(X) \rangle a(X) =\langle \vec a,\vec L(X) \rangleq_O(X)w(X)=q_L(X)u(X)+q_R(X)v(X)+q_M(X)u(X)v(X)+q_C(X)Lemma1\vec e_i \circ \vec r=k\times \vec e_i \\ \iff L_i(X)r(X)=k\times L_i(X) \\ \iff L_i(X)(r(X)-k)=0r_0=1 \iff L_0(X)(r(X)-1)=0 \\ r_n=c \iff L_n(X)(r(X)-c)=0 L_i(X)HLagrange BasisLemma1\vec r_{i}=\vec r_{i-1} \circ \vec b_{i-1} \\ \iff \langle \vec r,\vec L(\omega \cdot X) \rangle =\langle \vec r,\vec L(X) \rangle \cdot \langle \vec b,\vec L(X) \rangle

  • 至此,复制约束转换成了三个多项式约束。

2.3 A function commitment scheme

在2.3中得到了一系列多项式之间的约束,本节我们来看如何实现多项式约束,

令:

\mathcal F:=function\ family\mathbb F_p:= 有限域

对于\mathcal Fsetup(\lambda) \to ppcommit(pp,f,r) \to com_f基于随机数r对f\in \mathcal F的承诺eval(prover \ P,verifier\ V)com_fx\in X,y\in Y:证明f(x)=y,即所谓的将f在点(x,y)处打开P(pp,f,x,y,r) \to 简短证明\piV(pp,com_f,x,y,\pi)\to 接受/拒绝

三类典型的Function Family Commiments

  • Polynominal commitments:次数不大于d的单变量多项式承诺 f(X)\in \mathbb F_p^{(\leq d)} [X]f(X_1,...,X_k)\in \mathbb F_p^{(\leq 1)}[X_1,...,X_K]f_{\vec v}(\vec u)= \left \langle \vec u, \vec v \right \rangle=\sum _{i=1}^nu_iv_i

这三者从上到下,越来越general

PCS: Polynominal Commitment Scheme

适用于次数不大于d的单变量多项式 f(X)\in \mathbb F_p^{(\leq d)} [X]

Some usual PSC

  • Bulletproofs:基于椭圆曲线,verifier的算法复杂度与d成线性相关
  • KZG‘10,Dory’20:基于双线性椭圆曲线
  • Dark’20:基于阶未知的群
  • FRI:基于hash Function

KZG poly-commit scheme

  • 预备知识:阶为p的群\ \mathbb G:=\{1,G,2\cdot G,3\cdot G,...,(p-1)\cdot G\} ,其中,G为生成元setup(\lambda) \to pp\alpha\in \mathbb F_ppp=(H_0=1,H_1=\alpha \cdot G,H_2=\alpha^2 \cdot G,...,H_d=\alpha^d \cdot G)\in \mathbb G^{d+1}\alpha\alpha\alphacommit(pp,f,r) \to com_fcom_f:=f(\alpha)\cdot G \in \mathbb Gf(X)=f_0+f_1X+...+f_dX^d\implies com_f=f_0\cdot 1+f_1\cdot\alpha G+f_2\cdot\alpha^2 G+ ...+f_d\cdot \alpha^dG\iff com_f=f_0\cdot H——1+f_1\cdot H1+...+f_d\cdot H_deval(prover \ P,verifier\ V)f(u)=vf(u)=v\iff u是 \hat f =f-v的根\iff (X-u)整除\hat f\iff \exists q\in \mathbb{F}_p[X]\ \ s.t.\ \ q(X)\cdot(X-u)=f(X)-vProver(pp,f,u,v)商多项式q(X) 及其承诺com_qVerifier(pp,com_f,u,v)(\alpha-u)\cdot com_q=com_f-v\cdot G\alpha\alpha(\alpha-u)\cdot com_q=com_f-v\cdot Gcom_q和com_f\alpha(\alpha-u)setup(\lambda) \to pp\alpha\in \mathbb F_ppp=(H_0=1,H_1=\alpha \cdot G,H_2=\alpha^2 \cdot G,...,H_d=\alpha^d \cdot G)\in \mathbb G^{d+1} + (T_0=1,T_1=\alpha \cdot G_2) \in \mathbb G_2^1\alpha\alpha\alphaVerifier(pp,com_f,u,v)e\in \mathbb G \times \mathbb G_2 \to \mathbb G_X$ - 至此,将原来需要验证的,转换成了 在上验证

2.4 Polynominal IOP

Useful Lemma

  • Lemma1: Schwartz zipple定理

  • Lemma2: 单位根和乘法子群**:**

    为k次单位根,即

    乘法子群

    由于单位根的对称性,有

  • Lemma3: 中的元素均为的根,即

    存在商多项式

Poly-IOP可以高效完成的任务

  • Task1 zero-test:证明在H上等于0,即证明H中的元素均为的根
  • Task2 sum-check:证明,即证明在H上全部取值的和等于b
  • Task3 prod-check:证明,即证明在H上全部取值的和等于c

Zero Test on H

  1. Prover 向 Verifier Commit
  2. Verifier 向Prover 发送随机数r
  3. Verifier 检查

参考资料

https://github.com/sec-bit/learning-zkp/blob/develop/plonk-intro-cn/plonk-arithmetization.md

https://www.youtube.com/watch?v=J4pVTamUBvU&list=PLj80z0cJm8QErn3akRcqvxUsyXWC81OGq&index=2

https://github.com/sec-bit/learning-zkp/blob/develop/plonk-intro-cn/plonk-polycom.md ](https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md)https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md](https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md)https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md

本文假设您对椭圆曲线运算及哈希函数等有着基础的了解

简洁的 Schnorr 协议

Alice 拥有一个秘密数字,a,我们可以把这个数字想象成「私钥」,然后把它「映射」到椭圆曲线群上的一个点 a*G,简写为 aG。这个点我们把它当做「公钥」。

  • sk = a ( secret key = a )
  • PK = aG

a secret key  that corresponds to a public key .

请注意「映射」这个词,给任意一个有限域上的整数 r,我们就可以在循环群中找到一个对应的点  rG,或者用一个标量乘法来表示 r*G。但是反过来计算是很「困难」的,这是一个「密码学难题」—— 被称为离散对数难题。

取模之后 , 就很难知道原来的指数是多少了。 事实上,如果模取得相当大,从运算结果倒推指数运算就不可行了;现代密码学很大程度上就是基于这个问题的“困难”

也就是说,如果任意给一个椭圆曲线循环群上的点 R,那么到底是有限域中的哪一个整数对应 R,这个计算是很难的,如果有限域足够大,比如说 256bit 这么大,我们姑且可以认为这个反向计算是不可能做到的

Schnorr 协议充分利用了有限域和循环群之间单向映射,实现了最简单的零知识证明安全协议:Alice 向 Bob 证明她拥有 PK 对应的私钥 sk

  1. 第一步:为了保证零知识,Alice 需要先产生一个随机数 r,这个随机数的用途是用来保护私钥 无法被 Bob 抽取出来。这个随机数也需要映射到椭圆曲线群上即 rG。 ( 映射之后 , Bob 就不可能通过 rG 推算出 r )
  2. 第二步:Bob 要提供一个随机数进行挑战,我们把它称为  c
  3. 第三步:Alice 根据挑战数 c 计算  z = r + c * a (即sk),把 z 发给 Bob,Bob 在自己这边通过下式进行检验:
#![allow(unused)]
fn main() {
z*G ?= R + c*PK 
    ?= rG + c*(aG)
}

大家可以看到 Bob 在第三步「同态地」检验 z 的计算过程。如果这个式子成立,那么就能证明 Alice 确实有私钥 a。可是,这是为什么呢?

z 的计算和验证过程很有趣,有几个关键技巧:

  1. 首先 Bob 必须给出一个「随机」挑战数 ,然后 Bob 在椭圆曲线上同态地检查 z 。如果我们把挑战数    看成是一个未知数,那么 r+a*c=z 可以看成是一个一元一次方程,其中 r 与 a 是方程系数。请注意在 c 未知的前提下,如果 r + a*x = r' + a'*x 要成立,那么根据 Schwatz-Zippel 定理,极大概率上 r=r'a=a' 都成立。也就是说, Alice 在 c 未知的前提下,想找到另一对不同的 r',a' 来计算 z 骗过 Bob 是几乎不可能的。这个随机挑战数 c 实现了r 和 a 的限制。虽然 Bob 随机选了一个数,但是由于 Alice 事先不知道,所以 Alice 不得不使用私钥 a 来计算 z。这里的关键: c 必须是个随机数。
  2. Bob 验证是在椭圆曲线群上完成。Bob 不知道 r ,但是他知道 r  映射到曲线上的点 R ;Bob 也不知道 a,但是他知道 a 映射到曲线群上的点 PK,即 a*G。通过同态映射与Schwatz-Zippel 定理,Bob 可以校验 z 的计算过程是否正确,从而知道 Alice 确实是通过 r 和 a 计算得出的 z,但是又不暴露 r 与 a 的值。
  3. 还有,在协议第一步中产生的随机数 r 保证了 a 的保密性。因为任何一个秘密当和一个符合「一致性分布」的随机数相加之后的和仍然符合「一致性分布」。

看懂了这个图就看懂了 !!!!!

是 Sigma 零知识证明的一个特例

Schnorr 的非交互式版本

Schnorr 协议的非交互式版本可以避免 Prover 与 Verifier 的交互,但这要求 Prover 使用哈希函数,这样他就无法预测哈希函数的输出,非交互式版本的验证器实现非常简单,因为它不需要随机数生成器

(Making the protocol non-interactive)

首先定义: 即私钥 ; 是 Public key 即公钥 ;

  1. Prover 生成一个随机数 并创建一个承诺 , Prover 对 进行哈希处理以获得挑战值 ,
  2. Prover 创建对挑战的响应 , 然后将元组 (comm, s) 发送给验证者。

Verifier 自己计算 , 然后验证 :

如果 Verifier 自己验证这个等式相等, 则 Prover 就通过 这种方式隐藏了私钥 , 同时又能让对方确信自己真的有这个私钥 .

  1. The prover generates a random number r and creates a commitment com = gʳ. The prover hashes  gcom and y to get challenge cc = Hash(g, y, t).
  2. The prover creates a response to the challenge as s = r + c*x. The prover sends tuple (t, s) to the verifier.

The verifier now generates the same challenge c as Hash(g, y, t) and again checks if  equals yᶜ.tPython code demonstrating this protocol.

Schnorr 的问题

对不同的消息, 如果不幸选了相同的随机数 私钥就会泄露

如果 Alice 在两次交互过程中使用了同一个 K,那么 Bob 可以通过发送两个不同的 c 和 c' 来得到 s 和 s',然后通过下面的公式算出私钥 a

s  = (c +a*e)/k , 
s' = (c'+a*e)/k , 两式相减, 求出 k 

k = (c - c')/(s - s')
a = (k * s - c)/e

ECDSA

Bitcoin 和 ETH 都支持 ECDSA signature.

why need ECDSA?

除了显而易见的“我需要对一份文件/合同进行签名”,还有一个非常流行的应用场景:让我们以一个不想自己的数据被用户修改或者破坏的应用程序为例,比如一个只允许你载入官方地图和不可修改的模块的游戏,或者一部只允许你安装官方应用程序的手机或其它设备。

在这些案例当中,相关文件(应用程序、游戏地图、数据等)会用 ECDSA 进行签名,公钥会随应用程序/游戏/设备一起捆绑并用来验证签名来确保数据没有被修改,而私钥在本地一个私密的地方进行保存。由于你可以用公钥对签名进行验证,但是不能用它创建或者伪造新的签名,你可以无所顾忌地将公钥随应用程序/游戏/设备一起分发。

这与AES相比,区别是显而易见的。AES加密系统允许你对数据进行加密,但是你需要用密钥来解密,这就要求你将密钥与应用程序一起捆绑,破坏了对数据进行保护防止数据被用户修改的目的。

一个很好的例子就是PS3的控制台,它被大量的破解,所有的文件可以解密,所有的密钥可以从解密的文件当中抽取,但是为了能够在最新的固件上面运行程序,你还需要破解一个ECDSA的数字签名。

当你想要对一个文件进行签名的时候,你会用这个私钥 / 随机数 / 文件的哈希组成一个魔法数学方程,这将给出你的签名。签名本身将被分成两部分,称为 RS

  • 选择随机数 , 计算承诺 :
  • 挑战 : 取 的横坐标为 (先 mod , 再 mod )
  • 响应 :

为了验证签名的正确性,你只需要公钥(用私钥在曲线上面产生的点)并将公钥和签名的一部分 S 一起代入另外一个方程,如果这个签名是由私钥正确签名过的数字签名,那么它将给出签名的另外一部分 R

简单来说,一个数字签名包含两个数字,RS,然后你使用一个私钥来产生 RS ,如果将公钥和 S 代入被选定的魔法数学方程给出 , 且 的话,这个签名就是有效的。仅仅知道公钥是无法知道私钥或者创建出数字签名。

Algorithm

初始化: 椭圆曲线生成元为 ,标量域为 ,基域为

基域 理解为椭圆曲线点的横纵坐标的取值范围 标量域 即做倍点运算的标量的取值范围, 比如 里的 , 其不会超过椭圆曲线的阶

密钥生成: 私钥 和公钥

签名: 输入任意消息 , 计算

  • 选择随机数 , 计算承诺 :
  • 挑战 : 取 的横坐标为 (先 mod , 再 mod )
  • 响应 : ( k 增加了 ECDSA 的难度)

则签名为

的乘法逆元

我们是如何对一个文件或者一个信息进行签名的呢?

  1. 你需要知道签名本身是 40 字节,由各20字节的两个值来进行表示,第一个值叫作 ,第二个叫作
  2. 值对 放到一起就是你的 ECDSA 签名

验证 :

验证它,也非常的简单,你只需要 [公钥] 和导出这个公钥的曲线参数就可以了。你用以下方程来计算

Verifier :

  1. 输入消息 , 计算
  2. 校验
  3. 计算

的横坐标为 , 校验等式 : 如果相等, 则接受 , 否则拒绝

公式推导过程如下:

这里知道 还是可以推算私钥, 所以 EIP-32 要求 :

EdDSA

以太坊 BN256 曲线已经支持了 EdDSA

EdDSA 正是为了解决 Schnorr 签名私钥泄露的问题 : 他不是选择随机数, 而是计算随机数

初始化 : 椭圆曲线生成元为 , 阶为 密钥生成 : 私钥为 , 公钥为

签名: 消息为 ,计算随机数 **,计算承诺

计算挑战

计算响应

签名为(R,s)

验证: 重新计算挑战 ,然后校验

与 ECDSA 最大的区别在于 是算出来的, 没有使用随机数 这样产生的签名结果是确定性的,即对同一消息, 签名结果相同, 不会额外泄露信息

一般说来随机数是安全措施中重要的一种方法,但是随机数的产生也是安全隐患,著名的索尼公司产品 PS3 密钥泄露事件,就是随机数产生的问题导致的 (写死在了代码里, 晕)。

zk-SNARK

在聊 zk-SNARKs 之前, 首先来看 NARK(Non-interactive ARgument of Knowledge) :

  • C : 电路 Circuit
  • : 公开声明 public statement
  • : witness
  • 预处理(Preprocessing) 也称为 Setup, 它以电路的描述作为输入,然后输出这些公开参数,我们称之为 :
  • 表示公开的参数,供证明者使用。
  • 表示公开的参数,供验证者使用。

证明者和验证者各自会输入 :

  • prover takes the (public statement) & (public (circuit)params) & the Witness
  • Verifier takes & (public statement)

然后,证明者试图向验证者证明: It knows some such that

NARK Definition : A pre-processing NARK is a triple , where :

  • generate the Circuit’s as public params for P & V.
  • : proof
  • : or

所有算法和对手都可以访问 随机预言机 (random oracle)

zk-SNARKs 条件是苛刻的, 因为要让 Verifier 在如此短的时间内完成某些验证, 我们需要一些新的方法来去处理计算, 比如多项式承诺 (polynomial commitment)

(To be continued …)

Reference :

Vitalik ZK_Snark zk-learning Lectures 安比 zk-snarks https://vitalik.ca/general/2021/01/26/snarks.html Zero Knowledge Proofs with Sigma Protocols

从代码中学习 Plonk 协议

写作本文的目的主要是希望从代码的角度理解 Plonk 协议。因为我是开发者,之前读文章遇到公式感觉比较抽象,所以希望有这样的文章,可以从代码的角度来阐述 ZKP 的协议是如何工作的。

这篇文章对应的源代码在这里,主要实现了 Plonk 协议的核心概念,需要结合郭宇老师的 Plonk 系列文章阅读。

流程

通过测试 test.py 看到验证 Plonk 协议主要分为以下几个部分:

  • Setup
  • Program
  • Assignment
  • Generate proof
  • Verify
def prover_test():
    print("Beginning prover test")
    # powers should be 2^n so that we can use roots of unity for FFT
    # and should be bigger than len(coeffs) of polynomial to do KZG commitment
    # the value here is: powers = 4 * group_order
    # which is bigger than the order of quotient polynomial
    group_order = 8
    powers = group_order * 4
    setup = Setup.generate_srs(powers)

    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    assignments = {"a": 3, "b": 4, "c": 12, "d": 5, "e": 60}
    prover = Prover(setup, program)
    proof = prover.prove(assignments)
    print("Prover test success")
    return setup, proof, group_order

def verifier_test(setup, proof, group_order):
    print("Beginning verifier test")
    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    public = [60]
    vk = setup.verification_key(program.common_preprocessed_input())
    assert vk.verify_proof(group_order, proof, public)
    print("Verifier test success")

整个协议的过程:

  1. 给定一个计算/电路/程序:
a * b = c
c * d = e
其中 e 是公开值
  1. prover 选择特定的一组值 witness = (a, b, c, d, e),这组值满足上面的约束条件
  2. 在保持 witness 不公开的前提下,prover 生成一个证明 proof, 可以证明 prover 知道 witness
  3. verifier 验证 proof 的真实性

下面我们通过代码依次看看每个步骤都做了什么。

Setup

@dataclass
class Setup(object):
    #   ([1]₁, [x]₁, ..., [x^{d-1}]₁)
    # = ( G,    xG,  ...,  x^{d-1}G ), where G is a generator of G_1
    powers_of_x: list[G1Point]
    # [x]₂ = xH, where H is a generator of G_2
    X2: G2Point

    @classmethod
    def generate_srs(cls, powers: int):
        print("Start to generate structured reference string")
        # tau is a random number whatever you choose
        tau = 218313819403157342856071133

        # Initialize powers_of_x with 0 values
        powers_of_x = [0] * powers
        # powers_of_x[0] =  b.G1 * tau**0 = b.G1
        # powers_of_x[1] =  b.G1 * tau**1 = powers_of_x[0] * tau
        # powers_of_x[2] =  b.G1 * tau**2 = powers_of_x[1] * tau
        # ...
        # powers_of_x[i] =  b.G1 * tau**i = powers_of_x[i - 1] * tau
        powers_of_x[0] = b.G1

        for i in range(powers):
            if i > 0:
                powers_of_x[i] = b.multiply(powers_of_x[i - 1], tau)

        assert b.is_on_curve(powers_of_x[1], b.b)
        print("Generated G1 side, X^1 point: {}".format(powers_of_x[1]))

        X2 = b.multiply(b.G2, tau)
        assert b.is_on_curve(X2, b.b2)
        print("Generated G2 side, X^1 point: {}".format(X2))

        assert b.pairing(b.G2, powers_of_x[1]) == b.pairing(X2, b.G1)
        print("X^1 points checked consistent")
        print("Finished to generate structured reference string")

        return cls(powers_of_x, X2)

    # Encodes the KZG commitment that evaluates to the given values in the group
    def commit(self, values: Polynomial) -> G1Point:
        if (values.basis == Basis.LAGRANGE):
            # inverse FFT from Lagrange basis to monomial basis
            coeffs = values.ifft().values
        elif (values.basis == Basis.MONOMIAL):
            coeffs = values.values
        if len(coeffs) > len(self.powers_of_x):
            raise Exception("Not enough powers in setup")
        return ec_lincomb([(s, x) for s, x in zip(self.powers_of_x, coeffs)])

    # Generate the verification key for this program with the given setup
    def verification_key(self, pk: CommonPreprocessedInput) -> VerificationKey:
        return VerificationKey(
            pk.group_order,
            self.commit(pk.QM),
            self.commit(pk.QL),
            self.commit(pk.QR),
            self.commit(pk.QO),
            self.commit(pk.QC),
            self.commit(pk.S1),
            self.commit(pk.S2),
            self.commit(pk.S3),
            self.X2,
            Scalar.root_of_unity(pk.group_order),
        )
    

这里有几个函数,第一个函数 generate_srs 用于生成 structured reference string(SRS),用于给多项式在群上生成 KZG commitment。基本流程:

  1. 选择一个随机数作为 tau 的值
  2. 从椭圆曲线上获得两个生成元 G1 和 G2,有现成的函数库可以拿到
  3. 生成所需要的 SRS 值
  4. 最后对生成的值进行验证

第二个函数 commit,就是实际用来生成 KZG commitment 的函数

第三个函数 verification_key 用来给 verifier 生成 verification key,用来验证 proof

Program

program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)

Program 类的目标是 Arithmetization,将某种计算转换成数学表示。这里的计算指的是一段电路,数学表示指的是多项式。

在 Plonk 中,可以用八个多项式来表示这个 Program: QL, QR, QM, QO, QC, S1, S2, S3。所以 Program 类的主要目标就是处理上面电路的字符串的表示,最终得到这八个多项式。这八个多项式是公开的,prover 和 verifier 都可以得到这个信息。

经过一定的处理,prover 得到 prover key(pk)。

@dataclass
class CommonPreprocessedInput:
    """Common preprocessed input"""

    group_order: int
    # q_M(X) multiplication selector polynomial
    QM: Polynomial
    # q_L(X) left selector polynomial
    QL: Polynomial
    # q_R(X) right selector polynomial
    QR: Polynomial
    # q_O(X) output selector polynomial
    QO: Polynomial
    # q_C(X) constants selector polynomial
    QC: Polynomial
    # S_σ1(X) first permutation polynomial S_σ1(X)
    S1: Polynomial
    # S_σ2(X) second permutation polynomial S_σ2(X)
    S2: Polynomial
    # S_σ3(X) third permutation polynomial S_σ3(X)
    S3: Polynomial

verifier 得到 verification key,在上面的 Setup 步骤中也提到了。

    # Generate the verification key for this program with the given setup
    def verification_key(self, pk: CommonPreprocessedInput) -> VerificationKey:
        return VerificationKey(
            pk.group_order,
            self.commit(pk.QM),
            self.commit(pk.QL),
            self.commit(pk.QR),
            self.commit(pk.QO),
            self.commit(pk.QC),
            self.commit(pk.S1),
            self.commit(pk.S2),
            self.commit(pk.S3),
            self.X2,
            Scalar.root_of_unity(pk.group_order),
        )

和 prover 不一样的是,为什么中间 8 个值要用 commitment 的形式发给 verifier 呢?这是因为 Plonk 协议为了保证 verifier 端验证的计算复杂度尽量低,所以没有给出原始的多项式,而只给出了 KZG 承诺的值,后面会看到,verifier 通过 pairing 验证就可以保证这些承诺值和原始的多项式是一一对应的,prover 欺骗不了 verifier,这样既保证的正确性,也保证了 verifier 验证的简单性。

Assignment

assignments = {"a": 3, "b": 4, "c": 12, "d": 5, "e": 60}

Assignment 是对电路中引线的赋值,也叫 witness 或者 private input。这些值只有 prover 知道,对 verifier 是保密的。prover 最终要向 verifier 提供证明,保证将这些值输入到 program 中能得到指定的结果。

Generate proof

proof = prover.prove(assignments)

这里是协议的重点,分为五轮。

主体逻辑:

    def prove(self, witness: dict[Optional[str], int]) -> Proof:
        # Initialise Fiat-Shamir transcript
        transcript = Transcript(b"plonk")

        # Collect fixed and public information
        # FIXME: Hash pk and PI into transcript
        public_vars = self.program.get_public_assignments()
        PI = Polynomial(
            [Scalar(-witness[v]) for v in public_vars]
            + [Scalar(0) for _ in range(self.group_order - len(public_vars))],
            Basis.LAGRANGE,
        )
        self.PI = PI

        # Round 1
        msg_1 = self.round_1(witness)
        self.beta, self.gamma = transcript.round_1(msg_1)

        # Round 2
        msg_2 = self.round_2()
        self.alpha, self.fft_cofactor = transcript.round_2(msg_2)

        # Round 3
        msg_3 = self.round_3()
        self.zeta = transcript.round_3(msg_3)

        # Round 4
        msg_4 = self.round_4()
        self.v = transcript.round_4(msg_4)

        # Round 5
        msg_5 = self.round_5()

        return Proof(msg_1, msg_2, msg_3, msg_4, msg_5)

通过 5 轮的计算会生成必要的 proof,这些 proof 之后交给 verifier 进行验证,如果通过,则整个协议完成。

Round 0: 初始化:

    def __init__(self, setup: Setup, program: Program):
        self.group_order = program.group_order
        self.setup = setup
        self.program = program
        self.pk = program.common_preprocessed_input()

Round 1: 生成对 witness/assignments 多项式的承诺

这个过程和相关知识可以参考 理解 PLONK(一):Plonkish Arithmetization 理解 PLONK(二):多项式编码 。多项式承诺相关知识可以参考 理解 Plonk(五):多项式承诺

大体流程:

  1. 根据 group_order 初始化 A, B, C 这三个 witness 多项式的点值 A_values,B_values,C_values,这些点值用于后面生成多项式,也就是生成的 Polynomial
  2. 依次读取 program 中引线的值,将左引线的值 L 添加到 A_values, 右引线的值添加到 B_values 中,输出引线的值添加到 C_values 中
  3. 通过 Polynomial 类生成 A,B,C 多项式
  4. 生成 A,B,C 多项式的 KZG 承诺
  5. 验证门约束等式是否成立
    def round_1(
        self,
        witness: dict[Optional[str], int],
    ) -> Message1:
        program = self.program
        setup = self.setup
        group_order = self.group_order

        if None not in witness:
            witness[None] = 0

        # 1. 根据 group_order 初始化 A, B, C 多项式的点值
        # A_values,B_values,C_values,这些点值用于后面
        # 生成多项式,也就是 `Polynomial` 类
        # Compute wire assignments
        A_values = [Scalar(0) for _ in range(group_order)]
        B_values = [Scalar(0) for _ in range(group_order)]
        C_values = [Scalar(0) for _ in range(group_order)]

        # 2. 依次读取 program 中引线的值,将左引线的值 L 添加到 A_values, 
        # 右引线的值添加到 B_values 中,输出引线的值添加到 C_values 中
        for i, gate_wires in enumerate(program.wires()):
            A_values[i] = Scalar(witness[gate_wires.L])
            B_values[i] = Scalar(witness[gate_wires.R])
            C_values[i] = Scalar(witness[gate_wires.O])

        # 3. 通过 `Polynomial` 类生成 A,B,C 多项式
        self.A = Polynomial(A_values, Basis.LAGRANGE)
        self.B = Polynomial(B_values, Basis.LAGRANGE)
        self.C = Polynomial(C_values, Basis.LAGRANGE)

        # 4. 生成 A,B,C 多项式的 KZG 承诺
        a_1 = setup.commit(self.A)
        b_1 = setup.commit(self.B)
        c_1 = setup.commit(self.C)

        # 5. 验证门约束等式是否成立
        # Sanity check that witness fulfils gate constraints
        assert (
            self.A * self.pk.QL
            + self.B * self.pk.QR
            + self.A * self.B * self.pk.QM
            + self.C * self.pk.QO
            + self.PI
            + self.pk.QC
            == Polynomial([Scalar(0)] * group_order, Basis.LAGRANGE)
        )

        return Message1(a_1, b_1, c_1)

Round 2: 生成 Permutation Accumulator 多项式 Z 的 KZG 承诺

参考文章 理解 PLONK(三):置换证明

要给 Z 生成承诺,首先要构造Z,然后可以直接对多项式生成 KZG 承诺。

大体流程:

  1. 初始化Z 的点值数组 Z_values 第一个值为 1
  2. 在 group_order 内,依次用上一次的值乘以当前的累乘因子,获得当前 Z_values 的值
  3. 确保最后一项为 1(具体原理请看上面的文章)
  4. 检查生成值的有效性
  5. 用 Lagrange 形式构造多项式
  6. 生成 KZG 承诺
    def round_2(self) -> Message2:
        group_order = self.group_order
        setup = self.setup

        Z_values = [Scalar(1)]
        roots_of_unity = Scalar.roots_of_unity(group_order)
        for i in range(group_order):
            Z_values.append(
                Z_values[-1]
                * self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
                / self.rlc(self.A.values[i], self.pk.S1.values[i])
                / self.rlc(self.B.values[i], self.pk.S2.values[i])
                / self.rlc(self.C.values[i], self.pk.S3.values[i])
            )
        assert Z_values.pop() == 1

        # Sanity-check that Z was computed correctly
        for i in range(group_order):
            assert (
                self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
            ) * Z_values[i] - (
                self.rlc(self.A.values[i], self.pk.S1.values[i])
                * self.rlc(self.B.values[i], self.pk.S2.values[i])
                * self.rlc(self.C.values[i], self.pk.S3.values[i])
            ) * Z_values[
                (i + 1) % group_order
            ] == 0

        Z = Polynomial(Z_values, Basis.LAGRANGE)
        z_1 = setup.commit(Z)
        print("Permutation accumulator polynomial successfully generated")

        self.Z = Z
        return Message2(z_1)

其中 rlc 的定义:

    def rlc(self, term_1, term_2):
        return term_1 + term_2 * self.beta + self.gamma

Round 3: 生成商多项式的承诺

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

大体流程:

  1. 构造消失多项式(Vanishing Polynomial): ZH_coeff
  2. 构造电路的门约束多项式: gate_constraints_coeff
  3. 构造 Copy Constraints 的多项式: permutation_grand_product
  4. 构造 Copy Constraints 第一个值为 1 这个约束的多项式: permutation_first_row_coeff
  5. 求出商多项式 quotient polynomial: T_coeff
  6. 计算商多项式的 KZG 承诺
    def round_3(self) -> Message3:
        group_order = self.group_order
        setup = self.setup

        # Compute the quotient polynomial

        alpha = self.alpha

        roots_of_unity = Scalar.roots_of_unity(group_order)

        A_coeff, B_coeff, C_coeff, S1_coeff, S2_coeff, S3_coeff, Z_coeff, QL_coeff, QR_coeff, QM_coeff, QO_coeff, QC_coeff, PI_coeff = (
            x.ifft()
            for x in (
                self.A,
                self.B,
                self.C,
                self.pk.S1,
                self.pk.S2,
                self.pk.S3,
                self.Z,
                self.pk.QL,
                self.pk.QR,
                self.pk.QM,
                self.pk.QO,
                self.pk.QC,
                self.PI,
            )
        )

        L0_coeff = (
            Polynomial([Scalar(1)] + [Scalar(0)] * (group_order - 1), Basis.LAGRANGE)
        ).ifft()

        # x^8 - 1 coeffs are [-1, 0, 0, 0, 0, 0, 0, 0, 1]
        # which needs 9 points(n + 1) to determine the polynomial
        ZH_array = [Scalar(-1)] + [Scalar(0)] * (group_order - 1) + [Scalar(1)]
        ZH_coeff = Polynomial(ZH_array, Basis.MONOMIAL)

        gate_constraints_coeff = (
            A_coeff * QL_coeff
            + B_coeff * QR_coeff
            + A_coeff * B_coeff * QM_coeff
            + C_coeff * QO_coeff
            + PI_coeff
            + QC_coeff
        )

        normal_roots = Polynomial(
            roots_of_unity, Basis.LAGRANGE
        )

        roots_coeff = normal_roots.ifft()
        # z * w
        ZW = self.Z.shift(1)
        ZW_coeff = ZW.ifft()

        for i in range(group_order):
            assert (
                self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
            ) * self.Z.values[i] - (
                self.rlc(self.A.values[i], self.pk.S1.values[i])
                * self.rlc(self.B.values[i], self.pk.S2.values[i])
                * self.rlc(self.C.values[i], self.pk.S3.values[i])
            ) * ZW.values[
                i % group_order
            ] == 0

        permutation_grand_product_coeff = (
            (
                self.rlc(A_coeff, roots_coeff)
                * self.rlc(B_coeff, roots_coeff * Scalar(2))
                * self.rlc(C_coeff, roots_coeff * Scalar(3))
            )
            * Z_coeff
            - (
                self.rlc(A_coeff, S1_coeff)
                * self.rlc(B_coeff, S2_coeff)
                * self.rlc(C_coeff, S3_coeff)
            )
            * ZW_coeff
        )

        permutation_first_row_coeff = (Z_coeff - Scalar(1)) * L0_coeff

        all_constraints = (
            gate_constraints_coeff
            + permutation_grand_product_coeff * alpha
            + permutation_first_row_coeff * alpha**2
        )

        # quotient polynomial
        T_coeff = all_constraints / ZH_coeff

        print("Generated the quotient polynomial")

        W_t = setup.commit(T_coeff)

        self.A_coeff = A_coeff
        self.B_coeff = B_coeff
        self.C_coeff = C_coeff
        self.S1_coeff = S1_coeff
        self.S2_coeff = S2_coeff
        self.S3_coeff = S3_coeff
        self.Z_coeff = Z_coeff
        self.ZW_coeff = ZW_coeff
        self.QL_coeff = QL_coeff
        self.QR_coeff = QR_coeff
        self.QM_coeff = QM_coeff
        self.QO_coeff = QO_coeff
        self.QC_coeff = QC_coeff
        self.PI_coeff = PI_coeff
        self.T_coeff = T_coeff

        return Message3(W_t)

Round 4: 对各个多项式在一个随机的 zeta 点求值

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

这一步比较简单,对各个多项式在一个随机的 zeta 点求值。这里有一个知识点,就是如何获取 zeta 这个随机值,使用的方法叫做Fiat-Shamir 变换,可以将一个需要 prover 和 verifier 进行交互的证明转化成不需要交互的证明,简单介绍可以参考 这篇文章,代码可以参考 这里

    def round_4(self) -> Message4:
        group_order = self.group_order
        zeta = self.zeta

        a_eval = self.A_coeff.coeff_eval(zeta)
        b_eval = self.B_coeff.coeff_eval(zeta)
        c_eval = self.C_coeff.coeff_eval(zeta)
        s1_eval = self.S1_coeff.coeff_eval(zeta)
        s2_eval = self.S2_coeff.coeff_eval(zeta)
        s3_eval = self.S3_coeff.coeff_eval(zeta)
        root_of_unity = Scalar.root_of_unity(group_order)
        z_eval = self.Z_coeff.coeff_eval(zeta)
        zw_eval = self.Z_coeff.coeff_eval(zeta * root_of_unity)
        ql_eval = self.QL_coeff.coeff_eval(zeta)
        qr_eval = self.QR_coeff.coeff_eval(zeta)
        qm_eval = self.QM_coeff.coeff_eval(zeta)
        qo_eval = self.QO_coeff.coeff_eval(zeta)
        qc_eval = self.QC_coeff.coeff_eval(zeta)
        t_eval = self.T_coeff.coeff_eval(zeta)

        self.a_eval = a_eval
        self.b_eval = b_eval
        self.c_eval = c_eval
        self.ql_eval = ql_eval
        self.qr_eval = qr_eval
        self.qm_eval = qm_eval
        self.qo_eval = qo_eval
        self.qc_eval = qc_eval
        self.s1_eval = s1_eval
        self.s2_eval = s2_eval
        self.s3_eval = s3_eval
        self.z_eval = z_eval
        self.zw_eval = zw_eval
        self.t_eval = t_eval

        return Message4(
            a_eval,
            b_eval,
            c_eval,
            ql_eval,
            qr_eval,
            qm_eval,
            qo_eval,
            qc_eval,
            s1_eval,
            s2_eval,
            s3_eval,
            z_eval,
            zw_eval,
            t_eval
        )

coeff_eval 是一个多项式求值的函数。具体实现可以看这里

Round 5: 对每个多项式生成 KZG 承诺中需要的两个承诺

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

这一步也比较简单,生成所需的 KZG 承诺,为 verifier 进行 verify 做准备。

    def round_5(self) -> Message5:
        W_a, W_a_quot = self.generate_commitment(self.A_coeff, self.a_eval)
        W_b, W_b_quot = self.generate_commitment(self.B_coeff, self.b_eval)
        W_c, W_c_quot = self.generate_commitment(self.C_coeff, self.c_eval)
        W_ql, W_ql_quot = self.generate_commitment(self.QL_coeff, self.ql_eval)
        W_qr, W_qr_quot = self.generate_commitment(self.QR_coeff, self.qr_eval)
        W_qm, W_qm_quot = self.generate_commitment(self.QM_coeff, self.qm_eval)
        W_qo, W_qo_quot = self.generate_commitment(self.QO_coeff, self.qo_eval)
        W_qc, W_qc_quot = self.generate_commitment(self.QC_coeff, self.qc_eval)
        W_s1, W_s1_quot = self.generate_commitment(self.S1_coeff, self.s1_eval)
        W_s2, W_s2_quot = self.generate_commitment(self.S2_coeff, self.s2_eval)
        W_s3, W_s3_quot = self.generate_commitment(self.S3_coeff, self.s3_eval)
        W_z, W_z_quot = self.generate_commitment(self.Z_coeff, self.z_eval)
        W_zw, W_zw_quot = self.generate_commitment(self.ZW_coeff, self.zw_eval)
        W_t, W_t_quot = self.generate_commitment(self.T_coeff, self.t_eval)

        print("Generated final quotient witness polynomials")
        return Message5(
            W_a, W_a_quot,
            W_b, W_b_quot,
            W_c, W_c_quot,
            W_ql, W_ql_quot,
            W_qr, W_qr_quot,
            W_qm, W_qm_quot,
            W_qo, W_qo_quot,
            W_qc, W_qc_quot,
            W_s1, W_s1_quot,
            W_s2, W_s2_quot,
            W_s3, W_s3_quot,
            W_z, W_z_quot,
            W_zw, W_zw_quot,
            W_t, W_t_quot,
        )

生成承诺的代码:


    def generate_commitment(self, coeff: Polynomial, eval: Scalar):
        setup = self.setup
        zeta = self.zeta
        # Polynomial for (X - zeta)
        ZH_zeta_coeff = Polynomial([-zeta, Scalar(1)], Basis.MONOMIAL)
        quot_coeff = (coeff - eval) / ZH_zeta_coeff
        # witness for polynomial itself
        w = setup.commit(coeff)
        # witness for quotient polynomial
        w_quot = setup.commit(quot_coeff)
        return w, w_quot

Verify

再回顾一下测试中的代码是这样进行验证的:

def verifier_test(setup, proof, group_order):
    print("Beginning verifier test")
    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    public = [60]
    vk = setup.verification_key(program.common_preprocessed_input())
    assert vk.verify_proof(group_order, proof, public)
    print("Verifier test success")

VerificationKey 的代码在这里,最核心的方法是 verify_proof

verifier 主要做两件事情:

  1. 验证 KZG 承诺,保证多项式是和所承诺的一致
  2. 验证最终组合出来的多项式求值的相等性
    def verify_proof(self, group_order: int, pf, public=[]) -> bool:
        # 4. Compute challenges
        beta, gamma, alpha, zeta, v, u = self.compute_challenges(pf)
        proof = pf.flatten()

        # 5. Compute zero polynomial evaluation Z_H(ζ) = ζ^n - 1
        ZH_ev = zeta**group_order - 1

        # 6. Compute Lagrange polynomial evaluation L_0(ζ)
        L0_ev = ZH_ev / (group_order * (zeta - 1))

        # 7. Compute public input polynomial evaluation PI(ζ).
        PI = Polynomial(
            [Scalar(-x) for x in public]
            + [Scalar(0) for _ in range(group_order - len(public))],
            Basis.LAGRANGE,
        )
        PI_ev = PI.barycentric_eval(zeta)

        # verify KZG10 commitment
        self.verify_commitment(proof, proof["W_a"], "W_a_quot", "a_eval", zeta)
        self.verify_commitment(proof, proof["W_b"], "W_b_quot", "b_eval", zeta)
        self.verify_commitment(proof, proof["W_c"], "W_c_quot", "c_eval", zeta)
        self.verify_commitment(proof, proof["W_z"], "W_z_quot", "z_eval", zeta)
        self.verify_commitment(proof, proof["W_zw"], "W_zw_quot", "zw_eval", zeta)
        self.verify_commitment(proof, proof["W_t"], "W_t_quot", "t_eval", zeta)
        self.verify_commitment(proof, self.Ql, "W_ql_quot", "ql_eval", zeta)
        self.verify_commitment(proof, self.Qr, "W_qr_quot", "qr_eval", zeta)
        self.verify_commitment(proof, self.Qm, "W_qm_quot", "qm_eval", zeta)
        self.verify_commitment(proof, self.Qo, "W_qo_quot", "qo_eval", zeta)
        self.verify_commitment(proof, self.Qc, "W_qc_quot", "qc_eval", zeta)
        self.verify_commitment(proof, self.S1, "W_s1_quot", "s1_eval", zeta)
        self.verify_commitment(proof, self.S2, "W_s2_quot", "s2_eval", zeta)
        self.verify_commitment(proof, self.S3, "W_s3_quot", "s3_eval", zeta)

        a_eval = proof["a_eval"]
        b_eval = proof["b_eval"]
        c_eval = proof["c_eval"]
        ql_eval = proof["ql_eval"]
        qr_eval = proof["qr_eval"]
        qm_eval = proof["qm_eval"]
        qo_eval = proof["qo_eval"]
        qc_eval = proof["qc_eval"]
        s1_eval = proof["s1_eval"]
        s2_eval = proof["s2_eval"]
        s3_eval = proof["s3_eval"]
        z_eval = proof["z_eval"]
        zw_eval = proof["zw_eval"]
        t_eval = proof["t_eval"]

        f_eval = (
            (a_eval + beta * zeta + gamma)
            * (b_eval + beta * zeta * 2 + gamma)
            * (c_eval + beta * zeta * 3 + gamma)
        )
        g_eval = (
            (a_eval + beta * s1_eval + gamma)
            * (b_eval + beta * s2_eval + gamma)
            * (c_eval + beta * s3_eval + gamma)
        )

        gate_constraints_eval = (
            ql_eval * a_eval
            + qr_eval * b_eval
            + qm_eval * a_eval * b_eval
            + qo_eval * c_eval
            + qc_eval
            + PI_ev
        )

        permutation_grand_product_eval = z_eval * f_eval - zw_eval * g_eval

        permutation_first_row_eval = L0_ev * (z_eval - 1)

        left = (
            gate_constraints_eval
            + alpha * permutation_grand_product_eval
            +  alpha ** 2 * permutation_first_row_eval
        )

        right = t_eval * ZH_ev

        assert left == right

        print("Done equation check for all constraints")
        return True

    # Compute challenges (should be same as those computed by prover)
    def compute_challenges(
        self, proof
    ) -> tuple[Scalar, Scalar, Scalar, Scalar, Scalar, Scalar]:
        transcript = Transcript(b"plonk")
        beta, gamma = transcript.round_1(proof.msg_1)
        alpha, _fft_cofactor = transcript.round_2(proof.msg_2)
        zeta = transcript.round_3(proof.msg_3)
        v = transcript.round_4(proof.msg_4)
        u = transcript.round_5(proof.msg_5)

        return beta, gamma, alpha, zeta, v, u

    def verify_commitment(self, proof, W, W_quot_key, eval_key, zeta):
        W_quot = proof[W_quot_key]
        eval = proof[eval_key]
        ec_comb = ec_lincomb(
            [
                (W, 1),
                (W_quot, zeta),
                (b.G1, -eval),
            ]
        )

        assert b.pairing(self.X_2, W_quot) == b.pairing(b.G2, ec_comb)
        print(f"Done KZG10 commitment check for {eval_key} polynomial")

最后

以上就是 Plonk 协议的代码讲解,接下来建议读者亲自运行一下这个代码,打印其中一些值看看,这样会对协议的了解更加深刻。

  • Last update: 2023-10-23 1
  • 📢 Feel free to contribute! Pls fork zkp-co-learning.github.io and PR!
  • PS: In each title’s Content Block, the top part is newer, below is older.

ZKP building blocks

Cryptography

《公钥密码学研究方法论》[https://documents.uow.edu.au/~fuchun/methodology.html]

  • 这是 slides,这是最新的本体: [https://eprint.iacr.org/2023/715

  • 密码学纯理论进展速度也同样相当的惊人,

  • 清华毕业的Yanyi Liu跟着Rafael Pass 揪着OWF不放疯狂输出,试图彻底搞清楚密码学的复杂性理论基础。

  • Yilei Chen回国后负责清华姚班的Cryptography课 http://www.chenyilei.net/cryptography-s2023.html, 下一代姚班选手的ZKP就都是他教的了,他在FS要不要密码学哈希,iO/lattice方面也有很多工作。

  • Lijie Chen就更不用说了,华人TCS之光,博士论文写了551页https://www.mit.edu/~lijieche/Lijie-Chen-thesis.pdf,已经是average-case hardness 和 derandomization领域世界级的专家了,可以去预测哪年得图灵奖了。

Curves

Math.

Probability and Measure https://www.youtube.com/playlist?list=PL0vEWJI_pj7RZ51zecINlzWxpFv83r8RE 还有这个,我快看完了,超喜欢

Succinct Proofs and Linear Algebra

数学 :

  • 参考学习内容:多项式乘法和除法、拉格朗日插值、Schwartz-Zip-pel 引理、快速傅立叶变换(FFT) 、NTT、MSM 以及 Field extension等。
  • An Introduction to Mathematical Cryptography
  • https://explained-from-first-principles.com/number-theory 这个介绍数论的也不错

初等数论和群论 :

Rust

https://github.com/evcxr/evcxr/tree/main/evcxr_jupyter yeah, a Jupyter kernel for Rust

cargo install evcxr_jupyter
evcxr_jupyter --install
jupyter lab

ZKP Basics Tutorials

这个课也值得跟上: PSE Lectures - A full course on Elliptic Curve Cryptography https://www.youtube.com/playlist?list=PLV91V4b0yVqQ_inAjuIB5SwBNyYmA9S6M

新火公开课 https://space.bilibili.com/3493266041342842/channel/series 钟博的课

  • https://www.rareskills.io/zk-bootcamp
  • https://zkhack.dev/whiteboard/ 挺适合作为基础资料的
  • https://www.rareskills.io/zk-book

I try not to recommend too many sources, since everyone learns differently. But here are a few:

  • Least Authority’s Moonmath manual for learning Snarks. Starts with the basics.

  • For learning about Starks, StarkWare’s Stark 101 series.

  • For writing some ZK code and seeing what actually happens: Noir from Aztec

  • a16z 整理的比较全的 ZKP 的资料库 https://a16zcrypto.com/zero-knowledge-canon/

  • ZK 零知识: https://buidlerdao.notion.site/ZK-5963083942a744bbb60a0328008868e2

  • 零基础学习 ZK: https://mirror.xyz/searchblock.eth/y11EKtXAtK3aXRVMV1yYqw7FibKHxI0fK10vlVRDaD4

  • 万字长文捕获 ZK Rollup 时代价值|ZONFF Research: https://mp.weixin.qq.com/s/5zKdS-GL8w_z4XIDOjv7FA

  • zksync开源

怎么零基础学习零知识证明:

  • https://mirror.xyz/searchblock.eth/y11EKtXAtK3aXRVMV1yYqw7FibKHxI0fK10vlVRDaD4

Awesome ZKP

Nova / Folding schemes

nova 写的五子棋: https://www.zkconnect4.dev/

Nova 项目: https://github.com/microsoft/Nova/network/dependents

A Brief History of Folding Schemes https://arc.net/e/2831196C-9575-47A6-966E-B34DB840168E

  • Bootleproof-type IPA是「folding 前史」的一章

https://nova-browser-ecdsa-web.vercel.app/

Folding with ProtoGalaxy - Liam Eagen https://www.youtube.com/watch?v=SpkTvRia1EA

Benchmark on recursion Plonky vs Nova https://github.com/nikkolasg/recursive-bench

白菜: 有对NOVA-ML 感兴趣的不,这个课题可能比较大,可以考虑组个支队了 😂

  • https://github.com/socathie/nova-ml
  • awesome-zkml : https://github.com/worldcoin/awesome-zkml
  • Team Novi (Nova): https://0xparc.notion.site/Team-Novi-2d81bc06b0aa4c99b61a9ee06166b3b6
  • risc0-nova
    • https://github.com/hero78119/risc0-nova
  • Parallelizing Nova
  • Nova: A New Chapter in Zero-Knowledge Proofs (medium这糟糕的排版)

Halo2

https://github.com/CPerezz/halo2_sumcheck 👀

https://github.com/axiom-crypto/halo2-wasm

https://github.com/zkCert/halo2-zkcert

0xPARC Course Recordings, Slides, and Notes

Taiko: https://github.com/taikoxyz/circuit-tools/tree/main

  • 我们从 PSE 的 zkevm 里抽象出了一个 sdk,大家如果想用 halo2 开发可以试下 [Grin] 欢迎给我提 issue

solidity verifier: https://github.com/privacy-scaling-explorations/halo2-solidity-verifier/pulls

halo2 tinyram https://github.com/Orbis-Tertius/tiny-ram-halo2

  • Poseidon: https://github.com/zcash/halo2/blob/main/halo2_gadgets/src/poseidon/pow5.rs
  • proof of validator: https://github.com/asn-d6/halo2-merkle-tree-ipa-bench
  • social recovery: https://hackmd.io/@Nerolation/H1BvRWg02

axiom 的 open source program


使用了 halo2 的 Applications:

  • ZK Email https://github.com/zkemail halo2
  • ZK Wordle: https://zordle.xyz/ halo2
  • Hammster: https://github.com/ytham/hammster halo2
  • zk-draw : Verifiable random draw with zero-knowledge of the random seed https://github.com/jae-cuz/zk-draw halo2
  • ZK Microphone: https://github.com/Miyamura80/ZKMicrophone
  • Building a Zero Knowledge web app with Halo 2 and Wasm (part 1)
  • zk-img: Fighting Deepfakes with Zero-Knowledge Proofs https://medium.com/@danieldkang/zk-img-fighting-deepfakes-with-zero-knowledge-proofs-9b76c23e3789 尚未开源

大部分由 @Kurt Pan 博士整理

STARK

深入探索STARK的安全性和可靠性——STARKs全面安全分析 https://blog.csdn.net/mutourend/article/details/133821797

  • A Walk-Through of a Simple zk-STARK Proof

  • Boojum:zkSync的高性能去中心化STARK证明系统 https://blog.csdn.net/mutourend/article/details/131770996

  • 一个非常简洁的stark介绍,比vitalik版本更加适合初学者:

    • Zero Knowledge Virtual Machine step by step https://eprint.iacr.org/2023/1032.pdf
  • https://github.com/aszepieniec/stark-brainfuck/blob/master/docs/engine.md

    • 老外写个一个简单版本的zkvm,不过是基于stark的
    • 并且也配有文档,适合研究基于stark zkvm的原理理解。

@0xhhh: 总结了下 Stark的一些资料,很多来自于@Kurt Pan ,感谢潘老师:

  • https://www.notion.so/Stark-Cannon-0801f24ea9e543449e015bf4063bb71d?pvs=4
  • https://literate-wolfsbane-bf0.notion.site/Stark-Cannon-0801f24ea9e543449e015bf4063bb71d?pvs=4

Kurt Pan : https://github.com/erhant/zkbrainfuck A Brainfuck zkVM with Circom.

https://starkware.co/stark-101/ @Frank Jz Liu 推荐,亲测很不错

再推荐这几个,你可以去整理一下: https://aszepieniec.github.io/stark-anatomy/ https://aszepieniec.github.io/stark-brainfuck/ https://github.com/facebook/winterfell https://github.com/lambdaclass https://github.com/andrewmilson/ministark/

  • https://github.com/aszepieniec/stark-brainfuck/blob/master/docs/engine.md
    • 老外写个一个简单版本的zkvm,不过是基于stark的
    • 并且也配有文档,适合研究基于stark zkvm的原理理解。

实现一个 Baby Snark

  • 可以参考 https://github.com/initc3/babySNARK 。可以看到Andrew Miller,Ye Zhang这些大佬几年前也是这么一步步成长起来的

@Maxlion🦁 提供:

Cairo 环境配置➕ERC 实现(Cairo1) https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1

Cairo 语言中文文档 https://book.cairo-lang.org/zh-cn/index.html

开发工具版本号推荐 湘_Xiang 版本 https://w3hitchhiker.notion.site/Starknet-cc07119ed57648faa92f7630219225b3?pvs=4 鸦_Cryptonerdcn 版本 https://docs.google.com/document/d/1ehBfE2CBeVL9rDhSc_JiUtxZJKWVKl8A6o1f1Ee0X4M/mobilebasic

[Cairo合约示例] Nethermind 版 https://starknet-by-example.voyager.online/ LambdaClass 版 https://cairo-by-example.com/ OpenZeppelin 版 https://github.com/OpenZeppelin/cairo-contracts/tree/cairo-2

Starknet 文档 https://book.starknet.io/

Starknet 基金会给 Astro 社区开发者的答疑文档 https://docs.google.com/document/d/1ku_y4M9PUe_GcwiBlvq6Kz2LkagyXD2Za1XCZj-IHVo/mobilebasic

Starknet 中文知识库(生态向) https://starknet-astro.super.site/

ZKML

Zator: Verified inference of a 512-layer neural network using recursive SNARKsv

TensorPlonk: A “GPU” for ZKML, Delivering 1,000x Speedups https://medium.com/@danieldkang/tensorplonk-a-gpu-for-zkml-delivering-1-000x-speedups-d1ab0ad27e1c

  • https://github.com/worldcoin/awesome-zkml
  • https://github.com/socathie/circomlib-ml
  • https://github.com/socathie/nova-ml
  • https://github.com/lyronctk/zator

ZKML slides: https://www.canva.com/design/DAFi3o7FiR4/d9LMeacr6QQwYy9C0BQHgA/view

这篇 ZKML 文章很顶: https://mirror.xyz/sevenxventures.eth/3USbrj7kcK7lyq_7upA4iyWV5pWMII7KrM40z5zpEXo

zkVM

https://github.com/vacp2p/zk-explorations

  • 对不同 lib 和 algo 实现的 VM 做的性能测试 benchmark

发现了asz有好多好东西啊,stark很好的学习资料 https://github.com/aszepieniec/stark-anatomy

下一期共学STARK+zkVM的资料

https://neptune.cash/ STARK 算法解析(第 6 部分: 加速整个流程) https://asz.ink/alan-szepieniec/ he is an advisor to Nervos Foundation. https://neptune.cash/ https://neptune.cash/learn/brainfuck-tutorial/

  • snarkVM: Aleo 已经把 Marlin 更新成 Varuna 了 varuna

徒手写 zkVM - https://eprint.iacr.org/2023/1032.pdf

  • ZERO KNOWLEDGE VIRTUAL MACHINE STEP BY STEP

  • 稍微比较偏理论

  • https://github.com/aszepieniec/stark-brainfuck/blob/master/docs/engine.md

    • 老外写个一个简单版本的zkvm,不过是基于stark的
    • 并且也配有文档,适合研究基于stark zkvm的原理理解。

另外他去年一篇zkvm的概览,对理解zkvm的设计还是很棒的。 他是指 https://aszepieniec.github.io/stark-brainfuck/ 的作者?

  • 基本的输入输出,内存等。我一直认为那个教程就是最好的zkvm入门的。

https://github.com/cryptape/ckb-bf-zkvm 秘猿 A BrainFuck zkVM implementation on CKB, using Halo2. 一个只有几条指令的 vm 机器

  • snarkVM有啊,Aleo家的就是 https://github.com/AleoHQ/snarkVM。没记错底层proof sys用的Marlin。
    • 可能program execution是面向冯诺伊曼架构RAM模型,而AIR相比于电路抽象层次更高,设计STARK VM更直接,所以大多数项目会这么选吧。
    • 不过zkEVM都用SNARK堆出来了,同样的工程师去堆一个自定义ISA的VM对他们来说应该是个simpler task吧

Nova VM 来了, 看不过来了。。。。

我在找一个write a vm from scratch的课程

  • 要是只是vm不要求zkvm那就太多了,比如5天前的这篇 https://andreabergia.com/blog/2023/07/i-have-written-a-jvm-in-rust/

上周测试了一下 PSE evm circuit,生成的 raw proof 用 evm 验证需要大约 4000万+ gas。(本地调高了 block gas limit) 也测试了 scroll-prover 的 chunk proof,evm 验证大概需要 40万+ gas。感觉 gas fee 这块至少有 100 倍的以上的提升

chunk proof 里面包涵了 2次压缩,还是做了不少的工作

不太确定上面的代码是不是具体的电路,看了下感觉没多少行。[Facepalm]

zkevm super circuit 的 column 太多了,应该超过了 1000 个。 chunk 和 aggregation 的 column 少了很多,但是应该像 Frank 所说,电路 “平铺”在聚合电路里。不知道他们怎么优化到 40万 gas 的,这个水平已经和 circom 差不多了

cutting-edge

Lattice

Lattice-based cryptography: The tricky math of dots https://www.youtube.com/watch?v=QDdOoYdb748&list=PLMItfTVgwEAvTX4-sZkcF5s3-l1JDocY0&index=4

Quantum

Quantum Resistance and the Signal Protocol

  • Signal 已经集成 Kyber了

ZK Applications

ZK Email

最近有进展,确实值得研究:ZK Email https://github.com/zkemail

ZK Proof of Email:通往decentralized identity之路 https://blog.csdn.net/mutourend/article/details/129004763?ops_request_misc=&request_id=2425a5a6d21c4b6bbe440828ba478ccf&biz_id=

zkWASM

Exploring alternatives to WASM for smart contracts

一个华人团队搞的 zkwasm

  • https://github.com/DelphinusLab/zkWasm

  • 另一个zkwasm - supernova, 估计9-10月发布

    • https://twitter.com/powdr_labs/status/1679822931340173313
    • https://twitter.com/HoumanShadab/status/1679155719805755392
【zkwasm 分享总结】: 
程序泡在 webassembly 的 bytecode 里面。是一个 webassenmbly 的小程序。
不分链游的问题:游戏运行的结果有可能作弊,不知道你是怎么运行这个程序的,有可能你上传了一些裸数据。 所以仍依靠中心化的服务器

会在跑 webassembly 的时候跑一个,监听用户事件,产生输入输出的 sequence,把他编译成一个 webassembly 的文件。每个操作带来的 consequence 会被管控的,把 proof 上链。

how to generate proof:
- Prover 实例
- 不用每次都和链上交互(gas 太高),可以把本次所有的行动都压成一个 proof。

https://github.com/DelphinusLab/zkWasm

如果想 onchain Verify,需要 Deploy 一个测试合约

合约生成,prove 加速:
- https://github.com/DelphinusLab/continuation-batcher

https://hackmd.io/@sinka/BJUIyufEc

后续:模块化公开课

PSE

From CEX to CCEX with Summa https://mirror.xyz/privacy-scaling-explorations.eth/_1Y6ExFD_Rs3oDxwx5_kWAj_Tl_L9c0Hm7E6SVJei0A

https://mirror.xyz/privacy-scaling-explorations.eth/f2ZfkPXZpvc6DUmG5-SyLjjYf78bcOcFeiJX2tb2hS0

PSE Lectures - Ep 6 https://www.youtube.com/watch?v=l7bEN1V7qRM

Some Project Boards:

Proof of Innocence(清白证明):

Tricks & Tools

https://github.com/joelparkerhenderson/sha256-sentence

  • The SHA256 for this sentence begins with seven, seven, f, zero, a, b, b and five.
  • The SHA256 hash of this message begins with 534d765
  • 我去,厉害了,主要是里面没有什么随机数

https://zkbench.dev/ trade-offs and performance of different frameworks.

A survey of ZK Languages

books & websites & Podcasts..🎙📚

https://blog.lambdaclass.com/ 要经常看

https://blog.csdn.net/mutourend – 简中唯一zkp-complete博客[Grin]

三本叙事书籍 : pnp / tns / cw

https://cryptography.rs/ 这个页面很全也很漂亮,我cryptography.land也是受这里启发

刚才CJ还提到了这个网站 https://asecuritysite.com/ ,我也经常看这个,可以感到这个教授对密码学是真爱,做得非常全面且扎实


🎙 老牌密码学播客 : Security Cryptography Whatever https://securitycryptographywhatever.com/ Episode 288: Quantum Cryptography with Or Sattath

zkp info flow

https://twitter.com/i/spaces/1ynJOaMnBlOKR

郭老师推: https://twitter.com/1dot2 邹老师博: https://blog.csdn.net/mutourend 论文看我的这个: https://zk.cryptography.land/ zkmesh: https://zkmesh.substack.com/ 自定义Twitter Threads:比如我这个 https://twitter.com/i/lists/1522499398210531329 项目方的博客(这个很多,看几篇看对不对胃口,调整) Youtube:PSE/a16z/Eth Global etc podcast: https://zeroknowledge.fm/

以上所有内容(包括Twitter List)均可RSS订阅,且建议使用RSS订阅

项目程序一手信息几乎唯一的选择:Github 论文的话 https://eprint.iacr.org/ 又想起来这个重要的标准化组织(很少被提起但里面有好东西): https://zkproof.org/ 比如 https://docs.zkproof.org/presentations 沙漏老师的feed : https://github.com/PrimitivesLane/PrimitivesFeed 一个中科院数学所的学生做的密码学链接聚合网 https://link.fffmath.com/ 泛web3终极聚合器RAW: https://twitter.com/nake13/status/1526852356402745346

forum: https://ethresear.ch/ https://zkresear.ch/

Interdisciplinarity(e.g. AI)

AI:

Others (can’t archive)

当时逐字听译的一个 Brendan 的 talk: Plonky2 简介 https://mp.weixin.qq.com/s/qSWFLQPQJvWHclAvlEXEaQ

Q: 我想知道 这个 recursive 实际能跑起来的例子哪里有[Lol] A: Plonky2-based的吗?olavm肯定能实际跑起来 https://github.com/Sin7Y/olavm

Eos: Efficient Private Delegation of zkSNARK Provers

Lurk Beta 估计还得半年以上 https://github.com/lurk-lab/lurk-rs/issues/657

ABCDE ZK Hacker Camp |【Session 8: Efficient Zero-KnowledgeProofs: Theory and Practice】 https://www.youtube.com/watch?v=j2_9nwgfhEw

  • https://anoma.github.io/VampIR-Book/
  • zk 摩尔定律: zkintro
  • @Qi Zhou : 话说,我们在做zkWASM的Cannon,也计划用各种foding + aggregate的方案,感兴趣的小伙伴可以报名😄
  • risc0-nova
    • https://github.com/hero78119/risc0-nova
  • zkGeth, 看起来是把 geth 转化成risv64指令集,然后做一个zkrisc出来。
    • https://hackmd.io/@HqESr6_rQbmdCj2v03vrcQ/HyMA2pkmh
  • Towards a Nova-based ZK VM 的作者开始写的新书 :

网上有比较好的用bellperson或者bellman写 zkp的例子吗?

  • 具体一点 我觉得lurk的Poseidon 实现里有不错可以借鉴的 https://github.com/lurk-lab/neptune/blob/main/src/sponge/circuit.rs

  • 因为Nova用的是relaxed r1cs所以你也可以直接参考nova的examples比如 https://github.com/microsoft/Nova/blob/main/examples/minroot.rs , Constraint 部分都是一样的

  • 想找一些bellman 入门介绍快速上手,目前看到的最详细的就是 https://electriccoin.co/blog/bellman-zksnarks-in-rust/ 了

  • 仍然记得sinka(高老师)说学习框架最好写一个排序,当时他用的是halo2

  • Foundations of Data Availability Sampling

  • Boojum Upgrade: zkSync Era’s New High-performance Proof System for Radical Decentralization

https://slush.dev/ tendermint + Cairo vm 定制L3 as service https://0xparc.org/blog/parc-squad 这个有点意思

关于 20 年以后 zk 的综述, pan 老师有推荐吗 :

  • 20年以后形势就是社区为王,不少好东西先发hackmd了。综述当然也有不少,但都是更细分领域的了,比如 https://eprint.iacr.org/2023/671 , https://eprint.iacr.org/2023/857 等等

(发现和另一个研究群人员不overlap再发一遍)过了一遍HyperNova和Customizable constraint systems,感觉这个工作非常让人兴奋。CCS用来统一R1CS AIR PLONKish,然后用Nova IVC的方式multi-folding生成SNARK。有一种ZK界LLVM的即视感。各位老师怎么看。

zkp QA

Question Lists:

  • zkp 的前端?后端?算数化?
  • 什么是 MSM 加速?
  • P!=NP ?
  • SumCheck 没有办法转成非交互式的zk 证明吧?

zkp 的前端?后端?算数化? 我感觉应该是后端 需要@Kurt Pan 给出一个 前端 后端的定义

  • https://a16zcrypto.com/posts/article/measuring-snark-performance-frontends-backends-and-the-future/

有两种解释:

  1. 前端=算术化/后端=证明系统
  2. 前端=密码学编译器(比如多项式承诺)/后端=信息论证明系统(比如IOP)

什么是 MSM 加速?

以groth16为例,涉及7次NTT:4个INTT ,5次msm:其中1次G2的MSM 。 整体Prover消耗上,MSM应该占据了70%多吧. NTT应该占据10-20%。


P!=NP

刚郭老师说有人想听关于P vs NP问题的内容,这里刚好有一篇很新超棒的科普文章。我明天10:30可以给大家过一下这篇文章:
https://www.quantamagazine.org/complexity-theorys-50-year-journey-to-the-limits-of-knowledge-20230817/

“If you believe in hardness, then you should believe that it’s hard to prove hardness,” “why it had been so hard to prove that this seemingly hard problem about computational hardness was actually hard”

“which intuitively seem harder and thus are perhaps easier to prove hard.”

“complexity theory is itself complex” 看完了,这篇过于震撼

研究PRG,研究succinctness,研究NIZK,研究secret sharing,这些密码学研究全都反过来都跟证明P不等于NP密切相关。再反过来又可以做到直接基于P不等于NP假设(而不是存在单向函数)的密码学。

- https://www.quantamagazine.org/the-cryptographer-who-ensures-we-can-trust-our-computers-20230727/
- https://www.quantamagazine.org/a-short-guide-to-hard-problems-20180716/
- [P vs. NP and the Computational Complexity Zoo](https://www.youtube.com/watch?v=YX40hbAHx3s)
- https://www.quantamagazine.org/complexity-theorys-50-year-journey-to-the-limits-of-knowledge-20230817/


单向函数存在(P不等于NP),则存在对NP的ZKP;单向函数不存在(且P=NP),则也存在对NP的ZKP。

前半句是GMW86原话。后半句:如果P=NP,又因为对P永远存在无条件的ZKP system,所以也存在无条件的对NP的证明系统。

所以ZKP恰恰是少数无论P是否等于NP都存在的密码学协议。OTP也是。还有所有的「信息论安全密码学」。而大多数需要单向函数的密码协议,都活在Minicrypt之后。

这个才是需要展开说说的。但上次随便一讲都讲了两个半小时,还只讲了一条线,耽误大家吃饭都[Facepalm]

所以是不是可以理解为,只要有P问题的存在,就存在密码学协议
我不知道密码学协议的定义,所以没法讨论

对P问题的零知识证明系统trivial的无条件存在,这个证明系统就一个特点:完全无视prover

赶紧学lattice吧

请教大家一个问题,SumCheck 没有办法转成非交互式的zk 证明吧?

如果不强调非交互可以看这篇 https://arxiv.org/pdf/1704.02086.pdf 如果强调非交互,就要小心绕过一个结论:一个零知识的public coin的交互式协议是不能应用Fiat-Shair转换为非交互的,soundness不能保证。

  • 这句话难懂,pan老师这个有文章说明么

例子应该是 Barak01 的 non-blackbox simulation里的协议。上次Deng Yi老师讲的 就是说过早达到 zk 性质对 fs 并非好事

我的理解在 sumcheck 中,每一轮需 verifier 提供随机数给 prover,prover 计算 并发给verifier,这个计算 的过程 verifier 做不了,所以必须要交互

  • 没有让verifier做prover工作的。转非交互的思路是把verifier的工作(发随机数)让prover做(用哈希)。
  • 非交互这个词也不好,非交互不是没有交互,而是单向一轮交互
  • 在 sumcheck 的 step3 Verifier checks that the partial sum and total sum agree when the partial sum is evaluated at 0 and 1 and its outputs added。
  • 按照 https://semiotic.ai/articles/sumcheck-tutorial/ 中的描述只有verifier 验证partial sum 和total sum 的结果才能发送随机数

1 巨量资料 FYI,大部分出自网络及群聊整理,时间有限出处 没有做细致排版,请恕无法极尽详实覆盖,如需加上出处请联系作者 ~

经验心得

郭宇:ZK 技术的学习心得和经验分享

演讲:郭宇,安比实验室创始人兼 CEO

整理:Amber,Foresight News

今天我主要是跟大家分享一下关于 ZK 的学习心得和一点体会。因为在以太坊接下来的几个升级中,都包含了很多最新的 ZKP 或者是 zkSNARK 的技术。最近有很多人在学习这些知识,我们也参加了一些公学的小组,在这里深度讲一下其中很共性的问题。

Why ZKP or zkSNARK?

首先我们能感受到 ZK 的项目是很 FOMO 的,我更想从技术层面讲一下我们为什么要有 ZKP 和 zkSNARK。当然,它自身本来就是密码学里一个非常重要的分支,但我认为它在以太坊的发展过程中发现这个东西异常强大,包括 Vitalik 自身也做过一些布道的工作,所以说得到了非常长足的发展,也解决了以太坊未来的发展途径。

从最早期的 Sharding 发展到现在以 Rollup 为中心的新方向上,我认为 zkSNARK 或 ZKP 在其中起到了非常重要的作用。在我看来,首先它是「产生信任的机制」。比特币为我们带来的,像共识协、POW 协议和一些分布式算法,能够产生一些信任,但这种信任是有存在 51% 诚实的前提假设的。另一种是我在从事区块链相关研究之前,做了十几年的 formal logic 和 formal verification 的工作,本质上是通过 formal logic 保证代码的执行满足某种预期,但我认为这也是一种信任,只不过它的假设是通过逻辑系统本身的一致性去保证信任。ZKP 和 zkSNARK 是我第一眼看到的时候,就觉得和我研究了很长时间的领域不一样的信任创造方式,因为他们从另外一个角度,几乎正交地提供了另外一种创造信任的方式,就是通过交互进行概率性证明,同时提供信任。正如 Vitalik 所说,他们是正交于共识协议的另外一种非常重要的区块链核心技术。

第二点我觉得可能很少有人提到,但是我认为是非常重要的,就是可组合性。我们听到这个词大概是 19 年,以太坊还处在 DeFi 早期探索过程中的时候,大家提出来的概念。因为以太坊上的所有智能合约都部署在一个平台上,这些合约之间可以没有任何调用的障碍和壁垒,就像没有农田里没有横亘在里面的篱笆一样。正是这种可以无障碍调用的可组合性让 DeFi 有了现在这样的繁荣。虽然可组合性不是一个特定的编程语言,但它是一种更高维度的编程,是一种可编程的手段,本质上是更抽象的编程。其实在 zkSNARK 里,可组合性是肉眼可见的。且不用说最近两年 zkSNARK 技术本身的发展,当我们去看 paper 的时候,我们已经可以非常清晰地看到一个协议,是通过很多子协议的排列组合构造出来的,并且这种可组合性以肉眼可见的速度扩大,这也就意味着 zkSNARK 不再是我们想象中的某一种算法,比如 Groth16 或者 PlonK。它们本身也很复杂,拆开来看,每个算法都由一些精巧的小组件构成,这些精妙的算法之间又可以构成一个更大一点的协议,甚至比我的描述更加复杂,它不是严格的、有层级的划分,它甚至更灵活一些。

这些可组合性的发展、爆炸很有可能带来新的空间。而且这个空间能有多大?我觉得基本上可以说大到无法想象,尤其是最近一两年关于递归零知识证明技术的一些突破,带来了非常大的创新空间。而至于它是不是下一个「big thing」,我觉得冠上「之一」,可能是一个比较稳妥保守的说法,但我仍然觉得这是一个非常让人激动的领域,尤其是在 Blockchain 或 Crypto 的圈子之外,目前注意到的人还很少,因此我觉得这是非常好的机会。

说到体会,我先谈一谈我自己是如何入坑开始做 ZK 研究的。首先,我在 17 年买了很多 Zcash(ZEC),当然,从投资回报角度来看,这就是非常糟糕的一笔投资。但我觉得也正是这个举动让我深入地了解 zkSNARK,并不务正业地用业余时间读了一些东西,但我发现非常难懂。当时网上也很少这样的文章能作参考。同时当我去翻传统密码学的书时,也失望地发现几乎都没有提及到这类知识。

在一些国外的教材里面,可能在整个教科书的最后一章会花两三页讲解一下,就已经很不错了。但很幸运的是,我在 2017 年年底发现了 Dan Boneh 和 Victor Shuop 写的教材,虽然还没有完全写完,却已经出现了一部分关于 zkSNARK 的讲解。我觉得如获至宝,因为在资料很少的情况下,居然有一本书能从最简单的概念讲起,这本教材我认为是非常好的密码学入门教材。于是在 18 年的时候,我就开始慢慢读那本书,学习一些基本概念。

然后在 18 年年底到 19 年的一整年中,我们创立了安比实验室,早期我们一直在做 smart contract audit,去帮一些朋友解决智能合约的安全问题,但是我们同时还是用了大量的时间去学习基础的密码学知识。在 19 年我们开始着手做一个用于 data trading 的协议,叫做 zkPoD。20 年,我们讲之前的工作总结了一下并发表在了 CCS2021 上,也得到了一些密码学学者的帮助。而在 2021 年之后,因为整个密码学领域发展太快了,我们的很多想法跟不上整个领域的理论工具的发展速度,所以我们之前的工作就暂停了。那时候真的特别感慨,我们之前面临的很多技术困难,很快就会被人解决,甚至还有好多种解决办法,我们就不得不去看全这些解决方案,可能还没有完全理解,又有新的工具被创造出来。所以说我们这几年基本上都在不断学习,学习速度还远远赶不上创新的速度,说这是一个寒武纪大爆炸,我觉得一点都不夸张。我觉得它的威力可能不亚于 ChatGPT 对整个社会的影响,只是说现在还没有那么明显,没有特别杀手级应用出来,但我觉得这个是必然的。

另一点感想就是,在早期的时候,我们读的资料都是来源于学术界,来自于专门长期从事密码学理论研究的教授和专家学者的论文。但是从 2020 年往后,我们其实发现大量资料来源于工业界的工程师。因为在 Crypto 领域,大家迫切的想要使用密码学去解决 Blockchain 和 Crypto 中非常核心的问题,有些快速的工程应用甚至领先了理论的发展。我就举几个例子,首先是 Halo,Halo 最早是由 Zcash 团队创造出来的,然后学术界从中获得了启发发展出了 accumulation scheme,包括最新的在递归零知识证明里非常重要的技术。它最早的起点就是在工程实践中发现的一些一开始不起眼的一些小规律。

第二个就是 PlonKish Arithmetization,最早也是在工业应用领域从 Aztec 团队慢慢发展起来的,之前有很多 code,但是它真正的理论研究是后来逐步补上的。但现在为止基本上以 PlonKish Arithmetization 为主导的很多学术研究已经慢慢扩散开来了。很像我右边图上的几棵树,当然这几棵树我只是借用了 slides 中的,zkSNARK 就像一棵很大的树,并且在不断成长,于是就有了 zkEVM 和 zkVM 这样的概念。我经常感慨在 19 年,预想 zkEVM 或者 zkRollup 之类的概念,都要是 10 年之后才有可能出现的东西,因为在当时我们能感受到 zkSNARK 能做的应用是非常有限的。但是没想到这三年来,Halo 和 PlonKish 的发展能够让 zkEVM 基本上成为现实。这是非常恐怖的,包括像 Lookup Arugment 还有一些很有意思的技术,反过来对学术界都有很大的推动能力。

第三点就是这整个过程是非常愉悦的,不管里面遇到了多大的困难,或者是解决了一些小问题所带来的成就感,都让我们感到非常有趣。

我觉得绝大多数人对这个东西的印象就是特别难学的月球数学,但实际上从我们的经历来看其实也不是那样。

它其实要求的前置数学基础并不多,但它最难的部分其实是关键概念超级多,而且这些概念还在随着时间演化,也就是说你可能从别人嘴里听来了,或者在某个地方讲到的,或者是在一个文档里看到的东西,过了 3 年之后可能需要重新去审视它,它又可能表述都不一定准确,或者是它在表达另外一层意思。

第二个就是协议相对复杂,因为它属于密码学协议,可以说密码学这个分支,整个大方向上都是属于内部协议比较复杂的这类,于是它的形式化定义特别多。不同的学者去写形式化定义使用的符号体系也不尽相同,公式也是量多且复杂,即使背了也记不住,可能睡一觉醒来就忘。

第三点是,核心代码很短,但内部结构足够复杂,密码学有一个很有意思的点,就是你无法通过看代码去学习算法,因为你看他弄来弄去,都不知道他想干什么,还是需要一些理论的指引才能读懂代码。

第四点就是,很多朋友还是会按照论文上的算法去实现一遍,但能实现就证明真的懂了吗?这显然还是相去甚远的,这个我们会稍后讨论的。

零知识证明的学习曲线还是相对陡峭的,陡峭的原因并不在于 zkSNARK 或 ZKP 有多难,而是在于,首先开发工具很缺乏,也就是说想直观地体验一下并不容易,还需要做很多工作,去搜去看相关理论知识之后才能下手。第二就是学习资料其实更缺乏,现在跟 4 年前相比还是多了很多资料的,但要知道资料的增加速度远远赶不上创新的速度,也就是说有大量新的理论技术并没有被总结成文章,有的甚至都没有行文写下来,它们存在一些系统的角落里,或者说它只有代码但没有理论。因为是寒武纪大爆发,导致了学习资料相对更少,即便是需要的数学基础并不多,前置知识覆盖秒依然很广。所以有些朋友认为自己数学不好,并不是根本性问题。还有一点是论文挺难看懂的,这是我在早期学习时的感触,有些套路需要慢慢适应。

一些心得和经验

下面想给各位朋友再讲一些心得和经验。

首先给第一类想要快速入门的新手朋友,或者说看过一些非常简单的概念,无论出于哪种目的想要快速入门的朋友们。首先,有诀窍吗?诚实一点说时没有的,但确实可以把几个地方作为切入点。其一,写代码是一件很重要的事,这里的代码可以分很多种,我认为现在很多可用的 library 都是很不错的。比如 Circum,可以在网页端试着写一些电路代码。第二类的 Halo2,我认为如果想要深入学习的话,可以用 Rust 吗,Halo2 也恰好是 Rust 的一个 library,也是在社区中广泛使用的。现在以太坊基金会也在基于 Halo2 进行一些工作,可以去看一下这个 library,并根据它去写一些基础的 demo 或者例子。第三个是 Gnark,是 Consensys 开发的用 go 语言写的库。如果你对 Rust 不熟悉或者没有写过 Rust 的话,可以优先选择 Gnark,因为它是用 Golang 写的,上手更简单一些。最后,还有 zkSNARK 社区的 Cairo,虽然我自己没有写过 Cairo,但我已经看到不少人已经成功启动了,所以我认为 Cairo 的效果应该是不错的。

关于代码围绕什么去写,我认为可以基于 KZG 多样式承诺、IPA、Baby SNARK、Sumcheck 等非常简单的密码学协议,是可以做一些实现的。因为,实现了之后,可能会获得一些教科书上所不能提供的很直观的概念。比如,代码运行的速度,对稍大一点的例子电路是否能够正常使用,浏览器最大能承受的电路等等。

我觉得手动写代码这一方面,首先有助于大家理解很多概念,比如,到底什么是电路。相信不少朋友都听说过电路,就是挺难写的,如果动手写一下电路就会发现,它其实不算是在写正常的程序,而是在写一个证明过程,因为本身零知识证明就是一个证明过程,因此写电路其实就是写证明,而非写计算。写证明的过程有一些特定的套路和经验模式,这个需要慢慢熟悉和思考的。

第二个我觉得是有助于对运营效率有所感知,就是知道运行的速度能有多快,以及假如说你想去 build 一个应用,他大概能用在什么地方。比如说你在以太坊一层的合约里做一个电路的 verify,你大概需要多少 gas。事实上这些都是需要你实际操作完之后才能有的一个初步印象。

第三个就是对协议借口、协议流程有一个基础的了解和大概的感觉。

第四个就是你能对基础的数学原理有初步的认识,但不需要去学很多,不需要去重新找一本代数的书去翻,因为一本完整的教材包含太多内容了。我觉得通常情况下是没有必要的,但是可以通过 Google 或者 ChatGPT 去快速学习一下基础的数据原理。直接通过编程学习的话,感觉是完全不同的,特别是对于使用的参数、输出的结果,以及如何在代码中 encode,用什么样的数据结构,都会有非常直接的认识和理解。

这里我想要推荐 zkiap.com 的课程,它是一个注重实践的简单代码学习课程,每周花点时间就基本上可以慢慢明白是怎么一回事,推荐给所有能写代码的朋友们。

但有些朋友可能会问,假如说我不会写代码,或者说我没有时间写代码,但是我仍然想快速入门,那该怎么办呢。我认为第一点就是要准确地理解相关概念,所谓的准确理解就不是一些模模糊糊的理解,比如,我们说零知识有 zero-knowledge,那么到底什么是 knowledge?我觉得是需要搞很清楚的,这些关键概念搞得越清楚,看文章就会越顺利,包括还有像 witness 这种概念。还有像 Random Oracle 还有 Commitment 之类的概念。为了搞懂 Random Oracle 是什么,我前后可能花了一年时间去看各种东西,慢慢尝试理解,但但是的资料很少,我觉得现在的资料已经足够丰富了,已经有一些教科书能够吧这些概念讲的相当清楚了。然后还有一些概念,像 CRS、SRS 是什么,如何用 Polynomial IOP 做一些 plunk 应用。其次像 extractability 还有 preprocessing,这个适合具体应用开发相关的。我认为对这些基础概念有必要有非常精确的理解,如果不是很理解或者模糊的话,我的建议是先去看这些概念,Google 和 ChatGPT 能够在这里起到很大的作用。对于基本概念的掌握程度,我认为首先需要能看懂科普文章中的一些密码,一些黑话。

第二个是能够对 zkSNARK 有一个清晰的抽象的认识,也就是说能大概知道它是什么,能用一句话去讲一下什么是 zkSNARK,而不是只是将它描述为一个 library,或者用一个方式函数调用一下,或者说就是一段代码,真正的掌握是要比这些都再深入一层。第三个就是如果好奇的同学或者有更多时间的朋友可以去看一下学术论文,如果第一节能看懂的话,我觉得已经是相当不错了,这意味着很多密码学黑话都已经理解了。如果有一些频繁出现的黑话不理解,就可以去 Google 或者 ChatGPT 弄明白,有些实在很难懂了就可以先放一放。最后就是能初步理解 zkSNARK 的一些应用,如果能做到这一点的话,对一个不会写代码的人,已经是对 zkSNARK 已经又了相当深的认识了。

接下来我说的这一类朋友,他们不满足于一些基础概念的理解,始终对内部工作原理感到好奇,关于黑盒如何运行,为什么能 work,为什么能提供信任,他的信任又基于哪些东西,应用条件是什么,等等。我这边的建议是,需要先充分理解一个算法代码,就是先把一个算法吃透,其实非常重要。因为在算法之间就好比再学一个编程语言一样,如果 Java 用得很熟练了,再去切换到其他语言,其实并没有很复杂。前提是,这个语言需要用得特别熟练,如果只是浅浅地能写一些 Hello World,可能差距还比较大。我觉得可以先针对一个算法,比如说我觉得像 PlonK 是首选的,因为它比 Groth16 简单很多。PlonK 是我认为我见过的所有算法里最简单的,虽然 Groth16 的科普文章更多,但事实上它是一个复杂难懂的算法。然后还可以选择 zkSTARK,Marlin,Spartan,他们的对看论文的要求可能会更高一些。总之,我推荐先学 PlonK。

第二个就是去阅读一些 Github 上的开源代码,现在有非常非常多的有意思的项目,几乎每两三天都能发现一个很不错的开源项目,并且代码质量不错,我在这里就不给大家推荐了。

第三个点就是如果你想了解内部工作原理的话,你不可避免地要去理解这些公式,但对于初学者的最大问题是,公式无法理解或者并不能记住。我认为这里的核心窍门就是,多抄公式。不停地重复抄公式,同时再思考,在这个过程中就会有神奇的力量,能让人不知不觉中就明白了一些东西。

在这里推荐一个 MOOC,叫做 zk-learning.org,他们的课程深度不错,也会涉及到内部的工作原理,也会用非常简单的语言去抽象地解释一个或者一类算法是如何工作的。我认为如果能看懂并实现一个最简版本的代码并且能看懂 70% 的相关论文,且能不需要任何辅助地手推公式,并能够大致理解 Soundness 思路,我觉得已经达到了理解 zkSNARK 的要求了,但在这个阶段还是不要自行修改和优化协议,这是非常危险的动作,除非有非常多的经验,否则千万不要自行修改优化协议,因为密码学协议中有非常多没有写出来的东西。相关的反面教材有很多,我觉得最著名的就是 BCTV14,它是 14 年的一篇论文,他们的论文在做了一次修改优化之后出了一个 bug,然后修了 bug 之后又是一个 bug,而且都是非常严重的 bug,也就是说这些顶尖的密码学家都会犯这样的问题,那更不要说一个初学者。对初学者而言,你可以自己去设计一个协议,但是你一旦去修改别人的时候,你还要确保对这个东西有足够清晰的理解。

接下来对于更高阶的朋友,可能你是在 build 自己的项目,想用 ZK 去解决一些必要性的问题,需要自己设计协议。首先,我觉得你需要反反复复地读大量的论文。第二个就是需要补最基础的计算理论,而不是代数或者射影几何,或是那些数学概念。我觉得这里面其实最重要的是计算理论,我在这里推荐两本书,一本是《Computational Complexity: A Modern Approach》,另一本是《Computational Complexity: A Conceptual Perspective by Oded Goldreich》。第三点就是要跳出兔子洞,关注基本问题。因为有些算法还是很深、很烧脑的,有时候需要在一个非常小的细节卡上好几个月。但我觉得这里面的核心点是为了理解它为什么 work,有些时候你要跳到最外面来问一些最简单、最直接也最基础的问题,再去反思这个细节。就是你需要不断进到一个非常小的细节,同时又要不断地跳到外面去反复对比思考,这样就很容易弄懂这一点细枝末节的东西。第四个就是逆向思考,因为很多协议的设计是非常精巧的,有时候需要我们去理解而不是简单地 follow。但事实上理解又是一件很困难的事,多数情况下我在理解算法的过程中所用到的方法论是逆向思考,也就是说如果将原来的算法换成另外一种,又会产生怎样的效果。大多数情况下,它是不 work 的,那么它为什么不 work?这是很重要的。

最后,给大家说一下读论文的心得,因为论文确实比较难读,都是些非常学术的文章。第一,请务必反复仔细阅读论文第一节,因为论文第一节通常是论文最精华的部分,他通常描述了这篇论文最主要的贡献,包括他和前人的不同以及他的新协议或者新算法最重要的特点。通过读很多论文的第一节,能够快速找到一个知识网络。

第二点就是,你需要把论文的参考文献这几页,特地打出来,有助于形成一个知识网络,就可以沿着参考文献,找到更多的资料,而不单纯是通过 Google 或者 ChatGPT,而是通过这个论文所依赖的前置论文,形成一套理解体系。

第三点是要从历史的角度看概念的变迁、算法的演化和技巧的组合,这里面能发现非常非常有意思的故事。我觉得也可以找机会和大家分享一下,但这需要很多时间去准备。虽然密码学已经历经了 40 年的发展,但是从历史的角度看,它经过了一个非常快速的发展与变迁的过程。

第四点是,一篇论文需要反复看,大概看懂其中的 90%,需要好几个星期甚至好几个月。我最开始去学习这个东西的时候,一开始非常难受,非常有挫败感的一点是有篇论文我看了半年都还看不懂。但突然有一天,我看到一个密码学家他的分享里面说他看论文也是要几个星期才能看完,这时候我才如释重负,我才觉得原来他们也是这样。所以说一篇论文看不懂其实是非常正常的一件事。

第五个是,我认为参与社区讨论特别有用。

最后一点,找一个 Crypto 领域的问题点,找一个你觉得有意义的工作,然后 get hands dirty,这个能驱动你把关注点集中到一些点上面,更容易做出一些突破性的工作成绩。

对话姚翔:解读零知识证明的前世今生

https://daorayaki.org/post/63ec77b7d8c1277f21f6a8b1

作为互联网世界的最小单元,人们为了获得互联网所带来的便利,在参与线上的生活,工作,交易的过程中,不得不牺牲自己的隐私数据。例如在网站上输入信用卡号,向房屋中介提供自己的银行信用记录,向银行提供身份信息等,同时这也带来了各种各样的风险,例如身份信息的盗用,电子邮件泄露,数据泄露和其他形式的欺诈等。

而零知识证明的提出,正是为了解决这样的困境。它所具有的一系列先天优势,使其成为了各种区块链扩容和隐私解决方案的重要组成部分,从而让人们在不泄露任何自己隐私信息的情况下进行自身权益的认证。

鉴于零知识证明对于整个行业的未来发展起到至关重要的作用,DAOrayaki特别邀请了“原语里弄”的发起人姚翔老师,从密码学,隐私计算学实践者的角度,一起聊一聊如何看待并理解零知识证明。全文如下文所示。喜欢听音频的朋友,可点击播客:https://www.ximalaya.com/sound/560839397

Shaun:Hello,大家好,欢迎来到本期的Yaki叨叨!我是主持人Shaun。零知识证明作为计算机科学领域在近50年间最引人注目的创新之一,与EVM技术成为了当下web2向web3演化的两个重要入口,它提供了一种方便且实用的验证方法,使得web3的链外数据能够方便的取得链上验证,获得web3生态的信任,为数据资产的互通提供了可能。

作为互联网世界的最小单元,人们为了获得互联网所带来的便利,在参与线上的生活,工作,交易的过程中,不得不牺牲自己的隐私数据。例如在网站上输入信用卡号,向房屋中介提供自己的银行信用记录,向银行提供身份信息等,同时这也带来了各种各样的风险,例如身份信息的盗用,电子邮件泄露,数据泄露和其他形式的欺诈等。

而零知识证明的提出,正是为了解决这样的困境。它所具有的一系列先天优势,使其成为了各种区块链扩容和隐私解决方案的重要组成部分,从而让人们在不泄露任何自己隐私信息的情况下进行自身权益的认证。

鉴于零知识证明对于整个行业的未来发展起到至关重要的作用,Daorayaki社区认为有必要也有义务以行业资深研究者,观察者,分析者的角色向更广泛的受众群体普及零知识证明的相关知识。为此今天我们特别邀请了“原语里弄”的发起人姚翔老师,从密码学,隐私计算学实践者的角度,一起聊一聊如何看待并理解零知识证明,欢迎姚翔老师。

姚翔:大家好,我是姚翔,也是“原语里弄”的发起人。现在主要从事区块链包括密码学的相关研究,同时我们也举办一些线下的研讨活动,支持一些相关课题的研究,给予研究人员各种的帮助。

在启动“原语里弄”之前,我在金融机构工作了几年,然后在一家区块链初创公司开发智能合约钱包,我在Crypto领域大概也有七八年的时间了,我本人是密码学专业背景,虽然在工作后并没有从事太多的密码学研究,但是相关知识还有一些,所以应Shaun的邀请,来聊一聊零知识相关,我觉得也有必要做一些基础知识的普及,今天很高兴有机会和大家交流。

Shaun:谢谢姚翔老师,谦虚了。在深入展开零知识证明的探讨之前,我觉得追根溯源是难免的,所以我们一定会谈到那个不能“免俗”的问题:就是零知识证明的诞生背景是怎样的?或者说它的起源的故事是怎样的呢?有请姚翔老师。

姚翔:首先,零知识证明,并不是为了解决区块链的某个问题而提出的,但在近几年的发展过程当中,随着区块链的需求增加,随着这个行业吸引了更多的注意力,创造了更多的价值,零知识证明的发展速度,在近几年的时间里确实变快了。

但零知识证明的历史可以追溯到很远,如果以这个词的诞生或者这个概念的明确提出,来作为时间节点计算的话,至少也是在1985年的事情(将近40年),当时相关的论文提到了什么内容我会在后续说到。

我更想先从一个更贴近生活的角度,和大家去分享零知识证明的概念。相信大家看到零知识证明这个词,可能会在搜索引擎里面去搜索,搜索结果一般会给你一些案例,比如阿里巴巴坐到一个山洞里,他知道这里面能不能开门;再比如说三染色问题;如果用英文搜索的话,会有一个案例叫where’s waldo(寻找瓦尔多)。

但我们脱离干巴巴的教学,从生活经验出发。相信大家应该都参加过数学考试,数学考试都有证明题,证明题是什么呢,提供条件ABC,证明x=y或者其他结论。我们回想一下写下证明的这个过程,根据已知条件进行一些计算和推演,利用一些定理或公理写完这个证明,从考试的角度来说,证明的过程极其重要,这是判卷人给分的重要评判标准。那这里有一个很有趣的点在于,实际上判卷人不需要知道标准答案是什么(也就是不需要知道任何额外知识),因为根据提供的推演过程和题目中的已知条件,就可以验证你是否知道证明过程。

在这个例子中,存在两个角色,一个是证明者(Prover),也就是答卷人;还有一个就是验证者(Verifier),也就是判卷人。其次呢,证明者拥有某种知识,在这个例子中知识=证明过程,也就是答题内容。而验证者是不需要拥有这个知识的,看到证明过程随后就可以做出判断。

还有一点,证明的过程中,证明者和验证者没有对话,证明者写完证明,他就把卷子交上去了,而验证者看到这个证明,也不能再提什么问题(证明者也无法再回答任何问题),这在现今的语境下理解,就叫非交互。

还有一个很重要的问题,就是零知识证明中零知识的定义,在考试这个例子中,如果验证者(判卷人)之前不知道如何证明,但在读完一个人的完整证明过程(正确的前提下)之后,就知道(不需要去理解)如何证明了。或者说,在考试过程中,我看到前面人的证明过程,我发现是对的,不需要理解他是如何写出证明的(为什么这么证明),只需把它抄下来就可以。换言之,验证者在阅读证明过程之后,获得了额外的知识(即:一开始不会,在读完证明之后就会了)。

我想还有一个比较重要,但又有一些隐藏在里面的一个条件,就是这道题是有一点点难度的,它不是一个非常简单的题。因为如果这道题很简单,已知a=1,b=2,c=3,证明a加b等于c,那我们前面提到的,这种抄袭证明过程(或者说验证者获得额外信息)就不成立,因为这道题太简单了,验证者不需要去阅读证明者的答案,就知道如何证明。

那综上所述,我们提出了两个问题。第一个问题是:什么样的问题需要去证明;第二个是:有没有可能构造出一种证明,让验证者(判卷人)知道证明者(答卷人)有实际能力推演出证明过程,但验证者在过程中,不能学到如何去证明(或学习到如何伪造一个证明过程)。也就是当答卷人是证明者1,改卷人是验证者1,验证者1可以确认证明者1知道证明过程x,但是验证者1在验证后不能构造出一个证明(即证明过程伪x,甚至是证明过程x),让验证者2相信验证者1(此时他是证明者2)知道如何来证明。或许这个例子比较宽泛,不是非常精确,只是用一个更贴近生活的例子,尝试将大家带入这个语境。

而刚刚提到,在1985年(将近40年前),有三位学者,Goldwasser,Micali还有Rackoff发表了一篇论文,名字叫《交互式证明系统的知识复杂性》,标题中没有提到零知识证明,但在文章中有它的定义。文章中他们定义了一种交互式的证明系统,和刚才的例子有一个很大的区别在于,证明者和验证者之间,不是一次性的(证明者将东西交给验证者,随后验证者就做出判断)。而是有多轮的交互(证明者给验证者一个东西,随后验证者可以提问,然后证明者再给验证者一个东西,验证过程是多轮的),而同时这个证明系统允许有一些极小的误差,即证明者可能不知道这个题怎么证,有小概率可以欺骗验证者。文章中还提到了密码学上的一个常用的工具,叫不可区分性,来定义了零知识。

同时,我们刚才也提到:什么样的问题才需要去证明。这篇文章讨论的证明系统,针对的是NP问题,NP问题简单来说:是指多项式时间内,验证这个解是不是正确的问题。而现在人们普遍认为,P问题(多项式时间内可以解决的问题),和NP问题是否相等,这件事情仍然是不确定的。但我们仍倾向于P是不等于NP的,也就是说:存在一些问题,求解它是困难的,但是验证解的正确性是容易的。

所以我们就针对类似问题构造这种证明,可以理解成它存在一些不确定性:对于验证者来说,在多项式时间内,可以比较简单的验证它,但没有办法在多项式时间内找出它的解(即验证者不知道这个知识),这就是这篇文章中的一些核心观点。凭借这篇文章Goldwasser和Micali,包括第三作者Rackoff,获得了1993年的哥德尔奖,Goldwasser和Micali还获得了2012年的图灵奖。

一个题外话是,Goldwasser是一个以色列人,但她这个姓氏很有意思,这个词的词根意思是“金子的水”,也是一个很著名的金箔酒品牌,它诞生于现今波兰的格丹恩斯克,历史上也叫“但泽自由市”。如果去了解这个城市的历史,我觉得它和今天所描述的Crypto精神,也是有很多交织的部分。

所以正如刚刚提到的,零知识证明并不是为了解决区块链的问题提出的,它发生的很早,但是在近些年的过程中,确实是因为crypto世界的大量的需求推动了它的加速发展。

Shaun:谢谢姚翔老师的分享,前面我们聊到了零知识证明的起源,然后聊到了Crypto相关 ,那您作为这个行业的实践者之一,从您的角度出发,您觉得今天的零知识证明都主要应用在了Crypto里的哪些现实场景中呢?或者说它的出现为现今Crypto行业改变或带来的哪些东西呢?

姚翔:这个问题很好,大概十年前我在读书时,零知识证明在课上提到的时候,也只是一个比较简单的理论描述,看不到太多的实际应用,甚至当时的老师也觉得这个东西好像看上去挺有用,但是不知道怎么有用。

而随着Crypto技术的发展,零知识证明逐渐找到了自己的定位,有意思在于零知识证明和区块链(或者说分布式账本技术),其实是高度契合的。因为区块链解决的问题是:分布式网络对一个状态的共识,但关于状态的变化以及状态内容的保护做的还有欠缺。

现在的区块链,是由计算驱动的状态机,用以太坊为例,从S到S1’,它的变化是:知道S的状态,然后收到一笔Transaction(交易),计算后得到新的S1’,全网络的所有节点都要做这样的计算。而零知识证明在第一点上,可以将区块链由计算驱动的状态机,变成由验证驱动的状态机,也就是状态机从S到S1’的状态变化,不再需要全节点知道。其中的核心关系是计算,不需要知道计算细节,但是知道计算是正确的。当然这也提供了一种扩容思路,随后会再展开讲。

第二就是原本的状态机不保护隐私,比如说a给b转了一笔钱,首先要知道a和b各自有多少钱,节点才能对状态进行操作,但这在现实生活中是反直觉的(会觉得泄露隐私)。所以,如果可以让节点在不知道具体状态的情况下,执行并且验证状态变化的有效性,隐私就会得到很大程度的保护。但是到底具体是什么,可以一会再谈。

所以综合来讲,对于Crypto领域来说,零知识证明是一个非常好的工具;从计算哲学上来说,也是一种提高可扩展性的方法;从隐私保护的角度上说,它也具有一定普遍性,对于任何的数据,只需要付出一定的成本,你的隐私就可以得到比较好的保护,所以我认为零知识证明和Crypto是一个相互促进,相互成长的一个过程。

Shaun:我们聊到了隐私,聊到了扩容,正好到了您的专业领域,我们就接着谈一谈。纵观目前整个互联网的发展历程,(在区块链技术诞生之前)隐私就一直是互联网领域常讨论的话题。因为人们为了获得互联网带来各种便利,会以牺牲隐私为代价,在这其中,就会出现很多关于隐私暴露所带来的各种不便,零知识证明的出现,在细节上如何解决这种隐私相关问题呢?还有一个问题就是,我们如果再聊到区块链,一定会聊到一个话题,就是扩容(ZK-Rollup),尤其是现今结合了零知识证明的扩容技术,对于区块链的发展,它的重要性体现在哪?

姚翔:提到隐私问题的重要性,如果从负面角度,也就是一旦发生相关风险,它的危害程度是不言而喻的,所以我觉得也不必再去展开。大家在现实生活中,其实可以体会到个人数据被收集所带来的一些不便和风险。

而这件事情需要从根本上去解决,原因是什么呢?比如说,你要去酒吧,酒吧老板希望你不是一个未成年,他需要你提供证件,通过身份证上的出生年份计算你是否满18岁,同时他还要验证你提供的证件是不是本人的证件。但同时他也获得了一些额外信息,他知道了你叫什么,知道了你的住址,知道了你的身份证号,而这些信息其实他是没必要知道的(即信息泄露)。

那么如何优化这个系统呢,我们假设有一个机器,这个机器可以读取身份证(假设这个机器是可信的),把这个身份证放在上面,同时有人脸识别,你只需要站在那,然后机器会告诉酒吧老板你的条件符合要求,甚至可以更高级,扫描完符合条件就可以直接通过。这样的好处是,在可以信任这台机器的前提下,酒吧老板不会获得任何额外的信息(除非你主动告诉他),这个好处是显著的,通过采用一些算法或者新技术,可以对隐私进行更好的保护。

但在区块链上,隐私这个问题会被放大,实施起来会更困难。第一点原因是,数据是所有人都可以访问的,原本的数据只有老板知道,在老板不泄露的前提下,你的数据还是比较安全的,但在区块链上所有的数据都是可以公开访问的。第二点是,数据是永远可以访问的,如果把它当做一个持久化账本,所以那么数据公开带来的风险就更加持久。就像你不会把身份证号码传到以太坊上,即使它许诺验证后可以提供一个更低的贷款利率,你还是不会做的。所以如果不能解决隐私的问题,区块链所能支撑的应用类型,要比传统世界更加有限,和现实世界是隔绝的,如果你要把这些现实的数据放到链上,势必会向整个网络去广播,大家都会知道这个数据。

那么在采用了零知识证明技术之后,它的解决思路就在于刚描述的证明系统:我不会告诉你原来的数据,不会让你获得额外的知识(可能包含一些签名,比如身份证信息,需要有一个权威对这个数据来源的合法性进行验证)。通过构造一个算法,这个算法让区块链去验证里面的数据,是否满足某些要求,通过之后,就可以执行下一步指令,同时也不泄露任何之前的信息。

但我觉得对于大多数朋友来说,其实不用关心它到底是如何实现的,只需要知道有这个特性:数据构造出的解题过程可以让这个区块链上的智能合约或者程序,相信证明者满足某些特征,但又不告诉验证者这个数据是什么,甚至一点数据都没有泄露。这是关于隐私的一个解决方案,当然这里又有存在很多问题……

Shaun:据我所知,它可能会有一个非常中心化的节点对吗?

姚翔:对的,这个问题我觉得也需要去做辨析。首先要说的是,大多数的ZK-Rollup不是ZK的(不是零知识的),这个名字是约定俗成而来的,也就成为了一个大家的共识,但为什么不是零知识的,因为在提高扩容效率的过程中,在算法的设计上做了不少的妥协。

我们首先解释ZK-Rollup的定义,然后回到那个中心化的问题。还用以太坊为例,状态机从S到S1’的过程,是由一个Transaction(交易)驱动的。现在全节点运行的世界状态是S,然后执行一笔交易变成了S1‘,再来个交易再执行。这意味着,第一点,每个节点都要保存完整的世界状态(知道区块链上的所有的事实),才可以去做执行,因为如果把a的钱转给b,而a有多少钱不知道,是无法执行交易的,所以要保存所有的状态, 这就会带来一个问题,状态的数据越来越多,同时又希望这个网络是无许可的。如果状态数据以T为计算单位(虽然现在没有那么大,一般是几百G的级别),那么个人承担机器运行的成本就很巨大,所以我们一般希望每一个以太坊节点是一个家用机器,或至少不要过于昂贵。

第二个问题是,执行过程是串行的,先执行一才能执行二,没有办法并行,因为交易的执行顺序是确定的,每笔交易之间不是完全独立的事件,所以没有办法进行并行的优化。而ZK-Rollup的思路就是,不需要再计算,我只需要提供一个证明,证明中包含的数据是:S到S1’这件事一定是对的,然后区块链其他什么也不用做,只需要把S变成S1‘就可以。

这有两个好处,一个是:全节点不需要保存所有的状态数据(这是从理想状态来说,因为证明过程附带数据是难免的);第二个是:证明过程,也就是Proof的生成,可以用定型的方法,或者通过硬件和软件的优化来加速,而在ZK-Rollup里面,大部分通过sequence(序列)进行证明的生成,它可以把要求提的很高,原因是在这套系统里大家认为,它不会影响到系统的准入门槛,运行全节点要做的是验证状态变化的有效性(或正确性),但不需要去完整的执行整个过程。而我们也知道,验证和证明是不对称的,所以机器可能对计算的要求很高,但是验证仍可以由一个普通的节点来运行。

所以这也是很多人担忧的(或者说对这种扩容方案的一种攻击),它会带来一些其他的风险,比如节点可能会有单点故障,一个节点生成证明时掉线了怎么办,还有可能会遭受一些额外的攻击,比如它来做MEV(Miner extractable value-矿工可提取价值),或者对交易的审查,我们假设生成证明的节点,要求很高,全世界只有几台电脑可以做,但如果生产机器的人因为个人因素拒绝对某个人(或某些人)打包交易,这种情况是很难解决的。

所以从这个角度来说,这种中心化或集中化,更多是在一个是交易的审查中体现;第二个是通过交易的排序来崛取一些利益。而对一般意义上理解的安全性的影响可能并不是很大,但是它的活性可能是受到影响的,因为如果只有一个节点,但掉线了怎么办,这个是扩容角度上大家的一些担忧。

但是在隐私这个角度上,这件事可能不会成立,因为对于一个终端用户来说,一般不会把个人数据交给第三方做证明的生成。因为生成证明的过程需要原始信息,就像前面提到的,大家不会直接把身份证号码放到区块链上,但如果把身份证号码给第三方来生成证明,这里的风险仍然是存在的。所以最理想的解决方案,或许是用户在自己的电脑上或手机上,通过软件在本地生成证明,但是就现阶段来说,这件事情难度还是比较大的,但对于行业来说这是一个必须要解决的问题。

Shaun:明白了,我们聊回刚才被打断的话题,那您觉得今天在解决隐私,比如说匿名问题或者假名问题的时候,零知识证明会有哪些不足,或者解决不了的地方?

姚翔:我觉得从大的方向上来说,我们对零知识证明所能带来的Benefit(效益),都是非常期待的,但也还会有一些现实的难题,一部分可能是技术层面的,当然技术层面上,大家会觉得未来某个时间可以解决,抱一个乐观的态度。比如说证明的生成时间太长,或者是成本开销比较大,或者说针对一些特定的算法可以优化,但是针对一般化的算法可能效率就没有那么高,或者说如果在以太坊上做的话,消耗的Gas费会很高。

Shaun:这些都属于技术问题?

姚翔:对的,属于技术优化层面。虽然随着技术优化,有些事情是可以解决的,但还有一些问题,不确定性就会很大。随着这个算法的发展,它的效率也在不断的提升,但提升到什么时候,它有可能会变得平缓,这件事情是没有办法判断的。

其次从隐私保护的角度来说,它不只是一个单纯的技术问题,即使你对用户的数据进行了更好的保护,使用了再好的技术手段,最后也有可能是无用功,因为会有一个木桶效应:你建了一个非常坚固的房子,你用各种算法,设置成铜墙铁壁,但是你出门的时候,有个窗户忘记关了,那所有的防护都没有用。所以我觉得这是一个系统上的问题,大家可能意识到零知识证明或各种算法技术可以帮助大家解决隐私的一些问题,但更重要的是提高大家对这件事情的认知,知道隐私这件事情很重要,再去采用某种方法或工具进行隐私保护。只有这样才能发挥技术带给我们的好处,否则,如果你觉得只有技术就够了,然后有了一丝懈怠,那么保护效果可能不尽人意。

Shaun:我在翻阅大量的零知识证明的相关资料时,总会遇到两个跳不过的词,一个是ZK-STARKs,一个是ZK-SNARKs,所以要请教一下姚翔老师,简单来说,这两个词的区别是怎么样的呢?

姚翔:我们从它的名字上来看,ZK就是Zero Knowledge(零知识的),SNARK的S就是简洁的,N就是刚才说的非交互,ARKs是一种知识论证,连起来就是简洁的非交互知识论证,知识论证是这个证明系统的名称。我们可以这么理解,非交互体现的是刚才提到的,证明者提供了一个证明,然后验证者可以直接来检查,不需要再问额外的问题(事实上,在区块链上也没有办法构造额外提问的流程,这个事情会很复杂),所以核心词落在了简洁上,而简洁就是在说证明的长度,我们可以想象一下,考试的时候,老师让你证明a大于b或c等于d,然后你写了100页纸交上去,我觉得任何老师都不会去改这个答案,虽然你可能写的是对的,但是他没有时间来进行检查,所以我们希望这个证明的长度是比较短的,而且从可用性的角度来说,假如在区块链上,给以太坊上传一个证明,是需要支付Gas费的,所以太长显然是不可行的。所以SNARKs描述的这一类型的零知识证明。

那STARKs讲的什么呢,S是Scalable(可拓展的),T是Transparent(透明的),ARKs是知识论证。我们可以认为STARKs的证明长度更长一些,Scalable是一个描述性词汇,而它的优势体现在:Transparent。因为SNARKs需要Trusted Set Up的过程,即受信任的初始化,也就是系统最开始,需要一个秘密数(不能公开,否则系统安全性崩塌),所以SNARKs一开始,一般会有一个Ceremony(仪式),会请很多人,每人进行一个秘密的输入,通过某种算法混合起来,如果有1000人参加,那1000人里只要有1个人没参与各种形式的共谋,系统就是安全的。最近也可以看到像以太坊要做KZG Ceremony;像以前还会带上放射性元素,在直升机上利用放射性元素,来提供随机性。但在STARKs里,它认为这一套程序过于繁琐且大费周折,不需要有这种过程,整个系统会更安全。

但其实刚才你也提到,这两个词可能是绕不过的,但我认为对于大多数普通读者或使用者,没必要细究其背后的工作原理,因为可能还会看到一些词,比如Prover time或是Verifier time这些参数,只需要知道这个参数的含义就可以了,不需要去关心算法的具体细节。如果你是真的想学这个算法,那从头到尾把一个论文看完,或经过一段时间的学习也就知道是怎么回事了,但到那时可能两者之间的区别也就没那么重要了。

Shaun:那接下来我们聊个轻松一点的话题,除了Crypto领域,今天的零知识证明在其他的场景也会有应用吗?

姚翔:嗯,比如说呢?你有知道什么应用吗?

Shaun:我看到大概的,比如说云计算,线上投票,或者金融类应用。比如说像以太坊现今的发展,因为隐私各方面的问题,也使DeFi的发展受限,可能在未来它可能会跟扩容相结合去解决类似问题。所以我就在思考,比如对于现实中的金融(或线下金融),它会不会有帮助呢?

姚翔:从算法的角度(或者说理论角度),帮助一定是有的,但实现这套系统的成本也会比较高,而且这里面可能还涉及到,类似监管方面的问题。因为我曾经也在相关机构工作过,我觉得这件事情属于听上去很好,可能企业去做一个创新实验(或者说搭一个创新系统),也是可行的;但在实际业务中,是非常难实现的,所以它一般不会投入很大的精力。当然在云厂商中它会提到一些问题,所谓的秘密计算,我举个例子,你把你的数据放在云服务端,你会希望把这个数据进行加密,但加密以后又会出现一个问题:你要想查询,或者说想做一些运算的时候,就会变得比较困难。我举个简单的例子,你把公司的数据全部加密放在了阿里云上,阿里是看不到你公司员工的年龄或收入的数据的,但当你想求一下公司的相关平均数据是多少,你是没办法在云端计算的,得把数据全部又降落到本地,再进行计算,那这时云计算的意义就很有限。

所以一般会采用类似叫同态加密,或者是多方计算,将经过加密的计算结果解密就可以了,不需要在本地执行计算。但如果是做一个AI模型的训练,本地机器是支持不了的,肯定需要借助云端的运算能力。

当然这些我觉得都还在探索过程中,我觉得理论上,从工程上,这都是可以去探索的,但是企业其实不太会用这种技术手段来解决这些要求,它更多的是通过合规性方法或者是法律角度来进行系统保障。反而我认为在区块链当中,这件事情会发展更快,因为现在的银行系统都是有牌照的,这也是人们相信它的原因,但在区块链中,谁在运行开放节点或谁在开发DeFi协议这些你有可能是都不知道的,所以你使用它也是因为它的代码通过了审计,有很多人使用,系统运行了很长时间等等原因。

所以我自己认为刚刚提到的那些场景,金融或者云计算或者线上投票等等,我记得当年我们还研究过线上投票相关,就是每个人投票,然后怎么求出这个结果,但又不能暴露投票归属,还不能让一个人向别人证明他投了什么,以此防止贿选,比如说我投了a,然后我就可以拿着我投他的证据,来找他索贿。这种系统我觉得设计的都会很复杂,也想的很好,但在实际过程中实际上会简单很多,因为它是一个受监管的业务,它就不会在技术上有什么动力,因为监管说你是符合要求的,我也不会再对你提出什么额外的需求。

Shaun:明白了,因为您是密码学的科班出身,并且这些年一直专注在区块链底层技术,隐私计算技术等方面的建设,那从您的技术背景,密码学背景和区块链背景以及这么多年在前沿领域的实践,您怎么看待零知识证明,这项技术未来的发展呢?

姚翔:还是回到刚才提到的,我在上学时的那个感受,就那个时候你会发现,零知识证明你不知道有什么用,然后呢…..

Shaun:说到这个话题,我打断一下,当时老师讲课讲的是大家都觉得必须得讲,但却用不上的知识,结果发现,好像有一天离自己很近啊,这种感觉很奇妙,不得不感慨一下哈哈哈。

姚翔:是的是的,我们那个时候还学了很多,比如说盲签名,代理重加密,还有数字水印,在当时这些技术,都没有很直接的应用。但我举一个数字水印的例子,我上学的时候,数字水印最多应该就是实验用途,不会有太多商业用途,但我印象当中,有个电影叫画皮2,有一个电影院把母带打上数字水印,后面市场上流出那个版本,通过数字水印就能知道是谁泄露出来的,当时我看到那个新闻的时候,我还挺感慨的,会发现学到的东西在真实的世界产生价值。

那回到现在,密码学是一个很非常广阔的领地,比如说对称加密,非对称加密,数字签名,单项函数,它们每一个都可以分的很细。但零知识证明我觉得又比较特殊,因为它得到应用的时间,是比较短的,也就是未来仍可以有大量的新智慧投入到里面。在过去的几年里面,它的发展是很快的,我觉得这个可能有两个原因,一个是作为密码学家来说,自己的成果得到广泛应用,他肯定很开心,所以也会激励更多的人参与进来;第二个就是也要感谢,包括以太坊基金会,Zcash在内的各种Crypto项目的支持,它们会愿意为这样的研究提供经费上的支持,这些学者就不需要完全依赖学校的课题了(课题可能也提供不了那么多经费),他们通过做这些产业间的研究,一方面成果可以得到应用,另一方面生活或收入也得到了保障,我觉得这两个方面都推动了快速发展。

但至于在未来会变成什么样,我觉得很难预测,因为从非对称加密出现到如今比较普遍的应用,也就是30多年时间,RSA是1979年提出来的,这时间是不长的。同时也很难想象没有RSA或没有非对称密码学的网络世界是什么样子,它们已经遍布在现今的网络世界,例如账户系统,对网站的这种安全保护,数据的隐私保护等等方面,今天的互联网就非常依赖这些密码学的基础设施。

所以如果什么时候我们不会过多的讨论零知识证明,也就是它已经渗透到了我们的日常生活中,或者说只有一些研究的人员去讨论,它变成一个日常的,大家觉得好像是有这么回事。等那一天到来,也就说明这个技术可能达到了我们现在想要的一个阶段。

Shaun:好的,谢谢姚翔老师。Daorayaki社区一直保持着对web3领域前沿技术的持续且深入的跟踪式研究和报导,在内容选题方面,社区评审委员会所有成员一方面会继承24小时不间断的寻找行业内最新最前沿的技术发生,另一方面,也会对行业内最顶尖的前沿技术团队进行跟踪式的持续性的深入报道,这样做的目的是为了兼顾社区在内容方面的广度和宽度。

我们在研究中发现目前零知识证明在整个Web3领域,普及化的程度还很低,尤其是在中文社区,还没有一个成型的专业性的普及ZKP相关知识的社区,鉴于零知识证明这项技术可能对行业未来所产生的至关重要的作用,Daorayaki社区觉得有义务跟责任让更多的小伙伴了解熟悉甚至参与到零知识证明未来的建设中,结合Yaki叨叨,DeMedia,Podcast平台,由浅入深,多角度全方位的介绍,探讨与分析零知识证明的相关内容,而本期的Yaki叨叨作为零知识证明专题报导的第一期,只是拉开了整个篇章的序幕,更多关于零知识证明的精彩内容,敬请大家关注,感谢姚翔老师的倾情分享,让社区的小伙伴受益良多,谢谢姚翔老师。

同时也感谢Daorayaki社区,今天相关讨论的音频晚些时候会上传在 DAOrayaki官网,DAOrayaki.org的专属播客频道里。我们也会同时发布在国内的小宇宙播客平台,全球各大流媒体平台,例如苹果播客、谷歌播客、YouTube以及Spotify上,欢迎大家收听及订阅。

再次谢谢姚翔老师,我们期待下期还能跟小伙伴一起分享,我们下期不见不散。

致谢

本文的编译工作获得 DAOrayaki 的支持和资助。

邓燚:零知识,模拟与归约(一个个人化的视角)

ZKP Space

聊聊 ZKP Coprocessor

Author: Harry

Label: ZKP, ZKP Coprocessor, ZKP Space

Date: 2023-10-15

回放链接: https://twitter.com/i/spaces/1rmxPMaezNnKN?s=20

主聊人:

Harry:什么是 ZKP coprocessor?

Dong

  • 是区块链的 coprocessor
  • 能让智能合约有 dune analytics 的能力
  • 现在合约面临的问题是合约不能看到: - 历史数据 - 没有 public viewable 的数据 - 其它链的数据
  • example:为什么要有 ZKP coprocessor
    • dex 需要有 trading volume based filter 的事情
      • 没有基于交易量的一些功能
        • 比如打折等
      • 而这些功能对于 dex 很重要
        • 手续费打折可以吸引更多的流动性
        • 更多的流动性可以产生更有效的市场价格
    • 为什么 dex 没有呢
      • 因为难
      • 但 cex 实际是很简单的
      • dex 两种方式做
        • 记住每个 trade action
          • 在 uniswap 试验过,每个 trading fee 都贵三倍
        • on the fly
          • 每个 trade 加 counter,做 on the fly 的功能
          • 问题:
            • 给运营的功能加到核心业务里,这是一种 overhead,增加 trader 的成本
      • 所以没有任何的 dex 做这件事
    • 其实理论上,用区块链本身也能做
      • 通过将 block header 存储到一个 Merkle Tree 上面
      • 用到的时候再去查找
    • 但实际上,这个方案不可行,查询一天前的一个交易就要花费 8 百万的 gas fee
  • 只要基于用户的过去行为的逻辑,在区块链上都是不可能
  • 这就是为什么要引入 ZKP coprocessor
  • 计算的事情是在链下做的,不是在链上
  • 逻辑写到一个电路里面
    • 比如证明用户在 5 天交易了超过 1000 笔
  • public input 可以是一些 anchor point,比如某个时间段,这样可以给出这个时间段的证明,节省一些证明的时间
  • 这样可以给出证明
  • 为什么叫 coprocessor,这个和计算机发展有点类似,gpu handle cpu 不擅长的事情
  • ZKP coprocessor 就是为了解决区块链不擅长的计算,比如 data intensive 的计算: - 大量的数据 - 复杂 stateless 的计算 - 计算过程的中间值不需要存储到链上 - 只需要把最终的计算结果放到链上

0xhhh:ZKP coprocessor 和 rollup 的关系是什么?

Dong

  • 虽然广义上都是做链下计算,然后链上有一个 anchor
  • 但它们要解决的问题不一样
  • 主要的区别在于 state 有多重
  • rollup 是有 EVM equivalent 的需求的
  • 在 rollup 里面也是同样不适合做 data intensive 的计算
  • 而 ZKP coprocessor 没有持久的数据存储,计算完成将结果返回给调用方就完成任务了,不需要存储下来
  • 有些系统和 ZKP coprocessor 很像,比如 zkvm 和 zkwasm
  • 在 coprocessor 领域,单纯的用一个 vm 是不行的,因为需要的 overhead 是非常大的
    • RiskZero 已经很快了
  • 未来的 coprocessor
    • 对链上数据的计算可能需要定制化的电路和轻量级的 vm 结合的方式。
      • 定制化的电路去做什么呢?去做一些这种非常核心的,经常会要在 coprocessor 当中用到的,比如说 hash,Merkle Tree 的 inclusion,signature verification
    • 然后可以把 vm 当一个 aggregation layer
    • vm 上面给开发者一个友好的平台,提供一些接口,比如可以拿历史的 trade 数据做简单的加减运算
  • 有一个需求也在 rollup 上面也部署一个 ZKP coprocessor

Cecilia:技术上补充一下 - 拿 Axiom 来说 - 只去执行 view 函数的证明 - 不去做 state transition 的证明(不做 MPT 的状态转换) - 所以这样的 ZKP coprocessor 在执行逻辑和安全假设上,可以看做为 rollup 的子集 - 而如果把 view 函数的证明放到一个广义的 VM 上面,比如 RiscZero 的 zkvm - 从执行能力(execution power)上,可以看做是 EVM 的 super set - RiscZero 是针对 general purpose 运算,不只是针对区块链

ZKP processor 的三种 proof

Dong

  • ZKP processor 主要做三种 proof,以及这三种的排列组合
    • merkle state proof
      • 比如过去某个时间点 NFT 的 holder 是谁
      • 需要去 storage 去找这个过去的 value
    • transaction proof
      • transaction 也有一个 tree,也可以做证明
      • 不管 transaction 是否成功链上都有记录
    • transaction receipt proof
      • 成功后有 receipt,可以通过 event 发出来
      • 从这里可以看到交易了多少 token,这些可以用来计算 trading volume
  • Axiom 复用了 zkevm 中常用的电路
    • 向 zkevm 借鉴了一下电路的代码,也贡献了一些代码,比如 merkle tree inclusion 的代码
    • 从 zkevm 拆出来了一些电路,费了很大力气
      • 因为要做列的对齐
    • 也做了很多优化
      • lookup 优化
  • 几个做 ZKP coprocessor 的团队
    • Brevis
    • Axiom
    • Herodotus
    • Lagrange
  • 各个团队做的方式都不一样,比如用的 framework 和 scaling solution 方式都不一样
  • 大家在解决的技术问题
    • 基于一万个 transaction 做计算
    • 并以很低的成本去证明这些计算

Prover 性能优化

Dong

  • 做性能优化,三条路
    • 算法层
      • 用 halo2 怎么办
    • effective aggregation
      • 五花八门
      • 简单的多加两层 recursion,再加并行化
      • 有一些 aggregation 是可以用的
        • chain of curves:用两个 curve 导一下,试下来不比 folding 差
      • 能把一万个 transaction 压缩到分钟级别的证明
    • 还可以用 folding,比较适合 coprocessor,但 folding 还差的比较远
    • 差 parallelization
    • 不能上链。现在接的 IPA,但需要改成 groth16,但是个大工程

使用 Folding 提升性能

0xhhh

  • RiscZero 说用 folding 需要的带宽比较多
  • 用 prover 生成 proof,需要有个机器去管理 state 并分发给其它的 prover 生成 proof Dong
  • folding 处在三个和尚没水吃的阶段
    • 大家都觉得少点东西,但工作量大,谁来加呢,因为开源后,大家都会用
    • 所以 PSE 这样的比较公益的组织在做
    • 但这个事情很有意义,有人组织大家一起做也是不错的 Cecilia
  • 大家对 folding 太着迷了
  • 在 recursion 和 folding 之间,真正在 engineer 上面我们在 focus on 的一个东西叫就是 chunk prover,这是 Taiko 和 PSE 当前的 focus
  • 所作的事情就是如何把 execution 分成 chunk,然后就可以并行执行,并证明
  • 最后把所有的 chunk 的执行接到一起,生成一个证明
  • 这个其实是在目前工程中比较实际的做法,而不是用 folding Dong
    • 赞同通过 continuation 来实现并行
    • folding 目前短期来看投入产出比比较低 Cecilia
  • 其实我觉得只要在学术上,在理论上有一个基本的 support 能够把 combination 本身拆开,那在工程上面拆合就有很多 engineer 可以干的事情,engineer 可以把 prover 性能优化提升 100 倍
  • 我对 folding 的态度暂时是观望 Dong
  • 还有一个提升性能方式是用硬件加速
    • halo2 没有好的硬件加速
  • Ingonyama 的 icicle 挺好用的,有合作
    • 思路比较简单
      • 把常用的算子放到 GPU 中
    • 效果有 20 倍的提升
    • Cecilia 也在实现 low level 的 gpu 加速 的 api

Franci:ZKP coprocessor 和 the graph 和类似?

Dong - 大体上可以这样理解 - 但 the graph 安全性没有 ZKP coprocessor 高,因为你要信任跑节点的一方

0xhhh:ZKP coprocessor 有哪些 use case?

Dong

  • AMM DEX
    • 问题
      • 从 LP staking 看不出来 LP 对 liquidity pool 的贡献
      • 因为只看 staking 的数量不准确
    • 好的 liquidity incentive design
      • 回头看这一个月的情况去给奖励
      • 对 pool 贡献多的才给激励
  • 游戏
    • 传统的游戏除了内容之外,有两个支柱
      • 买量(获取用户)
        • 都是一锤子买卖,从广告商(youtube)买一个人,cost per install,安装一个就付钱就完了,不能再继续结合后续用户参与游戏的情况来做更多的收益分成和合作
        • 通过 coprocessor 可以改变这个单次博弈的状态
      • 运营
        • 弹窗问用户是否买 token 或者皮肤
        • 游戏体验是根据用户的历史交互决定的
    • 它们是数据驱动的
    • ZKP coprocessor 是可以解决这个问题的
    • 可以提供给游戏用户定制化的体验
  • Social & Identity
    • 根据用户在一个链上的 social 历史数据作为另外一个链上用户的身份证明
  • 广泛的说,如果需要用到非同步计算,并且计算量比较大,都可以放到 coprocessor 里面做

Dong:如何让开发者基于 ZKP coprocessor 开发

  • 让所有开发者都做电路的开发不太现实
  • 如何解决这个问题,大家也有自己的方式
    • Brevis 采用的是 specialized circuit + lightweight VM 的模式。coprocessor 的基底是很多的 specialized 的 circuit,比如 merkle tree 等
    • 还有的是先做一个 use case,比如做 DEX trading volume 数据相关的 API,把这个场景涉及到的功能封装成 API 一下给开发者用。好处是开发者用着比较方便,缺点是只适用于这一个场景,其它的场景需要单独做

ZKP coprocessor 和游戏

Cecilia

  • 因为游戏的执行对 consensus 安全性的要求是 relax 的,其实不需要像 rollup 这么频繁的提交状态,但是对 performance 的要求很高。对 performance 的这个执行环境的需求肯定是要比 ZKEVM 要 profound 很多,可能需要用 rust 代码去写,可能甚至要在 GPU 上面去跑。我觉得就是一个非常有意思的,可以 apply to ZKP coprocessor 的场景

Dong 还有些方式解决 scalability 的问题,比如用 optimistic 的方式,先相信节点的计算,如果有问题可以后面进行挑战,这种模式适用于有时间去做挑战的场景。

Cecilia 其实包括 rollup 也是这样的,你现在 Taiko 所选择的这个实现方式,我们现在叫 contestable rollup,就是随机选块去生成 ZK 证明,而不是每一个块都去 ZK 它,然后有的块用 SGX 去证明,在整个这个 space 里面就是你要去相信或者不相信一个 remote execution the result,这个完全是取决于你用什么样的机制去 secure 它,POW 还是 POS 还是 proof of ZK 这个东西完全是取决于实现。然后我觉得对于 Gaming 来说,肯定是要牺牲更多的安全性和 trust 去换他的这个optimization。

0xhhh

  • 感觉现在整个以太坊就像一个都在朝着 coprocessor 去扩展,比如说就以太坊一直在说 PBS,实际上本质上我觉得也算是一种 coprocessor,它算是 proposal 不需要去存储整个区块链状态了,只有 builder 需要去存储整个区块链的状态,所以感觉我现在就觉得好像整个以太坊都在 coprocessor Dong
  • 好像是最近的思潮
  • 都是问一个问题
    • 到底什么东西要放在什么地方
    • 什么计算放到以太坊上比较好
    • 通过功能的拆分,来做更好的状态和计算上的 scalability
  • 我觉得可能是这个 rollup 的大的这个蓝图已经确立了之后,大家在思考如何再进一步,在已有的这个蓝图里面,怎么去从不管是从系统层面还是从架构层面更好的去优化 performance 和 scalability。我觉得可能是这个思潮驱动的。 0xhhh
  • 我觉得确实应该说从 rollup 或者从一开始状态通道以来,其实整个区块链的发展方向,因为很慢的结算网络意味着支持比较大的计算都只能靠 coprocessor 化,所以感觉这是持续了很多年的思潮了,只不过今天可能真正给它定义成 coprocessor,大家对这个方向有一个比较明确的共识,都在推动整个以太坊的整个 layer 1 协议往 coprocessor 的方向去演变

ZKP bridge

Dong

  • ZKP bridge
    • 本质是 bridge 一个 block header,中间的 VM 没有什么必要,只需要一个 aggregation 功能,把支持的各个链的 block header aggregate 就好了
    • 开发不 scalable,需要针对每个链做一些特殊的定制方案
    • 开发的过程中发现了 coprocessor 的需求 Cecilia 补充关于 ZKP bridge
  • 假设要 bridge 一个以太坊的 block header
  • public input:下一个区块的 block hash
  • advice:当前区块的 BLS signature
  • aggregation:aggregate 所有的 signature
  • polyhedra 的 zkIBC 做出来了,是因为做了一套可以很快的 aggregate 以太坊 BLS signature 的电路。和 zkevm 没有关系
  • 跨链验证的是共识,不是执行

Ingonyama

Dong

  • 性能和体验还是不错的
  • 实现了很多 curve Cecilia
  • 用 GPU 封装算子这个事情很简单
  • 很多从矿场出来的团队都已经积累的相关的技术
  • 但为什么大家都选择了 Ingonyama? Dong
  • 因为 Ingonyama 是开源的,代码采用了 MIT 协议

参考

提到的一些zk项目和团队list:

感谢 Kurt 对部分内容的贡献