问题
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账户的变量即可。
首先构造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的优化规则是:
-
如果交易的一个input是typeid cell,且output里没有对应的typeid cell。这种情况保持这个typeid cell不动,即不消费它。
-
如果交易的一个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不能变,可以用来间接定位:
-
Cell data。可升级合约只是一个特例,其实任意的需要变更数据的场景都可以。
-
Lock script。可替换owner的xUDT只是一个特例。我们可以轻松实现抽象账户,简化DID实现,并且这个DID是与ckb深度绑定的。
更大胆的想法
我们实现了单纯的读取不会消费掉对应的cell,只有真正的修改才会发生消费。
所以可以高效的通过merkle tree把cell组织成树状结构,形成类似evm storage的东西,进而实现类似solidity这样更接近普通编程语言的合约语言。
ckb将会有两层合约语言:现有的script更像是builtin;上层的合约语言类似一个工作流语言。这样会比以太坊的表达能力更强大。