Блокчейн: что нам стоит PoC построить?
https://habr.com/ru/post/466157/
В прошлых статьях мы разобрались с технологиями, на которых строятся блокчейны (Что нам стоит блокчейн построить?) и кейсами, которые можно с их помощью реализовать (Что нам стоит кейс построить?). Настало время поработать руками! Для реализации пилотов и PoC (Proof of Concept) я предпочитаю использовать облака, т.к. к ним есть доступ из любой точки мира и, зачастую, не надо тратить время на нудную установку окружения, т.к. есть предустановленные конфигурации. Итак, давайте сделаем что-нибудь простое, например, сеть для перевода монет между участниками и назовем ее скромно Сitcoin. Для этого будем использовать облако IBM и универсальный блокчейн Hyperledger Fabric. Для начала разберемся, почему Hyperledger Fabric называют универсальным блокчейном?
Hyperledger Fabric — универсальный блокчейн
Если говорить в общем, то универсальная информационная система это:
- Набор серверов и программное ядро, выполняющее бизнес логику;
- Интерфейсы для взаимодействия с системой;
- Средства для регистрации, аутентификации и авторизации устройств /людей;
- База данных, хранящая оперативные и архивные данные:
Официальную версию, что такое Hyperledger Fabric можно почитать на сайте, а если коротко, то Hyperledger Fabric — это opensource платформа, позволяющая строить закрытые блокчейны и выполнять произвольные смарт-контракты, написанные на языках программирования JS и Go. Посмотрим детально на архитектуру Hyperledger Fabric и убедимcя, что это универсальная система, в которой только есть специфика по хранению и записи данных. Специфика заключается в том, что данные, как и во всех блокчейнах, хранятся в блоках, которые помещаются в блокчейн только, если участники пришли к консенсусу и после записи данные невозможно незаметно исправить или удалить.
Архитектура Hyperledger Fabric
На схеме представлена архитектура Hyperledger Fabric:
Organizations — организации содержат peer-ы, т.о. блокчейн существует за счет поддержки организациями. Разные организации могут входить в один channel.
Channel — логическая структура, объединяющая peer-ы в группы, т.о. задается блокчейн. Hyperledger Fabric может одновременно обрабатывать несколько блокчейнов с разной бизнес логикой.
Membership Services Provider (MSP) — это CA (Certificate Authority) для выдачи identity и назначения ролей. Для создания ноды нужно провзаимодействовать с MSP.
Peer nodes — проверяют транзакции, хранят блокчейн, выполняют смарт-контракты и взаимодействуют с приложениями. У peer-ов есть identity (цифровой сертификат), который выдает MSP. В отличии от сети Bitcoin или Etherium, где все ноды равноправны, в Hyperledger Fabric ноды играют разные роли:
- Peer может быть endorsing peer (EP) и выполнять смарт-контракты.
- Committing peer (CP) — только сохраняют данные в блокчейне и актуализируют «World state».
- Anchor Peer (AP) — если в блокчейне участвуют несколько организаций, то анкор peer-ы используются для связи между ними. Каждая организация должна иметь один или несколько анкор peer. С помощью AP любой peer в организации может получить информацию о всех peer-ах в других организациях. Для синхронизации информации между AP используется gossip протокол.
- Leader Peer — если организация имеет несколько peer-ов, то только лидер peer будет получать блоки из Ordering service и отдавать их остальным peer-ам. Лидер может как задаваться статически, так и выбираться динамически peer-ами в организации. Для синхронизации информации о лидерах также используется gossip протокол.
Assets — сущности, имеющие ценность, которые хранятся в блокчейне. Более конкретно — это key-value данные в формате JSON. Именно эти данные и записываются в блокчейн «Blockchain». У них есть история, которая хранится в блокчейне и текущее состояние, которое хранится в базе данных «World state». Структуры данных наполняются произвольно в зависимости от бизнес задач. Нет никаких обязательных полей, единственная рекомендация — asset-ы должны иметь владельца и представлять ценность.
Ledger — состоит из блокчейна «Blockchain» и базы данных «Word state», в которой хранится текущее состояние asset-ов. World state использует LevelDB или CouchDB.
Smart contract — с помощью смарт-контрактов реализуется бизнес логика системы. В Hyperledger Fabric смарт-контракты называются chaincode. С помощью chaincode задаются asset-ы и транзакции над ними. Если говорить техническим языком, то смарт-контракты — это программные модули, реализованные на языках программирования JS или Go.
Endorsement policy — для каждого chaincode можно задать политики сколько и от кого необходимо ожидать подтверждений для транзакции. Если политика не задана, то по умолчанию используется: “транзакцию должен подтвердить любой член (member) любой организации в channel”. Примеры политик:
- Транзакцию должен подтвердить любой администратор организации;
- Должен подтвердить любой член (member) или клиент организации;
- Должен подтвердить любой peer организации.
Ordering service — упаковывает транзакции в блоки и отправляет peer-ам в channel. Гарантирует доставку сообщений всем peer-ам в сети. Для промышленных систем используется брокер сообщений Kafka, для разработки и тестирования Solo.
CallFlow
- Приложение взаимодействует с Hyperledger Fabric, используя Go, Node.js или Java SDK;
- Клиент создает транзакцию tx и посылает ее на endorsing peer-ы;
- Peer проверяет подпись клиента, выполняет транзакцию и посылает endorsement signature обратно клиенту. Chaincode выполняются только на endorsing peer, а результат его выполнения рассылается на все peer-ы. Такой алгоритм работы называется — PBFT (Practical Byzantine Fault Tolerant) консенсус. Отличается от классического BFT тем, что сообщение рассылается и ожидается подтверждение не от всех участников, а только от определенного набора;
- После того как клиент получил число ответов, соответствующее endorsement policy, он посылает транзакцию на Ordering service;
- Ordering service формирует блок и посылает его на все committing peer-ы. Ordering service обеспечивает последовательную запись блоков, что исключает, так называемый, ledger fork (см. раздел «Форки»);
- Peer-ы получают блок, еще раз проверяют endorsement policy, записывают блок в блокчейн и меняют состояние в «World state» DB.
Т.е. получается разделение ролей между нодами. Это обеспечивает масштабировать и безопасность блокчейна:
- Смарт-контракты (chaincode) выполняют endorsing peer-ы. Это обеспечивает конфиденциальность смарт-контрактов, т.к. он хранится не у всех участников, а только на endorsing peer-ах.
- Ordering должен работать быстро. Это обеспечивается тем, что Ordering только формирует блок и отправляет его на фиксированный набор leader peer-ов.
- Committing peers только хранят блокчейн — их может быть много и они не требуют большой мощности и мгновенной работы.
Подробнее архитектурные решения Hyperledger Fabric и почему он работает так, а не иначе можно посмотреть тут: Architecture Origins или тут: Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains.
Итак, Hyperledger Fabric — это действительно универсальная система, с помощью которой можно:
- Реализовывать произвольную бизнес-логику, используя механизм смарт-контрактов;
- Записывать и получать данные из блокчейн базы данных формате JSON;
- Предоставлять и проверять доступ к API, используя Certificate Authority.
Теперь, когда мы немного разобрались со спецификой Hyperledger Fabric, давайте наконец сделаем что-нибудь полезное!
Разворачиваем блокчейн
Постановка задачи
Задача — реализовать сеть Citcoin со следующими функциями: создать account, получить баланс, пополнить счет, перевести монеты с одного счета на другой. Нарисуем объектную модель, которую далее реализуем в смарт-контракте. Итак, у нас будут account-ы, которые идентифицируются именами (name) и содержат баланс (balance), и список account-ов. Account-ы и список account-ов — это в терминах Hyperledger Fabric asset-ы. Соответственно, у них есть история и текущее состояние. Попробую это наглядно нарисовать:
Верхние фигуры — это текущее состояние, которое хранится в базе «World state». Под ними фигуры, показывающие историю, которая хранится в блокчейне. Текущее состояние asset-ов изменяется транзакциями. Asset изменяется только целиком, поэтому в результате выполнения транзакции создается новый объект, а текущее значение asset-а уходит в историю.
Облако IBM
Заводим учетную запись в облаке IBM. Для использования блокчейн платформы ее надо апгрейдить до Pay-As-You-Go. Этот процесс может быть не быстрым, т.к. IBM запрашивает дополнительную информацию и проверяет ее вручную. Из положительного могу сказать, что у IBM неплохие учебные материалы, позволяющие развернуть Hyperledger Fabric в их облаке. Мне понравился следующий цикл статей и примеров:
- Create a basic blockchain network using the Blockchain Platform
- Create and execute a blockchain smart contract
- Emit events from Blockchain Platform
Далее приведены скриншоты Blockchain платформы IBM. Это не инструкция по созданию блокчейна, а просто демонстрация объема задачи. Итак, для наших целей делаем одну Organization:
В ней создаем ноды: Orderer CA, Org1 CA, Orderer Peer:
Заводим юзеров:
Создаем Channel и называем его citcoin:
По сути Channel — это блокчейн, поэтому он начинается с нулевого блока (Genesis block):
Пишем Smart Contract
/*
* Citcoin smart-contract v1.5 for Hyperledger Fabric
* (c) Alexey Sushkov, 2019
*/
'use strict';
const { Contract } = require('fabric-contract-api');
const maxAccounts = 5;
class CitcoinEvents extends Contract {
async instantiate(ctx) {
console.info('instantiate');
let emptyList = [];
await ctx.stub.putState('accounts', Buffer.from(JSON.stringify(emptyList)));
}
// Get all accounts
async GetAccounts(ctx) {
// Get account list:
let accounts = '{}'
let accountsData = await ctx.stub.getState('accounts');
if (accountsData) {
accounts = JSON.parse(accountsData.toString());
} else {
throw new Error('accounts not found');
}
return accountsData.toString()
}
// add a account object to the blockchain state identifited by their name
async AddAccount(ctx, name, balance) {
// this is account data:
let account = {
name: name,
balance: Number(balance),
type: 'account',
};
// create account:
await ctx.stub.putState(name, Buffer.from(JSON.stringify(account)));
// Add account to list:
let accountsData = await ctx.stub.getState('accounts');
if (accountsData) {
let accounts = JSON.parse(accountsData.toString());
if (accounts.length < maxAccounts)
{
accounts.push(name);
await ctx.stub.putState('accounts', Buffer.from(JSON.stringify(accounts)));
} else {
throw new Error('Max accounts number reached');
}
} else {
throw new Error('accounts not found');
}
// return object
return JSON.stringify(account);
}
// Sends money from Account to Account
async SendFrom(ctx, fromAccount, toAccount, value) {
// get Account from
let fromData = await ctx.stub.getState(fromAccount);
let from;
if (fromData) {
from = JSON.parse(fromData.toString());
if (from.type !== 'account') {
throw new Error('wrong from type');
}
} else {
throw new Error('Accout from not found');
}
// get Account to
let toData = await ctx.stub.getState(toAccount);
let to;
if (toData) {
to = JSON.parse(toData.toString());
if (to.type !== 'account') {
throw new Error('wrong to type');
}
} else {
throw new Error('Accout to not found');
}
// update the balances
if ((from.balance - Number(value)) >= 0 ) {
from.balance -= Number(value);
to.balance += Number(value);
} else {
throw new Error('From Account: not enought balance');
}
await ctx.stub.putState(from.name, Buffer.from(JSON.stringify(from)));
await ctx.stub.putState(to.name, Buffer.from(JSON.stringify(to)));
// define and set Event
let Event = {
type: "SendFrom",
from: from.name,
to: to.name,
balanceFrom: from.balance,
balanceTo: to.balance,
value: value
};
await ctx.stub.setEvent('SendFrom', Buffer.from(JSON.stringify(Event)));
// return to object
return JSON.stringify(from);
}
// get the state from key
async GetState(ctx, key) {
let data = await ctx.stub.getState(key);
let jsonData = JSON.parse(data.toString());
return JSON.stringify(jsonData);
}
// GetBalance
async GetBalance(ctx, accountName) {
let data = await ctx.stub.getState(accountName);
let jsonData = JSON.parse(data.toString());
return JSON.stringify(jsonData);
}
// Refill own balance
async RefillBalance(ctx, toAccount, value) {
// get Account to
let toData = await ctx.stub.getState(toAccount);
let to;
if (toData) {
to = JSON.parse(toData.toString());
if (to.type !== 'account') {
throw new Error('wrong to type');
}
} else {
throw new Error('Accout to not found');
}
// update the balance
to.balance += Number(value);
await ctx.stub.putState(to.name, Buffer.from(JSON.stringify(to)));
// define and set Event
let Event = {
type: "RefillBalance",
to: to.name,
balanceTo: to.balance,
value: value
};
await ctx.stub.setEvent('RefillBalance', Buffer.from(JSON.stringify(Event)));
// return to object
return JSON.stringify(from);
}
}
module.exports = CitcoinEvents;
Интуитивно тут должно быть все понятно:
- Есть несколько функций (AddAccount, GetAccounts, SendFrom, GetBalance, RefillBalance), которые будет вызывать демо программа с помощью Hyperledger Fabric API.
- Функции SendFrom и RefillBalance генерируют события (Event), которые будет получать демо программа.
- Функция instantiate — вызывается один раз при инстанциировании смарт-контракта. На самом деле, она вызывается не один раз, а каждый раз при изменении версии смарт-контракта. Поэтому инициализация списка пустым массивом — это плохая идея, т.к. теперь при смене версии смарт-контракта мы будем терять текущий список. Но ничего, я же только учусь).
- Account-ы и список account-ов (accounts) — это JSON структуры данных. Для манипуляций с данными используется JS.
- Получить текущее значение asset-а можно с помощью вызова функции getState, а обновить с помощью putState.
- При создании Account вызывается функция AddAccount, в которой производится сравнение на максимальное число account-в в блокчейне (maxAccounts = 5). И тут есть косяк (заметили?), который приводит к бесконечному росту числа account-ов. Таких ошибок надо избегать)
Далее загружаем смарт-контракт в Channel и инстанциируем его:
Смотрим транзакцию на установку Smart Contract:
Смотрим подробности о нашем Channel:
В результате получаем следующую схему блокчейн сети в облаке IBM. Также на схеме присутствует демо программа, запущенная в облаке Amazon на виртуальном сервере (подробно про нее будет в следующем разделе):
Создание GUI для вызовов Hyperledger Fabric API
У Hyperledger Fabric есть API, которое может использоваться для:
- Создания channel;
- Подсоединения peer к channel;
- Установка и инстанциирование смарт-конкрактов в channel;
- Вызов транзакций;
- Запрос информации в блокчейне.
Разработка приложения
В нашей демо программе будем использовать API только для вызова транзакций и запроса информации, т.к. остальные шаги мы уже сделали, используя блокчейн платформу IBM. Пишем GUI, используя стандартный стек технологий: Express.js + Vue.js + Node.js. О том как начать создавать современные веб-приложения можно написать отдельную статью. Здесь оставлю ссылку на серию лекций, которая мне больше всего понравилась: Full Stack Web App using Vue.js & Express.js. В результате получилось клиент-серверное приложение со знакомым графическим интерфейсом в стиле Material Design от Google. REST API между клиентом и сервером состоит из нескольких вызовов:
- HyperledgerDemo/v1/init — инициализировать блокчейн;
- HyperledgerDemo/v1/accounts/list — получить список всех account-ов;
- HyperledgerDemo/v1/account?name=Bob&balance=100 — создать Bob account;
- HyperledgerDemo/v1/info?account=Bob — получить информацию о Bob account;
- HyperledgerDemo/v1/transaction?from=Bob&to=Alice&volume=2 — перевести две монеты от Bob к Alice;
- HyperledgerDemo/v1/disconnect — закрыть соединение с блокчейном.
Описание API c примерами положил на сайт «Postman» — широко известной программы для тестирования HTTP API.
Демо приложение в облаке Amazon
Приложение залил на Amazon, т.к. IBM со сих пор не смог апгрейдить мою учетную и разрешить создавать виртуальные сервера. Как вишенку приделал домен: www.citcoin.info. Поддержу немного сервер включенным, потом выключу, т.к. центы за аренду капают, а монеты citcoin на бирже еще не котируются) В статью помещаю скриншоты демо, чтобы была понятна логика работы. Демо приложение может:
- Инициализировать блокчейн;
- Создавать Account (но сейчас новый Account не создать, т.к. в блокчейне достигнуто максимальное число account-ов, прописанное в смарт-контракте);
- Получать список Account-ов;
- Переводить монеты citcoin между Alice, Bob и Alex;
- Получать события (но сейчас события никак не показать, поэтому в интерфейсе для простоты написано, что события не поддерживаются);
- Логировать действия.
Сначала инициализируем блокчейн:
Далее заводим свой account, не мелочимся с балансом:
Получаем список всех доступных account-ов:
Выбираем отправителя и получателя, получаем их балансы. Если отправитель и получатель один и тот же, то произойдет пополнение его счета:
В логе следим за выполнением транзакций:
Собственно c демо программой на этом все. Далее можно посмотреть нашу транзакцию в блокчейне:
И общий список транзакций:
На этом мы успешно завершили реализацию PoC по созданию сети Citcoin. Что нужно еще сделать, чтобы Citcoin стал полноценной сетью для перевода монет? Совсем немного:
- На этапе создания account-а реализовать генерацию приватного / публичного ключа. Приватный ключ должен хранится у пользователя account-а, публичный в блокчейне.
- Сделать перевод монет, в котором для идентификации пользователя используется не имя, а публичный ключ.
- Шифровать транзакции, идущие от пользователя на сервер его приватным ключом.
Заключение
Мы реализовали сеть Citcoin с функциями: добавить account, получить баланс, пополнить свой счет, перевести монеты с одного счета на другой. Итак, что нам стоило PoC построить?
- Надо изучить блокчейн вообще и Hyperledger Fabric в частности;
- Научиться пользоваться облаками IBM или Amazon;
- Выучить язык программирования JS и какой-нибудь web framework;
- Если какие-то данные нужно хранить не в блокчейне, а в отдельной базе, то научиться интегрироваться, например, с PostgreSQL;
- И последнее по списку, но не по важности — без знания Linux в современном мире никуда!)
Конечно, не rocket science, но попотеть придется!
Исходники на GitHub
Исходники положил на GitHub. Краткое описание репозитория:
Каталог «server» — Node.js сервер
Каталог «client» — Node.js клиент
Каталог «blockchain» (значения параметров и ключи, разумеется, нерабочие и приведены только для примера):
- contract — исходник смарт-контракта
- wallet — ключи юзера для использования Hyperledger Fabric API.
- *.cds — скомпилированные версии смартконтрактов
- *.json файлы — примеры файлов конфигурации для использования Hyperledger Fabric API
It’s only the beginning!