Cliente de teste TON (Telegram Open Network) e nova linguagem Fift para contratos inteligentes

Há mais de um ano, soube-se dos planos do mensageiro Telegram de lançar sua própria rede descentralizada Rede Aberta Telegram. Foi então disponibilizado um volumoso documento técnico, supostamente escrito por Nikolai Durov e que descrevia a estrutura da futura rede. Para quem perdeu, recomendo que leia minha releitura deste documento (parte 1, parte 2; a terceira parte, infelizmente, ainda está acumulando poeira nas correntes de ar).

Desde então, não houve nenhuma notícia significativa sobre o estado do desenvolvimento da TON até alguns dias atrás (em um dos canais não oficiais) o link para a página não apareceu https://test.ton.org/download.html, onde estão localizados:
ton-test-liteclient-full.tar.xz — fontes de um cliente leve para a rede de testes TON;
ton-lite-client-test1.config.json — arquivo de configuração para conexão à rede de teste;
README — informações sobre a construção e lançamento do cliente;
HOWTO — instruções passo a passo sobre como criar um contrato inteligente usando um cliente;
ton.pdf — documento atualizado (datado de 2 de março de 2019) com uma visão técnica da rede TON;
tvm.pdf — descrição técnica do TVM (TON Virtual Machine, máquina virtual TON);
tblkch.pdf — descrição técnica da blockchain TON;
quintabase.pdf — descrição da nova linguagem Fift, projetada para criar contratos inteligentes em TON.

Repito, não houve confirmação oficial da página e de todos esses documentos do Telegram, mas o volume desses materiais os torna bastante plausíveis. Inicie o cliente publicado por sua própria conta e risco.

Construindo um cliente de teste

Primeiro, vamos tentar construir e executar um cliente de teste - felizmente, README descreve esse processo simples em detalhes. Farei isso usando o macOS 10.14.5 como exemplo; não posso garantir o sucesso da compilação em outros sistemas.

  1. Baixe e descompacte arquivo de origem. É importante baixar a versão mais recente, pois a compatibilidade com versões anteriores não é garantida neste estágio.

  2. Certifique-se de que as versões mais recentes do make, cmake (versão 3.0.2 ou superior), OpenSSL (incluindo arquivos de cabeçalho C), g++ ou clang estejam instaladas no sistema. Não precisei instalar nada, tudo se encaixou na hora.

  3. Vamos supor que as fontes estejam descompactadas em uma pasta ~/lite-client. Separadamente dela, crie uma pasta vazia para o projeto montado (por exemplo, ~/liteclient-build), e a partir dele (cd ~/liteclient-build) chame os comandos:

    cmake ~/lite-client
    cmake --build . --target test-lite-client

    Cliente de teste TON (Telegram Open Network) e nova linguagem Fift para contratos inteligentes

    Para construir o intérprete da linguagem Fift para contratos inteligentes (mais sobre isso abaixo), também chamamos

    cmake --build . --target fift

  4. Baixe o atual arquivo de configuração conectar-se à rede de teste e colocá-la na pasta com o cliente montado.

  5. Feito, você pode iniciar o cliente:

    ./test-lite-client -C ton-lite-client-test1.config.json

Se tudo for feito corretamente, você deverá ver algo assim:

Cliente de teste TON (Telegram Open Network) e nova linguagem Fift para contratos inteligentes

Como podemos ver, existem poucos comandos disponíveis:
help — exibe esta lista de comandos;
quit - sair;
time — mostra a hora atual no servidor;
status — mostra a conexão e o status do banco de dados local;
last — atualize o estado do blockchain (baixe o último bloco). É importante executar este comando antes de qualquer solicitação para ter certeza de ver o estado atual da rede.
sendfile <filename> — carregue um arquivo local para a rede TON. É assim que ocorre a interação com a rede – incluindo, por exemplo, a criação de novos contratos inteligentes e solicitações de transferência de fundos entre contas;
getaccount <address> — mostra a corrente (no momento em que o comando foi executado) last) o status da conta com o endereço especificado;
privkey <filename> — carregue a chave privada de um arquivo local.

Se, ao iniciar o cliente, você transferir uma pasta para ele usando a opção -D, então ele adicionará o último bloco do masterchain:

./test-lite-client -C ton-lite-client-test1.config.json -D ~/ton-db-dir

Agora podemos passar para coisas mais interessantes - aprender a linguagem Fift, tentar compilar um contrato inteligente (por exemplo, criar uma carteira de teste), carregá-lo na rede e tentar transferir fundos entre contas.

Idioma Quinto

Do documento quintabase.pdf você pode descobrir que a equipe do Telegram criou uma nova linguagem de pilha para criar contratos inteligentes Quinto (aparentemente pelo numeral quinto, semelhante ao Forth, uma linguagem com a qual o Fifth tem muito em comum).

O documento é bastante volumoso, 87 páginas, e não vou recontar detalhadamente o seu conteúdo no âmbito deste artigo (pelo menos porque ainda não terminei de lê-lo :). Vou me concentrar nos pontos principais e dar alguns exemplos de código nesta linguagem.

Em um nível básico, a sintaxe do Fift é bastante simples: seu código consiste em palavras, geralmente separados por espaços ou quebras de linha (caso especial: algumas palavras não requerem um separador depois de si mesmas). Qualquer palavra é uma sequência de caracteres que diferencia maiúsculas de minúsculas que corresponde a um determinado definição (aproximadamente, o que o intérprete deve fazer ao encontrar esta palavra). Se não houver definição de uma palavra, o intérprete tenta analisá-la como um número e colocá-la na pilha. A propósito, os números aqui são - de repente - inteiros de 257 bits, e não há frações - mais precisamente, eles imediatamente se transformam em um par de inteiros, formando o numerador e o denominador de uma fração racional.

As palavras tendem a interagir com os valores no topo da pilha. Um tipo separado de palavras - prefixo — não usa a pilha, mas os caracteres subsequentes do arquivo de origem. Por exemplo, é assim que os literais de string são implementados - o caractere de aspas (") é uma palavra de prefixo que procura a próxima cotação (de fechamento) e coloca a string entre elas na pilha. One-liners se comportam da mesma maneira (//) e multilinha (/*) comentários.

É aqui que termina quase toda a estrutura interna da linguagem. Todo o resto (incluindo construções de controle) é definido como palavras (sejam internas, como operações aritméticas e a definição de novas palavras; ou definidas na "biblioteca padrão" Fift.fif, que está na pasta crypto/fift nas fontes).

Um programa de exemplo simples no Fift:

{ dup =: x dup * =: y } : setxy
3 setxy x . y . x y + .
7 setxy x . y . x y + .

A primeira linha define uma nova palavra setxy (observe o prefixo {, que cria um bloco antes do de fechamento } e prefixo :, que na verdade define a palavra). setxy pega um número do topo da pilha e o define (ou redefine) como global constante x, e o quadrado deste número como uma constante y (Dado que os valores das constantes podem ser redefinidos, prefiro chamá-las de variáveis, mas sigo a convenção de nomenclatura da linguagem).

As próximas duas linhas colocam um número na pilha e chamam setxy, então os valores das constantes são exibidos x, y (a palavra é usada para saída .), ambas as constantes são colocadas na pilha, somadas e o resultado também é impresso. Como resultado veremos:

3 9 12 ok
7 49 56 ok

(A linha “ok” é impressa pelo intérprete quando termina de processar a linha atual no modo de entrada interativo)

Bem, um exemplo de código completo:

"Asm.fif" include

-1 constant wc  // create a wallet in workchain -1 (masterchain)

// Create new simple wallet
<{  SETCP0 DUP IFNOTRET INC 32 THROWIF  // return if recv_internal, fail unless recv_external
    512 INT LDSLICEX DUP 32 PLDU   // sign cs cnt
    c4 PUSHCTR CTOS 32 LDU 256 LDU ENDS  // sign cs cnt cnt' pubk
    s1 s2 XCPU            // sign cs cnt pubk cnt' cnt
    EQUAL 33 THROWIFNOT   // ( seqno mismatch? )
    s2 PUSH HASHSU        // sign cs cnt pubk hash
    s0 s4 s4 XC2PU        // pubk cs cnt hash sign pubk
    CHKSIGNU              // pubk cs cnt ?
    34 THROWIFNOT         // signature mismatch
    ACCEPT
    SWAP 32 LDU NIP 
    DUP SREFS IF:<{
      8 LDU LDREF         // pubk cnt mode msg cs
      s0 s2 XCHG SENDRAWMSG  // pubk cnt cs ; ( message sent )
    }>
    ENDS
    INC NEWC 32 STU 256 STU ENDC c4 POPCTR
}>c
// code
<b 0 32 u, 
   newkeypair swap dup constant wallet_pk 
   "new-wallet.pk" B>file
   B, 
b> // data
// no libraries
<b b{00110} s, rot ref, swap ref, b>  // create StateInit
dup ."StateInit: " <s csr. cr
dup hash dup constant wallet_addr
."new wallet address = " wc . .": " dup x. cr
wc over 7 smca>$ type cr
256 u>B "new-wallet.addr" B>file
<b 0 32 u, b>
dup ."signing message: " <s csr. cr
dup hash wallet_pk ed25519_sign_uint rot
<b b{1000100} s, wc 8 i, wallet_addr 256 u, b{000010} s, swap <s s, b{0} s, swap B, swap <s s, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
"new-wallet-query.boc" tuck B>file
."(Saved to file " type .")" cr

Este arquivo de aparência assustadora serve para criar um contrato inteligente - ele será colocado em um arquivo new-wallet-query.boc após a execução. Observe que outra linguagem assembly é usada aqui para a Máquina Virtual TON (não vou me alongar sobre ela em detalhes), cujas instruções serão colocadas no blockchain.

Assim, o montador para TVM é escrito em Fift - as fontes deste montador estão no arquivo crypto/fift/Asm.fif e estão conectados no início do código acima.

O que posso dizer, aparentemente Nikolai Durov adora criar novas linguagens de programação :)

Criando um contrato inteligente e interagindo com a TON

Então, vamos supor que montamos o cliente TON e o intérprete Fift conforme descrito acima e nos familiarizamos com a linguagem. Como criar um contrato inteligente agora? Isso está descrito no arquivo HOWTO, anexado às fontes.

Contas em TON

Como descrevi em Revisão de TONELADAS, esta rede contém mais de um blockchain - existe um comum, o chamado. "cadeia mestre", bem como um número arbitrário de "cadeias de trabalho" adicionais, identificadas por um número de 32 bits. O masterchain possui identificador -1, além dele também pode ser utilizado um workchain “base” com identificador 0. Cada workchain pode ter sua própria configuração. Internamente, cada workchain é dividida em shardchains, mas esse é um detalhe de implementação que não precisa ser lembrado.

Dentro de uma cadeia de trabalho, muitas contas são armazenadas e possuem seus próprios identificadores account_id. Para a cadeia mestre e a cadeia de trabalho zero, elas têm 256 bits de comprimento. Assim, o identificador da conta é escrito, por exemplo, assim:

-1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Este é o formato “bruto”: primeiro o ID da cadeia de trabalho, depois dois pontos e o ID da conta em notação hexadecimal.

Além disso, existe um formato abreviado - o número da cadeia de trabalho e o endereço da conta são codificados em formato binário, uma soma de verificação é adicionada a eles e tudo isso é codificado em Base64:

Ef+BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb

Conhecendo este formato de registro, podemos solicitar o estado atual de uma conta através de um cliente de teste utilizando o comando

getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

Teremos algo assim:

[ 3][t 2][1558746708.815218925][test-lite-client.cpp:631][!testnode]    requesting account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D
[ 3][t 2][1558746708.858564138][test-lite-client.cpp:652][!testnode]    got account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D with respect to blocks (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F and (-1,8000000000000000,72355):F566005749C1B97F18EDE013EBA7A054B9014961BC1AD91F475B9082919A2296:1BD5DE54333164025EE39D389ECE2E93DA2871DA616D488253953E52B50DC03F
account state is (account
  addr:(addr_std
    anycast:nothing workchain_id:-1 address:x8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D)
  storage_stat:(storage_info
    used:(storage_used
      cells:(var_uint len:1 value:3)
      bits:(var_uint len:2 value:539)
      public_cells:(var_uint len:0 value:0)) last_paid:0
    due_payment:nothing)
  storage:(account_storage last_trans_lt:74208000003
    balance:(currencies
      grams:(nanograms
        amount:(var_uint len:7 value:999928362430000))
      other:(extra_currencies
        dict:hme_empty))
    state:(account_active
      (
        split_depth:nothing
        special:nothing
        code:(just
          value:(raw@^Cell 
            x{}
             x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
            ))
        data:(just
          value:(raw@^Cell 
            x{}
             x{0000000D}
            ))
        library:hme_empty))))
x{CFF8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D2068086C000000000000000451C90E00DC0E35B7DB5FB8C134_}
 x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

Vemos a estrutura que está armazenada no DHT da cadeia de trabalho especificada. Por exemplo, no campo storage.balance é o saldo da conta corrente, em storage.state.code - código de contrato inteligente, e em storage.state.data - seus dados atuais. Observe que o armazenamento de dados TON - Célula, células - é semelhante a uma árvore, cada célula pode ter seus próprios dados e células filhas. Isso é mostrado como recuo nas últimas linhas.

Construindo um contrato inteligente

Agora vamos criar nós mesmos essa estrutura (chama-se BOC - saco de células) usando a linguagem Fift. Felizmente, você não precisa redigir um contrato inteligente - na pasta crypto/block existe um arquivo do arquivo de origem new-wallet.fif, o que nos ajudará a criar uma nova carteira. Vamos copiá-lo para a pasta com o cliente montado (~/liteclient-build, se você seguiu as instruções acima). Citei seu conteúdo acima como um exemplo de código no Fift.

Execute este arquivo da seguinte maneira:

./crypto/fift -I"<source-directory>/crypto/fift" new-wallet.fif

é <source-directory> deve ser substituído pelo caminho para as fontes descompactadas (o símbolo “~”, infelizmente, não pode ser usado aqui, é necessário o caminho completo). Em vez de usar uma chave -I você pode definir uma variável de ambiente FIFTPATH e coloque esse caminho nele.

Desde que lançamos o Fift com o nome do arquivo new-wallet.fif, ele irá executá-lo e sair. Se você omitir o nome do arquivo, poderá brincar interativamente com o intérprete.

Após a execução, algo assim deverá ser exibido no console:

StateInit: x{34_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

new wallet address = -1 : 4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 
0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ
signing message: x{00000000}

External message for initialization is x{89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001_}
 x{FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED54}
 x{0000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B}

B5EE9C724104030100000000D60002CF89FEE120E20C7E953E31546F64C23CD654002C1AA919ADD24DB12DDF85C6F3B58AE41198A28AD8DAF3B9588E7A629252BA3DB88F030D00BC1016110B2073359EAC3C13823C53245B65D056F2C070B940CDA09789585935C7ABA4D2AD4BED139281CFA1200000001001020084FF0020DDA4F260810200D71820D70B1FED44D0D31FD3FFD15112BAF2A122F901541044F910F2A2F80001D31F3120D74A96D307D402FB00DED1A4C8CB1FCBFFC9ED5400480000000055375F730EDC2292E8CB15C42E8036EE9C25AA958EE002D2DE48A205E3A3426B6290698B
(Saved to file new-wallet-query.boc)

Isso significa que a carteira com o ID -1:4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2 (ou, o que é o mesmo, 0f9PzVILj8yglrVn1zS-NSjtxr7QBfaTCp7JrBqnFPIR8nhZ) criado com sucesso. O código correspondente estará no arquivo new-wallet-query.boc, o endereço dele está em new-wallet.addr, e a chave privada está em new-wallet.pk (tenha cuidado - executar o script novamente substituirá esses arquivos).

É claro que a rede TON ainda não conhece esta carteira, ela é armazenada apenas na forma desses arquivos. Agora ele precisa ser carregado na rede. Porém, o problema é que para criar um contrato inteligente você precisa pagar uma comissão e o saldo da sua conta ainda é zero.

No modo de trabalho, esse problema será resolvido comprando gramas na bolsa (ou transferindo de outra carteira). Pois bem, no modo de teste atual, foi criado um contrato inteligente especial, do qual você pode solicitar até 20 gramas sem mais nem menos.

Gerando uma solicitação para o contrato inteligente de outra pessoa

Fazemos uma solicitação a um contrato inteligente que distribui gramas a torto e a direito assim. Na mesma pasta crypto/block achar arquivo testgiver.fif:

// "testgiver.addr" file>B 256 B>u@ 
0x8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
dup constant wallet_addr ."Test giver address = " x. cr

0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2
constant dest_addr

-1 constant wc
0x00000011 constant seqno

1000000000 constant Gram
{ Gram swap */ } : Gram*/

6.666 Gram*/ constant amount

// b x --> b'  ( serializes a Gram amount )
{ -1 { 1+ 2dup 8 * ufits } until
  rot over 4 u, -rot 8 * u, } : Gram, 

// create a message (NB: 01b00.., b = bounce)
<b b{010000100} s, wc 8 i, dest_addr 256 u, amount Gram, 0 9 64 32 + + 1+ 1+ u, "GIFT" $, b>
<b seqno 32 u, 1 8 u, swap ref, b>
dup ."enveloping message: " <s csr. cr
<b b{1000100} s, wc 8 i, wallet_addr 256 u, 0 Gram, b{00} s,
   swap <s s, b>
dup ."resulting external message: " <s csr. cr
2 boc+>B dup Bx. cr
"wallet-query.boc" B>file

Também salvaremos na pasta com o cliente montado, mas corrigiremos a quinta linha - antes da linha “constant dest_addr". Vamos substituí-lo pelo endereço da carteira que você criou anteriormente (completo, não abreviado). Não há necessidade de escrever “-1:” no início, em vez disso coloque “0x” no início.

Você também pode alterar a linha 6.666 Gram*/ constant amount — esta é a quantidade em gramas que você está solicitando (não mais que 20). Mesmo se você especificar um número inteiro, deixe a vírgula decimal.

Finalmente, você precisa corrigir a linha 0x00000011 constant seqno. O primeiro número aqui é o número de sequência atual, que é armazenado na conta que emite gramas. Onde posso obtê-lo? Conforme declarado acima, inicie o cliente e execute:

last
getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d

No final, os dados do contrato inteligente conterão

...
x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
 x{0000000D}

O número 0000000D (o seu será maior) é o número de sequência que deve ser substituído em testgiver.fif.

É isso, salve o arquivo e execute (./crypto/fift testgiver.fif). A saída será um arquivo wallet-query.boc. Isto é o que é formado сообщение para o contrato inteligente de outra pessoa - uma solicitação “transfira tantos gramas para tal e tal conta”.

Usando o cliente, fazemos o upload para a rede:

> sendfile wallet-query.boc
[ 1][t 1][1558747399.456575155][test-lite-client.cpp:577][!testnode]    sending query from file wallet-query.boc
[ 3][t 2][1558747399.500236034][test-lite-client.cpp:587][!query]   external message status is 1

Se você ligar agora laste, em seguida, solicitar novamente o status da conta da qual solicitamos gramas, veremos que seu número de sequência aumentou em um - isso significa que ele enviou dinheiro para nossa conta.

Resta o último passo - baixar o código da nossa carteira (seu saldo já foi reposto, mas sem o código do contrato inteligente não poderemos gerenciá-lo). Nós realizamos sendfile new-wallet-query.boc — e pronto, você tem sua própria carteira na rede TON (mesmo que seja apenas uma carteira de teste por enquanto).

Criando transações de saída

Para transferir dinheiro do saldo da conta criada, existe um arquivo crypto/block/wallet.fif, que também precisa ser colocado na pasta com o cliente montado.

Semelhante às etapas anteriores, você precisa ajustar o valor que está transferindo, o endereço do destinatário (dest_addr) e o seqno da sua carteira (é igual a 1 após a inicialização da carteira e aumenta em 1 após cada transação de saída - você pode veja solicitando o status da sua conta). Para testes, você pode usar, por exemplo, minha carteira - 0x4fcd520b8fcca096b567d734be3528edc6bed005f6930a9ec9ac1aa714f211f2.

No arranque (./crypto/fift wallet.fif) o script pegará o endereço da sua carteira (de onde você transfere) e sua chave privada dos arquivos new-wallet.addr и new-wallet.pk, e a mensagem recebida será gravada em new-wallet-query.boc.

Como antes, para realizar a transação diretamente, ligue sendfile new-wallet-query.boc no cliente. Depois disso, não esqueça de atualizar o estado do blockchain (last) e verifique se o saldo e o seqno da nossa carteira foram alterados (getaccount <account_id>).

Cliente de teste TON (Telegram Open Network) e nova linguagem Fift para contratos inteligentes

Só isso, agora podemos criar contratos inteligentes no TON e enviar solicitações para eles. Como você pode ver, a funcionalidade atual já é suficiente para, por exemplo, tornar uma carteira mais amigável e com interface gráfica (porém, espera-se que ela já esteja disponível como parte do mensageiro).

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

Tem interesse em continuar os artigos com análises de TON, TVM, Fift?

  • Sim, estou aguardando a conclusão da série de artigos com uma visão geral do TON

  • Sim, é interessante ler mais sobre a linguagem Fift

  • Sim, quero aprender mais sobre a TON Virtual Machine e seu montador

  • Não, nada disso é interessante

39 usuários votaram. 12 usuários se abstiveram.

O que você acha dos planos do Telegram para lançar o TON?

  • Tenho grandes esperanças neste projeto

  • Estou apenas acompanhando seu desenvolvimento com interesse.

  • Sou cético e duvido do seu sucesso.

  • Estou inclinado a considerar esta iniciativa um fracasso e desnecessária para as grandes massas

47 usuários votaram. 12 usuários se abstiveram.

Fonte: habr.com

Adicionar um comentário