Блокчейн: что нам стоит PoC построить?

Блокчейн: что нам стоит PoC построить?

https://habr.com/ru/post/466157/

Глаза боятся, а руки чешутся!

В прошлых статьях мы разобрались с технологиями, на которых строятся блокчейны (Что нам стоит блокчейн построить?) и кейсами, которые можно с их помощью реализовать (Что нам стоит кейс построить?). Настало время поработать руками! Для реализации пилотов и PoC (Proof of Concept) я предпочитаю использовать облака, т.к. к ним есть доступ из любой точки мира и, зачастую, не надо тратить время на нудную установку окружения, т.к. есть предустановленные конфигурации. Итак, давайте сделаем что-нибудь простое, например, сеть для перевода монет между участниками и назовем ее скромно Сitcoin. Для этого будем использовать облако IBM и универсальный блокчейн Hyperledger Fabric. Для начала разберемся, почему Hyperledger Fabric называют универсальным блокчейном?

image

Hyperledger Fabric — универсальный блокчейн

Если говорить в общем, то универсальная информационная система это:

  • Набор серверов и программное ядро, выполняющее бизнес логику;
  • Интерфейсы для взаимодействия с системой;
  • Средства для регистрации, аутентификации и авторизации устройств /людей;
  • База данных, хранящая оперативные и архивные данные:

image

Официальную версию, что такое Hyperledger Fabric можно почитать на сайте, а если коротко, то Hyperledger Fabric — это opensource платформа, позволяющая строить закрытые блокчейны и выполнять произвольные смарт-контракты, написанные на языках программирования JS и Go. Посмотрим детально на архитектуру Hyperledger Fabric и убедимcя, что это универсальная система, в которой только есть специфика по хранению и записи данных. Специфика заключается в том, что данные, как и во всех блокчейнах, хранятся в блоках, которые помещаются в блокчейн только, если участники пришли к консенсусу и после записи данные невозможно незаметно исправить или удалить.

Архитектура Hyperledger Fabric

На схеме представлена архитектура Hyperledger Fabric:

image

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

image

  • Приложение взаимодействует с 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-ы. Соответственно, у них есть история и текущее состояние. Попробую это наглядно нарисовать:

image

Верхние фигуры — это текущее состояние, которое хранится в базе «World state». Под ними фигуры, показывающие историю, которая хранится в блокчейне. Текущее состояние asset-ов изменяется транзакциями. Asset изменяется только целиком, поэтому в результате выполнения транзакции создается новый объект, а текущее значение asset-а уходит в историю.

Облако IBM

Заводим учетную запись в облаке IBM. Для использования блокчейн платформы ее надо апгрейдить до Pay-As-You-Go. Этот процесс может быть не быстрым, т.к. IBM запрашивает дополнительную информацию и проверяет ее вручную. Из положительного могу сказать, что у IBM неплохие учебные материалы, позволяющие развернуть Hyperledger Fabric в их облаке. Мне понравился следующий цикл статей и примеров:

Далее приведены скриншоты Blockchain платформы IBM. Это не инструкция по созданию блокчейна, а просто демонстрация объема задачи. Итак, для наших целей делаем одну Organization:

image

В ней создаем ноды: Orderer CA, Org1 CA, Orderer Peer:

image

Заводим юзеров:

image

Создаем Channel и называем его citcoin:

image

По сути Channel — это блокчейн, поэтому он начинается с нулевого блока (Genesis block):

image

Пишем 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 и инстанциируем его:

image

Смотрим транзакцию на установку Smart Contract:

image

Смотрим подробности о нашем Channel:

image

В результате получаем следующую схему блокчейн сети в облаке IBM. Также на схеме присутствует демо программа, запущенная в облаке Amazon на виртуальном сервере (подробно про нее будет в следующем разделе):

image

Создание 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;
  • Получать события (но сейчас события никак не показать, поэтому в интерфейсе для простоты написано, что события не поддерживаются);
  • Логировать действия.

Сначала инициализируем блокчейн:

image

Далее заводим свой account, не мелочимся с балансом:

image

Получаем список всех доступных account-ов:

image

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

image

В логе следим за выполнением транзакций:

image

Собственно c демо программой на этом все. Далее можно посмотреть нашу транзакцию в блокчейне:

image

И общий список транзакций:

image

На этом мы успешно завершили реализацию 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!