Offentligt test: En lösning för integritet och skalbarhet på Ethereum

Blockchain är en innovativ teknik som lovar att förbättra många områden av mänskligt liv. Den överför verkliga processer och produkter till det digitala rummet, säkerställer snabbhet och tillförlitlighet för finansiella transaktioner, minskar deras kostnader och låter dig också skapa moderna DAPP-applikationer med smarta kontrakt i decentraliserade nätverk.

Med tanke på de många fördelarna och olika tillämpningarna av blockchain kan det tyckas förvånande att denna lovande teknik ännu inte har tagit sig in i alla branscher. Problemet är att moderna decentraliserade blockkedjor saknar skalbarhet. Ethereum bearbetar cirka 20 transaktioner per sekund, vilket inte räcker för att möta behoven hos dagens dynamiska företag. Samtidigt är företag som använder blockchain-teknik tveksamma till att överge Ethereum på grund av dess höga skyddsgrad mot hackning och nätverksfel.

För att säkerställa decentralisering, säkerhet och skalbarhet i blockkedjan, och därmed lösa skalbarhetstrilemma, utvecklar teamet Möjlighet skapade Plasma Cash, en dotterbolagskedja bestående av ett smart kontrakt och ett privat nätverk baserat på Node.js, som periodiskt överför sitt tillstånd till rotkedjan (Ethereum).

Offentligt test: En lösning för integritet och skalbarhet på Ethereum

Nyckelprocesser i Plasma Cash

1. Användaren kallar den smarta kontraktsfunktionen för 'insättning' och skickar in beloppet ETH som han vill sätta in på Plasma Cash-token. Den smarta kontraktsfunktionen skapar en token och genererar en händelse om den.

2. Plasma Cash-noder som prenumererar på smarta kontraktshändelser får en händelse om att skapa en insättning och lägga till en transaktion om att skapa en token till poolen.

3. Regelbundet tar speciella Plasma Cash-noder alla transaktioner från poolen (upp till 1 miljon) och bildar ett block från dem, beräknar Merkle-trädet och följaktligen hashen. Detta block skickas till andra noder för verifiering. Noderna kontrollerar om Merkle-hash är giltig och om transaktionerna är giltiga (till exempel om avsändaren av token är dess ägare). Efter att ha verifierat blocket anropar noden "submitBlock"-funktionen för det smarta kontraktet, vilket sparar blocknumret och Merkle-hash till kantkedjan. Det smarta kontraktet genererar en händelse som indikerar framgångsrikt tillägg av ett block. Transaktioner tas bort från poolen.

4. Noder som tar emot blocksubmission-händelsen börjar tillämpa de transaktioner som lagts till i blocket.

5. Vid någon tidpunkt vill ägaren (eller icke-ägaren) av tokenet ta ut det från Plasma Cash. För att göra detta anropar han "startExit"-funktionen och skickar in information om de två senaste transaktionerna på token, som bekräftar att han är ägaren till token. Det smarta kontraktet, med hjälp av Merkle-hash, kontrollerar närvaron av transaktioner i blocken och skickar token för uttag, vilket kommer att ske om två veckor.

6. Om tokenuttagsoperationen inträffade med överträdelser (poletten användes efter att uttagsproceduren började eller token var redan någon annans innan uttaget), kan tokens ägare motbevisa uttaget inom två veckor.

Offentligt test: En lösning för integritet och skalbarhet på Ethereum

Sekretess uppnås på två sätt

1. Rotkedjan vet ingenting om de transaktioner som genereras och vidarebefordras inom den underordnade kedjan. Information om vem som satte in och tog ut ETH från Plasma Cash förblir offentlig.

2. Den underordnade kedjan tillåter anonyma transaktioner med zk-SNARKs.

Teknikstapel

  • NodeJS
  • Redis
  • Eterium
  • soild

testning

När vi utvecklade Plasma Cash testade vi systemets hastighet och fick följande resultat:

  • upp till 35 000 transaktioner per sekund läggs till i poolen;
  • upp till 1 000 000 transaktioner kan lagras i ett block.

Tester utfördes på följande 3 servrar:

1. Intel Core i7-6700 Quad-Core Skylake inkl. NVMe SSD – 512 GB, 64 GB DDR4 RAM
3 validerande Plasma Cash-noder togs fram.

2. AMD Ryzen 7 1700X Octa-Core “Summit Ridge” (Zen), SATA SSD – 500 GB, 64 GB DDR4 RAM
Ropstens testnet ETH-nod höjdes.
3 validerande Plasma Cash-noder togs fram.

3. Intel Core i9-9900K Octa-Core inkl. NVMe SSD – 1 TB, 64 GB DDR4 RAM
1 Plasma Cash submission nod höjdes.
3 validerande Plasma Cash-noder togs fram.
Ett test lanserades för att lägga till transaktioner till Plasma Cash-nätverket.

Totalt: 10 Plasma Cash-noder i ett privat nätverk.

Test 1

Det finns en gräns på 1 miljon transaktioner per block. Därför faller 1 miljon transaktioner i 2 block (eftersom systemet lyckas ta del av transaktionerna och skicka medan de skickas).


Initialt tillstånd: sista block #7; 1 miljon transaktioner och tokens lagras i databasen.

00:00 — start av transaktionsgenereringsskript
01:37 - 1 miljon transaktioner skapades och sändningen till noden började
01:46 — submit nod tog 240 8 transaktioner från poolen och formulärblock #320. Vi ser också att 10 XNUMX transaktioner läggs till poolen på XNUMX sekunder
01:58 — block #8 signeras och skickas för validering
02:03 — block #8 är validerat och "submitBlock"-funktionen för det smarta kontraktet anropas med Merkle-hash och blocknummer
02:10 — demoskriptet slutade fungera, som skickade 1 miljon transaktioner på 32 sekunder
02:33 - noder började ta emot information om att block #8 lades till i rotkedjan och började utföra 240 XNUMX transaktioner
02:40 - 240 8 transaktioner togs bort från poolen, som redan finns i block #XNUMX
02:56 — submit-noden tog de återstående 760 9 transaktionerna från poolen och började beräkna Merkle-hash och signeringsblock #XNUMX
03:20 - alla noder innehåller 1 miljon 240 XNUMX transaktioner och tokens
03:35 — block #9 signeras och skickas för validering till andra noder
03:41 - nätverksfel uppstod
04:40 — väntar på block #9-validering har timeout
04:54 — submit-noden tog de återstående 760 9 transaktionerna från poolen och började beräkna Merkle-hash och signeringsblock #XNUMX
05:32 — block #9 signeras och skickas för validering till andra noder
05:53 — block #9 valideras och skickas till rotkedjan
06:17 - noder började ta emot information om att block #9 lades till i rotkedjan och började utföra 760 XNUMX transaktioner
06:47 — poolen har rensat från transaktioner som finns i block #9
09:06 - alla noder innehåller 2 miljoner transaktioner och tokens

Test 2

Det finns en gräns på 350k per block. Som ett resultat har vi 3 block.


Initialt tillstånd: sista block #9; 2 miljoner transaktioner och tokens lagras i databasen

00:00 — transaktionsgenereringsskript har redan lanserats
00:44 - 1 miljon transaktioner skapades och sändningen till noden började
00:56 — submit nod tog 320 10 transaktioner från poolen och formulärblock #320. Vi ser också att 10 XNUMX transaktioner läggs till poolen på XNUMX sekunder
01:12 — block #10 signeras och skickas till andra noder för validering
01:18 — demoskriptet slutade fungera, som skickade 1 miljon transaktioner på 34 sekunder
01:20 — block #10 valideras och skickas till rotkedjan
01:51 - alla noder fick information från rotkedjan att block #10 lades till och börjar tillämpa 320k transaktioner
02:01 - poolen har godkänt för 320 10 transaktioner som lades till i block #XNUMX
02:15 — inlämningsnoden tog 350 11 transaktioner från poolen och formulärblock #XNUMX
02:34 — block #11 signeras och skickas till andra noder för validering
02:51 — block #11 valideras och skickas till rotkedjan
02:55 — den sista noden som genomförde transaktioner från block #10
10:59 — transaktionen med inlämning av block #9 tog mycket lång tid i rotkedjan, men den slutfördes och alla noder fick information om den och började utföra 350 XNUMX transaktioner
11:05 - poolen har godkänt för 320 11 transaktioner som lades till i block #XNUMX
12:10 - alla noder innehåller 1 miljon 670 XNUMX transaktioner och tokens
12:17 — inlämningsnoden tog 330 12 transaktioner från poolen och formulärblock #XNUMX
12:32 — block #12 signeras och skickas till andra noder för validering
12:39 — block #12 valideras och skickas till rotkedjan
13:44 - alla noder fick information från rotkedjan att block #12 lades till och börjar tillämpa 330k transaktioner
14:50 - alla noder innehåller 2 miljoner transaktioner och tokens

Test 3

I den första och andra servern ersattes en validerande nod av en sändande nod.


Initialt tillstånd: sista block #84; 0 transaktioner och tokens sparade i databasen

00:00 — 3 skript har lanserats som genererar och skickar 1 miljon transaktioner vardera
01:38 — 1 miljon transaktioner skapades och sändningen för att skicka nod #3 började
01:50 — submit nod #3 tog 330k transaktioner från poolen och formulär block #85 (f21). Vi ser också att 350 10 transaktioner läggs till poolen på XNUMX sekunder
01:53 — 1 miljon transaktioner skapades och sändningen för att skicka nod #1 började
01:50 — submit nod #3 tog 330k transaktioner från poolen och formulär block #85 (f21). Vi ser också att 350 10 transaktioner läggs till poolen på XNUMX sekunder
02:01 — inlämningsnod #1 tog 250 85 transaktioner från poolen och formulärblock #65 (XNUMXe)
02:06 — block #85 (f21) signeras och skickas till andra noder för validering
02:08 — demoskript av server #3, som skickade 1 miljon transaktioner på 30 sekunder, slutade fungera
02:14 — block #85 (f21) valideras och skickas till rotkedjan
02:19 — block #85 (65e) signeras och skickas till andra noder för validering
02:22 — 1 miljon transaktioner skapades och sändningen för att skicka nod #2 började
02:27 — block #85 (65e) validerat och skickat till rotkedjan
02:29 — skicka nod #2 tog 111855 transaktioner från poolen och formulär block #85 (256).
02:36 — block #85 (256) signeras och skickas till andra noder för validering
02:36 — demoskript av server #1, som skickade 1 miljon transaktioner på 42.5 sekunder, slutade fungera
02:38 — block #85 (256) valideras och skickas till rotkedjan
03:08 — Server #2-skriptet slutade fungera, som skickade 1 miljon transaktioner på 47 sekunder
03:38 - alla noder fick information från rotkedjan att block #85 (f21), #86(65e), #87(256) lades till och började tillämpa 330k, 250k, 111855 transaktioner
03:49 - poolen rensades vid 330k, 250k, 111855 transaktioner som lades till i block #85 (f21), #86(65e), #87(256)
03:59 — submit nod #1 tog 888145 88 transaktioner från poolen och formulär block #214 (2), submit nod #750 tog 88k transaktioner från poolen och formulär block #50 (3a), submit nod #670 tog 88k transaktioner från poolen och bildar block #3 (dXNUMXb)
04:44 — block #88 (d3b) signeras och skickas till andra noder för validering
04:58 — block #88 (214) signeras och skickas till andra noder för validering
05:11 — block #88 (50a) signeras och skickas till andra noder för validering
05:11 — block #85 (d3b) valideras och skickas till rotkedjan
05:36 — block #85 (214) valideras och skickas till rotkedjan
05:43 - alla noder fick information från rotkedjan som block #88 (d3b), #89(214) har lagts till och börjar tillämpa 670k, 750k transaktioner
06:50 — på grund av ett kommunikationsfel validerades inte block #85 (50a)
06:55 — skicka nod #2 tog 888145 transaktioner från poolen och formulärblock #90 (50a)
08:14 — block #90 (50a) signeras och skickas till andra noder för validering
09:04 — block #90 (50a) valideras och skickas till rotkedjan
11:23 - alla noder fick information från rotkedjan att block #90 (50a) lades till, och börjar tillämpa 888145 transaktioner. Samtidigt har server #3 redan tillämpat transaktioner från block #88 (d3b), #89(214)
12:11 - alla bassänger är tomma
13:41 — alla noder på server #3 innehåller 3 miljoner transaktioner och tokens
14:35 — alla noder på server #1 innehåller 3 miljoner transaktioner och tokens
19:24 — alla noder på server #2 innehåller 3 miljoner transaktioner och tokens

Hinder

Under utvecklingen av Plasma Cash stötte vi på följande problem, som vi gradvis löste och håller på att lösa:

1. Konflikt i samspelet mellan olika systemfunktioner. Till exempel blockerade funktionen att lägga till transaktioner i poolen arbetet med att skicka in och validera block, och vice versa, vilket ledde till att hastigheten sjönk.

2. Det var inte omedelbart klart hur man skickar ett stort antal transaktioner samtidigt som kostnaderna för dataöverföringen minimeras.

3. Det var inte klart hur och var man skulle lagra data för att uppnå höga resultat.

4. Det var inte klart hur man organiserar ett nätverk mellan noder, eftersom storleken på ett block med 1 miljon transaktioner tar upp cirka 100 MB.

5. Att arbeta i enkeltrådigt läge bryter kopplingen mellan noder när långa beräkningar sker (till exempel bygga ett Merkle-träd och beräkna dess hash).

Hur hanterade vi allt detta?

Den första versionen av Plasma Cash-noden var en slags kombination som kunde göra allt på samma gång: acceptera transaktioner, skicka in och validera block och tillhandahålla ett API för åtkomst till data. Eftersom NodeJS är naturligt entrådad, blockerade den tunga Merkle-trädberäkningsfunktionen funktionen Lägg till transaktion. Vi såg två alternativ för att lösa detta problem:

1. Starta flera NodeJS-processer, som var och en utför specifika funktioner.

2. Använd worker_threads och flytta exekveringen av en del av koden till trådar.

Som ett resultat använde vi båda alternativen samtidigt: vi delade logiskt upp en nod i 3 delar som kan fungera separat, men samtidigt synkront

1. Submission nod, som accepterar transaktioner till poolen och skapar block.

2. En validerande nod som kontrollerar nodernas giltighet.

3. API-nod - tillhandahåller ett API för åtkomst till data.

I det här fallet kan du ansluta till varje nod via ett unix-uttag med cli.

Vi flyttade tunga operationer, som att beräkna Merkle-trädet, till en separat tråd.

Därmed har vi uppnått normal drift av alla Plasma Cash-funktioner samtidigt och utan fel.

När systemet väl fungerade började vi testa hastigheten och fick tyvärr otillfredsställande resultat: 5 000 transaktioner per sekund och upp till 50 000 transaktioner per block. Jag var tvungen att ta reda på vad som var felaktigt implementerat.

Till att börja med började vi testa kommunikationsmekanismen med Plasma Cash för att ta reda på systemets toppkapacitet. Vi skrev tidigare att Plasma Cash-noden tillhandahåller ett unix-socket-gränssnitt. Från början var det textbaserat. json-objekt skickades med `JSON.parse()` och `JSON.stringify()`.

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

Vi mätte överföringshastigheten för sådana föremål och hittade ~ 130k per sekund. Vi försökte ersätta standardfunktionerna för att arbeta med json, men prestandan förbättrades inte. V8-motorn måste vara väl optimerad för dessa operationer.

Vi arbetade med transaktioner, tokens och block genom klasser. När man skapade sådana klasser sjönk prestandan med 2 gånger, vilket indikerar att OOP inte är lämpligt för oss. Jag var tvungen att skriva om allt till ett rent funktionellt tillvägagångssätt.

Inspelning i databasen

Redis valdes initialt för datalagring som en av de mest produktiva lösningarna som uppfyller våra krav: nyckel-värdelagring, arbete med hashtabeller, set. Vi lanserade redis-benchmark och fick ~80 1 operationer per sekund i XNUMX pipelining-läge.

För hög prestanda har vi finjusterat Redis:

  • En anslutning för unix-uttag har upprättats.
  • Vi inaktiverade att spara tillståndet på disk (för tillförlitlighet kan du ställa in en replika och spara på disk i en separat Redis).

I Redis är en pool en hashtabell eftersom vi måste kunna hämta alla transaktioner i en fråga och radera transaktioner en efter en. Vi försökte använda en vanlig lista, men det går långsammare när man laddar ur hela listan.

Vid användning av standard NodeJS uppnådde Redis-biblioteken en prestanda på 18 9 transaktioner per sekund. Hastigheten sjönk XNUMX gånger.

Eftersom riktmärket visade oss att möjligheterna var klart 5 gånger större, började vi optimera. Vi ändrade biblioteket till ioredis och fick en prestanda på 25k per sekund. Vi lade till transaktioner en efter en med kommandot `hset`. Så vi genererade många frågor i Redis. Idén uppstod att kombinera transaktioner i batcher och skicka dem med ett kommando `hmset`. Resultatet är 32k per sekund.

Av flera anledningar, som vi kommer att beskriva nedan, arbetar vi med data med `Buffer` och, som det visar sig, om du konverterar den till text (`buffer.toString('hex')`) innan du skriver, kan du få ytterligare prestanda. Således höjdes hastigheten till 35k per sekund. För tillfället beslutade vi att avbryta ytterligare optimering.

Vi var tvungna att byta till ett binärt protokoll eftersom:

1. Systemet beräknar ofta hash, signaturer etc., och för detta behöver det data i `bufferten.

2. När den skickas mellan tjänster väger binär data mindre än text. Till exempel, när du skickar ett block med 1 miljon transaktioner kan data i texten ta upp mer än 300 megabyte.

3. Att ständigt omvandla data påverkar prestandan.

Därför tog vi som grund vårt eget binära protokoll för lagring och överföring av data, utvecklat på basis av det underbara "binära-data"-biblioteket.

Som ett resultat fick vi följande 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,
  }
  ```

— Token

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

-Blockera

  ```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 vanliga kommandona `BD.encode(block, Protocol).slice();` och `BD.decode(buffer, Protocol)` konverterar vi data till `Buffer` för att spara i Redis eller vidarebefordra till en annan nod och hämta data tillbaka.

Vi har också 2 binära protokoll för överföring av data mellan tjänster:

— Protokoll för interaktion med Plasma Node via unix-socket

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

där:

  • `Typ` — åtgärden som ska utföras, till exempel 1 — sendTransaction, 2 — getTransaction;
  • `nyttolast` — Uppgifter som måste skickas till lämplig funktion.
  • "meddelande-ID". — meddelande-id så att svaret kan identifieras.

— Protokoll för interaktion mellan 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)
  }
  ```

där:

  • `kod` — meddelandekod, till exempel 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • "versionProtocol". — Protokollversion, eftersom noder med olika versioner kan höjas på nätverket och de kan fungera annorlunda.
  • `seq` — meddelandeidentifierare.
  • `countChunk` и "chunkNumber". nödvändig för att dela upp stora meddelanden;
  • `längd` и `nyttolast` längd och själva data.

Eftersom vi förinmatade data är det slutliga systemet mycket snabbare än Ethereums `rlp`-bibliotek. Tyvärr har vi ännu inte kunnat vägra det, eftersom det är nödvändigt att slutföra det smarta kontraktet, vilket vi planerar att göra i framtiden.

Om vi ​​lyckades nå farten 35 000 transaktioner per sekund måste vi också bearbeta dem på optimal tid. Eftersom den ungefärliga blockbildningstiden tar 30 sekunder måste vi inkludera i blocket 1 000 000 transaktioner, vilket innebär att skicka fler 100 MB data.

Till en början använde vi "ethereumjs-devp2p"-biblioteket för att kommunicera mellan noder, men det kunde inte hantera så mycket data. Som ett resultat använde vi "ws"-biblioteket och konfigurerade att skicka binär data via websocket. Naturligtvis stötte vi också på problem när vi skickade stora datapaket, men vi delade upp dem i bitar och nu är dessa problem borta.

Bildar också ett Merkle-träd och beräknar hash 1 000 000 transaktioner kräver ca 10 sekunder av kontinuerlig beräkning. Under denna tid lyckas förbindelsen med alla noder bryta. Det beslutades att flytta denna beräkning till en separat tråd.

Slutsatser:

Faktum är att våra upptäckter inte är nya, men av någon anledning glömmer många experter bort dem när de utvecklar.

  • Att använda funktionell programmering istället för objektorienterad programmering förbättrar produktiviteten.
  • Monoliten är värre än en tjänstearkitektur för ett produktivt NodeJS-system.
  • Att använda `worker_threads` för tung beräkning förbättrar systemets lyhördhet, särskilt när man hanterar i/o-operationer.
  • unix-socket är stabilare och snabbare än http-förfrågningar.
  • Om du snabbt behöver överföra stora data över nätverket är det bättre att använda websockets och skicka binär data, uppdelad i bitar, som kan vidarebefordras om de inte kommer fram och sedan kombineras till ett meddelande.

Vi inbjuder dig att besöka GitHub projekt: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Artikeln skrevs tillsammans av Alexander Nashivan, senior utvecklare Clever Solution Inc.

Källa: will.com

Lägg en kommentar