Обложка: Разработка на блокчейне: от стека-зоопарка — к SDK на одном языке и с поддержкой low-code

Разработка на блокчейне: от стека-зоопарка — к SDK на одном языке и с поддержкой low-code

По мотивам выступления Дениса Васина, руководителя прикладной разработки платформы Waves Enterprise, на Blockchain Life 2021

Зачем нужны смарт-контракты? Сфер применения у них много. Cмарт-контракт, исполняемый внутри блокчейна, гарантирует нам то, что у некоторого NFT-токена в настоящий момент только один владелец. DeFi — сегмент децентрализованных финансов — основан на смарт-контрактах целиком. Любое DeFi-приложение — это фактически набор смарт-контрактов, правил, по которым функционирует экономическая система.

В смарт-контрактах рассчитывается, сколько каких токенов мы можем взять под какой залог, рассчитываются, например, правила ликвидации, когда стоимость нашего залога в результате колебаний рынка упала. Или, например, в децентрализованных автономных организациях (DAO) — все управление работает через смарт-контракты. Держатели токенов организации проводят голосование. Его результат регистрируется в смарт-контракте, и он обеспечивает необратимость принятого решения. В принципе, весь существующий сегодня публичный блокчейн работает на смарт-контрактах.

Внутри смарт-контрактов: уязвимости Solidity

Давайте заглянем внутрь смарт-контрактов. Для них по крайней мере в популярнейшем Ethereum используется язык Solidity. Пример функции из одного реального контракта в сети:

function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.center];
(bool success, ) = msg.center.call.calue(amountToWithdraw)(“”);
require(success);
userBalances[msg.sender] = 0;
}

Что у нас здесь? Видим метод withdraw balance() — этот кусок кода отвечает за то, чтобы забирать какое-то количество эфира из контракта. Вроде бы всё просто и понятно. Но даже эти несколько строк уже содержат критическую уязвимость, которую, скорее всего, не знакомый с Solidity человек не заметит.

Уязвимость эта связана с re-entrancy. Смотрим на третью строчку кода: здесь мы используем метод msg.sender.call.value и добавляем то количество денег, которое хотим забрать. Что же не так?

В Ethereum широко поддерживается композиция^. Пользователями смарт-контракта могут быть не только владельцы закрытого ключа, но и другие контракты. В sender’е может оказаться контракт, который после передачи ему управления методом call снова вызовет метод withdraw balance() и заберёт деньги. За счёт того, что вызов userBalances[msg.sender] = 0 находится после вызова другого контракта, данный метод оказывается уязвим.

Одна из самых знаменитых атак DAO — на Ethereum в 2016 году — была сделана через схожую уязвимость. С контракта вывели несколько миллионов долларов, Ethereum форкнулся, сеть развалилась на две.

Прошло пять лет, но в контрактах до сих пор попадаются уязвимости, связанные с re-entrancy. Да, контракты, оперирующие протоколами с большими суммами денег, подвергаются аудиту. Как говорится, рано или поздно любой контракт проходит аудит: вы заплатите за него либо аудиторам, либо хакерам.

Но контракты, которые разрабатываются на скорую руку — часто это форки известных проектов, сделанные, например, для новых токенов — сохраняют возможность для таких re-entrancy или более сложных атак, которые могут привести ко взлому протокола. Поэтому Solidity — не самый надежный язык для написания смарт-контрактов. Вот ещё один пример:

function at(address _addr) public view returns (bytes memory o_code) {
assembly {
	// retrieve the size of the code, this needs assembly
	let size := extcodesize(_addr)
	// allocate ouput byte array – this could also be done without assembly
	// by using o_code = new bytes(size)
	o_code := mload(0x40)
	// new “memory end” including padding
	mstore(0x40, add o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
	// store length in memory
	mstore(o_code, size)
	// actually retrieve the code, this needs assembly
	extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}

Перед нами ассемблерная вставка. Их можно размещать в Solidity, поскольку это достаточно низкоуровневый язык, где стоит думать об аллокации переменных в памяти, чтобы уменьшить стоимость газа. Для этих целей или, например, для того, чтобы использовать хранилище на другом адресе, мы можем использовать inline assembly.

Я как разработчик смарт-контрактов скептически отношусь к использованию парадигмы настолько низкого уровня — потому что так мы больше думаем про техническую реализацию, а не про бизнес. Но что поделать, ведь Solidity был разработан семь лет назад, стал первопроходцем в создании смарт-контрактов на блокчейне. И по сей день сохранил оригинальное низкоуровневое «видение».

Другие минусы экосистемы

Разберёмся теперь, что в принципе происходит в процессе выполнения смарт-контракта. У нас есть майнер блока, и он выполняет смарт-контракты, которые находятся в блоке. После выполнения смарт-контрактов начинается процесс PoW (proof of work) — подбор хеша определенной сложности. Процесс PoW-майнинга гораздо более ресурсоёмок, чем процесс обработки самого контракта. На исполнение контракта тратится очень малая часть машинного времени по сравнению с подбором PoW-хеша. Это довольно обидно, потому что разработчикам приходится тратить уйму времени на написание оптимального по газу кода: раскладывать переменные, оптимизировать записи и так далее. Хотя по сути это избыточная работа, которая на сам смарт-контракт не влияет.

Как мы уже сказали, Solidity небезопасен по своей природе. С ним можно легко выстрелить себе в ногу из-за неправильной последовательности вызовов. А использование кода, который позволяет напрямую работать с виртуальной машиной эфира, только усугубляет потенциальную опасность.

Эти две причины привели к возникновению целого направления — аудит смарт-контрактов. В последнее время это очень востребовано: энтузиасты форкают Uniswap и пишут свои смарт-контракты. Аудиторские конторы не отстают и задирают цены — десятки долларов за строчку кода. Так саморегулируется рынок, но, как мне кажется, это очень дорого.

Если свести особенности Solidity вместе, перед нами предстанет весьма непростая система. Оптимизация по газу, оптимизация переменных, написание кода на достаточно низкоуровневом языке, где нет большого количества примитивов — например, даже встроенной библиотеки для работы со строками (хотя для кого-то это некритично, ведь некоторые протоколы вполне себе обходятся без строк).

За счёт такой сложности разработчики на блокчейне стоят сравнительно дорого. Но сейчас разработка на блокчейне становится мейнстримом. Люди делают свои De-Fi протоколы, деген-фармы, NFT и всё прочее, не разбираясь с Solidity. Поэтому растёт потребность в более высокоуровневом инструменте, который позволит сразу уменьшить количество ошибок в разработке и снизить порог входа как для новичков в программировании, так и для специалистов по другим языкам.

Даже комьюнити Ethereum в некоторой степени разделяет подобные взгляды. Виталик Бутерин, создатель Ethereum, в качестве более простой и безопасной альтернативы Solidity предложил Vyper. Этот язык похож на Python, имеет ряд ограничений, которых нет в Solidity, но благодаря которым разработка контрактов становится проще.

Дело не только в смарт-контрактах

Мы рассмотрели проблему со смарт-контрактами, и она не последняя. Для работы DeFi требуются внешние источники данных и инструменты работы с ними — оракулы. Когда мы закладываем эфир под залог, необходимо, чтобы система знала текущую стоимость эфира — иначе она не будет понимать, когда нужно ликвидировать залог. Вопрос оракулов решается с помощью популярного протокола ChainLink, который фактически поставляет данные из реального мира в блокчейн.

Изображение: смарт-контракты

Роль дополнительных инструментов в блокчейне настолько важна, что без них он превращается в  дорогую, медленную и в целом ужасную по своим характеристикам key-value базу данных. Всё может быть прекрасно с записью, с поддержанием консенсуса… но ответить на простые вопросы вроде «какие топ-10 аккаунтов имеют больше всего эфира» нода сама по себе не в состоянии: у нее нет таких средств, нет даже языка запросов.

Для полноты картины стоит упомянуть, что кое-какая стандартизированная логика в смарт-контрактах типа ERC-20/ERC-721 или в NFC-токенах всё-таки присутствует, но работает она исключительно для простых запросов в узкой предметной области. Чтобы проследить, например, историю владения NFT на эфире, нужно разматывать весь контракт до основания, искать в транзакциях события. Это очень сложно и неудобно. Поэтому возникают инструменты, которые позволяют индексировать данные блокчейна и предоставлять их в простом, доступном виде. Сегодня наиболее известным из таких инструментов является The Graph.

The Graph — протокол, который позволяет нам писать собственные так называемые индексаторы. Они нужны, чтобы прочитать данные с какого-либо контракта — события, вызовы — и построить удобно читаемую через API модель данных в реляционной базе данных.

Зачем это надо? Простой пример: Uniswap — известная децентрализованная биржа, которая позволяет нам в любой момент покупать/продавать токены. У Uniswap есть потребность — отрисовать стандартный биржевой график. Делается это сейчас с помощью The Graph. На AssemblyScript — это подвид TypeScript, нацеленный на логику и скорость исполнения — пишут индексатор. Начиная с указанного блока он проходит по блокчейну и для каждого блока запоминает результат: какова была стоимость разных пар токенов в событиях покупок/продаж. Эти данные он складывает в обычную базу с доступом через GraphQL API в виде привычных нам «свечек», которые потом рисует интерфейс Uniswap.

Сырые данные блокчейна скрыты глубоко от наших глаз, и это приводит к серьёзным задержкам в синхронизации данных — как случилось, например, с эфир-совместимым протоколом Binance Smart Chain. Новые данные появляются так быстро, что индексаторы не успевают их выводить. Из-за этого пользователи могут отправить транзакции и не увидеть их результат в веб-сервисах. Но это не «блокчейн сломался» и не «токены украли» — это просто индексатор отстал от блокчейна.

Изображение: смарт-контракты

The Graph — это обратный оракул с механизмом обеспечения доверия. Он включает индексаторы, которые по итогам процесса индексирования и достижения консенсуса получают токены вознаграждения. Если консенсус не достигнут, токены слешатся. Таким образом реализуется механизм доверия к этим данным.

Причём здесь энтерпрайз?

На протяжении нескольких лет эволюция разработки на публичных блокчейнах пришла к разрозненному набору инструментов, которые создаются разными командами. Выше мы разобрали лишь часть этого набора. При этом без всех инструментов — Vyper, Etherscan, The Graph и так далее — полноценного приложения не построишь.

Изображение: смарт-контракты

Такой зоопарк инструментов неудобен для компаний. Очень мало разработчиков знают одновременно Solidity и Vyper, да ещё и AssemblyScript владеют. Поднять сразу все нужные компетенции для корпоративного блокчейна тяжело. Но необходимо, чтобы пройти одну из самых проблемных стадий для блокчейна — adoption, принятие решения как основного. Ведь если на блокчейн возложить лишь вспомогательные функции, его польза сводится к минимуму.

Публичные блокчейны столкнулись с этим во время «криптозимы» — периода активного поиска новых сценариев использования, который длился вплоть до распространения DeFi, начала «оттепели» летом 2020-го. Но это всего лишь оттепель — корпоративный блокчейн по-прежнему развивается медленно. Даже когда находится бизнес-применение технологии, все процессы спотыкаются о требования к разработчикам.

Waves Enterprise SDK

Мы в Waves Enterprise решили собрать в одной коробке набор инструментов, который бы инкапсулировал все лучшие практики разработки публичных блокчейнов. Мы занимаемся именно энтерпрайз-блокчейном и понимаем основные требования заказчиков к инструментам:

  • использование распространённого технологического стека;
  • отсутствие нескольких языков в рамках одного проекта;
  • предсказуемая, гарантированная поддержка от вендора.

Их мы учли при создании Waves Enterprise SDK. Он инкапсулирует лучшие паттерны из мира блокчейн-разработки, использует стандартные корпоративные языки программирования (Java, Kotlin) и единый набор инструментов как для смарт-контрактов, так и для приложений, которые берут из них данные.

Рассмотрим пример смарт-контракта в нашем SDK — это порт ERC-20 токена:

override fun transferFrom(from: String, to: String, amount: Long) {
require(allowance(from, са11.са11ег) >= amount) { "INSUFFICIENT_ALLOWANCE" }
require(balance0f(from) >= amount) { "INSUFFICIENT_BALANCE" }
modifyBalance(from) { it – amount }
modifyBaIance(to) { it + amount }
modifyAlIowance(from, саll.саllег) { it – amount }
}

fun modifyBalance(address: String, fn: (Long) –> Long) {
balance.put(address, fn(balance0f(address)))
}

fun modifyAllowance(from: String, to: String, fn: (Long) –> Long) {
allowances.put(allowanceKey(from, to), fn(allowance(from, то)))
}

Это код на Kotlin, который использует невысокоуровневые функции, чтобы проверить, кто кому сколько дал прав на перевод токенов, отследить баланс, по которому дали разрешение. В чём преимущество этого кода по сравнению с Solidity?

  • нет скрытых уязвимостей: невозможны re-entrancy атаки.
  • не нужно думать об оптимизации по газу — комиссия фиксирована.
  • пишется на более распространенном языке — после нашего обучения за один день Java-разработчики уже начинали писать свои контракты.

Но, как мы обсуждали, смарт-контракты — это ещё не всё. Нужно получить данные, информацию по обновлению контракта. У нас в SDK для этого есть библиотека, которая заменяет The Graph, и в этой библиотеке тоже всё пишется на Kotlin. Достаточно создать ротацию BlockListener, уточнить, какие ключи в блокчейне мы слушаем, — и можно писать произвольный код, который разложит данные транзакции в объектную модель для дальнейшей работы.

@VstBlockListener
fun handleEvent(
@VstKeyFiIter(keyPrefix = "BALANCE_")
event: VstKeyEvent<BigInteger>
)   {
val balance = event.payload
val tx = event.tx // транзакция
val contractID = tx.sender
}

В классическом Ethereum, чтобы вызвать контракт, нужно использовать библиотеку Web3j на JS. Нужен отдельный код, чтобы вызвать контракт, собрать транзакцию. У нас это уже реализовано в рамках единого языка стандартными инструментами. То есть наш SDK реализует полный жизненный цикл программной разработки на блокчейне в одной коробке.

Делаем ещё проще: lowcode-разработка

Итак, у нас есть единая парадигма разработки на основе кодинга. Но что если разрабатывать программное обеспечение без него? В публичных чейнах есть такая возможность.

Есть такой стартап — Furucombo — визуальный конструктор, который даёт возможность складывать DeFi-протоколы как кубики и получать готовую DeFi-стратегию. Раньше для стратегии нужно было писать код смарт-контракта, в котором в рамках одной транзакции вызывались бы разные протоколы. Сейчас мы складываем кубики, нажимаем Run — и всё готово. Так реализуется low-code подход к разработке — с помощью drag’n’drop и графического интерфейса. По большому счету, такой подход требует лишь знаний предметной области: в случае с Furucombo это DeFi, а в корпоративном мире — это бизнес-процессы компании, знание того, что мы хотим оптимизировать и зачем применять блокчейн.

Low-code разработка становится все более и более популярной. Вот выжимка из отчета Gartner по теме:

Изображение: смарт-контракты

Почти четверть пользователей, пришедших к low-code, вообще никогда не занимались программированием! И 70% из них освоили low-code менее чем за месяц. Тогда как самый простой курс программирования с нуля на питоне — это минимум полгода. Почти две трети абсолютных новичков разработали своё приложение менее чем за три месяца. Low-code позволяет быстро и просто автоматизировать типовые бизнес-процессы и снижать TTM, быстрее релизить приложение на рынке, что сегодня критически важно.

В экосистеме Waves Enterprise существует партнерский продукт EasyChain, который позволяет собирать приложения на основе нашего блокчейна аналогичным low-code способом.

С помощью визуального конструктора пользователь рисует в EasyChain Studio свой бизнес-процесс, через API он публикуется и попадает в блокчейн. Дальше со смарт-контрактом можно работать через API, через Studio или через эксплорер бизнес-процессов. Для любого бизнес-процесса можно сделать собственный интерфейс пользователя — через веб- или мобильное приложение.

Code и low-code: разрабатываем смарт-контракт

Сравним обычную и low-code разработку лоб в лоб на примере простого смарт-контракта уровня hello world. Вот, собственно, code-версия:

package com.wavesplatform.we.app.example.contract.impl

import com.wavesplatform.vst.contract.spring.annotation.ContractHandlerBean
import com.wavesplatform.vst.contract.state.ContractState
import com.wavesplatform.vst.contract.state.getValue
import com.wavesplatform.vst.contract.state.setValue
import com.wavesplatform.we.app.example.contract.ExampleContract

@ContractHandlerBean
class ExampleContractImpl(
    state: ContractState
) : ExampleContract {

    var result: String? by state

    override fun create(name: String) {
        result = "Hello $name"
    }
}

В Waves Enterprise все смарт-контракты представляют собой docker-образы, которые хранятся в docker registry. Кто-то скажет, что это небезопасно и «не тру». Но на самом деле это безопасно, поскольку в блокчейне хранится хеш от образа контракта, и этого достаточно для защиты неизменности кода.

Теперь попробуем сделать то же самое через no-code инструмент — вышеупомянутую Easy Chain Studio. Создаём стартовую точку, начальное состояние контракта, добавляем состояние SayHello и финишное состояние. Затем соединяем кубики стрелочками.

Набросок логики есть, осталось научить контракт говорить Hello. Для этого добавим переменную процесса result: она будет хранить в себе выводимый результат. Теперь необходимо определить входные данные. Нажмем на стрелку перехода к SayHello. Указываем, что пользователю нужно ввести данные и что переход совершается извне. Добавляем свойство name. Теперь необходимо собрать строчку SayHello. Для этого переменной result мы присвоим значение ‘Hello’ + signal.name.

На этом всё, идти в API и делать вызовы не надо — можно уже тестировать процесс. Мы выбираем среду, нажимаем Deploy и Run.

После запуска система даёт подсказку, что мы можем сделать на первом шаге. Единственное, что мы можем сделать — это прописать значение параметра, например «Blockchain Low Code Life». Вводим, отправляем и идём в эксплорер блокчейна посмотреть, как всё прошло:

Перед нами информация о вызове смарт-контракта. Видим, что в качестве параметра мы, как и предполагалось, отправили «Blockchain Low Code Life» и в переменной Result получили «Hello Blockchain Low Code Life». Для обычного пользователя второй, low-code способ создания выглядит гораздо дружелюбней.

Подведём итог. От зоопарка разномастных решений в начале статьи мы пришли к единому SDK на одном языке программирования, да ещё и с поддержкой low-code. Таким образом мы снижаем порог входа в блокчейн-разработку и максимально упрощаем самый важный шаг для технологии — adoption. И наш блокчейн оказывается удобоварим не только для программистов широкого профиля, но и для всех, кто способен структурированно описать бизнес-процессы на уровне шаблонов и переменных — например, для аналитиков.