FunC in functioneel veranderen met Haskell: hoe Serokell de Telegram Blockchain-wedstrijd won

Je hebt dat Telegram waarschijnlijk gehoord staat op het punt het Ton blockchain-platform te lanceren. Maar misschien heb je het nieuws van Telegram nog niet zo lang geleden gemist een wedstrijd aangekondigd voor de implementatie van één of meerdere smart contracts voor dit platform.

Het Serocell-team, met uitgebreide ervaring in het ontwikkelen van grote blockchain-projecten, kon niet aan de kant blijven staan. We hebben vijf medewerkers afgevaardigd naar de wedstrijd, en twee weken later behaalden zij de eerste plaats onder de (in)bescheiden willekeurige bijnaam Sexy Chameleon. In dit artikel zal ik vertellen hoe ze dat deden. We hopen dat je de komende tien minuten op zijn minst een interessant verhaal leest, en er hoogstens iets nuttigs in vindt dat je in je werk kunt toepassen.

Maar laten we beginnen met een beetje context.

Concurrentie en haar voorwaarden

De belangrijkste taken van de deelnemers waren dus de implementatie van een of meer van de voorgestelde slimme contracten, en het doen van voorstellen om het TON-ecosysteem te verbeteren. De wedstrijd liep van 24 september tot 15 oktober en de resultaten werden pas op 15 november bekendgemaakt. Een behoorlijk lange tijd, gezien het feit dat Telegram gedurende deze tijd erin slaagde de resultaten te houden en bekend te maken van wedstrijden over het ontwerp en de ontwikkeling van applicaties in C++ voor het testen en beoordelen van de kwaliteit van VoIP-gesprekken in Telegram.

We hebben twee slimme contracten geselecteerd uit de door de organisatoren voorgestelde lijst. Voor één ervan hebben we tools gebruikt die met TON werden gedistribueerd, en de tweede werd geïmplementeerd in een nieuwe taal die speciaal door onze ingenieurs voor TON was ontwikkeld en in Haskell was ingebouwd.

De keuze voor een functionele programmeertaal is niet toevallig. In onze bedrijfsblog We praten vaak over waarom we denken dat de complexiteit van functionele talen enorm overdreven is en waarom we ze over het algemeen verkiezen boven objectgeoriënteerde talen. Het bevat trouwens ook origineel van dit artikel.

Waarom hebben we überhaupt besloten om mee te doen?

Kortom omdat onze specialisatie niet-standaard en complexe projecten zijn die speciale vaardigheden vereisen en vaak van wetenschappelijke waarde zijn voor de IT-gemeenschap. We ondersteunen de ontwikkeling van open source krachtig en zijn betrokken bij de popularisering ervan, en werken ook samen met toonaangevende Russische universiteiten op het gebied van informatica en wiskunde.

De interessante taken van de wedstrijd en de betrokkenheid bij ons geliefde Telegram-project waren op zichzelf een uitstekende motivatie, maar het prijzengeld werd een extra stimulans. 🙂

TON blockchain-onderzoek

We volgen nieuwe ontwikkelingen op het gebied van blockchain, kunstmatige intelligentie en machine learning op de voet en proberen geen enkele belangrijke release te missen op elk van de gebieden waarop we werken. Daarom was ons team tegen de tijd dat de competitie begon al bekend met de ideeën van TON wit papier. Voordat we met TON aan de slag gingen, hebben we echter de technische documentatie en de daadwerkelijke broncode van het platform niet geanalyseerd, dus de eerste stap lag voor de hand: een grondige studie van de officiële documentatie over Online en project opslagplaatsen.

Tegen de tijd dat de wedstrijd begon, was de code al gepubliceerd, dus om tijd te besparen besloten we op zoek te gaan naar een gids of samenvatting geschreven door gebruikers. Helaas leverde dit geen resultaat op - afgezien van instructies voor het monteren van het platform op Ubuntu, hebben we geen ander materiaal gevonden.

De documentatie zelf was goed onderzocht, maar was op sommige gebieden moeilijk te lezen. Heel vaak moesten we terugkeren naar bepaalde punten en overschakelen van beschrijvingen op hoog niveau van abstracte ideeën naar implementatiedetails op laag niveau.

Het zou gemakkelijker zijn als de specificatie helemaal geen gedetailleerde beschrijving van de implementatie zou bevatten. Informatie over hoe een virtuele machine zijn stack vertegenwoordigt, zal ontwikkelaars die slimme contracten voor het TON-platform maken eerder afleiden dan hen helpen.

Nix: het project samenstellen

Bij Serokell zijn we grote fans Nix. We verzamelen er onze projecten mee en zetten ze in met behulp van NixOps, en geïnstalleerd op al onze servers Nix OS. Hierdoor zijn al onze builds reproduceerbaar en werken ze op elk besturingssysteem waarop Nix kan worden geïnstalleerd.

Dus zijn we begonnen met creëren Nix-overlay met uitdrukking voor TON-assemblage. Met zijn hulp is het samenstellen van TON zo eenvoudig mogelijk:

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

Houd er rekening mee dat u geen afhankelijkheden hoeft te installeren. Nix zal op magische wijze alles voor je doen, of je nu NixOS, Ubuntu of macOS gebruikt.

Programmering voor TON

De slimme contractcode in het TON-netwerk draait op de TON Virtual Machine (TVM). TVM is complexer dan de meeste andere virtuele machines en heeft bijvoorbeeld zeer interessante functionaliteiten waarmee het kan werken voortzettingen и koppelingen naar gegevens.

Bovendien hebben de jongens van TON drie nieuwe programmeertalen gemaakt:

Vijf is een universele stapelprogrammeertaal die lijkt op voort. Zijn supervermogen is het vermogen om te communiceren met TVM.

FunC is een slimme contract-programmeertaal die vergelijkbaar is met C en is gecompileerd in een andere taal: Fift Assembler.

Vijfde Assemblee — Vijft-bibliotheek voor het genereren van binaire uitvoerbare code voor TVM. Fifth Assembler heeft geen compiler. Dit Ingebedde domeinspecifieke taal (eDSL).

Onze concurrentie werkt

Eindelijk is het tijd om te kijken naar de resultaten van onze inspanningen.

Asynchrone betalingskanaal

Betaalkanaal is een slim contract waarmee twee gebruikers betalingen buiten de blockchain kunnen verzenden. Hierdoor bespaar je niet alleen geld (er is geen commissie), maar ook tijd (je hoeft niet te wachten tot het volgende blok is verwerkt). Betalingen kunnen zo klein zijn als gewenst en zo vaak als nodig. In dit geval hoeven de partijen elkaar niet te vertrouwen, aangezien de eerlijkheid van de eindafrekening wordt gegarandeerd door het slimme contract.

We hebben een vrij eenvoudige oplossing voor het probleem gevonden. Twee partijen kunnen ondertekende berichten uitwisselen, die elk twee nummers bevatten: het volledige bedrag dat elke partij heeft betaald. Deze twee cijfers werken als vectorklok in traditionele gedistribueerde systemen en stel de volgorde van 'er is eerder gebeurd' in op transacties. Met behulp van deze gegevens kan het contract elk mogelijk conflict oplossen.

In feite is één getal voldoende om dit idee te implementeren, maar we hebben beide laten staan ​​omdat we op deze manier een handiger gebruikersinterface konden maken. Daarnaast hebben we besloten om in elk bericht het betalingsbedrag te vermelden. Als het bericht om de een of andere reden verloren gaat, zal de gebruiker zonder dit bericht het verlies mogelijk niet opmerken, ook al zijn alle bedragen en de uiteindelijke berekening correct.

Om ons idee te testen, zochten we naar voorbeelden van het gebruik van zo’n eenvoudig en beknopt betalingskanaalprotocol. Verrassend genoeg vonden we er slechts twee:

  1. beschrijving een vergelijkbare aanpak, alleen in het geval van een unidirectioneel kanaal.
  2. Zelfstudie, dat hetzelfde idee beschrijft als het onze, maar zonder veel belangrijke details uit te leggen, zoals algemene correctheid en procedures voor het oplossen van conflicten.

Het werd duidelijk dat het zinvol is om ons protocol in detail te beschrijven, met speciale aandacht voor de juistheid ervan. Na een aantal iteraties was de specificatie gereed, en nu kunt u dat ook doen. kijk naar haar.

We hebben het contract in FunC geïmplementeerd en we hebben het opdrachtregelhulpprogramma voor de interactie met ons contract volledig in Fift geschreven, zoals aanbevolen door de organisatoren. We hadden elke andere taal voor onze CLI kunnen kiezen, maar we waren geïnteresseerd om Fit uit te proberen om te zien hoe het in de praktijk presteerde.

Om eerlijk te zijn, zagen we na het werken met Fift geen dwingende redenen om deze taal te verkiezen boven populaire en actief gebruikte talen met ontwikkelde tools en bibliotheken. Programmeren in een stack-gebaseerde taal is behoorlijk onaangenaam, omdat je constant in je hoofd moet houden wat er op de stack staat, en de compiler helpt hierbij niet.

Daarom is naar onze mening de enige rechtvaardiging voor het bestaan ​​van Fift zijn rol als gasttaal voor Fift Assembler. Maar zou het niet beter zijn om de TVM-assembler in een bestaande taal in te bedden, in plaats van een nieuwe uit te vinden voor dit in essentie enige doel?

TVM Haskell eDSL

Nu is het tijd om over ons tweede slimme contract te praten. We besloten een portemonnee met meerdere handtekeningen te ontwikkelen, maar het schrijven van nog een slim contract in FunC zou te saai zijn. We wilden wat smaak toevoegen, en dat was onze eigen assembleertaal voor TVM.

Net als Fift Assembler is onze nieuwe taal ingebed, maar we hebben Haskell als host gekozen in plaats van Fift, waardoor we volledig kunnen profiteren van het geavanceerde typesysteem. Bij het werken met slimme contracten, waarbij de kosten van zelfs een kleine fout erg hoog kunnen zijn, is statisch typen naar onze mening een groot voordeel.

Om te demonstreren hoe TVM assembler er in Haskell uitziet, hebben we er een standaard portemonnee op geïmplementeerd. Hier zijn een paar dingen waar u op moet letten:

  • Dit contract bestaat uit één functie, maar je kunt er zoveel gebruiken als je wilt. Wanneer u een nieuwe functie in de hosttaal (d.w.z. Haskell) definieert, kunt u met onze eDSL kiezen of u wilt dat dit een aparte routine in TVM wordt of gewoon inline op het oproeppunt.
  • Net als Haskell hebben functies typen die tijdens het compileren worden gecontroleerd. In onze eDSL is het invoertype van een functie het type stapel dat de functie verwacht, en het resultaattype is het type stapel dat na de oproep wordt geproduceerd.
  • De code bevat annotaties stacktype, waarin het verwachte stapeltype bij het handbrandmelder wordt beschreven. In het oorspronkelijke portemonneecontract waren dit slechts opmerkingen, maar in onze eDSL maken ze feitelijk deel uit van de code en worden ze tijdens het compileren gecontroleerd. Ze kunnen dienen als documentatie of verklaringen die de ontwikkelaar helpen het probleem te vinden als de code verandert en het stapeltype verandert. Dergelijke annotaties hebben uiteraard geen invloed op de runtimeprestaties, aangezien er geen TVM-code voor wordt gegenereerd.
  • Dit is nog steeds een prototype dat in twee weken is geschreven, dus er moet nog veel aan het project worden gewerkt. Alle exemplaren van de klassen die u in de onderstaande code ziet, moeten bijvoorbeeld automatisch worden gegenereerd.

Zo ziet de implementatie van een multisig wallet eruit op onze 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

De volledige broncode van ons eDSL- en multi-signature portemonnee-contract kunt u vinden op deze opslagplaats. En meer uitvoerig verteld over ingebouwde talen, onze collega Georgy Agapov.

Conclusies over de concurrentie en TON

In totaal duurde ons werk 380 uur (inclusief kennismaking met documentatie, bijeenkomsten en daadwerkelijke ontwikkeling). Aan het competitieproject namen vijf ontwikkelaars deel: CTO, teamleider, blockchainplatformspecialisten en Haskell-softwareontwikkelaars.

We hebben middelen gevonden om zonder problemen aan de wedstrijd deel te nemen, omdat de geest van een hackathon, hecht teamwerk en de noodzaak om ons snel te verdiepen in aspecten van nieuwe technologieën altijd spannend zijn. Verschillende slapeloze nachten om maximale resultaten te bereiken in omstandigheden met beperkte middelen worden gecompenseerd door onschatbare ervaring en uitstekende herinneringen. Bovendien is het werken aan dergelijke taken altijd een goede test voor de bedrijfsprocessen, omdat het uiterst moeilijk is om echt fatsoenlijke resultaten te behalen zonder goed functionerende interne interactie.

Afgezien van de teksten: we waren onder de indruk van de hoeveelheid werk die het TON-team erin heeft gestoken. Ze zijn erin geslaagd een complex, mooi en vooral werkend systeem te bouwen. TON heeft bewezen een platform te zijn met veel potentie. Om dit ecosysteem zich te laten ontwikkelen, moet er echter nog veel meer worden gedaan, zowel wat betreft het gebruik ervan in blockchain-projecten als wat betreft het verbeteren van ontwikkelingsinstrumenten. We zijn er trots op dat we nu deel uitmaken van dit proces.

Als u na het lezen van dit artikel nog vragen heeft of ideeën heeft over hoe u TON kunt gebruiken om uw problemen op te lossen, Schrijf ons – wij delen graag onze ervaringen.

Bron: www.habr.com

Voeg een reactie