Öffentlicher Test: Lösung fĂŒr Datenschutz und Skalierbarkeit im Ethereum

Blockchain — innovative Technologie, die verspricht, viele Bereiche des menschlichen Lebens zu verbessern. Sie ĂŒbertrĂ€gt reale Prozesse und Produkte in den digitalen Raum, gewĂ€hrleistet Geschwindigkeit und ZuverlĂ€ssigkeit bei finanziellen Transaktionen, senkt deren Kosten und ermöglicht es zudem, moderne DAPP-Anwendungen unter Verwendung intelligenter VertrĂ€ge in dezentralen Netzwerken zu erstellen.

Angesichts der zahlreichen Vorteile und vielfÀltigen Anwendungsmöglichkeiten von Blockchain mag es seltsam erscheinen, dass diese vielversprechende Technologie noch nicht in alle Sektoren vorgedrungen ist. Das Problem liegt darin, dass modernen dezentralen Blockchains die Skalierbarkeit fehlt. Ethereum verarbeitet etwa 20 Transaktionen pro Sekunde, was nicht ausreicht, um den Anforderungen eines dynamischen modernen GeschÀfts gerecht zu werden. Gleichzeitig zögern Unternehmen, die Blockchain-Technologie nutzen, einen Wechsel von Ethereum vorzunehmen, aufgrund seines hohen Schutzes vor Hacks und Netzwerkfehlern.

Um Dezentralisierung, Sicherheit und Skalierbarkeit in der Blockchain zu gewĂ€hrleisten, und somit das Skalierbarkeits-Trilemma zu lösen, hat das Entwicklerteam Opporty Plasma Cash entwickelt — eine Sidechain, die aus einem Smart Contract und einem privaten Netzwerk auf Grundlage von Node.js besteht und regelmĂ€ĂŸig ihren Zustand an die Haupt-Chain (Ethereum) ĂŒbertrĂ€gt.

Öffentlicher Test: Lösung fĂŒr Datenschutz und Skalierbarkeit im Ethereum

SchlĂŒsselfunktionen in Plasma Cash

1. Der Benutzer ruft die Funktion des Smart Contracts `deposit` auf und ĂŒbertrĂ€gt den Betrag in ETH, den er im Plasma Cash Token anlegen möchte. Die Funktion des Smart Contracts erstellt das Token und generiert ein Ereignis darĂŒber.

2. Die Plasma Cash-Knoten, die auf die Ereignisse des Smart Contracts lauschen, erhalten das Ereignis ĂŒber die Erstellung der Einzahlung und fĂŒgen die Transaktion zur Erstellung des Tokens in den Pool ein.

3. RegelmĂ€ĂŸig nehmen spezielle Plasma Cash-Knoten alle Transaktionen aus dem Pool (bis zu 1 Million) und bilden daraus einen Block, berechnen den Merkle-Baum und entsprechend den Hash. Dieser Block wird anderen Knoten zur Verifikation ĂŒbermittelt. Die Knoten ĂŒberprĂŒfen, ob der Merkle-Hash gĂŒltig ist und ob die Transaktionen gĂŒltig sind (z. B. ob der Absender des Tokens dessen EigentĂŒmer ist). Nach der Validierung des Blocks ruft der Knoten die Funktion `submitBlock` des Smart Contracts auf, die die Blocknummer und den Merkle-Hash in die Haupt-Chain speichert. Der Smart Contract generiert ein Ereignis ĂŒber die erfolgreiche HinzufĂŒgung des Blocks. Transaktionen werden aus dem Pool entfernt.

4. Knoten, die das Ereignis ĂŒber das Einreichen eines Blocks erhalten haben, beginnen, die im Block hinzugefĂŒgten Transaktionen anzuwenden.

5. Zu einem bestimmten Zeitpunkt möchte der EigentĂŒmer (oder NichtereigentĂŒmer) des Tokens ihn aus Plasma Cash abheben. Dazu ruft er die Funktion `startExit` auf und ĂŒbergibt Informationen zu den letzten 2 Transaktionen zum Token, die bestĂ€tigen, dass er der EigentĂŒmer des Tokens ist. Der Smart Contract ĂŒberprĂŒft mit Hilfe des Merkle-Hashes, ob die Transaktionen in den Blöcken enthalten sind, und sendet den Token zur Auszahlung, die in zwei Wochen erfolgen wird.

6. Wenn die Abhebungsoperation des Tokens gegen die Vorschriften verstĂ¶ĂŸt (der Token wurde nach Beginn des Abhebungsverfahrens ausgegeben oder der Token war vor der Abhebung bereits fremd), kann der EigentĂŒmer des Tokens die Abhebung innerhalb von zwei Wochen anfechten.

Öffentlicher Test: Lösung fĂŒr Datenschutz und Skalierbarkeit im Ethereum

Die PrivatsphÀre wird auf zwei Arten erreicht.

1. Die Hauptkette weiß nichts ĂŒber die Transaktionen, die innerhalb der untergeordneten Kette erstellt und gesendet werden. Öffentlich bleibt die Information darĂŒber, wer ETH in Plasma Cash eingezahlt und abgehoben hat.

2. Die untergeordnete Kette ermöglicht die DurchfĂŒhrung anonymer Transaktionen unter Verwendung von zk-SNARKs.

Technologischer Stack

  • NodeJS
  • Redis
  • Etherium
  • Solide

Tests

Bei der Entwicklung von Plasma Cash haben wir die Geschwindigkeit des Systems getestet und folgende Ergebnisse erhalten:

  • bis zu 35.000 Transaktionen pro Sekunde werden in den Pool hinzugefĂŒgt;
  • bis zu 1.000.000 Transaktionen können in einem Block gespeichert werden.

Die Tests wurden auf folgenden 3 Servern durchgefĂŒhrt:

1. Intel Core i7-6700 Quad-Core Skylake incl. NVMe SSD — 512 GB, 64 GB DDR4 RAM
Es wurden 3 validierende Plasma Cash Knoten eingerichtet.

2. AMD Ryzen 7 1700X Octa-Core „Summit Ridge“ (Zen), SATA SSD — 500 GB, 64 GB DDR4 RAM
Es wurde ein Ropsten-Testnetz-ETH-Knoten eingerichtet.
Es wurden 3 validierende Plasma Cash Knoten eingerichtet.

3. Intel Core i9-9900K Octa-Core incl. NVMe SSD — 1 TB, 64 GB DDR4 RAM
Es wurde 1 Submit Plasma Cash Knoten eingerichtet.
Es wurden 3 validierende Plasma Cash Knoten eingerichtet.
Ein Test fĂŒr das HinzufĂŒgen von Transaktionen zum Plasma Cash Netzwerk wurde gestartet.

Insgesamt: 10 Plasma Cash Knoten im privaten Netzwerk.

Test 1

Es gibt ein Limit von 1 Million Transaktionen pro Block. Daher gelangen 1 Million Transaktionen in 2 Blöcke (da das System eine Teilmenge der Transaktionen erfassen und einreichen kann, wÀhrend sie gesendet werden).

Video abspielen

Ausgangszustand: letzter Block #7; in der Datenbank sind 1 Million Transaktionen und Tokens gespeichert.

00:00 — Start des Skripts zur Generierung von Transaktionen
01:37 — 1 Million Transaktionen erstellt und Versendung an den Knoten begonnen
01:46 — der Submit-Knoten hat 240.000 Transaktionen aus dem Pool ĂŒbernommen und block #8 erstellt. Zudem sehen wir, dass in 10 Sekunden 320.000 Transaktionen zum Pool hinzugefĂŒgt werden.
01:58 — block #8 wurde signiert und zur Validierung gesendet.
02:03 — Block #8 wurde validiert und die Funktion `submitBlock` des Smart Contracts mit dem Merkle-Hash und der Blocknummer aufgerufen.
02:10 — Das Demo-Skript, das 1 Million Transaktionen in 32 Sekunden gesendet hat, wurde beendet.
02:33 — Die Nodes begannen Informationen darĂŒber zu erhalten, dass Block #8 zur Root-Chain hinzugefĂŒgt wurde, und begannen 240.000 Transaktionen auszufĂŒhren.
02:40 — 240.000 Transaktionen wurden aus dem Pool entfernt, die bereits in Block #8 waren.
02:56 — Der Submit-Node entnahm aus dem Pool die verbleibenden 760.000 Transaktionen und begann, den Merkle-Hash zu berechnen und Block #9 zu signieren.
03:20 — Alle Nodes enthalten 1.240.000 Transaktionen und Token.
03:35 — Block #9 wurde signiert und zur Validierung an andere Nodes gesendet.
03:41 — Ein Netzwerkfehler ist aufgetreten.
04:40 — Die Wartezeit zur Validierung von Block #9 wurde wegen Timeout beendet.
04:54 — Der Submit-Node entnahm aus dem Pool die verbleibenden 760.000 Transaktionen und begann, den Merkle-Hash zu berechnen und Block #9 zu signieren.
05:32 — Block #9 wurde signiert und zur Validierung an andere Nodes gesendet.
05:53 — Block #9 wurde validiert und in die Root-Chain gesendet.
06:17 — Die Nodes begannen, Informationen darĂŒber zu erhalten, dass Block #9 zur Root-Chain hinzugefĂŒgt wurde, und begannen 760.000 Transaktionen auszufĂŒhren.
06:47 — Der Pool wurde von Transaktionen, die in Block #9 waren, bereinigt.
09:06 — Alle Nodes enthalten 2 Millionen Transaktionen und Token.

Test 2

Es gibt ein Limit von 350.000 pro Block. Daraus ergibt sich, dass wir 3 Blöcke haben.

Video abspielen

Ausgangszustand: letzter Block #9; in der Datenbank sind 2 Millionen Transaktionen und Token gespeichert.

00:00 — Das Skript zur Generierung von Transaktionen wurde bereits gestartet.
00:44 — Es wurden 1 Million Transaktionen erstellt und die Übermittlung an den Node begann.
00:56 — Der Submit-Node entnahm 320.000 Transaktionen aus dem Pool und bildet Block #10. Wir sehen auch, dass 320.000 Transaktionen in 10 Sekunden zum Pool hinzugefĂŒgt werden.
01:12 — Block #10 wurde signiert und zur Validierung an andere Nodes gesendet.
01:18 — Das Demo-Skript, das 1 Million Transaktionen in 34 Sekunden gesendet hat, wurde beendet.
01:20 — Block #10 wurde validiert und in die Root-Chain gesendet.
01:51 — Alle Nodes erhielten Informationen aus der Root-Chain, dass Block #10 hinzugefĂŒgt wurde, und begannen, 320.000 Transaktionen anzuwenden.
02:01 — Der Pool wurde um 320.000 Transaktionen bereinigt, die in Block #10 hinzugefĂŒgt wurden.
02:15 — Der Submit-Node entnahm 350.000 Transaktionen aus dem Pool und bildet Block #11.
02:34 — Block #11 wurde signiert und an andere Nodes zur Validierung gesendet.
02:51 — Block #11 wurde validiert und in die Root-Chain gesendet.
02:55 — Der letzte Node fĂŒhrte die Transaktionen aus Block #10 aus.
10:59 — sehr lange wurde die Transaktion mit dem Einreichen des Blocks #9 in der Hauptkette ausgefĂŒhrt, aber sie wurde ausgefĂŒhrt und alle Knoten erhielten die Informationen und begannen, 350.000 Transaktionen auszufĂŒhren
11:05 — der Pool wurde um 320.000 Transaktionen bereinigt, die in Block #11 hinzugefĂŒgt wurden
12:10 — alle Knoten enthalten 1.670.000 Transaktionen und Token
12:17 — der Einreichungsknoten hat 330.000 Transaktionen aus dem Pool entnommen und bildet Block #12
12:32 — Block #12 wurde signiert und an die anderen Knoten zur Validierung gesendet
12:39 — Block #12 wurde validiert und in die Hauptkette gesendet
13:44 — alle Knoten haben aus der Hauptkette die Information erhalten, dass Block #12 hinzugefĂŒgt wurde und beginnen, 330.000 Transaktionen anzuwenden
14:50 — alle Knoten enthalten 2 Millionen Transaktionen und Token

Test 3

Auf dem ersten und zweiten Server wurde ein validierender Knoten durch einen Einreichungsknoten ersetzt.

Video abspielen

Ausgangszustand: letzter Block #84; in der Datenbank sind 0 Transaktionen und Token gespeichert

00:00 — 3 Skripte wurden gestartet, die jeweils 1 Million Transaktionen generieren und senden
01:38 — 1 Million Transaktionen wurden erstellt und der Versand an den Einreichungsknoten #3 hat begonnen
01:50 — der Einreichungsknoten #3 hat 330.000 Transaktionen aus dem Pool entnommen und bildet Block #85 (f21). Auch sehen wir, dass in den Pool 350.000 Transaktionen in 10 Sekunden hinzugefĂŒgt werden
01:53 — 1 Million Transaktionen wurden erstellt und der Versand an den Einreichungsknoten #1 hat begonnen
01:50 — der Einreichungsknoten #3 hat 330.000 Transaktionen aus dem Pool entnommen und bildet Block #85 (f21). Auch sehen wir, dass in den Pool 350.000 Transaktionen in 10 Sekunden hinzugefĂŒgt werden
02:01 — der Einreichungsknoten #1 hat 250.000 Transaktionen aus dem Pool entnommen und bildet Block #85 (65e)
02:06 — Block #85 (f21) wurde signiert und an die anderen Knoten zur Validierung gesendet
02:08 — das Demo-Skript des Servers #3 hat seine Arbeit beendet, das 1 Million Transaktionen in 30 Sekunden gesendet hat
02:14 — Block #85 (f21) wurde validiert und in die Hauptkette gesendet
02:19 — Block #85 (65e) wurde signiert und an die anderen Knoten zur Validierung gesendet
02:22 — 1 Million Transaktionen wurden erstellt und der Versand an den Einreichungsknoten #2 hat begonnen
02:27 — Block #85 (65e) wurde validiert und in die Hauptkette gesendet
02:29 — der Einreichungsknoten #2 hat 111.855 Transaktionen aus dem Pool entnommen und bildet Block #85 (256).
02:36 — Block #85 (256) wurde signiert und an die anderen Knoten zur Validierung gesendet
02:36 — das Demo-Skript des Servers #1 hat seine Arbeit beendet, das 1 Million Transaktionen in 42,5 Sekunden gesendet hat
02:38 — Block #85 (256) wurde validiert und in die Hauptkette gesendet
03:08 — das Demo-Skript des Servers #2 hat seine Arbeit beendet, das 1 Million Transaktionen in 47 Sekunden gesendet hat
03:38 — alle Knoten haben aus der Hauptkette die Information erhalten, dass die Blöcke #85 (f21), #86(65e), #87(256) hinzugefĂŒgt wurden und beginnen, 330.000, 250.000, 111.855 Transaktionen anzuwenden
03:49 — der Pool hat sich auf 330k, 250k, 111855 Transaktionen bereinigt, die in die Blöcke #85 (f21), #86(65e), #87(256) hinzugefĂŒgt wurden
03:59 — der Submit-Node #1 hat 888145 Transaktionen aus dem Pool genommen und block #88 (214) generiert, der Submit-Node #2 hat 750k Transaktionen aus dem Pool genommen und block #88 (50a) generiert, der Submit-Node #3 hat 670k Transaktionen aus dem Pool genommen und block #88 (d3b) generiert
04:44 — block #88 (d3b) wurde signiert und an andere Nodes zur Validierung gesendet
04:58 — block #88 (214) wurde signiert und an andere Nodes zur Validierung gesendet
05:11 — block #88 (50a) wurde signiert und an andere Nodes zur Validierung gesendet
05:11 — block #85 (d3b) wurde validiert und in die Hauptkette gesendet
05:36 — block #85 (214) wurde validiert und in die Hauptkette gesendet
05:43 — alle Nodes haben Informationen aus der Hauptkette darĂŒber erhalten, dass die Blöcke #88 (d3b), #89(214) hinzugefĂŒgt wurden und beginnen, 670k, 750k Transaktionen anzuwenden
06:50 — aufgrund eines Verbindungsabbruchs wurde block #85 (50a) nicht validiert
06:55 — der Submit-Node #2 hat 888145 Transaktionen aus dem Pool genommen und block #90 (50a) generiert
08:14 — block #90 (50a) wurde signiert und an andere Nodes zur Validierung gesendet
09:04 — block #90 (50a) wurde validiert und in die Hauptkette gesendet
11:23 — alle Nodes haben Informationen aus der Hauptkette erhalten, dass block #90 (50a) hinzugefĂŒgt wurde, und beginnen, 888145 Transaktionen anzuwenden. Dabei hat der Server #3 bereits Transaktionen aus den Blöcken #88 (d3b), #89(214) angewendet
12:11 — alle Pools sind leer
13:41 — alle Nodes des Servers #3 enthalten 3 Millionen Transaktionen und Tokens
14:35 — alle Nodes des Servers #1 enthalten 3 Millionen Transaktionen und Tokens
19:24 — alle Nodes des Servers #2 enthalten 3 Millionen Transaktionen und Tokens

Herausforderungen

WĂ€hrend der Entwicklung von Plasma Cash stießen wir auf folgende Probleme, die wir schrittweise gelöst haben und weiterhin lösen:

1. Konflikte bei der Interaktion verschiedener Funktionen des Systems. Beispielsweise blockierte die Funktion zum HinzufĂŒgen von Transaktionen zum Pool die Arbeit des Submitters und der Validierung von Blöcken und umgekehrt, was zu einer Verringerung der Geschwindigkeit fĂŒhrte.

2. Es war zunĂ€chst unklar, wie man eine große Menge an Transaktionen senden und dabei die Übertragungskosten minimieren kann.

3. Es war unklar, wie und wo die Daten gespeichert werden sollten, um hohe Ergebnisse zu erzielen.

4. Es war unklar, wie das Netzwerk zwischen den Nodes organisiert werden sollte, da die GrĂ¶ĂŸe eines Blocks mit 1 Million Transaktionen etwa 100 MB betrĂ€gt.

5. Die Arbeit im Ein-Thread-Modus unterbricht die Verbindung zwischen den Nodes, wenn lange Berechnungen stattfinden (zum Beispiel beim Aufbau des Merkle-Baums und der Berechnung seines Hashes).

Wie haben wir all das geschafft?

Die erste Version der Plasma Cash Node stellte eine Art Kombi dar, der alles gleichzeitig erledigen konnte: Transaktionen annehmen, Blöcke einreichen und validieren sowie eine API fĂŒr den Datenzugriff bereitstellen. Da NodeJS ursprĂŒnglich single-threaded ist, blockierte die rechenintensive Funktion zur Berechnung des Merkle-Baums die Funktion zur HinzufĂŒgung von Transaktionen. Wir sahen zwei Möglichkeiten zur Lösung dieses Problems:

1. Mehrere NodeJS-Prozesse starten, von denen jeder bestimmte Funktionen ausfĂŒhrt.

2. Worker-Threads verwenden und Teile des Codes in Threads auslagern.

Schließlich nutzten wir beide Optionen gleichzeitig: Wir unterteilten eine Node logisch in 3 Teile, die separat, aber gleichzeitig arbeiten können.

1. Die Submit-Node, die Transaktionen in den Pool aufnimmt und Blöcke erstellt.

2. Die validierende Node, die die GĂŒltigkeit der Nodes ĂŒberprĂŒft.

3. API-Node – stellt eine API fĂŒr den Datenzugriff bereit.

Alle Nodes sind ĂŒber einen Unix-Socket mittels CLI erreichbar.

Rechenintensive Operationen, wie die Berechnung des Merkle-Baums, lagerten wir in einen separaten Thread aus.

So erreichten wir eine normale Funktionsweise aller Plasma Cash Funktionen gleichzeitig und ohne AusfÀlle.

Sobald das System funktional arbeitete, begannen wir mit Tests der Geschwindigkeit und erhielten leider unzureichende Ergebnisse: 5.000 Transaktionen pro Sekunde und bis zu 50.000 Transaktionen im Block. Wir mussten herausfinden, was falsch implementiert wurde.

ZunĂ€chst testeten wir den Kommunikationsmechanismus mit Plasma Cash, um die maximale LeistungsfĂ€higkeit des Systems zu ermitteln. Zuvor hatten wir erwĂ€hnt, dass die Plasma Cash Node eine Unix-Socket-Schnittstelle bereitstellt. Anfangs war diese textbasiert. JSON-Objekte wurden mittels `JSON.parse()` und `JSON.stringify()` ĂŒbertragen.

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

Wir gemessene die Übertragungsgeschwindigkeit solcher Objekte und erhielten ~ 130.000 pro Sekunde. Wir versuchten, die Standardfunktionen zur Arbeit mit JSON zu ersetzen, aber die Leistung verbesserte sich nicht. Offensichtlich ist die V8-Engine gut fĂŒr diese Operationen optimiert.

Die Arbeit mit Transaktionen, Tokens und Blöcken erfolgte bei uns ĂŒber Klassen. Bei der Erstellung solcher Klassen fiel die Leistung um das Zweifache, was darauf hindeutet, dass OOP fĂŒr uns nicht geeignet ist. Wir mussten alles auf einen rein funktionalen Ansatz umschreiben.

Datenbankeintrag

UrsprĂŒnglich haben wir Redis als eine der leistungsfĂ€higsten Lösungen fĂŒr die Datenspeicherung gewĂ€hlt, die unseren Anforderungen entsprechen: ein Key-Value-Speicher, der mit Hash-Tabellen und Mengen arbeitet. Wir haben redis-benchmark gestartet und erhielten etwa 80.000 Operationen pro Sekunde im Modus 1 Pipelining.

FĂŒr eine hohe Leistung haben wir Redis feiner eingestellt:

  • Wir haben eine Unix-Socket-Verbindung eingerichtet.
  • Wir haben die Speicherung des Zustands auf die Festplatte deaktiviert (fĂŒr die ZuverlĂ€ssigkeit kann eine Replik eingerichtet werden, die die Speicherung auf der Festplatte durchfĂŒhrt).

In Redis ist ein Pool eine Hash-Tabelle, da wir die Möglichkeit benötigen, alle Transaktionen mit einer Anfrage zu erhalten und Transaktionen einzeln zu löschen. Wir haben versucht, eine normale Liste zu verwenden, aber sie arbeitet langsamer beim Laden der gesamten Liste.

Mit der Standard-NodeJS-Bibliothek fĂŒr Redis haben wir eine Leistung von 18.000 Transaktionen pro Sekunde erhalten. Die Geschwindigkeit fiel um das Neunfache.

Da der Benchmark uns offensichtlich die Möglichkeit von fĂŒnfmal mehr zeigte, begannen wir mit der Optimierung. Wir wechselten zur Bibliothek ioredis und erzielten eine Leistung von bereits 25.000 pro Sekunde. Die Transaktionen haben wir einzeln hinzugefĂŒgt, indem wir den Befehl `hset` verwendet haben. So generierten wir viele Anfragen an Redis. Es kam die Idee auf, Transaktionen in Chargen zusammenzufassen und sie mit einem Befehl `hmset` zu senden. Das Ergebnis waren 32.000 pro Sekunde.

Aus mehreren GrĂŒnden, die wir unten beschreiben, arbeiten wir mit Daten unter Verwendung von `Buffer`, und wie sich herausstellte, kann man durch die Umwandlung in Text (`buffer.toString('hex')`) vor dem Schreiben zusĂ€tzliche Leistung erzielen. So konnte die Geschwindigkeit auf 35.000 pro Sekunde erhöht werden. Im Moment haben wir beschlossen, die weiteren Optimierungen vorerst auszusetzen.

Wir mussten auf ein binÀres Protokoll umsteigen, da:

1. Das System hĂ€ufig Hashes, Signaturen und Ähnliches berechnet, und dafĂŒr benötigt es Daten im `Buffer.

2. Bei der Übertragung zwischen Diensten wiegen binĂ€re Daten weniger als Text. Zum Beispiel können beim Versenden eines Blocks mit 1 Million Transaktionen die Daten im Text ĂŒber 300 Megabyte belegen.

3. Die stÀndige Umwandlung von Daten wirkt sich auf die Leistung aus.

Deshalb haben wir unser eigenes binĂ€res Protokoll zur Speicherung und Übertragung von Daten als Grundlage genommen, das auf der großartigen Bibliothek `binary-data` basiert.

Infolgedessen haben wir die folgenden Datenstrukturen erhalten:

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

— Block

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

Mit den ĂŒblichen Befehlen `BD.encode(block, Protocol).slice();` und ` BD.decode(buffer, Protocol)` wandeln wir die Daten in einen `Buffer` um, um sie in Redis zu speichern oder an einen anderen Knoten zu ĂŒbertragen und die Daten zurĂŒckzuerhalten.

Außerdem haben wir 2 binĂ€re Protokolle fĂŒr die Übertragung von Daten zwischen Diensten:

— Protokoll zur Interaktion mit Plasma Node ĂŒber einen 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)
  }
  ```

{REPOSITORY_ABSOLUTE_PATH}

  • `type` — Die Aktion, die ausgefĂŒhrt werden soll, z.B. 1 — sendTransaction, 2 — getTransaction;
  • `payload` — Die Daten, die an die entsprechende Funktion ĂŒbergeben werden sollen;
  • `messageId` — ID der Nachricht, um die Antwort identifizieren zu können.

— Protokoll zur Interaktion zwischen Knoten

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

{REPOSITORY_ABSOLUTE_PATH}

  • `code` — Nachrichten-Code, z.B. 6 — PREPARE_NEW_BLOCK, 7 — BLOCK_VALID, 8 — BLOCK_COMMIT;
  • `versionProtocol` — Version des Protokolls, da im Netzwerk Knoten mit unterschiedlichen Versionen hochgefahren werden können und unterschiedlich arbeiten können;
  • `seq` — Nachrichten-Identifikator;
  • `countChunk` und `chunkNumber` sind erforderlich, um große Nachrichten zu fragmentieren;
  • `length` und `payload` LĂ€nge und die Daten selbst.

Da wir die Daten im Voraus typisiert haben, arbeitet das Endsystem viel schneller als die `rlp`-Bibliothek von Ethereum. Leider ist es uns bisher nicht gelungen, darauf zu verzichten, da der Smart Contract ĂŒberarbeitet werden muss, was wir in Zukunft planen.

Wenn es uns gelungen ist, eine Geschwindigkeit zu erreichen 35 000 Um Transaktionen pro Sekunde zu verarbeiten, mĂŒssen wir sie auch in optimaler Zeit abwickeln. Da die ungefĂ€hre Zeit zur Bildung eines Blocks 30 Sekunden betrĂ€gt, mĂŒssen wir diese in den Block einfĂŒgen. 1 000 000 Transaktionen, was bedeutet, dass mehr als 100 MB an Daten ĂŒbertragen werden.

UrsprĂŒnglich verwendeten wir die Bibliothek `ethereumjs-devp2p` zur Kommunikation zwischen den Knoten, aber sie konnte mit der Menge an Daten nicht umgehen. Daher haben wir die Bibliothek `ws` verwendet und die Übertragung von BinĂ€rdaten ĂŒber WebSockets eingerichtet. NatĂŒrlich hatten wir auch Probleme bei der Übertragung großer Datenpakete, aber wir haben sie in Chunks aufgeteilt, und jetzt haben wir diese Probleme nicht mehr.

Außerdem benötigt die Bildung des Merkle-Baums und die Berechnung des Hashes 1 000 000 der Transaktionen etwa 10 Sekunden kontinuierlicher Berechnungen. In dieser Zeit kann die Verbindung zu allen Knoten unterbrochen werden. Es wurde beschlossen, diese Berechnung in einen separaten Thread zu verlagern.

Fazit:

TatsÀchlich sind unsere Erkenntnisse nicht neu, aber aus irgendeinem Grund vergessen viele Spezialisten diese bei der Entwicklung.

  • Die Verwendung von Functional Programming anstelle von Object-Oriented Programming erhöht die Leistung.
  • Monolithische Architekturen sind schlechter als serviceorientierte Architekturen fĂŒr leistungsstarke Systeme auf NodeJS.
  • Die Verwendung von `worker_threads` fĂŒr rechenintensive Berechnungen verbessert die ReaktionsfĂ€higkeit des Systems, insbesondere bei I/O-Operationen.
  • Unix-Sockets sind stabiler und schneller als HTTP-Anfragen.
  • Wenn große Daten schnell ĂŒber das Netzwerk ĂŒbertragen werden mĂŒssen, ist es besser, WebSockets zu verwenden und BinĂ€rdaten in Chunks zu senden, die erneut ĂŒbermittelt werden können, wenn sie nicht ankommen, und dann zu einer Nachricht zusammengefĂŒgt werden können.

Wir laden Sie ein, zu besuchen GitHub Projekt definiert: https://github.com/opporty-com/Plasma-Cash/tree/new-version

Der Artikel wurde in Zusammenarbeit mit Alexander Nashivan, leitender Entwickler Clever Solution Inc.

Quelle: habr.com

60GB SSD 8Gb DDR4