FunC in FunCtional umwandeln mit Haskell: wie Serokell den Telegram Blockchain Wettbewerb gewonnen hat.

Sie haben sicherlich gehört, dass Telegram eine Blockchain-Plattform namens Ton starten möchte.Aber vielleicht haben Sie die Nachricht verpasst, dass Telegram einen Wettbewerb angekündigt hat zur Umsetzung eines oder mehrerer Smart Contracts für diese Plattform.

Das Serokell-Team mit umfangreicher Erfahrung in der Entwicklung großer Blockchain-Projekte konnte sich nicht zurückhalten. Wir haben fünf Mitarbeiter zum Wettbewerb delegiert, und bereits nach zwei Wochen erreichten sie den ersten Platz mit dem (nicht) bescheidenen zufälligen Spitznamen Sexy Chameleon. In diesem Artikel werde ich erzählen, wie ihnen das gelungen ist. Wir hoffen, dass Sie in den nächsten zehn Minuten zumindest eine interessante Geschichte lesen und bestenfalls etwas Nützliches finden, das Sie in Ihrer Arbeit anwenden können.

Aber lassen Sie uns mit einem kleinen Einblick in den Kontext beginnen.

Der Wettbewerb und seine Bedingungen

Die Hauptaufgaben der Teilnehmer bestanden also darin, einen oder mehrere der vorgeschlagenen Smart Contracts zu implementieren und Vorschläge zur Verbesserung des TON-Ökosystems einzubringen. Der Wettbewerb fand vom 24. September bis 15. Oktober statt, die Ergebnisse wurden jedoch erst am 15. November bekannt gegeben. Ziemlich lange, wenn man bedenkt, dass Telegram in dieser Zeit auch die Ergebnisse von Wettbewerben im Design und in der Entwicklung von Anwendungen in C++ zur Testung und Bewertung der Qualität von VoIP-Anrufen in Telegram durchgeführt und bekannt gegeben hat.

Wir haben aus der von den Organisatoren vorgeschlagenen Liste zwei Smart Contracts ausgewählt. Für einen von ihnen haben wir die Werkzeuge verwendet, die zusammen mit TON bereitgestellt wurden, während der andere in einer neuen Sprache implementiert wurde, die unsere Ingenieure speziell für TON entwickelt und in Haskell integriert haben.

Die Wahl der funktionalen Programmiersprache ist nicht zufällig. In unserem Unternehmensblog erläutern wir häufig, warum wir die Komplexität funktionaler Sprachen für stark übertrieben halten und warum wir sie insgesamt objektorientierten Sprachen vorziehen. Übrigens gibt es darin auch den Originaltext dieses Artikels..

Warum wir überhaupt entschieden haben, teilzunehmen

Kurz gesagt, weil unsere Spezialisierung auf nicht standardmäßige und komplexe Projekte abzielt, die besondere Fähigkeiten erforden und häufig wissenschaftlichen Wert für die IT-Community darstellen. Wir unterstützen die Open-Source-Entwicklung leidenschaftlich und setzen uns für deren Popularisierung ein, zudem arbeiten wir mit führenden Universitäten Russlands im Bereich der Computerwissenschaften und Mathematik zusammen.

Die interessanten Aufgaben des Wettbewerbs und die Zugehörigkeit zu unserem heiß geliebten Projekt Telegram waren bereits eine großartige Motivation, und der Preisfonds wurde zu einem zusätzlichen Anreiz. 🙂

Untersuchung der Blockchain TON

Wir verfolgen aufmerksam die neuen Entwicklungen in der Blockchain, künstlichen Intelligenz und im maschinellen Lernen und versuchen, keinen nennenswerten Release in den Bereichen, in denen wir tätig sind, zu verpassen. Daher war unser Team zum Zeitpunkt des Wettbewerbs bereits mit den Ideen aus TON Whitepapervertraut. Allerdings hatten wir vor Beginn der Arbeit mit TON die technische Dokumentation und den tatsächlichen Quellcode der Plattform nicht analysiert, daher war der erste Schritt ziemlich offensichtlich – eine gründliche Untersuchung der offiziellen Dokumentation auf Website und in Projekt-Repository.

Zu Beginn des Wettbewerbs war der Code bereits veröffentlicht, daher haben wir beschlossen, um Zeit zu sparen, nach einer Anleitung oder Zusammenfassung zu suchen, die von Benutzerngeschrieben wurde. Leider war dies nicht erfolgreich – außer der Anleitung zum Zusammenstellen der Plattform auf Ubuntu haben wir keine weiteren Materialien gefunden.

Die Dokumentation selbst stellte sich als sorgfältig ausgearbeitet heraus, allerdings war es manchmal schwierig, sie zu lesen. Oft mussten wir zu bestimmten Punkten zurückkehren und uns von hochrangigen Beschreibungen abstrakter Ideen auf nieder-rangige Details der Implementierung umschalten.

Es wäre einfacher gewesen, wenn die Spezifikation überhaupt keine detaillierte Beschreibung der Implementierung enthalten hätte. Informationen darüber, wie die virtuelle Maschine ihren Stack darstellt, lenken eher von den Entwicklern ab, die Smart Contracts für die TON-Plattform erstellen, als dass sie ihnen helfen.

Nix: wir stellen das Projekt zusammen

Bei Serokell sind wir große Fans Nixvon. Wir bauen unsere Projekte damit und setzen sie mit Hilfe von NixOpsum, und auf all unseren Servern ist installiert NixOS. Dadurch sind alle unsere Builds reproduzierbar und funktionieren auf jedem Betriebssystem, auf dem Nix installiert werden kann.

Deshalb haben wir mit der Erstellung eines Nix Overlays mit dem Ausdruck für den Bau von TONbegonnen. Damit ist es maximal einfach, TON zu kompilieren:

$ 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 alles auf magische Weise für Sie, unabhängig davon, ob Sie NixOS, Ubuntu oder macOS verwenden.

Programmierung für TON

Der Code der Smart Contracts im TON-Netzwerk wird auf der TON Virtual Machine (TVM) ausgeführt. TVM ist komplexer als die meisten anderen virtuellen Maschinen und bietet sehr interessante Funktionen, wie zum Beispiel die Fähigkeit, mit Fortsetzungen (continuations) und Daten-Links.

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

Fift — eine universelle stackbasierte Programmiersprache, die an Fortherinnert. Ihre Superfähigkeit ist die Interaktion mit der TVM.

FunC — eine Programmiersprache für Smart Contracts, die ähnlich ist wie C und in eine weitere Sprache namens Fift Assembler kompiliert wird.

Fift Assembler — eine Fift-Bibliothek zur Generierung von binärem ausführbarem Code für TVM. Fift Assembler hat keinen Compiler. Es handelt sich um eine eingebettete domänenspezifische Sprache (eDSL).

Unsere Wettbewerbsergebnisse

Es ist endlich an der Zeit, die Ergebnisse unserer Bemühungen zu betrachten.

Asynchroner Zahlungsweg

Ein Zahlungsweg (payment channel) ist ein Smart Contract, der es zwei Nutzern ermöglicht, Zahlungen außerhalb der Blockchain zu senden. Dadurch werden nicht nur Geld (keine Gebühren) gespart, sondern auch Zeit (man muss nicht warten, bis der nächste Block verarbeitet wird). Zahlungen können beliebig klein sein und so oft erfolgen, wie es erforderlich ist. Dabei müssen sich die Parteien nicht gegenseitig vertrauen, da die Fairness der endgültigen Abrechnung durch den Smart Contract garantiert ist.

Wir haben eine recht einfache Lösung für das Problem gefunden. Die beiden Parteien können unterzeichnete Nachrichten austauschen, von denen jede zwei Zahlen enthält — die Gesamtsumme, die jeder der Teilnehmer bezahlt hat. Diese beiden Zahlen funktionieren wie Vektoruhren in traditionellen verteilten Systemen und legen die Reihenfolge "geschah davor" bei den Transaktionen fest. Mithilfe dieser Daten kann der Vertrag jeden möglichen Konflikt lösen.

Tatsächlich genügt zur Umsetzung dieser Idee eine Zahl, aber wir haben beide beibehalten, da wir so eine benutzerfreundlichere Schnittstelle erstellen konnten. Darüber hinaus haben wir beschlossen, die Größe der Zahlung in jede Nachricht aufzunehmen. Ohne diese Information könnte der Benutzer im Falle eines Verlusts der Nachricht, wenn auch alle Beträge und die endgültige Abrechnung korrekt wären, den Verlust möglicherweise nicht bemerken.

Um unsere Idee zu überprüfen, haben wir nach Beispielen für die Verwendung eines so einfachen und prägnanten Zahlungsprotokolls gesucht. Zu unserer Überraschung fanden wir nur zwei:

  1. Beschreibung einen ähnlichen Ansatz, jedoch für einen einseitigen Kanal.
  2. Tutorial, in dem dieselbe Idee beschrieben wird wie bei uns, jedoch ohne Erklärung vieler wichtiger Details, wie allgemeine Korrektheit und die Konfliktlösungsverfahren.

Es wurde klar, dass es sinnvoll ist, unser Protokoll ausführlich zu beschreiben und besonderes Augenmerk auf seine Korrektheit zu legen. Nach mehreren Iterationen war die Spezifikation fertig, und jetzt könnt ihr auch einen Blick darauf werfen.

Wir haben einen Vertrag in FunC implementiert, und das Kommandozeilenwerkzeug zur Interaktion mit unserem Vertrag haben wir vollständig in Fift geschrieben, wie es die Organisatoren empfohlen haben. Wir hätten jede andere Sprache für unser CLI wählen können, aber wir fanden es interessant, es mit Fift auszuprobieren, um zu sehen, wie es sich in der Praxis schlägt.

Ehrlich gesagt, nach der Arbeit mit Fift, haben wir keine überzeugenden Gründe gesehen, diese Sprache populären und weitverbreiteten Sprachen mit ausgereiften Werkzeugen und Bibliotheken vorzuziehen. Das Programmieren in einer Stack-Sprache ist ziemlich unangenehm, da man ständig im Kopf behalten muss, was wo im Stack liegt, und der Compiler dabei nicht hilft.

Daher sehen wir den einzigen Grund für die Existenz von Fift in seiner Rolle als Host-Sprache für den Fift Assembler. Aber wäre es nicht besser gewesen, den TVM Assembler in eine bestehende Sprache zu integrieren, anstatt eine neue für diesen, im Grunde genommen, einzigen Zweck zu entwickeln?

TVM Haskell eDSL

Jetzt ist es an der Zeit, von unserem zweiten Smart Contract zu erzählen. Wir haben beschlossen, eine Wallet mit Mehrfachsignatur zu entwickeln, aber einen weiteren Smart Contract in FunC zu schreiben, wäre zu langweilig gewesen. Wir wollten etwas Besonderes hinzufügen und das wurde unsere eigene Assemblersprache für TVM.

Wie der Fift Assembler ist auch unsere neue Sprache eingebettet, jedoch haben wir als Host statt Fift Haskell gewählt. Dadurch konnten wir sein fortschrittliches Typsystem vollständig nutzen. Bei der Arbeit mit Smart Contracts, bei denen bereits kleine Fehler sehr kostspielig sein können, halten wir statische Typisierung für einen großen Vorteil.

