TON 的最终使命是让区块链得到大规模采用。到目前为止,世界上有多少人真正使用过区块链?以太坊的统计数据提到了数百万 ,因此迄今为止区块链全球用户的数字可能是 5000 万,这可能是比较慷慨的。我们如何将这个数字增加到 10 亿?
当前版本的以太坊在峰值容量下每天处理约 100 万笔交易。早在 2016 年,Telegram 是一款消息应用程序,其广泛采用率接近我们的目标,每天发送150 亿条消息。如此大量的数据导致了 TON 区块链设计中采用的架构设计选择。提高系统 x10000 的可扩展性通常不能仅通过协议改进来实现,这一壮举需要彻底改变方法。
分片的概念
分片是一个起源于数据库设计的成熟概念。它涉及将一个逻辑数据集拆分并分布到多个数据库中,这些数据库不共享任何内容,并且可以跨多个服务器部署。简而言之,分片允许水平可扩展性——将数据分割成可以并行处理的不同的、独立的部分。这是世界从数据向大数据转变的关键概念。当数据集变得太大而无法通过传统方式处理时,除了将它们分解成更小的部分之外,没有其他方法可以扩展。
TON并不是第一个将分片应用于区块链的。以太坊2.0支持固定数量的64个分片。TON 的方法之所以激进,并不是因为分片数量更大,而是因为两个独特的概念变化:
- 分片数量不固定 - TON 支持根据需要添加越来越多的分片,上限为 2^60(每个工作链)。这个数量几乎是无限的,足够世界上每个人分配 1 亿个分片,并且还有剩余。
- 分片数量是弹性的 - TON 支持在负载高时自动将分片链一分为二,然后在负载低时将它们合并在一起。这是处理无法提前预测的动态扩展需求的唯一方法。
您可以在TON 白皮书中阅读有关这些新颖想法的更多信息。
尝试从根本上改变世界很少是没有代价的。为了利用这种激进的方法,TON 智能合约开发人员必须以不同的方式设计他们的合约。如果您之前有智能合约经验(例如 Solidity),那么 TON 的架构会感觉很陌生。我建议阅读我之前的一篇文章,TON 区块链的六个独特方面将使 Solidity 开发人员感到惊讶 ,以简化过渡。
分片 TON 智能合约
TON 区块链的基本原子单元是智能合约实例。智能合约实例具有地址、代码和数据单元(持久状态)。我们将此单元称为原子单元,因为智能合约始终具有对其所有持久状态的原子同步访问。
TON 上的智能合约实例之间的通信既不是原子的也不是同步的。将 TON 上的智能合约想象成微服务。每个微服务仅对其本地数据进行原子同步访问。两个微服务之间的通信涉及通过网络发送异步消息。
每个系统架构师都知道,更大的系统需要从整体架构转向 微服务。这种分布式方法需要付出一些努力才能采用,但可以带来一些理想的好处。现代系统范例依赖于像Kubernetes这样的编排器来获取一组容器化微服务,并按需自动启动新实例(自动缩放),并在机器之间有效地对其进行分区。
我喜欢 Kubernetes 的类比,因为这正是 TON 所做的事情。随着特定分片链上的负载增加,它将被分成两部分。由于智能合约实例是原子的,因此它们永远不会被分成两半。这意味着曾经位于同一个分片链上的一些智能合约实例有一天可能会发现自己位于不同的分片链上!
综上所述,TON 的虚拟机(TVM)正在将分布式微服务的概念应用到以太坊 EVM 的整体架构中。
设计分片智能合约
新手系统架构师的一个常见问题是「我的微服务应该有多大?」 - 或者换句话说,「微服务什么时候会过于单一而应该一分为二?」
这个问题没有一个答案,这是一门艺术。这个想法是帮助 Kubernetes 完成它的事情。微服务越小,Kubernetes 就越容易通过创建新实例并按需移动它们来优化系统。但是,它们越小,开发人员就越难实现复杂的流程,因为越来越多的操作变得异步。
我发现同样的推理也适用于 TON 合约分片。这个想法是让 TON 自动分片发挥作用——将状态数据分割成多个智能合约实例,这样当负载增加时,它们可以分解成更小的部分并有效地移动到不同的分片链。但是,如果分片过于激进,则由于异步性增加,您将不得不处理太多的复杂性。
一个实际的例子——TON的Jetton合约
到目前为止,这篇文章非常理论化。我想急速转向实用性。让我们分析一个现实世界的示例来理解这种架构。我们将使用的示例是 TON 的Jetton智能合约。Jetton 是一种实现可替代代币的智能合约(与 TON 币本身非常相似)。这是以太坊流行的ERC20 代币标准的 TON 版本。
实现令牌非常简单。我们需要一个基本操作 -转账- 它允许所有者将一些代币金额转移给不同的所有者。我们还需要一个铸币行动——将新代币添加到流通中的能力,以及相反的销毁——将代币从流通中删除。那么持久状态呢?我们还需要存储所有用户的余额。在以太坊上,这通常需要一个映射,其中键是用户的钱包地址,值是余额金额。
作为 TON 上智能合约的架构师,我们必须决定是否以及如何需要将该智能合约分解为多个较小的实例,以有效支持自动分片。如果我们的Jetton有10亿用户会怎样?在这种情况下,我们的架构能承受得住吗?
将 Jetton 分发到多个智能合约
让我们尝试应用上面概述的推理来找到 Jetton 的“正确”分片量。我意识到这有点太理论化了。幸运的是,我发现有一个非常实用的测试效果很好:
如果您发现自己正在设计具有无限数据结构的智能合约,那么您很可能应该将该合约分解为多个实例。
无界数据结构是可以无限增长的数组或映射。在以太坊下,我们的智能合约需要一张保存所有用户余额的地图。该地图可以无限增长,因为我们代币的持有者数量是无限的。新帐户实际上可以无限期地创建,并且由于数值精度非常高,因此可以将少量代币转移到所有这些帐户。
让我们应用我们的实际规则。如果我们将所有余额保存在 TON 上的单个智能合约中,我们将拥有无限的数据结构。这意味着我们有一个优秀的分片候选者!
那么我们如何分片呢?这非常简单。如果我们不希望所有余额都列在单个智能合约实例中,那么如果我们拆分列表以便每个余额都保存在其自己的专用智能合约实例中怎么办?
Jetton 架构
假设我们的 Jetton 实例用于名为Shiba-Inu 或SHIB
简称的代币。我们有两个用户持有一些 SHIB - Alison 和 Becky。我们已经说过,每个用户的余额都保存在自己的合约实例中,这意味着我们有 2 个实例(“子级”)。事实证明,我们还需要另一个实例来保存有关 SHIB(“父级”)的全局共享信息。
这给我们带来了以下架构:
我答应要务实。让我们开始阅读实际的 Jetton 代码!TON 核心团队拥有 Jetton 标准的正式实施,您可以在此处找到。打开它,以便您可以熟悉代码。
您可以在代码中看到两个主要的 FunC 智能合约:
jetton-minter.fc
这是父级,它保存有关令牌的全局共享信息,例如其名称和符号。父级只有一个实例。我不完全确定核心团队为什么选择这个名字jetton-minter
,我更喜欢这个名字jetton-parent
。确实,这个合约负责铸币,但即使禁用铸币,你仍然需要它,这有点令人困惑。jetton-wallet.fc
这是子项,它保存单个用户的代币余额。该合约有多个实例,每个用户地址一个。这份合同的名字是核心团队选的jetton-wallet
,我更喜欢这个名字jetton-child
。
如果我们的代币由 1,000,000 个不同的用户持有,则将部署 1,000,001 个合约实例。这就是自动分片的神奇之处。默认情况下,所有合约实例都将在单个分片链上找到。但是,如果这些用户开始发出大量交易,并且该单个分片链处于高负载下,TON 会自动将其拆分为更小的分片链。理论上,系统可以不断分裂,直到在专用分片上找到每个合约实例。这就是 TON 能够扩展到数十亿用户的秘密。
各种 Jetton 用户故事
现在我们了解了基本架构,让我们看看几个不同的场景。例如,让我们探讨一下当一个用户将代币转移给另一个用户时会发生什么。
在 TON 下,参与的实体始终是智能合约实例。哪些合同将发挥作用?
你已经见过前三个了。这些的源代码可以在 Jetton repo中找到。右边的三份合同呢?我们的用户故事将涉及三个不同的用户。艾莉森和贝基是 SHIB 的持有者。用户 Admin 是部署 SHIB 的创建者。管理员有一个特殊的角色,因为它是唯一可以铸造新 SHIB 进入流通的用户(这就是新 SHIB 代币的诞生方式)。这是一个受信任的角色,一旦代币开始交易,通常应该被撤销(更改为零地址),以保持可能的总供应量上限。
TON 上的用户也由智能合约代表。这些是钱包智能合约,通常由TonKeeper等钱包应用程序为用户部署。如果您不熟悉钱包合约如何在 TON 上工作,请阅读我之前的文章TON 钱包如何工作以及如何从 JavaScript 访问它们 。Alison、Becky 和 Admin 各自在这些钱包中持有 TON 币余额。这些钱包与 Jetton 代码没有具体关系。以下是来自 TON 核心存储库的此类钱包合约的示例实现。
用户故事 1:Alison 有 SHIB 并向 Becky 发送了一些
我们的用户故事始终以我们的一位用户(本例中为 Alison)开始,该用户决定使用 SHIB Jetton 执行某些操作。在这种情况下,艾莉森决定向贝基发送一些代币。艾莉森将打开她选择的钱包应用程序(例如 TonKeeper)并批准该操作。一旦发生这种情况,钱包应用程序将向艾莉森的钱包合约发送签名交易。
该交易包含一条针对某些目标合约的消息。消息是智能合约在 TON 上进行通信的方式。消息被编码为一袋细胞,本质上是一种打包的二进制格式。消息的关键字段之一是一个 32 位整数,op
它描述了该消息的操作类型。
- 在我们的示例中,由于 Alison 想要发送一些代币,因此她将一条带有 op 类型的消息发送
transfer
到持有她的 SHIB 余额的智能合约实例。该消息被编码在她发送到钱包合约的交易中。一旦她的钱包合约验证了交易[code]上的签名 ,它就会将艾莉森的消息转发到她请求的目的地[code] 。 - 一旦
transfer
消息到达其目的地[code] ,即持有 Alison 的 SHIB 余额的合约,该合约将处理该消息并更改其持久状态(将 Alison 的 SHIB 余额减少发送的金额[code])。如果合约需要联系其他合约,它可能会发送额外的消息。在我们的例子中,合约将向internal transfer
持有 Becky 的 SHIB 余额[code]的合约发送一条 op 类型的消息 。 - 一旦
internal transfer
消息到达其目的地[code] ,该合约现在将处理该消息并更改其持久状态(将 Becky 的 SHIB 余额增加发送的金额[code])。该合约通常会发送最后一条带有 op 类型的消息,excesses
将所有剩余的 Gas 退还给 Alison 的钱包合约,并让它知道转账已完成[code] 。
这是消息流:
TON 上的消息是异步的。我们不知道具体什么时候会处理它们。所有消息有可能在单个块中处理,并且每条消息有可能在不同的块中处理。这意味着传输可能需要一些时间来处理。即使第一笔交易已成功确认,转账仍有可能失败。
用户故事 2:Alison 拥有 SHIB 并将一些 SHIB 发送给 Becky 并通知 Becky
如果 SHIB 接收者 Becky 不仅仅是一个人,而是一份在线商店合同,在付款时应该执行某些操作,该怎么办?例如,更改 DNS 记录以指向新所有者。如果我们可以用专用消息触发这个智能合约,那就太好了。
幸运的是,该transfer
消息支持这种行为。它允许原始发送者指定一些将转发给接收者 SHIB 钱包所有者的通知负载。
- 除了最后一步之外,这种情况下的流程几乎相同。在发送 op 类型的消息之前
excessestransfer notification
[code],持有 Becky 的 SHIB 余额的合约会首先向 Becky 的 SHIB 钱包的所有者——Becky 的钱包合约发送一条 op 类型的消息 。如果您将“Becky”重命名为“DNS-Superstore”等在线商店,这个故事会更有意义。在这种情况下,合约“DNS-Superstore”将收到此通知,因为它是“DNS-Superstore”的 SHIB 钱包的所有者。该合约在收到消息后,将根据消息中提供的数据实现更改 DNS 记录所有权的行为。
这是消息流:
您如何知道该transfer
消息还支持哪些其他功能?消息通常使用称为TL-B 的语言进行编码。作为最佳实践,合约创建者应为其合约处理的所有消息发布 TL-B 规范。这是相关的 TL-B 规范[代码]:
transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
response_destination:MsgAddress custom_payload:(Maybe ^Cell)
forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
= InternalMsgBody;
amount
是要转移的 SHIB 代币数量destination
是Becky的钱包合约地址response_destinationexcesse
是收件人的地址(通常是 Alison 的钱包合约)forward_payload
是“DNS-Superstore”用例的通知负载
用户故事 3:艾莉森有一些 SHIB 并将其烧毁
在这个用户故事中,艾莉森决定烧掉她拥有的一些 SHIB。燃烧 SHIB 会将其从流通中移除。她为什么要这么做?燃烧会减少代币的总供应量。用户关心代币的总供应量,因为这有助于计算代币的市值。燃烧代币就像股票回购一样,它增加了股票的价值。
总供应量存储在哪里?正如您可能已经猜到的那样,由于此持久状态数据是全局共享的,因此将其存储在我们的父级下是有意义的jetton-minter
。
- 为了启动销毁,Alison
burn
向持有她的 SHIB 余额的智能合约实例发送一条 op 类型的消息。这条消息像以前一样被编码在她发送到钱包合约的交易中,钱包合约在验证签名后将其转发到目的地。 - 一旦
burn
消息到达其目的地[code] ,即持有 Alison 的 SHIB 余额的合约,该合约将处理该消息并更改其持久状态(将 Alison 的 SHIB 余额减少已销毁的金额[code])。然后,合约将向burn notification
父铸币者合约[code]发送一条 op 类型的消息 。 - 一旦
burn notification
消息到达其目的地[code] ,该合约现在将处理该消息并改变其持久状态(通过燃烧量[code]减少总供应量)。该合约通常会发送最后一条带有 op 类型的消息,excesses
将所有剩余的 Gas 退还给 Alison 的钱包合约,并让它知道燃烧已完成[代码] 。
这是消息流:
母铸币者合约允许用户使用 Getter 方法[code]查询代币的总供应量 。
用户故事 4:管理员为 Becky 铸造 SHIB
当所有合约最初部署时,SHIB 的总供应量为零,并且没有人拥有任何代币。代币是如何创建的?创建新代币的操作称为铸造。它只能由特殊的管理员角色(我们的管理员用户)执行。管理员用户还可以将管理员权限转移到任何其他地址。作为最佳实践,在代币开始交易之前,应将管理权限转移到零地址,以确保没有人可以铸造新代币并增加总供应量。
- 要启动 Mint,管理员会
mint
向父级发送一条 op 类型的消息jetton-minter
。该消息在管理员发送到其钱包合约的交易中进行编码,钱包合约在验证签名后将其转发到目的地。 - 一旦
mint
消息到达其目的地[code] ,即父铸币者合约,该合约将处理该消息并验证该消息确实源自 Admin [code] 。然后,合约将改变其持久状态(将总供应量增加铸造量[code])。合约将向internal transfer
持有 Becky 的 SHIB 余额[code]的合约发送一条 op 类型的消息 。 - 一旦
internal transfer
消息到达其目的地[code] ,该合约现在将处理该消息并更改其持久状态(将 Becky 的 SHIB 余额增加铸造金额[code])。该合约通常会发送最后一条带有 op 类型的消息,excesses
以将任何剩余的 Gas 退还给管理员的钱包合约,并让它知道铸币已完成[code] 。
这是消息流:
该流程的最后一步几乎与传输流程相同。与转账流程类似,也可以使用专用消息通知 SHIB 收件人,以便他们可以处理付款 - 还记得我们的“DNS-Superstore”示例吗?我不会为此案例添加另一个完整的用户故事,但为了以防万一,以下是消息流:
谁部署子合约?
让我们回想一下 SHIB 合约架构 - 一个父级部署实例jetton-minter
和每个 SHIB 合约持有者一个部署实例jetton-wallet
:
父铸币者合约自然由 SHIB 的创建者(可能是管理员用户)部署。但是子合约又如何呢?谁来部署它们呢?该设计非常高效——子合约仅在其所有者第一次收到 SHIB 时才会部署。这听起来可能有点棘手,因为收件人不一定知道他们收到了任何 SHIB。
如果您还记得上面的转移用户故事,接收 SHIB 是由该internal transfer
消息触发的。如果该消息的接收者子合约从未被部署,则该消息的发送者将必须部署该子合约!您可以在此处的代码中看到这种情况的发生 。消息部分state_init
实际上负责部署。您可以在此处看到它是 根据子项的初始代码单元(此合约实现的已编译 TVM 字节码)及其初始数据单元计算得出的。
由于消息的发送者internal transfer
永远不确定接收者是否已部署,因此它始终 包含部署部分。如果合约之前已经部署过,TON 足够聪明,可以忽略部署元素。
验证父子之间的消息
在上面的用户故事中,我们看到完整的流程分布在多个消息上。internal transfer
例如,该消息会导致其收件人增加其 SHIB 余额(您还记得,它是在传输流程结束时发送的)。如果攻击者试图伪造此消息并将其发送到持有自己的 SHIB 余额的合约,会发生什么情况?如果我们不小心,这种伪造将导致攻击者能够凭空为自己生成新的令牌!
为了确保合约不被伪造,我们需要验证这些改变余额的关键消息确实来自有效的发送者。您可以在此处查看验证码
jetton_master_address
合约只会处理由铸币者父级(由于某种原因标记)或有效子级之一发送的消息。
这引出了一个非常有趣的问题 - 我们如何判断某个随机地址是否是有效的子 jetton 地址?等等,刚才艾莉森想要给贝基发消息的时候,她是怎么知道贝基的合约地址的?
这又是一个漂亮的系统设计 - TON 上智能合约的地址源自合约的初始代码单元(其实现的已编译 TVM 字节码)和合约的初始数据单元(其初始持久状态)建造)。如果我们知道这两个值,我们甚至可以在部署合约之前计算出合约的地址。该计算是确定性的且不可变的。
Jetton 代码包含一个实用函数,可以根据 Alison 的地址计算孩子的地址,即持有 Alison 的 SHIB 余额的合约地址。您可以在此处查看此功能 。正如您所看到的,它确实取决于 子级的初始代码单元格及其初始数据单元格 。
理解为什么这种机制是安全的有点棘手。是什么阻止攻击者以某种方式将恶意合约部署到合法子女之一的地址中?为了登陆合法地址,恶意合约必须拥有官方子代的初始代码单元——这已经限制了攻击者向该实现添加恶意代码的能力。此外,初始数据单元保证子节点仅服从正确的铸币者父节点,因为初始数据单元包含其地址。
当出现问题时处理部分更改
您还记得,传输流分布在多个异步消息上。第一条消息减少发件人的 SHIB 余额,第二条消息增加收件人的 SHIB 余额。当一切顺利时,这在愉快的流程中是有意义的,但如果第二条消息因某种原因失败了会发生什么?
大多数智能合约机器,如以太坊的 EVM,以完全原子和同步的方式处理交易 - 因此,如果后续阶段之一失败,整个交易将恢复,并且由该交易引起的所有状态更改也将被恢复。这个机制确实很容易理解。不幸的是,由于 TON 中的消息既不是原子的也不是同步的,我们无法立即获得这种自动恢复功能。
所以,我们能做些什么?我们需要自己处理恢复流程。这个例子说明了 TON 上的智能合约开发变得更加困难。
当 TON 上的消息处理由于抛出异常而失败时,如果bounce
设置了该消息的标志,系统将自动将失败的消息发送回带有该bounced
标志的发送者。您可以在此处阅读此消息弹跳 机制的规范 。
让我们回到上面的示例 - 传输流中的第二条消息失败。当 SHIB 发送方将其 SHIB 余额减少了发送的金额后,此消息失败。为了保持系统的一致性,我们需要以某种方式撤消这种故障减少。那会如何运作呢?假设第二条消息是在bounce
设置了标志的情况下发送的,当收到退回的第二条消息时,我们可以撤消发件人的减少。您可以在此处查看处理退回邮件的官方 Jetton 代码,并在此处 撤消减少操作 。
小心地做这件事!在 TON 上设计复杂的消息流时,请拿出白板并绘制不同的消息流图,就像我在这篇文章中所做的那样。对于这项工作,我最喜欢的工具是出色的开源Excalidraw。然后,开始模拟流程的每一步的潜在故障和消息反弹,以确保您的代码正确处理撤消。
本站所提供的所有资讯均仅供读者参考。这些资讯不代表任何投资建议、提供、邀请或推荐。读者在使用这些资讯时,应当考虑自己的个人需求、投资目标和财务状况。所有投资都伴随着一定的风险,在做出任何投资决策之前请多加留意。