Offentlig test: En løsning til privatliv og skalerbarhed på Ethereum

Blockchain er en innovativ teknologi, der lover at forbedre mange områder af menneskelivet. Det overfører rigtige processer og produkter til det digitale rum, sikrer hastighed og pålidelighed af finansielle transaktioner, reducerer deres omkostninger og giver dig også mulighed for at skabe moderne DAPP-applikationer ved hjælp af smarte kontrakter i decentraliserede netværk.

I betragtning af de mange fordele og forskellige anvendelser af blockchain, kan det virke overraskende, at denne lovende teknologi endnu ikke har fundet vej til alle brancher. Problemet er, at moderne decentraliserede blockchains mangler skalerbarhed. Ethereum behandler omkring 20 transaktioner i sekundet, hvilket ikke er nok til at opfylde behovene hos nutidens dynamiske virksomheder. Samtidig er virksomheder, der bruger blockchain-teknologi, tøvende med at opgive Ethereum på grund af dets høje grad af beskyttelse mod hacking og netværksfejl.

For at sikre decentralisering, sikkerhed og skalerbarhed i blockchainen og dermed løse Scalability Trilemma, udvikler teamet Opporty skabt Plasma Cash, en datterselskabskæde bestående af en smart kontrakt og et privat netværk baseret på Node.js, som med jævne mellemrum overfører sin tilstand til rodkæden (Ethereum).

Offentlig test: En løsning til privatliv og skalerbarhed på Ethereum

Nøgleprocesser i Plasma Cash

1. Brugeren kalder den smarte kontraktfunktion 'indbetaling' og overfører den mængde ETH, som han ønsker at indbetale til Plasma Cash-tokenet. Den smarte kontraktfunktion opretter et token og genererer en begivenhed om det.

2. Plasma Cash-noder, der abonnerer på smarte kontraktbegivenheder, modtager en begivenhed om oprettelse af et indskud og tilføjer en transaktion om oprettelse af et token til puljen.

3. Med jævne mellemrum tager specielle Plasma Cash-noder alle transaktioner fra puljen (op til 1 million) og danner en blok fra dem, beregner Merkle-træet og dermed hashen. Denne blok sendes til andre noder til verifikation. Noderne kontrollerer, om Merkle-hashen er gyldig, og om transaktionerne er gyldige (for eksempel om afsenderen af ​​tokenet er dets ejer). Efter at have verificeret blokken, kalder noden "submitBlock"-funktionen i den smarte kontrakt, som gemmer bloknummeret og Merkle-hash til kantkæden. Den smarte kontrakt genererer en hændelse, der indikerer vellykket tilføjelse af en blok. Transaktioner fjernes fra puljen.

4. Noder, der modtager blokindsendelseshændelsen, begynder at anvende de transaktioner, der blev tilføjet til blokken.

5. På et tidspunkt ønsker ejeren (eller ikke-ejeren) af tokenet at trække det tilbage fra Plasma Cash. For at gøre dette kalder han 'startExit'-funktionen og sender information om de sidste 2 transaktioner på tokenet, som bekræfter, at han er ejeren af ​​tokenet. Den smarte kontrakt, ved hjælp af Merkle-hash, kontrollerer tilstedeværelsen af ​​transaktioner i blokkene og sender tokenet til tilbagetrækning, hvilket vil ske om to uger.

6. Hvis tokentilbagetrækningen skete med overtrædelser (poletten blev brugt efter tilbagetrækningsproceduren begyndte, eller tokenet var allerede en andens før tilbagetrækningen), kan ejeren af ​​tokenet afvise tilbagetrækningen inden for to uger.

Offentlig test: En løsning til privatliv og skalerbarhed på Ethereum

Privatliv opnås på to måder

1. Rodkæden ved intet om de transaktioner, der genereres og videresendes i den underordnede kæde. Oplysninger om, hvem der deponerede og hævede ETH fra Plasma Cash forbliver offentlige.

2. Den underordnede kæde tillader anonyme transaktioner ved hjælp af zk-SNARK'er.

Teknologi stak

  • NodeJS
  • Omfor
  • Etherium
  • soild

Test

Mens vi udviklede Plasma Cash, testede vi systemets hastighed og opnåede følgende resultater:

  • op til 35 transaktioner i sekundet føjes til puljen;
  • op til 1 transaktioner kan gemmes i en blok.

Test blev udført på følgende 3 servere:

1. Intel Core i7-6700 Quad-Core Skylake inkl. NVMe SSD – 512 GB, 64 GB DDR4 RAM
3 validerende Plasma Cash noder blev rejst.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SATA SSD – 500 GB, 64 GB DDR4 RAM
Ropsten testnet ETH-knuden blev hævet.
3 validerende Plasma Cash noder blev rejst.

3. Intel Core i9-9900K Octa-Core inkl. NVMe SSD – 1 TB, 64 GB DDR4 RAM
1 Plasma Cash indsendelsesknude blev rejst.
3 validerende Plasma Cash noder blev rejst.
En test blev lanceret for at tilføje transaktioner til Plasma Cash-netværket.

Totalt: 10 Plasma Cash noder i et privat netværk.

Test 1

Der er en grænse på 1 million transaktioner pr. blok. Derfor falder 1 million transaktioner i 2 blokke (da systemet formår at tage en del af transaktionerne og indsende, mens de sendes).


Indledende tilstand: sidste blok #7; 1 million transaktioner og tokens er gemt i databasen.

00:00 — start af transaktionsgenereringsscript
01:37 - 1 million transaktioner blev oprettet og afsendelse til noden begyndte
01:46 — indsend node tog 240 transaktioner fra puljen og formularer blok #8. Vi ser også, at 320 transaktioner føjes til puljen på 10 sekunder
01:58 — blok #8 er underskrevet og sendt til validering
02:03 — blok #8 er valideret, og 'submitBlock'-funktionen i den smarte kontrakt kaldes med Merkle-hash og bloknummer
02:10 — demoscriptet var færdigt med at virke, som sendte 1 million transaktioner på 32 sekunder
02:33 - noder begyndte at modtage information om, at blok #8 blev tilføjet til rodkæden, og begyndte at udføre 240 transaktioner
02:40 - 240 transaktioner blev fjernet fra puljen, som allerede er i blok #8
02:56 — submit node tog de resterende 760 transaktioner fra puljen og begyndte at beregne Merkle-hash og signeringsblok #9
03:20 - alle noder indeholder 1 million 240 transaktioner og tokens
03:35 — blok #9 er underskrevet og sendt til validering til andre noder
03:41 - netværksfejl opstod
04:40 — venter på blok #9-validering er timeout
04:54 — submit node tog de resterende 760 transaktioner fra puljen og begyndte at beregne Merkle-hash og signeringsblok #9
05:32 — blok #9 er underskrevet og sendt til validering til andre noder
05:53 — blok #9 er valideret og sendt til rodkæden
06:17 - noder begyndte at modtage information om, at blok #9 blev tilføjet til rodkæden og begyndte at udføre 760 transaktioner
06:47 — puljen har ryddet for transaktioner, der er i blok #9
09:06 - alle noder indeholder 2 millioner transaktioner og tokens

Test 2

Der er en grænse på 350k pr. blok. Som et resultat har vi 3 blokke.


Indledende tilstand: sidste blok #9; 2 millioner transaktioner og tokens er gemt i databasen

00:00 — transaktionsgenereringsscript er allerede blevet lanceret
00:44 - 1 million transaktioner blev oprettet og afsendelse til noden begyndte
00:56 — indsend node tog 320 transaktioner fra puljen og formularer blok #10. Vi ser også, at 320 transaktioner føjes til puljen på 10 sekunder
01:12 — blok #10 er underskrevet og sendt til andre noder til validering
01:18 — demoscriptet var færdigt med at virke, som sendte 1 million transaktioner på 34 sekunder
01:20 — blok #10 er valideret og sendt til rodkæden
01:51 - alle noder modtog information fra rodkæden om, at blok #10 blev tilføjet og begynder at anvende 320 transaktioner
02:01 - puljen har ryddet for 320 transaktioner, der blev tilføjet til blok #10
02:15 — indsend node tog 350 transaktioner fra puljen og formularblok #11
02:34 — blok #11 er underskrevet og sendt til andre noder til validering
02:51 — blok #11 er valideret og sendt til rodkæden
02:55 — den sidste node gennemførte transaktioner fra blok #10
10:59 — transaktionen med indsendelse af blok #9 tog meget lang tid i rodkæden, men den blev afsluttet, og alle noder modtog information om den og begyndte at udføre 350 transaktioner
11:05 - puljen har ryddet for 320 transaktioner, der blev tilføjet til blok #11
12:10 - alle noder indeholder 1 million 670 transaktioner og tokens
12:17 — indsend node tog 330 transaktioner fra puljen og formularblok #12
12:32 — blok #12 er underskrevet og sendt til andre noder til validering
12:39 — blok #12 er valideret og sendt til rodkæden
13:44 - alle noder modtog information fra rodkæden om, at blok #12 blev tilføjet og begynder at anvende 330 transaktioner
14:50 - alle noder indeholder 2 millioner transaktioner og tokens

Test 3

I den første og anden server blev én validerende node erstattet af en indsendende node.


Indledende tilstand: sidste blok #84; 0 transaktioner og tokens gemt i databasen

00:00 — 3 scripts er blevet lanceret, der genererer og sender 1 million transaktioner hver
01:38 — 1 million transaktioner blev oprettet, og afsendelse til indsendelse af node #3 begyndte
01:50 — indsend node #3 tog 330k transaktioner fra puljen og formularer blok #85 (f21). Vi ser også, at 350 transaktioner tilføjes til puljen på 10 sekunder
01:53 — 1 million transaktioner blev oprettet, og afsendelse til indsendelse af node #1 begyndte
01:50 — indsend node #3 tog 330k transaktioner fra puljen og formularer blok #85 (f21). Vi ser også, at 350 transaktioner tilføjes til puljen på 10 sekunder
02:01 — indsend node #1 tog 250 transaktioner fra puljen og formularblok #85 (65e)
02:06 — blok #85 (f21) er underskrevet og sendt til andre noder til validering
02:08 — demoscript af server #3, som sendte 1 million transaktioner på 30 sekunder, færdig med at fungere
02:14 — blok #85 (f21) valideres og sendes til rodkæden
02:19 — blok #85 (65e) er underskrevet og sendt til andre noder til validering
02:22 — 1 million transaktioner blev oprettet, og afsendelse til indsendelse af node #2 begyndte
02:27 — blok #85 (65e) valideret og sendt til rodkæden
02:29 — indsend node #2 tog 111855 transaktioner fra puljen og formularer blok #85 (256).
02:36 — blok #85 (256) er underskrevet og sendt til andre noder til validering
02:36 — demoscript af server #1, som sendte 1 million transaktioner på 42.5 sekunder, færdig med at fungere
02:38 — blok #85 (256) er valideret og sendt til rodkæden
03:08 — server #2 script færdig med at fungere, som sendte 1 million transaktioner på 47 sekunder
03:38 - alle noder modtog information fra rodkæden om, at blokke #85 (f21), #86(65e), #87(256) blev tilføjet og begyndte at anvende 330k, 250k, 111855 transaktioner
03:49 - puljen blev clearet ved 330k, 250k, 111855 transaktioner, der blev tilføjet til blokke #85 (f21), #86(65e), #87(256)
03:59 — indsend node #1 tog 888145 transaktioner fra puljen og formularer blok #88 (214), indsend node #2 tog 750k transaktioner fra puljen og formular blok #88 (50a), indsend node #3 tog 670k transaktioner fra poolen og danner blok #88 (d3b)
04:44 — blok #88 (d3b) er underskrevet og sendt til andre noder til validering
04:58 — blok #88 (214) er underskrevet og sendt til andre noder til validering
05:11 — blok #88 (50a) er underskrevet og sendt til andre noder til validering
05:11 — blok #85 (d3b) valideres og sendes til rodkæden
05:36 — blok #85 (214) er valideret og sendt til rodkæden
05:43 - alle noder modtog information fra rodkæden om, at blokke #88 (d3b), #89(214) er blevet tilføjet og begynder at anvende 670k, 750k transaktioner
06:50 — på grund af en kommunikationsfejl blev blok #85 (50a) ikke valideret
06:55 — indsend node #2 tog 888145 transaktioner fra puljen og formularblok #90 (50a)
08:14 — blok #90 (50a) er underskrevet og sendt til andre noder til validering
09:04 — blok #90 (50a) valideres og sendes til rodkæden
11:23 - alle noder modtog information fra rodkæden om, at blok #90 (50a) blev tilføjet, og begynder at anvende 888145 transaktioner. Samtidig har server #3 allerede anvendt transaktioner fra blokke #88 (d3b), #89(214)
12:11 - alle bassiner er tomme
13:41 — alle noder på server #3 indeholder 3 millioner transaktioner og tokens
14:35 — alle noder på server #1 indeholder 3 millioner transaktioner og tokens
19:24 — alle noder på server #2 indeholder 3 millioner transaktioner og tokens

Forhindringer

Under udviklingen af ​​Plasma Cash stødte vi på følgende problemer, som vi gradvist løste og løser:

1. Konflikt i samspillet mellem forskellige systemfunktioner. For eksempel blokerede funktionen med at tilføje transaktioner til puljen arbejdet med at indsende og validere blokke og omvendt, hvilket førte til et fald i hastigheden.

2. Det var ikke umiddelbart klart, hvordan man sender et stort antal transaktioner og samtidig minimerer omkostningerne til dataoverførsel.

3. Det var ikke klart, hvordan og hvor man skulle gemme data for at opnå høje resultater.

4. Det var ikke klart, hvordan man organiserer et netværk mellem noder, da størrelsen af ​​en blok med 1 million transaktioner fylder omkring 100 MB.

5. Arbejde i enkelttrådstilstand bryder forbindelsen mellem noder, når der forekommer lange beregninger (for eksempel ved at bygge et Merkle-træ og beregne dets hash).

Hvordan håndterede vi alt dette?

Den første version af Plasma Cash-noden var en slags mejetærsker, der kunne gøre alt på samme tid: acceptere transaktioner, indsende og validere blokke og levere en API til at få adgang til data. Da NodeJS er naturligt enkelt-trådet, blokerede den tunge Merkle-træberegningsfunktion funktionen tilføj transaktion. Vi så to muligheder for at løse dette problem:

1. Start flere NodeJS-processer, som hver især udfører specifikke funktioner.

2. Brug worker_threads og flyt udførelsen af ​​en del af koden til tråde.

Som et resultat brugte vi begge muligheder på samme tid: vi opdelte logisk en node i 3 dele, der kan arbejde separat, men samtidig synkront

1. Submission node, som accepterer transaktioner i puljen og opretter blokke.

2. En validerende node, der kontrollerer gyldigheden af ​​noder.

3. API-node - giver en API til at få adgang til data.

I dette tilfælde kan du oprette forbindelse til hver node via et unix-stik ved hjælp af cli.

Vi flyttede tunge operationer, såsom at beregne Merkle-træet, ind i en separat tråd.

Dermed har vi opnået normal drift af alle Plasma Cash-funktioner samtidigt og uden fejl.

Da systemet var funktionelt, begyndte vi at teste hastigheden og fik desværre utilfredsstillende resultater: 5 transaktioner pr. sekund og op til 000 transaktioner pr. blok. Jeg skulle finde ud af, hvad der var implementeret forkert.

Til at begynde med begyndte vi at teste kommunikationsmekanismen med Plasma Cash for at finde ud af systemets højeste kapacitet. Vi skrev tidligere, at Plasma Cash-noden giver en unix-socket-grænseflade. I starten var det tekstbaseret. json-objekter blev sendt ved hjælp af `JSON.parse()` og `JSON.stringify()`.

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

Vi målte overførselshastigheden af ​​sådanne objekter og fandt ~ 130k pr. sekund. Vi forsøgte at erstatte standardfunktionerne for at arbejde med json, men ydeevnen blev ikke forbedret. V8-motoren skal være godt optimeret til disse operationer.

Vi arbejdede med transaktioner, tokens og blokeringer gennem klasser. Ved oprettelse af sådanne klasser faldt ydeevnen med 2 gange, hvilket indikerer, at OOP ikke er egnet for os. Jeg var nødt til at omskrive alt til en rent funktionel tilgang.

Optagelse i databasen

Oprindeligt blev Redis valgt til datalagring som en af ​​de mest produktive løsninger, der opfylder vores krav: nøgleværdilagring, arbejde med hashtabeller, sæt. Vi lancerede redis-benchmark og fik ~80 operationer i sekundet i 1 pipelining-tilstand.

For høj ydeevne tunede vi Redis mere fint:

  • Der er etableret en unix-stikforbindelse.
  • Vi deaktiverede lagring af tilstanden på disk (for pålidelighed kan du konfigurere en replika og gemme på disk i en separat Redis).

I Redis er en pulje en hash-tabel, fordi vi skal kunne hente alle transaktioner i én forespørgsel og slette transaktioner én efter én. Vi forsøgte at bruge en almindelig liste, men den er langsommere, når hele listen aflæses.

Ved brug af standard NodeJS opnåede Redis-bibliotekerne en ydeevne på 18 transaktioner i sekundet. Hastigheden faldt 9 gange.

Da benchmark viste os, at mulighederne var klart 5 gange større, begyndte vi at optimere. Vi ændrede biblioteket til ioredis og fik en ydeevne på 25k pr. sekund. Vi tilføjede transaktioner én efter én ved hjælp af 'hset'-kommandoen. Så vi genererede en masse forespørgsler i Redis. Ideen opstod til at kombinere transaktioner i batches og sende dem med én kommando `hmset`. Resultatet er 32k i sekundet.

