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

Bloccain é uma tecnologia inovadora que promete melhorar muitas áreas da vida humana. Transfere processos e produtos reais para o espaço digital, garante rapidez e confiabilidade nas transações financeiras, reduz seus custos e também permite criar aplicações DAPP modernas usando contratos inteligentes em redes descentralizadas.

Dados os muitos benefícios e diversas aplicações do blockchain, pode parecer surpreendente que esta tecnologia promissora ainda não tenha chegado a todos os setores. O problema é que os blockchains descentralizados modernos carecem de escalabilidade. 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 utilizam a tecnologia blockchain hesitam em abandonar o Ethereum devido ao seu alto grau de proteção contra hackers e falhas de rede.

Para garantir a descentralização, segurança e escalabilidade no blockchain, resolvendo assim o Trilema de Escalabilidade, a equipe de desenvolvimento Oportante criou o Plasma Cash, uma cadeia subsidiária composta por um contrato inteligente e uma rede privada baseada em Node.js, que transfere periodicamente seu estado para a cadeia raiz (Ethereum).

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

Processos-chave no Plasma Cash

1. O usuário chama a função do contrato inteligente de `depósito`, passando para ela a quantidade de ETH que deseja depositar no token 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 em eventos de 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 a outros nós para verificação. Os nós verificam se o hash Merkle é válido e se as transações são válidas (por exemplo, se o remetente do token é o seu proprietário). Após verificar o bloco, o nó chama a função `submitBlock` do contrato inteligente, que salva o número do bloco e o hash Merkle na cadeia de borda. O contrato inteligente gera um evento indicando a adição bem-sucedida de um bloco. As transações são removidas do pool.

4. Os nós que recebem o evento de 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 para ela informações sobre as 2 últimas transações do token, que confirmam que ele é o proprietário do token. O contrato inteligente, utilizando o hash Merkle, verifica a presença de transações nos blocos e envia o token para saque, o que ocorrerá em duas semanas.

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

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

A privacidade é alcançada de duas maneiras

1. A cadeia raiz não sabe nada sobre as transações geradas e encaminhadas na cadeia filha. As informações sobre quem depositou e retirou ETH do Plasma Cash permanecem públicas.

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

Pilha de tecnologia

  • NodeJS
  • Redis
  • Etherium
  • Soild

Teste

Durante o desenvolvimento do Plasma Cash, testamos a velocidade do sistema e obtivemos os seguintes resultados:

  • até 35 transações por segundo são adicionadas ao pool;
  • até 1 de transações podem ser armazenadas em um bloco.

Os testes foram realizados nos seguintes 3 servidores:

1. Intel Core i7-6700 Quad-Core Skylake incl. SSD NVMe – 512 GB, 64 GB de RAM DDR4
3 nós de validação do Plasma Cash foram criados.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SSD SATA – 500 GB, 64 GB de RAM DDR4
O nó ETH da rede de teste Ropsten foi criado.
3 nós de validação do Plasma Cash foram criados.

3. Intel Core i9-9900K Octa-Core incl. SSD NVMe – 1 TB, 64 GB de RAM DDR4
1 nó de envio do Plasma Cash foi gerado.
3 nós de validação do Plasma Cash foram criados.
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 caem em 2 blocos (já que o sistema consegue pegar parte das transações e enviar enquanto elas estão sendo enviadas).


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

00:00 — início do script de geração de transação
01:37 - 1 milhão de transações foram criadas e o envio para o nó começou
01:46 – o nó de envio pegou 240 mil transações do pool e formou o bloco nº 8. Também vemos que 320 mil transações são adicionadas ao pool em 10 segundos
01:58 — bloco #8 é assinado e enviado para validação
02:03 — o bloco #8 é validado e a função `submitBlock` do contrato inteligente é chamada com o hash Merkle e o número do bloco
02:10 — o script de demonstração terminou de funcionar, enviando 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 realizar 240 mil transações
02:40 - 240 mil transações foram removidas do pool, que já estão no bloco #8
02:56 – o nó de envio pegou as 760 mil transações restantes do pool e começou a calcular o hash Merkle e assinar o bloco nº 9
03:20 - todos os nós contêm 1 milhão de transações e tokens de 240 mil
03:35 — bloco #9 é assinado e enviado para validação para outros nós
03:41 - ocorreu um erro de rede
04:40 — a espera pela validação do bloco #9 expirou
04:54 – o nó de envio pegou as 760 mil transações restantes do pool e começou a calcular o hash Merkle e assinar o bloco nº 9
05:32 — 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 #9 foi adicionado à cadeia raiz e começaram a realizar 760 mil transações
06:47 — o pool foi liberado das 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 são armazenados no banco de dados

00:00 — o script de geração de transações já foi lançado
00:44 - 1 milhão de transações foram criadas e o envio para o nó começou
00:56 – o nó de envio pegou 320 mil transações do pool e formou o bloco nº 10. Também vemos que 320 mil 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, enviando 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 de que o bloco #10 foi adicionado e começaram a aplicar 320 mil transações
02:01 - o pool foi liberado para 320 mil transações que foram adicionadas ao bloco #10
02:15 – o nó de envio pegou 350 mil transações do pool e formou o bloco nº 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ó concluiu transações do bloco #10
10h59 — a transação com o envio do bloco #9 demorou muito na cadeia raiz, mas foi concluída e todos os nós receberam informações sobre ela e começaram a realizar 350 mil transações
11:05 - o pool foi liberado para 320 mil transações que foram adicionadas ao bloco #11
12:10 - todos os nós contêm 1 milhão de transações e tokens de 670 mil
12h17 - o nó de envio pegou 330 mil transações do pool e formou o bloco nº 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 de que o bloco #12 foi adicionado e começaram a aplicar 330 mil transações
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 salvos no banco de dados

00:00 — Foram lançados 3 scripts que geram e enviam 1 milhão de transações cada
01:38 — 1 milhão de transações foram criadas e o envio para o nó de envio nº 3 começou
01:50 – o nó de envio nº 3 pegou 330 mil transações do pool e formou o bloco nº 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ó de envio nº 1 começou
01:50 – o nó de envio nº 3 pegou 330 mil transações do pool e formou o bloco nº 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 pegou 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, que enviou 1 milhão de transações em 30 segundos, terminou de funcionar
02:14 — 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ó de envio nº 2 começou
02:27 — bloco #85 (65e) validado e enviado para a cadeia raiz
02:29 – o nó de envio nº 2 pegou 111855 transações do pool e forma o bloco nº 85 (256).
02:36 — bloco #85 (256) é assinado e enviado para outros nós para validação
02:36 — O script de demonstração do servidor nº 1, que enviou 1 milhão de transações em 42.5 segundos, terminou de funcionar
02:38 — bloco #85 (256) é validado e enviado para a cadeia raiz
03:08 — o script do servidor nº 2 terminou de funcionar, enviando 1 milhão de transações em 47 segundos
03:38 - todos os nós receberam informações da cadeia raiz que os blocos #85 (f21), #86(65e), #87(256) foram adicionados e começaram a aplicar 330k, 250k, 111855 transações
03:49 - o pool foi compensado em 330k, 250k, 111855 transações que foram adicionadas aos blocos #85 (f21), #86(65e), #87(256)
03:59 - o nó de envio nº 1 obteve 888145 transações do pool e formou o bloco nº 88 (214), o nó de envio nº 2 obteve 750 mil transações do pool e formou o bloco nº 88 (50a), o nó de envio nº 3 obteve 670 mil transações de o pool e o bloco de formulários # 88 (d3b)
04:44 — bloco #88 (d3b) é assinado e enviado para outros nós para validação
04:58 — bloco #88 (214) é assinado e enviado para outros nós para validação
05:11 — bloco #88 (50a) é assinado e enviado para outros nós para validação
05:11 — bloco #85 (d3b) é validado e enviado para a cadeia raiz
05:36 — 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 estão começando a aplicar transações de 670k, 750k
06:50 — devido a uma falha de comunicação, o bloco #85 (50a) não foi validado
06:55 – o nó de envio nº 2 pegou 888145 transações do pool e forma o bloco nº 90 (50a)
08:14 — bloco #90 (50a) é assinado e enviado para outros nós para validação
09:04 — bloco #90 (50a) é validado e enviado para a cadeia raiz
11:23 - todos os nós receberam informações da cadeia raiz de que o bloco #90 (50a) foi adicionado e começaram a aplicar 888145 transações. Ao mesmo tempo, o servidor #3 já aplicou transações dos blocos #88 (d3b), #89(214)
12:11 - todas as piscinas estão vazias
13:41 — todos os nós do servidor nº 3 contêm 3 milhões de transações e tokens
14:35 — todos os nós do servidor nº 1 contêm 3 milhões de transações e tokens
19:24 — todos os nós do servidor nº 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. Conflito na interação de várias funções do sistema. Por exemplo, a função de adicionar transações ao pool bloqueou o trabalho de envio e validação de blocos e vice-versa, o que levou a uma queda na velocidade.

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

3. Não estava claro como e onde armazenar os dados para obter resultados elevados.

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

5. Trabalhar no modo single-thread interrompe a conexão entre os nós quando ocorrem cálculos longos (por exemplo, construir uma árvore Merkle e calcular 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 e fornecer uma API para acesso aos dados. Como o NodeJS é nativamente de thread único, a pesada função de cálculo da árvore Merkle bloqueou a função de adição de transação. Vimos duas opções para resolver este problema:

1. Inicie vários processos NodeJS, cada um executando funções específicas.

2. Use trabalhador_threads e mova a execução de parte do código para 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. Nó de envio, que aceita transações no pool e cria blocos.

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

3. Nó API - fornece uma API para acessar dados.

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

Movemos operações pesadas, como calcular a árvore Merkle, para um thread separado.

Assim, conseguimos o funcionamento normal de todas as funções do Plasma Cash simultaneamente e sem falhas.

Uma vez funcional o sistema, começamos a testar a velocidade e, infelizmente, obtivemos resultados insatisfatórios: 5 transações por segundo e até 000 transações por 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 máxima do sistema. Escrevemos anteriormente que o nó Plasma Cash fornece uma interface de soquete unix. Inicialmente era baseado em texto. 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 encontramos aproximadamente 130k por segundo. Tentamos substituir as funções padrão para trabalhar com json, mas o desempenho não melhorou. O motor V8 deve estar bem otimizado para essas operações.

Trabalhamos com transações, tokens e blocos através de classes. Ao criar tais classes, o desempenho caiu 2 vezes, o que indica que OOP não é adequado para nós. Tive que reescrever tudo para uma abordagem puramente funcional.

Gravando no banco de dados

Inicialmente, o Redis foi escolhido para armazenamento de dados como uma das soluções mais produtivas que atende aos nossos requisitos: armazenamento de valores-chave, trabalho com tabelas hash, conjuntos. Lançamos o redis-benchmark e obtivemos aproximadamente 80 mil operações por segundo em 1 modo de pipeline.

Para alto desempenho, ajustamos o Redis com mais precisão:

  • Uma conexão de soquete unix foi estabelecida.
  • Desativamos o salvamento do estado em disco (para maior confiabilidade, você pode configurar uma réplica e salvar em disco em um Redis separado).

No Redis, um pool é uma tabela hash porque precisamos ser capazes de recuperar todas as transações em uma consulta e excluir as transações uma por uma. Tentamos usar uma lista normal, mas é mais lento ao descarregar a lista inteira.

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

Como o benchmark nos mostrou que as possibilidades eram claramente 5 vezes maiores, começamos a otimizar. Mudamos a biblioteca para ioredis e obtivemos desempenho de 25k por segundo. Adicionamos transações uma por uma usando o comando `hset`. Estávamos gerando muitas consultas no Redis. Surgiu a ideia de combinar transações em lotes e enviá-las com um comando `hmset`. O resultado é 32k por segundo.

Por vários motivos, que descreveremos a seguir, trabalhamos com dados usando `Buffer` e, como se vê, se você convertê-los em texto (`buffer.toString('hex')`) antes de escrever, você pode obter dados adicionais desempenho. Assim, a velocidade foi aumentada para 35k por segundo. No momento, decidimos suspender futuras otimizações.

Tivemos que mudar para um protocolo binário porque:

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

2. Quando enviados 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 do texto podem ocupar mais de 300 megabytes.

3. A transformação constante 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, obtivemos 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,
  }
  ```

- Símbolo

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

-Bloquear

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

Com os comandos usuais `BD.encode(block, Protocol).slice();` e `BD.decode(buffer, Protocol)` convertemos os dados em `Buffer` para salvar no Redis ou encaminhar para outro nó e recuperar o dados de volta.

Também temos 2 protocolos binários para transferência de dados entre serviços:

— Protocolo para interação com 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)
  }
  ```

em que:

  • `tipo` — a ação a ser executada, por exemplo, 1 — sendTransaction, 2 — getTransaction;
  • `carga útil` — dados que precisam ser passados ​​para a função apropriada;
  • `mensagemId` — ID da mensagem para que a resposta possa ser identificada.

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

em que:

  • `código` — código da mensagem, por exemplo 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versãoProtocolo` — versão do protocolo, uma vez que nós com versões diferentes podem ser criados na rede e podem funcionar de forma diferente;
  • `seq` — identificador da mensagem;
  • `contarChunk` и `chunkNumber` necessário para dividir mensagens grandes;
  • `comprimento` и `carga útil` comprimento e os próprios dados.

Como pré-digitamos os dados, o sistema final é muito mais rápido que a biblioteca `rlp` do Ethereum. Infelizmente, ainda não pudemos recusar, pois é necessário finalizar o contrato inteligente, o que pretendemos fazer no futuro.

Se conseguíssemos atingir a velocidade 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 no bloco 1 000 000 transações, o que significa enviar mais 100 MB de dados.

Inicialmente, usamos a biblioteca `ethereumjs-devp2p` para comunicação entre nós, mas ela não conseguia lidar com tantos dados. Como resultado, utilizamos a biblioteca `ws` e configuramos o envio de dados binários via websocket. É claro que também encontramos problemas ao enviar grandes pacotes de dados, mas os dividimos em partes e agora esses problemas desapareceram.

Também formando uma árvore Merkle e calculando o hash 1 000 000 transações exigem cerca de 10 segundos de cálculo contínuo. Durante esse tempo, a conexão com todos os nós consegue ser interrompida. Foi decidido mover este cálculo para um tópico separado.

Conclusões:

Na verdade, nossas descobertas não são novas, mas por alguma razão muitos especialistas as esquecem durante o desenvolvimento.

  • Usar Programação Funcional em vez de Programação Orientada a Objetos melhora a produtividade.
  • O monólito é pior do que uma arquitetura de serviço para um sistema NodeJS produtivo.
  • Usar `worker_threads` para computação pesada melhora a capacidade de resposta do sistema, especialmente ao lidar 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 websockets e enviar dados binários, divididos em pedaços, que podem ser encaminhados caso não cheguem e depois combinados em uma mensagem.

Nós convidamos você a visitar GitHub projeto: https://github.com/opporty-com/Plasma-Cash/tree/new-version

O artigo foi co-escrito por Alexandre Nashivan, desenvolvedor sênior Solução Inteligente Inc..

Fonte: habr.com

Adicionar um comentário