尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

nullifier 没和 note 绑死,隐私池就会被双花

nullifier 没和 note 绑死,隐私池就会被双花
📅 发布时间:2026/7/10 23:04:33

核心要点

  • 本周Ethereum上有1起代表性安全事件入选,涉及损失约$800K。
  • 隐私池协议Hinkal遭遇双花攻击,损失约$800K:旧版note格式的缺陷可能使攻击者从单笔存款派生出多个有效nullifier。
  • 基于nullifier的隐私协议的偿付能力取决于电路层面note与nullifier的绑定关系,而非仅依赖合约对有效证明的验证。

过去一周(2026/06/29 - 2026/07/05),以下代表性安全事件入选本期报告,涉及Ethereum上约$800K的损失。

  • Hinkal:隐私池协议遭遇双花攻击,旧版note格式的缺陷可能使同一笔存款产生多个有效nullifier。

本周看点:Hinkal

在本事件中,旧版note格式的缺陷可能使隐私池遭遇了一次双花攻击。基于nullifier的隐私协议中,偿付能力的保障依赖于电路层面的nullifier绑定,而不只是合约对有效证明的验证。

2026年7月2日,Ethereum上的隐私池协议Hinkal遭遇双花攻击,约$800K资产被盗。其可能的根本原因在于旧版note格式的缺陷:单个note未与唯一的nullifier紧密绑定,导致同一笔存款可被多次消费。项目方未开源电路实现,也尚未发布详细的技术分析报告。以下分析基于公开文档、反混淆后的客户端代码及链上观察。

背景

协议概览

Hinkal是一个隐私池协议。余额不以明文账户形式存在,而是以note的形式存储,作为commitment记录在链上Merkle树中。

要消费一个note,用户需提交zk证明和一个nullifier。合约记录每个nullifier并拒绝重复使用。"一个note只能产生一个nullifier"这一规则并非由合约强制执行,而是交由电路保证。

下图展示了存取款的整体流程:

Note格式(客户端) | 链上路径 | +--------------------------+ | +-------------------------------+ | 新版(默认): | | | transact()【含证明】 | | nk 直接进入 Poseidon6 | | | 所有操作:存款 / 转账 / 提款 | | -> 1个commitment:1个 | | | | | nullifier | | +-------------------------------+ +--------------------------+ | --> | --> +--------------------------+ | | 旧版: | | +-------------------------------+ | nk 仅通过乘积 e*nk 传入 | | | prooflessDeposit()【无证明】 | | -> 每个commitment可能允许 | | | 仅限存款 | | 多个nullifier | | +-------------------------------+ +--------------------------+ | | 两条存款路径最终都会使 | note进入Merkle树
Note格式

电路的实际实现代码未开源。以下分析基于Hinkal公开的电路设计文档和反混淆后的客户端证明代码。

文档和客户端代码显示,构建note的stealthAddress可能有两种方式,由isNewStyle标志选择:

  • 旧版(legacy):H0 = e*Base8,H1 = (e*nk)*Base8,stealthAddress = Poseidon3(signs, H0y, H1y)
  • 新版(new):H1 = nk*H0,stealthAddress = Poseidon6(signs, H0y, H1y, spk0, spk1, nk)

Poseidon2、Poseidon3、Poseidon6均为Poseidon哈希函数的实例,后缀数字表示输入参数的个数。

其中e为随机数,nk为秘密的nullifying key,Base8为固定基点。由于H0和H1是Baby Jubjub椭圆曲线上的点,各有x坐标和y坐标。signs值打包了它们x坐标H0x和H1x的符号位(2*L(H0x) + L(H1x))。旧版stealthAddress仅通过乘积e*nk间接包含nk,而非直接纳入spending key(spk0、spk1);新版则将nk、spk0和spk1一并写入Poseidon6。

nullifier直接从nk计算:nullifier = Poseidon2(commitment, Poseidon2(nk, commitment))。

以下是从zkProofWorkerNode.js反混淆得到的相关函数(S = Poseidon,Base8 = 固定生成器,e = 随机化参数,t = nk):

// 旧版 static getRandomizedStealthPairOld = (e, t) => { const a = e * (t % B) % B; // a = e*nk const H0 = mulPointEscalar(Base8, e); // H0 = e*Base8 const H1 = mulPointEscalar(Base8, a); // H1 = (e*nk)*Base8(nk仅通过乘积传入) return { H0, H1 }; }; // 新版 static getRandomizedStealthPair = (e, t) => { const a = t % B; // a = nk const H0 = mulPointEscalar(Base8, e); // H0 = e*Base8 const H1 = mulPointEscalar(H0, a); // H1 = nk*H0(nk直接绑定到note的点上) return { H0, H1 }; }; // nullifier直接从nk派生 getNullifier() { const c = this.getCommitment(); const sig = S(this.nullifyingKey, c); // Poseidon2(nk, commitment) this.nullifier = S(c, sig); // Poseidon2(commitment, sig) }

isNewStyle标志存储在note的stealthAddressStructure中一个名为extraRandomization的字段的最高位。客户端代码将其打包为extraRandomization = (isNewStyle << 255) | H0x,合约在解码时通过getPointSign(H) = H / 2<strong>255和getPointY(H) = H % 2</strong>255将其拆分。代码注释写道:"strip the isNewStyle flag (bit 255) so the circuit gets the clean H0x coordinate"。因此,通过getPointSign(extraRandomization)即可获取最高位来判断note类型。

以下CircomDataBuilder.sol:formBasicInput()的代码片段展示了这一拆解过程:

// CircomDataBuilder.sol:formBasicInput() ... // strip the isNewStyle flag (bit 255) so the circuit gets the clean H0x coordinate input[index++] = getPointY( circomData.stealthAddressStructure.extraRandomization ); // = H0x input[index++] = circomData.stealthAddressStructure.H0; // = H0y ...

在存款构建的客户端代码中,isNewStyle标志默认设为true,因此正常用户流程看起来不会创建旧版note。

// hinkalDeposit for self // 自身输出UTXO // @hinkal/common/common/src/functions/pre-transaction/outputUtxoProcessing.mjs let m = [new r({ amount: e(d + o, 0n), erc20TokenAddress: f, mintAddress: p, nullifyingKey: i.getShieldedPrivateKey(), timeStamp: s, spendingPublicKey: i.getSpendingKeyPair().pubSpendingBJJPoint, isNewStyle: !0 // 等价于 isNewStyle: true })]; // hinkalDeposit for others // @hinkal/common/common/src/data-structures/Hinkal/hinkalDeposit.mjs w = h.map((e, t) => [new s({ amount: o[t], erc20TokenAddress: e, H0: [BigInt(_), BigInt(y)], stealthAddress: g, encryptionKey: b, isNewStyle: !0 // 等价于 isNewStyle: true })])

将标志设为0的旧版存款并非正常用户流程所产生。

存取款路径

transact()是所有操作的统一入口,涵盖存款、隐私转账和提款。客户端在链下生成zk proof,transact()负责验证proof、检查Merkle root,然后写入nullifier和commitment。

prooflessDeposit()是链上一个独立的函数,完全绕过transact()。它直接接收代币,根据调用者指定的数据构建commitment,无需提供proof。

function prooflessDeposit( address[] calldata erc20Addresses, uint256[] calldata amounts, uint256[] calldata tokenIds, StealthAddressStructure[] calldata stealthAddressStructures ) public payable nonReentrant { hinkalHelper.performProoflessDepositChecks( erc20Addresses, amounts, tokenIds, stealthAddressStructures ); bytes memory returnData = address(hinkalInLogic).functionDelegateCall( abi.encodeWithSelector( hinkalInLogic.handleProoflessDeposit.selector, erc20Addresses, amounts, tokenIds, stealthAddressStructures ) ); UTXO[] memory utxoSet = abi.decode(returnData, (UTXO[])); uint256 length = utxoSet.length; OnChainCommitment[] memory commitmentArray = new OnChainCommitment[]( length ); for (uint256 i = 0; i < length; i++) { commitmentArray[i] = createCommitment(utxoSet[i]); } insertCommitments( new uint256[][](0), new bytes[][](0), commitmentArray, new bool[](0) ); }

对于ERC-20类型的note,commitment由amount、token、stealthAddress和timestamp派生。其中stealthAddress直接来自调用者。

commitment = hash4( utxo.amount, uint256(uint160(utxo.erc20Address)), utxo.stealthAddressStructure.stealthAddress, utxo.timeStamp );

漏洞分析

以下分析基于链上行为和反混淆后的客户端代码推断。电路的实际实现未开源,根本原因有待确认。

受影响的合约为Hinkal(0x25e5...a826)。

可能的缺陷。如背景部分所述,旧版格式中nk仅通过乘积e*nk进入note,而nullifier则直接从nk派生。如果旧版消费电路没有将nk与commitment唯一绑定,那么不同的(e, nk)对可以保持相同的乘积e*nk(从而保持同一commitment),同时改变nk,每次为同一叶子节点生成新的nullifier。新版格式将nk直接纳入Poseidon6,从而使每个commitment只对应一个nk。对Hinkal合约而言,每笔提款看起来都是合法的:证明通过,root存在,每个nullifier都是新的,因此insertNullifiers()都会接受。合约无法将nullifier关联回特定的叶子节点,因此将每次消费都作为正常提款处理。问题可能不在于链上检查,而在于旧版证明电路所允许证明的内容。

prooflessDeposit()与攻击面。prooflessDeposit()函数将检查委托给performProoflessDepositChecks(),但从链上行为来看未观察到任何格式验证:该函数并未拒绝旧版格式的note,合约也未使用其返回值。这使得旧版note可以在不受任何格式限制的情况下进入Merkle树,为上述缺陷打开了攻击面。

攻击分析

以下分析基于受影响的Hinkal合约的历史交易记录。攻击者针对多种资产(USDC、ETH等)实施了攻击,以下以USDC流程为例。

  • Step 1:攻击者通过交易0xfbedf0...8c2f11中的prooflessDeposit()存入100 USDC。该存款的extraRandomization最高位为0(getPointSign = 0),表明该note使用旧版格式。

  • Step 2:攻击者对这个100 USDC的旧版note反复调用transact(),每次使用相同的Merkle root但不同的nullifier,每次提取100 USDC。这就是双花:同一个note被多次消费,累积约25,000 USDC。

  • Step 3:攻击者通过交易0xbf7252...d50008中的另一次prooflessDeposit()调用,将全部25,000 USDC合并为单个旧版note。通过合并为更大金额的note,后续每次双花提取可得25,000 USDC而非100。

  • Step 4:攻击者对25,000 USDC的note重复同一过程,每次调用transact()时使用新的nullifier。存款之后攻击者再未注入任何资产,但提取总额远超存入的25,000 USDC。

所有transact()调用累计盗取了约$800K资产。

图:Phalcon 资金流图,展示 Hinkal 合约与攻击者之间的转账

结论

Hinkal合约接受了单独有效的证明,然而同一个note很可能被多次赎回,原因在于旧版note格式的缺陷。合约的链上检查(proof验证、nullifier唯一性)均已通过,但无法检测同一note产生了多个有效nullifier。团队已暂停所有链上的Hinkal智能合约,并承诺全额赔付所有受影响用户。

针对Hinkal,修复措施包括:彻底禁用旧版note格式路径、在prooflessDeposit()中强制执行格式校验(例如拒绝isNewStyle == 0的note),以及进行专项电路审计以确认一对一的nullifier绑定。

对基于nullifier的隐私协议而言,不变量始终是同一个:每个note必须通过唯一确定的派生路径映射到恰好一个nullifier。这一绑定必须在电路层面强制执行,因为合约只能检查nullifier是否已被使用,无法判断它是否是某个note唯一合法的nullifier。

相关新闻

  • 浩卡联盟(号卡联盟)一级账号怎么注册?一篇讲透为什么越来越多人要升一级? - 流量卡代理招商
  • 《唤醒你的AI同事:WorkBuddy从零上手》007:初始配置向导
  • 郑州城市职业学院参编清华教材背后的育人突围之路

最新新闻

  • 如何从零开始制作一台FOC轮腿机器人:开源DIY完整指南 [特殊字符]
  • 武器装备参数介绍
  • KDNN_torch_adapter完整安装指南:从源码编译到环境配置
  • 2026年评价好的省考冲刺营机构 - 热点品牌推荐
  • 亨得利官方名表服务中心|全新地址电话权威信息声明(2026年7月最新) - 亨得利官方博客
  • 2026年东乡黄金回收怎么选?3家正规靠谱渠道全公开,4个甄别技巧帮你避坑 - 福金阁黄金回收

日新闻

  • OpenClaw本地部署:一键直连微信的私有化AI Agent实战指南
  • Kubernetes 系列【10】控制器:ReplicaSet(副本集)
  • 怎么寄快递才能便宜呢?2026年7月寄快递省钱攻略 - 生活情报姬

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号