Skip to content

Instantly share code, notes, and snippets.

@yangby-cryptape
Last active March 27, 2024 04:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yangby-cryptape/0d59a7c48b2e414fced9261a3290ba16 to your computer and use it in GitHub Desktop.
Save yangby-cryptape/0d59a7c48b2e414fced9261a3290ba16 to your computer and use it in GitHub Desktop.
[2024-02-21] BTC SPV on CKB

BTC Simplified Payment Verification (SPV) on CKB

Requirements

BTC SPV on CKB 需要的需求有:

  1. 更新必须进行合法性验证。
  2. 所有权和使用权分离。
  3. 尽量避免读写竞争问题。
  4. 可以用来验证 BTC TX 。
  5. 支持一定高度的 Revert (or Reorg)。

Refs:

SPV Client contracts 需要 4 个 Operations:

  • Initialiaze
  • Append
  • Revert (or Reorg)
  • Destory (only owner)

SPV Client 还需要提供 library,包含 1 个 function:

  • Prove if a TXID in SPV Client.

Detailed Design

SPV Cell

⚠️ 我看目前设计选了 MMR ,我先这么写了,看后续有无别的想法,因为我还没看完别的 SPV clients 实现。

一个 SPV Cell 的 Data 包含:

- index (用来做 Multi SPV Cells )
- min height
- max height
- hash of the max height header
- MMR root digest
- total diff from 0 to max height

A SPV Client Contains Multi SPV Cells

使用 Multi SPV Cells 的设计,是为了解决上面需求中的 "3. 避免读写竞争" 和 "5. 支持 Revert" 。

将一个 BTC SPV Client 设计成 $N+1$ 个 Cells ,其中:

  • $N$ 个 SPV Cells

    • 所有 SPV Cells 的 min height 都是相同的,这里记做 $H_{min}$

    • 所有 SPV Cells 的 max height 都是不同的,分别是:

      • $H_{min} + a_{0}$
      • $H_{min} + a_{0} + a_{1}$
      • $\cdots$
      • $H_{min} + a_{0} + a_{1} + \cdots + a_{n-2}$
      • $H_{min} + a_{0} + a_{1} + \cdots + a_{n-2} + a_{n-1}$

      其中 $a_{i} \gt 0$ ,代表每次更新 SPV 对应的 BTC height interval 。
      如果每个 BTC block 都立刻更新,就是 $a_{i} = 1$

      如果同步服务出现了异常中断,再恢复时可以设置 $a_{i} \gt 1$ 来提速同步。
      或者 reorg 的时候,不可能再去一个一个补,肯定也是一次多个 heights 快速同步,即 $a_{i} \gt 1$

  • $1$ 个 Index Cell

    由于有 Multi SPV Cells ,循环更新,所以需要:

    • 每个 SPV Cells 里记录自己的 Index Number ,
    • 有一个 Index Cell 来保存当前最新的一个 SPV Cell 的 Index Number 。

Update 操作

假设 Index Cell 里记录 Latest SPV Cell 的 index 为 $t$ 时,更新 SPV Client 更新:

  • CellDep 包含:
    • index 为 $t$ 的 SPV Cell
  • Inputs 包含:
    • index 为 $t+1$ (如果 $t+1 = N$ 则取 index 为 $0$) 的 SPV Cell
    • Index Cell
  • Outputs 包含:
    • index 为 $t+1$ (如果 $t+1 = N$ 则取 index 为 $0$) 的 SPV Cell 更新 Cell Data
    • Index Cell 将 index 修改为 $t+1$(如果 $t+1 = N$ 则为 $0$
  • Witnesses 包含:
    • 从 index 为 $t$ 的 SPV Cell 的 max height 开始的新的 headers 。

验证逻辑:

  • 检查 Index Cell 里 index 为 $t$
  • 检查 Witnesses 包含的 tip headers 是连续的,且验证 POW
  • 检查 Witnesses 包含的 tip headers 可以和 index 为 $t$ 的 SPV Cell 连上
  • 检查 将要更新的 SPV Cell 的 index 为 $t+1$

总结下,每次更新的时候需要检查 Input,CellDep 和 Output 的 index :

  • $i_{\mathrm{Output}} = i_{\mathrm{Input}}$
  • $i_{\mathrm{Output}} \equiv i_{\mathrm{CellDep}} + 1 \pmod{N}$
  • $i \in {0, 1, \dots, N-1}$

Users 使用

用户如果要验证一笔高度在 $h$ 的 TX ,那么选择任何包含这个高度的 SPV Cell 都可以。

但建议选择 Index Cell 里记录的 Latest SPV Cell ,因为这个 Cell 的下一次更新时刻,距离当前时间最长。

这样可以 "3. 避免读写竞争" 。


Reorg 操作

$C_{i}$ 来表示 max height 为 $H_{min} + a_{0} + a_{1} + \cdots + a_{i-1} + a_{i}$ 的 SPV Cell 。

假设目前要从 $C_{r}$ 处进行 reorg (即 $C_{r}$ 是合法的,之后的全要 revert ),那么:

  • CellDep 包含:
    • $C_{r}$ 的 SPV Cell
  • Inputs 包含:
    • $C_{r+1}$ 的 SPV Cell
    • $C_{r+2}$ 的 SPV Cell
    • $\cdots$
    • $C_{N-1}$ 的 SPV Cell
    • 指向 $C_{N-1}$ 的 Index Cell
  • Outputs 包含:
    • $C_{r+1}$ 的 SPV Cell 更新 Cell Data 为最新 tip 对应的数据
    • $C_{r+2}$$C_{N-1}$ 的 SPV Cells 清空数据,等待后面 turns 再写入新数据
    • Index Cell 修改为 指向 $C_{r+1}$
  • Witnesses 包含:
    • $C_{r}$ 开始的,到要 reorg 到的最新 tip 之间的所有的 headers 。

验证逻辑:

  • 检查 Index Cell 指向的是 $C_{N-1}$
  • 检查 Witnesses 包含的 tip headers 是连续的,且验证 POW
  • 检查 Witnesses 包含的 tip headers 可以和 $C_{r}$ 连上
  • 检查 Witnesses 包含的 tip headers 比当前 $C_{N-1}$ 拥有 better total diff 。

Revert 操作

如果 Reorg 操作受到 CKB Block Size Limit 或 Cycles Limit 影响无法做到,那么只能先强行进行 Revert 操作,然后重新同步。

考虑到单纯的 revert 意味没有验证 better total diff ,所以这个操作应该是需要 Owenrship 的。

我理解这个操作不可以有,因为违背了需求 "2. 所有权和使用权分离。" 。

所以对于 reorg 不了的,无论是

  • 计算或数据 超出 CKB 的 limit ,无法验证;
  • 还是预先 Multi SPV Cells 个数设置太少了, reorg 不到。

我的建议是用户应该另部署一套 SPV Client 。


Usecase: How to verify a TX or TXID in BTC chain?

用户如果需要在一个 script 里校验 TX 或 TXID 在 BTC chain 上(仅限于 SPV Cell 的 min height 和 max height 之间的 TX),则:

  • CellDeps 包含:
    • 包含 有该 TX 的 Block 的 SPV Cell
  • Witnesses 包含:
    • TX data (如果不需要验证 data ,可以直接用 TXID )
    • TX 的 Merkle Proof
    • TX 所在 Header
    • TX 所在 Header 的 MMR Proof

验证逻辑:

  • [Optional] 用 TX data 计算出 TXID 。
  • 用 TX 的 Merkle Proof,TXID 和 Header 里的 merkle root 进行校验:TX 在 Header 里。
  • 用 TX 所在 Header 的 MMR Proof, TX 的 Header 和 SPV Cell 里的 MMR root digest 进行校验:header 在 SPV Cell 里。

References:

SPV Testnet Deployment Information

Contracts

SPV Instances

@Flouse
Copy link

Flouse commented Feb 21, 2024

context:
https://www.notion.so/BTC-b90f7341a6734c9490ae2171124500c2?d=47bec34d8962415e89ca247ccf2f4480&pvs=4#1cddc637eb7043fc9efebfb0f791e735

我对仅能验证 SPV Cell 的 min height 和 max height 之间的 TX 的原始需求抱有疑虑。

@Flouse
Copy link

Flouse commented Feb 21, 2024

关于 SPV 链上存储开销的估算

假设 SPV on CKB 只是存 BTC 区块头,大小固定为 80 个字节,每小时出 6 个块,每年出块 52560 个。则每年新增的存储量,仅 4MB,100 年后累计的存储量 400MB。

感觉我们可以考虑存尽可能多的 BTC headers,存储成本估计可控。

@CipherWang
Copy link

感觉是可行的,既然存储成本很低,是否可以考虑增加某些数据以达到简化证明的目的

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Feb 21, 2024

Reply @Flouse:

感觉我们可以考虑存尽可能多的 BTC headers,存储成本估计可控。

如果做成依赖 链上 存的 headers 的模式,那么就存在以下问题:

  • 必然存在有 数量 极限,如果由用户在 witness 提供,可以做到 SPV client 从开始时的 height 到未来的 tip height 之间都支持的。

    • 或者做成,链上有 headers 用链上的,链上没有就用户提供,就是有点儿麻烦。
  • CKB 的 Cell Data 不是追加存储,如果每个 BTC Header 都更新,如果用一个 Cell 更新,那就要反复写同一个(或一批) Cell 而且 Data 越来越大。

    如果一个 Header 一个 Cell ,那就是很多 Cell ,每个至少 61+80 bytes ,让用户提交交易还要找到对应 Cell ,我感觉也不好弄,还不如找一个 BTC Header 的 Data 快呢。

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Feb 21, 2024

Reply @CipherWang:

感觉是可行的,既然存储成本很低,是否可以考虑增加某些数据以达到简化证明的目的

链上存储不多,但 MMR 对链下存储有要求的。
用户(或者需要我们提供服务),必须同时自己也同步 min height 到 max height 的 headers ,才能算出来 Header 的 MMR Proof 。

所以我又想了下,如果要提高用户易用性,那还是要缓存一定量的最近的 BTC Headers ,作为免验证的 Headers 。

  • 免验证的 Headers 在更新的时候,就写到 SPV Cell 里,之后这些 Headers 里的 TX,就不验证了 Header 合法性了,只验证 TX 在 Headers 里。
  • 超出范围部分,需要在 Witness 里提供 TX 的 Header 和 Header 的 MMR Proof 。

如果不考虑历史 Headers ,只考虑近期的 Headers ,那 MMR 都可以不要了;
我理解目前我看设计有要用 MMR ,我理解就是还是要考虑 链上 不能存 Headers 的情况的。


还有一个办法,

  • 一个一个 Header 更新,每攒一定数量 Headers ,就写到一个额外的 Cell 里。
    • 用 Type Lock 控制保证里面 Headers 是验证过的。

这么想下去,要是所有 Headers 都上 CKB chain ,那就不需要 MMR 了。

@Flouse
Copy link

Flouse commented Feb 21, 2024

提个想法,我们可以分阶段实现:

  1. 第一版简化,不要 MMR
    a. 不考虑历史 Headers ,只考虑近期的 Headers
    b. 让 Multi SPV Cells N 取值足够大,比如能覆盖 1 个月的 BTC 区块历史,估计绝大多数场景都够用了

  2. 当有真实的需求要验证很久前的 BTC 交易时,再考虑升级合约引入 MMR

    因为第一次引入 MMR 时,可以通过链外计算所有的区块历史形成的 MMR

  3. 上 mainnet 前,有时间再评估是否设计 Multi SPV Cells 可回收的操作

@jjyr
Copy link

jjyr commented Feb 21, 2024

重新算了下,所有都提交的话,一个月需要 345,600ckb

如果可以接受确实最简单,以后可以再升级合约加上 MMR

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Feb 22, 2024

但,还有一个问题,用来证明的话,你需要提供那个 Cell 到 CellDep 里。
同样的问题,如果 SPV Cells 太多了,是不是还是需要一个服务来帮助用户找自己需要用作 Dep 的是哪个?
一般的 Indexer 可不帮忙分析 Data 数据并建立索引。

@CipherWang
Copy link

如果 SPV Cells 太多了,是不是还是需要一个服务来帮助用户找自己需要用作 Dep 的是哪个?

用户知道自己需要证明的那笔 BTC TX 所在的 block hash 吧,是不是就直接能找到对应的 spv?

@Flouse
Copy link

Flouse commented Feb 22, 2024

如果 SPV Cells 太多了,是不是还是需要一个服务来帮助用户找自己需要用作 Dep 的是哪个?

我以为,不管 SPV Cells 有 10 个或是 1000 个,提供给使用者 用于寻找指定 Cell 用作 CellDep工具是一样的?

SPV Cells 数量估算

  • 1 天约 144 Cells
  • 1 月约 4320 Cells
  • 1 周约 1008 Cells

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Feb 22, 2024

我以为,不管 SPV Cells 有 10 个或是 1000 个,提供给使用者 用于寻找指定 Cell 用作 CellDep工具是一样的?

你要是聚合在一个 Cell 或 几个 Cells ,那就好查了啊,显然多了才需要复杂的选择步骤。

比如,如果就 10 个在轮流,都是聚合的,那就很容易选中了;比如我原文中的方案,无脑选最新的 SPV Cell 就好了。

p.s. 我先边写边想,总体设计就是这里讨论的,具体的我自己先边实践边调整,先跑起来再改吧;不过,我倾向聚合信息在数量不大的 Cells 里,还没写到这,我继续想想。

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Feb 22, 2024

用户知道自己需要证明的那笔 BTC TX 所在的 block hash 吧,是不是就直接能找到对应的 spv?

从普通用户来说,要是采用一个 Header 一个 Cell ,并不好找,因为(我所知道的,目前还)没有对 Cell Data 索引的 Tool 。
除非有个服务接口提供给用户,通过 block hash 查询对应 SPV Cell 。

但假如考虑提供服务的情况,是不是可以直接 MMR 了,还是原文中我写的聚合信息的模式,提供 MMR Proof 服务给用户?


一个问题,我们的目标用户是:

  • 一个中心化服务商,他会部署一些自己的 services ,他提供服务给他的用户。比如 JoyGift 。
  • 还是尽量简单,面向不会部署 services 的 ”小白“ 用户。

理论上,不是所有 Header 都会被用到;其次,我理解 Witnesses 其实是未来可以裁剪的。

从长期对 disk 的消耗角度看,我觉得聚合成 MMR 用 witnesses 提交需要用的数据,是合理的。

@xcshuan
Copy link

xcshuan commented Feb 24, 2024

目前是使用了多个SPV Cell来解决读写冲突的问题。
但只使用一个Cell也可以解决这个问题,读的问题可以这样解决。

  1. 假设 BTC header A 在 CKB 交易 X 被记录在CKB链上,A + 6 在交易 Y 被记录在 CKB 链上。
  2. 需要引用的交易,将Y或者Y之后的交易所在的header作为header_dep,
  3. 证明该交易存在于对应header里(使用隔离见证的默克尔证明),并且利用交易的output data里的MMR根来验证当前引用的BTC区块已经过了6个快。

问题在于,假如真的出现了重组深度大于6的BTC区块,使用者可能可以使用一个历史区块来引用BTC重组区块,这里需要做一些约束,保证用户引用的交易所在的区块必须是足够新的。

为了生成MMR证明,既可以存储所有BTC区块头,也可以存储一小部分BTC区块头+MMR的中间节点。

@matt-nervos
Copy link

can someone move all Notion documents into a publicly accessible place?

@matt-nervos
Copy link

@CipherWang
Copy link

can someone move all Notion documents into a publicly accessible place?

Okay, I will move some of the design docs soon.

@Flouse
Copy link

Flouse commented Mar 7, 2024

can someone move all Notion documents into a publicly accessible place?

CKB Bitcoin SPV Type Script

https://github.com/ckb-cell/ckb-bitcoin-spv-contracts/tree/master/contracts/ckb-bitcoin-spv-type-lock

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Mar 7, 2024

但只使用一个Cell也可以解决这个问题,读的问题可以这样解决。

@xcshuan

我一开始没理解你的意思,理解之后就更不理解了 😵。

总结下,我理解的你的意思:你的意思是一个 Cell 循环用,直接去取其历史状态的数据;就算 Cell 消失了,但是历史 Block 可以取到,从里面得到历史的 Cell Data。

我的感觉:这个用法就没有销毁的概念了,有点儿危险。

@matt-nervos
Copy link

https://github.com/ckb-cell/ckb-bitcoin-spv-contracts/tree/master/contracts/ckb-bitcoin-spv-type-lock

thanks @Flouse . Could anyone share the advantages/disadvantages of having multiple SPV cells?

I understand the problem of cell contention and changing reference inputs but it seems awkward to have multiple on-chain sources for a canonical data source (they could differ in latest update value and this seems like another headache). Maybe there is something I'm missing.

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Mar 8, 2024

@matt-nervos Sorry for this late reply.

can someone move all Notion documents into a publicly accessible place?

Due to the deadline of this project, only few draft documents were used for internal communications when you asked that question.

Still no well-written documentation for public now, but this will be improved in several days or weeks.


In my own opinion, the Bitcoin SPV on CKB could be split into two parts:

  1. The cryptographic design of the SPV.

    The brief design document is submitted in ckb-cell/ckb-bitcoin-spv#5.

  2. The implementation of how to use the SPV.

    The document which Flouse pasted above, still more like a draft. It will be improved later.


... but it seems awkward to have multiple on-chain sources for a canonical data source ...

If multiple available client cells make users feel confused, just tell them to select the latest one.

There is an info cell, it records the ID of the latest client cell.
Or, users can just select the last updated client cell.

All client cells are not conflicted with each other.
All of them are correct, they are just snapshots in different heights.

@xcshuan
Copy link

xcshuan commented Mar 10, 2024

这个用法就没有销毁的概念了,有点儿危险。

@yangby-cryptape 我不认为这个方案更危险。

  1. 对于纯BTC的RGB++交易映射,即便出现重组,如果有人恶意攻击创建出冲突Cell,在几个区块后,由于不再可能找到某笔交易消费对应的UTXO,这个资产将永久锁死。
  2. 对于jump交易,假设必须等待六个区块才能解锁,那么这两种方案都是一样的,如果出现重组深度大于六的交易,都可能会导致冲突cell的产生。

并且SMT里存储的header是持续在更新的,如果想要限制用户用更新的Cell,那么可以加一些timestamp cell,比如说每隔10个Cell更新一次,那么依赖太老的SMT SPV Cell的交易将无法通过验证。

@Flouse
Copy link

Flouse commented Mar 10, 2024

目前是使用了多个SPV Cell来解决读写冲突的问题。 但只使用一个Cell也可以解决这个问题,读的问题可以这样解决。

我也没理解 @xcshuanhttps://gist.github.com/yangby-cryptape/0d59a7c48b2e414fced9261a3290ba16?permalink_comment_id=4934212#gistcomment-4934212 的单 Cell 思路,comment 描述得比较简单,不太好理解。

只使用一个 cell 的话,我理解某交易在引用它作用 cell_dep 时,总有一定概率会失效。


除非找出 @yangby-cryptape 的设计有安全隐患,否则倾向于不改设计,原因:

  1. boyu 提的 多个SPV Cell 设计方案,有在其他项目演练过;
  2. 现在时间比较紧张。除非新想法有详细设计推敲可行性和安全性,否则暂不考虑。
    Code review feedback is welcome.

@xcshuan
Copy link

xcshuan commented Mar 10, 2024

只使用一个 cell 的话,我理解某交易在引用它作用 cell_dep 时,总有一定概率会失效。

@Flouse 因为引用的不是cell本身,还是通过header_dep来引用某次 SPV Cell 的 root。

举个例子,在 tx_a MMR 更新了一个BTC header,那么该交易的input_data是更新前的root,该交易的output_data是更新后的root。

那么我可以将tx_a所在的header作为header_dep,同时提供Merkle证明这笔交易包含在对应区块(由于是隔离见证的,所以不需要包含Witnesses),然后使用 output_data (里面保存了某次更新后的MMR root)验证 MMR proof,证明某个BTC Header的存在性,现在就可以使用这个header证明某个BTC交易的存在性了。

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Mar 10, 2024

Reply @xcshuan:

然后使用 output_data (里面保存了某次更新后的MMR root)验证 MMR proof

抛开可行性先不谈

先不谈可行性,在于,我们是否推行这么实践。

我觉得你这个想法,甚至可以推广一下,我一个 code 部署上去,就可以销毁了。
通过 load_code 去 load 历史 data 就好了。

还要部署什么 lock 、type 啊,中间转发一次就好了。

一旦这么搞了:

  • 现在没 prune 但以后做 prune 感觉也很麻烦,明明 deleted 的 data 还要留一份;
  • light client 也是;
  • indexer 给 spent cell 做 index 吗(我没用过,不清楚)。

你这个观点应该先在更 General 的层面讨论一下,不绑定具体 功能:能不能利用 load_tx 或 load_block 去读取 dead cell data 。

可以在 Nervos Talk 上讨论下,让大佬们来定设计(游戏规则),我只负责遵循设计(在游戏规则下玩游戏)。

我说我的想法吧:你这个违背了(我理解的) CKB 设计的初衷了,你想存数据还不费 CKBytes ,看着,还能再存 DAO 赚利息。

谈谈可行性

另外,我没试,看 CKB 代码应该不可行:

  • 目前 load_cell_data 应该不能 load dead cell ,resolve 环节就报错了。

  • 目前貌似没有 load_block,单纯 load_header 的话,拿不到 cell data 。

    有个 load_transaction 只能 load 当前 transaction 。

@xcshuan
Copy link

xcshuan commented Mar 10, 2024

你这个违背了(我理解的) CKB 设计的初衷了,你想存数据还不费 CKBytes ,看着,还能再存 DAO 赚利息。

@yangby-cryptape 这个当然不违背 CKB 设计的初衷,在 CKB 脚本里读取链自身的历史,一开始就是 CKB 脚本具有的功能之一,使用这个功能并没有占用状态,也就不存在存数据还不占用CKB的问题。

看 CKB 代码应该不可行

这个方案当然是可行的,因为 cell_data 是由构建交易的链下模块自己检索,然后放在 witness 里的,而不是通过 load_dead_cell 这种目前不存在并且也不应该存在的 syscall。

以BTC的SPV来作为切入点方便理解,在CKB上实现BTC SPV Cell 验证某个BTC交易的存在性,是通过使用 BTC Header 里的 Transaction_root + Merkle Proof 实现的。

由于 CKB 使用和 BTC 类似的设计,那么使用 CKB SPV 为什么不行呢?如果这个方案不能用的话,那 BTC SPV on CKB 也同样不可行了。

由于 load_header 的存在,相当于 CKB 脚本内置了 CKB 自身的SPV节点,那么使用 CKB header 中的 transaction_root 自然可以验证某笔交易的存在性,从而获取在某个区块高度里交易对应的output,其lock,type,data分别是什么,于是脚本就获得了某个历史状态的 SPV Cell 的状态,由于这是在读取 CKB 的历史,所以并不会存在读的冲突,然后这个 SPV Cell 对应的 Live Cell 则持续根据 BTC 的状态持续更新,从而实现了读写分离。

使用这个方案时,历史 BTC SPV Cell 可能包含一些已被重组的BTC Header,但是我已经在之前的评论中对这种情况进行了安全性分析,结论是安全性与多SPV Cell是一样的。

这个设计,并不会对全节点和轻节点造成额外负担,以及影响交易历史的裁剪,因为验证一笔交易的合法性,依然只需要存储以下两项:

  1. 当前的Live Cell集合;
  2. 所有的CKB 区块头。

上面的论述应该已经表述清楚:

  1. 只使用一个 SPV MMR Cell 可以解决读写冲突问题,并且其合约实现上应该是会更简洁,只需要维护和更新一个Cell,无需维护 Multi SPV Cell的链接。
  2. 该设计不需要 CKB 本身新增任何功能,并且不违背 CKB 的设计理念。
  3. 其本质上是一种,使用 Witnesses 的空间,从而节省链上状态占用的tradeoff,正如在BTC某些应用的设计中,使用 Witnesses 会比交易的其他部分要便宜一样。

@yangby-cryptape
Copy link
Author

Reply @xcshuan:

我这次应该是真的完全理解你的意思了 😅 :就是 Cell Data 放 Witness 里。


理论上可以的,但 Witness 里至少要放:

  • 一个完整 CKB raw transaction (算 tx hash,用来保证 cell data )
    table RawTransaction {
        version:        Uint32,
        cell_deps:      CellDepVec,
        header_deps:    Byte32Vec,
        inputs:         CellInputVec,
        outputs:        CellOutputVec,
        outputs_data:   BytesVec,
    }
    
  • CKB Transaction Merkle Proof (和 tx hash 一起,用来和 header 里的 transaction root 做校验)
  • 以及原本的 BTC 相关的 Proof

这里我不清楚这样会不会导致更容易触发什么 limit :

  • Witnesses 是算 tx size 的。
  • Load witnesses 到 CKB-VM 是按照大小算 cycles 。
  • 算 Hash 的函数也是数据越大越费 cycles 的。

由于 CKB 使用和 BTC 类似的设计,那么使用 CKB SPV 为什么不行呢?

那边 Header 走额外提交的 我的想法也是为了节省:

  • 可以预计的,并非所有 BTC headers 都是要用到的,而且就算用到,一次才 80 bytes ;
  • 避免了滚动更新数据(header append 到 cell data)。
  • 再者, BTC 的 TxOut Proof 的格式上,本身就包含了 Header 。
    综上,那边设计上,提交的数据可以理解为:就是随用随提交,不和以往 数据 重合,所以没有冗余性。

我直觉感觉:你这个方案,成本高,贵!

  • 每个 tx 提交, witness 里还要再多一个 tx (而且本身还是 chain 上存在过的 tx )。

而且大部分数据还是无用的:

  • 只提交 Cell Data 证明不了 Tx Hash 的;
  • 你这个要多好多其他 Raw Tx 结构上的数据,比如就算 1 in 1 out ,就都有 lock script 和 type script。

然后放在 witness 里的,...,如果这个方案不能用的话,那 BTC SPV on CKB 也同样不可行了。

在 witness 里放 Proof 不是必须的,SPV 就提供函数而已,Proof 想放哪里随便,只要能调用到函数;我没试过,但我也想过会不会触发什么 limit 导致 witness 不够用,实在不行就放一个 Proof Cell 。

你这个方案,先放个 Proof Cell 我更嫌贵~~ (:woozy_face: 大概我小气惯了,哈哈~~)

@xcshuan
Copy link

xcshuan commented Mar 11, 2024

你这个方案,成本高,贵!
@yangby-cryptape 相比之前确实多占了一点 Witnesses 的空间,但是Witnesses的使用成本比 Cell Data便宜得多。可以进行简单的成本核算。

一个 Header 的 CellData 大致需要,80Bytes+lock(假定53Bytes)+type(假定53Bytes)+4Bytes=190Bytes,事实上不可能只存80Bytes,最终可能会占用到200Bytes。

而假定用Witnesses方案,假定SPV Cell交易都是 2 in 2 out的。

  • 两个 Outpoint+Since,其中一个付手续费,就是 74Bytes,两个output 加 cell_data,假定为 300 Bytes,2个Cell_deps,66Bytes,由此可以假定交易总占用500字节。
  • 由于CKB一个区块最多1000笔交易左右,树高度为11,则 Proof 约为12*32Bytes=384Bytes。

所以相比使用Cell存Header,每笔调用SPV Cell的交易,最终在Witnesses部分多出来的大小小于1KB,按2000shannons/KB的FeeRate,190Bytes的链上状态占用成本,够覆盖多出来的CKB花费成本执行 950 万次,考虑到一个BTC 区块最多四五千笔交易,这个方案的成本不可能更贵,反而便宜得多。

至于 Cycle 的问题,1KB的额外数据加一个哈希计算,可能顶多花100w Cycle吧,而Lockscript验证一个签名就几百万Cycle了,但是这还是远小于 Cycle上限。

并且这个方案是把额外成本分摊给了使用SPV Cell的用户,从经济模型上更合理,不然每次都创建新Cell,到后面谁来出资,过期的SPV Cell谁有权力回收?这就变成公地问题了。

@yangby-cryptape
Copy link
Author

yangby-cryptape commented Mar 11, 2024

@xcshuan

  • 现在的 Multi Cell 方案也暂时不需要在 Cell Data 里放 Headers 。

    我说的也可以把数据放 Cell Data 里,是一个通用讨论,无论什么方案, Witnesses 如果不够用都可以走 Cell Data 。
    强调下,目前的设计 Multi Cell 设计下,SPV 只提供 API 和一个 Cell :通过 Cell Dep 引用 Cell Data,然后调用 API , Proof 想放哪儿都可以。

    总之,用于 Prove ,用户需要的是:

    • Header MMR Proof
    • TxOutProof (Header 在里面了,不需要额外提供)
    • 如果用户需要相信 BTC Tx 里的数据,肯定还要完整 BTC Tx 来算 Hash 的。
  • 你的方案下,

    用户需要,除了上面的 Proof 用的,还需要:

    • 一个完整的 CKB Raw Tx (就是 Tx without witnesses)
    • CKB Tx Proof

    按照你说的:

    由此可以假定交易总占用500字节。

    不包含 Type Script ,没有 Cell Data ,总之,最简化的一个 2-in-2-out 的 secp256k1-blake160 交易的 size 是 597 (含了 witnesses )

    去掉 witnesses 也还有 560 ,假设 Client Data 本身还要 100 bytes ,就是 660 了 ,再加一个 type ,算 700 好了。

    假设引用它的 tx 也是一个 2-in-2-out 的,那么不考虑用户业务占用的 witnesses ,就已经是一个大约 1300 bytes 的 Transaction (含了 witnesses , 其中 witnesses 算 700 ,比本身 tx 还大)。

    一个怂人听闻的说法可以这么说:你这个设计使得 交易 尺寸 翻倍,使得 链 性能减半了。

  • 用户随着自己的设计,Witnesses 里大概还要放自己的业务相关数据。

    上面的 witnesses 算 700 还没考虑用户自己的数据呢。

  • Multi Cells 方案里,需要用户提供的数据,都是 CKB 确实没有的数据;你的方案的数据,对于 Chain 来说就是冗余数据。

    而且以上 Proof 都是用一次,就要提交一次的(除非使用 Cell Data 先存 Chain 上)。

    现在 mainnet 是 40Gib 的 DB, testnet 已经 128 GiB 。

    不是和 BTC 比价格,也是资源浪费。

  • 你的经济模型问题,我就不能理解了,现在是 N 个 SPV Cells 循环的模式,怎么会有公地问题?

    假设用户的 各种 Proof 合起来过大了,在 Witnesses 里提交不了,放一个 Cell Data 提交 Proof,验证完 BTC Tx 自己回收掉就好了。


Summary

我觉得我和你的分歧,类比一下,就是,我相对更认同 Luke Dashjr ,我觉得这样利用 Witnesses 塞 冗余数据 ,是不好的。

同时我还觉得你的方案,点儿当年 ETH 的 io 指令 price 设置低了,被钻空子了的感觉,感觉 CKB 目前对 witness 太宽松了。


Update:

  • 我的想法是:看了你的设计,我觉得应该提高 witness 的成本。

  • 于是,我在 CKB Core 里问了下,有人说,“这个模式已经泛滥了,我记得很久之前提过,说是不处理,不知道现在有没有态度上的变化” ;有待进一步讨论。

@xcshuan
Copy link

xcshuan commented Mar 11, 2024

@yangby-cryptape

去掉 witnesses 也还有 560

input * 2 = (32(tx_hash) + 4(index) + 8(since)) * 2 = 44, output * 2 = lock (32 + 1 + 20) + capacity(4) + lock (32 + 1 + 20) + capacity(4) + type(32+1+32) + output_data(100) = 279, cell_deps * 2 = 33 * 2 = 66, version = 4.

total = 393.

现在是 N 个 SPV Cells 循环的模式

如果是MMR Cell进行循环队列的模式,那我觉得是OK的。

看了你的设计,我觉得应该提高 witness 的成本

我相对更认同 Luke Dashjr ,我觉得这样利用 Witnesses 塞 冗余数据 ,是不好的。

在整个CKB交易中,Witness, output_data 以及script args 是用户可以自由写入的区域,如果攻击者想要恶意占用网络带宽和大幅度增大历史存储,在这三项里写,攻击者付出的成本是相差无几的。因为即便 args 和 output_data 需要占用CKB,也因可随时解锁无限复用失去约束效果,所以不存在提高 Witness 成本的问题,如果要提高攻击成本,就应该抬高整个 1000 Shannon/KB 的 基础 feeRate,而不只是Witnesses的定价。

身份不明的攻击者正尝试对 Zcash 网络进行垃圾交易攻击,相比 Zcash,至少 CKB 拥有动态的费率调整,以及根据交易大小定价的机制。但CKB目前经济模型过度依赖于状态占用,而将 FeeRate 设置为一个极低的值,可能也不是一个合理的设计,其对于网络带宽,历史存储,全节点同步速度都有影响。

在进行历史裁剪时,由于隔离见证的设计,所有的Witness数据可以首先被裁剪,并且影响最小,从这个角度考虑,Witnesses 的 定价理应比 outputs_data 的 feeRate 便宜一些。

@yangby-cryptape
Copy link
Author

input * 2 = (32(tx_hash) + 4(index) + 8(since)) * 2 = 44, output * 2 = lock (32 + 1 + 20) + capacity(4) + lock (32 + 1 + 20) + capacity(4) + type(32+1+32) + output_data(100) = 279, cell_deps * 2 = 33 * 2 = 66, version = 4.

不是你这么算的,还有结构上的部分,比如记录 input len ,记录 output len 等。
我算得是,我是写代码跑了一个出来的。

use ckb_types::{core, packed, prelude::*};

fn main() {
    let lock = packed::Script::new_builder().args([0u8; 20].pack()).build();
    let in1 = packed::CellInput::new_builder().build();
    let in2 = in1.clone();
    let cd1 = packed::CellDep::new_builder().build();
    let cd2 = cd1.clone();
    let cd3 = cd1.clone();
    let out1 = packed::CellOutput::new_builder().lock(lock.clone()).build();
    let out1 = packed::CellOutput::new_builder().lock(lock.clone()).build();
    let out2 = packed::CellOutput::new_builder()
        .lock(lock.clone())
        .type_(Some(lock).pack())
        .build();
    let empty: Vec<u8> = vec![];
    let raw = packed::RawTransaction::new_builder()
        .cell_deps(vec![cd1, cd2, cd3].pack())
        .inputs(vec![in1, in2].pack())
        .outputs(vec![out1, out2].pack())
        .outputs_data(vec![empty.pack(), [0u8; 1].pack()].pack())
        .build();
    let tx = packed::Transaction::new_builder().raw(raw).build();
    let sz = tx.serialized_size_in_block();
    println!("Size {sz}");
}

@yangby-cryptape
Copy link
Author

在这个项目之前,我还没写过最终 生产 部署的 合约项目,在 ckb-core 写合约多,但大部分都是写 tests 。

这个设计模式我确实觉得不是很认同。
但讨论后,貌似现在普遍都是这么做的,那我也不好说什么了。

按照当前 10s 才 1 个 Block , Block limit 是 597×1000 ,一年极限才 4 GiB 原始数据(不算存下来实际磁盘会放大的部分,比如 索引 之类)。


但我还是觉得这个方案是在制造不必要的冗余数据。

@xcshuan
Copy link

xcshuan commented Mar 11, 2024

反正要限制,只限制Witness定价肯定是不够的。

一年极限才 4 GiB 原始数据

@yangby-cryptape 这是一天的极限数据吧。。

做一个简单的运算,一个block最大597*1000 Bytes= 583 KB,按照 1000 feeRate来计算,假设出块时间 12s,则一天出块个数为 86400/12 = 7200,一年出块个数为 2628000,总大小为 2628000 * 583 KB = 1461 GB,那么攻击者需要的成本是多少呢?
攻击成本 = 2628000 * 583 * 1000 * 10^-8 = 15321.16 CKB,可以说,即便CKB涨到 10 美金,这个成本也算不上多高,所以目前的 feeRate 设置是极不合理的,之前就是没人用,如果有人想要恶意攻击的,完全可以在协议允许的情况下攻击。
如果将最低 feeRate 提高 100 倍,一笔普通转账交易的手续费从 0.00001 变成 0.001,对普通用户应该体感相差不大。

@matt-nervos
Copy link

matt-nervos commented Mar 12, 2024

regarding spam through witness data- Signature verification is the heaviest part of syncing a UTXO chain. In the case of recent zcash and monero spam attacks, though the transaction fee is priced in bytes, the verification burden is calculated in cycles. Downloading data is much faster than verifying signatures.

though CKB fees are calculated based on bytes, there is a limit to the number of cycles per block. I hope the market can find some equilibrium that discourages spam (because those spam transactions will reduce the # of cycles available to other transactions). Overall, an important distinction is that CKB does limit the verification burden explicitly, while other UTXO chains do not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment