Prueba pública: solución para privacidad y escalabilidad en Ethereum

Blockchain es una tecnología innovadora que promete mejorar muchas áreas de la vida humana. Transfiere procesos y productos reales al espacio digital, garantiza la velocidad y la fiabilidad de las transacciones financieras, reduce su costo y también le permite crear aplicaciones DAPP modernas utilizando contratos inteligentes en redes descentralizadas.

Dadas las muchas ventajas y los diversos usos de blockchain, puede parecer extraño que esta prometedora tecnología aún no haya penetrado en todos los sectores. El problema es que las cadenas de bloques modernas descentralizadas carecen de escalabilidad. Ethereum procesa alrededor de 20 transacciones por segundo, lo que no es suficiente para satisfacer las necesidades del negocio dinámico actual. Al mismo tiempo, las empresas que usan la tecnología blockchain no se atreven a abandonar Ethereum debido a su alto grado de protección contra piratería informática y fallas en la red.

Para garantizar la descentralización, la seguridad y la escalabilidad en la cadena de bloques, resolviendo así el Trilema de la escalabilidad, el equipo de desarrollo de Opporty creó Plasma Cash, una cadena secundaria que consiste en un contrato inteligente y una red privada basada en Node.js, que transfiere periódicamente su estado a la cadena raíz ( Ethereum).



Procesos clave en Plasma Cash


1. El usuario llama a la función del contrato inteligente 'depósito', transfiriendo la cantidad en ETH, que quiere poner en el token de Plasma Cash. La función de contrato inteligente crea un token y genera un evento al respecto.

2. Los nodos de Plasma Cash suscritos a los eventos del contrato inteligente reciben un evento sobre la creación de un depósito y agregan una transacción sobre la creación de un token al grupo.

3. Periódicamente, los nodos especiales de Plasma Cash toman todas las transacciones del grupo (hasta 1 millón) y forman un bloque a partir de ellas, calculan el árbol Merkle y, en consecuencia, el hash. Este bloque se envía a otros nodos para su verificación. Los nodos verifican si el hash Merkle es válido, si las transacciones son válidas (por ejemplo, si el remitente del token es su propietario). Después de verificar el bloque, el nodo llama a la función `submitBlock` del contrato inteligente, que almacena el número y el hash Merkle del bloque en la cadena de rastreo. Un contrato inteligente genera un evento sobre la adición exitosa de un bloque. Las transacciones se eliminan del grupo.

4. Los nodos que recibieron el evento sobre el envío del bloque comienzan a aplicar las transacciones que se agregaron al bloque.

5. En algún momento, el propietario (o no propietario) del token quiere retirarlo de Plasma Cash. Para hacer esto, llama a la función `startExit`, pasando información sobre las últimas 2 transacciones en el token, que confirman que él es el propietario del token. El contrato inteligente, utilizando el hash Merkle, verifica las transacciones en bloques y envía un token a la salida, lo que sucederá en dos semanas.

6. Si la operación de extracción del token se produjo con violaciones (el token se gastó después del inicio del procedimiento de retiro o el token ya era un extraño antes del retiro), el propietario del token puede refutar el retiro dentro de dos semanas.



La privacidad se logra de dos maneras.


1. La cadena raíz no sabe nada sobre las transacciones que se forman y reenvían dentro de la cadena secundaria. Queda información sobre quién comenzó y retiró ETH a / desde Plasma Cash.

2. La cadena secundaria le permite organizar transacciones anónimas utilizando zk-SNARKs.

Pila tecnológica


  • NodeJS
  • Redis
  • Ethereum
  • Soild

Prueba


Al desarrollar Plasma Cash, probamos la velocidad del sistema y obtuvimos los siguientes resultados:

  • se agregan hasta 35,000 transacciones por segundo al grupo;
  • Se pueden almacenar hasta 1,000,000 de transacciones en el bloque.

Se realizaron pruebas en los siguientes 3 servidores:

1. Intel Core i7-6700 Quad-Core Skylake incl. NVMe SSD: 512 GB, 64 GB de RAM DDR4
Se generaron 3 nodos de Plasma Cash validados.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SSD SATA - 500 GB, 64 GB de RAM DDR4
Se levantó el nodo ETH Ropsten testnet.
Se generaron 3 nodos de Plasma Cash validados.

3. Intel Core i9-9900K Octa-Core incl. NVMe SSD: 1 TB, 64 GB de RAM DDR4
1 Se envió el nodo de Plasma Cash.
Se generaron 3 nodos de Plasma Cash validados.
Se lanzó una prueba para agregar transacciones a la red de Plasma Cash.

Total: 10 nodos de Plasma Cash en una red privada.

Prueba 1


Hay un límite de 1 millón de transacciones por bloque. Por lo tanto, 1 millón de transacciones se dividen en 2 bloques (ya que el sistema logra tomar parte de las transacciones y las envía mientras se envían).


Estado inicial: último bloque # 7; 1 millón de transacciones y tokens se almacenan en la base de datos.

00:00 - inicia el script de generación de transacciones
01:37 - Se crearon 1 millón de transacciones y comenzó el envío al nodo
01:46 - el nodo de envío tomó 240k transacciones del grupo y forma el bloque # 8. También vemos que se agregan 320k transacciones al grupo en 10 segundos
01:58 - el bloque # 8 se firma y se envía para su validación
02:03 - el bloque # 8 se valida y la función `submitBlock` del contrato inteligente con el hash Merkle y el número de bloque se llama
02:10 - el script de demostración terminó de funcionar, lo que envió 1 millón de transacciones en 32 segundos
02:33 - los nodos comenzaron a recibir información que el bloque # 8 fue agregado a la cadena raíz, y comenzó a realizar 240k transacciones
02:40 - Se borraron 240k transacciones del grupo, que ya están en el bloque # 8
02:56 - el nodo de envío tomó las 760k transacciones restantes del grupo y comenzó a calcular el hash Merkle y firmar el bloque # 9
03:20 - todos los nodos contienen 1 mln de 240k transacciones y tokens
03:35 - el bloque # 9 se firma y se envía para su validación a otros nodos
03:41 - se ha producido un error de red
04:40 - por tiempo de espera, la espera para la validación del bloque # 9 se ha detenido
04:54 - el nodo de envío tomó las 760k transacciones restantes del grupo y comenzó a calcular el hash Merkle y firmar el bloque # 9
05:32 - el bloque # 9 se firma y se envía para su validación a otros nodos
05:53 - el bloque # 9 se valida y se envía a la cadena raíz
06:17 - los nodos comenzaron a recibir información que el bloque # 9 se agregó a la cadena raíz y comenzó a realizar transacciones de 760k
06:47 - el grupo se borró de las transacciones que están en el bloque # 9
09:06 - todos los nodos contienen 2 millones de transacciones y tokens

Prueba 2


Hay un límite de 350k por bloque. Como resultado, tenemos 3 bloques.


Estado inicial: último bloque # 9; 2 millones de transacciones y tokens almacenados en la base de datos

00:00 - el script de generación de transacciones ya se está ejecutando
00:44 - Se crearon 1 millón de transacciones y comenzó el envío al nodo
00:56 - el nodo de envío tomó 320k transacciones del grupo y forma el bloque # 10. También vemos que se agregan 320k transacciones al grupo en 10 segundos
01:12 - el bloque # 10 se firma y se envía a otros nodos para su validación
01:18 - el script de demostración terminó de funcionar, lo que envió 1 millón de transacciones en 34 segundos
01:20 - el bloque # 10 se valida y se envía a la cadena raíz
01:51 - todos los nodos recibieron información de la cadena raíz que se ha agregado el bloque # 10, y están comenzando a aplicar 320k transacciones
02:01 - el grupo se borró para 320k transacciones que se agregaron al bloque # 10
02:15 - el nodo de envío tomó 350k transacciones del grupo y forma el bloque # 11
02:34 - el bloque # 11 se firma y se envía a otros nodos para su validación
02:51 - el bloque # 11 se valida y se envía a la cadena raíz
02:55 - el último nodo ejecutó transacciones desde el bloque # 10
10:59 - durante mucho tiempo, se ejecutó una transacción en la cadena raíz con el envío del bloque # 9, pero se completó y todos los nodos recibieron información sobre esto y comenzaron a ejecutar 350k transacciones
11:05 - el grupo se borró para 320k transacciones que se agregaron al bloque # 11
12:10: todos los nodos contienen 1 millón de transacciones y tokens de 670k
12:17 - el nodo de envío tomó 330k transacciones del grupo y forma el bloque # 12
12:32 - el bloque # 12 se firma y se envía a otros nodos para su validación
12:39 - el bloque # 12 se valida y se envía a la cadena raíz
13:44 - todos los nodos recibieron información de la cadena raíz que el bloque # 12 ha sido agregado y están comenzando a aplicar transacciones de 330k
14:50 - todos los nodos contienen 2 millones de transacciones y tokens

Prueba 3


En el primer y segundo servidor, un nodo de validación fue reemplazado por un nodo de envío.


Estado inicial: último bloque # 84; 0 transacciones y tokens se almacenan en la base de datos

00:00 - Se lanzan 3 scripts que generan y envían 1 millón de transacciones
01:38 - Se crearon 1 millón de transacciones y comenzó el envío para enviar el nodo # 3
01:50 - el nodo de envío # 3 tomó 330k transacciones del grupo y forma el bloque # 85 (f21). También vemos que se agregan 350k transacciones al grupo en 10 segundos
01:53 - Se crearon 1 millón de transacciones y comenzó el envío para enviar el nodo # 1
01:50 - el nodo de envío # 3 tomó 330k transacciones del grupo y forma el bloque # 85 (f21). También vemos que se agregan 350k transacciones al grupo en 10 segundos
02:01 - el nodo de envío # 1 tomó 250k transacciones del grupo y forma el bloque # 85 (65e)
02:06 - el bloque # 85 (f21) se firma y se envía a otros nodos para su validación
02:08 - el script de demostración del servidor # 3 ha terminado de funcionar, lo que envió 1 mln de transacciones en 30 segundos
02:14 - el bloque # 85 (f21) se valida y se envía a la cadena raíz
02:19 - el bloque # 85 (65e) se firma y se envía a otros nodos para su validación
02:22 - Se crearon 1 millón de transacciones y comenzó el envío para enviar el nodo # 2
02:27 - el bloque # 85 (65e) se valida y se envía a la cadena raíz
02:29 - el nodo de envío # 2 tomó del grupo 111855 transacciones y forma el bloque # 85 (256).
02:36 - el bloque # 85 (256) se firma y se envía a otros nodos para su validación
02:36 - el script de demostración del servidor # 1 terminó de funcionar, lo que envió 1 mln de transacciones en 42.5 segundos
02:38 - el bloque # 85 (256) se valida y se envía a la cadena raíz
03:08 - el script del servidor # 2, que envió 1 millón de transacciones en 47 segundos, terminó de funcionar
03:38 - todos los nodos recibieron información de la cadena raíz de que los bloques # 85 (f21), # 86 (65e), # 87 (256) se agregaron y comenzaron a aplicar transacciones de 330k, 250k, 111855
03:49 - el grupo se borró en 330k, 250k, 111855 transacciones que se agregaron a los bloques # 85 (f21), # 86 (65e), # 87 (256)
03:59 - el nodo de envío # 1 tomó del grupo 888145 transacciones y formularios bloque # 88 (214), el nodo de envío # 2 tomó del grupo 750k transacciones y el bloque de formularios # 88 (50a), el nodo de envío # 3 tomó del grupo 670k transacciones y Bloque de formas # 88 (d3b)
04:44 - el bloque # 88 (d3b) se firma y se envía a otros nodos para su validación
04:58 - el bloque # 88 (214) se firma y se envía a otros nodos para su validación
05:11 - el bloque # 88 (50a) se firma y se envía a otros nodos para su validación
05:11 - el bloque # 85 (d3b) se valida y se envía a la cadena raíz
05:36 - el bloque # 85 (214) se valida y se envía a la cadena raíz
05:43 - todos los nodos recibieron información de la cadena raíz que bloquea los bloques # 88 (d3b), # 89 (214) y se agregaron 670k, 750k transacciones
06:50 - debido a una desconexión, el bloque # 85 (50a) no fue validado
06:55 - el nodo de envío # 2 tomó 888145 transacciones del grupo y forma el bloque # 90 (50a)
08:14 - el bloque # 90 (50a) se firma y se envía a otros nodos para su validación
09:04 - el bloque # 90 (50a) se valida y se envía a la cadena raíz
11:23 - todos los nodos recibieron información de la cadena raíz que se agregó el bloque # 90 (50a), y comenzaron a aplicarse 888145 transacciones. Al mismo tiempo, el servidor n. ° 3 ha aplicado durante mucho tiempo las transacciones de los bloques n. ° 88 (d3b), n. ° 89 (214)
12:11 - todas las piscinas están vacías
13:41 - todos los nodos del servidor # 3 contienen 3 millones de transacciones y tokens
14:35 - todos los nodos del servidor # 1 contienen 3 millones de transacciones y tokens
19:24 - todos los nodos del servidor # 2 contienen 3 millones de transacciones y tokens

Los obstáculos


Durante el desarrollo de Plasma Cash, encontramos los siguientes problemas, que gradualmente resolvimos y estamos resolviendo:

1. El conflicto de interacción de varias funciones del sistema. Por ejemplo, la función de agregar transacciones al grupo bloqueó el envío y la validación de bloques, y viceversa, lo que condujo a una disminución de la velocidad.

2. No estaba claro de inmediato cómo enviar una gran cantidad de transacciones y, al mismo tiempo, minimizar el costo de la transferencia de datos.

3. No estaba claro cómo y dónde almacenar los datos para lograr altos resultados.

4. No estaba claro cómo organizar una red entre nodos, ya que el tamaño del bloque con 1 millón de transacciones toma aproximadamente 100 MB.

5. Trabajar en modo de subproceso único interrumpe la conexión entre nodos cuando se realizan cálculos largos (por ejemplo, construir un árbol Merkle y calcular su hash).

¿Cómo lidiamos con todo esto?


La primera versión del nodo Plasma Cash era una especie de combinación que podía hacer todo al mismo tiempo: aceptar transacciones, enviar y validar bloques, proporcionar una API para acceder a los datos. Dado que NodeJS fue inicialmente de un solo subproceso, la pesada función de cálculo del árbol de Merkle bloqueó la función de agregar transacción. Vimos dos opciones para resolver este problema:

1. Ejecute varios procesos NodeJS, cada uno de los cuales realiza ciertas funciones.

2. Use worker_threads y ponga la ejecución del código en hilos.

Como resultado, utilizamos ambas opciones al mismo tiempo: dividimos lógicamente un nodo en 3 partes, que pueden funcionar por separado, pero al mismo tiempo sincrónicamente

1. Envíe un nodo que acepte transacciones al grupo y cree bloques.

2. Validar el nodo que verifica la validez de los nodos.

3. API de nodo: proporciona una API para acceder a los datos.

Al mismo tiempo, puede conectarse a cada nodo a través de un socket Unix usando CLI.

Operaciones pesadas, como el cálculo del árbol Merkle, lo colocamos en una secuencia separada.

Por lo tanto, logramos el funcionamiento normal de todas las funciones de Plasma Cash simultáneamente y sin fallas.

Tan pronto como el sistema funcionó funcionalmente, comenzamos a probar la velocidad y, desafortunadamente, obtuvimos resultados insatisfactorios: 5,000 transacciones por segundo y hasta 50,000 transacciones en un bloque. Tenía que averiguar qué se implementó incorrectamente.

Para empezar, comenzamos a probar el mecanismo de comunicación con Plasma Cash para descubrir la capacidad máxima del sistema. Anteriormente escribimos que el nodo Plasma Cash proporciona una interfaz de socket unix. Originalmente era textual. Los objetos json se enviaron usando `JSON.parse ()` y `JSON.stringify ()`.

```json { "action": "sendTransaction", "payload":{ "prevHash": "0x8a88cc4217745fd0b4eb161f6923235da10593be66b841d47da86b9cd95d93e0", "prevBlock": 41, "tokenId": "57570139642005649136210751546585740989890521125187435281313126554130572876445", "newOwner": "0x200eabe5b26e547446ae5821622892291632d4f4", "type": "pay", "data": "", "signature": "0xd1107d0c6df15e01e168e631a386363c72206cb75b233f8f3cf883134854967e1cd9b3306cc5c0ce58f0a7397ae9b2487501b56695fe3a3c90ec0f61c7ea4a721c" } } ``` 

Medimos la velocidad de transferencia de dichos objetos y recibimos ~ 130k por segundo. Intentaron reemplazar las funciones estándar con json, pero el rendimiento no mejoró. Debe haber un motor V8 bien optimizado para estas operaciones.

El trabajo con transacciones, tokens, bloques se realizó a través de clases. Al crear tales clases, el rendimiento se redujo 2 veces, lo que indica: OOP no es adecuado para nosotros. Tuve que reescribir todo con un enfoque puramente funcional.

Escribir en la base de datos


Inicialmente, Redis fue elegida para el almacenamiento de datos como una de las soluciones más productivas que satisface nuestros requisitos: almacenamiento de valor clave, trabajo con tablas hash y muchas más. Lanzamos Redis-benchmark y obtuvimos ~ 80k operaciones por segundo en 1 modo de canalización.

Para un alto rendimiento, ajustamos Redis más finamente:

  • Estableció una conexión de socket unix.
  • Deshabilite el guardado de estado en el disco (para mayor confiabilidad, puede configurar la réplica y guardarla en el disco en un Redis separado).

En Redis, un grupo es una tabla hash, ya que necesitamos la capacidad de recibir todas las transacciones en una solicitud y eliminar las transacciones una por una. Intentamos usar una lista regular, pero funciona más lentamente al descargar la lista completa.

Usando la biblioteca estándar NodeJS, las bibliotecas Redis lograron un rendimiento de 18k transacciones por segundo. La velocidad cayó 9 veces.

Como el punto de referencia nos mostró las posibilidades claramente 5 veces más, comenzaron a optimizar. Cambiamos la biblioteca a ioredis y obtuvimos un rendimiento de 25k por segundo. Agregamos transacciones una por una usando el comando `hset`. Por lo tanto, generamos muchas solicitudes en Redis. Hubo una idea de fusionar transacciones en paquetes y enviarlas con un comando hmset. El resultado es 32k por segundo.

Por varias razones, que se describirán a continuación, trabajamos con datos usando `Buffer` y, como se vio después, si lo traduce a texto (` buffer.toString ('hex') `) antes de escribir, puede obtener un rendimiento adicional. Por lo tanto, la velocidad se incrementó a 35k por segundo. Por el momento, decidimos suspender aún más la optimización.

Tuvimos que cambiar al protocolo binario porque:

1. El sistema a menudo calcula hashes, firmas, etc., y para esto necesita datos en `Buffer.

2. Al transferir entre servicios, los datos binarios pesan menos que el texto. Por ejemplo, al enviar un bloque con 1 millón de transacciones, los datos en el texto pueden ocupar más de 300 megabytes.

3. La conversión continua de datos afecta el rendimiento.

Por lo tanto, tomamos como base nuestro propio protocolo binario para almacenar y transmitir datos, desarrollado sobre la base de la maravillosa biblioteca de datos binarios.

Como resultado, tenemos las siguientes estructuras de datos:

- Transacción


  ```json { prevHash: BD.types.buffer(20), prevBlock: BD.types.uint24le, tokenId: BD.types.string(null), type: BD.types.uint8, newOwner: BD.types.buffer(20), dataLength: BD.types.uint24le, data: BD.types.buffer(({current}) => current.dataLength), signature: BD.types.buffer(65), hash: BD.types.buffer(32), blockNumber: BD.types.uint24le, timestamp: BD.types.uint48le, } ``` 

- Token


  ```json { id: BD.types.string(null), owner: BD.types.buffer(20), block: BD.types.uint24le, amount: BD.types.string(null), } ``` 

- bloque


  ```json { number: BD.types.uint24le, merkleRootHash: BD.types.buffer(32), signature: BD.types.buffer(65), countTx: BD.types.uint24le, transactions: BD.types.array(Transaction.Protocol, ({current}) => current.countTx), timestamp: BD.types.uint48le, } ``` 

Mediante los comandos habituales `BD.encode (block, Protocol) .slice ();` y `BD.decode (buffer, Protocol)`, convertimos los datos a `Buffer` para guardarlos en Redis o enviar otro nodo y recuperar los datos.

También tenemos 2 protocolos binarios para transferir datos entre servicios:

- Protocolo para interactuar con Plasma Node a través de unix socket

  ```json { type: BD.types.uint8, messageId: BD.types.uint24le, error: BD.types.uint8, length: BD.types.uint24le, payload: BD.types.buffer(({node}) => node.length) } ``` 

donde:

  • `type` - acción a realizar, por ejemplo, 1 - sendTransaction, 2 - getTransaction;
  • `payload ' - datos a transferir a la función correspondiente;
  • `messageId` - ID del mensaje para que se pueda identificar la respuesta.

- Protocolo de interacción entre nodos

  ```json { code: BD.types.uint8, versionProtocol: BD.types.uint24le, seq: BD.types.uint8, countChunk: BD.types.uint24le, chunkNumber: BD.types.uint24le, length: BD.types.uint24le, payload: BD.types.buffer(({node}) => node.length) } ``` 

donde:

  • `code` - código de mensaje, por ejemplo 6 - PREPARE_NEW_BLOCK, 7 - BLOCK_VALID, 8 - BLOCK_COMMIT;
  • `versionProtocol` - versión de protocolo, ya que los nodos con diferentes versiones se pueden generar en la red y pueden funcionar de diferentes maneras;
  • `seq` - identificador de mensaje;
  • `countChunk` y` chunkNumber` son necesarios para dividir mensajes grandes;
  • `length` y` payload` la longitud y los datos en sí.

Como escribimos los datos de antemano, el sistema final es mucho más rápido que la biblioteca `rlp` de Ethereum. Desafortunadamente, aún no hemos podido rechazarlo, ya que es necesario finalizar el contrato inteligente, que planeamos hacer en el futuro.

Si logramos alcanzar una velocidad de 35,000 transacciones por segundo, también debemos procesarlas en un tiempo óptimo. Dado que el tiempo aproximado de formación del bloque toma 30 segundos, necesitamos incluir 1,000,000 de transacciones en el bloque, lo que significa enviar más de 100 mb de datos.

Inicialmente, utilizamos la biblioteca `ethereumjs-devp2p` para comunicar nodos, pero no podía hacer frente a tantos datos. Como resultado, utilizamos la biblioteca `ws` y configuramos la transferencia de datos binarios en websocket. Por supuesto, también encontramos problemas al enviar grandes paquetes de datos, pero los dividimos en fragmentos y ahora no existen tales problemas.

Además, la formación del árbol Merkle y el cálculo del hash de 1,000,000 de transacciones requiere aproximadamente 10 segundos de cálculo continuo. Durante este tiempo, la conexión con todos los nodos logra romperse. Se decidió transferir este cálculo a un hilo separado.

Conclusiones:


De hecho, nuestros hallazgos no son nuevos, pero por alguna razón, muchos expertos se olvidan de ellos durante el desarrollo.

  • El uso de la programación funcional en lugar de la programación orientada a objetos aumenta el rendimiento.
  • Un monolito es peor que una arquitectura de servicio para un sistema de producción en NodeJS.
  • El uso de `worker_threads` para computación pesada mejora la capacidad de respuesta del sistema, especialmente cuando se trabaja con operaciones de E / S.
  • Unix socket es más estable y más rápido que las solicitudes http.
  • Si necesita transferir rápidamente datos de gran tamaño a través de la red, es mejor usar sockets web y enviar datos binarios, divididos en fragmentos, que pueden reenviarse si no llegan y luego fusionarse en un solo mensaje.

Te invitamos a visitar el proyecto GitHub : https://github.com/opporty-com/Plasma-Cash/tree/new-version

El artículo fue coescrito por Alexander Nashivan , desarrollador senior de Clever Solution Inc.

Source: https://habr.com/ru/post/471096/


All Articles