EVM vs SVM: por que Solana consegue executar em paralelo
1. Visão geral
A Ethereum Virtual Machine (EVM) processa transações sequencialmente, cada bloco é uma fila de operações encadeadas. A Solana Virtual Machine (SVM) é o oposto: executa dezenas de milhares de transações por segundo em paralelo em múltiplos cores da CPU. Este artigo explica por que existe essa diferença — não porque SVM é “mais rápido” por magia, mas porque seu modelo de contas deixa o runtime saber antecipadamente quem vai se colidir com quem. Após ler, você entenderá o mecanismo Sealevel, conseguirá simular sozinho o agendador paralelo em Java, e saberá quando esse modelo falha.
2. Modelo de execução sequencial da EVM
A EVM armazena todo o estado em uma árvore Merkle Patricia Trie compartilhada entre todos os contratos inteligentes. Uma transação que chama a função increment() do contrato A não declara antecipadamente qual slot de armazenamento vai acessar — isso só é conhecido durante a execução, porque a EVM é uma máquina de estados Turing-completa, as ramificações (if/else) podem decidir em qual slot escrever dependendo dos dados de entrada.
contract Counter {
uint256 public count;
function increment() public {
count += 1; // slot de armazenamento único, não declarado antecipadamente para ninguém
}
}
count é um slot de armazenamento fixo, mas o runtime da EVM não tem como saber antecipadamente — antes de executar o bytecode — que essa função vai ler/escrever nesse slot específico sem executá-la primeiro. Como “saber antecipadamente” é impossível com bytecode arbitrário, os clientes Ethereum (geth, Nethermind…) são forçados a executar transações sequencialmente dentro de um bloco, garantindo que a transação seguinte sempre veja o estado atualizado pela transação anterior.
2.1. Por que não é possível prever conflitos
Não há mecanismo de “análise estática” barato o suficiente para analisar todos os caminhos do bytecode da EVM e deduzir o conjunto de slots de armazenamento afetados antes da execução. Alguns projetos como Monad e Block-STM (Aptos) resolvem isso com concorrência otimista: simplesmente executam em paralelo, e se detectarem uma colisão, fazem rollback e executam novamente. É uma solução “execute primeiro, verifique depois” — completamente diferente de como SVM funciona, que é “declare antecipadamente, agende depois”.
Bloco EVM (padrão na mainnet):
tx1 -> tx2 -> tx3 -> tx4 (sequencial, um de cada vez)
3. Modelo de contas explícitas da SVM (Sealevel)
Solana força toda transação a declarar explicitamente a lista de contas que vai ler ou escrever, logo na mensagem da transação, antes de enviá-la para a rede. O runtime não precisa executar para descobrir — o cliente já deixou claro.
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 é um slice passado de fora — o cliente deve especificar qual é a counter_account, se é gravável ou apenas leitura, logo ao construir a transação. O programa não tem permissão para “descobrir” outras contas no meio do caminho. É justamente essa restrição — que parece limitadora — que permite ao Sealevel fazer agendamento paralelo.
3.1. Declaração de contas graváveis/apenas leitura na transação
Mensagem de transação (simplificada):
instruction: increment_counter
accounts:
- counter_pda (gravável)
- fee_payer (gravável, assinante)
- system_program (apenas leitura)
O runtime só precisa ler esses metadados — sem necessidade de executar bytecode — para saber que essa transação vai escrever em counter_pda e fee_payer, e ler system_program. Essa é a diferença fundamental em relação à EVM.
4. Mecanismo de detecção de conflitos e agendamento em lotes paralelos
Uma vez que o runtime tem o read-set/write-set de cada transação, o problema de agendamento paralelo se reduz ao conhecido problema de coloração de grafos: duas transações podem executar simultaneamente se o conjunto de contas que acessam não se sobrepõem em padrões write-write ou write-read.
public record BlockTransaction(String id, Set<String> writes, Set<String> reads) {}
writes e reads correspondem às contas que a transação vai modificar e apenas ler. Duas transações colidem quando:
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());
}
}
Explicação de cada condição:
writes∩writesnão vazio → ambas querem escrever na mesma conta → devem ser sequenciais.writes∩readsnão vazio → uma lê, uma escreve na mesma conta → devem ser sequenciais.reads∩readsnão vazio → sem problema, leituras simultâneas não precisam de lock exclusivo.
4.1. Simulação em Java: agrupe em lotes e execute em paralelo
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 varre a fila de transações, coloca as que não tocam lockedAccounts atuais no lote, depois bloqueia as contas que ela grava. runBatch usa virtual threads (Java 21) para executar todo o lote simultaneamente, join aguarda o lote terminar antes de criar o lote seguinte — exatamente como o agendador Sealevel agrupa transações em “ondas”.
Nota: essa é uma simulação simplificada para ilustrar a ideia. O Sealevel real usa algoritmos mais complexos (grafo de dependências por conta, não apenas por lotes sequenciais) para maximizar o paralelismo.
4.2. Verificação da lógica de colisão com 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();
}
}
Esses três testes descrevem com precisão o princípio de read-read sem lock, write-write com lock, write-read com lock — a base de todo o problema de paralelização da SVM.
5. Comparação EVM vs SVM
| Critério | EVM | SVM |
|---|---|---|
| Modelo de estado | Uma trie compartilhada, acesso implícito via bytecode | Contas discretas, declaração explícita na transação |
| Quando sabe sobre conflito | Só descobre após executar (ou tentar executar) | Sabe antes de executar, pelos metadados da transação |
| Execução | Sequencial conforme a ordem no bloco | Paralela em lotes sem sobreposição |
| Chamada entre contratos | Chama outro contrato arbitrariamente em tempo de execução | Cross-Program Invocation (CPI) ainda precisa declarar as contas antecipadamente |
| Responsabilidade do dev | Não precisa pensar sobre conjunto de contas | Deve declarar corretamente e completamente as contas — declarar de menos causa erro, declarar de mais perde paralelismo |
| Direção de melhoria em paralelismo | Concorrência otimista (Block-STM, Monad) | Otimização do agendamento de grafo de dependências (Sealevel, Firedancer) |
6. Tradeoffs e erros comuns ao escrever programas Solana
Declaração explícita de contas é a origem do paralelismo, mas também é a origem de dois erros comuns:
Contas não declaradas. Se a lógica do programa precisa escrever em uma conta que o cliente não listou na transação, o programa falha imediatamente em tempo de execução com um erro do tipo “account not found in accounts list” — porque SVM não permite “descobrir” contas no meio do caminho como EVM.
Contas redundantes ou hotspots. Se muitas transações escrevem na mesma conta (por exemplo, um único pool de liquidez, um contador global), elas são forçadas a serializar — exatamente como na situação da EVM, só que você mesmo que criou isso. Esse é um erro de pensamento comum de devs acostumados com EVM: levar a inércia de “uma variável global” para Solana sem separar contas por shard.
Errado: todos os usuários escrevem em `global_stats` -> perde paralelismo
Certo: cada usuário tem sua própria conta (PDA por usuário), apenas agrega periodicamente
7. Quando o paralelismo não ajuda
Nota: se seu aplicativo é fundamentalmente uma fila sequencial real (por exemplo, um leilão que precisa precificar na ordem de submissão), paralelizar no nível de runtime não ajuda — você ainda precisa serializar sua lógica de negócio, SVM apenas economiza na parte de transações independentes umas das outras.
Da mesma forma, se você está projetando um sistema centralizado (não blockchain) mas quer “aprender” o modelo Sealevel para aumentar throughput, é válido apenas quando a maioria das tarefas tem read/write sets claramente separados. Se toda tarefa toca na mesma tabela/registro compartilhado, o custo de gerenciar um grafo de dependências é maior que um simples lock sequencial.
8. Conclusão
A EVM executa sequencialmente porque seu bytecode não deixa o runtime saber antecipadamente quem vai colidir com quem — toda previsão de conflito só pode ser feita executando ou fazendo rollback. SVM executa em paralelo porque força cada transação a declarar contas de leitura/escrita desde o início, permitindo que Sealevel construa um grafo de dependências e crie lotes sem sobreposição para executar em múltiplos cores. O tradeoff é que devs de Solana precisam projetar contas separadas, evitando criar hotspots — senão, você inadvertidamente transforma SVM de volta em um EVM sequencial e lento.