Rust智能合约数值精算:浮点数陷阱与整数精度优化

Rust智能合约养成日记(7)数值精算

1. 浮点数运算的精度问题

Rust语言原生支持浮点数运算,但浮点数运算存在着无法避免的计算精度问题。在编写智能合约时,不推荐使用浮点数运算,尤其是处理涉及重要经济/金融决策的比率或利率时。

Rust语言中浮点数遵循IEEE 754标准。以双精度浮点类型f64为例,其内部二进制表示如下:

浮点数采用底数为2的科学计数法表达。例如0.8125可用有限位数的二进制数0.1101表示:

0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1

然而对于0.7这样的小数,会出现无限循环的情况:

0.7 = 0.1011001100110011...

这就导致无法用有限位长的浮点数准确表示,存在"舍入"现象。

以在NEAR公链上分发0.7个NEAR代币给十位用户为例:

rust #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("The value of amount: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }

执行结果:

running 1 test The value of amount: 0.69999999999999995559 thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right) left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9

可见amount的值并非准确的0.7,而是一个近似值0.69999999999999995559。进一步的除法运算结果也变为不精确的0.06999999999999999。

为解决这个问题,可以考虑使用定点数。在NEAR Protocol中,通常采用10^24个yoctoNEAR等价于1个NEAR代币的表示方法。

修改后的测试代码:

rust #[test] fn precision_test_integer() { let N: u128 = 1_000_000_000_000_000_000_000_000;
let amount: u128 = 700_000_000_000_000_000_000_000; let divisor: u128 = 10;
let result_0 = amount / divisor; assert_eq!(result_0, 70_000_000_000_000_000_000_000, ""); }

执行结果:

running 1 test test tests::precision_test_integer ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s

2. Rust整数计算精度的问题

2.1 运算顺序

同一算数优先级的乘法与除法,其前后顺序的变化可能直接影响到计算结果,导致整数计算精度的问题。

rust #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;

let result_0 = a
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV");

let result_1 = a
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL");

assert_eq!(result_0,result_1,"");

}

执行结果:

running 1 test thread 'tests::precision_test_0' panicked at 'assertion failed: (left == right) left: 2, right: 0: ', src/lib.rs:175:9

可以发现result_0 = a * c / b 和 result_1 = (a / b) * c 尽管计算公式相同,但结果不同。原因是整数除法会舍弃小于除数的精度。在计算result_1时,先计算 (a / b) 会导致精度丢失变为0;而计算result_0时,先算得a * c = 20_0000大于除数b,避免了精度丢失。

2.2 过小的数量级

rust #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;

let result_0 = a
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL");

let result_1 = a
    .checked_mul(decimal)  
    .expect("ERR_MUL")
    .checked_div(b)
    .expect("ERR_DIV")
    .checked_mul(c)
    .expect("ERR_MUL")
    .checked_div(decimal)  
    .expect("ERR_DIV");

println!("{}:{}", result_0, result_1);
assert_eq!(result_0, result_1, "");

}

执行结果:

running 1 test 12:13 thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right) left: 12, right: 13: ', src/lib.rs:214:9

可见运算过程等价的result_0和result_1结果不同,且result_1 = 13更接近实际预期的13.3333....

3. 如何编写数值精算的Rust智能合约

3.1 调整运算的操作顺序

  • 令整数乘法优先于整数的除法。

3.2 增加整数的数量级

  • 整数使用更大的数量级,创造更大的分子。

3.3 积累运算精度的损失

对于无法避免的整数计算精度问题,可以考虑记录累计的运算精度损失。

rust const USER_NUM: u128 = 3;

fn distribute(amount: u128, offset: u128) -> u128 { let token_to_distribute = offset + amount; let per_user_share = token_to_distribute / USER_NUM; println!("per_user_share {}", per_user_share); let recorded_offset = token_to_distribute - per_user_share * USER_NUM; recorded_offset }

#[test] fn record_offset_test() { let mut offset: u128 = 0; for i in 1..7 { println!("Round {}", i); offset = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }

执行结果:

running 1 test Round 1 per_user_share 3333333333333333333333333 Offset 1

Round 2 per_user_share 3333333333333333333333333 Offset 2

Round 3 per_user_share 4000000000000000000000000 Offset 0

Round 4 per_user_share 3333333333333333333333333 Offset 1

Round 5 per_user_share 3333333333333333333333333 Offset 2

Round 6 per_user_share 4000000000000000000000000 Offset 0

test tests::record_offset_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 0.00s

3.4 使用Rust Crate库rust-decimal

该库适用于需要有效精度计算和没有舍入误差的小数金融计算。

3.5 考虑舍入机制

在设计智能合约时,舍入问题通常采用"我要占便宜,他人不得薅我羊毛"的原则。根据这个原则,如果向下取整对我有利,则向下;如果向上取整对我有利,则向上;四舍五入不能确定是对谁有利,因此极少被采用。

此页面可能包含第三方内容,仅供参考(非陈述/保证),不应被视为 Gate 认可其观点表述,也不得被视为财务或专业建议。详见声明
  • 赞赏
  • 5
  • 分享
评论
0/400
LiquidationKingvip
· 07-16 06:01
你说的0.7不是什么大事儿 来个大崩盘才带劲
回复0
破产艺术家vip
· 07-16 05:57
唉 写智能合约真的被浮点数坑惨过
回复0
GmGmNoGnvip
· 07-16 05:55
这bug能把我精度整丢了
回复0
NotSatoshivip
· 07-16 05:55
智能合约里玩浮点 脑袋都要炸咯
回复0
SandwichVictimvip
· 07-16 05:40
薛定谔的精度问题
回复0
交易,随时随地
qrCode
扫码下载 Gate APP
社群列表
简体中文
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)