Trasformando FunC in FunCtional con Haskell: come Serokell ha vinto il Telegram Blockchain Competition

Hai sicuramente sentito che Telegram intende lanciare una piattaforma blockchain Ton. Ma potresti aver perso la notizia che poco tempo fa Telegram ha annunciato un concorso per l'implementazione di uno o più smart contract per questa piattaforma.

Il team di Serokell, con una ricca esperienza nello sviluppo di grandi progetti blockchain, non poteva rimanere a guardare. Abbiamo delegato al concorso cinque dipendenti e già dopo due settimane hanno conquistato il primo posto con il (non)modesto nickname random Sexy Chameleon. In questo articolo ti racconterò come ci sono riusciti. Speriamo che nei prossimi dieci minuti tu possa almeno leggere una storia interessante e, nel migliore dei casi, trovare qualcosa di utile da applicare nel tuo lavoro.

Ma iniziamo con un breve approfondimento del contesto.

Il concorso e le sue condizioni

Dunque, le principali task dei partecipanti erano l'implementazione di uno o più degli smart contract proposti, oltre a presentare suggerimenti 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. Abbastanza lungo, considerando che nel frattempo Telegram ha condotto e reso pubblici i risultati dei contest sul design e sullo sviluppo di applicazioni in C++ per il test e la valutazione della qualità delle chiamate VoIP su Telegram.

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

La scelta del linguaggio di programmazione funzionale non è casuale. Nel nostro blog aziendale parliamo spesso del motivo per cui consideriamo la complessità dei linguaggi funzionali un grande esagerazione e perché in generale li preferiamo a quelli orientati agli oggetti. A proposito, lì è disponibile anche l'originale di questo articolo.

Perché abbiamo deciso di partecipare

In sintesi, perché la nostra specializzazione sono progetti non standard e complessi che richiedono abilità particolari e spesso rappresentano un valore scientifico per la comunità IT. Sosteniamo con fervore lo sviluppo open-source e ci dedichiamo alla sua diffusione, collaborando anche con le principali università della Russia nel campo delle scienze informatiche e della matematica.

Le interessanti sfide del concorso e il coinvolgimento nel progetto Telegram, che amiamo molto, erano già di per sé una grande motivazione, e il montepremi è stato uno stimolo aggiuntivo. 🙂

Ricerca sulla blockchain TON

Seguiamo attentamente le nuove sviluppi nel campo della blockchain, dell'intelligenza artificiale e dell'apprendimento automatico, cercando di non perdere nessun rilascio significativo in ciascuno dei settori in cui lavoriamo. Pertanto, al momento dell'inizio del concorso, il nostro team era già a conoscenza delle idee contenute in TON white paper. Tuttavia, prima di iniziare a lavorare con TON, non avevamo analizzato la documentazione tecnica e il codice sorgente effettivo della piattaforma, quindi il primo passo è stato piuttosto ovvio: un'attenta ricerca della documentazione ufficiale su sito e in repository del progetto.

All'inizio del concorso, il codice era già stato pubblicato, quindi, per risparmiare tempo, abbiamo deciso di cercare una guida o un riassunto scritto da utenti. Purtroppo, questo non ha portato a risultati: oltre alle istruzioni per compilare la piattaforma su Ubuntu, non abbiamo trovato altri materiali.

La documentazione stessa si è rivelata ben dettagliata, ma in alcuni punti era difficile da leggere. Spesso ci siamo trovati a tornare su certi punti e a passare da descrizioni di alto livello di idee astratte a dettagli di basso livello sull'implementazione.

Sarebbe stato più semplice se nella specifica non ci fosse stato affatto alcun dettaglio sull'implementazione. Le informazioni su come la macchina virtuale presenta il suo stack tendono a distrarre più gli sviluppatori che creano smart contract per la piattaforma TON, piuttosto che aiutarli.

Nix: compiliamo il progetto

In Serokell siamo grandi fan di Nix. Utilizziamo Nix per compilare i nostri progetti e distribuirli tramite NixOps, e tutti i nostri server hanno installato NixOS. Grazie a ciò, tutte le nostre build sono riproducibili e funzionano su qualsiasi sistema operativo in cui può essere installato Nix.

Quindi abbiamo iniziato creando un overlay Nix con un'espressione per la compilazione di TON. Con questo, compilare TON è estremamente semplice:

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

Si noti che non è necessario installare alcuna dipendenza. Nix farà tutto magicamente per voi, indipendentemente dal fatto che stiate utilizzando NixOS, Ubuntu o macOS.

Programmazione per TON

Il codice dei contratti intelligenti nella TON Network viene eseguito sulla TON Virtual Machine (TVM). La TVM è più complessa rispetto alla maggior parte delle altre macchine virtuali e offre funzionalità molto interessanti, ad esempio può lavorare con continuazioni e riferimenti ai dati.

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

Fift — un linguaggio di programmazione a stack universale, simile a Forth. La sua super-capacità è la possibilità di interagire con la TVM.

FunC — un linguaggio di programmazione per contratti intelligenti, simile a C e si compila in un altro linguaggio — Fift Assembler.

Fift Assembler — una libreria Fift per generare codice eseguibile binario per la TVM. Fift Assembler non ha un compilatore. È un linguaggio specifico orientato ai domini (eDSL).

I nostri lavori per il concorso

Finalmente, è tempo di vedere i risultati dei nostri sforzi.

Canale di pagamento asincrono

Un canale di pagamento (payment channel) è un contratto intelligente che consente a due utenti di inviare pagamenti al di fuori della blockchain. Di conseguenza, si risparmia non solo denaro (non ci sono commissioni), ma anche tempo (non è necessario attendere che venga elaborato il blocco successivo). I pagamenti possono essere di qualsiasi dimensione e avvenire così frequentemente come necessario. Inoltre, non è necessario fidarsi l'uno dell'altro, poiché la giustizia del pagamento finale è garantita dal contratto intelligente.

Abbiamo trovato una soluzione piuttosto semplice al problema. Le due parti possono scambiarsi messaggi firmati, ognuno dei quali contiene due numeri: l'importo totale pagato da ciascun partecipante. Questi due numeri funzionano come orologi vettoriali nelle tradizionali sistemi distribuiti e definiscono l'ordine 'è accaduto prima' delle transazioni. Utilizzando questi dati, il contratto potrà risolvere qualsiasi possibile conflitto.

In realtà, per implementare questa idea basta un solo numero, ma abbiamo mantenuto entrambi, poiché in questo modo siamo riusciti a creare un'interfaccia utente più comoda. Inoltre, abbiamo deciso di includere in ogni messaggio la dimensione del pagamento. Senza di essa, se un messaggio dovesse andare perso per qualche motivo, sebbene tutti gli importi e il regolamento finale siano corretti, l'utente potrebbe non accorgersi della perdita.

Per verificare la nostra idea, abbiamo cercato esempi di utilizzo di un protocollo di pagamento così semplice e conciso. Con sorpresa, abbiamo trovato solo due:

  1. Descrizione approcci simili, ma per un canale unidirezionale.
  2. Tutorial, che descrive la stessa idea che abbiamo, ma senza spiegare molti dettagli importanti, come la correttezza generale e la procedura 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 potete anche voi darle un'occhiata.

Abbiamo implementato il contratto su FunC, mentre l'utilità della riga di comando per interagire con il nostro contratto è stata completamente scritta in Fift, come consigliato dagli organizzatori. Avremmo potuto scegliere qualsiasi altro linguaggio per il nostro CLI, ma ci interessava provare proprio Fift, per vedere come si comportava.

A dire il vero, lavorando con Fift, non abbiamo visto valide ragioni per preferire questo linguaggio a linguaggi più popolari e attivamente utilizzati, dotati di strumenti e librerie sviluppati. Programmare in un linguaggio stack non è particolarmente piacevole, poiché bisogna sempre tenere a mente cosa sia dove nello stack, e il compilatore non aiuta affatto.

Pertanto, l'unica giustificazione per noi dell'esistenza di Fift è il suo ruolo come linguaggio host per Fift Assembler. Ma non sarebbe stato meglio integrare l'assemblatore TVM in qualche linguaggio esistente, piuttosto che inventarne uno nuovo per questo, in sostanza, unico scopo?

TVM Haskell eDSL

È giunto il momento di parlare del nostro secondo smart contract. Abbiamo deciso di sviluppare un portafoglio con firma multipla, ma scrivere un altro smart contract su FunC sarebbe stato troppo noioso. Volevamo aggiungere un tocco speciale, e questo è stato il nostro linguaggio di assemblaggio per TVM.

Proprio come Fift Assembler, il nostro nuovo linguaggio è integrabile, solo che come host al posto di Fift abbiamo scelto Haskell, il che ci ha permesso di sfruttare appieno il suo avanzato sistema di tipi. Lavorando con smart contract, dove il costo anche di un piccolo errore può essere molto elevato, riteniamo che la tipizzazione statica sia un grande vantaggio.

Per dimostrare come appare l'assemblatore TVM integrato in Haskell, abbiamo implementato un wallet standard. Ecco alcune cose a cui prestare attenzione:

  • Questo contratto consiste in una sola funzione, ma puoi usarne quante vuoi. Quando definisci una nuova funzione nel linguaggio host (cioè in Haskell), il nostro eDSL ti consente di scegliere se desideri che diventi un sottoprogramma separato in TVM o venga semplicemente incorporata nel punto di chiamata.
  • Come in Haskell, le funzioni hanno tipi che vengono verificati durante la compilazione. Nel nostro eDSL, il tipo di ingresso della funzione è il tipo di stack che la funzione si aspetta, mentre il tipo di risultato è il tipo di stack che si otterrà dopo la chiamata.
  • Nel codice ci sono annotazioni stacktype, che descrivono il tipo di stack atteso nel punto di chiamata. Nel contratto originale del wallet erano semplicemente commenti, ma nel nostro eDSL sono effettivamente parte del codice e vengono verificati durante la compilazione. Possono servire come documentazione o assert che aiutano lo sviluppatore a trovare problemi nel caso in cui, modificando il codice, il tipo di stack cambi. È scontato che tali annotazioni non influenzano le prestazioni durante l'esecuzione, poiché non viene generato alcun codice TVM per esse.
  • È ancora un prototipo, scritto in due settimane, quindi c'è ancora molto lavoro da fare. Ad esempio, tutte le istanze delle classi che vedi nel codice qui sotto dovrebbero essere generate automaticamente.

Ecco come appare l'implementazione di un wallet multisig nel nostro 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 eDSL e del contratto del portafoglio multi-firma può essere trovato in questo repository. E in più ha parlato più dettagliatamente sui linguaggi incorporati, il nostro collega Georgy Agapov.

Conclusioni sul concorso e TON

In totale, il nostro lavoro ha richiesto 380 ore (incluso il tempo per familiarizzare con la documentazione, riunioni e sviluppo diretto). Al progetto del concorso hanno partecipato cinque sviluppatori: CTO, team lead, specialisti di piattaforme blockchain e sviluppatori di software in Haskell.

Abbiamo trovato facilmente le risorse per partecipare al contest, poiché lo spirito dell'hackathon, il lavoro di squadra ravvicinato e la necessità di immergersi rapidamente negli aspetti delle nuove tecnologie sono sempre entusiasmanti. Diverse notti insonni dedicate a raggiungere il massimo risultato in condizioni di risorse limitate vengono compensate da un'esperienza inestimabile e ottimi ricordi. Inoltre, lavorare su compiti simili è sempre un buon test per i processi aziendali, poiché ottenere risultati veramente degni senza un'interazione interna ben collaudata è estremamente difficile.

Oltre alla lirica: siamo stati colpiti dall'enorme lavoro svolto dal team di TON. Sono riusciti a costruire un sistema complesso, bello e, soprattutto, funzionante. TON si è dimostrato una piattaforma con un grande potenziale. Tuttavia, affinché questo ecosistema si sviluppi, c'è ancora molto da fare sia dal punto di vista del suo utilizzo nei progetti blockchain, sia dal punto di vista del miglioramento degli strumenti di sviluppo. Siamo orgogliosi di far parte di questo processo.

Se dopo aver letto questo articolo avete domande o idee su come utilizzare TON per risolvere i vostri problemi, contattateci — saremo lieti di condividere la nostra esperienza.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster