Telegram Open Network (TON): Рекомендации по смарт-контрактам

На этой странице собраны рекомендации и информация полученная на основе опыта, которым можно следовать при разработке новых смарт-контрактов для блокчейна TON.

Данная статья является переводом официальной документации размещенной здесь – https://github.com/ton-blockchain/docs/blob/master/docs/howto/smart-contract-guidelines.md

Внутренние сообщения (Internal messages)

Смарт-контракты взаимодействуют друг с другом, отправляя так называемые внутренние сообщения (internal messages).

Когда внутреннее сообщение достигает получателя, то от имени учетной записи получателя создается обычная транзакция, и внутреннее сообщение обрабатывается в соответствии с логикой кода смарт-контракта получателя.

В частности, транзакция обработки может создавать одно или несколько исходящих внутренних сообщений, часть из которых может быть адресована на исходный адрес обрабатываемого внутреннего сообщения. Это можно использовать для создания простых «клиент-серверных приложений», когда запрос инкапсулируется во внутреннее сообщение и отправляется другому смарт-контракту, который обрабатывает запрос и отправляет ответ, опять же как внутреннее сообщение.

Такой подход приводит к необходимости различать, предназначено ли внутреннее сообщение как «запрос» или как «ответ», или не требует дополнительной обработки (например, «простой денежный перевод»). Кроме того, при получении ответа должны быть средства для понимания того, какому запросу он соответствует.

Для достижения этой цели можно использовать следующий рекомендуемый внутренний шаблон сообщения (обратите внимание, что блокчейн TON не налагает никаких ограничений на тело сообщения, так что это действительно просто рекомендации):

  1. Тело сообщения (body) может быть встроено в само сообщение или храниться в отдельной ячейке, на которую ссылается сообщение, на что указывает следующий фрагмент схемы TL-B:
    message$_ {X:Type} ... body:(Either X ^X) = Message X;

    Принимающий смарт-контракт должен принимать как минимум внутренние сообщения со встроенным body (когда он помещает сообщение в ячейку). Если он принимает body в отдельные ячейки (используя правильный конструктор (Either X ^X)),  то обработка входящего сообщения не должна зависеть от конкретного параметра встраивания, выбранного для body. С другой стороны, вполне допустимо вообще не поддерживать body в отдельных ячейках для более простых запросов и ответов.

  2. Тело сообщения (body) обычно начинается со следующих полей:
    • 32-битное (с обратным порядком байтов) целое число без знака, определяющее операцию, которая должна быть выполнена, или метод вызываемого смарт-контракта.
    • 64-битное (с обратным порядком байтов) целое число без знака query_id, используемое во всех внутренних сообщениях типа «запрос-ответ», чтобы указать, что ответ связан с запросом (query_id ответа должен быть равен query_id соответствующего запроса). Если op не является методом запроса-ответа (например, он вызывает метод, от которого не ожидается отправка ответа), тогда query_id может быть опущен.
    • Остальная часть тела сообщения специфична для каждого поддерживаемого значения op.
  3. Если op = 0, то сообщение представляет собой «простое сообщение о передаче с комментарием». Комментарий содержится в остальной части тела сообщения (без поля query_id, т. е. начиная с пятого байта).
    1. Если он не начинается с байта 0xff, комментарий является текстовым; он может отображаться «как есть» для конечного пользователя кошелька (после фильтрации недопустимых и управляющих символов и проверки правильности строки UTF-8). Например, в этом текстовом поле пользователи могут указать цель простого перевода со своего кошелька на кошелек другого пользователя.
    2. Если комментарий начинается с байта 0xff, остаток представляет собой «двоичный комментарий», который не должен отображаться для конечного пользователя в виде текста (только в виде шестнадцатеричного дампа, если это необходимо). Предполагаемое использование «бинарных комментариев» состоит, например, в том, чтобы содержать идентификатор покупки для платежей в магазине, который будет автоматически генерироваться и обрабатываться программным обеспечением магазина.Большинство смарт-контрактов не должны выполнять нетривиальные действия или отклонять входящее сообщение при получении «простого сообщения о передаче». Таким образом, как только op окажется равным нулю, функция смарт-контракта для обработки входящих внутренних сообщений (обычно называемая recv_internal()) должна немедленно завершиться с нулевым кодом выхода, указывающим на успех (например, выбрасывая исключение 0, если нет пользовательского исключения обработчик был установлен смарт-контрактом). Это приведет к тому, что принимающей учетной записи будет зачислено значение, переданное сообщением, без каких-либо дальнейших последствий.
  4. «Простое сообщение о передаче без комментариев» имеет пустое тело (даже без поля операции op). Приведенные выше соображения применимы и к таким сообщениям. Обратите внимание, что такие сообщения должны быть встроены в ячейку сообщения.
  5. Мы ожидаем, что сообщение-запрос будут иметь операцию со сброшенным старшим битом, т. е. в диапазоне 1 .. 2^31-1, а сообщение-ответ будут иметь операцию с установленным старшим битом, т. е. , в диапазоне 2^31 .. 2^32-1. Если метод не является ни запросом, ни ответом (чтобы соответствующее тело сообщения не содержало поля query_id), он должен использовать операцию в диапазоне «запрос» 1 .. 2^31 – 1.

    Выражение 2^31 означает 2 в степени 31, то есть 2 умноженное на само себя 31 раз. Это равно 2147483648. Следовательно, выражение 1 .. 2^31-1 означает диапазон чисел от 1 до 2147483647.
    2^31 .. 2^32-1 означает диапазон чисел от 2^31 до (2^32)-1. Это соответствует диапазону беззнаковых 32-битных целых чисел, которые могут быть представлены в двоичной системе счисления. Диапазон содержит 2,147,483,648 различных значений, начиная с 2,147,483,648 и заканчивая 4,294,967,295.

  6. Есть несколько «стандартных» ответных сообщений с op=0xffffffff и op=0xfffffffe. Как правило, значения op от 0xffffffff0 до 0xffffffff зарезервированы для таких стандартных ответов.
    • op = 0xffffffff означает, что операция не поддерживается. За ним следует 64-битный query_id, извлеченный из исходного запроса, и 32-битная операция исходного запроса. Все смарт-контракты, кроме простейших, должны возвращать эту ошибку при получении запроса с неизвестной операцией в диапазоне 1 .. 2^31-1.
    • op = 0xfffffffe означает, что операция запрещена. За ним следует 64-битный query_id исходного запроса, за которым следует 32-битная операция, извлеченная из исходного запроса.

      Обратите внимание, что неизвестные «ответы» (с операцией в диапазоне 2^31 .. 2^32-1) следует игнорировать (в частности, в ответ на них не следует генерировать ответ с операцией, равной 0xffffffff), как и неожиданные возвраты сообщений (с установленным флагом “bounced”).

