Exonum — это фреймворк с открытым исходным кодом для создания приложений на основе блокчейна. Он ориентирован на работу с закрытыми блокчейнами и применим в любых сферах: FinTech, GovTech и LegalTech.
Сегодня мы проведем небольшой обзор решения, а также в рамках образовательного формата разберемся с тем, как построить простой блокчейн-based проект на Exonum. Весь код, приведенный ниже, вы найдете в репозитории на GitHub.
/ Exonum. Your next step to blockchain / Exonum
Фреймворк Exonum создавался специально для разработки приватных блокчейнов. Это система, в которой создавать новые блоки в блокчейне может только предварительно определенная группа узлов. В еe основе лежит желание специалистов Bitfury создать инструмент, который бы позволил относительно просто запустить систему, схожую по свойствам с публичными блокчейнами (надежность, неизменяемость данных, аудитируемость и т. д.), но при этом был бы более удобным в поддержании и обслуживании.
В отличие от Ethereum, который представляет собой виртуальную децентрализованную машину и выполняется одновременно на множестве узлов по всему миру, блокчейн, постороенный на Exonum, работает исключительно на вычислительных мощностях узлов-валидаторов, которые заинтересованы в работе этой системы, и будут обеспечивать ее надежное функционирование.
Развернутый на заранее определенных узлах приватный блокчейн Exonum как минимум иcключает возможность его внезапного хард-форка, засорения пула транзакций и других проблем, характерных для открытых блокчейнов, а операторы узлов следят за его эффективной работой: обновляют правила обработки транзакций и т. п.
Помимо этого, выполнение смарт-контрактов на Ethereum сильно зависит от колебания курса криптовалюты — эфира, что делает его непредсказуемым для использования, например, в государственных структурах, которые не могут оплачивать выполнение транзакций валютой, находящейся в нерегулируемой «серой зоне». В Exonum подобные зависимости отсутствуют в принципе.
И наконец Exonum-блокчейн работает существенно быстрее, чем публичные блокчейны (Bitcoin, Ethereum и др.), а именно обрабатывает несколько тысяч транзакций в секунду против нескольких десятков, обрабатываемых последними. Выбор стратегии обусловлен общей тенденцией к созданию большого количества независимых блокчейнов, которые бы взаимодействовали между собой посредством технологий сайдчейнов, привязки к публичным блокчейнам (анкоринг) и т. д.
Главными компонентами Exonum являются: византийский консенсус, легкие клиенты, привязка к биткойну и сервисы.
В системе используется особый алгоритм византийского консенсуса для синхронизации данных среди узлов. Он гарантирует целостность данных и корректное выполнение транзакций даже в случае выхода из строя вплоть до 1/3 узлов из-за неисправности или намеренной зловредной деятельности, при этом не требуя майнинга блоков.
Говоря о преимуществах Exonum над существующими аналогами можно отметить развитую модель данных (storage), которая представляет собой индексы, содержащие зависимости друг от друга (по сути таблицы) — они позволяют реализовать эффективную структуру данных, направленную на решение частных задач. Клиенты такого блокчейна могут получать криптографические доказательства корректности загружаемых данных (деревья Меркла), которые проверяются локально на машине клиента, и не могут быть подделаны даже оператором узла Exonum.
Легкие клиенты — это узлы сети, которые хранят у себя только небольшую часть блокчейна, представляющую интерес. Они позволяют взаимодействовать с блокчейном с помощью мобильных приложений или веб-браузеров. Клиенты «общаются» с одним и более сервисами на полнофункциональном узле через API. Работа таких тонких клиентов специфична для каждого отдельного сервиса и реализована настолько сложно, насколько того требует конкретный сервис.
Суть работы тонких клиентов Exonum и построения доказательств сводится к тому, что конечный пользователь, который осуществил привязку к биткойн-блокчейну, может не доверять оператору приватного блокчейна. Но может быть уверен, что данные, которые у него отображаются, получены в соответствии с правилами, заложенными в этот конкретный приватный блокчейн.
Защищенность легких клиентов в Exonum, сравнимую с той, которую предоставляет permissionless-блокчейн, обеспечивает уже упомянутая выше привязка к биткойну, так называемый анкоринг. Сервис периодически высылает хеши блоков в общедоступный биткойн-блокчейн в формате транзакций-свидетельств. В этом случае, даже если Exonum-блокчейн прекратит работать, данные все равно можно будет верифицировать. Более того, для атаки на такую сеть злоумышленникам приходится преодолевать защитные механизмы обоих блокчейнов, что требует колоссальных вычислительных мощностей.
И, наконец, сервисы — это основа фреймворка Exonum. Они напоминают смарт-контракты на других платформах и содержат бизнес-логику блокчейн-приложений. Но, в отличие от умных контрактов, сервисы в Exonum не «заперты» в виртуальной машине и не контейнеризованы.
Это делает их более эффективными и гибкими. Однако такой подход требует большей осторожности при программировании (изоляция сервисов отмечена на дорожной карте Exonum). Сервисы определяют правила обработки транзакций, а также открывают доступ к данным внешним клиентам.
Ознакомившись с основными компонентами, мы можем переходить к разбору примера.
Второго ноября состоялся релиз версии Exonum 0.3, и дальнейшее руководство написано с учетом внесенных изменений и усовершенствований в систему (о них вы можете прочитать в репозитории на GitHub). Мы создадим блокчейн с одним узлом, который реализует криптовалюту. Сеть будет принимать два типа транзакций: «создать кошелек» и «перевести средства с одного кошелька на другой».
Exonum написан на Rust, поэтому вам нужно установить компилятор. Для этого вы можете воспользоваться нашим руководством.
Для начала создадим новый crate:
И добавим необходимые зависимости в созданный cargo.toml:
Импортируем crate с необходимыми типами. Для этого нужно подредактировать файл src/main.rs:
Определим константы:
И функцию main:
Все это позволяет настроить логгер, который будет выводить информацию об активности узлов Exonum в консоль.
Чтобы сформировать сам блокчейн, нужно создать экземпляр базы данных (в нашем случае MemoryDB, однако можно воспользоваться и RocksDB) и объявить список сервисов. Помещаем этот код после инициализации логгера:
По сути, блокчейн готов, однако взаимодействовать с ним не получится — у нас еще нет узла и API для обращения к нему. Узел потребуется сконфигурировать. В конфигурации указывается список открытых ключей валидаторов (в нашем случае он будет один). По сути каждому узлу требуется две пары открытых и закрытых ключей: одна для взаимодействия с другими узлами в процессе достижения консенсуса, а вторая — для сервисов. Для нашего примера создаем временные открытые ключи командой exonum::crypto::gen_keypair() и прописываем их в файл конфигурации.
Далее, настраиваем REST API для работы с внешними веб-запросами — для этого открываем порт 8000. Также откроем порт 2000, чтобы полные узлы Exonum-сети могли общаться друг с другом.
На этом этапе нам нужно определить, какие данные мы хотим хранить в блокчейне. В нашем случае — это информация о кошельке и балансе, публичный ключ для проверки запросов от владельца кошелька и имя владельца. Структура будет выглядеть следующим образом:
Макрос encoding_struct! помогает объявить упорядочиваемую структуру и обозначить границы полей значений. Нам нужно изменять баланс кошелька, потому добавим методы в Wallet:
Также нужно сформировать хранилище «ключ-значение» в MemoryDB. Для этого мы используем форк, чтобы иметь возможность в крайнем случае откатить все изменения.
Однако форк дает доступ к любой информации в базе данных. Чтобы изолировать кошельки, добавим уникальный префикс и используем карту абстракций MapIndex.
Как уже было отмечено, для работы нашего образовательного примера нам понадобятся следующие типы транзакций: создать кошелек и добавить в него средства, а также перевести их на другой кошелек.
Транзакция для создания кошелька должна содержать его открытый ключ и имя пользователя.
Перед созданием кошелька будем проверять его уникальность. Также зачислим на него 100 монет.
Транзакция для перевода денег выглядит так:
В ней отмечены два публичных ключа (для обоих кошельков) и количество монет, которые переводятся. Поле seed добавлено для того, чтобы транзакцию было невозможно повторить. Также нужно проверить, что отправитель не пересылает средства самому себе:
Для того чтобы транзакции корректно отображались в обозревателе блоков блокчейна, нам также необходимо переопределить метод `info()`. Реализация будет одинаковой для обоих типов транзакций и будет выглядеть следующим образом:
Для этого создадим структуру с каналом и экземпляром блокчейна, который будет необходим для реализации запросов на чтение:
Чтобы упростить обработку процессов, добавим TransactionRequest enum, объединяющий оба типа транзакций: «создать кошелек» и «перевести средства».
Осталось «подружить» наш обработчик с HTTP-обработчиком веб-сервера. Для этого реализуем метод wire. В приведенном ниже примере мы добавим обработчик, который конвертирует ввод JSON в Transaction.
Для того чтобы иметь возможность проверить, что транзакции действительно выполняются, реализуем два вида запросов на чтение: возврат информации о всех кошельках системы и возврат информации только о конкретном кошельке, соответствующем публичному ключу.
Для этого определим пару методов в CryptocurrencyApi, которые будут обращаться к полю blockchain для чтения информации из хранилища блокчейна.
Стоит обратить внимание на то, что в данном случае мы используем метод fork, несмотря на то, что он дает доступ на запись и чтение данных (чтобы не перегружать пример). В реальных условиях целесообразно использовать формат доступа только на чтение (обращаясь к снапшотам).
Далее, также как и для транзакций, добавляем обработку запросов при помощи методов get_wallets() и get_wallet() в CryptocurrencyApi::wire().
Чтобы превратить структуру CurrencyService в блокчейн-сервис, мы должны назначить ей свойство Service. Оно имеет два метода: service_name, который возвращает имя нашего сервиса, и service_id, возвращающий его уникальный ID.
Метод tx_from_raw будет использоваться для десериализации транзакций, а метод public_api_handler — для создания REST Handler для обработки веб-запросов к узлу. Он будет применять логику, уже определенную в CryptocurrencyApi.
Мы реализовали все части нашего мини-блокчейна. Теперь осталось добавить CryptocyrrencyService в список сервисов блокчейна и запустить демо:
Exonum позволяет протестировать работу сервисов. Для этого используется пакет Sandbox — он симулирует работу сети. Мы можем отправить запрос к узлу и получить ответ, а затем пронаблюдать за происходящими изменениями в блокчейне. Инстанс Sandbox создается методом sandbox_with_services, позволяющим специфицировать сервисы для тестирования. Например, вот так:
В целом Sandbox может симулировать процесс получения сообщения узлом, проверять, какой узел его отправил, и что в нем находилось. Также «песочница» может работать со временем, например, моделировать истечение какого-либо временного периода.
Теперь попробуем отправить несколько транзакций в нашей демоверсии блокчейна. Сперва создаем кошелек. Так будет выглядеть файл create-wallet-1.json:
Используем команду curl, чтобы отправить транзакцию по HTTP:
После этого в консоли мы увидим, что кошелек был создан:
Второй кошелек формируется аналогично. После его создания можем перевести средства. Файл transfer-funds.json выглядит так:
Эта транзакция переводит 10 монет из первого кошелька на второй. Отправим команду узлу с помощью curl:
Узел покажет, что сумма успешно переведена:
А теперь проверим, что конечная точка обработки запросов на чтение действительно работает. Состояние обоих кошельков в системе мы можем запросить следующим образом:
Данный запрос выдаст информацию о кошельках в таком виде:
Вторая конечная точка также работает. Мы можем убедиться в этом, отправив следующий запрос:
Получаем ответ:
Таким образом, в рамках нашего образовательного материала мы разобрались с тем, как работает простой блокчейн с одним валидатором. В следующих постах мы подробнее поговорим о привязке к блокчейнам, управлении узлами и консенсусе в Exonum.
Сегодня мы проведем небольшой обзор решения, а также в рамках образовательного формата разберемся с тем, как построить простой блокчейн-based проект на Exonum. Весь код, приведенный ниже, вы найдете в репозитории на GitHub.
/ Exonum. Your next step to blockchain / Exonum
Exonum в двух словах
Фреймворк Exonum создавался специально для разработки приватных блокчейнов. Это система, в которой создавать новые блоки в блокчейне может только предварительно определенная группа узлов. В еe основе лежит желание специалистов Bitfury создать инструмент, который бы позволил относительно просто запустить систему, схожую по свойствам с публичными блокчейнами (надежность, неизменяемость данных, аудитируемость и т. д.), но при этом был бы более удобным в поддержании и обслуживании.
В отличие от Ethereum, который представляет собой виртуальную децентрализованную машину и выполняется одновременно на множестве узлов по всему миру, блокчейн, постороенный на Exonum, работает исключительно на вычислительных мощностях узлов-валидаторов, которые заинтересованы в работе этой системы, и будут обеспечивать ее надежное функционирование.
Развернутый на заранее определенных узлах приватный блокчейн Exonum как минимум иcключает возможность его внезапного хард-форка, засорения пула транзакций и других проблем, характерных для открытых блокчейнов, а операторы узлов следят за его эффективной работой: обновляют правила обработки транзакций и т. п.
Помимо этого, выполнение смарт-контрактов на Ethereum сильно зависит от колебания курса криптовалюты — эфира, что делает его непредсказуемым для использования, например, в государственных структурах, которые не могут оплачивать выполнение транзакций валютой, находящейся в нерегулируемой «серой зоне». В Exonum подобные зависимости отсутствуют в принципе.
И наконец Exonum-блокчейн работает существенно быстрее, чем публичные блокчейны (Bitcoin, Ethereum и др.), а именно обрабатывает несколько тысяч транзакций в секунду против нескольких десятков, обрабатываемых последними. Выбор стратегии обусловлен общей тенденцией к созданию большого количества независимых блокчейнов, которые бы взаимодействовали между собой посредством технологий сайдчейнов, привязки к публичным блокчейнам (анкоринг) и т. д.
Главными компонентами Exonum являются: византийский консенсус, легкие клиенты, привязка к биткойну и сервисы.
В системе используется особый алгоритм византийского консенсуса для синхронизации данных среди узлов. Он гарантирует целостность данных и корректное выполнение транзакций даже в случае выхода из строя вплоть до 1/3 узлов из-за неисправности или намеренной зловредной деятельности, при этом не требуя майнинга блоков.
Говоря о преимуществах Exonum над существующими аналогами можно отметить развитую модель данных (storage), которая представляет собой индексы, содержащие зависимости друг от друга (по сути таблицы) — они позволяют реализовать эффективную структуру данных, направленную на решение частных задач. Клиенты такого блокчейна могут получать криптографические доказательства корректности загружаемых данных (деревья Меркла), которые проверяются локально на машине клиента, и не могут быть подделаны даже оператором узла Exonum.
Легкие клиенты — это узлы сети, которые хранят у себя только небольшую часть блокчейна, представляющую интерес. Они позволяют взаимодействовать с блокчейном с помощью мобильных приложений или веб-браузеров. Клиенты «общаются» с одним и более сервисами на полнофункциональном узле через API. Работа таких тонких клиентов специфична для каждого отдельного сервиса и реализована настолько сложно, насколько того требует конкретный сервис.
Суть работы тонких клиентов Exonum и построения доказательств сводится к тому, что конечный пользователь, который осуществил привязку к биткойн-блокчейну, может не доверять оператору приватного блокчейна. Но может быть уверен, что данные, которые у него отображаются, получены в соответствии с правилами, заложенными в этот конкретный приватный блокчейн.
Защищенность легких клиентов в Exonum, сравнимую с той, которую предоставляет permissionless-блокчейн, обеспечивает уже упомянутая выше привязка к биткойну, так называемый анкоринг. Сервис периодически высылает хеши блоков в общедоступный биткойн-блокчейн в формате транзакций-свидетельств. В этом случае, даже если Exonum-блокчейн прекратит работать, данные все равно можно будет верифицировать. Более того, для атаки на такую сеть злоумышленникам приходится преодолевать защитные механизмы обоих блокчейнов, что требует колоссальных вычислительных мощностей.
И, наконец, сервисы — это основа фреймворка Exonum. Они напоминают смарт-контракты на других платформах и содержат бизнес-логику блокчейн-приложений. Но, в отличие от умных контрактов, сервисы в Exonum не «заперты» в виртуальной машине и не контейнеризованы.
Это делает их более эффективными и гибкими. Однако такой подход требует большей осторожности при программировании (изоляция сервисов отмечена на дорожной карте Exonum). Сервисы определяют правила обработки транзакций, а также открывают доступ к данным внешним клиентам.
Ознакомившись с основными компонентами, мы можем переходить к разбору примера.
Создание сервисов в Exonum
Второго ноября состоялся релиз версии Exonum 0.3, и дальнейшее руководство написано с учетом внесенных изменений и усовершенствований в систему (о них вы можете прочитать в репозитории на GitHub). Мы создадим блокчейн с одним узлом, который реализует криптовалюту. Сеть будет принимать два типа транзакций: «создать кошелек» и «перевести средства с одного кошелька на другой».
Exonum написан на Rust, поэтому вам нужно установить компилятор. Для этого вы можете воспользоваться нашим руководством.
Создание узла
Для начала создадим новый crate:
cargo new --bin cryptocurrency
И добавим необходимые зависимости в созданный cargo.toml:
[package]
name = "cryptocurrency"
version = "0.3.0"
authors = ["Your Name <your@email.com>"]
[dependencies]
iron = "0.5.1"
bodyparser = "0.7.0"
router = "0.5.1"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
exonum = "0.3.0"
Импортируем crate с необходимыми типами. Для этого нужно подредактировать файл src/main.rs:
extern crate serde;
extern crate serde_json;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate exonum;
extern crate router;
extern crate bodyparser;
extern crate iron;
use exonum::blockchain::{Blockchain, Service, GenesisConfig,
ValidatorKeys, Transaction, ApiContext};
use exonum::node::{Node, NodeConfig, NodeApiConfig, TransactionSend,
ApiSender };
use exonum::messages::{RawTransaction, FromRaw, Message};
use exonum::storage::{Fork, MemoryDB, MapIndex};
use exonum::crypto::{PublicKey, Hash, HexValue};
use exonum::encoding::{self, Field};
use exonum::api::{Api, ApiError};
use iron::prelude::*;
use iron::Handler;
use router::Router;
Определим константы:
// Service identifier
const SERVICE_ID: u16 = 1;
// Identifier for wallet creation transaction type
const TX_CREATE_WALLET_ID: u16 = 1;
// Identifier for coins transfer transaction type
const TX_TRANSFER_ID: u16 = 2;
// Starting balance of a newly created wallet
const INIT_BALANCE: u64 = 100;
И функцию main:
fn main() {
exonum::helpers::init_logger().unwrap();
}
Все это позволяет настроить логгер, который будет выводить информацию об активности узлов Exonum в консоль.
Чтобы сформировать сам блокчейн, нужно создать экземпляр базы данных (в нашем случае MemoryDB, однако можно воспользоваться и RocksDB) и объявить список сервисов. Помещаем этот код после инициализации логгера:
let db = MemoryDB::new();
let services: Vec<Box<Service>> = vec![ ];
let blockchain = Blockchain::new(Box::new(db), services);
По сути, блокчейн готов, однако взаимодействовать с ним не получится — у нас еще нет узла и API для обращения к нему. Узел потребуется сконфигурировать. В конфигурации указывается список открытых ключей валидаторов (в нашем случае он будет один). По сути каждому узлу требуется две пары открытых и закрытых ключей: одна для взаимодействия с другими узлами в процессе достижения консенсуса, а вторая — для сервисов. Для нашего примера создаем временные открытые ключи командой exonum::crypto::gen_keypair() и прописываем их в файл конфигурации.
let validator_keys = ValidatorKeys {
consensus_key: consensus_public_key,
service_key: service_public_key,
};
let genesis = GenesisConfig::new(vec![validator_keys].into_iter());
Далее, настраиваем REST API для работы с внешними веб-запросами — для этого открываем порт 8000. Также откроем порт 2000, чтобы полные узлы Exonum-сети могли общаться друг с другом.
let api_address = "0.0.0.0:8000".parse().unwrap();
let api_cfg = NodeApiConfig {
public_api_address: Some(api_address),
..Default::default()
};
let peer_address = "0.0.0.0:2000".parse().unwrap();
// Complete node configuration
let node_cfg = NodeConfig {
listen_address: peer_address,
peers: vec![],
service_public_key,
service_secret_key,
consensus_public_key,
consensus_secret_key,
genesis,
external_address: None,
network: Default::default(),
whitelist: Default::default(),
api: api_cfg,
mempool: Default::default(),
services_configs: Default::default(),
};
let node = Node::new(blockchain, node_cfg);
node.run().unwrap();
Объявляем данные
На этом этапе нам нужно определить, какие данные мы хотим хранить в блокчейне. В нашем случае — это информация о кошельке и балансе, публичный ключ для проверки запросов от владельца кошелька и имя владельца. Структура будет выглядеть следующим образом:
encoding_struct! {
struct Wallet {
const SIZE = 48;
field pub_key: &PublicKey [00 => 32]
field name: &str [32 => 40]
field balance: u64 [40 => 48]
}
}
Макрос encoding_struct! помогает объявить упорядочиваемую структуру и обозначить границы полей значений. Нам нужно изменять баланс кошелька, потому добавим методы в Wallet:
impl Wallet {
pub fn increase(self, amount: u64) -> Self {
let balance = self.balance() + amount;
Self::new(self.pub_key(), self.name(), balance)
}
pub fn decrease(self, amount: u64) -> Self {
let balance = self.balance() - amount;
Self::new(self.pub_key(), self.name(), balance)
}
}
Также нужно сформировать хранилище «ключ-значение» в MemoryDB. Для этого мы используем форк, чтобы иметь возможность в крайнем случае откатить все изменения.
pub struct CurrencySchema<'a> {
view: &'a mut Fork,
}
Однако форк дает доступ к любой информации в базе данных. Чтобы изолировать кошельки, добавим уникальный префикс и используем карту абстракций MapIndex.
impl<'a> CurrencySchema<'a> {
pub fn wallets(&mut self) -> MapIndex<&mut Fork, PublicKey, Wallet> {
let prefix = blockchain::gen_prefix(SERVICE_ID, 0, &());
MapIndex::new("cryptocurrency.wallets", self.view)
}
// Utility method to quickly get a separate wallet from the storage
pub fn wallet(&mut self, pub_key: &PublicKey) -> Option<Wallet> {
self.wallets().get(pub_key)
}
}
Определяем транзакции
Как уже было отмечено, для работы нашего образовательного примера нам понадобятся следующие типы транзакций: создать кошелек и добавить в него средства, а также перевести их на другой кошелек.
Транзакция для создания кошелька должна содержать его открытый ключ и имя пользователя.
message! {
struct TxCreateWallet {
const TYPE = SERVICE_ID;
const ID = TX_CREATE_WALLET_ID;
const SIZE = 40;
field pub_key: &PublicKey [00 => 32]
field name: &str [32 => 40]
}
}
Перед созданием кошелька будем проверять его уникальность. Также зачислим на него 100 монет.
impl Transaction for TxCreateWallet {
fn verify(&self) -> bool {
self.verify_signature(self.pub_key())
}
fn execute(&self, view: &mut Fork) {
let mut schema = CurrencySchema { view };
if schema.wallet(self.pub_key()).is_none() {
let wallet = Wallet::new(self.pub_key(),
self.name(),
INIT_BALANCE);
println!("Create the wallet: {:?}", wallet);
schema.wallets().put(self.pub_key(), wallet)
}
}
}
Транзакция для перевода денег выглядит так:
message! {
struct TxTransfer {
const TYPE = SERVICE_ID;
const ID = TX_TRANSFER_ID;
const SIZE = 80;
field from: &PublicKey [00 => 32]
field to: &PublicKey [32 => 64]
field amount: u64 [64 => 72]
field seed: u64 [72 => 80]
}
}
В ней отмечены два публичных ключа (для обоих кошельков) и количество монет, которые переводятся. Поле seed добавлено для того, чтобы транзакцию было невозможно повторить. Также нужно проверить, что отправитель не пересылает средства самому себе:
impl Transaction for TxTransfer {
fn verify(&self) -> bool {
(*self.from() != *self.to()) &&
self.verify_signature(self.from())
}
fn execute(&self, view: &mut Fork) {
let mut schema = CurrencySchema { view };
let sender = schema.wallet(self.from());
let receiver = schema.wallet(self.to());
if let (Some(mut sender), Some(mut receiver)) = (sender, receiver) {
let amount = self.amount();
if sender.balance() >= amount {
let sender.decrease(amount);
let receiver.increase(amount);
println!("Transfer between wallets: {:?} => {:?}",
sender,
receiver);
let mut wallets = schema.wallets();
wallets.put(self.from(), sender);
wallets.put(self.to(), receiver);
}
}
}
}
Для того чтобы транзакции корректно отображались в обозревателе блоков блокчейна, нам также необходимо переопределить метод `info()`. Реализация будет одинаковой для обоих типов транзакций и будет выглядеть следующим образом:
impl Transaction for TxCreateWallet {
// `verify()` and `execute()` code...
fn info(&self) -> serde_json::Value {
serde_json::to_value(&self)
.expect("Cannot serialize transaction to JSON")
}
}
Реализуем API для транзакций
Для этого создадим структуру с каналом и экземпляром блокчейна, который будет необходим для реализации запросов на чтение:
#[derive(Clone)]
struct CryptocurrencyApi {
channel: ApiSender,
blockchain: Blockchain,
}
Чтобы упростить обработку процессов, добавим TransactionRequest enum, объединяющий оба типа транзакций: «создать кошелек» и «перевести средства».
#[serde(untagged)]
#[derive(Clone, Serialize, Deserialize)]
enum TransactionRequest {
CreateWallet(TxCreateWallet),
Transfer(TxTransfer),
}
impl Into<Box<Transaction>> for TransactionRequest {
fn into(self) -> Box<Transaction> {
match self {
TransactionRequest::CreateWallet(trans) => Box::new(trans),
TransactionRequest::Transfer(trans) => Box::new(trans),
}
}
}
#[derive(Serialize, Deserialize)]
struct TransactionResponse {
tx_hash: Hash,
}
Осталось «подружить» наш обработчик с HTTP-обработчиком веб-сервера. Для этого реализуем метод wire. В приведенном ниже примере мы добавим обработчик, который конвертирует ввод JSON в Transaction.
impl Api for CryptocurrencyApi {
fn wire(&self, router: &mut Router) {
let self_ = self.clone();
let tx_handler = move |req: &mut Request| -> IronResult<Response> {
match req.get::<bodyparser::Struct<TransactionRequest>>() {
Ok(Some(tx)) => {
let tx: Box<Transaction> = tx.into();
let tx_hash = tx.hash();
self_.channel.send(tx).map_err(ApiError::from)?;
let json = TransactionResponse { tx_hash };
self_.ok_response(&serde_json::to_value(&json).unwrap())
}
Ok(None) => Err(ApiError::IncorrectRequest(
"Empty request body".into()))?,
Err(e) => Err(ApiError::IncorrectRequest(Box::new(e)))?,
}
};
// (Read request processing skipped)
// Bind the transaction handler to a specific route.
router.post("/v1/wallets/transaction", transaction, "transaction");
// (Read request binding skipped)
}
}
Реализуем API для запросов на чтение
Для того чтобы иметь возможность проверить, что транзакции действительно выполняются, реализуем два вида запросов на чтение: возврат информации о всех кошельках системы и возврат информации только о конкретном кошельке, соответствующем публичному ключу.
Для этого определим пару методов в CryptocurrencyApi, которые будут обращаться к полю blockchain для чтения информации из хранилища блокчейна.
impl CryptocurrencyApi {
fn get_wallet(&self, pub_key: &PublicKey) -> Option<Wallet> {
let mut view = self.blockchain.fork();
let mut schema = CurrencySchema { view: &mut view };
schema.wallet(pub_key)
}
fn get_wallets(&self) -> Option<Vec<Wallet>> {
let mut view = self.blockchain.fork();
let mut schema = CurrencySchema { view: &mut view };
let idx = schema.wallets();
let wallets: Vec<Wallet> = idx.values().collect();
if wallets.is_empty() {
None
} else {
Some(wallets)
}
}
}
Стоит обратить внимание на то, что в данном случае мы используем метод fork, несмотря на то, что он дает доступ на запись и чтение данных (чтобы не перегружать пример). В реальных условиях целесообразно использовать формат доступа только на чтение (обращаясь к снапшотам).
Далее, также как и для транзакций, добавляем обработку запросов при помощи методов get_wallets() и get_wallet() в CryptocurrencyApi::wire().
impl Api for CryptocurrencyApi {
fn wire(&self, router: &mut Router) {
let self_ = self.clone();
// (Transaction processing skipped)
// Gets status of all wallets in the database.
let self_ = self.clone();
let wallets_info = move |_: &mut Request| -> IronResult<Response> {
if let Some(wallets) = self_.get_wallets() {
self_.ok_response(&serde_json::to_value(wallets).unwrap())
} else {
self_.not_found_response(
&serde_json::to_value("Wallets database is empty")
.unwrap(),
)
}
};
// Gets status of the wallet corresponding to the public key.
let self_ = self.clone();
let wallet_info = move |req: &mut Request| -> IronResult<Response> {
// Get the hex public key as the last URL component;
// return an error if the public key cannot be parsed.
let path = req.url.path();
let wallet_key = path.last().unwrap();
let public_key = PublicKey::from_hex(wallet_key)
.map_err(ApiError::FromHex)?;
if let Some(wallet) = self_.get_wallet(&public_key) {
self_.ok_response(&serde_json::to_value(wallet).unwrap())
} else {
self_.not_found_response(
&serde_json::to_value("Wallet not found").unwrap(),
)
}
};
// (Transaction binding skipped)
// Bind read request endpoints.
router.get("/v1/wallets", wallets_info, "wallets_info");
router.get("/v1/wallet/:pub_key", wallet_info, "wallet_info");
}
Определяем сервис
Чтобы превратить структуру CurrencyService в блокчейн-сервис, мы должны назначить ей свойство Service. Оно имеет два метода: service_name, который возвращает имя нашего сервиса, и service_id, возвращающий его уникальный ID.
Метод tx_from_raw будет использоваться для десериализации транзакций, а метод public_api_handler — для создания REST Handler для обработки веб-запросов к узлу. Он будет применять логику, уже определенную в CryptocurrencyApi.
impl Service for CurrencyService {
fn service_name(&self) -> &'static str { "cryptocurrency" }
fn service_id(&self) -> u16 { SERVICE_ID }
fn tx_from_raw(&self, raw: RawTransaction)
-> Result<Box<Transaction>, encoding::Error> {
let trans: Box<Transaction> = match raw.message_type() {
TX_TRANSFER_ID => Box::new(TxTransfer::from_raw(raw)?),
TX_CREATE_WALLET_ID => Box::new(TxCreateWallet::from_raw(raw)?),
_ => {
return Err(encoding::Error::IncorrectMessageType {
message_type: raw.message_type()
});
},
};
Ok(trans)
}
fn public_api_handler(&self, ctx: &ApiContext) -> Option<Box<Handler>> {
let mut router = Router::new();
let api = CryptocurrencyApi {
channel: ctx.node_channel().clone(),
blockchain: ctx.blockchain().clone(),
};
api.wire(&mut router);
Some(Box::new(router))
}
}
Мы реализовали все части нашего мини-блокчейна. Теперь осталось добавить CryptocyrrencyService в список сервисов блокчейна и запустить демо:
let services: Vec<Box<Service>> = vec![
Box::new(CurrencyService),
];
cargo run
Тестирование сервисов
Exonum позволяет протестировать работу сервисов. Для этого используется пакет Sandbox — он симулирует работу сети. Мы можем отправить запрос к узлу и получить ответ, а затем пронаблюдать за происходящими изменениями в блокчейне. Инстанс Sandbox создается методом sandbox_with_services, позволяющим специфицировать сервисы для тестирования. Например, вот так:
let s = sandbox_with_services(vec![Box::new(CurrencyService::new()),
Box::new(ConfigUpdateService::new())]);
В целом Sandbox может симулировать процесс получения сообщения узлом, проверять, какой узел его отправил, и что в нем находилось. Также «песочница» может работать со временем, например, моделировать истечение какого-либо временного периода.
Отправка транзакций
Теперь попробуем отправить несколько транзакций в нашей демоверсии блокчейна. Сперва создаем кошелек. Так будет выглядеть файл create-wallet-1.json:
{
"body": {
"pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472",
"name": "Johnny Doe"
},
"network_id": 0,
"protocol_version": 0,
"service_id": 1,
"message_id": 1,
"signature": "ad5efdb52e48309df9aa582e67372bb3ae67828c5eaa1a7a5e387597174055d315eaa7879912d0509acf17f06a23b7f13f242017b354f682d85930fa28240402"
}
Используем команду curl, чтобы отправить транзакцию по HTTP:
curl -H "Content-Type: application/json" -X POST -d @create-wallet-1.json \
http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction
После этого в консоли мы увидим, что кошелек был создан:
Create the wallet: Wallet { pub_key: PublicKey(3E657AE),
name: "Johnny Doe", balance: 100 }
Второй кошелек формируется аналогично. После его создания можем перевести средства. Файл transfer-funds.json выглядит так:
{
"body": {
"from": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472",
"to": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819",
"amount": "10",
"seed": "12623766328194547469"
},
"network_id": 0,
"protocol_version": 0,
"service_id": 1,
"message_id": 2,
"signature": "2c5e9eee1b526299770b3677ffd0d727f693ee181540e1914f5a84801dfd410967fce4c22eda621701c2b9c676ed62bc48df9c973462a8514ffb32bec202f103"
}
Эта транзакция переводит 10 монет из первого кошелька на второй. Отправим команду узлу с помощью curl:
curl -H "Content-Type: application/json" -X POST -d @transfer-funds.json \
http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction
Узел покажет, что сумма успешно переведена:
Transfer between wallets: Wallet { pub_key: PublicKey(3E657AE),
name: "Johnny Doe", balance: 90 }
=> Wallet { pub_key: PublicKey(D1E87747),
name: "Janie Roe", balance: 110 }
А теперь проверим, что конечная точка обработки запросов на чтение действительно работает. Состояние обоих кошельков в системе мы можем запросить следующим образом:
curl http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets
Данный запрос выдаст информацию о кошельках в таком виде:
[
{
"balance": "90",
"name": "Johnny Doe",
"pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472"
},
{
"balance": "110",
"name": "Janie Roe",
"pub_key": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819"
}
]
Вторая конечная точка также работает. Мы можем убедиться в этом, отправив следующий запрос:
curl "http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallet/\
03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472"
Получаем ответ:
{
"balance": "90",
"name": "Johnny Doe",
"pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472"
}
Таким образом, в рамках нашего образовательного материала мы разобрались с тем, как работает простой блокчейн с одним валидатором. В следующих постах мы подробнее поговорим о привязке к блокчейнам, управлении узлами и консенсусе в Exonum.