Veřejné testování: řešení pro soukromí a škálovatelnost v Ethereum

Blockchain — inovativní technologie, která slibuje zlepšení mnoha oblastí lidského života. Přenáší skutečné procesy a produkty do digitálního prostoru, zajišťuje rychlost a spolehlivost finančních operací, snižuje jejich náklady a umožňuje vytvářet moderní DAPP aplikace s využitím chytrých kontraktů v decentralizovaných sítích.

Vzhledem k mnoha výhodám a rozmanitým oblastem použití blockchainu se může zdát divné, že tato slibná technologie ještě nepronikla do všech odvětví. Problém spočívá v tom, že moderním decentralizovaným blockchainům chybí škálovatelnost. Ethereum zpracovává přibližně 20 transakcí za sekundu, což není dostatečné pro uspokojení potřeb dnešního dynamického podnikání. Současně společnosti využívající technologii blockchain nechtějí opustit Ethereum kvůli jeho vysoké úrovni ochrany před hacknutím a selháním sítě.

Aby se zajistila decentralizace, bezpečnost a škálovatelnost v blockchainu, což řeší problém názvaný Škálovatelnost Trilemma, vývojářský tým Opporty vytvořil Plasma Cash — vedlejší řetězec složený z chytrého kontraktu a soukromé sítě založené na Node.js, která pravidelně přenáší svůj stav do hlavního řetězce (Ethereum).

Veřejné testování: řešení pro soukromí a škálovatelnost v Ethereum

Klíčové procesy v Plasma Cash

1. Uživatel vyvolá funkci chytrého kontraktu `deposit`, do které předá částku v ETH, kterou chce umístit do tokenu Plasma Cash. Funkce chytrého kontraktu vytvoří token a vygeneruje událost o tom.

2. Node Plasma Cash, které jsou přihlášeny k událostem chytrého kontraktu, obdrží událost o vytvoření vkladu a přidají do poolu transakci o vytvoření tokenu.

3. Pravidelně speciální nodové Plasma Cash berou všechny transakce z poolu (až 1 milion) a vytvářejí z nich blok, vypočítávají Merkleho strom a tím pádem hash. Tento blok je zaslán ostatním nodům k ověření. Nodové kontrolují, zda je hash Merkle platný, zda jsou transakce platné (např. zda je odesílatel tokenu jeho vlastníkem). Po ověření bloku nod vyvolá funkci `submitBlock` chytrého kontraktu, která uloží do hlavního řetězce číslo a hash Merkleho bloku. Chytrý kontrakt generuje událost o úspěšném přidání bloku. Transakce jsou odstraněny z poolu.

4. Uzly, které obdržely událost o odeslání bloku, začínají aplikovat transakce, které byly přidány do bloku.

5. V určitém okamžiku chce vlastník (nebo ne vlastník) tokenu vybrat jej z Plasma Cash. Za tímto účelem vyvolá funkci `startExit`, předá jí informace o posledních 2 transakcích týkajících se tokenu, které potvrzují, že právě on je vlastníkem tokenu. Chytrý kontrakt, pomocí Merkleho hash, ověřuje přítomnost transakcí v blocích a odesílá token k výběru, který se uskuteční za dva týdny.

6. Pokud došlo k porušení během operace výběru tokenu (token byl utracen po zahájení výběrového procesu nebo token byl před výběrem již cizí), může vlastník tokenu popřít výběr během dvou týdnů.

Veřejné testování: řešení pro soukromí a škálovatelnost v Ethereum

Soukromí je dosaženo dvěma způsoby

1. Kořenový řetězec nic neví o transakcích, které se formují a přenášejí uvnitř dceřiného řetězce. Veřejné zůstává informace o tom, kdo vložil a vybral ETH do/z Plasma Cash.

2. Dceřiný řetězec umožňuje organizovat anonymní transakce pomocí zk-SNARKs.

Technologický stack

  • NodeJS
  • Redis
  • Etherium
  • Soild

Testování

Při vývoji Plasma Cash jsme testovali rychlost systému a získali následující výsledky:

  • do 35 000 transakcí za sekundu se přidává do poolu;
  • do 1 000 000 transakcí může být uloženo v bloku.

Testy byly prováděny na 3 následujících serverech:

1. Intel Core i7-6700 Quad-Core Skylake včetně NVMe SSD — 512 GB, 64 GB DDR4 RAM
Byly vztyčeny 3 validující Plasma Cash uzly.

2. AMD Ryzen 7 1700X Octa-Core „Summit Ridge“ (Zen), SATA SSD — 500 GB, 64 GB DDR4 RAM
Byla vztyčena Ropsten testnet ETH uzel.
Byly vztyčeny 3 validující Plasma Cash uzly.

3. Intel Core i9-9900K Octa-Core včetně NVMe SSD — 1 TB, 64 GB DDR4 RAM
Byl vztyčen 1 odesílací Plasma Cash uzel.
Byly vztyčeny 3 validující Plasma Cash uzly.
Byl spuštěn test na přidání transakcí do Plasma Cash sítě.

Celkem: 10 Plasma Cash uzlů v soukromé síti.

Test 1

Existuje limit na 1 milion transakcí v bloku. Proto 1 milion transakcí se dostane do 2 bloků (protože systém má čas vzít část transakcí a odeslat zatímco jsou odesílány).

Přehrát video

Počáteční stav: poslední blok #7; v databázi je uloženo 1 milion transakcí a tokenů.

00:00 — spuštění skriptu pro generování transakcí
01:37 — vytvořeno 1 milion transakcí a začalo odesílání do uzlu
01:46 — odesílací uzel vzal z poolu 240k transakcí a formuje blok #8. Také vidíme, že do poolu se během 10 sekund přidalo 320k transakcí.
01:58 — blok #8 je podepsán a odeslán na validaci.
02:03 — blok #8 byl validován a byla vyvolána funkce `submitBlock` chytrého kontraktu s hash Merkle a číslem bloku
02:10 — demo skript přestal běžet, který odeslal 1 milion transakcí za 32 sekund
02:33 — uzly začaly přijímat informace o tom, že blok #8 byl přidán do kořenového řetězce, a začaly provádět 240 tisíc transakcí
02:40 — z poolu bylo odstraněno 240 tisíc transakcí, které jsou již v bloku #8
02:56 — submit uzel vzal z poolu zbývajících 760 tisíc transakcí a začal počítat hash Merkle a podepisovat blok #9
03:20 — všechny uzly obsahují 1 milion 240 tisíc transakcí a tokenů
03:35 — blok #9 byl podepsán a odeslán na validaci k ostatním uzlům
03:41 — došlo k chybě sítě
04:40 — z důvodu timeoutu bylo zastaveno čekání na validaci bloku #9
04:54 — submit uzel vzal z poolu zbývajících 760 tisíc transakcí a začal počítat hash Merkle a podepisovat blok #9
05:32 — blok #9 byl podepsán a odeslán na validaci k ostatním uzlům
05:53 — blok #9 byl validován a odeslán do kořenového řetězce
06:17 — uzly začaly přijímat informace o tom, že blok #9 byl přidán do kořenového řetězce a začaly provádět 760 tisíc transakcí
06:47 — pool byl vyčištěn od transakcí, které jsou v bloku #9
09:06 — všechny uzly obsahují 2 miliony transakcí a tokenů

Test 2

Existuje limit 350 tisíc na blok. V důsledku máme 3 bloky.

Přehrát video

Výchozí stav: poslední blok #9; v databázi je uloženo 2 miliony transakcí a tokenů

00:00 — skript na generování transakcí již běží
00:44 — bylo vytvořeno 1 milion transakcí a začalo odesílání do uzlu
00:56 — submit uzel vzal z poolu 320 tisíc transakcí a formuje blok #10. Také vidíme, že do poolu je přidáno 320 tisíc transakcí za 10 sekund
01:12 — blok #10 byl podepsán a odeslán k ostatním uzlům na validaci
01:18 — demo skript přestal běžet, který odeslal 1 milion transakcí za 34 sekund
01:20 — blok #10 byl validován a odeslán do kořenového řetězce
01:51 — všechny uzly obdržely z kořenového řetězce informace o tom, že blok #10 byl přidán, a začínají aplikovat 320 tisíc transakcí
02:01 — pool byl vyčištěn na 320 tisíc transakcí, které byly přidány do bloku #10
02:15 — submit uzel vzal z poolu 350 tisíc transakcí a formuje blok #11
02:34 — blok #11 byl podepsán a odeslán ostatním uzlům na validaci
02:51 — blok #11 byl validován a odeslán do kořenového řetězce
02:55 — poslední uzel provedl transakce z bloku #10
10:59 — velmi dlouhá transakce s odesláním bloku #9 byla vykonána v kořenovém řetězci, ale byla úspěšně dokončena a všechny uzly byly informovány a začaly vykonávat 350k transakcí.
11:05 — pool se vyčistil o 320k transakcí, které byly přidány do bloku #11.
12:10 — všechny uzly obsahují 1 milion 670k transakcí a tokenů.
12:17 — submit uzel vzal z poolu 330k transakcí a vytváří blok #12.
12:32 — blok #12 byl podepsán a odeslán ostatním uzlům k validaci.
12:39 — blok #12 byl validován a odeslán do kořenového řetězce.
13:44 — všechny uzly obdržely z kořenového řetězce informaci, že blok #12 byl přidán, a začínají aplikovat 330k transakcí.
14:50 — všechny uzly obsahují 2 miliony transakcí a tokenů.

Test 3

Na prvním a druhém serveru byla jedna validační uzel nahrazena submit uzlem.

Přehrát video

Výchozí stav: poslední blok #84; v databázi uloženo 0 transakcí a tokenů.

00:00 — Spuštěny 3 skripty, které generují a odesílají po 1 milionu transakcí.
01:38 — vytvořeno 1 milion transakcí a začalo se odesílání do submit uzlu #3.
01:50 — submit uzel #3 vzal z poolu 330k transakcí a vytváří blok #85 (f21). Také vidíme, že do poolu přibylo 350k transakcí za 10 sek.
01:53 — vytvořeno 1 milion transakcí a začalo se odesílání do submit uzlu #1.
01:50 — submit uzel #3 vzal z poolu 330k transakcí a vytváří blok #85 (f21). Také vidíme, že do poolu přibylo 350k transakcí za 10 sek.
02:01 — submit uzel #1 vzal z poolu 250k transakcí a vytváří blok #85 (65e).
02:06 — blok #85 (f21) byl podepsán a odeslán ostatním uzlům k validaci.
02:08 — demo skript serveru #3 přestal pracovat, který odeslal 1 milion transakcí za 30 sekund.
02:14 — blok #85 (f21) byl validován a odeslán do kořenového řetězce.
02:19 — blok #85 (65e) byl podepsán a odeslán ostatním uzlům k validaci.
02:22 — vytvořeno 1 milion transakcí a začalo se odesílání do submit uzlu #2.
02:27 — blok #85 (65e) byl validován a odeslán do kořenového řetězce.
02:29 — submit uzel #2 vzal z poolu 111855 transakcí a vytváří blok #85 (256).
02:36 — blok #85 (256) byl podepsán a odeslán ostatním uzlům k validaci.
02:36 — demo skript serveru #1 přestal pracovat, který odeslal 1 milion transakcí za 42.5 sekund.
02:38 — blok #85 (256) byl validován a odeslán do kořenového řetězce.
03:08 — demo skript serveru #2 přestal pracovat, který odeslal 1 milion transakcí za 47 sek.
03:38 — všechny uzly obdržely z kořenového řetězce informaci, že bloky #85 (f21), #86 (65e), #87 (256) byly přidány a začínají aplikovat 330k, 250k, 111855 transakcí.
03:49 — pool se vyčistil o 330k, 250k, 111855 transakcí, které byly přidány do bloků #85 (f21), #86(65e), #87(256)
03:59 — submit uzel #1 vzal z poolu 888145 transakcí a vytváří blok #88 (214), submit uzel #2 vzal z poolu 750k transakcí a vytváří blok #88 (50a), submit uzel #3 vzal z poolu 670k transakcí a vytváří blok #88 (d3b)
04:44 — blok #88 (d3b) byl podepsán a odeslán ostatním uzlům k validaci
04:58 — blok #88 (214) byl podepsán a odeslán ostatním uzlům k validaci
05:11 — blok #88 (50a) byl podepsán a odeslán ostatním uzlům k validaci
05:11 — blok #85 (d3b) byl validován a odeslán do kořenového řetězce
05:36 — blok #85 (214) byl validován a odeslán do kořenového řetězce
05:43 — všechny uzly obdržely z kořenového řetězce informace, že bloky #88 (d3b), #89(214) byly přidány a začínají aplikovat 670k, 750k transakcí
06:50 — kvůli přerušení spojení blok #85 (50a) nebyl validován
06:55 — submit uzel #2 vzal z poolu 888145 transakcí a vytváří blok #90 (50a)
08:14 — blok #90 (50a) byl podepsán a odeslán ostatním uzlům k validaci
09:04 — blok #90 (50a) byl validován a odeslán do kořenového řetězce
11:23 — všechny uzly obdržely z kořenového řetězce informace, že blok #90 (50a) byl přidán, a začínají aplikovat 888145 transakcí. Mezitím server #3 již dávno aplikoval transakce z bloků #88 (d3b), #89(214)
12:11 — všechny pooly jsou prázdné
13:41 — všechny uzly serveru #3 obsahují 3 miliony transakcí a tokenů
14:35 — všechny uzly serveru #1 obsahují 3 miliony transakcí a tokenů
19:24 — všechny uzly serveru #2 obsahují 3 miliony transakcí a tokenů

Překážky

Během vývoje Plasma Cash jsme se setkali s následujícími problémy, které jsme postupně řešili a řešíme:

1. Konflikt interakce různých funkcí systému. Například funkce přidání transakcí do poolu blokovala práci submitu a validace bloků, a naopak, což vedlo k poklesu rychlosti.

2. Nebyla hned jasná cesta, jak odeslat obrovské množství transakcí a zároveň minimalizovat náklady na přenos dat.

3. Nebyla jasná cesta a místo, kde ukládat data, abychom dosáhli vysokých výsledků.

4. Nebyla jasná cesta, jak organizovat síť mezi uzly, protože velikost bloku s 1 milionem transakcí zabírá přibližně 100 MB.

5. Práce v jednovláknovém režimu přerušuje spojení mezi uzly, když probíhají dlouhé výpočty (např. sestavení Merkleho stromu a výpočet jeho hash).

Jak jsme si s tím vším poradili?

První verze Plasma Cash uzlu představovala jakýsi kombajn, který mohl dělat všechno najednou: přijímat transakce, sbírat a validovat bloky, poskytoval API pro přístup k datům. Jelikož NodeJS byl původně jednovláknový, složitá funkce výpočtu Merkleho stromu zablokovala přidávání transakce. Viděli jsme dvě možnosti řešení tohoto problému:

1. Spustit několik procesů NodeJS, z nichž každý vykonává určité funkce.

2. Použít worker_threads a přesunout část kódu do vláken.

Nakonec jsme využili obě možnosti současně: logicky jsme rozdělili jeden uzel na 3 části, které mohou fungovat samostatně, ale zároveň synchronně.

1. Uzel pro sběr, který přijímá transakce do poolu a zabývá se vytvářením bloků.

2. Validující uzel, který kontroluje platnost uzlů.

3. API uzel — poskytuje API pro přístup k datům.

Na každém uzlu se lze připojit přes unix socket pomocí cli.

Těžké operace, jako je výpočet Merkleho stromu, jsme přesunuli do samostatného vlákna.

Tímto způsobem jsme dosáhli normálního fungování všech funkcí Plasma Cash současně a bez výpadků.

Jakmile systém začal funkčně fungovat, začali jsme testovat rychlost a bohužel jsme získali neuspokojivé výsledky: 5 000 transakcí za sekundu a až 50 000 transakcí v bloku. Museli jsme zjistit, co bylo implementováno nesprávně.

Nejprve jsme začali testovat mechanismus komunikace s Plasma Cash, abychom zjistili vrcholné možnosti systému. Dříve jsme psali, že Plasma Cash uzel poskytuje unix socket rozhraní. Původně bylo textové. JSON objekty byly posílány pomocí `JSON.parse()` a `JSON.stringify()`.

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

Změřili jsme rychlost přenosu takových objektů a dostali jsme ~ 130k za sekundu. Zkoušeli jsme nahradit standardní funkce pro práci s json, ale výkon se nezvýšil. Musí být engine V8 dobře optimalizován na tyto operace.

Práce s transakcemi, tokeny a bloky u nás probíhala prostřednictvím tříd. Při vytváření těchto tříd poklesla výkon na polovinu, což naznačuje, že OOP pro nás není vhodné. Museli jsme vše přepsat na čistě funkcionální přístup.

Zápis do databáze

Původně byla pro ukládání dat zvolena služba Redis jako jedno z nejvýkonnějších řešení, které splňuje naše požadavky: key-value úložiště, práce s hash tabulkami, množinami. Spustili jsme redis-benchmark a získali ~80k operací za sekundu v režimu 1 pipelining.

Pro vysoký výkon jsme Redis nastavili více precizně:

  • Nastavili jsme unix socket připojení.
  • Vypnuli jsme ukládání do souboru (pro spolehlivost lze nakonfigurovat replikaci a v jiném Redis provádět ukládání na disk).

V Redis je pool - to je hash tabulka, protože potřebujeme možnost získat všechny transakce jedním dotazem a odstranit transakce jednotlivě. Zkoušeli jsme použít obyčejný seznam, ale při vykládání celého seznamu funguje pomaleji.

Při použití standardní knihovny Redis pro NodeJS jsme dosáhli výkonu 18k transakcí za sekundu. Rychlost klesla 9krát.

Protože benchmark jasně ukazoval možnosti 5krát vyšší, začali jsme optimalizovat. Změnili jsme knihovnu na ioredis a dosáhli jsme výkonu už 25k za sekundu. Transakce jsme přidávali jednotlivě pomocí příkazu `hset`. Takto jsme generovali mnoho dotazů do Redis. Napadlo nás spojit transakce do balíčků a posílat je jedním příkazem `hmset`. Výsledek — 32k za sekundu.

Z několika důvodů, které popíšeme níže, pracujeme s daty pomocí `Buffer` a ukázalo se, že pokud je převedeme na text (`buffer.toString('hex')`) před zápisem, můžeme získat dodatečný výkon. Tímto způsobem se nám podařilo zvýšit rychlost na 35k za sekundu. V tuto chvíli jsme se rozhodli přestat s dalším optimalizováním.

Museli jsme přejít na binární protokol, protože:

1. Systém často počítá hashe, podpisy atd., a k tomu potřebuje data v `Buffer`.

2. Při přenosu mezi službami mají binární data menší hmotnost než text. Například při odesílání bloku s 1 milionem transakcí mohou data v textu zabírat více než 300 megabajtů.

3. Neustálá konverze dat ovlivňuje výkon.

Proto jsme vycházeli z vlastního binárního protokolu pro ukládání a přenos dat, který byl vyvinut na základě vynikající knihovny `binary-data`.

Díky tomu jsme vytvořili následující datové struktury:

— Transakce

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

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

Obvyklými příkazy `BD.encode(block, Protocol).slice();` a ` BD.decode(buffer, Protocol)` převádíme data na `Buffer` pro uložení do Redis nebo odeslání jiné nódě a zpětné získání dat.

Také máme 2 binární protokoly pro přenos dat mezi službami:

— Protokol pro komunikaci s Plasma Node pomocí unix socketu

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

kde:

  • `type` — akce, kterou je třeba provést, například 1 — sendTransaction, 2 — getTransaction;
  • `payload` — data, která je třeba předat odpovídající funkci;
  • `messageId` — id zprávy, abychom mohli identifikovat odpověď.

— Protokol pro komunikaci mezi nódami

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

kde:

  • `code` — kód zprávy, například 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — verze protokolu, protože v síti mohou běžet nódové s různými verzemi a mohou pracovat odlišně;
  • `seq` — identifikátor zprávy;
  • `countChunk` a `chunkNumber` nepostradatelné pro rozdělení velkých zpráv;
  • `length` a `payload` délka a samotná data.

Protože jsme data předem typizovali, konečný systém funguje mnohem rychleji než knihovna `rlp` od Etherea. Bohužel se nám zatím nepodařilo se jí vzdát, protože je potřeba dokončit smart kontrakt, což plánujeme udělat v budoucnosti.

Pokud se nám podařilo dosáhnout rychlosti 35 000 transakcí za sekundu, musíme je také zpracovávat v optimálním čase. Jelikož přibližná doba pro vytvoření bloku trvá 30 sekund, musíme zahrnout do bloku 1 000 000 transakcí, což znamená přenos více než 100 MB dat.

Původně jsme používali knihovnu `ethereumjs-devp2p` pro propojení uzlů, ale ta si s takovým množstvím dat neporadila. V důsledku toho jsme přešli na knihovnu `ws` a nastavili přenos binárních dat pomocí websocketů. Samozřejmě jsme také narazili na problémy s přenosem velkých datových balíků, ale rozdělili jsme je na menší části a nyní již tyto problémy nemáme.

Také vytvoření Merkleova stromu a výpočet hash 1 000 000 transakcí vyžaduje asi 10 sekund nepřetržitého výpočtu. Během této doby se spojení se všemi uzly přeruší. Bylo rozhodnuto přesunout tento výpočet do samostatného vlákna.

Závěry:

Ve skutečnosti naše zjištění nejsou nová, ale z nějakého důvodu na ně mnoho specialistů zapomíná při vývoji.

  • Použití funkcionálního programování místo objektového programování zvyšuje výkon.
  • Monolit je horší než servisní architektura pro výkonný systém na NodeJS.
  • Použití `worker_threads` pro náročné výpočty zlepšuje odezvu systému, zejména při práci s operacemi i/o.
  • unix socket je stabilnější a rychlejší než http požadavky.
  • Pokud je potřeba rychle přenášet velká data po síti, je lepší použít websockety a posílat binární data rozdělená na části, které lze znovu odeslat, pokud nedorazí, a poté je sloučit do jedné zprávy.

Zveme vás k návštěvě GitHub projektu: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Článek byl napsán ve spolupráci s Alexandrem Našivanem, vedoucím vývojářem Clever Solution Inc.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster