跳到主要内容

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,除了 PREVMCBLOCKSPREVKEYBLOCK(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 类似)。

还提供了静默变体(例如 QMULADDDIVMODQUIET MULADDDIVMOD)。

如果返回值

不适应 257 位整数或除数为零,非静默操作会引发整数溢出异常。静默操作返回 NaN 而不是不适应的值(如果除数为零则返回两个 NaN)。

gas 成本等于 10 加上操作码长度:大多数操作码为 26 gas,LSHIFT#/RSHIFT# 额外加 8,静默额外加 8。

xxxxxxxxxxxxxxxxxxxxxx
Fift 语法
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
堆栈
MULADDDIVMODx y w z - q=floor((xy+w)/z) r=(xy+w)-zq
MULADDDIVMODRx y w z - q=round((xy+w)/z) r=(xy+w)-zq
MULADDDIVMODCx y w z - q=ceil((xy+w)/z) r=(xy+w)-zq
ADDDIVMODx w z - q=floor((x+w)/z) r=(x+w)-zq
ADDDIVMODRx w z - q=round((x+w)/z) r=(x+w)-zq
ADDDIVMODCx w y - q=ceil((x+w)/z) r=(x+w)-zq
ADDRSHIFTMODx w z - q=floor((x+w)/2^z) r=(x+w)-q*2^z
ADDRSHIFTMODRx w z - q=round((x+w)/2^z) r=(x+w)-q*2^z
ADDRSHIFTMODCx w z - q=ceil((x+w)/2^z) r=(x+w)-q*2^z
z ADDRSHIFT#MODx w - q=floor((x+w)/2^z) r=(x+w)-q*2^z
z ADDRSHIFTR#MODx w - q=round((x+w)/2^z) r=(x+w)-q*2^z
z ADDRSHIFTC#MODx w - q=ceil((x+w)/2^z) r=(x+w)-q*2^z
MULADDRSHIFTMODx y w z - q=floor((xy+w)/2^z) r=(xy+w)-q*2^z
MULADDRSHIFTRMODx y w z - q=round((xy+w)/2^z) r=(xy+w)-q*2^z
MULADDRSHIFTCMODx y w z - q=ceil((xy+w)/2^z) r=(xy+w)-q*2^z
z MULADDRSHIFT#MODx y w - q=floor((xy+w)/2^z) r=(xy+w)-q*2^z
z MULADDRSHIFTR#MODx y w - q=round((xy+w)/2^z) r=(xy+w)-q*2^z
z MULADDRSHIFTC#MODx y w - q=ceil((xy+w)/2^z) r=(xy+w)-q*2^z
LSHIFTADDDIVMODx w z y - q=floor((x*2^y+w)/z) r=(x*2^y+w)-zq
LSHIFTADDDIVMODRx w z y - q=round((x*2^y+w)/z) r=(x*2^y+w)-zq
LSHIFTADDDIVMODCx w z y - q=ceil((x*2^y+w)/z) r=(x*2^y+w)-zq
y LSHIFT#ADDDIVMODx w z - q=floor((x*2^y+w)/z) r=(x*2^y+w)-zq
y LSHIFT#ADDDIVMODRx w z - q=round((x*2^y+w)/z) r=(x*2^y+w)-zq
y LSHIFT#ADDDIVMODCx w z - q=ceil((x*2^y+w)/z) r=(x*2^y+w)-zq

Stack 操作

目前所有 stack 操作的参数都限制在 256 内。这意味着如果 stack 变得比 256 深,就会变得难以管理深层 stack 元素。在大多数情况下,没有为此限制的安全原因,即参数没有限制是为了防止过于昂贵的操作。对于一些大规模 stack 操作,比如 ROLLREV(其中计算时间线性依赖于参数值),gas 成本也线性依赖于参数值。

  • PICKROLLROLLREVBLKSWXREVXDROPXXCHGXCHKDEPTHONLYTOPXONLYX 的参数现在是不受限制的。
  • ROLLROLLREVREVXONLYTOPX 的参数越大,消耗的 gas 越多:额外的 gas 成本是 max(arg-255,0)(对于小于 256 的参数,gas 消耗是恒定的,对应于当前行为)。
  • 对于 BLKSWX,额外成本是 max(arg1+arg2-255,0)(这不符合当前行为,因为当前 arg1arg2 都限制为 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
描述
ECRECOVERhash v r s - 0 or h x1 x2 -1从签名恢复公钥,与比特币/以太坊操作相同。
以 32 字节哈希作为 uint256 hash;以 65 字节签名作为 uint8 v 和 uint256 rs
失败返回 0,成功返回公钥和 -1
以 65 字节公钥返回为 uint8 h,uint256 x1x2
1526 gas

secp256r1

使用 OpenSSL 实现。接口类似于 CHKSIGNS/CHKSIGNU。与 Apple Secure Enclave 兼容。

| xxx

xxxxxxxxxx
Fift 语法
xxxxxxxxxxxxxxxxx
堆栈
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
描述
P256_CHKSIGNSd sig k - ?检查 secp256r1 签名 sig 是否与切片 d 的数据部分和公钥 k 匹配。成功返回 -1,失败返回 0。
公钥是一个 33 字节切片(按照 SECG SEC 1 第 2.3.4 节第 2 点编码)。
签名 sig 是一个 64 字节切片(两个 256 位无符号整数 rs)。
3526 gas
P256_CHKSIGNUh sig k - ?相同,但签名的数据是 32 字节对 256 位无符号整数 h 的编码。
3526 gas

Ristretto

更详细的文档在这里。简而言之,Curve25519 是为了性能而开发的,但由于对称性而表现出多重表示的问题,使得群元素具有多个表示。简单的协议,如Schnorr签名或Diffie-Hellman,在协议级别应用一些技巧以减轻一些问题,但破坏了密钥推导和密钥遮蔽方案。而这些技巧在更复杂的协议,如Bulletproofs上无法扩展。Ristretto是对Curve25519的算术抽象,使得每个群元素对应于唯一的点,这是大多数密码协议的要求。Ristretto实质上是Curve25519的压缩/解压缩协议,提供所需的算术抽象。因此,可以认为我们在一步中添加了Ristretto和Curve25519曲线操作。

libsodium 实现

所有 ristretto-255 点都在TVM中表示为 256 位无符号整数。非静默操作在参数无效的情况下引发 range_chk。零点表示为整数 0

xxxxxxxxxxxxx
Fift 语法
xxxxxxxxxxxxxxxxx
堆栈
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
描述
RIST255_FROMHASHh1 h2 - x从 512 位哈希(由两个 256 位整数给出)确定性生成有效点 x
626 gas
RIST255_VALIDATEx -检查整数 x 是否是某个曲线点的有效表示。出错时引发 range_chk
226 gas
RIST255_ADDx y - x+y在曲线上两个点的相加。
626 gas
RIST255_SUBx y - x-y在曲线上两个点的相减。
626 gas
RIST255_MULx n - x*n将点 x 乘以标量 n
任何 n 都有效,包括负数。
2026 gas
RIST255_MULBASEn - g*n将生成器点 g 乘以标量 n
任何 n 都有效,包括负数。
776 gas
RIST255_PUSHL- l推送整数 l=2^252+27742317777372353535851937790883648493,这是群的阶。
26 gas
RIST255_QVALIDATEx - 0 or -1RIST255_VALIDATE 的静默版本。
234 gas
RIST255_QADDx y - 0 or x+y -1RIST255_ADD 的静默版本。
634 gas
RIST255_QSUBx y - 0 or x-y -1RIST255_SUB 的静默版本。
634 gas
RIST255_QMULx n - 0 or x*n -1RIST255_MUL 的静默版本。
2034 gas
RIST255_QMULBASEn - 0 or g*n -1RIST255_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_VERIFYpk msg sgn - bool检查BLS签名,成功时返回true,否则返回false。
61034 gas
BLS_AGGREGATEsig_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_AGGREGATEVERIFYpk_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_ADDx y - x+yG1上的加法。
3934 gas
BLS_G1_SUBx y - x-yG1上的减法。
3934 gas
BLS_G1_NEGx - -xG1上的取反。
784 gas
BLS_G1_MULx s - x*s将G1点x乘以标量s
任何s都是有效的,包括负数。
5234 gas
BLS_G1_MULTIEXPx_1 s_1 ... x_n s_n n - x_1*s_1+...+x_n*s_n计算G1点x_i和标量s_ix_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_G1f - x将FP元素f转换为G1点。
2384 gas
BLS_G1_INGROUPx - bool检查切片x是否表示有效的G1元素。
2984 gas
BLS_G1_ISZEROx - bool检查G1点x是否等于零。
34 gas
BLS_G2_ADDx y - x+yG2上的加法。
6134 gas
BLS_G2_SUBx y - x-yG2上的减法。
6134 gas
BLS_G2_NEGx - -xG2上的取反。
1584 gas
BLS_G2_MULx s - x*s将G2点x乘以标量s
任何s都是有效的,包括负数。
10584 gas
BLS_G2_MULTIEXPx_1 s_1 ... x_n s_n n - x_1*s_1+...+x_n*s_n计算G2点x_i和标量s_ix_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_G2f - x将FP2元素f转换为G2点。
7984 gas
BLS_G2_INGROUPx - bool检查切片x是否表示有效的G2元素。
4284 gas
BLS_G2_ISZEROx - bool检查G2点x是否等于零。
34 gas
BLS_PAIRINGx_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

INGROUPISZERO在无效的点上(除了cell下溢异常)不会引发异常,而是返回false。

其他算术操作在无效的曲线点上引发异常。请注意,它们不检查给定的曲线点是否属于G1/G2群。使用INGROUP指令来检查这一点。

RUNVM

目前在TVM中无法以“沙盒”中的方式调用外部不受信任的代码。换句话说,外部代码始终可以不可逆地更新合约的代码、数据,或设置操作(例如发送所有资金)。RUNVM指令允许生成独立的VM实例,运行所需的代码,并在不污染调用者状态的情况下获取所需的数据(堆栈、寄存器、气体消耗等)。以安全的方式运行任意代码可能对v4样式的插件、Tact的init样式子合约计算等非常有用。

xxxxxxxxxxxxx
Fift 语法
xxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
描述
flags RUNVMx_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。
其他标志位启用了其他参数和返回值,见下文。
RUNVMXx_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,返回消耗的gas g_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费用不考虑在内)。
  • SENDRAWMSGRAWRESERVESETLIBCODECHANGELIB - 添加了+16标志位,这意味着在操作失败时反弹交易。如果使用了+2,则没有效果。