参考
安全模型
一笔操作的每道印记依靠什么,Ksana 要抵御哪些攻击、又如何抵御,做过哪些检验,以及还有哪些问题没有解决。
Ksana 主网尚未上线。公开 signet 和 devnet 在共识改动需要时就会从创世重启,上面的一切都没有价值。本页描述的是代码现在的做法;完整的威胁表见规范 §16。
每道印记依靠什么
一笔操作要经过三道印记,每一道依靠的东西各不相同。要等到哪一道,按金额来选。
- ≈ 5 秒 快轨确认 已在快轨上执行:余额已经变动,合约看得到新状态。
- 下一个比特币区块 已写入比特币 区块的数据已在一个比特币区块里,任何人都能读到。
- 比特币 6 个确认 BTC 最终 由比特币判定:任何快轨链都取代不了它,要推翻它只能重组比特币。
一笔操作约 5 秒后得到快轨确认;确认其结算的比特币区块一经挖出,就已写入比特币;有了 6 个比特币确认,即为 BTC 最终。
| 印记 | 依靠什么 | 还有什么能推翻它 |
|---|---|---|
快轨确认(fast_confirmed) |
快轨的工作量证明:一个主链区块收录并执行了这笔操作 | 一条更重的快轨分支:可能来自占有一部分算力的攻击者,也可能出现在一次深于 ANCHOR_DEPTH 的比特币重组之后 |
已写入比特币(所在区块的 settled) |
这个区块及其所有祖先区块的数据都已在比特币区块里,任何人都能在那里读到,索引器也能据此执行 | 与快轨确认相同,因为区块还没有最终;比特币重组还可能把这些数据重新移出比特币 |
BTC 最终(btc_final) |
比特币的工作量证明:这些数据有了 6 个确认(BTC_FINALITY_CONFS),而且区块的执行有效 |
只有比特币重组;区块成为最终之后,每挖出一个比特币区块,这就更难一分 |
比特币保存 Ksana 的数据,并为它排序,但并不执行 Ksana 的规则。每个节点和每个索引器都亲自检查每个区块,索引器还只凭比特币数据重算全部状态,所以违反规则的区块,不会因为写进了比特币就成为最终。
最终性是逐个比特币区块判定的,每个节点和索引器用的都是同一个函数。在每个比特币高度上,这个函数从最后一个已最终的区块开始,沿着区块头已在比特币上的最重的链往下走:一个区块只要数据有了 6 个确认、锚定块都在比特币最优链上、执行也有效,就成为最终,主链必须经过它。一条更晚才到达比特币的更重的链,取代不了已最终的区块,所以单凭快轨算力推翻不了它。
判定跟随比特币的最优链:比特币发生重组后,每个节点和索引器都会依据新分支上的数据重新判定。只有比特币重组才能推翻已最终的区块;区块成为最终之后,每挖出一个比特币区块,这就更难一分,和比特币交易一样。
威胁与防御
撤销付款
占有快轨一部分算力的攻击者,可以从装着你这笔付款的区块的父块开始,私下挖一条分支,在里面放一笔与之冲突的操作,一旦这条分支变得更重就把它公布出来。你等得越久,得逞的概率越低;而在链空闲时,这个概率要高得多。用真实的分叉选择模拟(reorg_race.rs,见下文),掌握 10% 算力的攻击者,在繁忙的链上能撤销等待一分钟后接受的付款中的 6.5%,等待五分钟后的 0.91%;而在空闲的链上,这两个数字是 33% 和 13%。完整的表格见确认与最终性。
在空闲的链上,诚实的工作量以空块的形式到来,每个空块都要几分钟,所以在这几分钟里,保护一笔付款的几乎只有它自己所在的区块。在繁忙的链上,攻击者运气好挖到的一个空块,分量相当于 12 个诚实区块,而且一挖到就算数。所以,只在小额时把快轨确认当作最终;在繁忙的链上,中等金额要等约 5 分钟;在空闲的链上,或者金额较大时,要等到操作最终。
掌握多数算力的攻击者,能回滚任何尚未最终的区块;而且针对 Argon2id 租用 CPU 或 GPU 算力,比攻击比特币便宜得多。已最终的区块不在它的能力范围之内,除非它还能重组比特币,正如每道印记依靠什么一节所述。
扣留区块
扣留区块,也叫自私挖矿,是指私下挖出区块、过后才公布,让诚实的区块变成红块,失去它们的奖励。在 Ksana 上,有两种形式值得关注。
- 用廉价区块对付一个重块:在空闲的链上,诚实的网络挖出一个重空块期间,矿工藏起超过
GHOSTDAG_K个权重为 1 的区块。分叉选择只计蓝工作量的时候,占 5%、10% 或 20% 算力的矿工靠这种办法,分别拿走了诚实一方 30%、45% 或 56% 的奖励槽,而且毫无代价。现在分叉选择把一个顶端自身的工作量也算进去,所以重块一公布就胜出,被扣留的区块则变成红块:在模拟中,诚实一方的损失最多为 1.2%(面对占 30% 算力的矿工),攻击者自己则损失十分之一到将近一半的收入。 - 用一个重块对付廉价区块:矿工私下留着一个空块,在它上面接着挖权重为 1 的区块,并赶在这期间诚实挖出的区块在分量上超过它之前把它公布;诚实区块中,排在前
GHOSTDAG_K个之后的都会变成红块。GHOSTDAG_K取 16,就给权重 12 的空块所能压过的那些诚实区块留出了位置。每格模拟 2,400 小时,攻击者能拿走诚实一方如下比例的奖励槽,自己的则一个不丢:
| 攻击者的算力 | 繁忙的链 | 空闲的链 |
|---|---|---|
| 5% | 0 | 0.50% |
| 10% | 0.02% | 2.9% |
| 20% | 1.1% | 9.6% |
| 30% | 10% | 16% |
空闲链上的这些数字仍是未解决的问题:那里的诚实区块是相隔几分钟才挖出的空块,私下挖的链仍能在其中某个空块的反锥(anticone)里放进超过 GHOSTDAG_K 个区块。
抢先把链写进比特币
最终性只认及时写进比特币的数据。一条被扣留、过后才公布的更重的链,取代不了已最终的区块,因为主链必须经过它们。反过来的攻击,是趁网络空闲,或者趁诚实的结算因为手续费暴涨而在后面排队时,抢先把一条较轻的链的区块头写进比特币,让这条较轻的链的区块先达到 6 个确认。
当这样一条链已经在比特币上待了一个比特币区块,而节点自己的链在那里仍然更轻时,节点会以能进入下一个区块的费率(不超过 --max-settlement-feerate)补发自己直到链顶的区块头,并通过 P2A 输出,把自己正在等待的结算加速到这个费率。诚实的链在比特币上连通之后,链工作量更多的那个候选者就会成为最终。剩下的情况是:连续 6 个比特币区块都没有哪个运营者出钱,把诚实链的数据写进比特币。
审查与路径 C
出块者可以自由挑选路径 B 操作,所以某个出块者可以把你的操作排除在外;但其他任何出块者都可以收录它,而下一个区块由谁来出,随算力轮换。路径 C 就是保证:你的比特币交易一旦有了 NATIVE_CONFIRMATIONS 加 ANCHOR_DEPTH 个确认(signet 上 2 个,主网上 5 个),每个区块都必须按比特币上的顺序收录它,漏掉它的区块无效。强制收录条目不受区块 gas 上限的约束。
谁也无法把你的路径 C 操作复制进另一笔交易:它的签名承诺了携带这笔操作的交易的第一个输入,所以复制件通不过认证,也不会用掉你的 nonce。和任何比特币交易一样,你的交易何时确认,仍由比特币矿工决定;Bitcoin Core 30 及更高版本默认会转发它那个较大的 OP_RETURN 输出。万一出块者变得集中、并且实施审查,规范给出的对策是通过协议升级加入包含列表(inclusion list)。
始终不上链的数据
出块者可以挖出一个区块,却始终不把它的数据写进比特币。Ksana 用四种方式应对:
- 谁都能把它送上链:任何节点都可以用自己的资金重建结算(这叫救援),或者补发这些数据:先由这个区块的出块者来,然后是它之后各主链区块的出块者,最后是其他所有节点。比特币内存池里若有声称结算该区块的交易,救援最多推迟 8 个比特币区块(
CLAIM_PATIENCE);节点自己的一笔结算,若以低于本节点费率的费率等了 2 个比特币区块,节点就会给它加速(BUMP_AFTER)。 - 上链之前,谁都拿不到钱:奖励和路径 B 手续费要等一个区块及其全部祖先都已结算才解锁,所以之后的每个出块者都有理由把缺失的数据送上链。携带者的那一份,归其结算最先携带这些数据的出块者;或者,如果是一次补发把数据写进比特币的,而且上链位置比区块的锚定块高出超过
ANCHOR_DEPTH+ 2 个比特币区块,就归这次补发的收款账户。 - 积压上限:一个区块体在其区块的锚定块之后等了 6 个比特币区块(
SETTLEMENT_TIMEOUT)仍未上链时,在它上链之前,新区块只能包含强制收录条目。路径 B 宁可暂停,也不在谁都无法检查的数据上继续往下建;路径 C 照常进行,快轨也不会回滚。 - 不存在上不了链的区块:区块体装不进一个载荷的区块是无效的;结算交易必须把
nLockTime设为 0,每个输入的nSequence都设为0xFFFFFFFD,所以它带不了时间锁。
比特币重组
出块者锚定在自己的比特币链顶之下 ANCHOR_DEPTH 个区块处,所以不超过这个深度(signet 上为 1,主网上为 2)的比特币重组,不会动任何一个 Ksana 区块。更深的重组会把锚定在成为孤块的比特币区块上的 Ksana 区块移出主链。这些区块中的路径 B 操作会回到条目池,在新的分支上以同样的操作 ID 再次被收录;路径 C 操作要等它们的交易在新链上确认后才会再次被收录,如果重组双花了这些交易,它们就消失了。
能造成深于 ANCHOR_DEPTH 的重组的比特币矿工,可以在一条锚定在它私下扣着的比特币区块上的快轨分支里,预先备好一次双花:面对这样的矿工,快轨确认的分量等于其锚定块的比特币确认数,要撤销 m 个比特币区块之前产出的区块,至少需要 ANCHOR_DEPTH + 1 + m 个区块的重组。开启剪枝时,深到节点保留范围之下(至少 144 个区块深)的重组,会让节点停下来等待重新同步,并让索引器从创世锚定重新开始。
已最终的区块同样跟随比特币的最优链(见每道印记依靠什么):只有比特币重组才能推翻它;区块成为最终之后,每挖出一个比特币区块,这就更难一分;而深到让区块的锚定块(位于其数据之下)成为孤块的重组,会像对待锚定在那里的任何区块一样,把它移出主链。
锚定块不在比特币上的区块
一个区块指明的比特币区块,可能不在比特币最优链上:也许这个锚定块是伪造的,也许一次重组让它成了孤块。这样的区块,以及合并了它的任何区块,在最终性判定中首先就被排除,其他什么都不用再问。出块者从不合并它,主链也从不接纳它;对等节点发来的区块,如果锚定得高于本节点的比特币链顶减去 ANCHOR_DEPTH(除非它的父块已经锚定得这么高),节点会暂缓处理,直到自己的比特币视图跟上。这种区块的强制收录条目也必须能对照当前的比特币链检查,所以它带不了一个索引器永远无法确定的区块体。
无效区块
每个节点都检查每个区块:工作量证明、锚定块、时间戳、强制收录、区块体大小、gas,以及执行之后的状态根。违反规则的区块会被拒收,建在它之上的每个区块也一同被拒收。只在执行中出错的区块会被取消进入主链的资格:它留在区块 DAG 里,其他区块也可以合并它,但它本身,以及选定链经过它的任何区块,都不能进入主链。
比特币分辨不出区块有效还是无效,所以攻击者可以抢先把一个无效区块写进比特币。区块成为最终之前,最终性要先拿到它的裁决:在最终链顶上执行它的结果,也就是索引器所做的同一种执行。无效的候选区块会被排除,选定链经过它的每个区块也一起被排除,转而判定次优的候选区块。同一条比特币链上的每个节点和索引器都会得出同样的裁决,所以已最终的区块只可能因为非确定性 bug 而无效;那时索引器会停下,每个节点都会发出告警。
确定性另有专门的防护:共识代码不用浮点运算、不读系统时间、不依赖无序的遍历,出错时返回错误而不是 panic;还有一项两进程测试,在 x86_64 和 aarch64 上把每个区块的状态根与固定值比对。
其他防御
| 威胁 | 防御 |
|---|---|
| 把时间戳推到未来,以降低难度 | 时间戳最多比它锚定的比特币区块的时间超前 4 小时(MAX_TIMESTAMP_AHEAD_OF_ANCHOR),所以一个区块最多能把下一个区块的目标值提高到 4 倍 |
| 向比特币刷空块 | 空块需要 12 到 120 倍的工作量,换来的单位时间奖励却相同;在一串连续的空块中,每个区块头占 14 字节 |
| 签名被重放到别处 | 带标签哈希,以及每个 VmOp 里的链 ID 和 nonce;路径 C 的签名承诺了携带这笔操作的交易的第一个输入 |
| 把 KSA 转到比特币地址,或把 BTC 转到 Ksana 账户 | 账户就是一个 Taproot 地址,所以两者都落在同一把密钥上;客户端也不接受其他类型的地址作为账户。合约 ID 不是密钥:千万不要把比特币转到合约 ID(账户与地址) |
| 耗尽节点资源的合约 | 精确的 gas 计量,内存、实例、调用深度和栈的上限,以及区块 gas 上限;只读的 ksana_call 最多用 BLOCK_GAS_LIMIT 的 gas,一个节点同时最多运行 4 个 |
| 通过条目池或 P2P 发来的垃圾消息 | 准入时检查签名、nonce 和余额,每个账户的上限,带逐出的字节上限,有上限的孤块池,以及对等节点的打分与封禁 |
| 客户端为错误的网络签名 | ksana-cli 和网页钱包在签任何东西之前,先根据节点的链 ID 确定网络;TypeScript SDK 没有默认网络 |
| 出块者的热钱包被盗 | 出资密钥与 --producer 账户相互独立;--revenue-address 配合 --hot-wallet-cap-sat,在超过上限时把结算找零转到别处 |
做过哪些检验
2026 年 10 月 6 日,两次内部安全审计审查了代码。它们代替不了独立审计,规范把独立审计列为主网上线之前要做的工作。
| 审计 | 发现的问题 | 结果 |
|---|---|---|
第一次内部审计(ef07411) |
42 | 通过本地加固(D-098)和新的共识规则(D-100,devnet 和 signet 为此从创世重启)修复;三个严重问题全部修复,五个高危问题修复了四个,第五个等待 gas 校准 |
第二次内部审计(11f0576,在 5819b26 上复核) |
22(严重 2 个、高危 1 个、中危 9 个、低危 10 个),另有 8 条说明 | 严重和高危问题以及三个中危问题在代码合并之前就已修复,另外两个中危问题随后修复(D-102、D-106);12 个仍未解决(中危 2 个、低危 10 个),2 个部分修复 |
第二份报告列出了两次审计每个问题的状态,见 audits/2026-10-06-second-internal-audit.md;第一份报告不在仓库里。
审计之外,还会运行下面这些检查:
| 检查 | 说明什么 | 何时运行 |
|---|---|---|
| 单元测试、属性测试和向量测试 | 每一条规则;对照 Rust 与 TypeScript 共用的测试向量检查编码 | CI |
| 确定性测试 | 两个进程对一个固定场景的每个区块都得出相同的状态根,并与固定值比对 | CI,在 x86_64 和 aarch64 上 |
| 模糊测试(cargo-fuzz) | 五个目标(VmOp 解码、载荷解码、结算交易、宿主函数参数、BIP322 见证)从不崩溃,解码也始终只认规范编码 | CI,每个目标 60 秒 |
| regtest 端到端测试 | 在真实的 bitcoind 上测试路径 B 和 C、充值、结算和索引器 | CI |
| devnet 冒烟测试 | 三个节点收敛,一笔转账顺利完成,索引器的状态根在每个高度都与节点的一致;随后是 3 分钟的随机操作 | CI |
| 浸泡测试 | 连续数小时随机提交路径 B 和路径 C 操作,可选加入 1 到 3 个区块的随机比特币重组,全程对照节点检查索引器 | 按需运行:在 GitHub 托管的 runner 上 5 小时,在项目负责人的 Mac 上 10 小时 |
| 对抗模拟 | 付款撤销、扣留区块,以及在锚定深度 0 到 3 下,1 到 ANCHOR_DEPTH + 2、6 和 7 个区块的比特币重组 |
按需运行 |
最近在项目负责人的 Mac 上进行的 10 小时浸泡测试中,一次跑了 4,007 笔路径 B 和 2,010 笔路径 C 操作,没有一笔失败,结束时三个节点和索引器停在同一高度(7,231);另一次带着 39 次随机比特币重组,跑了 3,675 笔路径 B 和 1,863 笔路径 C 操作,每次重组之后,最终性都重新越过了快轨链顶。那一次有 9 笔路径 C 操作在重组后失败,也就是下文所说的那项限制。
你可以在仓库里自己运行这些模拟:
cargo test -p ksana-consensus --release --test reorg_race -- --ignored --nocapture
cargo test -p ksana-consensus --release --test selfish_mining -- --ignored --nocapture
cargo test -p ksana-indexer --release --test deep_reorg -- --ignored --nocaptureKSANA_RACE_TRIALS 设定竞赛的场数,KSANA_SM_HOURS 设定模拟的小时数;KSANA_SM_K 和 KSANA_SM_N 用 GHOSTDAG_K 和 EMPTY_BLOCK_DIFFICULTY 的其他取值重跑扣留区块那张表。
已知限制
- 空闲链上的确认:链空闲时发出的付款,在几分钟内只受到很弱的保护(见撤销付款);请等到它最终。
- 在空闲的链上扣留重块:占 5% 到 30% 算力的矿工,仍能拿走诚实一方 0.50% 到 16% 的奖励槽(见扣留区块)。
- 有待实测的数值:GPU 在 Argon2id 上的优势(估计为 10 到 30 倍)、真实网络的传播延迟(
GHOSTDAG_K为 16 时可容忍约 17.6 秒)、BLOCK_GAS_LIMIT背后的 gas 表,以及真实手续费市场下的SETTLEMENT_TIMEOUT和携带者报酬,都要在主网上线前实测。 - 上链竞赛:如果连续 6 个比特币区块都没人出钱把诚实链的数据写进比特币,较轻的链就可能成为最终。
- 深度比特币重组之后的路径 C:再次被收录的操作按新的比特币顺序执行,如果那时它的 nonce 不合次序,它就会失败。这个账户之后的操作要等缺口补上才能执行,节点的条目池最多保留其中 8 笔,失败的那笔则要你重新提交。
- 救援的成本:救援照抄原来的载荷,所以携带者报酬仍归区块原来的出块者,比特币手续费却由救援者支付。
- 尚未解决的审计问题:大多是针对本地拒绝服务的加固:P2P 连接数上限、RPC 批量请求与请求体的上限、让出块者反复重建出块模板的条目池变动、来自比特币载荷且数量不受限制的待处理区块头、编译合约代码的 gas 价格,以及
ksana-cli、SDK 和网页钱包中客户端一侧的问题。 - 审计范围之外:wasmtime 自身的正确性、Bitcoin Core、浏览器钱包、真实网络负载下的性能,以及模拟之外的奖励经济学。
报告漏洞
不要在 issue、discussion 或 pull request 里描述漏洞。请通过你和 ksana-in-bitcoin 组织某位维护者之间已有的私密渠道发给对方;如果没有这样的渠道,就开一个 issue,请求一个私下联系的方式,但不要写任何细节。报告里请包括:
- 受影响的组件(crate、SDK、合约或网页应用),以及你查看的提交(commit);
- 攻击者能做什么,要付出多少代价,谁会受影响(一个节点、所有节点、索引器、用户的资金);
- 能复现它的步骤或测试。
在修复合并之前,报告及其后续沟通都不公开。哪些情况算作漏洞(共识分歧;让出块、结算、最终性或索引器停摆;拒绝服务;在规则之外拿走、锁住或创造资金;外部输入导致的崩溃),列在 SECURITY.md 里。