Доступ к объемным файлам и различные внешние динамические данные часто являются очень важной частью децентрализованного приложения. При этом в самом по себе Ethereum механизма обращения наружу не предусмотрено — смарт контракты могут читать и писать только в рамках самого блокчейна. В этой статье рассмотрим Oraclize, который как раз дает возможность взаимодействия с внешним миром путем запросов к практически любым интернет-ресурсам. Смежной темой является IPFS, вкратце упомянем и о ней.



IPFS


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

Зачем использовать IPFS в связке с Ethereum?


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

  • Ссылка на файл — это хеш, уникальный для конкретного содержимого файла, поэтому если мы положим этот хеш на блокчейн, то можем быть уверены, что получаемый по нему файл именно тот, который изначально и добавлялся, файл невозможно подменить
  • Распределенная система страхует от недоступности конкретного сервера (из-за блокировки или других причин)
  • Ссылка на файл и хеш-подтверждение объединены в одну строку, значит можно меньше записывать в блокчейн и экономить газ

Среди недостатков можно упомянуть, что раз центрального сервера нет, то для доступности файлов необходимо, чтобы хотя бы кто-то один этот файл “раздавал”. Но если у вас есть определенный файл, то подключиться к раздающим легко — запускаете у себя ipfs-демон и добавляете файл через ipfs add.

Технология очень подходит под идеологию децентрализации, поэтому рассматривая сейчас Oraclize, мы не раз столкнемся с использованием IPFS в разных механизмах оракулов.

Oraclize


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

Поэтому сервис, предоставляемый Oraclize, приходится как нельзя кстати: в смарт контракте можно отправить запрос к практически любому API или ресурсу в интернете, быть уверенным, что полученные данные пришли с указанного ресурса в неизменном виде, а результат использовать в том же самом смарт контракте.

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

Начало работы


Все, что нужно для начала работы — это добавить себе в проект один из файлов oraclizeAPI из репозитория. Надо только выбрать подходящий для вашей версии компилятора (solc): oraclizeAPI_0.5.sol для версий начиная с 0.4.18, oraclizeAPI_0.4.sol — для версий от 0.4.1, oraclizeAPI_pre0.4.sol — для всего более старого, поддержка этой версии уже прекращена. Если пользуетесь truffle, то не забудьте переименовать файл в usingOraclize — там требуется, чтобы имя файла и контракта совпадали.

Включив подходящий файл себе в проект, наследуете контракт от usingOraclize. И можно начинать пользоваться ораклайзом, что сводится к двум основным вещам: отправка запроса с помощью хелпера oraclize_query, а затем обработка результата в функции __callback. Простейший смарт контракт (для получения текущей цены эфира в долларах) может выглядеть так:

pragma solidity 0.4.23;
import "./usingOraclize.sol";

contract ExampleContract is usingOraclize {

    string public ETHUSD;
    event updatedPrice(string price);
    event newOraclizeQuery(string description);

    function ExampleContract() payable {
        updatePrice();
    }

    function __callback(bytes32 myid, string result) {
        require (msg.sender == oraclize_cbAddress());
        ETHUSD = result;
        updatedPrice(result);
    }

    function updatePrice() payable {
        if (oraclize_getPrice("URL") > this.balance) {
            newOraclizeQuery("Oraclize query was NOT sent, please add some ETH to cover for the query fee");
        } else {
            newOraclizeQuery("Oraclize query was sent, standing by for the answer..");
            oraclize_query("URL", "json(https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=USD).0.price_usd");
        }
    }
}

Функция, отправляющая запрос — updatePrice. Вы можете видеть, что сначала идет проверка, что oraclize_getPrice(“URL”) больше текущего баланса контракта. Это делается потому, что вызов oraclize_query должен быть оплачен, цена расчитывается как сумма фиксированной комиссии и оплаты газа для вызова коллбэка. “URL” — это обозначение одного из типов источников данных, в данном случае это обычный запрос по https, далее рассмотрим и другие варианты. Ответы по запросу могут быть заранее разобраны как json (как в примере) и несколькими другими способами (рассмотрим дальше). В __callback возвращается строка с ответом. В самом начале проверяется, что вызов прошел от доверенного адреса oraclize

Все варианты использования oraclize строятся по одной схеме, отличаются только источники данных и возможность добавления проверки подлинности в __callback. Поэтому в будущих примерах будем приводить только значащие различия.

Цена использования


Как уже было сказано, за oraclize-запросы платится дополнительный эфир, причем снимается он с баланса контракта, а не вызывающего адреса. Исключением является только первый запрос с каждого нового контракт, он предоставляется бесплатно. Интересно еще то, что в тестовых сетях эта же механика сохраняется, но оплата идет эфиром соответствующей сети, то есть в тестнетах запросы фактически бесплатные.

Уже упоминалось, что цена запроса складывается из двух величин: фиксированной комиссии и оплаты за газ вызова callback. Фиксированная комиссия определена в долларах, и количество эфира высчитывается из текущего курса. Комиссия зависит от источника данных и дополнительных подтверждающих механизмов, на которых мы еще остановимся. Текущая таблица цен выглядит так:


Как видите цена за URL запрос равна нескольким центам. Много это или мало? Для этого давайте рассмотрим сколько стоит вторая часть — плата за газ вызова callback.
Работает это по следующей схеме: с контракта вместе с запросом заранее переводится количество эфира, нужное для оплаты фиксированного количества газа по фиксированной цене. Этого количества должно хватить для выполнения callback, а цена должна быть адекватна рынку, иначе транзакция не пройдет или будет висеть очень долго. При этом понятно, что заранее знать количество газа не всегда возможно, поэтому и плата должна быть с запасом (запас при этом не возвращается). Значения по умолчанию — лимит 200 тысяч газа по цене 20 gwei. Этого хватает на средний callback с несколькими записями и какой-то логикой. А цена 20 gwei хоть и может в данный момент казаться слишком большой (на момент написания средняя равна 4 gwei), но в моменты наплыва транзакций рыночная цена может неожиданно подскакивать и быть даже больше, поэтому в целом эти значения близки к реально используемым. Так вот, с такими значениями и ценой эфира в районе $500, оплата газа будет приближаться к $2, так что можно сказать, что фиксированная комиссия занимает незначительную часть.

Если вы знаете что делаете, то есть вариант изменять лимит и цену газа, таким образом значительно сэкономив на запросах.

Цену на газ можно задавать отдельной функцией — oraclize_setCustomGasPrice(<цена в wei>). После вызова цена сохраняется и используется во всех последующих запросах.
Лимит можно задать в самом запросе oraclize_query, указав его последним аргументом, например так:

oraclize_query("URL", "<запрос>", 50000);

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

Кстати недавно у oraclize появилась информация, что за запросы можно платить вне блокчейна, что позволит не расходовать весь лимит или возвращать остаток (и оплата идет не с контракта). Нам еще не приходилось этим пользоваться, но oraclize предлагает обращаться к ним на info@oraclize.it, если такой вариант интересен. Поэтому имейте в виду.

Как работает


Почему унаследовавшись от обычного смарт контракта мы получаем функциональность, которая изначально не поддерживалась механизмами блокчейна? На самом деле сервис ораклайз состоит не только из контрактов с функциями-хелперами. Основную работу по получению данных делает внешний сервис. Смарт контракты формируют заявки на доступ к внешним данным и кладут их в блокчейн. Внешний сервис — мониторит новые блоки блокчейна и если обнаруживает заявку — выполняет ее. Схематично это можно изобразить так:


Источники данных


Помимо рассмотренного URL, oraclize предоставляет еще 4 варианта (которые вы видели в разделе по ценам): WolframAlpha, IPFS, random и computation. Рассмотрим каждый из них.

1. URL


Уже рассмотренный пример использует этот источник данных. Это источник для HTTP запросов к различным API. В примере было следующее:

oraclize_query("URL", "json(https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=USD).0.price_usd");

Это получение цены эфира, и так как api предоставляет json строку с набором данных, запрос оборачивается в json-парсер и возвращает только нужное нам поле. В данном случае это GET, но источник URL поддерживает и POST запросы. Тип запроса автоматически определяется по дополнительному аргументу. Если там стоит валидный json как в этом примере:

oraclize_query("URL", "json(https://shapeshift.io/sendamount).success.deposit",
  '{"pair":"eth_btc","amount":"1","withdrawal":"1AAcCo21EUc1jbocjssSQDzLna9Vem2UN5"}')

то запрос обрабатывается как POST (использующееся api описано тут, если интересно)

2. WolframAlpha


Этот источник данных позволяет обращаться к сервису WolframAlpha, который может давать ответы на различные запросы фактов или вычислений, к примеру

oraclize_query(“WolframAlpha”, “president of Russia”)

вернет Vladimir Putin, а запрос

oraclize_query(“WolframAlpha”, “solve x^2-4”)

вернет x = 2.
Как видите результат оказался неполным, потому что потерялся символ ±. Поэтому перед тем как пользоваться этим источником, нужно проверить, что значение конкретного запроса может быть использовано в смарт контракте. Кроме того для ответов не поддерживается подтверждение подлинности, поэтому сами oraclize рекомендуют использовать этот источник только для тестирования.

3. IPFS


Как можно догадаться, позволяет получать содержимое файла в IPFS по мультихешу. Таймаут получения контента составляет 20 секунд.

oraclize_query(“IPFS”, “QmTL5xNq9PPmwvM1RhxuhiYqoTJcmnaztMz6PQpGxmALkP”)

вернет Hello, Habr! (если файл с таким содержимым все еще доступен)

4. random


Генерация случайного числа работает по той же схеме, что и другие источники, но если использовать oraclize_query, то требуется трудоемкая подготовка аргументов. Чтобы этого избежать можно использовать функцию-хелпер oraclize_newRandomDSQuery(delay, nbytes, customGasLimit), задав только задержку выполнения (в секундах), количество генерируемых байтов и лимит газа для вызова __callback.
У использования random есть пара особенностей, о которых нужно помнить:

  • Для подтверждения того, что число на самом деле случайное, используется особый тип проверки — Ledger, — который можно выполнить на блокчейне (в отличие от всех остальных, но об этом позже). Это значит, что в конструкторе смарт контракта надо задать этот метод проверки функцией:

    oraclize_setProof(proofType_Ledger);

    А в начале коллбэка должна быть сама проверка:

            function __callback(bytes32 _queryId, string _result, bytes _proof)
    	{
    	    require (oraclize_randomDS_proofVerify__returnCode(_queryId, _result, _proof) == 0) );
                <...>
            

    Эта проверка требует реальной сети и не сработает на ganache, поэтому для локального тестирования можно временно убрать эту строчку. Кстати, третьим аргументом в __callback здесь выступает дополнительный параметр _proof. Он требуется всегда, когда используется один из типов подтверждения.
  • Если вы используете случайное число для критических моментов, например для определения победителя в лотерее, фиксируйте пользовательский ввод до того, как отправляете newRandomDSQuery. Иначе может сложиться такая ситуация: oraclize вызывает _callback и транзакция видна всем в списке pending. Вместе с этим видно само случайное число. Если пользователи могут продолжать, грубо говоря, делать ставки, то они смогут указать цену на газ побольше, и пропихнуть свою ставку перед тем, как выполнится _callback, наперед зная что оно будет выигрышным.


5. computation


Это самый гибкий из источников. Он позволяет написать свои собственные скрипты и использовать их в качестве источника данных. Вычисления проходят на AWS. Для выполнения нужно описать Dockerfile и положить его вместе с произвольными дополнительными файлами в zip-архив, а архив загрузить в IPFS. Выполнение должно удовлетворять таким условиям:

  • Писать ответ, который необходимо вернуть, последней строчкой в stdout
  • Ответ должен быть не больше 2500 символов
  • Инициализация и выполнение не должны идти дольше 5 минут в сумме

Для примера того, как это делается, рассмотрим как выполнить простейшее объединение переданных строк и возвращение результата.

Dockerfile:

FROM ubuntu:16.04
MAINTAINER "info@rubyruby.ru"

CMD echo "$ARG0 $ARG1 $ARG2 $ARG3"

Переменные окружения ARG0, ARG1 и т.д. — это параметры, переданные вместе с запросом.
Добавляем докерфайл в архив, запускаем ipfs сервер и добавляем туда этот архив

$ zip concatenation.zip Dockerfile
$ ipfs daemon &
$ ipfs add concatenation.zip
QmWbnw4BBFDsh7yTXhZaTGQnPVCNY9ZDuPBoSwB9A4JNJD

Полученный хеш используем для отправки запроса через oraclize_query в смарт контракте:

oraclize_query("computation", ["QmVAS9TNKGqV49WTEWv55aMCTNyfd4qcGFFfgyz7BYHLdD", "s1", "s2", "s3", "s4"]);

В качестве аргумента выступает массив, в котором первый элемент — это мультихеш архива, а все остальные — параметры, которые попадут в переменные окружения.

Если дождаться выполнения запроса, то в __callback придет результат s1 s2 s3 s4.

Хелперы-парсеры и вложенные запросы


Из ответа, возвращаемого любым источником, можно заранее выделить только требуемую информацию при помощи ряда хелперов, таких как:

1. JSON парсер


Этот метод вы видели в самом первом примере, где из результата, который возвращает coinmarketcap, возвращалась только цена:

json(https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=USD).0.price_usd

Вариант использования довольно очевидный, возвращается к примеру:

[
    {
        "id": "ethereum", 
        "name": "Ethereum", 
        "symbol": "ETH", 
        "rank": "2", 
        "price_usd": "462.857", 
        "price_btc": "0.0621573", 
        "24h_volume_usd": "1993200000.0", 
        "market_cap_usd": "46656433775.0", 
        "available_supply": "100800968.0", 
        "total_supply": "100800968.0", 
        "max_supply": null, 
        "percent_change_1h": "-0.5", 
        "percent_change_24h": "-3.02", 
        "percent_change_7d": "5.93", 
        "last_updated": "1532064934"
    }
]

Так как это массив, берем элемент 0, а из него — поле price_usd

2. XML


Использование аналогично JSON, например:

xml(https://informer.kovalut.ru/webmaster/getxml.php?kod=7701).Exchange_Rates.Central_Bank_RF.USD.New.Exch_Rate

3. HTML


Можно парсить XHTML при помощи XPath. К примеру получить market cap с etherscan:

html(https://etherscan.io/).xpath(string(//*[contains(@href, '/stat/supply')]/font))

Получаем MARKET CAP OF $46.148 BillionB

4. Бинарный хелпер


Позволяет вырезать куски из raw данных, используя функцию slice(offset, length). То есть например имеем файл с содержимым “abc”:

echo "abc" > example.bin

Положим его на IPFS:

$ ipfs add example.bin
added Qme4u9HfFqYUhH4i34ZFBKi1ZsW7z4MYHtLxScQGndhgKE

А теперь вырежем 1 символ из середины:

binary(Qme4u9HfFqYUhH4i34ZFBKi1ZsW7z4MYHtLxScQGndhgKE).slice(1, 1)

В ответе получаем b

Как вы возможно обратили внимание, в случае с бинарным хелпером использовался не URL источник, а IPFS. На самом деле парсеры можно применять к любым источникам, скажем не обязательно применять JSON к тому, что вернет URL, можно добавить такое содержимое в файл:

{
  "one":"1",
  "two":"2"
}

Добавить его в IPFS:

$ ipfs add test.json
added QmZinLwAq5fy4imz8ZNgupWeNFTneUqHjPiTPX9tuR7Vxp

И потом разбирать так:

json(QmZinLwAq5fy4imz8ZNgupWeNFTneUqHjPiTPX9tuR7Vxp).one

Получаем 1

И особенно интересный вариант использования — это совмещения любых источников данных и любых парсеров в одном запросе. Такое возможно при помощи отдельного источника данных nested. Используем только что созданный файл в более сложном запросе (сложение значений в двух полях):

[WolframAlpha] add ${[IPFS] json(QmZinLwAq5fy4imz8ZNgupWeNFTneUqHjPiTPX9tuR7Vxp).one} to ${[IPFS] json(QmZinLwAq5fy4imz8ZNgupWeNFTneUqHjPiTPX9tuR7Vxp).two}

Получаем 3
Запрос формируется следующим образом: указываете источник данных nested, далее для каждого запроса добавлятете имя источника перед ним в квадратных скобках, а все вложенные запросы дополнительно обрамляете в ${..}.

Тестирование


Oraclize предоставляет полезный сервис проверки запросов без необходимости использовать смарт контракты. Просто заходите, выбираете источник данных, метод проверки и можете видеть, что вернется в __callback, если отправлять соответветствующие запросы

Для локальной проверки в связке со смарт контрактом можно использовать специальную версию Remix IDE, поддерживающую oraclize-запросы.

А для проверки локально с ganache вам понадобится ethereum bridge, который деплоит смарт контракты oraclize в ваш тестнет. Для тестирования сначала добавьте следующую строчку в конструктор вашего контракта:

OAR = OraclizeAddrResolverI(0x6f485C8BF6fc43eA212E93BBF8ce046C7f1cb475);

запустите

ganache-cli

Потом

node bridge --dev

Дождитесь когда контракты задеплоятся и можно тестировать. В выводе node bridge можно будет видеть отправленные запросы и полученные ответы.

Еще одна помощь не только при тестировании, но и при реальном использовании — возможность мониторинга запросов здесь. Если вы запрашиваете в публичной сети, то можно использовать хеш транзакции, в которой выполняется запрос. Если используете подтверждения подлинности, то имейте в виду, что они гарантированно присылаются только в mainnet, для остальных сетей может приходить 0. Если запрос был в локальной сети, то можно использовать id запроса, который возвращает oraclize_query. К слову, этот id рекомендуется всегда сохранять, например в подобном маппинге:

mapping(bytes32=>bool) validIds;

Во время запроса помечать id отправленных как true:

bytes32 queryId = oraclize_query(<...>);
validIds[queryId] = true;

А потом в __callback проверять, что запрос с таким id еще не обрабатывался:

function __callback(bytes32 myid, string result) {
        require(validIds[myid] != bytes32(0));
        require(msg.sender == oraclize_cbAddress());       
        validIds[myid] = bytes32(0);
        <...>

Нужно это потому, что __callback на один запрос может вызываться не один раз из-за особенностей работы механизмов Oraclize.

Проверка подлинности


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

Чаще всего используемый механизм, по крайней мере нами — TLSNotary с хранением в IPFS. Хранение в IPFS эффективнее, потому что в __callback возвращается не само доказательство (может быть в районе 4-5 килобайт), а намного меньший по размеру мультихеш. Чтобы задать этот тип, добавьте строку в конструкторе:

oraclize_setProof(proofType_TLSNotary | proofStorage_IPFS);

Можем сказать только, что этот тип, грубо говоря, защищает нас от недостоверности данных, полученных от Oraclize. Но Oraclize использует сервера Amazon, которые и выступают аудитором, так что им приходится только доверять.

Подробнее читайте тут.

Заключение


Oraclize предоставляет средства, которые значительно увеличивают количество use-кейсов для смарт контрактов, как и IPFS, который можно увидеть и в нескольких вариантах запросов ораклайза. Основная проблема в том, что мы опять же используем внешние данные, которые подвержены тем угрозам, от которых блокчейн должен был защитить: централизация, возможности блокировки, изменения кода, подмены результата. Но пока это все неизбежно, и вариант получения данных весьма полезный и жизнеспособный, просто надо отдавать себе отчет, зачем вводилось использование блокчейна в проект и не сводит ли обращение к внешним ненадежным источникам пользу к нулю.

Если интересны какие-то темы разработки на Ethereum пока не раскрытые в этих статьях — пишите в комментариях, возможно раскроем в следующих.

Погружение в разработку на Ethereum:
Часть 1: введение
Часть 2: Web3.js и газ
Часть 3: приложение для пользователя
Часть 4: деплой и дебаг в truffle, ganache, infura

Комментарии (0)