在zerocash的论文中,提出的基本构造由:承诺(commitment)、无效符(nullifier)、简洁非交互零知识证明(zk-SNARK)、签名(signature)、带内秘密分发(in-band secret distribution)等。
本文介绍ZCash的角度为:从ZCash目录结构入手,分析各个参数以及函数的意义,以及我作为小白初看ZCash协议的一些困惑的解答。
目前发布的是初步的版本,我将会继续跟进细化这篇文章。
在阅读本文前,可能需要掌握的知识有:
1.区块链是一个难以篡改的公共信息存储平台
2.UTXO交易结构
3.零知识证明的基本原理(建议阅读:https://github.com/sec-bit/learning-zkp),以及zk-SNARK是干啥的
4.稍微看了下ZCash Protocol,知道ZCash是干啥的(截止本文最后更新,ZCash最新的协议文档版本为[NU5]:https://zips.z.cash/protocol/canopy.pdf)
ZCash基于UTXO的转账交易基本可以概括为三个步骤,以Alice给Bob转账为例:
1.Alice证明自己能够花费某个UTXO
2.Alice花费该UTXO,生成一个属于Bob的新UTXO
3.Alice将新UTXO的花费能力转交给Bob
接着,Bob就能继续花费这笔新的UTXO。在这之中需要解决以下的问题:
1.Alice已花费的UTXO不能被再次花费(解决方法:nullifier)
2.Alice
note
note为交易的基本参数,也可以理解为交易承诺的明文,以下为三个版本的note:
Sprout:
n
=
(
a
p
k
,
v
,
ρ
,
r
c
m
)
n=(a_pk,v,\rho,rcm)
n=(apk,v,ρ,rcm)
Sapling:
n
=
(
d
,
p
k
d
,
v
,
r
c
m
)
n=(d,pk_d,v,rcm)
n=(d,pkd,v,rcm)
Orchard:
n
=
(
d
,
p
k
d
,
v
,
ρ
,
ψ
,
r
c
m
)
n=(d,pk_d,v,\rho,\psi,rcm)
n=(d,pkd,v,ρ,ψ,rcm)
参数意义:
ρ
\rho
ρ用于导出nullifier。同时,
ρ
\rho
ρ也是note的一部分(Sapling除外,但是Sapling中
ρ
\rho
ρ也是通过note中参数计算得到的,所以总体而言,
ρ
\rho
ρ都与note绑定)。也就是说,从input的角度讲,当发送方需要使用某个UTXO时,
ρ
\rho
ρ确保了nullifier与该UTXO承诺的绑定性,避免了双花攻击。从output的角度来讲,
ρ
\rho
ρ也可以看作是硬币的标识符,标识符作为硬币的秘密,只有知道该标识符的人能够花费该硬币(当然该标识符未出现在已有的nullifier中并且与这个人的公钥地址绑定)。
在Sprout版本中,
ρ
\rho
ρ由随机生成的
ψ
\psi
ψ、交易output的索引i、签名
h
s
i
g
h_{sig}
hsig生成:
ρ
i
=
P
R
F
φ
ρ
(
i
,
h
S
i
g
)
\rho_i=PRF_\varphi^\rho(i,h_{Sig})
ρi=PRFφρ(i,hSig), 在Sapling版本中,取消了note中的
ρ
\rho
ρ,在导出nullifier中使用承诺
c
m
cm
cm和承诺在merkle tree中的位置
p
o
s
pos
pos计算
ρ
\rho
ρ:
ρ
:
=
M
i
x
i
n
g
P
e
d
e
r
s
e
n
H
a
s
h
(
c
m
,
p
o
s
)
\rho:= MixingPedersenHash(cm,pos)
ρ:=MixingPedersenHash(cm,pos)。
在Orchard版本中,
ρ
:
=
n
f
o
l
d
\rho:=nf^{old}
ρ:=nfold,其中nf是nullifier
d
,
p
k
d
d,pk_d
d,pkd代表用户的公钥地址,用于接收交易,同时作为用户的身份标记,在交易中被混淆。其中d用于推导出椭圆曲线的一个基点
g
d
g_d
gd,其对应的私钥为
i
v
k
ivk
ivk,公私钥计算的公式为
p
k
d
:
=
[
i
v
k
]
g
d
{pk}_d := [ivk]g_d
pkd:=[ivk]gd(将椭圆曲线上的点
g
d
g_d
gd乘以一个标量
i
v
k
ivk
ivk,得到点
p
k
d
{pk}_d
pkd)。也就是说,对于每一个交易公私钥地址对,其计算的基点都是不同的,扩展了密钥的空间。
v为交易金额
rcm为生成commitment的后门参数:
在Sprout中rcm随机生成:
r
c
m
i
←
R
N
o
t
e
C
o
m
m
i
t
S
p
r
o
u
t
.
G
e
n
T
r
a
p
d
o
o
r
(
)
rcm_i\xleftarrow{R}NoteCommit^{Sprout}.GenTrapdoor()
rcmiRNoteCommitSprout.GenTrapdoor()
在Sapling中rcm也随机生成:
r
c
m
i
←
R
N
o
t
e
C
o
m
m
i
t
.
G
e
n
T
r
a
p
d
o
o
r
(
)
rcm_i\xleftarrow{R}NoteCommit.GenTrapdoor()
rcmiRNoteCommit.GenTrapdoor()
同时,Sapling中的rseed等同于rcm:
r
s
e
e
d
:
=
I
2
L
E
O
S
P
256
(
r
c
m
)
rseed:=I2LEOSP_{256}(rcm)
rseed:=I2LEOSP256(rcm)
其中I2LEOSP是数据类型转换函数,将整数形式数据转换为字节序列。
在Orchard中rcm由rseed和
ρ
\rho
ρ以伪随机函数(单向函数)产生:
r
c
m
=
T
o
S
c
a
l
a
r
O
r
c
h
a
r
d
(
P
R
F
r
s
e
e
d
e
x
p
a
n
d
(
[
5
]
∥
ρ
)
)
rcm=ToScalar^{Orchard}(PRF^{expand}_{rseed}([5] \| \rho))
rcm=ToScalarOrchard(PRFrseedexpand([5]∥ρ))
其中rseed随机产生:
r
s
e
e
d
←
R
B
Y
[
32
]
rseed \xleftarrow{R} \mathbb{B}^{\mathbb{Y}^{[32]}}
rseedRBY[32]
dummy note
转账金额为0的交易note。可能会用于一些交易的混淆。
但是文中在提到这一类型的note时。和一种攻击方式联系紧密:精灵的黄金攻击(Faerie Gold attack)。该种攻击通过构造多个虚假交易和一个真实交易,虚假交易转账金额为0,这些交易的总和即真实交易的金额,所以能够通过验证,但是交易的发送方确将虚假交易的note发送给交易接收方,接收方从该note中无法获得交易的金额。ZCash规避这一交易的方式是强迫每一条交易的 ρ \rho ρ都不同,这样即使虚假交易存在,也能通过不同的 ρ \rho ρ与真实交易区分,因此交易的接收方总能获得真实交易的note。
承诺&无效符
承诺(commitment)和无效符(nullifier)是理解ZCash设计的核心,
Sprout版本中交易的金额v和交易地址等信息仅通过一个note commitment计算并上链,。
Sapling版本及之后,原本承诺中的发送金额
v
v
v被单独作为value commitment计算,这样做的目的是为了将原本的承诺中数值的部分以pedersen commitment的形式承诺,更利于进行同态计算;而其余文本的部分用更为高效的非同态承诺(Sapling版本使用Windowed Pedersen commitments或Orchard版本使用Sinsemilla commitments)形式。
三个版本中value commitment均为 c v : = V a l u e C o m m i t r c v S a p l i n g ( v ) cv := ValueCommit^{Sapling}_{rcv}(v) cv:=ValueCommitrcvSapling(v),即 H o m o m o r p h i c P e d e r s e n C o m m i t r c v S a p l i n g ( D , v ) = [ v ] F i n d G r o u p H a s h J ( r ) ∗ ( D , " r " ) HomomorphicPedersenCommit^{Sapling}_{rcv}(D,v)=[v]FindGroupHash^{\mathbb{J}^{(r)*}}(D,"r") HomomorphicPedersenCommitrcvSapling(D,v)=[v]FindGroupHashJ(r)∗(D,"r"),note commitment均由对应的n导出(见note部分)。Sprout: c m i = N o t e C o m m i t r c m i S p r o u t ( a p k , i , v i , ρ i ) cm_i=NoteCommit^{Sprout}_{{rcm}_i}(a_{pk,i},v_i,\rho_i) cmi=NoteCommitrcmiSprout(apk,i,vi,ρi)。Sapling: c m = N o t e C o m m i t r c m S a p l i n g ( r e p r J ( g d ) , r e p r J ( p k d ) , v ) cm = NoteCommit^{Sapling}_{rcm}(repr_{\mathbb{J}}(g_d),repr_{\mathbb{J}}(pk_d),v) cm=NoteCommitrcmSapling(reprJ(gd),reprJ(pkd),v)。Orchard: c m x = E x t r a c t P ⊥ ( N o t e C o m m i t r c m O r c h a r d ( r e p r P ( g d ) , r e p r P ( p k d ) , v , ρ , ψ ) ) cm_x=Extract^{\perp}_{\mathbb{P}}(NoteCommit^{Orchard}_{rcm}(repr_{\mathbb{P}}(g_d),repr_{\mathbb{P}}(pk_d),v,\rho,\psi)) cmx=ExtractP⊥(NoteCommitrcmOrchard(reprP(gd),reprP(pkd),v,ρ,ψ))。
无效符(nullifier)的目的是表示硬币的花费,这一功能的引入是为了避免双花攻击
Sprout:nullifier通过
a
s
k
,
ρ
a_{sk},\rho
ask,ρ得出:
P
R
F
a
s
k
n
f
S
p
r
o
u
t
(
ρ
)
PRF^{nfSprout}_{a_{sk}}(\rho)
PRFasknfSprout(ρ)。
Sapling:nullifier通过 n k , ρ nk,\rho nk,ρ得出: P R F n k ⋆ n f S a p l i n g ( ρ ⋆ ) PRF^{nfSapling}_{nk\star}(\rho\star) PRFnk⋆nfSapling(ρ⋆),其中 ⋆ \star ⋆表示将椭圆曲线上的点转换为整数。
Orchard:nullifier通过 n k , ρ , ψ , c m nk,\rho,\psi,cm nk,ρ,ψ,cm得出: D e r i v e N u l l i f i e r n k ( ρ , ψ , c m ) = E x t r a c t P ( [ P R F n k n f O r c h a r d ( ρ ) + ψ ) m o d q P ] K O r c h a r d + c m ) DeriveNullifier_{nk}(\rho,\psi,cm)=Extract_\mathbb{P}([PRF_{nk}^{nfOrchard}(\rho)+\psi)\mod q_\mathbb{P}]\mathcal{K}^{Orchard}+cm) DeriveNullifiernk(ρ,ψ,cm)=ExtractP([PRFnknfOrchard(ρ)+ψ)modqP]KOrchard+cm).
签名
对于签名密钥
a
s
k
ask
ask首先为了避免重放攻击,引入随机化因子
生成随机化的签名密钥:
,也就是
r
s
k
=
α
∗
a
s
k
,
r
k
=
[
r
s
k
]
P
G
rsk=\alpha*ask, rk=[rsk]\mathcal{P}_\mathbb{G}
rsk=α∗ask,rk=[rsk]PG。使用rsk对交易的hash结果进行签名
带内秘密分发(in-band secret distribution)
在交易过程中,交易的接收方如果想要花费某个硬币,需要知道该硬币的标识符,所以交易发送方需要将标识符通过某种加密方式发送给接收方。我们用np表示这些秘密的明文。
Sprout: n p = ( 0 x 00 , v , ρ , r c m , m e m o ) np=(0x00,v,\rho,rcm,memo) np=(0x00,v,ρ,rcm,memo)
Sapling: n p = ( l e a d B y t e , d , v , m e m o ) np=(leadByte,d,v,memo) np=(leadByte,d,v,memo)
Orchard: n p = ( l e a d B y t e , d , v , r s e e d , m e m o ) np=(leadByte,d,v,rseed,memo) np=(leadByte,d,v,rseed,memo)
参数意义:其中leadByte表示使用的不同版本的协议;d在上文的note中解释过,为公钥地址的生成基点;v为交易金额;memo为交易的备注信息;rseed和 ρ \rho ρ可以推导出 r c m , ψ , e s k rcm,\psi,esk rcm,ψ,esk。
Sapling encryption:
-
随机生成esk:
-
生成一次性对称加密密钥 K e n c = [ e s k ] p k d K^{enc}=[esk]pk_d Kenc=[esk]pkd(注:这里的 p k d = [ i v k ] g d pk_d=[ivk]g_d pkd=[ivk]gd)。
-
使用 K e n c K^{enc} Kenc加密np获得密文 C e n c C^{enc} Cenc。
-
(此步可选)使用 o v k , c v , c m , e s k ovk,cv,cm,esk ovk,cv,cm,esk(其中ovk为接收方的ovk)导出对称密钥ock:
(其中ephemeralKey就是epk转化为字符的格式),使用ock加密op:
,
。 -
将 e p k , C e n c , C o u t epk,C^{enc},C^{out} epk,Cenc,Cout发送给接收方。
Orchard encryption:
除了第一步中esk由rseed和 ρ \rho ρ推导出,其他步骤与Sapling相同。
Sapling\Orchard decryption:
接收方通过
i
v
k
,
e
p
k
ivk,epk
ivk,epk获得对称密钥
K
e
n
c
=
[
i
v
k
]
e
p
k
=
[
e
s
k
]
p
k
d
K^{enc}=[ivk]epk=[esk]pk_d
Kenc=[ivk]epk=[esk]pkd解密
C
e
n
c
C^{enc}
Cenc获得np。Sapling中通过
r
c
m
,
v
rcm,v
rcm,v和自己的
p
k
d
,
g
d
pk_d,g_d
pkd,gd计算承诺cm:
,通过承诺cm计算
ρ
\rho
ρ:
,即获得硬币的使用权;Orchard中直接从np中获得
ρ
\rho
ρ,即获得硬币的使用权。
接着,接收方可以通过出示ovk解密得到 p k d , e s k pk_d,esk pkd,esk,进一步可以解开 C e n c C^{enc} Cenc(ovk的作用是在不暴露私钥ivk的基础上打开 C e n c C^{enc} Cenc)
密钥结构
上图的每一个密钥的作用均在文中其他部分提及,除了sk,sk是用于派生其余所有密钥的密钥,本身没有用在交易的过程中。
以下说明Sapling和Orchard两个版本的内部派生密钥的生成区别,内部派生密钥用于在转帐中生成找零的地址,个人认为内部派生密钥是相对的,假如我进行了一笔转账a,a中生成了找零的内部密钥ka,那么我使用ka接着进行转账b,那么对于b而言,ka就是外部密钥。
Sapling和Orchard的变化在于Orchard只产生nk而无nsk,而Sapling产生的nk是由对应的私钥nsk派生而来,同时导出的内部密钥组中nk和外部密钥中的nk相同。nk用于生成nullifier,在Sapling中需要经过以下的zk-SNARK证明:
,在这里使用公私钥的方式去证明nullifier的目的是引入随机性,换言之,即使有相同的
ρ
\rho
ρ,也能得出不同的nullifier,所以每生成一个内部密钥都要重新生成一个nk公私钥对;而在Orchard中,巧妙地使用了
ψ
\psi
ψ解决了这一问题:
,其中
,我们可以看到,
ψ
\psi
ψ在这里起到了随机化的作用,即使使用了相同的
ρ
\rho
ρ也会得到不同的nullifier,同时,由于减少了zk-SNARK中证明nk和nsk关联的这一步,这一优化也提高了运算效率