针对typeid的优化与扩展

问题

ckb上编程非常麻烦,其中很重要一点编程模型比较受限,典型的一点就是没有表达状态的变量。

以可升级合约这个具体的例子进行说明。

可升级合约在以太坊上实现非常简单, 参见 如何部署和使用可升级的智能合约 。以太坊上部署不同版本的合约会随机产生出不同的合约地址,但是只要设置一个proxy合约,里面有一个变量指向真正的合约地址。当需要升级合约的时候,只要部署新版本的合约,将新合约地址赋值给proxy合约中的变量即可。

ckb是以合约的二进制的hash,即codehash来定位合约代码,不同版本的合约同样会有不同的hash。ckb提出的解决方案是typeid,参见 Tutorial: Upgradable Scripts with Type ID | Nervos CKB 。其实思路跟以太坊上的proxy合约非常类似,即通过一个升级合约不会变化的unique id间接去定位合约代码。


这个方案针对可升级合约是没有问题的,但是在另外一个场景里就显得有点繁琐了。


新的场景是可替换owner的xUDT。

xUDT的type arg是lock hash(基本可以等同于账户地址),表示该token的owner。这个设计也非常类似与之对标的以太坊上的ERC20。ERC20合约通常会在部署的时候通过构造函数将部署合约的账户设置为该token的owner。

目前ckb上出现的UDT大部分都不考虑可替换owner,因为它们采用类似BTC的固定上限发行模式,生命周期中只会有一次mint。

但是对于稳定币等不是固定上限发行的UDT来说,就会有多次mint。出于安全的考虑(比如私钥丢失,泄露),就需要可替换owner。

类似前面的场景,以太坊上实现可替换owner非常简单,且同样是因为有表达状态的变量,只要在合约中增加一个函数,修改保存owner账户的变量即可。

ckb上目前的方案是 https://github.com/ckb-ecofund/ccc/blob/master/packages/demo/src/app/connected/(tools)/IssueXUdtTypeId/page.tsx。

首先构造typeid cell,它的lock script是真正的owner账户;然后使用OutputTypeProxyLock这个特殊的lock script构造一个lock cell,lock arg是前面的type id,最后xUDT的type arg是第二部lock cell的lock hash。

当要变更xUDT的owner的时候,只需要变更typeid cell的lock script,lock cell因为里面放的是type id,所以不会跟着变化,进而xUDT的type arg也可以保持不变。

其原理主要也是借助typeid这样一个不会随owner变更而变化的unique id间接去定位lock script。

繁琐的原因是,前面可升级合约场景中,调用合约代码是不会导致typeid cell被消费的,只有要升级合约(变更合约代码)的时候才会把cell消费掉。但是在后面的场景中,虽然合约只是比较了一下owner,但是ckb中解锁lock script就必须要消费掉相应的cell。因为就要在每一次mint的时候,将原来的type id cell 和lock cell消费掉,然后在output中产生一模一样的type id cell 和lock cell。

理论

UTXO类型的区块链是典型的,非常严格的linear system。每个cell只能用一次,或者想想Rust的move语义。

但是严格的linear system表达能力确实是非常受限的,因此会增加 exponential modality 支持可受控的复制。类比Rust中的Copy/Clone。

ckb中的typeid其实就是 exponential modality 一个非常朴素的实现。就像前面举例的场景,可以工作,但是非常的繁琐。

针对性的优化手段可以参考 linear system中一个非常基础的优化 in-place update。

同样以Rust的move语义举例。

    let x = "hello".to_string();
    let y = x; // move
    println!("x: {}", x); // error: value borrowed here after move

但是我们从优化角度考虑,第2句将变量move的时候,实际代码是为y新分配一块内存,把x指向的字符串拷贝过来,再把x指向的内存释放掉吗?不会的,更高效的做法是直接复用原有的内存,把指针换一下即可。

这个优化是很安全的,其安全性恰恰是由linear system提供的。因为其保证了后续x变量不会再被使用了。

优化

具体的针对typeid的优化规则是:

  1. 如果交易的一个input是typeid cell,且output里没有对应的typeid cell。这种情况保持这个typeid cell不动,即不消费它。

  2. 如果交易的一个input是typeid cell,且ouput里有对应的typeid cell。这种情况的处理跟现在保持一致,消费并生成新的typeid cell

安全性:typeid本身就保证了一换一,类似前面的理论分析的情况,具体优化也就是in-place update,所以是安全的。

兼容性:第2条规则对应的场景就是确实要更改typeid cell里的内容了。现有的方案(比如前面提到的可更换owner的xUDT),只不过相当于不管cell内容变没变都实施一次变更,自然退化到没优化的情况,语义上是兼容的。

扩展

经过前面的分析可以看到,typeid的威力远比我们想象的强大。它可以实现类似以太坊中表达状态的变量。

Tyepid cell除了type script本身作为unique id不能变,可以用来间接定位:

  1. Cell data。可升级合约只是一个特例,其实任意的需要变更数据的场景都可以。

  2. Lock script。可替换owner的xUDT只是一个特例。我们可以轻松实现抽象账户,简化DID实现,并且这个DID是与ckb深度绑定的。

更大胆的想法

我们实现了单纯的读取不会消费掉对应的cell,只有真正的修改才会发生消费。

所以可以高效的通过merkle tree把cell组织成树状结构,形成类似evm storage的东西,进而实现类似solidity这样更接近普通编程语言的合约语言。

ckb将会有两层合约语言:现有的script更像是builtin;上层的合约语言类似一个工作流语言。这样会比以太坊的表达能力更强大。

2 Likes

发现一个问题:优化规则1和现有实现不兼容。现有实现input中有typeid cell,而output里没有对应的typeid cell,其语义是销毁typeid。

要实施这个优化需要增加新的typeid script。

very cool idea :cowboy_hat_face:

3 Likes

我可以单纯的来增补另一个角度(但是具体哪个方向更合适,我不做过多评论):

lock hash(基本可以等同于账户地址),表示该token的owner

注意这一条仅仅是我们在目前使用 CKB 时,选择的一种使用方式,它并不代表 CKB 就只能这么使用。我其实完全可以设计一个不同的 lock script,这个 lock script 并不从 script args 中读取账户对应的公钥信息,而从某一个 cell 的 cell data 端来读取公钥信息。

当然,这样以 cell data 来提供公钥信息的 cell,也就有了防止篡改的需求,我们可以类似引入 type id 解决问题。

其实在最早设计 owner lock 时,就有考虑过这种用法:我们构建一个特殊的 cell,以如下结构呈现:

type: type id script B
data: secp256k1(or other signature verification algo) pubkey hash
lock:
    code_hash: lock A
    hash_type: lock A
    args: type id script B

这里 lock A 就是我们新设计的 lock,它以自己 args 中的内容作为 type script hash,找到一个实际的 cell,以这个 cell 的 cell data 中的内容作为 pubkey hash,完成对当前 tx 的验签流程。

以这里的 lock A 作为 sUDT / xUDT 的 owner lock,我们就可以实现 UDT 的多次增发,同时也可以根据实际的需求,更换 owner lock 对应的 owner(或者说公私钥对,或者叫地址,其实都是指一样东西)。

在这里前面关于 xUDT owner 的问题,补充完了,我想继续问一下优化部分的问题:

  1. 如果交易的一个input是typeid cell,且output里没有对应的typeid cell。这种情况保持这个typeid cell不动,即不消费它。
  2. 如果交易的一个input是typeid cell,且ouput里有对应的typeid cell。这种情况的处理跟现在保持一致,消费并生成新的typeid cell

我没有理解这两个优化规则,对第一条来讲,“不消费它” 是什么意思?在类 UTXO 模型中,一个 cell 放在 input cell 中,CKB 就会认为这个 cell 被销毁了,这里跟实际用的是不是 type id 合约已经没有关系了。所以我比较好奇,这里优化的点能不能再详细阐述下?

我们实现了单纯的读取不会消费掉对应的cell,只有真正的修改才会发生消费。

我理解这个需求在 CKB 里对应的是 dep cell,而不是 input cell。所以其实这里想说的是 dep cell 也有执行合约的需求?对这个需求的话,我个人双手双脚赞成,我想要很久了。

最后再补充一点:type id 现有的设计,其实就是满足 type id 所需功能的最小实现,我们其实在多个不同的场景都讨论过,type id 的规则,根据不同的实际情况,完全可以继续扩充,这里都没有问题。比如我记得曾经讨论过一个 case,可以给 type id 要 hash 的内容里,加一个 salt,这样可以先生成一个 type id hash,而延后部署这个 type id hash 所对应的 cell。所以本身要扩充 type id,其实并没有问题。只不过我们要看每个单独的需求,是扩充 type id 合适,还是有其他的解决方案,比如前面聊的对 lock script 的需求,至少是可以选择提供一个不同的 lock script 来解决,并不一定需要调整 type id。

1 Like

我这这个仓库中做过一个这个想法的实现, 参考里面的 lock-wrapper:

它需要依赖一个 global registry 来实现链上的 uuid, 后来我们抽象出了一个更通用的 global-registry :

关于 xUDT owner 的问题:你举例的lock A已经实现了,就是 @Hanssen 提供的解决方案——OutputTypeProxyLock。
我们也讨论过,优化掉这个特殊的lock,直接把xUDT的arg从lock hash改成typeid。这个在技术上是完全没问题的,唯一的缺点就是跟现在的xUDT不兼容。

