前言
当前,区块链之上的各种应用的雏形已经具备,但如何让区块链技术在更广泛的用户和场景中应用,是所有的公链需要解答的问题。
这个问题进一步拆解,其实有两个问题:
区块链如何承受大规模用户?也就是区块链的扩容难题。
去中心化应用,也就是 DApp 应该以什么形态和链结合?如何降低应用的开发成本。
关于扩容的问题,当前区块链世界主要有三条路线:
通过改进共识机制,或者减少验证节点等方式来实现扩容。
通过分片或者平行链来实现扩容。
通过分层的方式进行扩容。
这三条路线,各有优劣,Starcoin 选择了第三种路线,主要是基于以下理由:
分层是一种人类社会习惯的解决扩容问题的方式,比如司法系统,政治机构,都是通过分层的方式来解决扩容难题,只要实现不同层之间的制约机制即可保证安全。
区块链的不可能三角中,Layer1 应该更侧重安全。
不同的应用,应用的不同阶段,对去中心化的要求,对安全的要求,对吞吐的要求都不一样,需要有一种渐进的演变的方式和定制化的方案,通过分层的方式更容易实现。
Stargate 是 Starcoin 区块链网络上的分层协议框架。它通过统一的抽象,支持不同的分层方案。DApp 可以根据自己的需求进行定制,但可以和主网共享同一套基础设施。
关键方案
分层的所有方案中,主要面临三个技术难题:
如何在 Layer1 校验 Layer2 的执行结果并进行仲裁?Layer3 到 Layer2 同理。
Layer2 以及 Layer3 能否依赖 Layer1 的合约?
合约的状态如何在不同的层之间迁移?
交易校验
区块链上执行一个交易,虽然要基于历史状态进行,但实际上不会读取全部的历史状态。如果只提供该交易所依赖的状态,也可以计算出新的状态。
区块链的状态转换可以通过以下公式表达(以太坊黄皮书):
σt+1 ≡ Υ(σt, T)
σ𝑡+1 代表下一个世界状态
Υ 代表状态转换函数
σ𝑡 代表当前世界状态
T 代表一个交易
Y 要执行 T 的时候,依赖当前状态 σ𝑡,但 Y 并不会读取所有的历史状态,如果把 Y 需要的状态抽出一个子集 σ`𝑡,并同时提供证明,σ`𝑡 属于 σ𝑡,就可以实现状态转换。
我们把交易所依赖的前置状态子集叫做读取集(ReadSet),状态的证明叫读取集证明(ReadSetProof),把它们和交易以及执行后的状态树的根哈希一起打包,叫做富状态交易(StateFullTransaction)。
数据结构表达如下:
pub struct ReadSet{ state:Vec<(AccessPath,Vec<u8>)>, } pub struct ReadSetProof{ //TODO define proof } pub struct StateFullTransaction{ read_set: ReadSet, proof: ReadSetProof, transaction: SignedUserTransaction, //Transaction info is transaction execute result, include new state_root, event_root, etc. transaction_info: TransactionInfo, }
富状态交易(StateFullTransaction)本身包含了交易执行的前置状态,是可以自校验的,校验方法的伪代码表达如下:
StateLessVM{ fn eval(txn: StateFullTransaction, prev_state_root: HashValue): bool{ //通过 ReadSet 构造状态树 let state_tree = build_state_tree(txn.read_set); //验证 state_tree 的 root 和 prev_state_root 是否一致 assert(state_tree .root == prev_state_root); //在状态树的基础上执行交易 let output = execute_txn(&state_tree, txn.transaction); //将执行结果中的 WriteSet 写入 state_tree state_tree.apply(output.write_set); //验证执行后的结果和 StateFullTransaction 的 transaction info 匹配。 assert(state_tree.root == txn.transaction_info.state_root); //验证 transaction_info 中的其他字段 } }
校验富状态交易不依赖于外部状态,所以校验所需的 VM 是 StateLess 的,交易执行所包含的合约代码以及其依赖也一并包含在 ReadSet 中。
智能合约的依赖
区块链分层后,对智能合约之间的依赖关系带来了难题,Layer2 的合约能否依赖 Layer 1 的合约?这里的依赖包含两层意义,一代码的依赖,二状态的依赖。在 Ethereum 上,合约之间不能在编译期依赖,合约之间的依赖都是通过合约内的动态调用的方式实现的,一和二是同一个问题。这个问题在Ethereum 当前的所有 Layer2 方案中,都未得到很好的解决。
再 Stargate 中,由于 Move 的静态依赖的特性,Layer2 可以在编译期直接依赖 Layer1 的合约。当 Layer2 的合约执行时:
如果 Layer2 合约依赖的 Layer1 的合约是无状态的,不需要读取状态(比如纯算法的合约),则和 Layer1 合约和 Layer1 合约之间的依赖一样。
如果 Layer2 合约依赖的 Layer1 的合约获取了只读状态(使用 borrow_global 指令),则通过远程状态加载器从 Layer1 获取状态,不过依赖的状态 Layer2 交易关联的 Layer1 的区块的历史状态。
如果 Layer2 合约依赖的 Layer1 的合约获取了可修改状态(使用 borrow_global_mut/move_from/move_to 指令),则表明这个交易是一个跨层的交易,需要到 Layer1 执行跨层的状态迁移交易。这部分是否可以做成对开发者完全透明,需要进一步技术调研。当前先通过一种显示的方式进行状态迁移。
这样,就可以提供一种近乎于无缝的跨层的编程体验。
状态迁移
状态在不同的 Layer 之间迁移,类似于跨链方案,当前大多数方案都是通过合约将 Token 或者资产在某一层锁定,然后在另外一层铸造出来,需要针对每一种状态或者资产类型设计校验以及铸造方案(比如 Token 和 NFT 的校验是不一样的),或者只实现跨链的合约调用,确保交易执行成功,并不校验状态(有安全风险)。
使用 Move 这种面向资源的编程语言时,状态在不同的 Layer 之间迁移和合约中从外部存储加载状态类似,我们可以设计一种通用的状态迁移模型。
定义一种跨层的交易类型,该类型的交易实际上包含两个交易,一个需要在 Layer1 执行,一个需要在 Layer2 执行。
Layer1 的交易中,通过调用跨层的合约,将状态 S 从 Layer1 移动出来,锁定在一个特殊的数据结构中,叫做 SpacetimeBox, 这个结构同时存在与 Layer1 和 Layer2 的状态中。
Layer2 的交易中,通过调用跨层的合约,将 SpacetimeBox 移动出来并销毁,得到 S。
Layer2 给 Layer1 汇报 StateRoot 时,同时提交 SpacetimeBox 的不存在状态证明,证明 SpacetimeBox 已经在 Layer2 得到正确处理,Layer1 释放 SpacetimeBox。
如果想从 Layer2 将 S 迁移回 Layer 1, 方法同理,不过顺序相反。
从 Layer2 到 Layer1 的迁移,不同的方案有不同的挑战期,挑战期间中间状态会锁在 SpacetimeBox 中。
示例代码如下:
module CrossLayer{ // Move state `s` to layer2 with the `id`, only can call on layer1 public native move_to_layer2<S>(signer: &signer, id: Layer2ID, s: S); // Move state `S` from layer2 with the `id`, only can call on layer1 public native move_from_layer2<S>(signer: &signer, id: Layer2ID): S; // Move state `s` to layer1, only can call on layer2 public native move_to_layer1<S>(signer: &signer, s: S); // Move state `S` from layer1, only can call on layer2 public native move_from_layer1<S>(signer: &signer):S; } //transaction on layer1 public(script) script_on_layer1(signer: Signer){ let s = MyModule::get_state_from_somewhere(&signer); CrossLayer::move_to_layer2(&siger, dappx_layer2, s); } //transaction on dappx layer2 public(script) script_on_layer2(signer: Signer){ let s = CrossLayer::move_from_layer1<S>(&siger); //do something with s. LocalModule::save_to_layer2(&signer,s); }
SpacetimeBox 的封装以及校验是在 native 层实现的,对合约层是透明的。通用的状态迁移最大的难题在于状态的校验,我们可以把状态分为两种:
可以合并的状态,例如 Token。比如 1000 个 A Token 和 100 个 A Token 可以合并为 1100 个 Token。每种可以合并的状态需要在 Layer1 累加一个总数,跨层迁移时进行校验,保证二层不能凭空创造出 Token。
不可合并的状态,例如 NFT,或者用户合约自定义的自由状态。SpacetimeBox 中记录了原始状态的哈希,保证状态跨层迁移时状态不会被改变。不可合并的状态,从一层迁移到二层,只能改变归属,不可变更。
技术架构
整体上分为三层,
Layer1 通过 PoW 共识保证安全(Security)和无准入(Permissionless)。这部分已经实现。
Layer2 通过 PoS 共识给 Layer1 提供终局性(Finality),同时给 Rollup 的 Accumulator 提供去中心化能力。 Rollup 方案可以让网络整体的 TPS 提高到 10~100 倍。
Layer3 通过状态通道(State Channel)以及 DAppChain ,将不同的应用的共识隔离在应用的局部网络中,可提供无限的扩展能力。
Rollup
Rollup 是一种相对成熟的扩容机制,一些项目已经实现,在 Rollup 中有几个角色:
累加器(Accumulator),在其他项目中叫定序器(Sequencer)或者聚合器(Aggregator)。它主要的功能是给交易排序,并且定期将 Layer2 的交易提交到 Layer1 。Stargate 的 Rollup 方案中,它不仅仅提供排序,同时会将交易哈希进行累加,用于提供交易的顺序证明。累加器同时也要执行交易,并且定时将每个交易执行之后的状态树的根哈希提交到 Layer1。
状态提供者(State Provider),按照累加器提供的交易顺序,执行交易,给第三方提供状态查询接口。
校验者(Verifier),执行交易并与累加器提交到 Layer1 的状态树的根哈希进行比较,如果发现累加器作弊则向 Layer1 提交欺诈证明。
当前 Stargate 的 Rollup 方案属于 Optimistic Rollup 的一种。Optimistic Rollup 方案下,Layer1 并不会校验 Layer2 的每个交易,而是乐观的相信 Layer2 的 Accumulator 节点,直到有校验者提出欺诈证明,这样相当于把计算和状态都迁移到了 Layer2。
考虑到未来可能引入 ZK Rollup 方案,Stargate 的架构上,交易的校验机制设计为一种抽象的方案,可以通过富状态交易的重复执行的方式实现,也可以通过零知识证明的方式实现,可以通过统一的架构适配不同的方案。
Stargate 的 Rollup 方案相对于其他的 Rollup 方案有几个差异点:
累加器复用 Layer1 的 PoS 共识机制以及节点集群,实现去中心化以及高可用。
通过累加器提供的顺序证明,以及富状态交易的验证机制,让钱包客户端也具有验证交易的能力,从而承担校验者的能力。
数据可用性由 Layer2 PoS 共识网络以及钱包客户端来保证,累加器可以只给 Layer1 提交 Layer2 交易的哈希,这样 Rollup 可以将吞吐再提升一个量级。
Rollup 依然依赖 Layer1 的全局共识,所以受限于 Layer1 的吞吐能力。它的主要目标是降低 Layer1 的计算成本,将状态维护在链下,通过乐观(Optimistic)挑战机制保证安全,可以即时确认 Layer2 的交易状态,给用户提供类似互联网应用的体验,同时也为 Layer3 的扩展方案打好基础。
State Channel
状态通道(State Channel) ,或者叫支付通道(Payment Channel),以闪电网络(Lightning Network )为代表,也是一种比较成熟的扩容方案。它的思路是双方各自抵押一部分资产在链上,然后在链下维护一个两个参与方(理论上也可以扩展到多个参与方)的局部共识状态,每次变更都需要双方确认。但为了解决单方不合作难题,任何一方都可以单方在链上发起关闭通道的交易,等待一个挑战期后,通道自动关闭,参与方按最后一次双方确认的状态进行清算。
Stargate 中的 State Channel 方案和其他状态通道有几个差异点:
State Channel 中可以执行合约,这样通道不仅仅用来转账,还可以执行一些复杂的,有状态累计的合约。
因为它可以执行合约,所以也可以支付任意类型的 Token,以及 NFT。
状态通道构建在 Layer2 之上,而不是 Layer1 之上,主要是要依赖 Layer2 的即时确认能力,降低建立通道的成本以及确认时间。
状态通道之所以没有大规模的应用,关键原因有两个,Layer1 上的状态通道创建成本比较高,等待确认时间比较长。
但如果状态通道在 Layer2 之上,则可以消除这两个难题。这样 Starcoin/Stargate P2P 网络中的任意两个节点,都可以将自己的 P2P 连接升级为状态通道,然后通过状态通道进行数据传输和流式计费。同时,也为 DAppService 提供了基础设施。
DAppService
基于 Stargate 框架搭建的,通过状态通道网络提供付费的 RPC 服务。
DAppService 本身不是去中心化的,但它是在 P2P 网络上的,可以通过 P2P 网络进行服务发现以及远程调用,通过状态通道进行计费。
它相当于 Web2 服务到 Web3 服务的一个桥,任意当前的互联网服务都可以将自己的付费机制变更为流式计费机制,直接接入到 Web3 的 P2P 网络中。
DAppChain
基于 Stargete 框架搭建的,包含子共识机制的 DApp Chain。它的核心逻辑由合约写成,并且注册到 Layer2 中,注册时需要抵押一定数额的链上资产。它的安全受 Layer2 约束,但因为它是一个局部共识,Layer2 并不知道它的所有交易,所以安全性比 Rollup 方案要低。如果用户遇到欺诈,可向 Layer2 提交欺诈证明,Layer2 会对 DApp 进行惩罚,但惩罚的上限以 DApp 的注册抵押资产为上限。
整体概览架构如下图:
Layer1 与 Layer2,Layer3 以及用户的终端钱包,都在同一个 P2P 网络中,都通过 P2P 网络进行通信。
终端钱包有自己在 P2P 网络中的身份,可以执行和验证交易,可以存储交易历史,如果发现 Layer2 节点作弊可以直接提交 Layer1 仲裁。
不同的 DApp,比如 X DApp, Y DApp 可选择不同的 Layer2 方案接入到 Starcoin 网络。
这个方案有几个关键点:
充分发挥终端的价值。
状态通道在基础的通信层发挥作用,给节点运营方提供激励。
路线图
术语说明
自由状态:在 Move 中,如果某种类型的实例可以由外部 Module 持有,则认为该状态的自由的。