1. 背景分析
前段时间开展了批量匿踪查询算法迭代优化的工作,取得了一些进展。不得不说,甲方爸爸永远会提出非常有挑战性的目标,push你去想各种解决方案。在实际的算法研发落地上,我们会结合算法本身的机制改进以及工程优化这两方面来尽可能逼近目标。
在这期间,调研了一些学术资料和成果,其中有两篇比较新的论文,在批量匿踪查询做了一些改进,分别是:vector-batch pir(S&P '23) 【1】 以及 PIRANA【2】。批量匿踪查询(batch pir)的思想主要是通过批量的处理,来实现对查询向量计算的耗时均摊,这样能够获得比较好的计算开销和通信开销,最终实现比多次单个查询的任务整体时间短的多的目的。vector-batch pir通过返回多项查询结果降低开销,而PIRANA是在vector-batch pir基础上,做了进一步的优化,主要体现在对比现有基于定权码的解决方案(Usenix SEC ’22)性能有大幅提升,批量查询场景友好,相比(S&P '23) 方案提速明显。
如果有同学对匿踪查询不了解的话,可以再看下我们之前的几篇文章:《隐私计算匿踪查询技术深入浅出》、《Simple PIR-单服务器开源最快匿踪查询算法解析》。另外也可以了解下不经意传输以及安全求交等基础技术:《OT&OT扩展(不经意传输扩展)深入浅出》、《不经意传输协议(OT/OTE)的进一步补充》、《不经意传输OT及扩展协议OT Extension的进一步探索》、《隐私集合求交(PSI)原理深入浅出》。还有匿踪查询其实只是保护了查询方的信息,并不保护服务端,可以看下《隐私计算使用不当也会泄露原始数据》中的分析。
本文主要会对PIRANA做算法原理的分析。网上也有几篇对PIRANA的介绍,但看下来感觉解释的并不是特别清晰。所以本文会结合原始论文【2】以及作者的分享视频【3,4】做一些个人的技术解读。
在具体介绍算法之前,贴一下作者对于现有单服务器pir的一些观点,个人还是挺赞同的,在日常的工作中,确实也同样考虑到了类似的问题,比如像simple pir,需要依赖大量的预处理工作,并且一些算法需要提前发送处理后的数据到客户端进行存储,这类算法对于数据库经常更新的场景,以及给予客户端的压力,在实际业务场景中很难真正落地。比如服务端数据庞大或者经常更新,客户端与服务端之间的交互机制也需要更多额外的设计。这类方案往往看似很美好,真的应用到业务中,就会出现各种局限性和掣肘。想起来隐语提出的基于ecdh的非平衡求交算法,同样有类似的问题需要考虑解决,但当前隐私计算的使用还处于初期,所以这类问题并没有明显暴露出来,还有一定的时间和空间做进一步的思考和完善。
2. 算法原理介绍
了解PIRANA之前,需要对全同态加密(BFV)、定权码 (Constant-Weight Codes)以及概率批处理码(Probabilistic Batch Codes)。
2.1 预备知识
2.1.1 全同态加密(BFV)
全同态加密 (Fully Homomorphic Encryption, FHE) 允许对加密数据直接进行计算,而无需解密。BFV(Brakerski-Fan-Vercauteren)是全同态加密的一种具体实现方案,属于基于Learning with Errors类的加密方案。BFV 支持对加密数据进行加法和乘法运算,同时保持数据加密状态。BFV 方案适合处理加法、乘法等整数运算,适用于需要执行加法链或多次乘法链的场景。
下图展示了相关的计算示例说明,并且支持Single Instruction Multiple Data,在BFV全同态加密算法中,数据以多项式的形式存储在密文中。通过多项式环的构造,SIMD 技术可以让一个密文承载多个独立的数据项,并且能够对这些数据项并行执行相同的操作(如加法或乘法)。这也就意味着一次加密操作可以同时对多个数据项进行操作,从而减少重复计算的时间开销。另外,给出了各种操作耗时的比较:SIMDMul > SIMDRotate > SIMDPmul > SIMDAdd,密文乘密文最耗时,其次是密文旋转,再是明密文乘法和密文加法。
2.1.2 定权码 (Constant-Weight Codes)
CwPIR(Usenix SEC ’22)中引入了定权码技术,PIRANA相对于CwPIR实现了进一步的优化。Constant-Weight Codes是一类特殊的编码方案,所有合法编码字的汉明权重(Hamming weight)相同。汉明权重是指编码字中值为 1
的比特数,因此 Constant-Weight Codes 中的每个代码字具有相同数量的 1
,并且这些 1
的位置在不同的编码字中是有差异的。
假设你有一个编码字的长度为 5 的二进制码,并且规定所有编码字的汉明重量为 3。可以构造的合法 Constant-Weight Codes 如下,这些编码字都是 5 位长,每个编码字有 3 个 1。
11100, 11010, 11001, 10110, 10101, 10011, 01110, 01101, 01011, 00111
用数学描述就是:
给定一个码字长度 n
和重量 w
,Constant-Weight Codes 是从长度为 n
的二进制向量中选取所有重量为 w
的编码字集合。可以通过组合数计算可能的编码字总数,公式为:
这是从长度为 n
的二进制序列中选择 w
个位置为 1
的组合数。
在匿踪查询中,对于定权码的使用,是将索引进行定权码编码,比如这里举例使用CW(8, 2),对1-n的索引值进行编码,索引1编码为00000011,索引2编码为00000101,简单理解就是将索引与定权码建立既定规则的映射关系,客户端和服务端的编码规则是保持一致的。然后涉及到一种相等判断,当x和y相等的时候,才为1,否则为0,这里的x和y理解为两个索引值,比如客户端查询索引为x,服务端数据索引为y,只有匹配的项才是1,否则为0,这个应该比较好理解,因为你要拿到的结果就是需要查询的。用一个简单的图帮助理解下为什么这么设置:第一列是客户端的选择向量(密文形态),第二列是服务端的payload。
再接着解释下怎么处理相等,因为客户端传给服务端的定权码是密文的,服务端其实只做计算,计算出客户端查询索引的位置为密文的1,并不是显式地去展示匹配到哪个索引。因为需要保护查询向量。计算的逻辑比较简单,就是按照既定的索引与定权码的匹配规则进行,比如图中举例的查询索引定权码密文为(enc[0], enc[0], enc[0], enc[0], enc[0], enc[1], enc[0], enc[1])。由于k=2,所以计算一个索引只需要1次乘法,也就是k-1。然后需要计算n次,也就是对n个索引都计算一遍,总的复杂度就是n(k-1)密文-密文乘法。比如对于索引1,那么按照既定定权码规则,就是用最右侧的第一个密文与第二个密文做乘法,即enc[1] * enc[0] = enc[0]。同理,对于索引2,用最右侧的第一个密文与第三个密文做乘法,即enc[1] * enc[1] = enc[1]。依次类推,完成n个索引的密文乘法计算。其中要查询的对应索引密文结果是enc[1],其他都是enc[0],所以再进一步与服务端payload的相乘再相加,就能拿到最终的目标payload的密文结果,发回给客户端解密,就完成了一次单条匿踪查询任务。
2.1.3 概率批处理码(Probabilistic Batch Codes)
批处理码(Batch Codes, BC)是一类编码技术,目标是减少计算负担,但通常会带来网络通信开销的增加。在传统的批处理码中,访问任何 k个元素时,系统总能通过查询不同桶中的码字一次性恢复这些元素。但是这种设计为确保每次查询都成功,增加了复杂性和网络开销。
为了优化这一问题,概率批处理码(PBC)通过放宽传统批处理码的保证,引入一定失败概率 p,允许在少数情况下无法恢复特定集合的 k 个元素。这种设计可以明显减少计算复杂性和通信开销,同时保持了高效的数据恢复能力。
PBC 有三个主要模块算法组成:编码(Encode)、调度生成(GenSchedule)和解码(Decode)。工作原理如下:
Encode:编码算法将 n 个元素编码成 b 个桶,每个桶中包含多个码字。每个桶存储的码字数量不一,总码字数为 m,且 。这些码字通过冗余设计以保证容错性和高效恢复。
输入:DB 数据库,包含 n 个元素。
输出:b 个桶,其中存储 m 个冗余码字。
GenSchedule:调度生成算法根据请求的 k 个索引 I(代表需要检索的元素),生成一个调度计划 ,为每个索引分配一个或多个桶,从这些桶中可以检索码字以重构元素。如果不能生成满足所有元素要求的调度,则算法返回失败 ⊥,此事件的发生概率为 p。
输入:需要访问的 k 个索引 I。
输出:调度计划 σ 或失败标志 ⊥。
Decode:解码算法根据调度返回的码字集合 W,恢复并输出相应的数据元素。
输入:码字集合 W。
输出:恢复的元素。
PBC 的设计借鉴了许多数据结构的hash技术,包括布谷鸟hash。通过这种策略生成冗余数据,使得在查询时只需访问部分桶即可恢复所需元素。相比于传统批处理码,PBC 放宽了查询时的恢复保证。具体来说,PBC 在查询时有一定概率 p 无法恢复 k 个元素,但这种失败的概率非常小(如1/万亿次)。PBC 应用于多查询私密信息检索(Multi-query PIR)中,客户端在发出所有查询前就会得知是否能够成功恢复所有元素,因此可以提前调整查询策略。
在多查询隐私信息检索(PIR)系统中,用户希望在保证隐私的前提下,从一个分布式数据库中检索多个数据元素。假设数据库中有 n 个元素,用户希望查询 k 个元素。
步骤 1:数据编码。首先,数据库 DB 中的 n 个元素通过 PBC 编码成 b 个桶,这些桶分布在多个服务器或节点上。每个桶中存储的是数据库元素的冗余码字。
步骤 2:生成调度计划。用户发出查询请求,表示需要查询的 k 个元素的索引 I。系统通过 GenSchedule 算法生成一个查询计划,决定从哪些桶中检索码字。根据查询成功的概率设计,该计划在大多数情况下能够成功生成。
步骤 3:解码恢复。用户根据查询计划从指定的桶中检索码字,然后通过 Decode 算法恢复出所需的 k 个数据元素。
2.2 PIRANA单次匿踪查询
下图展示了PIRANA单条匿踪查询的流程,这里对处理流程做一下解释:
首先,数据库会排列成矩阵的形式,也就是N行t列,t是数据总条数n/N然后向上取整得到。比如数据库中有1000000条数据,N取100,那么t就是10000。
然后客户端需要查询第条数据,比如第N+2,如果N是100,那么N+2就是102。至于为什么知道要查哪一条,可以在前述步骤先用PSI求交技术获得。然后计算出第条数据位于数据库中的坐标信息(row, col)。在 PIRANA 算法中,数据库中的所有数据被组织成一个 N 行的矩阵。为了将第 i个元素映射到矩阵的具体位置,算法将其分配到矩阵中的某一行和某一列。计算逻辑如下:
行 r: 利用 计算出第 i 个元素所在的行。这里的操作是取模运算,将索引 i 以 N 为模进行计算,确保结果在 [0, N-1] 的范围内。这样可以确保第 i 个元素分布在矩阵的某一行中。
列 c: 通过 计算出第 i 个元素所在的列。这里的操作是将 i 除以 N,并取上整,确保每列的元素分布均匀,避免元素遗漏或过多。
然后使用定权码对列索引进行编码,例子中列索引为2,所以可以采用我们在定权码章节介绍的方法进行编码,比如采用CW(8,2)的话,列索引为2可以编码成[0,0,0,0,0,1,0,1]。最终处理成的查询向量其实会变成一个矩阵的形式:N行m列,m是CW中的维度参数。而且因为我们要查询的对象位于第2行,所以列索引定权码有值的只会出现在第二行,其他行都用0填充。这样就可以完成一次查询索引的编码了。然后客户端将该查询矩阵进行同态加密成密文,发送给服务端。
伪代码如下:
服务端接收到查询矩阵后,对每一行的定权码,需要执行t(k-1)次密文计算,得到t个索引的密文值,当然只有我们要查询的那一列的索引值会是enc[1], 其他列都是enc[0]。执行方式可以参考我们在上述定权码 (Constant-Weight Codes)章节的介绍。服务端执行完就可以恢复出N x t 大小的密文查询矩阵。
接下来就是将该密文查询矩阵与数据库的明文payload矩阵进行点乘,这里采用多项式的SIMDPmul算子计算,能提速不少。然后再将密文结果合并为N行1列,返回给客户端进行解密。
(small payload场景)伪代码如下:
理解了small payload单条查询后,其他对于该算法的实现的原理已经基本理解了。后续更多的会讨论其他场景的适配和调整。
对于large payload的场景,作者提出的解决方案是将大的payload切分成多个小的payload,然后通过旋转的方式,在服务端进行压缩返回的密文大小。这样虽然降低了通信的开销,但是密文的旋转操作成为了新的性能瓶颈。
进一步地,讨论了如何降低旋转次数的解决方案,提出通过比较旋转选择向量、旋转内积结果、以及将这两种方法结合,探讨了降低总旋转次数的可能性。如果选择旋转选择向量,因为是在密文状态下执行,所以需要旋转t(N-1)。
伪代码如下:
单次查询性能对比:
2.3 PIRANA批量匿踪查询
批量匿踪是采用基于布谷鸟hash的PBC来实现,思路就是将k个查询向量分到b个筐,然后将每个筐视为一行,来打包全部的query,每一行发送一个索引定权码,就可以实现批量的效果。
具体实施步骤如下:
设置(Setup)
选择同态加密的参数并生成密钥。服务端 S 使用批处理编码 BC(n, M, L, B) 对其数据库进行编码:
其中, 表示第 i 个桶中的编码向量。如果桶的数量 B 小于 N,则将每个桶分成 s := N/B 个小桶(为简单起见,假设 B 可以整除 N)。也就是说,数据库实际上被编码为:
查询(Query)
客户端 C 运行Multi-query query算法生成查询密文。更具体地,给定 L 个原始索引 ,客户端首先运行批处理编码的生成计划函数 来生成每个 B 个桶的索引 。如果桶被划分为 N 个小桶,则需要计算相应的索引。
由于每个桶被划分为 行,索引可以按两种方式分组:可以将这 s 个索引组合在一起(例如, 是第一个桶的索引),或者将不同桶的索引组合在一起(例如, 是第一个桶的索引)。论文选择后一种方式,因为它对大容量数据更为友好。
接下来,按照类似单次查询的query算法的方式运行,区别在于它需要为每个 N 槽计算一个定权码字 ,并使用 来确定该插槽的值。
应答(Answer)
服务端 S 运行multi-query answer算法来批量响应这些 L 个查询。它与单次查询的answer算法大体相同,区别在于 是每个桶中第 j个元素的组合。
提取(Extract)
客户端 C 解密 ,得到。然后运行:
批量匿踪查询性能对比:
这个性能其实相对于MR这篇文章确实在回答响应上提升了不少。但是在数据库量级和批量查询的量都不是很大的情况下,通信量可能依然过大。可能还是难以满足限制带宽下的大批量处理的实际业务要求。绝对的任务处理时间,也可能过长。
2.4 PIRANA-LPSI
3. 参考材料
【1】Vectorized Batch Private Information Retrieval
【2】PIRANA: Faster Multi-query PIR via Constant-weight Codes
【3】91 PIRANA Faster Multi query PIR via Constant weight Codes
【4】Live #10 《PIRANA: Faster Multi-query PIR via Constant-weight Codes》论文解读