Como escrever e publicar um contrato inteligente no TON
Sobre o que é este artigo?
Neste artigo, vou contar como participei do primeiro (de dois) concursos do Telegram sobre blockchain, não ganhei prêmios e decidi registrar a experiência em um artigo, para que não se perca no tempo e, talvez, ajude alguém.
Como eu não queria escrever um código abstrato, mas fazer algo funcional, escrevi um contrato inteligente de loteria instantânea e um site que mostra os dados do contrato inteligente diretamente do TON sem usar repositórios intermediários.
Este artigo será útil para quem deseja criar seu primeiro contrato inteligente no TON, mas não sabe por onde começar.
Usando o exemplo da loteria, vou do ambiente de instalação até a publicação do contrato inteligente, a interação com ele e escreverei um site para obter e publicar dados.
Sobre a participação no concurso
Em outubro do ano passado, o Telegram anunciou um concurso sobre blockchain com novas linguagens Fift e FunC. Era necessário escolher e escrever qualquer um dos cinco contratos inteligentes propostos. Eu achei que seria interessante me dedicar a algo incomum, estudar a linguagem e fazer algo, mesmo que no futuro não precisasse escrever mais nada. Além disso, o tema está sempre em alta.
Vale mencionar que eu não tinha experiência no desenvolvimento de contratos inteligentes.
Eu planejava participar até o fim, enquanto fosse possível, e depois escrever um artigo de visão geral, mas falhei logo no primeiro. Eu com multipágina para FunC e ela funcionou, no geral. Baseei-me em um .
Naquele momento, achei que isso era suficiente para conquistar pelo menos algum prêmio. No final, cerca de 40 dos 60 participantes se tornaram premiados e eu não estava entre eles. No geral, não foi tão grave, mas uma coisa me incomodou. No momento da divulgação dos resultados, a revisão com o teste do meu contrato não tinha sido feita; perguntei aos participantes no chat se havia alguém mais que não o tivesse, e ninguém se manifestou.
Aparentemente, prestando atenção às minhas mensagens, dois dias depois os juízes publicaram um comentário e eu não entendi se eles simplesmente perderam meu contrato inteligente durante a avaliação ou se acharam que ele era tão ruim que não merecia um comentário. Fiz uma pergunta na página, mas não obtive resposta. Embora quem avaliou não seja um segredo, considerei desnecessário enviar mensagens pessoais.
O tempo gasto para entender não foi pouco, então decidiu-se escrever um artigo. Como ainda não há muita informação, o artigo ajudará a economizar tempo para todos os interessados.
O conceito de funcionamento de contratos inteligentes no TON
Antes de escrever qualquer coisa, é necessário entender por onde começar. Portanto, agora vou explicar das quais partes o sistema é composto. Mais precisamente, quais partes é necessário conhecer para escrever pelo menos um contrato funcional.
Vamos nos concentrar na escrita de um contrato inteligente e no trabalho com a Máquina Virtual TON (TVM), Fift e FunC, por isso o artigo se assemelha mais a uma descrição do desenvolvimento de um programa comum. Não vamos abordar aqui como a plataforma funciona.
Na verdade, a documentação oficial sobre como funciona a TVM e a linguagem Fift está disponível. Durante a participação no concurso e agora, enquanto escrevo o contrato atual, frequentemente consultei essa documentação.
A linguagem principal na qual os contratos inteligentes são escritos é FunC. Atualmente não há documentação sobre ela, então, para escrever algo, é necessário estudar exemplos de contratos inteligentes do repositório oficial e a própria implementação da linguagem lá, além de poder consultar exemplos de contratos inteligentes dos últimos dois concursos. Os links estão no final do artigo.
Vamos supor que já escrevemos um contrato inteligente em FunC, depois disso, compilamos o código para assembly Fift.
O contrato inteligente compilado deve ser publicado. Para isso, precisamos escrever uma função em Fift, que aceitará o código do contrato inteligente e outros parâmetros, e, como saída, teremos um arquivo com a extensão .boc (que significa "bag of cells"), e, dependendo de como escrevermos, a chave privada e o endereço, que é gerado com base no código do contrato inteligente. Para o endereço do contrato inteligente, que ainda não foi publicado, já é possível enviar gramas.
Para publicar um contrato inteligente no TON, o arquivo obtido precisa ser enviado para a blockchain com a ajuda do light client (sobre isso mais adiante). Mas antes de publicar, é necessário transferir gramas para o endereço gerado, caso contrário, o contrato inteligente não será publicado. Após a publicação, será possível interagir com o contrato inteligente, enviando mensagens para ele de fora (por exemplo, usando o light client) ou de dentro (por exemplo, um contrato inteligente envia uma mensagem para outro dentro do TON). .boc O arquivo precisará ser enviado para a blockchain através de um light client (sobre isso mais adiante). Mas antes de publicar, é necessário transferir gramas para o endereço gerado; caso contrário, o smart contract não será publicado. Após a publicação do smart contract, será possível interagir com ele, enviando mensagens de fora (por exemplo, usando o light client) ou de dentro (por exemplo, um smart contract enviando uma mensagem para outro dentro do TON).
Depois que entendemos como o código é publicado, tudo fica mais fácil. Sabemos mais ou menos o que queremos escrever e como nosso programa funcionará. E durante a escrita, procuramos como isso já foi implementado em contratos inteligentes existentes, ou olhamos o código de implementação. Fift e FunC no repositório oficial, ou verificamos na documentação oficial.
Com frequência, procurei por palavras-chave no chat do Telegram onde se reuniram todos os participantes do concurso e funcionários do Telegram, pois durante o concurso todos estavam lá discutindo sobre Fift e FunC. O link está no final do artigo.
É hora de passar da teoria à prática.
Preparação do ambiente para trabalhar com TON.
Tudo o que será descrito neste artigo fiz no MacOS e verifiquei em uma instalação limpa do Ubuntu 18.04 LTS no Docker.
A primeira coisa que precisamos fazer é baixar e instalar lite-client , com o qual podemos enviar solicitações para o TON.
O guia no site oficial descreve o processo de instalação de forma bastante detalhada e clara, omitindo alguns detalhes. Aqui seguimos as instruções enquanto instalamos as dependências que faltam. Não compilei cada projeto manualmente e instalei a partir do repositório oficial do Ubuntu (no MacOS, usei brew).
apt -y install git
apt -y install wget
apt -y install cmake
apt -y install g++
apt -y install zlib1g-dev
apt -y install libssl-dev Depois que todas as dependências estão instaladas, podemos instalar lite-client, Fift, FunC.
Primeiro, clonamos o repositório TON com as dependências. Para conveniência, faremos tudo na pasta ~\/TON.
cd ~\/TON
git clone https:\/\/github.com\/ton-blockchain\/ton.git
cd .\/ton
git submodule update --init --recursiveAs implementações também estão armazenadas no repositório. Fift e FunC.
Agora estamos prontos para compilar o projeto. O código do repositório foi clonado para a pasta ~\/TON\/ton. No ~\/TON criamos a pasta build e compilamos o projeto nela.
mkdir ~\/TON\/build
cd ~\/TON\/build
cmake ..\/tonComo estamos prestes a escrever um contrato inteligente, precisamos não só lite-client, mas também Fift com FunC, por isso compilamos tudo. Não é um processo rápido, então aguardamos.
cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target funcDepois, baixamos o arquivo de configuração que contém os dados sobre o nó ao qual lite-client será conectada.
wget https://test.ton.org/ton-lite-client-test1.config.jsonFazendo nossas primeiras solicitações ao TON.
Agora vamos executar lite-client.
cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.jsonSe a compilação foi bem-sucedida, após a execução você verá o log de conexão do cliente leve ao nó.
[ 1][t 2][1582054822.963129282][lite-client.h:201][!testnode] conn ready
[ 2][t 2][1582054823.085654020][lite-client.cpp:277][!testnode] server version is 1.1, capabilities 7
[ 3][t 2][1582054823.085725069][lite-client.cpp:286][!testnode] server time is 1582054823 (delta 0)
...Você pode executar o comando ajuda e veja quais comandos estão disponíveis.
ajudaVamos listar os comandos que utilizaremos neste artigo.
lista de comandos disponíveis:
last Obtém o último bloco e informações de estado do servidor
sendfile Carrega uma mensagem serializada de e a envia para o servidor
getaccount [] Carrega o estado mais recente da conta especificada; está no formato [:]
runmethod [] ... Executa o método GET da conta com os parâmetros especificadoslast obtém o último bloco criado no servidor.
sendfile envia para o TON um arquivo com mensagem, é com este comando que o contrato inteligente é publicado e as solicitações para ele são feitas.
getaccount carrega o estado atual do contrato inteligente com o endereço especificado.
runmethod [] executa os métodos GET do contrato inteligente. Agora estamos prontos para escrever o contrato em si.
Implementação
Ideia
Como já mencionado, o contrato inteligente que estamos escrevendo é uma loteria.
E não é uma loteria na qual você precisa comprar um bilhete e esperar uma hora, um dia ou um mês, mas sim instantânea, na qual o usuário transfere para o endereço do contrato N gramas, e recebe imediatamente de volta 2 * N gramas ou perde. Faremos a probabilidade de vitória em torno de 40%. Se os gramas para o pagamento não forem suficientes, consideraremos a transação como um aporte.
É importante que as apostas possam ser visualizadas em tempo real e de forma conveniente, para que o usuário possa entender imediatamente se ganhou ou perdeu. Portanto, precisamos criar um site que mostre as apostas e o resultado diretamente do TON.
Escrevendo o contrato inteligente
Para conveniência, criei a realce de código para FunC, o plugin pode ser encontrado e instalado na busca do Visual Studio Code; caso queiram adicionar algo, disponibilizei o plugin em acesso aberto. Também anteriormente, alguém fez um plugin para trabalhar com Fift, que também pode ser encontrado e instalado no VSC.
Vamos imediatamente criar um repositório onde iremos commitar os resultados intermediários.
Para facilitar nossa vida, escreveremos o contrato inteligente e testaremos localmente até que ele esteja pronto. Somente depois disso o publicaremos no TON.
O contrato inteligente tem dois métodos externos aos quais podemos chamar. O primeiro, recv_external() essa função é executada quando uma solicitação ao contrato ocorre de fora do mundo, ou seja, não do TON, por exemplo, quando nós mesmo formulamos a mensagem e a enviamos através do lite-client. O segundo, recv_internal() isto é quando dentro do próprio TON algum contrato interage com o nosso. Em ambos os casos, parâmetros podem ser passados para a função.
Vamos começar com um exemplo simples, que funcionará se publicado, mas não possui nenhuma carga funcional.
() recv_internal(slice in_msg) impure {
;; TODO: implementação
}
() recv_external(slice in_msg) impure {
;; TODO: implementação
}Aqui precisamos esclarecer o que é slice. Todos os dados armazenados na TON Blockchain são uma coleção TVM cell ou simplesmente cell, nessa célula é possível armazenar até 1023 bits de dados e até 4 referências a outras células.
TVM cell slice ou slice é parte de um existente cell é usada para seu parsing, e logo ficará claro. O principal para nós é que podemos passar slice e dependendo do tipo de mensagem processar os dados em recv_external() ou recv_internal().
impure — palavra-chave que indica que a função altera os dados do contrato inteligente.
Vamos salvar o código do contrato em lottery-code.fc e compilar.
~\/TON\/build\/crypto\/func -APSR -o lottery-compiled.fif ~\/TON\/ton\/crypto\/smartcont\/stdlib.fc .\/lottery-code.fc Os valores dos flags podem ser visualizados usando o comando
~\/TON\/build\/crypto\/func -helpObtivemos um código de assembler Fift compilado em lottery-compiled.fif:
// lottery-compiled.fif
"Asm.fif" include
// automatically generated from `/Users/rajymbekkapisev/TON/ton/crypto/smartcont/stdlib.fc` `./lottery-code.fc`
PROGRAM{
DECLPROC recv_internal
DECLPROC recv_external
recv_internal PROC:<{
// in_msg
DROP //
}>
recv_external PROC:<{
// in_msg
DROP //
}>
}END>cEle pode ser executado localmente, para isso preparamos o ambiente.
Observe que a primeira linha importa Asm.fif, este é o código escrito em Fift para o assembler Fift.
Como queremos executar e testar o contrato inteligente localmente, criaremos o arquivo lottery-test-suite.fif e copiaremos o código compilado, substituindo a última linha, que grava o código do contrato inteligente em uma constante code, para depois passá-lo para a máquina virtual:
"TonUtil.fif" include
"Asm.fif" include
PROGRAM{
DECLPROC recv_internal
DECLPROC recv_external
recv_internal PROC:<{
\/\/ in_msg
DROP \/\/
}>
recv_external PROC:<{
\/\/ in_msg
DROP \/\/
}>
}END>s constant code
Por enquanto parece claro, agora vamos adicionar ao mesmo arquivo o código que vamos usar para executar o TVM.
0 tuple 0x076ef1ea , \/\/ magic
0 , 0 , \/\/ ações msg_sents
1570998536 , \/\/ unix_time
1 , 1 , 3 , \/\/ block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , \/\/ saldo restante
0 , dictnew , \/\/ contract_address, global_config
1 tuple \/\/ embrulhar em outra tupla
constant c7
0 constant recv_internal \/\/ para executar recv_internal()
-1 constant recv_external \/\/ para invocar recv_external()Em c7 gravamos o contexto, ou seja, os dados com os quais o TVM será executado (ou o estado da rede). Além disso, durante o concurso, um dos desenvolvedores mostrou como foi criado c7 e eu copiei. Neste artigo, pode ser que precisemos mudar rand_seed Como sua geração de números aleatórios depende dele e não muda, ele retornará sempre o mesmo número.
recv_internal e recv_external As constantes com os valores 0 e -1 serão responsáveis pela chamada das funções correspondentes no contrato inteligente.
Agora estamos prontos para criar o primeiro teste para o nosso contrato inteligente vazio. Por clareza, adicionaremos todos os testes neste mesmo arquivo. lottery-test-suite.fif.
Vamos criar uma variável storage e gravar nela um vazio cell, que será o armazenamento do contrato inteligente.
message Esta é a mensagem que enviaremos ao contrato inteligente de fora. Também a deixaremos vazia por enquanto.
armazenamento de variável
<b b> armazenamento !
mensagem de variável
<b b> mensagem ! Depois de preparar as constantes e variáveis, iniciamos o TVM com o comando runvmctx e passamos os parâmetros criados como entrada.
message @
recv_external
code
storage @
c7
runvmctx No final, teremos Agora podemos executar o código resultante. Fift.
export FIFTPATH=~\/TON\/ton\/crypto\/fift\/lib \/\/ executamos uma vez para conveniência ~\/TON\/build\/crypto\/fift -s lottery-test-suite.fif
O programa deve rodar sem erros e na saída veremos o log de execução: execute SETCP 0 execute DICTPUSHCONST 19 (xC_,1) execute DICTIGETJMPZ execute DROP execute implicit RET [ 3][t 0][1582281699.325381279][vm.cpp:479] steps: 5 gas: used=304, max=9223372036854775807, limit=9223372036854775807, credit=0
Ótimo, escrevemos a primeira versão funcional do contrato inteligente.Agora precisamos adicionar funcionalidade. Primeiro, lidaremos com mensagens que vêm do mundo externo em
O desenvolvedor escolhe o formato da mensagem que o contrato pode aceitar. recv_external()
Mas geralmente,
em primeiro lugar, queremos proteger nosso contrato do mundo exterior e garantir que apenas o proprietário do contrato possa enviar mensagens externas para ele.
- Em segundo lugar, quando enviamos uma mensagem válida ao TON, queremos que isso aconteça apenas uma vez, e ao reenvio da mesma mensagem, o contrato inteligente a rejeite.
- Portanto, quase todo contrato resolve esses dois problemas. Como nosso contrato aceita mensagens externas, também precisamos nos preocupar com isso.
Vamos fazer isso de forma reversa. Primeiro, resolveremos o problema de repetição; se o contrato já recebeu e processou tal mensagem, ele não a executará uma segunda vez. Em seguida, resolveremos o problema de garantir que apenas um grupo específico de pessoas possa enviar mensagens ao contrato inteligente.
Faremos isso em ordem inversa. Primeiro, resolveremos o problema da repetição; se o contrato já recebeu uma mensagem assim e a processou, ele não a executará novamente. Depois, resolveremos o problema de garantir que apenas um círculo específico de pessoas possa enviar mensagens ao contrato inteligente.
Existem diferentes maneiras de resolver o problema de mensagens repetidas. Vamos fazer da seguinte forma. No contrato inteligente, inicializamos um contador de mensagens recebidas com o valor inicial 0. Em cada mensagem, vamos adicionar o valor atual do contador ao contrato inteligente. Se o valor do contador na mensagem não coincidir com o valor no contrato inteligente, não o processamos; se coincidir, processamos e aumentamos o contador no contrato inteligente em 1.
Voltamos para lottery-test-suite.fif e adicionamos um segundo teste. Vamos enviar um número incorreto; o código deve lançar uma exceção. Por exemplo, suponha que os dados do contrato armazenem 166, mas enviamos 165.
<b 166 32 u, b> arma !
<b 165 32 u, b> mensagem !
mensagem @
recv_external
código
arma @
c7
runvmctx
dropar
exit_code !
."Código de saída " exit_code @ . cr
exit_code @ 33 - abortar"Teste #2 Não passado"Vamos executar.
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fif E veremos que o teste falha.
[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Teste #2 Não passou
[ 1][t 0][1582283084.210941076][fift-main.cpp:196] Erro ao interpretar o arquivo `lottery-test-suite.fif`: erro ao interpretar o arquivo incluído `lottery-test-suite.fif`: lottery-test-suite.fif:67: abort": Teste #2 Não passouNesta etapa lottery-test-suite.fif deve parecer como .
Agora vamos adicionar a lógica do contador no contrato inteligente em lottery-code.fc.
() recv_internal(slice in_msg) impure {
;; TODO: implementação
}
() recv_external(slice in_msg) impure {
if (slice_empty?(in_msg)) {
return ();
}
int msg_seqno = in_msg~load_uint(32);
var ds = begin_parse(get_data());
int stored_seqno = ds~load_uint(32);
throw_unless(33, msg_seqno == stored_seqno);
}Em slice in_msg contém a mensagem que estamos enviando.
A primeira coisa que fazemos é verificar se há dados na mensagem; se não houver, simplesmente saímos.
Em seguida, fazemos o parse da mensagem. in_msg~load_uint(32) carrega o número 165, 32 bits unsigned int da mensagem recebida.
Depois, carregamos 32 bits da armazenamento do contrato inteligente. Verificamos se o número carregado coincide com o enviado; caso contrário, lançamos uma exceção. No nosso caso, como estamos enviando um número que não coincide, a exceção deve ser lançada.
Agora vamos compilar.
~\/TON\/build\/crypto\/func -APSR -o lottery-compiled.fif ~\/TON\/ton\/crypto\/smartcont\/stdlib.fc .\/lottery-code.fc Vamos copiar o código resultante para lottery-test-suite.fif, lembrando de substituir a última linha.
Verificamos se o teste passa:
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fifpodemos olhar o commit correspondente com os resultados atuais.
Observamos que é inconveniente copiar constantemente o código compilado do contrato inteligente para o arquivo de testes, então vamos escrever um script que grabe o código em uma constante para nós, e nós apenas incluiremos o código compilado em nossos testes usando "include".
Na pasta do projeto, criaremos um arquivo build.sh com o seguinte conteúdo.
#!/bin/bash
~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fcVamos torná-lo executável.
chmod +x .\/build.shAgora, basta executar nosso script para compilar o contrato. Mas além disso, precisamos gravá-lo em uma constante code. Portanto, criaremos um novo arquivo lotter-compiled-for-test.fif, que incluiremos no arquivo lottery-test-suite.fif.
Vamos adicionar ao script sh um código que simplesmente duplicará o arquivo compilado em lotter-compiled-for-test.fif e mudará a última linha dele.
# copy and change for test
cp lottery-compiled.fif lottery-compiled-for-test.fif
sed '$d' lottery-compiled-for-test.fif > test.fif
rm lottery-compiled-for-test.fif
mv test.fif lottery-compiled-for-test.fif
echo -n "}END>s constant code" >> lottery-compiled-for-test.fifAgora, para verificar, vamos executar o script resultante e teremos um arquivo gerado lottery-compiled-for-test.fif, que incluiremos em nosso lottery-test-suite.fif
Em lottery-test-suite.fif remova o código do contrato e adicione a linha "lottery-compiled-for-test.fif" include.
Executamos os testes para verificar se eles passam.
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fifÓtimo, agora, para automatizar a execução dos testes, criaremos um arquivo test.sh, que primeiro executará build.sh, e depois iniciará os testes.
touch test.sh
chmod +x test.shDentro escrevemos
.\/build.sh
echo "nCompilação concluídan"
export FIFTPATH=~\/TON\/ton\/crypto\/fift\/lib
~\/TON\/build\/crypto\/fift -s lottery-test-suite.fifVamos fazer test.sh e rodar para garantir que os testes funcionem.
chmod +x .\/test.sh
.\/test.shVerificamos se o contrato compila e os testes executam.
Ótimo, agora ao iniciar test.sh a compilação e execução dos testes acontecerão automaticamente. Aqui está o link para .
Ok, antes de continuarmos, vamos fazer mais uma coisa para facilitar.
Vamos criar uma pasta build onde guardaremos o contrato compilado e sua cópia gravada em constante lottery-compiled.fif, lottery-compiled-for-test.fif. Também criaremos uma pasta test onde estarão os arquivos de teste lottery-test-suite.fif e potencialmente outros arquivos auxiliares. .
Vamos continuar o desenvolvimento do smart contract.
A seguir deve haver um teste que verifica se a mensagem é recebida e o contador é atualizado no armazenamento quando enviamos um número correto. Mas faremos isso mais tarde.
Agora vamos pensar sobre qual estrutura de dados e quais dados precisam ser armazenados no smart contract.
Descreverei tudo o que armazenamos.
`seqno` é um número inteiro positivo de 32 bits que atua como contador.
`pubkey` é um número inteiro positivo de 256 bits que representa a chave pública, com a qual iremos verificar a assinatura da mensagem enviada externamente, conforme explicado abaixo.
`order_seqno` é um número inteiro positivo de 32 bits que armazena o contador de apostas.
`number_of_wins` é um número inteiro positivo de 32 bits que armazena o número de vitórias.
`incoming_amount` é um tipo de dado Gram (os primeiros 4 bits representam o comprimento), que armazena o total de gramas enviados ao contrato.
`outgoing_amount` é a quantidade total de gramas enviada aos vencedores.
`owner_wc` é o número do blockchain, um número inteiro de 32 bits (em alguns casos é descrito como 8 bits). Atualmente, existem apenas dois: -1 e 0.
`owner_account_id` é um número inteiro positivo de 256 bits, que é o endereço do contrato no blockchain atual.
`orders` é uma variável do tipo dicionário que armazena as últimas vinte apostas. Em seguida, precisamos escrever duas funções. A primeira chamaremos de pack_state(), que irá empacotar os dados para posterior armazenamento no repositório do contrato inteligente. A segunda, chamaremos de unpack_state() , que irá ler e retornar os dados do repositório.
_ pack_state(int seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) inline_ref {
return begin_cell()
.store_uint(seqno, 32)
.store_uint(pubkey, 256)
.store_uint(order_seqno, 32)
.store_uint(number_of_wins, 32)
.store_grams(incoming_amount)
.store_grams(outgoing_amount)
.store_int(owner_wc, 32)
.store_uint(owner_account_id, 256)
.store_dict(orders)
.end_cell();
}
_ unpack_state() inline_ref {
var ds = begin_parse(get_data());
var unpacked = (ds~load_uint(32), ds~load_uint(256), ds~load_uint(32), ds~load_uint(32), ds~load_grams(), ds~load_grams(), ds~load_int(32), ds~load_uint(256), ds~load_dict());
ds.end_parse();
return unpacked;
}Adicionamos essas duas funções ao início do contrato inteligente. O resultado será um resultado intermediário.
Para salvar os dados, precisaremos chamar a função incorporada set_data() , e ela gravará os dados de pack_state() no repositório do contrato inteligente.
cell packed_state = pack_state(arg_1, .., arg_n);
set_data(packed_state);Agora que temos funções convenientes para gravar e ler dados, podemos avançar.
Precisamos verificar se a mensagem recebida externamente foi assinada pelo proprietário do contrato (ou por outro usuário que tenha acesso à chave privada).
Ao publicar um contrato inteligente, podemos inicializá-lo com os dados que precisamos armazenar, que serão mantidos para uso futuro. Vamos registrar a chave pública para que possamos verificar se a assinatura da mensagem recebida foi feita com a chave privada correspondente.
Antes de prosseguir, vamos criar uma chave privada e registrá-la em test/keys/owner.pk. Para isso, iniciaremos o Fift em modo interativo e executaremos quatro comandos.
`newkeypair` gera uma chave pública e uma chave privada e grava-as na pilha.
`drop` remove o elemento superior da pilha (neste caso, a chave pública)
`.s` simplesmente exibe o que está atualmente na pilha
`"owner.pk" B>file` grava a chave privada em um arquivo chamado `owner.pk`.
`bye` encerra o trabalho com o Fift. Vamos criar uma pasta keys dentro da pasta test e gravaremos a chave privada lá.
mkdir test/keys
cd test/keys
~/TON/build/crypto/fift -i
newkeypair
ok
.s
BYTES:128DB222CEB6CF5722021C3F21D4DF391CE6D5F70C874097E28D06FCE9FD6917 BYTES:DD0A81AAF5C07AAAA0C7772BB274E494E93BB0123AA1B29ECE7D42AE45184128
drop
ok
"owner.pk" B>file
ok
byePodemos ver o arquivo owner.pk.
Removemos a chave pública da pilha, e quando necessário, podemos obtê-la a partir da privada.
Agora precisamos escrever a verificação da assinatura. Começaremos com um teste. Primeiro, lemos a chave privada do arquivo usando a função file>B e a registramos na variável owner_private_key, em seguida, usando a função priv>pub , convertemos a chave privada para pública e registramos o resultado em owner_public_key.
variable owner_private_key
variable owner_public_key
"./keys/owner.pk" file>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !Ambas as chaves serão necessárias.
Inicializaremos o armazenamento do contrato inteligente com dados arbitrários na mesma sequência que na função pack_state()e registraremos na variável storage.
variável owner_private_key
variável owner_public_key
variável orders
variável owner_wc
variável owner_account_id
".\/keys\/owner.pk" arquivo>B owner_private_key !
owner_private_key @ priv>pub owner_public_key !
dictnew orders !
0 owner_wc !
0 owner_account_id !
<b 0 32 u, owner_public_key @ b, gram, owner_wc i, owner_account_id 256 orders dict, b> arma !Em seguida, montaremos a mensagem assinada, que conterá apenas a assinatura e o valor do contador.
Primeiro criamos os dados que queremos transmitir, depois os assinamos com a chave privada e, por fim, formamos a mensagem assinada.
mensagem_variável_a_assinar
mensagem_variável_a_enviar
assinatura_variável
<b 0 32 u, b> mensagem_a_assinar !
mensagem_a_assinar @ proprietário_chave_privada @ ed25519_assinar_uint assinatura !
<b signature @ b, 0 32 u, b> <s ! Assim, a mensagem que enviaremos ao contrato inteligente está registrada na variável message_to_send, sobre as funções hashu, ed25519_sign_uint pode ser lido .
E para executar o teste novamente, chamamos.
message_to_send @
recv_external
code
storage @
c7
runvmctxo arquivo deve aparecer com os testes nesta etapa.
Vamos executar o teste e ele falhará, então alteraremos o contrato inteligente para que possa receber mensagens desse formato e verificar a assinatura.
Primeiro, lemos 512 bits da assinatura da mensagem e gravamos em uma variável; em seguida, lemos 32 bits da variável contador.
Como temos uma função para ler dados do armazenamento do contrato inteligente, vamos utilizá-la.
Em seguida, verificamos o contador enviado com o armazenamento e a assinatura. Se algo não coincidir, lançamos uma exceção com o código apropriado.
var signature = in_msg~load_bits(512);
var message = in_msg;
int msg_seqno = message~load_uint(32);
(int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, check_signature(slice_hash(in_msg), signature, pubkey));Commit correspondente .
Vamos executar os testes e veremos que o segundo teste falha. Por duas razões: falta de bits na mensagem e falta de bits no armazenamento, portanto, o código falha ao fazer o parsing. Precisamos adicionar a assinatura da mensagem que estamos enviando e copiar o armazenamento do último teste.
No segundo teste, vamos adicionar a assinatura da mensagem e mudar o armazenamento do contrato inteligente. o arquivo com os testes atualmente.
Vamos escrever um quarto teste, no qual enviaremos uma mensagem assinada com a chave privada de outra pessoa. Vamos criar mais uma chave privada e salvá-la em um arquivo not-owner.pk. Com esta chave privada, assinaremos a mensagem. Vamos executar os testes e garantir que todos os testes passam. no momento.
Agora, finalmente podemos passar para a implementação da lógica do contrato inteligente.
Em recv_external() Vamos aceitar dois tipos de mensagens.
Como nosso contrato vai acumular as perdas dos jogadores, esse dinheiro deve ser transferido ao criador da loteria. O endereço da carteira do criador da loteria é gravado no armazenamento ao criar o contrato.
Para garantir, precisamos da capacidade de mudar o endereço para o qual enviar os gramas dos perdedores. Também devemos ser capazes de enviar gramas da loteria para o endereço do proprietário.
Começaremos pelo primeiro. Primeiro, escreveremos um teste que verificará se, após o envio da mensagem, o contrato inteligente salvou o novo endereço no armazenamento. Notemos que, na mensagem, além do contador e do novo endereço, também passamos ação um número inteiro não negativo de 7 bits; dependendo disso, escolheremos como processar a mensagem no contrato inteligente.
<b 0 32 u, 1 @ 7 new_owner_wc i, new_owner_account_id 256 b> mensagem_a_assinar !No teste, pode-se ver como ocorre a desserialização do armazenamento do contrato inteligente. storage No Fift. A deserialização de variáveis está descrita na documentação do Fift.
com a adição do teste.
Executamos o teste e verificamos que ele falha. Agora adicionaremos a lógica para alterar o endereço do proprietário da loteria.
No contrato inteligente, continuamos a fazer o parsing message, lendo em ação. Lembre-se de que teremos dois ação: alteração de endereço e envio de gramas.
Em seguida, lemos o novo endereço do proprietário do contrato e o salvamos no armazenamento.
Executamos os testes e vemos que o terceiro teste falha. Falha porque o contrato agora faz o parsing adicional de 7 bits da mensagem, que estão faltando no teste. Vamos adicionar à mensagem um ação. Vamos executar os testes e ver que todos passam. commit com as alterações. Ótimo.
Agora vamos escrever a lógica para enviar a quantidade especificada de gramas para o endereço previamente salvo.
Primeiro, vamos escrever um teste. Vamos escrever dois testes: um quando o saldo não é suficiente e outro quando tudo deve passar com sucesso. Os testes podem ser vistos .
Agora vamos completar o código. Primeiro, escreveremos dois métodos auxiliares. O primeiro, um método get para saber o saldo atual do contrato inteligente.
int balance() inline_ref method_id {
return get_balance().pair_first();
}E o segundo para enviar gramas para outro contrato inteligente. Este método eu copiei completamente de outro contrato inteligente.
() send_grams(int wc, int addr, int grams) impure {
;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
cell msg = begin_cell()
;; .store_uint(0, 1) ;; 0 <= indicador de formato int_msg_info$0
;; .store_uint(1, 1) ;; 1 <= ihr desabilitado
;; .store_uint(1, 1) ;; 1 <= bounce = true
;; .store_uint(0, 1) ;; 0 <= bounced = false
;; .store_uint(4, 5) ;; 00100 <= flags de endereço, qualquer transmissão = false, workchain 8 bits
.store_uint (196, 9)
.store_int(wc, 8)
.store_uint(addr, 256)
.store_grams(grams)
.store_uint(0, 107) ;; 106 zeros + 0 como um indicador de que não há célula com os dados.
.end_cell();
send_raw_message(msg, 3); ;; modo, 2 para ignorar erros, 1 para o remetente pagar taxas, 64 para retornar o valor da mensagem recebida
}Vamos adicionar esses dois métodos ao contrato inteligente e escrever a lógica. Primeiro, analisamos a quantidade de gramas da mensagem. Em seguida, verificamos o saldo; se não for suficiente, lançamos uma exceção. Se tudo estiver bem, enviamos as gramas para o endereço salvo e atualizamos o contador.
int amount_to_send = message~load_grams();
throw_if(36, amount_to_send + 500000000 > balance());
accept_message();
send_grams(owner_wc, owner_account_id, amount_to_send);
set_data(pack_state(stored_seqno + 1, pubkey, order_seqno, number_of_wins, incoming_amount, outgoing_amount, owner_wc, owner_account_id, orders));o contrato inteligente se parece até agora. Vamos executar os testes e garantir que eles passam.
A propósito, a cada mensagem processada pelo contrato inteligente, uma taxa é cobrada. Para que o contrato inteligente execute a solicitação da mensagem, após as verificações básicas, é necessário chamar accept_message().
Agora, vamos lidar com mensagens internas. Na verdade, estaremos apenas aceitando gramas e enviando de volta ao jogador o valor dobrado em caso de vitória e um terço ao proprietário em caso de derrota.
Primeiro, vamos escrever um teste simples. Para isso, precisaremos de um endereço de teste do contrato inteligente de onde supostamente estamos enviando gramas para o contrato inteligente.
O endereço do contrato inteligente consiste em dois números, um inteiro de 32 bits responsável pelo workchain e um número de conta único não negativo de 256 bits dentro desse workchain. Por exemplo, -1 e 12345, iremos salvar esse endereço em um arquivo.
Copiei a função para salvar o endereço de .
// ( wc addr fname -- ) Save address to file in 36-byte format
{ -rot 256 u>B swap 32 i>B B+ swap B>file } : save-addressVamos analisar como a função funciona, isso ajudará a entender como o Fift opera. Iniciamos o Fift em modo interativo.
~\/TON\/build\/crypto\/fift -i Primeiro, colocamos na pilha -1, 12345 e o nome do futuro arquivo "sender.addr":
-1 12345 "sender.addr" A próxima etapa executa a função -rot, que move a pilha de forma que o número único do contrato inteligente fique no topo:
"sender.addr" -1 12345256 u>B converte um inteiro não negativo de 256 bits em bytes.
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039swap troca os dois elementos superiores da pilha.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -132 i>B converte um inteiro de 32 bits em bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFFB+ concatena duas Sequências de bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFFNovamente swap.
BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" E, finalmente, os bytes são gravados no arquivo B>file. Após isso, nossa pilha está vazia. Paramos Fift. Um arquivo chamado sender.addrfoi criado na pasta atual. Vamos mover o arquivo para a pasta criada test\/addresses\/.
Vamos escrever um teste simples que enviará gramas para o contrato inteligente. .
Agora vamos lidar com a lógica da loteria.
Primeiro, verificamos se a mensagem bounced ou não, se sim, ignoramos. bounced, o que significa que o contrato devolverá gramas se ocorrer algum erro. Não devolveremos gramas se de repente houver um erro. bounced Verificamos o saldo, se for menor que meio grama, então simplesmente aceitamos a mensagem e ignoramos.
Verificamos o saldo; se for menor que meio grama, simplesmente aceitamos a mensagem e ignoramos.
Em seguida, analisamos o endereço do contrato inteligente de onde veio a mensagem.
Lemos os dados do armazenamento e depois removemos as apostas antigas do histórico se houver mais de vinte. Para facilitar, escrevi três funções adicionais. pack_order(), unpack_order(), remove_old_orders().
Em seguida, verificamos se o saldo é insuficiente para o pagamento; se for, consideramos que isso não é uma aposta, mas um depósito e salvamos o depósito em orders.
Finalmente, vamos ao cerne do contrato inteligente.
Primeiro, se o jogador perdeu, salvamos sua aposta no histórico e, se a quantia for maior que 3 gramas, enviamos 1/3 ao proprietário do contrato inteligente.
Se o jogador ganhou, enviamos o dobro do valor ao endereço do jogador e depois salvamos as informações da aposta no histórico.
() recv_internal(int order_amount, cell in_msg_cell, slice in_msg) impure {
var cs = in_msg_cell.begin_parse();
int flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
if (flags & 1) { ;; ignora o retornado
return ();
}
if (order_amount < 500000000) { ;; apenas recebe gramas sem alterar o estado
return ();
}
slice src_addr_slice = cs~load_msg_addr();
(int src_wc, int src_addr) = parse_std_addr(src_addr_slice);
(int stored_seqno, int pubkey, int order_seqno, int number_of_wins, int incoming_amount, int outgoing_amount, int owner_wc, int owner_account_id, cell orders) = unpack_state();
orders = remove_old_orders(orders, order_seqno);
if (balance() = 4) {
builder order = pack_order(order_seqno, 3, now(), order_amount, src_wc, src_addr);
orders~udict_set_builder(32, order_seqno, order);
set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins, incoming_amount + order_amount, outgoing_amount, owner_wc, owner_account_id, orders));
if (order_amount > 3000000000) {
send_grams(owner_wc, owner_account_id, order_amount / 3);
}
return ();
}
send_grams(src_wc, src_addr, 2 * order_amount);
builder order = pack_order(order_seqno, 2, now(), order_amount, src_wc, src_addr);
orders~udict_set_builder(32, order_seqno, order);
set_data(pack_state(stored_seqno, pubkey, order_seqno + 1, number_of_wins + 1, incoming_amount, outgoing_amount + 2 * order_amount, owner_wc, owner_account_id, orders));
}Isso é tudo. .
Agora resta o simples: faremos métodos GET para que o mundo externo possa obter informações sobre o estado do contrato (basicamente, contar os dados de seu armazenamento do contrato inteligente).
. Sobre como obter informações do contrato inteligente, escreveremos a seguir.
Além disso, eu esqueci de adicionar o código que irá processar a primeira solicitação que ocorre ao publicar o contrato inteligente. . E também o bug que envia 1/3 do valor para a conta do proprietário.
Depois, resta publicar o contrato inteligente. Vamos criar uma pasta requests.
Baseei-me no código de publicação que está no repositório oficial.
Devo destacar que estamos formando o armazenamento do contrato inteligente e a mensagem de entrada. Depois disso, é gerado o endereço do contrato inteligente, ou seja, o endereço é conhecido antes mesmo da publicação no TON. Em seguida, precisamos enviar algumas gramas para esse endereço e só depois enviar o arquivo com o próprio contrato inteligente, pois a rede cobra uma taxa pelo armazenamento do contrato inteligente e suas operações (validador que armazena e executa contratos inteligentes). .
Em seguida, executamos o código de publicação e obtemos lottery-query.boc o arquivo e o endereço do contrato inteligente.
~\/TON\/build\/crypto\/fift -s requests\/new-lottery.fif 0Não esqueça de salvar os arquivos gerados: lottery-query.boc, lottery.addr, lottery.pk.
Entre outras coisas, nos logs de execução veremos o endereço do contrato inteligente.
endereço da nova carteira = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a
( Salvando endereço no arquivo lottery.addr)
Endereço não reembolsável (para inicialização): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Endereço reembolsável (para acesso posterior): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFYPor curiosidade, vamos fazer uma solicitação no TON
$ .\/lite-client\/lite-client -C ton-lite-client-test1.config.json
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydE veremos que a conta com esse endereço está vazia.
o estado da conta está vazioEnviamos para o endereço 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 gramas e, após alguns segundos, executamos o mesmo comando. Para enviar gramas, utilizo , e gramas de teste podem ser solicitados a alguém do chat, que mencionarei no final do artigo.
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydVeja que na rede apareceu um contrato inteligente não inicializado (estado:account_uninit) com esse endereço e um saldo de 1.000.000.000 нанограмм.
o estado da conta é (conta
addr:(addr_std
anycast:nothing workchain_id:0 address:x044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A)
storage_stat:(storage_info
used:(storage_used
cells:(var_uint len:1 value:1)
bits:(var_uint len:1 value:103)
public_cells:(var_uint len:0 value:0)) last_paid:1583257959
due_payment:nothing)
storage:(account_storage last_trans_lt:3825478000002
balance:(currencies
grams:(nanograms
amount:(var_uint len:4 value:2000000000))
other:(extra_currencies
dict:hme_empty))
state:account_uninit))
x{C00044910149DBEAF8EADBB2B28722E7D6A2DC6E264EC2F1D9BEBD6FB209079BC2A20259C2F2F4CB3800000DEAC10776091DCD650004_}
última transação lt = 3825478000001 hash = B043616AE016682699477FFF01E6E903878CDFD6846042BA1BFC64775E7AC6C4
o saldo da conta é 2000000000ngAgora vamos publicar o contrato inteligente. Iniciaremos o cliente lite e executaremos.
> sendfile lottery-query.boc
[ 1][t 2][1583008371.631410122][lite-client.cpp:966][!testnode] enviando consulta do arquivo lottery-query.boc
[ 3][t 1][1583008371.828550100][lite-client.cpp:976][!query] status da mensagem externa é 1 Vamos verificar se o contrato foi publicado.
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydEntre outras coisas, receberemos.
storage:(account_storage last_trans_lt:3825499000002
balance:(currencies
grams:(nanograms
amount:(var_uint len:4 value:1987150999))
other:(extra_currencies
dict:hme_empty))
state:(account_activeVemos que account_active.
O commit correspondente com as alterações .
Agora vamos criar consultas para interagir com o contrato inteligente.
Mais especificamente, deixaremos o primeiro para alterar o endereço como trabalho autônomo, e o segundo para enviar gramas para o endereço do proprietário. Na verdade, precisamos fazer a mesma coisa que no teste de envio de gramas.
Esta é a mensagem que iremos enviar ao contrato inteligente, onde msg_seqno 165, ação 2 e 9.5 gramas para envio.
<b 165 32 u, 2 7 9500000000 gram, b>Não esqueçamos de assinar a mensagem com a chave privada lottery.pk, que foi gerada anteriormente ao criar o contrato inteligente. .
Obtemos informações do contrato inteligente usando métodos get
Agora veremos como executar os métodos get do contrato inteligente.
Iniciamos lite-client e executamos os métodos get que escrevemos.
$ .\/lite-client\/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments: [ 104128 ]
result: [ 64633878952 ]
...Em result contém o valor que a função balance() retorna do nosso contrato inteligente.
Faremos o mesmo para vários métodos.
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments: [ 77871 ]
result: [ 1 ] Vamos solicitar o histórico de apostas.
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_orders
...
arguments: [ 67442 ]
result: [ ([0 1 1583258284 10000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [1 3 1583258347 4000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308] [2 1 1583259901 50000000000 0 74649920601963823558742197308127565167945016780694342660493511643532213172308]) ] Vamos usar o lite-client e métodos get para exibir informações sobre o contrato inteligente no site.
Exibindo dados do contrato inteligente no site
Eu escrevi um site simples em Python para mostrar os dados do contrato inteligente de forma conveniente. Não vou entrar em detalhes sobre ele e vou publicar o site. .
As solicitações para o TON são feitas de Python com lite-client. Para conveniência, o site é empacotado em Docker e publicado no Google Cloud. .
Vamos tentar
Agora vamos tentar enviar gramas para recarga de . Vamos enviar 40 gramas. E faremos algumas apostas para visualização. Vemos que o site exibe o histórico de apostas, a porcentagem atual de vitórias e outras informações úteis.
, que ganhamos a primeira, perdemos a segunda.
Epílogo
O artigo ficou muito mais longo do que eu esperava, talvez pudesse ser mais curto, ou talvez seja exatamente para uma pessoa que não sabe nada sobre o TON e quer escrever e publicar um contrato inteligente que não é tão simples, com a possibilidade de interagir. Talvez algumas coisas pudessem ser explicadas de forma mais simples.
Talvez alguns pontos na implementação pudessem ser feitos de forma mais eficiente e elegante, mas então levaria ainda mais tempo para preparar o artigo. Também é possível que eu tenha cometido algum erro ou não entendido algo, então se você está fazendo algo sério, precisa se basear na documentação oficial ou no repositório oficial com o código do TON.
Devo notar que, como o próprio TON ainda está em fase ativa de desenvolvimento, mudanças podem ocorrer que quebrem algum dos passos deste artigo (o que ocorreu enquanto eu escrevia, já corrigi), mas a abordagem geral provavelmente não mudará.
Não vou especular sobre o futuro do TON. Pode ser que a plataforma se torne algo maior e devemos dedicar tempo ao seu estudo e ocupar um espaço com nossos produtos desde já.
Há também o Libra do Facebook, que possui uma audiência potencial de usuários maior do que a do TON. Quase não sei nada sobre o Libra, mas pelo que vi no fórum, a atividade lá é muito maior do que na comunidade do TON. Embora os desenvolvedores e a comunidade do TON pareçam mais com um underground, o que também é legal.
Links
- Documentação oficial do TON:
- Repositório oficial do TON:
- Carteira oficial para diferentes plataformas:
- Repositório de contrato inteligente deste artigo:
- Link para o site do contrato inteligente:
- Repositório da extensão para Visual Studio Code para FunC:
- Chat sobre TON no Telegram, que ajudou muito a entender no início. Acho que não é um erro dizer que ali estão todos que escreveram algo para o TON. É possível também pedir gramas de teste lá.
- Mais um chat sobre TON onde encontrei informações úteis:
- Primeira fase do concurso:
- Segunda fase do concurso:
Fonte: habr.com
