这篇报告是zkSecurity对Penumbra主要电路的审计结果。Penumbra是一个Cosmos zone,其主要功能包括多资产屏蔽池和去中心化交易所(DEX)。报告详细介绍了Penumbra中零知识证明的应用,包括交易、多资产屏蔽池、治理、去中心化交易以及质押等方面的电路逻辑和实现。
我们已经审计了 Penumbra 的主电路,并发布了发现的报告。你可以在 Penumbra 的博客文章中阅读,或 直接阅读报告。
在发现的 8 个问题中,Penumbra Labs 团队认为影响最大的两个错误是“double spend(双重支付)”和“double vote(双重投票)”错误(zkSecurity 评为高风险,见报告项目 #0 和 #1),每个错误都有明确的利用路径。截至发布之时,Penumbra Labs 团队已经解决了所有高于“信息性”的问题,这些问题由两个审计团队中的任何一个发现,并通过与原始审计执行者的后续跟进审查确认了这些修复。详细信息可以在审计报告中找到,但我们想总结两个关键发现及其解决方案如下。
正如我们在报告中指出的那样,代码被发现有完整的文档记录,经过严格的测试,并且有良好的规范。
我们很高兴看到 Penumbra 团队对我们的发现反应非常迅速,并且我们期待看到该项目尽快启动。
以下是我们报告引言的复制/粘贴。
Penumbra 是一个 Cosmos zone(Cosmos 生态系统中的一个特定于应用程序的区块链),其中的主要 token 用于将权力委托给验证者并对提案进行投票。该协议允许 Penumbra token 本身以及外部 token(通过 IBC 协议 连接到 Penumbra)存在于一个类似于 Zcash 的 shielded pool 中,其中同一 token 中的交易提供完全的隐私。
该协议的主要功能是一个去中心化交易所(也称为 DEX),用户可以在其中公开交易 token(通过将其受保护的 token 转移到开放头寸),或者如果他们选择以市场价格交易,则以私密方式进行交易。
为了启用 Penumbra 的隐私功能,使用了 zero-knowledge proofs。具体来说,Groth16 证明系统用于 BLS12-377 椭圆曲线之上。
为了在电路中执行群组运算,ZEXE 论文中介绍的 Edwards 曲线 被选为“内部曲线”。此外,该曲线从不直接使用,而是通过 decaf377 抽象/编码来使用。
在接下来的章节中,我们将介绍协议的不同方面,当与协议的 zero-knowledge proofs 功能相关时。
Penumbra 中的 zero-knowledge proofs 可以在用户的交易中观察到,证明特定的声明对验证者来说是正确的,并允许验证者在共识协议中安全地执行状态转换。
每个交易可以包含一个 actions 列表,定义在 specification 中。并非所有 actions 都包含 zero-knowledge proofs,因为其中一些 actions 将公开执行。修改余额的 actions 之间的交互,特别是在私有 actions 之间,或在私有 actions 和公共 actions 之间,发生在承诺中隐藏的交互。
例如,作为单个交易的一部分,一些 actions 可能会为隐藏在承诺中的某些 token 创建正余额,而另一些 actions 将为隐藏在承诺中的一些 token 创建负余额。结果将被验证为对 0 余额的承诺,无论是在公开场合还是通过用户的证明(因为承诺可以隐藏)。(该证明也通过使用 binding signatures 嵌入到用户的签名中。)
在代码库中,每个组件都与其他组件清晰地分离,并且包含在 crate(Rust 库)中。Actions 被编码在不同 crates 下的 action.rs
文件中,电路被编码在同一 crate 下的 proof.rs
文件中。一些 gadget 在 r1cs.rs
文件下实现。
电路是使用 arkworks r1cs-std 库实现的,因此可以很容易地找到作为电路结构上的 ConstraintSynthesizer
trait 的实现:
pub trait ConstraintSynthesizer<F: Field> {
fn generate_constraints(self, cs: ConstraintSystemRef<F>) -> crate::r1cs::Result<()>;
}
要形成一个交易,用户需要遵循一个“transaction plan(交易计划)”。交易的实现总是将逻辑引导到 plan.rs
文件,其中包含 actions 的创建,包括为私有 actions 创建证明。
另一方面,对于每个组件,总是可以找到一个 action_holder/
文件夹,它将确定验证者需要如何处理 actions。对于私有 actions,这将导致验证者验证交易 actions 中包含的证明。验证分为两个部分,一个是有状态的,执行需要读取状态的检查,另一个是无状态的,执行任何其他检查:
pub trait ActionHandler {
type CheckStatelessContext: Clone + Send + Sync + 'static;
async fn check_stateless(&self, context: Self::CheckStatelessContext) -> Result<()>;
async fn check_stateful<S: StateRead + 'static>(&self, state: Arc<S>) -> Result<()>;
async fn execute<S: StateWrite>(&self, state: S) -> Result<()>;
}
ActionHandler
也为交易实现,它将为交易中包含的每个 action 调用相关的 action handlers。除此之外,它还会将每个 action 的 balance commitment 相加,以确保(使用前面提到的签名绑定方案)它们都加起来为 0。
在其余部分中,我们将回顾与 zero-knowledge proofs 相关的不同 actions,同时给出电路的高级伪代码描述(不包括 gadgets 和 building blocks)。
与 Zcash 类似,penumbra 中的值以承诺的形式存储在 Merkle 树中。Penumbra 的 Merkle 树的一个特殊之处在于,每个节点都链接到四个子节点。从技术上讲,它也是一个超树(一个树的树),其中叶子树包含在一个区块中创建的所有承诺,中间树包含在一个 epoch 中创建的所有区块,顶层树路由到不同的 epochs。
为了花费一个承诺,一个 spend action(及其 spend proof)必须存在于交易中。实际上,spend proof 验证了 note 的存在,并且它从未被花费过。揭示承诺本身可能允许验证者检查承诺是否以前被花费过,这会导致较差的隐私。因此,另一个与承诺密切相关的值被派生出来(可证明的),称为 nullifier。从某种意义上说,如果比特币跟踪所有未花费的 outputs,那么 Penumbra 跟踪所有已花费的 outputs。
在伪代码中,spend 电路的逻辑如下:
def spend(private, public):
# private inputs(私有输入)
note = NoteVar(private.note)
claimed_note_commitment = StateCommitmentVar(private.state_commitment_proof.commitment)
position = PositionVar(private.state_commitment_proof.position)
merkle_path = MerkleAuthPathVar(private.state_commitment_proof)
v_blinding = uint8vec(private.v_blinding)
spend_auth_randomizer = SpendAuthRandomizer(private.spend_auth_randomizer)
ak_element = AuthorizationKeyVar(private.ak)
nk = NullifierKeyVar(private.nk)
# public inputs(公有输入)
anchor = public.anchor
claimed_balance_commitment = BalanceCommitmentVar(public.balance_commitment)
claimed_nullifier = NullifierVar(public.nullifier)
rk = RandomizedVerificationKey(public.rk)
# dummy spends have amounts set to 0(虚拟花费的金额设置为 0)
is_dummy = (note.amount == 0)
is_not_dummy = not is_dummy
# note commitment integrity(note 承诺完整性)
note_commitment = note.commit()
if is_not_dummy:
assert(note_commitment == claimed_note_commitment)
# nullifier integrity(nullifier 完整性)
nullifier = NullifierVar.derive(nk, position, claimed_note_commitment)
if is_not_dummy:
assert(nullifier == claimed_nullifier)
# merkle auth path verification against the provided anchor(针对提供的锚点的 merkle 认证路径验证)
if is_not_dummy:
merkle_path.verify(position, anchor, claimed_note_commitment)
# check integrity of randomized verification key(检查随机验证密钥的完整性)
computed_rk = ak_element.randomize(spend_auth_randomizer)
if is_not_dummy:
assert(computed_rk == rk)
# check integrity of diversified address(检查多样化地址的完整性)
ivk = IncomingViewingKey.derive(nk, ak_element)
computed_transmission_key = ivk.diversified_public(note.diversified_generator)
if is_not_dummy:
assert(computed_transmission_key == note.transmission_key)
# check integrity of balance commitment(检查余额承诺的完整性)
balance_commitment = note.value.commit(v_blinding)
if is_not_dummy:
assert(balance_commitment == claimed_balance_commitment)
# check the diversified base is not identity(检查多样化 base 不是 identity)
if is_not_dummy:
assert(decaf377.identity != note.diversified_generator)
assert(decaf377.identity != ak_element)
spend proof 在其公共输入中包含一个已提交的余额,公开了包含在该 note 中的余额的隐藏版本。由于 notes 可以持有不同类型的资产,因此对 notes 的承诺被计算为 Pedersen 承诺,该承诺对不同的资产使用不同的基点。
如前所述,验证者最终将检查来自交易的所有 actions 是否具有相互抵消的余额承诺。减少现在为正的余额的可能方法之一是为其他人(或自己)创建一个 spendable output(可花费 output)。执行此操作的 action 称为 output action,它只是公开了不同类型的(已提交的)余额:一个负余额,可能否定 spend action 的余额,但也可能否定其他 actions 的余额。一个 output proof 还公开了对其刚刚创建的新 note 的承诺,允许验证者将其添加到超树中。
该电路的伪代码如下:
def output(private, public):
# private inputs(私有输入)
note = NoteVar(private.note)
v_blinding = uint8vec(private.v_blinding)
# public inputs(公有输入)
claimed_note_commitment = StateCommitmentVar(public.note_commitment)
claimed_balance_commitment = BalanceCommitmentVar(public.balance_commitment)
# check the diversified base is not identity(检查多样化 base 不是 identity)
assert(decaf377.identity != note.diversified_generator)
# check integrity of balance commitment(检查余额承诺的完整性)
balance_commitment = BalanceVar(note.value, negative).commit(v_blinding)
assert(balance_commitment == claimed_balance_commitment)
# note commitment integrity(note 承诺完整性)
note_commitment = note.commit()
assert(note_commitment == claimed_note_commitment)
委托者投票是对提案的投票,该提案来自已将其某些 token 委托给验证者的人。他们的投票权与委托给该验证者的 token 总额中的份额有关。因此,委托者投票证明仅显示他们在验证者 Y 的池中拥有(或在提案创建时拥有)X 数量的委托 token,从而揭示了这两个值。此外,它还揭示了与当时的委托 note 相关的 nullifier,以防止重复投票。
为了换取投票,会创建一个 NFT 并且必须将其路由到 shielded pool 中的私有 output(使用 output action)。
委托者投票声明的伪代码如下:
def delegator_vote(private, public):
# private inputs(私有输入)
note = NoteVar(private.note)
claimed_note_commitment = StateCommitmentVar(private.state_commitment_proof.commitment)
delegator_position = private.state_commitment_proof.delegator_position
merkle_path = merkleAuthPathVar(private.state_commitment_proof)
v_blinding = u8vec(private.v_blinding)
spend_auth_randomizer = SpendAuthRandomizer(private.spend_auth_randomizer)
ak_element = AuthorizationKeyVar(private.ak)
nk = NullifierKeyVar(private.nk)
# public inputs(公有输入)
anchor = public.anchor
claimed_balance_commitment = BalanceCommitmentVar(
public.balance_commitment)
claimed_nullifier = NullifierVar(public.nullifier)
rk = RandomizedVerificationKey(public.rk)
start_position = PositionVar(public.start_position)
# note commitment integrity(note 承诺完整性)
note_commitment = note.commit()
assert(note_commitment == claimed_note_commitment)
# nullifier integrity(nullifier 完整性)
nullifier = NullifierVar.derive(
nk, delegator_position, claimed_note_commitment)
assert(nullifier == claimed_nullifier)
# merkle auth path verification against the provided anchor(针对提供的锚点的 merkle 认证路径验证)
merkle_path.verify(delegator_position.bits(),
anchor, claimed_note_commitment)
# check integrity of randomized verification key(检查随机验证密钥的完整性)
# ak_element + [spend_auth_randomizer] * SPEND_AUTH_BASEPOINT
computed_rk = ak_element.randomize(spend_auth_randomizer)
assert(computed_rk == rk)
# check integrity of diversified address(检查多样化地址的完整性)
ivk = IncomingViewingKey: : derive(nk, ak_element)
computed_transmission_key = ivk.diversified_public(
note.address.diversified_generator)
assert(computed_transmission_key == note.transmission_key)
# check integrity of balance commitment(检查余额承诺的完整性)
balance_commitment = note.value.commit(v_blinding)
assert(balance_commitment == claimed_balance_commitment)
# check elements were not identity(检查元素不是 identity)
assert(identity != note.address.diversified_generator)
assert(identity != ak_element)
# check that the merkle path to the proposal starts at the first commit of a block(检查提案的 merkle 路径是否从区块的第一个 commit 开始)
assert(start_position.commitment == 0)
# ensure that the note appeared before the proposal was created(确保 note 出现在提案创建之前)
assert(delegator_position.position < start_position.position)
Penumbra 的主要功能(除了其 multi-asset shielded pool)是一个去中心化交易所(也称为 DEX)。在 DEX 中,peer 可以通过创建头寸公开交换不同的 token 对。
由 position actions(头寸 actions) 创建的头寸会创建作为非隐藏承诺的负余额,旨在抵消由 spend actions 创建的 spending。由于头寸是 DEX 的 public(公共) 做市功能,允许用户公开使用不同的交易策略(或为 token 对提供流动性),因此我们不会在本文档中对此进行更多讨论。
DEX 的另一面是 Zswaps(Z 交换),它允许用户以“市场价格”私下交易 token。一个 Zswap 分为两个步骤:一个 swap(交换),和一个 swap claim(交换声明)(或 sweep(清扫))。
swap action 和 proof 采用一个私有的 swap plaintext(交换明文),它描述了正在交换的 token 对,以及每种资产要交换的 token 数量。拥有两个金额的意图是隐藏正在交易的内容。因此在实践中,只有一个金额将被设置为非零。
该证明可验证地发布了交换明文的承诺,这将允许用户在交易执行后声明他们的 token。交换明文的承诺被派生为一个唯一的资产 id,以便像价值为 1 的承诺 note(即作为一个 non-fungible token(不可替代 token))一样存储在同一个 multi-asset shielded pool 中。
此外,该证明还发布了负余额的隐藏承诺,从用户的余额中减去两个金额。验证者最终将验证与这些负余额匹配的正余额是在同一交易的其他 actions 中创建的。
(请注意,预付费用也会从余额中扣除,这将允许交易稍后在不必花费 notes 来支付交易费用的情况下声明该交易的结果。)
swap 声明的伪代码如下:
def swap(private, public):
# private inputs(私有输入)
swap_plaintext = SwapPlaintextVar(private.swap_plaintext)
fee_blinding = uint8vec(private.fee_blinding_bytes)
# public inputs(公有输入)
claimed_balance_commitment = BalanceCommitmentVar(
public.balance_commitment)
claimed_swap_commitment = StateCommitmentVar(public.swap_commitment)
claimed_fee_commitment = BalanceCommitmentVar(public.fee_commitment)
# swap commitment integrity check(swap 承诺的完整性检查)
swap_commitment = swap_plaintext.commit()
assert(swap_commitment == claimed_swap_commitment)
# fee commitment integrity check(费用承诺的完整性检查)
fee_balance = BalanceVar.from_negative_value_var(swap_plaintext.claim_fee)
fee_commitment = fee_balance.commit(fee_blinding)
assert(fee_commitment == claimed_fee_commitment)
# reconstruct swap action balance commitment(重建 swap action 余额承诺)
balance_1 = BalanceVar.from_negative_value_var(swap_plaintext.delta_1)
balance_2 = BalanceVar.from_negative_value_var(swap_plaintext.delta_2)
balance_1_commit = balance_1.commit(0) # will be blinded by fee(将被费用盲化)
balance_2_commit = balance_2.commit(0) # will be blinded by fee(将被费用盲化)
transparent_balance_commitment = balance_1_commit + balance_2_commit
total_balance_commitment = transparent_balance_commitment + fee_commitment
# balance commitment integrity check(余额承诺的完整性检查)
assert(claimed_balance_commitment == total_balance_commitment)
一旦一个区块成功处理了包含此交易的交易,用户就可以声明交易的结果(即交换的 token)。为此,必须提供一个 swap claim proof,其中用户在承诺树中提供已提交的交换明文的路径,并将其交换(或转换)为两种交易 token 的两个已提交的余额(然后可以使用同一交易中的 output actions 将其路由到 output notes)。
该电路的伪代码如下:
def swap_claim(private, public):
# private inputs(私有输入)
swap_plaintext = SwapPlaintextVar(private.swap_plaintext)
claimed_swap_commitment = StateCommitmentVar(
private.state_commitment_proof.commitment)
position_var = PositionVar(private.state_commitment_proof.position)
position_bits = position.to_bits_le()
merkle_path = MerkleAuthVar(private.state_commitment_proof)
nk = NullifierKeyVar(private.nk)
lambda_1_i = AmountVar(private.lambda_1_i)
lambda_2_i = AmountVar(private.lambda_2_i)
note_blinding_1 = private.note_blinding_1
note_blinding_2 = private.note_blinding_2
# public inputs(公有输入)
anchor = public.anchor
claimed_nullifier = NullifierVar(public.nullifier)
claimed_fee = ValueVar(public.claim_fee)
output_data = BatchSwapOutputDataVar(public.output_data)
claimed_note_commitment_1 = StateCommitmentVar(public.note_commitment_1)
claimed_note_commitment_2 = StateCommitmentVar(public.note_commitment_2)
# swap commitment integrity check(swap 承诺的完整性检查)
swap_commitment = swap_plaintext.commit()
assert(swap_commitment == claimed_swap_commitment)
# merkle path integrity. Ensure the provided note commitment is in the TCT(merkle 路径完整性。确保提供的 note 承诺在 TCT 中)
merkle_path.verify(position_bits, anchor, claimed_swap_commitment)
# nullifier integrity(nullifier 完整性)
nullifier = NullifierVar.derive(nk, position_var, claimed_swap_commitment)
assert(nullifier == claimed_nullifier)
# fee consistency check(费用一致性检查)
assert(claimed_fee == swap_plaintext.claim_fee)
# validate the swap commitment's height matches the output data's height (i.e. the clearing price height)(验证 swap 承诺的高度是否与 output 数据的高度匹配 (即清算价格高度))
block = position_var.block # BooleanVar[16..32] as FqVar
note_commitment_block_height = output_data.epoch_starting_height + block
assert(output_data.height == note_commitment_block_height)
# validate that the output data's trading pair matches the note commitment's trading pair(验证 output 数据的交易对是否与 note 承诺的交易对匹配)
assert(output_data.trading_pair == swap_plaintext.trading_pair)
# output amounts integrity(输出金额完整性)
computed_lambda_1_i, computed_lambda_2_i = output_data.pro_rata_outputs(
swap_plaintext.delta_1_i, swap_plaintext.delta_2_i)
assert(computed_lambda_1_i == lambda_1_i)
assert(computed_lambda_2_i == lambda_2_i)
# output note integrity(输出 note 完整性)
output_1_note = NoteVar(address=swap_plaintext.claim_address, amount=lambda_1_i,
asset_id=swap_plaintext.trading_pair.asset_1, note_blinding=note_blinding_1)
output_1_commitment = output_1_note.commit()
output_2_note = NoteVar(address=swap_plaintext.claim_address, amount=lambda_2_i,
asset_id=swap_plaintext.trading_pair.asset_2, note_blinding=note_blinding_2)
output_2_commitment = output_2_note.commit()
assert(output_1_commitment == claimed_note_commitment_1)
assert(output_2_commitment == claimed_note_commitment_2)
委托和取消委托到验证者的池中都是“half in the clear(半公开的)”。其中取消委托部分会受到延迟的影响。
委托通过提供在 multi-asset shielded pool 中花费已提交的 Penumbra notes 的 spend actions 和 proofs 来实现。然后,一个 public delegation action(委托 action) 共同用于从交易的余额中减去 Penumbra token 的数量,并将计算出的验证者的 token 数量添加到交易的余额中。所有这些额外的余额都以非隐藏方式提交,以便它们可以与私有 actions 的公开已提交余额进行交互。最后,output actions 和 proofs 可以使用正验证者 token 余额来在 multi-asset shielded pool 中生成新的 note 承诺(通过公开等量的负验证者 token 余额)。
取消委托分两步进行。第一步是 public 的,并将委托 token (通过 undelegation action(取消委托 action)) 转换为 unbounding tokens(解绑 token)。Unbounding tokens 是 token,其资产 id 派生自验证者的唯一身份以及 token 被解绑的 epoch。因此,一个 undelegation action 只是创建一个委托 token 的负余额(交易通过 spend actions 提供)和一个 unbounding token 的正余额(交易可以通过 output actions 将其存储回 multi-asset shielded pool)。
一旦观察到适当的延迟(对于适当的某些定义),用户可以向网络提交一个包含 undelegate claim action(取消委托声明 action) 的新交易。此 action 最终会消耗先前创建的 unbounding tokens,在应用penalty(惩罚)(在验证者可能行为不端的情况下)后,将它们转换回 Penumbra token。伴随该 action 的证明确保发布的余额承诺正确地包含 Penumbra token 的正余额(在正确应用penalty之后)和 unbounding tokens 的负余额。因此,这种 action 伴随着与 unbounding tokens 余额匹配的 spend actions 和与 Penumbra token 余额匹配的 output actions。
以下是取消委托声明电路的伪代码:
def undelegate_claim(private, public):
# private inputs(私有输入)
unbonding_amount = AmountVar(private.unbounding_amount)
balance_blinding = Uint8vec(private.balance_blinding)
# public inputs(公有输入)
claimed_balance_commitment = BalanceCommitment(public.balance_commitment)
unbonding_id = AssetVarId(public.unbonding_id)
penalty = PenaltyVar(public.penalty)
# verify balance commitment(验证余额承诺)
expected_balance = penalty.balance_for_claim(
unbonding_id, unbonding_amount)
expected_balance_commitment = expected_balance.commit(balance_blinding)
assert(claimed_balance_commitment == expected_balance_commitment)
- 原文链接: blog.zksecurity.xyz/post...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!