Esta investigación compara sistemas de implementación similares a Ethereum y analiza las dificultades y posibilidades de lograr la ejecución paralela de transacciones.
Vale la pena señalar que las cadenas analizadas para esta investigación se basan en el esquema de diseño del modelo Cuenta, sin incluir el esquema UTXO.
FISCO-BCOS, una de las cadenas de bloques del consorcio que admite la ejecución paralela de la verificación de transacciones dentro de bloques.
Cadena pública Khipu, implementación scala del protocolo Ethereum.
Cadena pública de Aptos, Move Virtual Machine.
Echemos un vistazo al proceso tradicional de ejecución de transacciones.
El módulo de ejecución saca cada transacción del bloque y la ejecuta secuencialmente.
El último estado mundial se modificará durante el proceso de ejecución, y luego el estado se sumará después de la finalización de una transacción para alcanzar el último estado mundial después de la finalización del bloque.
La ejecución del siguiente bloque depende estrictamente del estado mundial del bloque actual/anterior, por lo tanto, este proceso de ejecución secuencial de un solo subproceso no es muy adecuado para la ejecución en paralelo.
A continuación, se muestran los principales conflictos en los métodos de ejecución paralelos actuales de Ethereum:
Conflicto de almacenamiento de la misma dirección : donde ambos contratos han modificado el almacenamiento de la misma variable global.
Conflicto de llamadas entre contratos: si el contrato A se implementa primero, el contrato B debe esperar hasta que se complete la implementación del contrato A para llamar al contrato A. Sin embargo, cuando las transacciones son paralelas, no existe tal secuencia, lo que genera un conflicto.
FISCO-BCOS 2.0 utiliza una estructura gráfica en el procesamiento de transacciones. Los desarrolladores diseñaron un ejecutor de transacciones paralelas (PTE) basado en el modelo de gráfico acíclico dirigido (DAG).
PTE puede ayudarlo a aprovechar al máximo las ventajas de un procesador multinúcleo para que las transacciones en el bloque se puedan ejecutar en paralelo en la medida de lo posible.
Al mismo tiempo, proporciona una interfaz de programación simple y amigable para el usuario, para que el usuario no tenga que preocuparse por los tediosos detalles de la implementación en paralelo.
Los resultados experimentales del programa de prueba de referencia muestran que, en comparación con el esquema tradicional de ejecución de transacciones en serie, el PTE que se ejecuta en un procesador de 4 núcleos en condiciones ideales puede lograr una mejora del rendimiento de aproximadamente un 200 %~300 %, y la mejora computacional es proporcional al número de núcleos.
Cuantos más núcleos, mejor rendimiento.
Un gráfico de dirección acíclica a menudo se denomina Gráfico acíclico dirigido (DAG).
En un lote de transacciones, se identifican los recursos mutuamente excluyentes ocupados por cada transacción; luego se construye un DAG dependiente de la transacción de acuerdo con la secuencia de transacciones en el bloque y la relación de ocupación de los recursos mutuamente excluyentes.
Como se muestra en la figura a continuación, todas las transacciones con un grado de entrada de 0 (sin tareas de pedido anticipado dependientes) se pueden ejecutar en paralelo. El DAG de transacción de la derecha se puede obtener mediante clasificación topológica basada en el orden de la lista de transacciones original de la izquierda.
Tome todas las transacciones en el bloque del bloque empaquetado.
Inicialice una instancia de DAG con la cantidad de transacciones como la cantidad máxima de vértices.
Leer todas las transacciones en orden. Si una transacción se puede fusionar, resuelva su campo de conflicto y verifique si alguna transacción anterior entra en conflicto con ella. Si es así, construya un borde de dependencia entre las transacciones correspondientes. Si la transacción no se puede fusionar, se considera que debe ejecutarse después de que se hayan ejecutado todas las transacciones anteriores, por lo que se crea un borde de dependencia entre la transacción y todos sus predecesores.
Nota : una vez que se han creado todos los bordes dependientes, no se pueden fusionar y solo se pueden ejecutar secuencialmente.
El subproceso principal primero inicializará un pequeño grupo de subprocesos según la cantidad de núcleos de hardware y, si los núcleos de hardware fallan, no se crearán otros subprocesos.
Cuando el DAG no se completa, el bucle del subproceso espera que la transacción lista con el grado de entrada 0 se extraiga del método waitPop del DAG. Si la transacción a ejecutar se realiza con éxito, la transacción se ejecutará. Si falla, el DAG ha completado la ejecución y el subproceso finaliza.
FISCO BCOS verifica que los triples, es decir, la raíz del estado, la raíz de la transacción y la raíz del recibo, sean iguales entre sí para determinar si los estados están de acuerdo. Una raíz de transacción es un valor hash calculado en base a todas las transacciones en el bloque.
Siempre que todos los nodos de consenso procesen los mismos datos de bloque, la raíz de la transacción debe ser la misma, lo cual es relativamente fácil de garantizar. La clave es asegurarse de que el estado y la raíz del recibo generados después de la transacción sean los mismos.
Es bien sabido que el orden de ejecución entre instrucciones ejecutadas en paralelo en diferentes núcleos de CPU no se puede predecir de antemano, y lo mismo ocurre con las transacciones ejecutadas en paralelo.
En el esquema tradicional de ejecución de transacciones, la raíz del estado cambia una vez que se ejecuta cada transacción, y la raíz del estado modificada se escribe en el recibo de la transacción.
Una vez que se ejecutan todas las transacciones, la raíz del estado final representa el estado actual de la cadena de bloques. Al mismo tiempo, se calcula una raíz de recibo basada en todos los recibos de transacciones.
Se puede ver que en la implementación tradicional, la raíz del estado actúa como una variable compartida global.
Cuando las transacciones se ejecutan en paralelo y fuera de orden, el cálculo tradicional de la raíz del estado ya no es aplicable porque las transacciones se ejecutan en un orden diferente en diferentes máquinas y no se garantiza que la raíz del estado final sea consistente, ni se garantiza que la raíz del recibo ser consistente.
En FISCO BCOS, las transacciones primero se ejecutan en paralelo y se registra el historial del cambio de estado de cada transacción. Una vez que se ejecutan todas las transacciones, se calcula una raíz de estado en función del historial.
Al mismo tiempo, la raíz del estado en el reconocimiento de la transacción se convierte en la raíz del estado final después de que se hayan ejecutado todas las transacciones, lo que garantiza que los nodos de consenso aún puedan llegar a un acuerdo incluso si las transacciones se ejecutan en paralelo.
Si dos transacciones no son dependientes pero se considera que lo son, se producirá una pérdida de rendimiento innecesaria. Por el contrario, si ambas transacciones reescriben el estado de la misma cuenta pero se ejecutan en paralelo, el estado final de la cuenta puede ser incierto.
Por lo tanto, la determinación de la dependencia es un tema importante que afecta el rendimiento e incluso puede determinar si la cadena de bloques puede funcionar correctamente.
En una transacción de transferencia simple, podemos juzgar si dos transacciones dependen de las direcciones del remitente y el destinatario. Tome las siguientes tres transacciones de transferencia como ejemplo, A→B, C→D y D→E.
Es fácil ver que la transacción D→E depende del resultado de la transacción C→D, pero la transacción A→B no tiene nada que ver con las otras dos transacciones, por lo que se puede ejecutar en paralelo.
Este tipo de análisis es cierto en una cadena de bloques que solo admite transferencias simples, pero puede no ser tan preciso en una cadena de bloques completa de Turing que ejecuta contratos inteligentes, porque no sabemos exactamente qué sucede en un contrato de transferencia escrito por el usuario. . Esto es lo que podría pasar.
Parece que la transacción de A→B no tiene nada que ver con el estado de la cuenta de C y D, pero en la implementación subyacente del usuario, A es una cuenta especial y se debe deducir una determinada tarifa de la cuenta de C para cada dinero transferido a través de la cuenta de A.
En este escenario, las tres transacciones están todas relacionadas, por lo que no se pueden ejecutar en paralelo. Si las transacciones se dividen de acuerdo con el método de análisis de dependencia anterior, es probable que se produzcan errores.
¿Podemos deducir automáticamente qué dependencias existen realmente en una transacción en función del contenido del contrato del usuario? La respuesta es no. Como se mencionó anteriormente, es difícil analizar las dependencias contractuales y el proceso de ejecución en el análisis estático.
En FISCO BCOS, la asignación de dependencias comerciales se deja a los desarrolladores que están más familiarizados con el contenido del contrato. Específicamente, los recursos mutuamente excluyentes de los que depende la transacción se pueden representar mediante un conjunto de cadenas.
FISCO BCOS expone la interfaz al desarrollador, quien define los recursos de los que depende la transacción en forma de cadenas e informa al ejecutor de la cadena.
El ejecutor organizará automáticamente todas las transacciones del bloque en el DAG de transacciones de acuerdo con las dependencias de transacciones especificadas por el desarrollador.
Por ejemplo, en un contrato de transferencia simple, el desarrollador simplemente especifica que la dependencia para cada transacción de transferencia es {dirección del remitente + dirección del receptor}.
Además, si el desarrollador introduce otra dirección de terceros en la lógica de transferencia, la dependencia debe definirse como {dirección del remitente + dirección del destinatario + dirección de terceros}.
Este enfoque es intuitivo, simple y general, y se aplica a todos los contratos inteligentes, pero también aumenta la responsabilidad sobre los hombros del desarrollador.
El desarrollador debe tener mucho cuidado al especificar las dependencias de las transacciones. Si las dependencias no se escriben correctamente, las consecuencias son impredecibles.
Para que los desarrolladores utilicen el marco de contratos paralelos, FISCO BCOS ha establecido algunas especificaciones para la redacción de contratos. Las especificaciones son las siguientes:
Que dos transacciones se puedan ejecutar en paralelo depende de si las dos transacciones son mutuamente excluyentes. La exclusión mutua se refiere a la intersección del conjunto de variables de almacenamiento de dos transacciones.
Por ejemplo, en un escenario de transferencia de activos, una transacción es una operación de transferencia entre usuarios. transfer(X, Y) representa la interfaz de transferencia del usuario X al usuario Y, y la exclusión mutua es la siguiente.
Parámetro mutuamente excluyente: Parámetro relacionado con la operación de “lectura/escritura” de la variable de almacenamiento del contrato en la interfaz del contrato. Tome la transferencia de interfaz de transferencia (X, Y) por ejemplo. X e Y son parámetros mutuamente excluyentes.
Mutex: el contenido mutex específico extraído de una transacción de acuerdo con los parámetros mutex. Tome la transferencia de interfaz de transferencia (X, Y) por ejemplo. En una transacción de transferencia que usa esta interfaz, el parámetro específico es transferencia (A, B) y el mutex de esta operación es [A, B]. Para otra transacción, se llama transfer(A, C) y el mutex para esta operación es [A, C].
Determinar si dos transacciones se pueden ejecutar en paralelo al mismo tiempo es determinar si la exclusión mutua de dos transacciones se cruza. Las transacciones cuyas intersecciones están vacías se pueden ejecutar en paralelo.
FFISCO-BCOS ofrece dos formas de redactar contratos paralelos, contratos precompilados y contratos de solidez, de los cuales solo se describen aquí los últimos. Lo mismo ocurre con los contratos precompilados.
Para escribir un contrato de solidez en paralelo, además de eso, simplemente haga ParallelContract.sol la clase base para los contratos que desea poner en paralelo. Se llama al método RegisterParallelFunction() para registrar interfaces que se pueden paralelizar.
El código del Contrato Paralelo es el siguiente:
pragma solidity ^0.4.25; //Precompile the contract interface contract ParallelConfigPrecompiled { function registerParallelFunctionInternal(address, string, uint256) public returns (int); function unregisterParallelFunctionInternal(address, string) public returns (int); } //The parallel contract base class needs to be registered and the subcontract needs to be implement enable or disable interface contract ParallelContract { ParallelConfigPrecompiled precompiled = ParallelConfigPrecompiled(0x1006); function registerParallelFunction(string functionName, uint256 criticalSize) public { precompiled.registerParallelFunctionInternal(address(this), functionName, criticalSize); } function unregisterParallelFunction(string functionName) public { precompiled.unregisterParallelFunctionInternal(address(this), functionName); } function enableParallel() public; function disableParallel() public; }
El siguiente ejemplo es un contrato de transferencia redactado en base a un contrato marco paralelo:
pragma solidity ^0.4.25; import "./ParallelContract.sol"; // Introduce ParallelContract.sol contract ParallelOk is ParallelContract // useParallelContract as a base class { // Contract implementation mapping (string => uint256) _balance; // Global mapping // The mutually exclusive variables from and to are the first two parameters at the beginning of transfer (). It can be seen that the contract requirements are still very strict, which will make users uncomfortable to write function transfer(string from, string to, uint256 num) public { _balance[from] -= num; // From is the key of the global mapping, and is a mutually exclusive parameter _balance[to] += num; //// To is the key of the global mapping, and is a mutually exclusive parameter } // The mutex variable name comes first as an argument to the beginning of set() function set(string name, uint256 num) public { _balance[name] = num; } function balanceOf(string name) public view returns (uint256) { return _balance[name]; } // Register contract interfaces that can be parallel function enableParallel() public { // The function definition string (note that there are no Spaces after ",") and the first few arguments are mutex arguments (mutex arguments must be first when designing a function) //The number 2 indicates that the first two are mutex parameters, and the system decodes the mutex according to the function signature and abi registerParallelFunction("transfer(string,string,uint256)", 2); // critical: string string // registerParallelFunction("set(string,uint256)", 1); // critical: string } // Deregister the parallel contract interface function disableParallel() public { unregisterParallelFunction("transfer(string,string,uint256)"); unregisterParallelFunction("set(string,uint256)"); } }
Una interfaz de contrato paralelo debe satisfacer:
Antes de programar una interfaz, determine los parámetros mutuamente excluyentes de la interfaz. Los parámetros mutuamente excluyentes de la interfaz son mutuamente excluyentes para las variables globales. Las reglas para determinar parámetros mutuamente excluyentes son las siguientes:
Por ejemplo, hay múltiples variables globales de tipos simples en el contrato y diferentes interfaces acceden a diferentes variables globales.
Si desea conectar diferentes interfaces en paralelo, debe definir un parámetro mutex en el parámetro de interfaz con la variable global modificada para indicar qué variable global se usa durante la llamada.
Cuando se llama, el parámetro mutex pasa activamente el "nombre de variable" modificado de la variable global para identificar el mutex de la transacción.
Por ejemplo: si setA(int x)
modifica globalA
como un parámetro global, setA
debe definirse como set(string aflag, int x)
. Cuando se llama, se setA("globalA", 10)
. Utilice el nombre de variable “globalA”
para indicar que la exclusión mutua para esta transacción es globalA
.
Después de determinar los parámetros mutuamente excluyentes, determine el tipo de parámetro y el orden de acuerdo con las reglas. Las reglas son las siguientes:
Se puede observar que la transacción paralela de FISCO-BCOS depende en gran medida de las especificaciones de los contratos escritos por los usuarios.
Si las especificaciones de los contratos escritos por los usuarios no están estandarizadas, el sistema realiza apresuradamente la ejecución en paralelo, lo que puede causar la raíz de la inconsistencia de los libros de contabilidad.
Khipu cree que no es realista que los usuarios identifiquen y etiqueten el rango de direcciones que crearán conflictos estáticos al momento de redactar el contrato sin error. Esto contrasta con la opinión de FISCO-BCOS.
Si, dónde y bajo qué condiciones aparecerá la condición de carrera solo se puede juzgar cuando la adquisición de certeza involucra el estado actual.
Este tipo de juicio, con los lenguajes de programación de contratos actuales, hace que sea casi imposible que el análisis estático del código obtenga resultados completamente correctos e imperceptibles.
Khipu ha hecho un intento más completo para abordar este problema y ha completado un proceso para implementarlo.
En Khipu, cada transacción en el mismo bloque comienza desde el estado mundial del bloque anterior y luego se ejecuta en paralelo, registrando las tres condiciones de carrera anteriores que se encuentran a lo largo de todos los caminos de experiencia ideales durante la ejecución.
Después de la fase de ejecución paralela está la fase de fusión, cuando los estados del mundo paralelo se fusionan uno por uno. Al fusionar una transacción, primero juzgue si tiene un conflicto con las condiciones de carrera previamente fusionadas a partir de las condiciones estáticas registradas.
Si no, fusionar directamente. Si es así, la transacción se ejecuta nuevamente comenzando con el estado anterior del mundo que se ha fusionado.
El último estado mundial fusionado se compara con el hash del bloque. Esta es la última línea de defensa. Si la comprobación es incorrecta, se abandona la fusión anterior y se vuelve a ejecutar el bloque.
Aquí, Khipu introduce un índice de paralelismo, que se refiere a la proporción de transacciones en un bloque que pueden combinar resultados directamente sin tener que volver a ejecutarse.
La observación de Khipu de la repetición de Ethereum durante varios días desde el bloque de creación hasta el bloque más nuevo muestra que esta proporción (paralelismo) puede alcanzar el 80% en promedio.
En general, si las tareas informáticas se pueden paralelizar por completo, la escalabilidad de una sola cadena es infinita. Porque siempre puede agregar más núcleos de CPU a un nodo. Si este no es el caso, entonces la tasa teórica máxima está limitada por el teorema de Andal:
El límite al que se puede acelerar el sistema depende del recíproco de las partes que no se pueden paralelizar. Entonces, si puede paralelizar el 99%, puede acelerar hasta 100 veces. Pero si solo puede lograr una paralelización del 95 %, entonces solo puede obtener hasta 20 veces más rápido.
De todas las transacciones en Ethereum, alrededor del 80 % se puede paralelizar y el 20 % no, por lo que el límite de velocidad de Khipu es de alrededor de 5 veces.
Al comprender las instrucciones en el código evm, se encontró que un número limitado de instrucciones había creado procesos de lectura y escritura para el almacenamiento, por lo que fue posible registrar estos procesos de lectura y escritura para formar una colección de lectura y escritura, pero código estático. el análisis no pudo asegurar que estos procesos fueran registrados.
Por lo tanto, es necesario ejecutar previamente cada transacción una vez al procesar cada bloque. El proceso de ejecución previa nos dice si las transacciones son de lectura y escritura en la misma cuenta o almacenamiento, y crea un conjunto de lectura y un conjunto de escritura para cada transacción.
Si hay 100 transacciones en la cadena de bloques, estas 100 transacciones se pueden ejecutar en paralelo a través del grupo de subprocesos. Cada contrato tiene el mismo estado mundial inicial y se crearán 100 readSets y writeSets durante la ejecución, así como 100 nuevos estados cada uno.
Cuando finaliza la ejecución previa, comienza la siguiente etapa de procesamiento. Idealmente, si las 100 entradas readSet y writeSet no entran en conflicto, entonces pueden fusionarse directamente para producir el estado mundial final de todas las transacciones en el bloque. Sin embargo, la transacción a menudo no es tan ideal.
La forma correcta de manejarlo es comparar readSet y writeSet después de la ejecución de la primera transacción con readSet y writeSet después de la ejecución del segundo contrato, y ver si han leído y escrito la misma cuenta o almacenamiento.
Si es así, eso significa que las dos ofertas están en conflicto. Luego, la segunda transacción comenzará después de la finalización de la primera transacción y se ejecutará nuevamente.
De manera similar, a medida que la máquina de estado de combinación continúa, el conjunto de conflictos continuará acumulándose y, mientras las transacciones posteriores entren en conflicto con las transacciones anteriores, se ejecutarán secuencialmente hasta que se hayan ejecutado todas las transacciones.
A través de la reproducción de transacciones en la red principal de Ethereum, se encuentra que donde hay muchos conflictos, la mayoría de los casos son intercambios en el mismo bloque con transacciones interrelacionadas, lo que también es consistente con este proceso.
Aptos se basa en el lenguaje Move de Diem y MoveVM para crear una cadena de alto rendimiento que permite la ejecución en paralelo. El enfoque de Aptos es detectar asociaciones mientras es transparente para los usuarios/desarrolladores.
Es decir, no se requiere que las transacciones indiquen explícitamente qué parte del estado (ubicación de memoria) utilizan.
Aptos utiliza una versión modificada de la memoria de transacciones de software llamada Block-STM e implementa su motor de ejecución en paralelo basado en Block-STM .
Block-STM utiliza MVCC (Control de concurrencia de múltiples versiones) para evitar conflictos de escritura-escritura. Todas las escrituras en la misma ubicación se almacenan con sus versiones, que contienen su TX-ID y la cantidad de veces que se ha vuelto a ejecutar la escritura tx.
Cuando una transacción (tx) lee un valor para una ubicación de memoria, obtiene el valor escrito de MVCC en esa ubicación que ocurrió antes de tx junto con la versión asociada para determinar si hay un conflicto de lectura/escritura.
En Block-STM, las transacciones se clasifican previamente dentro de bloques y se dividen entre subprocesos de procesador para ejecución paralela durante la ejecución. En la ejecución en paralelo, se supone que no existen dependencias para ejecutar la transacción.
Se registran las posiciones de memoria modificadas por la transacción. Después de la ejecución, verifique todos los resultados de la transacción. Durante la validación, si se encuentra una transacción para acceder a una ubicación de memoria modificada por una transacción anterior, la transacción no es válida.
Actualice el resultado de la operación y luego vuelva a ejecutar la operación. Este proceso se repite hasta que se hayan ejecutado todas las transacciones del bloque. Block-STM acelera la ejecución cuando se utilizan varios núcleos de procesador. La aceleración depende de cuán interdependientes sean las transacciones.
Se puede ver que el esquema utilizado por Aptos es más o menos similar al Khipu mencionado anteriormente, pero existen algunas diferencias en la implementación, que se detallan a continuación:
Aptos realizó un benchmark correspondiente después de la integración de bloque-STM y comparó entre ejecución secuencial y ejecución paralela de un bloque de 10k transacciones. El resultado de la comparación se muestra a continuación:
Se puede ver en la figura anterior que Block STM logra 16 veces más rápido que la ejecución secuencial con 32 subprocesos en paralelo, y más de 8 veces más rápido bajo alta contención.
Con base en la comparación y el análisis anteriores, se puede concluir que algunos esquemas requieren que los usuarios escriban el almacenamiento de acuerdo con las reglas establecidas al escribir contratos para que las dependencias se puedan encontrar mediante análisis estático y dinámico.
Solana y Sui usan esquemas similares, pero la percepción del usuario es diferente. Este esquema es esencialmente un cambio de modelo de almacenamiento para obtener mejores resultados de análisis.
Khipu y Aptos son esquemas independientes del usuario. La sobrecarga de la ejecución paralela no recae sobre los desarrolladores, y no necesitan pensar en esto al escribir sus contratos.
La máquina virtual analiza dinámicamente las relaciones de dependencia antes de la ejecución, implementando así la ejecución paralela sin relaciones de dependencia.
Esto es difícil de implementar y el grado de paralelismo depende en cierta medida de la división de cuentas de la transacción. Cuando hay muchos conflictos de transacciones, el rendimiento se deteriora significativamente debido a la reejecución constante.
Aptos mencionó que realizarán una optimización futura de los contratos creados por los usuarios para analizar mejor las dependencias y así lograr una ejecución más rápida.
La simple modificación de un esquema basado en serie a un esquema paralelo puede generar entre 3 y 16 veces una mejora del rendimiento transaccional en un entorno de cadena pública, y si eso se puede combinar con bloques grandes y límites de gas grandes, el rendimiento L2 se optimizará aún más, potencialmente alrededor de 100 veces.
Desde una perspectiva de ingeniería, en lo que respecta a la implementación y la eficiencia, lo más probable es que OlaVM adopte el esquema Khipu más una solución de modelo de almacenamiento personalizado, que puede mejorar el rendimiento al tiempo que evita la complejidad causada por la introducción de Block-STM y facilita una mejor optimización de la ingeniería.
Fundada en 2021 e impulsada por desarrolladores de cadenas de bloques de primer nivel, Sin7y es una incubadora de proyectos y un equipo de investigación de tecnología de cadenas de bloques que explora las tecnologías más importantes y de vanguardia, incluidas EVM, Layer2, cadenas cruzadas, computación de privacidad, soluciones de pago autónomas, etc. .
Actualmente estamos trabajando en un ZKVM compatible con EVM, rápido y escalable llamado OlaVM. Si está interesado en hablar con nosotros, no dude en unirse a nuestro grupo TG o envíenos un correo electrónico a [email protected] .