Оплата при обработке запросов и отправки ответов

Если смарт-контракт хочет отправить запрос другому смарт-контракту, он должен заплатить:

  • за отправку внутреннего сообщения в смарт-контракте назначения (плата за пересылку сообщения),
  • за обработку этого сообщения в пункте назначения (плата за газ),
  • и за отправку ответа, если требуется (плата за пересылку сообщения).

В большинстве случаев отправитель прикрепляет небольшое количество монет TON (например, одну монету TON) к внутреннему сообщению (достаточное для оплаты обработки этого сообщения) и устанавливает флаг «отказа» (т. е. внутреннее сообщение); получатель вернет неиспользованную часть полученного значения с ответом (вычитая из него плату за пересылку сообщения). Обычно это достигается вызовом SENDRAWMSG с mode = 64 (см. Приложение A документации TON VM).

Если получатель не может проанализировать полученное сообщение и завершает работу с ненулевым кодом выхода (например, из-за необработанного исключения десериализации ячейки), сообщение будет автоматически «возвращено» обратно отправителю с очкисткой флага «bounce» и установкой флага «bounced». Тело возвращенного сообщения будет таким же, как и у исходного сообщения; поэтому важно проверять флаг «bounced» входящих внутренних сообщений перед разбором поля op в смарт-контракте и обработкой соответствующего запроса (иначе есть риск, что запрос, содержащийся в возвращенном сообщении, будет обработан его исходным отправитель как новый отдельный запрос). Если установлен флаг «bounced», специальный код может определить, какой запрос не удался (например, путем десериализации op и query_id из возвращенного сообщения) и предпринять соответствующие действия. Более простой смарт-контракт может просто игнорировать все возвращенные сообщения (завершаться с нулевым кодом выхода, если установлен флаг «bounced»).

С другой стороны, получатель может успешно проанализировать входящий запрос и обнаружить, что запрошенный метод op не поддерживается или что выполняется другое условие ошибки. Затем ответ с op=0xffffffff или другим подходящим значением должен быть отправлен обратно, используя SENDRAWMSG с mode = 64, как указано выше.

В некоторых ситуациях отправитель хочет как передать отправителю некоторое значение, так и получить либо подтверждение, либо сообщение об ошибке. Например, смарт-контракт валидатора выборов получает запрос на участие в выборах вместе со ставкой в ​​качестве прикрепленного значения. В таких случаях имеет смысл привязать к предполагаемой стоимости, скажем, одну дополнительную монету TON. В случае ошибки (например, ставка может быть не принята по какой-либо причине), вся полученная сумма (за вычетом платы за обработку) должна быть возвращена отправителю вместе с сообщением об ошибке, например, с помощью SENDRAWMSG с mode = 64, как объяснялось ранее). В случае успеха создается подтверждающее сообщение и возвращается ровно одна монета TON (с вычетом комиссии за передачу сообщения; это mode = 1 SENDRAWMSG).

Использование сообщений типа non-bounceable (сообщения без ответа)

Почти все внутренние сообщения, отправляемые между смарт-контрактами, должны иметь возможность возврата, то есть должны иметь установленный бит «bounce». Затем, если смарт-контракт назначения не существует или если он выдает необработанное исключение при обработке этого сообщения, сообщение будет «bounced» (отброшено) обратно с остатком от исходного значения (за вычетом всех сборов за передачу сообщения и газа). Отброшенное сообщение будет иметь то же самое тело, но со снятым флагом “bounce” и установленным флагом “bounced“. Следовательно, все смарт-контракты должны проверять флаг «bounced» всех входящих сообщений и либо молча принимать их (путем немедленного завершения с нулевым кодом выхода), либо выполнять некоторую специальную обработку, чтобы определить, какой исходящий запрос не удался. Запрос, содержащийся в теле возвращенного сообщения, никогда не должен выполняться.

В некоторых случаях необходимо использовать внутренние сообщения, не подлежащие возврату. Например, новые учетные записи не могут быть созданы без отправки им хотя бы одного внутреннего сообщения, не подлежащего возврату. Если это сообщение не содержит StateInit с кодом и данными нового смарт-контракта, не имеет смысла иметь непустое тело во внутреннем сообщении без возможности возврата.

Хорошей идеей будет не позволять конечному пользователю (например, кошельку) отправлять неотправляемые сообщения с большой стоимостью (например, более пяти монет TON) или, по крайней мере, предупреждать их, если они попытаются это сделать. Лучше сначала отправить небольшую сумму, затем инициализировать новый смарт-контракт, а затем отправить большую сумму.

Внешние сообщения (External messages)

Внешние сообщения отправляются извне смарт-контрактам, находящимся в блокчейне TON, чтобы заставить их выполнять определенные действия. Например, смарт-контракт кошелька ожидает получения внешних сообщений, содержащих приказы (например, внутренние сообщения, которые будут отправлены из смарт-контракта кошелька), подписанных владельцем кошелька; когда смарт-контракт кошелька получает такое внешнее сообщение, он сначала проверяет подпись, затем принимает сообщение (запустив TVM примитив ACCEPT), а затем выполняет все необходимые действия.

Обратите внимание, что все внешние сообщения должны быть защищены от повторных атак. Валидаторы обычно удаляют внешнее сообщение из пула предлагаемых внешних сообщений (полученных из сети); однако в некоторых ситуациях другой валидатор может обрабатывать одно и то же внешнее сообщение дважды (таким образом создавая вторую транзакцию для того же внешнего сообщения, что приводит к дублированию исходного действия). Хуже того, злоумышленник может извлечь внешнее сообщение из блока, содержащего обрабатываемую транзакцию, и повторно отправить его позже. Например, это может заставить смарт-контракт кошелька повторить платеж.

Самый простой способ защитить смарт-контракты от повторных атак, связанных с внешними сообщениями, — сохранить 32-битный счетчик cur-seqno в постоянных данных смарт-контракта и ожидать значение req-seqno в (подписанной части) любого входящие внешние сообщения. Тогда внешнее сообщение принимается ACCEPT только в том случае, если и подпись действительна, и req-seqno = cur-seqno. После успешной обработки значение cur-seqno в постоянных данных увеличивается на единицу, поэтому такое же внешнее сообщение больше никогда не будет принято.

Можно также включить во внешнее сообщение поле срока действия и принять внешнее сообщение только в том случае, если текущее время Unix меньше значения этого поля. Этот подход можно использовать в сочетании с seqno; в качестве альтернативы, принимающий смарт-контракт может хранить набор (хэши) всех последних (не просроченных) принятых внешних сообщений в своих постоянных данных и отклонять новое внешнее сообщение, если оно является дубликатом одного из сохраненных сообщений. Также следует выполнять некоторую сборку мусора для сообщений с истекшим сроком действия в этом наборе, чтобы избежать раздувания постоянных данных.

Как правило, внешнее сообщение начинается с 256-битной подписи (при необходимости), 32-битного req-seqno (при необходимости), 32-битного expire-at (при необходимости) и, возможно, 32-битной операции и другие необходимые параметры в зависимости от op. Макет внешних сообщений не должен быть таким же стандартизированным, как у внутренних сообщений, потому что внешние сообщения не используются для взаимодействия между разными смарт-контрактами (написанными разными разработчиками и управляемыми разными владельцами).

Get-методы

Ожидается, что некоторые смарт-контракты реализуют четко определенные методы для получения данных. Например, ожидается, что любой смарт-контракт распознавателя поддоменов для TON DNS будет реализовывать метод получения dnsresolve. Пользовательские смарт-контракты могут определять свои конкретные методы для получения каких-либо данных. Наша единственная общая рекомендация на данный момент — реализовать seqno метода get (без параметров), который возвращает текущее seqno смарт-контракта, использующего порядковые номера, для предотвращения повторных атак, связанных с входящими внешними методами, всякий раз, когда такой метод имеет смысл.

Рейтинг
( 1 оценка, среднее 5 из 5 )
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: