Publiczny test: rozwiązanie dla prywatności i skalowalności w Ethereum

Blockchain — innowacyjna technologia, obiecująca poprawę wielu dziedzin życia ludzkiego. Przenosi rzeczywiste procesy i produkty do przestrzeni cyfrowej, zapewniając szybkość i niezawodność operacji finansowych, obniżając ich koszt, a także pozwala na tworzenie nowoczesnych aplikacji DAPP przy użyciu inteligentnych kontraktów w zdecentralizowanych sieciach.

Biorąc pod uwagę liczne zalety i różnorodność zastosowań blockchaina, może wydawać się dziwne, że ta obiecująca technologia jeszcze nie przeniknęła do wszystkich branż. Problemem jest to, że nowoczesne zdecentralizowane blockchainy brakuje skalowalności. Ethereum obsługuje około 20 transakcji na sekundę, co jest niewystarczające, aby zaspokoić potrzeby dynamicznego współczesnego biznesu. Jednocześnie firmy korzystające z technologii blockchain nie decydują się na rezygnację z Ethereum ze względu na jego wysoki poziom ochrony przed włamaniami i awariami sieci.

Aby zapewnić decentralizację, bezpieczeństwo i skalowalność w blockchainie, rozwiązując w ten sposób Trilemmę Skalowalności, zespół programistów Opporty stworzył Plasma Cash — podrzędny łańcuch składający się z inteligentnego kontraktu oraz prywatnej sieci opartej na Node.js, która okresowo przekazuje swoje stany do łańcucha głównego (Ethereum).

Publiczny test: rozwiązanie dla prywatności i skalowalności w Ethereum

Kluczowe procesy w Plasma Cash

1. Użytkownik wywołuje funkcję inteligentnego kontraktu `deposit`, przekazując do niej kwotę w ETH, którą chce wpłacić do tokena Plasma Cash. Funkcja inteligentnego kontraktu tworzy token i generuje zdarzenie o tym.

2. Węzły Plasma Cash, subskrybowane na zdarzenia inteligentnego kontraktu, otrzymują zdarzenie o utworzeniu depozytu i dodają do puli transakcję o utworzeniu tokena.

3. Okresowo specjalne węzły Plasma Cash biorą wszystkie transakcje z puli (do 1 miliona) i formują z nich blok, obliczają drzewo Merkle i odpowiednio hash. Blok ten jest wysyłany do innych węzłów do weryfikacji. Węzły sprawdzają, czy hash Merkle jest ważny, czy transakcje są ważne (na przykład, czy nadawca tokena jest jego właścicielem). Po weryfikacji bloku węzeł wywołuje funkcję `submitBlock` inteligentnego kontraktu, która zapisuje w łańcuchu głównym numer i hash bloku Merkle. Inteligentny kontrakt generuje zdarzenie o pomyślnym dodaniu bloku. Transakcje są usuwane z puli.

4. Węzły, które otrzymały zdarzenie o submicie bloku, zaczynają stosować transakcje, które zostały dodane do bloku.

5. W pewnym momencie właściciel (lub nie właściciel) tokena chce go wycofać z Plasma Cash. W tym celu wywołuje funkcję `startExit`, przekazując jej informacje o ostatnich 2 transakcjach dotyczących tokena, które potwierdzają, że faktycznie jest on właścicielem tokena. Smart kontrakt, używając hasha Merkle'a, sprawdza obecność transakcji w blokach i wysyła token do wycofania, które nastąpi za dwa tygodnie.

6. Jeżeli operacja wycofania tokena miała miejsce z naruszeniami (token został wydany po rozpoczęciu procedury wycofania lub token przed wycofaniem już należał do kogoś innego), właściciel tokena może obalić wycofanie w ciągu dwóch tygodni.

Publiczny test: rozwiązanie dla prywatności i skalowalności w Ethereum

Prywatność osiąga się na dwa sposoby.

1. Główna sieć nie wie nic o transakcjach, które są formowane i przesyłane wewnątrz podrzędnej sieci. Publiczne pozostaje jedynie informacje o tym, kto wprowadził i wycofał ETH z/do Plasma Cash.

2. Podrzędna sieć umożliwia organizowanie anonimowych transakcji, korzystając z zk-SNARKs.

Stos technologiczny

  • NodeJS
  • Redis
  • Etherium
  • Soild

Testowanie

Podczas opracowywania Plasma Cash przetestowaliśmy szybkość działania systemu i uzyskaliśmy następujące wyniki:

  • do 35 000 transakcji na sekundę dodawanych do puli;
  • do 1 000 000 transakcji może być przechowywanych w bloku.

Testy przeprowadzono na 3 następujących serwerach:

1. Intel Core i7-6700 Quad-Core Skylake z NVMe SSD — 512 GB, 64 GB DDR4 RAM
Zostały uruchomione 3 walidujące węzły Plasma Cash.

2. AMD Ryzen 7 1700X Octa-Core «Summit Ridge» (Zen), SATA SSD — 500 GB, 64 GB DDR4 RAM
Uruchomiono węzeł ETH w testnet Ropsten.
Zostały uruchomione 3 walidujące węzły Plasma Cash.

3. Intel Core i9-9900K Octa-Core z NVMe SSD — 1 TB, 64 GB DDR4 RAM
Uruchomiono 1 węzeł submittujący Plasma Cash.
Zostały uruchomione 3 walidujące węzły Plasma Cash.
Testowano dodawanie transakcji do sieci Plasma Cash.

Podsumowując: 10 węzłów Plasma Cash w prywatnej sieci.

Test 1

Jest limit na 1 milion transakcji w bloku. Dlatego 1 milion transakcji trafia do 2 bloków (ponieważ system ma czas na zabranie części transakcji i ich submittowanie, podczas gdy są one wysyłane).

Odtwarzaj wideo

Stan początkowy: ostatni blok #7; w bazie zapisano 1 mln transakcji i tokenów.

00:00 — uruchomienie skryptu generującego transakcje
01:37 — utworzono 1 mln transakcji i rozpoczęto wysyłanie do węzła
01:46 — węzeł submittujący wziął z puli 240k transakcji i formuje blok #8. Widzimy również, że do puli dodano 320k transakcji w ciągu 10 sekund.
01:58 — blok #8 podpisany i wysłany do walidacji.
02:03 — blok #8 został zwalidowany i wywołana została funkcja `submitBlock` smart kontraktu z hashem Merkle i numerem bloku
02:10 — zakończono działanie skryptu demo, który wysłał 1 mln transakcji w 32 sekundy
02:33 — węzły zaczęły otrzymywać informacje, że blok #8 został dodany do głównego łańcucha i zaczęły realizować 240k transakcji
02:40 — z puli usunięto 240k transakcji, które już znajdują się w bloku #8
02:56 — węzeł submit pobrał z puli pozostałe 760k transakcji i rozpoczął obliczanie hasha Merkle oraz podpisywanie bloku #9
03:20 — wszystkie węzły zawierają 1 mln 240k transakcji i tokenów
03:35 — blok #9 został podpisany i wysyłany do walidacji do innych węzłów
03:41 — wystąpił błąd sieci
04:40 — wyczekiwano walidacji bloku #9, której czas oczekiwania wygasł
04:54 — węzeł submit pobrał z puli pozostałe 760k transakcji i rozpoczął obliczanie hasha Merkle oraz podpisywanie bloku #9
05:32 — blok #9 został podpisany i wysyłany do walidacji do innych węzłów
05:53 — blok #9 został zwalidowany i wysłany do głównego łańcucha
06:17 — węzły zaczęły otrzymywać informacje, że blok #9 został dodany do głównego łańcucha i zaczęły realizować 760k transakcji
06:47 — pula została oczyszczona z transakcji, które stanowiły blok #9
09:06 — wszystkie węzły zawierają 2 mln transakcji i tokenów

Test 2

Ustalono limit na 350k na blok. W rezultacie mamy 3 bloki.

Odtwarzaj wideo

Stan początkowy: ostatni blok #9; w bazie zapisano 2 mln transakcji i tokenów

00:00 — skrypt generowania transakcji został już uruchomiony
00:44 — stworzono 1 mln transakcji i rozpoczęto ich wysyłanie do węzła
00:56 — węzeł submit pobrał z puli 320k transakcji i formuje blok #10. Również widzimy, że do puli dodano 320k transakcji w ciągu 10 sekundy
01:12 — blok #10 został podpisany i wysyłany do innych węzłów do walidacji
01:18 — zakończono działanie skryptu demo, który wysłał 1 mln transakcji w 34 sekundy
01:20 — blok #10 został zwalidowany i wysłany do głównego łańcucha
01:51 — wszystkie węzły otrzymały z głównego łańcucha informację, że blok #10 został dodany i zaczynają realizować 320k transakcji
02:01 — pula została oczyszczona o 320k transakcji, które zostały dodane do bloku #10
02:15 — węzeł submit pobrał z puli 350k transakcji i formuje blok #11
02:34 — blok #11 został podpisany i wysyłany do innych węzłów do walidacji
02:51 — blok #11 został zwalidowany i wysłany do głównego łańcucha
02:55 — ostatni węzeł wykonał transakcje z bloku #10
10:59 — transakcja z dodaniem bloku #9 trwała bardzo długo w łańcuchu głównym, ale zakończyła się pomyślnie, a wszystkie węzły otrzymały o tym informację i zaczęły realizować 350 tys. transakcji.
11:05 — pula została oczyszczona o 320 tys. transakcji, które zostały dodane do bloku #11.
12:10 — wszystkie węzły zawierają 1 mln 670 tys. transakcji i tokenów.
12:17 — węzeł zgłoszenia pobrał z puli 330 tys. transakcji i formuje blok #12.
12:32 — blok #12 został podpisany i wysłany do innych węzłów do weryfikacji.
12:39 — blok #12 został zweryfikowany i wysłany do łańcucha głównego.
13:44 — wszystkie węzły otrzymały z łańcucha głównego informację o tym, że blok #12 został dodany i zaczynają stosować 330 tys. transakcji.
14:50 — wszystkie węzły zawierają 2 mln transakcji i tokenów.

Test 3

Na pierwszym i drugim serwerze jedna weryfikująca węzeł została zastąpiona węzłem zgłoszenia.

Odtwarzaj wideo

Stan początkowy: ostatni blok #84; w bazie zapisano 0 transakcji i tokenów.

00:00 — Uruchomiono 3 skrypty, które generują i wysyłają po 1 mln transakcji.
01:38 — stworzono 1 mln transakcji i rozpoczęto przesyłanie do węzła zgłoszenia #3.
01:50 — węzeł zgłoszenia #3 pobrał z puli 330 tys. transakcji i formuje blok #85 (f21). Widzimy również, że do puli dodano 350 tys. transakcji w ciągu 10 sek.
01:53 — stworzono 1 mln transakcji i rozpoczęto przesyłanie do węzła zgłoszenia #1.
01:50 — węzeł zgłoszenia #3 pobrał z puli 330 tys. transakcji i formuje blok #85 (f21). Widzimy również, że do puli dodano 350 tys. transakcji w ciągu 10 sek.
02:01 — węzeł zgłoszenia #1 pobrał z puli 250 tys. transakcji i formuje blok #85 (65e).
02:06 — blok #85 (f21) został podpisany i wysłany do innych węzłów do weryfikacji.
02:08 — zakończył pracę skrypt-demo serwera #3, który przesłał 1 mln transakcji w ciągu 30 sekund.
02:14 — blok #85 (f21) został zweryfikowany i wysłany do łańcucha głównego.
02:19 — blok #85 (65e) został podpisany i wysłany do innych węzłów do weryfikacji.
02:22 — stworzono 1 mln transakcji i rozpoczęto przesyłanie do węzła zgłoszenia #2.
02:27 — blok #85 (65e) został zweryfikowany i wysłany do łańcucha głównego.
02:29 — węzeł zgłoszenia #2 pobrał z puli 111855 transakcji i formuje blok #85 (256).
02:36 — blok #85 (256) został podpisany i wysłany do innych węzłów do weryfikacji.
02:36 — zakończył pracę skrypt-demo serwera #1, który przesłał 1 mln transakcji w ciągu 42,5 sekundy.
02:38 — blok #85 (256) został zweryfikowany i wysłany do łańcucha głównego.
03:08 — zakończył pracę skrypt-demo serwera #2, który przesłał 1 mln transakcji w ciągu 47 sek.
03:38 — wszystkie węzły otrzymały z łańcucha głównego informację o tym, że bloki #85 (f21), #86(65e), #87(256) zostały dodane i rozpoczynają stosowanie 330 tys., 250 tys., 111855 transakcji.
03:49 — pula oczyściła się o 330k, 250k, 111855 transakcji, które zostały dodane do bloków #85 (f21), #86(65e), #87(256)
03:59 — zgłoszenie węzła #1 wzięło z puli 888145 transakcji i formuje blok #88 (214), zgłoszenie węzła #2 wzięło z puli 750k transakcji i formuje blok #88 (50a), zgłoszenie węzła #3 wzięło z puli 670k transakcji i formuje blok #88 (d3b)
04:44 — blok #88 (d3b) został podpisany i wysłany do innych węzłów do walidacji
04:58 — blok #88 (214) został podpisany i wysłany do innych węzłów do walidacji
05:11 — blok #88 (50a) został podpisany i wysłany do innych węzłów do walidacji
05:11 — blok #85 (d3b) został zwalidowany i wysłany do głównego łańcucha
05:36 — blok #85 (214) został zwalidowany i wysłany do głównego łańcucha
05:43 — wszystkie węzły otrzymały z głównego łańcucha informację, że bloki #88 (d3b), #89(214) zostały dodane i zaczynają stosować 670k, 750k transakcji
06:50 — z powodu przerwania połączenia blok #85 (50a) nie został zwalidowany
06:55 — zgłoszenie węzła #2 wzięło z puli 888145 transakcji i formuje blok #90 (50a)
08:14 — blok #90 (50a) został podpisany i wysłany do innych węzłów do walidacji
09:04 — blok #90 (50a) został zwalidowany i wysłany do głównego łańcucha
11:23 — wszystkie węzły otrzymały z głównego łańcucha informację, że blok #90 (50a) został dodany, i zaczynają stosować 888145 transakcji. W międzyczasie serwer #3 już zastosował transakcje z bloków #88 (d3b), #89(214)
12:11 — wszystkie pule są puste
13:41 — wszystkie węzły serwera #3 zawierają 3 miliony transakcji i tokenów
14:35 — wszystkie węzły serwera #1 zawierają 3 miliony transakcji i tokenów
19:24 — wszystkie węzły serwera #2 zawierają 3 miliony transakcji i tokenów

Przeszkody

Podczas rozwoju Plasma Cash napotkaliśmy następujące problemy, które stopniowo rozwiązaliśmy i rozwiązujemy:

1. Konflikt interakcji różnych funkcji systemu. Na przykład, funkcja dodawania transakcji do puli blokowała działanie zgłoszeń i walidacji bloków, i odwrotnie, co prowadziło do spadku prędkości.

2. Nie było od razu jasne, w jaki sposób wysłać ogromną ilość transakcji i jednocześnie zminimalizować koszty przesyłania danych.

3. Nie było jasno określone, w jaki sposób i gdzie przechowywać dane, aby osiągnąć wysokie wyniki.

4. Nie było jasne, jak zorganizować sieć między węzłami, ponieważ rozmiar bloku z 1 milionem transakcji zajmuje około 100 MB.

5. Praca w trybie jednowątkowym przerywa połączenie między węzłami podczas długich obliczeń (na przykład budowa drzewa Merkle i obliczanie jego hasza).

Jak sobie z tym wszystkim poradziliśmy?

Pierwsza wersja węzła Plasma Cash była pewnym kombajnem, który mógł wykonywać wszystko jednocześnie: przyjmować transakcje, przesyłać i walidować bloki, a także udostępniać API do dostępu do danych. Ponieważ NodeJS jest z założenia jednowątkowy, ciężka funkcja obliczania drzewa Merkle blokowała funkcję dodawania transakcji. Zobaczyliśmy dwa sposoby na rozwiązanie tego problemu:

1. Uruchomić kilka procesów NodeJS, z których każdy wykonuje określone funkcje.

2. Zastosować worker_threads i przenieść wykonanie części kodu do wątków.

Ostatecznie skorzystaliśmy z obu opcji jednocześnie: logicznie podzieliliśmy jeden węzeł na 3 części, które mogą działać oddzielnie, ale jednocześnie synchronicznie.

1. Węzeł do przesyłania, który przyjmuje transakcje do puli i zajmuje się tworzeniem bloków.

2. Węzeł walidujący, który sprawdza ważność innych węzłów.

3. Węzeł API — udostępnia API do dostępu do danych.

Do każdego węzła można podłączyć się przez unix socket za pomocą cli.

Ciężkie operacje, takie jak obliczanie drzewa Merkle, przenieśliśmy do osobnego wątku.

W ten sposób osiągnęliśmy normalne działanie wszystkich funkcji Plasma Cash jednocześnie i bez zakłóceń.

Gdy system funkcjonalnie zadziałał, zaczęliśmy testować szybkość i, niestety, uzyskaliśmy niezadowalające wyniki: 5 000 transakcji na sekundę i do 50 000 transakcji w bloku. Musieliśmy dowiedzieć się, co zostało zaimplementowane niepoprawnie.

Na początku zaczęliśmy testować mechanizm komunikacji z Plasma Cash, aby zobaczyć maksymalne możliwości systemu. Wcześniej pisaliśmy, że węzeł Plasma Cash udostępnia interfejs unix socket. Początkowo był on tekstowy. Obiekty JSON były przesyłane, korzystając z `JSON.parse()` i `JSON.stringify()`.

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

Zmierzono prędkość przesyłania takich obiektów i uzyskano ~ 130k na sekundę. Próbowaliśmy zastąpić standardowe funkcje pracy z json, ale wydajność się nie poprawiła. Musi być, że silnik V8 jest dobrze zoptymalizowany do tych operacji.

Praca z transakcjami, tokenami i blokami odbywała się przez klasy. Podczas tworzenia takich klas wydajność spadła dwukrotnie, co świadczy o tym, że OOP nam nie odpowiada. Musieliśmy przepisać wszystko na czysto funkcyjny sposób.

Zapis do bazy

Początkowo wybrano Redis jako jedno z najbardziej wydajnych rozwiązań, które spełnia nasze wymagania: przechowywanie w formacie key-value, praca z tabelami haszującymi i zbiorami. Uruchomiliśmy redis-benchmark i uzyskaliśmy ~80k operacji na sekundę w trybie 1 pipelining.

Aby osiągnąć wysoką wydajność, skonfigurowaliśmy Redis bardziej precyzyjnie:

  • Ustawiliśmy połączenie unix socket.
  • Wyłączyliśmy zapisywanie stanu na dysk (dla niezawodności można skonfigurować replikację i już w osobnym Redisie robić zapisywanie na dysk).

W Redisie pula to tabela haszująca, ponieważ potrzebujemy możliwości pobierania wszystkich transakcji jednym zapytaniem oraz usuwania transakcji pojedynczo. Próbowaliśmy używać zwykłej listy, ale działała wolniej podczas ładowania całej listy.

Przy użyciu standardowej biblioteki NodeJS Redis uzyskano wydajność na poziomie 18k transakcji na sekundę. Prędkość spadła dziewięciokrotnie.

Ponieważ benchmark wskazywał, że możliwości są wyraźnie pięciokrotnie większe, zaczęliśmy optymalizować. Zmieniliśmy bibliotekę na ioredis i uzyskaliśmy wydajność już 25k na sekundę. Transakcje dodawaliśmy pojedynczo, używając polecenia `hset`. W ten sposób generowaliśmy wiele zapytań do Redis. Pojawił się pomysł, aby łączyć transakcje w paczki i wysyłać je jednym poleceniem `hmset`. Wynik — 32k na sekundę.

Z kilku powodów, które opiszemy poniżej, pracujemy z danymi przy użyciu `Buffer`, a jak się okazało, jeśli przetłumaczymy go na tekst (`buffer.toString('hex')`) przed zapisem, można uzyskać dodatkową wydajność. W ten sposób udało się zwiększyć prędkość do 35 tys. operacji na sekundę. Na chwilę obecną zdecydowaliśmy się wstrzymać dalszą optymalizację.

Musieliśmy przejść na protokół binarny, ponieważ:

1. System często oblicza hashe, podpisy itp., a do tego potrzebuje danych w `Buffer.

2. Podczas przesyłania danych między usługami dane binarne zajmują mniej miejsca niż tekst. Na przykład, przy wysyłaniu bloku z 1 milionem transakcji, dane w tekście mogą zajmować ponad 300 megabajtów.

3. Ciągła konwersja danych wpływa na wydajność.

Dlatego na podstawie wzięliśmy nasz własny binarny protokół przechowywania i przesyłania danych, opracowany na podstawie znakomitej biblioteki `binary-data`.

W rezultacie otrzymaliśmy następujące struktury danych:

— Transakcja

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

Zwykłymi poleceniami `BD.encode(block, Protocol).slice();` i ` BD.decode(buffer, Protocol)` przekształcamy dane do `Buffer`, aby zapisać je w Redis lub przesłać innemu węzłowi i wyciągnąć dane z powrotem.

Mamy również 2 binarne protokoły do przesyłania danych między usługami:

— Protokół do interakcji z Plasma Node poprzez gniazdo unixowe

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

gdzie:

  • `type` — działanie, które należy wykonać, na przykład 1 — sendTransaction, 2 — getTransaction;
  • `payload` — dane, które należy przekazać do odpowiedniej funkcji;
  • `messageId` — identyfikator wiadomości, aby można było zidentyfikować odpowiedź.

— Protokół interakcji między węzłami

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

gdzie:

  • `code` — kod wiadomości, na przykład 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — wersja protokołu, ponieważ w sieci mogą być uruchomione węzły z różnymi wersjami i mogą działać w różny sposób;
  • `seq` — identyfikator wiadomości;
  • `countChunk` i `chunkNumber` są potrzebne do podziału dużych wiadomości;
  • `length` i `payload` długość i same dane.

Ponieważ wcześniej zdefiniowaliśmy typy danych, końcowy system działa znacznie szybciej niż biblioteka `rlp` z Ethereum. Niestety, na razie nie udało nam się z niej zrezygnować, ponieważ musimy poprawić inteligentny kontrakt, co planujemy zrobić w przyszłości.

Jeśli udało nam się osiągnąć prędkość 35 000 transakcji na sekundę, musimy także przetwarzać je w optymalnym czasie. Ponieważ przybliżony czas formowania bloku wynosi 30 sekund, musimy uwzględnić w bloku 1 000 000 transakcji, co oznacza przesyłanie ponad 100 MB danych.

Początkowo użyliśmy biblioteki `ethereumjs-devp2p` do komunikacji węzłów, ale nie radziła sobie z taką ilością danych. W rezultacie skorzystaliśmy z biblioteki `ws` i skonfigurowaliśmy przesyłanie danych binarnych przez websocket. Oczywiście napotkaliśmy również problemy przy przesyłaniu dużych pakietów danych, ale podzieliliśmy je na fragmenty i teraz tych problemów nie ma.

Formowanie drzewa Merkle i obliczanie hasha 1 000 000 transakcji zajmuje około 10 sekund ciągłego obliczania. W tym czasie połączenie z wszystkimi węzłami zdążyło się zerwać. Zdecydowano się przenieść te obliczenia do osobnego wątku.

Wnioski:

Tak naprawdę nasze wnioski nie są nowe, ale z jakiegoś powodu wielu specjalistów zapomina o nich podczas tworzenia.

  • Wykorzystanie programowania funkcyjnego zamiast programowania obiektowego zwiększa wydajność.
  • Monolit jest gorszy niż architektura serwisowa dla wydajnego systemu na NodeJS.
  • Wykorzystanie `worker_threads` do ciężkich obliczeń poprawia responsywność systemu, szczególnie przy operacjach i/o.
  • Unix socket jest bardziej stabilny i szybszy niż zapytania http.
  • Jeśli trzeba szybko przesyłać duże dane przez sieć, lepiej skorzystać z websocketów i wysyłać dane binarne podzielone na fragmenty, które można przesłać ponownie, jeśli nie dotrą, a następnie połączyć w jedną wiadomość.

Zapraszamy do odwiedzenia GitHub projekcie: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Artykuł został napisany we współpracy z Aleksandrem Naszivanem, starszym programistą Clever Solution Inc.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster