NEAR Live Contract Review | Phần 1: Voting Contracts

23 min read
To Share and +4 nLEARNs

Giới thiệu

Xin chào, tôi là Eugene và tôi ở đây với NEAR, hôm nay chúng ta sẽ thử một thứ mới. Chúng ta sẽ cùng tìm hiểu quá trình phát triển hợp đồng thông minh bằng ngôn ngữ Rust. Đây là buổi đầu tiên, hãy để tôi giới thiệu bản thân mình. Tôi làm việc tại NEAR, xử lý phần runtime (thời gian chạy) và contracts (hợp đồng). Ngày hôm nay chúng ta sẽ thảo luận về hợp đồng bỏ phiếu và đây là contract đã được sử dụng trên mainnet để bỏ phiếu cho việc NEAR Protocol chuyển từ phase 1 sang phase 2.

Những Validator phải bỏ phiếu để cho phép việc chuyển đổi diễn ra, đây là điều cần thiết để giúp mạng lưới hoàn toàn phi tập trung.

Đây là video gốc mà bài viết này dựa theo.

Tổng quan về cấu trúc

Tôi sử dụng CLI trên Rust vì đây là một trong những công cụ tuyệt vời nhất để làm việc trên rust và bên tay trái, bạn có thể thấy kho lưu trữ (repo) cho những core contracts (hợp đồng cốt lõi). Bạn có thể tìm thấy mã nguồn của dự án này trên GitHub chính thức của chúng tôi, có rất nhiều hợp đồng, và contract mà chúng ta sẽ tìm hiểu trong bài viết này là voting contract (Hợp đồng bỏ phiếu).

Đầu tiên, tôi sẽ giới thiệu khái quát cấu trúc của repo này. Trong Rust, bạn có src lib, lib.rs là file chính, nó là một file thư viện (library) vì chúng ta sẽ không biên dịch một file binary mà là biên dịch một thư viện thành một file định dạng WebAssembly.

Đây là file Cargo.toml: đây là nơi chúng ta mô tả các dependency (các thư viện, package có sẵn mà chúng ta muốn tái sử dụng trong một dự án Rust). Chúng ta sẽ sử dụng NEAR SDK phiên bản 2.0 để xây dựng nên hợp đồng bỏ phiếu. Bên cạnh đó, các bạn có thể thấy trong file Cargo.toml này có cấu hình một số flags (dưới phần khai báo [profile.release])  như ‘panic’, ‘debug’ nhằm tối ưu hóa file kết quả sau khi biên dịch.

Có một loạt các tối ưu hóa nhằm giảm kích thước của mã nguồn sau khi biên dịch. Trong phần [profile.release], flag ‘debug’ được gán giá trị ‘false’, cùng với đó việc gán giá trị flag ‘panic’ bằng ‘abort’ đồng nghĩa với việc khi gặp trường hợp ngoại lệ trình biên dịch sẽ không mất thêm thời gian để truy ngược lại lỗi, việc này rất quan trọng đối với WebAssembly.

Cuối cùng chúng ta để overflow-checks true (bật kiểm tra tràn), có nghĩa là khi xảy ra hiện tượng tràn số nguyên bạn có thể trừ đi một số. Giả sử bạn lấy 2-1 và là số âm và nếu số nguyên thuộc dạng unsigned, trình biên dịch của Rust sẽ panic (tương tự như throw error trong các ngôn ngữ lập trình khác). Việc này thường âm thầm xảy ra trong C++, nhưng trong Rust bạn có thể kích hoạt flag (overflow-checks=true) để trình biên dịch panic khi overflow hoặc nếu code bị vượt qua giới hạn của số nguyên theo cả hai hướng. Vì vậy nếu code bạn vượt qua giới hạn lưu trữ của kiểu dữ liệu u32 sẽ gây ra code panic. Khi bạn phát triển hợp đồng thông minh cho bất kỳ nền tảng nào, thường thì overflow và silent overflows không phải là điều mà bạn mong muốn và lỗi này có thể bị các hacker lợi dụng, nên lý tưởng nhất là contract của bạn phải panic ngay để báo cho bạn xử lý nó. Điều này cũng nhằm đảm bảo tính an toàn cho contract.

Lib.rs

Chúng ta có nhiều file khác nhau và sẽ xem chúng sau, đầu tiên, hãy bắt đầu với contract chính. 

Đây là một file khá đơn giản, nhưng đồng thời cũng có vài thứ thú vị. Các contracts trên NEAR Protocol được viết bằng ngôn ngữ Rust có một cấu trúc dữ liệu gọi là struct chính (có thể có thêm một số struct phụ khác trong mã nguồn). Trong trường hợp được xét đến trong bài viết này thì hợp đồng của chúng ta có một struct tên là VotingContract, cấu trúc dữ liệu struct tương tự như khái niệm class trong một số ngôn ngữ khác. Bên cạnh đó chúng ta cũng sử dụng một số macros trong mã nguồn sau đây.

near_bindgen là một macro từ NEAR SDK biến struct thành thứ mà chúng ta sử dụng cho các contract, tôi sẽ giải thích kỹ phần này ở phía sau. BorshDeserialize BorshSerialize là hai macro cho phép struct này và tất cả các fields trong nó được serialize thành định dạng nhị phân, được gọi là borsh và cũng dùng để deserialized (giải mã hóa). 

Điều này là cần thiết để có thể lưu trữ toàn bộ contract trong một state của blockchain.

Cách thức hoạt động của các contract là chúng có một số persistent state (trạng thái liên tục) mà từ đó chúng có thể đọc thông tin về mọi lệnh gọi của hợp đồng và chúng cũng có bộ nhớ cục bộ chỉ khả dụng khi hợp đồng đã được triển khai lên blockchain NEAR. Tôi cũng sẽ giải thích kỹ hơn khi chúng ta đi đến phần phía sau. Đến đây, chúng ta có một struct là VotingContract và trong ngôn ngữ Rust, chúng ta có khái niệm implementation của struct – tương tự như việc định nghĩa các method cho một class trong các ngôn ngữ hướng đối tượng khác.

Constructor

Một trong số các method của struct VotingContract được implement là new, trong Rust thì method này được gọi là một constructor. Method này không có tham số và giá trị trả về có kiểu Self. Ở đây, Self tương đương với một voting contract, hiểu theo các ngôn ngữ hướng đối tượng khác thì phương thức new này sẽ trả về một instance của class VotingContract. Chúng ta có một decorator macro tên là init và init macro này cho phép khởi tạo state của contract mà không cần có state trước đó. Cách mà các contract thường được triển khai trên NEAR Protocol là bạn deploy code của contract. Về cơ bản, bạn đã deploy một class sau đó bạn cần khởi tạo một instance bằng cách gọi tới constructor. Việc này sẽ lưu một state, cụ thể là struct này vào persistent storage của contract. Vậy nên phương thức này sẽ luôn có sẵn mỗi khi bạn gọi tới contract.

Khi bạn lần đầu tiên khởi tạo một voting contract, voting contract không cần bất kỳ tham số nào vì nó chưa cần biết thông tin gì. Nó sẽ chỉ dựa vào current context (bối cảnh hiện tại) của blockchain và thông tin về bối cảnh này được lấy từ module tên là env (viết tắt của environment) trong NEAR SDK.

Macro nào được sử dụng để serialize mọi thứ thành dạng nhị phân?

Correct! Wrong!

Đây là nơi chúng ta sẽ cần thông tin và sử dụng những tham số mà các contracts hay người dùng khác có thể gọi tới. Có hai thứ xảy ra trong constructor. Đầu tiên, chúng ta kiểm tra xem state đã tồn tại chưa (hợp đồng đã được khởi tạo chưa)

init macro cho phép contract có thể được gọi nhiều lần, vì vậy nó không cần kiểm tra xem state hiện tại đã có hay chưa, nó cho phép chúng ta làm vậy. Chúng tôi đang làm việc để thêm một macro khác gọi là init_once sẽ cho phép chỉ cần khởi tạo contract một lần và nó sẽ thực hiện việc kiểm tra này tự động. 

Ở dòng code thứ năm ta thấy macro assert sau: (assert!(!env::state_exists)), tương tự như một unit test, nó xác minh rằng biểu thức đã cho là true, nếu không sẽ raise panic làm function call bị dừng, và sẽ báo lỗi: “the contract is already initialized”. Ở đây chúng ta kiểm tra xem state đã được khởi tạo hay chưa, nếu rồi thì chúng ta sẽ không thể khởi tạo lại nó. Nếu chúng ta không check cái này thì chúng ta có thể ghi đè bất kỳ thứ gì trong contract bằng cách khởi tạo một state mới.

Bạn có thể tham khảo thêm về việc kiểm tra này qua documentation của NEAR-SDK-RS (SDK của NEAR viết bằng ngôn ngữ Rust). Mỗi khi một phương thức không phải là constructor được gọi, ví dụ như phương thức ping dưới đây, phương thức đó sẽ lấy đối số có thể thay đổi và nó sinh ra code cụ thể từ contract này thực hiện một loạt các hoạt động bằng cách sử dụng decorator near_bindgen trước khi tham gia vào execution phase.


Nếu bạn dùng mutable self hoặc self thì một trong những thao tác đầu tiên nó sẽ thử là deserialize state hiện tại dựa trên một api cấp thấp là nhận tham số đầu vào là một state key. Key này là key lưu trữ dữ liệu của struct chính (VotingContract).

Giả sử chúng ta xây dựng state trong một constructor thì về cơ bản đây là địa chỉ trả về của giá trị cuối.

Phương thức khởi tạo này trả về một struct VotingContract mới, tiếp theo, struct VotingContract mới này sẽ được serialized thành định dạng nhị phân borsh bởi near_bindgen macro và sau đó được ghi vào persistent storage sử dụng một key state. 

Giá trị được lưu trữ sẽ là giá trị hiện tại của VotingContract struct, nên lần tới khi method như ping được gọi, đầu tiên nó sẽ đọc phần nhị phân từ storage của blockchain, deserialize nó để tạo lại struct VotingContract.

Sau khi tạo lại struct VotingContract từ storage, method ping sẽ được chạy. Phương thức state_exists() tới từ NEAR SDK. Phương thức này là một dạng wrapper có tác dụng kiểm tra xem bộ nhớ đã cho có một key cụ thể hay không, nếu có thì nó sẽ biết rằng state này đã tồn tại.

Vậy thì init thực sự làm gì? init chờ bạn trả về một class hoặc một instance mới, sau đó nó sẽ ghi vào storage.

Tóm lại, phương thức new() không nhận vào tham số nào, khi được gọi tới thì nó ghi một state mới vào storage của blockchain NEAR. Chúng ta cũng có thể viết các phương thức mặc định cho contract của mình. Chúng ta không muốn việc này xảy ra nên struct VotingContract cần được khởi tạo trước. Vì vậy nó sẽ bị lỗi và code panic. NEAR SDK sẽ cố gắng đọc state từ storage từ một state key trước, và nếu nó không tồn tại, SDK sẽ call default method. Nếu method tồn tại, SDK sẽ không cần phải gọi hàm mặc định nữa.

Interface của Contract

Hãy xem interface của contact trước. Chúng ta có vài methods và Rust có pub decorator có nghĩa là hàm public, nếu không có pub thì hàm là private.

Vậy NEAR decorators sẽ chỉ biến các method public sang những method bạn có thể gọi được trong conrtact. Nghĩa là check_result sẽ không thể gọi được bởi người khác, trong khi những method khác có thể được gọi tới bởi bất kỳ ai. Điều quan trọng nữa về NEAR SDK là sự khác biệt trong các tham số bạn truyền vào. Nếu bạn truyền tham số mut self, nghĩa là method sẽ được cho phép sửa state. Chúng ta gọi kiểu method này là gọi hàm bình thường(regular function call), chúng chỉ có thể tới thông qua giao dịch(transaction).

Nếu self là mutable, chúng gọi là change method. Ngược lại với không có mut self, state của cấu trúc sẽ trở thành immutable (bất biến). Tham số self thông thường tạo một trạng thái chỉ đọc (read-only state),không thể chỉnh sửa. Loại phương thức này có thể được gọi sử dụng hàm view call và chúng sẽ không chỉnh sửa state. 

Nếu bạn không để mut self thì nó sẽ không thử write vào state. 

Phần quan trọng là chúng ta nên coi chúng là phương thức không thể thay đổi để chúng không chỉnh sửa state. Trước khi đi sâu vào các method cụ thể nào thì hãy nhìn vào method vote, nó có một tham số  thêm vào trong state. Theo mặc định near_bingen chờ các tham số được chuyển tới dưới dạng  JSON. Bộ giải mã mặc định của tham số là JSON, vì vậy phần thân của method chờ được gọi với dạng kiểu: ‘{“is_vote”: true}’. Tên method sẽ là vote và tham số sẽ là JSON is_vote bằng true.  Việc này quan trọng vì bạn không thể truyền các tham số bằng cách sử dụng lệnh gọi vị trí(positional arguments) vào contract nhưng trong ví dụ này, chúng ta có thể gọi vote với positional arguments. Và trong các unit test, chúng ta có thể sử dụng các positional argument.

Nhưng nếu bạn đang gọi contract từ unit test hoặc trên blockchain thì bạn đang gọi tới contract WebAssembly được biên dịch trước đó, và hợp đồng sẽ mong đợi tham số ở dạng JSON. Raw WebAssembly không biết bất kỳ loại input đầu vào nào và phân tích cú pháp input hoàn toàn khác biệt.

Các loại kết quả trả về sau khi mở rộng conrtact macro near_bingen cũng là JSON. Loại này là dạng u64 tùy chỉnh từ NEAR SDK JSON. Đó là một cấu trúc có kiểu u64 nhưng khi JSON được deserialization hay serialization được gọi trên nó, nó sẽ biến thành một chuỗi(string). Lý do cho điều này là JSON specs không hỗ trợ tất cả các loại u64. Giới hạn là 2 ^ 53 vì đây chính là giới hạn số ban đầu của javascript. Chuẩn JSON mặc định của frontend sẽ không thể serialize số lớn u64 sang một con số chính xác. Thay vào đó, chúng ta bọc u64 interger và u128 interger vào dưới dạng string để cho method trả về một JSON. Ví dụ như chuỗi “1000” lại thực ra là một số, hoặc nếu kết quả không tồn tại, sẽ trả về null.

Đây là tổng quan của NEAR SDK

Mut selft nghĩa là method có thể sửa đổi state? Đúng hay sai?

Correct! Wrong!

Voting Call

Bây giờ chúng ta đi sâu vào một trong các method. Method chính mà ta có trong contract này là một voting call. Khi bạn là  một Validator đang hoạt động, bạn có một tài khoản có tối thiểu một vị trí trong NEAR Protocol, bạn phải stake số NEAR khá lớn để trở thành validator. Sau đó, bạn có thể gọi method này trước giai đoạn hai để tiến hành vote.

Nếu is_vote true có nghĩa rằng bạn muốn vote cho bước chuyển sang giai đoạn hai, nếu false, có nghĩa là bạn rút lại phiếu bầu của mình. 

Giả sử lần đầu tiên bạn kiểm tra phiếu bầu của mình, và sau đó lại muốn hủy bỏ phiếu bầu vì một lí do nào đó, chẳng hạn như bạn cảm thấy bạn không hiểu rõ gì đó nên muốn rút lại. Hãy xem cách nó hoạt động. Vote là một method thay đổi và nó tự động nhận mối số tham số được deserialize bởi near_binden. Như chúng ta đã nói trước đây, mặc dù mong đợi định dajg JSON, bên trong nó chỉ là một biến boolean mà bạn có thể dùng như bình thường trên Rust. Method này không nói về cách state bị thay đổi hoặc những action bạn đã thực hiện, cho nên bạn không cần phải lưu state theo cách thủ công. Nó sẽ tự động được lưu một cách tự động nếu method hoàn tất.  Vì vậy điều đầu tiên nó làm là gọi method ping. Chúng ta sẽ quay lại method đấy sau, nhưng nó cập nhật internal state bằng cách kiểm tra nếu epoch đã thay đổi và Validator đang hoạt động là ai để cập nhật dữ liệu.

Sau đó, vote method kiểu tra nếu chúng ta đã đạt được tới kết quả vote chưa: if self.result.is_some() 

Trong trường hợp này, kết quả là một field cho biết nếu chúng ta đã đạt tỉ lệ ⅔ số Validators, chỉ cần một dấu timestamp để bắt đầu mỗi giai đoạn.

Timestamp tính bằng nano giây vì đây là cách chúng ta hiển thị timestamp bên trong context. Option nghĩa là nó có thể tồn tại hoặc nó không tồn tại, nếu tồn tại, nó sẽ trả về timestamp. Tức là là quá trình bỏ phiếu đã xảy ra, và chúng ta đã đạt tới giai đoạn 2 ở timestamp nhất định nào đó, đơn vị nano giây, nếu không có thì nghĩa là quá trình bỏ phiếu không đạt được. Vậy nên chỉ cần kiểm tra xem nếu chúng ta đã đạt được một phiếu hay chưa. Nếu quá trình vote đã đạt được tới giai đoạn 2 thì chúng ta coi như hoàn thành, phiếu vote của bạn không còn quan trọng nữa vì ta đã tới được giai đoạn 2 và timestamp đã có sẵn ở đấy. 

Rust không có null mà thay vào đó, nó có các tùy chọn có thể trả về giá trị hoặc không, bạn phải xử lý chúng thật rõ ràng. Khi nó được mã hóa để gửi ra ngoài, nó sẽ phải gửi và lưu dưới dạng JSON, chẳng hạn như nó đang được lưu trữ và gửi dưới dạng null.

Tiếp theo là truy vấn, ngữ cảnh của NEAR SDK, nơi chúng ta kiểm tra được ai là người gọi hàm.

Có 3 loại tài khoản khả dụng cho context, một trong số đó là predecessor (tài khoản tiền nhiệm) – đây là account id của người tiền nhiệm trực tiếp(immediate predecessor) của người gọi vote. 

NEAR là môi trường đồng bộ và bạn có thể có một chuỗi các cross-contract call.

Ví dụ Alice gọi contract của Bob, Bob gọi contract của Cheryl và trong trường hợp này, Bob là predecessor. Thông qua contract của Cheryl, predessor sẽ là tài khoản vừa gọi hàm xong, trong case này là Bob. 

Ngược lại, cũng có tài khoản là signer account id (tài khoản người ký). Đây là tài khoản đã sign transaction ban đầu. Giả sử Alice là người đã gọi contract của Bob và sau đó, signer account id sẽ là Alice. Tài khoản hiện tại là tài khoản của contract, trong trường hợp này, là Cheryl, tài khoản nắm giữ hợp đồng. 

Trên NEAR, contract đó luôn thuộc về một số tài khoản mà ở đó có trạng thái(state) và balance(số dư) , và đã được deploy code lên. Ví lý do bảo mật, điều rất quan trọng là không nên dựa vào người ký giao dịch, mà cần dựa vào người predecessor ngay trước đó.

Ví dụ Bob có một contract độc hại và nếu anh ta dựa vào signer account, Bob có thể call một contract chuyển token và rút số dư từ Alice vào tài khoản của mình bằng cách thay mặt Alice. Nếu Bob thử làm như vậy thì token contract sẽ lấy predecessor, chính là Bob, nên contract của Bob chỉ có thể tiêu token của chính mình. Nó có một số hạn chế nhưng điều này đảm bảo rằng nếu bạn gọi contract thì contract không thể thay mặt bạn xử lý transaction, contract chỉ có thể tương tác với chính nó.

Việc tiếp theo chúng ta làm là kiểm tra tham số được truyền vào. Nếu đó là phiếu bầu thì có nghĩa là chúng ta muốn thêm số từ caller vào một tập hợp những validator đang hoạt động đã bỏ phiếu cho giai đoạn hai, vì vậy nếu không thì giá trị bằng không. Nó có nghĩa là chúng ta muốn đặt stake bằng 0, vì vậy ta gọi một phương thức đặc biệt: validator_stake trên môi trường là phương thức đóng một hàm máy chủ lưu trữ trong runtime của NEAR Protocol lấy được số active stake đang hoạt động từ epoch của id tài khoản đã cho.

Nó trả về số lượng stake từ một validator nhất định và sẽ được thực hiện ngay lập tức.

Nên assert!(stake>0, ‘ {} is not a validator’, account_id); là một lệnh gọi đồng bộ khi chúng ta xác minh rằng số stake bằng 0 hoặc lớn hơn 0 để chắc chắn rằng ta không gây rối cho state với các validator không hoạt động. Bằng cách này, chúng ta có thể giới hạn số lượng validator có thể đưa vào trong state, vì chúng ta chỉ có 100 chỗ khi contract được sử dụng.

Có nghĩa rằng bạn có thể giới hạn số lượng validator vào trong state này là 100. Về cơ bản, nếu assert kích hoạt điều khoản stake bằng 0 thì ta sẽ trả về lỗi panic và mọi thứ đã thực hiện trước đây sẽ được chuyển sang state. Nó sẽ được rollback (quay trở lại) tới phần đầu của function call chỉ cho hợp đồng này. Vì vậy, quay lại với ví dụ về Alice, Bob và Cheryl, chỉ contract state của Cheryl sẽ được hoàn nguyên, trạng thái của Bob thì không vì là môi trường đồng bộ. 

Tiếp theo, chúng ta kiểm tra xem mình đã vote chưa. Những votes là dạng hashmap.

Hashmaps

HashMap nằm trong memory map, nghĩa rằng nó được mã hóa, hoặc sau khi giải mã, tất cả giá trị phải được mã hóa trở lại thành một phần, và lại được giải mã trở lại. 

Mỗi khi chúng ta gọi một method trong contract này, nó sẽ  giải mã tất cả các field của HashMap, vì vậy cả hàng trăm tài khoản sẽ được giải mã hóa cùng một lúc, nhưng nó sẽ thực hiện việc này trong một lần đọc bộ nhớ thay vì hàng trăm lần. Mặc dù kích thước tổng thể lớn vì mỗi tài khoản có thể có tối đa 64 ký tự và số dư là 16 byte, nhưng kích thước tổng thể không quá lớn, cỡ bằng 100 x 80 nên tương đương 8 kilobyte.

Những gì chúng ta cần cho method ping là lặp lại mỗi lần trên mỗi vote để cập nhật số stake của validator. Ví dụ, cho một ordered map thì chúng ta cần làm gì đó theo thứ tự 100 lần đọc, và trong trường hợp này, chúng ta cần ghi đè chúng, nghĩa là cần 100 lần đọc và 100 lần ghi trong trường hợp này, đắt hơn nhiều so với việc làm một lần cho 100 tài khoản, vì ta biết được cấu trúc bị giới hạn tới 100 tài khoản. Bạn không cần dùng phân trang mà có thể bạn sẽ cần như khi dùng persistent map. Do bản thử đã được lưu trữ trong storage, nó được lưu giữ như một bản thử và việc này khiến việc đọc nhiều lần trở nên tốn kém. Vì vậy, trong trường hợp này, sẽ có lợi nếu có một map để có thể lặp lại quá trình kiểm tra.

Hướng dẫn ở đây sẽ là nếu bạn cần lặp lại trên mỗi key và với tối đa là 100, bạn nên dùng hashmap hoặc vector.

Nếu NEAR tiến tới sharded, thì cái này cần sửa đổi với một persistent map hoặc có thể bạn có thể làm với sharded map. Hiện tại thì chưa, nó sẽ không phải dạng bulk map. Nó sẽ là dạng persistent map với bulk objects, bởi vì bạn vẫn cần lặp qua tất cả.

Có thể bạn sẽ cần chia account sang 20 buckets riêng, và đối với mỗi bucket sẽ có một vector, nhưng bạn sẽ cần lưu trữ chúng riêng thành một continuous map để khi cần trả lại, bạn sẽ chỉ cần đọc 20 key hoặc bạn có thể làm một phương thức phân trang cho hàm ping.

Hiện tại vấn đề lưu trữ storage có lẽ là vấn đề tốn kém nhất mà chúng ta cần phải đối mặt, và nếu bạn thích ghi đè lên mọi thứ thì nó sẽ có khả năng còn đắt hơn sử dụng hashmap.

Đó là lý do sử dụng hashmap và nó thực sự không quá tệ. Nhưng để an toàn, chúng ta có thể ước tính 100 feet vào trong gas limit, và nó trở thành một lựa chọn tốt cho trường hợp này.

Voting Call (phần tiếp)

Ở phần vote_stake, chúng ta xóa bỏ phần stake cũ đi, đây là một bước kiểm tra an toàn. Nó là bất biến, có nghĩa là nó không có khả năng kích hoạt trong môi trường thực tế. Sau đó, nó xác minh rằng số stake hiện tại nhỏ hơn tổng số stake, sau đó ta trừ số stake cũ là ra một khoản stake mới. 

Tôi đồng ý rằng một thứ tự tốt hơn sẽ là trước tiên trừ đi số stake cũ và sau đó đưa ra số stake mới để tránh bị overflow. Mặc dù với giới hạn về tổng cung, nó sẽ không bị overflow, nhưng cẩn thận không thừa.

Nếu đây là một cuộc bỏ phiếu tích cực, nó không phải là một cuộc rút phiếu thì ta đưa kết quả vào storage.

Cuối cùng, ta sẽ kiểm tra kết quả một lần nữa cho chắc.

Hãy đến với check_result trước. Chúng ta xác minh rằng khi check_result được gọi thì kiểm tra rằng kết quả không phải là một tổng. Đó là một cách khác để kiểm tra, và nó cho chúng ta biết nó bất biến.

Ta thấy được tổng số stake của validator là bao nhiêu. total_stake là một phương thức khác được trả về từ context, nó nói chúng ta có tổng stake là 100 triệu. Sau đó, bên trong total_voted_stake là số stake đang hoạt động bao gồm tất cả những validator đã vote.

Giả sử nếu số stake đang hoạt động được làm tròn hơn hai phần ba tổng số stake, thì chúng ta sẽ đạt được kết quả và đó là phần cuối của contract này. 

Vậy là đã hoàn thành việc bỏ phiếu, đây là một phương pháp khá đơn giản.

Có bao nhiêu validator bỏ phiếu trong quá trình chuyển sang phase 2?

Correct! Wrong!

Ping 

Giờ chúng ta chuyển qua method ping. Method này xảy ra ở ranh giới của mỗi epoch. Mỗi khi chuyển đổi epoch có nghĩa là một tập hợp các validator đã hoạt động trong epoch trên NEAR Protocol. Validators thì không thay đổi, vì vậy kể cả khi ai đó bị kick ra ngoài thì họ không hoạt động trong các epoch tiếp theo. Chúng ta xác minh rằng hàm không có kết quả qua self.result.is_none(). Sau đó, chúng ta lấy chiều dài của epoch hiện tại, method tạo số nguyên luôn tăng, và sau đó chúng ta xác thực nó không khớp với chiều dài của epoch trước đó. Vì vậy, ta bắt đầu với chiều dài epoch bằng 0, mỗi lần ping được gọi, nó lại update vào epoch. Vậy nên mỗi khi epoch được đổi, nó có thể qua một hoặc 5 epoch không thành vấn đề. Tất cả những gì chúng ta sẽ làm là kiểm tra tất cả những phiếu trước đó đã vote cho phase 2. Bạn reset tổng stake bằng 0, và đây là cách cuối cùng của việc lấy tất cả data ra khỏi một map mới và đặt nó về mặc định. Vì vậy về cơ bản chúng ta đã có một bản copy của map từ cấu trúc bên trong và bây giờ nó chứa một hashmap rỗng. Đây là một thứ mới được giới thiệu gần đây trên Rust, được gọi là default hashmap, đơn giản là chứa một map trống. Nó cung cấp cho ta dữ liệu từ cấu trúc và giờ cấu trúc của contract chứa một map trống. Việc này giúp chúng ta có thể sửa đổi map hiện tại hoặc map mới dựa trên data có sẵn của map cũ trong một lần. Cơ bản là ta chỉ cần gọi tới account_id, vì không cần phải biết số stake trước đó là bao nhiêu. Vì vậy tiếp theo ta sẽ kiểm tra nếu có người đã bị xóa khỏi map (Line 9) hoặc bị xóa khỏi active validators. Ta lấy số stake nếu stake của validator là dương, cộng vào total stake và giữ vote của họ, nếu như họ bị kick ra thì họ sẽ phải vote lại. Cơ bản là ta update total stake bằng cách tính toán lại số total stake mới. 

Sau đó ta lưu tất cả kết quả trở lại vào contract stake, cuối cùng ta làm 2 việc.

Check kết quả sau khi chuyển đổi epoch, nếu các validators có được nhiều stake hơn hoặc total stake bị giảm xuống thì kết quả có thể đã xảy ra. Chúng ta nhớ được đã thực hiện việc này ở epoch nào nên lần tới khi có người gọi tới ping hoặc vote, ta không cần phải thực hiện lại nữa nếu epoch chưa bị chuyển.

Phần cuối

Đây là voting contract. Mỗi khi ai đó gọi hàm vote, đầu tiên chúng ta sẽ cập nhật data từ các validator, sau đó ghi vào cấu trúc nội bộ (internal structure). Quan trọng nhất, ta update total_voted_stake sau đó call check_result hai lần trong một lần gọi. Quan trọng là dù ta có thể gọi ping như một method, nhưng ta có thể gọi trực tiếp ping qua transaction. Không cần phải reload, chỉ cần call ping.

Có một loạt các view call ở đây, hàm đầu tiên chỉ đơn giản là trả về kết quả, hàm thứ 2 trả về lượng stake trong pool, hàm thứ 3 thì trả về tổng số votes. Cơ bản là lặp qua map rồi sau đó kết hợp nó lại thành kết quả mong muốn. Ngoài ra còn có unit test trong code, đây chỉ là cách thực hiện trong Rust và unit test còn có thể xác thực lại logic trong contract bằng cách giả mạo các ngữ cảnh (mocking context) và làm các thứ khác nữa.

Generate comment with AI 2 nL
338

Leave a Comment


To leave a comment you should to:


Scroll to Top
Report a bug👀