Um zu demonstrieren, wie der in Haskell integrierte TVM-Assembler aussieht, haben wir eine Standard-Wallet mit ihm umgesetzt. Hier sind einige Punkte, auf die Sie achten sollten:

  • Dieser Vertrag besteht aus einer Funktion, aber Sie können so viele verwenden, wie Sie möchten. Wenn Sie eine neue Funktion in der Host-Sprache (d.h. in Haskell) definieren, ermöglicht Ihnen unser eDSL zu wählen, ob sie in ein separates Unterprogramm in TVM verwandelt werden soll oder einfach am Aufrufort eingebettet wird.
  • Wie in Haskell haben Funktionen Typen, die während der Kompilierung überprüft werden. In unserem eDSL ist der Eingabetyp der Funktion der Typ des Stacks, den die Funktion erwartet, und der Rückgabewert ist der Typ des Stacks, der nach dem Aufruf folgt.
  • Im Code gibt es Anmerkungen stacktype, die den erwarteten Typ des Stacks am Aufrufort beschreiben. Im Originalvertrag der Wallet waren dies einfach Kommentare, aber in unserem eDSL sind sie tatsächlich Teil des Codes und werden während der Kompilierung überprüft. Sie können als Dokumentation oder Behauptungen dienen, die dem Entwickler helfen, ein Problem zu finden, falls sich der Typ des Stacks bei einer Codeänderung ändert. Natürlich wirken sich solche Anmerkungen nicht auf die Laufzeitleistung aus, da für sie kein TVM-Code generiert wird.
  • Es handelt sich immer noch um einen Prototyp, der in zwei Wochen erstellt wurde, sodass im Projekt noch viel Arbeit vor uns liegt. Zum Beispiel sollten alle Instanzen der Klassen, die Sie im folgenden Code sehen, automatisch generiert werden.

So sieht die Implementierung einer Multisig-Wallet in 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 des Wallet-Vertrags mit Mehrfachsignatur finden Sie in diesem Repository. Und detaillierter hat unser Kollege Georgiy Agapov über eingebettete Sprachen gesprochen.

Die Ergebnisse des Wettbewerbs und TON

Insgesamt hat unsere Arbeit 380 Stunden in Anspruch genommen (einschließlich der Einarbeitung in die Dokumentation, Besprechungen und der eigentlichen Entwicklung). An dem Wettbewerbsprojekt waren fünf Entwickler beteiligt: CTO, Teamleiter, Spezialisten für Blockchain-Plattformen und Softwareentwickler in Haskell.

Die Ressourcen für die Teilnahme am Wettbewerb haben wir ohne Mühe gefunden, denn der Geist eines Hackathons, enge Teamarbeit und die Notwendigkeit, sich schnell in neue Technologien einzuarbeiten, sind immer aufregend. Mehrere schlaflose Nächte, um im Rahmen knapper Ressourcen das bestmögliche Ergebnis zu erzielen, werden durch wertvolle Erfahrungen und großartige Erinnerungen belohnt. Zudem ist die Arbeit an solchen Aufgaben immer eine gute Prüfung der internen Prozesse des Unternehmens, da es äußerst schwierig ist, wirklich gute Ergebnisse ohne perfekte interne Zusammenarbeit zu erzielen.

Abgesehen von der Lyrik: Wir waren von dem Umfang der Arbeiten, die das Team von TON geleistet hat, beeindruckt. Ihnen ist es gelungen, ein komplexes, ansprechendes und vor allem funktionierendes System aufzubauen. TON hat sich als eine Plattform mit großem Potenzial erwiesen. Damit sich dieses Ökosystem weiterentwickeln kann, muss jedoch noch viel getan werden, sowohl hinsichtlich der Nutzung in Blockchain-Projekten als auch bei der Verbesserung der Entwicklungstools. Wir sind stolz darauf, jetzt Teil dieses Prozesses zu sein.

Wenn Sie nach dem Lesen dieses Artikels noch Fragen haben oder Ideen, wie Sie TON zur Lösung Ihrer Aufgaben einsetzen können, kontaktieren Sie uns — teilen wir gerne unsere Erfahrungen.

Quelle: habr.com

60GB SSD 8Gb DDR4