Öffentlicher Test: Eine Lösung für Datenschutz und Skalierbarkeit auf Ethereum

Blockchain ist eine innovative Technologie, die verspricht, viele Bereiche des menschlichen Lebens zu verbessern. Es überträgt reale Prozesse und Produkte in den digitalen Raum, sorgt für Geschwindigkeit und Zuverlässigkeit von Finanztransaktionen, senkt deren Kosten und ermöglicht Ihnen außerdem die Erstellung moderner DAPP-Anwendungen mithilfe intelligenter Verträge in dezentralen Netzwerken.

Angesichts der vielen Vorteile und vielfältigen Einsatzmöglichkeiten der Blockchain mag es überraschend erscheinen, dass diese vielversprechende Technologie noch nicht in allen Branchen Einzug gehalten hat. Das Problem besteht darin, dass es modernen dezentralen Blockchains an Skalierbarkeit mangelt. Ethereum verarbeitet etwa 20 Transaktionen pro Sekunde, was nicht ausreicht, um die Anforderungen dynamischer Unternehmen von heute zu erfüllen. Gleichzeitig zögern Unternehmen, die die Blockchain-Technologie einsetzen, aufgrund des hohen Schutzniveaus von Ethereum vor Hacking und Netzwerkausfällen, Ethereum aufzugeben.

Um Dezentralisierung, Sicherheit und Skalierbarkeit in der Blockchain zu gewährleisten und so das Skalierbarkeitstrilemma zu lösen, hat das Entwicklungsteam Gelegenheit erstellte Plasma Cash, eine Tochterkette bestehend aus einem Smart Contract und einem privaten Netzwerk auf Basis von Node.js, das seinen Status regelmäßig an die Root-Chain (Ethereum) überträgt.

Öffentlicher Test: Eine Lösung für Datenschutz und Skalierbarkeit auf Ethereum

Schlüsselprozesse in Plasma Cash

1. Der Benutzer ruft die Smart-Contract-Funktion „Einzahlung“ auf und übergibt ihr den ETH-Betrag, den er in den Plasma Cash-Token einzahlen möchte. Die Smart-Contract-Funktion erstellt einen Token und generiert ein Ereignis darüber.

2. Plasma-Cash-Knoten, die Smart-Contract-Ereignisse abonniert haben, erhalten ein Ereignis zum Erstellen einer Einzahlung und fügen dem Pool eine Transaktion zum Erstellen eines Tokens hinzu.

3. In regelmäßigen Abständen nehmen spezielle Plasma-Cash-Knoten alle Transaktionen aus dem Pool (bis zu 1 Million) und bilden daraus einen Block, berechnen den Merkle-Baum und dementsprechend den Hash. Dieser Block wird zur Überprüfung an andere Knoten gesendet. Die Knoten prüfen, ob der Merkle-Hash gültig ist und ob die Transaktionen gültig sind (z. B. ob der Absender des Tokens sein Eigentümer ist). Nach der Überprüfung des Blocks ruft der Knoten die Funktion „submitBlock“ des Smart Contracts auf, die die Blocknummer und den Merkle-Hash in der Edge-Chain speichert. Der Smart Contract generiert ein Ereignis, das das erfolgreiche Hinzufügen eines Blocks anzeigt. Transaktionen werden aus dem Pool entfernt.

4. Knoten, die das Blockübermittlungsereignis empfangen, beginnen mit der Anwendung der Transaktionen, die dem Block hinzugefügt wurden.

5. Irgendwann möchte der Besitzer (oder Nichtbesitzer) des Tokens ihn von Plasma Cash abheben. Dazu ruft er die Funktion „startExit“ auf und übergibt ihr Informationen über die letzten beiden Transaktionen auf dem Token, die bestätigen, dass er der Eigentümer des Tokens ist. Der Smart Contract prüft mithilfe des Merkle-Hashs das Vorhandensein von Transaktionen in den Blöcken und sendet den Token zur Auszahlung, was in zwei Wochen erfolgen wird.

6. Wenn es bei der Token-Entnahme zu Verstößen kam (der Token wurde nach Beginn des Entnahmevorgangs ausgegeben oder der Token gehörte bereits vor der Entnahme einer anderen Person), kann der Besitzer des Tokens die Entnahme innerhalb von zwei Wochen widerlegen.

Öffentlicher Test: Eine Lösung für Datenschutz und Skalierbarkeit auf Ethereum

Datenschutz wird auf zwei Arten erreicht

1. Die Root-Kette weiß nichts über die Transaktionen, die innerhalb der Child-Kette generiert und weitergeleitet werden. Informationen darüber, wer ETH von Plasma Cash eingezahlt und abgehoben hat, bleiben öffentlich.

2. Die untergeordnete Kette ermöglicht anonyme Transaktionen mithilfe von zk-SNARKs.

Technologie-Stack

  • KnotenJS
  • Redis
  • Etherium
  • Soild

Testing

Während der Entwicklung von Plasma Cash haben wir die Geschwindigkeit des Systems getestet und folgende Ergebnisse erzielt:

  • bis zu 35 Transaktionen pro Sekunde werden dem Pool hinzugefügt;
  • Bis zu 1 Transaktionen können in einem Block gespeichert werden.

Die Tests wurden auf den folgenden 3 Servern durchgeführt:

1. Intel Core i7-6700 Quad-Core Skylake inkl. NVMe SSD – 512 GB, 64 GB DDR4 RAM
3 validierende Plasma Cash-Knoten wurden erstellt.

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

3. Intel Core i9-9900K Octa-Core inkl. NVMe SSD – 1 TB, 64 GB DDR4 RAM
1 Plasma Cash-Übermittlungsknoten wurde erstellt.
3 validierende Plasma Cash-Knoten wurden erstellt.
Es wurde ein Test gestartet, um Transaktionen zum Plasma Cash-Netzwerk hinzuzufügen.

Total: 10 Plasma Cash-Knoten in einem privaten Netzwerk.

Test 1

Es gibt ein Limit von 1 Million Transaktionen pro Block. Daher fallen 1 Million Transaktionen in 2 Blöcke (da es dem System gelingt, einen Teil der Transaktionen zu übernehmen und zu übermitteln, während sie gesendet werden).


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

00:00 – Beginn des Skripts zur Transaktionsgenerierung
01:37 – 1 Million Transaktionen wurden erstellt und der Versand an den Knoten begann
01:46 – Der Submit-Knoten hat 240 Transaktionen aus dem Pool übernommen und bildet Block Nr. 8. Wir sehen auch, dass in 320 Sekunden 10 Transaktionen zum Pool hinzugefügt werden
01:58 – Block Nr. 8 wird signiert und zur Validierung gesendet
02:03 – Block Nr. 8 wird validiert und die Funktion „submitBlock“ des Smart Contracts wird mit dem Merkle-Hash und der Blocknummer aufgerufen
02:10 – Demo-Skript funktioniert nicht mehr und sendet 1 Million Transaktionen in 32 Sekunden
02:33 – Knoten begannen, Informationen darüber zu empfangen, dass Block Nr. 8 zur Root-Kette hinzugefügt wurde, und begannen, 240 Transaktionen durchzuführen
02:40 – 240 Transaktionen wurden aus dem Pool entfernt, die sich bereits in Block #8 befinden
02:56 – Submit-Knoten nahm die verbleibenden 760 Transaktionen aus dem Pool und begann mit der Berechnung des Merkle-Hashs und dem Signieren von Block Nr. 9
03:20 – Alle Knoten enthalten 1 Million 240 Transaktionen und Token
03:35 – Block Nr. 9 wird signiert und zur Validierung an andere Knoten gesendet
03:41 – Netzwerkfehler aufgetreten
04:40 – Das Warten auf die Validierung von Block Nr. 9 ist abgelaufen
04:54 – Submit-Knoten nahm die verbleibenden 760 Transaktionen aus dem Pool und begann mit der Berechnung des Merkle-Hashs und dem Signieren von Block Nr. 9
05:32 – Block Nr. 9 wird signiert und zur Validierung an andere Knoten gesendet
05:53 – Block Nr. 9 wird validiert und an die Root-Kette gesendet
06:17 – Knoten erhielten Informationen darüber, dass Block Nr. 9 zur Root-Kette hinzugefügt wurde und begannen, 760 Transaktionen durchzuführen
06:47 – Der Pool wurde von Transaktionen befreit, die sich in Block Nr. 9 befinden
09:06 – Alle Knoten enthalten 2 Millionen Transaktionen und Token

Test 2

Es gibt ein Limit von 350 pro Block. Als Ergebnis haben wir 3 Blöcke.


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

00:00 – Das Skript zur Transaktionsgenerierung wurde bereits gestartet
00:44 – 1 Million Transaktionen wurden erstellt und der Versand an den Knoten begann
00:56 – Der Submit-Knoten hat 320 Transaktionen aus dem Pool übernommen und bildet Block Nr. 10. Wir sehen auch, dass in 320 Sekunden 10 Transaktionen zum Pool hinzugefügt werden
01:12 – Block Nr. 10 wird signiert und zur Validierung an andere Knoten gesendet
01:18 – Demo-Skript funktioniert nicht mehr und sendet 1 Million Transaktionen in 34 Sekunden
01:20 – Block Nr. 10 wird validiert und an die Root-Kette gesendet
01:51 – Alle Knoten haben von der Root-Kette die Information erhalten, dass Block Nr. 10 hinzugefügt wurde, und beginnen mit der Anwendung von 320 Transaktionen
02:01 – Der Pool wurde für 320 Transaktionen freigegeben, die Block Nr. 10 hinzugefügt wurden
02:15 – Der Submit-Knoten hat 350 Transaktionen aus dem Pool übernommen und bildet Block Nr. 11
02:34 – Block Nr. 11 wird signiert und zur Validierung an andere Knoten gesendet
02:51 – Block Nr. 11 wird validiert und an die Root-Kette gesendet
02:55 – Der letzte Knoten hat Transaktionen aus Block Nr. 10 abgeschlossen
10:59 – Die Transaktion mit der Übermittlung von Block Nr. 9 dauerte in der Root-Kette sehr lange, wurde aber abgeschlossen und alle Knoten erhielten Informationen darüber und begannen mit der Durchführung von 350 Transaktionen
11:05 – Der Pool wurde für 320 Transaktionen freigegeben, die Block Nr. 11 hinzugefügt wurden
12:10 – Alle Knoten enthalten 1 Million 670 Transaktionen und Token
12:17 – Der Submit-Knoten hat 330 Transaktionen aus dem Pool übernommen und bildet Block Nr. 12
12:32 – Block Nr. 12 wird signiert und zur Validierung an andere Knoten gesendet
12:39 – Block Nr. 12 wird validiert und an die Root-Kette gesendet
13:44 – Alle Knoten haben von der Root-Kette die Information erhalten, dass Block Nr. 12 hinzugefügt wurde, und beginnen mit der Anwendung von 330 Transaktionen
14:50 – Alle Knoten enthalten 2 Millionen Transaktionen und Token

Test 3

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


Ausgangszustand: letzter Block #84; 0 Transaktionen und Token in der Datenbank 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 Submit Node Nr. 3 begann
01:50 – Submit-Knoten Nr. 3 hat 330 Transaktionen aus dem Pool übernommen und bildet Block Nr. 85 (f21). Wir sehen auch, dass in 350 Sekunden 10 Transaktionen zum Pool hinzugefügt werden
01:53 – 1 Million Transaktionen wurden erstellt und der Versand an Submit Node Nr. 1 begann
01:50 – Submit-Knoten Nr. 3 hat 330 Transaktionen aus dem Pool übernommen und bildet Block Nr. 85 (f21). Wir sehen auch, dass in 350 Sekunden 10 Transaktionen zum Pool hinzugefügt werden
02:01 – Submit-Knoten Nr. 1 hat 250 Transaktionen aus dem Pool übernommen und bildet Block Nr. 85 (65e)
02:06 – Block Nr. 85 (f21) wird signiert und zur Validierung an andere Knoten gesendet
02:08 – Demoskript von Server Nr. 3, der 1 Million Transaktionen in 30 Sekunden gesendet hat, funktioniert nicht mehr
02:14 – Block Nr. 85 (f21) wird validiert und an die Root-Kette gesendet
02:19 – Block Nr. 85 (65e) wird signiert und zur Validierung an andere Knoten gesendet
02:22 – 1 Million Transaktionen wurden erstellt und der Versand an Submit Node Nr. 2 begann
02:27 – Block Nr. 85 (65e) validiert und an die Root-Kette gesendet
02:29 – Submit-Knoten Nr. 2 hat 111855 Transaktionen aus dem Pool übernommen und bildet Block Nr. 85 (256).
02:36 – Block Nr. 85 (256) wird signiert und zur Validierung an andere Knoten gesendet
02:36 – Demoskript von Server Nr. 1, der 1 Million Transaktionen in 42.5 Sekunden gesendet hat, funktioniert nicht mehr
02:38 – Block Nr. 85 (256) wird validiert und an die Root-Kette gesendet
03:08 – Das Skript für Server Nr. 2 hat seine Arbeit beendet und 1 Million Transaktionen in 47 Sekunden gesendet
03:38 – Alle Knoten haben Informationen von der Root-Kette erhalten, dass die Blöcke #85 (f21), #86(65e), #87(256) hinzugefügt wurden und begonnen haben, 330, 250, 111855 Transaktionen anzuwenden
03:49 – Der Pool wurde bei 330, 250 und 111855 Transaktionen gelöscht, die den Blöcken Nr. 85 (f21), Nr. 86 (65e), Nr. 87 (256) hinzugefügt wurden.
03:59 – Submit-Knoten Nr. 1 nahm 888145 Transaktionen aus dem Pool auf und bildete Block Nr. 88 (214), Submit-Knoten Nr. 2 nahm 750 Transaktionen aus dem Pool und bildete Block Nr. 88 (50a), Submit-Knoten Nr. 3 nahm 670 Transaktionen aus dem Pool auf den Pool und bildet Block #88 (d3b)
04:44 – Block Nr. 88 (d3b) wird signiert und zur Validierung an andere Knoten gesendet
04:58 – Block Nr. 88 (214) wird signiert und zur Validierung an andere Knoten gesendet
05:11 – Block Nr. 88 (50a) wird signiert und zur Validierung an andere Knoten gesendet
05:11 – Block Nr. 85 (d3b) wird validiert und an die Root-Kette gesendet
05:36 – Block Nr. 85 (214) wird validiert und an die Root-Kette gesendet
05:43 – Alle Knoten haben Informationen von der Root-Kette erhalten, dass die Blöcke #88 (d3b), #89(214) hinzugefügt wurden und beginnen, 670, 750 Transaktionen anzuwenden
06:50 – aufgrund eines Kommunikationsfehlers wurde Block Nr. 85 (50a) nicht validiert
06:55 – Submit-Knoten Nr. 2 hat 888145 Transaktionen aus dem Pool übernommen und bildet Block Nr. 90 (50a)
08:14 – Block Nr. 90 (50a) wird signiert und zur Validierung an andere Knoten gesendet
09:04 – Block Nr. 90 (50a) wird validiert und an die Root-Kette gesendet
11:23 – Alle Knoten haben von der Root-Kette die Information erhalten, dass Block Nr. 90 (50a) hinzugefügt wurde, und beginnen mit der Anwendung von 888145 Transaktionen. Gleichzeitig hat Server Nr. 3 bereits Transaktionen aus den Blöcken Nr. 88 (d3b), Nr. 89 (214) angewendet.
12:11 – alle Becken sind leer
13:41 – alle Knoten von Server Nr. 3 enthalten 3 Millionen Transaktionen und Token
14:35 – alle Knoten von Server Nr. 1 enthalten 3 Millionen Transaktionen und Token
19:24 – alle Knoten von Server Nr. 2 enthalten 3 Millionen Transaktionen und Token

Hindernisse

Bei der Entwicklung von Plasma Cash sind wir auf folgende Probleme gestoßen, die wir nach und nach gelöst haben und lösen:

1. Konflikt im Zusammenspiel verschiedener Systemfunktionen. Beispielsweise blockierte die Funktion zum Hinzufügen von Transaktionen zum Pool die Arbeit zum Senden und Validieren von Blöcken und umgekehrt, was zu einem Geschwindigkeitsabfall führte.

2. Es war nicht sofort klar, wie man eine große Anzahl von Transaktionen senden und gleichzeitig die Datenübertragungskosten minimieren kann.

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

4. Es war nicht klar, wie ein Netzwerk zwischen Knoten organisiert werden sollte, da die Größe eines Blocks mit 1 Million Transaktionen etwa 100 MB einnimmt.

5. Beim Arbeiten im Single-Thread-Modus wird die Verbindung zwischen Knoten unterbrochen, wenn lange Berechnungen durchgeführt werden (z. B. beim Erstellen eines Merkle-Baums und Berechnen seines Hashs).

Wie sind wir mit all dem umgegangen?

Die erste Version des Plasma-Cash-Knotens war eine Art Kombination, die alles gleichzeitig tun konnte: Transaktionen akzeptieren, Blöcke übermitteln und validieren sowie eine API für den Zugriff auf Daten bereitstellen. Da NodeJS nativ Single-Threaded ist, blockierte die umfangreiche Merkle-Baumberechnungsfunktion die Funktion zum Hinzufügen von Transaktionen. Wir sahen zwei Möglichkeiten, dieses Problem zu lösen:

1. Starten Sie mehrere NodeJS-Prozesse, von denen jeder bestimmte Funktionen ausführt.

2. Verwenden Sie worker_threads und verschieben Sie die Ausführung eines Teils des Codes in Threads.

Aus diesem Grund haben wir beide Optionen gleichzeitig genutzt: Wir haben einen Knoten logisch in drei Teile unterteilt, die separat, aber gleichzeitig synchron arbeiten können

1. Übermittlungsknoten, der Transaktionen in den Pool akzeptiert und Blöcke erstellt.

2. Ein Validierungsknoten, der die Gültigkeit von Knoten überprüft.

3. API-Knoten – stellt eine API für den Zugriff auf Daten bereit.

In diesem Fall können Sie mit cli über einen Unix-Socket eine Verbindung zu jedem Knoten herstellen.

Wir haben schwere Operationen, wie die Berechnung des Merkle-Baums, in einen separaten Thread verschoben.

Somit haben wir den normalen Betrieb aller Plasma Cash-Funktionen gleichzeitig und ohne Ausfälle erreicht.

Sobald das System funktionsfähig war, begannen wir mit dem Testen der Geschwindigkeit und erhielten leider unbefriedigende Ergebnisse: 5 Transaktionen pro Sekunde und bis zu 000 Transaktionen pro Block. Ich musste herausfinden, was falsch implementiert wurde.

Zunächst haben wir damit begonnen, den Kommunikationsmechanismus mit Plasma Cash zu testen, um die Spitzenleistung des Systems herauszufinden. Wir haben zuvor geschrieben, dass der Plasma Cash-Knoten eine Unix-Socket-Schnittstelle bereitstellt. Ursprünglich war es textbasiert. JSON-Objekte wurden mit „JSON.parse()“ und „JSON.stringify()“ gesendet.

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

Wir haben die Übertragungsgeschwindigkeit solcher Objekte gemessen und ca. 130 pro Sekunde ermittelt. Wir haben versucht, die Standardfunktionen für die Arbeit mit JSON zu ersetzen, aber die Leistung hat sich nicht verbessert. Der V8-Motor muss für diese Einsätze gut optimiert sein.

Wir haben über Klassen mit Transaktionen, Token und Blöcken gearbeitet. Beim Erstellen solcher Klassen ist die Leistung um das Zweifache gesunken, was darauf hindeutet, dass OOP für uns nicht geeignet ist. Ich musste alles auf einen rein funktionalen Ansatz umschreiben.

Aufnahme in die Datenbank

Ursprünglich wurde Redis für die Datenspeicherung als eine der produktivsten Lösungen ausgewählt, die unsere Anforderungen erfüllt: Schlüsselwertspeicherung, Arbeiten mit Hash-Tabellen, Sets. Wir haben Redis-Benchmark gestartet und in einem Pipelining-Modus etwa 80 Vorgänge pro Sekunde erzielt.

Für eine hohe Leistung haben wir Redis feiner abgestimmt:

  • Eine Unix-Socket-Verbindung wurde hergestellt.
  • Wir haben das Speichern des Status auf der Festplatte deaktiviert (aus Gründen der Zuverlässigkeit können Sie ein Replikat einrichten und in einem separaten Redis auf der Festplatte speichern).

In Redis ist ein Pool eine Hash-Tabelle, da wir in der Lage sein müssen, alle Transaktionen in einer Abfrage abzurufen und Transaktionen einzeln zu löschen. Wir haben versucht, eine reguläre Liste zu verwenden, aber das Entladen der gesamten Liste ist langsamer.

Bei Verwendung von Standard-NodeJS erreichten die Redis-Bibliotheken eine Leistung von 18 Transaktionen pro Sekunde. Die Geschwindigkeit sank um das 9-fache.

Da uns der Benchmark zeigte, dass die Möglichkeiten deutlich um das Fünffache größer waren, begannen wir mit der Optimierung. Wir haben die Bibliothek auf ioredis umgestellt und eine Leistung von 5 pro Sekunde erreicht. Wir haben Transaktionen einzeln mit dem Befehl „hset“ hinzugefügt. Wir haben also viele Abfragen in Redis generiert. Es entstand die Idee, Transaktionen in Stapeln zusammenzufassen und mit einem Befehl „hmset“ zu versenden. Das Ergebnis sind 25 pro Sekunde.

Aus mehreren Gründen, die wir im Folgenden beschreiben werden, arbeiten wir mit Daten mithilfe von „Puffer“. Wenn Sie diese vor dem Schreiben in Text konvertieren („buffer.toString('hex')`), können Sie, wie sich herausstellt, zusätzliche Informationen erhalten Leistung. Dadurch wurde die Geschwindigkeit auf 35 pro Sekunde erhöht. Im Moment haben wir uns entschieden, die weitere Optimierung auszusetzen.

Wir mussten auf ein Binärprotokoll umsteigen, weil:

1. Das System berechnet häufig Hashes, Signaturen usw. und benötigt dazu Daten im „Puffer“.

2. Beim Senden zwischen Diensten wiegen Binärdaten weniger als Text. Wenn beispielsweise ein Block mit 1 Million Transaktionen gesendet wird, können die Daten im Text mehr als 300 Megabyte beanspruchen.

3. Ständige Datentransformationen wirken sich auf die Leistung aus.

Daher haben wir unser eigenes binäres Protokoll zum Speichern und Übertragen von Daten als Grundlage genommen, das auf der Grundlage der wunderbaren „Binary-Data“-Bibliothek entwickelt wurde.

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

- Zeichen

  ```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)“ konvertieren wir die Daten in „Buffer“, um sie in Redis zu speichern oder an einen anderen Knoten weiterzuleiten und abzurufen Daten zurück.

Wir verfügen außerdem über zwei Binärprotokolle für die Datenübertragung zwischen Diensten:

— Protokoll für die Interaktion mit Plasma Node über 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)
  }
  ```

wo:

  • `Type` – die auszuführende Aktion, zum Beispiel 1 – sendTransaction, 2 – getTransaction;
  • „Nutzlast“. – Daten, die an die entsprechende Funktion übergeben werden müssen;
  • „messageId“. — Nachrichten-ID, damit die Antwort identifiziert werden kann.

– Protokoll für die 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)
  }
  ```

wo:

  • "Code". – Nachrichtencode, zum Beispiel 6 – PREPARE_NEW_BLOCK, 7 – BLOCK_VALID, 8 – BLOCK_COMMIT;
  • `versionProtocol` — Protokollversion, da Knoten mit unterschiedlichen Versionen im Netzwerk erstellt werden können und unterschiedlich funktionieren können;
  • `seq` — Nachrichtenkennung;
  • `countChunk` и „chunkNumber“. notwendig zum Aufteilen großer Nachrichten;
  • „Länge“. и „Nutzlast“. Länge und die Daten selbst.

Da wir die Daten vorab eingegeben haben, ist das endgültige System viel schneller als die „rlp“-Bibliothek von Ethereum. Leider konnten wir dies noch nicht ablehnen, da der Abschluss des Smart Contracts erforderlich ist, was wir in Zukunft planen.

Wenn es uns gelingt, die Geschwindigkeit zu erreichen 35 000 Da wir Transaktionen pro Sekunde durchführen, müssen wir sie auch in der optimalen Zeit verarbeiten. Da die ungefähre Blockbildungszeit 30 Sekunden beträgt, müssen wir sie in den Block aufnehmen 1 000 000 Transaktionen, was bedeutet, dass mehr gesendet werden 100 MB Daten.

Ursprünglich verwendeten wir die Bibliothek „ethereumjs-devp2p“ für die Kommunikation zwischen Knoten, diese konnte jedoch nicht so viele Daten verarbeiten. Aus diesem Grund haben wir die „ws“-Bibliothek verwendet und das Senden von Binärdaten über Websocket konfiguriert. Natürlich hatten wir auch Probleme beim Versenden großer Datenpakete, aber wir haben sie in Blöcke aufgeteilt und jetzt sind diese Probleme verschwunden.

Außerdem wird ein Merkle-Baum erstellt und der Hash berechnet 1 000 000 Transaktionen erfordern etwa 10 Sekunden kontinuierlicher Berechnung. Während dieser Zeit kann die Verbindung zu allen Knoten unterbrochen werden. Es wurde beschlossen, diese Berechnung in einen separaten Thread zu verschieben.

Schlussfolgerungen:

Tatsächlich sind unsere Erkenntnisse nicht neu, aber aus irgendeinem Grund vergessen viele Experten sie bei der Entwicklung.

  • Die Verwendung funktionaler Programmierung anstelle objektorientierter Programmierung verbessert die Produktivität.
  • Der Monolith ist schlechter als eine Servicearchitektur für ein produktives NodeJS-System.
  • Die Verwendung von „worker_threads“ für umfangreiche Berechnungen verbessert die Reaktionsfähigkeit des Systems, insbesondere bei E/A-Vorgängen.
  • Der Unix-Socket ist stabiler und schneller als HTTP-Anfragen.
  • Wenn Sie große Datenmengen schnell über das Netzwerk übertragen müssen, ist es besser, Websockets zu verwenden und in Blöcke unterteilte Binärdaten zu senden, die weitergeleitet werden können, wenn sie nicht ankommen, und dann in einer Nachricht zusammengefasst werden.

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

Der Artikel wurde mitgeschrieben von Alexander Nashivan, leitender Entwickler Clever Solution Inc.

Source: habr.com

Kommentar hinzufügen