ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Zcash zcashd 的 P2P 数据传播机制:recentRejects 与孤儿交易池的设计与源码剖析

2026/9/17 20:51:19 拓冰建站 浏览量
Zcash zcashd 的 P2P 数据传播机制:recentRejects 与孤儿交易池的设计与源码剖析 Zcash zcashd 的 P2P 数据传播机制recentRejects 与孤儿交易池的设计与源码剖析【免费下载链接】zcashZcash - Internet Money项目地址: https://gitcode.com/GitHub_Trending/zc/zcash本篇技术指南围绕 Zcash 全节点实现zcashd中区块与交易数据如何被跟踪、验证与传播展开聚焦两大核心机制recentRejects交易拒绝过滤器与mapOrphanTransactions孤儿交易池。通过本文读者将理解 zcashd 如何在继承 Bitcoin Core 行为的基础上针对屏蔽交易shielded transaction演进出的 txid/wtxid 差异以及这些设计如何抵御针对钱包节点的 DoS 攻击。全文结合 doc/book/src/design/p2p-data-propagation.md 的设计笔记与仓库源码逐层展开。概述zcashd 如何跟踪并传播交易数据zcashd 的 P2P 层在网络中广播新区块与新交易其大部分行为继承自 Bitcoin Core但因 Zcash 的屏蔽交易Sprout JoinSplit、Sapling 与 Orchard 屏蔽组件引入了若干差异。原设计文档指出这些差异的完整脉络散落在代码注释中将其汇总为一份整体说明有助于理解整个网络层的动态行为。本文探讨的核心问题可以归纳为三条主线重复处理抑制一笔已被拒绝的交易如何避免被节点反复处理recentRejects输入缺失交易的处理依赖尚未到达的父交易的交易如何被暂存并在父交易到达后重验证mapOrphanTransactions版本相关的标识符语义v5 前后交易中 txid 与 wtxid 的承诺范围差异如何影响拒绝过滤与孤儿交易索引。recentRejects交易拒绝的记忆设计目的与基本行为当一笔交易被AcceptToMemoryPool拒绝时节点会将其加入recentRejectsBloom 过滤器以避免后续反复收到并处理同一笔交易。该过滤器基于CRollingBloomFilter实现在 src/main.cpp 中声明为全局变量。过滤器的生命周期由链尖chain tip状态驱动每当链尖发生变化过滤器即被重置。原因在于之前无效的交易可能在新的链尖下变为有效——例如带有nLockTime的交易在时间条件满足后变得可花费或原本的双花交易随重组失效。源码在AlreadyHave函数中实现了这一逻辑// src/main.cpp, AlreadyHave() assert(recentRejects); if (chainActive.Tip()-GetBlockHash() ! hashRecentRejectsChainTip) { // If the chain tip has changed previously rejected transactions // might be now valid, e.g. due to a nLockTimed tx becoming valid, // or a double-spend. Reset the rejects filter and give those // txs a second chance. hashRecentRejectsChainTip chainActive.Tip()-GetBlockHash(); recentRejects-reset(); }这段代码位于 src/main.cpp 的AlreadyHave中与设计文档的描述完全一致重置后这些被拒交易将获得第二次机会。防 DoS 的核心必须承诺整个交易recentRejects的一个关键设计约束是它需要存储对整个交易的承诺commitment而不仅仅是 txid。设计文档给出的推理如下To prevent DoS attacks against wallets submitting transactions,recentRejectsneeds to store a commitment to the entire transaction.考虑如下攻击场景钱包提交了一笔交易网络中的恶意节点对该交易进行变形malleate使其授权数据authorizing data失效从而让钱包的交易被节点拒绝。若recentRejects只记录 txid那么被变形后的交易会污染该 txid 的拒绝记录导致钱包广播的、带有有效授权数据的原始交易也被忽略——这就是针对钱包的 DoS。解决方案是记录wtxidwhole-transaction id全交易标识recentRejects记忆的是对完整交易的承诺。这样节点会忽略特定交易内容的后续广告但仍然会请求同一 txid 的其他版本那些可能带有有效授权数据的版本。txid 与 wtxid 的版本差异设计文档明确指出两种交易版本在标识符语义上的区别pre-v5 交易txid 对整个交易作出承诺wtxid 是 txid 加上一个全局固定的全 1后缀v5 交易wtxid 对整个交易作出承诺。这一设计在源码中体现为WTxId结构与LEGACY_TX_AUTH_DIGEST常量。src/primitives/transaction.h 定义了WTxIdstruct WTxId { const uint256 hash; // txid const uint256 authDigest; // 授权数据摘要 ... const std::vectorunsigned char ToBytes() const { std::vectorunsigned char vData(hash.begin(), hash.end()); vData.insert(vData.end(), authDigest.begin(), authDigest.end()); return vData; } ... };而 src/uint256.h 定义了 pre-v5 交易的占位授权摘要/* The placeholder value used for the auth digest of pre-v5 transactions. */ static const uint256 LEGACY_TX_AUTH_DIGEST uint256S(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);即全 1all-ones占位符对应文档所述globally-fixed (all-ones) suffix。对于 v5 交易authDigest则是真实提交整个交易含全部授权数据的摘要。因此在 src/main.cpp 的AlreadyHave中节点只使用 wtxid 查询recentRejects// We only need to use wtxid with recentRejects. Orphan map entries // are re-validated once their parents have arrived, and all other // locations are only possible if the transaction has already been // validated (we dont care about alternative authorizing data). return recentRejects-contains(inv.GetWideHash()) || mempool.exists(inv.hash) || mapOrphanTransactions.count(inv.hash) || pcoinsTip-HaveCoins(inv.hash);过滤器的实现参数过滤器在 src/main.cpp 的InitBlockIndex中初始化recentRejects.reset(new CRollingBloomFilter(120000, 0.000001));两个参数分别为参数值含义元素容量120000过滤器可容纳约 12 万个元素交易的 wtxid误报率0.000001目标假阳性率 10⁻⁶允许的哈希/内存开销与误报概率的平衡误报意味着偶尔会把其实可以接受的交易视为已拒绝但由于链尖变化时过滤器会被重置误报窗口有限。而将交易写入过滤器使用recentRejects-insert(tx.GetWTxId().ToBytes())写入的正是 src/primitives/transaction.h 中WTxId::ToBytes()拼接出的 64 字节承诺。mapOrphanTransactions孤儿交易池用途与 zcashd 的收窄上游 Bitcoin Core 使用mapOrphanTransactions暂存因缺少透明输入transparent inputs而被AcceptToMemoryPool拒绝的交易——这些交易的父交易可能尚未到达本节点。zcashd 继承了这一行为但将其严格限制为纯透明交易如果一笔交易包含任何屏蔽组件Sprout JoinSplit、Sapling 或 Orchard bundle节点直接将其视为无效并加入recentRejects而不会进入孤儿池。这一限制在交易接收处理分支中体现得十分清晰src/main.cpp// TODO: currently, prohibit joinsplits and shielded spends/outputs/actions from entering mapOrphans else if (fMissingInputs tx.vJoinSplit.empty() !tx.GetSaplingBundle().IsPresent() !tx.GetOrchardBundle().IsPresent())也就是说只有fMissingInputs为真存在缺失输入、且不携带任何屏蔽组的交易才可能进入孤儿池分支其余情况落入recentRejects写入分支src/main.cpp。为什么按 txid 索引一个精心权衡的边界设计文档特别强调了一个微妙之处mapOrphanTransactions按 txid 索引而非 wtxidsrc/main.cpp 中uint256 hash tx.GetHash();。其潜在影响是如果一笔孤儿交易花费了节点未知的透明 UTXO同时因其他原因无效——即后续尚未检查的AcceptToMemoryPool规则——那么节点将不会请求同一 txid 的 v5 交易因为 v5 中同一 txid 可能对应不同的授权数据。设计文档论证了这不会构成对钱包的 DoS 问题an adversary that manipulated an orphan transaction to be invalid under the above constraints would also need to prevent the orphans parent from entering the mempool, and eventually a parent is reached that is not an orphan.攻击者若想让一笔孤儿交易在缺少输入之外被额外操纵成无效就必须同时阻止其父交易进入内存池而沿着父链向上追溯最终必然存在一个非孤儿的祖先。一旦孤儿交易的直接父交易被接受孤儿交易即被重新评估若它被操纵成无效其wtxid 被加入recentRejects其txid 从mapOrphanTransactions中移除于是钱包得以重新广播未变形的原始交易攻击者无法通过污染孤儿池来阻止钱包交易上链。这正是txid 索引 wtxid 拒绝组合的安全边界所在。该设计意图也直接以注释形式写进了AddOrphanTxsrc/main.cpp// See doc/book/src/design/p2p-data-propagation.md for why mapOrphanTransactions uses // txid to index transactions instead of wtxid.数据结构与辅助索引孤儿池由两个全局容器构成src/main.cppmapuint256, COrphanTx mapOrphanTransactions GUARDED_BY(cs_main); mapCOutPoint, setmapuint256, COrphanTx::iterator, IteratorComparator mapOrphanTransactionsByPrev GUARDED_BY(cs_main);mapOrphanTransactions以 txid 为键值为COrphanTx包含交易本身、来源 peer 的 NodeId、过期时间戳mapOrphanTransactionsByPrev以输出点COutPoint为键指向引用该输出的孤儿交易迭代器集合用于父交易到达时快速找到依赖它的子交易。AddOrphanTxsrc/main.cpp在插入时维护两处索引并实施大小限制防止大孤儿内存耗尽攻击// Ignore big transactions, to avoid a // send-big-orphans memory exhaustion attack. ... // 100 orphans, each of which is at most 99,999 bytes big is // at most 10 megabytes of orphans and somewhat more byprev index (in the worst case): unsigned int sz GetSerializeSize(tx, SER_NETWORK, tx.nVersion); if (sz 100000) { LogPrint(mempool, ignoring large orphan tx (size: %u, hash: %s)\n, sz, hash.ToString()); return false; }即超过 100,000 字节的孤儿交易被直接忽略——按默认 100 个孤儿、每个约 10 万字节估算孤儿池内存占用上限约 10 MB含 byprev 索引的额外开销。容量上限、过期与随机驱逐孤儿池的容量与过期策略定义在 src/main.hstatic const unsigned int DEFAULT_MAX_ORPHAN_TRANSACTIONS 100; ... static const int64_t ORPHAN_TX_EXPIRE_TIME 20 * 60;默认最多保留100笔孤儿交易可通过启动参数-maxorphantxn调整帮助文本见 src/init.cpp孤儿交易默认20 分钟后过期。LimitOrphanTxSizesrc/main.cpp周期性执行两种清理过期扫描按ORPHAN_TX_EXPIRE_INTERVAL批量删除已过期条目并在下一个过期条目前 5 分钟再扫描一次以批量线性扫描、摊薄成本随机驱逐当孤儿数超过上限时随机选择一个 txidGetRandHash()lower_bound驱逐直到数量回落避免攻击者按可预测的顺序塞满池子。断开连接时EraseOrphansForsrc/main.cpp会清除来自该 peer 的所有孤儿交易链重组或回滚时mapOrphanTransactions与mapOrphanTransactionsByPrev会被整体清空src/main.cpp。父交易到达后的重验证链当一笔新交易被接受进内存池时节点通过mapOrphanTransactionsByPrev找到依赖该交易输出的孤儿将其加入orphan_work_setsrc/main.cpp随后调用ProcessOrphanTx递归处理src/main.cpp。ProcessOrphanTxsrc/main.cpp对每个孤儿交易重新调用AcceptToMemoryPool接受中继该交易RelayTransaction将其输出指向的孙孤儿加入工作集然后从孤儿池移除形成递归的瀑布式处理有输入但被拒绝若因非标准或费用不足等原因被拒将orphanTx.GetWTxId().ToBytes()无条件写入recentRejects注释特别说明与上游不同zcashd 可以无条件写入因为这些 wtxid 无论交易版本如何都绑定完整交易并从孤儿池移除恶意行为若孤儿被判定无效且 DoS 分数大于零则惩罚提供该交易的 peerMisbehaving。端到端流程一笔交易从接收到裁决综合上述机制一笔tx消息在 zcashd 中的处理可归纳为三态分支src/main.cpp进入内存池AlreadyHave含recentRejects与孤儿池查询未命中、AcceptToMemoryPool接受 → 中继交易并递归处理依赖它的孤儿进入孤儿池输入缺失、且为纯透明交易、且其父交易未被recentRejects拒绝 → 请求缺失的父交易AskFor加入mapOrphanTransactions并执行LimitOrphanTxSize容量控制加入拒绝过滤器其余所有被拒情况 → 将交易的wtxid写入recentRejectssrc/main.cpp。分支 2 中还有一个值得注意的细节若孤儿交易的父交易已被recentRejects记录则节点不会保留该孤儿not keeping orphan with rejected parents避免徒劳缓存注定无效的链式交易src/main.cpp。在recentRejects的所有写入路径中交易被拒时 src/main.cpp、孤儿重验证失败时 src/main.cpp使用的都是GetWTxId().ToBytes()即对整个交易承诺的 64 字节 wtxid而非仅 txid——这与设计文档的核心结论严格一致。DoS 防护与安全边界总结威胁场景防御机制源码依据反复收到同一笔被拒交易浪费处理资源recentRejectsBloom 过滤器命中即视为已拥有src/main.cpp变形交易污染 txid 的拒绝记录阻止钱包重播有效交易拒绝记录写入的是 wtxid对整个交易的承诺有效替代版本仍会被请求src/main.cpp、src/main.cpp利用孤儿池塞入大体积交易耗尽内存拒绝 100,000 字节的孤儿默认上限 100 个超限随机驱逐src/main.cpp、src/main.h操纵孤儿交易使其永久滞留、阻碍钱包广播按 txid 索引 父交易到达后重验证 wtxid 记入recentRejects txid 移出孤儿池形成闭环src/main.cpp、src/main.cpp攻击者按固定顺序填充孤儿池随机驱逐而非顺序淘汰src/main.cpp上述边界行为在单元测试中亦有覆盖 src/test/DoS_tests.cpp 验证了LimitOrphanTxSize的随机驱逐逻辑与孤儿池容量上限如mapOrphanTransactions.size() 40/ 10的断言场景以及链尖变化后recentRejects的语义。结语recentRejects与mapOrphanTransactions构成了 zcashd 交易传播层的短期记忆一个记住什么是坏的且只针对完整交易承诺防止变形攻击一个暂存什么是缺东西的且只针对纯透明交易规避屏蔽组件的复杂性。两者围绕 txid/wtxid 的版本语义差异精巧配合在不牺牲钱包可用性的前提下抑制了重复处理、内存耗尽与广告污染三类 DoS 向量。对网络层整体设计感兴趣的读者可进一步阅读设计文档书中的 链状态chain-state、UTXO 视图coins-view 以及设计章节总览并结合 src/main.cpp 的ProcessMessage、AlreadyHave与ProcessOrphanTx三个函数作对照阅读。【免费下载链接】zcashZcash - Internet Money项目地址: https://gitcode.com/GitHub_Trending/zc/zcash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考