Acerca de cómo escribir y publicar un contrato inteligente en TON
¿De qué trata este artículo?
En el artículo contaré cómo participé en el primer (de dos) concurso blockchain de Telegram, no obtuve ningún premio y decidí registrar mi experiencia en un artículo para que no caiga en el olvido y, tal vez, ayude. alguien.
Como no quería escribir código abstracto, sino hacer algo que funcionara, para el artículo escribí un contrato inteligente para una lotería instantánea y un sitio web que muestra datos de contratos inteligentes directamente desde TON sin usar almacenamiento intermedio.
El artículo será útil para aquellos que quieran realizar su primer contrato inteligente en TON, pero no saben por dónde empezar.
Usando la lotería como ejemplo, pasaré de instalar el entorno a publicar un contrato inteligente, interactuar con él y escribir un sitio web para recibir y publicar datos.
Sobre la participación en el concurso.
En octubre pasado, Telegram anunció un concurso blockchain con nuevos idiomas Fift и FunC. Era necesario elegir entre redactar cualquiera de los cinco contratos inteligentes propuestos. Pensé que sería bueno hacer algo diferente, aprender un idioma y hacer algo, incluso si no tengo que escribir nada más en el futuro. Además, el tema está constantemente en boca de todos.
Vale la pena decir que no tenía experiencia en el desarrollo de contratos inteligentes.
Planeé participar hasta el final hasta que pudiera y luego escribir un artículo de revisión, pero fallé de inmediato en el primero. I con firma múltiple activada FunC y en general funcionó. lo tomé como base .
En ese momento pensé que esto definitivamente era suficiente para llevarme al menos algún premio. Como resultado, alrededor de 40 de 60 participantes resultaron ganadores del premio y yo no estaba entre ellos. En general, esto no tiene nada de malo, pero una cosa me molestaba. Al momento del anuncio de los resultados no se había hecho la revisión de la prueba para mi contrato, pregunté a los participantes en el chat si había alguien más que no la tuviera, no había ninguno.
Aparentemente prestando atención a mis mensajes, dos días después los jueces publicaron un comentario y todavía no entiendo si accidentalmente se perdieron mi contrato inteligente durante la evaluación o simplemente pensaron que era tan malo que no necesitaba un comentario. Hice una pregunta en la página, pero no recibí respuesta. Aunque no es ningún secreto quién juzgó, consideré innecesario escribir mensajes personales.
Se dedicó mucho tiempo a comprenderlo, por lo que se decidió escribir un artículo. Dado que todavía no hay mucha información, este artículo ayudará a ahorrar tiempo a todos los interesados.
El concepto de contratos inteligentes en TON
Antes de escribir algo, debes averiguar desde qué lado abordarlo. Por eso, ahora te diré de qué partes consta el sistema. Más precisamente, qué partes necesita saber para redactar al menos algún tipo de contrato de trabajo.
Nos centraremos en redactar un contrato inteligente y trabajar con TON Virtual Machine (TVM), Fift и FunC, por lo que el artículo se parece más a una descripción del desarrollo de un programa normal. No nos detendremos aquí en cómo funciona la plataforma en sí.
En general sobre cómo funciona. TVM e idioma Fift Hay buena documentación oficial. Mientras participaba en el concurso y ahora mientras escribía el contrato actual, a menudo recurría a ella.
El idioma principal en el que se escriben los contratos inteligentes es FunC. No hay documentación al respecto en este momento, por lo que para escribir algo necesitas estudiar ejemplos de contratos inteligentes del repositorio oficial y la implementación del lenguaje en sí, además puedes ver ejemplos de contratos inteligentes de los dos últimos. competiciones. Enlaces al final del artículo.
Digamos que ya hemos escrito un contrato inteligente para FunC, después de eso compilamos el código en el ensamblador Fivet.
El contrato inteligente compilado aún no se ha publicado. Para hacer esto necesitas escribir una función en Fift, que tomará el código del contrato inteligente y algunos otros parámetros como entrada, y la salida será un archivo con la extensión .boc (que significa “bolsa de celdas”) y, dependiendo de cómo la escribamos, una clave privada y una dirección, que se genera en base al código del contrato inteligente. Ya puedes enviar gramos a la dirección de un contrato inteligente que aún no ha sido publicado.
Para publicar un contrato inteligente en TON recibido .boc el archivo deberá enviarse a la cadena de bloques mediante un cliente ligero (más sobre esto a continuación). Pero antes de publicar, debe transferir gramos a la dirección generada; de lo contrario, el contrato inteligente no se publicará. Después de la publicación, puede interactuar con el contrato inteligente enviándole mensajes desde el exterior (por ejemplo, utilizando un cliente ligero) o desde el interior (por ejemplo, un contrato inteligente envía a otro un mensaje dentro de TON).
Una vez que entendemos cómo se publica el código, resulta más fácil. Sabemos aproximadamente lo que queremos escribir y cómo funcionará nuestro programa. Y mientras escribimos, buscamos cómo esto ya está implementado en los contratos inteligentes existentes, o examinamos el código de implementación. Fift и FunC en el repositorio oficial, o busque en la documentación oficial.
Muy a menudo buscaba palabras clave en el chat de Telegram donde se reunían todos los participantes de la competencia y los empleados de Telegram, y sucedió que durante la competencia todos se reunieron allí y comenzaron a discutir sobre Fift y FunC. Enlace al final del artículo.
Es hora de pasar de la teoría a la práctica.
Preparando el entorno para trabajar con TON
Hice todo lo que se describe en el artículo sobre macOS y lo revisé dos veces en un entorno limpio. Ubuntu Ubuntu 18.04 LTS en Docker.
Lo primero que debes hacer es descargar e instalar. lite-client con el que podrás enviar solicitudes a TON.
Las instrucciones en el sitio web oficial describen el proceso de instalación de forma bastante completa y clara, aunque omiten algunos detalles. Aquí seguimos las instrucciones, instalando las dependencias faltantes según sea necesario. No compilé cada proyecto personalmente, sino que los instalé desde el repositorio oficial. Ubuntu (en MacOS usé 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 Una vez que todas las dependencias estén instaladas, puede instalar lite-client, Fift, FunC.
Primero, clonamos el repositorio TON junto con sus dependencias. Para mayor comodidad, haremos todo en una carpeta. ~/TON.
cd ~/TON
git clone https://github.com/ton-blockchain/ton.git
cd ./ton
git submodule update --init --recursiveEl repositorio también almacena implementaciones. Fift и FunC.
Ahora estamos listos para montar el proyecto. El código del repositorio se clona en una carpeta. ~/TON/ton. En ~/TON crear una carpeta build y recoger el proyecto en él.
mkdir ~/TON/build
cd ~/TON/build
cmake ../tonDado que vamos a escribir un contrato inteligente, no solo necesitamos lite-clientpero Fift с FunC, así que recopilemos todo. No es un proceso rápido, así que estamos esperando.
cmake --build . --target lite-client
cmake --build . --target fift
cmake --build . --target funcA continuación, descargue el archivo de configuración que contiene datos sobre el nodo al que lite-client se conectará.
wget https://test.ton.org/ton-lite-client-test1.config.jsonRealizando las primeras solicitudes a TON
Ahora lancemos lite-client.
cd ~/TON/build
./lite-client/lite-client -C ton-lite-client-test1.config.jsonSi la compilación fue exitosa, después del lanzamiento verá un registro de la conexión del cliente ligero al nodo.
[ 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)
...Puedes ejecutar el comando help y vea qué comandos están disponibles.
helpEnumeremos los comandos que usaremos en este artículo.
list of available commands:
last Get last block and state info from server
sendfile <filename> Load a serialized message from <filename> and send it to server
getaccount <addr> [<block-id-ext>] Loads the most recent state of specified account; <addr> is in [<workchain>:]<hex-or-base64-addr> format
runmethod <addr> [<block-id-ext>] <method-id> <params>... Runs GET method <method-id> of account <addr> with specified parameterslast получает последний созданный блок с сервера.
sendfile <filename> отправляет в TON файл с сообщением, именно с помощью этой команды публикуется смарт-контракт и запрсосы к нему.
getaccount <addr> загружает текущее состояние смарт-контракта с указанным адресом.
runmethod <addr> [<block-id-ext>] <method-id> <params> запускает get-методы смартконтракта. Ahora estamos listos para redactar el contrato en sí.
implementación
Idea
Como escribí anteriormente, el contrato inteligente que estamos redactando es una lotería.
Además, no se trata de una lotería en la que hay que comprar un billete y esperar una hora, un día o un mes, sino una lotería instantánea en la que el usuario se traslada a la dirección del contrato. N gramos, y lo recupera instantáneamente 2 * N gramos o pierde. Haremos que la probabilidad de ganar sea de aproximadamente el 40%. Si no hay gramos suficientes para el pago, consideraremos la transacción como una recarga.
Además, es importante que las apuestas se puedan ver en tiempo real y de forma cómoda, para que el usuario pueda entender inmediatamente si ganó o perdió. Por lo tanto, necesita crear un sitio web que muestre las apuestas y los resultados directamente desde TON.
Escribir un contrato inteligente
Para mayor comodidad, he resaltado el código de FunC; el complemento se puede encontrar e instalar en la búsqueda de Visual Studio Code; si de repente desea agregar algo, he hecho que el complemento esté disponible públicamente. Además, alguien creó previamente un complemento para trabajar con Fift, también puedes instalarlo y encontrarlo en VSC.
Creemos inmediatamente un repositorio donde enviaremos los resultados intermedios.
Para hacernos la vida más fácil, escribiremos un contrato inteligente y lo probaremos localmente hasta que esté listo. Sólo después de eso lo publicaremos en TON.
El contrato inteligente tiene dos métodos externos a los que se puede acceder. Primero, recv_external() esta función se ejecuta cuando una solicitud al contrato proviene del mundo exterior, es decir, no de TON, por ejemplo, cuando nosotros mismos generamos un mensaje y lo enviamos a través del cliente lite. Segundo, recv_internal() es entonces cuando, dentro de la propia TON, cualquier contrato se refiere al nuestro. En ambos casos, puede pasar parámetros a la función.
Comencemos con un ejemplo simple que funcionará si se publica, pero no contiene carga funcional.
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() recv_external(slice in_msg) impure {
;; TODO: implementation
}Aquí tenemos que explicar qué es. slice. Todos los datos almacenados en TON Blockchain son una colección TVM cell o simplemente cell, en una celda de este tipo se pueden almacenar hasta 1023 bits de datos y hasta 4 enlaces a otras celdas.
TVM cell slice o slice esto es parte del existente cell se utiliza para analizarlo, quedará claro más adelante. Lo principal para nosotros es que podemos transferir. slice y dependiendo del tipo de mensaje, procesar los datos en recv_external() o recv_internal().
impure – una palabra clave que indica que la función modifica los datos del contrato inteligente.
Guardemos el código del contrato en lottery-code.fc y compilar.
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc El significado de las banderas se puede ver usando el comando
~/TON/build/crypto/func -helpHemos compilado el código ensamblador de Fivet en 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>cSe puede lanzar localmente, para ello prepararemos el entorno.
Tenga en cuenta que la primera línea conecta Asm.fif, este es un código escrito en Fift para el ensamblador de Fift.
Como queremos ejecutar y probar el contrato inteligente localmente, crearemos un archivo lottery-test-suite.fif y copie el código compilado allí, reemplazando la última línea, que escribe el código del contrato inteligente en una constante codepara luego transferirlo a la 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
Hasta aquí parece claro, ahora agreguemos al mismo archivo el código que usaremos para iniciar TVM.
0 tuple 0x076ef1ea , // magic
0 , 0 , // actions msg_sents
1570998536 , // unix_time
1 , 1 , 3 , // block_lt, trans_lt, rand_seed
0 tuple 100000000000000 , dictnew , , // remaining balance
0 , dictnew , // contract_address, global_config
1 tuple // wrap to another tuple
constant c7
0 constant recv_internal // to run recv_internal()
-1 constant recv_external // to invoke recv_external()В c7 registramos el contexto, es decir, los datos con los que se lanzará el TVM (o estado de la red). Incluso durante la competencia, uno de los desarrolladores mostró cómo crear c7 y lo copié. En este artículo es posible que necesitemos cambiar rand_seed ya que de ello depende la generación de un número aleatorio y si no se cambia, se devolverá el mismo número cada vez.
recv_internal и recv_external Las constantes con valores 0 y -1 serán las encargadas de llamar a las funciones correspondientes en el contrato inteligente.
Ahora estamos listos para crear la primera prueba para nuestro contrato inteligente vacío. Para mayor claridad, por ahora agregaremos todas las pruebas al mismo archivo. lottery-test-suite.fif.
Creemos una variable storage y escribe uno vacío en él cell, este será el almacenamiento de contratos inteligentes.
message Este es el mensaje que transmitiremos al contacto inteligente desde el exterior. También lo dejaremos vacío por ahora.
variable storage
<b b> storage !
variable message
<b b> message ! Después de haber preparado las constantes y variables, lanzamos TVM usando el comando runvmctx y pasar los parámetros creados a la entrada.
message @
recv_external
code
storage @
c7
runvmctx Al final lo lograremos código intermedio para Fift.
Ahora podemos ejecutar el código resultante.
export FIFTPATH=~/TON/ton/crypto/fift/lib // выполняем один раз для удобства
~/TON/build/crypto/fift -s lottery-test-suite.fif El programa debería ejecutarse sin errores y en el resultado veremos el registro de ejecución:
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=0Genial, hemos escrito la primera versión funcional del contrato inteligente.
Ahora necesitamos agregar funcionalidad. Primero, abordemos los mensajes que vienen del mundo exterior a recv_external()
El propio desarrollador elige el formato de mensaje que puede aceptar el contrato.
Pero usualmente
- En primer lugar, queremos proteger nuestro contrato del mundo exterior y hacer que solo el propietario del contrato pueda enviarle mensajes externos.
- en segundo lugar, cuando enviamos un mensaje válido a TON, queremos que esto suceda exactamente una vez y cuando enviamos el mismo mensaje nuevamente, el contrato inteligente lo rechaza.
Entonces, casi todos los contratos resuelven estos dos problemas, dado que nuestro contrato acepta mensajes externos, debemos encargarnos de eso también.
Lo haremos en orden inverso. Primero, solucionemos el problema de la repetición: si el contrato ya recibió dicho mensaje y lo procesó, no lo ejecutará por segunda vez. Y luego resolveremos el problema para que solo un determinado círculo de personas pueda enviar mensajes al contrato inteligente.
Existen diferentes formas de solucionar el problema de los mensajes duplicados. Así es como lo haremos. En el contrato inteligente, inicializamos el contador de mensajes recibidos con el valor inicial 0. En cada mensaje del contrato inteligente, agregaremos el valor del contador actual. Si el valor del contador en el mensaje no coincide con el valor en el contrato inteligente, entonces no lo procesamos; si coincide, lo procesamos y aumentamos el contador en el contrato inteligente en 1.
volvamos a lottery-test-suite.fif y agregarle una segunda prueba. Si enviamos un número incorrecto, el código debería generar una excepción. Por ejemplo, deje que los datos del contrato almacenen 166 y enviaremos 165.
<b 166 32 u, b> storage !
<b 165 32 u, b> message !
message @
recv_external
code
storage @
c7
runvmctx
drop
exit_code !
."Exit code " exit_code @ . cr
exit_code @ 33 - abort"Test #2 Not passed"Lancemos.
~/TON/build/crypto/fift -s lottery-test-suite.fif Y veremos que el test se ejecuta con error.
[ 1][t 0][1582283084.210902214][words.cpp:3046] lottery-test-suite.fif:67: abort": Test #2 Not passed
[ 1][t 0][1582283084.210941076][fift-main.cpp:196] Error interpreting file `lottery-test-suite.fif`: error interpreting included file `lottery-test-suite.fif` : lottery-test-suite.fif:67: abort": Test #2 Not passedEn esta etapa lottery-test-suite.fif debería verse como .
Ahora agreguemos la contralógica al contrato inteligente en lottery-code.fc.
() recv_internal(slice in_msg) impure {
;; TODO: implementation
}
() 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);
}В slice in_msg Miente el mensaje que enviamos.
Lo primero que hacemos es comprobar si el mensaje contiene datos, si no es así simplemente salimos.
A continuación analizamos el mensaje. in_msg~load_uint(32) carga el número 165, 32 bits unsigned int del mensaje transmitido.
A continuación cargamos 32 bits del almacenamiento del contrato inteligente. Comprobamos que el número cargado coincide con el pasado; si no, lanzamos una excepción. En nuestro caso, dado que estamos pasando una no coincidencia, se debe lanzar una excepción.
Ahora vamos a compilar.
~/TON/build/crypto/func -APSR -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fc Copie el código resultante a lottery-test-suite.fif, sin olvidar reemplazar la última línea.
Comprobamos que pasa la prueba:
~/TON/build/crypto/fift -s lottery-test-suite.fifPuede ver el compromiso correspondiente con los resultados actuales.
Tenga en cuenta que es inconveniente copiar constantemente el código compilado de un contrato inteligente en un archivo con pruebas, por lo que escribiremos un script que escribirá el código en una constante para nosotros y simplemente conectaremos el código compilado a nuestras pruebas usando "include".
Crear un archivo en la carpeta del proyecto. build.sh con el siguiente contenido.
#!/bin/bash
~/TON/build/crypto/func -SPA -R -o lottery-compiled.fif ~/TON/ton/crypto/smartcont/stdlib.fc ./lottery-code.fcHagámoslo ejecutable.
chmod +x ./build.shAhora, simplemente ejecute nuestro script para compilar el contrato. Pero además de esto, necesitamos escribirlo en una constante code. Entonces crearemos un nuevo archivo. lotter-compiled-for-test.fif, que incluiremos en el archivo lottery-test-suite.fif.
Agreguemos código de falda a sh, que simplemente duplicará el archivo compilado en lotter-compiled-for-test.fif y cambie la última línea en él.
# 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.fifAhora, para comprobarlo, ejecutemos el script resultante y se generará un archivo. lottery-compiled-for-test.fif, que incluiremos en nuestro lottery-test-suite.fif
В lottery-test-suite.fif elimine el código del contrato y agregue la línea "lottery-compiled-for-test.fif" include.
Realizamos pruebas para comprobar que pasan.
~/TON/build/crypto/fift -s lottery-test-suite.fifGenial, ahora para automatizar el lanzamiento de pruebas, creemos un archivo. test.sh, que se ejecutará primero build.shy luego ejecute las pruebas.
touch test.sh
chmod +x test.shescribimos dentro
./build.sh
echo "nCompilation completedn"
export FIFTPATH=~/TON/ton/crypto/fift/lib
~/TON/build/crypto/fift -s lottery-test-suite.fifHará test.sh y ejecútelo para asegurarse de que las pruebas funcionen.
chmod +x ./test.sh
./test.shComprobamos que el contrato se compila y las pruebas se ejecutan.
Genial, ahora en inicio test.sh Las pruebas se compilarán y ejecutarán inmediatamente. Aquí está el enlace a .
Bien, antes de continuar, hagamos una cosa más por conveniencia.
Creemos una carpeta build donde almacenaremos el contrato copiado y su clon escrito en una constante lottery-compiled.fif, lottery-compiled-for-test.fif. Creemos también una carpeta. test ¿Dónde se almacenará el archivo de prueba? lottery-test-suite.fif y potencialmente otros archivos de soporte. .
Sigamos desarrollando el contrato inteligente.
A continuación debería hacer una prueba que compruebe que se recibe el mensaje y se actualiza el contador en la tienda cuando enviamos el número correcto. Pero lo haremos más tarde.
Ahora pensemos en qué estructura de datos y qué datos deben almacenarse en el contrato inteligente.
Describiré todo lo que almacenamos.
`seqno` 32-х битное целое положительное число счетчик.
`pubkey` 256-ти битное целое положительное число публичный ключ, с помощью которого, мы будем проверять подпись отправленного извне сообщения, о чем ниже.
`order_seqno` 32-х битное целое положительное число хранит счетчик количества ставок.
`number_of_wins` 32-х битное целое положительное число хранит количество побед.
`incoming_amount` тип данных Gram (первые 4 бита отвечает за длину), хранит общее количество грамов, которые были отправлены на контртакт.
`outgoing_amount` общее количество грамов, которое было отправлено победителям.
`owner_wc` номер воркчейна, 32-х битное (в некоторых местах написано, что 8-ми битное) целое число. В данный момент всего два -1 и 0.
`owner_account_id` 256-ти битное целое положительное число, адрес контракта в текущем воркчейне.
`orders` переменная типа словарь, хранит последние двадцать ставок. A continuación necesitas escribir dos funciones. Llamemos al primero pack_state(), que empaquetará los datos para guardarlos posteriormente en el almacenamiento del contrato inteligente. Llamemos al segundo unpack_state() leerá y devolverá datos del almacenamiento.
_ 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;
}Agregamos estas dos funciones al comienzo del contrato inteligente. Funcionará resultado intermedio.
Para guardar datos deberá llamar a la función incorporada set_data() y escribirá datos de pack_state() en el almacenamiento de contratos inteligentes.
cell packed_state = pack_state(arg_1, .., arg_n);
set_data(packed_state);Ahora que tenemos funciones convenientes para escribir y leer datos, podemos seguir adelante.
Necesitamos verificar que el mensaje entrante desde el exterior esté firmado por el propietario del contrato (u otro usuario que tenga acceso a la clave privada).
Cuando publicamos un contrato inteligente, podemos inicializarlo con los datos que necesitamos almacenados, que se guardarán para uso futuro. Allí registraremos la clave pública para que podamos verificar que el mensaje entrante fue firmado con la clave privada correspondiente.
Antes de continuar, creemos una clave privada y escribámosla en test/keys/owner.pk. Para hacer esto, iniciemos Fift en modo interactivo y ejecutemos cuatro comandos.
`newkeypair` генерация публичного и приватного ключа и запись их в стек.
`drop` удаления из стека верхнего элемента (в данном случае публичный ключ)
`.s` просто посмотреть что лежит в стеке в данный момент
`"owner.pk" B>file` запись приватного ключа в файл с именем `owner.pk`.
`bye` завершает работу с Fift. Creemos una carpeta keys dentro de la carpeta test y escribe la clave privada allí.
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
byeVemos un archivo en la carpeta actual. owner.pk.
Eliminamos la clave pública de la pila y cuando sea necesario podemos obtenerla de la privada.
Ahora necesitamos escribir una verificación de firma. Empecemos con la prueba. Primero leemos la clave privada del archivo usando la función file>B y escribirlo en una variable owner_private_key, luego usando la función priv>pub convierta la clave privada en una clave pública y escriba el resultado en 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 !Necesitaremos ambas llaves.
Inicializamos el almacenamiento del contrato inteligente con datos arbitrarios en la misma secuencia que en la función pack_state()y escribirlo en una variable storage.
variable owner_private_key
variable owner_public_key
variable orders
variable owner_wc
variable owner_account_id
"./keys/owner.pk" file>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, 0 32 u, 0 32 u, 0 Gram, 0 Gram, owner_wc @ 32 i, owner_account_id @ 256 u, orders @ dict, b> storage !A continuación, redactaremos un mensaje firmado, este solo contendrá la firma y el valor del contador.
Primero creamos los datos que queremos transmitir, luego los firmamos con una clave privada y finalmente generamos un mensaje firmado.
variable message_to_sign
variable message_to_send
variable signature
<b 0 32 u, b> message_to_sign !
message_to_sign @ hashu owner_private_key @ ed25519_sign_uint signature !
<b signature @ B, 0 32 u, b> <s message_to_send ! Como resultado, el mensaje que enviaremos al contrato inteligente queda registrado en una variable message_to_send, sobre funciones hashu, ed25519_sign_uint puedes leer .
Y para ejecutar la prueba volvemos a llamar.
message_to_send @
recv_external
code
storage @
c7
runvmctxEl archivo con las pruebas debería verse así en esta etapa.
Ejecutemos la prueba y fallará, por lo que cambiaremos el contrato inteligente para que pueda recibir mensajes de este formato y verificar la firma.
Primero, contamos 512 bits de la firma del mensaje y los escribimos en una variable, luego contamos 32 bits de la variable del contador.
Como tenemos una función para leer datos del almacenamiento de contratos inteligentes, la usaremos.
Lo siguiente es verificar el contador transferido con el almacenamiento y verificar la firma. Si algo no coincide, lanzamos una excepción con el código apropiado.
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));compromiso relevante .
Ejecutemos las pruebas y veamos que la segunda prueba falla. Por dos razones: no hay suficientes bits en el mensaje y no hay suficientes bits en el almacenamiento, por lo que el código falla durante el análisis. Necesitamos agregar una firma al mensaje que enviamos y copiar el almacenamiento de la última prueba.
En la segunda prueba, agregaremos una firma de mensaje y cambiaremos el almacenamiento del contrato inteligente. El archivo con las pruebas se ve como en este momento.
Escribamos una cuarta prueba, en la que enviaremos un mensaje firmado con la clave privada de otra persona. Creemos otra clave privada y guárdela en un archivo. not-owner.pk. Firmaremos el mensaje con esta clave privada. Ejecutemos las pruebas y asegurémonos de que todas pasen. por el momento
Ahora finalmente podemos pasar a implementar la lógica del contrato inteligente.
В recv_external() Aceptaremos dos tipos de mensajes.
Dado que nuestro contrato acumulará las pérdidas de los jugadores, este dinero debe transferirse al creador de la lotería. La dirección de la billetera del creador de la lotería se registra en el almacenamiento cuando se crea el contrato.
Por si acaso, necesitamos la posibilidad de cambiar la dirección a la que enviamos gramos de los perdedores. También deberíamos poder enviar gramos de la lotería a la dirección del propietario.
Empecemos por el primero. Primero escribamos una prueba que verifique que después de enviar el mensaje, el contrato inteligente guardó la nueva dirección en el almacenamiento. Tenga en cuenta que en el mensaje, además del mostrador y la nueva dirección, también transmitimos action Un número entero no negativo de 7 bits, dependiendo de él elegiremos cómo procesar el mensaje en el contrato inteligente.
<b 0 32 u, 1 @ 7 u, new_owner_wc @ 32 i, new_owner_account_id @ 256 u, b> message_to_sign !En la prueba puedes ver cómo se deserializa el almacenamiento de contratos inteligentes storage en cinco. La deserialización de variables se describe en la documentación de Fift.
con masa añadida.
Ejecutemos la prueba y asegurémonos de que falla. Ahora agreguemos lógica para cambiar la dirección del dueño de la lotería.
En el contrato inteligente continuamos analizando message, leer en action. Os recordamos que tendremos dos action: cambiar dirección y enviar gramos.
Luego leemos la nueva dirección del propietario del contrato y la guardamos.
Ejecutamos las pruebas y vemos que la tercera prueba falla. Se bloquea debido a que el contrato ahora analiza adicionalmente 7 bits del mensaje que faltan en la prueba. Añade uno inexistente al mensaje. action. Hagamos las pruebas y veamos que todo pasa. comprometerse con los cambios. Excelente.
Ahora escribamos la lógica para enviar la cantidad especificada de gramos a la dirección previamente guardada.
Primero, escribamos una prueba. Escribiremos dos pruebas, una cuando no haya suficiente saldo y la segunda cuando todo debería pasar con éxito. Se pueden ver las pruebas .
Ahora agreguemos el código. Primero, escribamos dos métodos auxiliares. El primer método de obtención consiste en averiguar el saldo actual de un contrato inteligente.
int balance() inline_ref method_id {
return get_balance().pair_first();
}Y el segundo es para enviar gramos a otro contrato inteligente. Copié completamente este método de otro 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 <= format indicator int_msg_info$0
;; .store_uint(1, 1) ;; 1 <= ihr disabled
;; .store_uint(1, 1) ;; 1 <= bounce = true
;; .store_uint(0, 1) ;; 0 <= bounced = false
;; .store_uint(4, 5) ;; 00100 <= address flags, anycast = false, 8-bit workchain
.store_uint (196, 9)
.store_int(wc, 8)
.store_uint(addr, 256)
.store_grams(grams)
.store_uint(0, 107) ;; 106 zeroes + 0 as an indicator that there is no cell with the data.
.end_cell();
send_raw_message(msg, 3); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value
}Agreguemos estos dos métodos al contrato inteligente y escribamos la lógica. Primero, analizamos la cantidad de gramos del mensaje. A continuación comprobamos el saldo, si no es suficiente lanzamos una excepción. Si todo está bien, enviamos los gramos a la dirección guardada y actualizamos el 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));Parece el contrato inteligente en este momento. Ejecutemos las pruebas y asegurémonos de que pasen.
Por cierto, cada vez que se procesa un mensaje se deduce una comisión del contrato inteligente. Para que los mensajes del contrato inteligente ejecuten la solicitud, después de las comprobaciones básicas, debe llamar accept_message().
Ahora pasemos a los mensajes internos. De hecho, solo aceptaremos gramos y devolveremos el doble de la cantidad al jugador si gana y un tercio al propietario si pierde.
Primero, escribamos una prueba simple. Para hacer esto, necesitamos una dirección de prueba del contrato inteligente desde la cual supuestamente enviamos gramos al contrato inteligente.
La dirección del contrato inteligente consta de dos números, un número entero de 32 bits responsable de la cadena de trabajo y un número de cuenta único entero no negativo de 256 bits en esta cadena de trabajo. Por ejemplo, -1 y 12345, esta es la dirección que guardaremos en un archivo.
Copié la función para guardar la dirección 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-addressVeamos cómo funciona la función, esto le permitirá comprender cómo funciona Fift. Inicie Fivet en modo interactivo.
~/TON/build/crypto/fift -i Primero insertamos -1, 12345 y el nombre del futuro archivo "sender.addr" en la pila:
-1 12345 "sender.addr" El siguiente paso es ejecutar la función. -rot, que desplaza la pila de tal manera que en la parte superior de la pila hay un número de contrato inteligente único:
"sender.addr" -1 12345256 u>B convierte un entero no negativo de 256 bits en bytes.
"sender.addr" -1 BYTES:0000000000000000000000000000000000000000000000000000000000003039swap intercambia los dos elementos superiores de la pila.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 -132 i>B convierte un entero de 32 bits a bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039 BYTES:FFFFFFFFB+ conecta dos secuencias de bytes.
"sender.addr" BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFFDe nuevo swap.
BYTES:0000000000000000000000000000000000000000000000000000000000003039FFFFFFFF "sender.addr" Y finalmente los bytes se escriben en el archivo. B>file. Después de esto nuestra pila está vacía. Nosotros paramos Fift. Se ha creado un archivo en la carpeta actual. sender.addr. Movamos el archivo a la carpeta creada. test/addresses/.
Escribamos una prueba simple que envíe gramos a un contrato inteligente. .
Ahora veamos la lógica de la lotería.
Lo primero que hacemos es comprobar el mensaje. bounced o no si bounced, entonces lo ignoramos. bounced significa que el contrato devolverá gramos si ocurre algún error. No devolveremos gramos si de repente ocurre un error.
Comprobamos, si el saldo es inferior a medio gramo, simplemente aceptamos el mensaje y lo ignoramos.
A continuación, analizamos la dirección del contrato inteligente del que procede el mensaje.
Leemos los datos del almacenamiento y luego eliminamos las apuestas antiguas del historial si hay más de veinte. Por conveniencia, escribí tres funciones adicionales. pack_order(), unpack_order(), remove_old_orders().
A continuación, miramos si el saldo no es suficiente para el pago, entonces consideramos que esto no es una apuesta, sino una reposición y guardamos la reposición en orders.
Finalmente, la esencia del contrato inteligente.
Primero, si el jugador pierde, lo guardamos en el historial de apuestas y si la cantidad es superior a 3 gramos, enviamos 1/3 al propietario del contrato inteligente.
Si el jugador gana, enviamos el doble de la cantidad a la dirección del jugador y luego guardamos la información sobre la apuesta en el historial.
() 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) { ;; ignore bounced
return ();
}
if (order_amount < 500000000) { ;; just receive grams without changing state
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() < 2 * order_amount + 500000000) { ;; not enough grams to pay the bet back, so this is re-fill
builder order = pack_order(order_seqno, 1, 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));
return ();
}
if (rand(10) >= 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));
}Eso es todo. .
Ahora todo lo que queda es simple, creemos métodos de obtención para que podamos obtener información sobre el estado del contrato del mundo exterior (de hecho, leer los datos de su almacenamiento de contrato inteligente).
. Escribiremos a continuación sobre cómo recibir información sobre un contrato inteligente.
También olvidé agregar el código que procesará la primera solicitud que ocurre al publicar un contrato inteligente. . Y además error al enviar 1/3 del monto a la cuenta del propietario.
El siguiente paso es publicar el contrato inteligente. Creemos una carpeta requests.
Tomé como base el código de publicación. который en el repositorio oficial.
Algo a lo que vale la pena prestarle atención. Generamos un almacenamiento de contrato inteligente y un mensaje de entrada. Después de esto, se genera la dirección del contrato inteligente, es decir, la dirección se conoce incluso antes de la publicación en TON. A continuación, debe enviar varios gramos a esta dirección, y solo después de eso debe enviar un archivo con el contrato inteligente en sí, ya que la red cobra una comisión por almacenar el contrato inteligente y las operaciones en él (validadores que almacenan y ejecutan el contrato inteligente). contratos). .
A continuación ejecutamos el código de publicación y obtenemos lottery-query.boc archivo y dirección del contrato inteligente.
~/TON/build/crypto/fift -s requests/new-lottery.fif 0No olvides guardar los archivos generados: lottery-query.boc, lottery.addr, lottery.pk.
Entre otras cosas, veremos la dirección del contrato inteligente en los registros de ejecución.
new wallet address = 0:044910149dbeaf8eadbb2b28722e7d6a2dc6e264ec2f1d9bebd6fb209079bc2a
(Saving address to file lottery.addr)
Non-bounceable address (for init): 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd
Bounceable address (for later access): kQAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KpFYSólo por diversión, hagamos una solicitud a TON.
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydY veremos que la cuenta con esta dirección está vacía.
account state is emptyenviamos a la dirección 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd 2 Gram y después de unos segundos ejecutamos el mismo comando. Para enviar gramos uso , y puedes pedirle a alguien del chat gramos de prueba, de los que hablaré al final del artículo.
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydParece un no inicializado (state:account_uninit) un contrato inteligente con la misma dirección y un saldo de 1 de nanogramos.
account state is (account
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_}
last transaction lt = 3825478000001 hash = B043616AE016682699477FFF01E6E903878CDFD6846042BA1BFC64775E7AC6C4
account balance is 2000000000ngAhora publiquemos el contrato inteligente. Iniciemos lite-client y ejecutemos.
> sendfile lottery-query.boc
[ 1][t 2][1583008371.631410122][lite-client.cpp:966][!testnode] sending query from file lottery-query.boc
[ 3][t 1][1583008371.828550100][lite-client.cpp:976][!query] external message status is 1 Comprobemos que el contrato ha sido publicado.
> last
> getaccount 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8KsydEntre otras cosas conseguimos.
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 eso account_active.
Compromiso correspondiente con cambios .
Ahora creemos solicitudes para interactuar con el contrato inteligente.
Más precisamente, dejaremos el primero de cambio de dirección como un trabajo independiente, y el segundo lo haremos de enviar gramos a la dirección del propietario. De hecho, tendremos que hacer lo mismo que en la prueba de envío de gramos.
Este es el mensaje que enviaremos al contrato inteligente, donde msg_seqno 165, action 2 y 9.5 gramos para envío.
<b 165 32 u, 2 7 u, 9500000000 Gram, b>No olvides firmar el mensaje con tu clave privada. lottery.pk, que se generó anteriormente al crear el contrato inteligente. .
Recibir información de un contrato inteligente utilizando métodos de obtención
Ahora veamos cómo ejecutar métodos de obtención de contratos inteligentes.
Lanzamos lite-client y ejecute los métodos get que escribimos.
$ ./lite-client/lite-client -C ton-lite-client-test1.config.json
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd balance
arguments: [ 104128 ]
result: [ 64633878952 ]
...В result contiene el valor que devuelve la función balance() de nuestro contrato inteligente.
Haremos lo mismo con varios métodos más.
> runmethod 0QAESRAUnb6vjq27KyhyLn1qLcbiZOwvHZvr1vsgkHm8Ksyd get_seqno
...
arguments: [ 77871 ]
result: [ 1 ] Preguntemos por tu historial de apuestas.
> 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]) ] Usaremos lite-client y obtendremos métodos para mostrar información sobre el contrato inteligente en el sitio.
Mostrar datos de contratos inteligentes en el sitio web
Escribí un sitio web sencillo en Python para mostrar los datos del contrato inteligente de una manera conveniente. Aquí no me detendré en ello en detalle y publicaré el sitio. .
Las solicitudes a TON se realizan desde Python a través de lite-client. Para mayor comodidad, el sitio está empaquetado en Docker y publicado en Google Cloud. .
Difícil
Ahora intentemos enviar gramos allí para reponerlos desde . Enviaremos 40 gramos. Y hagamos un par de apuestas para mayor claridad. Vemos que el sitio muestra el historial de apuestas, el porcentaje de ganancias actual y otra información útil.
que ganamos el primero, perdimos el segundo.
Epílogo
El artículo resultó ser mucho más largo de lo que esperaba, tal vez podría haber sido más corto, o tal vez solo para una persona que no sabe nada sobre TON y quiere escribir y publicar un contrato inteligente no tan simple con la capacidad de interactuar con él. Quizás algunas cosas podrían haberse explicado de forma más sencilla.
Quizás algunos aspectos de la implementación se podrían haber realizado de manera más eficiente y elegante, pero entonces habría llevado aún más tiempo preparar el artículo. También es posible que haya cometido un error en alguna parte o que no haya entendido algo, por lo que si está haciendo algo serio, debe confiar en la documentación oficial o en el repositorio oficial con el código TON.
Cabe señalar que, dado que TON en sí aún se encuentra en la etapa activa de desarrollo, pueden ocurrir cambios que interrumpan cualquiera de los pasos de este artículo (lo que sucedió mientras escribía, ya ha sido corregido), pero el enfoque general es es poco probable que cambie.
No hablaré sobre el futuro de TON. Quizás la plataforma se convierta en algo grande y deberíamos dedicar tiempo a estudiarla y llenar un nicho con nuestros productos ahora.
También está Libra de Facebook, que tiene una audiencia potencial de usuarios mayor que TON. No sé casi nada sobre Libra, a juzgar por el foro hay mucha más actividad allí que en la comunidad TON. Aunque los desarrolladores y la comunidad de TON son más clandestinos, lo cual también es genial.
referencias
- Documentación oficial de TON:
- Repositorio oficial de TON:
- Billetera oficial para diferentes plataformas:
- Repositorio de contratos inteligentes de este artículo:
- Enlace al sitio web del contrato inteligente:
- Repositorio de la extensión de Visual Studio Code para FunC:
- Charle sobre TON en Telegram, lo que realmente ayudó a resolverlo en la etapa inicial. Creo que no será un error si digo que todos los que escribieron algo para TON están ahí. También puedes solicitar gramos de prueba allí.
- Otro chat sobre TON en el que encontré información útil:
- Primera etapa del concurso:
- Segunda etapa del concurso:
Fuente: habr.com
