Transformando FunC em FunCtional com Haskell: como a Serokell venceu a Telegram Blockchain Competition

Você provavelmente ouviu que o Telegram está prestes a lançar a plataforma de blockchain Ton. Mas você pode ter perdido a notícia de que, não faz muito tempo, o Telegram anunciou um concurso para a implementação de um ou mais contratos inteligentes para essa plataforma.

A equipe da Serokell, com vasta experiência no desenvolvimento de grandes projetos de blockchain, não poderia ficar de fora. Delegamos cinco funcionários para o concurso e, em apenas duas semanas, eles conquistaram o primeiro lugar com o (não)modesto apelido aleatório Sexy Chameleon. Neste artigo, vou contar como eles conseguiram isso. Esperamos que, nos próximos dez minutos, você encontre pelo menos uma história interessante e, na melhor das hipóteses, algo útil que possa aplicar em seu trabalho.

Mas vamos começar com uma breve imersão no contexto.

O concurso e suas condições

As principais tarefas dos participantes foram a implementação de um ou mais dos contratos inteligentes propostos, bem como a apresentação de sugestões para aprimorar o ecossistema TON. O concurso ocorreu de 24 de setembro a 15 de outubro, e os resultados foram anunciados apenas em 15 de novembro. Bastante tempo, considerando que, durante esse período, o Telegram conseguiu realizar e divulgar os resultados de competições de design e de desenvolvimento de aplicativos em C++ para testar e avaliar a qualidade das chamadas VoIP no Telegram.

Escolhemos dois contratos inteligentes da lista proposta pelos organizadores. Para um deles, utilizamos ferramentas que são distribuídas junto com o TON, e o segundo foi implementado em uma nova linguagem, desenvolvida por nossos engenheiros especialmente para o TON e embutida no Haskell.

A escolha da linguagem de programação funcional não é casual. Em nosso blog corporativo costumamos falar sobre por que consideramos a complexidade das linguagens funcionais um grande exagero e por que, de modo geral, preferimos elas às orientadas a objetos. Aliás, nele tem também a versão original deste artigo.

Por que decidimos participar

Em resumo, porque nossa especialização são projetos não convencionais e complexos que exigem habilidades especiais e frequentemente apresentam valor científico para a comunidade de TI. Apoiamo-nos fervorosamente no desenvolvimento open-source e promovemos sua popularização, além de colaborarmos com as principais universidades da Rússia na área de ciências da computação e matemática.

As tarefas interessantes do concurso e a participação em um projeto que amamos, o Telegram, foram uma grande motivação por si mesmas, e o prêmio em dinheiro se tornou um incentivo adicional. 🙂

Pesquisa sobre o blockchain TON

Acompanhamos de perto as novas desenvolvimentos no blockchain, inteligência artificial e aprendizado de máquina, e tentamos não perder nenhum lançamento significativo em cada uma das áreas em que trabalhamos. Portanto, no momento do início do concurso, nossa equipe já estava familiarizada com as ideias do white paper do TON. No entanto, antes de começar a trabalhar com o TON, não analisamos a documentação técnica nem o código-fonte real da plataforma, então o primeiro passo foi bastante claro – um estudo detalhado da documentação oficial em site e no repositório do projeto.

Com o início do concurso, o código já foi publicado, então, para economizar tempo, decidimos procurar um guia ou resumo escrito por usuários. Infelizmente, isso não trouxe resultados — além das instruções para compilar a plataforma no Ubuntu, não encontramos outros materiais.

A própria documentação estava bem elaborada, mas a leitura dela foi difícil em alguns momentos. Frequentemente, tivemos que voltar a certos pontos e alternar entre descrições de alto nível de ideias abstratas e detalhes de implementação de baixo nível.

Seria mais fácil se a especificação não tivesse uma descrição detalhada da implementação. A informação sobre como a máquina virtual representa sua pilha, de fato, distrai mais os desenvolvedores que criam contratos inteligentes para a plataforma TON do que os ajuda.

Nix: construindo o projeto

Na Serokell somos grandes fãs Nix. Reunimos nossos projetos com eles e os implantamos usando NixOps, e todos os nossos servidores estão rodando NixOS. Com isso, todas as nossas construções são reproduzíveis e funcionam em qualquer sistema operacional onde possa ser instalado o Nix.

Portanto, começamos criando um overlay Nix com a expressão para construir o TON. Com isso, compilar o TON é bem simples:

$ 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. O Nix fará tudo magicamente por você, independentemente de estar usando NixOS, Ubuntu ou macOS.

Programação para o TON

O código dos contratos inteligentes na TON Network é executado na TON Virtual Machine (TVM). A TVM é mais complexa do que a maioria das outras máquinas virtuais e possui funcionalidades bastante interessantes, como a capacidade de trabalhar com continuações (continuations) e links de dados.

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

Fift — uma linguagem de programação empilhada universal, semelhante a Forth. Sua superhabilidade é a possibilidade de interagir com a TVM.

FunC — uma linguagem de programação para contratos inteligentes, que se assemelha a C e é compilada em outra linguagem — Fift Assembler.

Fift Assembler — uma biblioteca Fift para gerar código executável binário para a TVM. O Fift Assembler não possui um compilador. É um linguagem de domínio específico embutida (eDSL).

Nossos projetos de competição

Finalmente, chegou a hora de ver os resultados de nossos esforços.

Canal de pagamento assíncrono

O canal de pagamento (payment channel) é um contrato inteligente que permite que dois usuários enviem pagamentos fora da blockchain. Como resultado, não apenas economiza-se dinheiro (sem taxas), mas também tempo (não é necessário esperar que um novo bloco seja processado). Os pagamentos podem ser tão pequenos quanto necessário e ocorrer com a frequência desejada. Além disso, as partes não precisam confiar uma na outra, já que a equidade do ajuste final é garantida pelo contrato inteligente.

Encontramos uma solução bastante simples para o problema. As duas partes podem trocar mensagens assinadas, cada uma contendo dois números — o total pago por cada participante. Esses dois números funcionam como relógios vetoriais em sistemas distribuídos tradicionais e definem a ordem "ocorreu antes" nas transações. Usando esses dados, o contrato poderá resolver qualquer possível conflito.

Na verdade, para implementar essa ideia, basta um número, mas deixamos ambos, pois assim conseguimos criar uma interface do usuário mais conveniente. Além disso, decidimos incluir em cada mensagem o montante do pagamento. Sem isso, se a mensagem se perder por algum motivo, embora todos os totais e o ajuste final sejam corretos, o usuário pode não perceber a perda.

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

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

Ficou claro que fazia sentido descrever nosso protocolo em detalhes, com especial atenção à sua correção. Após várias iterações, a especificação estava pronta, e agora você também pode dá uma olhada nela.

Implementamos o contrato em FunC, e a ferramenta de linha de comando para interagir com nosso contrato foi completamente escrita em Fift, como recomendaram os organizadores. Poderíamos ter escolhido qualquer outra linguagem para nosso CLI, mas estávamos interessados em experimentar o Fift, para ver como ele se comportaria na prática.

Sinceramente, após trabalhar com Fift, não percebemos razões convincentes para preferir essa linguagem a linguagens populares e amplamente utilizadas, que têm ferramentas e bibliotecas desenvolvidas. Programar em uma linguagem de pilha é bastante desagradável, pois você precisa manter constantemente em mente o que está onde na pilha, e o compilador não ajuda nisso.

Portanto, a única justificativa que vemos para a existência do Fift é seu papel como linguagem host para o Fift Assembler. Mas não seria melhor embutir o assembler TVM em alguma linguagem existente, ao invés de inventar uma nova para esse, essencialmente, único propósito?

TVM Haskell eDSL

Agora é hora de falar sobre nosso segundo smart contract. Decidimos desenvolver uma carteira multifirma, mas escrever mais um smart contract em FunC seria muito chato. Queríamos adicionar algum diferencial, e esse diferencial se tornou nossa própria linguagem de assembleia para TVM.

Assim como o Fift Assembler, nossa nova linguagem é embutível, mas em vez de Fift, escolhemos Haskell como host, o que nos permitiu aproveitar ao máximo seu sistema de tipos avançado. Ao trabalhar com smart contracts, onde o custo de um pequeno erro pode ser muito alto, a tipagem estática, em nossa opinião, é uma grande vantagem.

Para demonstrar como é a linguagem de montagem TVM, incorporada ao Haskell, implementamos uma carteira padrão. Aqui estão algumas coisas a serem observadas:

  • Este contrato consiste em uma função, mas você pode usar quantas quiser. Ao definir uma nova função na linguagem host (ou seja, em Haskell), nosso eDSL permite que você escolha se ela deve se tornar um subprograma separado no TVM ou apenas ser incorporada no local da chamada.
  • Assim como em Haskell, funções têm tipos que são verificados em tempo de compilação. No nosso eDSL, o tipo de entrada da função é o tipo de pilha que a função espera, e o tipo de resultado é o tipo de pilha que resultará após a chamada.
  • O código contém anotações stacktype, descrevendo o tipo esperado da pilha no ponto de chamada. No contrato original da carteira, eram apenas comentários, mas no nosso eDSL, elas são parte do código e são verificadas em tempo de compilação. Elas podem servir como documentação ou como afirmações, ajudando o desenvolvedor a identificar um problema caso o tipo da pilha mude quando o código for alterado. Claro, tais anotações não afetam a performance em tempo de execução, uma vez que nenhum código TVM é gerado para elas.
  • Ainda é um protótipo, escrito em duas semanas, portanto, ainda há muito trabalho a fazer no projeto. Por exemplo, todas as instâncias de classes que você vê no código abaixo devem ser geradas automaticamente.

Veja como é a 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

Você pode encontrar o código-fonte completo do nosso eDSL e do contrato de carteira com múltiplas assinaturas em este repositório. E mais detalhes foram fornecidos por nosso colega Georgiy Agapov.

Conclusões sobre o concurso e TON

No total, nosso trabalho consumiu 380 horas (incluindo a familiarização com a documentação, reuniões e o desenvolvimento propriamente dito). No projeto do concurso participaram cinco desenvolvedores: CTO, líder de equipe, especialistas em plataformas de blockchain e desenvolvedores de software em Haskell.

Encontramos recursos para participar do concurso sem dificuldades, pois o espírito do hackathon, o trabalho em equipe próximo, e a necessidade de rápida imersão em novos aspectos da tecnologia são sempre empolgantes. Algumas noites sem dormir para alcançar o melhor resultado possível em condições de recursos limitados são compensadas por uma experiência inestimável e ótimas recordações. Além disso, trabalhar em tarefas como essas é sempre uma boa verificação dos processos da empresa, já que alcançar resultados realmente dignos sem uma interação interna bem ajustada é bastante difícil.

Além da parte poética: ficamos impressionados com o volume de trabalho realizado pela equipe TON. Eles conseguiram construir um sistema complexo, bonito e, o mais importante, funcional. O TON se mostrou uma plataforma com grande potencial. No entanto, para que esse ecossistema se desenvolva, ainda há muito a ser feito, tanto em termos de utilização em projetos de blockchain quanto em relação ao aprimoramento das ferramentas de desenvolvimento. Temos orgulho de agora fazer parte desse processo.

Se após ler este artigo você tiver alguma dúvida ou ideias sobre como aplicar o TON para resolver suas necessidades, entre em contato conosco — teremos o prazer de compartilhar nossa experiência.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster