Transformând FunC în FunCtional folosind Haskell: cum a câștigat Serokell în Telegram Blockchain Competition

Cu siguranță ați auzit că Telegram se pregătește să lanseze o platformă blockchain, Ton.Dar s-ar putea să fi pierdut vestea că, nu cu mult timp în urmă, Telegram a anunțat un concurs pentru realizarea unuia sau mai multor contracte inteligente pentru această platformă.

Echipa Serokell, cu o vastă experiență în dezvoltarea proiectelor mari de blockchain, nu putea rămâne indiferentă. Am delegat cinci angajați la concurs, iar după doar două săptămâni au câștigat locul întâi sub (ne)modestul nickname Sexy Chameleon. În acest articol, vă voi povesti cum au reușit. Sperăm că, în următoarele zece minute, veți citi cel puțin o poveste interesantă, iar, în cel mai bun caz, veți găsi în ea ceva util pe care să-l aplicați în munca dumneavoastră.

Dar să începem cu o scurtă introducere în context.

Concursul și condițiile acestuia

Așadar, principalele sarcini ale participanților au fost realizarea unuia sau mai multor contracte inteligente propuse, precum și formularea de sugestii pentru îmbunătățirea ecosistemului TON. Concursul a avut loc între 24 septembrie și 15 octombrie, iar rezultatele au fost anunțate abia pe 15 noiembrie. Destul de lung, având în vedere că, între timp, Telegram a reușit să desfășoare și să anunțe rezultatele concursurilor de design și de dezvoltare a aplicațiilor în C++ pentru testarea și evaluarea calității apelurilor VoIP în Telegram.

Am ales din lista propusă de organizatori două contracte inteligente. Pentru unul dintre ele am folosit instrumentele furnizate împreună cu TON, iar al doilea l-am implementat într-un nou limbaj, dezvoltat de inginerii noștri special pentru TON și integrat în Haskell.

Alegerea unui limbaj de programare funcțional nu este întâmplătoare. În blogul nostru corporativ, adesea discutăm despre de ce considerăm că complexitatea limbajelor funcționale este o exagerare și de ce, în general, le preferăm pe cele orientate obiect. Apropo, aici găsiți și versiunea originală a acestui articol..

De ce am decis să participăm de fapt

Pe scurt, pentru că specializarea noastră sunt proiectele neconvenționale și complexe, care necesită abilități speciale și care, adesea, au o valoare științifică pentru comunitatea IT. Susținem cu pasiune dezvoltarea open-source și ne implicăm în popularizarea acesteia, și colaborează cu cele mai importante universități din Rusia în domeniul științelor informației și matematicii.

Provocările interesante ale concursului și implicarea în proiectul nostru iubit, Telegram, au fost o motivație excelentă, iar premiul a reprezentat un stimulent suplimentar. 🙂

Cercetarea blockchain-ului TON

Urmărim cu atenție noile dezvoltări în blockchain, inteligența artificială și învățarea automată, încercând să nu ratăm nicio lansare semnificativă din fiecare domeniu în care activăm. Așadar, cu ocazia începutului concursului, echipa noastră era deja familiarizată cu ideile din white paper-ul TON. Totuși, înainte de a începe să lucrăm cu TON, nu am analizat documentația tehnică și codul sursă efectiv al platformei, așa că primul pas a fost destul de evident — o cercetare atentă a documentației oficiale de pe site și în repository-ul proiectului.

La începutul concursului, codul era deja publicat, așa că, pentru a economisi timp, am decis să căutăm un ghid sau un rezumat scris de utilizatori. Din păcate, acest lucru nu a adus rezultate — în afară de instrucțiuni pentru construirea platformei pe Ubuntu, nu am găsit alte materiale.

Documentația în sine s-a dovedit a fi bine structurată, dar a fost uneori dificil de citit. Destul de des a trebuit să ne întoarcem la anumite puncte și să trecem de la descrierile de nivel înalt ale ideilor abstracte la detalii de nivel scăzut ale implementării.

Ar fi fost mai simplu dacă specificația nu ar fi avut deloc o descriere detaliată a implementării. Informația despre modul în care mașina virtuală își reprezintă stiva mai mult îi distrage pe dezvoltatorii care creează smart contracts pentru platforma TON, decât să îi ajute.

Nix: construim proiectul

La Serokell, suntem mari fani Nix. Ne construim proiectele folosind NixOps, iar pe toate serverele noastre este instalat NixOS. Datorită acestui lucru, toate construcțiile noastre sunt reproducibile și funcționează pe orice sistem de operare pe care poate fi instalat Nix.

Așadar, am început cu crearea Nix overlay cu expresia pentru construirea TON. Cu ajutorul acestuia, compilarea TON este extrem de simplă:

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

Rețineți că nu trebuie să instalați nicio dependență. Nix va face totul magic pentru dumneavoastră, indiferent dacă utilizați NixOS, Ubuntu sau macOS.

Programarea pentru TON

Codul contractelor inteligente în TON Network este executat pe TON Virtual Machine (TVM). TVM este mai complexă decât majoritatea celorlalte mașini virtuale și oferă funcționalități interesante, cum ar fi capacitatea de a lucra cu continuări (continuations) și linkuri către date.

Mai mult, echipa de la TON a creat nu mai puțin de trei limbaje de programare noi:

Fift — un limbaj de programare stivuit universal, asemănător cu Forth. Abilitatea sa superioară este capacitatea de a interacționa cu TVM.

FunC — un limbaj de programare pentru contracte inteligente, care este similar cu C și se compilează într-un alt limbaj — Fift Assembler.

Fift Assembler — o bibliotecă Fift pentru generarea de cod executabil binar pentru TVM. Fift Assembler nu dispune de un compilator. Este un limbaj încorporat orientat pe domeniu (eDSL).

Lucrările noastre de concurs

În sfârșit, a venit timpul să ne uităm la rezultatele eforturilor noastre.

Canal de plată asincron

Canalul de plată (payment channel) este un contract inteligent care permite doi utilizatori să trimită plăți în afara blockchain-ului. Ca rezultat, se economisesc nu doar bani (nu există comision), ci și timp (nu trebuie să aștepți procesarea unui nou bloc). Plățile pot fi orice sumă mică și se pot face atât de des cât este necesar. De asemenea, părțile nu trebuie să se încreadă una în cealaltă, deoarece corectitudinea plății finale este garantată de contractul inteligent.

Am găsit o soluție destul de simplă la problemă. Cele două părți pot schimba mesaje semnate, fiecare dintre acestea conținând două numere — suma totală plătită de fiecare dintre participanți. Aceste două numere funcționează ca ceasuri vectoriale în sistemele distribuite tradiționale și stabilesc ordinea „a avut loc înainte” asupra tranzacțiilor. Folosind aceste date, contractul va putea rezolva orice conflict posibil.

În realitate, pentru implementarea acestei idei este suficient un singur număr, dar am lăsat ambele, deoarece astfel am putut crea o interfață mai convenabilă pentru utilizatori. În plus, am decis să includem în fiecare mesaj și dimensiunea plății. Fără aceasta, dacă mesajul se pierde din anumite motive, deși toate sumele și calculul final vor fi corecte, utilizatorul ar putea să nu observe pierderea.

Pentru a verifica ideea noastră, am căutat exemple de utilizare a unui astfel de protocol simplu și concis de canal de plată. Spre surprinderea noastră, am găsit doar două:

  1. Descriere o abordare similară, doar pentru un canal unidirecțional.
  2. Tutorial, care descrie aceeași idee ca a noastră, dar fără explicația multor detalii importante, cum ar fi corectitudinea generală și procedura de rezolvare a conflictelor.

A devenit clar că are sens să descriem în detaliu protocolul nostru, acordând o atenție deosebită corectitudinii sale. După câteva iterații, specificația a fost finalizată, iar acum o puteți consulta.

Am implementat un contract pe FunC, iar utilitarul de linie de comandă pentru interacțiunea cu contractul nostru l-am scris complet în Fift, așa cum au recomandat organizatorii. Am fi putut alege orice altă limbă pentru CLI-ul nostru, dar am fost curioși să încercăm Fift pentru a vedea cum se comportă în practică.

Sincer, după ce am lucrat cu Fift, nu am văzut motive convingătoare de a prefera această limbă în fața limbilor populare și larg utilizate cu un ecosistem bine dezvoltat de instrumente și biblioteci. Programarea într-o limbă bazată pe stivă este destul de neplăcută, deoarece trebuie să ții mereu minte ce se află unde în stivă, iar compilatorul nu ajută cu nimic în acest sens.

De aceea, singura justificare pe care o vedem pentru existența Fift este rolul său ca limbaj gazdă pentru Fift Assembler. Dar nu ar fi fost mai bine să integrăm assemblerul TVM într-o limbă existentă, decât să inventăm una nouă pentru acest scop, care este, în esență, singurul?

TVM Haskell eDSL

Acum este timpul să vorbim despre al doilea nostru smart contract. Am decis să dezvoltăm un portofel cu multiple semnături, dar să scriem încă un smart contract pe FunC ar fi fost prea plictisitor. Ne-am dorit să adăugăm un pic de originalitate, iar aceasta a fost noua noastră limbă de asamblare pentru TVM.

Ca și Fift Assembler, noua noastră limbă este integrabilă, doar că, în loc de Fift, am ales Haskell ca gazdă, ceea ce ne-a permis să beneficiem pe deplin de sistemul său avansat de tipuri. Când lucrăm cu smart contracte, unde prețul unei greșeli minore poate fi foarte mare, tipizarea statică, în opinia noastră, este un mare avantaj.

Pentru a demonstra cum arată asamblatorul TVM, integrat în Haskell, am implementat un portofel standard. Iată câteva aspecte la care merită să fii atent:

  • Acest contract constă într-o singură funcție, dar poți utiliza câte vrei. Atunci când definești o nouă funcție în limbajul gazdă (adică în Haskell), eDSL-ul nostru îți permite să alegi dacă vrei să se transforme într-o subprogramă separată în TVM sau să fie integrată direct în locul apelului.
  • La fel ca în Haskell, funcțiile au tipuri care sunt verificate în timpul compilării. În eDSL-ul nostru, tipul de intrare al funcției este tipul stivei pe care funcția o așteaptă, iar tipul rezultat este tipul stivei care va rezulta după apel.
  • Codul conține adnotări stacktype, care descriu tipul așteptat al stivei la punctul de apel. În contractul original al portofelului, acestea erau doar comentarii, dar în eDSL-ul nostru ele fac parte efectiv din cod și sunt verificate în timpul compilării. Ele pot servi ca documentație sau afirmații care ajută dezvoltatorul să identifice o problemă în cazul în care tipul stivei se va schimba datorită modificărilor în cod. Desigur, astfel de adnotări nu afectează performanța în timpul execuției, deoarece nu este generat niciun cod TVM pentru ele.
  • Acesta este încă un prototip, scris în două săptămâni, așa că proiectul necesită încă multă muncă. De exemplu, toate instanțele claselor pe care le vezi în codul de mai jos trebuie generate automat.

Iată cum arată implementarea portofelului multisig în eDSL-ul nostru:

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

Codul sursă complet al eDSL-ului nostru și contractul portofelului cu semnături multiple pot fi găsite în această repo. Un mai detaliat a explicat despre limbajele integrate colegul nostru Georgii Agapov.

Concluziile despre competiție și TON

În total, munca noastră a durat 380 de ore (inclusiv familiarizarea cu documentația, întâlnirile și dezvoltarea propriu-zisă). La proiectul competițional au participat cinci dezvoltatori: CTO, team lead, specialiști în platforme blockchain și dezvoltatori de software în Haskell.

Resursele pentru a participa la concurs le-am găsit fără dificultate, deoarece spiritul hackathon-ului, munca în echipă strânsă și necesitatea de a ne adapta rapid la aspectele noilor tehnologii sunt întotdeauna captivante. Câteva nopți albastre în numele obținerii celor mai bune rezultate în condiții de resurse limitate sunt compensate de experiența inestimabilă și amintirile excelente. În plus, lucrul la astfel de sarcini reprezintă întotdeauna o bună verificare a proceselor companiei, deoarece obținerea unor rezultate cu adevărat demne fără o interacțiune internă bine pusă la punct este extrem de dificilă.

În afară de lirică: am fost impresionați de volumul de muncă realizat de echipa TON. Au reușit să construiască un sistem complex, frumos și, cel mai important, funcțional. TON s-a dovedit a fi o platformă cu un mare potențial. Totuși, pentru ca această ecosistemă să evolueze, mai este mult de făcut, atât din perspectiva utilizării sale în proiectele blockchain, cât și din perspectiva îmbunătățirii instrumentelor de dezvoltare. Suntem mândri că acum facem parte din acest proces.

Dacă după citirea acestui articol aveți întrebări sau ați dezvoltat idei despre cum să aplicați TON pentru a rezolva sarcinile dumneavoastră, scrieți-ne — ne face plăcere să împărtășim experiența noastră.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster