Trasformare FunC in FunCtional con Haskell: come Serokell ha vinto il concorso Blockchain di Telegram

Probabilmente hai sentito quel Telegram sta per lanciare la piattaforma blockchain Ton. Ma potresti aver perso la notizia che non molto tempo fa Telegram bandito un concorso per l'implementazione di uno o più smart contract per questa piattaforma.

Il team Serokell, con una vasta esperienza nello sviluppo di grandi progetti blockchain, non poteva restare da parte. Abbiamo delegato cinque dipendenti al concorso e due settimane dopo si sono classificati al primo posto con il (in)modesto soprannome casuale Sexy Chameleon. In questo articolo parlerò di come hanno fatto. Ci auguriamo che nei prossimi dieci minuti leggerai almeno una storia interessante, e al massimo troverai in essa qualcosa di utile che potrai applicare nel tuo lavoro.

Ma cominciamo con un piccolo contesto.

La concorrenza e le sue condizioni

Pertanto, i compiti principali dei partecipanti erano l’implementazione di uno o più contratti intelligenti proposti, nonché la presentazione di proposte per migliorare l’ecosistema TON. Il concorso si è svolto dal 24 settembre al 15 ottobre e i risultati sono stati annunciati solo il 15 novembre. Parecchio tempo, considerando che durante questo periodo Telegram è riuscito a tenere e annunciare i risultati dei concorsi sulla progettazione e lo sviluppo di applicazioni in C++ per testare e valutare la qualità delle chiamate VoIP in Telegram.

Abbiamo selezionato due smart contract dalla lista proposta dagli organizzatori. Per uno di essi abbiamo utilizzato gli strumenti distribuiti con TON, mentre il secondo è stato implementato in un nuovo linguaggio sviluppato dai nostri ingegneri appositamente per TON e integrato in Haskell.

La scelta di un linguaggio di programmazione funzionale non è casuale. Nel nostro blog aziendale Parliamo spesso del motivo per cui riteniamo che la complessità dei linguaggi funzionali sia un’enorme esagerazione e del motivo per cui generalmente li preferiamo a quelli orientati agli oggetti. A proposito, contiene anche originale di questo articolo.

Perché abbiamo deciso di partecipare?

In breve, perché la nostra specializzazione sono progetti non standard e complessi che richiedono competenze particolari e spesso hanno valore scientifico per la comunità IT. Sosteniamo fortemente lo sviluppo open source e siamo impegnati nella sua divulgazione e collaboriamo anche con le principali università russe nel campo dell'informatica e della matematica.

Gli interessanti compiti del concorso e il coinvolgimento nel nostro amato progetto Telegram sono stati di per sé un'ottima motivazione, ma il montepremi è diventato un ulteriore incentivo. 🙂

Ricerca sulla blockchain di TON

Monitoriamo attentamente i nuovi sviluppi nel campo della blockchain, dell'intelligenza artificiale e dell'apprendimento automatico e cerchiamo di non perdere nemmeno un rilascio significativo in ciascuna delle aree in cui operiamo. Pertanto, quando è iniziata la competizione, il nostro team aveva già familiarità con le idee di TON carta bianca. Tuttavia, prima di iniziare a lavorare con TON, non abbiamo analizzato la documentazione tecnica e il codice sorgente effettivo della piattaforma, quindi il primo passo era abbastanza ovvio: uno studio approfondito della documentazione ufficiale su sito web e repository di progetti.

Al momento dell'inizio del concorso il codice era già stato pubblicato, quindi per risparmiare tempo abbiamo deciso di cercare una guida o un riassunto scritto da dagli utenti. Sfortunatamente, questo non ha dato alcun risultato: oltre alle istruzioni per assemblare la piattaforma su Ubuntu, non abbiamo trovato altro materiale.

La documentazione in sé era ben documentata, ma in alcune aree era difficile da leggere. Molto spesso siamo dovuti ritornare su certi punti e passare dalle descrizioni di alto livello delle idee astratte ai dettagli di implementazione di basso livello.

Sarebbe più semplice se le specifiche non includessero affatto una descrizione dettagliata dell'implementazione. È più probabile che le informazioni su come una macchina virtuale rappresenta il suo stack distraggano gli sviluppatori che creano contratti intelligenti per la piattaforma TON piuttosto che aiutarli.

Nix: mettere insieme il progetto

A Serokell siamo grandi fan Nix. Raccogliamo i nostri progetti con esso e li distribuiamo utilizzando NixOpse installato su tutti i nostri server Sistema operativo Nix. Grazie a questo, tutte le nostre build sono riproducibili e funzionano su qualsiasi sistema operativo su cui è possibile installare Nix.

Quindi abbiamo iniziato creando Sovrapposizione Nix con espressione per l'assemblaggio TON. Con il suo aiuto, compilare TON è il più semplice possibile:

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

Tieni presente che non è necessario installare alcuna dipendenza. Nix farà magicamente tutto per te, sia che tu utilizzi NixOS, Ubuntu o macOS.

Programmazione per TON

Il codice del contratto intelligente nella rete TON viene eseguito sulla TON Virtual Machine (TVM). TVM è più complessa della maggior parte delle altre macchine virtuali e ha funzionalità molto interessanti con cui, ad esempio, può funzionare continuazioni и collegamenti ai dati.

Inoltre, i ragazzi di TON hanno creato tre nuovi linguaggi di programmazione:

Cinquanta è un linguaggio di programmazione stack universale che assomiglia a Via. La sua super abilità è la capacità di interagire con TVM.

FunC è un linguaggio di programmazione per contratti intelligenti simile a C ed è compilato in un altro linguaggio: Fift Assembler.

Quinto assemblatore — Libreria Fift per generare codice eseguibile binario per TVM. Fifth Assembler non ha un compilatore. Questo Linguaggio specifico del dominio incorporato (eDSL).

Il nostro concorso funziona

Infine, è il momento di guardare ai risultati dei nostri sforzi.

Canale di pagamento asincrono

Il canale di pagamento è un contratto intelligente che consente a due utenti di inviare pagamenti al di fuori della blockchain. Di conseguenza, risparmi non solo denaro (non ci sono commissioni), ma anche tempo (non devi aspettare l’elaborazione del blocco successivo). I pagamenti possono essere piccoli quanto desiderato e con la frequenza richiesta. In questo caso le parti non devono fidarsi l’una dell’altra, poiché l’equità della transazione finale è garantita dallo smart contract.

Abbiamo trovato una soluzione abbastanza semplice al problema. Due parti possono scambiarsi messaggi firmati, ciascuno contenente due numeri: l'intero importo pagato da ciascuna parte. Questi due numeri funzionano come orologio vettoriale nei sistemi distribuiti tradizionali e impostare l'ordine "accaduto prima" sulle transazioni. Utilizzando questi dati, il contratto sarà in grado di risolvere ogni possibile conflitto.

In effetti, un numero è sufficiente per implementare questa idea, ma li abbiamo lasciati entrambi perché in questo modo avremmo potuto creare un'interfaccia utente più comoda. Inoltre, abbiamo deciso di includere l'importo del pagamento in ogni messaggio. Senza di esso, se per qualche motivo il messaggio viene perso, anche se tutti gli importi e il calcolo finale saranno corretti, l'utente potrebbe non notare la perdita.

Per testare la nostra idea, abbiamo cercato esempi di utilizzo di un protocollo di canale di pagamento così semplice e conciso. Sorprendentemente, ne abbiamo trovati solo due:

  1. descrizione un approccio simile, solo per il caso di un canale unidirezionale.
  2. Tutorial, che descrive la nostra stessa idea, ma senza spiegare molti dettagli importanti, come la correttezza generale e le procedure di risoluzione dei conflitti.

È diventato chiaro che ha senso descrivere il nostro protocollo in dettaglio, prestando particolare attenzione alla sua correttezza. Dopo diverse iterazioni, la specifica era pronta e ora puoi farlo anche tu. guardala.

Abbiamo implementato il contratto in FunC e abbiamo scritto l'utilità della riga di comando per interagire con il nostro contratto interamente in Fift, come raccomandato dagli organizzatori. Avremmo potuto scegliere qualsiasi altra lingua per la nostra CLI, ma eravamo interessati a provare Fit per vedere come funzionava nella pratica.

Ad essere onesti, dopo aver lavorato con Fift, non abbiamo visto alcun motivo convincente per preferire questo linguaggio a linguaggi popolari e utilizzati attivamente con strumenti e librerie sviluppate. Programmare in un linguaggio basato sullo stack è piuttosto spiacevole, poiché devi tenere costantemente a mente ciò che è nello stack e il compilatore non aiuta in questo.

Pertanto, a nostro avviso, l’unica giustificazione per l’esistenza di Fift è il suo ruolo come linguaggio host per Fift Assembler. Ma non sarebbe meglio incorporare l'assemblatore TVM in qualche linguaggio esistente, piuttosto che inventarne uno nuovo per questo scopo essenzialmente unico?

TVM Haskell eDSL

Ora è il momento di parlare del nostro secondo contratto intelligente. Abbiamo deciso di sviluppare un portafoglio multifirma, ma scrivere un altro contratto intelligente in FunC sarebbe troppo noioso. Volevamo aggiungere un po' di sapore e quello era il nostro linguaggio assembly per TVM.

Come Fift Assembler, il nostro nuovo linguaggio è incorporato, ma abbiamo scelto Haskell come host invece di Fift, permettendoci di sfruttare appieno il suo sistema di tipi avanzato. Quando si lavora con contratti intelligenti, dove il costo anche di un piccolo errore può essere molto alto, la tipizzazione statica, a nostro avviso, è un grande vantaggio.

Per dimostrare come appare l'assemblatore TVM incorporato in Haskell, abbiamo implementato un portafoglio standard su di esso. Ecco alcune cose a cui prestare attenzione:

  • Questo contratto consiste in una funzione, ma puoi utilizzarne quante vuoi. Quando definisci una nuova funzione nella lingua host (ad esempio Haskell), la nostra eDSL ti consente di scegliere se desideri che diventi una routine separata in TVM o semplicemente incorporata nel punto di chiamata.
  • Come Haskell, le funzioni hanno tipi che vengono controllati in fase di compilazione. Nella nostra eDSL, il tipo di input di una funzione è il tipo di stack che la funzione si aspetta, e il tipo di risultato è il tipo di stack che verrà prodotto dopo la chiamata.
  • Il codice ha annotazioni stacktype, che descrive il tipo di stack previsto nel punto di chiamata. Nel contratto originale del wallet questi erano solo commenti, nella nostra eDSL invece fanno parte del codice e vengono controllati in fase di compilazione. Possono fungere da documentazione o istruzioni che aiutano lo sviluppatore a individuare il problema se il codice cambia e il tipo di stack cambia. Naturalmente, tali annotazioni non influiscono sulle prestazioni di runtime, poiché per esse non viene generato alcun codice TVM.
  • Questo è ancora un prototipo scritto in due settimane, quindi c'è ancora molto lavoro da fare sul progetto. Ad esempio, tutte le istanze delle classi visualizzate nel codice seguente dovrebbero essere generate automaticamente.

Ecco come appare l'implementazione di un portafoglio multisig sulla nostra 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

Il codice sorgente completo del nostro contratto eDSL e portafoglio multifirma è disponibile all'indirizzo questo deposito. E altro ancora raccontato in dettaglio sui linguaggi integrati, il nostro collega Georgy Agapov.

Conclusioni sul concorso e su TON

In totale, il nostro lavoro ha richiesto 380 ore (inclusa la familiarità con la documentazione, le riunioni e lo sviluppo effettivo). Al progetto del concorso hanno preso parte cinque sviluppatori: CTO, team leader, specialisti della piattaforma blockchain e sviluppatori di software Haskell.

Abbiamo trovato le risorse per partecipare al concorso senza difficoltà, poiché lo spirito di un hackathon, il lavoro di squadra e la necessità di immergerci rapidamente negli aspetti delle nuove tecnologie sono sempre entusiasmanti. Diverse notti insonni per ottenere i massimi risultati in condizioni di risorse limitate sono compensate da un'esperienza preziosa e da ricordi eccellenti. Inoltre, lavorare su tali compiti è sempre un buon test per i processi aziendali, poiché è estremamente difficile ottenere risultati veramente dignitosi senza un'interazione interna ben funzionante.

Testi a parte: siamo rimasti colpiti dalla mole di lavoro svolta dal team TON. Sono riusciti a costruire un sistema complesso, bello e, soprattutto, funzionante. TON ha dimostrato di essere una piattaforma con un grande potenziale. Tuttavia, affinché questo ecosistema possa svilupparsi, occorre fare molto di più, sia in termini di utilizzo nei progetti blockchain, sia in termini di miglioramento degli strumenti di sviluppo. Siamo orgogliosi di essere ora parte di questo processo.

Se dopo aver letto questo articolo hai ancora domande o hai idee su come utilizzare TON per risolvere i tuoi problemi, Scrivi il nome — saremo felici di condividere la nostra esperienza.

Fonte: habr.com

Aggiungi un commento