Att förvandla FunC till FunCtional med Haskell: Hur Serokell vann Telegram Blockchain-tävlingen

Du har säkert hört det där Telegram är på väg att lansera Ton blockchain-plattformen. Men du kanske har missat nyheterna som inte så länge sedan Telegram utlyst en tävling för implementering av ett eller flera smarta kontrakt för denna plattform.

Serokell-teamet, med lång erfarenhet av att utveckla stora blockchain-projekt, kunde inte stå åt sidan. Vi delegerade fem anställda till tävlingen och två veckor senare tog de första platsen i den under det (o)modiga slumpmässiga smeknamnet Sexy Chameleon. I den här artikeln kommer jag att prata om hur de gjorde det. Vi hoppas att du under de närmaste tio minuterna åtminstone kommer att läsa en intressant berättelse, och som mest kommer du att hitta något användbart i den som du kan tillämpa i ditt arbete.

Men låt oss börja med ett litet sammanhang.

Konkurrens och dess förutsättningar

Så deltagarnas huvuduppgifter var implementeringen av ett eller flera av de föreslagna smarta kontrakten, samt att lägga fram förslag för att förbättra TON-ekosystemet. Tävlingen pågick från 24 september till 15 oktober, och resultaten tillkännagavs först den 15 november. Ganska lång tid, med tanke på att Telegram under denna tid lyckades hålla och tillkännage resultaten av tävlingar om design och utveckling av applikationer i C++ för att testa och bedöma kvaliteten på VoIP-samtal i Telegram.

Vi valde ut två smarta kontrakt från listan som föreslagits av arrangörerna. För en av dem använde vi verktyg som distribuerades med TON, och det andra implementerades i ett nytt språk utvecklat av våra ingenjörer specifikt för TON och inbyggt i Haskell.

Valet av ett funktionellt programmeringsspråk är inte av misstag. I vår företagsblogg Vi pratar ofta om varför vi tycker att komplexiteten hos funktionella språk är en enorm överdrift och varför vi i allmänhet föredrar dem framför objektorienterade. Den innehåller förresten också original av denna artikel.

Varför valde vi ens att delta?

Kort sagt för att vår specialisering är icke-standardiserade och komplexa projekt som kräver speciell kompetens och ofta är av vetenskapligt värde för IT-gemenskapen. Vi stöder starkt utveckling av öppen källkod och är engagerade i dess popularisering och samarbetar även med ledande ryska universitet inom datavetenskap och matematik.

Tävlingens intressanta uppgifter och engagemang i vårt älskade Telegram-projekt var i sig en utmärkt motivation, men prisfonden blev ytterligare ett incitament. 🙂

TON blockchain forskning

Vi övervakar noggrant ny utveckling inom blockchain, artificiell intelligens och maskininlärning och försöker att inte missa en enda betydande release inom vart och ett av de områden där vi arbetar. Därför, när tävlingen startade, var vårt team redan bekant med idéer från TON vitt papper. Men innan vi började arbeta med TON analyserade vi inte den tekniska dokumentationen och själva källkoden för plattformen, så det första steget var ganska uppenbart - en grundlig studie av den officiella dokumentationen på Online och projektförråd.

När tävlingen startade hade koden redan publicerats, så för att spara tid bestämde vi oss för att leta efter en guide eller sammanfattning skriven av användare. Tyvärr gav detta inga resultat - förutom instruktioner för montering av plattformen på Ubuntu hittade vi inget annat material.

Själva dokumentationen var väl genomarbetad, men svårläst på vissa områden. Ganska ofta var vi tvungna att återvända till vissa punkter och byta från högnivåbeskrivningar av abstrakta idéer till lågnivåimplementeringsdetaljer.

Det skulle vara lättare om specifikationen inte alls innehöll en detaljerad beskrivning av implementeringen. Information om hur en virtuell maskin representerar sin stack är mer sannolikt att distrahera utvecklare som skapar smarta kontrakt för TON-plattformen än att hjälpa dem.

Nix: sätta ihop projektet

På Serokell är vi stora fans Nix. Vi samlar våra projekt med den och distribuerar dem med hjälp av NixOps, och installerat på alla våra servrar Nix OS. Tack vare detta är alla våra builds reproducerbara och fungerar på alla operativsystem som Nix kan installeras på.

Så vi började med att skapa Nix överlägg med uttryck för TON montering. Med dess hjälp är det så enkelt som möjligt att kompilera TON:

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

Observera att du inte behöver installera några beroenden. Nix kommer magiskt att göra allt för dig, oavsett om du använder NixOS, Ubuntu eller macOS.

Programmering för TON

Den smarta kontraktskoden i TON-nätverket körs på TON Virtual Machine (TVM). TVM är mer komplex än de flesta andra virtuella maskiner, och har mycket intressant funktionalitet, den kan till exempel fungera med fortsättningar и länkar till data.

Dessutom skapade killarna från TON tre nya programmeringsspråk:

Fift är ett universellt stackprogrammeringsspråk som liknar Vidare. Hans superförmåga är förmågan att interagera med TVM.

FunC är ett smart kontraktsprogrammeringsspråk som liknar C och är sammanställt till ett annat språk - Fift Assembler.

Femte Assembler — Fift-bibliotek för att generera binär körbar kod för TVM. Fifth Assembler har ingen kompilator. Detta Embedded Domain Specific Language (eDSL).

Vår tävling fungerar

Äntligen är det dags att titta på resultaten av våra ansträngningar.

Asynkron betalningskanal

Betalningskanal är ett smart kontrakt som låter två användare skicka betalningar utanför blockkedjan. Som ett resultat sparar du inte bara pengar (det finns ingen provision), utan också tid (du behöver inte vänta på att nästa block ska behandlas). Betalningar kan vara så små som önskas och så ofta som krävs. I det här fallet behöver parterna inte lita på varandra, eftersom rättvisan i den slutliga uppgörelsen garanteras av det smarta kontraktet.

Vi hittade en ganska enkel lösning på problemet. Två parter kan utbyta undertecknade meddelanden som var och en innehåller två nummer – hela beloppet som betalas av varje part. Dessa två siffror fungerar som vektor klocka i traditionella distribuerade system och ställ in "hände före"-ordningen på transaktioner. Genom att använda dessa data kommer kontraktet att kunna lösa eventuella konflikter.

Faktum är att ett nummer räcker för att implementera denna idé, men vi lämnade båda eftersom vi på så sätt kunde skapa ett mer bekvämt användargränssnitt. Dessutom bestämde vi oss för att ta med betalningsbeloppet i varje meddelande. Utan det, om meddelandet försvinner av någon anledning, så, även om alla belopp och den slutliga beräkningen kommer att vara korrekta, kanske användaren inte märker förlusten.

För att testa vår idé letade vi efter exempel på att använda ett så enkelt och kortfattat betalningskanalprotokoll. Överraskande nog hittade vi bara två:

  1. beskrivning ett liknande tillvägagångssätt, endast för fallet med en enkelriktad kanal.
  2. Handledning, som beskriver samma idé som vår, men utan att förklara många viktiga detaljer, såsom allmän korrekthet och konfliktlösningsprocedurer.

Det blev tydligt att det är vettigt att beskriva vårt protokoll i detalj, med särskild uppmärksamhet på dess riktighet. Efter flera iterationer var specifikationen klar, och nu kan du också. kolla på henne.

Vi implementerade kontraktet i FunC, och vi skrev kommandoradsverktyget för att interagera med vårt kontrakt helt och hållet i Fift, som rekommenderas av arrangörerna. Vi kunde ha valt vilket annat språk som helst för vår CLI, men vi var intresserade av att prova Fit för att se hur det fungerade i praktiken.

För att vara ärlig, efter att ha arbetat med Fift såg vi inga övertygande skäl att föredra detta språk framför populära och aktivt använda språk med utvecklade verktyg och bibliotek. Att programmera på ett stackbaserat språk är ganska obehagligt, eftersom man hela tiden måste hålla i huvudet vad som finns på stacken, och kompilatorn hjälper inte till med detta.

Därför, enligt vår åsikt, är den enda motiveringen för existensen av Fift dess roll som värdspråk för Fift Assembler. Men skulle det inte vara bättre att bädda in TVM assembler i något befintligt språk, snarare än att uppfinna ett nytt för detta i huvudsak enda syfte?

TVM Haskell eDSL

Nu är det dags att prata om vårt andra smarta kontrakt. Vi bestämde oss för att utveckla en plånbok med flera signaturer, men att skriva ett annat smart kontrakt i FunC skulle vara för tråkigt. Vi ville tillföra lite smak, och det var vårt eget assemblerspråk för TVM.

Precis som Fift Assembler är vårt nya språk inbäddat, men vi valde Haskell som värd istället för Fift, vilket gör att vi kan dra full nytta av dess avancerade typsystem. När man arbetar med smarta kontrakt, där kostnaden för även ett litet fel kan vara mycket hög, är statisk skrivning enligt vår uppfattning en stor fördel.

För att demonstrera hur TVM assembler ser ut inbäddad i Haskell, implementerade vi en standardplånbok på den. Här är några saker att vara uppmärksam på:

  • Detta kontrakt består av en funktion, men du kan använda så många du vill. När du definierar en ny funktion på värdspråket (d.v.s. Haskell) låter vår eDSL dig välja om du vill att den ska bli en separat rutin i TVM eller helt enkelt infogas vid anropspunkten.
  • Liksom Haskell har funktioner typer som kontrolleras vid kompilering. I vår eDSL är ingångstypen för en funktion den typ av stack som funktionen förväntar sig, och resultattypen är den typ av stack som kommer att produceras efter anropet.
  • Koden har anteckningar stacktype, som beskriver den förväntade stacktypen vid anropspunkten. I det ursprungliga plånbokskontraktet var dessa bara kommentarer, men i vår eDSL är de faktiskt en del av koden och kontrolleras vid kompilering. De kan fungera som dokumentation eller uttalanden som hjälper utvecklaren att hitta problemet om koden ändras och stacktypen ändras. Naturligtvis påverkar sådana anteckningar inte körningsprestandan, eftersom ingen TVM-kod genereras för dem.
  • Det här är fortfarande en prototyp skriven på två veckor, så det återstår mycket arbete med projektet. Till exempel bör alla instanser av klasserna du ser i koden nedan genereras automatiskt.

Så här ser implementeringen av en multisig-plånbok 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 fullständiga källkoden för vårt eDSL- och multisignaturplånbokskontrakt finns på detta förråd. Och mer berättat i detalj om inbyggda språk, vår kollega Georgy Agapov.

Slutsatser om tävlingen och TON

Totalt tog vårt arbete 380 timmar (inklusive bekantskap med dokumentation, möten och faktisk utveckling). Fem utvecklare deltog i tävlingsprojektet: CTO, teamleader, blockchain-plattformsspecialister och Haskell mjukvaruutvecklare.

Vi hittade resurser för att delta i tävlingen utan svårighet, eftersom andan av ett hackathon, nära lagarbete och behovet av att snabbt fördjupa oss i aspekter av ny teknik alltid är spännande. Flera sömnlösa nätter för att uppnå maximala resultat under förhållanden med begränsade resurser kompenseras av ovärderlig erfarenhet och utmärkta minnen. Att arbeta med sådana uppgifter är dessutom alltid ett bra test av företagets processer, eftersom det är extremt svårt att uppnå riktigt anständiga resultat utan välfungerande intern interaktion.

Texten åsido: vi var imponerade av mängden arbete som lagts ner av TON-teamet. De lyckades bygga ett komplext, vackert och viktigast av allt fungerande system. TON har visat sig vara en plattform med stor potential. Men för att detta ekosystem ska utvecklas måste mycket mer göras, både när det gäller dess användning i blockchain-projekt och när det gäller att förbättra utvecklingsverktyg. Vi är stolta över att nu vara en del av denna process.

Om du efter att ha läst den här artikeln fortfarande har några frågor eller har idéer om hur du använder TON för att lösa dina problem, Skriv till oss — Vi delar gärna med oss ​​av våra erfarenheter.

Källa: will.com

Lägg en kommentar