Aztec中的合规与选择性披露

Jack Gilcrest 发布于 2026-05-17 阅读 88

本文提出两种构建地址集合证明的方法:全隐私地址簿和公开链接地址集合证明。全隐私方法通过ECDH共享秘密和空值追踪确保交易双方已互加地址簿;公开方法牺牲部分隐私用公共计数器追踪交易次数。随后介绍地址交易摘要证明,包括节点序列化和标签密钥集证明,用于汇总所有交易记录。最后讨论合规证明类型如非交互证明、交易摘要证明、交易量或税务基础证明,并详细图解交易量证明流程。

地址集合证明

为了建立所有交易的完整视图,我们需要某种方式来证明我们交易过的完整地址集合。有几种方法可以实现。下面我们提出两种可能的路径,让合约集成所需的数据存储以创建“地址集合证明”。

完全私密的地址集合证明

为了私下证明一个人交易过的所有地址集合,我们引入“通讯簿”的概念。这是一个仅追加的索引默克尔树,我们将所有地址插入其中。要收集完整的地址集合,我们可以简单地从树中的索引 0 开始,遍历链表直到达到最高空值(其 next_idx 值为 0)。当然,要在合规证明中使用它,我们只需使用存储证明来注册树根。

然而,我们需要确保在这棵树中同时包含发送方和接收方,而发送方无法私下将值插入到接收方的通讯簿树中。为了解决这个问题,我们需要一个双方都必须调用的一次性授权握手函数:

// 伪代码,非完美 API / 简化输入
##[private]
fn add_to_address_book(counterparty: AztecAddress, membership_proof: MerkleProof) {
    // 推导 ecdh 共享密钥
    let nsk_secret = context.request_nsk_app(context.msg_sender());
    let shared_secret = derive_ecdh_shared_secret_using_aztec_address(nsk_secret, counterparty);
    // 计算通讯簿插入空值并推送
    let nullifier = hash([shared_secret, context.msg_sender(), counterparty]);
    context.push_nullifier(nullifier);

    // 插入通讯簿
    storage.address_book.insert(counterparty, membership_proof);
}

空值 H(shared_secret, caller, counterparty) 的存在足以表明调用方已将对方添加到其通讯簿,并且具有完全合规的设置——也就是说,使用通讯簿树的证明将始终要求发送给或接收自对方的票据被包含在合规证明中。然而,这一步并不能保证对方也调用了此函数。这意味着对方可以在不将其包含在证明中的情况下从调用方接收票据。因此,我们需要在私下转移票据的任何函数中添加一步:

// 伪代码
##[private]
pub fn transfer_private(from: AztecAddress, to: AztecAddress, amount: u128) {
    // 推导 ecdh 共享密钥
    let nsk_secret = context.request_nsk_app(context.msg_sender());
    let shared_secret = derive_ecdh_shared_secret_using_aztec_address(nsk_secret, counterparty);
    // 检查两个空值是否存在
    let caller_nullifier = hash([shared_secret, context.msg_sender(), counterparty]);
    let counterparty_nullifier = hash([shared_secret, counterparty, context.msg_sender()]);
    context.historical_header.prove_nullifier_inclusion(caller_nullifier);
    context.historical_header.prove_nullifier_inclusion(counterparty_nullifier);
    ...
    // 此处为正常转移逻辑
}

通过在任何票据转移函数开头包含此步骤,我们保证了双方都调用了 add_to_address_book(),并且所有参与方都被约束为生成包含来自这些地址的任何票据的证明。

这种构造的好处是整个系统是私密的——发出的空值被 ECDH 共享密钥适当地混淆,并且超出此范围的任何跟踪逻辑都发生在不离开客户端设备的私有状态中。

然而,缺点也很明显。最麻烦的是,除非双方都在线调用初始设置函数,否则你无法开始与一方进行交易。此外,我们在每个转移函数中都会产生不可忽视的约束增长。最后,必须存在用于跟踪通讯簿树叶节点的基础设施。客户端要么自己维护这些数据(存在丢失风险),要么端到端地加密这些数据,从而实质上泄露一些关于访问模式的信息。

公开链接的地址集合证明

我们可以引入一个公开计数器来牺牲一些隐私,而不是使用通讯簿。我们仍然需要空值来跟踪对方是否已递增空值,但只需在内部的公开函数中递增一个计数器:

##[storage]
struct storage {
    ...
    address_count: Map<AztecAddress, PublicMutable<(u32, u32), Context>, Context>
    ...
}
/// 伪代码
##[private]
pub fn transfer_private(from: AztecAddress, to: AztecAddress, amount: u128) {
    // 推导 ecdh 共享密钥
    let nsk_secret = context.request_nsk_app(context.msg_sender());
    let shared_secret = derive_ecdh_shared_secret_using_aztec_address(nsk_secret, counterparty);
    // 计算空值
    let nullifier = hash([shared_secret, context.msg_sender(), counterparty]);
    Token::at(context.this_address())
        .increment_counter(from, to, nullifier)
        .enqueue(&mut context);
    ...
    // 正常转移逻辑
}

##[public]
##[internal]
pub fn increment_counter(from: AztecAddress, to: AztecAddress, nullifier: Field) {
    if (!context.nullifier_exists(nullifier)) {
        context.push_nullifier(nullifier);
        let from_loc = storage.address_count.at(from);
        let mut from_count = from_loc.read();
        from_count.0 += 1;
        from_loc.write(from_count);
        let to_loc = storage.address_count.at(to);
        let mut to_count = from_loc.read();
        to_count.1 += 1;
        to_loc.write(from_count);
    }
}

我们不遍历地址映射,而是直接获取发送方/接收方计数的存储证明。然后,我们必须提供恰好这么多地址交易摘要证明,以满足包含所有发送方和接收方的约束。

显然,这种构造会泄露交易图隐私。可以通过将发送方映射存储为 PrivateMutable 而不是将两个计数存储在公共元组中来解除这种关联,从而仅公开表示某人开始与某个地址进行交易。然而,这引入了一种 griefing 攻击,即恶意发送方可以在不向接收方提供用于调用 pxe.registerSender(address) 的 Aztec 地址的情况下递增接收方的计数。约束性的加密交付此日志并不能解决这个问题,因为如果事先不知道地址,日志就无法解密。

地址交易摘要证明

一旦我们有了所有交互的摘要,我们就可以构建我们在给定合约上进行的所有交易的完整摘要。我们通过遍历每个地址并为每个地址创建一个“地址交易摘要证明”来实现这一点。此外,可能向证明者发出挑战,要求其总结与一个特定地址(而非整个集合)的交互,在这种情况下,摘要可以作为独立证明构建。

此描述中的地址交易摘要证明有点通用——具体来说,我们只关心获取与特定地址交易时使用的所有票据的完整集合。这些票据的具体用途则由实现决定。例如,我们可能想要跟踪税基,或确保特定时间范围内的交易量不超过 CTR 报告阈值。然而,通常这些证明提供了做出这些陈述的基础设施,而不是试图预测合规证明所需的确切要求。

票据序列化

与知道完整地址集合的问题类似,我们需要知道要包含的完整票据集合。我们通过为所有发送的票据添加序列号并跟踪我们发送给的每个接收方的序列高度来实现这一点:

// 伪代码
// 可能是一个扩展的 uint note
##[note]
struct MyNote {
    owner: AztecAddress,
    randomness: Field,
    serial: Field // 可能可以压缩到像 u16 以进行打包
    ...
    // 剩余票据字段
}

##[storage]
struct storage {
    serial_numbers: Map<AztecAddress, PrivateImmutable<u32, Context>, Context>,
    ...
    // 剩余存储值
}

pub fn transfer_private(from: AztecAddress, to: AztecAddress, amount: u128) {
    // 首先进行通讯簿Hook
    let serial_number_loc = storage.serial_numbers.at(to);
    let serial_number = serial_number_loc.read() + 1;
    serial_number_loc.write(serial_number)
    // 正常转移逻辑
    // 确保在发送时在票据中插入序列号
}

标记密钥

或者,在更具交互性且某种程度上可信的环境中,我们可以为地址集合证明中的所有地址创建一个“标记密钥集证明”。对于集合中的每个地址,我们只需证明我们的地址和对方地址之间的标记密钥推导。

todo: 找到包含此逻辑的代码

此解决方案有一些缺点:它需要向第三方披露标记密钥。这意味着,永久地,第三方将能够看到每次你和对方之间发送票据的时间。然而,他们只会看到票据的密文——而不是票据的内容。如果我们愿意接受这个缺点,那么使用标记密钥将成为一个非常强大的解决方案。

首先,只需要对合约进行地址集合证明修改——不需要票据序列化逻辑!其次,我们可以在交互式设置中加速证明。一旦验证者收到标记密钥集证明,他们就可以自行收集所有密文,并交互式地挑战证明者以证明每个票据的属性,而不是聚合所有内容。

当然,如果我们要保持对任何单个交易属性的更高保密级别,验证者可以创建他们想要揭示的所有票据哈希的默克尔树,证明者可以递归地验证总结他们与某个地址整个交易历史的证明,同时证明所有票据都被包含。

合规证明类型

我们可以设想几种有用的合规证明类型:

与地址无交互的证明

这只需通过检查空值 H(ecdh_secret, sender, recipient)H(ecdh_secret, recipient, sender) 不存在来处理。这可以针对黑名单合规进行默克尔化。

地址交易摘要证明:

上述通过票据序列化/标记密钥证明描述。

交易量或税基证明

结合地址集合证明和地址交易摘要证明,为制作这些证明提供了通用基础设施。一个简单的交易量证明如下图所示。

图解交易量证明

步骤 0:验证者设定最大区块高度(例如 2025 年产生的最后一个区块)

步骤 1:证明者执行标记密钥集证明(TKSP)

image

步骤 2:证明者将 TKSP 发送给验证者(链下),验证者通过对照步骤 0 设定的区块验证证明来确认标记密钥的真实性。

步骤 3:验证者加载标记密钥并收集每个账户的所有密文。

步骤 4:对于每个账户,验证者生成一个“账户票据密文树”,其中包含他们想要证明交易量的所有密文的票据哈希。

步骤 5:验证者生成一个“票据密文树”,这是一个树的树——包含每个账户票据密文树的根。

步骤 6:验证者将票据密文树(以及任何所需的成员资格见证数据)共享给证明者。

步骤 7:对于证明者与之交互的每个地址,他们创建一个“账户交易量摘要证明”(ATVSP)

image

步骤 8:一旦证明者为票据密文树中的每个叶子构建了每个 ATVSP,他们就构建一个最终的“交易量摘要证明”(VSP),将所有交易聚合为单一可证明的交易量总和。

image

步骤 9:证明者将 VSP 发送给验证者。由于验证者使用 TKSP 的标记密钥构建了票据密文树,因此他们可以完全确信整个交易历史已被包含在 VSP 中,并且输出的交易量是准确的。

其他想法

以下来自初版探索草案的其他一些想法。

纪元查看密钥

用户可以通过与查看者创建共享密钥(该密钥源自共享密钥加上某个纪元编号)来揭示特定纪元(例如一个月、一年等)的所有票据。每次加密时都必须推导加密密钥,以确保使用当前的纪元编号。这为查看者提供了完全可见性,但限制了时间范围。

这对于自动税务信息转储可能比对于监管合规更可取。

暴力解密

注意:我们如何同时实现解密失败和不回退电路?

查看者可以请求在从 X 到 Y 的范围内所有区块的合规证明。然后用户必须为每个区块构建一个证明,证明每个票据要么可以解密(将其添加到要证明的票据堆栈中),要么不可以解密。这将耗费大量资源,但不需要任何额外集成来促进。

一旦用户生成了一个证明,展示了所有发送到他们地址的票据,他们执行上述两个部分中描述的相同选择性披露步骤。

合规后门/机密转移

很简单,一个(过于)合规的智能合约可能会选择复制所有日志并将副本发送给查看者密钥。这种解决方案缺乏想象力,并且可能对隐私构成危险,但也可能是像当前大型托管稳定币等服务感到放心允许交易在私有链上进行的必要条件。

MPC 存储

就像合规后门一样,智能合约可以复制重复日志并将副本加密给查看密钥。然而,这些日志被加密给一个密钥,其份额分散在 MPC 网络中(例如 NillionDB)。查看者可以随后向 MPC 网络请求提供数据。

这可以通过两种方式发生——

  1. 代理重加密:MPC 网络执行计算,将其自身密钥的数据重新加密给查看者,而无需实际解密数据。
  2. TEE 重加密:加密数据仅在 TEE 内解密,在其中发生向查看者密钥的重加密。
  • 原文链接: hackmd.io/bVRw0Rc4TNOFy2...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论