Mit Haskell aus FunC FunCtional machen: Wie Serokell den Telegram-Blockchain-Wettbewerb gewann

Sie haben dieses Telegramm wahrscheinlich gehört ist im Begriff, die Ton-Blockchain-Plattform zu starten. Aber vielleicht haben Sie die Nachricht von Telegram vor nicht allzu langer Zeit verpasst einen Wettbewerb ausgeschrieben für die Umsetzung eines oder mehrerer Smart Contracts für diese Plattform.

Das Serokell-Team mit umfangreicher Erfahrung in der Entwicklung großer Blockchain-Projekte konnte nicht daneben stehen. Wir haben fünf Mitarbeiter zum Wettbewerb entsandt, und zwei Wochen später belegten sie unter dem (un)bescheidenen zufälligen Spitznamen Sexy Chameleon den ersten Platz. In diesem Artikel werde ich darüber sprechen, wie sie es gemacht haben. Wir hoffen, dass Sie in den nächsten zehn Minuten zumindest eine interessante Geschichte lesen und höchstens etwas Nützliches darin finden, das Sie in Ihrer Arbeit anwenden können.

Aber beginnen wir mit einem kleinen Kontext.

Wettbewerb und seine Bedingungen

Die Hauptaufgaben der Teilnehmer bestanden also in der Umsetzung eines oder mehrerer der vorgeschlagenen Smart Contracts sowie in der Unterbreitung von Vorschlägen zur Verbesserung des TON-Ökosystems. Der Wettbewerb lief vom 24. September bis 15. Oktober und die Ergebnisse wurden erst am 15. November bekannt gegeben. Eine ziemlich lange Zeit, wenn man bedenkt, dass es Telegram in dieser Zeit gelungen ist, Wettbewerbe zum Design und zur Entwicklung von Anwendungen in C++ zum Testen und Bewerten der Qualität von VoIP-Anrufen in Telegram abzuhalten und die Ergebnisse bekannt zu geben.

Aus der von den Organisatoren vorgeschlagenen Liste haben wir zwei Smart Contracts ausgewählt. Für eines davon verwendeten wir Tools, die mit TON vertrieben wurden, und das zweite wurde in einer neuen Sprache implementiert, die von unseren Ingenieuren speziell für TON entwickelt und in Haskell integriert wurde.

Die Wahl einer funktionalen Programmiersprache ist kein Zufall. In unserem Unternehmensblog Wir sprechen oft darüber, warum wir die Komplexität funktionaler Sprachen für gewaltig übertrieben halten und warum wir sie im Allgemeinen objektorientierten Sprachen vorziehen. Es enthält übrigens auch Original dieses Artikels.

Warum haben wir uns überhaupt für die Teilnahme entschieden?

Kurz gesagt, weil wir uns auf nicht standardmäßige und komplexe Projekte spezialisiert haben, die besondere Fähigkeiten erfordern und oft von wissenschaftlichem Wert für die IT-Community sind. Wir unterstützen nachdrücklich die Open-Source-Entwicklung und engagieren uns für deren Popularisierung. Außerdem arbeiten wir mit führenden russischen Universitäten im Bereich Informatik und Mathematik zusammen.

Die interessanten Aufgaben des Wettbewerbs und die Beteiligung an unserem geliebten Telegram-Projekt waren an sich schon eine hervorragende Motivation, aber der Preisfonds wurde zu einem zusätzlichen Anreiz. 🙂

TON-Blockchain-Forschung

Wir beobachten neue Entwicklungen in den Bereichen Blockchain, künstliche Intelligenz und maschinelles Lernen genau und versuchen, in jedem unserer Arbeitsbereiche keine einzige bedeutende Veröffentlichung zu verpassen. Daher war unser Team zu Beginn des Wettbewerbs bereits mit den Ideen von bekannt TON-Whitepaper. Bevor wir jedoch mit der Arbeit mit TON begannen, analysierten wir nicht die technische Dokumentation und den eigentlichen Quellcode der Plattform, sodass der erste Schritt ganz offensichtlich war – ein gründliches Studium der offiziellen Dokumentation Webseite und Projektrepositorys.

Als der Wettbewerb begann, war der Code bereits veröffentlicht. Um Zeit zu sparen, entschieden wir uns, nach einem Leitfaden oder einer Zusammenfassung zu suchen, die von geschrieben wurde Benutzer. Leider hat dies zu keinem Ergebnis geführt – außer einer Anleitung zum Aufbau der Plattform unter Ubuntu haben wir keine weiteren Materialien gefunden.

Die Dokumentation selbst war gut recherchiert, an manchen Stellen jedoch schwer lesbar. Sehr oft mussten wir zu bestimmten Punkten zurückkehren und von Beschreibungen abstrakter Ideen auf hoher Ebene zu Implementierungsdetails auf niedriger Ebene wechseln.

Es wäre einfacher, wenn die Spezifikation überhaupt keine detaillierte Beschreibung der Implementierung enthalten würde. Informationen darüber, wie eine virtuelle Maschine ihren Stack darstellt, lenken Entwickler eher davon ab, Smart Contracts für die TON-Plattform zu erstellen, als ihnen zu helfen.

Nix: das Projekt zusammenstellen

Bei Serokell sind wir große Fans Nixe. Wir sammeln damit unsere Projekte und stellen sie bereit NixOps, und auf allen unseren Servern installiert Nix OS. Dadurch sind alle unsere Builds reproduzierbar und funktionieren auf jedem Betriebssystem, auf dem Nix installiert werden kann.

Also begannen wir mit der Kreation Nix-Overlay mit Ausdruck für TON-Assembly. Mit seiner Hilfe ist das Kompilieren von TON so einfach wie möglich:

$ cd ~/.config/nixpkgs/overlays && git clone https://github.com/serokell/ton.nix
$ cd /path/to/ton/repo && nix-shell
[nix-shell]$ cmakeConfigurePhase && make

Beachten Sie, dass Sie keine Abhängigkeiten installieren müssen. Nix erledigt auf magische Weise alles für Sie, egal ob Sie NixOS, Ubuntu oder macOS verwenden.

Programmierung für TON

Der Smart-Contract-Code im TON-Netzwerk läuft auf der TON Virtual Machine (TVM). TVM ist komplexer als die meisten anderen virtuellen Maschinen und verfügt beispielsweise über sehr interessante Funktionen, mit denen es arbeiten kann Fortsetzungen и Links zu Daten.

Darüber hinaus haben die Jungs von TON drei neue Programmiersprachen entwickelt:

Fünfte ist eine universelle Stack-Programmiersprache, die ähnelt Weiter. Seine Superfähigkeit ist die Fähigkeit, mit TVM zu interagieren.

FunC ist eine intelligente Vertragsprogrammiersprache, die ähnelt C und wird in eine andere Sprache kompiliert - Fift Assembler.

Fünfter Assembler – Fift-Bibliothek zum Generieren von binärem ausführbarem Code für TVM. Fifth Assembler verfügt über keinen Compiler. Das Eingebettete domänenspezifische Sprache (eDSL).

Unser Wettbewerb funktioniert

Schließlich ist es an der Zeit, einen Blick auf die Ergebnisse unserer Bemühungen zu werfen.

Asynchroner Zahlungskanal

Der Zahlungskanal ist ein intelligenter Vertrag, der es zwei Benutzern ermöglicht, Zahlungen außerhalb der Blockchain zu senden. Dadurch sparen Sie nicht nur Geld (es fällt keine Provision an), sondern auch Zeit (Sie müssen nicht auf die Bearbeitung des nächsten Blocks warten). Zahlungen können so klein wie gewünscht und so oft wie erforderlich erfolgen. In diesem Fall müssen die Parteien einander nicht vertrauen, da die Fairness der endgültigen Abrechnung durch den Smart Contract gewährleistet ist.

Wir haben eine ziemlich einfache Lösung für das Problem gefunden. Zwei Parteien können signierte Nachrichten austauschen, die jeweils zwei Nummern enthalten – der volle Betrag, den jede Partei zahlt. Diese beiden Zahlen funktionieren wie folgt Vektoruhr in traditionellen verteilten Systemen und legen Sie die Reihenfolge fest, in der Transaktionen vorher ausgeführt wurden. Anhand dieser Daten kann der Vertragspartner eventuelle Konflikte lösen.

Tatsächlich reicht eine Zahl aus, um diese Idee umzusetzen, aber wir haben beide belassen, weil wir auf diese Weise eine komfortablere Benutzeroberfläche erstellen konnten. Darüber hinaus haben wir uns entschieden, den Zahlungsbetrag in jeder Nachricht anzugeben. Wenn die Nachricht ohne sie aus irgendeinem Grund verloren geht, bemerkt der Benutzer den Verlust möglicherweise nicht, obwohl alle Beträge und die endgültige Berechnung korrekt sind.

Um unsere Idee zu testen, haben wir nach Beispielen für die Verwendung eines so einfachen und prägnanten Zahlungskanalprotokolls gesucht. Überraschenderweise haben wir nur zwei gefunden:

  1. Beschreibung ein ähnlicher Ansatz, nur für den Fall eines unidirektionalen Kanals.
  2. Tutorial, das die gleiche Idee wie unsere beschreibt, jedoch ohne viele wichtige Details zu erklären, wie z. B. allgemeine Korrektheit und Verfahren zur Konfliktlösung.

Es wurde deutlich, dass es sinnvoll ist, unser Protokoll detailliert zu beschreiben und dabei besonders auf seine Richtigkeit zu achten. Nach mehreren Iterationen war die Spezifikation fertig, und jetzt können Sie es auch tun. Schau sie an.

Wir haben den Vertrag in FunC implementiert und das Befehlszeilenprogramm für die Interaktion mit unserem Vertrag vollständig in Fift geschrieben, wie von den Organisatoren empfohlen. Wir hätten jede andere Sprache für unsere CLI wählen können, aber wir waren daran interessiert, Fit auszuprobieren, um zu sehen, wie es in der Praxis funktioniert.

Ehrlich gesagt sahen wir nach der Zusammenarbeit mit Fift keine zwingenden Gründe, diese Sprache den beliebten und aktiv genutzten Sprachen mit entwickelten Tools und Bibliotheken vorzuziehen. Das Programmieren in einer stapelbasierten Sprache ist ziemlich unangenehm, da man ständig im Kopf behalten muss, was sich auf dem Stapel befindet, und der Compiler hilft dabei nicht.

Daher ist unserer Meinung nach die einzige Rechtfertigung für die Existenz von Fift seine Rolle als Hostsprache für Fift Assembler. Aber wäre es nicht besser, den TVM-Assembler in eine bestehende Sprache einzubetten, als eine neue Sprache im Wesentlichen nur für diesen Zweck zu erfinden?

TVM Haskell eDSL

Jetzt ist es an der Zeit, über unseren zweiten Smart Contract zu sprechen. Wir haben uns entschieden, ein Multi-Signatur-Wallet zu entwickeln, aber das Schreiben eines weiteren Smart Contracts in FunC wäre zu langweilig. Wir wollten etwas Würze hinzufügen, und das war unsere eigene Assemblersprache für TVM.

Wie Fift Assembler ist unsere neue Sprache eingebettet, aber wir haben Haskell als Host anstelle von Fift gewählt, sodass wir das fortschrittliche Typsystem voll ausnutzen können. Bei der Arbeit mit Smart Contracts, bei denen selbst ein kleiner Fehler sehr hohe Kosten verursachen kann, ist die statische Typisierung unserer Meinung nach ein großer Vorteil.

Um zu demonstrieren, wie der in Haskell eingebettete TVM-Assembler aussieht, haben wir darauf eine Standard-Wallet implementiert. Hier sind ein paar Dinge, auf die Sie achten sollten:

  • Dieser Vertrag besteht aus einer Funktion, Sie können jedoch beliebig viele nutzen. Wenn Sie eine neue Funktion in der Host-Sprache (d. h. Haskell) definieren, können Sie mit unserem eDSL wählen, ob sie zu einer separaten Routine in TVM werden oder einfach am Aufrufpunkt integriert werden soll.
  • Wie Haskell haben Funktionen Typen, die zur Kompilierzeit überprüft werden. In unserem eDSL ist der Eingabetyp einer Funktion der Stapeltyp, den die Funktion erwartet, und der Ergebnistyp ist der Stapeltyp, der nach dem Aufruf erzeugt wird.
  • Der Code enthält Anmerkungen stacktype, beschreibt den erwarteten Stapeltyp am Aufrufpunkt. Im ursprünglichen Wallet-Vertrag waren dies nur Kommentare, aber in unserem eDSL sind sie tatsächlich Teil des Codes und werden zur Kompilierzeit überprüft. Sie können als Dokumentation oder Anweisungen dienen, die dem Entwickler helfen, das Problem zu finden, wenn sich der Code und der Stack-Typ ändern. Natürlich haben solche Annotationen keinen Einfluss auf die Laufzeitleistung, da für sie kein TVM-Code generiert wird.
  • Dies ist immer noch ein Prototyp, der in zwei Wochen geschrieben wurde, es gibt also noch viel Arbeit an dem Projekt. Beispielsweise sollten alle Instanzen der Klassen, die Sie im folgenden Code sehen, automatisch generiert werden.

So sieht die Implementierung eines Multisig-Wallets auf unserem eDSL aus:

main :: IO ()
main = putText $ pretty $ declProgram procedures methods
  where
    procedures =
      [ ("recv_external", decl recvExternal)
      , ("recv_internal", decl recvInternal)
      ]
    methods =
      [ ("seqno", declMethod getSeqno)
      ]

data Storage = Storage
  { sCnt :: Word32
  , sPubKey :: PublicKey
  }

instance DecodeSlice Storage where
  type DecodeSliceFields Storage = [PublicKey, Word32]
  decodeFromSliceImpl = do
    decodeFromSliceImpl @Word32
    decodeFromSliceImpl @PublicKey

instance EncodeBuilder Storage where
  encodeToBuilder = do
    encodeToBuilder @Word32
    encodeToBuilder @PublicKey

data WalletError
  = SeqNoMismatch
  | SignatureMismatch
  deriving (Eq, Ord, Show, Generic)

instance Exception WalletError

instance Enum WalletError where
  toEnum 33 = SeqNoMismatch
  toEnum 34 = SignatureMismatch
  toEnum _ = error "Uknown MultiSigError id"

  fromEnum SeqNoMismatch = 33
  fromEnum SignatureMismatch = 34

recvInternal :: '[Slice] :-> '[]
recvInternal = drop

recvExternal :: '[Slice] :-> '[]
recvExternal = do
  decodeFromSlice @Signature
  dup
  preloadFromSlice @Word32
  stacktype @[Word32, Slice, Signature]
  -- cnt cs sign

  pushRoot
  decodeFromCell @Storage
  stacktype @[PublicKey, Word32, Word32, Slice, Signature]
  -- pk cnt' cnt cs sign

  xcpu @1 @2
  stacktype @[Word32, Word32, PublicKey, Word32, Slice, Signature]
  -- cnt cnt' pk cnt cs sign

  equalInt >> throwIfNot SeqNoMismatch

  push @2
  sliceHash
  stacktype @[Hash Slice, PublicKey, Word32, Slice, Signature]
  -- hash pk cnt cs sign

  xc2pu @0 @4 @4
  stacktype @[PublicKey, Signature, Hash Slice, Word32, Slice, PublicKey]
  -- pubk sign hash cnt cs pubk

  chkSignU
  stacktype @[Bool, Word32, Slice, PublicKey]
  -- ? cnt cs pubk

  throwIfNot SignatureMismatch
  accept

  swap
  decodeFromSlice @Word32
  nip

  dup
  srefs @Word8

  pushInt 0
  if IsEq
  then ignore
  else do
    decodeFromSlice @Word8
    decodeFromSlice @(Cell MessageObject)
    stacktype @[Slice, Cell MessageObject, Word8, Word32, PublicKey]
    xchg @2
    sendRawMsg
    stacktype @[Slice, Word32, PublicKey]

  endS
  inc

  encodeToCell @Storage
  popRoot

getSeqno :: '[] :-> '[Word32]
getSeqno = do
  pushRoot
  cToS
  preloadFromSlice @Word32

Den vollständigen Quellcode unseres eDSL- und Multi-Signatur-Wallet-Vertrags finden Sie unter dieses Repository. Und vieles mehr ausführlich erzählt über integrierte Sprachen, unser Kollege Georgy Agapov.

Fazit zum Wettbewerb und TON

Insgesamt dauerte unsere Arbeit 380 Stunden (einschließlich Einarbeitung in die Dokumentation, Besprechungen und tatsächliche Entwicklung). An dem Wettbewerbsprojekt nahmen fünf Entwickler teil: CTO, Teamleiter, Blockchain-Plattformspezialisten und Haskell-Softwareentwickler.

Wir fanden problemlos Ressourcen, um am Wettbewerb teilzunehmen, da der Geist eines Hackathons, die enge Teamarbeit und die Notwendigkeit, schnell in Aspekte neuer Technologien einzutauchen, immer spannend sind. Mehrere schlaflose Nächte, um unter Bedingungen begrenzter Ressourcen maximale Ergebnisse zu erzielen, werden durch unschätzbare Erfahrungen und hervorragende Erinnerungen entschädigt. Darüber hinaus ist die Bearbeitung solcher Aufgaben immer ein guter Test für die Unternehmensprozesse, da es ohne ein gut funktionierendes internes Zusammenspiel äußerst schwierig ist, wirklich gute Ergebnisse zu erzielen.

Abgesehen vom Text: Wir waren beeindruckt von der Menge an Arbeit, die das TON-Team geleistet hat. Es ist ihnen gelungen, ein komplexes, schönes und vor allem funktionierendes System aufzubauen. TON hat sich als Plattform mit großem Potenzial erwiesen. Damit sich dieses Ökosystem entwickeln kann, muss jedoch noch viel mehr getan werden, sowohl im Hinblick auf seinen Einsatz in Blockchain-Projekten als auch im Hinblick auf die Verbesserung der Entwicklungstools. Wir sind stolz, nun Teil dieses Prozesses zu sein.

Wenn Sie nach der Lektüre dieses Artikels noch Fragen oder Ideen haben, wie Sie TON zur Lösung Ihrer Probleme nutzen können, wenden Sie sich bitte an uns. Schreib uns — Gerne teilen wir unsere Erfahrungen.

Source: habr.com

Kommentar hinzufügen