Af flere grunde, som vi vil beskrive nedenfor, arbejder vi med data ved hjælp af `Buffer`, og som det viser sig, hvis du konverterer det til tekst (`buffer.toString('hex')`), før du skriver, kan du få yderligere ydeevne. Dermed blev hastigheden øget til 35k i sekundet. I øjeblikket besluttede vi at indstille yderligere optimering.

Vi var nødt til at skifte til en binær protokol, fordi:

1. Systemet beregner ofte hashes, signaturer osv., og til dette har det brug for data i `bufferen.

2. Når de sendes mellem tjenester, vejer binære data mindre end tekst. For eksempel, når du sender en blok med 1 million transaktioner, kan dataene i teksten fylde mere end 300 megabyte.

3. Konstant transformation af data påvirker ydeevnen.

Derfor tog vi udgangspunkt i vores egen binære protokol til lagring og transmission af data, udviklet på basis af det vidunderlige `binære-data`-bibliotek.

Som et resultat fik vi følgende datastrukturer:

-Transaktion

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

- Polet

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

-Blok

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

Med de sædvanlige kommandoer `BD.encode(block, Protocol).slice();` og `BD.decode(buffer, Protocol)` konverterer vi dataene til `Buffer` for at gemme i Redis eller videresende til en anden node og hente data tilbage.

Vi har også 2 binære protokoller til overførsel af data mellem tjenester:

— Protokol for interaktion med Plasma Node via unix-sokkel

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

hvor:

  • `Type` — handlingen, der skal udføres, for eksempel 1 — sendTransaction, 2 — getTransaction;
  • `nyttelast` — data, der skal videregives til den relevante funktion;
  • 'messageId' — meddelelses-id, så svaret kan identificeres.

— Protokol for interaktion mellem knudepunkter

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

hvor:

  • 'kode' — meddelelseskode, for eksempel 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • "versionsprotokol". — protokolversion, da knudepunkter med forskellige versioner kan oprettes på netværket, og de kan fungere forskelligt;
  • 'seq' — meddelelsesidentifikation;
  • 'countChunk' и `chunkNumber` nødvendigt for at opdele store beskeder;
  • 'længde' и `nyttelast` længde og selve dataene.

Da vi på forhånd har indtastet dataene, er det endelige system meget hurtigere end Ethereums `rlp`-bibliotek. Desværre har vi endnu ikke været i stand til at afvise det, da det er nødvendigt at færdiggøre den smarte kontrakt, som vi planlægger at gøre i fremtiden.

Hvis det lykkedes os at nå hastigheden 35 000 transaktioner i sekundet, skal vi også behandle dem på det optimale tidspunkt. Da den omtrentlige blokdannelsestid tager 30 sekunder, skal vi inkludere i blokken 1 000 000 transaktioner, hvilket betyder at sende flere 100 MB data.

Til at begynde med brugte vi `ethereumjs-devp2p`-biblioteket til at kommunikere mellem noder, men det kunne ikke håndtere så meget data. Som et resultat brugte vi 'ws'-biblioteket og konfigurerede at sende binære data via websocket. Vi stødte selvfølgelig også på problemer ved afsendelse af store datapakker, men vi delte dem op i bidder, og nu er disse problemer væk.

Danner også et Merkle-træ og beregner hashen 1 000 000 transaktioner kræver ca 10 sekunders kontinuerlig beregning. I løbet af denne tid lykkes forbindelsen med alle noder at bryde. Det blev besluttet at flytte denne beregning til en separat tråd.

Konklusioner:

Faktisk er vores resultater ikke nye, men af ​​en eller anden grund glemmer mange eksperter dem, når de udvikler.

  • Brug af funktionel programmering i stedet for objektorienteret programmering forbedrer produktiviteten.
  • Monolitten er værre end en servicearkitektur for et produktivt NodeJS-system.
  • Brug af 'worker_threads' til tunge beregninger forbedrer systemets reaktionsevne, især når det drejer sig om i/o-operationer.
  • unix-socket er mere stabil og hurtigere end http-anmodninger.
  • Hvis du hurtigt skal overføre store data over netværket, er det bedre at bruge websockets og sende binære data, opdelt i bidder, som kan videresendes, hvis de ikke når frem, og derefter kombineres til én besked.

Vi inviterer dig på besøg GitHub projekt: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Artiklen er medskrevet af Alexander Nashivan, seniorudvikler Clever Solution Inc.

Kilde: www.habr.com

Tilføj en kommentar