Nhật ký phát triển hợp đồng thông minh Rust (7) Tính toán số học
1. Vấn đề độ chính xác của phép toán số thực
Ngôn ngữ Rust hỗ trợ tính toán số thực một cách tự nhiên, nhưng phép toán số thực có vấn đề về độ chính xác không thể tránh khỏi. Khi viết hợp đồng thông minh, không nên sử dụng phép toán số thực, đặc biệt là khi xử lý các tỷ lệ hoặc lãi suất liên quan đến các quyết định kinh tế/tài chính quan trọng.
Trong ngôn ngữ Rust, số thực tuân theo tiêu chuẩn IEEE 754. Lấy loại số thực độ chính xác gấp đôi f64 làm ví dụ, biểu diễn nhị phân nội bộ của nó như sau:
Số thực được biểu diễn theo dạng số khoa học với cơ số 2. Ví dụ, 0.8125 có thể được biểu diễn bằng số nhị phân có số chữ số hữu hạn là 0.1101:
Tuy nhiên, đối với những số thập phân nhỏ như 0.7, sẽ xuất hiện tình huống lặp lại vô hạn.
0.7 = 0.1011001100110011...
Điều này dẫn đến việc không thể biểu diễn chính xác bằng số thực có độ dài hữu hạn, tồn tại hiện tượng "làm tròn".
Lấy ví dụ về việc phân phối 0.7 NEAR token cho mười người dùng trên chuỗi công khai NEAR:
gỉ
#[test]
fn precision_test_float() {
let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("Giá trị của số tiền: {:.20}", amount);
assert_eq!(result_0, 0.07, "");
}
Kết quả thực thi:
chạy 1 bài kiểm tra
Giá trị của số tiền: 0.69999999999999995559
thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right)
left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9
Có thể thấy giá trị của amount không phải là 0.7 chính xác, mà là một giá trị xấp xỉ 0.69999999999999995559. Kết quả của phép chia tiếp theo cũng trở nên không chính xác là 0.06999999999999999.
Để giải quyết vấn đề này, có thể xem xét sử dụng số cố định. Trong giao thức NEAR, thường sử dụng cách biểu diễn 10^24 yoctoNEAR tương đương với 1 mã thông báo NEAR.
Mã thử nghiệm đã chỉnh sửa:
rỉ sét
#[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, "");
}
Kết quả thực thi:
chạy 1 bài kiểm tra
test tests::precision_test_integer ... ok
kết quả kiểm tra: ok. 1 đã qua; 0 thất bại; 0 bị bỏ qua; 0 đã đo; 8 đã bị lọc; hoàn thành trong 0.00s
2. Vấn đề độ chính xác trong tính toán số nguyên Rust
2.1 Thứ tự toán học
Thứ tự trước sau của phép nhân và phép chia có cùng độ ưu tiên số học có thể ảnh hưởng trực tiếp đến kết quả tính toán, dẫn đến vấn đề độ chính xác của phép tính số nguyên.
gỉ
#[test]
fn precision_test_div_before_mul() {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
let result_1 = a
.checked_div(b)
.expect("ERR_DIV")
.checked_mul(c)
.expect("ERR_MUL");
assert_eq!(result_0,result_1,"");
}
Kết quả thực thi:
chạy 1 bài kiểm tra
thread 'tests::precision_test_0' panicked at 'assertion failed: (left == right)
left: 2, right: 0: ', src/lib.rs:175:9
Có thể nhận thấy result_0 = a * c / b và result_1 = (a / b) * c mặc dù công thức tính giống nhau, nhưng kết quả khác nhau. Nguyên nhân là phép chia số nguyên sẽ bỏ qua độ chính xác nhỏ hơn mẫu số. Trong khi tính toán result_1, việc tính (a / b) trước sẽ dẫn đến việc mất độ chính xác trở thành 0; trong khi tính toán result_0, việc tính trước a * c = 20_0000 lớn hơn mẫu số b đã tránh được việc mất độ chính xác.
2.2 quy mô quá nhỏ
gỉ
#[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")
test tests::record_offset_test ... ok
kết quả kiểm tra: ok. 1 đã qua; 0 thất bại; 0 bị bỏ qua; 0 được đo; 9 đã bị lọc; hoàn thành trong 0.00s
( 3.4 Sử dụng thư viện Rust Crate rust-decimal
Thư viện này phù hợp cho các phép toán tài chính với số thập phân yêu cầu tính toán chính xác và không có sai số làm tròn.
) 3.5 Xem xét cơ chế làm tròn
Khi thiết kế hợp đồng thông minh, vấn đề làm tròn thường áp dụng nguyên tắc "Tôi muốn kiếm lời, người khác không được lợi dụng tôi". Theo nguyên tắc này, nếu làm tròn xuống có lợi cho tôi, thì làm tròn xuống; nếu làm tròn lên có lợi cho tôi, thì làm tròn lên; làm tròn đến gần nhất không thể xác định ai có lợi, do đó rất ít được sử dụng.
Trang này có thể chứa nội dung của bên thứ ba, được cung cấp chỉ nhằm mục đích thông tin (không phải là tuyên bố/bảo đảm) và không được coi là sự chứng thực cho quan điểm của Gate hoặc là lời khuyên về tài chính hoặc chuyên môn. Xem Tuyên bố từ chối trách nhiệm để biết chi tiết.
15 thích
Phần thưởng
15
5
Chia sẻ
Bình luận
0/400
LiquidationKing
· 10giờ trước
Bạn nói 0.7 không phải là chuyện lớn, một cú sập lớn mới thú vị.
Xem bản gốcTrả lời0
BankruptcyArtist
· 10giờ trước
Ôi, việc viết hợp đồng thông minh thật sự đã bị số thực làm khổ.
Rust hợp đồng thông minh số liệu chính xác: bẫy số thực và tối ưu độ chính xác số nguyên
Nhật ký phát triển hợp đồng thông minh Rust (7) Tính toán số học
1. Vấn đề độ chính xác của phép toán số thực
Ngôn ngữ Rust hỗ trợ tính toán số thực một cách tự nhiên, nhưng phép toán số thực có vấn đề về độ chính xác không thể tránh khỏi. Khi viết hợp đồng thông minh, không nên sử dụng phép toán số thực, đặc biệt là khi xử lý các tỷ lệ hoặc lãi suất liên quan đến các quyết định kinh tế/tài chính quan trọng.
Trong ngôn ngữ Rust, số thực tuân theo tiêu chuẩn IEEE 754. Lấy loại số thực độ chính xác gấp đôi f64 làm ví dụ, biểu diễn nhị phân nội bộ của nó như sau:
Số thực được biểu diễn theo dạng số khoa học với cơ số 2. Ví dụ, 0.8125 có thể được biểu diễn bằng số nhị phân có số chữ số hữu hạn là 0.1101:
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Tuy nhiên, đối với những số thập phân nhỏ như 0.7, sẽ xuất hiện tình huống lặp lại vô hạn.
0.7 = 0.1011001100110011...
Điều này dẫn đến việc không thể biểu diễn chính xác bằng số thực có độ dài hữu hạn, tồn tại hiện tượng "làm tròn".
Lấy ví dụ về việc phân phối 0.7 NEAR token cho mười người dùng trên chuỗi công khai NEAR:
gỉ #[test] fn precision_test_float() { let amount: f64 = 0.7;
let divisor: f64 = 10.0;
let result_0 = amount / divisor;
println!("Giá trị của số tiền: {:.20}", amount); assert_eq!(result_0, 0.07, ""); }
Kết quả thực thi:
chạy 1 bài kiểm tra Giá trị của số tiền: 0.69999999999999995559 thread 'tests::precision_test_float' panicked at 'assertion failed: (left == right) left: 0.06999999999999999, right: 0.07: ', src/lib.rs:185:9
Có thể thấy giá trị của amount không phải là 0.7 chính xác, mà là một giá trị xấp xỉ 0.69999999999999995559. Kết quả của phép chia tiếp theo cũng trở nên không chính xác là 0.06999999999999999.
Để giải quyết vấn đề này, có thể xem xét sử dụng số cố định. Trong giao thức NEAR, thường sử dụng cách biểu diễn 10^24 yoctoNEAR tương đương với 1 mã thông báo NEAR.
Mã thử nghiệm đã chỉnh sửa:
rỉ sét #[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, ""); }
Kết quả thực thi:
chạy 1 bài kiểm tra test tests::precision_test_integer ... ok kết quả kiểm tra: ok. 1 đã qua; 0 thất bại; 0 bị bỏ qua; 0 đã đo; 8 đã bị lọc; hoàn thành trong 0.00s
2. Vấn đề độ chính xác trong tính toán số nguyên Rust
2.1 Thứ tự toán học
Thứ tự trước sau của phép nhân và phép chia có cùng độ ưu tiên số học có thể ảnh hưởng trực tiếp đến kết quả tính toán, dẫn đến vấn đề độ chính xác của phép tính số nguyên.
gỉ #[test] fn precision_test_div_before_mul() { let a: u128 = 1_0000; let b: u128 = 10_0000; let c: u128 = 20;
.checked_mul(c) .expect("ERR_MUL") .checked_div(b) .expect("ERR_DIV");
}
Kết quả thực thi:
chạy 1 bài kiểm tra thread 'tests::precision_test_0' panicked at 'assertion failed: (left == right) left: 2, right: 0: ', src/lib.rs:175:9
Có thể nhận thấy result_0 = a * c / b và result_1 = (a / b) * c mặc dù công thức tính giống nhau, nhưng kết quả khác nhau. Nguyên nhân là phép chia số nguyên sẽ bỏ qua độ chính xác nhỏ hơn mẫu số. Trong khi tính toán result_1, việc tính (a / b) trước sẽ dẫn đến việc mất độ chính xác trở thành 0; trong khi tính toán result_0, việc tính trước a * c = 20_0000 lớn hơn mẫu số b đã tránh được việc mất độ chính xác.
2.2 quy mô quá nhỏ
gỉ #[test] fn precision_test_decimals() { let a: u128 = 10; let b: u128 = 3; let c: u128 = 4; let decimal: u128 = 100_0000;
.checked_mul(c) .expect("ERR_MUL") .checked_div(decimal)
.expect("ERR_DIV");
}
Kết quả thực thi:
chạy 1 bài kiểm tra 12:13 thread 'tests::precision_test_decimals' panicked at 'assertion failed: (left == right) left: 12, right: 13: ', src/lib.rs:214:9
Có thể thấy rằng kết quả result_0 và result_1 có cùng quá trình tính toán nhưng khác nhau, và result_1 = 13 gần với dự đoán thực tế hơn là 13.3333....
3. Cách viết hợp đồng thông minh Rust cho tính toán số
3.1 Điều chỉnh thứ tự thao tác tính toán
3.2 tăng cường độ lớn của số nguyên
3.3 Tích lũy tổn thất độ chính xác tính toán
Đối với vấn đề độ chính xác của phép toán số nguyên không thể tránh khỏi, có thể xem xét việc ghi lại tổn thất độ chính xác tích lũy.
rỉ sét const USER_NUM: u128 = 3;
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 }
#( fn record_offset_test)[test] { let mut offset: u128 = 0; cho i trong 1..7 { println!("Round {}", i); offset = distribute(10_000_000_000_000_000_000_000_000, offset); println!("Offset {}\n", offset); } }
Kết quả thực hiện:
chạy 1 bài kiểm tra Vòng 1 per_user_share 3333333333333333333333333 Offset 1
Vòng 2 per_user_share 3333333333333333333333333 Offset 2
Vòng 3 per_user_share 4000000000000000000000000 Offset 0
Vòng 4 per_user_share 3333333333333333333333333 Offset 1
Vòng 5 per_user_share 3333333333333333333333333 Offset 2
Vòng 6 per_user_share 4000000000000000000000000 Offset 0
test tests::record_offset_test ... ok kết quả kiểm tra: ok. 1 đã qua; 0 thất bại; 0 bị bỏ qua; 0 được đo; 9 đã bị lọc; hoàn thành trong 0.00s
( 3.4 Sử dụng thư viện Rust Crate rust-decimal
Thư viện này phù hợp cho các phép toán tài chính với số thập phân yêu cầu tính toán chính xác và không có sai số làm tròn.
) 3.5 Xem xét cơ chế làm tròn
Khi thiết kế hợp đồng thông minh, vấn đề làm tròn thường áp dụng nguyên tắc "Tôi muốn kiếm lời, người khác không được lợi dụng tôi". Theo nguyên tắc này, nếu làm tròn xuống có lợi cho tôi, thì làm tròn xuống; nếu làm tròn lên có lợi cho tôi, thì làm tròn lên; làm tròn đến gần nhất không thể xác định ai có lợi, do đó rất ít được sử dụng.
![]###https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp###