EVM vs SVM: vì sao Solana chạy song song được
1. Tổng quan
Ethereum Virtual Machine (EVM) xử lý transaction tuần tự, mỗi block là một hàng đợi các thao tác nối tiếp nhau. Solana Virtual Machine (SVM) thì ngược lại: chạy song song hàng chục nghìn transaction mỗi giây trên nhiều core CPU. Bài này giải thích tại sao lại có sự khác biệt đó — không phải vì SVM “nhanh hơn” một cách ma thuật, mà vì mô hình account của nó cho runtime biết trước ai sẽ đụng ai. Sau khi đọc, bạn sẽ hiểu cơ chế Sealevel, tự mô phỏng được bộ lập lịch song song bằng Java, và biết khi nào mô hình này thất bại.
2. Mô hình thực thi tuần tự của EVM
EVM lưu toàn bộ state trong một cây Merkle Patricia Trie dùng chung cho mọi smart contract. Một transaction gọi hàm increment() của contract A không khai báo trước nó sẽ đụng đến storage slot nào — điều đó chỉ được biết trong lúc chạy, vì EVM là máy trạng thái tùy biến (Turing-complete), nhánh rẽ (if/else) có thể quyết định ghi vào slot nào tùy dữ liệu input.
contract Counter {
uint256 public count;
function increment() public {
count += 1; // slot storage duy nhất, không khai báo trước cho ai biết
}
}
count là một storage slot cố định, nhưng runtime EVM không có cách nào biết trước — trước khi thực thi bytecode — rằng hàm này sẽ đọc/ghi đúng slot đó mà không chạy thử. Vì việc “biết trước” là bất khả thi với bytecode tùy biến, máy khách Ethereum (geth, Nethermind…) buộc phải thực thi transaction tuần tự trong một block, đảm bảo transaction sau luôn thấy state đã được transaction trước cập nhật.
2.1. Vì sao không thể đoán trước xung đột
Không có cơ chế “static analysis” đủ rẻ để phân tích mọi đường đi của bytecode EVM và suy ra tập storage slot bị ảnh hưởng trước khi chạy. Một số dự án như Monad hay Block-STM (Aptos) giải quyết bằng optimistic concurrency: cứ chạy song song, nếu phát hiện đụng độ thì rollback và chạy lại. Đó là giải pháp “chạy trước, kiểm tra sau” — khác hẳn cách SVM làm, là “khai báo trước, lập lịch sau”.
Block EVM (mặc định mainnet):
tx1 -> tx2 -> tx3 -> tx4 (tuần tự, từng cái một)
3. Mô hình tài khoản tường minh của SVM (Sealevel)
Solana bắt mọi transaction phải khai báo tường minh danh sách account nó sẽ đọc hoặc ghi, ngay trong transaction message, trước khi gửi lên mạng. Runtime không cần chạy thử để biết — client đã nói rõ.
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let mut data = counter_account.try_borrow_mut_data()?;
let mut count = u64::from_le_bytes(data[0..8].try_into().unwrap());
count += 1;
data[0..8].copy_from_slice(&count.to_le_bytes());
Ok(())
}
accounts là slice được truyền vào từ bên ngoài — client phải chỉ rõ counter_account là account nào, writable hay chỉ đọc, ngay khi build transaction. Program không được tự “khám phá” thêm account khác giữa đường. Chính ràng buộc này — tưởng như gò bó — mới là thứ cho phép Sealevel lập lịch song song.
3.1. Khai báo writable/readonly ngay trong transaction
Transaction message (giản lược):
instruction: increment_counter
accounts:
- counter_pda (writable)
- fee_payer (writable, signer)
- system_program (readonly)
Runtime chỉ cần đọc metadata này — không cần thực thi bytecode — để biết transaction này sẽ ghi vào counter_pda và fee_payer, đọc system_program. Đây là điểm khác biệt cốt lõi so với EVM.
4. Cơ chế phát hiện xung đột và lập batch song song
Khi runtime đã có read-set/write-set của từng transaction, bài toán lập lịch song song trở về bài toán graph coloring quen thuộc: hai transaction có thể chạy đồng thời nếu tập account chúng truy cập không giao nhau theo kiểu write-write hoặc write-read.
public record BlockTransaction(String id, Set<String> writes, Set<String> reads) {}
writes và reads tương ứng account mà transaction sẽ sửa và chỉ đọc. Hai transaction đụng độ khi:
public final class SealevelScheduler {
public static boolean conflicts(BlockTransaction a, BlockTransaction b) {
return !Collections.disjoint(a.writes(), b.writes())
|| !Collections.disjoint(a.writes(), b.reads())
|| !Collections.disjoint(a.reads(), b.writes());
}
}
Giải thích từng điều kiện:
writes∩writeskhác rỗng → cả hai muốn ghi cùng account → phải tuần tự.writes∩readskhác rỗng → một bên đọc, một bên ghi cùng account → phải tuần tự.reads∩readskhác rỗng → không sao, đọc đồng thời không cần khóa độc quyền.
4.1. Mô phỏng bằng Java: nhóm batch rồi chạy song song
public final class ParallelExecutor {
private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
public List<BlockTransaction> nextNonConflictingBatch(
Deque<BlockTransaction> queue, Set<String> lockedAccounts) {
List<BlockTransaction> batch = new ArrayList<>();
for (BlockTransaction tx : queue) {
boolean overlaps = Stream.concat(tx.writes().stream(), tx.reads().stream())
.anyMatch(lockedAccounts::contains);
if (!overlaps) {
batch.add(tx);
lockedAccounts.addAll(tx.writes());
}
}
queue.removeAll(batch);
return batch;
}
public void runBatch(List<BlockTransaction> batch, Consumer<BlockTransaction> execute) {
List<CompletableFuture<Void>> futures = batch.stream()
.map(tx -> CompletableFuture.runAsync(() -> execute.accept(tx), pool))
.toList();
futures.forEach(CompletableFuture::join);
}
}
nextNonConflictingBatch quét hàng đợi transaction, nhận transaction nào không chạm lockedAccounts hiện tại vào batch, rồi khóa thêm account mà nó ghi. runBatch dùng virtual thread (Java 21) chạy toàn bộ batch đồng thời, join chờ cả batch xong rồi mới lập batch kế tiếp — đúng với cách Sealevel scheduler nhóm transaction theo “wave”.
Lưu ý: đây là mô phỏng đơn giản để minh họa ý tưởng. Sealevel thật dùng thuật toán phức tạp hơn (dependency graph theo từng account, không chỉ theo batch tuần tự) để tối đa hóa độ song song.
4.2. Kiểm chứng logic conflict bằng JUnit 5
class SealevelSchedulerTest {
@Test
void khongDungAccountThiKhongConflict() {
var tx1 = new BlockTransaction("tx1", Set.of("walletA"), Set.of());
var tx2 = new BlockTransaction("tx2", Set.of("walletB"), Set.of());
assertThat(SealevelScheduler.conflicts(tx1, tx2)).isFalse();
}
@Test
void cungGhiMotAccountThiConflict() {
var tx1 = new BlockTransaction("tx1", Set.of("poolAccount"), Set.of());
var tx2 = new BlockTransaction("tx2", Set.of("poolAccount"), Set.of());
assertThat(SealevelScheduler.conflicts(tx1, tx2)).isTrue();
}
@Test
void docChungThiKhongConflict() {
var tx1 = new BlockTransaction("tx1", Set.of(), Set.of("priceOracle"));
var tx2 = new BlockTransaction("tx2", Set.of(), Set.of("priceOracle"));
assertThat(SealevelScheduler.conflicts(tx1, tx2)).isFalse();
}
}
Ba test này đúng chính xác mô tả nguyên lý read-read không khóa, write-write khóa, write-read khóa — nền tảng của toàn bộ bài toán song song hóa SVM.
5. So sánh EVM vs SVM
| Tiêu chí | EVM | SVM |
|---|---|---|
| Mô hình state | Một trie chung, truy cập ngầm qua bytecode | Account rời rạc, khai báo tường minh trong transaction |
| Biết xung đột khi nào | Chỉ biết sau khi chạy (hoặc chạy thử) | Biết trước khi chạy, từ metadata transaction |
| Thực thi | Tuần tự theo thứ tự trong block | Song song theo batch không giao nhau |
| Cross-contract call | Gọi contract khác tùy ý lúc runtime | Cross-Program Invocation (CPI) vẫn phải khai báo account trước |
| Trách nhiệm dev | Không cần nghĩ về account set | Phải khai đúng, đủ account — khai thiếu sẽ lỗi, khai dư sẽ mất song song |
| Hướng cải thiện song song | Optimistic concurrency (Block-STM, Monad) | Tối ưu lập lịch dependency graph (Sealevel, Firedancer) |
6. Đánh đổi và lỗi hay gặp khi viết program Solana
Khai báo account tường minh là nguồn gốc của song song, nhưng cũng là nguồn gốc của hai lỗi phổ biến:
Khai thiếu account. Nếu logic trong program cần ghi vào một account mà client không liệt kê trong transaction, chương trình fail ngay lúc runtime với lỗi kiểu “account not found trong accounts list” — vì SVM không cho phép “khám phá” account giữa đường như EVM.
Khai dư hoặc tạo hotspot. Nếu quá nhiều transaction đều ghi vào cùng một account (ví dụ một pool thanh khoản duy nhất, một global counter), chúng buộc phải tuần tự hóa — đúng y hệt tình huống EVM, chỉ khác là bạn tự tạo ra nó. Đây là lỗi tư duy phổ biến của dev quen EVM: mang quán tính “một biến global” sang Solana mà không tách account theo shard.
Sai: mọi user giao dịch đều ghi vào `global_stats` -> mất song song
Đúng: mỗi user có account riêng (PDA theo user), chỉ tổng hợp định kỳ
7. Khi nào song song không giúp được gì
Lưu ý: nếu ứng dụng của bạn về bản chất là một hàng đợi tuần tự thật (ví dụ auction tính giá theo thứ tự nộp lệnh), việc song song hóa ở tầng runtime không giúp gì — bạn vẫn phải tự serialize logic nghiệp vụ, SVM chỉ tiết kiệm được phần các transaction độc lập với nhau.
Tương tự, nếu bạn đang thiết kế hệ thống centralized (không phải blockchain) nhưng muốn “học” mô hình Sealevel để tăng throughput, nó chỉ đáng áp dụng khi phần lớn tác vụ có read/write set tách biệt rõ ràng. Nếu mọi tác vụ đều đụng vào một bảng/record chung, chi phí quản lý dependency graph còn tốn hơn khóa tuần tự đơn giản.
8. Kết luận
EVM chạy tuần tự vì bytecode của nó không cho runtime biết trước ai sẽ đụng ai — mọi suy đoán xung đột chỉ có thể làm được bằng cách chạy thử hoặc rollback. SVM chạy song song vì buộc mọi transaction khai báo account đọc/ghi ngay từ đầu, cho phép Sealevel dựng dependency graph và lập batch không giao nhau để chạy trên nhiều core. Đánh đổi là dev Solana phải tự thiết kế account tách biệt, tránh tạo hotspot — nếu không, bạn vô tình biến SVM trở lại thành một EVM tuần tự chậm chạp.