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

Avrete sicuramente sentito che Telegram si prepara a lanciare la piattaforma blockchain Ton. Ma potreste aver perso la notizia che poco tempo fa Telegram ha annunciato un concorso per implementare uno o più contratti smart per questa piattaforma.

Il team di Serokell, con una vasta esperienza nello sviluppo di grandi progetti blockchain, non poteva rimanere indifferente. Abbiamo delegato cinque membri al concorso e, già dopo due settimane, hanno conquistato il primo posto con il (non)modesto nickname Sexy Chameleon. In questo articolo vi racconterò come ci sono riusciti. Speriamo che nei prossimi dieci minuti leggiate almeno una storia interessante, e al massimo troviate qualcosa di utile da applicare nel vostro lavoro.

Ma iniziamo con un breve approfondimento sul contesto.

Il concorso e le sue condizioni

Così, le principali responsabilità dei partecipanti sono state l'implementazione di uno o più dei contratti intelligenti proposti, oltre a presentare suggerimenti per migliorare l'ecosistema TON. Il concorso si è tenuto dal 24 settembre al 15 ottobre, e i risultati sono stati annunciati solo il 15 novembre. Abbastanza a lungo, considerando che in quel periodo Telegram ha già condotto e annunciato i risultati dei contest sul design e dello sviluppo di applicazioni in C++ per testare e valutare la qualità delle chiamate VoIP su Telegram.

Abbiamo scelto due contratti intelligenti dalla lista fornita dagli organizzatori. Per uno di essi abbiamo utilizzato gli strumenti forniti insieme a TON, e il secondo è stato realizzato 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 riteniamo che la complessità dei linguaggi funzionali sia una grande esagerazione e perché, in generale, li preferiamo a quelli orientati agli oggetti. A proposito, qui c'è anche l'originale di questo articolo.

Perché abbiamo deciso di partecipare

In breve, la nostra specializzazione è in progetti personalizzati e complessi che richiedono abilità particolari e spesso rappresentano un valore scientifico per la comunità IT. Sosteniamo con entusiasmo lo sviluppo open-source e ci impegniamo nella sua promozione, collaborando anche con le principali università della Russia nel campo delle scienze informatiche e della matematica.

Le sfide interessanti del concorso e il coinvolgimento nel progetto Telegram, che amiamo, sono stati già di per sé una grande motivazione, mentre il montepremi ha fornito un ulteriore stimolo. 🙂

Ricerca sulla blockchain TON

Seguiamo da vicino le nuove sviluppi nel campo della blockchain, dell'intelligenza artificiale e dell'apprendimento automatico, cercando di non perdere nessun rilascio significativo in ciascuna delle aree in cui operiamo. Pertanto, al momento dell'inizio del concorso, il nostro team era già a conoscenza delle idee provenienti da TON white paper. Tuttavia, prima di iniziare a lavorare con TON, non avevamo analizzato la documentazione tecnica e il codice sorgente attuale della piattaforma, quindi il primo passo è stato piuttosto ovvio: un'attenta analisi della documentazione ufficiale su sito e nel il 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 dagli utenti. Sfortunatamente, non abbiamo ottenuto risultati - oltre a un'istruzione su come assemblare la piattaforma su Ubuntu, non abbiamo trovato altri materiali.

La documentazione stessa si è rivelata accuratamente sviluppata, ma in alcuni punti era difficile da leggere. Spesso ci siamo trovati a tornare su determinati argomenti e a passare da descrizioni ad alto livello di concetti astratti a dettagli di implementazione a basso livello.

Sarebbe stato più semplice se le specifiche non avessero fornito affatto una descrizione dettagliata dell'implementazione. Le informazioni su come la macchina virtuale rappresenta il proprio stack piuttosto distraggono gli sviluppatori che creano contratti intelligenti per la piattaforma TON, piuttosto che aiutarli.

Nix: stiamo assemblando il progetto

In Serokell siamo grandi fan Nix. Assemblando i nostri progetti e distribuendoli utilizzando NixOps, tutti i nostri server sono dotati di NixOS. Grazie a questo, tutte le nostre build sono riproducibili e funzionano su qualsiasi sistema operativo su cui può essere installato Nix.

Pertanto, abbiamo iniziato con la creazione Overlay Nix con un'espressione per la build di TON. Con questo, compilare TON diventa molto semplice:

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

Notate che non è necessario installare alcuna dipendenza. Nix fa tutto magicamente per voi, indipendentemente dal fatto che stiate usando 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 della maggior parte delle altre macchine virtuali e offre funzionalità molto interessanti, come la capacità di 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 stack universale, simile a Forth. La sua superpotenza è la possibilità di interagire con la TVM.

FunC — un linguaggio di programmazione per contratti intelligenti, simile a C e compilato 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. Questa è una lingua orientata agli oggetti integrata (eDSL).

I nostri lavori di concorso

Finalmente, è il momento di dare un'occhiata ai 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, non solo si risparmiano soldi (non ci sono commissioni), ma anche tempo (non è necessario attendere l'elaborazione di un blocco). I pagamenti possono essere di qualsiasi importo e avvenire con la frequenza necessaria. Inoltre, le parti non devono necessariamente fidarsi l'una dell'altra, poiché l'equità del regolamento finale è garantita dal contratto intelligente.

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

In realtà, per realizzare questa idea è sufficiente un solo numero, ma abbiamo voluto mantenerne entrambi, poiché ciò ci ha permesso di creare un'interfaccia utente più comoda. Inoltre, abbiamo deciso di includere in ogni messaggio l'importo del pagamento. Senza di esso, se il messaggio per qualche motivo dovesse andare perso, anche se tutte le somme e il conteggio finale sono corretti, l'utente potrebbe non accorgersi della perdita.

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

  1. Descrizione un approccio simile, ma per un canale unidirezionale.
  2. Tutorial, che descrive la stessa idea della nostra, ma senza spiegare molti dettagli importanti, come la correttezza complessiva e la procedura di risoluzione dei conflitti.

È diventato chiaro che ha senso descrivere dettagliatamente il nostro protocollo, prestando particolare attenzione alla sua correttezza. Dopo diverse iterazioni, la specifica è stata completata e ora anche voi potete darle un'occhiata..

Abbiamo implementato un contratto su FunC e l'utilità della riga di comando per interagire con il nostro contratto è stata completamente scritta in Fift, come raccomandato dagli organizzatori. Avremmo potuto scegliere qualsiasi altra lingua per il nostro CLI, ma siamo stati curiosi di provare proprio Fift per vedere come si sarebbe comportato in azione.

A dire il vero, dopo aver lavorato con Fift, non abbiamo visto motivi validi per preferire questo linguaggio a linguaggi più popolari e utilizzati, con strumenti e librerie più sviluppati. Programmare in un linguaggio a stack è piuttosto scomodo, poiché bisogna continuamente tenere a mente dove si trova ogni cosa nello stack, e il compilatore non aiuta affatto in questo.

Perciò, l'unica giustificazione che vediamo per l'esistenza di Fift è il suo ruolo come linguaggio ospite per l'Assemblatore Fift. Ma non sarebbe stato meglio integrare l'assemblatore TVM in un linguaggio esistente piuttosto che inventarne uno nuovo per questa, fondamentalmente, unica finalità?

TVM Haskell eDSL

È ora di parlare del nostro secondo smart contract. Abbiamo deciso di sviluppare un wallet con firma multipla, ma scrivere un altro smart contract in FunC sarebbe stato troppo noioso. Volevamo aggiungere un tocco speciale e questo è diventato il nostro linguaggio assembler personale per TVM.

Proprio come Fift Assembler, il nostro nuovo linguaggio è incorporato, ma invece di Fift abbiamo scelto Haskell come host, il che ci ha permesso di sfruttare appieno il suo avanzato sistema di tipi. Quando si lavora con smart contract, dove il costo anche di un piccolo errore può essere molto elevato, la tipizzazione statica è, a nostro avviso, un grande vantaggio.

Per dimostrare come appare l'assembler TVM integrato in Haskell, abbiamo implementato un wallet standard. Ecco alcune cose su cui vale la pena prestare attenzione:

  • Questo contratto è composto da una sola funzione, ma puoi utilizzare quante ne vuoi. Quando definisci una nuova funzione nel linguaggio host (cioè in Haskell), il nostro eDSL ti consente di scegliere se desideri che diventi una sottoprogramma separato in TVM o semplicemente sia incorporata nel punto di chiamata.
  • Come in Haskell, anche le funzioni hanno tipi che vengono verificati durante la compilazione. Nel nostro eDSL, il tipo di input di una 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 portafoglio erano semplici commenti, ma nel nostro eDSL fanno effettivamente parte del codice e vengono verificati durante la compilazione. Possono fungere da documentazione o affermazioni che aiutano lo sviluppatore a trovare un problema nel caso in cui il tipo di stack cambi durante le modifiche al codice. Naturalmente, tali annotazioni non influenzano le performance durante l'esecuzione, poiché non viene generato alcun codice TVM per esse.
  • È ancora un prototipo, scritto in due sole settimane, quindi c'è ancora molto lavoro da fare sul progetto. Ad esempio, tutte le istanze delle classi che vedete nel codice sottostante dovrebbero essere generate automaticamente.

Ecco a che punto siamo con l'implementazione del portafoglio 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 il contratto del portafoglio con firma multipla possono essere trovati in questo repository. Un approfondimento è stato fornito dal nostro collega Georgij Agapov. Conclusioni sul concorso e TON

In totale, il nostro lavoro ha richiesto 380 ore (compreso il tempo per familiarizzare con la documentazione, le riunioni e lo sviluppo vero e proprio). Cinque sviluppatori hanno partecipato al progetto: il CTO, il team leader, specialisti in piattaforme blockchain e sviluppatori software in Haskell.

Abbiamo trovato senza difficoltà le risorse per partecipare al contest, poiché lo spirito dell'hackathon, il lavoro di squadra intenso e la necessità di un rapido approccio a nuove tecnologie sono sempre entusiasmanti. Alcune notti insonni per raggiungere il risultato massimo in condizioni di risorse limitate sono compensate da preziose esperienze e ottimi ricordi. Inoltre, lavorare su compiti di questo tipo è sempre una buona verifica dei processi aziendali, poiché ottenere risultati veramente validi senza un'interazione interna ben collaudata è estremamente difficile.

Le risorse per partecipare al contest sono state facilmente trovate, poiché lo spirito dell'hackathon, il lavoro di squadra stretto e la necessità di immergersi rapidamente in nuovi aspetti tecnologici sono sempre stimolanti. Diverse notti insonni per raggiungere il massimo risultato in condizioni di risorse limitate sono compensate da un'esperienza preziosa e ottimi ricordi. Inoltre, lavorare su compiti simili è sempre una buona verifica dei processi dell'azienda, poiché ottenere risultati veramente significativi senza una perfetta interazione interna è estremamente difficile.

Oltre alla lirica: siamo rimasti colpiti dalla quantità di lavoro svolta dal team TON. Sono riusciti a costruire un sistema complesso, bello e soprattutto funzionante. TON si è dimostrato una piattaforma con un grande potenziale. Tuttavia, per far sì che questo ecosistema cresca, c'è ancora molto da fare, sia in termini di utilizzo nei progetti blockchain sia in termini di perfezionamento degli strumenti di sviluppo. Siamo orgogliosi di far parte di questo processo.

Se dopo aver letto questo articolo avete domande o idee su come applicare TON alle vostre necessità, contattateci — saremo felici 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