Einführung
Hey, ich bin Eugene und dies ist der zweite Teil der Serie zum Live Contract Review. Heute werden wir den Staking Pool Contract besprechen, der im Moment verwendet wird, um das NEAR Protocol Proof of Stake System zu sichern. Im Grunde genommen operieren alle Validatoren, die derzeit auf dem NEAR Protocol laufen, im Namen dieses Contracts. Sie kontrollieren nicht selbst den Account, welcher die für den Proof of Stake erforderliche Menge an NEAR-Token staked, sondern der Contract staked diese Menge an Tokens, und die Validatoren stellen lediglich einen Staking-Pool bereit und betreiben ihre Nodes. Heute werden wir diesen Contract genauer unter die Lupe nehmen. In den Kern Contracten haben wir einen Staking-Pool-Contract, der etwas komplizierter ist als der vorherige Contract, den wir reviewed haben (der Voting-Contract). Daher werden wir uns heute mehr auf die Logik und weniger auf die near_bindgen und Rust spezifischen Dinge konzentrieren, aber es wird wahrscheinlich ein wenig mehr Wissen über das NEAR-Protokoll erfordern. Hier ist der Contract für den Staking Pool auf dem NEAR Github Account. Unten ist das Originalvideo, auf welchem diese Anleitung basiert.
lib.rs
Constructor Structure
As before, the contract starts from the main structure. In this case it’s a staking contract structure. As you can see there’s near_bindgen, and BorshSerialize and BorshDeserialize. The structure now has way more fields than the past one, and there are some comments on them, most of them are probably up to date. The logic of the staking pool contract allows us to do the following: basically anyone can deposit some amount of NEAR tokens to the staking pool, and delegate them to the staking pool by staking them in the pool. That allows us to bundle together balances from multiple people (we call them accounts here) into one large stake, and this way this large state may actually qualify for validator seats. NEAR Protocol has a limited amount of seats for a single shard right now, there’s at most 100 validator seats. You can think about the seats in the following way: if you take the total amount of token staked and divide it by 100 the result will be the minimum amount of tokens required for a single seat, except it’s a little bit more complicated to involve removing the stakes that are not qualified to contain this minimum amount, etc. This contract basically is a standalone contract without any access key on it that is controlled by the owner. In this case the owner is provided in the initialization method.
Initialization Method
Gehen wir also zur Initialisierungsmethode. Sie hat drei Parameter. Der erste ist owner_id, die Konto-ID des Besitzerkontos. Der Eigentümer verfügt über eine Reihe von Berechtigungen für diesen Contract, die es dem Contract erlauben, Aktionen auszuführen, die für den Rest der Accounts nicht verfügbar sind. Eine dieser Methoden besteht darin, im Namen des staking pools für den Voting-Contract abzustimmen, den wir beim letzten Mal besprochen haben. Der Eigentümer kann also die Abstimmungsmethode aufrufen.
Wir überprüfen dann, ob der Vorgänger gleich dem Besitzer ist, da diese Methode nur vom Besitzer aufgerufen werden kann.
Die Vote-Methode prüft also, ob die Methode nur vom Eigentümer aufgerufen wurde, und überprüft dann eine gewisse Logik, aber das ist im Moment nicht wichtig.
Der Contract ist also der Eigentümer welcher durch seine erweiterten Berechtigungen mehr Dinge tun kann, als alle anderen Accounts im Netzwerk. Dann werden noch ein paar weitere Felder abgefragt: der stake_public_key. Wenn Sie sich am NEAR-Protokoll beteiligen, müssen Sie einen öffentlichen Schlüssel angeben, der von Ihrem Validator-nodes verwendet wird, um Nachrichten im Namen des Validator-nodes zu signieren. Dieser öffentliche Schlüssel sollte einzigartig sein da Ihr Knoten möglicherweise in einem Rechenzentrum betrieben wird, das für einige Angriffe anfällig ist. In diesem Fall können die Angreifer höchstens dem Netzwerk schaden, aber nicht Ihrem Konto. Sie können Ihr Guthaben nicht stehlen, und Sie können diesen Schlüssel leicht ersetzen, verglichen mit dem, was Sie für den Ersatz des mangelhaften Zugangsschlüssels benötigen. Das dritte Argument des Vertrags ist die anfängliche Reward_fee_fraction. Dies ist die Provision, die der Eigentümer des Staking Pools für den Betrieb des Validierungs Knotens erhält.
Dies ist ein Bruch, der einen Zähler und einen Nenner hat, und es erlaubt Ihnen im Grunde zu sagen “Ich nehme 1% der Belohnungen für den Betrieb dieses bestimmten Pools”, zum Beispiel. Sagen wir, Sie haben 1 000 000 Token, die eine Belohnung erhalten haben, sagen wir, es gibt eine Belohnung von 10 000 Token, dann wird der Besitzer 1% davon nehmen, was 100 Token sind. Floats verhalten sich unvorhersehbar, wenn man sie multipliziert. Bei Brüchen kann man zum Beispiel mit einer größeren Anzahl von Bits rechnen. Bei der Division multipliziert man zum Beispiel zuerst den Betrag, der u128 ist, mit einem Zähler (dieser kann schon bei u128 überschreiten), aber deshalb machen wir das ja in u256. Dann dividiert man ihn durch den Nenner, was ihn wieder unter u128 bringen sollte. Dadurch erhält man eine höhere Genauigkeit als bei float64, das nicht mit u128-Bit-Präzision arbeiten kann, so dass es bei der Berechnung zu Rundungs- oder Präzisionsfehlern kommen kann. Sie benötigen also entweder eine höhere Genauigkeit von Floats, die sich nicht wirklich von der Mathematik unterscheiden, bei der wir dies mit u256 simulieren. Ursprünglich unterstützte Solidity keine Floats, und auch wir taten das ursprünglich nicht, aber das warf einige Probleme bei der String-Formatierung in Rust für das Debugging auf, so dass wir beschlossen, dass es nicht schadet, Floats zu unterstützen, besonders da wir dies auf der vm-Seite standardisieren. Das größte Problem mit Floats war das undefinierte Verhalten bei bestimmten Werten von Lasten. Zum Beispiel, was andere Bits enthalten, wenn man einen unendlichen Float hat. Wir haben dies standardisiert, und jetzt sind sie plattformunabhängig gleichwertig. Es ist also kein Problem, Floats jetzt in unserer VM-Umgebung zu verwenden.
Die Standardpraxis bei init ist, dass wir zuerst prüfen, ob der Wert nicht vorhanden ist. Dann überprüfen wir die Eingabe. Als Erstes wird überprüft, ob der Bruch gültig ist und ob der Nenner nicht Null ist. Anschließend wird mit einer else-Anweisung überprüft, ob der Zähler kleiner oder gleich dem Nenner ist, d. h. ob der Bruch kleiner oder gleich 1 ist. Das ist wichtig, um einige logische Fehler zu vermeiden. Als Nächstes überprüfen wir, ob das Konto gültig ist. Dieser Contract wurde geschrieben, bevor es einige der Hilfsmetriken gab, die heute existieren. Zum Beispiel haben wir die gültige Konto-ID in JSON-Types, die diese Prüfung automatisch während der Deserialisierung durchführt, wenn sie ungültig ist, wird sie einfach in einen Panic-Modus versetzt. Danach ziehen wir den aktuellen Kontostand des Staking Contracts. Dieser Kontostand ist in der Regel groß genug, weil er für die Speicherung dieses speziellen Contracts bezahlt werden muss, und dann sagen wir, dass wir einige Token für den STAKE_SHARE_PRICE_GUARANTEE_FUND zuteilen werden. Der Staking Pool hat bestimmte Garantien, die für lokale Contracts wichtig sind. Garantien stellen sicher, dass Sie, wenn Sie in den Staking Pool einzahlen, in der Lage sein sollten, mindestens die gleiche Menge an Token abzuheben, und Sie können keine Token verlieren, auch nicht für so viel wie 1 000 000 000 000 yocto NEAR auf diesen Vertrag, indem Sie in den Staking Pool einzahlen und abheben. Der STAKE_SHARE_PRICE_GUARANTEE_FUND-Fonds beläuft sich auf etwa 1 Billion Yocto NEAR, während wir normalerweise etwa 1 oder 2 Billionen Yocto NEAR für Rundungsfehler verbrauchen. Schließlich erinnern wir uns daran, wie hoch der Saldo ist, den wir im Namen dieses Vertrags einsetzen werden. Dies ist erforderlich, um eine Ausgangsbasis für die Begrenzung der Rundungsdifferenzen zu schaffen. Als Nächstes überprüfen wir, ob das Konto nicht bereits einen Stack eingesetzt hat. Dies könnte eine gewisse Logik durchbrechen. Das soll nicht passieren, also initialisieren wir den Contract, bevor er etwas staked. Schließlich initialisieren wir die Struktur, aber wir geben sie nicht sofort zurück. Wir haben hier gerade die Struktur :StakingContract erstellt.
Dann führen wir eine Wiederherstellungs-Transaktion durch. Dies ist wichtig, denn wir müssen sicherstellen, dass der bereitgestellte Staking-Schlüssel ein gültiger, auf Ristretto beschränkter Schlüssel ist, z. B. ein 5 119 gültiger Schlüssel. Es gibt einige Schlüssel auf der Kurve, die zwar gültig, aber nicht ristretto-spezifisch sind, und Validierungsschlüssel können nur ristretto-spezifisch sein. Dies ist eine NEAR-Protokoll-spezifische Sache, und was passiert, ist, dass es eine Staking-Transaktion mit dem gegebenen Schlüssel macht. Sobald diese Transaktion aus dem Contract erstellt wurde, validieren wir diese Transaktion, wenn sie den Contract verlässt. Wenn der Schlüssel ungültig ist, wird ein Fehler ausgegeben und die gesamte Initialisierung des Staking Pools schlägt fehl. Wenn Sie einen ungültigen stake_public_key als Eingabe übermitteln, wird Ihre Vertragskonsolidierung und -bereitstellung sowie alles, was in dieser einen Batch-Transaktion geschieht, rückgängig gemacht. Dies ist wichtig, damit der Pool keinen ungültigen Schlüssel hat, denn dadurch könnten Sie den Stake anderer Leute blockieren. Als Teil der Garantien sagen wir, dass Ihre Token in 4 Epochen zurückgegeben werden, wenn Sie Ihren Einsatz zurücknehmen. Sie können dann abgehoben werden, und das ist wichtig, um sie an die Lockups zurückgeben zu können.
Ich denke, das sind zu viele Details, bevor ich den allgemeinen Überblick darüber erkläre, wie Contracts und Guthaben funktionieren. Lassen Sie uns das Konzept erklären, wie wir tatsächlich Belohnungen an Kontoinhaber in konstanten Zeitabschnitten verteilen können, wenn eine Epoche vergeht. Dies ist für die meisten Smart Contracts wichtig. Sie wollen in konstanten Zeitabschnitten für jede Methode agieren, anstatt in linearer Zeit für die Anzahl der Nutzer, denn wenn die Anzahl der Nutzer wächst, dann wächst auch die Menge an Gas, die für den Betrieb einer linearen Skala erforderlich ist, und irgendwann geht das Gas aus. Deshalb müssen alle Smart Contracts in konstanter Zeit agieren.
Account Struktur
Die Art und Weise, wie es funktioniert, ist, dass wir für jeden Benutzer eine Struktur namens Account haben. Jeder Benutzer, der an diesen Staking-Pool delegiert hat, hat eine Struktur namens account, die die folgenden Felder enthält: unstaked ist der Saldo in yocto NEAR, der nicht gestaked ist, also nur der Saldo des Benutzers. Stake_shares ist der Saldo, aber nicht in NEAR, sondern in der Anzahl der Stake-Shares. Stake_shares ist ein Konzept, das zu diesem speziellen Stake-Pool hinzugefügt wurde. Wenn Sie einen Stake tätigen, kaufen Sie im Grunde neue Anteile zum aktuellen Preis, indem Sie Ihr Guthaben in Stake-Anteile umwandeln. Der Preis eines Anteils ist ursprünglich 1, aber mit der Zeit wächst er mit den Belohnungen, und wenn das Konto Belohnungen erhält, erhöht sich sein Gesamtguthaben, aber die Anzahl der Anteile ändert sich nicht. Wenn ein Konto Validierungsprämien oder andere Einzahlungen direkt auf das Guthaben erhält, erhöht sich der Betrag, den Sie für jeden stake share erhalten können. Nehmen wir zum Beispiel an, Sie hatten ursprünglich 1 Million NEAR, die auf dieses Konto eingezahlt wurde. Sagen wir, Sie erhalten 1 Million Anteile (ignorieren Sie die yocto NEAR für jetzt), wenn der Staking-Pool 10 000 NEAR an Belohnungen erhalten hat, haben Sie immer noch 1 Million Anteile, aber die 1 Million Anteile entsprechen jetzt 1 010 000 NEAR. Wenn nun jemand anderes zu diesem Zeitpunkt einen Einsatz tätigen möchte, wird er intern innerhalb des Contracts Anteile zum Preis von 1,001 NEAR kaufen, da jeder Anteil nun so viel wert ist. Wenn Sie eine weitere Belohnung erhalten, brauchen Sie trotz des Gesamtguthabens keine weiteren Anteile zu kaufen, und in der konstanten Zeit teilt jeder die Belohnung proportional zur Anzahl der Anteile, die er hat. Wenn Sie nun den Einsatz aufheben, verkaufen Sie im Wesentlichen diese Anteile oder verbrauchen sie unter Verwendung des Konzepts der fungiblen Token zugunsten des nicht gesetzten Guthabens. Wenn Sie also zum aktuellen Preis verkaufen, verringern Sie sowohl den Gesamtbetrag des Einsatzes als auch die Gesamtzahl der Anteile, und wenn Sie kaufen, erhöhen Sie den Gesamtbetrag des Einsatzes und die Gesamtzahl der Anteile, während der Preis konstant bleibt. Wenn Sie Ihren Einsatz erhöhen oder verringern, ändert sich der Preis nicht, wenn Sie die Belohnungen erhalten, erhöht sich der Preis.
Der Preis kann nur steigen, und das kann zu Rundungsfehlern führen, wenn Ihr yocto NEAR und Ihr Saldo nicht genau übereinstimmen. Deshalb haben wir diesen Garantiefonds von 1 Billion Yocto NEAR, der ein paar Mal einen zusätzlichen Yocta NEAR in den Mix werfen wird. Und schließlich ist da noch der letzte Teil, denn das NEAR-Protokoll hebt den Einsatz nicht sofort auf und gibt das Guthaben zurück, sondern muss drei Epochen warten, bis Ihr Guthaben freigegeben und auf das Konto zurückgeführt wird. Wenn Sie die Einsätze aufheben, können Sie das Guthaben nicht sofort aus dem Einsatzpool abheben, sondern müssen drei Epochen lang warten. Erinnern Sie sich dann daran, in welcher Epoche Sie die letzte Aktion zum Aufheben des Einsatzes aufgerufen haben, und nach drei Epochen wird Ihr Guthaben wieder freigegeben, und Sie sollten es aus dem Einsatzpool abheben können. Es gibt jedoch eine Einschränkung: Wenn Sie die Freigabe im letzten Block der Epoche aufrufen, kommt das eigentliche Versprechen, das die Freigabe vornimmt, erst in der nächsten Epoche. Es kommt im ersten Block der nächsten Epoche an, und das verzögert die Freigabe des gesperrten Saldos auf vier statt drei Epochen. Das liegt daran, dass wir die Epoche im vorherigen Block aufgezeichnet haben, die eigentliche Transaktion aber im nächsten Block, in der nächsten Epoche, stattfand. Damit das nicht passiert, sperren wir den Saldo um vier Epochen statt um drei Epochen, um diesen Ausnahmefall zu berücksichtigen. Das ist es, was ein Konto ausmacht. Die Idee der Anteile ist nicht so neu, denn auf Ethereum verwenden die meisten Liquiditätsanbieter und automatisierten Market Maker dieses ähnliche Konzept. Wenn Sie zum Beispiel in den Liquiditätspool einzahlen, erhalten Sie eine Art Token aus diesem Pool anstelle des tatsächlichen Betrags, der dort vertreten ist. Wenn Sie aus dem Liquiditätspool abheben, verbrennen Sie diesen Token und erhalten die tatsächlich repräsentierten Token. Die Idee ist sehr ähnlich wie die, sie als Aktien zu bezeichnen, weil sie einen zugehörigen Preis haben, wir hätten sie auch anders nennen können. Das war fast von Anfang an der Fall bei diesem Staking-Pool-Contract. Es wurde erforscht, wie wir das richtig machen können, und eine Möglichkeit war, dass wir die Anzahl der Konten begrenzen, die auf ein bestimmtes Pool-Konto für diese spezielle Aktualisierung einzahlen können. Schließlich haben wir uns für die konstante Komplexitätszeit entschieden, und das war tatsächlich ein einfacheres Modell. Dann wurde die Berechnung der stake_shares-Struktur einigermaßen vernünftig, obwohl auch hier einiges zu berücksichtigen ist.
Contracts-Arten
Gehen wir diesen Contract einmal durch. Er ist nicht so gut strukturiert wie z. B. ein Lockup-Vertrag, denn ein Lockup ist noch komplizierter. Die Typen sind immer noch im selben Vertrag gebündelt. Es gibt eine Reihe von Arten von Typen, zum Beispiel ist reward_fee_fraction ein eigener Typ.
Account ist ein separater Typ, und es gibt auch ein für den Menschen lesbares Konto, das ebenfalls ein Typ ist, der nur für Aufrufe der Ansicht verwendet wird, also intern nicht für Logik verwendet wird.
Wenn wir dann mit allen Typen fertig sind, haben wir cross contract calls, die ein High-Level-Interface verwenden.
Es gibt zwei davon. Die Art und Weise, wie es funktioniert, ist, dass Sie ein Makro von near_bindgen namens ext_contract (steht für externen Contract) haben. Sie können ihm einen kurzen Namen geben, den er generiert und den Sie verwenden können. Dann haben Sie eine Trait-Beschreibung, die die Schnittstelle des externen Contracs beschreibt, den Sie verwenden wollen. Hier wird beschrieben, dass Sie eine Vote-Methode auf einem externen Vertrag aufrufen und ein Attribut übergeben können. Das Attribut is_vote ist ein boolescher Wert, der true oder false ist. Jetzt können Sie ein Versprechen erstellen, wenn Sie es brauchen, und ein Positionsargument anstelle eines serialisierten JSON-Arguments übergeben. Das Makro wird hinter den Kulissen in Low-Level-Promise-Apis eingebaut. Die zweite Schnittstelle ist für einen Callback auf unser Selbst, das ist ziemlich häufig, können Sie es ext_self nennen. Wenn Sie einen Callback benötigen und etwas mit dem Ergebnis des asynchronen Versprechens machen wollen, können Sie diese Art von Interface verwenden. Wir prüfen, ob die Staking-Aktion erfolgreich war. Schließlich haben wir diesen Hauptimplementierungsstruktur-Implementierungskörper des Staking-Pools.
Contract Dateistruktur
Dieser Contract ist in mehrere Module aufgeteilt.
Sie haben libs.rs, welche die Hauptdatei ist, und Sie haben auch ein internes Modul. Das interne Modul hat die Implementierung ohne das near_bindgen-Makro, so dass keine dieser Methoden sichtbar ist, um durch einen Contract von jemand anderem in der Blockchain aufgerufen zu werden. Sie können nur innerhalb dieses Contracts intern aufgerufen werden, womit sie keine JSON-Formate erzeugen und keinen Zustand deserialisieren. Sie verhalten sich alle wie normale Rust-Methoden. Wie dieser Vertrag auf hohem Niveau funktioniert ist, dass, wenn eine Epoche vergeht Sie bestimmte Belohnungen als Validator erwerben können.
Wichtige Methoden des Contracts
Wir haben eine ping-Methode, die den Contract anpingt. Die Ping-Methode prüft, ob eine Epoche vergangen ist, und dann müssen wir die Belohnungen verteilen. Wenn sich die Epoche geändert hat, wird sie auch neu gestartet, da sich die Höhe des Gesamteinsatzes, den der Contract zu leisten hat, ändern könnte. Der nächste Schritt ist die Einzahlung.
Die Deposit-Methode ist eine payable, was bedeutet, dass sie eine angehängte Einzahlung akzeptieren kann. Dies ähnelt dem Ethereum-Dekorator, der es Ihnen ermöglicht, Geld nur für die Methoden zu erhalten, die es erwarten. Daher wird near_bindgen standardmäßig in einen Panikzustand geraten, wenn Sie versuchen, eine Methode aufzurufen, z. B. ping, und eine Einzahlung an diese Methode anhängen. Folglich erlaubt uns payable, Einzahlungen anzuhängen. In jeder Methode gibt es einen internen Ping, um sicherzustellen, dass wir vorherige Belohnungen verteilt haben, bevor wir irgendeine Logik ändern. Die gemeinsame Struktur besteht darin, dass wir, wenn wir neu einzahlen müssen, zuerst die Logik ändern und dann neu einzahlen.
Die nächste Methode ist deposit_and_stake. Dies ist eine Kombination aus zwei Methoden. Zum einen zahlen Sie das Saldo auf den Einsatzsaldo Ihres Accounts ein, und zum anderen wollen Sie den gleichen Betrag sofort einsetzen, anstatt zwei Transaktionen durchzuführen. Sie ist auch payable, weil sie auch eine Einzahlung akzeptiert.
Die nächste Methode ist withdraw_all. Sie versucht, das gesamte nicht eingesetzte Guthaben von dem Account abzuheben, der ihn aufgerufen hat. Wenn Sie mit dem Stake-Pool interagieren, müssen Sie mit dem Account interagieren, dem das Guthaben gehört. In diesem Fall ist dies die predecessor_account_id, und wir überprüfen das Konto und ziehen dann den nicht gesetzten Betrag ab, wenn wir können. Wenn er nicht abgehoben wird, kommt es zur Alarmierung. Zum Beispiel, wenn es immer noch gesperrt ist, weil es vor weniger als 4 Epochen freigegeben wurde.
Abheben ermöglicht es Ihnen, nur einen Teil des Guthabens abzuheben.
Dann setzt stake_all das gesamte nicht eingesetzte Guthaben ein, und es ist ziemlich selten, diese Methode zu verwenden, weil man normalerweise den deposit stake verwendet, welcher bereits das gesamte Guthaben hat.
In der Stake Methode wird nur eine spezifische Menge des Guthabens gestaked. Beispielsweise das Moonlight Wallet verwendet separate Kosten für die Einzahlung in den Stake, aber eine gebündelte Transaktion um dies zu tun.
Schließlich gibt es noch die Methode unstake_all, welche im Grunde alle Anteile aufhebt, indem die Anteile in yocto NEAR umwandelt werden. Es gibt eine Hilfsmethode, die sagt: “Konvertiere meine Anzahl von Anteilen in einen Betrag von yocto NEAR und runde ab, denn wir können dir nicht mehr geben für deinen Anteil multipliziert mit dem Preis. So erhalten wir den Betrag, und dann rufen wir unstake für den angegebenen Betrag auf.
Die Logik staked_amount_from_num_shares_ rounded_down verwendet u256, weil balances auf u128 arbeiten. Um einen Überlauf zu vermeiden, wird der Gesamtbetrag der Einsätze mit der Anzahl der Anteile in u256 multipliziert. Der Preis ist der abgerundete Quotient.
Die Aufrundungsversion staked_amount_from_num_shares_rounded_up ist sehr ähnlich, mit dem Unterschied, dass wir eine Prüfung durchführen, die es uns erlaubt, aufzurunden. Am Ende beider Varianten wird der Betrag auf u128 zurückgesetzt.
Dann haben wir eine unstake-Aktion, die der unstake_all-Aktion sehr ähnlich ist, außer dass man den Betrag übergibt.
Getter/View-Methoden
Danach gibt es eine Reihe von Getter-Methoden, die View-Calls sind, die Ihnen einige Beträge zurückgeben. Sie können den Kontostand ohne Einsätze, den Kontostand mit Einsätzen, den Gesamtsaldo des Kontos abfragen, prüfen, ob Sie sich auszahlen lassen können, und den Gesamteinsatz abfragen, also den Gesamtbetrag der aktiven Einsätze.
Dann können Sie herausfinden, wer der Eigentümer des Staking Pools ist, Sie können die aktuelle Belohnungsgebühr oder Provision des Staking Pools abrufen, den aktuellen Staking Key abrufen und es gibt eine separate Funktion, die überprüft, ob der Eigentümer den Staking Pool pausiert hat.
Angenommen, der Eigentümer führt eine Migration des Staking-Pools auf dem Node durch. Dafür muss der Staking Pool vollständig aufgehoben werden. Erst danach kann der Staking Pool beispielsweise pausiert werden. Diese Aufhebung sendet eine Status Transaktion an das NEAR-Protokoll. Erst danach kann kann der Staking Pool erst wieder in Betrieb genommen werden. Sie können jedoch immer noch Ihr Guthaben abheben, aber Sie werden keine Rewards mehr erhalten, bis die Migration vorüber ist.
Schließlich können Sie eine menschenlesbare Abrechnung erhalten, die Ihnen anzeigt, wie viele Token Sie für die Anzahl der Anteile zum aktuellen Preis tatsächlich haben, und schließlich steht dort, ob Sie Geld abheben können oder nicht.
Sie erhalten dann die Anzahl der Accounts, d.h. die Anzahl der Delegatoren für diesen Staking Pool, und Sie können auch mehrere Delegatoren auf einmal abrufen. Dies ist eine Paginierung für eine große Anzahl von Accounts innerhalb der Unordered Map. Eine Möglichkeit, dies zu tun, ist die Verwendung der Hilfsfunktion keys_as a_vector aus der unordered map. Damit erhält man eine dauerhafte Sammlung von Keys aus der Map, und dann kann man einen Iterator verwenden, um Konten aus diesen Keys abzufragen. Das ist nicht die effizienteste Methode, aber sie ermöglicht die Implementierung einer Paginierung für Unordered Maps.
Owner Methoden
Es gibt eine Reihe von Owner Methoden. Eine Owner-Methode ist eine Methode, die nur vom Owner aufgerufen werden kann. Der Owner kann den Staking Key aktualisieren. Nehmen wir an, sie haben einen anderen node, und der Owner muss einen anderen Schlüssel verwenden. Alle diese Methoden prüfen zunächst, ob nur der Owner sie aufrufen kann.
Dies ist die Methode, mit der die Provision für den Staking-Pool geändert wird. Der Owner kann die Provision, die ab dieser Epoche aktiv ist, sofort ändern, aber alle vorherigen Provisionen werden mit der vorherigen Gebühr berechnet.
Dies war dann die Voting Methode, die uns den Übergang zu Phase zwei des Mainnets ermöglichte.
Als Nächstes folgen die beiden Methoden, die ich bereits beschrieben habe und die es ermöglichen, das Staking zu pausieren und wieder aufzunehmen.
Der Rest sind nur Tests. Der größte Teil der Logik findet in den Interna statt.
Simulationstest
Wir haben auch Simulationstests für einen bestimmten Pool. Diese Simulationstests zeigen, wie das Netzwerk tatsächlich funktionieren wird. Zunächst haben wir den Pool initialisiert.
Bob ist der Delegator. Bob ruft die Deposit Methode des Pools auf, bei der es sich um den deposit_amount handelt. Dann kann Bob überprüfen, ob das nicht eingesetzte Guthaben korrekt funktioniert. Wenn Bob den Betrag einsetzt wird dessen höhe zunächst überprüft.
Bob ruft die Ping-Methode auf. Es gibt keine Belohnungen, aber in Simulationen funktionieren die Belohnungen sowieso nicht, also muss man das manuell machen. Wir überprüfen noch einmal, ob Bobs Betrag immer noch derselbe ist. Dann wird der Pool wieder aufgenommen. Wir verifizieren, dass der Pool wieder aufgenommen wurde, und sperren dann auf Null. Dann simulieren wir, dass der Pool einige Belohnungen erworben hat (1 NEAR) und Bob pingt den Pool an. Dann überprüfen wir, ob der Betrag, den Bob erhalten hat, positiv ist. Das ist ein sehr einfacher Simulationsfall, der besagt, dass Bob zuerst in den Pool eingezahlt hat, was verifiziert, dass das Anhalten und Wiederaufnehmen funktioniert, oder simuliert, dass es funktioniert, und sicherstellt, dass der Pool während des Pausierens nicht sperrt. Bei der Wiederaufnahme des Vorgangs wird der Pool dann tatsächlich eingesetzt. Mit diesem Test wird also nicht nur dies überprüft, sondern auch, ob Bob die Belohnung erhalten hat und ob die Belohnung verteilt wurde. Es gibt noch einen weiteren Test, der eine gewisse Logik überprüft, aber der ist komplizierter. Es gibt einige Unit-Tests, die bestimmte Dinge überprüfen sollen.
Einige dieser Tests sind nicht ideal, aber sie überprüfen bestimmte Dinge, die gut genug waren, um sicherzustellen, dass die Berechnung aufgeht.
internal.rs
Internal Ping Method
Kommen wir nun zu internal_ping. Das ist die Methode, die jeder über ping aufrufen kann, um sicherzustellen, dass die Belohnungen verteilt werden. Im Moment haben wir aktive Stake-Pools und es gibt einen Account, der von einem der NEAR-Leute gesponsert wird, der im Grunde genommen jeden Stake im Pool alle 15 Minuten anpingt, um sicherzustellen, dass die Rewards verteilt wurden und auf dem Kontostand angezeigt werden. Auf diese Weise funktioniert die Reward-Verteilung. Wir überprüfen zunächst die aktuelle Epochenhöhe. Wenn die Epochenhöhe gleich ist, hat sich die Epoche nicht geändert, wir geben false zurück, so dass Sie nicht neu starten müssen. Wenn sich die Epoche geändert hat, erinnern wir uns daran, dass die aktuelle Epoche (Epochenhöhe) existiert, und wir erhalten den neuen Gesamtsaldo des Accounts. Ping kann aufgerufen werden, wenn einige Token durch Depotabstimmungen eingezahlt wurden, und sie sind bereits Teil des account_balance sind. Da ping vorher aufgerufen wurde, müssen wir diesen Saldo abziehen, bevor wir die Belohnungen verteilen. Wir erhalten den Gesamtbetrag, den das Konto hat, einschließlich des gesperrten und des nicht gesperrten Guthabens. Der gesperrte Kontostand ist ein Einsatz, der Belohnungen erhält Dieser kann in bestimmten Szenarien, in denen Sie Ihren Einsatz verringern, ebenfalls Belohnungen erhalten, aber Ihre Belohnungen werden noch für die nächsten zwei Epochen berücksichtigt. Danach werden sie auf den nicht eingesetzten Betrag zurückgesetzt. Wir überprüfen mit assert!, dass der Gesamtsaldo höher ist als der vorherige Gesamtsaldo. Dies ist eine Invariante, die der Staking-Pool benötigt. Im Testnetz gab es eine Reihe von Fällen, in denen diese Invariante nicht erfüllt wurde, weil die Leute immer noch Zugriffsschlüssel auf denselben Staking-Pool hatten, und wenn man diesen hat, gibt man das Guthaben für Gas aus, und man kann sein Gesamtguthaben verringern, ohne die Belohnung zu erhalten. Schließlich berechnen wir den Betrag der Rewards, die der Staking-Pool erhalten hat. Dies ist das Gesamtguthaben abzüglich des vorher bekannten Gesamtguthabens, des Guthabens aus der vorherigen Epoche. Wenn die Rewards positiv sind, verteilen wir sie. Als erstes berechnen wir den Reward, den der Besitzer für sich selbst als Provision nimmt.
Wir multiplizieren die Reward_fee_fraction mit dem Gesamtbetrag der erhaltenen Rewards und runden diesen ebenfalls ab, indem wir den Zähler in u256 mit dem Wert multiplizieren und durch den Nenner in u256 dividieren.
Die owner_fee ist der Betrag in yocto NEAR, den der Eigentümer für sich selbst behält. Die remaining_reward ist die verbleibende Reward, die zurückgegeben werden muss. Dann geht es weiter zum Restaked. Der Eigentümer hat die Rewards in yocta NEAR erhalten, nicht in Anteilen, aber da die gesamte Logik in Anteilen sein muss, kauft der Eigentümer des Staking-Pools Anteile zum Preis der Post-Reward-Ausschüttungen an den Rest der Delegierten. num_shares ist also die Anzahl der Anteile, die der Owner als Entschädigung für den Betrieb des Staking Pools erhält. Wenn sie positiv ist, erhöhen wir die Anzahl der Anteile, schonen den Account des Owners und erhöhen auch den Gesamtbetrag des Einsatzes in Anteilen. Wenn aus irgendeinem Grund während der Abrundung dieser Saldo Null wurde, war die Belohnung sehr klein, und der Preis pro Anteil war sehr groß, und der Pool erhielt nur Null Belohnungen. In diesem Fall wird dieser Saldo einfach auf den Preis pro Anteil aufgeschlagen, anstatt den Eigentümer zu entschädigen. Als Nächstes fügen wir einige Gesamtprotokollierungsdaten ein, die besagen, dass die aktuelle Epoche existiert, dass wir die Belohnungen in Form von Anteilen oder Token erhalten haben und dass das Gesamtguthaben des Pools positiv ist. Anschließend wird die Anzahl der Anteile protokolliert.
Die einzige Möglichkeit, die Anzahl der Anteile nach außen zu tragen, sind die Protokolle. Wenn der Owner dann Belohnungen erhalten hat, heißt es, dass die Gesamtbelohnung so viele Anteile waren. Zum Schluss merken wir uns nur noch den neuen Gesamtsaldo und das war’s. Wir haben alle Rewards in konstanter Zeit verteilt und nur ein Account (das Account des Owners) für die Provision aktualisiert, und das auch nur, wenn die Provision positiv war.
Interne Stake-Methode
Der internal_stake ist der Ort, an dem wir den Preisgarantiefonds implementieren. Nehmen wir an, der Vorgänger, in diesem Fall nennen wir ihn account_id, möchte eine bestimmte Menge an Token einsetzen. Der Saldo ist eigentlich kein JSON-Typ, da es sich um eine interne Methode handelt und wir hier kein JSON benötigen. Wir berechnen, wie viele Anteile abgerundet werden müssen, um den gegebenen Betrag zu setzen, also wie viele Anteile der Owner erhalten wird. Der Wert muss positiv sein. Dann prüfen wir den Betrag, den der Owner für die Anteile zahlen muss, ebenfalls abgerundet. Damit soll sichergestellt werden, dass der Owner beim Kauf von Anteilen und deren Rücktausch ohne Belohnung niemals die 1 yocto NEAR verliert, da dies die Garantie brechen könnte. Schließlich stellen wir fest, dass der Account über genug Geld verfügt, um den belasteten Betrag zu bezahlen, und wir verringern den internen unbesetzten Saldo und erhöhen den internen Anteilssaldo des Accounts. Als Nächstes runden wir den Einsatzbetrag für die Anzahl der Anteile auf, so dass die Anzahl der Anteile tatsächlich aufgerundet wird. Dieser 1 zusätzliche Penny oder 1 zusätzlicher Yocto NEAR wird aus dem Garantiefonds während der Aufrundung der Anteile kommen. Wir haben dem Nutzer weniger berechnet, aber wir haben mehr zu dem Betrag dieser 1 Billion Yocto NEAR beigetragen, den wir ursprünglich dafür vorgesehen hatten. Diese Differenz beträgt in der Regel nur 1 Yocto NEAR, die durch Auf- oder Abrundung entstehen kann. Danach gibt es den Betrag von total_staked_balance und total_stake_shares. Als nächstes geben wir neue Anteile dazu. Schließlich erstellen wir ein Protokoll und geben das Ergebnis zurück.
Unstaking funktioniert ganz ähnlich. Sie runden auf die Anzahl der Anteile auf, die Sie bezahlen müssen. Dann berechnen wir den Betrag, den Sie erhalten, wobei wir wiederum aufrunden, um Ihnen eine Überzahlung zu ermöglichen. Auch dieser Betrag stammt aus einem Garantiefonds. Dann verringern wir die Anteile, um den Betrag zu erhöhen und geben an, wann Sie das Guthaben zwischen vier Epochen freischalten können. Der unstake_amount wird abgerundet, so dass wir etwas weniger freigeben, um den Preis der anderen Teilnehmer des Pools zu garantieren. Das ist so ziemlich das, wie der Stake-Pool funktioniert und wie die Mathematik funktioniert. Wir kompensieren Rundungsfehler aus den Mitteln, die wir zugeteilt haben.
Fazit
Wir haben die Ristretto-Schlüssel während des Entwurfs dieses Contracts aktualisiert und es war überraschend, dass wir dies einkalkulieren mussten. Im STAKE_SHARE_PRICE_GUARANTEE_FUND sollte 1 Billion yocto NEAR für 500 Milliarden Transaktionen ausreichen, was für den Staking-Pool lang genug sein sollte, damit er nicht wieder aufgefüllt werden kann, da die Belohnungen beim nächsten Ping sofort wieder auf den total_stake_balance verteilt werden. Wir haben ziemlich viel Zeit und Mühe auf diesen Contract verwendet, weil wir tonnenweise Sicherheitsüberprüfungen durchgeführt haben, sowohl intern als auch extern, vor allem im Hinblick auf die Mathematik. Das war kompliziert und es wurden einige Dinge entdeckt, wie der Ristretto-Schlüssel, welcher erst bei den Überprüfungen auftauchte. Wir haben das Änderungsprotokoll dieses Contracts markiert, und auch in der Readme gibt es einen Haufen Dinge, die während der Entwicklung und des Testens auf dem Live-System aufgetaucht sind, aber die ursprüngliche Version benötigte etwa eine Woche zum Schreiben. Später haben wir sie bereinigt, getestet und verbessert. Dann haben wir eine Reihe von Überarbeitungen vorgenommen. Das Pausieren und Wiederaufnehmen wurde vom Pool gefordert, weil der Owner sonst nicht in der Lage wäre, den Stake aufzuheben, wenn sein Node ausfällt. Sie würden das Netzwerk angreifen. Im Wesentlichen würde dieser aktive Einsatz die Validierung fordern und nicht das Netzwerk betreiben. Früher hatten wir kein Slashing. Das war zwar kein Problem für die Teilnehmer, aber ein Problem für das Netzwerk selbst. Auf diese Weise kann der Eigentümer das Staking pausieren, wenn er den Pool nicht betreiben will, wenn er in den Pool migriert, und vorher so viel wie möglich kommunizieren. Als Nächstes haben wir das Vote-Interface aktualisiert, um sie an den endgültigen Voting-Contract der zweiten Phase anzupassen. Wir fügten helper view-Methoden hinzu, um Konten in einer für Menschen lesbaren Weise abfragen zu können. Schließlich gab es einige Verbesserungen bei der Zusammenführung von Methoden, so deposit_and_stake, stake_all, unstake_all und withdraw_all. Anstelle eines initialen View-Calls, welcher den Betrag zu erhält und den Betrag setzt um den Einsatz aufzurufen, wurde der folgende Weg implementiert:
Wenn Sie einen Einsatz tätigen, setzen Sie nicht nur den Betrag ein, sondern wir fügen auch ein Versprechen hinzu, um zu prüfen, ob der Einsatz erfolgreich war. Das Versprechen wird für zwei Dinge benötigt: Wenn Sie versuchen, mit einem ungültigen Schlüssel (nicht ristretto-spezifischen Schlüssel) zu setzen, wird das Versprechen vor der Ausführung fehlschlagen. Es scheitert bei der Validierung, bevor es gesendet wird. Dies wird erreicht, ohne dass eine Prüfung im Contract notwendig ist. Dadurch ist es möglich den letzten Call rückgängig zu machen, und alles wird gut sein. Wir haben auch den Mindesteinsatz auf Protokollebene eingeführt. Der Mindesteinsatz beträgt ein Zehntel des letzten Platzierungspreises, und wenn Ihr Contract versucht, einen geringeren Einsatz zu leisten, wird die Aktion fehlschlagen, und Sie werden das Versprechen nicht senden. Angenommen, Sie möchten einen bestimmten Betrag abheben und Ihr Guthaben ist unter ein Zehntel des Einsatzes gefallen. Die Aktion kann fehlschlagen, und Sie werden den Stake nicht aufheben, obwohl Sie ihn brauchen, um zu garantieren, dass der Unstake stattfindet. In diesem Fall haben wir diesen Callback, der prüft, ob die Staking-Aktion erfolgreich abgeschlossen wurde. Dieser Callback prüft im Grunde, dass wir die Absteckung aufheben müssen, wenn sie fehlschlägt und der Saldo positiv ist. Es wird also unstake für eine Aktion aufgerufen, bei der der Einsatz null ist, um sicherzustellen, dass das gesamte Guthaben freigegeben wird. Während der Tests dieser Contracte, die wir im Beta-9-Testnetz vor der Wartung durchgeführt haben, kann man sich um 4 Epochen in der Zeit zurückversetzen lassen. Der Contract war etwa im Sommer fertig, so dass die Tests dieser Iteration aufgrund der Komplexität, die die Interaktion mit dem Protokoll mit sich bringt, wahrscheinlich 2-4 Monate dauerten. Es gab eine ganze Menge zu lernen, von der Paginierung bis zu den Hilfsmethoden und der Zusammenstellung einiger Dinge. Eine Sache, die wirklich schön wäre, wäre die Möglichkeit, Einsätze oder Einzahlungen und Einsätze in einem lock-up-Contract zusammenzufassen. Im Moment muss man manuell angeben, wie viel man auf einen Lockup Contract setzen will, aber es wäre toll, wenn man nicht über sein Yocto NEAR nachdenken müsste und wie viel es für die Lagerung gesperrt ist. Sie wollen einfach alles von deinem Lockup staken, aber da es bereits eingesetzt wurde, war es zu spät, darüber nachzudenken. Es gibt auch Gas, das fest kodiert ist, und mit der allgemeinen Gebührensenkung können diese Zahlen nicht geändert werden, weil sie bereits in der Chain ist.
Die Vote ist also nicht wichtig, aber die ON_STAKE_ACTION_GAS-Methode erfordert, dass man eine große Zahl für jeden Einsatz hat, und man kann sie nicht verringern. Das Risiko, bei jedem Call in diesem Contract Aktionen durchzuführen, erfordert eine große Menge an Gas, und das Problem ist, dass das verschwenderisch ist. Angenommen, wir einigen uns darauf, das gesamte Gas zu verbrauchen, dann wird dieses Gas immer verbraucht und verschwendet, und außerdem wird die Anzahl der Transaktionen, die man in einen Block packen kann, eingeschränkt, wenn wir das Gas in diesem Fall beschränken. Es gab viele Iterationen beim Testen des Contracts unter Verwendung des Simulationstest-Frameworks, welche wir stark verbessert haben. Wenn wir später zu den Lockup Contracts kommen, können Sie sehen, wie sich die Struktur der Lockup Contracts gegenüber diesem verbessert hat.