Transformando FunC em FunCtional com Haskell: Como Serokell venceu a competição Telegram Blockchain

Você provavelmente já ouviu falar que Telegram está prestes a lançar a plataforma blockchain Ton. Mas você pode ter perdido a notícia de que há pouco tempo o Telegram anunciou uma competição para a implementação de um ou mais contratos inteligentes para esta plataforma.

A equipe Serokell, com vasta experiência no desenvolvimento de grandes projetos de blockchain, não poderia ficar de fora. Delegamos cinco funcionários para a competição e, duas semanas depois, eles conquistaram o primeiro lugar com o (in)modesto apelido aleatório Sexy Chameleon. Neste artigo vou falar sobre como eles fizeram isso. Esperamos que nos próximos dez minutos você leia pelo menos uma história interessante e, no máximo, encontre nela algo útil que possa aplicar em seu trabalho.

Mas vamos começar com um pouco de contexto.

Concorrência e suas condições

Assim, as principais tarefas dos participantes foram a implementação de um ou mais dos contratos inteligentes propostos, bem como a apresentação de propostas para melhorar o ecossistema TON. A competição aconteceu de 24 de setembro a 15 de outubro, e os resultados foram divulgados apenas no dia 15 de novembro. Muito tempo, visto que durante este tempo o Telegram conseguiu realizar e divulgar os resultados de concursos de concepção e desenvolvimento de aplicações em C++ para teste e avaliação da qualidade das chamadas VoIP no Telegram.

Selecionamos dois contratos inteligentes da lista proposta pelos organizadores. Para um deles, utilizamos ferramentas distribuídas com TON, e o segundo foi implementado em uma nova linguagem desenvolvida por nossos engenheiros especificamente para TON e integrada em Haskell.

A escolha de uma linguagem de programação funcional não é acidental. Na nossa blog corporativo Freqüentemente falamos sobre por que achamos que a complexidade das linguagens funcionais é um grande exagero e por que geralmente as preferimos às orientadas a objetos. A propósito, também contém original deste artigo.

Por que decidimos participar?

Em suma, porque a nossa especialização são projetos complexos e não padronizados que exigem habilidades especiais e muitas vezes têm valor científico para a comunidade de TI. Apoiamos fortemente o desenvolvimento de código aberto e estamos empenhados na sua popularização, e também cooperamos com as principais universidades russas no campo da ciência da computação e da matemática.

As interessantes tarefas do concurso e o envolvimento no nosso querido projeto Telegram foram por si só uma excelente motivação, mas o fundo de prémios tornou-se um incentivo adicional. 🙂

Pesquisa de blockchain TON

Acompanhamos de perto os novos desenvolvimentos em blockchain, inteligência artificial e aprendizado de máquina e tentamos não perder um único lançamento significativo em cada uma das áreas em que atuamos. Portanto, quando a competição começou, nossa equipe já conhecia as ideias de TONELADAS de papel branco. Porém, antes de começarmos a trabalhar com TON, não analisamos a documentação técnica e o próprio código-fonte da plataforma, então o primeiro passo foi bastante óbvio - um estudo aprofundado da documentação oficial sobre On-line e repositórios de projetos.

Quando a competição começou o código já havia sido publicado, então para economizar tempo decidimos procurar um guia ou resumo escrito por por usuários. Infelizmente, isso não deu nenhum resultado - além das instruções para montar a plataforma no Ubuntu, não encontramos nenhum outro material.

A documentação em si foi bem pesquisada, mas era difícil de ler em algumas áreas. Muitas vezes tivemos que retornar a certos pontos e passar de descrições de alto nível de ideias abstratas para detalhes de implementação de baixo nível.

Seria mais fácil se a especificação não incluísse uma descrição detalhada da implementação. As informações sobre como uma máquina virtual representa sua pilha têm mais probabilidade de distrair os desenvolvedores que criam contratos inteligentes para a plataforma TON do que ajudá-los.

Nix: montando o projeto

Na Serokell somos grandes fãs Nix. Coletamos nossos projetos com eles e os implantamos usando NixOpse instalado em todos os nossos servidores Nix OS. Graças a isso, todas as nossas compilações são reproduzíveis e funcionam em qualquer sistema operacional no qual o Nix possa ser instalado.

Então começamos criando Sobreposição Nix com expressão para montagem TON. Com sua ajuda, compilar o TON é o mais simples possível:

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

Observe que você não precisa instalar nenhuma dependência. Nix fará tudo magicamente por você, esteja você usando NixOS, Ubuntu ou macOS.

Programação para TON

O código do contrato inteligente na Rede TON é executado na Máquina Virtual TON (TVM). O TVM é mais complexo que a maioria das outras máquinas virtuais e possui funcionalidades muito interessantes, por exemplo, pode trabalhar com continuações и links para dados.

Além disso, o pessoal da TON criou três novas linguagens de programação:

Quinto é uma linguagem de programação de pilha universal que se assemelha Adiante. Sua super habilidade é a capacidade de interagir com TVM.

DiversãoC é uma linguagem de programação de contrato inteligente semelhante a C e é compilado em outra linguagem - Fift Assembler.

Quinta Montadora — Fift biblioteca para geração de código binário executável para TVM. O Quinto Assembler não possui um compilador. Esse Linguagem específica de domínio incorporada (eDSL).

Nossa competição funciona

Finalmente, é hora de analisar os resultados dos nossos esforços.

Canal de pagamento assíncrono

O canal de pagamento é um contrato inteligente que permite que dois usuários enviem pagamentos fora do blockchain. Como resultado, você economiza não apenas dinheiro (não há comissão), mas também tempo (você não precisa esperar o processamento do próximo bloco). Os pagamentos podem ser tão pequenos quanto desejado e com a frequência necessária. Nesse caso, as partes não precisam confiar uma na outra, uma vez que a equidade do acordo final é garantida pelo contrato inteligente.

Encontramos uma solução bastante simples para o problema. Duas partes podem trocar mensagens assinadas, cada uma contendo dois números – o valor total pago por cada parte. Esses dois números funcionam como relógio vetorial em sistemas distribuídos tradicionais e definir a ordem "aconteceu antes" nas transações. Utilizando esses dados, o contrato poderá resolver qualquer possível conflito.

Na verdade, um número é suficiente para implementar essa ideia, mas deixamos os dois porque assim poderíamos fazer uma interface de usuário mais conveniente. Além disso, decidimos incluir o valor do pagamento em cada mensagem. Sem ele, se a mensagem for perdida por algum motivo, então, embora todos os valores e o cálculo final estejam corretos, o usuário poderá não perceber a perda.

Para testar nossa ideia, procuramos exemplos de uso de um protocolo de canal de pagamento tão simples e conciso. Surpreendentemente, encontramos apenas dois:

  1. descrição uma abordagem semelhante, apenas para o caso de um canal unidirecional.
  2. Tutorial, que descreve a mesma ideia que a nossa, mas sem explicar muitos detalhes importantes, como correção geral e procedimentos de resolução de conflitos.

Ficou claro que faz sentido descrever detalhadamente nosso protocolo, prestando especial atenção à sua correção. Depois de várias iterações, a especificação estava pronta e agora você também pode. olha para ela.

Implementamos o contrato no FunC e escrevemos o utilitário de linha de comando para interagir com nosso contrato inteiramente no Fift, conforme recomendado pelos organizadores. Poderíamos ter escolhido qualquer outra linguagem para nossa CLI, mas estávamos interessados ​​em experimentar o Fit para ver como ele funcionava na prática.

Para ser honesto, depois de trabalhar com Fift, não vimos nenhuma razão convincente para preferir esta linguagem a linguagens populares e usadas ativamente com ferramentas e bibliotecas desenvolvidas. Programar em uma linguagem baseada em pilha é bastante desagradável, pois você precisa manter constantemente em mente o que está na pilha, e o compilador não ajuda nisso.

Portanto, em nossa opinião, a única justificativa para a existência do Fift é o seu papel como linguagem hospedeira do Fift Assembler. Mas não seria melhor incorporar o montador TVM em alguma linguagem existente, em vez de inventar uma nova para esse propósito essencialmente exclusivo?

TVM Haskell eDSL

Agora é hora de falar sobre nosso segundo contrato inteligente. Decidimos desenvolver uma carteira com múltiplas assinaturas, mas escrever outro contrato inteligente no FunC seria muito chato. Queríamos adicionar um pouco de sabor, e essa era nossa própria linguagem assembly para TVM.

Assim como o Fift Assembler, nossa nova linguagem é incorporada, mas escolhemos Haskell como host em vez de Fift, o que nos permite aproveitar ao máximo seu sistema de tipos avançado. Ao trabalhar com contratos inteligentes, onde o custo até mesmo de um pequeno erro pode ser muito alto, a digitação estática, em nossa opinião, é uma grande vantagem.

Para demonstrar a aparência do montador TVM incorporado em Haskell, implementamos uma carteira padrão nele. Aqui estão algumas coisas para prestar atenção:

  • Este contrato consiste em uma função, mas você pode usar quantas quiser. Quando você define uma nova função na linguagem host (ou seja, Haskell), nosso eDSL permite que você escolha se deseja que ela se torne uma rotina separada no TVM ou simplesmente incorporada no ponto de chamada.
  • Assim como Haskell, as funções possuem tipos que são verificados em tempo de compilação. Em nosso eDSL, o tipo de entrada de uma função é o tipo de pilha que a função espera, e o tipo de resultado é o tipo de pilha que será produzida após a chamada.
  • O código tem anotações stacktype, descrevendo o tipo de pilha esperado no ponto de chamada. No contrato original da carteira estes eram apenas comentários, mas em nosso eDSL eles fazem parte do código e são verificados em tempo de compilação. Eles podem servir como documentação ou instruções que ajudam o desenvolvedor a encontrar o problema se o código mudar e o tipo de pilha mudar. É claro que tais anotações não afetam o desempenho do tempo de execução, uma vez que nenhum código TVM é gerado para elas.
  • Este ainda é um protótipo escrito em duas semanas, então ainda há muito trabalho a ser feito no projeto. Por exemplo, todas as instâncias das classes que você vê no código abaixo devem ser geradas automaticamente.

Esta é a aparência da implementação de uma carteira multisig em nosso 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

O código-fonte completo do nosso contrato eDSL e carteira com múltiplas assinaturas pode ser encontrado em este repositório. E mais contado em detalhes sobre linguagens integradas, nosso colega Georgy Agapov.

Conclusões sobre a concorrência e a TON

No total, nosso trabalho durou 380 horas (incluindo familiarização com documentação, reuniões e desenvolvimento real). Cinco desenvolvedores participaram do projeto de competição: CTO, líder de equipe, especialistas em plataforma blockchain e desenvolvedores de software Haskell.

Encontramos recursos para participar do concurso sem dificuldade, pois o espírito de um hackathon, o trabalho em equipe próximo e a necessidade de mergulhar rapidamente nos aspectos das novas tecnologias são sempre emocionantes. Várias noites sem dormir para alcançar resultados máximos em condições de recursos limitados são compensadas por uma experiência inestimável e excelentes memórias. Além disso, trabalhar nessas tarefas é sempre um bom teste para os processos da empresa, pois é extremamente difícil alcançar resultados verdadeiramente decentes sem uma interação interna que funcione bem.

Letras à parte: ficamos impressionados com a quantidade de trabalho realizado pela equipe TON. Eles conseguiram construir um sistema complexo, bonito e, o mais importante, funcional. A TON provou ser uma plataforma com grande potencial. No entanto, para que este ecossistema se desenvolva, muito mais precisa de ser feito, tanto em termos da sua utilização em projetos de blockchain como em termos de melhoria das ferramentas de desenvolvimento. Estamos orgulhosos de agora fazer parte deste processo.

Se depois de ler este artigo você ainda tiver alguma dúvida ou ideia de como usar o TON para resolver seus problemas, escreva para nós - teremos o maior prazer em compartilhar nossa experiência.

Fonte: habr.com

Adicionar um comentário