Offentlig test: En løsning for personvern og skalerbarhet på Ethereum

Blockchain er en innovativ teknologi som lover å forbedre mange områder av menneskelivet. Den overfører virkelige prosesser og produkter til det digitale rommet, sikrer hastighet og pålitelighet av økonomiske transaksjoner, reduserer kostnadene deres, og lar deg også lage moderne DAPP-applikasjoner ved hjelp av smarte kontrakter i desentraliserte nettverk.

Gitt de mange fordelene og varierte bruksområdene til blockchain, kan det virke overraskende at denne lovende teknologien ennå ikke har kommet inn i alle bransjer. Problemet er at moderne desentraliserte blokkjeder mangler skalerbarhet. Ethereum behandler omtrent 20 transaksjoner per sekund, noe som ikke er nok til å møte behovene til dagens dynamiske virksomheter. Samtidig er selskaper som bruker blokkjedeteknologi nølende med å forlate Ethereum på grunn av sin høye grad av beskyttelse mot hacking og nettverksfeil.

For å sikre desentralisering, sikkerhet og skalerbarhet i blokkjeden, og dermed løse Scalability Trilemma, har utviklingsteamet Mulighet opprettet Plasma Cash, en datterselskapskjede bestående av en smart kontrakt og et privat nettverk basert på Node.js, som med jevne mellomrom overfører sin tilstand til rotkjeden (Ethereum).

Offentlig test: En løsning for personvern og skalerbarhet på Ethereum

Nøkkelprosesser i Plasma Cash

1. Brukeren kaller den smarte kontraktsfunksjonen «innskudd», og overfører til den mengden ETH som han ønsker å sette inn på Plasma Cash-tokenet. Den smarte kontraktsfunksjonen lager et token og genererer en hendelse om det.

2. Plasma Cash-noder som abonnerer på smarte kontraktshendelser mottar en hendelse om å opprette et innskudd og legger til en transaksjon om å opprette et token til bassenget.

3. Med jevne mellomrom tar spesielle Plasma Cash-noder alle transaksjoner fra bassenget (opptil 1 million) og danner en blokk fra dem, beregner Merkle-treet og følgelig hashen. Denne blokken sendes til andre noder for verifisering. Nodene sjekker om Merkle-hashen er gyldig og om transaksjonene er gyldige (for eksempel om avsenderen av tokenet er eieren). Etter å ha verifisert blokkeringen, kaller noden «submitBlock»-funksjonen til den smarte kontrakten, som lagrer blokknummeret og Merkle-hash til kantkjeden. Den smarte kontrakten genererer en hendelse som indikerer vellykket tillegg av en blokk. Transaksjoner fjernes fra bassenget.

4. Noder som mottar blokkinnsendingshendelsen begynner å bruke transaksjonene som ble lagt til blokken.

5. På et tidspunkt vil eieren (eller ikke-eieren) av tokenet trekke det fra Plasma Cash. For å gjøre dette kaller han «startExit»-funksjonen, og sender inn informasjon om de siste 2 transaksjonene på tokenet, som bekrefter at han er eieren av tokenet. Den smarte kontrakten, ved hjelp av Merkle-hash, sjekker tilstedeværelsen av transaksjoner i blokkene og sender tokenet for uttak, som vil skje om to uker.

6. Hvis tokenuttaksoperasjonen skjedde med brudd (tokenet ble brukt etter at uttaksprosedyren startet eller tokenet allerede var noen andres før uttaket), kan eieren av tokenet avvise uttaket innen to uker.

Offentlig test: En løsning for personvern og skalerbarhet på Ethereum

Personvern oppnås på to måter

1. Rotkjeden vet ingenting om transaksjonene som genereres og videresendes i underkjeden. Informasjon om hvem som satte inn og tok ut ETH fra Plasma Cash forblir offentlig.

2. Barnekjeden tillater anonyme transaksjoner ved hjelp av zk-SNARKs.

Teknologistabel

  • NodeJS
  • Redis
  • Etherium
  • soild

Testing

Mens vi utviklet Plasma Cash, testet vi hastigheten til systemet og oppnådde følgende resultater:

  • opptil 35 000 transaksjoner per sekund legges til bassenget;
  • opptil 1 000 000 transaksjoner kan lagres i en blokk.

Tester ble utfø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 ble hevet.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SATA SSD – 500 GB, 64 GB DDR4 RAM
Ropsten testnet ETH-noden ble hevet.
3 validerende Plasma Cash-noder ble hevet.

3. Intel Core i9-9900K Octa-Core inkl. NVMe SSD – 1 TB, 64 GB DDR4 RAM
1 Plasma Cash innsendingsnode ble hevet.
3 validerende Plasma Cash-noder ble hevet.
En test ble lansert for å legge til transaksjoner til Plasma Cash-nettverket.

totalt: 10 Plasma Cash noder i et privat nettverk.

Test 1

Det er en grense på 1 million transaksjoner per blokk. Derfor faller 1 million transaksjoner inn i 2 blokker (siden systemet klarer å ta en del av transaksjonene og sende inn mens de sendes).


Opprinnelig tilstand: siste blokk #7; 1 million transaksjoner og tokens er lagret i databasen.

00:00 — start av transaksjonsgenereringsskript
01:37 - 1 million transaksjoner ble opprettet og sending til noden begynte
01:46 — innsendingsnoden tok 240 8 transaksjoner fra bassenget og skjemablokk #320. Vi ser også at 10 XNUMX transaksjoner legges til bassenget på XNUMX sekunder
01:58 — blokk #8 er signert og sendt for validering
02:03 — blokk #8 er validert og «submitBlock»-funksjonen til smartkontrakten kalles opp med Merkle-hash og blokknummer
02:10 — demoskriptet sluttet å virke, som sendte 1 million transaksjoner på 32 sekunder
02:33 - noder begynte å motta informasjon om at blokk #8 ble lagt til rotkjeden, og begynte å utføre 240 XNUMX transaksjoner
02:40 - 240 8 transaksjoner ble fjernet fra bassenget, som allerede er i blokk #XNUMX
02:56 — innsendingsnoden tok de resterende 760 9 transaksjonene fra bassenget og begynte å beregne Merkle-hashen og signeringsblokk #XNUMX
03:20 - alle noder inneholder 1 million 240k transaksjoner og tokens
03:35 — blokk #9 er signert og sendt for validering til andre noder
03:41 - nettverksfeil oppstod
04:40 — venter på blokk #9-validering har gått ut
04:54 — innsendingsnoden tok de resterende 760 9 transaksjonene fra bassenget og begynte å beregne Merkle-hashen og signeringsblokk #XNUMX
05:32 — blokk #9 er signert og sendt for validering til andre noder
05:53 — blokk #9 er validert og sendt til rotkjeden
06:17 - noder begynte å motta informasjon om at blokk #9 ble lagt til rotkjeden og begynte å utføre 760 XNUMX transaksjoner
06:47 — bassenget har fjernet transaksjoner som er i blokk #9
09:06 - alle noder inneholder 2 millioner transaksjoner og tokens

Test 2

Det er en grense på 350k per blokk. Som et resultat har vi 3 blokker.


Opprinnelig tilstand: siste blokk #9; 2 millioner transaksjoner og tokens er lagret i databasen

00:00 — transaksjonsgenereringsskript er allerede lansert
00:44 - 1 million transaksjoner ble opprettet og sending til noden begynte
00:56 — innsendingsnoden tok 320 10 transaksjoner fra bassenget og skjemablokk #320. Vi ser også at 10 XNUMX transaksjoner legges til bassenget på XNUMX sekunder
01:12 — blokk #10 er signert og sendt til andre noder for validering
01:18 — demoskriptet sluttet å virke, som sendte 1 million transaksjoner på 34 sekunder
01:20 — blokk #10 er validert og sendt til rotkjeden
01:51 - alle noder mottok informasjon fra rotkjeden om at blokk #10 ble lagt til og begynner å bruke 320k transaksjoner
02:01 - bassenget har klarert for 320 10 transaksjoner som ble lagt til blokk #XNUMX
02:15 — innsendingsnoden tok 350 11 transaksjoner fra bassenget og skjemablokk #XNUMX
02:34 — blokk #11 er signert og sendt til andre noder for validering
02:51 — blokk #11 er validert og sendt til rotkjeden
02:55 — den siste noden fullførte transaksjoner fra blokk #10
10:59 — transaksjonen med innsending av blokk #9 tok veldig lang tid i rotkjeden, men den ble fullført og alle noder mottok informasjon om den og begynte å utføre 350k transaksjoner
11:05 - bassenget har klarert for 320 11 transaksjoner som ble lagt til blokk #XNUMX
12:10 - alle noder inneholder 1 million 670k transaksjoner og tokens
12:17 — innsendingsnoden tok 330 12 transaksjoner fra bassenget og skjemablokk #XNUMX
12:32 — blokk #12 er signert og sendt til andre noder for validering
12:39 — blokk #12 er validert og sendt til rotkjeden
13:44 - alle noder mottok informasjon fra rotkjeden om at blokk #12 ble lagt til og begynner å bruke 330k transaksjoner
14:50 - alle noder inneholder 2 millioner transaksjoner og tokens

Test 3

I den første og andre serveren ble en validerende node erstattet av en innsendingsnode.


Opprinnelig tilstand: siste blokk #84; 0 transaksjoner og tokens lagret i databasen

00:00 — 3 skript har blitt lansert som genererer og sender 1 million transaksjoner hver
01:38 — 1 million transaksjoner ble opprettet og sendingen til innsendingsnoden #3 begynte
01:50 — send node #3 tok 330k transaksjoner fra bassenget og skjemablokk #85 (f21). Vi ser også at 350 10 transaksjoner legges til bassenget på XNUMX sekunder
01:53 — 1 million transaksjoner ble opprettet og sendingen til innsendingsnoden #1 begynte
01:50 — send node #3 tok 330k transaksjoner fra bassenget og skjemablokk #85 (f21). Vi ser også at 350 10 transaksjoner legges til bassenget på XNUMX sekunder
02:01 — send node #1 tok 250 85 transaksjoner fra bassenget og skjemablokk #65 (XNUMXe)
02:06 — blokk #85 (f21) er signert og sendt til andre noder for validering
02:08 — demoskript av server #3, som sendte 1 million transaksjoner på 30 sekunder, fullførte arbeidet
02:14 — blokk #85 (f21) er validert og sendt til rotkjeden
02:19 — blokk #85 (65e) er signert og sendt til andre noder for validering
02:22 — 1 million transaksjoner ble opprettet og sendingen til innsendingsnoden #2 begynte
02:27 — blokk #85 (65e) validert og sendt til rotkjeden
02:29 — send node #2 tok 111855 transaksjoner fra bassenget og skjemablokk #85 (256).
02:36 — blokk #85 (256) er signert og sendt til andre noder for validering
02:36 — demoskript av server #1, som sendte 1 million transaksjoner på 42.5 sekunder, fullførte arbeidet
02:38 — blokk #85 (256) er validert og sendt til rotkjeden
03:08 — server #2-skriptet fullførte å virke, som sendte 1 million transaksjoner på 47 sekunder
03:38 - alle noder mottok informasjon fra rotkjeden som blokker #85 (f21), #86(65e), #87(256) ble lagt til og begynte å bruke 330k, 250k, 111855 transaksjoner
03:49 - bassenget ble klarert ved 330k, 250k, 111855 transaksjoner som ble lagt til blokker #85 (f21), #86(65e), #87(256)
03:59 — send node #1 tok 888145 88 transaksjoner fra bassenget og skjemablokk #214 (2), send node #750 tok 88k transaksjoner fra bassenget og skjemablokk #50 (3a), send node #670 tok 88k transaksjoner fra bassenget og danner blokk #3 (dXNUMXb)
04:44 — blokk #88 (d3b) er signert og sendt til andre noder for validering
04:58 — blokk #88 (214) er signert og sendt til andre noder for validering
05:11 — blokk #88 (50a) er signert og sendt til andre noder for validering
05:11 — blokk #85 (d3b) er validert og sendt til rotkjeden
05:36 — blokk #85 (214) er validert og sendt til rotkjeden
05:43 - alle noder mottok informasjon fra rotkjeden som blokker #88 (d3b), #89(214) er lagt til og begynner å bruke 670k, 750k transaksjoner
06:50 — på grunn av en kommunikasjonsfeil ble blokk #85 (50a) ikke validert
06:55 — send node #2 tok 888145 transaksjoner fra bassenget og skjemablokk #90 (50a)
08:14 — blokk #90 (50a) er signert og sendt til andre noder for validering
09:04 — blokk #90 (50a) er validert og sendt til rotkjeden
11:23 - alle noder mottok informasjon fra rotkjeden om at blokk #90 (50a) ble lagt til, og begynner å bruke 888145 transaksjoner. Samtidig har server #3 allerede brukt transaksjoner fra blokkene #88 (d3b), #89(214)
12:11 - alle bassengene er tomme
13:41 — alle noder på server #3 inneholder 3 millioner transaksjoner og tokens
14:35 — alle noder på server #1 inneholder 3 millioner transaksjoner og tokens
19:24 — alle noder på server #2 inneholder 3 millioner transaksjoner og tokens

Hindringer

Under utviklingen av Plasma Cash møtte vi følgende problemer, som vi gradvis løste og løser:

1. Konflikt i samspillet mellom ulike systemfunksjoner. Funksjonen med å legge til transaksjoner i bassenget blokkerte for eksempel arbeidet med å sende inn og validere blokker, og omvendt, noe som førte til et fall i hastighet.

2. Det var ikke umiddelbart klart hvordan man sender et stort antall transaksjoner samtidig som man minimerer dataoverføringskostnadene.

3. Det var ikke klart hvordan og hvor data skulle lagres for å oppnå høye resultater.

4. Det var ikke klart hvordan man organiserer et nettverk mellom noder, siden størrelsen på en blokk med 1 million transaksjoner tar opp omtrent 100 MB.

5. Arbeid i entrådsmodus bryter forbindelsen mellom noder når lange beregninger forekommer (for eksempel ved å bygge et Merkle-tre og beregne hashen).

Hvordan taklet vi alt dette?

Den første versjonen av Plasma Cash-noden var en slags skurtresker som kunne gjøre alt på samme tid: godta transaksjoner, sende inn og validere blokker, og gi en API for tilgang til data. Siden NodeJS er naturlig enkelt-trådet, blokkerte den tunge Merkle-treberegningsfunksjonen funksjonen legg til transaksjon. Vi så to alternativer for å løse dette problemet:

1. Start flere NodeJS-prosesser, som hver utfører spesifikke funksjoner.

2. Bruk worker_threads og flytt utførelsen av en del av koden til tråder.

Som et resultat brukte vi begge alternativene samtidig: vi delte logisk inn en node i 3 deler som kan fungere separat, men samtidig synkront

1. Innsendingsnode, som aksepterer transaksjoner inn i bassenget og oppretter blokker.

2. En validerende node som sjekker gyldigheten til noder.

3. API-node - gir en API for tilgang til data.

I dette tilfellet kan du koble til hver node via en unix-kontakt ved å bruke cli.

Vi flyttet tunge operasjoner, som å beregne Merkle-treet, inn i en egen tråd.

Dermed har vi oppnådd normal drift av alle Plasma Cash-funksjoner samtidig og uten feil.

Når systemet var funksjonelt begynte vi å teste hastigheten og fikk dessverre utilfredsstillende resultater: 5 000 transaksjoner per sekund og opptil 50 000 transaksjoner per blokk. Jeg måtte finne ut hva som ble implementert feil.

Til å begynne med begynte vi å teste kommunikasjonsmekanismen med Plasma Cash for å finne ut toppkapasiteten til systemet. Vi skrev tidligere at Plasma Cash-noden gir et unix-socket-grensesnitt. Opprinnelig var det tekstbasert. json-objekter ble sendt ved hjelp av `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øringshastigheten til slike objekter og fant ~130k per sekund. Vi prøvde å erstatte standardfunksjonene for arbeid med json, men ytelsen ble ikke bedre. V8-motoren må være godt optimalisert for disse operasjonene.

Vi jobbet med transaksjoner, tokens og blokker gjennom klasser. Når du opprettet slike klasser, falt ytelsen med 2 ganger, noe som indikerer at OOP ikke passer for oss. Jeg måtte skrive om alt til en rent funksjonell tilnærming.

Opptak i databasen

Opprinnelig ble Redis valgt for datalagring som en av de mest produktive løsningene som tilfredsstiller våre krav: nøkkelverdilagring, arbeid med hashtabeller, sett. Vi lanserte redis-benchmark og fikk ~80 1 operasjoner per sekund i XNUMX pipelining-modus.

For høy ytelse har vi finjustert Redis:

  • En unix-kontaktforbindelse er opprettet.
  • Vi deaktiverte lagring av tilstanden til disk (for pålitelighet kan du sette opp en replika og lagre til disk i en separat Redis).

I Redis er en pool en hash-tabell fordi vi må kunne hente alle transaksjoner i en spørring og slette transaksjoner én etter én. Vi prøvde å bruke en vanlig liste, men den er tregere når du laster ut hele listen.

Ved bruk av standard NodeJS oppnådde Redis-bibliotekene en ytelse på 18 9 transaksjoner per sekund. Hastigheten falt XNUMX ganger.

Siden benchmark viste oss at mulighetene var klart 5 ganger større, begynte vi å optimalisere. Vi endret biblioteket til ioredis og fikk ytelse på 25k per sekund. Vi la til transaksjoner en etter en ved å bruke 'hset'-kommandoen. Så vi genererte mange søk i Redis. Ideen oppsto om å kombinere transaksjoner i batcher og sende dem med én kommando `hmset`. Resultatet er 32k per sekund.

Av flere grunner, som vi vil beskrive nedenfor, jobber vi med data ved å bruke `Buffer`, og som det viser seg, hvis du konverterer det til tekst (`buffer.toString('hex')`) før du skriver, kan du få ytterligere opptreden. Dermed ble hastigheten økt til 35k per sekund. For øyeblikket bestemte vi oss for å stoppe ytterligere optimalisering.

Vi måtte bytte til en binær protokoll fordi:

1. Systemet beregner ofte hasher, signaturer osv., og for dette trenger det data i `Buffer.

2. Når de sendes mellom tjenester, veier binære data mindre enn tekst. For eksempel, når du sender en blokk med 1 million transaksjoner, kan dataene i teksten ta opp mer enn 300 megabyte.

3. Stadig transformering av data påvirker ytelsen.

Derfor tok vi utgangspunkt i vår egen binære protokoll for lagring og overføring av data, utviklet på grunnlag av det fantastiske `binære-data`-biblioteket.

Som et resultat fikk vi følgende datastrukturer:

-Transaksjon

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

-Blokkere

  ```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 vanlige kommandoene `BD.encode(block, Protocol).slice();` og `BD.decode(buffer, Protocol)` konverterer vi dataene til `Buffer` for å lagre i Redis eller videresende til en annen node og hente data tilbake.

Vi har også 2 binære protokoller for overføring av data mellom tjenester:

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

der:

  • `Typen` — handlingen som skal utføres, for eksempel 1 — sendTransaction, 2 — getTransaction;
  • `nyttelast` — data som må sendes til den aktuelle funksjonen;
  • 'meldings-ID' — meldings-ID slik at svaret kan identifiseres.

— Protokoll for samhandling mellom noder

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

der:

  • `kode` — meldingskode, for eksempel 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • "versjonsprotokoll". — protokollversjon, siden noder med forskjellige versjoner kan heves på nettverket og de kan fungere annerledes;
  • `seq` — meldingsidentifikator;
  • `countChunk` и `chunkNumber` nødvendig for å dele store meldinger;
  • `lengde` и `nyttelast` lengde og selve dataene.

Siden vi forhåndsskrev dataene, er det endelige systemet mye raskere enn Ethereums `rlp`-bibliotek. Dessverre har vi ennå ikke vært i stand til å nekte det, siden det er nødvendig å fullføre den smarte kontrakten, som vi planlegger å gjøre i fremtiden.

Hvis vi klarte å nå farten 35 transaksjoner per sekund, må vi også behandle dem på optimal tid. Siden den omtrentlige blokkdannelsestiden tar 30 sekunder, må vi inkludere i blokken +1 000 000 XNUMX transaksjoner, som betyr å sende flere 100 MB data.

Til å begynne med brukte vi `ethereumjs-devp2p`-biblioteket for å kommunisere mellom noder, men det kunne ikke håndtere så mye data. Som et resultat brukte vi `ws`-biblioteket og konfigurerte sending av binære data via websocket. Vi har selvfølgelig også støtt på problemer ved sending av store datapakker, men vi delte dem opp i biter og nå er disse problemene borte.

Danner også et Merkle-tre og beregner hasjen +1 000 000 XNUMX transaksjoner krever ca 10 sekunder med kontinuerlig beregning. I løpet av denne tiden klarer forbindelsen med alle noder å bryte. Det ble besluttet å flytte denne beregningen til en egen tråd.

Konklusjoner:

Faktisk er funnene våre ikke nye, men av en eller annen grunn glemmer mange eksperter dem når de utvikler.

  • Bruk av funksjonell programmering i stedet for objektorientert programmering forbedrer produktiviteten.
  • Monolitten er verre enn en tjenestearkitektur for et produktivt NodeJS-system.
  • Å bruke "worker_threads" for tung beregning forbedrer systemets reaksjonsevne, spesielt når du arbeider med i/o-operasjoner.
  • unix-socket er mer stabil og raskere enn http-forespørsler.
  • Hvis du raskt trenger å overføre store data over nettverket, er det bedre å bruke websockets og sende binære data, delt inn i biter, som kan videresendes hvis de ikke kommer, og deretter kombineres til én melding.

Vi inviterer deg på besøk GitHub prosjekt: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Artikkelen er skrevet i samarbeid med Alexander Nashivan, seniorutvikler Clever Solution Inc.

Kilde: www.habr.com

Legg til en kommentar