Michelson: почему контракты на Tezos ломаются на формальной проверке
Michelson — это низкоуровневый язык со стековой машиной, где нет привычных переменных и циклов: программа собирается из последовательности инструкций, каждая из которых снимает значения со стека и кладёт результат обратно. Ошибка в порядке операндов или в аннотации типа не «вылезает» в рантайме — контракт просто не проходит формальную проверку и не публикуется в сеть. Разработка смарт-контрактов на Tezos потому и начинается не с кода, а с точной модели типов и потока данных.
Самая частая причина падения на проверке — ошибки типизации Michelson. Стековая машина строго следит за тем, что лежит на вершине, и требует явных аннотаций (:int, :nat, :address) там, где тип не выводится однозначно. Если аннотация расходится с фактическим значением — например, nat передаётся туда, где ожидается int, — проверка отклоняет контракт целиком, а не отдельную ветку.
Вторая категория — неучтённые побочные эффекты. Операции вроде TRANSFER_TOKENS или CREATE_CONTRACT меняют состояние сети, и их нельзя вызывать в произвольном месте: порядок операций и обработка отказов (FAILWITH) должны быть продуманы так, чтобы частичное выполнение не оставляло контракт в несогласованном состоянии.
Отдельно бьёт по проекту стоимость газа Tezos. Неоптимальные структуры данных — большие map и big_map, лишние промежуточные копии, неаккуратные циклы через ITER — раздувают потребление газа, и транзакция может не пройти по лимиту. Разбираем, как это закрывается на этапе проектирования типов и структур.
Что даёт грамотная архитектура контракта на Michelson
Архитектура хранения и стоимость операций
В Michelson нет динамических структур данных в привычном виде: big-map вынесен в отдельное хранилище, а стоимость операции определяется объёмом записей в storage и числом шагов интерпретатора. Поэтому раскладка состояния по нескольким big-map с ключами фиксированной формы вместо одного вложенного дерева снижает цену каждой транзакции: интерпретатору не нужно разворачивать глубокую вложенность, а обновление затрагивает одну ветку. На реальных контрактах такая перестройка сокращала расход газа на массовых операциях в разы — при том же функционале.
Формальная верификация как часть архитектуры
Контракт, где инварианты вынесены в отдельные проверяемые функции, а не размазаны по ветвям IF, проходит формальную верификацию быстрее и с меньшим числом ручных допущений. Мы проектируем так, чтобы ключевые свойства — сохранность баланса, недоступность повторного вывода, корректность прав — формулировались как отдельные леммы. Это убирает целый класс отказов, которые не ловятся тестами: гонки между вызовами, необработанные ветви FAILWITH, неявные переполнения.
Сравнение подходов к разработке
| Подход | Когда подходит | Ограничения |
|---|---|---|
| Ручной Michelson | Жёсткие требования к газу, аудит критичных участков | Дороже в поддержке, выше порог входа для команды |
| Компиляция из высокоуровневого языка | Быстрый вывод продукта, частая логика | Меньше контроля над раскладкой storage и расходом газа |
| Гибрид: ядро на Michelson, обвязка компилируемая | Сложные контракты с критичным ядром | Требует дисциплины на стыке слоёв |
Безопасность и проверяемость
Отдельный слой — статический анализ и проверка типов до деплоя: он отсекает ошибки, которые на тестовой сети проявляются редко, а в основной приводят к потере средств. Мы фиксируем инварианты в коде и в спецификации, чтобы аудитор проверял свойства, а не восстанавливал замысел.
Michelson — низкоуровневый стековый язык Tezos, где стоимость и безопасность контракта закладываются на этапе раскладки состояния, а не оптимизируются после.
Этапы разработки и внедрения контракта на Michelson
Разработка контракта на Michelson начинается не с кода, а с формализации правил: кто вызывает точки входа, какие состояния допустимы, что происходит при конфликте условий. На этом этапе фиксируем схему хранения — раскладку по big_map и обычным полям стораджа, потому что от неё зависит стоимость операций и простота будущих изменений. Ошибка в модели данных обходится дороже всего: её правка после запуска требует миграции и повторного аудита.
Дальше идёт проектирование, реализация и проверка — каждый шаг с воспроизводимым результатом, который можно передать другой команде.
- Аудит задачи и границ контракта. Разбираем бизнес-логику, роли участников, внешние вызовы и лимиты. На выходе — спецификация точек входа и инвариантов, которые контракт обязан сохранять при любом сценарии.
- Проектирование схемы хранения и потоков вызовов. Определяем структуру стораджа, типы данных Michelson, размещение коллекций в big_map. Проверяем, что операции не упираются в лимиты газа при росте числа записей.
- Реализация на Michelson. Пишем контракт и вспомогательные view-точки, держим логику плоской и явной — сложные ветвления усложняют аудит и повышают цену ошибки. Компиляцию ведём в актуальной ветке инструментов.
- Тестирование. Прогоняем юнит-сценарии на успешные и провальные вызовы, проверяем граничные состояния и права доступа. Отдельно моделируем поведение при параллельных вызовах и повторных операциях.
- Аудит контракта. Независимая проверка логики, прав и обработки исключений до публикации. Все замечания закрываем с фиксацией изменений, чтобы правки не ломали уже проверенные инварианты.
- Развёртывание в тестовой сети. Прогоняем полный сценарий жизненного цикла на копии реальных данных, замеряем расход газа и поведение при отказах.
- Передача контракта заказчику. Отдаём исходники, схему стораджа, отчёт аудита и инструкцию по вызовам. Команда заказчика может сопровождать контракт без нас.
Опишите задачу — вернёмся с оценкой и планом по каждому этапу.
Чем отличается Michelson от высокоуровневых языков для Tezos?
Michelson — это низкоуровневый стековый язык, в который в итоге компилируется любой контракт для Tezos. Высокоуровневые языки (LIGO, SmartPy, Archetype) дают синтаксис, типы и абстракции, но на цепочку попадает один и тот же байткод. Отсюда практический вывод: michelson vs liquidity — это не про «мощнее или слабее», а про то, на каком слое команда хочет контролировать результат.
Компиляция в Michelson не бесплатна по предсказуемости. Компилятор может развернуть цикл, добавить проверки типов или иначе разложить хранение — и вы получите контракт, который работает, но расходует газ не так, как вы рассчитывали. Прямая разработка на Michelson убирает этот слой: вы видите каждую инструкцию, каждое обращение к стеку и точно знаете размер хранилища и стоимость вызова. Это критично для контрактов с жёстким лимитом газа или для аудита, где важно доказать отсутствие скрытых путей исполнения.
Обратная сторона — цена поддержки. На Michelson нет привычных функций высшего порядка, словарей и строк в том виде, к которому привыкли разработчики. Логика вроде «распределить доли между участниками» превращается в десятки строк ручного управления стеком, где легко перепутать порядок операндов. Ошибка здесь стоит дороже: типы не поймают смысловую путаницу, а откатить уже задеплоенный контракт нельзя.
Поэтому сравнение языков смарт-контрактов для Tezos на практике сводится к выбору точки входа. Высокоуровневый язык берут для бизнес-логики, где важны скорость разработки и читаемость: токены, голосования, реестры. Michelson — когда нужен ручной контроль над газом, нестандартная криптография или когда контракт пишется как эталон для аудита и последующих реализаций.
Выбор языка Tezos редко бывает «или-или». Рабочая схема: основную логику пишут на высокоуровневом языке, а критичные участки — проверку подписей, работу с бинарными данными, оптимизацию хранения — выносят в отдельные контракты на Michelson и вызывают их из основного. Так команда получает и скорость разработки, и предсказуемость там, где она важна.
Кейс: как мы устранили класс отказов в контракте на Michelson
Заказчик пришёл с контрактом на Michelson, который работал «в основном нормально»: аудит пройден, тесты зелёные, но в проде часть вызовов падала с ошибкой исполнения, а комиссия за успешные операции гуляла в широком диапазоне. Разбор показал классику — вычисления в storage без учёта порядка операций и ветвления, где FAILWITH срабатывал уже после списания газа. Поведение контракта зависело от состояния, которое никто не проверял до входа в критичную ветку.
Что изменили
Переписали точку входа: проверки предусловий (amount, права, границы значений) вынесли в начало, до любых изменений storage. Логику разбили на мелкие внутренние функции, чтобы каждая ветка читалась и тестировалась отдельно. Убрали дублирующие вычисления — промежуточные значения считаются один раз и переиспользуются.
Метрики до и после
| Показатель | До | После |
|---|---|---|
| Доля неудачных транзакций | заметная | единичные случаи на граничных данных |
| Разброс расхода газа | широкий | узкий, предсказуемый |
| Время разбора инцидента | часы | минуты по логам |
Отказы класса «упало после списания» ушли полностью — теперь любая ошибка возвращается до изменения состояния, и вызывающая сторона видит причину, а не факт списания.
Что это дало
Контракт стал предсказуемым: одинаковый вход даёт одинаковый расход газа, а поведение на граничных значениях описано в тестах, а не выясняется в проде. Мы опирались на формальную модель исполнения Michelson — она позволяет проверять инварианты до деплоя, а не после. Для заказчика это означало меньше ручных разборов и понятный бюджет комиссий на каждую операцию.
Оптимизация контракта на Tezos без потери читаемости — это баланс между строгостью проверок и стоимостью исполнения. Мы держим его за счёт тестов на каждую ветку и явных предусловий.
Сомнения перед стартом: совместимость, миграция, доступы
Совместимость Michelson с уже работающей инфраструктурой — первый вопрос, который снимаем до старта. Контракт на Michelson компилируется в тот же формат, что и существующие контракты в сети Tezos, поэтому вызовы из внешних сервисов, индексаторов и кошельков остаются рабочими. Если у вас уже есть контракты на LIGO или SmartPy, миграция не требует переписывания с нуля: логику переносим в Michelson, интерфейсы точек входа сохраняем совместимыми, а состояние переносим отдельным скриптом с проверкой на тестовой сети.
Доступы и роли проектируем до деплоя, а не после. Административные точки входа (обновление логики, пауза, вывод средств) закрываем мультиподписью, операционные — отдельными ключами с ограниченным набором прав. Это снимает класс отказов, когда один скомпрометированный ключ открывает весь контракт.
Что если контракт уже задеплоен и его нельзя менять?
Michelson не позволяет править код неизменяемого контракта, но обновление логики закладывается заранее — через прокси-контракт или точку входа с проверкой прав. Если такой возможности нет, разбираем текущее состояние, переносим логику в новый контракт и готовим план перехода с сохранением данных. Откат на предыдущую версию остаётся возможным, пока старый контракт не выведен из эксплуатации.
Что если у нас своя команда и мы хотим поддерживать код сами? Передаём не только исходники, но и логику решений: почему выбрана такая структура хранения, где узкие места по расходу газа, какие проверки обязательны при обновлении. Инженеры заказчика выходят на сопровождение без нас.
Что если нужен аудит перед запуском? Готовим контракт к внешней проверке: убираем неиспользуемые точки входа, документируем инварианты, прикладываем сценарии тестов — это сокращает время аудита и число замечаний.
На руки заказчик получает:
- исходники контрактов на Michelson с комментариями по каждой точке входа;
- схему ролей и матрицу доступов: кто и что может вызвать;
- скрипты деплоя и миграции состояния с проверкой на тестовой сети;
- набор тестов, покрывающих ключевые сценарии и граничные случаи;
- инструкцию по обновлению логики и порядок действий при откате;
- документацию по взаимодействию контракта с внешними сервисами.
Опишите задачу — вернёмся с оценкой и планом: что переносим, какие роли закрываем, в каком порядке выходим на деплой.
Сроки разработки контракта на Michelson по этапам
Сроки на Michelson определяет не объём кода, а формализация инвариантов: контракт на Tezos описывает состояние и переходы, и любая неоднозначность в требованиях всплывает на этапе проверок, а не при написании кода. Поэтому график строим от модели хранения и списка условий, а не от «количества строк». Ниже — ориентировочные длительности этапов; они не привязаны к календарным датам и уточняются после разбора задачи.
Разброс по срокам даёт именно этап проверок: чем больше внешних вызовов и условий в контракте, тем дольше разбор граничных случаев. Если модель данных и переходы зафиксированы заранее, реализация идёт предсказуемо, а правки после сдачи сводятся к минимуму.
Работаем на актуальной ветке инструментов Tezos — Michelson и сопутствующие средства разработки, поэтому контракт сразу пишется под текущие требования сети, без переписывания под устаревший синтаксис. Опишите задачу — вернёмся с оценкой и планом по этапам.







