MỚI: Helius mua lại Light Protocol
Máy ảo Solana (SVM) là gì?
Blog/Kiến thức nền tảng

Máy ảo Solana (SVM) là gì?

Developer Experience Engineer0xIchigo trên X0xIchigo trên LinkedIn0xIchigo trên GitHub
Đọc trong 67 phút

Xin chân thành cảm ơn Lostin, Alessandro, Brian, Brady và Daniel Cumming đã đánh giá các phiên bản trước của bài viết này. 

Những điểm chính có thể áp dụng

  • SVM bao hàm toàn bộ ngăn xếp thực thi giao dịch, không giống EVM—thuật ngữ chỉ rõ ràng một trình thực thi bytecode.
  • Yêu cầu giao dịch khai báo trước khi thực thi những tài khoản sẽ truy cập giúp mở ra khả năng thực thi song song trên các lõi CPU và thị trường phí cục bộ.
  • Mã nguồn Rust được biên dịch qua rustc thành LLVM IR, sau đó backend eBPF của LLVM—cụ thể là bản fork sBPF của Solana—hạ cấp mã thành bytecode sBPF. Điều này có nghĩa là mọi ngôn ngữ có frontend LLVM (ví dụ: C, C++, Zig) đều có thể dùng để viết chương trình Solana.
  • sBPF là bản fork eBPF của Linux do Solana phát triển. Về cơ bản, hai hệ thống gần như giống nhau, ngoại trừ một số phần bổ sung có tính lịch sử. Tuy nhiên, các phần bổ sung này dự kiến sẽ bị hoàn tác. Khác biệt đáng kể duy nhất là các hàm eBPF thượng nguồn có tối đa 5 đối số, trong khi bản fork của Solana có thể có nhiều hơn.
  • Bytecode sBPF đã biên dịch được lưu trong các tệp ELF chứa những phần dành cho lệnh và hằng số, cùng các bảng tái định vị. Trình liên kết phân giải tham chiếu syscall thành các hàm băm Murmur3 32 bit tất định và viết lại các hàm nội bộ thành bước nhảy tương đối để bảo đảm tính di động. 
  • BPF Loader Upgradeable sử dụng mô hình hai tài khoản để nâng cấp tại chỗ và triển khai, trong khi Loader V4 sắp ra mắt đơn giản hóa mô hình này thành một tài khoản duy nhất với tùy chọn nén. Toàn bộ bytecode được xác minh tĩnh trước khi được đánh dấu là có thể thực thi.
  • Giao dịch chứa một mảng địa chỉ tài khoản kèm quyền đọc và ghi, các lệnh và chữ ký. Định dạng có cấu trúc này cho phép phát hiện xung đột và lập lịch song song cho các giao dịch không xung đột.
  • Banking Stage của TPU chịu trách nhiệm lập lịch song song cho các giao dịch không xung đột. Bank tải trạng thái tài khoản từ AccountsDB. BPF Loader cấp phát các VM sBPF biệt lập với vùng bộ nhớ và ngân sách tính toán có giới hạn. Các thay đổi trạng thái thành công được commit nguyên tử, còn khi thất bại thì được hoàn tác hoàn toàn.
  • SVM ISA là đặc tả chính thức duy nhất—ngoài sách trắng của Alpenglow và loạt bài viết ban đầu của Toly, không có một “đặc tả SVM” duy nhất. Runtime hình thành từ sự tương tác giữa Bank, trình lập lịch, BPF Loader và chính VM sBPF.

Giới thiệu

Máy ảo Solana (SVM) là một trong những hệ thống bị hiểu sai nhiều nhất trong blockchain hiện nay. Không giống Máy ảo Ethereum (EVM), thuật ngữ chỉ rõ ràng một trình thực thi opcode, SVM bao hàm toàn bộ quy trình thực thi giao dịch, từ trình lập lịch Banking Stage đến chính trình thông dịch bytecode sBPF. Sự mơ hồ này phản ánh khác biệt về kiến trúc của Solana: không có đặc tả truyền thống nào định nghĩa riêng “SVM”. Đặc tả gần nhất là Kiến trúc Tập lệnh Máy ảo Solana (SVM ISA), mô tả cách bytecode sBPF phải được thực thi nhưng không đề cập đến runtime rộng hơn.

Bài viết này hướng đến việc trở thành tài liệu tham khảo toàn diện về SVM là gì, cách nó hoạt động và lý do nó khác biệt về căn bản dưới góc nhìn từ triển khai trình xác thực Agave của Anza. Việc tìm hiểu cách ứng dụng khách Firedancer vận hành cũng như triển khai máy ảo tùy chỉnh tuân thủ SVM ISA của họ nằm ngoài phạm vi bài viết.

Chúng tôi muốn khảo sát cơ sở mã thực tế thay vì một đặc tả trừu tượng. Bài viết lần theo toàn bộ quy trình thực thi—cách mã nguồn Rust được biên dịch qua LLVM thành bytecode sBPF, cách chương trình được triển khai và xác minh, cách runtime cấp phát môi trường thực thi biệt lập để thực thi song song và cách giao dịch tương tác với bytecode đã triển khai.

Một vài phần đầu cung cấp bối cảnh cho sự mơ hồ của SVM và trình bày tổng quan cấp cao về cách hệ thống hoạt động. Phần còn lại dành cho độc giả có chuyên môn kỹ thuật muốn hiểu thấu đáo lớp thực thi của Solana.

Một định nghĩa gây tranh luận

Thuật ngữ “Máy ảo Solana” (SVM) đã làm dấy lên nhiều tranh luận trong cộng đồng, đặc biệt kể từ khi xuất hiện các phần mở rộng mạng và những blockchain lớp khác được xây dựng trên Solana. Tranh cãi bắt nguồn từ phạm vi của thuật ngữ: SVM chỉ là trình thông dịch sBPF cấp thấp hay bao hàm toàn bộ ngăn xếp thực thi giao dịch? 

Quan điểm hẹp coi SVM tương tự một máy ảo (VM) truyền thống, chẳng hạn trình thực thi opcode của EVM. Cụ thể hơn, đó là máy ảo bắt nguồn từ eBPF (rBPF, nay là sBPF) có nhiệm vụ thông dịch và biên dịch JIT bytecode. Quan điểm này nhấn mạnh SVM là một trình thực thi dựa trên thanh ghi, chạy trong sandbox và xử lý các lệnh như phép toán ALU hoặc lời gọi hệ thống dành riêng cho Solana. Về bản chất, SVM lấy cảm hứng từ mô hình an toàn của eBPF trên Linux nhưng được tùy chỉnh cho hạ tầng blockchain. Điều này phù hợp với những cụm từ như SVM ISA (Kiến trúc Tập lệnh) trong mã trình xác thực, nơi SVM chỉ là lớp VM.

Quan điểm rộng định nghĩa SVM là toàn bộ lớp thực thi giao dịch của một trình xác thực Solana. Lớp này không chỉ chạy bytecode mà còn bao hàm các thành phần thượng nguồn như trình lập lịch của Banking Stage, lập ngân sách đơn vị tính toán và cập nhật trạng thái qua cơ sở dữ liệu tài khoản, thường được gọi là AccountsDB. Đây là “runtime” biến các giao dịch thô thành những thay đổi trạng thái đã được xác thực.

Sự mơ hồ xuất hiện vì các thông tin chính thức từ Solana sử dụng hai thuật ngữ “runtime” và “SVM” thay thế cho nhau mà không có một định nghĩa thống nhất. Anza đã mang lại sự rõ ràng rất cần thiết cho cuộc tranh luận, xác nhận rõ điều này đồng thời thúc đẩy góc nhìn thực dụng, hướng đến hành động và dựa trên kỹ thuật. Cách họ mô tả SVM là runtime do Bank điều khiển, có nhiệm vụ cấp phát VM eBPF, mang lại góc nhìn rộng hơn nhiều và bao gồm toàn bộ quy trình, từ đó có thể hình thành một định nghĩa phù hợp về SVM.

Điều này được chính thức hóa trong đặc tả SVM chính thức của Anza, trong đó định nghĩa SVM là “các thành phần chịu trách nhiệm thực thi giao dịch”, được đóng gói thành thư viện độc lập dành cho trình xác thực, bằng chứng gian lận, sidecar và nhiều mục đích khác.

Trong phạm vi bài viết này, chúng ta có thể định nghĩa Máy ảo Solana là:

Giao diện runtime tách rời và quy trình xử lý giao dịch trong các trình xác thực Solana, do thành phần Bank điều khiển, điều phối việc thực thi song song các lệnh và chương trình on-chain, đồng thời cấp phát một máy ảo tùy chỉnh dựa trên eBPF để thông dịch bytecode an toàn, biên dịch JIT và đo lường tài nguyên.

Tổng quan về SVM

Máy ảo Solana (SVM) đóng vai trò là môi trường thực thi để xử lý các giao dịch tương tác với chương trình on-chain trên toàn mạng. Đây là lớp runtime nơi mã gặp trạng thái—môi trường thực thi biến các giao dịch được ký bằng mật mã thành những thay đổi trạng thái đã được xác thực. 

Để thực sự hiểu SVM, trước tiên chúng ta phải hiểu máy ảo có ý nghĩa gì trong bối cảnh blockchain.

Máy ảo

Máy ảo (VM) là phần mềm ảo hóa hoặc mô phỏng một hệ thống máy tính, cung cấp môi trường thực thi biệt lập hoạt động như phần cứng vật lý. Khái niệm này bắt nguồn từ công trình của IBM về hệ thống máy tính lớn trong thập niên 1960, cho phép nhiều người dùng chạy các hệ điều hành khác nhau trên cùng một máy vật lý. Có hai loại VM chính: VM hệ thống và VM tiến trình. Loại đầu thay thế cho một máy thật, còn loại sau được thiết kế để thực thi chương trình trong môi trường độc lập với nền tảng. Trong phạm vi bài viết, chúng ta quan tâm đến máy ảo hệ thống và từ đây sẽ gọi chúng là “máy ảo” hoặc đơn giản là “VM”.

Máy ảo giải quyết một số vấn đề nền tảng. Trước hết, chúng cung cấp một lớp trừu tượng cho phần cứng. Nghĩa là chương trình viết cho một VM có thể chạy trên mọi phần cứng vật lý hỗ trợ VM đó mà không cần viết lại. Triết lý “viết một lần, chạy ở mọi nơi” của Java là ví dụ điển hình: bytecode Java chạy giống hệt nhau trên Windows, macOS, Linux và các hệ thống khác đã cài Máy ảo Java (JVM).

VM cũng bảo đảm tính biệt lập và bảo mật. Mỗi phiên bản VM hoạt động trong một sandbox, nghĩa là không thể truy cập tài nguyên của hệ thống máy chủ hoặc VM khác nếu không được cho phép rõ ràng. Vì vậy, nếu một chương trình gặp sự cố hoặc chứa mã độc, thiệt hại sẽ chỉ giới hạn trong phiên bản VM đó. Nguyên tắc biệt lập này là lý do các nhà cung cấp đám mây như Google Cloud và AWS sử dụng VM để tách biệt khối lượng công việc của khách hàng.

VM cũng tạo ra đầu ra có thể dự đoán. Điều này có nghĩa là VM cung cấp một môi trường được kiểm soát, nơi cùng một đầu vào luôn tạo ra cùng một đầu ra, bất kể phần cứng bên dưới. Khả năng dự đoán này rất quan trọng cho việc gỡ lỗi, kiểm thử và đạt đồng thuận giữa các hệ thống phân tán.

VM cũng có thể đạt hiệu năng cực cao. VM hiện đại sử dụng biên dịch Just-In-Time (JIT) để giảm thiểu chi phí hiệu năng. Biên dịch JIT dịch bytecode VM thành mã máy nguyên bản trong thời gian chạy để đạt hiệu năng gần tương đương mã nguyên bản, đồng thời duy trì tính di động và các bảo đảm bảo mật đã nêu.

Máy ảo trong blockchain

Blockchain đã điều chỉnh khái niệm VM để giải quyết một thách thức đặc thù: làm thế nào hàng nghìn máy tính độc lập trên khắp thế giới có thể thực thi mã không đáng tin cậy và đi đến kết quả giống hệt nhau? VM đóng vai trò là môi trường runtime tất định để thực thi hợp đồng thông minh (tức các chương trình trên Solana) và quản lý trạng thái mạng (tức trạng thái hiện tại của mọi tài khoản, số dư và dữ liệu khác trên toàn mạng).

Khi một giao dịch được gửi đến blockchain, VM chịu trách nhiệm:

  • Tải dữ liệu tài khoản cần thiết từ bộ nhớ lưu trữ.
  • Thực thi bytecode chương trình được chỉ định trong giao dịch.
  • Đo lường mức tiêu thụ tài nguyên để ngăn vòng lặp vô hạn hoặc các cuộc tấn công từ chối dịch vụ (DoS).
  • Xác thực rằng mọi thay đổi trạng thái đều tuân theo các quy tắc đồng thuận được xác định trước của mạng.
  • Commit trạng thái đã cập nhật trở lại bộ nhớ lưu trữ vĩnh viễn (tức sổ cái).

Các quy tắc cụ thể về cách diễn ra quá trình chuyển đổi trạng thái được xác định bởi kiến trúc tập lệnh và các ràng buộc runtime của VM.

Cách SVM hoạt động

SVM là một quy trình gồm nhiều hệ thống con phối hợp để thực thi giao dịch an toàn và hiệu quả. Bank điều phối việc thực thi cho một slot cụ thể, quản lý trạng thái tài khoản, thực thi các quy tắc đồng thuận và phối hợp giữa Banking Stage với bộ nhớ lưu trữ bền vững (tức AccountsDB). Mỗi Bank đại diện cho trạng thái của tất cả tài khoản tại một slot cụ thể và trải qua ba vòng đời: đang hoạt động (tức mở cho giao dịch mới), bị đóng băng (tức không mở cho giao dịch mới vì slot đã hoàn tất) và đã root (tức thuộc chuỗi chính thức).

Banking Stage là nơi diễn ra quá trình thực thi giao dịch trong Transaction Processing Unit (TPU) của trình xác thực. Thành phần này nhận các giao dịch đã xác minh từ giai đoạn SigVerify, đưa chúng vào bộ đệm và lập lịch thực thi song song bằng cách phát hiện xung đột trên khóa tài khoản. Các luồng worker trong Banking Stage xử lý những lô giao dịch không xung đột, gọi các phương thức thực thi của Bank để tải tài khoản, cấp phát phiên bản VM sBPF cho từng lệnh, thực thi bytecode chương trình và thu thập kết quả. Banking Stage tiếp tục xử lý các lô giao dịch không xung đột cho đến khi Bank bị đóng băng tại ranh giới slot. Lưu ý rằng lô khác với entry—đơn vị giao dịch được ghi vào sổ cái để sao chép và đồng thuận.

BPF Loader quản lý vòng đời chương trình: triển khai, biên dịch JIT, nâng cấp và thực thi. Khi một lệnh nhắm đến một chương trình nhất định, một VM sBPF được cấp phát với các vùng bộ nhớ và ngân sách tính toán riêng, sau đó quyền thực thi được chuyển cho bytecode của chương trình.

VM sBPF là môi trường thực thi trong sandbox, nơi bytecode chương trình thực sự chạy. Nó bắt nguồn từ eBPF của Linux và sử dụng kiến trúc dựa trên thanh ghi với 11 thanh ghi đa dụng. VM thực thi tính biệt lập bộ nhớ thông qua năm vùng bộ nhớ riêng biệt, mỗi vùng có giới hạn và quyền rõ ràng. VM cũng đo lường mức tiêu thụ đơn vị tính toán để ngăn thực thi mất kiểm soát, đồng thời điều phối các lời gọi hệ thống cho những thao tác đặc quyền như mật mã học, ghi log hoặc Gọi Chéo Chương trình (CPI).

AccountsDB là lớp trạng thái bền vững nơi lưu trữ toàn bộ dữ liệu tài khoản. Trạng thái tài khoản được tải vào trước khi thực thi, tận dụng bộ nhớ đệm để tránh đọc đĩa nhiều lần đối với các tài khoản thường xuyên được truy cập. Sau khi thực thi thành công, các cập nhật được commit trở lại AccountsDB. Nếu thực thi thất bại, mọi thay đổi trạng thái đều được hoàn tác nguyên tử.

Cùng nhau, các thành phần này tạo thành SVM, một công cụ thực thi tách rời và có thể tái sử dụng.

Điều làm nên sự đặc biệt của SVM: Khai báo trước tài khoản

Quyết định kiến trúc mang tính định hình của SVM là mọi giao dịch phải khai báo rõ những tài khoản sẽ đọc và ghi trước khi bắt đầu thực thi. Yêu cầu đơn giản này được tích hợp vào chính định dạng giao dịch và mở ra hai khả năng mang tính chuyển đổi giúp Solana trở nên khác biệt: thực thi song song và thị trường phí cục bộ.

Thực thi song song (Sealevel)

Không giống Máy ảo Ethereum (EVM), vốn xử lý giao dịch tuần tự—từng giao dịch một và chờ giao dịch hiện tại hoàn tất trước khi chuyển sang giao dịch tiếp theo—SVM cho phép mở rộng theo chiều ngang bằng cách thực thi đồng thời nhiều giao dịch trên nhiều lõi CPU. Khả năng song song hóa này có được vì tất cả giao dịch Solana đều khai báo rõ những tài khoản sẽ đọc và ghi trước khi bắt đầu thực thi. 

Việc khai báo những tài khoản mà giao dịch sẽ đọc và ghi cho phép runtime phân tích quan hệ phụ thuộc giữa các tài khoản để phát hiện xung đột và lập lịch cho các giao dịch không xung đột:

  • Các giao dịch tác động đến những tài khoản hoàn toàn khác nhau có thể chạy song song mà không phát sinh chi phí điều phối.
  • Các giao dịch chỉ đọc từ cùng một tài khoản cũng có thể chạy song song vì thao tác đọc không xung đột.
  • Các giao dịch cố ghi vào cùng một tài khoản sẽ chạy tuần tự để ngăn điều kiện tranh chấp và bảo đảm tính nhất quán của trạng thái. 

Thị trường phí cục bộ

Vì runtime biết chính xác từng giao dịch sẽ truy cập tài khoản nào trước khi thực thi, phí có thể được giới hạn cục bộ ở những tài khoản cụ thể thay vì cạnh tranh trên toàn mạng. Khái niệm này được gọi là thị trường phí cục bộ. 

Trên Ethereum và các chuỗi EVM khác, mọi giao dịch đều cạnh tranh trong một thị trường phí toàn cầu duy nhất—gửi ETH cho bạn bè, đúc NFT hay giao dịch trên Uniswap đều cạnh tranh với nhau để giành cùng một không gian khối. Hoạt động tăng vọt ở một lĩnh vực sẽ đẩy phí lên cho tất cả mọi người, bất kể họ đang cố thực hiện một việc hoàn toàn khác. 

Trên Solana, chỉ những giao dịch truy cập cùng tài khoản mới cạnh tranh với nhau. Người dùng chuyển SOL giữa hai tài khoản không cần lo lắng về một đợt đúc NFT phổ biến diễn ra đồng thời. Phí ưu tiên của giao dịch chỉ được xác định bởi mức độ tranh chấp tài khoản. Tính cục bộ này là lý do giao dịch Solana vẫn có chi phí thấp ngay cả trong giai đoạn hoạt động cao.

Chẳng hạn, vào ngày 10 tháng 10, thị trường tiền mã hóa đã trải qua sự kiện thanh lý lớn nhất từ trước đến nay. Bất chấp mức tăng hoạt động kỷ lục, giao dịch Solana vẫn tương đối rẻ: phí giao dịch trung vị đạt $0.007, phí trung bình trong thời gian ngắn đạt $0.10 và 1% giao dịch có phí cao nhất chỉ đạt đỉnh hơn $1.00. Trong cùng khoảng thời gian, phí trung vị của cả Ethereum và Arbitrum đều tăng vọt lên trên $100, còn phí của Base đạt đỉnh hơn $3.

Một sự chuyển đổi mô hình

Khía cạnhEVMSVM
Kiến trúcVM dựa trên ngăn xếpVM dựa trên thanh ghi (bắt nguồn từ eBPF)
Thực thiTuần tựSong song (phát hiện xung đột)
Thị trường phíToàn cầuCục bộ (tranh chấp theo tài khoản)
Khai báo tài khoảnKhông cần khai báo trướcPhải khai báo trước
ISA~140 opcode, thao tác ngăn xếp~100 opcode, thanh ghi kiểu RISC
Biên dịch JITTùy chọn (phụ thuộc ứng dụng khách)Tiêu chuẩn (hiệu năng nguyên bản)
Mô hình trạng tháiPhí lưu trữ hợp đồngCơ sở dữ liệu tài khoản phẳng
Ngôn ngữSolidity/Vyper → bytecode EVMRust/C/C++ → LLVM → sBPF

SVM đại diện cho một cách tiếp cận khác biệt về căn bản đối với việc thực thi blockchain. Bitcoin giới thiệu tiền có thể lập trình. Ethereum giới thiệu hợp đồng thông minh đa dụng và khả năng thực thi on-chain tùy ý. Tuy nhiên, cả hai đều bị giới hạn bởi thực thi tuần tự và thị trường phí toàn cầu—những quyết định kiến trúc đặt ra giới hạn căn bản cho cả thông lượng lẫn chi phí.

SVM tách khỏi các ràng buộc truyền thống, mang đến một mạng có thể xử lý thông lượng cao mà không phải hy sinh khả năng lập trình hoặc buộc người dùng tham gia những cuộc đấu giá phí đắt đỏ quá mức. Quyết định yêu cầu khai báo trước tài khoản tuy đơn giản nhưng mạnh mẽ, vì nó cho phép thực thi song song trên các lõi CPU và cục bộ hóa phí theo thị trường cấp tài khoản.

Tất nhiên, đây không phải là những tối ưu hóa duy nhất Solana cung cấp so với các blockchain khác. Phương châm Tăng Băng thông, Giảm Độ trễ của Solana cùng sự tập trung cao độ vào việc hiện thực hóa giấc mơ Thị trường Vốn Internet đã tạo ra nhiều tối ưu hóa hiệu năng, lựa chọn thiết kế và phương thức triển khai để xây dựng một mạng có thông lượng cao.

Phần còn lại của bài viết khám phá chính xác cách hệ thống này hoạt động—cách mã nguồn Rust được biên dịch thành bytecode, cách bytecode đó được triển khai và xác minh, cũng như cách runtime cấp phát các môi trường thực thi biệt lập để chạy song song hàng nghìn chương trình một cách an toàn trong khi vẫn duy trì nghiêm ngặt tính tất định và các bảo đảm bảo mật. 

Từ mã nguồn Rust đến bytecode sBPF: Quy trình biên dịch

Rust

Rust là ngôn ngữ chung trong hoạt động phát triển chương trình Solana. Các framework như Anchor cung cấp cho nhà phát triển một phương pháp mạnh mẽ, có định hướng rõ ràng để xây dựng chương trình an toàn và hiệu quả. solana_program được thiết kế làm thư viện nền tảng cho mọi chương trình on-chain. Gần đây, Pinocchio, một thư viện không có phần phụ thuộc và được tối ưu hóa cao, đã trở thành lựa chọn ưu tiên cho các nhà phát triển muốn xây dựng chương trình Solana nguyên bản. 

Bất kể framework hay thư viện được sử dụng, mọi chương trình đều có một entrypoint mà runtime sẽ gọi khi chương trình được kích hoạt. Macro entrypoint của solana_program tạo mã mẫu tiêu chuẩn cần thiết để bắt đầu thực thi chương trình. Cụ thể là giải tuần tự hóa đầu vào, thiết lập trình cấp phát toàn cục và trình xử lý panic. Pinocchio xuất các macro entrypoint hoạt động tương tự nhưng tách entrypoint khỏi quá trình thiết lập trình cấp phát heap và trình xử lý panic, mang đến cho nhà phát triển nhiều lựa chọn hơn. 

Cấu trúc cơ bản của chương trình 

Chương trình là một loại tài khoản có khả năng chạy mã. Cụ thể hơn, chương trình là một tài khoản thực thi lưu trữ một khối bytecode sBPF trong tài khoản do BPF Loader sở hữu, với một khóa công khai duy nhất. Theo thiết kế, chương trình không có trạng thái: mọi dữ liệu bền vững nằm trong các tài khoản riêng biệt mà chương trình có thể đọc hoặc ghi khi được gọi.

SVM yêu cầu mọi chương trình phải có một khung cụ thể—một entrypoint chấp nhận ba đầu vào:

  • Program ID: Địa chỉ của chính chương trình, dùng cho các bước kiểm tra tự tham chiếu (ví dụ: quyền sở hữu).
  • Accounts: Một mảng siêu dữ liệu tài khoản (tức pubkey, số dư lamport, bộ đệm dữ liệu, chủ sở hữu và cờ). Đây là “trạng thái” mà chương trình cần đọc và ghi.
  • Instruction Data: Một lát byte chứa dữ liệu tùy ý từ giao dịch.

Chương trình phải xử lý các đầu vào này qua entrypoint, thay đổi những tài khoản có thể ghi liên quan, phát log hoặc sự kiện, rồi trả về trạng thái thành công cho biết liệu chương trình có thực hiện thành công tất cả các thao tác hay không. Tất cả quy về một hàm process_instruction.

Một chương trình đơn giản được viết bằng Rust với crate solana_program có dạng:

simple_rust_program.rs
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, Solana!");

    Ok(())
}

Bên dưới, tất cả đều được xác định bởi Giao diện Nhị phân Ứng dụng (ABI) của SVM, nội dung mà chúng ta sẽ tìm hiểu sau.

Trình biên dịch Rust và LLVM IR

Rust, giống nhiều ngôn ngữ lập trình khác, là một lớp trừu tượng bậc hai được xây dựng trên hợp ngữ. Nó được thiết kế để con người viết mã an toàn, đồng thời và dễ đọc mà không cần quản lý chi li từng tương tác nhỏ với phần cứng. Tuy nhiên, máy tính không hiểu Rust hay bất kỳ ngôn ngữ bậc cao nào. 

Máy tính hiểu mã máy—các lệnh nhị phân được thiết kế riêng cho một kiến trúc hoặc máy ảo cụ thể. Cuối cùng, mọi chương trình đều được chuyển đổi thành mã nhị phân và chính máy tính thực hiện quá trình chuyển đổi này. Biên dịch là một quy trình dịch nhiều bước nhằm loại bỏ các lớp trừu tượng bậc cao, tối ưu hóa hiệu quả và xuất ra bytecode có thể thực thi.

rustc là trình biên dịch chính thức của Rust. Hầu hết nhà phát triển thường không tương tác trực tiếp với rustc mà gọi nó thông qua Cargo, trình quản lý gói của Rust. Tuy vậy, rustc đưa mã nguồn Rust qua ba giai đoạn chính trước khi tạo bytecode thực thi. Mỗi giai đoạn loại bỏ một lớp trừu tượng, thực thi các yêu cầu an toàn và chuẩn bị mã cho bước chuyển đổi tiếp theo:

  • Phân tích cú pháp và Mở rộng
  • MIR (Biểu diễn Trung gian Cấp giữa)
  • LLVM IR (Biểu diễn Trung gian Máy ảo Cấp thấp)

Phân tích cú pháp và Mở rộng

Trình biên dịch đọc một tệp Rust nhất định, được biểu thị bằng phần mở rộng .rs, dưới dạng văn bản thuần túy. Nó tìm kiếm các token cụ thể (ví dụ: use, fn, None, impl, &[u8]) trong một quy trình gọi là phân tích từ vựng. Token hóa từ vựng là quá trình chuyển đổi văn bản thành các token từ vựng có nghĩa thuộc một danh mục nhất định (ví dụ: định danh, toán tử, dấu phân cách, ký tự trực tiếp, từ khóa). 

rustc lấy các token từ vựng này và chuyển đổi chúng thành một cấu trúc dữ liệu gọi là Cây Cú pháp Trừu tượng (AST). Cấu trúc dạng cây này biểu diễn cấu trúc phân cấp lồng nhau của mã nguồn Rust. Nghĩa là hàm chứa khối, khối chứa biểu thức, biểu thức chứa toán tử, v.v. Dù vẫn ở cấp cao, AST cung cấp một biểu diễn trung thực về mã nguồn và logic nền tảng của mã.

Sau khi AST được dựng, trình biên dịch thực hiện một số phép chuyển đổi chính:

  • Mở rộng Macro: Các macro như entrypoint! và println! được mở rộng thành mã Rust thô để mọi macro được chuyển thành nút AST thông thường.
  • Hạ cấp: Cú pháp viết tắt cấp cao được thiết kế để giúp mã dễ đọc hơn sẽ được viết lại thành các dạng nguyên thủy hơn trong quy trình gọi là hạ cấp. Ví dụ, vòng lặp for được chuyển đổi thành loop với cơ chế lặp thủ công. Kết quả của quá trình hạ cấp là Biểu diễn Trung gian Cấp cao (HIR).
  • Kiểm tra mượn và Phân tích an toàn: Rust lấy HIR và thực hiện kiểm tra kiểu, phân giải trait và suy luận kiểu. Kết quả của quy trình này là Biểu diễn Trung gian Cấp cao có Kiểu (THIR).

Ở giai đoạn này, trình biên dịch xử lý mã unsafe. Mã unsafe cho phép nhà phát triển thực hiện các thao tác bỏ qua bảo đảm an toàn của Rust (ví dụ: giải tham chiếu con trỏ thô, gọi hàm ngoại, triển khai trait unsafe). Nó đóng vai trò là một lối thoát có chủ đích để kiểm soát cấp thấp, nới lỏng các quy tắc cụ thể trong khi vẫn yêu cầu mã phải biên dịch theo ngữ nghĩa của Rust. Điều này rất quan trọng đối với các chương trình Solana, nơi mã unsafe có thể được sử dụng hạn chế để cải thiện những thao tác đòi hỏi hiệu năng cao, chẳng hạn giải tuần tự hóa tài khoản zero-copy (ví dụ: trong struct bao bọc Account của Pinocchio).    

Mã unsafe lần đầu được nhận diện sau bước phân tích từ vựng trong quá trình dựng AST, khi các token unsafe được nhận biết và đánh dấu là những nút đặc biệt. Sau đó, mã được xử lý sau bước mở rộng AST trong quá trình kiểm tra kiểu và mượn. Trình biên dịch bảo đảm các thao tác unsafe chỉ nằm trong ngữ cảnh unsafe và báo lỗi nếu không (ví dụ: “không thể giải tham chiếu con trỏ thô bên ngoài unsafe”). Chẳng hạn, trình biên dịch không kiểm tra xem mã unsafe có làm hỏng hoặc quản lý sai bộ nhớ hay không.

Khi kết thúc giai đoạn này, AST đã mở rộng—nay là THIR—trở thành một biểu diễn đã được xác thực và hạ cấp của mã nguồn Rust.

MIR

THIR sau đó được hạ cấp thành Biểu diễn Trung gian Cấp giữa (MIR), một dạng lấy Rust làm trung tâm, biểu diễn mã nguồn dưới dạng đồ thị luồng điều khiển (CFG) đơn giản hóa. Mọi cú pháp tiện dụng và cấu trúc phức tạp dành riêng cho Rust (ví dụ: khớp mẫu, trait, closure) được biểu diễn bằng các khối cơ bản, bao gồm phép gán và nhánh. Các khối cơ bản này được kết nối bằng bước nhảy—cụ thể hơn là Goto—và các nhánh, giúp dễ dàng suy luận về luồng của chương trình. 

MIR không hoàn toàn bắt buộc. Trình biên dịch có thể hạ cấp trực tiếp THIR thành LLVM IR. Tuy nhiên, MIR cung cấp một lớp hiểu Rust, cho phép trình biên dịch thực thi các quy tắc dành riêng cho Rust và tối ưu hóa trước khi áp dụng các tối ưu hóa chung của LLVM. Điều này khiến MIR trở thành nơi lý tưởng cho những bước kiểm tra và chuyển đổi quá cấp cao đối với LLVM nhưng quá cấp thấp đối với THIR, bao gồm:

  • Kiểm tra mượn: Các bước kiểm tra ngữ nghĩa ban đầu diễn ra trong quá trình phân tích kiểu trên THIR. Tuy nhiên, CFG đơn giản hóa của MIR cho phép kiểm tra mượn đầy đủ và chính xác để thực thi mọi quy tắc về quyền sở hữu, mượn và vòng đời.
  • Kiểm tra Move và Drop: Trình biên dịch bảo đảm mọi giá trị được move và drop phù hợp với các bảo đảm an toàn bộ nhớ của Rust, ngăn lỗi use-after-free.
  • Phân tích khởi tạo: Trình biên dịch bảo đảm mọi biến đều được khởi tạo trước khi sử dụng.
  • Inline và Tối ưu hóa sớm: Trình biên dịch có thể inline các hàm nhỏ, đơn giản hóa biểu thức số học và loại bỏ mã không thể truy cập.

Ví dụ, trước đó chúng ta đã gọi msg!(“Hello, Solana!”) trong chương trình Rust mẫu. Đây là một macro được định nghĩa trong crate solana-program. Với các biểu thức đơn lẻ như chuỗi tĩnh, macro này mở rộng trong giai đoạn mở rộng macro để gọi trực tiếp sol_log($msg), trong đó $msg là biểu thức. Syscall sol_log nhận con trỏ đến dữ liệu chuỗi và độ dài của chuỗi, rồi ghi chuỗi vào đầu ra của SVM mà không phát sinh chi phí định dạng. Trong MIR, thao tác này có thể được đơn giản hóa thành:

Mã
bb0: {
  _0 = const "Hello, Solana!"; // Constant string allocation
  _1 = len(_0); // Compute length
  sol_log(move _0, move _1); // Syscall invocation with explicit moves for ownership
  return = Ok(());
}

Trong đó,

  • _0 = const “Hello, Solana!”;—MIR đưa vào các biến tạm thời (tức _0) cho giá trị trung gian. Chuỗi được xem là một lát hằng số và được cấp phát trong dữ liệu chỉ đọc.
  • _1 = len(_0);—MIR hiển thị một phép tính độ dài đơn giản trên lát để có thể gộp hằng số.
  • sol_log(move _0, move _1);—Lệnh gọi syscall được dịch thành một chuỗi lệnh sBPF để tải các thanh ghi và gọi ID syscall. Chúng ta sẽ phân tích chính xác ý nghĩa của điều này trong các phần sau, nhưng điểm quan trọng cần lưu ý là các thao tác move này gắn với ngữ nghĩa quyền sở hữu của Rust và thực thi ngữ nghĩa đó khi biên dịch.
  • Return = Ok(());—Kết thúc khối bằng một phần tử kết thúc, báo hiệu thành công cho SVM.

Việc tập trung vào ngữ nghĩa Rust khiến MIR trở thành nơi lý tưởng để phát hiện điểm kém hiệu quả và gỡ lỗi những mẫu tiêu tốn nhiều CU. Chẳng hạn, nếu log chứa các chuỗi động, MIR có thể xác định những phép cấp phát hoặc vòng lặp bổ sung có thể được tối ưu hóa. Nhà phát triển có thể dùng lệnh cargo rustc -- -Z dump-mir=all để xuất MIR.

MIR bảo đảm mã hợp lệ về ngữ nghĩa, được tối ưu hóa và loại bỏ mọi quy tắc dành riêng cho Rust trước khi cuối cùng hạ cấp thành LLVM IR.

Lưu ý rằng quá trình này thường được gọi là giai đoạn Sinh Mã. Nó không nhất thiết phải là LLVM. Tuy nhiên, LLVM rất phổ biến và là thứ hầu hết mọi người nghĩ đến khi nhắc tới quá trình sinh mã Rust. Trình biên dịch Rust cũng đi kèm các backend GCC và Cranelift, lần lượt tạo ra GIMPLE và CLIF. Chúng ta tập trung vào LLVM IR trong bối cảnh Solana, nhưng lưu ý rằng điều này không phải lúc nào cũng đúng với Rust nói chung.

LLVM IR

LLVM, ban đầu có nghĩa là "Low Level Virtual Machine", là một framework trình biên dịch dạng mô-đun. Thay vì là một trình biên dịch duy nhất, LLVM là bộ công cụ gồm các thành phần có thể tái sử dụng để xây dựng trình biên dịch, trình tối ưu hóa và trình sinh mã. Nhiều ngôn ngữ (ví dụ: Rust, C, C++, Julia, Swift, Brainfuck, Zig) sử dụng LLVM để tận dụng khả năng nhắm đến nhiều kiến trúc khác nhau, từ CPU x86 đến ISA ảo. 

rustc dịch MIR thành LLVM IR (Biểu diễn Trung gian Máy ảo Cấp thấp)—cầu nối giữa ngữ nghĩa Rust và bytecode cuối cùng được triển khai trên Solana. Nó gần với mã máy hơn nhiều, với các thao tác cấp phát bộ nhớ rõ ràng (tức alloca), lưu, tải và gọi hàm. Nó không có khái niệm về quyền sở hữu, vòng đời hoặc trait vì các lớp trừu tượng Rust đó đã được mở rộng và không còn tồn tại—các bảo đảm từ những vòng trước vẫn được duy trì trong các bước tiếp theo.

Nhiều phép tối ưu hóa được áp dụng ở giai đoạn này, bao gồm:

  • Gộp Hằng số (tức đánh giá hằng số tại thời điểm biên dịch).
  • Inline (tức thay thế lời gọi bằng phần thân của hàm).
  • Loại bỏ Mã chết (tức xóa các lệnh không ảnh hưởng đến kết quả).
  • Trải vòng lặp và Vector hóa (tức viết lại vòng lặp để thực thi nhanh hơn).

Do đó, LLVM cung cấp cho chúng ta:

  • LLVM IR: Một định dạng trung gian có tính di động, tương tự hợp ngữ.
  • Các lượt tối ưu hóa: Để tạo LLVM IR, LLVM sử dụng dạng Gán Tĩnh Đơn (SSA), bảo đảm mỗi biến chỉ được gán đúng một lần, qua đó cho phép các tối ưu hóa như inline và loại bỏ mã chết.
  • Trình sinh mã: Các đích hạ cấp LLVM IR thành mã máy thực tế (tức x86_64, ARM, WebAssembly, eBPF).

Các chương trình Rust thường được biên dịch cho những đích phần cứng như x86_64 hoặc ARM. Tuy nhiên, chương trình Solana không chạy trực tiếp trên phần cứng. Thay vào đó, chúng chạy bên trong Máy ảo Solana. Vì vậy, backend LLVM hạ cấp LLVM IR thành bytecode BPF; trên Solana, nó trở thành bytecode sBPF (tức một bản fork của eBPF loại bỏ các tính năng không tất định và thêm những syscall dành riêng cho Solana).

Lưu ý rằng dù Rust là ngôn ngữ chung trong hoạt động phát triển chương trình Solana, mọi ngôn ngữ có thể nhắm đến backend BPF của LLVM (ví dụ: C, Nim, Swift, Zig) đều có thể được sử dụng.

eBPF

LLVM IR được hạ cấp thành eBPF, ISA dựa trên thanh ghi tạo thành nền tảng cho runtime của Solana. eBPF (Extended Berkeley Packet Filter) bắt nguồn từ Berkeley Packet Filter (BPF), được Steven McCanne và Van Jacobson phát triển vào năm 1992 tại Lawrence Berkeley Laboratory cho các hệ thống Unix Berkeley Software Distribution (BSD). Về bản chất, BPF là một điểm chạm mạng và bộ lọc gói tin, cho phép thu thập và lọc các gói mạng ở cấp hệ điều hành mà không cần sao chép dữ liệu, bằng cách tận dụng các bộ định tính. 

Kể từ đó, eBPF đã phát triển (tức được mở rộng) thành một VM đa dụng, chạy trong sandbox bên trong nhân Linux. Khả năng mà nó mở ra tương tự những gì JavaScript mang lại cho phát triển web—một công cụ script an toàn dành cho nhân. eBPF cho phép nhà phát triển chạy các chương trình nhỏ đã được xác minh ngay trong nhân Linux với một tập lệnh bị giới hạn để thực hiện các tác vụ như giám sát hiệu năng, khả năng quan sát, bảo mật và kết nối mạng.

Điều này quan trọng vì nhà phát triển có được:

  • Thực thi trong Sandbox: Các chương trình eBPF chạy trong một máy ảo bị hạn chế bên trong nhân, nghĩa là chúng không thể làm sập hoặc hỏng bộ nhớ nhân.
  • Bảo đảm An toàn: Bytecode eBPF được xác minh tĩnh trước khi tải để bảo đảm không xảy ra truy cập bộ nhớ không hợp lệ, bước nhảy vượt giới hạn hoặc thao tác đặc quyền khác, mang lại độ an toàn mà không có chi phí runtime.
  • Hiệu quả: eBPF dựa trên thanh ghi (thay vì dựa trên ngăn xếp như EVM) và có thể được biên dịch JIT thành mã máy để đạt tốc độ gần tương đương mã nguyên bản nhờ thiết kế gọn nhẹ (tức không có chi phí của một hệ điều hành đầy đủ).
  • Tính linh hoạt: eBPF cung cấp các lời gọi hệ thống, còn được gọi là syscall, về cơ bản là các hook vào tính năng của nhân. Syscall có thể được mở rộng bằng những khả năng mới mà không cần thiết kế lại tập lệnh.

Solana cần một VM tất định, an toàn và hiệu năng cao để chạy các chương trình không đáng tin cậy trên toàn bộ tập hợp trình xác thực. eBPF cung cấp một mô hình an toàn đã được chứng minh, ISA di động và hiệu quả được thiết kế để chạy hàng nghìn chương trình gọn nhẹ, cùng khả năng hỗ trợ JIT để cải thiện hiệu năng. Vì vậy, thay vì phát minh một VM hoàn toàn mới, Solana đã fork eBPF và tạo ra sBPF.

sBPF 

Ban đầu, Solana Labs đã fork rBPF của Quentin Monnet để tạo phiên bản rBPF của Solana, bảo đảm mọi trình xác thực đều có một định dạng bytecode tạo ra kết quả hoàn toàn giống nhau khi thực thi một chương trình và đầu vào nhất định. 

Một bản fork của eBPF từng được cho là cần thiết vì cơ chế đồng thuận của Solana yêu cầu thực thi tất định và sử dụng tài nguyên có giới hạn. Mặc dù bản thân eBPF mang tính tất định, Solana cần thêm các bảo đảm và chức năng dành riêng cho blockchain:

  • Thời gian và chi phí lệnh cố định.
  • Thực thi trong không gian người dùng.
  • Một runtime tất định.

Đáng chú ý, nó được thiết kế để chạy trong không gian người dùng thay vì nhân, nhờ đó không cần đặc quyền hoặc sửa đổi nhân. Điều này cho phép triển khai trên nhiều môi trường hệ điều hành mà không cần quyền root hay mô-đun nhân tùy chỉnh. Không gian người dùng là lựa chọn thực tế cho Solana—mang lại tính di động, khả năng kiểm thử và triển khai dễ dàng hơn. Dù chạy trong không gian người dùng, JIT vẫn đạt hiệu năng gần tương đương mã nguyên bản. Hơn nữa, thực thi trong không gian người dùng cũng cho phép kiểm thử và fuzzing mà không cần quyền truy cập nhân.

rBPF không còn được sử dụng. Thay vào đó, khi Anza được thành lập, họ đã fork rBPF để tạo sBPF (Solana Berkeley Packet Filter). Kho GitHub rBPF thuộc sở hữu của Solana Labs đã được lưu trữ vào ngày 10 tháng 1 năm 2025.

SVM ISA

SVM ISA (Kiến trúc Tập lệnh Máy ảo Solana) là đặc tả cốt lõi xác định cách các VM tương thích với Solana (ví dụ: sBPF của Agave hoặc bản triển khai lại của Firedancer) phải thực thi chương trình. Nó không phải chính VM mà là tiêu chuẩn hoặc hợp đồng bảo đảm tính nhất quán và tuân thủ giao thức giữa nhiều triển khai SVM. Chính SVM ISA áp đặt các ràng buộc về an toàn và tính tất định này lên eBPF, loại bỏ các tính năng tập trung vào nhân trong khi bổ sung chức năng dành riêng cho blockchain.

ISA chi phối các thanh ghi, cách mã hóa lệnh, opcode, lớp, quy tắc xác minh, điều kiện panic và Giao diện Nhị phân Ứng dụng (ABI). Mọi thay đổi đối với SVM ISA phải được triển khai thông qua SIMD để hỗ trợ quá trình phát triển có kiểm soát của tập lệnh này, bảo đảm thực thi tất định trên các trình xác thực.

Thanh ghi

Thanh ghi là các ô lưu trữ cực nhỏ bên trong VM, giữ số hoặc địa chỉ trong khi lệnh chạy, tương tự biến hoặc hộp có nhãn trên bàn làm việc. SVM ISA định nghĩa kiến trúc thanh ghi 64 bit với 11 thanh ghi đa dụng (R0-R10) và một bộ đếm chương trình ẩn. Thanh ghi có độ rộng 64 bit cho số nguyên và địa chỉ, giúp xử lý hiệu quả các giá trị lớn hoặc con trỏ. R0 giữ giá trị trả về của hàm; R1-R5 truyền năm đối số hàm đầu tiên như các tham số; R6-R9 được hàm được gọi bảo toàn và duy trì qua các lần gọi hàm; còn R10 đóng vai trò là con trỏ khung chỉ đọc, đánh dấu khung ngăn xếp hiện tại. Bộ đếm chương trình ẩn theo dõi quá trình thực thi và chỉ ra lệnh tiếp theo cần thực thi.

Lệnh

Lệnh là một thao tác duy nhất mà VM biết cách thực hiện, chẳng hạn “cộng hai số này” hoặc “nhảy đến dòng mã này”. Các lệnh có thiết kế kiểu RISC với khoảng 100 opcode, so với hàng nghìn opcode trong kiến trúc CISC như x86. Chính điều này giúp quá trình xác minh nhanh và biên dịch JIT hiệu quả.

Lệnh được mã hóa dưới dạng giá trị 64 bit ở định dạng Little Endian với cấu trúc:

  • opcode: 8 bit
  • dst_reg: 4 bit
  • src_reg: 4 bit
  • offset: 16 bit (có dấu)
  • immediate: 32 bit (có dấu)

opcode cho biết cần làm gì, dst_reg cho biết kết quả đi đâu, src_reg cho biết đầu vào đến từ đâu, offset cho biết cần xem xét độ lệch bộ nhớ nào, còn immediate là một hằng số bổ sung có thể được đưa vào lệnh.

lddw, hay load double word, là lệnh rộng duy nhất chiếm hai slot 64 bit để hỗ trợ đầy đủ các giá trị tức thời 64 bit. 

Các lệnh được phân thành nhiều lớp, bao gồm thao tác bộ nhớ, thao tác số học hoặc logic, nhánh có điều kiện và không điều kiện, gọi và trả về hàm, cùng chuyển đổi Endianness.

Vùng bộ nhớ

ISA xác định năm vùng bộ nhớ, mỗi vùng có giới hạn rõ ràng (tức [addr, addr+len]) về nơi chương trình có thể đọc hoặc ghi:

  • Mã chương trình: chính các lệnh đã biên dịch (đọc + thực thi).
  • Ngăn xếp: không gian làm việc tạm thời cho các hàm (đọc + ghi, thường là 4KB cho mỗi khung).
  • Heap: bộ nhớ động mà chương trình có thể yêu cầu (đọc + ghi).
  • Dữ liệu đầu vào: các byte chỉ đọc được truyền vào cùng giao dịch.
  • Dữ liệu chỉ đọc: hằng số và giá trị bất biến.

Các chương trình có một sơ đồ bộ nhớ ảo được xác định trước: mã chương trình bắt đầu tại địa chỉ 0x000000000 hoặc 0x100000000 tùy phiên bản biên dịch, các khung ngăn xếp bắt đầu tại 0x200000000, heap bắt đầu tại 0x300000000 và dữ liệu đầu vào bắt đầu tại 0x400000000.

Trình xác minh

Trình xác minh thực hiện phân tích tĩnh trước khi thực thi—kiểm tra mọi đường dẫn mã có thể có mà không chạy chương trình—để bảo đảm an toàn tại thời điểm tải thay vì trong runtime. Quy trình này bao gồm việc kiểm tra:

  • Không có lệnh không xác định hoặc không được hỗ trợ.
  • Mọi đích nhảy đều nằm trên ranh giới lệnh hợp lệ và các bước nhảy ngược được xử lý
  • Không có đường dẫn mã không thể truy cập.
  • Các giới hạn độ sâu gọi hàm được thực thi.
  • Phép chia hoặc modulo cho không bị từ chối tĩnh.
  • Chương trình có giới hạn kích thước tối đa và phải nằm trong giới hạn đó.

Mặc dù hữu ích, trình xác minh không ngăn nhà phát triển tạo ra hành vi ngoài dự kiến. Nghĩa là nhà phát triển vẫn có thể gây ra các lỗi use-after-free và tràn bộ đệm, chẳng hạn trong một chương trình Solana. 

Điều kiện panic

Điều kiện panic là danh sách các trường hợp lỗi runtime do SVM ISA định nghĩa. Chúng bao gồm:

  • Lệnh không hợp lệ hoặc không được hỗ trợ.
  • Phép chia hoặc modulo cho không.
  • Truy cập bộ nhớ vượt giới hạn.
  • Truy cập bộ nhớ không hợp lệ đối với các vùng bộ nhớ khác nhau (tức vi phạm quyền).
  • Tràn ngăn xếp.
  • Vượt quá độ sâu gọi.
  • Vượt quá số lệnh tối đa được phép.
  • Chương trình trả về mã lỗi.

ABI

Giao diện Nhị phân Ứng dụng (ABI) là hợp đồng định dạng giữa một chương trình Solana và SVM. Trong khi phần Cấu trúc cơ bản của chương trình trước đó đã minh họa cách hoạt động trong Rust (tức process_instruction với ba đầu vào), ABI chỉ định cách các đầu vào và đầu ra đó được biểu diễn trong bộ nhớ, bảo đảm mọi trình xác thực đều có thể thực thi chương trình một cách tất định.

Ở cấp cao, ABI định nghĩa ba yếu tố: quy ước entrypoint, quy ước gọi và thanh ghi, cùng bố cục bộ nhớ.

Mọi chương trình Solana phải cung cấp một hàm entrypoint. Loader tuần tự hóa đầu vào chương trình vào không gian bộ nhớ của VM theo thứ tự chuẩn: program ID, mảng tài khoản và dữ liệu lệnh. Sau đó, VM truyền các con trỏ đến những vùng này vào entrypoint của chương trình.

Năm thanh ghi đầu tiên (tức R1-R5) được dành riêng cho các đối số entrypoint, còn thanh ghi trả về (tức R0) giữ mã thoát của chương trình. Mã thoát bằng không được xem là thành công, còn giá trị khác không là thất bại được ánh xạ tới một InstructionError cụ thể. Điều này bảo đảm mọi chương trình đều trả về mã trạng thái theo cách nhất quán. Ngoài ra, các tham số sau năm tham số đầu tiên được truyền trên ngăn xếp. R6-R9 tuân theo quy ước được hàm được gọi bảo toàn, nghĩa là các hàm phải giữ nguyên những giá trị này nếu sử dụng chúng.

Các tài khoản và dữ liệu được tuần tự hóa thành các lát byte trong bộ nhớ tuyến tính của VM. Các chương trình phải giải tuần tự hóa chúng thành các kiểu Rust cấp cao hơn (ví dụ: AccountInfo, Pubkey). ABI áp dụng các giới hạn nghiêm ngặt để không chương trình nào có thể truy cập bộ nhớ bên ngoài vùng được cấp phát.

Cùng nhau, các quy tắc này biến ABI thành “chất kết dính” liên kết trải nghiệm phát triển cấp cao với ISA cấp thấp. ABI đảm bảo một chữ ký hàm Rust đơn giản được biên dịch thành cách sử dụng thanh ghi, bố cục bộ nhớ và mã trả về chính xác, để mọi validator luôn diễn giải một chương trình nhất định theo cùng một cách.

Syscall

ISA được thiết kế tối giản, không tích hợp sẵn tài khoản hoặc trạng thái. Nó cũng không trực tiếp cung cấp bất kỳ chức năng cấp cao nào như ghi log, băm hoặc gọi liên chương trình. Thay vào đó, các lệnh gọi hệ thống—những hàm đặc biệt được tích hợp vào VM để cho phép chương trình tương tác với thế giới bên ngoài—được cung cấp. Những lệnh gọi này thường được gọi là syscall.

Có thể xem syscall là các API do VM cung cấp. Thay vì mỗi chương trình phải tự triển khai lại các nguyên hàm mật mã hoặc logic tài khoản cụ thể, syscall cung cấp các thao tác an toàn, chuẩn hóa và được đảm bảo hoạt động tất định trên mọi validator.

Các nhóm syscall phổ biến gồm:

  • Ghi log và gỡ lỗi (ví dụ: syscall sol_log ghi một chuỗi UTF-8 vào log chương trình và được msg! sử dụng bên dưới).
  • Gọi liên chương trình (CPI) (ví dụ: sol_invoke_signed cho phép một chương trình gọi một chương trình on-chain khác, truyền vào các tài khoản và dữ liệu lệnh, điều tối quan trọng đối với khả năng kết hợp của Solana).
  • Mật mã học (ví dụ: các syscall sol_sha256, sol_keccak256 và sol_ed25519_verify đều bổ sung các nguyên hàm mật mã nhanh, tất định mà nhà phát triển không cần tự triển khai).
  • Tiện ích bộ nhớ và tài khoản (tức là các syscall cung cấp trình hỗ trợ để mượn dữ liệu tài khoản, cấp phát lại bộ nhớ hoặc làm việc với các vùng cấp phát heap thuộc sở hữu chương trình).
  • Ngân sách và đo lường tài nguyên tính toán (tức là mọi syscall đều tiêu thụ CU, do hệ thống đo lường của runtime thực thi).

Syscall được gọi bằng lệnh CALL_IMM đặc biệt với một mã định danh băm duy nhất. Khi chương trình gọi syscall, sBPF VM chặn quá trình thực thi, tra cứu mã băm trong sổ đăng ký syscall và chuyển tiếp đến phần triển khai native chạy trong mã runtime đặc quyền. Syscall thực thi bên ngoài sandbox và có quyền truy cập trạng thái runtime, hoàn toàn khác với cách các lệnh gọi hàm thông thường trong chương trình thực thi.

Syscall tuân theo cùng ABI như các hàm thông thường, trong đó năm đối số đầu tiên được truyền vào các thanh ghi từ R1 đến R5 và giá trị trả về nằm trong R0. Mỗi syscall có chi phí đơn vị tính toán cố định, đảm bảo mức tiêu thụ tài nguyên tất định. Ví dụ: mọi lệnh gọi syscall secp256k1_recover đều tiêu thụ 25.000 CU.

Syscall tạo thành một ranh giới bảo mật có kiểm soát vì mỗi syscall xác thực dữ liệu đầu vào và kiểm tra các quyền liên quan trước khi thực hiện bất kỳ thao tác đặc quyền nào. Ví dụ: syscall Gọi liên chương trình (CPI) xác minh bên gọi có quyền phù hợp đối với các tài khoản được truyền vào.

Lưu ý rằng có thể thêm syscall mới thông qua feature gate mà không cần sửa đổi chính ISA. Điều này cho phép Solana mở rộng khả năng của VM, bao gồm hỗ trợ các nguyên hàm mật mã mới, trong khi vẫn duy trì khả năng tương thích ngược với các chương trình hiện có.  

Tệp nhị phân chương trình

Khi quá trình biên dịch kết thúc, tất cả các giai đoạn—từ mã nguồn Rust đến LLVM IR, eBPF, sBPF và việc tuân thủ SVM ISA—tạo ra một đầu ra duy nhất: tệp nhị phân chương trình. Đây là tệp nhị phân thực sự được triển khai lên Solana. 

ELF

Các chương trình Solana được biên dịch thành tệp Executable and Linkable Format (ELF), một định dạng nhị phân tiêu chuẩn được sử dụng trên các hệ thống tương tự Unix. Định dạng ELF đóng vai trò là vùng chứa đóng gói mọi thứ VM cần để thực thi một chương trình nhất định, đồng thời duy trì tính độc lập với nền tảng.

Một tệp ELF thường chứa các phần sau:

  • Phần bytecode: Chứa các lệnh sBPF đã biên dịch trong phần .text.
  • Phần dữ liệu chỉ đọc: Chứa các hằng số, chuỗi tĩnh và giá trị bất biến trong phần .rodata.
  • Các phần BSS và dữ liệu: Lần lượt chứa các biến toàn cục hoặc biến tĩnh có thể thay đổi trong các phần .bss và .data. Lưu ý rằng Solana không cho phép dữ liệu có thể thay đổi. Nghĩa là ELF có thể có .rodata, nhưng không thể có các phần BSS và dữ liệu. 
  • Bảng ký hiệu và tái định vị: Xác định cách phân giải các lệnh gọi hàm, syscall và tham chiếu bộ nhớ trong quá trình tải, tại các phần .symtab và .strtab dành cho ký hiệu, cũng như các phần .rel.dyn và .rela.dyn dành cho mục tái định vị.

Mỗi tệp ELF cũng có một header mô tả kiến trúc, độ rộng lệnh (tức là 64 bit), thứ tự byte (tức là Little Endian) và địa chỉ entrypoint.

Liên kết và tái định vị

Quá trình chuyển đầu ra của trình biên dịch thành một tệp ELF thực thi duy nhất có một thành phần cuối cùng: trình liên kết. Trình liên kết chịu trách nhiệm kết hợp nhiều đơn vị mã đã biên dịch thành một tệp nhị phân thống nhất. Nó cũng phân giải mọi tham chiếu ký hiệu (tức là phần giữ chỗ) mà trình biên dịch chưa phân giải. Ví dụ:

  • Khi chương trình gọi một hàm như sol_log, trình biên dịch không biết hàm đó nằm ở đâu trong bộ nhớ nên sử dụng một phần giữ chỗ.
  • Trình liên kết thay thế tham chiếu ký hiệu này bằng mã định danh băm duy nhất của syscall (tức là mã băm Murmur3 32 bit tất định).
  • Tương tự, các lệnh gọi giữa những hàm nội bộ được viết lại thành bước nhảy tương đối đến độ lệch lệnh trong phần .text.

Quá trình viết lại các tham chiếu ký hiệu thành địa chỉ cụ thể hoặc ID syscall đã băm được gọi là tái định vị. Tuy nhiên, cần lưu ý rằng tái định vị phần lớn là sản phẩm của cách bộ công cụ ban đầu được xây dựng chứ không phải yêu cầu nền tảng. Trên thực tế, có kế hoạch loại bỏ hoàn toàn việc tái định vị trong các phiên bản toolchain tương lai để đơn giản hóa quá trình triển khai.

Bước tái định vị này cần thiết để đảm bảo cùng một tệp nhị phân ELF chạy giống hệt nhau trên mọi validator vì không có địa chỉ bộ nhớ tuyệt đối hoặc ký hiệu dành riêng cho hệ thống nào được nhúng. 

Ngoài ra, bytecode sau khi đã áp dụng các tái định vị này được lưu vào bộ nhớ đệm, nên mọi lần thực thi sau đều dựa trên bytecode đã cập nhật mà không cần xử lý lại các tái định vị.

Sau khi trình liên kết tạo ra một tệp ELF đã tái định vị hoàn chỉnh, chương trình sẵn sàng để triển khai. Kết quả cuối cùng là một tệp nhị phân:

  • Có tính di động: Chạy giống hệt nhau trên mọi validator hoặc phần triển khai SVM.
  • Tất định: Không chứa syscall bất định hoặc phần phụ thuộc vào hệ điều hành.
  • Độc lập: Chứa toàn bộ bytecode và metadata cần thiết để thực thi.

Cách tải bytecode lên Solana

Sau khi một chương trình Solana được biên dịch và liên kết thành tệp ELF hợp lệ, bước tiếp theo là tải tệp đó lên blockchain để các validator có thể thực thi. Quá trình này, được gọi là triển khai chương trình, gồm nhiều thành phần phối hợp với nhau: BPF Loader, mô hình tài khoản, xác minh bytecode và quản lý trạng thái.

Chương trình BPF Loader

BPF Loader là một chương trình native xác thực, tái định vị và đánh dấu tệp ELF là có thể thực thi. Về cơ bản, nó quản lý vòng đời của các chương trình đã triển khai—xử lý các lệnh để khởi tạo tài khoản, ghi bytecode, triển khai chương trình và xử lý nâng cấp.

Solana đã phát triển qua nhiều phiên bản loader, mỗi phiên bản đều cải tiến so với phiên bản trước:

  • BPF Loader: Loader ban đầu dành cho các chương trình tĩnh, không thể nâng cấp, hiện không còn được hỗ trợ. 
  • BPF Loader V2: Loader đơn giản hóa, không có lệnh quản lý.
  • BPF Loader Upgradeable: Loader hiện tại, bổ sung khả năng nâng cấp chương trình.
  • BPF Loader V4: Phiên bản mới nhất với các tính năng triển khai tốt hơn, đơn giản hóa mô hình hai tài khoản hiện tại thành mô hình một tài khoản.

Kiến trúc triển khai: Các mô hình tài khoản

Mô hình tài khoản hiện tại

Loader hiện tại sử dụng kiến trúc hai tài khoản để tách logic chương trình khỏi dữ liệu chương trình. Do đó, một chương trình có hai tài khoản: tài khoản Program và tài khoản ProgramData.

Tài khoản Program là một tài khoản nhỏ, kích thước khoảng 36 byte, chứa metadata và được đánh dấu là có thể thực thi. Nó lưu một tham chiếu đến ProgramData thông qua UpgradeableLoaderState::Program { programdata_address }.

Tài khoản ProgramData là tài khoản lớn hơn, lưu bytecode ELF thực tế cùng metadata triển khai (ví dụ: slot, địa chỉ thẩm quyền nâng cấp) thông qua UpgradeableLoaderState::ProgramData.

Việc tách hai tài khoản cho phép nâng cấp tại chỗ. Nghĩa là tài khoản Program vẫn ở cùng một địa chỉ trong khi bytecode của tài khoản ProgramData có thể được thay thế.

Mô hình tài khoản tương lai

Loader V4 hướng đến tinh giản quá trình triển khai bằng mô hình một tài khoản. Theo đó, tài khoản chương trình sẽ trực tiếp lưu metadata và bytecode, loại bỏ nhu cầu về tài khoản ProgramData riêng biệt. Nó cũng cho phép nhà phát triển lưu image được nén bằng zstd để tiết kiệm chi phí rent.

Cách triển khai chương trình Solana

Quá trình triển khai bao gồm việc tải lên một tệp nhị phân ELF đã biên dịch và để BPF Loader xác minh, lưu vào bộ nhớ đệm rồi đánh dấu tệp đó là có thể thực thi. Quá trình này hơi khác nhau giữa loader có thể nâng cấp và V4 do kiến trúc triển khai được trình bày trong phần trước.

BPF Loader Upgradeable

Quy trình triển khai hiện tại với loader có thể nâng cấp bao gồm việc khởi tạo một tài khoản buffer để tạm giữ bytecode ELF. Bên triển khai gửi một lệnh InitializeBuffer đến BPF Loader Upgradeable. Lệnh này tạo một tài khoản mới thuộc sở hữu của loader và đặt trạng thái tài khoản thành UpgradeableLoaderState::Buffer { authority_address }, ghi lại địa chỉ được phép ghi vào buffer. 

Tệp nhị phân ELF đã biên dịch được tải lên buffer theo từng phần bằng lệnh Write { offset, bytes }. Mỗi lệnh ghi xác minh người ký khớp với thẩm quyền của buffer, kiểm tra buffer vẫn có thể thay đổi (tức là chưa được triển khai) và ghi byte tại độ lệch được chỉ định sau header metadata. Lưu ý rằng do giới hạn kích thước transaction, các chương trình lớn cần nhiều lệnh Write để tải lên toàn bộ tệp ELF.

Sau khi buffer chứa ELF hoàn chỉnh, bên triển khai gửi lệnh DeployWithMaxDataLen { max_data_len }. Đây là bước phức tạp nhất trong toàn bộ quá trình triển khai vì nó điều phối hoạt động triển khai thực tế, từ xác thực tài khoản đến hoàn tất trạng thái.

Trước tiên, loader xác thực mọi tài khoản trong quá trình triển khai và kiểm tra rằng:

  • Tài khoản chương trình chưa được khởi tạo và được miễn rent.
  • Buffer chứa dữ liệu hợp lệ và thẩm quyền đã khởi tạo buffer đã ký transaction.
  • max_data_len đủ lớn để chứa dữ liệu của buffer.
  • Tổng kích thước không vượt quá MAX_PERMITTED_DATA_LENGTH (tức là 10MiB hoặc 10.485.760 byte). 

Sau đó, loader tạo tài khoản ProgramData, dẫn xuất địa chỉ dưới dạng PDA bằng ID chương trình và ID loader. Tiếp theo, nó hoàn trả lamport của buffer cho bên thanh toán vì tài khoản buffer không còn cần thiết sau khi triển khai. 

Ngoài ra, nó tạo tài khoản ProgramData qua CPI đến System Program, cấp phát đủ không gian cho metadata và số byte max_data_len. Sau đó, loader dùng bump seed của PDA để ký CPI. 

Macro deploy_program! đảm bảo bytecode an toàn để thực thi. Trước tiên, macro phân tích cấu trúc tệp ELF để xác thực magic byte ELF (tức là 0x7f ‘E’ ‘L’ ‘F’) và header (tức là 64 bit, Little Endian), trích xuất các phần chương trình, xử lý bảng tái định vị, đồng thời xác thực ranh giới và căn chỉnh của các phần. Quá trình tải sẽ thất bại ngay lập tức nếu ELF không hợp lệ hoặc sử dụng tính năng không được hỗ trợ.

Sau đó, RequisiteVerifier (tức là trình xác minh của sBPF) thực hiện phân tích tĩnh trên mọi đường dẫn thực thi có thể có mà không chạy chương trình, đảm bảo chương trình an toàn một cách có thể chứng minh trước khi thực thi bất kỳ lệnh nào. Trình xác minh cũng thực thi các ràng buộc SVM ISA đã nêu. Nếu xác minh thất bại, quá trình triển khai bị từ chối với InstructionError::InvalidAccountData và chương trình không bao giờ được đánh dấu là có thể thực thi.

Sau khi xác minh thành công, bytecode được biên dịch và lưu vào bộ nhớ đệm để thực thi. Hàm load_program_from_bytes tạo một ProgramCacheEntry chứa:

  • Tệp thực thi được biên dịch JIT: Bytecode sBPF được biên dịch Just-In-Time (JIT) thành mã máy native dành cho kiến trúc CPU của validator. Điều này mang lại tốc độ thực thi gần với native trong khi vẫn duy trì tính an toàn.
  • Metadata slot: Thời điểm triển khai và thời điểm chương trình bắt đầu hiển thị lần lượt được ghi là deployment_slot và effective_slot. Độ trễ này ngăn chương trình được sử dụng ngay trong slot mà nó được triển khai.
  • Môi trường runtime: Các tham chiếu đến sổ đăng ký syscall để xác định những syscall khả dụng và cấu hình thực thi sẽ được dùng khi chương trình chạy. 

Mục bộ nhớ đệm được lưu trong program_cache_for_tx_batch, giúp chương trình sẵn sàng để thực thi trong các transaction tiếp theo. Sau khi chương trình được xác minh và lưu vào bộ nhớ đệm thành công, loader cập nhật trạng thái tài khoản để hoàn tất triển khai. Trạng thái của tài khoản ProgramData được cập nhật để ghi lại thời điểm chương trình được triển khai và người có quyền nâng cấp chương trình. Bytecode ELF cũng được sao chép từ buffer vào tài khoản. Trạng thái của tài khoản Program cũng được cập nhật để liên kết với tài khoản ProgramData và được đánh dấu là có thể thực thi. Cuối cùng, độ dài dữ liệu của buffer được đặt bằng kích thước metadata, qua đó xóa bytecode và thu hồi không gian.

Chương trình hiện đã được triển khai đầy đủ và có thể được transaction gọi.

BPF Loader V4

BPF Loader V4 tinh giản quá trình triển khai bằng cách loại bỏ nhu cầu về tài khoản ProgramData riêng, cho phép tài khoản chương trình lưu trực tiếp bytecode. Nó cũng bổ sung hỗ trợ lưu trữ ELF nén bằng zstd, giúp giảm đáng kể chi phí rent và được giải nén theo nhu cầu trong quá trình tải.

Bên triển khai gọi SetProgramLength { new_size } để cấp phát không gian cho metadata và bytecode của chương trình. Với chương trình mới, thao tác này khởi tạo tài khoản ở trạng thái LoaderV4State::Retracted, ghi lại thẩm quyền và đánh dấu tài khoản là có thể thực thi, dù chưa thể gọi tài khoản đó.

Sau đó, bên triển khai ghi tệp nhị phân ELF trực tiếp vào tài khoản chương trình thông qua các lệnh Write { offset, bytes }. Các thao tác ghi này chỉ được phép khi chương trình ở trạng thái Retracted. Lệnh Copy cũng có thể được dùng để sao chép bytecode từ một chương trình khác, bất kể phiên bản loader, rất hữu ích khi di chuyển.

Sau đó, lệnh Deploy được dùng để chuyển chương trình từ trạng thái Retracted sang Deployed. Về cơ bản, lệnh này trích xuất bytecode từ tài khoản chương trình tại độ lệch và chạy chính xác cùng quy trình xác minh như BPF Loader Upgradeable (tức là phân tích ELF, xác minh tĩnh, biên dịch JIT, lưu vào bộ nhớ đệm). Nếu xác minh thành công, trạng thái chương trình được cập nhật thành LoaderV4Status::Deployed và slot triển khai được ghi lại.

Chương trình hiện đã được triển khai đầy đủ và có thể được transaction gọi.

Loader V4 cũng áp dụng thời gian chờ giữa các lần chuyển đổi trạng thái (tức là deployed và retracted) để ngăn các cuộc tấn công tái triển khai. Chương trình không thể được triển khai hoặc thu hồi trong vòng một slot kể từ lần triển khai gần nhất. Điều này giúp ngăn tác nhân độc hại cập nhật nhanh chương trình để khai thác race condition hoặc gây nhầm lẫn cho người dùng, đảm bảo tính nguyên tử theo từng slot thay vì độ trễ nhiều slot. Lưu ý rằng thời gian chờ này áp dụng cho cả lệnh Deploy và Retract.

Chương trình cũng có thể trở thành bất biến thông qua lệnh Finalize. Lệnh này chuyển chương trình từ trạng thái Deployed sang Finalized, nghĩa là chương trình không thể bị thu hồi hoặc nâng cấp nữa. Trường thẩm quyền được tái sử dụng để trỏ đến địa chỉ chương trình “phiên bản tiếp theo”, cho phép các lộ trình nâng cấp rõ ràng trong khi vẫn duy trì tính bất biến của chương trình.

Cách hoạt động của quá trình thực thi trong SVM

SVM là công cụ xử lý transaction trong validator, chịu trách nhiệm thực thi các lệnh gọi chương trình và cập nhật trạng thái tương ứng. 

Khi một transaction đến validator, nó đi qua quy trình nhiều giai đoạn: xác thực, tải tài khoản, thực thi chương trình trong sBPF VM cô lập, xác minh các bất biến và ghi nhận trạng thái. Nếu mọi lệnh đều thành công, các thay đổi tài khoản được ghi vào AccountsDB. Nếu bất kỳ lệnh nào thất bại, toàn bộ transaction được hoàn tác nguyên tử.

SVM hoạt động như một công cụ thực thi tách rời. Nghĩa là nó không quản lý cơ chế đồng thuận, mạng hoặc lịch sử sổ cái. Thay vào đó, nó chỉ tập trung vào việc thực thi chương trình an toàn, tất định và hiệu quả. 

Bank điều phối quá trình thực thi của SVM, cung cấp ngữ cảnh runtime (ví dụ: blockhash, rent, tập hợp tính năng) và ghi nhận kết quả vào bộ nhớ lưu trữ bền vững. SVM quản lý quá trình thực thi chương trình, từ tải bytecode đến thực thi ngân sách tính toán. Sự phân tách trách nhiệm này giúp SVM có thể được tái sử dụng ngoài validator.

Transaction 

Transaction là huyết mạch của Solana, cũng như của bất kỳ blockchain nào—chúng gọi chương trình để thực hiện thay đổi trạng thái. 

Transaction là một gói lệnh mô tả những hành động cần thực hiện, trên những tài khoản nào và liệu các tài khoản đó có quyền cần thiết để thực hiện hay không. 

Lệnh là chỉ thị cho một lần gọi chương trình. Đây là đơn vị logic thực thi nhỏ nhất, đóng vai trò là đơn vị vận hành cơ bản nhất trên Solana.

Chương trình diễn giải dữ liệu được truyền từ một lệnh để thao tác trên các tài khoản đã chỉ định. Một lệnh bao gồm ID chương trình (tức là chương trình được gọi), danh sách tài khoản cần đọc và ghi, cùng dữ liệu đầu vào được truyền cho chương trình.

Transaction bắt đầu khi người dùng xác định một mục tiêu, chẳng hạn như chuyển 10 SOL sang tài khoản khác. Ý định này được chuyển thành một lệnh yêu cầu System Program chuyển 10 SOL từ tài khoản A sang tài khoản B. Tài khoản A được truyền vào transaction dưới dạng người ký có thể ghi, còn tài khoản B được truyền vào dưới dạng tài khoản có thể ghi. Sau đó, lệnh được đóng gói vào một transaction cũng chỉ định bên trả phí, những người ký và một blockhash gần đây.

Thông thường, transaction sau đó được gửi đến một nhà cung cấp RPC như Helius. Node RPC nhận transaction sẽ xác minh rằng mọi chữ ký bắt buộc đều có mặt và hợp lệ, transaction chưa được xử lý, blockhash gần đây được cung cấp vẫn hợp lệ và transaction không vượt quá kích thước tối đa (tức là 1232 byte).

Sau đó, RPC chuyển tiếp transaction đến Transaction Processing Unit (TPU) của leader hiện tại. 

Transaction Processing Unit (TPU)

Transaction Processing Unit (TPU) là quy trình tiếp nhận và xử lý transaction trong các validator Solana. Nó có nhiều giai đoạn để nhận, xác minh, lên lịch và thực thi transaction trước khi ghi nhận vào sổ cái của Solana.

Trong phạm vi bài viết này, chúng ta sẽ xem xét chi tiết Fetch Stage, SigVerify Stage và Banking Stage trước khi chuyển sang Bank và việc khởi tạo sBPF VM. 

Để tìm hiểu TPU chi tiết hơn, hãy xem Chất lượng dịch vụ theo trọng số stake: Mọi điều bạn cần biết.

Fetch Stage

Fetch Stage là giai đoạn đầu tiên trong quy trình TPU. Nó nhận mọi transaction đến qua kết nối QUIC, sử dụng socket UDP làm lớp truyền tải bên dưới, rồi gom chúng thành các batch để xử lý ở các giai đoạn sau.

Ba socket UDP được tạo:

  • tpu: Các transaction thông thường như chuyển token, mint NFT và tương tác với chương trình.
  • tpu_vote: Transaction biểu quyết từ validator—điều này sẽ thay đổi với Alpenglow khi transaction biểu quyết bị loại bỏ.
  • tpu_forwards: Các transaction chưa xử lý được chuyển tiếp từ leader trước đó vì không thể xử lý kịp thời.

Các socket này được đăng ký trong dịch vụ gossip của validator và lưu trong struct ContactInfo, cho phép các validator và node RPC khác tìm ra nơi cần gửi transaction.

Fetch Stage tạo một luồng cho mỗi socket và tất cả liên tục:

  • Thăm dò socket UDP để tìm gói tin đến.
  • Tạo một batch gồm 64 gói tin.
  • Gửi batch qua kênh không giới hạn của nó.

Các kênh không giới hạn hiện được dùng để chuyển batch đến những giai đoạn sau, nghĩa là kênh có dung lượng không giới hạn. Việc sử dụng kênh không giới hạn cũng đồng nghĩa Fetch Stage có thể hoạt động độc lập với tốc độ xử lý ở các giai đoạn sau. 

Mặc dù điều này ngăn việc loại bỏ gói tin ngay lập tức khi lưu lượng tăng đột biến, nó có thể gây vấn đề về bộ nhớ: nếu các giai đoạn sau không theo kịp Fetch Stage, kênh sẽ tăng trưởng không giới hạn, có khả năng gây chậm hoặc sự cố hết bộ nhớ (OOM). 

Công việc triển khai các kênh có giới hạn với cơ chế backpressure phù hợp đang được tiến hành, cho phép hệ thống báo hiệu tắc nghẽn và ngăn bộ nhớ tăng trưởng không giới hạn.

Fetch Stage cũng tạo một luồng khác chuyên xử lý các gói tin được chuyển tiếp. Những gói tin này được đánh dấu bằng cờ FORWARDED và được giữ lại hoặc loại bỏ dựa trên lịch trình leader:

  • Được chấp nhận: Nếu validator hiện tại sắp trở thành leader, các gói tin được chuyển tiếp sẽ được xử lý qua kênh TPU thông thường và gửi đến giai đoạn tiếp theo.
  • Bị loại bỏ: Nếu validator hiện tại không sắp trở thành leader, các gói tin được chuyển tiếp sẽ bị loại bỏ để tránh lãng phí tài nguyên xử lý. 

Fetch Stage sử dụng PacketBatchRecycler để cấp phát trước 1.000 batch gói tin, mỗi batch chứa 1.024 gói tin. Việc này giúp giảm chi phí cấp phát bộ nhớ vì bộ nhớ của batch gói tin có thể được tái sử dụng thay vì mỗi lần đều cấp phát batch mới. Trước đây, recycler cần thiết cho việc ghim bộ nhớ CUDA, nhưng chức năng này không còn hoạt động và phần lớn có thể được xem là nợ kỹ thuật. 

SigVerify Stage

SigVerify Stage là giai đoạn thứ hai trong quy trình TPU, chịu trách nhiệm xác minh chữ ký đúng như tên gọi. Việc này được thực hiện sớm trong quy trình vì xác minh chữ ký Ed25519 tốn nhiều tài nguyên tính toán, dù vẫn không tốn bằng thực thi transaction. Xác minh transaction trước khi thực thi cho phép validator từ chối transaction gian lận, ngăn các cuộc tấn công từ chối dịch vụ và đảm bảo chỉ những transaction đúng định dạng mới đến Banking Stage.

SigVerify Stage hoạt động như một luồng duy nhất, liên tục nhận gói tin từ các kênh của Fetch Stage và xử lý chúng qua một quy trình xác minh. Dù chỉ là một luồng, bên trong vẫn có mức độ song song hóa rất cao.

Theo mặc định, chữ ký được xác minh trên CPU bằng trình lặp song song. Cách này phân phối việc xác minh trên mọi lõi CPU khả dụng, cho phép mỗi lõi xác minh độc lập một tập hợp con các chữ ký. 

Việc xác minh chữ ký cũng có thể được chuyển sang GPU nếu phát hiện thư viện hiệu năng thông qua perf_libs::api(). Chỉ có thể dùng GPU nếu có ít nhất 64 gói tin và dự kiến 90% trong số đó hợp lệ. Lý do là GPU có chi phí thiết lập và truyền dữ liệu khoảng ~15-20ms, trong khi CPU có thể xác minh 64 chữ ký trong khoảng ~10-20ms. Trên thực tế, tính năng này đã tỏ ra không thực tiễn trong môi trường production do chi phí độ trễ, khiến nó chậm hơn nhiều so với xác minh bằng CPU với khối lượng công việc thực tế. Đã có kế hoạch loại bỏ đường dẫn mã không được sử dụng này.

Quá trình xác minh tương đối đơn giản:

  • Nhận các batch gói tin từ những kênh không giới hạn của Fetch Stage.
  • Loại bỏ ngẫu nhiên transaction bằng cơ chế giảm tải nếu số lượng gói tin vượt quá 165.000.
  • Loại bỏ transaction trùng lặp.
  • Loại bỏ gói tin dư thừa để không IP nào có thể độc chiếm băng thông xác minh.
  • Thu gọn trước và sắp xếp lại các batch để cải thiện tính cục bộ của bộ nhớ đệm và giảm lãng phí bộ nhớ.
  • Xác minh chữ ký.

Mọi gói tin hợp lệ đều chuyển đến Banking Stage. 

Banking Stage

Banking Stage là nơi transaction được thực thi. Tại đây, transaction được đưa vào buffer, lên lịch và thực thi bởi các luồng worker song song.

Một mô hình bộ lập lịch trung tâm được sử dụng, tách biệt cách xử lý transaction biểu quyết và không biểu quyết:

  • Một luồng worker xử lý transaction biểu quyết và biểu quyết gossip.
  • Bốn luồng worker xử lý mọi transaction không biểu quyết.
  • Một luồng điều phối việc phân phối công việc giữa các luồng worker.

Các gói tin đến được giải tuần tự hóa và đưa vào buffer với tối đa 100.000 transaction. Bộ lập lịch liên tục nhận transaction mới trong khi quản lý buffer này, loại bỏ các transaction hết hạn hoặc không hợp lệ trong các thao tác dọn hàng đợi, mỗi lần kiểm tra tối đa 10.000 transaction.

Bộ lập lịch xác định thứ tự thực thi transaction bằng cách phát hiện xung đột (tức là các transaction muốn giành khóa đọc và ghi cho cùng một tài khoản). Có hai cách triển khai bộ lập lịch mặc định:

  • PrioGraphScheduler: Bộ lập lịch xây dựng đồ thị ưu tiên để phát hiện xung đột tài khoản.
  • GreedyScheduler: Bộ lập lịch mặc định, sử dụng cách tiếp cận FIFO đơn giản hơn để sắp xếp transaction.

Sau đó, bộ lập lịch chọn các transaction không xung đột và chuyển chúng đến các luồng worker. Mỗi worker nhận một batch và bắt đầu xử lý.

Điều phối Bank

Bank đại diện cho trạng thái của tất cả tài khoản tại một slot cụ thể. Đây là cấu trúc dữ liệu trung tâm quản lý dữ liệu tài khoản, thực thi các quy tắc runtime và đóng vai trò điều phối giữa các worker của Banking Stage với sBPF VM trong quá trình thực thi transaction. 

Vòng đời

Mỗi Bank trải qua ba trạng thái:

  • Hoạt động: Một Bank mới tạo và đang mở để nhận transaction. Các worker của Banking Stage áp dụng transaction cho đến khi Bank đạt số tick mục tiêu hoặc mọi entry trong slot đã được xử lý.
  • Đóng băng: Khi đạt số tick hoặc mọi entry đã được xử lý, Bank bị đóng băng. Không thể áp dụng thêm transaction. Tại thời điểm này, phí transaction được cộng dồn cho leader của block, các tài khoản sysvar được cập nhật và hàm băm cuối cùng của bank được tính.
  • Đặt làm gốc: Sau khi Bank đã đóng băng nhận đủ phiếu bầu từ validator, nó được đặt làm gốc. Trạng thái lúc này được hoàn tất và trở thành một phần trong sổ cái của chuỗi.

Mỗi Bank, ngoại trừ Bank khởi nguyên, đều trỏ ngược đến một Bank cha, tạo thành cấu trúc cây đại diện cho các fork khác nhau của sổ cái. 

Luồng thực thi

Các worker của Banking Stage hoạt động trên working bank hiện tại (tức là Bank đang hoạt động, chưa đóng băng và đang được xây dựng cho slot hiện tại). Khi worker nhận một batch transaction từ bộ lập lịch, Bank sẽ điều phối việc thực thi:

  • Khóa tài khoản: Các worker gọi hàm prepare_sanitized_batch_with_results() để khóa mọi tài khoản được tham chiếu trong transaction, nhằm ngăn sửa đổi đồng thời.
  • Tải tài khoản: Bank lấy dữ liệu tài khoản từ AccountsDB cho mọi tài khoản đã khóa.
  • Khấu trừ phí: Bank khấu trừ phí transaction từ tài khoản của bên trả phí trước khi thực thi.
  • Xác thực: Bank xác thực blockhash còn mới, kiểm tra trạng thái tài khoản nonce và xác minh quyền sở hữu tài khoản.
  • Chuyển giao cho VM: Bank gọi load_and_execute_transactions(), chuyển các tài khoản đã tải sang sBPF VM.
  • Thực thi: sBPF VM thực thi bytecode chương trình của từng lệnh.
  • Kết quả: sBPF VM trả về toàn bộ đầu ra thực thi (tức là LoadAndExecuteTransactionOutput).
  • Ghi nhận: Bank ghi các trạng thái tài khoản đã cập nhật trở lại AccountsDB thông qua bank.commit_transactions().

Giờ đây, quá trình thực thi đi vào SVM thực sự (tức là sBPF VM). Sau khi Bank gọi load_and_execute_transactions(), các transaction đi qua một quy trình nhiều giai đoạn, trong đó mỗi lệnh được xử lý bằng một phiên bản sBPF VM mới, cô lập và được khởi tạo để thực thi bytecode của chương trình.

Các lệnh của một transaction được thực thi tuần tự. Quy trình cho mỗi lệnh là:

  • Xác định chương trình: Tra cứu tài khoản chương trình từ ID chương trình của lệnh.
  • Tải từ bộ nhớ đệm: Kiểm tra xem chương trình đó đã được biên dịch JIT trong bộ nhớ đệm chương trình chưa. 
  • Khởi tạo VM: Tạo một phiên bản sBPF VM cô lập với các vùng bộ nhớ và ngân sách tính toán cụ thể.
  • Thực thi bytecode: Chạy entrypoint của chương trình với dữ liệu đầu vào của lệnh.
  • Xác minh bất biến: Kiểm tra việc thực thi không vi phạm bất kỳ quy tắc runtime nào.
  • Thu thập kết quả: Đóng gói kết quả thực thi.

Tải chương trình

Trước khi có thể thực thi, chương trình phải được tải từ tài khoản on-chain, xác minh và biên dịch thành mã máy native. Việc này diễn ra thông qua bộ nhớ đệm chương trình và quy trình biên dịch JIT.

Bộ nhớ đệm chương trình là một cơ chế tối ưu hóa hiệu năng, giúp tránh phải tải lại và biên dịch lại chương trình ở mỗi lần gọi. Nó được duy trì ở cấp batch transaction và lưu các đối tượng ProgramCacheEntry, bao gồm:

  • Tệp thực thi được biên dịch JIT: Mã máy native dành cho kiến trúc CPU của validator.
  • Metadata triển khai: Slot khi chương trình được triển khai và bắt đầu có hiệu lực.
  • Môi trường runtime: Các tham chiếu đến sổ đăng ký syscall và cấu hình thực thi.

Khi một transaction tham chiếu đến ID chương trình, trình tự tra cứu sau được thực hiện:

  • Kiểm tra bộ nhớ đệm batch transaction: Tìm phiên bản đã lưu vào bộ nhớ đệm và được biên dịch JIT.
  • Kiểm tra bộ nhớ đệm chương trình toàn cục: Nếu không có trong bộ nhớ đệm batch, kiểm tra bộ nhớ đệm toàn cục của validator.
  • Tải từ tài khoản: Nếu không có trong tất cả bộ nhớ đệm, tải tài khoản chương trình từ AccountsDB.
  • Phân tích ELF: Trích xuất bytecode từ dữ liệu tài khoản chương trình.
  • Xác minh bytecode: Chạy trình xác minh tĩnh để đảm bảo an toàn.
  • Biên dịch JIT: Chuyển bytecode sBPF thành mã máy native.
  • Lưu mục vào bộ nhớ đệm: Lưu chương trình đã biên dịch cho những lần gọi sau.

Điều này nghĩa là lần gọi đầu tiên của một chương trình mới triển khai phải chịu toàn bộ chi phí tải, xác minh và biên dịch, trong khi các lần gọi sau thực thi trực tiếp mã native trong bộ nhớ đệm. 

Khi không tìm thấy trong bộ nhớ đệm, chương trình phải được tải từ tài khoản on-chain. Với các chương trình thuộc sở hữu của BPF Loader Upgradeable, tài khoản chương trình chứa tham chiếu đến tài khoản ProgramData, và đây là tài khoản được tải từ AccountsDB. Với mô hình một tài khoản của Loader V4, bytecode được tải trực tiếp từ tài khoản chương trình và có thể cần giải nén nếu được nén bằng zstd.

Các byte ELF đã trích xuất được phân tích để xác định bytecode thực thi và những phần đã nêu (tức là .text, .rodata, .data / .bss, .symtab / .strtab). Sau khi ELF được phân tích thành công, RequisiteVerifier (tức là trình phân tích tĩnh của sBPF) xác minh mọi đường dẫn thực thi có thể có mà không thực sự chạy chương trình, như đã mô tả trong các phần trước.

Biên dịch JIT

Sau khi xác minh thành công, bytecode được biên dịch Just-In-Time (JIT) thành mã máy native. Trình biên dịch JIT chuyển từng lệnh sBPF thành các lệnh CPU native tương đương cho kiến trúc của validator. 

Biên dịch JIT giúp sBPF VM đạt hiệu năng đủ để xử lý thông lượng cao của Solana. Nếu không có JIT, VM sẽ phải diễn giải từng lệnh bytecode sBPF, gây ra chi phí đáng kể. 

Mỗi lệnh bytecode cần được tìm nạp, giải mã và chuyển đến mã xử lý, làm phát sinh chi phí diễn giải. Ngoài ra, mã được diễn giải không thể tận dụng các cơ chế tối ưu hóa cấp CPU như pipeline, dự đoán nhánh hoặc thực thi không theo thứ tự. Do đó, mỗi lệnh sBPF trở thành một lệnh gọi hàm trong trình thông dịch.

Biên dịch JIT loại bỏ hoàn toàn các chi phí này bằng cách tạo ra mã máy native chạy trực tiếp trên CPU. Kết quả là hiệu năng gần với native, kiểm tra giới hạn được tối ưu hóa, đo lường tài nguyên tính toán nội tuyến, tối ưu hóa ở cấp phần cứng và ánh xạ cấp phát thanh ghi rõ ràng.

Trình biên dịch JIT thực hiện một lượt chuyển đổi duy nhất từ bytecode sBPF sang mã máy native. Với mỗi lệnh sBPF:

  • Lệnh được giải mã để trích xuất opcode, thanh ghi, độ lệch và giá trị tức thời.
  • Thiết lập stack frame và lưu các thanh ghi do hàm được gọi bảo toàn.
  • Ánh xạ thao tác sBPF sang lệnh CPU tương đương để mỗi thao tác được biên dịch thành lệnh native. Ví dụ: trong ánh xạ x86-64, rax ánh xạ tới R0 và rbp ánh xạ tới R10.
  • Phát sinh kiểm tra giới hạn và dịch địa chỉ bằng phần mềm khi truy cập bộ nhớ—việc chuyển địa chỉ guest thành địa chỉ host là một trong những thao tác chậm nhất của VM do chi phí xử lý.
  • Duy trì sự cô lập của stack (tức là stack của guest nằm trong một buffer được cấp phát trên heap thay vì stack của host).
  • Chèn thao tác khấu trừ CU và kiểm tra ngân sách để phát sinh cơ chế đo lường tài nguyên tính toán.
  • Khôi phục các thanh ghi và dọn dẹp stack.

Chuyển đổi lệnh

Trình biên dịch JIT chuyển đổi các loại lệnh khác nhau (tức là số học, truy cập bộ nhớ, thao tác lưu, nhánh có điều kiện, điều phối syscall) theo những cách cụ thể. 

Các phép toán số học được ánh xạ trực tiếp tới từng lệnh CPU native mà không có chi phí bổ sung, vì các thanh ghi của sBPF ánh xạ phù hợp với các thanh ghi phần cứng tương ứng của validator. 

Các thao tác truy cập bộ nhớ cần kiểm tra giới hạn để ngăn đọc và ghi ngoài phạm vi. Trình biên dịch JIT tạo mã xác thực để kiểm tra giới hạn dưới và trên của từng lần truy cập bộ nhớ so với ranh giới vùng hợp lệ. 

Về cơ bản, việc này gồm 3 đến 6 lệnh native để tính địa chỉ hiệu dụng, xác minh địa chỉ nằm trong giới hạn dự kiến rồi thực hiện thao tác tải thực tế. Cơ chế này được xử lý tương đối hiệu quả nhờ dự đoán nhánh vì hiếm khi xảy ra vi phạm giới hạn. 

Các thao tác lưu bao gồm kiểm tra giới hạn và xác thực quyền ghi. Mã đã biên dịch xác minh địa chỉ đích nằm trong giới hạn và vùng bộ nhớ đã bật quyền ghi trước khi ghi vào bộ nhớ.

Các nhánh có điều kiện được biên dịch thành lệnh nhảy có điều kiện native. Trình biên dịch JIT phân giải mọi đích nhảy trong quá trình biên dịch để độ lệch lệnh tương đối của sBPF được chuyển thành địa chỉ tuyệt đối trong mã native. 

Một lần điều phối syscall yêu cầu lưu trạng thái của VM—toàn bộ 11 thanh ghi—và gọi trình xử lý syscall native. Sau đó, trạng thái VM được khôi phục cùng giá trị trả về. Chi phí quản lý trạng thái này là lý do syscall có chi phí CU cố định, cao hơn các lệnh thông thường.

Đo lường đơn vị tính toán

Trình biên dịch JIT nội tuyến việc theo dõi đơn vị tính toán trực tiếp vào mã được tạo. Mỗi lệnh sBPF đều có bước kiểm tra ngân sách để đảm bảo ngân sách chưa cạn khi các lệnh trừ dần số CU còn lại. 

Việc nội tuyến này tránh chi phí gọi hàm và có thể được thực hiện hiệu quả nhờ dự đoán nhánh, thực thi không theo thứ tự (tức là kiểm tra CU và thao tác có thể chạy song song) và khả năng song song ở cấp lệnh.

Đối với syscall có chi phí thay đổi (ví dụ: syscall sol_sha256 tăng theo độ dài dữ liệu), việc tính chi phí diễn ra bên trong phần triển khai syscall native trước khi trở về VM.

Lưu mã đã biên dịch vào bộ nhớ đệm

Sau khi hoàn tất biên dịch JIT, tệp thực thi native được lưu trong một ProgramCacheEntry, bao gồm:

  • Mã được biên dịch JIT
  • Metadata của slot triển khai và slot có hiệu lực
  • Tham chiếu sổ đăng ký syscall
  • Cấu hình môi trường runtime

Mục bộ nhớ đệm được đặt trong bộ nhớ đệm batch transaction và bộ nhớ đệm chương trình toàn cục. Bộ nhớ đệm thứ nhất khả dụng cho mọi lệnh trong batch hiện tại, còn bộ nhớ đệm thứ hai khả dụng cho mọi transaction trong tương lai. 

Mục bộ nhớ đệm có thể bị vô hiệu hóa khi chương trình được nâng cấp (tức là bytecode mới được triển khai), tài khoản chương trình bị đóng, feature gate thay đổi tính khả dụng của syscall hoặc bất cứ khi nào validator quyết định xóa bộ nhớ đệm.

Độ trễ slot có hiệu lực

Lưu ý rằng một chương trình được triển khai ở slot n không thể được gọi cho đến slot n + 1. Độ trễ này đảm bảo mọi validator đều quan sát được việc triển khai, bộ nhớ đệm chương trình được đồng bộ trên toàn mạng và tính nguyên tử theo từng slot được thực thi. 

Khởi tạo sBPF VM

Sau khi chương trình được tải và biên dịch JIT hoặc được truy xuất từ bộ nhớ đệm, một sBPF VM mới được khởi tạo cho mỗi lần thực thi lệnh. Quá trình này diễn ra trong BPF Loader và bao gồm thiết lập năm vùng bộ nhớ riêng biệt, khởi tạo ngân sách tính toán và đăng ký syscall.

Các vùng bộ nhớ

Như đã đề cập trong phần SVM ISA, VM tạo năm vùng bộ nhớ riêng biệt. Cùng nhau, các vùng này tạo thành sandbox cô lập nơi các chương trình Solana thực thi. Vùng bộ nhớ chương trình thường bắt đầu tại địa chỉ 0x100000000. Nó chứa mã native được biên dịch JIT để thực thi—đây là mã máy thực tế, tùy thuộc vào kiến trúc CPU của validator. Nếu JIT bị tắt để gỡ lỗi, vùng này sẽ chứa bytecode sBPF được diễn giải. Quyền cho phần này chỉ là đọc và thực thi.

Dữ liệu chỉ đọc cũng nằm trong 0x100000000. Nó chứa các hằng số và chuỗi tĩnh được trích xuất từ phần .rodata của ELF trong quá trình tải chương trình. Mục đích của vùng bộ nhớ này là cung cấp khả năng truy cập hiệu quả vào các hằng số tại thời điểm biên dịch mà không cần cấp phát heap. 

Stack bắt đầu tại 0x200000000 và chứa các biến cục bộ, frame gọi hàm và địa chỉ trả về. Đây là nơi diễn ra các phép tính tạm thời trong khi thực thi. Nó phát triển hướng xuống từ đỉnh vùng, với thanh ghi R10 (tức là frame pointer) đánh dấu ranh giới frame hiện tại. Vùng này cho phép đọc và ghi, với kích thước cố định là 4KB cho mỗi call frame. Vượt quá giới hạn này sẽ phát sinh lỗi StackAccessViolation, cho biết stack bị tràn. Kích thước stack nhỏ khuyến khích nhà phát triển dùng heap hoặc tốt hơn là lưu dữ liệu trong tài khoản thay vì dựa vào lưu trữ trên stack.

Heap bắt đầu tại 0x300000000 và chứa bộ nhớ được cấp phát động cho các cấu trúc dữ liệu runtime không vừa trên stack hoặc trong tài khoản. Kích thước dao động từ mức mặc định 32KB đến tối đa 256KB. Trước đây, chương trình có thể mở rộng heap thông qua syscall sol_alloc_free. Tuy nhiên, syscall sol_alloc_free đã bị ngừng sử dụng và bị vô hiệu hóa đối với các lần triển khai chương trình mới. Chương trình phải chỉ định kích thước heap cần thiết tại thời điểm triển khai thay vì mở rộng động. 

Lưu ý: việc mở rộng heap tiêu thụ đơn vị tính toán theo công thức (heap_size / 32KB) * 8,000 CUs, với chi phí heap mặc định là 8 CU.

Vùng bộ nhớ dữ liệu đầu vào bắt đầu tại địa chỉ 0x400000000. Nó chứa các tham số entrypoint đã tuần tự hóa mà chương trình nhận được khi được gọi. Đây là vùng bộ nhớ chỉ đọc có kích thước thực tế thay đổi theo transaction. Ba thành phần được tuần tự hóa gồm pubkey 32 byte của chương trình đang được gọi, một mảng tài khoản và dữ liệu lệnh.

Thực thi quyền truy cập bộ nhớ

Mọi lệnh tải và lưu bộ nhớ đều được VM kiểm tra giới hạn. Trước mỗi lần truy cập bộ nhớ, VM xác minh địa chỉ nằm trong phạm vi hợp lệ của vùng và loại truy cập (tức là đọc hoặc ghi) được phép đối với vùng đó. 

Quá trình thực thi dừng ngay lập tức với lỗi AccessViolation nếu một trong hai bước kiểm tra thất bại. Toàn bộ transaction được hoàn tác và không có thay đổi trạng thái nào được ghi nhận. 

Việc thực thi quy tắc này gần như không phát sinh chi phí runtime vì trình biên dịch JIT biên dịch các bước kiểm tra thành mã native hiệu quả mà CPU có thể thực thi trực tiếp. Cơ chế dự đoán nhánh hiện đại xử lý các bước kiểm tra hiệu quả vì vi phạm hiếm khi xảy ra. 

Khởi tạo ngân sách tính toán

Mỗi phiên bản VM được khởi tạo với ngân sách đơn vị tính toán giới hạn tổng khối lượng công việc mà một chương trình nhất định có thể thực hiện. Mô hình thực thi có giới hạn này đảm bảo chương trình không thể chạy vô thời hạn và mọi validator đều thực thi transaction trong một khoảng thời gian có thể dự đoán.

Các tham số ngân sách hiện tại như sau:

  • Giới hạn đơn vị tính toán mặc định cho mỗi lệnh: 200.000 CU.
  • Giới hạn đơn vị tính toán tối đa cho mỗi giao dịch: 1.400.000 CU.
  • Giới hạn lệnh tích hợp sẵn: 3.000 CU.

Số CU mặc định cho mỗi giao dịch là min(1_400_00, (200_000*non_reserve_instructions + 3_000*reserve_instructions)). Về cơ bản, đây là giá trị nhỏ hơn giữa giới hạn đơn vị tính toán tối đa và chi phí mặc định theo từng loại lệnh dựa trên các lệnh được cung cấp.

Ngân sách tính toán theo dõi số đơn vị còn lại trong suốt quá trình thực thi. Khi mỗi lệnh sBPF được thực thi, chi phí CU của lệnh đó sẽ bị trừ khỏi ngân sách còn lại. Nếu ngân sách về 0 trước khi chương trình hoàn tất, quá trình thực thi sẽ dừng ngay lập tức với lỗi InstructionError::ComputationalBudgetExceeded. Giao dịch thất bại và không có thay đổi trạng thái nào được ghi nhận, nhưng người trả phí vẫn bị tính phí giao dịch để bù đắp cho validator đã xử lý giao dịch đó. 

Sổ đăng ký syscall

Trong quá trình cấp phát VM, hệ thống cũng tạo ánh xạ giữa mã định danh băm Murmur 32 bit duy nhất của từng syscall và phần triển khai Rust nguyên bản tương ứng, nhờ đó tất cả syscall khả dụng đều được đăng ký. 

Quy trình sau diễn ra khi một chương trình thực thi lệnh CALL_IMM với hàm băm syscall:

  • VM tạm dừng luồng lệnh sBPF.
  • Bộ điều phối syscall dùng hàm băm để tìm phần triển khai trong sổ đăng ký.
  • Syscall xác minh bên gọi có các quyền cần thiết hay không (ví dụ: để thực hiện CPU, quyền sở hữu tài khoản).
  • Phần triển khai Rust của syscall được thực thi với toàn quyền truy cập runtime bên ngoài sandbox.
  • Chi phí cố định của syscall được trừ khỏi ngân sách tính toán còn lại.
  • Kết quả được đặt vào thanh ghi R0 và quá trình thực thi sBPF tiếp tục. 

Thực thi chương trình

Sau khi VM sBPF được cấp phát, quá trình thực thi bắt đầu tại hàm entrypoint của chương trình. Với các chương trình được biên dịch JIT, VM chuyển thẳng đến mã máy nguyên bản và để CPU của validator thực thi trực tiếp. Như đã trình bày trong phần trước, mã được biên dịch JIT bao gồm toàn bộ cơ chế giám sát cần thiết—kiểm tra giới hạn bộ nhớ, đo lường mức sử dụng tài nguyên tính toán và xác thực luồng điều khiển—tất cả đều được nội tuyến để đạt hiệu năng tối đa.

Thanh ghi R1 chứa con trỏ đến vùng dữ liệu đầu vào, nơi lưu ba tham số đã tuần tự hóa (tức là pubkey của chương trình được gọi, một mảng tài khoản và dữ liệu lệnh). 

Lưu ý: dữ liệu tài khoản được truy cập qua con trỏ thay vì được sao chép. Việc dùng con trỏ cho phép chương trình đọc và sửa đổi dữ liệu tài khoản tại chỗ, điều này rất quan trọng đối với hiệu năng.

Mỗi lời gọi hàm sẽ cấp phát một khung ngăn xếp mới có kích thước 4KB, đồng thời cập nhật thanh ghi R10 để trỏ đến khung mới. Mức sử dụng tài nguyên tính toán được đo theo ngân sách tính toán đã nêu ở trên.

Lời gọi liên chương trình (CPI)

Trong quá trình thực thi, các chương trình có thể gọi chương trình khác thông qua Lời gọi liên chương trình (CPI)—nền tảng cho khả năng kết hợp của SVM. 

Một CPI được khởi tạo thông qua syscall sol_invoke_signed, có chi phí 1.000 CU, cộng thêm chi phí dựa trên dữ liệu tài khoản đã tuần tự hóa được truyền vào. Cả dữ liệu tài khoản và quá trình tuần tự hóa dữ liệu lệnh đều có chi phí 250 byte cho mỗi CU.

Khi một chương trình thực hiện CPI, một ngữ cảnh thực thi mới với khung ngăn xếp lệnh riêng sẽ được tạo. Tại thời điểm viết bài, độ sâu tối đa của ngăn xếp lệnh là 5, hoặc 9 khi SIMD-0268 được bật. Điều này có nghĩa là một chương trình có thể gọi một chương trình khác, chương trình đó lại có thể gọi chương trình tiếp theo, cho đến giới hạn độ sâu. Mỗi lời gọi lồng nhau duy trì tập hợp tài khoản có thể ghi và đặc quyền người ký riêng. Một CPI có thể có tối đa 16 người ký và truyền vào 128 cấu trúc AccountInfo. 

Bên gọi tuần tự hóa ID chương trình đích, các tài khoản và dữ liệu lệnh, sau đó gọi syscall. Quá trình thực thi của chương trình hiện tại bị tạm dừng và một phiên bản VM sBPF mới được cấp phát cho chương trình được gọi, theo cùng quy trình cấp phát đã nêu ở trên. Sau đó, chương trình được gọi bắt đầu thực thi. Chương trình này chạy với ngân sách tính toán riêng được lấy từ phần ngân sách còn lại của bên gọi, nghĩa là các lời gọi CPI dùng chung tổng ngân sách tính toán của giao dịch.

Các chương trình có thể ký thay cho những tài khoản mà chúng sở hữu thông qua Địa chỉ bắt nguồn từ chương trình (PDA). Khi gọi bằng sol_invoke_signed, bên gọi cung cấp các seed chứng minh quyền sở hữu PDA. Quá trình dẫn xuất PDA được xác minh trước khi quyền ký được cấp cho chương trình được gọi. 

Khi chương trình được gọi hoàn tất, quyền điều khiển được trả về cho bên gọi. Các thay đổi tài khoản do chương trình được gọi thực hiện sẽ hiển thị với bên gọi, cho phép trạng thái truyền qua chuỗi lời gọi. Nếu bất kỳ chương trình nào trong chuỗi CPI thất bại, toàn bộ giao dịch sẽ bị hủy và mọi thay đổi trạng thái đều được hoàn tác.

Lưu ý: sol_invoke là một hàm trợ giúp gọi sol_invoke_signed mà không có seed.

Xác minh sau thực thi

Sau khi quá trình thực thi của chương trình hoàn tất, dù được thực thi trực tiếp hay là một phần của chuỗi CPI, một số bước kiểm tra sau thực thi sẽ diễn ra để bảo đảm tính nhất quán của trạng thái và các bất biến bảo mật. 

Ví dụ, runtime xác minh rằng mọi tài khoản được đánh dấu là có thể ghi thực sự thuộc sở hữu của chương trình hoặc đã được ký hợp lệ—chương trình không thể sửa đổi tài khoản mà mình không sở hữu, trừ khi tài khoản đó được đánh dấu rõ ràng là có thể ghi và chủ sở hữu đã cấp quyền. Điều này ngăn chặn các thay đổi trạng thái trái phép.

Runtime cũng kiểm tra để bảo đảm tổng số lamport trên tất cả tài khoản trong giao dịch không thay đổi, trừ khi lamport được chuyển rõ ràng thông qua các lệnh của System Program. Bước kiểm tra bảo toàn này ngăn chương trình tạo hoặc hủy lamport, qua đó giữ cho tổng nguồn cung SOL không đổi.

Runtime xác thực rằng tất cả tài khoản có cờ thực thi (tức là các chương trình) đều không bị sửa đổi. Dữ liệu của chương trình không thể bị thay đổi trong quá trình thực thi thông thường và chương trình chỉ có thể được nâng cấp thông qua cơ chế thẩm quyền nâng cấp của BPF Loader.

Kết quả thực thi

Sau khi quá trình xác minh sau thực thi hoàn tất, kết quả thực thi được trả về cho bộ xử lý giao dịch của Bank. Kết quả chứa trạng thái thành công hoặc lỗi (tương ứng là kết quả bằng 0 hoặc khác 0), số đơn vị tính toán đã tiêu thụ và mọi thay đổi được thực hiện đối với trạng thái tài khoản. 

Bank ghi nhận nguyên tử tất cả thay đổi tài khoản đối với những lần thực thi thành công. Dữ liệu tài khoản, số dư lamport và siêu dữ liệu đã cập nhật được ghi vào AccountsDB và trở nên khả dụng với các giao dịch tiếp theo. Số đơn vị tính toán đã tiêu thụ được ghi log để tính phí giao dịch và các chỉ số mạng.

Không có thay đổi trạng thái nào được ghi nhận đối với giao dịch thất bại—Solana không hoàn tác từng phần vì toàn bộ giao dịch đều được khôi phục về trạng thái trước đó. Tuy nhiên, phí giao dịch vẫn bị trừ khỏi tài khoản của người trả phí để bù đắp cho validator đối với khối lượng tính toán đã thực hiện. Mã lỗi và số đơn vị tính toán đã tiêu thụ được ghi lại trong siêu dữ liệu giao dịch để phục vụ gỡ lỗi và phân tích.

Kết quả thực thi được chuyển ngược qua bộ lập lịch Banking Stage. Bộ lập lịch cập nhật các chỉ số nội bộ rồi chuyển sang giao dịch tiếp theo. Cả giao dịch thành công lẫn thất bại đều được ghi vào luồng Proof of History và đóng góp vào block hiện đang được xây dựng. Các giao dịch thất bại cũng được đưa vào để giúp ngăn chặn tấn công phát lại và duy trì lịch sử giao dịch đầy đủ.

Khi slot hoàn tất và đạt số tick tối đa, Bank chuyển sang trạng thái đóng băng. Đóng băng là thao tác một chiều, ngăn các giao dịch mới được ghi nhận và tính toán hàm băm của Bank. Lưu ý rằng đóng băng không có nghĩa là đã hoàn tất—slot vẫn có thể nằm trên một fork bị loại bỏ. 

Bank trở thành root khi validator gọi BankForks::set_root() để chỉ định Bank đó là một phần của chuỗi chính tắc. Việc đặt root kích hoạt thao tác squash, làm phẳng trạng thái tài khoản của Bank đã được đặt root vào AccountsDB, hợp nhất toàn bộ trạng thái cha và biến trạng thái đó thành vĩnh viễn theo góc nhìn của validator. Các fork chưa được đặt root sẽ bị cắt bỏ và loại bỏ. Ngay cả các Bank đã được đặt root vẫn chưa được hoàn tất theo góc nhìn của cụm do Solana có nhiều mức cam kết khác nhau.

Hướng tới tương lai

Solana Virtual Machine đại diện cho một cách tiếp cận hoàn toàn khác đối với việc thực thi blockchain—một blockchain có khả năng mở rộng dành cho số đông nhờ xử lý song song, thị trường phí cục bộ và runtime hiệu năng cao, có tính xác định, bắt nguồn từ eBPF. 

Để hiểu SVM, cần xem xét toàn bộ quy trình thực thi, từ việc biên dịch mã nguồn Rust xuống LLVM, sBPF và cuối cùng là cấp phát các phiên bản VM cô lập. 

Không có một “đặc tả” duy nhất định nghĩa SVM. Thay vào đó, SVM hình thành từ sự tương tác giữa Bank, bộ lập lịch, các BPF Loader, VM sBPF và SVM ISA.

Tương lai rất hứa hẹn khi SVM tiếp tục phát triển. 

Toolchain Solana đang được đại tu toàn diện để loại bỏ hạ tầng LLVM tùy chỉnh vốn đã gây khó khăn cho quá trình làm quen của nhà phát triển trong nhiều năm. 

Cách tiếp cận hiện tại yêu cầu nhà phát triển cài đặt toolchain tùy chỉnh thông qua các script dành riêng cho từng nền tảng. Giải pháp là áp dụng cùng toolchain mà Aya, thư viện eBPF cho Rust, đang sử dụng. Nhà phát triển sẽ có thể chạy hai lệnh đơn giản để biên dịch trực tiếp thành bytecode eBPF:

Mã
rustup toolchain install nightly
cargo build --target=bpfel-unknown-none

Không script. Không fork LLVM tùy chỉnh. Chỉ cần công cụ Rust tiêu chuẩn biên dịch trực tiếp thành bytecode eBPF bằng target thượng nguồn bpfel-unknown-none, tận dụng vô số năm phát triển kernel Linux và cải tiến hạ tầng LLVM.

SVM ISA cũng dự kiến được cập nhật qua SIMD-0377, đề xuất điều chỉnh phần triển khai eBPF của Solana (tức sBPF) theo các tiêu chuẩn eBPF hiện đại. Nội dung này bao gồm việc giới thiệu các biến thể lệnh JMP32, phép chia và phép modulo có dấu, bước nhảy gián tiếp và khung ngăn xếp động. Những thay đổi này sẽ giúp giảm chi phí tính toán của chương trình, cải thiện khả năng tương thích với hạ tầng LLVM thượng nguồn và cho phép tạo mã hiệu quả hơn. 

Về cơ bản, SVM là một hệ thống được đo lường thông qua ngân sách tính toán. SIMD-0370 có thể thay đổi cách hoạt động này bằng việc loại bỏ giới hạn tính toán ở cấp block và có thể cả giới hạn giao dịch. Việc loại bỏ các giới hạn tính toán này sẽ cho phép nhà sản xuất block tối đa hóa thông lượng dựa trên khả năng phần cứng thay vì các giới hạn nhân tạo. Khi kết hợp với cơ chế timeout của Alpenglow, thay đổi này sẽ để các lực lượng thị trường, thay vì ràng buộc ở cấp giao thức, quyết định kích thước block tối ưu. Tất nhiên, đây là kế hoạch cho tương lai rất xa, vì trước tiên Anza muốn tăng giới hạn CU lên hơn 100 triệu trước khi loại bỏ các giới hạn đó.

Nền tảng của tất cả những thay đổi này là tinh thần của người xây dựng: không ngừng mở rộng giới hạn về những gì blockchain có thể đạt được mà không đánh đổi tính an toàn, tính xác định hay tính phi tập trung. 

SVM không chỉ đơn thuần là một trình thông dịch bytecode—đó là một quy trình thực thi hoàn chỉnh, tạo ra cuộc cách mạng về khả năng của blockchain. Đây là thành quả của các quyết định kiến trúc ưu tiên thông lượng và độ trễ thấp. Khi Solana trưởng thành, SVM sẽ tiếp tục phát triển để hỗ trợ các ứng dụng có hiệu năng cao và sử dụng vốn hiệu quả.

Giấc mơ về Thị trường vốn Internet đòi hỏi hạ tầng có khả năng đáp ứng các yêu cầu về thông lượng, độ trễ và chi phí của hệ thống tài chính toàn cầu. Solana Virtual Machine là một bước tiến quan trọng để hiện thực hóa tầm nhìn đó.

Tài nguyên bổ sung

Đăng ký nhận tin từ Helius

Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài

Hình ảnh phóng to