TON 智能合约开发与 Solidity 的六大不同
TON 是一种非常现代的区块链,为智能合约开发带来了一些激进的新理念。它是在以太坊推出相当长时间后设计的,并有幸学习了 EVM 模型中运作良好的内容以及可以改进的内容。
如果你具有一些智能合约经验,你可能熟悉以太坊的 Solidity 语言和其 EVM。学习 TON 开发时,你应该了解某些设计差异,这些差异使 TON 上的事物表现出你所期望的完全不同。本文的目的是突出这些差异,并为你提供一些关于它们产生的一般想法。
了解 TON 的主要内容是,它旨在将区块链带到地球上每个人的手中。这意味着大规模 - 十亿用户每天发送数十亿交易。
将这视为从“数据”到“大数据”的转变。当你需要存储一家餐厅的菜单时,SQL 数据库是一个很好的选择 - 它可以运行强大且灵活的查询,因为所有数据都可以立即使用。当你需要存储地球上每个人的 Facebook 帖子时,SQL 数据库可能不是要走的路。这些“大数据”必须被大幅分片 - 限制了你可以运行的查询的灵活性。不同的目的有不同的权衡。
以下是 TON 区块链的六个独特方面,这些方面可能会让大多数 Solidity 开发人员感到惊讶:
区块链作为不可变且永恒的数据存储,从理论上来说是一个很好的概念,但正如我们很快将看到的那样,实际上很难扩展。以太坊的费用模型受到了“银行”的启发。你想发送一些钱,你需要向银行支付交易费用。谁负责支付费用?发起转账的用户。
那么在区块链上存储数据呢 - 例如部署新智能合约的字节码?以太坊模型规定,发送部署交易的人将支付费用。但是这笔费用只支付一次,但是如果链上的数据是永恒的,矿工们将不得不持续支付基础设施成本以保留这些数据多年。这些费用经济学不成立,如果你尝试将其扩展到数十亿用户,最终它们将崩溃。
从银行转到即时信息
TON 上的费用模型有很大不同。与模仿银行账户不同,TON 受到了诸如即时通讯应用程序之类的网络应用程序的启发。在 Facebook Messenger 上发送消息的成本由谁支付?绝对不是发起转账的人。应用程序开发者,Facebook Inc(或 Meta Inc,我不清楚,我自己使用 Telegram),实际上承担了这些成本,并且由 Facebook Inc 负责如何收回这些成本并资助自己。
因此,在 TON 中,dapp 本身需要为其资源成本付费。每个智能合约都持有一定数量的 TON 代币余额,并使用此余额支付租金。如果智能合约用尽了资金,它最终将被删除(不用担心,一切都是可恢复的)。请注意,支付链上存储的费用不是一次性的,租金支付是持续的。如果你只保存数据一小段时间,你将支付更少。这些费用经济学更符合矿工的成本,因此更容易扩展。
与 Facebook Inc 非常相似,TON 中的合约开发者有很大自由度选择如何资助其运营。开发者可以自掏腰包用 TON 代币资助合约并为其用户提供补贴;或者可以从用户那里收取不同操作的 gas,并将此 gas 保留在其余额中以供将来支付租金。
在以太坊上,强大 DeFi 生态系统的一个重要推动因素是合约的无缝组合性。在单个交易中,你可以获取一些 WBTC,将其作为抵押品与 Compound 的合约供应,并使用它借入 USDC,再使用 Uniswap 的合约交易此 USDC 以获取更多 WBTC - 从而提高你的 WBTC 头寸。整个过程甚至是原子的 - 如果其中任何步骤失败,甚至是最后一步,整个交易将回滚,就像从未发生过一样。
当你的智能合约调用不同智能合约的方法时,调用将立即在同一交易中处理。在这方面,以太坊非常类似于在单个服务器上运行整个后端。你的后端的每个部分都可以同步访问其他每个部分 - 这是一种非常容易理解的方法。但它会受一个警告,它有增长瓶颈。
从单一服务器转移到微服务集群
如果你将以太坊想象为单个服务器上的单体,TON 更类似于一组微服务的集群。想象每个智能合约可能在不同的机器上运行。如果两个智能合约想要相互调用,就像两个微服务进行通信一样,它们可以通过网络发送消息。这条消息需要一些时间传输,因此通信突然变得异步!这意味着当你的智能合约调用不同智能合约的方法时,调用将在交易终止后,在某个不同的未来区块上处理。
这更难理解。如果消息发送时的条件发生变化并且直到接收消息时发生变化会发生什么?例如,调用合约余额有一个值,但到第二个合约处理调用时,余额已更改。保持一致性更加困难,错误可能会潜在出现。原子性呢?如果你链接三个调用,只有最后一个失败怎么办?如果你需要回滚所有更改,你将不得不手动执行。
这实际上是列表中前一项的另一面。在以太坊上,合约之间的调用是同步的,从不同智能合约读取数据是直截了当的。假设我的合约有一笔 USDC 的余额。由于 USDC 本身也是一个合约,为了知道其自身余额,我的合约将不得不调用 USDC 合约的getBalance
方法。
还记得在单个服务器上运行的单体吗?这种方法的一个巨大好处是每个服务都可以直接读取每个其他服务的状态内存。
从单一服务器转移到微服务集群
当我们有在不同机器上运行的单独微服务时,跨服务读取状态内存突然变得不可能。TON 上的智能合约只能通过发送异步消息进行通信。如果你需要从另一个合约查询数据并且需要立即获得答案,你将毫无办法。
事实上,情况变得更加奇怪。如果你在 TON 智能合约中看到getBalance
等 getter 方法 - 这些方法无法从其他智能合约中访问。Getter 方法只能由链外客户调用,类似于你的以太坊钱包可以使用 Infura 等全节点查询任何智能合约状态。
以太坊上dapp的最初灵感来自于由“律师”起草的法律文件——因此被称为“智能合约”。开发者将法律合约的条款写成代码,而代码,如你所知,就是法律。当现实世界中的双方签订合约时,合约是不可变的。如果任何一方想改变合约条款,他们将起草一份新合约。
与这种方法一样,以太坊上的智能合约代码被设计为不可变的,永远不会被修改。多年来,开发人员社区已经学会了克服这一限制,并生成了一些繁琐的模式,这些模式依赖于一些技巧,比如指向不同合约的代理合约,以实现升级。
从律师到软件工程师
与律师不同,软件工程师被教导说,每段代码都有错误。即使错误永远不会发生,需求仍然会随着时间的推移而变化,代码也必须经常升级和修改。
在TON下,合约应该是不可变的借口被完全抛弃了。智能合约可以自由地修改自己的代码,就像编写任何其他状态变量一样。如果合约写入代码变量,那么它是可变的,如果没有,那么它是不可变的。这在实践中并不是一个大的变化,它只是使繁琐的代理模式变得多余。
这是一个需要一段时间才能理解的棘手问题,但会解释为什么一些 TON 上的智能合约被设计成这样。
无界数据结构是智能合约中的状态变量,可以无限增长。考虑实现 USDC 代币的 ERC20 合约。该合约需要维护每个用户地址的余额映射。由于 USDC 可以大量铸造并分解成微小的部分,不同的 USDC 持有者数量可以无限增长。换句话说,映射中的键的数量可以随意增加。
如果攻击者试图通过添加越来越多的垃圾条目来泛滥合约会发生什么?他们能够发动 DoS 攻击并阻止其他诚实用户使用此合约吗?以太坊为智能合约开发人员相当巧妙地解决了这个问题。以太坊的费用模型规定,写入新状态数据的用户为此支付费用。这意味着我们的攻击者将不得不为他们的垃圾交易支付高昂的成本。此外,在以太坊上写入映射的 gas 成本是恒定的,不取决于该映射包含多少数据,这意味着其他用户不会因垃圾邮件而受到影响。总的来说,在以太坊上垃圾交易映射是不经济的,系统提供了保护。
从无界映射(mapping)到无界合约
不幸的是,对于 TON 智能合约开发人员,系统不会保护免受合约状态中无界数据结构的垃圾邮件攻击。TON 的 gas 费用模型规定,写入的成本不是恒定的,成本通常与数据结构中存在的数据量成正比。这种行为源于 TON 对“Bag of Cells”架构的依赖 - 合约状态被划分为称为“单元(Cell)”的 1023 位块,开发人员需要维护这些块。映射被实现为单元树,写入树中的叶子需要沿着整个高度写入新哈希。如果攻击者试图在映射中垃圾交易键,一些用户余额将被推到树的深处,以至于更新它们将超过 gas 限制。
因此,TON 中的最佳实践是避免在状态中使用无界数据结构。这将保护合约免受狡猾的 DoS 漏洞。这个主题可能值得有自己独立的博客文章,但简而言之,解决方案是依赖于合约分片。如果我们的 USDC 合约中可能有无限数量的用户余额,我们应该将单个合约拆分为多个子合约 - 每个子合约保存单个用户的余额。
这解释了为什么 TON 上的 NFT 收藏合约将每个项目放在自己独立的合约中(项目数量可能是无限的);以及为什么 TON 上的可替代代币合约将每个用户的余额放在自己独立的合约中。
我们通常会提供一些关于为什么 TON 以这种方式设计的理由。以太坊的费用模型中,映射写入是固定的,与映射大小无关,这是过于简化的。实际上,随着映射的增长,矿工需要更多的工作来更改它们的内容。只要映射很小,这种额外的工作是微不足道的,但是当映射可以增长到数十亿条目时,情况就不同了。
在以太坊上,用户的钱包等同于他们的地址,地址直接由公钥(及其对应的私钥)派生。这是一对一的关系,一个公钥对应一个地址,一个地址对应一个公钥。只要用户知道他们的私钥,他们就永远不会丢失他们的钱包。
此外,在以太坊上,用户无需采取任何特殊措施就可以拥有钱包。以太坊地址就是钱包。地址可以持有原生货币 ETH,地址可以持有 ERC20 代币和 NFT,地址可以直接向其他智能合约发送和签署交易。
从地址到合约
在 TON 上,钱包不是隐含的,它们是必须像任何其他智能合约一样部署的独立智能合约。当新用户想要开始使用 TON 区块链时,他们的第一步将是在链上部署一个钱包。这个钱包的地址是从钱包合约代码和各种初始化参数(如用户的公钥)派生的。
这意味着用户可以部署多个钱包,每个钱包都有自己的地址。这些钱包可以在它们的代码上有所不同(基金会不时发布不同的官方代码版本)或者在它们的初始化参数上有所不同(其中一个参数通常是序列号)。这也意味着即使用户知道他们的私钥,他们仍然必须刻意记住他们的钱包地址(或用于构建的初始化参数)。
向 TON 上的某个 dapp 发送交易涉及使用用户的私钥签署消息。与以太坊不同,这个交易不是发送到 dapp 智能合约,而是发送到用户的钱包合约,后者将转发消息到 dapp 智能合约。
这种设计方法为 TON 开辟了一个新的灵活维度。社区随着时间可以发明新的钱包合约,例如考虑一个没有 nonce(交易序列号)的钱包,允许从不同客户端并行发送多个交易而无需事先同步。此外,特殊钱包如多重签名钱包(即使在以太坊上也需要部署特殊智能合约),表现就像它们的常规对应物一样。
--
作者 Tal 是 Orbs Network 的创始人。他是一位充满激情的区块链开发人员,开源倡导者,也是 TON 生态系统的贡献者。关注 Tal 在 TON 上的工作,请访问 GitHub。关注 Tal 的个人工作,请访问 GitHub 和Twitter。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!