就在不久前,NEAR宣布与ZeroPool建立合作关系,后者将在NEAR协议中增加对私密交易功能的支持。目前,在NEAR的平台上,所有交易都和比特币和以太坊一样,全部公开可查。也就是说,对于每笔交易,发起方、接收方和交易金额等信息都是对外公开的。采用这种方式能够保证每个人都有权利审计账簿,确认交易的有效性,同时还可以有效杜绝双花现象的发生。
在很多情况下,理想的交易方式是:只有交易的参与者才能看到交易的详细内容。而要满足这一要求,同时保证账簿仍然是可被验证的是一项复杂的工作,需要用到高深的密码学技术。在这篇文章中,笔者将从技术的角度深度解析如何在NEAR系统中实现私密交易功能,即在保证验证交易正确性的工作不受到任何损害的情况下,使交易的各个参与方与交易金额等信息处于隐藏状态。下面我们切入主题:
未经花费的输出
在NEAR生态里,我们一般使用账户模型作为记账方式,私密交易使用的则是UTXO(未经花费的输出)模型。每一个UTXO都是一个元组,包含数额,接收方,salt三个元素,其中数额指交易金额,接收方其实是通证接收方的公钥,而“salt”只是一些随机数。
所有的UTXO都储存在一个默克尔树上,该默克尔树有一个预设高度,高度值的大小决定了在该默克尔树存在的整个生命周期内,交易池可以处理的交易数量的上限/下限。
图:UTXO默克尔根
默克尔树上的每一个分支(树叶)或是一个UTXO,或是null(空值)。每一个null代表一个空位,未来可以填充进一个新的UTXO。一旦一个空值被填满后,便不可回归到初始状态。所有的分支最初都是空值状态,即都为null。
除了接收方,真正的UTXO不会透露给任何人。因此,默克尔树上的分支实际上是UTXO的哈希值,这也是为什么“salt”需要存在的原因。如果没有了它,一旦Alice知道了Bob的公钥,就可以使用不同的数字,来验证Bob的公钥(哈希值)是否出现在了默克尔树上,从而暴力破解交易金额。此时,Bob的交易也就不再匿名了。
图: 默克尔根
交易
假设Alice想私下里给Bob转一些通证。属于Alice的通证存放在UTXO里,通证的接收者相当于Alice的公钥。为了让交易保持私密状态,Alice用如下形式创建了一次交易:
这笔交易有2个输入和2个输出(输入和输出的确切数量不一定为2,但是必须要相同——所有交易都是如此),这里的输入是一些既有的UTXO,对应默克尔树上的分支;输出则是全新的UTXO,未来将被添加至默克尔树。
当Alice发起交易时,如果她发布的恰好是两个正在被花费的UTXO,就会将该交易链接至生成这两个UTXO的交易上。而建立交易池的目的恰恰是为了保证这样的链接不能被建立,以确保输入型UTXO不会被发布出去。那么问题来了,如何才能确保在交易的过程中,验证节点可以确认一笔交易花费的是已有的UTXO,同时又不会对外公布正在被花费的真实的UTXO呢?
与众多隐私保护交易引擎一样,ZeroPool使用了一种非交互性的零知识证明(zk-SNARK)来实现私密交易。针对某种特定计算的零知识证明能够支持以下形式的密码证明:
给定的公开输入1,公开输入2…
已知某种私密输入1,私密输入2…
可得到【某种结论】
此类知识证明的工作原理不在本文讨论范围之内。有关此问题的更多信息,可以点击该博客链接查询。如果想以一种最简单的方式达成私密交易,知识论证可以具有以下形式:
给定默克尔根以及两个哈希值OUT_HASH1和OUT_HASH2,
已知有四个这样的UTXO:IN1,IN2,OUT1,OUT2,两个默克尔证明P1和P2以及一个私钥x,
OUT1和OUT2对应的哈希值分别为OUT_HASH1和OUT_HASH2;IN1和IN2中的接收者相当于与x对应的公钥X;merkel证明P1和P2是在默克尔根已确定的默克尔树中包含IN1和IN2的有效证明;IN1和IN2中的数量之和等于OUT1和OUT2中的数量之和。
交易则包含merkle_root,out_hash1,out_hash2,以及知识证明。交易中的任何内容都不会暴露输出型UTXO的接收者,也不会将输出型UTXO链接到特定的输入型UTXO。并且,连交易金额这样的信息也不会在交易过程中对外展示。
举例说明:如上图所示,假设该默克尔树的第一个和第三个UTXO以Alice为接收者,对应的金额分别为$100和$17,Alice知道这两个UTXO,但是对树上的其他任何UTXO都未知。如果她想向Bob发送$42,通常的做法是她会首先创建一笔交易,这笔交易会使用她的两个UTXO作为输入,同时创建两个输出:——一个向Bob发送$42,另一个将剩下的75$返给她自己。她将属于Bob的UTXO告诉了对方,但是其余的UTXO只有她自己知道,旁人无从知晓。并且,甚至连输入型的UTXO的哈希值也是保密的。
智能合约负责对交易池进行日常维护,一旦该合约收到了一笔这样的交易,便会对知识证明进行验证。验证无误后,就会将两个新的UTXO添加至默克尔树上。
Bob从Alice处收到UTXO后,会等待一段时间,直到UTXO的哈希值被默克尔树收录之后,他才从真正意义上获得了这些通证。
这种方式虽看似简单,但存在一个问题,即相同的UTXO可能会被重复使用。由于除了Alice没有人知道输入型UTXO的哈希值,交易池便无法将已消耗的UTXO从默克尔树上移除,因为连它也不知道先从哪个开始移除。
如果Alice创建2个不同的零知识证明,但是消耗了同样的2个输入,每个人都能验证两笔交易消耗了一些原本存在于默克尔树上的UTXO,但是却无法得知两笔交易中消耗的UTXO其实是一样的。为了解决这个问题,我们引入了nullifier的概念。简单来说,nullifier是UTXO的哈希值,同时也是UTXO接收者的私钥。然后,我们将交易的知识证明更改为以下内容:
给定默克尔根,以及两个哈希值OUT_HASH1和OUT_HASH2,另外两个哈希值NULLIFIER1和NULLIFIER2,
已知有四个这样的UTXO:IN1,IN2,OUT1,OUT2,两个默克尔证明P1和P2以及一个私钥x,
OUT1和OUT2对应的哈希分别是OUT_HASH1和OUT_HASH2;IN1和IN2中的接收者相当于与x对应的公钥X;默克尔证明P1和P2是在有确定的默克尔根的树中包含IN1和IN2的有效证明;IN1和IN2中的总和等于OUT1和OUT2中的总和;并且hash(IN1,x)等于NULLIFIER1,hash(IN2,x)等于NULLIFIER2
请注意,任何花费特定UTXO的交易计算的都是相同的NULLIFIER值,因为NULLIFIER仅取决于UTXO的哈希值和UTXO中公钥对应的私钥。由于上述知识证明中的NULLIFIER是在公开的“给定”语句(Clause)中,因此如果曾经发布过使用相同UTXO的两笔交易,每个人都可以得知它们具有相同的NULLIFIER,并舍弃后出现的那笔交易。同样值得注意的是,只要用于计算它的哈希是抗原像攻击的,NULLIFIER就不会显示有关输入型UTXO或接收方私钥的任何信息。
通证充提
上述类型的交易可用于池内资产的转移,但对于一个功能完善的交易池来说,必须同时支持池内外的资金转移。因此,我们在私密交易的格式中加入了一个额外的字段,称为delta,如此一来,输入型UTXO的总量就等于输出型UTXO数量+delta。
给定默克尔树,两个哈希值OUT_HASH1和OUT_HASH2,另外两个哈希值NULLIFIER1和NULLIFIER2,以及一个delta值。
已知对于IN1,IN2,OUT1,OUT2计4个UTXO,两个默克尔证明P1和P2,还有一个私钥x,
则OUT1与OUT2对应的哈希值分别为OUT_HASH1和OUT_HASH2;IN1和IN2中的交易接收方相当于与私钥x对应的公钥X;默克尔证明P1和P2是有给定哈希根的包含IN1和IN2的merkel树的有效证明;IN1和IN2数量等于OUT1,OUT2数量总和再加上delta值;hash(IN1,x)等于NULLIFIER1;hash(IN2,x)等于NULLIFIE2R。
需要注意的是,delta在给定语句中,因此是公开的。NEAR在处理上述类型的交易时,若delta值为负(即一笔私密交易中输入通证比输出通证少),多出来的通证会存入交易发起方的账户中;若delta值为正(即一笔私密交易中输入通证比输出通证多),则只有在剩余通证被补足时交易才有效。
交易手续费
私密交易禁止在新创建的UTXO和曾被用作交易输入的UTXO之间建立联系。不过对于任何将被记录在NEAR链上的交易而言,交易的发出者都必须支付gas费。也正是因为这个原因,该发出者的账户中必须有一定数量的NEAR通证。该账户可能不知通过什么手段获得了一些NEAR通证,从而在上文提到的两种UTXO之间创建了某种联系,私密交易的目的会因此大为受挫。
为了解决这个问题,我们在系统中引入了中继者(Relayer)的概念。假设Alice想给Bob发送一笔交易,并打算让Ryan充当中继者。这笔交易的gas费用小于Ⓝ1,而Alice愿意支付给RyanⓃ1,让Ryan帮他把交易上链。
Alice甚至可能都没有NEAR账户,此时的她创建了一笔私密交易。在这笔交易中,作为交易输入的两个UTXO的资金总额比作为交易输出的UTXO的资金总额正好少Ⓝ1。剩余的Ⓝ1被提取至交易提交方的账户中。Ryan从Alice处收集到这笔交易信息,验证其有效性,并用自己的账户提交交易。整个过程消耗的gas费小于Ⓝ1,但是他最后却得到了Ⓝ1的酬劳 。
Alice最终在没有暴露自己身份的情况下提交了一笔交易,Ryan则获得了一笔小额的回报。请注意在交互中,任何一方都无需取得他人的信任:Ryan不能以任何形式篡改交易,留给他的只有两个选择:要么提交,要么放弃。因此,对于Alice来说最大的风险就是她的交易可能没有被提交(这种情况下她可以请另一个中继人)。由于Ryan在交易提交之前就验证了交易,因此除非另一个中继人捷足先登,否则Ryan便不会有花费了gas却得不到回报的风险。
未来发展
上文描述的这种模型已经是一个可以完全满足私密交易的交易池了。接下来,笔者会从几个方面简要描述一下如何提高交易池的安全性或使用性。
支付
在我们描述如上交易的过程中,笔者曾提到当Alice悄悄将通证发给Bob的同时,也将新创建的UTXO分享给了对方。这一过程的实现需要Alice与Bob在链下交流,而这是我们不希望看到的。为了解决这一问题,我们可以对交易池进行扩展,使其可以存储所有的用交易接收方的公钥加密的UTXO。当Alice将资产转给Bob并创建2个新的UTXO后,Bob作为接收方,其公钥会被Alice用来对UTXO加密。
再来看Bob,他监控着所有新创建的UTXO,并试着用自己的私钥对其逐个解密。一旦他试到Alice创建的UTXO,则解密成功——这样,Bob就完全通过NEAR特有的链上沟通的方式发现了他们的UTXO。
解耦签名与证明
当Alice创建一笔交易后,她需要使用自己的私钥信息,设计一个复杂的证明。因此,计算该证明的机器需要获取她的私钥,而这是我们不愿意看到的。总的来说,私钥最好用某些外部的硬件设备加以存储,这类硬件的功能通常会比较单一,比如有些产品只有消息签名这一个功能。对于这种硬件来说,运算知识证明通常在他们的功能之外。
为了适配这些硬件,我们生成3个密钥,而不是传统的公私钥对:私钥,解译钥和公钥。在这种情况下,用公钥加密的某条信息可以使用解译钥来解密。同理,私钥签名可以通过解译钥来验证。只有公钥是大家都能看到的。解译钥被储存在一个运算交易证明的设备上,而私钥被储存在外接设备中,该设备只能对信息进行签名。
我们可以使用以下方式对知识证明做一些修改:
给定默克尔根,两个哈希值OUT_HASH1和OUT_HASH2,以及另外两个哈希值NULLIFIER1和NULLIFIER2
已知有IN1,IN2,OUT1,OUT2四个UTXO,两个默克尔证明P1和P2,一个解译钥d和一个签名s。
OUT1,OUT2对应的哈希值为OUT_HASH1和OUT_HASH2;IN1和IN2中的接收方相当于和d对应的公钥X;默克尔证明P1和P2是给定哈希根的默克尔树包含IN1,IN2的有效证明;IN1与IN2数量之和等于OUT1与OUT2的数量之和;hash(IN1,d)等于NULLIFIER1;hash(IN2,d)等于NULLIFIER2,s是message(IN1,IN2,OUT1,OUT2)的签名、并对于d有效。
当Alice想要创建一笔交易时,她会使用硬件设备签名(IN1,IN2,OUT1,OUT2),然后使用签名s在只能获取解译钥的机器上生成证明。需要注意的是,如果无法使用硬件设备,Alice就无法生成签名s,也就不能花出这笔钱——即使获取了用于生成证明的机器的使用权限也不行。
支持更多通证
私密交易并不仅仅局限于NEAR通证。开发人员只需作轻微调整,便可让交易池可以支持任何发行在NEAR平台上的通证。具体的实现方式本文不再赘述。