先叠个甲,这个帖子描述的只是一个还不成熟的想法,我们先把改造的难度放一边,仅从设计的优雅度考虑。

我想表达的是,如果我们意识到typeid可以是表达状态的变量,那如果重新设计的话,从合约的可扩展性考虑就会把xUDT的arg直接设计成typeid。
就像以太坊的ERC20,其实大部分链上的合约并没有设计change_owner的函数,但是依然会有owner这个变量。


然后是优化部分。
实际上我帖子中描述的事情,现在都可以实现,比如下面 @quake 已经做过一些尝试。
问题就是比较繁琐,交易里会有很多一换一的typeid cell。

这里就是testnet上用OutputTypeProxyLock实现可替换owner之后的一笔mint交易 CKB Explorer
input-1和output-0; input-2和output-1 是两组一模一样的cell。
每次mint交易都得构造这样两组一换一的cell。

所以这个优化是纯实现上的优化,就是 in-palce update。

关于“不消费它”,这个确实打破了 一个 cell 放在 input cell 中,CKB 就会认为这个 cell 被销毁了 的底层概念,但是就像我前面分析的,其实是很安全的。

最后你提到了dep cell,单纯从读取不消费的角度来说,在当前的设计中确实是体现在dep cell里。但是目前dep cell只能用于code或者data,不能用于lock吧?dep cell我不太了解,如果理解有问题,大家可以指正。

这个也是我觉得设计上不一致的一个点。
在可升级合约场景里,我们可以引用代码而不把放代码的cell消费掉。
但是在可替换owner的xUDT场景里,其实合约也只是引用了一下owner,但是限于现有规则,就必须把它消费掉。

类比到以太坊中。可升级合约关键是proxy合约中存放实际合约地址的一个变量,可替换owner的关键是Erc20合约中存放owner的变量。
这两个变量都是可以只读引用而不引起world state变更的。
但是在ckb里面,两者的行为是不一致的。引用合约不会产生cell的消费,但是引用owner就要产生cell的消费。

最后,如果能实现的话,落地方案是扩展typeid还是扩展dep cell我觉得都可以,具体的我们可以再展开讨论。

1 Like

这个的确,两种方式其实都能工作,至于选哪种,哪种更优雅,则是一个我不想过多参与的话题了。我只是想点出其实两个角度都可以工作。

我个人认为这个根本假设,我们不太可能做改动。一个 input cell 不会被销毁,其实打破了 UTXO 模型的根本假设了。

我觉得这里想聊的问题其实是能不能给增加 dep cell 比如增加某些 flag,做到某些 dep cell 的 type 是会在当前 tx 中需要执行的。

2 Likes

是的,我比较认可谨慎考虑和处理问题的方式;站在用户使用的角度,如果一个底层的变更需要大量上层应用做适配,或者需要用户去配合迁移,我觉得都需要绝对慎重的考虑。

有些事情不光是一次硬分叉看起来那样的难度,还要考虑到用户和市场这些更广更泛层面的问题;就像近期CCC库虽然统一了地址问题,确实是一个非常棒的举措,但是即便是这样一个有百利而无一害的事情,你像很多交易所目前对接的仍然都是原来旧的CKB地址,这对用户的使用上尤其是新用户多少都会造成一些困惑;如果要是CKB一开始就能统一地址,那该多好啊,相信对于CKB的使用和流行一定会是别一番景象!

举这个例子,不是说CCC库做的不好,恰恰相反,是这个太好的举措来的有点太晚,也是想借统一地址这个事情遇到的周围方方面面的情况,来表达,关键的节点和事件,就像蝴蝶煽动翅膀,它对未来造成的影响,是很难估量的,有时候需要放到很长的时间线里才能看到。

所以感恩每一个为CKB深度和慎重思考的构建者!

1 Like

我看了一下你那个lock-wrapper,挺有意思的,单纯靠lock script就解决了替换账户的问题。

其实我遇到可替换owner场景,第一时间想的也是能不能仅靠lock script来解决。
我直觉上是觉得type script和lock script产生耦合不大好。
我去看了Identity-Based Crypto之类的密码学解决方案,但是都有各自的问题。
我还去看了.bit的一些资料。我文中说DID跟ckb结合的不够深入,就是说没法直接把.bit域名作为xUDT的arg。

@xxuejie 说的其实就是在当前设计下,如果一个cell在input里用到,但是又不想消费它,那就是dep cell。

我昨天以为dep cell只能间接定位数据,没法间接定位lock script。今天看了lock-wrapper,发现也是可以的。
dep cell这块我还没深入去看,我先看一下相关的资料。

1 Like

There are 7 bits available to use in cell dep dep_type.