相信很多开发者朋友在使用 sCrypt 开发比特币智能合约时,会有这样一个问题:为什么没有浮点数的数据类型?比如 Float 或者 Double?这篇文章我们就来聊聊在合约中如何实现模拟浮点数的高精度计算。
常见的编程语言一般都会有原生的浮点数支持,大部分都会参考实现 IEEE754 标准,但包括比特币在内的很多区块链的智能合约都没有原生支持浮点数,这其中可能有两个重要原因:1. 如果使用基于 FPU 硬件的实现(hard float)可能会有不同架构的节点在验证单笔交易时产生不同结果,从而导致共识失败;2. 如果使用基于软件模拟的实现(soft float)则执行效率会比较低,也可能有精度问题。
那么如果我们需要在合约内使用基于浮点数的高精度计算时应该怎么做呢?这里我们介绍两种可行的方法供大家参考。
一个最简单直接的方法就是将其转换为整数运算问题,如直接乘以一个10的 n 次方(这里的 n 等于需要小数点后的精度)。这种方法的优点是实现简单且成本低;缺点是如果有计算会产生分步的中间结果时,会有累计的精度损失误差。这里我们实现了一个基于定点数的库合约:
struct FP {
int val; // scaled-up value depends on precison.
}
//
library FixedPoint {
int precision; // should be 10^n
function add(FP x, FP y) : FP {
return {x.val + y.val};
}
function sub(FP x, FP y) : FP {
return {x.val - y.val};
}
function mul(FP x, FP y) : FP {
return {x.val * y.val / this.precision};
}
function div(FP x, FP y) : FP {
return {x.val * this.precision / y.val};
}
function abs(FP x) : FP {
return {abs(x.val)};
}
function fromInt(int i) : FP {
return {i * this.precision};
}
function toInt(FP fp) : int {
return fp.val / this.precision;
}
}
以下是它的使用示例合约:
import "fixedPoint.scrypt";
contract Test {
public function unlock(int precision, int x, int y, int op, int r) {
FixedPoint fp = new FixedPoint(precision);
FP result = fp.fromInt(0);
FP fpX = {x};
FP fpY = {y};
if (op == 0) {
result = fp.add(fpX, fpY);
} else if (op == 1) {
result = fp.sub(fpX, fpY);
} else if (op == 2) {
result = fp.mul(fpX, fpY);
} else if (op == 3) {
result = fp.div(fpX, fpY);
} else if (op == 4) {
result = fp.abs(fpX);
}
require(result == {r});
require(fp.toInt(result) == r / precision);
}
}
另外一种方法利用了比特币智能合约中的整数为非固定位大整数这一特点,实现了一种基于分数的高精度计算,即将所有运算因子都保存为分数形式。这样的优点是哪怕有中间结果也无需担心精度损失,因为舍尾操作可以只在最后进行;缺点是实现和使用成本较前一种高一些。下面是我们实现的一个库合约:
struct Fraction {
int n; // numerator
int d; // denominator
}
// A fraction-based math library for high precision calculation.
library FRMath {
static function add(Fraction x, Fraction y) : Fraction {
return {
x.n * y.d + y.n * x.d,
x.d * y.d
};
}
// safe add, requires both argument denominators > 0
static function sAdd(Fraction x, Fraction y) : Fraction {
require(x.d > 0 && y.d > 0);
return {
x.n * y.d + y.n * x.d,
x.d * y.d
};
}
static function sub(Fraction x, Fraction y) : Fraction {
return {
x.n * y.d - y.n * x.d,
x.d * y.d
};
}
// safe sub, requires both argument denominators > 0
static function sSub(Fraction x, Fraction y) : Fraction {
require(x.d > 0 && y.d > 0);
return {
x.n * y.d - y.n * x.d,
x.d * y.d
};
}
static function mul(Fraction x, Fraction y) : Fraction {
return {
x.n * y.n,
x.d * y.d
};
}
// safe mul, requires both argument denominators > 0
static function sMul(Fraction x, Fraction y) : Fraction {
require(x.d > 0 && y.d > 0);
return {
x.n * y.n,
x.d * y.d
};
}
static function div(Fraction x, Fraction y) : Fraction {
return {
x.n * y.d,
x.d * y.n
};
}
// safe div, requires both argument denominators > 0 and y != 0
static function sDiv(Fraction x, Fraction y) : Fraction {
require(x.d > 0 && y.d > 0 && y.n != 0);
return {
x.n * y.d,
x.d * y.n
};
}
static function abs(Fraction x) : Fraction {
return {
abs(x.n),
abs(x.d)
};
}
// safe abs, requires both argument denominators > 0
static function sAbs(Fraction x) : Fraction {
require(x.d > 0);
return {
abs(x.n),
x.d
};
}
static function equal(Fraction x, Fraction y) : bool {
return sub(x, y).n == 0;
}
static function sEqual(Fraction x, Fraction y) : bool {
return sSub(x, y).n == 0;
}
static function toInt(Fraction x) : int {
return x.n / x.d;
}
static function fromInt(int numerator, int denominator) : Fraction {
return {numerator, denominator};
}
static function scaleUp(Fraction x, int s) : int {
return x.n * s / x.d;
}
}
已经使用它的示例合约:
import "fractionMath.scrypt";
contract Main {
function runOp(int op, Fraction x, Fraction y) : Fraction {
Fraction r = {0, 1};
if (op == 0) {
r = FRMath.add(x, y);
} else if(op == 1) {
r = FRMath.sub(x, y);
} else if(op == 2) {
r = FRMath.mul(x, y);
} else if(op == 3) {
r = FRMath.div(x, y);
} else if(op == 4) {
r = FRMath.abs(x);
}
return r;
}
function runSafeOp(int op, Fraction x, Fraction y) : Fraction {
Fraction r = {0, 1};
if (op == 0) {
r = FRMath.sAdd(x, y);
} else if(op == 1) {
r = FRMath.sSub(x, y);
} else if(op == 2) {
r = FRMath.sMul(x, y);
} else if(op == 3) {
r = FRMath.sDiv(x, y);
} else if(op == 4) {
r = FRMath.sAbs(x);
}
return r;
}
public function unlock(Fraction x, Fraction y, Fraction z, int op, bool strict) {
Fraction r = {0, 1};
if (strict) {
r = this.runSafeOp(op, x, y);
require(FRMath.sEqual(r, z));
} else {
r = this.runOp(op, x, y);
require(FRMath.equal(r, z));
}
require(true);
}
public function unlockScaled(int s, Fraction x, Fraction y, int op, bool strict, int sr) {
Fraction r = {0, 1};
if (strict) {
r = this.runSafeOp(op, x, y);
require(FRMath.scaleUp(r, s) == sr);
} else {
r = this.runOp(op, x, y);
require(FRMath.scaleUp(r, s) == sr);
}
require(true);
}
}
上面是我们针对合约内使用浮点数的需求提供的两种实现参考,希望对各位有所帮助。如果有进一步的需求和改进方案,也欢迎和我们进一步交流或提供代码 PR,再次感谢。