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

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

    В прошлых статьях мы разобрались с технологиями, на которых строятся блокчейны (Что нам стоит блокчейн построить?) и кейсами, которые можно с их помощью реализовать (Что нам стоит кейс построить?). Настало время поработать руками! Для реализации пилотов и 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!
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0
      А про Corda будет статья? Интересно было бы почитать стороннее мнение и сравнение.
        0

        Судя по информации из интернета, платформа Corda предназначена для использования исключительно финансовыми учреждениями, а я ищу универсальный блокчейн. Вот, например, одна из статей:


        "Сравнение Ethereum, Corda и Fabric" http://chainmedia.ru/articles/eth-vs-fabric-vs-codra/

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое