Gjør FunC til FunCtional med Haskell: Hvordan Serokell vant Telegram Blockchain-konkurransen

Du har sikkert hørt det Telegram er i ferd med å lansere Ton blockchain-plattformen. Men du har kanskje gått glipp av nyhetene som ikke lenge siden Telegram utlyst en konkurranse for implementering av en eller flere smarte kontrakter for denne plattformen.

Serokell-teamet, med lang erfaring i å utvikle store blokkjedeprosjekter, kunne ikke stå til side. Vi delegerte fem ansatte til konkurransen, og to uker senere tok de førsteplassen i den under det (u)beskjedne tilfeldige kallenavnet Sexy Chameleon. I denne artikkelen vil jeg snakke om hvordan de gjorde det. Vi håper at du i løpet av de neste ti minuttene i det minste vil lese en interessant historie, og på det meste vil du finne noe nyttig i den som du kan bruke i arbeidet ditt.

Men la oss starte med en liten kontekst.

Konkurranse og dens betingelser

Så, hovedoppgavene til deltakerne var implementeringen av en eller flere av de foreslåtte smarte kontraktene, samt å lage forslag for å forbedre TON-økosystemet. Konkurransen gikk fra 24. september til 15. oktober, og resultatene ble offentliggjort først 15. november. Ganske lang tid, med tanke på at Telegram i løpet av denne tiden klarte å holde og kunngjøre resultatene av konkurranser om design og utvikling av applikasjoner i C++ for testing og vurdering av kvaliteten på VoIP-samtaler i Telegram.

Vi valgte ut to smarte kontrakter fra listen foreslått av arrangørene. For ett av dem brukte vi verktøy distribuert med TON, og det andre ble implementert i et nytt språk utviklet av våre ingeniører spesielt for TON og innebygd i Haskell.

Valget av et funksjonelt programmeringsspråk er ikke tilfeldig. I vår bedriftsblogg Vi snakker ofte om hvorfor vi synes kompleksiteten til funksjonelle språk er en stor overdrivelse og hvorfor vi generelt foretrekker dem fremfor objektorienterte. Den inneholder forresten også originalen til denne artikkelen.

Hvorfor bestemte vi oss for å delta?

Kort sagt, fordi vår spesialisering er ikke-standardiserte og komplekse prosjekter som krever spesielle ferdigheter og ofte er av vitenskapelig verdi for IT-miljøet. Vi støtter sterkt åpen kildekode-utvikling og er engasjert i populariseringen av den, og samarbeider også med ledende russiske universiteter innen informatikk og matematikk.

De interessante oppgavene til konkurransen og engasjementet i vårt elskede Telegram-prosjekt var i seg selv en utmerket motivasjon, men premiefondet ble et ekstra insentiv. 🙂

TON blockchain forskning

Vi følger nøye med på ny utvikling innen blokkjede, kunstig intelligens og maskinlæring og prøver å ikke gå glipp av en eneste betydelig utgivelse innen hvert av områdene vi jobber med. Derfor, da konkurransen startet, var teamet vårt allerede kjent med ideer fra TONN hvitt papir. Men før vi startet arbeidet med TON, analyserte vi ikke den tekniske dokumentasjonen og den faktiske kildekoden til plattformen, så det første trinnet var ganske åpenbart - en grundig studie av den offisielle dokumentasjonen på nettsted og prosjektdepoter.

Da konkurransen startet var koden allerede publisert, så for å spare tid bestemte vi oss for å se etter en guide eller et sammendrag skrevet av av brukere. Dette ga dessverre ingen resultater – bortsett fra instruksjoner for montering av plattformen på Ubuntu, fant vi ikke noe annet materiale.

Selve dokumentasjonen var godt undersøkt, men var vanskelig å lese på enkelte områder. Ganske ofte måtte vi gå tilbake til visse punkter og bytte fra beskrivelser på høyt nivå av abstrakte ideer til implementeringsdetaljer på lavt nivå.

Det ville vært enklere om spesifikasjonen ikke inneholdt en detaljert beskrivelse av implementeringen i det hele tatt. Informasjon om hvordan en virtuell maskin representerer stabelen sin er mer sannsynlig å distrahere utviklere som lager smarte kontrakter for TON-plattformen enn å hjelpe dem.

Nix: sette sammen prosjektet

Hos Serokell er vi store fans Nix. Vi samler prosjektene våre med den og distribuerer dem ved hjelp av NixOps, og installert på alle våre servere Nix OS. Takket være dette er alle byggene våre reproduserbare og fungerer på alle operativsystemer som Nix kan installeres på.

Så vi startet med å lage Nix overlegg med uttrykk for TON montering. Med dens hjelp er kompilering av TON så enkelt som mulig:

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

Merk at du ikke trenger å installere noen avhengigheter. Nix vil på magisk vis gjøre alt for deg, enten du bruker NixOS, Ubuntu eller macOS.

Programmering for TON

Den smarte kontraktskoden i TON-nettverket kjører på TON Virtual Machine (TVM). TVM er mer kompleks enn de fleste andre virtuelle maskiner, og har svært interessant funksjonalitet, den kan for eksempel jobbe med fortsettelser и lenker til data.

Dessuten har gutta fra TON laget tre nye programmeringsspråk:

Fift er et universelt stabel programmeringsspråk som ligner Frem. Hans superevne er evnen til å samhandle med TVM.

FunC er et smart kontraktsprogrammeringsspråk som ligner på C og er kompilert til et annet språk - Fift Assembler.

Femte montør — Fift-bibliotek for å generere binær kjørbar kode for TVM. Fifth Assembler har ikke en kompilator. Dette Embedded Domain Specific Language (eDSL).

Konkurransen vår fungerer

Endelig er det på tide å se på resultatene av vår innsats.

Asynkron betalingskanal

Betalingskanal er en smart kontrakt som lar to brukere sende betalinger utenfor blokkjeden. Som et resultat sparer du ikke bare penger (det er ingen provisjon), men også tid (du trenger ikke å vente på at neste blokk skal behandles). Betalinger kan være så små som ønsket og så ofte som nødvendig. I dette tilfellet trenger ikke partene å stole på hverandre, siden rettferdigheten til det endelige oppgjøret er garantert av den smarte kontrakten.

Vi fant en ganske enkel løsning på problemet. To parter kan utveksle signerte meldinger, som hver inneholder to numre – hele beløpet betalt av hver part. Disse to tallene fungerer som vektor klokke i tradisjonelle distribuerte systemer og sett "hendt før"-rekkefølgen på transaksjoner. Ved å bruke disse dataene vil kontrakten kunne løse enhver mulig konflikt.

Faktisk er ett tall nok til å implementere denne ideen, men vi forlot begge fordi vi på denne måten kunne lage et mer praktisk brukergrensesnitt. I tillegg bestemte vi oss for å inkludere betalingsbeløpet i hver melding. Uten det, hvis meldingen går tapt av en eller annen grunn, kan det hende at brukeren ikke legger merke til tapet, selv om alle beløpene og den endelige beregningen vil være korrekte.

For å teste ideen vår så vi etter eksempler på bruk av en så enkel og kortfattet betalingskanalprotokoll. Overraskende nok fant vi bare to:

  1. beskrivelse en lignende tilnærming, bare for tilfellet med en ensrettet kanal.
  2. Opplæringen, som beskriver samme idé som vår, men uten å forklare mange viktige detaljer, som generell korrekthet og konfliktløsningsprosedyrer.

Det ble klart at det er fornuftig å beskrive protokollen vår i detalj, med spesiell oppmerksomhet til korrektheten. Etter flere iterasjoner var spesifikasjonen klar, og nå kan du også. se på henne.

Vi implementerte kontrakten i FunC, og vi skrev kommandolinjeverktøyet for samhandling med kontrakten vår helt i Fift, som anbefalt av arrangørene. Vi kunne ha valgt et hvilket som helst annet språk for vår CLI, men vi var interessert i å prøve Fit for å se hvordan det presterte i praksis.

For å være ærlig, etter å ha jobbet med Fift, så vi ingen tvingende grunner til å foretrekke dette språket fremfor populære og aktivt brukte språk med utviklede verktøy og biblioteker. Programmering i et stack-basert språk er ganske ubehagelig, siden du hele tiden må ha i hodet hva som er på stabelen, og kompilatoren hjelper ikke med dette.

Derfor, etter vår mening, er den eneste begrunnelsen for eksistensen av Fift dens rolle som vertsspråk for Fift Assembler. Men ville det ikke vært bedre å bygge inn TVM-samleren i et eksisterende språk, i stedet for å finne opp et nytt for dette i hovedsak eneste formål?

TVM Haskell eDSL

Nå er det på tide å snakke om vår andre smarte kontrakt. Vi bestemte oss for å utvikle en lommebok med flere signaturer, men å skrive en annen smart kontrakt i FunC ville vært for kjedelig. Vi ønsket å tilføre litt smak, og det var vårt eget assembly-språk for TVM.

I likhet med Fift Assembler er det nye språket vårt innebygd, men vi valgte Haskell som vert i stedet for Fift, slik at vi kan dra full nytte av det avanserte systemet. Når man jobber med smarte kontrakter, hvor kostnaden for selv en liten feil kan være svært høy, er statisk skriving etter vår mening en stor fordel.

For å demonstrere hvordan TVM assembler ser ut innebygd i Haskell, implementerte vi en standard lommebok på den. Her er noen ting du bør være oppmerksom på:

  • Denne kontrakten består av én funksjon, men du kan bruke så mange du vil. Når du definerer en ny funksjon på vertsspråket (dvs. Haskell), lar vår eDSL deg velge om du vil at den skal bli en egen rutine i TVM eller bare innebygd ved anropspunktet.
  • I likhet med Haskell har funksjoner typer som kontrolleres ved kompilering. I vår eDSL er inngangstypen til en funksjon den typen stabel som funksjonen forventer, og resultattypen er stabeltypen som vil bli produsert etter anropet.
  • Koden har merknader stacktype, som beskriver forventet stabeltype ved melderen. I den opprinnelige lommebokkontrakten var dette bare kommentarer, men i vår eDSL er de faktisk en del av koden og blir sjekket på kompileringstidspunktet. De kan tjene som dokumentasjon eller uttalelser som hjelper utvikleren med å finne problemet hvis koden endres og stabeltypen endres. Slike merknader påvirker selvfølgelig ikke kjøretidsytelsen, siden ingen TVM-kode genereres for dem.
  • Dette er fortsatt en prototype skrevet på to uker, så det gjenstår fortsatt mye arbeid med prosjektet. For eksempel bør alle forekomster av klassene du ser i koden nedenfor genereres automatisk.

Slik ser implementeringen av en multisig-lommebok ut på vår 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

Den fullstendige kildekoden til vår eDSL- og multisignaturlommebokkontrakt finner du på dette depotet. Og mer fortalt i detalj om innebygde språk, vår kollega Georgy Agapov.

Konklusjoner om konkurransen og TON

Totalt tok arbeidet vårt 380 timer (inkludert kjennskap til dokumentasjon, møter og faktisk utvikling). Fem utviklere deltok i konkurranseprosjektet: CTO, teamleder, blockchain-plattformspesialister og Haskell-programvareutviklere.

Vi fant ressurser til å delta i konkurransen uten problemer, siden ånden til et hackathon, tett teamarbeid og behovet for raskt å fordype oss i aspekter av nye teknologier er alltid spennende. Flere søvnløse netter for å oppnå maksimale resultater under forhold med begrensede ressurser blir kompensert av uvurderlig erfaring og gode minner. I tillegg er det å jobbe med slike oppgaver alltid en god test av selskapets prosesser, siden det er ekstremt vanskelig å oppnå virkelig anstendige resultater uten velfungerende intern interaksjon.

Til side for tekster: vi ble imponert over mengden arbeid som ble lagt ned av TON-teamet. De klarte å bygge et komplekst, vakkert og viktigst av alt, fungerende system. TON har vist seg å være en plattform med stort potensial. Men for at dette økosystemet skal utvikle seg, må mye mer gjøres, både når det gjelder bruken i blokkjedeprosjekter og når det gjelder å forbedre utviklingsverktøyene. Vi er stolte av å nå være en del av denne prosessen.

Hvis du etter å ha lest denne artikkelen fortsatt har spørsmål eller ideer om hvordan du bruker TON for å løse problemene dine, skriv til oss — Vi deler gjerne vår erfaring.

Kilde: www.habr.com

Legg til en kommentar