Solidity 中的数学(第 1 部分:数字)
本文开启了一系列关于在 Solidity 中进行数学运算的文章。第一个要讨论的话题是:数字。
介绍
以太坊是一个可编程的区块链,其功能可以通过将称为智能合约的可执行代码片段发布到区块链本身来扩展。这将以太坊与第一代区块链区分开来,在第一代区块链中,新功能需要修改客户端软件、升级节点以及分叉整个区块链。
智能合约是在链上发布的一段可执行代码,分配有唯一的区块链地址。智能合约控制属于其地址的所有资产,并在与其他智能合约交互时可以代表该地址进行操作。每个智能合约都有持久存储,用于保存调用之间的智能合约状态。
Solidity是以太坊以及其他几个使用以太坊虚拟机 (EVM) 的区块链平台上智能合约开发的主要编程语言。
编程总是关于数学的,区块链总是关于金融的,金融自古就是关于数学的(或者也许数学就是关于金融的)。作为以太坊区块链的主要编程语言,Solidity 必须做好数学运算。
在本系列中,我们讨论了 Solidity 如何进行数学运算的各个方面,以及开发人员如何在 Solidity 中进行数学运算。第一个要讨论的话题是:数字。
Solidity 中的数值类型
与主流编程语言相比,Solidity 的数值类型相当多:即 5,248。是的,根据文档,有 32 种有符号整数、32 种无符号整数、2592 种有符号定点和 2592 种无符号定点类型。JavaScript 只有两种数字类型。Python 2 曾经有四个,但在 Python 3 中删除了“long”类型,所以现在只有三个。Java 有七个,C++ 大约有十四个。
有这么多数字类型,Solidity 应该有适合每个人的类型,对吧?没那么快。让我们仔细看看这些数字类型。
我们将从以下问题开始:
为什么我们需要多种数值类型?
剧透:我们没有。
纯数学中没有数字类型。一个数可能是整数或非整数,有理数或无理数,正数或负数,实数或虚数等,但这些只是属性,数字可能有也可能没有,单个数字可能同时具有多个这样的属性。
许多高级编程语言都有单一的数字类型。在 2019 年引入“BigInt”之前,JavaScript 只有“数字”。
除非做硬核低级的东西,否则开发人员并不真正需要多种数字类型,他们只需要具有任意范围和精度的纯数字。然而,这些数字本身不受硬件支持,并且在软件中模拟有些昂贵。
这就是为什么低级编程语言和以高性能为目标的语言通常具有多种数值类型,例如有符号/无符号,8/16/32/64/128位宽,整数/浮点数等。这些类型是原生支持的硬件,广泛用于文件格式、网络协议等,因此低级代码从中受益。
然而,出于性能原因,这些类型通常会继承底层 CPU 指令的所有奇怪语义,例如静默溢出和下溢、不对称范围、二进制分数、字节顺序问题等。这使得它们在高级业务逻辑代码中很痛苦。直接使用通常显得不安全,而安全使用通常变得麻烦且不可读。
那么,下一个问题是:
为什么 Solidity 有这么多数值类型?
剧透:它没有。
EVM 原生支持两种数据类型:256 位字和 8 位字节。堆栈元素、存储键和值、指令和内存指针、时间戳、余额、交易和块哈希、地址等都是 256 位字。内存、字节码、调用数据、返回数据都是由字节组成的。大多数 EVM 操作码都处理单词,包括所有数学运算。一些数学运算将单词视为有符号整数,一些作为无符号整数,而其他运算只是以相同的方式工作,而不管参数是否在无符号上签名。
所以 EVM 原生支持两种数值类型:有符号 256 位整数和无符号 256 位整数。这些类型在 Solidity 中分别称为int
和uint
。
除了这两种类型(以及它们的别名int256
和uint256
)之外,Solidity 有 62 种整数类型int<N>
,和uint<N>
,其中<N>
可以是 8 到 248 之间的任意 8 的倍数,即 8、16、…、248。在 EVM 级别上,所有这些类型都由相同的 256 位字,但每个操作的结果都被截断为 N 位。它们可能对特定情况有用,当需要特定位宽时,但对于一般计算,这些类型只是功能较弱且效率较低(在每个操作后截断不是免费的)版本int
和uint
.
最后,Solidity 有 5184 种定点类型fixedNxM
,ufixedNxM
其中 N 是从 8 到 256 的 8 的倍数,N 是从 0 到 80 的整数。这些类型应该实现各种范围和精度的十进制定点算术,但截至目前(Solidity 0.6.2)文档说:
Solidity 尚未完全支持定点数。它们可以声明,但不能分配给或分配自。
因此目前一般不支持定点数和分数数。
那么,下一个问题是:
如果我们需要大于 256 位的小数或整数怎么办?
剧透:你必须效仿他们。
有人会说,256 位应该对任何人都足够了。然而,一旦以太坊中的大多数数字都是 256 位宽,那么即使简单的两个数相加也可能有 257 位宽,而两个数的乘积可能高达 512 位宽。
模拟比编程语言本身支持的类型更宽的固定或可变宽度整数的常见方法是将它们表示为固定或可变长度的较短的、本机支持的整数序列。因此,宽整数的位图是较短整数的位图的串联。
在 Solidity 中,宽整数可以表示为固定或动态数组,其元素是字节或uint
值。
对于分数,情况有点复杂,因为它们有不同的风格,每种都有自己的优点和缺点。
最基本的是简单分数:只有一个整数,称为“分子”,除以另一个整数,称为“分母”。在 Solidity 中,简单分数可以表示为一对两个整数,也可以表示为一个整数,其位图是分子和分母位图的串联。在后一种情况下,分子和分母必须具有相同的宽度。
另一种流行的分数格式是定点数。定点数基本上是一个简单的分数,其分母是一个预定义的常数,通常是 2 或 10 的幂。前一种情况称为“二进制”定点,而后者称为“十进制”定点。只要分母是预定义的,就不需要明确指定,所以只需要指定分子。在 Solidity 中,定点数通常表示为单个整数分子,而常用的分母是 10¹⁸、10²⁷、2⁶⁴ 和 2¹²⁸。
另一种众所周知的小数格式是浮点数。基本上,浮点数可以描述如下:
其中m(尾数)和e(指数)是整数,B(基数)是预定义的整数常量,通常为 2 或 10。B =2 的情况称为“二进制”浮点数,B = =10 被称为“十进制”浮点数。
IEEE-754标准化了几种常见的浮点格式,包括称为“半”、“单”、“双”、“四倍”和“八倍”精度的五种二进制格式。这些格式中的每一种都将尾数和指数打包成分别为 16、32、64、128 或 256 位的单个序列。在 Solidity 中,这些标准格式可以用二进制类型bytes2
、bytes4
、bytes8
、bytes16
和表示bytes32
。或者,尾数和指数可以分别表示为一对整数。
这部分的文件问题:
我们必须自己实施这一切吗?
剧透:没有必要。
好消息是,有各种数字格式的 Solidity 库,例如:fixidity(具有任意小数位数的十进制定点)、DSMath(具有 18 位或 27 位小数的十进制定点)、BANKEX 库(IEEE-754 八元组精度浮点数)、ABDK 库(二进制定点数和四倍精度浮点数)等。
坏消息是不同的库使用不同的格式,所以很难将它们结合起来。这个问题的根源将在下一节中讨论。
Solidity 中的数字文字
在上一节中,我们讨论了数字在运行时是如何表示的。在这里,我们将看看它们在开发时是如何表示的,即在代码本身中。
与主流语言相比,Solidity 具有相当丰富的数字字面量语法。首先,支持良好的旧十进制整数,例如42
. 与其他类 C 语言一样,有十六进制整数文字,如0xDeedBeef
. 到目前为止,一切都很好。
在 Solidity 中,文字可能有单位后缀,例如6 ether
, 或3 days
。一个单位,基本上就是一个因数,字面意思是乘以。这ether
是 10¹⁸ 和days
86,400(24 小时 × 60 分钟 × 60 秒)。
除此之外,Solidity 还支持整数文字的科学记数法,例如2.99792458e8
. 这很不寻常,因为主流语言仅支持小数文字的科学记数法。
但可能整个 Solidity 语言最独特的功能是它对有理文字表达式的支持。实际上每个成熟的编译器都能够在编译时计算常量表达式,因此x = 2 + 2
不生成add
操作码,而是等同于x = 4
. Solidity 也能做到这一点,但实际上,它远远不止于此。
在主流语言中,常量表达式的编译时求值只是一种优化,因此常量表达式在编译时的求值方式与在运行时的求值方式完全相同。这使得可以用具有相同值的命名常量或变量替换此类表达式的任何部分,并获得完全相同的结果。然而,对于 Solidity 来说,情况并非如此。
在运行时,Solidity 循环中的除法结果趋向于零,其他算术运算在溢出时回绕,而在编译时,使用具有任意大分子和分母的简单分数计算表达式。因此,在运行时,表达式((7 / 11 + 3 / 13) * 22 + 1) * 39
将被计算为 39,而在编译时,相同的表达式被计算为 705。不同之处在于,在运行时,7 / 11
和3 / 13
被四舍五入为零,但在编译时,整个表达式被计算在简单的分数中,根本没有任何四舍五入。
更有趣的是,以下表达式在 Solidity 中是有效的:7523 /48124631 * 6397
,而这不是有效的:7523 / 48125631 * 6397
。不同之处在于前者评估为整数,而后者评估为非整数。请记住,Solidity 在运行时不支持分数,因此所有文字都必须是整数。
虽然小数和大整数可以在运行时在 Solidity 中表示,如前几节所述,但没有方便的方法在代码中表示它们。这使得使用此类数字执行操作的任何代码都相当神秘。
只要 Solidity 没有标准的定点或浮点格式,每个库都使用自己的格式,这使得库彼此不兼容。
结论
智能合约从以太坊区块链诞生之初就开始计算。从简单的百分比到复杂的衍生品估值。然而,智能合约开发的主要语言 Solidity 在数学方面并没有走得更远,只是揭示了 EVM 操作码能够做什么。
图书馆试图弥补基本语言遗漏的内容,但缺乏标准化的数字格式。
核心语言有一些无与伦比的特性,但同时缺乏对基本的、必备的东西的支持。
在后续的文章中,我们将讨论如何处理这一切,下一个要讨论的话题就是:溢出