Proba pública: unha solución para a privacidade e a escalabilidade en Ethereum

Blockchain é unha tecnoloxía innovadora que promete mellorar moitas áreas da vida humana. Transfire procesos e produtos reais ao espazo dixital, garante a velocidade e fiabilidade das transaccións financeiras, reduce o seu custo e tamén permite crear aplicacións DAPP modernas mediante contratos intelixentes en redes descentralizadas.

Tendo en conta os moitos beneficios e as diversas aplicacións da cadea de bloques, pode parecer sorprendente que esta prometedora tecnoloxía aínda non chegou a todas as industrias. O problema é que as cadeas de bloques descentralizadas modernas carecen de escalabilidade. Ethereum procesa unhas 20 transaccións por segundo, o que non é suficiente para satisfacer as necesidades dos negocios dinámicos actuais. Ao mesmo tempo, as empresas que utilizan a tecnoloxía blockchain dubidan en abandonar Ethereum debido ao seu alto grao de protección contra a piratería e os fallos da rede.

Para garantir a descentralización, seguridade e escalabilidade na cadea de bloques, resolvendo así o trilema de escalabilidade, o equipo de desenvolvemento Oportunidade creou Plasma Cash, unha cadea subsidiaria formada por un contrato intelixente e unha rede privada baseada en Node.js, que transfire periodicamente o seu estado á cadea raíz (Ethereum).

Proba pública: unha solución para a privacidade e a escalabilidade en Ethereum

Procesos clave en Plasma Cash

1. O usuario chama á función de contrato intelixente "depósito", pasando nela a cantidade de ETH que quere depositar no token Plasma Cash. A función de contrato intelixente crea un token e xera un evento sobre el.

2. Os nodos de Plasma Cash subscritos a eventos de contrato intelixente reciben un evento sobre a creación dun depósito e engaden unha transacción sobre a creación dun token ao grupo.

3. Periodicamente, os nodos especiais de Plasma Cash toman todas as transaccións do grupo (ata 1 millón) e forman un bloque a partir delas, calculan a árbore de Merkle e, en consecuencia, o hash. Este bloque envíase a outros nodos para a súa verificación. Os nodos verifican se o hash de Merkle é válido e se as transaccións son válidas (por exemplo, se o remitente do token é o seu propietario). Despois de verificar o bloque, o nodo chama á función "submitBlock" do contrato intelixente, que garda o número de bloque e o hash de Merkle na cadea de bordo. O contrato intelixente xera un evento que indica a adición exitosa dun bloque. As transaccións elimínanse do grupo.

4. Os nós que reciben o evento de envío do bloque comezan a aplicar as transaccións que se engadiron ao bloque.

5. Nalgún momento, o propietario (ou non propietario) do token quere retiralo de Plasma Cash. Para iso, chama á función `startExit`, pasando a ela información sobre as 2 últimas transaccións do token, que confirman que el é o propietario do token. O contrato intelixente, utilizando o hash de Merkle, verifica a presenza de transaccións nos bloques e envía o token para a súa retirada, que se producirá en dúas semanas.

6. Se a operación de retirada do token ocorreu con infraccións (o token gastouse despois de comezar o procedemento de retirada ou o token xa era doutro antes da retirada), o propietario do token pode refutar a retirada nun prazo de dúas semanas.

Proba pública: unha solución para a privacidade e a escalabilidade en Ethereum

A privacidade conséguese de dúas formas

1. A cadea raíz non sabe nada sobre as transaccións que se xeran e envían dentro da cadea filla. A información sobre quen depositou e retirou ETH de Plasma Cash segue sendo pública.

2. A cadea secundaria permite transaccións anónimas mediante zk-SNARK.

Pila de tecnoloxía

  • NodeJS
  • Redis
  • eterio
  • Sild

Probas

Mentres desenvolvemos Plasma Cash, probamos a velocidade do sistema e obtivemos os seguintes resultados:

  • engádense ao grupo ata 35 transaccións por segundo;
  • nun bloque pódense almacenar ata 1 de transaccións.

Realizáronse probas nos seguintes 3 servidores:

1. Intel Core i7-6700 de catro núcleos Skylake incl. SSD NVMe: 512 GB, 64 GB de RAM DDR4
Levantáronse 3 nodos de Plasma Cash validadores.

2. AMD Ryzen 7 1700X Octa-Core "Summit Ridge" (Zen), SSD SATA - 500 GB, 64 GB de RAM DDR4
O nodo ETH de Ropsten testnet foi creado.
Levantáronse 3 nodos de Plasma Cash validadores.

3. Intel Core i9-9900K Octa-Core incl. SSD NVMe: 1 TB, 64 GB de RAM DDR4
Creouse 1 nodo de envío de Plasma Cash.
Levantáronse 3 nodos de Plasma Cash validadores.
Lanzouse unha proba para engadir transaccións á rede Plasma Cash.

Total: 10 nodos Plasma Cash nunha rede privada.

Proba 1

Hai un límite de 1 millón de transaccións por bloque. Polo tanto, 1 millón de transaccións caen en 2 bloques (xa que o sistema consegue tomar parte das transaccións e enviar mentres se envían).


Estado inicial: último bloque #7; Na base de datos gárdanse 1 millón de transaccións e tokens.

00:00 — inicio do script de xeración de transaccións
01:37 - Creáronse 1 millón de transaccións e comezou o envío ao nodo
01:46 — O nodo de envío levou 240 transaccións do grupo e do bloque de formularios #8. Tamén vemos que 320k transaccións engádense ao grupo en 10 segundos
01:58 — o bloque #8 está asinado e enviado para validación
02:03 — o bloque #8 é validado e a función `submitBlock` do contrato intelixente chámase co hash de Merkle e o número de bloque
02:10: o script de demostración rematou de funcionar, que enviou 1 millón de transaccións en 32 segundos
02:33 - os nós comezaron a recibir información de que o bloque #8 foi engadido á cadea raíz e comezaron a realizar 240k transaccións
02:40: elimináronse do grupo 240 transaccións, que xa están no bloque número 8
02:56: o nodo de envío tomou as 760 transaccións restantes do grupo e comezou a calcular o hash de Merkle e o bloque de sinatura #9
03:20: todos os nós conteñen 1 millón de transaccións e tokens de 240k
03:35 — asinase o bloque #9 e envíase para a súa validación a outros nodos
03:41: produciuse un erro de rede
04:40 — esgotouse o tempo de espera da validación do bloque #9
04:54: o nodo de envío tomou as 760 transaccións restantes do grupo e comezou a calcular o hash de Merkle e o bloque de sinatura #9
05:32 — asinase o bloque #9 e envíase para a súa validación a outros nodos
05:53 — o bloque #9 é validado e enviado á cadea raíz
06:17 - os nós comezaron a recibir información de que o bloque #9 foi engadido á cadea raíz e comezaron a realizar 760k transaccións
06:47 — a agrupación limpou as transaccións que están no bloque #9
09:06: todos os nós conteñen 2 millóns de transaccións e tokens

Proba 2

Hai un límite de 350k por bloque. Como resultado, temos 3 bloques.


Estado inicial: último bloque #9; Na base de datos gárdanse 2 millóns de transaccións e tokens

00:00 — o script de xeración de transaccións xa se lanzou
00:44 - Creáronse 1 millón de transaccións e comezou o envío ao nodo
00:56 — O nodo de envío levou 320 transaccións do grupo e do bloque de formularios #10. Tamén vemos que 320k transaccións engádense ao grupo en 10 segundos
01:12 — o bloque #10 asínase e envíase a outros nodos para a súa validación
01:18: o script de demostración rematou de funcionar, que enviou 1 millón de transaccións en 34 segundos
01:20 — o bloque #10 é validado e enviado á cadea raíz
01:51: todos os nós recibiron información da cadea raíz que se engadiu o bloque número 10 e comezan a aplicar 320k transaccións
02:01: o grupo eliminouse para 320 transaccións que se engadiron ao bloque #10
02:15: o nodo de envío levou 350 transaccións do bloque de formularios e grupo n.º 11
02:34 — asinase o bloque #11 e envíase a outros nodos para a súa validación
02:51 — o bloque #11 é validado e enviado á cadea raíz
02:55 — o último nodo completou transaccións do bloque #10
10:59: a transacción co envío do bloque #9 levou moito tempo na cadea raíz, pero completouse e todos os nodos recibiron información sobre ela e comezaron a realizar 350k transaccións
11:05: o grupo eliminouse para 320 transaccións que se engadiron ao bloque #11
12:10: todos os nós conteñen 1 millón de 670k transaccións e tokens
12:17: o nodo de envío levou 330 transaccións do grupo e do bloque de formularios #12
12:32 — asinase o bloque #12 e envíase a outros nodos para a súa validación
12:39 — o bloque #12 é validado e enviado á cadea raíz
13:44: todos os nós recibiron información da cadea raíz que se engadiu o bloque número 12 e comezan a aplicar 330k transaccións
14:50: todos os nós conteñen 2 millóns de transaccións e tokens

Proba 3

No primeiro e segundo servidor, un nodo de validación foi substituído por un nodo de envío.


Estado inicial: último bloque #84; 0 transaccións e tokens gardados na base de datos

00:00 — Lanzáronse 3 scripts que xeran e envían 1 millón de transaccións cada un
01:38 — Creáronse 1 millón de transaccións e comezou o envío para enviar o nodo #3
01:50: o nodo de envío n.º 3 levou 330 transaccións do bloque de formularios e grupo n.º 85 (f21). Tamén vemos que 350k transaccións engádense ao grupo en 10 segundos
01:53 — Creáronse 1 millón de transaccións e comezou o envío para enviar o nodo #1
01:50: o nodo de envío n.º 3 levou 330 transaccións do bloque de formularios e grupo n.º 85 (f21). Tamén vemos que 350k transaccións engádense ao grupo en 10 segundos
02:01: o nodo de envío n.º 1 levou 250 transaccións do grupo e do bloque de formularios n.º 85 (65e)
02:06 — asinase o bloque #85 (f21) e envíase a outros nodos para a súa validación
02:08 — o script de demostración do servidor #3, que enviou 1 millón de transaccións en 30 segundos, rematou de funcionar
02:14 — o bloque #85 (f21) é validado e enviado á cadea raíz
02:19 — asinase o bloque #85 (65e) e envíase a outros nodos para a súa validación
02:22 — Creáronse 1 millón de transaccións e comezou o envío para enviar o nodo #2
02:27 — bloque #85 (65e) validado e enviado á cadea raíz
02:29 — O nodo de envío n.º 2 levou 111855 transaccións do bloque de formularios e grupo n.º 85 (256).
02:36 — asinase o bloque #85 (256) e envíase a outros nodos para a súa validación
02:36 — o script de demostración do servidor #1, que enviou 1 millón de transaccións en 42.5 segundos, rematou de funcionar
02:38 — o bloque #85 (256) é validado e enviado á cadea raíz
03:08 — O script do servidor #2 rematou de funcionar, que enviou 1 millón de transaccións en 47 segundos
03:38 - todos os nós recibiron información da cadea raíz que engadíronse os bloques #85 (f21), #86(65e), #87(256) e comezaron a aplicar transaccións de 330k, 250k, 111855
03:49: o grupo foi borrado en 330k, 250k, 111855 transaccións que se engadiron aos bloques #85 (f21), #86(65e), #87(256)
03:59 — O nodo de envío n.º 1 levou 888145 transaccións do bloque de formularios n.º 88 (214), o no de envío n.º 2 levou 750 transaccións do bloque de grupos e de formularios n.º 88 (50a), o no de envío n.º 3 levou 670 transaccións de a piscina e forma o bloque #88 (d3b)
04:44 — asinase o bloque #88 (d3b) e envíase a outros nodos para a súa validación
04:58 — asinase o bloque #88 (214) e envíase a outros nodos para a súa validación
05:11 — asinase o bloque #88 (50a) e envíase a outros nodos para a súa validación
05:11 — o bloque #85 (d3b) é validado e enviado á cadea raíz
05:36 — o bloque #85 (214) é validado e enviado á cadea raíz
05:43 - todos os nós recibiron información da cadea raíz que engadíronse os bloques #88 (d3b), #89(214) e comezan a aplicar transaccións de 670k e 750k
06:50 — debido a un fallo de comunicación, o bloque #85 (50a) non se validou
06:55 — o nodo de envío n.º 2 levou 888145 transaccións do bloque de formularios e grupo n.º 90 (50a)
08:14 — asinase o bloque #90 (50a) e envíase a outros nodos para a súa validación
09:04 — o bloque #90 (50a) é validado e enviado á cadea raíz
11:23: todos os nós recibiron información da cadea raíz que se engadiu o bloque #90 (50a) e comezan a aplicar 888145 transaccións. Ao mesmo tempo, o servidor #3 xa aplicou transaccións dos bloques #88 (d3b), #89(214)
12:11 - todas as piscinas están baleiras
13:41: todos os nodos do servidor #3 conteñen 3 millóns de transaccións e tokens
14:35: todos os nodos do servidor #1 conteñen 3 millóns de transaccións e tokens
19:24: todos os nodos do servidor #2 conteñen 3 millóns de transaccións e tokens

Obstáculos

Durante o desenvolvemento de Plasma Cash, atopamos os seguintes problemas, que pouco a pouco resolvemos e estamos resolvendo:

1. Conflito na interacción de varias funcións do sistema. Por exemplo, a función de engadir transaccións ao pool bloqueou o traballo de envío e validación de bloques, e viceversa, o que provocou unha baixada da velocidade.

2. Non estaba claro de inmediato como enviar un gran número de transaccións minimizando os custos de transferencia de datos.

3. Non estaba claro como e onde almacenar os datos para conseguir altos resultados.

4. Non estaba claro como organizar unha rede entre nós, xa que o tamaño dun bloque con 1 millón de transaccións ocupa uns 100 MB.

5. Traballar en modo de fío único rompe a conexión entre os nós cando se producen cálculos longos (por exemplo, construír unha árbore Merkle e calcular o seu hash).

Como afrontamos todo isto?

A primeira versión do nodo Plasma Cash era unha especie de combinación que podía facer todo ao mesmo tempo: aceptar transaccións, enviar e validar bloques e proporcionar unha API para acceder aos datos. Dado que NodeJS ten un fío único nativo, a función de cálculo da árbore de Merkle pesada bloqueou a función de engadir transaccións. Vimos dúas opcións para resolver este problema:

1. Inicie varios procesos NodeJS, cada un dos cales realiza funcións específicas.

2. Use worker_threads e move a execución de parte do código a threads.

Como resultado, usamos ambas opcións ao mesmo tempo: dividimos loxicamente un nodo en 3 partes que poden funcionar por separado, pero ao mesmo tempo de forma sincronizada.

1. Nodo de envío, que acepta transaccións no grupo e crea bloques.

2. Un nodo validador que verifica a validez dos nodos.

3. Nodo API: proporciona unha API para acceder aos datos.

Neste caso, pode conectarse a cada nodo a través dun socket Unix usando cli.

Movamos operacións pesadas, como o cálculo da árbore de Merkle, nun fío separado.

Así, conseguimos o funcionamento normal de todas as funcións de Plasma Cash de forma simultánea e sen fallos.

Unha vez que o sistema estivo funcional, comezamos a probar a velocidade e, por desgraza, recibimos resultados pouco satisfactorios: 5 transaccións por segundo e ata 000 transaccións por bloque. Tiven que descubrir o que se implementou incorrectamente.

Para comezar, comezamos a probar o mecanismo de comunicación con Plasma Cash para descubrir a capacidade máxima do sistema. Escribimos anteriormente que o nodo Plasma Cash ofrece unha interface de socket Unix. Inicialmente estaba baseado en texto. Os obxectos json enviáronse usando `JSON.parse()` e `JSON.stringify()`.

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

Medimos a velocidade de transferencia destes obxectos e atopamos ~ 130k por segundo. Tentamos substituír as funcións estándar para traballar con json, pero o rendemento non mellorou. O motor V8 debe estar ben optimizado para estas operacións.

Traballamos con transaccións, fichas e bloques a través de clases. Ao crear tales clases, o rendemento baixou 2 veces, o que indica que OOP non é axeitado para nós. Tiven que reescribir todo a un enfoque puramente funcional.

Gravación na base de datos

Inicialmente, Redis foi escollida para o almacenamento de datos como unha das solucións máis produtivas que satisface os nosos requisitos: almacenamento de clave-valor, traballo con táboas hash, conxuntos. Lanzamos redis-benchmark e obtivemos ~80 operacións por segundo nun modo de canalización.

Para un alto rendemento, afinamos Redis con máis precisión:

  • Estableceuse unha conexión de socket Unix.
  • Desactivamos o gardar o estado no disco (para fiabilidade, pode configurar unha réplica e gardar no disco nun Redis separado).

En Redis, un pool é unha táboa hash porque necesitamos poder recuperar todas as transaccións nunha consulta e eliminar as transaccións unha por unha. Tentamos usar unha lista normal, pero é máis lento ao descargar a lista completa.

Ao usar NodeJS estándar, as bibliotecas Redis lograron un rendemento de 18k transaccións por segundo. A velocidade baixou 9 veces.

Dado que o benchmark nos mostrou que as posibilidades eran claramente 5 veces maiores, comezamos a optimizar. Cambiamos a biblioteca a ioredis e obtivemos un rendemento de 25k por segundo. Engadimos transaccións unha por unha usando o comando `hset`. Así que estabamos xerando moitas consultas en Redis. Xurdiu a idea de combinar transaccións en lotes e envialas cun só comando `hmset`. O resultado é 32k por segundo.

Por varias razóns, que describiremos a continuación, traballamos con datos usando `Buffer` e, como resulta, se os convertes en texto (`buffer.toString('hex')`) antes de escribir, podes obter máis información. rendemento. Así, a velocidade aumentou a 35k por segundo. Polo momento, decidimos suspender máis optimizacións.

Tivemos que cambiar a un protocolo binario porque:

1. O sistema adoita calcular hash, sinaturas, etc., e para iso necesita datos no `Buffer.

2. Cando se envían entre servizos, os datos binarios pesan menos que o texto. Por exemplo, ao enviar un bloque con 1 millón de transaccións, os datos do texto poden ocupar máis de 300 megabytes.

3. A transformación constante dos datos afecta o rendemento.

Polo tanto, tomamos como base o noso propio protocolo binario para almacenar e transmitir datos, desenvolvido sobre a base da marabillosa biblioteca de datos binarios.

Como resultado, obtivemos as seguintes estruturas 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,
  }
  ```

- Ficha

  ```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,
  }
  ```

Cos comandos habituais `BD.encode(block, Protocol).slice();` e `BD.decode(buffer, Protocol)` convertemos os datos en `Buffer` para gardar en Redis ou reenviar a outro nodo e recuperar o datos de volta.

Tamén temos 2 protocolos binarios para transferir datos entre servizos:

— Protocolo para a interacción con Plasma Node a través do socket Unix

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

en que:

  • `tipo` — a acción que se vai realizar, por exemplo, 1 — sendTransaction, 2 — getTransaction;
  • 'carga útil' — datos que deben pasarse á función apropiada;
  • `messageId` — ID da mensaxe para que se poida identificar a resposta.

— 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)
  }
  ```

en que:

  • 'código' — código de mensaxe, por exemplo 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — versión do protocolo, xa que os nodos con versións diferentes poden ser creados na rede e poden funcionar de forma diferente;
  • `seq` - identificador da mensaxe;
  • `countChunk` и `número de anaco` necesario para dividir mensaxes grandes;
  • 'lonxitude' и 'carga útil' lonxitude e os propios datos.

Xa que escribimos previamente os datos, o sistema final é moito máis rápido que a biblioteca `rlp` de Ethereum. Desafortunadamente, aínda non puidemos rexeitalo, xa que é necesario finalizar o contrato intelixente, o que pensamos facer no futuro.

Se conseguimos alcanzar a velocidade 35 000 transaccións por segundo, tamén necesitamos procesalas no momento óptimo. Dado que o tempo aproximado de formación do bloque leva 30 segundos, debemos incluír no bloque 1 000 000 transaccións, o que significa enviar máis 100 MB de datos.

Inicialmente, usamos a biblioteca `ethereumjs-devp2p` para comunicarnos entre nós, pero non podía xestionar tantos datos. Como resultado, usamos a biblioteca `ws` e configuramos o envío de datos binarios a través de websocket. Por suposto, tamén atopamos problemas ao enviar paquetes de datos grandes, pero dividímolos en anacos e agora estes problemas desapareceron.

Tamén formando unha árbore de Merkle e calculando o hash 1 000 000 transaccións require sobre 10 segundos de cálculo continuo. Durante este tempo, a conexión con todos os nodos consegue romper. Decidiuse mover este cálculo a un fío separado.

Conclusións:

De feito, os nosos descubrimentos non son novos, pero por algún motivo moitos expertos esquécense deles ao desenvolver.

  • Usar a programación funcional en lugar da programación orientada a obxectos mellora a produtividade.
  • O monolito é peor que unha arquitectura de servizo para un sistema NodeJS produtivo.
  • Usar `worker_threads` para computación pesada mellora a capacidade de resposta do sistema, especialmente cando se trata de operacións de E/S.
  • o socket unix é máis estable e máis rápido que as solicitudes http.
  • Se precisa transferir rapidamente grandes datos a través da rede, é mellor usar websockets e enviar datos binarios, divididos en anacos, que se poden reenviar se non chegan e combinalos nunha soa mensaxe.

Convidámosvos a visitar GitHub proxecto: https://github.com/opporty-com/Plasma-Cash/tree/new-version

O artigo foi co-escrito por Alexander Nashivan, desenvolvedor senior Clever Solution Inc.

Fonte: www.habr.com

Engadir un comentario