查看原文
其他

Cartesi Rollups 之状态折叠

cartesi CTSI中文社区 2022-12-17



在 Cartesi,我们每天都会遇到与缺乏成熟工具相关的问题。 区块链是一项新兴技术,软件堆栈仍处于起步阶段,并且发展速度很快。 针对这些问题编写临时解决方案是不可扩展的。 更好的方法是以可重用的方式在已建立的较低级别解决方案之上构建抽象。

必须指出的是,这些较低级别的解决方案是开源的,由社区开发。 同样,我们也从中受益,我们正在发布自己的内部工具,这可能会使社区受益。

我们要在这里解决的问题是读取区块链的状态。 这是与区块链交互的第一步:要决定我们每时每刻要做什么,我们首先需要知道我们的智能合约的状态。

01
阅读区块链的状态Reading the State of the Blockchain

区块链的状态是不断变化的。对于每个新块,事情可能会任意改变。读者的目标是跟上这些变化。请注意,阅读器是特定于 dApp 的,必须创建以适应特定的智能合约。

与区块链交互的规范方式是通过以太坊的 JSON-RPC API。通常,API 被封装在一些高级库中,例如 web3.js。必须信任实现此 API 的远程服务器。恶意服务器可能会给我们错误的响应,或者干脆不响应。该服务器必须与以太坊网络通信,因此必须运行以太坊节点。

通常有两种设置此服务器的方法。第一个是自己做。我们选择以太坊协议的一个实现,例如 Geth 或 Parity,并在某些机器上运行它。这个设置并不简单。第二种方法是将其委托给某些外部提供商,例如 Infura 或 Alchemy。这是一个更简单的解决方案,但有相关的成本。这些服务对超过一定限制的每个查询收费。决定使用 Infura 的读者必须考虑到这一点。

智能合约发布状态的一种便捷方式是通过 diff 事件。想法很简单:在每次状态更改时,我们只通知更改的内容。例如,想象一个智能合约,它存储某个资产的所有过去价格。我们不是在每次添加新条目时都发出包含所有过去价格的事件,而是发出一个仅包含新条目的事件。这样,读者可以通过累积所有过去的事件来重建整个列表。复杂的数据结构,如图形和集合,只能通过差异进行有效的交流。我们在 Cartesi 经常遇到这些。

创建一致读者的主要障碍与共识的终结性有关。Finality 与持久性有关:一旦提交了一个块,它就不能被更改。然而,以太坊中的东西并不是一成不变的。在任何时候,历史都可能通过一个叫做链重组的过程被改写,我们过去读到的一些状态可能不再是区块链的一部分。

当同时开采两个或多个区块时,通常会发生链重组,从而产生交替的现实。在某些时候,其中一个现实将被认为是正确的,而其他现实将消亡。如果我们阅读一个死亡的状态,我们可能会遇到不一致的情况。例如,假设我们正在处理 diff 事件。如果我们混合来自不同现实的事件,我们最终会处于不一致的状态。

一线希望是以太坊具有所谓的概率终结性。越往前走,重组的机会就越小。因此,创建一致读取器的一个简单解决方案是等待块变旧。我们可以将块的年龄校准到某个安全阈值:它越老,阅读器就越安全。然而,这会导致高延迟阅读器。

因此,创建一致且实时的阅读器并非易事。当我们考虑到每个 dApp 都有自己的特性(例如事件和 getter)时,问题变得更糟。我们还需要一个避免对远程服务器进行过多调用的阅读器,因为我们想使用像 Infura 这样的服务。我们的阅读器应该能够在本地和远程运行,并且通常是轻量级和快速的。


02
状态折叠The State Fold

State Fold 是我们解决读取区块链状态这一问题的解决方案。它的设计深受函数式编程思想的启发,这使我们能够创建更强大的软件。潜在的见解是区块链的状态在特定的块哈希上是不可变的。最新的区块是不断变化的,一定数量的区块可能会因重组而发生变化。但是,具有特定哈希值的块永远不会改变。它们最终可能会从区块链中被撤销,但它们仍然是不可变的。有了这种不变性,我们可以从函数式编程中引入概念。

在函数式编程中,当我们想要累积集合元素时,通常使用 fold(有时称为 reduce)来迭代集合。它是一个高阶函数,它接受一个初始值和组合运算符,并产生一个最终值。如果您不熟悉这个概念,我们建议您了解更多!

我们必须处理的第一个问题是灵活性。由于读者是特定于 dApp 的,因此必须有一种方法来配置 State Fold 以适应每个智能合约的需求。State Fold 通过可编程性实现这一点:开发人员通过充当委托的回调对象指定其行为。这些代表依赖于 dApp,并实现了每个特定智能合约所需的必要的区块链状态获取逻辑。

为此,开发者必须提供一对功能:同步和折叠。sync 和 fold 都创建了一个区块状态,对应于特定区块的智能合约状态。Sync 通过查询区块链从头开始创建一个新状态。折叠使用先前的状态和区块链查询将状态向前推进一步。请注意,这些查询只能在特定的块哈希上完成,因此它们是不可变的。

在高层次上,状态折叠逻辑相当简单。State Fold 调用 sync 来生成第一个状态,然后使用 fold 一次一个块地更新状态。因此,当我们第一次打开 State Fold 时,它会调用 sync 来生成初始状态。然后,在每个新块上,它将调用前一个状态的 fold,生成下一个状态。这两个函数必须满足的属性是同步到一个块并折叠到该块必须产生相同的结果。我们可以将一个区块的同步视为从创世到该区块的快速折叠。

更详细地说,区块链的分支性质产生了块的树状结构。例如,如果多个矿工同时创建一个区块,区块链将暂时分叉,从而创建不同的现实。状态折叠在内部将这些现实存储在树状数据结构中,代表区块链本身。这种结构将区块与区块状态相关联,它完整地描述了我们智能合约在区块链中每个区块的状态。这个结构只是一个缓存,因为我们可以完全从区块链中重建它。

当一个新块被添加到区块链时,状态折叠会在缓存中找到这个新块的父级,并通过折叠将其状态推进一步。如果缓存中有间隙(例如,它只有祖父块),则递归应用此过程。如果缓存不包含祖先块,我们使用同步代替。这个过程在面临重组时优雅地保持了一致性,这使得 State Fold 能够实时运行,而不必担心状态不一致。

在委托内部,开发人员可以使用整个 JSON-RPC 读取器方法,尽管仅限于特定块哈希的查询(因为我们的不变性限制)。这允许与使用原始 web3.js 相同的灵活性,但实时,具有一致性保证且不会增加复杂性。

我们是在 Rust 中实现的,可以作为一个库(用 Rust 的说法,一个板条箱)。委托也是用 Rust 编写的,必须与库一起编译。因此,它可以在本地和远程运行。


03
分析Analysis

以不同方式解决同一问题的好工具称为 The Graph。然而,The Graph 做出了某些设计选择,对我们软件堆栈的某些部分进行了令人不快的权衡。首先要注意的是,我们的目标比 The Graph 小得多,这反过来又使 The Graph 比我们的需求复杂得多。在这里,我们对读者实现的目标进行了简短分析,以证明我们选择推出自己的解决方案是合理的。

第一个目标是灵活性。灵活性是读者能够读取任意可观察区块链状态的能力。如果我们可以通过 Ethereum JSON-RPC API 读取状态,我们就认为它是可观察的。我们的解决方案向开发人员公开了整个 API,这使其具有最大的灵活性。

对我们的 State Fold 进行编程完全是在 Rust 中完成的。我们提供了一种实际的强类型编程语言,其中开发人员使用标准 API 主动获取数据。我们相信这会施加更少的限制,简化整体计算模型,并减少认知负担。

请注意,这种灵活性反过来又支持多项优化,以更轻松地使用 Infura 等服务。这些服务超过了一定的限制,每次查询都会向用户收费。如果编程正确,状态折叠对它们来说非常简单。此外,在折叠时,我们自然会重用过去的状态,绕过过多查询 Infura 的需要。

应该注意的是,状态折叠适用于任何提供者。它所需要的只是一个以太坊 JSON-RPC 端点。我们将阅读器设计为轻量级且易于提供者使用。这样做的一个后果是同步时没有太多开销。它也可以轻松地在本地或远程部署在我们自己的基础设施上,从而简化了它的使用。

第二个目标是实时一致性。一致性是观察到的状态相对于区块链的正确程度,实时性是在最新的块上这样做的能力。挑战在于创建一个可以在“量子泡沫”下操作的阅读器,同时安全地处理重组。我们通过使用函数式编程折叠来实现这些属性。

另一个不容忽视的设计目标是组合状态折叠的能力。这允许在编写委托时采用模块化方法,这极大地简化了复杂逻辑的实现并完成了代码的重用。创建抽象的能力是程序员工具箱中最重要的工具之一。


关于Cartesi

Cartesi正在将智能合约推向新的高度。它是一个与链无关的第二层基础架构,解决了区块链上最紧迫的可扩展性问题。最值得注意的是,Cartesi实现了独特的支持Linux的VM,rollups和侧链,以彻底改变开发人员创建区块链应用程序的方式,允许他们使用主流软件组件。



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存