Nhật ký phát triển hợp đồng thông minh Rust (1) Định nghĩa dữ liệu trạng thái hợp đồng và thực hiện phương pháp
Nhật ký phát triển hợp đồng thông minh Rust ( Viết kiểm tra đơn vị cho hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust ) 3( Triển khai hợp đồng thông minh Rust, gọi hàm và sử dụng Explorer
Nhật ký phát triển hợp đồng thông minh Rust )4( Tràn số nguyên hợp đồng thông minh Rust
Nhật ký phát triển hợp đồng thông minh Rust )5( tấn công tái nhập
Rust hợp đồng thông minh养成日记)6( từ chối dịch vụ tấn công
1. Vấn đề độ chính xác trong phép toán số thực
Khác với ngôn ngữ lập trình hợp đồng thông minh phổ biến là Solidity, ngôn ngữ Rust hỗ trợ tính toán số thực một cách tự nhiên. Tuy nhiên, tính toán số thực có những vấn đề về độ chính xác không thể tránh khỏi. Do đó, khi viết hợp đồng thông minh, không khuyến nghị sử dụng tính 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 quyết định kinh tế/tài chính quan trọng (.
Hiện nay, hầu hết các ngôn ngữ lập trình chính thống đều tuân theo tiêu chuẩn IEEE 754 để biểu diễn số thực, ngôn ngữ Rust cũng không phải là ngoại lệ. Dưới đây là mô tả về kiểu số thực chính xác gấp đôi f64 trong ngôn ngữ Rust và hình thức lưu trữ dữ liệu nhị phân bên trong máy tính:
Số thực được biểu diễn dưới dạng ký hiệu khoa học với cơ số 2. Ví dụ, nó có thể được biểu diễn bằng số nhị phân hữu hạn 0.1101 để biểu diễn số thập phân 0.8125, cách chuyển đổi cụ thể như sau:
0.8125 * 2 = 1 .625 // 0.1 nhận được chữ số nhị phân thứ 1 là 1
0.625 * 2 = 1 .25 // 0.11 nhận được chữ số nhị phân thứ 2 là 1
0.25 * 2 = 0 .5 // 0.110 nhận được chữ số nhị phân thứ 3 là 0
0.5 * 2 = 1 .0 // 0.1101 nhận được chữ số thập phân nhị phân thứ 4 là 1
0.8125 = 0.5 * 1 + 0.25 * 1 + 0.125 * 0 + 0.0625 * 1
Tuy nhiên, đối với một số thập phân khác là 0.7, quá trình chuyển đổi thực tế sang số thực sẽ gặp phải vấn đề như sau:
0.7 x 2 = 1. 4 // 0.1
0.4 x 2 = 0. 8 // 0.10
0.8 x 2 = 1. 6 // 0.101
0.6 x 2 = 1. 2 // 0.1011
0.2 x 2 = 0. 4 // 0.10110
0.4 x 2 = 0. 8 // 0.101100
0.8 x 2 = 1. 6 // 0.1011001
....
Do đó, số thập phân 0.7 sẽ được biểu diễn là 0.101100110011001100.....) lặp vô hạn (, không thể được biểu diễn chính xác bằng số thực với độ dài hữu hạn, và có hiện tượng "làm tròn )Rounding(".
Giả sử trên chuỗi công khai NEAR, cần phân phối 0.7 NEAR token cho mười người dùng, số lượng NEAR token mà mỗi người dùng nhận được sẽ được tính và lưu trữ trong biến result_0.
#)
fn precision_test_float[test]( {
// Số thực không thể biểu thị chính xác số nguyên
let amount: f64 = 0.7; // Biến amount đại diện cho 0.7 đồng NEAR
let divisor: f64 = 10.0; // định nghĩa số chia
let result_0 = a / b; // Thực hiện phép chia số thực
println!)"Giá trị của a: {:.20}", a(;
assert_eq!)result_0, 0.07, ""(;
}
Kết quả đầu ra của trường hợp thử nghiệm này như sau:
chạy 1 bài kiểm tra
Giá trị của a: 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 trong phép toán số thực ở trên, giá trị của amount không chính xác biểu thị 0.7, mà là một giá trị rất gần 0.69999999999999995559. Hơn nữa, đối với phép chia đơn giản như amount/divisor, kết quả phép toán cũng sẽ trở nên không chính xác là 0.06999999999999999, không phải 0.07 như mong đợi. Như vậy, có thể thấy sự không chắc chắn của phép toán số thực.
Đối với điều này, chúng ta không thể không xem xét việc sử dụng các phương pháp biểu diễn số khác trong hợp đồng thông minh, chẳng hạn như số cố định.
Theo vị trí cố định của dấu phẩy thập phân, số cố định có hai loại là số nguyên cố định ) thuần ( và số thập phân cố định ) thuần (.
Dấu phẩy được cố định sau vị trí thấp nhất của số, thì được gọi là số nguyên cố định.
Trong việc viết hợp đồng thông minh thực tế, thường sẽ sử dụng một phân số có mẫu số cố định để biểu thị một giá trị nào đó, chẳng hạn như phân số "x/N", trong đó "N" là hằng số, "x" có thể thay đổi.
Nếu "N" có giá trị là "1,000,000,000,000,000,000", tức là "10^18", lúc này số thập phân có thể được biểu diễn dưới dạng số nguyên, như vậy:
Trong giao thức NEAR, giá trị phổ biến của N là "10^24", tức là 10^24 yoctoNEAR tương đương với 1 token NEAR.
Dựa trên điều này, chúng ta có thể sửa đổi bài kiểm tra đơn vị trong phần này để tính toán theo cách sau:
#)
fn precision_test_integer[test]( {
// Đầu tiên định nghĩa hằng số N, biểu thị độ chính xác.
let N: u128 = 1_000_000_000_000_000_000_000_000; // tức là định nghĩa 1 NEAR = 10^24 yoctoNEAR
// Khởi tạo amount, thực tế giá trị mà amount biểu thị lúc này là 700_000_000_000_000_000 / N = 0.7 NEAR;
let amount: u128 = 700_000_000_000_000_000_000_000; // yoctoNEAR
// Khởi tạo số chia divisor
let divisor: u128 = 10;
// Tính toán có được:result_0 = 70_000_000_000_000_000_000_000 // yoctoNEAR
// Thực tế thể hiện 700_000_000_000_000_000_000_000 / N = 0.07 NEAR;
let result_0 = amount / divisor;
assert_eq!)result_0, 70_000_000_000_000_000_000_000, ""(;
}
Với điều này, có thể nhận được kết quả tính toán giá trị: 0.7 NEAR / 10 = 0.07 NEAR
chạy 1 bài kiểm tra
test tests::precision_test_integer ... ok
kết quả kiểm tra: ok. 1 đã pass; 0 thất bại; 0 bị bỏ qua; 0 đã đo; 8 đã bị lọc ra; hoàn thành trong 0.00s
2. Vấn đề về độ chính xác của phép tính số nguyên Rust
Từ mô tả ở phần 1 phía trên, có thể thấy rằng việc sử dụng phép toán số nguyên có thể giải quyết vấn đề mất độ chính xác trong phép toán số thực ở một số tình huống.
Nhưng điều này không có nghĩa là kết quả của việc tính toán số nguyên hoàn toàn chính xác và đáng tin cậy. Phần này sẽ giới thiệu một số nguyên nhân ảnh hưởng đến độ chính xác của tính toán số nguyên.
) 2.1 Thứ tự hoạt động
Thay đổi thứ tự trước và sau của phép nhân và phép chia cùng cấp độ ưu tiên toán 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 trong tính toán số nguyên.
Ví dụ có phép toán sau:
####
fn precision_test_div_before_mul[test]( {
let a: u128 = 1_0000;
let b: u128 = 10_0000;
let c: u128 = 20;
// result_0 = a * c / b
let result_0 = a
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(;
// result_0 = a / b * c
let result_1 = a
Kết quả của việc thực hiện kiểm tra đơn vị như sau:
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
Chúng ta có thể thấy result_0 = a * c / b và result_1 = )a / b(* c mặc dù công thức tính toán của chúng giống nhau, nhưng kết quả tính toán lại khác nhau.
Phân tích nguyên nhân cụ thể là: đối với phép chia số nguyên, độ chính xác nhỏ hơn số chia sẽ bị loại bỏ. Do đó, trong quá trình tính toán result_1, phép tính đầu tiên )a / b( sẽ mất độ chính xác tính toán trước, trở thành 0; trong khi trong quá trình tính toán result_0, kết quả tính toán đầu tiên là a * c là 20_0000, kết quả này sẽ lớn hơn số chia b, do đó tránh được vấn đề mất độ chính xác, có thể đạt được kết quả tính toán chính xác.
) 2.2 kích thước quá nhỏ
####
fn precision_test_decimals[test]( {
let a: u128 = 10;
let b: u128 = 3;
let c: u128 = 4;
let decimal: u128 = 100_0000;
// result_0 = )a / b( * c
let result_0 = a
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(;
// result_1 = )a * decimal / b( * c / decimal;
let result_1 = a
.checked_mul)decimal( // mul decimal
.expect)"ERR_MUL"(
.checked_div)b(
.expect)"ERR_DIV"(
.checked_mul)c(
.expect)"ERR_MUL"(
.checked_div)decimal( // div decimal
.expect)"ERR_DIV"(;
println!)"{}:{}", result_0, result_1(;
assert_eq!)result_0, result_1, ""(;
}
Kết quả cụ thể của bài kiểm tra đơn vị này như sau:
Có thể thấy rằng kết quả của quá trình tính toán tương đương result_0 và result_1 không giống nhau, và result_1 = 13 gần hơn với giá trị tính toán thực tế dự kiến: 13.3333....
3. Cách viết hợp đồng thông minh Rust cho định giá số
Đảm bảo độ chính xác đúng trong hợp đồng thông minh là rất quan trọng. Mặc dù trong ngôn ngữ Rust cũng tồn tại vấn đề mất độ chính xác trong kết quả phép toán số nguyên, nhưng chúng ta có thể áp dụng một số biện pháp bảo vệ sau đây để cải thiện độ chính xác, đạt được hiệu quả đáng kể.
) 3.1 điều chỉnh thứ tự thực hiện phép toán
Làm cho phép nhân số nguyên ưu tiên hơn phép chia số nguyên.
3.2 Tăng số lượng bậc của số nguyên
Sử dụng bậc số lớn hơn, tạo ra tử số lớn hơn.
Ví dụ đối với một token NEAR, nếu định nghĩa N = 10 như đã mô tả ở trên, thì điều này có nghĩa là: nếu cần biểu thị giá trị NEAR là 5.123, thì giá trị số nguyên thực tế được sử dụng trong phép toán sẽ được biểu thị là 5.123 * 10^10 = 51_230_000_000. Giá trị này tiếp tục tham gia vào các phép toán số nguyên tiếp theo, có thể nâng cao độ chính xác của phép toán.
3.3 Tổn thất độ chính xác trong tính toán tích lũy
Đối với vấn đề độ chính xác của phép toán nguyên không thể tránh khỏi, nhóm dự án có thể xem xét ghi lại tổn thất độ chính xác tích lũy.
u128 là cảnh phân phát token cho USER_NUM người dùng.
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() {
let mut offset: u128 = 0;
cho i trong 1..7 {
println!("Round {}",i);
offset = distribute[test]to_yocto("10"), offset(;
println!)"Offset {}\n",offset(;
}
}
Trong trường hợp thử nghiệm này, hệ thống sẽ phân phát 10 Token cho 3 người dùng mỗi lần. Tuy nhiên, do vấn đề độ chính xác của phép toán số nguyên, khi tính toán per_user_share trong vòng đầu tiên, kết quả phép toán số nguyên nhận được là 10 / 3 = 3, tức là người dùng được phân phát trong vòng đầu tiên sẽ nhận trung bình 3 token, tổng cộng 9 token đã được phân phát.
Tại thời điểm này, có thể thấy rằng vẫn còn 1 token chưa được phân phối cho người dùng trong hệ thống. Do đó, có thể xem xét việc tạm thời lưu trữ token còn lại này trong biến toàn cầu offset của hệ thống. Chờ đến lần hệ thống gọi lại distribute để phân phối token cho người dùng, giá trị này sẽ được lấy ra và cố gắng phân phối cùng với số tiền token của đợt phân phối này.
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.
9 thích
Phần thưởng
9
6
Chia sẻ
Bình luận
0/400
WalletWhisperer
· 22giờ trước
thú vị làm sao điểm nổi của rust có thể là hũ mật ong dễ bị tổn thương tiếp theo của chúng ta... theo dõi sát sao
Xem bản gốcTrả lời0
OnlyOnMainnet
· 22giờ trước
Tính toán số thực + on-chain Hehe dọa chết tôi
Xem bản gốcTrả lời0
TopEscapeArtist
· 22giờ trước
Các bạn ơi, vấn đề độ chính xác này giống như tôi đang đứng trên đỉnh vậy.
Xem bản gốcTrả lời0
RamenDeFiSurvivor
· 22giờ trước
Chạy mất rồi, vấn đề độ chính xác này thật khiến người ta bực mình.
Xem bản gốcTrả lời0
NFTArchaeologist
· 22giờ trước
Vấn đề độ chính xác mới là chết người nhất... Nếu không cẩn thận sẽ mất trắng.
Tính toán số chính xác trong hợp đồng thông minh Rust: số nguyên vs số thực
Rust hợp đồng thông minh养成日记(7):数值精算
Nhìn lại các kỳ trước:
1. Vấn đề độ chính xác trong phép toán số thực
Khác với ngôn ngữ lập trình hợp đồng thông minh phổ biến là Solidity, ngôn ngữ Rust hỗ trợ tính toán số thực một cách tự nhiên. Tuy nhiên, tính toán số thực có những vấn đề về độ chính xác không thể tránh khỏi. Do đó, khi viết hợp đồng thông minh, không khuyến nghị sử dụng tính 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 quyết định kinh tế/tài chính quan trọng (.
Hiện nay, hầu hết các ngôn ngữ lập trình chính thống đều tuân theo tiêu chuẩn IEEE 754 để biểu diễn số thực, ngôn ngữ Rust cũng không phải là ngoại lệ. Dưới đây là mô tả về kiểu số thực chính xác gấp đôi f64 trong ngôn ngữ Rust và hình thức lưu trữ dữ liệu nhị phân bên trong máy tính:
Số thực được biểu diễn dưới dạng ký hiệu khoa học với cơ số 2. Ví dụ, nó có thể được biểu diễn bằng số nhị phân hữu hạn 0.1101 để biểu diễn số thập phân 0.8125, cách chuyển đổi cụ thể như sau:
Tuy nhiên, đối với một số thập phân khác là 0.7, quá trình chuyển đổi thực tế sang số thực sẽ gặp phải vấn đề như sau:
Do đó, số thập phân 0.7 sẽ được biểu diễn là 0.101100110011001100.....) lặp vô hạn (, không thể được biểu diễn chính xác bằng số thực với độ dài hữu hạn, và có hiện tượng "làm tròn )Rounding(".
Giả sử trên chuỗi công khai NEAR, cần phân phối 0.7 NEAR token cho mười người dùng, số lượng NEAR token mà mỗi người dùng nhận được sẽ được tính và lưu trữ trong biến result_0.
Kết quả đầu ra của trường hợp thử nghiệm này như sau:
Có thể thấy trong phép toán số thực ở trên, giá trị của amount không chính xác biểu thị 0.7, mà là một giá trị rất gần 0.69999999999999995559. Hơn nữa, đối với phép chia đơn giản như amount/divisor, kết quả phép toán cũng sẽ trở nên không chính xác là 0.06999999999999999, không phải 0.07 như mong đợi. Như vậy, có thể thấy sự không chắc chắn của phép toán số thực.
Đối với điều này, chúng ta không thể không xem xét việc sử dụng các phương pháp biểu diễn số khác trong hợp đồng thông minh, chẳng hạn như số cố định.
Trong việc viết hợp đồng thông minh thực tế, thường sẽ sử dụng một phân số có mẫu số cố định để biểu thị một giá trị nào đó, chẳng hạn như phân số "x/N", trong đó "N" là hằng số, "x" có thể thay đổi.
Nếu "N" có giá trị là "1,000,000,000,000,000,000", tức là "10^18", lúc này số thập phân có thể được biểu diễn dưới dạng số nguyên, như vậy:
Trong giao thức NEAR, giá trị phổ biến của N là "10^24", tức là 10^24 yoctoNEAR tương đương với 1 token NEAR.
Dựa trên điều này, chúng ta có thể sửa đổi bài kiểm tra đơn vị trong phần này để tính toán theo cách sau:
Với điều này, có thể nhận được kết quả tính toán giá trị: 0.7 NEAR / 10 = 0.07 NEAR
![])https://img-cdn.gateio.im/webp-social/moments-7bdd27c1211e1cc345bf262666a993da.webp(
2. Vấn đề về độ chính xác của phép tính số nguyên Rust
Từ mô tả ở phần 1 phía trên, có thể thấy rằng việc sử dụng phép toán số nguyên có thể giải quyết vấn đề mất độ chính xác trong phép toán số thực ở một số tình huống.
Nhưng điều này không có nghĩa là kết quả của việc tính toán số nguyên hoàn toàn chính xác và đáng tin cậy. Phần này sẽ giới thiệu một số nguyên nhân ảnh hưởng đến độ chính xác của tính toán số nguyên.
) 2.1 Thứ tự hoạt động
Thay đổi thứ tự trước và sau của phép nhân và phép chia cùng cấp độ ưu tiên toán 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 trong tính toán số nguyên.
Ví dụ có phép toán sau:
.checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"(; assert_eq!)result_0,result_1,""(; }
Kết quả của việc thực hiện kiểm tra đơn vị như sau:
Chúng ta có thể thấy result_0 = a * c / b và result_1 = )a / b(* c mặc dù công thức tính toán của chúng giống nhau, nhưng kết quả tính toán lại khác nhau.
Phân tích nguyên nhân cụ thể là: đối với phép chia số nguyên, độ chính xác nhỏ hơn số chia sẽ bị loại bỏ. Do đó, trong quá trình tính toán result_1, phép tính đầu tiên )a / b( sẽ mất độ chính xác tính toán trước, trở thành 0; trong khi trong quá trình tính toán result_0, kết quả tính toán đầu tiên là a * c là 20_0000, kết quả này sẽ lớn hơn số chia b, do đó tránh được vấn đề mất độ chính xác, có thể đạt được kết quả tính toán chính xác.
) 2.2 kích thước quá nhỏ
.checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"(; // result_1 = )a * decimal / b( * c / decimal;
let result_1 = a .checked_mul)decimal( // mul decimal .expect)"ERR_MUL"( .checked_div)b( .expect)"ERR_DIV"( .checked_mul)c( .expect)"ERR_MUL"( .checked_div)decimal( // div decimal .expect)"ERR_DIV"(; println!)"{}:{}", result_0, result_1(; assert_eq!)result_0, result_1, ""(; }
Kết quả cụ thể của bài kiểm tra đơn vị này như sau:
Có thể thấy rằng kết quả của quá trình tính toán tương đương result_0 và result_1 không giống nhau, và result_1 = 13 gần hơn với giá trị tính toán thực tế dự kiến: 13.3333....
![])https://img-cdn.gateio.im/webp-social/moments-1933a4a2dd723a847f0059d31d1780d1.webp(
3. Cách viết hợp đồng thông minh Rust cho định giá số
Đảm bảo độ chính xác đúng trong hợp đồng thông minh là rất quan trọng. Mặc dù trong ngôn ngữ Rust cũng tồn tại vấn đề mất độ chính xác trong kết quả phép toán số nguyên, nhưng chúng ta có thể áp dụng một số biện pháp bảo vệ sau đây để cải thiện độ chính xác, đạt được hiệu quả đáng kể.
) 3.1 điều chỉnh thứ tự thực hiện phép toán
3.2 Tăng số lượng bậc của số nguyên
Ví dụ đối với một token NEAR, nếu định nghĩa N = 10 như đã mô tả ở trên, thì điều này có nghĩa là: nếu cần biểu thị giá trị NEAR là 5.123, thì giá trị số nguyên thực tế được sử dụng trong phép toán sẽ được biểu thị là 5.123 * 10^10 = 51_230_000_000. Giá trị này tiếp tục tham gia vào các phép toán số nguyên tiếp theo, có thể nâng cao độ chính xác của phép toán.
3.3 Tổn thất độ chính xác trong tính toán tích lũy
Đối với vấn đề độ chính xác của phép toán nguyên không thể tránh khỏi, nhóm dự án có thể xem xét ghi lại tổn thất độ chính xác tích lũy.
u128 là cảnh phân phát token cho USER_NUM người dùng.
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() { let mut offset: u128 = 0; cho i trong 1..7 { println!("Round {}",i); offset = distribute[test]to_yocto("10"), offset(; println!)"Offset {}\n",offset(; } }
Trong trường hợp thử nghiệm này, hệ thống sẽ phân phát 10 Token cho 3 người dùng mỗi lần. Tuy nhiên, do vấn đề độ chính xác của phép toán số nguyên, khi tính toán per_user_share trong vòng đầu tiên, kết quả phép toán số nguyên nhận được là 10 / 3 = 3, tức là người dùng được phân phát trong vòng đầu tiên sẽ nhận trung bình 3 token, tổng cộng 9 token đã được phân phát.
Tại thời điểm này, có thể thấy rằng vẫn còn 1 token chưa được phân phối cho người dùng trong hệ thống. Do đó, có thể xem xét việc tạm thời lưu trữ token còn lại này trong biến toàn cầu offset của hệ thống. Chờ đến lần hệ thống gọi lại distribute để phân phối token cho người dùng, giá trị này sẽ được lấy ra và cố gắng phân phối cùng với số tiền token của đợt phân phối này.
Dưới đây là quá trình phân phối token mô phỏng: