Parallelitätsmuster für das Konto Nonce

Das Konto Nonce ist das einzige Element in der Transaktionsstruktur, das einen Replay-Angriff verhindert (wie in dieser sehr guten Antwort erläutert ). Es erzwingt im Wesentlichen die Sequenzialisierung der Transaktionen eines Kontos.

Dies verursacht normalerweise keine Probleme, wenn Transaktionen über die web3-Schnittstelle an einen Ethereum-Knoten (z. B. Geth, Parity) gesendet werden, der dann das Signieren durchführt. Die Sequenzierung erfolgt intern durch den Knoten und ist somit für den Endbenutzer transparent.

Dies ist nicht der Fall, wenn Transaktionen von einer Benutzeranwendung (dh nicht Geth oder Parity) erstellt und signiert werden, bevor sie an einen Knoten gesendet werden (zB durch web3.eth.sendRawTransaction).

Die Erstellung der Rohtransaktion erfordert das Abrufen einer Konto-Nonce, was durch web3.eth.getTransactionCount erfolgen kann, wie in dieser Frage erläutert . Dies funktioniert gut, wenn keine Parallelität beteiligt ist.

Stellen Sie sich jedoch einen Endbenutzer vor, der mehrere Transaktionen parallel signieren muss. Die Transaktionssequentialisierung wird eine Verantwortung der Benutzeranwendung.

"web3.eth.getTransactionCount" gibt nur bestätigte Transaktionszahlen zurück (siehe diese Frage ). Wenn Transaktionen parallel signiert werden, wird daher nur eine von ihnen erfolgreich sein, es sei denn, wir manipulieren die Zählung auf intelligente Weise.

Meine Frage ist: Was sind einige gute Muster, um mit dieser Situation umzugehen?

Kandidat 1: mehrere Schlüssel/Konten pro Benutzer

Dies ermöglicht gleichzeitige Transaktionen. Es führt jedoch zu zusätzlicher Komplexität in der Benutzeranwendung. Außerdem ist es nicht zu skalierbar: Überlegen Sie, ob Dutzende oder Hunderte gleichzeitiger Transaktionen erforderlich sind. Angesichts der Tatsache, dass die hierachische deterministische Brieftasche in Ethereum nicht nativ ist (ich weiß, dass es die LightWallet gibt ), klingt dies nicht nach einer einfachen Lösung.

Kandidat 2: Schleife und erneuter Versuch

Dadurch wird eine Schleife erstellt, die das Sendeergebnis der Transaktion überprüft. Wenn das Rückgabeergebnis eine doppelte Nonce anzeigt (z. B. würde Geth „Ersatztransaktion unterbewertet“ zurückgeben), versuchen Sie es erneut mit einer neuen Nonce.

Diese Lösung ist nicht portabel: Verschiedene Clients geben für denselben Fehler unterschiedliche Zeichenfolgen zurück. In der Praxis scheint mir aufgefallen zu sein, dass Geth manchmal nicht einmal die obige Zeichenfolge zurückgibt, sondern stillschweigend die anderen Transaktionen verwirft (nicht bestätigt, da ich keinen zuverlässigen Weg zur Reproduktion gefunden habe). Dadurch funktioniert die Erkennung nicht zuverlässig.

Kandidat 3: ein Singleton-'Nonce-Manager'

Jede Transaktionskonstruktion beinhaltet die "Beantragung" einer Nonce von einem globalen "Nonce-Manager". Der Singleton-"Nonce-Manager" kann über eine intelligente Logik verfügen, um aufsteigende Nonce-Nummern zu verteilen. Dies nutzt das hier diskutierte Verhalten aus .

Dies führt nicht nur zu einem Singleton (möglicherweise ein Anti-Pattern), sondern es klingt auch nicht einfach, es richtig zu machen: Wenn eine Low-Nonce-Transaktion irgendwie fehlschlägt (z. B. nicht einmal an das Netzwerk gesendet wird), der Rest der Transaktionen werden auf unbestimmte Zeit stecken bleiben. Im Allgemeinen ist es eine schlechte Lösung, die mehr Komplexität und Probleme mit sich bringt, als zu versuchen, sie zu lösen.

Kandidat 4: Delegieren Sie an einen lokalen Geth/Parity-Knoten

Das ist nicht wirklich eine Lösung. Das offensichtlichste Problem ist, dass die zusätzliche Abhängigkeit nicht gerechtfertigt zu sein scheint, wenn sie nur für das Nonce-Feld gilt.

Wenn die Benutzeranwendung bereits private Schlüssel verwaltet, führt dies zu zusätzlicher Komplexität: Die privaten Schlüssel müssen vom Geth- oder Paritätsknoten gemeinsam verwaltet (oder kopiert) werden. Auch hier erscheint die Komplexität (und potenzielle Schwachstelle) nicht vertretbar, wenn es nur um den Umgang mit der Nonce geht.


Off-Topic. Es scheint mir, dass das Konto Nonce nicht das beste Design in Ethereum ist. Es zwingt Benutzeranwendungen, entweder Low-Level-Protokolldetails zu handhaben oder eine Abhängigkeit von einem Knoten (Geth/Parity) einzuführen. In jedem Fall fügt es den Benutzeranwendungen eine unverhältnismäßige Sperrigkeit hinzu.

Antworten (3)

Ich bin noch nicht an dem Punkt angelangt, an dem ich eines dieser Dinge testen kann, aber mein Bauchgefühl ist, dass der Singleton Nonce Manager mit einigen Verbesserungen der richtige Weg wäre:

  • Machen Sie es zu einem Singleton-Transaktionssender
  • Das hat einen maximalen Puffer oder 'Kopf' von ausstehenden Transaktionen pro 'Von'-Adresse, der mit der maximalen Anzahl von Transaktionen übereinstimmt oder kleiner ist als die maximale Anzahl von Transaktionen, die der Peer in seiner Transaktionspool-Warteschlange pro Von-Adresse haben kann. Nennen wir diese ausstehenden Transaktionen „Slots“.
  • Ist als fortlaufende Warteschlange implementiert, in der die ältesten bestätigten Transaktionen aus dem „Schwanz“ gelöscht werden.
  • Jeder neue Slot erhält eine neue Nonce
  • Jeder neue Slot ist einem asynchronen Aufruf von sendRawTransaction zugeordnet. Bei einem Ausfall jeglicher Art wird der Steckplatz frei.
  • Alle neuen Transaktionen werden dem niedrigsten freien Slot hinzugefügt.
  • Für den Fall, dass die höheren Slots für mehr als N Millisekunden anstehen, nachdem ein Slot aufgrund eines Fehlers freigegeben wurde, stornieren Sie die höchste Transaktion, indem Sie 0 eth an sich selbst mit derselben Nonce wie die Nonce des höchsten Slots senden und diese Transaktion in dem freigegebenen Slot wiederholen.

Die Implementierungsdetails würden eine genaue Vertrautheit mit gleichzeitiger Programmierung, Sperren, Mutex, Asynchronität oder was auch immer erfordern. Im Wesentlichen ist das Obige eine Vermutung, da ich noch nicht direkt mit den RPC-Aufrufen gearbeitet habe und es nicht testen kann, aber ich denke, das ist das Grundgerüst der Route, die ich gehen würde. Ich sehe, dass die Warteschlangenstruktur eine interne API zum Auflisten von ausstehenden Transaktionen, verarbeiteten und unterstützenden anderen UI-Operationen als Bonus bieten würde.

Nettes Design. Danke vielmals. Der Ringpuffer sieht nach einer genialen Lösung aus.
'Ringpuffer.' Ja. Das ist ein viel besserer Ausdruck als „rollende Schlange“. 👍
Nachdem ich tagelang gedacht habe, dass ich verrückt werde und meine Programmierung einfach scheiße ist (was sie tut), ist dies das gleiche Problem, mit dem ich konfrontiert bin: Potenziell viele von mehreren Anwendungen signierte Transaktionen gleichzeitig mit Nonce-Verwaltung. Ethers.js, eine „bessere“ Version von Web3, hat einen kommenden NonceManager, aber der Nonce-Zähler ist immer noch auf das Netzwerk angewiesen, um die Transaktionsanzahl zu erhalten, also bin ich mir nicht ganz sicher, wie das Problem gelöst werden soll. Hast du, @LinmaoSong, in den letzten zwei Jahren etwas entdeckt?
@EvilJordan sorry leider nicht. Es ist ein Singleton-Muster auf Protokollebene, also ist ein Singleton-Manager am wenigsten schlimm
Warum muss die Anzahl der Slots im Nonce-Puffer " weniger als die maximale Anzahl von Transaktionen sein, die der Peer in seiner Transaktionspool-Warteschlange pro Absenderadresse haben kann "? Mir scheint, wir brauchen diese Einschränkung nicht, solange wir nicht versuchen, alle Transaktionen gleichzeitig zu veröffentlichen.

getTransactionCount kann auch ausstehende Transaktionen enthalten, wodurch die Nonce für eine nachfolgende Rohtransaktion besser bestimmt werden kann. Diese Lösung ist einfach, aber unwahrscheinlich ohne eigene Probleme. Aber für das Abfeuern einer Handvoll Transaktionen in enger Folge scheint es wie erwartet zu funktionieren.

nonce: web3.toHex(web3.eth.getTransactionCount(fromAddress, "pending")),
Danke Ryanh. Dies funktioniert nur, wenn eine Transaktion erst erstellt wird, nachdem andere Transaktionen an gesendet wurden UND bereits in den Transaktionspool aufgenommen wurden. Stellen Sie sich zwei Transaktionen vor, die gleichzeitig erstellt werden, selbst mit dem Flag „ausstehend“ werden sie dasselbe Ergebnis abrufen. Das Problem besteht also weiterhin.
@LinmaoSong Ich habe die Rückgabe von getTransactionCount() immer noch unverändert (dh nicht inkrementiert) gehabt, nachdem ich den Tx-Hash erhalten habe, und habe nicht gesehen, dass er inkrementiert wurde, bis die Transaktion abgebaut wurde. Beachten Sie, dass dies durch Web3.js ging, falls dies wichtig ist. Aus diesem Grund erhielt ich doppelte Nonces.

Es hängt alles von Ihrem Kontext ab. Wenn Sie ein Endbenutzer sind, der versucht, selbst mehrere Transaktionen auszuführen, können Sie die Nonce einfach irgendwo speichern und bei jeder parallelen Transaktion erhöhen. Natürlich möchten Sie vermeiden, Lücken in der Nonce zu schaffen, aber solange Sie zu 100 % sicher sind, dass Sie die Mittel decken können, wenn die Transaktionen ausgeführt werden, ist eine Lücke vorübergehend in Ordnung. Das Schlimmste, was passieren kann, ist, dass eine Nonce zweimal gespielt wird. In diesem Fall haben Sie ungültige Transaktionen erstellt.

Wenn Sie im Namen eines Benutzers handeln und versuchen, Massentransaktionen auszuführen, könnten Sie einfach eine atomare Datenbankoperation verwenden , um die Nonce zu speichern. Dies könnte in einer hypothetischen Batching-Wallet funktionieren, die versucht, mehrere Transaktionen gleichzeitig zu senden. Sie möchten wahrscheinlich einen Haftungsausschluss darauf setzen, dass ein Benutzer die Verwendung des Schlüssels vermeiden sollte , während er mit der Brieftasche interagiert, um zu verhindern, dass der interne Zähler nicht mehr synchron ist.

Danke. Es scheint mir, dass das Speichern der Nonce (in DB oder wo auch immer) im Wesentlichen das oben erwähnte Singleton-Muster ist. Wie würden Sie mit dem Problem des Transaktionsfehlers bei niedriger Nonce umgehen?
Ich meine, ich würde definitiv den Weg gehen, um sicherzustellen, dass eine Sendung auf andere Weise stattfindet. Legen Sie beispielsweise jeden Gaspreis dynamisch über dem mittleren Gaspreis fest, um sicherzustellen, dass das Netzwerk ihn tatsächlich verarbeitet. Erhöhen Sie vielleicht den Benzinpreis ein wenig, um sicherzustellen, dass er immer „der Zeit voraus“ ist. Sie können jederzeit eine Transaktion oder eine Reihe von Transaktionen beobachten, und wenn es scheint, dass eine fehlende Transaktion nicht vorhanden ist, übertragen Sie einfach die fehlende Transaktion erneut. Es ist eine Menge Buchhaltungsarbeit, aber es lohnt sich möglicherweise.