Zcash 不透明(加密)交易的分析
Zcash 不透明交易中未花费的交易使用Note表示,对应于透明交易中的UTXO。 Note分为SproutNote和SaplingNote不同的数据结构,对应了两套处理流程,在版本 OverWinter以及该版本前都是使用SproutNote,在版本Sapling加入了SaplingNote, 目前代码中是这两套并存,以SproutNote为主,SaplingNote未使用,之后会转向 SaplingNote。
SproutNote 和 SaplingNote 区别
下面是Note的数据结构。
基于Note的数据结构有两套不同的机制,这两套不同机制主要区别点如下:
(1)涉及到的key不同,如下图所示。
其中双竖线表示相同,箭头表示生成关系,A 指向B, 表示A可以生成B。
(2)nullifier生成方式不同。
每个Note都对应一个nullifier,如果一个Note使用了,链上就会记录它对应的nullifier,如果没有使用链上就不会存在,这是避免双花。
1)对于SproutNote:
nullifier基于以下值生成: spendkey `//用户秘钥`
rho `//每个SproutNote唯一标识`
见代码:
uint256 SproutNote::nullifier(const SproutSpendingKey& a_sk) const { return PRF nf(a_sk, rho);
}
2)对于SaplingNote:
见代码:
boost::optional SaplingNote::nullifier(const SaplingFullViewingKey& vk, const uint64_t position) const{.......}
nullifier生成与SaplingFullViewKey以及position值相关,其中postion在是交 易相关的承诺在承诺树中的位置。
对于 zcash 钱包开发分为两个模块(需要用户上传 IncomingViewKey):
解析交易:
服务端基于用户上传的 IncomingViewKey 解析新收到的交易,ES 保存更新 与用户相关的为花费的 UTXO,和 Note 数据。
构造交易:
客户端构造涉及不透明地址交易,基于私钥完成 nullifier 计算,零知识
证明计算,以及交易签名,然后发送给区块链节点。
2.SproutNote 对应的处理流程
(1)解析交易
交易中对应于不透明地址的数据在 vjoinsplit 属性中, 我们首先使用
IncomingViewKey 生成 RecievingKey,然后使用 RecievingKey 解密 vjoinsplit 生成相应的 SproutNote。
具体代码调用如下:
见代码 src/wallet/wallet.cpp
入口函数是 FindMySproutNotes(…),函数遍历数组 tx.vjoinsplit,对于数组的 每个元素 tx.vjoinsplit[i]生成 hSig,然后对于 tx.vjoinsplite[i]的子元 素,使用节点钱包存放的各个 paymentaddress 依次尝试去调用 GetSproutNoteNullifier(…)去解密。其中图中 address = item.first 就是获取到的其中一个 paymentaddress。函数 GetSproutNoteNullifier 如下图:
函数完成解密数据, 构建 note,然后基于 SproutSpendingKey 生成 nullifier,我们可以不用关注 nullifier 生成,因为在使用 note 构造交易时 会重新生成 nullifier。所以关注在函 SproutNotePlaintext::decrypt(…)。 该函数首先调用了 NoteEncryption::decrypt(…)所以先看该函数, 如下图:
上图中,调用函数 cyption_scalarmult()生成 dhsecret,输入了参数 sk_enc.begin(),即字符数组 sk_enc 首地址,而字节数组 sk_enc 是 recievingkey, 来源于 IncomingViewKey,在第一部分中可以看到 IncomingViewKey 是由 apk, sk_enc 组成,一共 64 字节,他们各占 32 字节。 回到函数中,调用完 cyption_scalarmult()经过一系列调用追后使用crypto_aead_chacha20poly1305_ietf_decrypt(…)解密,解密结果是字节数组 plaintext。
现在我们回到函数 SproutNotePlaintext::decrypt(…),如下图:
我们看到他调用了 NoteEncryption::decrypt(…)获取了字节数组 plaintext, 然后使用初始化了字节流 ss, 最后使用字节流创建了 SproutNotePlaintext 对 象。而 SproutNotePlaintext 如下图:
该类中包含了创建 SproutNote 的 rho, r,其中 value、momo 在父类 BaseNotePlaintext 有定义,同时我们可以从 IncomingViewKey 中获取 payingkey,所以基于值 payingkey, r, rho, value 就可以生成 SproutNote,表 示一个未花费的交易。
(2)生成交易
生成交易需要用户的私钥,所以在客户端完成交易构造,交易签名,然后把交易发送给区块链节点。交易涉及到透明地址和不透明地址交叉交易,这里 只介绍 zaddress ==> zaddress, 对于与 taddress 相关的与和 bitcoin 类似。 首先 Trasaction 数据结构,创建交易其实是填充该数据结构对象。
sprout 加密交易主要是对属性 vjoinsplit 的构造赋值,而构造 vjoinsplit, 核心是构造 JSDescription, 该数据结构如下:
在上图中 nullifiers 对应交易输入的为花费的 SproutNote, ciphertexts 记录 了交易的输出 SproutNote。ciphertexts 是一个长度为 2 的数组,每个数据都 是 Ciphertext 的对象,该对象封装了输出交易的 SproutNote 信息,他基于 ephemeralKey,transmissingKey, joinSplitPubKey 的 hash 值做加密,可以 基于 ephemeralKey,receivingKey, joinSplitPubKey 做解密。
见代码 src/wallet/ asyncrpcoperation_sendmany.cpp
源码中类 asyncrycoperation_sendymany 实现了交易的生成、签名、发送。该 类接收交易的输入地址,输出地址,交易金额,然后打包成一个待操作的行为 对象,放到操作队列里等待调用,调用时会回调到该类的 main_impl()方法。 首先看该类的构造函数,如下图:
参数解释:
contextualTx: 模板交易对象,初始化了交易的费用,交易的度,交易的版本等基本信息。
fromAddress: 交易的输入地址。
tOutput: 透明交易的输入地址,
zOutput: 不透明交易地址。 其他的定义了费用、最小深度、上下文信息。
其中 SendManyRecipient:std::tuple
下面介绍 main_impl(),函数实现了透明地址给透明地址打钱、透明地址给不透 明地址和透明地址打钱(可不含),不透明地址给不透明地址和透明地址打 钱。透明地址相关的和比特币流程相似,所以关注在交易发起方是不透明地 址,由于函数较大所以分块介绍:
如上图所示,如果交易发送方是不透明地址,则调用函数 find_unspend_note(),该函数是查询节点数据库返回该地址受到的未花费的 SproutNote。并且按照 Note 中金额降序,结果保存在对象属性 z_input_中。
如上图中
下一步:
创建关键数据结构 AsycJoinSplitInfo info, 并且初始化部分属性,接下来大 部分步骤目的都是初始化该对象,该数据结果如下:
接着:拆解输入队列 zInputsDeque,分别用 vInputNote 保存输入 SproutNote,用 vOutPoints 保存输入交易对应的其在上笔交易中的信息,同时 将 vInputNote 数据信息复制到 info.notes。如下图
然后:如下图取出输入队列的第一个节点,获取转账金额, 给 info.vpub_new 赋值。(这里涉及的多地址的就不展开了 )
如果上一步取值的交易的输出地址不为空,这用它创建 JSoutput 对象, 加入到队列 info.vjsout
最后:在把交易进行签名发送出去之前,调用了函数 perform_joinsplit (),在改函数里不仅完成了 info.vjinput 的初始化,同时完成了输入 Note 相关 nullifiers 的计算,零知识证明计算,最终生成了 JSDescription 就是我 们最初一直在构建的数据结构。该函数如下图:
而函数 JSDescription::Randomized(…)最终会调用到函数如下图:
上图中 prove 完成了 nullifier,承诺,零知识证明的计算等。 最后使用私钥对交易进行前签名,签名这部分没有太多变化。
ps:这些代码是2018年8月份整理的,与现在线上的代码可能有些出入,但是区别不大。
3.SaplingNote 对应的处理流程。
(1)接收数据:
流程与 Sprout 类似, 返回 SaplingNote。
见代码 src/wallet/wallet.cpp
解密的是 vShieldedOutput 中数据 encCiphertext,使用的 SaplingIncomingViwingKey。
解密函数如下图:
Ps: AttemptSaplingEncDecryption (…)中的librustzcash_sapling_compute_cm(…)是为了验证收到的commitment是否合法。AttemptSaplingEncDecryption(…)返回结果:
d: Note对应的地址的组成参数。
rcm: Note对应的commitment。
value_: 金额。
memo_: 备注。
结点会为每一个Note 生成一棵Sapling Commitment Tree, 是一棵Merkle树, 该树存放了历史所有commitment,当然废弃的加密地址交易中涉及的commitment不包含,然后在向该Sapling Commitment Tree添加该Note对应的commitment。同时可以获取到添加commitment在该树中的位置position。之后在接收的区块中包含加密地址交易时,会不断更新Sapling Commitment Tree。
在使用该Note发送交易时,会使用该Note对应Sapling Commitment Tree中三个值:
anchor: Sapling Commitment Tree的root值。
position: Note的在改树中的位置。
witness: Sapling Commitment Tree中每个节点,以及节点的位置信息的集合。//ToDo这块我没有很好的方案。
(2)发送数据:
Zcash中类TransactionBuilder用于构造SaplingNote相关的交易, 该函数主要完成的也是nullifier, commitment,zproof等值计算。需要使用ExpendedSpendKey, FullViewingKey。ExpendedSpendKey由SpendKey生成, FullViewingKey由ExpendedSpendKey生成。
我使用了go语言完成了交易的发送,所以代码以go语言切入吧。
这次我们需要构造的数据结构在上图中红框内:
ValueBalance: 交易的矿工费。
VShieldedSpend: 交易输入的描述信息,这里指Sapling版本加密
地址交易。
VShieldedOutput: 交易输出的描述信息,这里指Sapling版本加密
地址交易。
BindingSignature: Sapling加密地址交易的签名。
下面介绍下VShieldedSpend, VShieldedOutput的数据类型。VShieldedSpend是元素结构SpendDecription的数组,SpendDecription结构如下:
CV:输入Note金额的commitment值。
Anchor:构造交易时Sapling Commitment Tree的根hash值。
RK: 随机的公钥,与花费的授权签名SendAuthSig相匹配。
Nullifier: 该Note的唯一标识,链基于Nullifier值是否出现过来判断Note是否花费。
ZKproof: Note可以花费的零知识证明。
SendAuthSig:花费的授权签名。
VShieldedOutput是元素结构OutputDecription的数组, OutputDecription结构如下:
CV: 输出Note金额的commitment值。
CM:输入Note的commitment值。
EphemeralKey:JubJub的公钥,与节点密文EncCiphertext相匹配。
EncCiphertext:输出Note的内容密文,解析该密文可以获取Note的相关信息,例如金额,收款地址的中随机数等。
OutCiphertext: 对生成生EncCiphertext过程使用到的密钥等数据成密密文。
Zkproof:输出Note的零知识证明。
构造交易。
分为三部分初始化工作、数据基础、交易构建。下面分别展开。
①初始化工作:
Zcash基于库librustzcash.a生成加密地址交易数据生成,而librustzash在使用前需要导入参数文件:sapling-spend.params, sapling-output.params, sprout-groth16.params,使用库函数librustzcash_init_zksnark_params(…)导入。
然后调用库函数librustzcash_sapling_proving_ctx_int(),获取上下文ctx。
备注:在之后构造数据过程中如果在使用librustzcash.a库时出错,退出之前需要释放获取的上下文ctx, 使用库函数librustzcash_sapling_proving_ctx_free()。
②数据基础:
交易输入输出地址
SaplingPaymentAddress{
d, //地址组成随机数, 11字节
pkd,//transmissionkey, 32字节
}
ExpendedSpendKey{
Ask,
Nsk,
Ovk,
}// ExpendedSpendKey由Ask, Nsk, Ovk组成它们都是32字节
FullViewingKey{
Ak,
Nk,
Ovk
}// FullViewingKey由Ak, Nk, Ovk组成它们都是32字节
交易输入和输出Note。
Note{
d, // Note对应的地址的组成参数。输出和输入Note取值有一些不同。输入Note:解析之前收到加密地址交易获取,或者也可以解析输入
地址获取。输出Note:解析输出地址获取
Rcm, // Note对应的commitment。输出和输入Note取值有一些不同。输入Note:解析之前收到的加密交易交易获取。输出Note: 由librustzcash.a中库函数。
librustzcash_sapling_generate_r()生成。
value_,//金额。
memo_,//备注。
}
Note在Sapling Commitment Tree中的三个数值。
anchor: //Sapling Commitment Tree的root值。
position: //Note树中的位置。
witness: // Sapling Commitment Tree中每个节点,以及节点的位置信息的合。
③交易构造
我们现在已有数据的基础上构造只有Sapling版本的加密地址交易,不涉及到透明地址交易。(目前源码里不支持Sapling和Sprout地址件交易,但是与透明地址允许)。在Transaction数据结构中给下图出现的字段赋值:
而ValueBalance,VShieledSpend, VShieledOuput,BindingSignature外的其他字段置空值或者初始值就行。
下面分别构造ValueBalance,VShieledSpend, VShieledOuput, BindingSignature。
=> ValueBalance:直接赋值矿工费就行。
=> VShieledSpend构造。
VShieledSpend是数组结构,每个元素存放节点的描述信息。
描述信息的数据结构SpendDecription,如下图:
==> Anchor值
Sapling Commitment Tree的root值,已有所以我们需要获取其
他字段数值。
==> Nullifier 计算:
调用librustzcash.a里提供的库函数。
librustzcash_sapling_compute_nf(…)生成nullifier值,由
于该函数返回值是bool,cgo 不支持。所以我封装了一层函数:
librustzcash_sapling_compute_nf_int,返回值是0,1。
diversifier: 输入Note字段d
pkd: 交易输入地址SaplingPaymentAddress中字段pkd
value: 输入Note字段Value,
r:输入Note字段Rcm,
ak: FullViewingKey里字段Ak
nk:FullViewingKey里字段Nk
position:Note的在SaplingComitmentTree中位置。
res: 存放返回记过值,就是nullifier值。
==> CV, RK, Zkproof值生成:
调用库函数librustzcash_sapling_spend_proof(…)来生成,
cgo调用这里也是封装了一层。
ak: FullViewingKey里字段Ak
nsk:ExpendedSpendKey里字段Nsk
diversifier: 输入Note里字段d
rcm: 输入Note里字段Rcm
ar: 32随机数值,由librustzcash.a中库函数
librustzcash_sapling_generate_r()生成。
anchor, witness:note对应的Sapling Commitment Tree 的信息。
cv,rk,zkproof 用于存放结果值。
到此除了里SpendAuthSign其他都构造完毕。SpendAuthSign值,需在在VShieledOutput构造完毕后才能生成。
=>VShieledOutput构造
VShieledSpend是数组结构,每个元素存放输出节点的描述信息。该描述信息的数据结构OutputDecription。
下面介绍该结构中字段的值的获取。
==> CM值生成:
调用库函数librustzcash_sapling_compute_cm(…)来生成,cgo调用这里也是封装了一层。
输入参数:
diversifier: 交易输出地址SaplingPaymentAddres字段d。
pkd: 交易输出地址SaplingPaymentAddress中字段pkd,
value: 输出Note里字段Value。
rcm: 输出Note里字段Rcm。
cm: 用于存放结果值。
==> EphemeralKey, EncCiphertext值生成:
这块主要完成的是对输出Note数据进行加密,生成密文,给出对应的公钥。
①序列化输出Note,序列化结果值作为明文。
如图,写入一个字节loadingByte值为0x01, 然后依次写入 Diverifier, Value, Rcm, Memo数据。
②获取Note内容加密的公钥epk,私钥esk。字段 EphemeralKey(Esk)赋值值为epk。
在上图中函数LibrustZcashSaplingGenerateR()调用了
库函数:librust_zcacsh_saplinggenerate_r()。
函数 LibrustZcashSaplingKaDerivePublic()调用了
库函数:librustzcash_sapling_ka_derivepublic_int()
③基于第一步的序列化结果值明文,第二步的私钥公钥以及输
出地址中字段pkd,生成密文。
如上图中所示:首先使用私钥Esk和pkd生成私钥dhsecret, 后在计算出加盐加密需要的K值,最后对明文message进行加密,获取密文ciphertext,赋值给字段EncCiphertext。
==> OutCiphertext数值生成。
生成过程与EncCiphertext大致相同,区别点是明文的内容不同,EncCiphertext明文内容是Note内容的序列化字节数组,OutCiphertext明文内容是上步中Esk和pkd,还有加盐加密所需的K值的生成函数不同等区别。在这里就先不展开了。
==> CV和Zkproof值生成。
使用了库函数librustzash_sapling_output_proof,cgo调用这里也是封装了一层。
输入的参数:
esk是之前获取的密钥,
diversifier, pkd, rcm, value取值和前文描述中相同。
=>VShieledSpend里元素的字段SpendAuthSig生成。
使用了库函数librustzash_sapling_spend_sig,cgo调用这里也是封装了一层。
输入参数
ask是ExpendedSpendKey里的Ask值。
ar取值与在Zkproof生成步骤中输出参数ar相同。
sighash值为交易序列话后的hash值,这个过程和之前透明地址
=>BindingSignature值生成。
使用了库函数librustzash_sapling_binding_sig,cgo调用这里也是封装了一层。
参数:
valueBalance: 取值为交易字段ValueBalance
sighash:与上一步同,值为交易序列话后的hash值。
res:用于接收结果值
到此完成了对加密地址的交易的构建。
如果加密地址和透明地址之间交易呢?过程是对于加密地址相关的我们按照上面步骤构建对应的字段,而对于透明地址相关的我们按之前分享的透明地址交易构建文中所述来构建对应字段。
Ps: 1.在源码中代码是这样,但是我这边目前只是成功发送加密地址间的交易。
2.源码中明确强调了Sprout版本加密地址和Sapling版本加密地址是不允许交易的。
PS: 寒冬之际,求一个好坑!