TVM 升级 2023.07
此升级于 2023 年 12 月在主网上启动,详细信息请参考 run。
c7
c7 是存储有关合约执行所需的本地 context 信息的寄存器 (如时间、lt、网络配置等)。
c7 元组从 10 扩展到 14 个元素:
- 10: 存储智能合约本身的
cell。 - 11:
[integer, maybe_dict]:传入消息的 TON 值,额外代币。 - 12:
integer,存储阶段收取的费用。 - 13:
tuple包含有关先前区块的信息。
10 当前智能合约的代码仅以可执行继续的形式在 TVM 级别呈现,无法转换为cell。这段代码通常用于授权相同类型的 neighbor 合约,例如 Jetton 钱包授权 Jetton 钱包。目前我们需要显式地代码cell存储在存储器中,这使得存储和 init_wrapper 变得更加麻烦。 使用 10 作为代码对于 tvm 的 Everscale 更新兼容。
11 当前,传入消息的值在 TVM 初始化后以堆栈形式呈现,因此如果在执行过程中需要,
则需要将其存储到全局变量或通过本地变量传递(在 funC 级别看起来像所有函数中的额外 msg_value 参数)。通过将其放在 11 元素中,我们将重复合约余额的行为:它既出现在堆栈中,也出现在 c7 中。
12 目前计算存储费用的唯一方法是在先前的交易中存储余额,以某种方式计算 prev 交易中的 gas 用量,然后与当前余额减去消息值进行比较。与此同时,经常希望考虑存储费用。
13 目前没有办法检索先前区块的数据。TON 的一个关键特性是每个结构都是 Merkle 证明友好的cell(树),此外,TVM 也是cell和 Merkle 证明友好的。通过在 TVM context中包含区块信息,将能够实现许多不信任的情景:合约 A 可以检查合约 B 上的交易(无需 B 的合作),可以恢复中断的消息链(当恢复合约获取并检查某些事务发生但被还原的证明时),还需要了解主链区块哈希以在链上进行某些验证 fisherman 函数功能。
区块 id 的表示如下:
[ wc:Integer shard:Integer seqno:Integer root_hash:Integer file_hash:Integer ] = BlockId;
[ last_mc_blocks:[BlockId0, BlockId1, ..., BlockId15]
prev_key_block:BlockId ] : PrevBlocksInfo
包括主链的最后 16 个区块的 id(如果主链 seqno 小于 16,则为少于 16 个),以及最后的关键区块。包含有关分片区块的数据可能会导致一些数据可用性问题(由于合并/拆分事件),这并非必需(因为可以使用主链区块来证明任何事件/数据),因此我们决定不包含。
新的操作码
在选择新操作码的 gas 成本时的经验法则是它不应少于正常成本(从操作码长度计算)且不应超过每个 gas 单位 20 ns。
用于处理新 c7 值的操作码
每个操作码消耗 26 gas,除了 PREVMCBLOCKS 和 PREVKEYBLOCK(34 gas)。
| xxxxxxxxxxxxxxxxxxxxxx Fift 语法 | xxxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
MYCODE | - c | 从 c7 检索智能合约的代码 |
INCOMINGVALUE | - t | 从 c7 检索传入消息的值 |
STORAGEFEES | - i | 从 c7 检索存储阶段费用的值 |
PREVBLOCKSINFOTUPLE | - t | 从 c7 中检索 PrevBlocksInfo: [last_mc_blocks, prev_key_block] |
PREVMCBLOCKS | - t | 仅检索 last_mc_blocks |
PREVKEYBLOCK | - t | 仅检索 prev_key_block |
GLOBALID | - i | 从网络配置的第 19 项检索 global_id |
Gas
| xxxxxxxxxxxxxx Fift 语法 | xxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
GASCONSUMED | - g_c | 返回到目前为止 VM 消耗的 gas(包括此指令)。 26 gas |
算术
添加了 除法操作码(A9mscdf)的新变体:
d=0 从堆栈中获取一个额外的整数,并将其添加到除法/右移之前的中间值。这些操作返回商和余数(与 d=3 类似)。
还提供了静默变体(例如 QMULADDDIVMOD 或 QUIET MULADDDIVMOD)。
如果返回值
不适应 257 位整数或除数为零,非静默操作会引发整数溢出异常。静默操作返回 NaN 而不是不适应的值(如果除数为零则返回两个 NaN)。
gas 成本等于 10 加上操作码长度:大多数操作码为 26 gas,LSHIFT#/RSHIFT# 额外加 8,静默额外加 8。
| xxxxxxxxxxxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 堆栈 |
|---|---|
MULADDDIVMOD | x y w z - q=floor((xy+w)/z) r=(xy+w)-zq |
MULADDDIVMODR | x y w z - q=round((xy+w)/z) r=(xy+w)-zq |
MULADDDIVMODC | x y w z - q=ceil((xy+w)/z) r=(xy+w)-zq |
ADDDIVMOD | x w z - q=floor((x+w)/z) r=(x+w)-zq |
ADDDIVMODR | x w z - q=round((x+w)/z) r=(x+w)-zq |
ADDDIVMODC | x w y - q=ceil((x+w)/z) r=(x+w)-zq |
ADDRSHIFTMOD | x w z - q=floor((x+w)/2^z) r=(x+w)-q*2^z |
ADDRSHIFTMODR | x w z - q=round((x+w)/2^z) r=(x+w)-q*2^z |
ADDRSHIFTMODC | x w z - q=ceil((x+w)/2^z) r=(x+w)-q*2^z |
z ADDRSHIFT#MOD | x w - q=floor((x+w)/2^z) r=(x+w)-q*2^z |
z ADDRSHIFTR#MOD | x w - q=round((x+w)/2^z) r=(x+w)-q*2^z |
z ADDRSHIFTC#MOD | x w - q=ceil((x+w)/2^z) r=(x+w)-q*2^z |
MULADDRSHIFTMOD | x y w z - q=floor((xy+w)/2^z) r=(xy+w)-q*2^z |
MULADDRSHIFTRMOD | x y w z - q=round((xy+w)/2^z) r=(xy+w)-q*2^z |
MULADDRSHIFTCMOD | x y w z - q=ceil((xy+w)/2^z) r=(xy+w)-q*2^z |
z MULADDRSHIFT#MOD | x y w - q=floor((xy+w)/2^z) r=(xy+w)-q*2^z |
z MULADDRSHIFTR#MOD | x y w - q=round((xy+w)/2^z) r=(xy+w)-q*2^z |
z MULADDRSHIFTC#MOD | x y w - q=ceil((xy+w)/2^z) r=(xy+w)-q*2^z |
LSHIFTADDDIVMOD | x w z y - q=floor((x*2^y+w)/z) r=(x*2^y+w)-zq |
LSHIFTADDDIVMODR | x w z y - q=round((x*2^y+w)/z) r=(x*2^y+w)-zq |
LSHIFTADDDIVMODC | x w z y - q=ceil((x*2^y+w)/z) r=(x*2^y+w)-zq |
y LSHIFT#ADDDIVMOD | x w z - q=floor((x*2^y+w)/z) r=(x*2^y+w)-zq |
y LSHIFT#ADDDIVMODR | x w z - q=round((x*2^y+w)/z) r=(x*2^y+w)-zq |
y LSHIFT#ADDDIVMODC | x w z - q=ceil((x*2^y+w)/z) r=(x*2^y+w)-zq |
Stack 操作
目前所有 stack 操作的参数都限制在 256 内。这意味着如果 stack 变得比 256 深,就会变得难以管理深层 stack 元素。在大多数情况下,没有为此限制的安全原因,即参数没有限制是为了防止过于昂贵的操作。对于一些大规模 stack 操作,比如 ROLLREV(其中计算时间线性依赖于参数值),gas 成本也线性依赖于参数值。
PICK、ROLL、ROLLREV、BLKSWX、REVX、DROPX、XCHGX、CHKDEPTH、ONLYTOPX、ONLYX的参数现在是不受限制的。ROLL、ROLLREV、REVX、ONLYTOPX的参数越大,消耗的 gas 越多:额外的 gas 成本是max(arg-255,0)(对于小于 256 的参数,gas 消耗是恒定的,对应于当前行为)。- 对于
BLKSWX,额外成本是max(arg1+arg2-255,0)(这不符合当前行为,因为当前arg1和arg2都限制为 255)。
Hashes
目前 TVM 中只有两个哈希操作:计算cell/切片的 representation hash ,以及对数据进行 sha256,但只支持最多 127 字节(只有这么多数据适应一个cell)。
添加了 HASHEXT[A][R]_(HASH) 系列操作:
| xxxxxxxxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
HASHEXT_(HASH) | s_1 ... s_n n - h | 计算并返回切片(或构建器)s_1...s_n 的连接的哈希。 |
HASHEXTR_(HASH) | s_n ... s_1 n - h | 同样,但参数以相反的顺序给出。 |
HASHEXTA_(HASH) | b s_1 ... s_n n - b' | 将结果的哈希附加到构建器 b 而不是将其推送到堆栈。 |
HASHEXTAR_(HASH) | b s_n ... s_1 n - b' | 参数以相反的顺序给出,将哈希附加到构建器。 |
仅使用 s_i 的根cell的位。
每个块 s_i 可能包含非整数数量的字节。但所有块的位的和应该是 8 的倍数。注意 TON 使用最高位优先顺序,因此当连接两个具有非整数字节的切片时,第一个切片的位变为最高位。
gas 消耗取决于哈希字节数和所选算法。每个块额外消耗 1 gas 单位。
如果未启用 [A],则哈希的结果将作为无符号整数返回,如果适应 256 位,否则返回整数的元组。
可用以下算法:
SHA256- openssl 实现,每字节 1/33 gas,哈希为 256 位。SHA512- openssl 实现,每字节 1/16 gas,哈希为 512 位。BLAKE2B- openssl 实现,每字节 1/19 gas,哈希为 512 位。KECCAK256- 以太坊兼容实现,每字节 1/11 gas,哈希为 256 位。KECCAK512- 以太坊兼容实现,每字节 1/6 gas,哈希为 512 位。
Gas 使用会四舍五入。
Crypto
目前唯一可用的密码算法是 CHKSIGN:检查哈希 h 的 Ed25519 签名是否与公钥 k 匹配。
- 为了与以前的区块链(如比特币和以太坊)兼容,我们还需要检查
secp256k1签名。 - 对于现代密码算法,最低要求是曲线的加法和乘法。
- 为了与以太坊 2.0 PoS 和其他现代密码学的兼容性,我们需要在 bls12-381 曲线上进行 BLS 签名方案。
- 对于某些安全硬件,需要
secp256r1 == P256 == prime256v1。
secp256k1
Bitcoin/Ethereum签名。使用 libsecp256k1 实现。
| xxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
ECRECOVER | hash v r s - 0 or h x1 x2 -1 | 从签名恢复公钥,与比特币/以太坊操作相同。 以 32 字节哈希作为 uint256 hash;以 65 字节签名作为 uint8 v 和 uint256 r、s。失败返回 0,成功返回公钥和 -1。以 65 字节公钥返回为 uint8 h,uint256 x1、x2。1526 gas |
secp256r1
使用 OpenSSL 实现。接口类似于 CHKSIGNS/CHKSIGNU。与 Apple Secure Enclave 兼容。
| xxx
| xxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
P256_CHKSIGNS | d sig k - ? | 检查 secp256r1 签名 sig 是否与切片 d 的数据部分和公钥 k 匹配。成功返回 -1,失败返回 0。公钥是一个 33 字节切片(按照 SECG SEC 1 第 2.3.4 节第 2 点编码)。 签名 sig 是一个 64 字节切片(两个 256 位无符号整数 r 和 s)。3526 gas |
P256_CHKSIGNU | h sig k - ? | 相同,但签名的数据是 32 字节对 256 位无符号整数 h 的编码。3526 gas |
Ristretto
更详细的文档在这里。简而言之,Curve25519 是为了性能而开发的,但由于对称性而表现出多重表示的问题,使得群元素具有多个表示。简单的协议,如Schnorr签名或Diffie-Hellman,在协议级别应用一些技巧以减轻一些问题,但破坏了密钥推导和密钥遮蔽方案。而这些技巧在更复杂的协议,如Bulletproofs上无法扩展。Ristretto是对Curve25519的算术抽象,使得每个群元素对应于唯一的点,这是大多数密码协议的要求。Ristretto实质上是Curve25519的压缩/解压缩协议,提供所需的算术抽象。因此,可以认为我们在一步中添加了Ristretto和Curve25519曲线操作。
所有 ristretto-255 点都在TVM中表示为 256 位无符号整数。非静默操作在参数无效的情况下引发 range_chk。零点表示为整数 0。
| xxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 堆栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
RIST255_FROMHASH | h1 h2 - x | 从 512 位哈希(由两个 256 位整数给出)确定性生成有效点 x。626 gas |
RIST255_VALIDATE | x - | 检查整数 x 是否是某个曲线点的有效表示。出错时引发 range_chk。226 gas |
RIST255_ADD | x y - x+y | 在曲线上两个点的相加。 626 gas |
RIST255_SUB | x y - x-y | 在曲线上两个点的相减。 626 gas |
RIST255_MUL | x n - x*n | 将点 x 乘以标量 n。任何 n 都有效,包括负数。2026 gas |
RIST255_MULBASE | n - g*n | 将生成器点 g 乘以标量 n。任何 n 都有效,包括负数。776 gas |
RIST255_PUSHL | - l | 推送整数 l=2^252+27742317777372353535851937790883648493,这是群的阶。26 gas |
RIST255_QVALIDATE | x - 0 or -1 | RIST255_VALIDATE 的静默版本。234 gas |
RIST255_QADD | x y - 0 or x+y -1 | RIST255_ADD 的静默版本。 634 gas |
RIST255_QSUB | x y - 0 or x-y -1 | RIST255_SUB 的静默版本。634 gas |
RIST255_QMUL | x n - 0 or x*n -1 | RIST255_MUL 的静默版本。2034 gas |
RIST255_QMULBASE | n - 0 or g*n -1 | RIST255_MULBASE 的静默版本。784 gas |
BLS12-381
在配对友好的 BLS12-381 曲线上进行操作。使用BLST实现。此外,还有基于该曲线的 BLS 签名方案的操作。
在TVM中,BLS值以以下方式表示:
- G1点和公钥:48字节切片。
- G2点和签名:96字节切片。
- 字段FP的元素:48字节切片。
- 字段FP2的元素:96字节切片。
- 消息:切片。位数应该是8的倍数。
当输入值是点或字段元素时,切片可能具有超过48/96字节。在这种情况下,只采用前48/96字节。如果切片字节数较少(或消息大小不是8的倍数),则引发cell下溢异常。
高级操作
这些是用于验证BLS签名的高级操作。
| xxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
BLS_VERIFY | pk msg sgn - bool | 检查BLS签名,成功时返回true,否则返回false。 61034 gas |
BLS_AGGREGATE | sig_1 ... sig_n n - sig | 聚合签名。n>0。如果n=0或某个sig_i不是有效签名,则引发异常。gas=n*4350-2616 |
BLS_FASTAGGREGATEVERIFY- | pk_1 ... pk_n n msg sig - bool | 检查聚合的BLS签名,对于密钥pk_1...pk_n和消息msg返回true,否则返回false。如果n=0,返回false。gas=58034+n*3000 |
BLS_AGGREGATEVERIFY | pk_1 msg_1 ... pk_n msg_n n sgn - bool | 检查密钥-消息对pk_1 msg_1...pk_n msg_n的聚合BLS签名。如果成功,返回true,否则返回false。如果n=0,返回false。gas=38534+n*22500 |
VERIFY指令在无效的签名和公钥上(除了cell下溢异常)不会引发异常,而是返回false。
低层级操作
这些是群元素上的算术操作。
| xxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
BLS_G1_ADD | x y - x+y | G1上的加法。 3934 gas |
BLS_G1_SUB | x y - x-y | G1上的减法。 3934 gas |
BLS_G1_NEG | x - -x | G1上的取反。 784 gas |
BLS_G1_MUL | x s - x*s | 将G1点x乘以标量s。任何 s都是有效的,包括负数。5234 gas |
BLS_G1_MULTIEXP | x_1 s_1 ... x_n s_n n - x_1*s_1+...+x_n*s_n | 计算G1点x_i和标量s_i的x_1*s_1+...+x_n*s_n。如果n=0,返回零点。任何 s_i都是有效的,包括负数。gas=11409+n*630+n/floor(max(log2(n),4))*8820 |
BLS_G1_ZERO | - zero | 推送零点到G1中。 34 gas |
BLS_MAP_TO_G1 | f - x | 将FP元素f转换为G1点。2384 gas |
BLS_G1_INGROUP | x - bool | 检查切片x是否表示有效的G1元素。2984 gas |
BLS_G1_ISZERO | x - bool | 检查G1点x是否等于零。34 gas |
BLS_G2_ADD | x y - x+y | G2上的加法。 6134 gas |
BLS_G2_SUB | x y - x-y | G2上的减法。 6134 gas |
BLS_G2_NEG | x - -x | G2上的取反。 1584 gas |
BLS_G2_MUL | x s - x*s | 将G2点x乘以标量s。任何 s都是有效的,包括负数。10584 gas |
BLS_G2_MULTIEXP | x_1 s_1 ... x_n s_n n - x_1*s_1+...+x_n*s_n | 计算G2点x_i和标量s_i的x_1*s_1+...+x_n*s_n。如果n=0,返回零点。任何 s_i都是有效的,包括负数。gas=30422+n*1280+n/floor(max(log2(n),4))*22840 |
BLS_G2_ZERO | - zero | 推送零点到G2中。 34 gas |
BLS_MAP_TO_G2 | f - x | 将FP2元素f转换为G2点。7984 gas |
BLS_G2_INGROUP | x - bool | 检查切片x是否表示有效的G2元素。4284 gas |
BLS_G2_ISZERO | x - bool | 检查G2点x是否等于零。34 gas |
BLS_PAIRING | x_1 y_1 ... x_n y_n n - bool | 给定G1点x_i和G2点y_i,计算并乘积x_i,y_i的配对。如果结果是FP12的乘法单位元素,则返回true,否则返回false。如果n=0,返回false。gas=20034+n*11800 |
BLS_PUSHR | - r | 推送G1和G2的阶(约为2^255)。34 gas |
INGROUP,ISZERO在无效的点上(除了cell下溢异常)不会引发异常,而是返回false。
其他算术操作在无效的曲线点上引发异常。请注意,它们不检查给定的曲线点是否属于G1/G2群。使用INGROUP指令来检查这一点。
RUNVM
目前在TVM中无法以“沙盒”中的方式调用外部不受信任的代码。换句话说,外部代码始终可以不可逆地更新合约的代码、数据,或设置操作(例如发送所有资金)。RUNVM指令允许生成独立的VM实例,运行所需的代码,并在不污染调用者状态的情况下获取所需的数据(堆栈、寄存器、气体消耗等)。以安全的方式运行任意代码可能对v4样式的插件、Tact的init样式子合约计算等非常有用。
| xxxxxxxxxxxxx Fift 语法 | xxxxxxxxxxxxxxxxx 栈 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 描述 |
|---|---|---|
flags RUNVM | x_1 ... x_n n code [r] [c4] [c7] [g_l] [g_m] - x'_1 ... x'_m exitcode [data'] [c4'] [c5] [g_c] | 以代码code和栈x_1...x_n运行子VM。返回结果堆栈x'_1...x'_m和exit code。其他标志位启用了其他参数和返回值,见下文。 |
RUNVMX | x_1 ... x_n n code [r] [c4] [c7] [g_l] [g_m] flags - x'_1 ... x'_m exitcode [data'] [c4'] [c5] [g_c] | 同样的事情,但从堆栈弹出标志位。 |
标志位类似于fift中的runvmx:
+1:将c3设置为代码+2:在运行代码之前推送一个隐式的0+4:从堆栈中取c4(持久性数据),返回其最终值+8:从堆栈中取gas限制g_l,返回消耗的gasg_c+16:从堆栈中取c7(智能合约 context)+32:返回c5的最终值(操作)+64:从堆栈中弹出硬gas限制(由ACCEPT启用)g_m+128: “隔离的gas消耗”。子VM将具有一个单独的访问cell集合和一个单独的chksgn计数器。+256:弹出整数r,从堆栈顶部返回确切的r个值(仅当exitcode=0或1时;如果不够,则exitcode=stk_und)
gas成本:
- 66 gas
- 为提供给子VM的每个堆栈元素支付1 gas(前32个是免费的)
- 为从子VM返回的每个堆栈元素支付1 gas(前32个是免费的)
发送消息
目前在合约中难以计算发送消息的成本(导致了一些近似,比如在jettons中)并且如果 Action Phase 不正确,则无法将请求反弹回。精确减去传入消息的“合约逻辑的常量费用”和“gas费用”是不可能的。
SENDMSG以cell和模式为输入。创建一个输出操作并返回创建消息的费用。模式在SENDRAWMSG的情况下具有与SENDRAWMSG相同的效果。此外,+1024表示- 不要创建操作,只估算费用。其他模式影响费用计算如下:+64将传入消息的整个余额替换为传出值(略微不准确,不能在计算完成之前估算的gas费用不考虑在内),+128将合约在 Compute Phase 开始之前的整个余额的值替换为传出值(略微不准确,因为在 Compute Phase 完成之前不能估算的gas费用不考虑在内)。SENDRAWMSG,RAWRESERVE,SETLIBCODE,CHANGELIB- 添加了+16标志位,这意味着在操作失败时反弹交易。如果使用了+2,则没有效果。