Transformer FunC en FunCtional avec Haskell : comment Serokell a remporté le concours Telegram Blockchain

Vous avez probablement entendu ce télégramme est sur le point de lancer la plateforme blockchain Ton. Mais vous avez peut-être raté la nouvelle qu'il n'y a pas si longtemps, Telegram a annoncé un concours pour la mise en œuvre d'un ou plusieurs contrats intelligents pour cette plateforme.

L'équipe Serokell, possédant une vaste expérience dans le développement de grands projets blockchain, ne pouvait pas rester à l'écart. Nous avons délégué cinq employés au concours et, deux semaines plus tard, ils ont pris la première place sous le surnom (peu) modeste et aléatoire de Sexy Chameleon. Dans cet article, je vais parler de la façon dont ils l'ont fait. Nous espérons qu'au cours des dix prochaines minutes, vous lirez au moins une histoire intéressante et que vous y trouverez tout au plus quelque chose d'utile que vous pourrez appliquer dans votre travail.

Mais commençons par un peu de contexte.

La concurrence et ses conditions

Ainsi, les tâches principales des participants étaient la mise en œuvre d'un ou plusieurs des contrats intelligents proposés, ainsi que la formulation de propositions visant à améliorer l'écosystème TON. Le concours s'est déroulé du 24 septembre au 15 octobre et les résultats n'ont été annoncés que le 15 novembre. Assez longtemps, sachant que pendant ce temps Telegram a réussi à organiser et à annoncer les résultats de concours sur la conception et le développement d'applications en C++ pour tester et évaluer la qualité des appels VoIP dans Telegram.

Nous avons sélectionné deux contrats intelligents dans la liste proposée par les organisateurs. Pour l'un d'eux, nous avons utilisé des outils distribués avec TON, et le second a été implémenté dans un nouveau langage développé par nos ingénieurs spécifiquement pour TON et intégré à Haskell.

Le choix d'un langage de programmation fonctionnel n'est pas accidentel. Dans notre blog d'entreprise Nous expliquons souvent pourquoi nous pensons que la complexité des langages fonctionnels est une énorme exagération et pourquoi nous les préférons généralement aux langages orientés objet. D'ailleurs, il contient également original de cet article.

Pourquoi avons-nous décidé de participer ?

En bref, parce que notre spécialisation concerne les projets non standards et complexes qui nécessitent des compétences particulières et ont souvent une valeur scientifique pour la communauté informatique. Nous soutenons fermement le développement du code source ouvert, nous nous engageons dans sa vulgarisation et coopérons également avec les principales universités russes dans le domaine de l'informatique et des mathématiques.

Les tâches intéressantes du concours et l'implication dans notre projet Telegram bien-aimé étaient en soi une excellente motivation, mais le fonds du prix est devenu une incitation supplémentaire. 🙂

Recherche sur la blockchain TON

Nous suivons de près les nouveaux développements dans les domaines de la blockchain, de l’intelligence artificielle et de l’apprentissage automatique et essayons de ne manquer aucune version significative dans chacun des domaines dans lesquels nous travaillons. Par conséquent, au moment où le concours a commencé, notre équipe connaissait déjà les idées de Livre blanc TON. Cependant, avant de commencer à travailler avec TON, nous n'avons pas analysé la documentation technique et le code source réel de la plateforme, la première étape était donc assez évidente : une étude approfondie de la documentation officielle sur En ligne et référentiels de projet.

Au début du concours, le code avait déjà été publié, donc pour gagner du temps, nous avons décidé de chercher un guide ou un résumé rédigé par utilisateurs. Malheureusement, cela n'a donné aucun résultat - à part les instructions pour assembler la plateforme sur Ubuntu, nous n'avons trouvé aucun autre matériel.

La documentation elle-même était bien documentée, mais était difficile à lire dans certains domaines. Très souvent, nous avons dû revenir sur certains points et passer de descriptions de haut niveau d'idées abstraites à des détails de mise en œuvre de bas niveau.

Ce serait plus facile si la spécification n'incluait pas du tout de description détaillée de la mise en œuvre. Les informations sur la façon dont une machine virtuelle représente sa pile sont plus susceptibles de distraire les développeurs créant des contrats intelligents pour la plate-forme TON que de les aider.

Nix : monter le projet

Chez Serokell, nous sommes de grands fans Nix. Nous collectons nos projets avec et les déployons en utilisant NixOps, et installé sur tous nos serveurs Nix OS. Grâce à cela, toutes nos builds sont reproductibles et fonctionnent sur n'importe quel système d'exploitation sur lequel Nix peut être installé.

Nous avons donc commencé par créer Superposition Nix avec expression pour l'assemblage TON. Avec son aide, compiler TON est aussi simple que possible :

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

Notez que vous n’avez pas besoin d’installer de dépendances. Nix fera tout pour vous comme par magie, que vous utilisiez NixOS, Ubuntu ou macOS.

Programmation pour TON

Le code de contrat intelligent du réseau TON s'exécute sur la machine virtuelle TON (TVM). TVM est plus complexe que la plupart des autres machines virtuelles et possède des fonctionnalités très intéressantes, par exemple, il peut fonctionner avec suites и liens vers des données.

De plus, les gars de TON ont créé trois nouveaux langages de programmation :

Cinquante est un langage de programmation à pile universelle qui ressemble à En avant. Sa super capacité est la capacité d'interagir avec TVM.

FunC est un langage de programmation de contrats intelligents similaire à C et est compilé dans un autre langage - Fift Assembler.

Cinquième Assembleur — Cinquième bibliothèque pour générer du code exécutable binaire pour TVM. Fifth Assembler n'a pas de compilateur. Ce Langage spécifique au domaine intégré (eDSL).

Notre concours fonctionne

Enfin, il est temps d'examiner les résultats de nos efforts.

Canal de paiement asynchrone

Le canal de paiement est un contrat intelligent qui permet à deux utilisateurs d'envoyer des paiements en dehors de la blockchain. En conséquence, vous économisez non seulement de l’argent (il n’y a pas de commission), mais aussi du temps (vous n’avez pas besoin d’attendre que le bloc suivant soit traité). Les paiements peuvent être aussi modestes que souhaités et aussi souvent que nécessaire. Dans ce cas, les parties ne sont pas obligées de se faire confiance, puisque l’équité du règlement final est garantie par le contrat intelligent.

Nous avons trouvé une solution assez simple au problème. Deux parties peuvent échanger des messages signés, chacun contenant deux chiffres, soit le montant total payé par chaque partie. Ces deux nombres fonctionnent comme horloge de vecteur dans les systèmes distribués traditionnels et définir l'ordre « arrivé avant » sur les transactions. Grâce à ces données, le contrat pourra résoudre tout conflit éventuel.

En fait, un seul numéro suffit pour mettre en œuvre cette idée, mais nous avons laissé les deux car nous pourrions ainsi créer une interface utilisateur plus pratique. De plus, nous avons décidé d'inclure le montant du paiement dans chaque message. Sans cela, si le message est perdu pour une raison quelconque, même si tous les montants et le calcul final seront corrects, l'utilisateur risque de ne pas remarquer la perte.

Pour tester notre idée, nous avons recherché des exemples d’utilisation d’un protocole de canal de paiement aussi simple et concis. Étonnamment, nous n’en avons trouvé que deux :

  1. description une approche similaire, uniquement pour le cas d'un canal unidirectionnel.
  2. Didacticiel, qui décrit la même idée que la nôtre, mais sans expliquer de nombreux détails importants, tels que l'exactitude générale et les procédures de résolution des conflits.

Il est devenu évident qu'il était logique de décrire notre protocole en détail, en accordant une attention particulière à son exactitude. Après plusieurs itérations, la spécification était prête, et maintenant vous le pouvez aussi. regarde la.

Nous avons implémenté le contrat dans FunC et nous avons écrit l'utilitaire de ligne de commande pour interagir avec notre contrat entièrement dans Fift, comme recommandé par les organisateurs. Nous aurions pu choisir n'importe quel autre langage pour notre CLI, mais nous souhaitions essayer Fit pour voir comment il fonctionnait dans la pratique.

Pour être honnête, après avoir travaillé avec Fift, nous n'avons vu aucune raison impérieuse de préférer ce langage aux langages populaires et activement utilisés avec des outils et des bibliothèques développés. Programmer dans un langage basé sur la pile est assez désagréable, car vous devez constamment garder en tête ce qui se trouve sur la pile, et le compilateur ne vous aide pas.

Par conséquent, à notre avis, la seule justification de l’existence de Fift est son rôle de langage hôte pour Fift Assembler. Mais ne serait-il pas préférable d'intégrer l'assembleur TVM dans un langage existant, plutôt que d'en inventer un nouveau dans ce seul but ?

TVM Haskell eDSL

Il est maintenant temps de parler de notre deuxième contrat intelligent. Nous avons décidé de développer un portefeuille multi-signatures, mais écrire un autre contrat intelligent dans FunC serait trop ennuyeux. Nous voulions ajouter un peu de saveur, et c'était notre propre langage d'assemblage pour TVM.

Comme Fift Assembler, notre nouveau langage est intégré, mais nous avons choisi Haskell comme hôte au lieu de Fift, ce qui nous permet de profiter pleinement de son système de types avancé. Lorsque l’on travaille avec des contrats intelligents, où le coût même d’une petite erreur peut être très élevé, la saisie statique constitue, à notre avis, un gros avantage.

Pour démontrer à quoi ressemble l'assembleur TVM intégré dans Haskell, nous avons implémenté un portefeuille standard dessus. Voici quelques éléments auxquels il faut prêter attention :

  • Ce contrat comprend une fonction, mais vous pouvez en utiliser autant que vous le souhaitez. Lorsque vous définissez une nouvelle fonction dans le langage hôte (c'est-à-dire Haskell), notre eDSL vous permet de choisir si vous souhaitez qu'elle devienne une routine distincte dans TVM ou simplement intégrée au point d'appel.
  • Comme Haskell, les fonctions ont des types qui sont vérifiés au moment de la compilation. Dans notre eDSL, le type d'entrée d'une fonction est le type de pile attendu par la fonction, et le type de résultat est le type de pile qui sera produit après l'appel.
  • Le code a des annotations stacktype, décrivant le type de pile attendu au point d'appel. Dans le contrat original du portefeuille, il ne s'agissait que de commentaires, mais dans notre eDSL, ils font en fait partie du code et sont vérifiés au moment de la compilation. Ils peuvent servir de documentation ou d'instructions qui aident le développeur à trouver le problème si le code change et le type de pile change. Bien entendu, de telles annotations n’ont pas d’impact sur les performances d’exécution, puisqu’aucun code TVM n’est généré pour elles.
  • Il s'agit encore d'un prototype écrit en deux semaines, il reste donc encore beaucoup de travail à faire sur le projet. Par exemple, toutes les instances des classes que vous voyez dans le code ci-dessous doivent être générées automatiquement.

Voici à quoi ressemble la mise en place d'un portefeuille multisig sur notre eDSL :

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

Le code source complet de notre contrat eDSL et portefeuille multi-signatures est disponible sur ce référentiel. Et plus raconté en détail sur les langages intégrés, notre collègue Georgy Agapov.

Conclusions sur la compétition et TON

Au total, notre travail a duré 380 heures (y compris la familiarisation avec la documentation, les réunions et le développement proprement dit). Cinq développeurs ont participé au projet du concours : le CTO, le chef d'équipe, les spécialistes de la plateforme blockchain et les développeurs de logiciels Haskell.

Nous avons trouvé les ressources pour participer au concours sans difficulté, car l'esprit d'un hackathon, le travail d'équipe serré et le besoin de s'immerger rapidement dans les aspects des nouvelles technologies sont toujours passionnants. Plusieurs nuits blanches pour obtenir des résultats optimaux dans des conditions de ressources limitées sont compensées par une expérience inestimable et d'excellents souvenirs. De plus, travailler sur de telles tâches est toujours un bon test des processus de l’entreprise, car il est extrêmement difficile d’obtenir des résultats vraiment décents sans une interaction interne qui fonctionne bien.

Paroles mises à part : nous avons été impressionnés par la quantité de travail fourni par l’équipe de TON. Ils ont réussi à construire un système complexe, beau et, surtout, fonctionnel. TON s’est révélé être une plateforme à fort potentiel. Cependant, pour que cet écosystème se développe, il reste encore beaucoup à faire, tant en termes d’utilisation dans les projets blockchain qu’en termes d’amélioration des outils de développement. Nous sommes fiers de faire désormais partie de ce processus.

Si après avoir lu cet article vous avez encore des questions ou des idées sur la façon d'utiliser TON pour résoudre vos problèmes, écrivez-nous — nous serons heureux de partager notre expérience.

Source: habr.com

Ajouter un commentaire