Prova pública: solució d'escalabilitat i privadesa d'Ethereum

Blockchain és una tecnologia innovadora que promet millorar moltes àrees de la vida humana. Transfereix processos i productes reals a l'espai digital, garanteix la velocitat i la fiabilitat de les transaccions financeres, redueix el seu cost i també permet crear aplicacions DAPP modernes mitjançant contractes intel·ligents en xarxes descentralitzades.

Tenint en compte els nombrosos beneficis i les diverses aplicacions de la cadena de blocs, pot semblar sorprenent que aquesta tecnologia prometedora encara no s'hagi introduït a totes les indústries. El problema és que les cadenes de blocs descentralitzades modernes no tenen escalabilitat. Ethereum processa unes 20 transaccions per segon, cosa que no és suficient per satisfer les necessitats dels negocis dinàmics actuals. Al mateix temps, les empreses que utilitzen la tecnologia blockchain dubten a abandonar Ethereum a causa del seu alt grau de protecció contra la pirateria i les fallades de la xarxa.

Per garantir la descentralització, la seguretat i l'escalabilitat a la cadena de blocs, resolent així el trilema de l'escalabilitat, l'equip de desenvolupament Oportunitat va crear Plasma Cash, una cadena subsidiària formada per un contracte intel·ligent i una xarxa privada basada en Node.js, que transfereix periòdicament el seu estat a la cadena arrel (Ethereum).

Prova pública: solució d'escalabilitat i privadesa d'Ethereum

Processos clau en Plasma Cash

1. L'usuari anomena "dipòsit" a la funció de contracte intel·ligent, passant-hi la quantitat d'ETH que vol dipositar al testimoni Plasma Cash. La funció de contracte intel·ligent crea un testimoni i genera un esdeveniment al respecte.

2. Els nodes de Plasma Cash subscrits a esdeveniments de contracte intel·ligent reben un esdeveniment sobre la creació d'un dipòsit i afegeixen una transacció sobre la creació d'un testimoni al grup.

3. Periòdicament, els nodes especials de Plasma Cash prenen totes les transaccions del grup (fins a 1 milió) i en formen un bloc, calculen l'arbre Merkle i, en conseqüència, el hash. Aquest bloc s'envia a altres nodes per a la seva verificació. Els nodes comproven si el hash Merkle és vàlid i si les transaccions són vàlides (per exemple, si el remitent del testimoni és el seu propietari). Després de verificar el bloc, el node crida a la funció "submitBlock" del contracte intel·ligent, que desa el número de bloc i el hash de Merkle a la cadena de vora. El contracte intel·ligent genera un esdeveniment que indica l'addició correcta d'un bloc. Les transaccions s'eliminen del grup.

4. Els nodes que reben l'esdeveniment d'enviament del bloc comencen a aplicar les transaccions que s'han afegit al bloc.

5. En algun moment, el propietari (o no propietari) del testimoni vol retirar-lo de Plasma Cash. Per fer-ho, crida a la funció `startExit`, passant-hi informació sobre les 2 últimes transaccions del token, que confirmen que ell és el propietari del token. El contracte intel·ligent, utilitzant el hash de Merkle, verifica la presència de transaccions als blocs i envia el testimoni per a la seva retirada, que es produirà en dues setmanes.

6. Si l'operació de retirada del testimoni es va produir amb infraccions (el testimoni es va gastar després de començar el procediment de retirada o el testimoni ja era d'una altra persona abans de la retirada), el propietari del testimoni pot refutar la retirada en dues setmanes.

Prova pública: solució d'escalabilitat i privadesa d'Ethereum

La privadesa s'aconsegueix de dues maneres

1. La cadena arrel no sap res sobre les transaccions que es generen i reenvien dins de la cadena secundària. La informació sobre qui va dipositar i retirar ETH de Plasma Cash continua sent pública.

2. La cadena secundària permet transaccions anònimes mitjançant zk-SNARK.

Pila de tecnologia

  • NodeJS
  • Redis
  • Eteri
  • Sòl

Proves

Durant el desenvolupament de Plasma Cash, vam provar la velocitat del sistema i vam obtenir els resultats següents:

  • S'afegeixen fins a 35 transaccions per segon al grup;
  • es poden emmagatzemar fins a 1 de transaccions en un bloc.

Les proves es van dur a terme als 3 servidors següents:

1. Intel Core i7-6700 Quad-Core Skylake incl. SSD NVMe: 512 GB, 64 GB de RAM DDR4
Es van generar 3 nodes Plasma Cash validadors.

2. AMD Ryzen 7 1700X Octa-Core "Summit Ridge" (Zen), SSD SATA - 500 GB, 64 GB de RAM DDR4
Es va plantejar el node ETH de Ropsten testnet.
Es van generar 3 nodes Plasma Cash validadors.

3. Intel Core i9-9900K Octa-Core incl. SSD NVMe: 1 TB, 64 GB de RAM DDR4
S'ha generat 1 node d'enviament de Plasma Cash.
Es van generar 3 nodes Plasma Cash validadors.
Es va llançar una prova per afegir transaccions a la xarxa Plasma Cash.

Total: 10 nodes Plasma Cash en una xarxa privada.

Prova 1

Hi ha un límit d'1 milió de transaccions per bloc. Per tant, 1 milió de transaccions es divideixen en 2 blocs (ja que el sistema aconsegueix agafar part de les transaccions i enviar-les mentre s'envien).


Estat inicial: darrer bloc #7; 1 milió de transaccions i fitxes s'emmagatzemen a la base de dades.

00:00 — inici de l'script de generació de transaccions
01:37 - Es van crear 1 milió de transaccions i va començar l'enviament al node
01:46: el node d'enviament va prendre 240 transaccions del bloc i el bloc de formularis #8. També veiem que s'afegeixen 320 transaccions al grup en 10 segons
01:58 — el bloc #8 està signat i enviat per a la validació
02:03: el bloc #8 es valida i la funció "submitBlock" del contracte intel·ligent es crida amb el hash de Merkle i el número de bloc
02:10: l'script de demostració ha acabat de funcionar, que va enviar 1 milió de transaccions en 32 segons
02:33: els nodes van començar a rebre informació que el bloc #8 es va afegir a la cadena arrel i van començar a realitzar 240 transaccions
02:40 - S'han eliminat 240 transaccions del grup, que ja es troben al bloc núm. 8
02:56: el node d'enviament va agafar les 760 transaccions restants del grup i va començar a calcular el hash de Merkle i el bloc de signatura #9
03:20: tots els nodes contenen 1 milió de 240 transaccions i fitxes
03:35 — el bloc #9 està signat i enviat per a la validació a altres nodes
03:41: s'ha produït un error de xarxa
04:40 — S'ha esgotat el temps d'espera de la validació del bloc núm. 9
04:54: el node d'enviament va agafar les 760 transaccions restants del grup i va començar a calcular el hash de Merkle i el bloc de signatura #9
05:32 — el bloc #9 està signat i enviat per a la validació a altres nodes
05:53 — el bloc #9 es valida i s'envia a la cadena arrel
06:17: els nodes van començar a rebre informació que el bloc #9 es va afegir a la cadena arrel i van començar a realitzar 760k transaccions
06:47: el grup ha esborrat les transaccions que es troben al bloc #9
09:06: tots els nodes contenen 2 milions de transaccions i fitxes

Prova 2

Hi ha un límit de 350k per bloc. Com a resultat, tenim 3 blocs.


Estat inicial: últim bloc #9; S'emmagatzemen 2 milions de transaccions i fitxes a la base de dades

00:00 — Ja s'ha llançat l'script de generació de transaccions
00:44 - Es van crear 1 milió de transaccions i va començar l'enviament al node
00:56: el node d'enviament va prendre 320 transaccions del bloc i el bloc de formularis #10. També veiem que s'afegeixen 320 transaccions al grup en 10 segons
01:12: el bloc núm. 10 està signat i enviat a altres nodes per a la seva validació
01:18: l'script de demostració ha acabat de funcionar, que va enviar 1 milió de transaccions en 34 segons
01:20 — el bloc #10 es valida i s'envia a la cadena arrel
01:51: tots els nodes van rebre informació de la cadena arrel que es va afegir el bloc núm. 10 i van començar a aplicar 320k transaccions
02:01: el grup s'ha eliminat per a 320 transaccions que es van afegir al bloc núm. 10
02:15: el node d'enviament ha pres 350 transaccions del grup i el bloc de formularis #11
02:34 — el bloc #11 es signa i s'envia a altres nodes per a la seva validació
02:51 — el bloc #11 es valida i s'envia a la cadena arrel
02:55: l'últim node va completar transaccions del bloc #10
10:59: la transacció amb l'enviament del bloc #9 va trigar molt de temps a la cadena arrel, però es va completar i tots els nodes van rebre informació al respecte i van començar a realitzar transaccions de 350k
11:05: el grup s'ha eliminat per a 320 transaccions que es van afegir al bloc núm. 11
12:10: tots els nodes contenen 1 milió de 670 transaccions i fitxes
12:17: el node d'enviament ha pres 330 transaccions de l'agrupació i el bloc de formularis #12
12:32 — el bloc #12 es signa i s'envia a altres nodes per a la seva validació
12:39 — el bloc #12 es valida i s'envia a la cadena arrel
13:44: tots els nodes van rebre informació de la cadena arrel que es va afegir el bloc núm. 12 i van començar a aplicar 330 transaccions
14:50: tots els nodes contenen 2 milions de transaccions i fitxes

Prova 3

Al primer i segon servidor, un node de validació es va substituir per un node d'enviament.


Estat inicial: últim bloc #84; 0 transaccions i fitxes desades a la base de dades

00:00 — S'han llançat 3 scripts que generen i envien 1 milió de transaccions cadascun
01:38 — Es van crear 1 milió de transaccions i es va començar a enviar el node núm. 3
01:50: el node d'enviament núm. 3 va prendre 330 transaccions del bloc i formularis núm. 85 (f21). També veiem que s'afegeixen 350 transaccions al grup en 10 segons
01:53 — Es van crear 1 milió de transaccions i es va començar a enviar el node núm. 1
01:50: el node d'enviament núm. 3 va prendre 330 transaccions del bloc i formularis núm. 85 (f21). També veiem que s'afegeixen 350 transaccions al grup en 10 segons
02:01: el node d'enviament núm. 1 va prendre 250 transaccions del bloc i formularis núm. 85 (65e)
02:06 — el bloc #85 (f21) està signat i enviat a altres nodes per a la validació
02:08: l'script de demostració del servidor núm. 3, que va enviar 1 milió de transaccions en 30 segons, va acabar de funcionar
02:14 — El bloc #85 (f21) es valida i s'envia a la cadena arrel
02:19 — el bloc #85 (65e) està signat i enviat a altres nodes per a la validació
02:22 — Es van crear 1 milió de transaccions i es va començar a enviar el node núm. 2
02:27 — bloc #85 (65e) validat i enviat a la cadena arrel
02:29: el node d'enviament núm. 2 ha pres 111855 transaccions del bloc i formularis núm. 85 (256).
02:36 — el bloc #85 (256) està signat i enviat a altres nodes per a la validació
02:36: l'script de demostració del servidor núm. 1, que va enviar 1 milió de transaccions en 42.5 segons, va acabar de funcionar
02:38 — el bloc #85 (256) es valida i s'envia a la cadena arrel
03:08 — L'script del servidor núm. 2 ha acabat de funcionar, que ha enviat 1 milió de transaccions en 47 segons
03:38: tots els nodes van rebre informació de la cadena arrel que es van afegir els blocs #85 (f21), #86(65e), #87(256) i van començar a aplicar transaccions de 330k, 250k, 111855
03:49: el grup s'ha esborrat a les transaccions 330k, 250k, 111855 que es van afegir als blocs #85 (f21), #86(65e), #87(256)
03:59: el node d'enviament núm. 1 ha pres 888145 transaccions de l'agrupació i el bloc de formularis núm. 88 (214), el node d'enviament núm. 2 ha pres 750 transaccions de l'agrupament i el bloc de formularis n.º 88 (50a), el node d'enviament n.º 3 ha agafat 670 transaccions de la piscina i forma el bloc #88 (d3b)
04:44 — el bloc #88 (d3b) està signat i enviat a altres nodes per a la validació
04:58 — el bloc #88 (214) està signat i enviat a altres nodes per a la validació
05:11 — el bloc #88 (50a) es signa i s'envia a altres nodes per a la seva validació
05:11 — El bloc #85 (d3b) es valida i s'envia a la cadena arrel
05:36 — el bloc #85 (214) es valida i s'envia a la cadena arrel
05:43: tots els nodes han rebut informació de la cadena arrel que s'han afegit els blocs #88 (d3b), #89(214) i comencen a aplicar transaccions de 670k i 750k
06:50 — a causa d'un error de comunicació, el bloc #85 (50a) no s'ha validat
06:55: el node d'enviament núm. 2 ha pres 888145 transaccions del bloc i formularis núm. 90 (50a)
08:14 — el bloc #90 (50a) es signa i s'envia a altres nodes per a la seva validació
09:04: el bloc #90 (50a) es valida i s'envia a la cadena arrel
11:23: tots els nodes van rebre informació de la cadena arrel que es va afegir el bloc #90 (50a) i van començar a aplicar 888145 transaccions. Al mateix temps, el servidor #3 ja ha aplicat transaccions dels blocs #88 (d3b), #89(214)
12:11 - totes les piscines estan buides
13:41: tots els nodes del servidor #3 contenen 3 milions de transaccions i fitxes
14:35: tots els nodes del servidor #1 contenen 3 milions de transaccions i fitxes
19:24: tots els nodes del servidor #2 contenen 3 milions de transaccions i fitxes

Obstacles

Durant el desenvolupament de Plasma Cash, ens hem trobat amb els problemes següents, que hem anat resolent i anem resolent:

1. Conflicte en la interacció de diverses funcions del sistema. Per exemple, la funció d'afegir transaccions al pool bloquejava el treball d'enviament i validació de blocs, i viceversa, fet que provocava una disminució de la velocitat.

2. No va quedar clar immediatament com enviar un gran nombre de transaccions alhora que es minimitzaven els costos de transferència de dades.

3. No estava clar com i on emmagatzemar les dades per aconseguir resultats alts.

4. No estava clar com organitzar una xarxa entre nodes, ja que la mida d'un bloc amb 1 milió de transaccions ocupa uns 100 MB.

5. Treballar en mode d'un sol fil trenca la connexió entre nodes quan es produeixen càlculs llargs (per exemple, la construcció d'un arbre Merkle i el càlcul del seu hash).

Com hem tractat tot això?

La primera versió del node Plasma Cash era una mena de combinació que podia fer tot alhora: acceptar transaccions, enviar i validar blocs i proporcionar una API per accedir a les dades. Com que NodeJS és nativament d'un sol fil, la funció de càlcul de l'arbre de Merkle pesat va bloquejar la funció d'afegir transacció. Hem vist dues opcions per resoldre aquest problema:

1. Inicieu diversos processos NodeJS, cadascun dels quals realitza funcions específiques.

2. Utilitzeu worker_threads i moveu l'execució d'una part del codi als fils.

Com a resultat, vam utilitzar les dues opcions alhora: vam dividir lògicament un node en 3 parts que poden funcionar per separat, però alhora de manera sincrònica.

1. Node d'enviament, que accepta transaccions al grup i crea blocs.

2. Un node de validació que verifica la validesa dels nodes.

3. Node API: proporciona una API per accedir a les dades.

En aquest cas, podeu connectar-vos a cada node mitjançant un sòcol Unix mitjançant cli.

Vam traslladar operacions pesades, com ara el càlcul de l'arbre Merkle, a un fil separat.

Així, hem aconseguit el funcionament normal de totes les funcions de Plasma Cash simultàniament i sense errors.

Un cop el sistema va ser funcional, vam començar a provar la velocitat i, malauradament, vam rebre resultats insatisfactoris: 5 transaccions per segon i fins a 000 transaccions per bloc. Vaig haver d'esbrinar què s'havia implementat incorrectament.

Per començar, vam començar a provar el mecanisme de comunicació amb Plasma Cash per esbrinar la capacitat màxima del sistema. Hem escrit abans que el node Plasma Cash proporciona una interfície de socket Unix. Inicialment es basava en text. Els objectes json es van enviar mitjançant `JSON.parse()` i `JSON.stringify()`.

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

Vam mesurar la velocitat de transferència d'aquests objectes i vam trobar ~ 130k per segon. Hem intentat substituir les funcions estàndard per treballar amb json, però el rendiment no ha millorat. El motor V8 ha d'estar ben optimitzat per a aquestes operacions.

Hem treballat amb transaccions, fitxes i blocs mitjançant classes. En crear aquestes classes, el rendiment es va reduir 2 vegades, la qual cosa indica que la POO no és adequada per a nosaltres. Vaig haver de reescriure-ho tot amb un enfocament purament funcional.

Enregistrament a la base de dades

Inicialment, Redis va ser escollit per a l'emmagatzematge de dades com una de les solucions més productives que satisfà els nostres requisits: emmagatzematge clau-valor, treball amb taules hash, conjunts. Vam llançar redis-benchmark i vam obtenir ~ 80 operacions per segon en 1 mode de canalització.

Per obtenir un alt rendiment, hem ajustat Redis de manera més fina:

  • S'ha establert una connexió de socket Unix.
  • Hem desactivat l'estalvi de l'estat al disc (per a la fiabilitat, podeu configurar una rèplica i desar-lo al disc en un Redis separat).

A Redis, un grup és una taula hash perquè hem de poder recuperar totes les transaccions en una sola consulta i eliminar les transaccions una per una. Hem provat d'utilitzar una llista normal, però és més lent quan es descarrega tota la llista.

Quan s'utilitzava NodeJS estàndard, les biblioteques Redis van aconseguir un rendiment de 18 transaccions per segon. La velocitat va baixar 9 vegades.

Com que el benchmark ens va mostrar que les possibilitats eren clarament 5 vegades més grans, vam començar a optimitzar. Vam canviar la biblioteca a ioredis i vam aconseguir un rendiment de 25k per segon. Hem afegit transaccions una per una mitjançant l'ordre `hset`. Així que estàvem generant moltes consultes a Redis. Va sorgir la idea de combinar transaccions en lots i enviar-les amb una ordre `hmset`. El resultat és de 32k per segon.

Per diversos motius, que descriurem a continuació, treballem amb dades utilitzant `Buffer` i, com a resultat, si les convertiu a text (`buffer.toString('hex')`) abans d'escriure, podeu obtenir més rendiment. Així, la velocitat es va augmentar a 35 k per segon. De moment, hem decidit suspendre més optimització.

Hem hagut de canviar a un protocol binari perquè:

1. El sistema sovint calcula hash, signatures, etc., i per a això necessita dades al `Buffer.

2. Quan s'envien entre serveis, les dades binàries pesen menys que el text. Per exemple, quan s'envia un bloc amb 1 milió de transaccions, les dades del text poden ocupar més de 300 megabytes.

3. La transformació constant de les dades afecta el rendiment.

Per tant, vam prendre com a base el nostre propi protocol binari per emmagatzemar i transmetre dades, desenvolupat a partir de la meravellosa biblioteca `binary-data`.

Com a resultat, hem obtingut les següents estructures de dades:

—Transacció

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

—Fitxa

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

-Bloc

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

Amb les ordres habituals `BD.encode(block, Protocol).slice();` i `BD.decode(buffer, Protocol)` convertim les dades en `Buffer' per desar-les a Redis o reenviar-les a un altre node i recuperar el dades enrere.

També disposem de 2 protocols binaris per transferir dades entre serveis:

— Protocol d'interacció amb Plasma Node a través del sòcol 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)
  }
  ```

on:

  • "tipus" — l'acció que s'ha de realitzar, per exemple, 1 — sendTransaction, 2 — getTransaction;
  • 'càrrega útil' — dades que s'han de passar a la funció adequada;
  • `messageId` — identificador del missatge perquè es pugui identificar la resposta.

— Protocol d'interacció entre nodes

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

on:

  • 'codi' — codi de missatge, per exemple 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — versió del protocol, ja que nodes amb versions diferents es poden plantejar a la xarxa i poden funcionar de manera diferent;
  • 'seq' - identificador del missatge;
  • `countChunk` и `número de fragment` necessari per dividir missatges grans;
  • 'longitud' и 'càrrega útil' longitud i les dades en si.

Com que vam escriure prèviament les dades, el sistema final és molt més ràpid que la biblioteca `rlp` d'Ethereum. Malauradament, encara no hem pogut rebutjar-ho, ja que cal finalitzar el contracte intel·ligent, cosa que pensem fer en el futur.

Si aconseguim assolir la velocitat 35 000 transaccions per segon, també hem de processar-les en el temps òptim. Com que el temps aproximat de formació del bloc triga 30 segons, hem d'incloure'l al bloc 1 000 000 transaccions, que significa enviar més 100 MB de dades.

Inicialment, vam utilitzar la biblioteca `ethereumjs-devp2p` per comunicar-nos entre nodes, però no podia gestionar tantes dades. Com a resultat, vam utilitzar la biblioteca `ws` i vam configurar l'enviament de dades binàries mitjançant websocket. Per descomptat, també hem trobat problemes a l'hora d'enviar paquets de dades grans, però els hem dividit en trossos i ara aquests problemes han desaparegut.

També formant un arbre Merkle i calculant el hash 1 000 000 transaccions requereix aproximadament 10 segons de càlcul continu. Durant aquest temps, la connexió amb tots els nodes aconsegueix trencar-se. Es va decidir traslladar aquest càlcul a un fil separat.

Conclusions:

De fet, les nostres troballes no són noves, però per alguna raó molts experts s'obliden d'elles quan es desenvolupen.

  • L'ús de la programació funcional en lloc de la programació orientada a objectes millora la productivitat.
  • El monòlit és pitjor que una arquitectura de servei per a un sistema NodeJS productiu.
  • L'ús de `worker_threads' per a càlculs pesats millora la capacitat de resposta del sistema, especialment quan es tracta d'operacions d'e/s.
  • El sòcol Unix és més estable i més ràpid que les sol·licituds http.
  • Si necessiteu transferir ràpidament dades grans a la xarxa, és millor utilitzar websockets i enviar dades binàries, dividides en fragments, que es poden reenviar si no arriben i combinar-les en un sol missatge.

Us convidem a visitar-lo GitHub projecte: https://github.com/opporty-com/Plasma-Cash/tree/new-version

L'article ha estat coescrit per Alexander Nashivan, desenvolupador sènior Clever Solution Inc.

Font: www.habr.com

Afegeix comentari