EVM vs. SVM: Warum Solana parallel läuft
1. Übersicht
Die Ethereum Virtual Machine (EVM) verarbeitet Transaktionen sequenziell – jeder Block ist eine Warteschlange aufeinanderfolgender Operationen. Die Solana Virtual Machine (SVM) macht das Gegenteil: Sie führt zehntausende Transaktionen pro Sekunde parallel auf mehreren CPU-Cores aus. Dieser Artikel erklärt, warum dieser Unterschied existiert – nicht weil die SVM „magisch schneller” ist, sondern weil sein Account-Modell der Laufzeit vorher offenbart, wer wen anfassen wird. Nach dem Lesen werden Sie den Sealevel-Mechanismus verstehen, den parallelen Scheduler in Java simulieren können und wissen, wann dieses Modell scheitert.
2. Das sequenzielle Ausführungsmodell der EVM
Die EVM speichert den gesamten State in einem Merkle Patricia Trie, der von allen Smart Contracts gemeinsam genutzt wird. Eine Transaktion, die die Funktion increment() eines Contracts A aufruft, deklariert nicht im Voraus, welche Storage-Slots Sie anfassen wird – das wird nur während der Ausführung bekannt, weil die EVM eine modifizierbare State Machine ist (Turing-complete), und Verzweigungen (if/else) können je nach Input-Daten entscheiden, in welche Speicherplätze geschrieben wird.
contract Counter {
uint256 public count;
function increment() public {
count += 1; // eindeutiger Speicherplatz, der Laufzeit ist nicht vorher bekannt
}
}
count ist ein fester Storage-Slot, aber die EVM-Laufzeit kann vorher – bevor Sie den Bytecode ausführt – nicht wissen, dass diese Funktion genau diesen Slot liest/schreibt, ohne es zu probieren. Weil es „vorher zu wissen” mit beliebigem Bytecode unmöglich ist, sind Ethereum-Clients (geth, Nethermind…) gezwungen, Transaktionen sequenziell innerhalb eines Blocks auszuführen und stellen sicher, dass die nachfolgende Transaktion immer den State sieht, der von der vorherigen aktualisiert wurde.
2.1. Warum Konflikte nicht vorhersehbar sind
Es gibt keinen praktikablen „Static Analysis”-Mechanismus, um alle Pfade des EVM-Bytecodes zu analysieren und vor dem Ausführen die Menge der betroffenen Storage-Slots herzuleiten. Einige Projekte wie Monad oder Block-STM (Aptos) lösen dies mit Optimistic Concurrency: Sie führen einfach parallel aus, erkennen Konflikte und führen erneut durch. Das ist eine Lösung vom Typ „laufen, dann prüfen” – ganz anders als die SVM-Vorgehensweise, die „vorher deklarieren, dann planen” ist.
EVM-Block (Standard Mainnet):
tx1 -> tx2 -> tx3 -> tx4 (sequenziell, eine nach der anderen)
3. Das explizite Account-Modell der SVM (Sealevel)
Solana zwingt jede Transaktion, die Accounts, die sie liest oder schreibt, explizit zu deklarieren, direkt in der Transaktionsnachricht, bevor sie ins Netzwerk gesendet wird. Die Laufzeit muss nicht probieren, um zu wissen – der Client hat es bereits gesagt.
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 ist ein Slice, das von außen übergeben wird – der Client muss genau angeben, welches Account counter_account ist, ob schreibbar oder nur lesbar, direkt beim Erstellen der Transaktion. Das Programm kann nicht „neue” Accounts während der Ausführung entdecken. Diese Einschränkung – so beschränkend sie scheint – ist genau das, was es Sealevel ermöglicht, Transaktionen parallel zu planen.
3.1. Writable/Readonly-Deklaration direkt in der Transaktion
Transaktionsnachricht (vereinfacht):
instruction: increment_counter
accounts:
- counter_pda (schreibbar)
- fee_payer (schreibbar, Unterzeichner)
- system_program (schreibgeschützt)
Die Laufzeit muss diese Metadaten nur lesen – braucht den Bytecode nicht auszuführen – um zu wissen, dass diese Transaktion in counter_pda und fee_payer schreibt und system_program liest. Das ist der Kernunterschied zur EVM.
4. Konflikterkennung und parallele Batch-Planung
Sobald die Laufzeit die Read-Set/Write-Set jeder Transaktion hat, wird das Problem der parallelen Planung zum bekannten Graph-Coloring-Problem: Zwei Transaktionen können gleichzeitig laufen, wenn ihre Account-Zugriffe nach Write-Write- oder Write-Read-Konflikt nicht überlappen.
public record BlockTransaction(String id, Set<String> writes, Set<String> reads) {}
writes und reads entsprechen den Accounts, die die Transaktion ändern bzw. nur lesen wird. Zwei Transaktionen konfligieren, wenn:
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());
}
}
Jede Bedingung erklären:
writes∩writesnicht leer → beide wollen in dasselbe Account schreiben → müssen sequenziell sein.writes∩readsnicht leer → eine Seite liest, die andere schreibt in dasselbe Account → müssen sequenziell sein.reads∩readsnicht leer → kein Problem, gleichzeitiges Lesen braucht keine exklusive Sperre.
4.1. Java-Simulation: Batches bilden und parallel ausführen
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 durchsucht die Transaktionswarteschlange, nimmt alle Transaktionen, die nicht die derzeit lockedAccounts berühren, in den Batch auf, und sperrt dann die Accounts, in die es schreibt. runBatch benutzt Virtual Threads (Java 21), um den ganzen Batch gleichzeitig auszuführen, join wartet, bis der ganze Batch fertig ist, bevor der nächste Batch geplant wird – genau wie Sealevel den Scheduler macht, indem es Transaktionen nach „Wave” gruppiert.
Hinweis: Das ist eine vereinfachte Simulation, um die Idee zu veranschaulichen. Der echte Sealevel nutzt einen komplexeren Algorithmus (Dependency Graph pro Account, nicht nur sequenzielle Batches), um Parallelität zu maximieren.
4.2. Konfliktlogik mit JUnit 5 verifizieren
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();
}
}
Diese drei Tests beschreiben genau das Prinzip: Lesen teilt keine Sperre, Schreiben-Schreiben sperrt, Lesen-Schreiben sperrt – das Fundament des ganzen SVM-Parallelisierungsproblems.
5. EVM vs. SVM Vergleich
| Kriterium | EVM | SVM |
|---|---|---|
| State-Modell | Ein gemeinsamer Trie, impliziter Zugriff über Bytecode | Diskrete Accounts, explizite Deklaration in der Transaktion |
| Konflikt bekannt wann | Nur nach Ausführung bekannt (oder durch Versuchsausführung) | Vor Ausführung bekannt, aus Transaction-Metadaten |
| Ausführung | Sequenziell nach Blockreihenfolge | Parallel in nicht überlappenden Batches |
| Cross-Contract-Aufruf | Beliebigen anderen Contract zur Laufzeit aufrufen | Cross-Program Invocation (CPI) muss Accounts trotzdem vorher deklarieren |
| Entwickler-Verantwortung | Account-Sets nicht berücksichtigen | Accounts korrekt und vollständig deklarieren – unvollständig = Fehler, über-deklariert = Parallelität verloren |
| Optimierungsrichtung | Optimistic Concurrency (Block-STM, Monad) | Dependency-Graph-Scheduling optimieren (Sealevel, Firedancer) |
6. Trade-offs und häufige Fehler bei der Solana-Programmierung
Explizite Account-Deklaration ist die Quelle der Parallelität, aber auch Quelle von zwei häufigen Fehlern:
Account-Deklaration unvollständig. Wenn die Programmlogik in ein Account schreiben muss, das der Client nicht in der Transaktion aufgelistet hat, schlägt das Programm zur Laufzeit mit einem Fehler fehl wie „Account nicht in Accounts-Liste gefunden” – weil die SVM keine „Account-Entdeckung” während der Ausführung erlaubt wie die EVM.
Über-Deklaration oder Hotspot-Erstellung. Wenn zu viele Transaktionen in dasselbe Account schreiben (z.B. einen einzigen Liquiditäts-Pool, einen globalen Zähler), müssen sie sequenzialisiert werden – genau wie bei der EVM, nur dass Sie es selbst verursacht haben. Das ist ein häufiger Denkfehler von EVM-gewöhnten Entwicklern: ein „globales Variable”-Muster nach Solana bringen, ohne Accounts nach Shard zu teilen.
Falsch: Alle Benutzertransaktionen schreiben in `global_stats` -> Parallelität verloren
Richtig: Jeder Benutzer hat sein eigenes Account (PDA pro Benutzer), nur periodisch aggregieren
7. Wenn Parallelisierung nicht hilft
Beachte: Wenn Ihre Anwendung von Natur aus eine echte sequenzielle Warteschlange ist (z.B. Auktion mit Preisberechnung nach Gebots-Reihenfolge), hilft Parallelisierung auf Runtime-Ebene nicht – Sie müssen die Geschäftslogik selbst serialisieren; SVM kann nur bei unabhängigen Transaktionen sparen.
Ähnlich, wenn Sie ein zentralisiertes System (keine Blockchain) entwerfen, aber das Sealevel-Modell „erlernen” möchten, um Durchsatz zu erhöhen, lohnt es sich nur, wenn die meisten Aufgaben klare getrennte Read/Write-Sets haben. Wenn jede Aufgabe auf ein gemeinsames Feld/Record zugreift, kostet die Dependency-Graph-Verwaltung mehr als einfaches sequenzielles Locking.
8. Fazit
Die EVM wird sequenziell ausgeführt, weil ihr Bytecode der Laufzeit nicht vorher mitteilt, wer mit wem konfligiert – alle Konfliktvorhersagen können nur durch Versuchsausführung oder Rollback erfolgen. Die SVM wird parallel ausgeführt, weil es jede Transaktion zwingt, Lese-/Schreib-Accounts von Anfang an zu deklarieren, wodurch Sealevel einen Dependency-Graph erstellen kann und nicht überlappende Batches bildet, um auf mehreren Cores auszuführen. Der Preis ist, dass Solana-Entwickler Accounts selbst trennbar designen müssen und Hotspots vermeiden – sonst verwandeln Sie die SVM versehentlich zurück in eine langsame sequenzielle EVM.