Teste público: solução para privacidade e escalabilidade no Ethereum

Blockchain é uma tecnologia inovadora que promete melhorar muitas áreas da vida humana. Ele transfere processos e produtos reais para o espaço digital, garante a velocidade e a confiabilidade das transações financeiras, reduz seus custos e também permite criar aplicativos DAPP modernos usando contratos inteligentes em redes descentralizadas.

Dadas as muitas vantagens e os diversos usos do blockchain, pode parecer estranho que essa tecnologia promissora ainda não tenha penetrado em todos os setores. O problema é que as blockchains descentralizadas modernas não têm escalabilidade. A Ethereum processa cerca de 20 transações por segundo, o que não é suficiente para atender às necessidades dos negócios dinâmicos de hoje. Ao mesmo tempo, as empresas que usam a tecnologia blockchain não se atrevem a abandonar o Ethereum devido ao seu alto grau de proteção contra hackers e falhas de rede.

Para garantir a descentralização, a segurança e a escalabilidade do blockchain, resolvendo o Trilemma de Escalabilidade, a equipe de desenvolvimento do Opporty criou o Plasma Cash - uma cadeia filha que consiste em um contrato inteligente e uma rede privada baseada no Node.js, transferindo periodicamente seu estado para a cadeia raiz ( Ethereum).



Principais processos na Plasma Cash


1. O usuário chama a função do contrato inteligente de 'depósito', transferindo a quantia em ETH para ele, que ele deseja colocar no token de Plasma Cash. A função de contrato inteligente cria um token e gera um evento sobre ele.

2. Os nós do Plasma Cash inscritos nos eventos do contrato inteligente recebem um evento sobre a criação de um depósito e adicionam uma transação sobre a criação de um token ao pool.

3. Periodicamente, nós especiais do Plasma Cash pegam todas as transações do pool (até 1 milhão) e formam um bloco a partir delas, calculam a árvore Merkle e, consequentemente, o hash. Este bloco é enviado para outros nós para verificação. Os nós verificam se o hash Merkle é válido, se as transações são válidas (por exemplo, se o remetente do token é seu proprietário). Após verificar o bloco, o nó chama a função `submitBlock` do contrato inteligente, que armazena o número e o hash Merkle do bloco na cadeia de rastreio. Um contrato inteligente gera um evento sobre a adição bem-sucedida de um bloco. As transações são excluídas do pool.

4. Os nós que receberam o evento sobre o envio do bloco começam a aplicar as transações que foram adicionadas ao bloco.

5. Em algum momento, o proprietário (ou não proprietário) do token deseja retirá-lo do Plasma Cash. Para fazer isso, ele chama a função `startExit`, passando informações sobre as duas últimas transações no token, o que confirma que ele é o proprietário do token. O contrato inteligente, usando o hash Merkle, verifica transações em blocos e envia um token para a saída, o que acontecerá em duas semanas.

6. Se a operação de retirada de token ocorreu com violações (o token foi gasto após o início do procedimento de retirada ou o token já era um estranho antes da retirada), o proprietário do token pode refutá-la dentro de duas semanas.



A privacidade é alcançada de duas maneiras.


1. A cadeia raiz não sabe nada sobre transações que são formadas e encaminhadas dentro da cadeia filha. Restam informações sobre quem iniciou e retirou a ETH de / para o Plasma Cash.

2. A cadeia filha permite que você organize transações anônimas usando zk-SNARKs.

Pilha tecnológica


  • NodeJS
  • Redis
  • Ethereum
  • Soild

Teste


Ao desenvolver o Plasma Cash, testamos a velocidade do sistema e obtivemos os seguintes resultados:

  • até 35.000 transações por segundo são adicionadas ao pool;
  • até 1.000.000 de transações podem ser armazenadas no bloco.

Os testes foram realizados nos três servidores a seguir:

1. Intel Core i7-6700 Skylake de quatro núcleos, incl. SSD NVMe - 512 GB, 64 GB DDR4 RAM
Foram levantados 3 nós de Plasma Cash validados.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SSD SATA - 500 GB, RAM DDR4 de 64 GB
O nó ETH da Ropsten testnet foi criado.
Foram levantados 3 nós de Plasma Cash validados.

3. Intel Core i9-9900K Octa-Core incl. SSD NVMe - 1 TB, 64 GB de RAM DDR4
1 O nó Submit Plasma Cash foi criado.
Foram levantados 3 nós de Plasma Cash validados.
Foi lançado um teste para adicionar transações à rede Plasma Cash.

Total: 10 nós do Plasma Cash em uma rede privada.

Teste 1


Há um limite de 1 milhão de transações por bloco. Portanto, 1 milhão de transações se enquadram em 2 blocos (já que o sistema consegue participar das transações e envia enquanto são enviadas).


Estado inicial: último bloco 7; 1 milhão de transações e tokens são armazenados no banco de dados.

00:00 - inicia o script de geração de transação
01:37 - 1 milhão de transações foram criadas e o envio ao nó começou
01:46 - o nó de envio pegou 240k transações do pool e forma o bloco 8. Também vemos que 320k transações são adicionadas ao pool em 10 segundos
01:58 - o bloco 8 é assinado e enviado para validação
02:03 - o bloco 8 é validado e a função `submitBlock` do contrato inteligente com o hash Merkle e o número do bloco é chamada
02:10 - o script de demonstração terminou de funcionar, o que enviou 1 milhão de transações em 32 segundos
02:33 - os nós começaram a receber informações de que o bloco 8 foi adicionado à cadeia raiz e começaram a executar transações de 240k
02:40 - 240k transações foram excluídas do pool, que já estão no bloco 8
02:56 - o nó de envio pegou as 760k transações restantes do pool e começou a calcular o hash e o bloco de sinal Merkle # 9
03:20 - todos os nós contêm transações e tokens de 1mln 240k
03:35 - o bloco 9 é assinado e enviado para validação para outros nós
03:41 - ocorreu um erro de rede
04:40 - por tempo limite, a espera pela validação do bloco # 9 parou
04:54 - o nó de envio pegou as 760k transações restantes do pool e começou a calcular o hash e o bloco de sinal Merkle # 9
05:32 - o bloco 9 é assinado e enviado para validação para outros nós
05:53 - o bloco 9 é validado e enviado para a cadeia raiz
06:17 - os nós começaram a receber informações de que o bloco n ° 9 foi adicionado à cadeia raiz e começaram a executar transações de 760k
06:47 - o pool limpo de transações que estão no bloco # 9
09:06 - todos os nós contêm 2 milhões de transações e tokens

Teste 2


Há um limite de 350k por bloco. Como resultado, temos 3 blocos.


Estado inicial: último bloco # 9; 2 milhões de transações e tokens armazenados no banco de dados

00:00 - o script de geração de transação já está em execução
00:44 - 1 milhão de transações foram criadas e o envio ao nó começou
00:56 - o nó de envio retirou 320k transações do pool e forma o bloco # 10. Também vemos que 320k transações são adicionadas ao pool em 10 segundos
01:12 - o bloco 10 é assinado e enviado para outros nós para validação
01:18 - o script de demonstração terminou de funcionar, o que enviou 1 milhão de transações em 34 segundos
01:20 - o bloco 10 é validado e enviado para a cadeia raiz
01:51 - todos os nós receberam informações da cadeia raiz que o bloco nº 10 foi adicionado e estão começando a aplicar transações de 320k
02:01 - o pool foi limpo para 320k transações adicionadas ao bloco # 10
02:15 - o nó de envio recebeu 350 mil transações do pool e forma o bloco # 11
02:34 - o bloco 11 é assinado e enviado para outros nós para validação
02:51 - o bloco 11 é validado e enviado para a cadeia raiz
02:55 - o último nó executou transações do bloco # 10
10:59 - muito tempo na cadeia raiz, uma transação foi executada com o envio do bloco nº 9, mas foi concluída e todos os nós receberam informações sobre isso e começaram a executar transações de 350k
11:05 - o pool foi limpo para 320k transações adicionadas ao bloco # 11
12:10 - todos os nós contêm 1 milhão de transações e tokens de 670k
12:17 - o nó de envio recebeu 330k transações do pool e forma o bloco # 12
12:32 - o bloco 12 é assinado e enviado para outros nós para validação
12:39 - o bloco 12 é validado e enviado para a cadeia raiz
13:44 - todos os nós receberam informações da cadeia raiz que o bloco nº 12 foi adicionado e estão começando a aplicar transações de 330k
14:50 - todos os nós contêm 2 milhões de transações e tokens

Teste 3


No primeiro e no segundo servidores, um nó de validação foi substituído por um nó de envio.


Estado inicial: último bloco # 84; 0 transações e tokens são armazenados no banco de dados

00:00 - 3 scripts são lançados que geram e enviam 1 milhão de transações
01:38 - 1 milhão de transações foram criadas e o envio para enviar o nó nº 3 começou
01:50 - o nó de envio 3 levou 330 mil transações do pool e forma o bloco 85 (f21). Também vemos que 350 mil transações são adicionadas ao pool em 10 segundos
01:53 - 1 milhão de transações foram criadas e o envio para o nó nº 1 começou
01:50 - o nó de envio 3 levou 330 mil transações do pool e forma o bloco 85 (f21). Também vemos que 350 mil transações são adicionadas ao pool em 10 segundos
02:01 - o nó de envio nº 1 recebeu 250 mil transações do pool e forma o bloco nº 85 (65e)
02:06 - o bloco 85 (f21) é assinado e enviado para outros nós para validação
02:08 - o script de demonstração do servidor nº 3 terminou de funcionar, o que enviou transações de 1mln em 30 segundos
02:14 - o bloco 85 (f21) é validado e enviado para a cadeia raiz
02:19 - o bloco 85 (65e) é assinado e enviado para outros nós para validação
02:22 - 1 milhão de transações foram criadas e o envio para o nó 2 foi iniciado
02:27 - o bloco 85 (65e) é validado e enviado para a cadeia raiz
02:29 - o nó de envio 2 retirou do pool 111855 transações e formulários de bloco 85 (256).
02:36 - o bloco 85 (256) é assinado e enviado para outros nós para validação
02:36 - o script de demonstração do servidor nº 1 terminou de funcionar, que enviou transações de 1 mln em 42,5 segundos
02:38 - o bloco 85 (256) é validado e enviado para a cadeia raiz
03:08 - o script do servidor nº 2, que enviou 1 milhão de transações em 47 segundos, terminou de funcionar
03:38 - todos os nós receberam informações da cadeia raiz de que os blocos # 85 (f21), # 86 (65e), # 87 (256) foram adicionados e estão começando a aplicar transações de 330k, 250k, 111855
03:49 - o pool foi limpo em 330k, 250k, 111855 transações que foram adicionadas aos blocos # 85 (f21), # 86 (65e), # 87 (256)
03:59 - enviar o nó nº 1 do bloco 888145 de transações e formulários do bloco # 88 (214), enviar nó nº 2 do bloco de transações 750k e formulários nº 750 (50a), enviar o nó nº 3 do bloco 670k das transações e forma o bloco # 88 (d3b)
04:44 - o bloco 88 (d3b) é assinado e enviado para outros nós para validação
04:58 - o bloco 88 (214) é assinado e enviado para outros nós para validação
05:11 - o bloco 88 (50a) é assinado e enviado para outros nós para validação
05:11 - o bloco 85 (d3b) é validado e enviado para a cadeia raiz
05:36 - o bloco 85 (214) é validado e enviado para a cadeia raiz
05:43 - todos os nós receberam informações da cadeia raiz que bloqueia # 88 (d3b), # 89 (214) foram adicionados e começam a aplicar transações de 670k e 750k
06:50 - devido a uma desconexão, o bloco 85 (50a) não foi validado
06:55 - o nó de envio 2 retirou 888145 transações do pool e forma o bloco 90 (50a)
08:14 - o bloco 90 (50a) é assinado e enviado para outros nós para validação
09:04 - o bloco 90 (50a) é validado e enviado para a cadeia raiz
11:23 - todos os nós receberam informações da cadeia raiz que adicionaram o bloco 90 (50a) e 888145 transações começaram a ser aplicadas. Ao mesmo tempo, o servidor nº 3 aplica transações há muito tempo dos blocos nº 88 (d3b), nº 89 (214)
12:11 - todas as piscinas estão vazias
13:41 - todos os nós do servidor # 3 contêm 3 milhões de transações e tokens
14:35 - todos os nós do servidor # 1 contêm 3 milhões de transações e tokens
19:24 - todos os nós do servidor # 2 contêm 3 milhões de transações e tokens

Obstáculos


Durante o desenvolvimento do Plasma Cash, encontramos os seguintes problemas, que gradualmente resolvemos e estamos resolvendo:

1. O conflito de interação de várias funções do sistema. Por exemplo, a função de adicionar transações ao pool bloqueou o envio e a validação de blocos e vice-versa, o que levou a uma redução na velocidade.

2. Não ficou claro imediatamente como enviar um grande número de transações e, ao mesmo tempo, minimizar o custo da transferência de dados.

3. Não ficou claro como e onde armazenar dados para obter altos resultados.

4. Não ficou claro como organizar uma rede entre nós, pois o tamanho do bloco com 1 milhão de transações leva cerca de 100 MB.

5. Trabalhar no modo de thread único interrompe a conexão entre nós quando ocorrem cálculos longos (por exemplo, construindo uma árvore Merkle e calculando seu hash).

Como lidamos com tudo isso?


A primeira versão do nó Plasma Cash era uma espécie de combinação que podia fazer tudo ao mesmo tempo: aceitar transações, enviar e validar blocos, fornecer uma API para acessar dados. Como o NodeJS foi inicialmente encadeado único, a função pesada de cálculo da árvore Merkle bloqueou a função de adição de transação. Vimos duas opções para resolver esse problema:

1. Execute vários processos do NodeJS, cada um deles executando determinadas funções.

2. Use worker_threads e coloque a execução do código em threads.

Como resultado, usamos as duas opções ao mesmo tempo: dividimos logicamente um nó em 3 partes, que podem funcionar separadamente, mas ao mesmo tempo de forma síncrona

1. Envie um nó que aceite transações para o pool e crie blocos.

2. Nó de validação que verifica a validade dos nós.

3. API do nó - fornece uma API para acessar dados.

Ao mesmo tempo, você pode conectar-se a cada nó através de um soquete unix usando cli.

Operações pesadas, como o cálculo da árvore Merkle, foram realizadas em um fluxo separado.

Assim, alcançamos a operação normal de todas as funções do Plasma Cash simultaneamente e sem falhas.

Assim que o sistema funcionou funcionalmente, começamos a testar a velocidade e, infelizmente, obtivemos resultados insatisfatórios: 5.000 transações por segundo e até 50.000 transações em um bloco. Eu tive que descobrir o que foi implementado incorretamente.

Para começar, começamos a testar o mecanismo de comunicação com o Plasma Cash para descobrir a capacidade de pico do sistema. Escrevemos anteriormente que o nó Plasma Cash fornece uma interface de soquete unix. Foi originalmente textual. Os objetos json foram enviados 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 transferência de tais objetos e recebemos ~ 130k por segundo. Eles tentaram substituir as funções padrão pelo json, mas o desempenho não melhorou. Deve haver um mecanismo V8 bem otimizado para essas operações.

O trabalho com transações, tokens, blocos foi realizado por meio de classes. Ao criar essas classes, o desempenho caiu 2 vezes, o que indica: OOP não é adequado para nós. Eu tive que reescrever tudo em uma abordagem puramente funcional.

Gravar no banco de dados


Inicialmente, o Redis foi escolhido para armazenamento de dados como uma das soluções mais produtivas que satisfazem nossos requisitos: armazenamento de valor-chave, trabalho com tabelas de hash e muitos. Lançamos o benchmark redis e obtivemos ~ 80k operações por segundo no modo 1 pipelining.

Para alto desempenho, ajustamos o Redis com mais detalhes:

  • Estabeleceu uma conexão de soquete unix.
  • Desative o salvamento de estado no disco (para confiabilidade, você pode configurar a réplica e já salvar no disco em um Redis separado).

No Redis, um pool é uma tabela de hash, pois precisamos receber todas as transações em uma solicitação e excluir transações uma a uma. Tentamos usar uma lista regular, mas ela funciona mais lentamente ao descarregar a lista inteira.

Usando a biblioteca NodeJS padrão, as bibliotecas Redis alcançaram 18 mil transações por segundo de desempenho. A velocidade caiu 9 vezes.

Como o benchmark nos mostrou as possibilidades claramente 5 vezes mais, elas começaram a otimizar. Alteramos a biblioteca para ioredis e obtivemos um desempenho de 25k por segundo. Adicionamos transações uma a uma usando o comando `hset`. Assim, geramos muitas solicitações no Redis. Havia uma idéia de mesclar transações em pacotes configuráveis ​​e enviá-las com um comando hmset. O resultado é 32k por segundo.

Por várias razões, que serão descritas abaixo, trabalhamos com dados usando o `Buffer` e, como se viu, se você o traduzir em texto (` buffer.toString ('hex') `) antes de escrever, você poderá obter desempenho adicional. Assim, a velocidade foi aumentada para 35k por segundo. No momento, decidimos suspender a otimização adicional.

Tivemos que mudar para o protocolo binário porque:

1. O sistema geralmente calcula hashes, assinaturas etc., e para isso precisa de dados no `Buffer.

2. Ao transferir entre serviços, os dados binários pesam menos que o texto. Por exemplo, ao enviar um bloco com 1 milhão de transações, os dados no texto podem ocupar mais de 300 megabytes.

3. A conversão contínua de dados afeta o desempenho.

Portanto, tomamos como base nosso próprio protocolo binário para armazenamento e transmissão de dados, desenvolvido com base na maravilhosa biblioteca de dados binários.

Como resultado, temos as seguintes estruturas de dados:

- Transação


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

- Bloco


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

Pelos comandos usuais `BD.encode (block, Protocol) .slice ();` e `BD.decode (buffer, Protocol)` convertemos os dados em `Buffer` para salvá-los no Redis ou transferi-los para outro nó e recuperá-los novamente.

Também temos 2 protocolos binários para transferir dados entre serviços:

- Protocolo para interagir com o Plasma Node via soquete 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) } ``` 

onde:

  • `type` - ação a ser executada, por exemplo, 1 - sendTransaction, 2 - getTransaction;
  • `carga útil ' - dados a serem transferidos para a função correspondente;
  • `messageId` - ID da mensagem para que a resposta possa ser identificada.

- Protocolo de interação entre nós

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

onde:

  • `code` - código de mensagem, por exemplo 6 - PREPARE_NEW_BLOCK, 7 - BLOCK_VALID, 8 - BLOCK_COMMIT;
  • `versionProtocol` - versão do protocolo, já que nós com versões diferentes podem ser criados na rede e podem trabalhar de maneiras diferentes;
  • `seq` - identificador de mensagem;
  • `countChunk` e` chunkNumber` são necessários para dividir mensagens grandes;
  • ` length` e` payload` o comprimento e os dados em si.

Desde que digitamos os dados de antemão, o sistema final é muito mais rápido que a biblioteca `rlp` do Ethereum. Infelizmente, ainda não fomos capazes de recusar, pois é necessário refinar o contrato inteligente, que planejamos fazer no futuro.

Se conseguimos atingir uma velocidade de 35.000 transações por segundo, também precisamos processá-las no tempo ideal. Como o tempo aproximado de formação do bloco leva 30 segundos, precisamos incluir 1.000.000 de transações no bloco, o que significa enviar mais de 100 mb de dados.

Inicialmente, usamos a biblioteca `ethereumjs-devp2p` para comunicar nós, mas ela não suportava tantos dados. Como resultado, usamos a biblioteca `ws` e configuramos a transferência de dados binários no websocket. Obviamente, também encontramos problemas ao enviar pacotes de dados grandes, mas os dividimos em partes e agora não existem esses problemas.

Além disso, a formação da árvore Merkle e o cálculo do hash de 1.000.000 transações requerem cerca de 10 segundos de cálculo contínuo. Durante esse período, a conexão com todos os nós consegue quebrar. Decidiu-se transferir esse cálculo para um thread separado.

Conclusões:


De fato, nossas descobertas não são novas, mas, por algum motivo, muitos especialistas as esquecem durante o desenvolvimento.

  • Usar a Programação Funcional em vez da Programação Orientada a Objetos aumenta o desempenho.
  • Um monólito é pior que uma arquitetura de serviço para um sistema de produção no NodeJS.
  • O uso de `worker_threads` para computação pesada melhora a capacidade de resposta do sistema, especialmente ao trabalhar com operações de E / S.
  • O soquete unix é mais estável e mais rápido que as solicitações http.
  • Se você precisar transferir rapidamente grandes dados pela rede, é melhor usar os soquetes da Web e enviar dados binários, divididos em partes, que podem ser encaminhadas se não chegarem e depois mesclados em uma única mensagem.

Convidamos você a visitar o projeto GitHub : https://github.com/opporty-com/Plasma-Cash/tree/new-version

O artigo foi co-escrito por Alexander Nashivan , desenvolvedor sênior da Clever Solution Inc.

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


All Articles