学习
核心概念
对象
存储抽象

存储抽象

动机

智能合约编程语言和传统编程语言最大的区别是智能合约编程语言需要在编程语言内部提供标准化的状态存储接口,屏蔽状态存储的底层实现,智能合约应用主要关心自己的业务逻辑。

而“存储抽象(Storage Abstraction)”的目标是让开发者可以在智能合约中更灵活地定义应用的状态存储结构,而不局限于平台提供的标准化方案

我们先回顾一下当前的智能合约平台提供的方案,然后提出 Rooch 的 Storage Abstraction 方案。

EVM 的方案

EVM 中,合约的状态是通过合约的字段变量来存储,虚拟机直接将合约的字段映射到存储槽(Slot)中持久化。这种方案的优点是对开发者几乎透明,开发者可以把存储当内存使用,但也有一些缺点:

  1. 设计复杂的状态存储时,会遇到扩展性难题,开发者依然需要理解合约变量是如何映射到底层存储的。
  2. 存储槽对外部系统访问不友好,外部系统很难直接解析存储槽中的数据。
  3. 状态锁定在合约内部,链无法区分用户的状态和合约内的共享状态。这给状态计费 (opens in a new tab),以及在链的层面提供状态的安全保证都带来了难题。
  4. 同时,也因为状态锁定在合约内部,合约之间只能进行“信息”互通,无法进行“状态”互通。

Move 的方案

Move 对智能合约状态存储做了改进,应用需要通过全局存储指令进行显式操作,它主要提供以下指令:

  1. move_to<T:key>(&signer,T):将 T 类型的资源存储在 signer 的用户状态空间内,这个只能通过用户自己发起的交易执行。
  2. move_from<T:key>(address):T:将 T 类型的资源从用户状态空间中取出来。
  3. borrow_global<T:key>(address):&T:从用户空间中读取 T 类型的的不可变引用。
  4. borrow_global_mut<T:key>(address):&mut T:从用户空间中读取 T 类型的的可变引用。

以上指令都包含两个安全层面的约束:

  1. T 类型必须是拥有 key 能力(Ability)的结构体(Struct)。而结构体中的字段都必须是拥有 store 能力的数据类型。
  2. T 类型必须定义在当前的模块中。

第一个约束确保只有开发者明确声明具有 store 能力的数据类型才能写入存储层。第二个约束确保合约之间的状态安全。当前模块中数据结构的存储操作只能通过当前合约提供的方法进行,开发者可以在方法中封装访问校验逻辑,保证安全性。

这种方式带来了几个好处:

  1. 明确了状态的所有权,方便底层的系统设计状态计费以及提供安全措施。
  2. 类型系统是全局的,合约之间可以进行状态互通。当然,这点也依赖 Move 提供的类型安全机制。

同时,Move 提供了 Table<K,V> 扩展,开发者可以通过 Table<K,V> 自定义 Key-Value 存储。

Sui Move 的 Object 改进

Sui Move 中废弃了上面的 Move 的全局存储指令,提供了一种对象(Object)模型。Object 是一种特殊的结构体,它需要拥有 key 能力,同时第一个字段必须是 UID。Sui Move 设计了一套所有权机制,来定义 Object 的所有权。Sui Move 这样设计主要是为了通过类似 UTXO 的模式来实现交易的并行,所以它需要客户端在交易参数中指定合约中需要操作的 Object,这样可以快速判断交易之间是否有冲突。同时,它还提供了 Object 的父子关系机制,方便开发者设计复杂的状态结构。

Sui Move 的 Object 安全约束:

  1. Object 有明确的拥有者(Owner),或者是共享的(Shared)Object,虚拟机加载参数的时候需要校验 Object 的所有权。
  2. 合约内的 Object 有两种方式转让所有权。一种是 transfer<T:key>(T,address),这个方法只能是在定义 T 的当前模块中调用,需要开发者再次封装给用户使用,开发者可以自定义转让的验证逻辑。另外一种是 public_transfer<T:key,store>(T,address),对象拥有者可以直接调用该方法转让 Object,但对 T 有额外的能力约束,必须拥有 store 能力。

Move 的全局存储指令都是基于用户的,获取一个资源必须先知道它存在哪个用户的空间下,然后基于该资源的类型来获取。Object 模型给 Move 的状态存储引入一种基于 ID 的存储。同时,它通过 public_transfer 提供了一种用户可以跳过开发者直接操作状态的能力。

Rooch Storage Abstraction 设计原则

通过以上方案的分析,我们发现合约的存储模型主要是定义合约的执行平台、合约开发者和用户三者之间的关系。所以在 Rooch 中,借鉴以上方案的优点,提出了 Storage Abstraction 的概念,有以下设计原则:

  1. 简单抽象原则。底层的存储接口尽量抽象简单。
  2. 合约优先原则。更丰富的状态存储接口应该尽量通过合约来实现,而不依赖底层的智能合约平台的实现。
  3. 自我感知(self-aware)原则。合约内部对合约的存储数据结构有感知。
  4. 互操作友好原则。外部系统可以方便地访问到合约定义的数据结构,同时也可以方便地获取到存储证明。
  5. 所有权明确原则。所有的存储数据所有权明确,合约的执行平台、合约开发者和用户三者之间的关系明确。

方案设计

Storage Abstraction

前面我们提到,智能合约需要给应用提供状态管理的接口。我们认为智能合约的状态管理应该像堆内存操作一样简单,但有明确的所有权。在 Rooch 中,Object 是状态存储的基本单元。Object 类似于一个“智能指针”,保存一个状态空间的地址,同时也代表这块状态空间的所有权。

然后我们通过 Object 以及 Object 提供的动态字段,封装出上层的存储数据结构,Table, TypeTable, Bag 等。在 Rooch 中,Account 也是一个 Object,用户的 Resource 保存在 Object<Account> 的动态字段中。这样,我们可以通过 account 模块提供的方法,替代了 Move 的全局存储指令,实现了 Object 模式和 Account Resource 模式的统一。

在 Rooch 中,ModuleStore 是个特殊的 Object,它保存了所有的用户部署的 Package。每个 Package 都是 ModuleStore 子 Object。而每个 Package 包含了多个 ModuleModulePackage Object 的动态字段。

而基于这样的分层对象模型,应用可以设计出 AppSpecificStorage。它可以让应用的状态都保存在同一个 Object 状态空间里,而不同应用的状态空间可以分布在不同的节点上,这是 DSTP 的智能合约状态层的基础。

状态树的设计

我们认为通过状态树提供状态证明,是 Web3 系统和外部系统实现互操作的重要特性,所以 Rooch 的状态存储都围绕状态树来设计。

Rooch 的状态树使用稀疏默克尔树(Sparse Merkle Tree (opens in a new tab))实现。SMT 的两个特性对状态树非常有帮助:

  1. 通过压缩优化 SMT 的中间节点,性能良好。
  2. 可以同时提供包含证明和不包含证明,这点在 Rollup 场景尤其有用。

Rooch 中的每个 Object 都代表一个 SMT,Object 中的 state_root 字段保存了 SMT 的根哈希。Object 的动态字段的 Key 是 SMT 的路径,Value 是 SMT 的叶子节点。

statedb

在 Rooch 中,第一层的状态树的叶子结点是 Object,而每个 Object 也携带着一个状态子树,子树中可以保存该 Object 的动态字段,或者子 Object。比如 BitcoinStore 是一个 Object,保存了 Bitcoin 链上的所有状态,UTXO 以及 Inscription 都是该 Object 的子 Object。

而这种模式也可以用在应用中,比如有一个游戏,它的状态表达为 Gameworld,游戏中的状态都在这个 Object 中,这样可以实现应用间的交易并行以及状态拆分,Gameworld 的状态可以存在专门的节点上。

但所有的子树,最后会汇总成一个 Root SMT,这个 Root SMT 的根哈希会被写入到 Bitcoin 的交易中,保证了状态的可验证性,应用也通过 Bitcoin 的时间戳来证明,用户的状态在某个时间点之前就已经存在。

Object

Rooch 的 Object Storage 中,存储数据结构是 ObjectEntity<T>,而 Object<T> 相当于 ObjectEntity<T> 的手柄或者钥匙。

module moveos_std::object{
    
    struct ObjectEntity<T>{
        id: ObjectID,
        owner: address,
        /// A flag to indicate whether the object is shared or frozen
        flag: u8,
        value: T,
    }

    struct Object<phantom T> has key, store {
        id: ObjectID,
    }
}

ObjectEntity 没有任何 ability,只能被底层的存储接口访问。开发者可以通过 Object<T> 访问 ObjectEntity<T> 中封装的数据,二者的生命周期也是相同的,开发者只需操作 Object<T> 相关的接口,不需要关心 ObjectEntity<T>

关于 Object 的使用,请参看文档 Object

Private Generics

Move 的全局存储指令对其泛型参数进行了限制,以确保合约状态的安全。在 Rooch 中,我们引入了 #[private_generics] 注解,允许开发者将这些限制附加到自定义函数上。这样,开发者可以在智能合约中定义更丰富的存储数据结构,同时保证这些结构的安全性。

总结

Rooch 通过结合 Move 的 Object 模型和状态树,实现了智能合约状态的抽象存储。这种设计不仅继承了 Move 的状态所有权明确和类型安全特性,还通过状态树提供了高效的状态证明机制,增强了系统的可验证性和安全性。

在执行平台、合约开发者和用户三者的关系上,Rooch 提供了更大的灵活性,使开发者可以自由地设计和实现应用的状态存储结构。这样,开发者不仅可以更好地满足各类应用的需求,还能确保存储的高效性和安全性,促进智能合约生态系统的进一步发展和创新。