EVM vs SVM: por qué Solana puede ejecutarse en paralelo

1. Descripción general

Ethereum Virtual Machine (EVM) procesa transacciones secuencialmente, cada bloque es una cola de operaciones encadenadas una tras otra. Solana Virtual Machine (SVM) es lo opuesto: ejecuta decenas de miles de transacciones por segundo en paralelo en múltiples núcleos de CPU. Este artículo explica por qué existe esa diferencia — no es porque SVM sea “más rápido” de forma mágica, sino porque su modelo de cuenta permite al runtime saber de antemano quién tocará a quién. Después de leer, entenderás el mecanismo de Sealevel, podrás simular el planificador paralelo en Java, y sabrás cuándo falla este modelo.

2. Modelo de ejecución secuencial de EVM

EVM almacena todo el estado en un árbol Merkle Patricia Trie compartido por todos los contratos. Una transacción que llama a la función increment() del contrato A no declara de antemano qué slot de almacenamiento tocará — eso solo se sabe durante la ejecución, porque EVM es una máquina de estados personalizable (Turing-complete), las bifurcaciones (if/else) pueden decidir escribir en diferentes slots según los datos de entrada.

contract Counter {
    uint256 public count;

    function increment() public {
        count += 1; // único slot de almacenamiento, no declarado de antemano para nadie
    }
}

count es un slot de almacenamiento fijo, pero el runtime de EVM no tiene forma de saberlo — antes de ejecutar el bytecode — que esta función leerá/escribirá exactamente en ese slot sin ejecutarlo primero. Dado que “saber de antemano” es imposible con bytecode personalizable, el cliente Ethereum (geth, Nethermind…) se ve obligado a ejecutar transacciones secuencialmente en un bloque, garantizando que cada transacción posterior siempre vea el estado actualizado por la transacción anterior.

2.1. Por qué no se pueden predecir los conflictos

No hay un mecanismo de “análisis estático” lo suficientemente barato para analizar todos los caminos del bytecode de EVM e inferir el conjunto de slots de almacenamiento afectados antes de ejecutar. Algunos proyectos como Monad o Block-STM (Aptos) lo resuelven mediante optimistic concurrency: simplemente ejecuta en paralelo, si detectas un conflicto, retrocede y vuelve a ejecutar. Es una solución “ejecuta primero, verifica después” — completamente diferente a cómo lo hace SVM, que es “declara primero, planifica después”.

Bloque EVM (por defecto en mainnet):
tx1 -> tx2 -> tx3 -> tx4   (secuencial, uno tras otro)

3. Modelo de cuenta explícita de SVM (Sealevel)

Solana obliga a cada transacción declarar explícitamente la lista de cuentas que leerá o escribirá, directamente en el mensaje de transacción, antes de enviar a la red. El runtime no necesita ejecutarlo para saber — el cliente ya lo ha dicho claramente.

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 es un slice que se pasa desde el exterior — el cliente debe especificar exactamente qué es counter_account, si es escribible o solo lectura, justo cuando construye la transacción. El programa no puede “descubrir” cuentas adicionales sobre la marcha. Precisamente esta restricción — aparentemente limitante — es lo que permite a Sealevel planificar en paralelo.

3.1. Declarar writable/readonly directamente en la transacción

Mensaje de transacción (simplificado):
  instruction: increment_counter
  accounts:
    - counter_pda      (writable)
    - fee_payer        (writable, signer)
    - system_program    (readonly)

El runtime solo necesita leer estos metadatos — sin necesidad de ejecutar bytecode — para saber que esta transacción escribirá en counter_pda y fee_payer, leerá system_program. Este es el punto clave de diferencia con EVM.

4. Mecanismo de detección de conflictos y planificación de lotes en paralelo

Una vez que el runtime tiene el read-set/write-set de cada transacción, el problema de planificación paralela se reduce al conocido problema de graph coloring: dos transacciones pueden ejecutarse simultáneamente si los conjuntos de cuentas que acceden no se intersecan de manera write-write o write-read.

public record BlockTransaction(String id, Set<String> writes, Set<String> reads) {}

writes y reads corresponden a las cuentas que la transacción modificará y solo leerá. Dos transacciones entran en conflicto cuando:

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());
    }
}

Explicación de cada condición:

  • writes ∩ writes no vacío → ambos quieren escribir en la misma cuenta → deben ser secuenciales.
  • writes ∩ reads no vacío → uno lee, otro escribe en la misma cuenta → deben ser secuenciales.
  • reads ∩ reads no vacío → está bien, lectura simultánea no requiere bloqueo exclusivo.

4.1. Simulación en Java: agrupar lotes y ejecutar en 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 recorre la cola de transacciones, toma las que no toquen lockedAccounts actuales para el lote, y luego bloquea las cuentas adicionales que escribirá. runBatch usa virtual threads (Java 21) para ejecutar todo el lote simultáneamente, join espera a que se complete el lote antes de crear el siguiente — exacto cómo el planificador de Sealevel agrupa transacciones por “waves”.

Nota: esta es una simulación simplificada para ilustrar la idea. El Sealevel real usa algoritmos más complejos (dependency graph por cuenta, no solo por lotes secuenciales) para maximizar el paralelismo.

4.2. Verificar la lógica de conflicto con JUnit 5

class SealevelSchedulerTest {

    @Test
    void sinCuentasEnComun_sinConflicto() {
        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 mismaEscrituraEnCuenta_hayConflicto() {
        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 lecturaDeLasMismasCuentas_sinConflicto() {
        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();
    }
}

Estas tres pruebas describen exactamente el principio de que lectura-lectura no se bloquea, escritura-escritura se bloquea, escritura-lectura se bloquea — la base de todo el problema de paralelización de SVM.

5. Comparación EVM vs SVM

Criterio EVM SVM
Modelo de estado Un trie compartido, acceso implícito a través de bytecode Cuentas discretas, declaradas explícitamente en la transacción
Cuándo se detectan conflictos Solo después de ejecutar (o ejecutar en prueba) Antes de ejecutar, desde los metadatos de la transacción
Ejecución Secuencial en el orden del bloque Paralela en lotes sin intersecciones
Llamada entre contratos Llamar a otro contrato arbitrariamente en tiempo de ejecución Cross-Program Invocation (CPI) aún debe declarar cuentas de antemano
Responsabilidad del desarrollador No necesita pensar en el conjunto de cuentas Debe declarar correctamente y completamente las cuentas — declarar de menos causa error, declarar de más reduce paralelismo
Dirección de mejora en paralelismo Optimistic concurrency (Block-STM, Monad) Optimizar la planificación del dependency graph (Sealevel, Firedancer)

6. Compensaciones y errores comunes al escribir programas en Solana

La declaración explícita de cuentas es el origen del paralelismo, pero también es el origen de dos errores comunes:

Declarar cuentas de menos. Si la lógica dentro del programa necesita escribir en una cuenta que el cliente no ha incluido en la transacción, el programa falla inmediatamente en tiempo de ejecución con un error tipo “cuenta no encontrada en la lista de cuentas” — porque SVM no permite “descubrir” cuentas sobre la marcha como EVM.

Declarar de más o crear hotspots. Si demasiadas transacciones escriben en la misma cuenta (por ejemplo, un pool de liquidez único, un contador global), se ven obligadas a serializarse — exactamente como en EVM, solo que te lo hiciste a ti mismo. Este es un error de pensamiento común en desarrolladores acostumbrados a EVM: traer la mentalidad de “una variable global” a Solana sin fragmentar cuentas por shard.

Incorrecto: todos los usuarios intercambian en la misma `global_stats` -> pierde paralelismo
Correcto: cada usuario tiene su propia cuenta (PDA por usuario), solo agregar periódicamente

7. Cuándo el paralelismo no ayuda

Nota: si tu aplicación es por naturaleza una cola estrictamente secuencial (por ejemplo, una subasta donde los precios se calculan por orden de entrada), el paralelismo en la capa de runtime no ayuda — todavía debes serializar la lógica empresarial por ti mismo, SVM solo ahorra para transacciones independientes entre sí.

Del mismo modo, si estás diseñando un sistema centralizado (no blockchain) pero quieres “aprender” el modelo de Sealevel para aumentar throughput, solo vale la pena aplicarlo cuando la mayoría de operaciones tienen read/write sets claramente separados. Si cada operación toca la misma tabla/registro compartido, el costo de gestionar el dependency graph es mayor que un simple bloqueo secuencial.

8. Conclusión

EVM se ejecuta secuencialmente porque su bytecode no permite al runtime saber de antemano quién tocará a quién — toda predicción de conflictos solo puede hacerse ejecutando o revirtiendo. SVM se ejecuta en paralelo porque obliga a cada transacción a declarar qué cuentas leerá/escribirá desde el principio, permitiendo a Sealevel construir un dependency graph y agrupar lotes sin intersecciones para ejecutar en múltiples núcleos. La compensación es que los desarrolladores de Solana deben diseñar cuentas fragmentadas, evitar crear hotspots — si no lo hacen, accidentalmente convierten a SVM en una EVM lenta y secuencial.

svmevmsolanablockchaincomputacion-paralela