编码
2023年9月18日
20
阅读分钟数
线程化 Solidity:为兼容 TVM 的区块链编写智能合约
在 2020 年,为了编写在 TON 虚拟机 (TON Virtual Machine, TVM) 上运行的唯一去中心化网络的智能合约,开发者必须学习 Fift 语言。该语言专为开发者创建和测试在該虚拟机上执行的智能合约而设计。
随后,开发团队分化为两个独立的小组。其中一个新开发团队致力于改进 TON 虚拟机,改进的结果是出现了线程化虚拟机 (Threaded Virtual Machine, TVM)。接着,Everscale 和 Venom 网络(后者目前处于测试网阶段)相继发布,两者都依赖线程化虚拟机来执行智能合约。
之后,一种专用的 Solidity 代码编译器被整合进 TVM 的机器指令中。这自然为 Everscale 和 Venom 区块链的智能合约开发带来了便利,因为它能够将高级代码转换为机器可处理的代码。
在本文中,我们将描述为 TVM 区块链编写智能合约的过程。Everscale 网络和 Venom 网络上的智能合约文件使用 .tsol 扩展名,它代表 Threaded Solidity(线程化 Solidity),这使得异步智能合约与标准的同步 Solidity 合约得以区分。然而,这些网络中也同样使用 .sol 扩展名。
TVM 解决方案的开发工具
借助 Jetbrains IDEA (https://plugins.jetbrains.com/plugin/20696-t-sol) 或 Visual Studio Code (https://marketplace.visualstudio.com/items?itemName=everscale.solidity-support) 的社区插件,为 TVM 区块链编写智能合约变得更加便捷。
智能合约的测试是使用 Locklift 框架 来执行的。它支持在 Mocha 上进行自动化测试,此外,它还允许在 Locklift 网络的自有环境中 而非在测试网/主网上进行测试。
与在本地节点上进行的测试不同,Locklift 网络更适合测试智能合约执行的各种场景,因为它不保存状态。
Locklift 具有出色的异常跟踪功能,能够在其自有环境内测试时突出显示 .tsol 代码中的特定行。
此外,在 Locklift 环境中测试智能合约不涉及与节点的交互,完全在机器的 RAM 中进行,这显著加快了测试速度并提高了高负载下的稳定性。
为了与智能合约进行进一步交互,开发人员应使用 Everscale Inpage Provider——一个包含一系列 API 的 SDK,用于与 Everscale 和 Venom 区块链进行交互。该工具包可以构建为 Typescript 或 Javascript 库。其编写体验类似于使用 web3.js 框架为 Solidity 编写智能合约。
线程化 Solidity 的特殊性
在为 Venom 和 Everscale 去中心化网络编写智能合约时,需要理解一些基本要素:
智能合约代码在线程化虚拟机(Threaded Virtual Machine, TVM)上执行,该虚拟机使用其自身的数据结构或单元(cell)树进行操作;
这些网络以异步方式运行;
网络上的所有操作都是智能合约之间通过内部消息进行的交互;
每份智能合约都会被收取一笔
storageFee(存储费用),即在网络上存储数据的佣金。当智能合约余额降至 -0.1 时,它首先会被冻结,然后从网络状态中移除。为实现恢复,智能合约状态的哈希值将保留在网络上。
这些方面是线程化 Solidity 的决定性特征,并使其与标准 Solidity 区分开来。
TVM 中的 Cell、Slice 和 Builder
如前所述,TVM 处理一种特殊的数据结构,即 Cell 树。一个 Cell 包含一个最多 1023 位的别信息数组,或最多四个指向相邻 Cell 的引用(每个 256 位)。得益于指向相邻 Cell 的链接,从而构建起了树形结构。Cell 中包含特定“片段”数据和单个 Cell 部分链接的结构,被称为 Cell Slice 或简称 Slice。这是一种独立的数据结构,在 Threaded Solidity 中拥有其专属方法。
整个 Cell 可以与另一个 Cell 进行比较(比较运算符 = 和 != -> bool),并且可以查询子 Cell 的深度(直接或间接引用给定 Cell 的 Cell 数量)——即 <TvmCell>.depth()。
空 Cell 的构造函数 TvmCell() 也可用于判断特定 Cell 是否为空:
以下方法可用于计算单个 Cell 的数据量、子 Cell 的数量以及链接的数量:
发送的 Cell 数量少于返回的数量将导致抛出 Cell 溢出异常。
为避免异常,可使用 <TvmCell>.dataSizeQ(uint n) 方法。使用此方法时,将返回一个空值而非抛出异常:
正如所见,完整的 Cell 并没有提供足够的方法来充分处理其包含的数据。在 Threaded Solidity 中,数据处理主要涉及使用 Slice 类型。
可使用以下 Cell 方法将 Cell 转换为 Slice:
Slice 之间可通过以下运算符完全比较:<=, <, ==, !=, >=, >。此外,Slice 还可以转换为字节(bytes)。
Slice 拥有其自身检查其中数据是否存在的方法,包括引用计数器、位权计数器以及类似于完整 Cell 的 dataSize() 和 dataSizeQ() 方法。
要从其他 Slice、函数参数及静态变量(包括其他合约的变量)中获取数据,需使用以下方法:
与 load() 不同,使用 preLoad() 时,Slice 本身不会被修改。
以下是加载合约 A 的数据字段中静态变量的示例:
另一种 Cell 类型是 TVM Cell Builder,它是一种“部分” Cell 构建器,它从栈顶获取数据并快速将其序列化,从而创建一个新的 Cell 或 Slice。该构建器具有独特的、用于计算剩余加载数据空间和可添加链接的可用槽位的方法:
Builder 类型用于处理先前未知的数据。我们了解消息结构与智能合约交互的方式,即可编写一个合约来向另一个合约发送消息并调用其逻辑。
TVM 命名空间
智能合约代码必须与虚拟机交互。编译器 API 提供了处理 TVM 的函数,这些函数本质上是对发送给虚拟机的指令的封装。
tvm.accept() — 此指令将燃气限制(gas limit)设置为最大值。这对于处理可能未附加 EVER 代币以支付 TVM 佣金的外部消息是必需的。我们将在消息部分更详细地介绍消息类型。
tvm.commit() — 创建静态变量的快照(将变量从寄存器 c7 复制到寄存器 c4)以及寄存器 c5,以防代码执行过程中抛出异常,智能合约的状态会回滚到此状态。在这种情况下,TVM 的操作阶段仍被视为成功完成。如果在智能合约代码执行期间未发生任何异常,则此指令对交易无效。
线程虚拟机寄存器
寄存器(控制寄存器)是虚拟机内存中为存储特定不可变数据结构而分配的空间,这些结构需要持续访问以执行功能。将此数据存储在栈上会需要多次操作来移动它,从而对性能产生负面影响。TVM 支持使用 16 个寄存器,但实际上只使用寄存器 0-5 和 7。
我们曾简要提及与寄存器 c4、c5 和 c7 的操作:
c4存储了一个与智能合约单元树根部关联的单元,该树存储了写入网络状态的合约数据;c5存储了关于智能合约代码执行后所执行操作的数据。如果执行合约代码的结果是发送消息,则消息数据将被写入存储在寄存器 c5 中的单元;c7存储临时数据。该数据存储在一个元组(tuple)中,该元组通过访问一个空元组进行初始化,并在智能合约代码执行完成后被删除。
tvm.rawCommit() — 这与 tvm.commit() 相同,但它不会将静态变量从寄存器 c7 复制到寄存器 c4,这在使用此函数且在处理外部消息期间调用 tvm.accept() 之后,可能会导致数据丢失。
tvm.setData(TvmCell data) — 将单元数据存储到寄存器 c4 中。此函数可与 tvm.rawCommit() 结合使用:
tvm.getData() -> (TvmCell) — 从寄存器 c4 返回数据。这在升级合约时可能很有用。
外部消息授权
某些智能合约函数只能由特定参与者调用。为了验证调用者,智能合约可以检查用于签署外部消息的公钥或发送内部消息的智能合约地址。智能合约的地址是其代码和静态变量的哈希值,这足以验证调用方。
tvm.pubkey() -> (uint256) — 返回智能合约数据字段中的公钥。如果未提供密钥,则返回 0。
tvm.setPubkey(uint256 newPubkey) — 将公钥写入智能合约的数据字段中。
tvm.checkSign() — 计算作为参数发送的数据是否由公钥签名,然后返回一个布尔值。
tvm.insertPubkey() — 将作为参数传入的公钥插入 stateInit 单元(cell)的数据字段中。
tvm.initCodeHash() — 返回智能合约部署到网络时源代码的哈希值。例如,可用于计算 TIP-3 代币 代码,以便验证用户尝试发送的是哪个代币。
异步区块链中预先计算交易费用的难题
由于我们无法确切知道一笔交易是成功还是失败(在 Everscale 或 Venom 网络中,交易可能会被拆分为多个独立交易),因此我们无法精确确定初始消息应分配的燃气(Gas)量。线程虚拟机(Threaded Virtual Machine, TVM)内置了保障机制,限制完成一笔交易所消耗的最大燃气量,这既是防御恶意攻击的机制,也为开发者提供了便利。
tvm.setGasLimit(uint g) — 为处理单笔交易设置燃气上限。此函数允许您限制支付给 TVM 的资金消耗量,若想防止来自被盗账户的外部消息进行垃圾信息轰炸,此功能将非常有用。如果函数参数超过网络配置中指定的最高燃气限制值,该函数将按以下公式设定的值运行,与 tvm.accept() 的效果相同:min(g, g_max)。
tvm.buyGas(uint value) — 此指令根据所提供的 nano EVER 数量计算可购买的燃气量。此后其运行方式与 tvm.setGasLimit() 类似。
tvm.rawReserve(uint value, uint8 flag) — 此函数可用于预留指定数量的 nano EVER 并将其发送给自己。它可用于限制后续传出调用(outgoing calls)的燃气消耗。
在介绍 flag 选项之前,需要指出的是,合约的初始余额(original_balance)是指 TVM 执行交易且已扣除存储费用(storageFee)后合约的余额。剩余余额(remaining_balance)是指在 TVM 完成工作并处理完部分传出调用后合约中剩下的余额(一次传出调用后的剩余余额大于三次传出调用后的剩余余额。总共最多可进行 255 次传出调用,即单个智能合约的执行最多可导致发送 255 条传出消息)。
flag 选项如下:
0 -> 预留金额 = value nEVER。
1 -> 预留金额 = remaining_balance – value nEVER。
2 -> 预留金额 = min(value, remaining_balance) nEVER。
3 = 2 + 1 -> 预留金额 = remaining_balance – min(value, remaining_balance) nEVER。
4 -> 预留金额 = original_balance + value nEVER。
5 = 4 + 1 -> 预留金额 = remaining_balance – (original_balance + value) nEVER。
6 = 4 + 2 -> 预留金额 = min(original_balance + value, remaining_balance) = remaining_balance nEVER。
7 = 4 + 2 + 1 -> 预留金额 = remaining_balance – min(original_balance + value, remaining_balance) nEVER。
12 = 8 + 4 -> 预留金额 = original_balance – value nEVER。
13 = 8 + 4 + 1 -> 预留金额 = remaining_balance – (original_balance – value) nEVER。
14 = 8 + 4 + 2 -> 预留金额 = min(original_balance – value, remaining_balance) nEVER。
15 = 8 + 4 + 2 + 1 -> 预留金额 = remaining_balance – min(original_balance – value, remaining_balance) nEVER
所有其他 flag 均为无效。
使用 Threaded Solidity 更新智能合约代码(含两个函数)
在兼容 TVM 的区块链上更新智能合约代码的操作,与更新任何其他软件代码非常相似:下载新版本的代码并进行部署。在使用 Threaded Solidity 时,您需要通过消息发送新版本的代码,然后将其保存在智能合约的代码中。
tvm.setcode(TvmCell newCode) — 此函数将智能合约代码更新为所发送 Cell 中包含的版本。此更改将在当前智能合约代码执行会话结束后生效。
tvm.setCurrentCode(TvmCell newCode) — 此函数会更改当前 TVM 会话的智能合约代码。在智能合约代码执行完毕后,这些更改将失效,下一次交易将依据原始版本的代码执行。
代码可以在当前交易中被更新并保存,方法是发送包含代码的消息并使用交替调用:即同时调用 tvm.setCurrentCode() 和 tvm.setcode()。当前交易将得益于 tvm.setCurrentCode() 而按照新版本的代码运行,而 tvm.setcode() 则会为后续交易重写智能合约代码。
全局配置
区块链的全局配置存储在主链(稍后将在本文中描述)。利用全局配置,可以设置一些网络操作参数,例如验证者数量、佣金设置等。
tvm.configParam(uint8 paramNumber) 和 tvm.rawConfigParam(uint8 paramNumber) — 从全局配置中返回参数。
这些函数的返回结构有所不同:configParam() 返回类型化的值,而 configParamRaw() 返回一个单元格(cell)和一个布尔值。全局参数的索引作为函数参数(paramNumber)传入:1、15、17、34。
部署智能合约的方法
部署智能合约时,需要计算其地址,该地址是初始状态(stateInit)的哈希值,而初始状态包含智能合约的代码及其静态变量。
tvm.buildStateInit() — 从代码和数据单元(cell data)生成合约的初始状态(stateInit)。
tvm.buildDataInit() — 生成智能合约初始状态中的数据字段。
tvm.stateInitHash() — 返回智能合约初始状态的哈希值。
TVM 命名空间中的附加函数
Threaded Virtual Machine(线程化虚拟机)编译器的 API 包含用于计算随机值和执行代数运算的方法。您可以在 编译器 API 文档 中找到它们。
下面我们将深入探讨可用于编写 TVM 智能合约的若干更有用的方法。
tvm.hash() — 返回所传入数据的 256 位哈希值。如果传入的参数是字节数据或字符串类型,则哈希的不是数据本身,而是包含该数据的单元格(cell)树。
tvm.code() -> (TvmCell) — 返回合约代码(code)。
tvm.codeSalt(TvmCell code) -> (optional(TvmCell) optSalt) — 如果代码使用了“盐值”(salt),则返回的 optSalt 单元格将包含其值;否则,将返回一个空值。
“Salt”(盐值)指的是在不更改源代码结构的情况下,用于获取新哈希值的附加数据。
tvm.setCodeSalt(TvmCell code, TvmCell salt) -> (TvmCell newCode) — 将盐值作为单元格添加到代码中,并返回新代码。
tvm.resetStorage() — 将静态变量的值重置为其默认值。
tvm.functionId() — 返回一个 32 位无符号整数,即公共或外部函数或智能合约构造函数的编号。这用于描述智能合约在接收弹跳消息(bounce message)时的逻辑,用途之一。关于处理弹跳消息的细节,我们将在相应章节中讨论。
tvm.log() 类似于 print() 函数。长度超过 127 个字符的代码行将会被截短。
tvm.hexdump() 和 tvm.bindump() — 以二进制或十六进制格式将单元格(cell)或整数数据输出到控制台。
tvm.exit() 和 tvm.exit1() — 是保存静态变量并分别以代码 0 和 1 终止智能合约执行的函数。
TVM 消息功能
tvm.buildIntMsg() — 生成一条将导致目标合约执行另一函数的传出内部消息。此函数还会返回一个可用于 tvm.sendrawmsg() 中作为函数参数的单元格(Cell)。
tvm.sendrawmsg(TvmCell msg, uint8 flag) — 发送带有给定标志位的外部/内部消息。内部消息可以使用上述 tvm.buildIntMsg() 函数生成。其他潜在的标志位变体包含在 address.transfer() 函数中。发送消息时,您必须确保参数格式正确。
tvm.encodeBody(function, arg0, arg1, arg2, ...) -> (TvmCell) — 此函数对消息正文进行编码,编码后的内容随后可作为 address.transfer() 函数的参数。如果该函数是回调函数(responsible),则还必须发送一个回调函数。
TVM 智能合约通过消息进行通信
在兼容 TVM 的网络中,所有操作均由接收到传入消息的智能合约触发。
消息的类型包括:
外部消息和内部消息:
外部消息(External messages)是从 Everscale 网络外部发送的,或旨在发送给网络外部的接收方;
内部消息(Internal messages)是在网络内部由智能合约发送和接收的;
此外,还可以将外部-内部消息(external-internal messages)定义为从一个工作链(workchain)发送到另一个工作链的消息。
传入消息和传出消息。
消息具有其自身的特定格式,称为消息X(message-X)。在调用某些函数时,未遵循此特定格式可能会导致异常。
消息命名空间(msg)具有以下函数:
msg.sender (address) — 返回内部消息的发件人地址,对于外部消息或 tick/tock 交易,则返回 0 地址。
滴答(Tick/Tock)交易
msg.value (uint128) — 返回附加到内部消息的金额,单位为 nanoEVER (nEVER)。如果有外部消息,此函数返回 0;如果是滴答/滴答(tick/tock)交易,则此函数返回未定义值。
msg.pubkey() -> (uint256) — 返回用于签署外部消息的公钥。如果处理的是内部消息,则返回 0;如果外部消息未签名(外部消息调用了不需要需要参与者验证的智能合约函数),则也返回 0。这是一个重要的方法,可用于验证请求执行智能合约代码的参与者。
msg.createdAt (uint32) — 返回外部消息的创建时间。
msg.data (TvmCell) — 返回消息中包含的所有数据(包括信头和正文)。
msg.body (TvmSlice) — 返回消息的正文部分。
msg.hasStateInit (bool) — 根据消息是否包含 stateInit 字段,返回一个布尔值。
msg.forwardFee (varUint16) — 返回发送内部消息所收取的 forwardFee(转发费用)值。
根据消息类型,此费用可细分为不同部分,其中一部分作为处理消息的奖励支付给验证者,并计入总操作费用(Total action fee)的计算中。
总操作费用是指从所有发送的消息(给定交易产生的所有智能合约操作)中计算出的总佣金,如果在智能合约代码执行过程中未发送任何消息,则不收取此费用。
msg.importFee (varUint16) — 返回处理外部消息所收取的 importFee(导入费用)值。此值很可能不能反映消息的真实价值,因为它是由网络外部的用户设置的。
externalMsg 和 internalMsg 修饰符:这些修饰符用于确定哪些类型的消息可以调用已定义的函数。如果省略任一修饰符,则允许函数同时被外部消息和内部消息调用。
TVM 区块链上的地址
TVM 网络支持以下几种地址类型:
addr_none— 尚未在网络中使用的地址;addr_extern— 不在Everscale网络上的地址;addr_std— 标准地址;addr_var— 任意地址。
地址(address_value)构造函数在第 0 工作链中创建一个标准地址。TVM 区块链具有异构性,这意味着一个主链(工作链 -1)可以联合多达 $2^{32}$ 个工作链,每个工作链都可以灵活配置。简而言之,这意味着网络的操作方式如同第二层解决方案直接内置于其内部,而非像以太坊那样建立在其之上。
“特殊”地址的创建
在特定工作链或必要工作链中创建地址:
address addrStd = address.makeAddrStd(wid, address);address addrNone = address.makeAddrNone();address addrExtern = address.makeAddrExtern(addrNumber, bitCnt);
还存在一些实用函数:
<address>.wid— 返回地址部署所在的工作链编号。如果地址不属于addr_std或addr_var类型,则抛出范围检查错误(range check error)异常。<address>.value— 返回addr_std和addr_var类型的 256 位地址值(如果addr_var具有 256 位值)。否则,抛出范围检查错误(range check error)异常。<address>.balance— 返回对应地址的余额。<address>.currencies— 返回对应地址的资产。
转账与标志位
为向目标地址发送消息,需要使用以下函数:
除 value 参数外,所有参数均可留空。
参数:
value— 附加到消息的原生货币金额。这将用于支付网络费用。由于代码执行是异步的,用户无法计算完成操作所需的准确 Gas 量。bounce— 如果此标志设为“true”,且交易因错误提前终止,所发送的资金必须退还给发送者余额。将标志设为“false”将导致资金交付给指定地址,即使该地址不存在或处于冻结状态。用户可能需要使用bounce = false函数在向地址发送资金的同时部署新合约。flag— 默认为 0。这意味着消息附加的资金量等于value参数中指定的金额。其他标志位参数值:128 — 消息将附带合约的全部余额,该余额在消息发送后将变为 0;
64 — 消息附加的 EVER 数量等于
value参数中指定的金额,加上从触发智能合约执行的传入消息中收到的全部金额;flag + 1(例如 64 + 1)— 发送方希望从地址余额中单独支付费用;
flag + 2 — 忽略在动作阶段发生的所有错误(动作阶段是 TVM 执行计算后网络状态更新的阶段);
flag + 32 — 如果地址余额为零,则将其从网络中移除。带有 128 + 32 标志的消息发送了相应智能合约的全部余额后,该合约将被从网络中移除。
body— 消息正文。这是默认类型的 TVM Cell;currencies— 附加到消息的额外货币。这用于发送 TIP3 代币。stateInit— 标准消息的初始化字段。如果stateInit以不正确的格式发送,将抛出 cell underflow 异常。通常,在部署或解冻合约时会发送stateInit。
考虑到 Everscale 区块链上的所有活动都是智能合约通过消息之间的通信,transfer() 函数被频繁使用。毋庸置疑,通过 transfer 函数,用户甚至可以在网络上部署新的智能合约。这对应于 Actor 模型,其中一个 Actor 可以通过消息与其他 Actor 进行通信并创建新的 Actor。
区块与交易
在 Everscale 和 Venom 网络中,区块可分为主链区块(master blocks)或工作链区块(workchain blocks)。在为组装主链区块所分配的时间内最终确定的所有工作链区块的哈希值都记录在主链区块中。
工作链区块包含来自工作链线程中所有智能合约的消息和交易信息。
根据定义,TVM 交易是智能合约代码执行的记录。
API 编译器提供了方法,允许用户获取区块/交易时间和逻辑区块/交易时间。
block.timestamp -> (uint32) — 返回当前区块的 UNIX 格式时间。块中的所有交易都将分配此时间值。
block.logicaltime -> (uint64) — 返回当前区块的逻辑时间起点,即该区块逻辑时间的起始点。
tx.logicaltime -> (uint64) — 返回当前交易的逻辑时间。
tx.storageFee -> (uint120) — 返回交易期间支付的存储费用(storageFee)的数值。
如果 UNIX 格式时间是直接的,那么逻辑时间(LT)就需要更详细地考虑。它与通常意义上的时间无关。逻辑时间涉及消息从一个智能合约传递到另一个的严格顺序。
新主链区块的逻辑时间总是比前一个主链区块的逻辑时间多 1,000,000。因此,所有依赖于主链区块的实体都将具有比主链区块本身更低的逻辑时间。
新线程区块的逻辑时间等于最后一个主链区块的逻辑时间 + 1,000,000。因此,在起始点,未来线程区块的逻辑时间等于未来主链区块的逻辑时间(线程区块引用前一个主链区块并增加 1,000,000 作为其逻辑时间,而未来主链区块则自动在前一个主链区块时间上增加 1,000,000)。
逻辑交易时间如下:
tx.LT = max(block.LT, msgInbound.LT, prevTx.LT) + 1
消息的逻辑时间如下:
msg.LT = max(tx.LT, txPrevMsg.LT) + 1
如果一个智能合约向同一收件人发送两条消息,它们将严格按照发送顺序被投递和执行。
显著的特殊性:
来自不同智能合约发往不同收件人的两条消息可能具有相同的逻辑时间。
消息可以在创建该消息的交易所在的同一个区块中投递,前提是该消息的接收者位于同一线程中。此时消息队列应为空。
如果例如合约 A 向合约 B 和合约 C 各发送一条消息,并且在执行了导致合约 B 和合约 C 都向合约 D 发送消息的代码之后,接收消息的严格顺序不能保证。如果合约 B 和 C 位于不同的线程中,则消息发送时间将取决于线程的繁忙程度。在编写智能合约时,应牢记这种异步架构的特性。尽管这可能看起来是一个严重的问题,但编写尽可能独立、逻辑互不依赖的智能合约,将有助于防止负面后果。这种风险(接收错误)的代价,远低于 TVM 网络异步架构所带来的高吞吐量或高可扩展性,且没有牺牲区块链的去中心化和安全性。
抽象二进制接口
pragma 关键字还用于告知编译器哪些头文件将被包含在智能合约的 .abi 文件中。ABI 是抽象二进制接口(Abstract Binary Interface)的缩写,它对于构建正确的算法至关重要,该算法负责将消息数据转换为细胞(cell)树,以供后续的 TVM(TON 虚拟机)操作。头文件是妥善处理外部消息(从 Everscale 或 Venom 网络外部发送的消息)所必需的。
ABI 文件可以包含以下头文件:
timestamp— 发送外部消息所需的时间戳。这对于防止重放攻击是必需的;expire— 消息的生命周期,默认情况下等于时间戳加 2 分钟;signature— 用于使用公钥对消息进行签名。外部消息可以采用公钥签名,以便调用需要用户验证的智能合约函数(内部消息不进行签名,因为它们是由智能合约发送的。智能合约通过地址(本质上是智能合约代码和静态变量的哈希值)来相互验证)。智能合约代码必须明确指出该函数可以通过接收外部消息来调用;如果智能合约 ABI 文件中包含此头文件,则所有外部消息都将对其签名进行检查。
以下是 ABI 文件内部消息体的外观:
使用 ABI 命名空间,我们可以将数据序列化为细胞数据类型,并从细胞中反序列化比特数据。这对于描述传输接收到消息的逻辑非常方便。
abi.encode(TypeA a, TypeB b, ...) -> (TvmCell /*cell*/) – 此函数对不同类型进行编码并返回一个细胞。
abi.decode(TvmCell cell, (TypeA, TypeB, ...)) -> (TypeA /*a*/, TypeB /*b*/, ...) — 此函数解码细胞并返回不同类型。并非所有类型都可以作为参数传递。如果传递了不正确的类型,该函数将抛出异常。
关于部署和升级
在“tvm 函数”一节中,我们已经涉及了在 Threaded Solidity 上升级和部署智能合约的过程。现在让我们仔细研究一下。
合约地址是通过对 stateInit 进行哈希计算得出的。为确保新智能合约的地址具有唯一性,你需要对用于部署合约的外部消息进行签名。如果是内部消息(通过现有智能合约部署的新合约),则必须将调用智能合约的地址(即新合约所有者的地址)传递给构造函数。
如果你需要部署多个具有相同代码的智能合约,那么值得在 stateInit 中包含一个额外的静态变量,例如不同的数字,以确保最终生成的哈希地址彼此不同。
new、stateInit 和 code 关键字
在使用带有 new 关键字的函数将智能合约部署到网络上时,必须使用 stateInit 和 code 关键字。
您可以编写一个包含所有静态变量的智能合约,然后该合约的代码和所有数据将以单元格树(tree of cells)的形式构成 stateInit。
stateInit— 决定新合约的初始状态。code— 定义新智能合约的代码。
请注意传递给 new 的变量:
value— 将附加到部署消息中的 EVER 数量;currencies— 将附加到部署消息中的额外货币(代币);bounce— 默认情况下此项设置为true,如果部署消息所附带的资金在 TVM 操作阶段发生异常,这些资金将被退回到发送部署消息的合约的余额中。若要使资金保留在新智能合约上,您必须传入bounce = false标志;wid— 应在新地址部署的工作链编号;默认为 0。目前,Everscale 网络上只部署了一个工作链,编号为 0;flag— 默认为 0。您可以参阅<address>.transfer()部分中关于初始消息标志的更多信息。
可以通过 <address>.transfer() 函数在网络上部署合约。为此,您只需将新合约的 stateInit 发送到函数参数即可。
更新智能合约代码
与以太坊虚拟机(EVM)网络通过代理合约和地址重新分配来更新智能合约不同,在兼容 TVM 的网络中,合约所有者可以通过一条消息以及对逻辑应用更新的简短描述,直接发送新版本的代码。
更新 TIP-3 代币的智能合约
tvm.setcode(TvmCell newCode) — 将智能合约的代码更新为所发送的 Cell 中包含的版本。此项更改将在当前智能合约代码完成执行后生效。
处理 fallback() 与 onBounce()
由于使用 Threaded Solidity 编写的智能合约之间的交互是异步的,并且部分数据是在特定操作执行期间才能确定的,因此无法预先确定特定交易是否会成功完成。为了使异步区块链如预期般运行,需要与异常处理机制类似的机制,但需要在网络中活跃的智能合约层面实现。
fallback() 方法定义了合约在接收到无效消息时的行为:
消息中调用了智能合约代码中不存在的函数 ID;
消息的位长度在 1 到 31 位之间;
消息长度为 0 位,但消息中包含了链接(links)。
onBounce() 方法定义了合约在接收到“反弹消息”(bounce message)时的行为。当在发出的消息中设置了 <address>.transfer() 部分所述的 bounce = true 标志时,网络会生成反弹消息。此外,如果发生以下情况,网络也会生成反弹消息:
接收智能合约消息的地址尚未在网络上部署;
被调用的智能合约无法执行其代码。
只有当附加到原始消息的 EVER 数量足以支付网络费用时,才能发送反弹消息。
tvm.functionId() — 返回一个 32 位无符号数,该数字是公共函数、外部函数或智能合约构造函数的编号。例如,这可用于描述智能合约在接收到反弹消息时的行为逻辑:
其他线程化 Solidity 功能
尝试-捕获 (Try-Catch)
与 Solidity 不同,智能合约代码中对 try-catch 的异常处理不仅适用于外部函数调用,同时也适用于新合约的创建。
同步(与异步相对的)调用
函数代码的一部分应在接收到被调用智能合约的响应后执行,通过使用 .await 后缀:
为此操作设置了一个特殊的 responsible 修饰符。执行带有 responsible 的函数总是会导致发送一条内部消息。在全文中,我们已经指出,某些方法在应用于带有和不带 responsible 修饰符的不同函数时会表现出不同的行为。
selfdestruct(address dest_addr) — 此操作会将智能合约余额中的所有资金发送到指定地址,并删除该账户。
新协议 — 旧语言
尽管我们详细介绍了编译器 API 和 Everscale 协议本身的诸多独特特性,但我们仍然认为,对于习惯于标准 Solidity 的开发者而言,使用 Threaded Solidity 编写智能合约代码在今天依然是便捷且易于理解的。
如果您对现代 TVM 兼容区块链的能力感到振奋,或者希望深入研究以符合 Actor 模型的异步范式编写的智能合约代码,您应该查阅我们公开的存储库,其中包含智能合约的示例,以便熟悉各种用例。您还应该前往我们的 GitHub,在那里您会发现已经在主网上运行了数年的真实解决方案(FlatQube、Octus Bridge、Gravix)。


