Блокчейн (blockchain) – это технология распределенного хранения данных в одноранговой сети в виде непрерывной последовательности блоков взаимосвязанных с помощью алгоритма хеширования. Давайте подробнее познакомимся с этой технологией и рассмотрим пример ее реализации на языке программирования C#.
Что такое Блокчейн?
Как я уже писал выше блокчейн это распределенное хранилище данных. То есть, блокчейн можно представить себе как непрерывную последовательность зависящих друг от друга блоков, содержащих как полезную информацию, так и дополнительные служебные данные, которые гарантируют достоверность хранимой информации. Главной особенностью данной технологии является распределенность (децентрализация) – не существует единого центрального хранилища данных, каждый пользователь хранит у себя полную копию всей информации. Каждый экземпляр хранилища синхронизуется с другими по заданным системой правилам. Второй важной особенностью блокчейна является ее криптозащищенность. Под этим подразумевается использование специализированных алгоритмов хеширования данных, которые гарантируют их целостность и неизменность. Это особенно важно, если брать во внимание, что не существует эталона данных, каждый экземпляр является полноценным и равным другому. Поэтому очень важно защищать данные от внесения изменений. Давайте чуть подробнее рассмотрим принцип работы технологии блокчейн.
Как работает Блокчейн?
Вводимые данные «упаковываются» в специализированные блоки. В данном блоке содержится дополнительная служебная информация, которая необходима для обеспечения целостности и неизменности введенных данных, а также для работы самой цепочки блоков. Сама по себе цепочка блоков очень похожа по структуре на односвязный список. Подробнее данная структура данных описана в статье Связный список (Linked List) C#. Блок данных содержит следующие важные поля:
- Data – поле, в котором сохранены сами полезные данные. Это может быть как простая строка, так и более сложные структуры.
- CreatedOn – дата и время создания блока данных. Важно использовать универсальное UTC время, чтобы не возникало конфликтов из-за разных часовых поясов.
- Hash – уникальный ключ, созданный на основе хранимых данных блока с помощью специализированной хеш-функции, которая подразумевает только одностороннее шифрование.
- PreviousHash – указатель на предыдущий хеш-блока. Это необходимо для связывания блоков в единую цепочку.
Также возможно использование и дополнительных данных, в данном случае также использовались следующие поля:
- User – данные о пользователе, создавшем блок
- Algorithm – используемый алгоритм хеширования
- Version – версия блока

При создании блока данных выполняется хеширование введенных данных. Затем выполняется хеширование всех хранимых данных блока, после чего этот результат записывается в поле Hash. Данный подход позволяет гарантировать сохранность данных, потому что при изменении любого из полей блока хотя бы на один символ хеш функция вернет совершенно другой результат, и система легко сможет это определить, выполнив повторное хеширование блока и сравнив с хранимым хешем. При этом каждый блок зависит от хеша предыдущего блока, в результате чего при изменении любого блока в цепочке, вся цепочка становится некорректной.
Что такое хеширование?
Хеширование – это процесс преобразования входных данных произвольной длины в значение определенного формата, путем преобразования по заданному алгоритму. Основной особенность хеш-функции является возможность легкого преобразования входных данных в хеш, и невозможностью однозначного восстановления исходных данных из хеша.
Рассмотрим пример самой элементарной хеш-функции, это суммирование всех цифр числа. Например, если на вход мы получим число 73, то хешем данного числа будет 7 + 3 = 10. Это очень простая операция, которая позволяет получить хеш. Но вот обратное преобразование однозначно выполнить невозможно, так как существуют многие числа, дающие такой же результат: 64, 37, 181, 11116 и так далее.
Конечно же, это очень простой пример, и настоящие хеш функции намного сложнее, но принцип работы у них всех идентичен – получение ключа на основе входных данных, при условии, что даже минимальное изменение входных данных приведет к изменению ключа.
Реализация Блокчейн на языке C#
Обратите внимание, что в данной статье рассматривается прототип приложения, а не готовый к использованию продукт. Код далеко не идеален, и не весь изначально задуманный функционал реализован, но для знакомства с технологией блокчейн этого более чем достаточно. Изначально, данный проект реализовывался в рамках ежегодного соревнования среди разработчиков департамента CRM компании ООО «Норбит» (New Year Coding Challenge 2018).
Вот краткая формулировка задачи: необходимо разработать защищенное хранилище информации на базе технологии блокчейн. Решение должно обеспечивать хранение информации без возможности модификации (кроме полного стирания всех данных). Решение должно позволять хранить произвольный набор данных в виде строки размером до 5 МБ. Кроме того, необходимо реализовать ролевую модель пользователей и возможность разделения хранимых данных по типу.

Приложение разделено на несколько проектов.
- BlockchainData — уровень данных. Я заранее предусмотрел возможность расширения возможных средств для сохранения данных. В данном случае используется единственная реализация сохранения в базу данных MS SQL с использованием EntityFramework.
- Blockchain — уровень логики. Здесь реализованы все основные классы и логика работы приложения.
- BlockchainService — WCF-служба, предоставляющая REST API интерфейс для клиентских приложений.
- BlockchainExplorerDesktop — WinForms-приложение для просмотра данных хранимых в блокчейн.
- BlockchainExplorerWeb — MVC ASP.NET приложение для просмотра данных хранимых в блокчейн.
Рассмотрим подробнее реализацию основной библиотеки классов Blockchain.dll.

Реализация цепочки блоков Chain
Итак, основным классом библиотеки является класс Chain.cs. Он взаимодействует с уровнем данных использую принципы инверсии управления и внедрения зависимостей. Подробнее про это можно прочитать в статье Инверсия управления и Внедрение зависимостей (IoС & DI). Таким же образом реализован подход к алгоритму хеширования. Chain содержит в себе список всех блоков в хранилище данных, а также подсписки для разделения блоков по типам (блоки данных, блоки с данными о пользователях, блоки с данными о IP адресах серверов). При создании экземпляра класса цепочки блоков выполняется обращение к глобальной сети для синхронизации с другими хостами. Также выполняется получение блоков из локального хранилища. На данный момент не реализовано корректного алгоритма слияния этих двух цепочек. Выбирается просто выбирается наиболее длинная цепь. Если не было получено ни локальной, ни глобальной цепочки, то выполняется создание новой цепочки состоящей из одного генезис-блока. Генезис блок — это единый начальный блок цепочки. Он всегда генерируется по одинаковым правилам у всех экземпляров блокчейн. После создания экземпляра цепочки выполняется ее полная проверка на корректность, чтобы удостовериться, что не было внесено никаких изменений.
using Blockchain.Algorithms; using System; using System.Collections.Generic; using System.Linq; using Blockchain.Exceptions; using BlockchainData; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization.Json; using System.IO; using System.Text; namespace Blockchain { /// <summary> /// Цепочка блоков. /// </summary> public class Chain { /// <summary> /// Алгоритм хеширования. /// </summary> private IAlgorithm _algorithm = AlgorithmHelper.GetDefaultAlgorithm(); /// <summary> /// Провайдер данных. /// </summary> private IDataProvider _dataProvider = DataProviderHelper.GetDefaultDataProvider(); /// <summary> /// Список, содержащий в себе все блоки. /// </summary> private List<Block> _blockChain = new List<Block>(); /// <summary> /// Список IP адресов хостов. /// </summary> private List<string> _hosts = new List<string>(); /// <summary> /// Список пользователей. /// </summary> private List<User> _users = new List<User>(); /// <summary> /// Список данных. /// </summary> private List<Data> _data = new List<Data>(); /// <summary> /// Цепочка блоков. /// </summary> public IEnumerable<Block> BlockChain => _blockChain; /// <summary> /// Крайний блок в цепочке блоков. /// </summary> public Block PreviousBlock => _blockChain.Last(); /// <summary> /// Информационные данные. /// </summary> public IEnumerable<Data> Content => _data; /// <summary> /// Пользователи системы. /// </summary> public IEnumerable<User> Users => _users; /// <summary> /// IP адреса серверов. /// </summary> public IEnumerable<string> Hosts => _hosts; public int Length => _blockChain.Count; /// <summary> /// Создать новый экземпляр цепочки блоков. /// </summary> public Chain() { // Получаем цепочки блоков. var globalChain = GetGlobalChein(); // TODO: Решить проблему когда служба обращается сама к себе. var localChain = GetLocalChain(); if (globalChain != null && localChain != null) { if (globalChain.Length > localChain.Length) { ReplaceLocalChainFromGlobalChain(globalChain); } else { LoadDataFromLocalChain(localChain); } } else if (globalChain != null) { ReplaceLocalChainFromGlobalChain(globalChain); } else if (localChain != null) { LoadDataFromLocalChain(localChain); } else { CreateNewBlockChain(); } if (!CheckCorrect()) { throw new MethodResultException(nameof(Chain), "Ошибка создания цепочки блоков. Цепочка некорректна."); } } /// <summary> /// Получить данные из локальной цепочки. /// </summary> /// <param name="localChain"> Локальная цепочка блоков. </param> private void LoadDataFromLocalChain(Chain localChain) { if(localChain == null) { throw new MethodRequiresException(nameof(localChain), "Локальная цепочка блоков не может быть равна null."); } foreach(var block in localChain._blockChain) { _blockChain.Add(block); AddDataInList(block); SendBlockToGlobalChain(block); } } /// <summary> /// Заменить локальную цепочку данных блоками из глобальной цепочки. /// </summary> /// <param name="globalChain"> Глобавльная цепочка данных. </param> private void ReplaceLocalChainFromGlobalChain(Chain globalChain) { if(globalChain == null) { throw new MethodRequiresException(nameof(globalChain), "Глобальная цепочка блоков не может быть равна null."); } // TODO: Очень топорная синхронизация (полная замена). Необходимо разработать алгоритм слияния. _dataProvider.Crear(); foreach (var block in globalChain._blockChain) { AddBlock(block); } } /// <summary> /// Создание цепочки блоков из списка блоков провайдера данных. /// </summary> /// <param name="blocks"> Блоки провайдера данных. </param> private Chain(List<BlockchainData.Block> blocks) { if (blocks == null) { throw new MethodRequiresException(nameof(blocks), "Список блоков провайдера данных не может быть равным null."); } foreach (var block in blocks) { var b = new Block(block); _blockChain.Add(b); AddDataInList(b); } if (!CheckCorrect()) { throw new MethodResultException(nameof(Chain), "Ошибка создания цепочки блоков. Цепочка некорректна."); } } /// <summary> /// Создание цепочки блоков из блоков данных. /// </summary> /// <param name="blocks"> Список блоков данных. </param> private Chain(List<Block> blocks) { if (blocks == null) { throw new MethodRequiresException(nameof(blocks), "Список блоков не может быть равным null."); } foreach (var block in blocks) { _blockChain.Add(block); AddDataInList(block); } if (!CheckCorrect()) { throw new MethodResultException(nameof(Chain), "Ошибка создания цепочки блоков. Цепочка некорректна."); } } /// <summary> /// Создать новую пустую цепочку блоков. /// </summary> private void CreateNewBlockChain() { _dataProvider.Crear(); _blockChain = new List<Block>(); var genesisBlock = Block.GetGenesisBlock(_algorithm); AddBlock(genesisBlock); } /// <summary> /// Проверить корректность цепочки блоков. /// </summary> /// <returns> Корректность цепочки блоков. true - цепочка блоков корректна, false - цепочка некорректна. </returns> public bool CheckCorrect() { foreach (var block in _blockChain) { if (!block.IsCorrect(_algorithm)) { return false; } } return true; } /// <summary> /// Получить глобальную цепочку блоков. /// </summary> /// <returns> Цепочка блоков. </returns> private Chain GetGlobalChein() { #if DEBUG //_hosts.Add("http://blockchain-dev-as.azurewebsites.net"); // TODO: Сделать получение из конфиг файла. #endif foreach (var host in Hosts) { // TODO: Здесь нужно будет переделать. Предварительно выбирается хост с самой большой цепочкой блоков и уже он синхранизуется. var blocks = GetBlocksFromHosts(host); if (blocks != null && blocks.Count > 0) { var chain = new Chain(blocks); return chain; } } return null; } /// <summary> /// Получение цепочки блоков из локального хранилища. /// </summary> /// <returns></returns> private Chain GetLocalChain() { var blocks = _dataProvider.GetBlocks(); if (blocks.Count > 0) { var chain = new Chain(blocks); return chain; } return null; } /// <summary> /// Добавить данные в цепочку блоков. /// </summary> /// <param name="text"> Добавляемые данные. </param> public Block AddContent(string text) { if (string.IsNullOrEmpty(text)) { throw new MethodRequiresException(nameof(text), "Текст не должен быть пустым или равен null."); } var data = new Data(text, DataType.Content); var block = new Block(PreviousBlock, data, User.GetCurrentUser(), _algorithm); AddBlock(block); return block; } /// <summary> /// Добавить данные о пользователе в цепочку. /// </summary> /// <param name="login"> Имя пользователя. </param> /// <param name="password"> Пароль пользователя. </param> /// <param name="role"> Права доступа пользователя. </param> public Block AddUser(string login, string password, UserRole role = UserRole.Reader) { if (string.IsNullOrEmpty(login)) { throw new MethodRequiresException(nameof(login), "Логин не может быть пустым или равным null."); } if (string.IsNullOrEmpty(password)) { throw new MethodRequiresException(nameof(password), "Пароль не может быть пустым или равным null."); } if (Users.Any(b => b.Login == login)) { return null; } var user = new User(login, password, role); var data = user.GetData(); var block = new Block(PreviousBlock, data, User.GetCurrentUser()); AddBlock(block); return block; } /// <summary> /// Добавление сведений об адресе глобальной цепочки. /// </summary> /// <param name="ip"> Адрес хоста в сети. </param> /// <returns> Блок данных с адресом хоста. </returns> public Block AddHost(string ip) { if (string.IsNullOrEmpty(ip)) { throw new MethodRequiresException(nameof(ip), "IP адрес хоста не может быть пустым или равным null."); } // TODO: Добавить проверку формата ip адреса var data = new Data(ip, DataType.Node); var block = new Block(PreviousBlock, data, User.GetCurrentUser(), _algorithm); AddBlock(block); return block; } /// <summary> /// Авторизоваться пользователем сети. /// </summary> /// <param name="login"> Логин. </param> /// <param name="password"> Пароль. </param> /// <returns> Пользователь сети. null если не удалось авторизоваться. </returns> public User LoginUser(string login, string password) { if (string.IsNullOrEmpty(login)) { throw new MethodRequiresException(nameof(login), "Логин не может быть пустым или равным null."); } if (string.IsNullOrEmpty(password)) { throw new MethodRequiresException(nameof(password), "Пароль не может быть пустым или равным null."); } var user = Users.SingleOrDefault(b => b.Login == login); if (user == null) { return null; } var passwordHash = password.GetHash(); if (user.Password != passwordHash) { return null; } return user; } /// <summary> /// Добавить блок. /// </summary> /// <param name="block"> Добавляемый блок. </param> private void AddBlock(Block block) { if (!block.IsCorrect()) { throw new MethodRequiresException(nameof(block), "Блок не корректный."); } // Не добавляем уже существующий блок. if(_blockChain.Any(b => b.Hash == block.Hash)) { return; } // TODO: Реализовать транзакцию. _blockChain.Add(block); _dataProvider.AddBlock(block.Version, block.CreatedOn, block.Hash, block.PreviousHash, block.Data.GetJson(), block.User.GetJson()); AddDataInList(block); SendBlockToGlobalChain(block); if (!CheckCorrect()) { throw new MethodResultException(nameof(Chain), "Была нарушена корректность после добавления блока."); } } /// <summary> /// Добавление данных из блоков в списки быстрого доступа. /// </summary> /// <param name="block"> Блок. </param> private void AddDataInList(Block block) { switch (block.Data.Type) { case DataType.Content: _data.Add(block.Data); foreach (var host in _hosts) { SendBlockToHosts(host, "AddData", block.Data.Content); } break; case DataType.User: var user = new User(block); _users.Add(user); foreach (var host in _hosts) { SendBlockToHosts(host, "AddUser", $"{user.Login}&{user.Password}&{user.Role}"); } break; case DataType.Node: _hosts.Add(block.Data.Content); foreach (var host in _hosts) { SendBlockToHosts(host, "AddHost", block.Data.Content); } break; default: throw new MethodRequiresException(nameof(block), "Неизвестный тип блока."); } } /// <summary> /// Добавление данных из блоков в списки быстрого доступа. /// </summary> /// <param name="block"> Блок. </param> private void SendBlockToGlobalChain(Block block) { switch (block.Data.Type) { case DataType.Content: foreach (var host in _hosts) { SendBlockToHosts(host, "AddData", block.Data.Content); } break; case DataType.User: var user = new User(block); foreach (var host in _hosts) { // TODO: Исправить. Получаем хеш пароля а не пароль. некорректно. SendBlockToHosts(host, "AddUser", $"{user.Login}&{user.Password}&{user.Role}"); } break; case DataType.Node: _hosts.Add(block.Data.Content); foreach (var host in _hosts) { SendBlockToHosts(host, "AddHost", block.Data.Content); } break; default: throw new MethodRequiresException(nameof(block), "Неизвестный тип блока."); } } /// <summary> /// Получение всех блоков от хоста через api. /// </summary> /// <param name="ip"> Адрес хоста в сети. </param> /// <returns> Список блоков. </returns> private static List<Block> GetBlocksFromHosts(string ip) { // http://localhost:28451/BlockchainService.svc/api/getchain/ пример запроса. var response = SendRequest(ip, "getchain", ""); if(string.IsNullOrEmpty(response)) { return null; } else { var blocks = DeserializeCollectionBlocks(response); return blocks; } } /// <summary> /// Отправка запроса к api хоста. /// </summary> /// <param name="ip"> Адрес хоста сети. </param> /// <param name="method"> Метод вызываемый у хоста. </param> /// <param name="data"> Передаваемые параметры метода через &. </param> /// <returns> Json ответ хоста. </returns> private static string SendRequest(string ip, string method, string data) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(20); // http://localhost:28451/BlockchainService.svc/api/getchain/ пример запроса. string repUri = $"{ip}/BlockchainService.svc/api/{method}/{data}"; var response = client.GetAsync(repUri).Result; if (response.IsSuccessStatusCode) { var result = response.Content.ReadAsStringAsync().Result; return result; } } return null; } /// <summary> /// Формирование списка блоков на основе полученого json ответа хоста. /// </summary> /// <param name="json"> Json ответ хоста на запрос получения всех блоков. </param> /// <returns> Список блоков глобальной цепочки. </returns> private static List<Block> DeserializeCollectionBlocks(string json) { using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json))) { var deserializer = new DataContractJsonSerializer(typeof(GetChainResultRoot)); var requestResult = (GetChainResultRoot)deserializer.ReadObject(ms); var result = new List<Block>(); foreach (var block in requestResult.GetChainResult) { result.Add(new Block(block)); } return result; } } /// <summary> /// Запрос к api хоста на добавление блока данных. /// </summary> /// <param name="ip"> Адрес хоста в сети. </param> /// <param name="method"> Вызываемый метод хоста. </param> /// <param name="data"> Параметры метода хоста через &.</param> /// <returns> Успешность выполнения запроса. </returns> private bool SendBlockToHosts(string ip, string method, string data) { var result = SendRequest(ip, method, data); var success = !string.IsNullOrEmpty(result); return success; } } }
Реализация блока цепочки Block
Класс блок реализует структуру блока данных. Он содержит в себе все необходимые поля, которые должны быть сохранены в хранилище данных. При создании блока выполняется хеширование входных данных, а также хеширование всего блока, что позволяет гарантировать неизменность хранимых данных и метаданных. При выполнении хешированию учитываются следующие поля: версия, время создания блока, хеш предыдущего блока, хеш хранимых данных, хеш пользователя. Результат сохраняется в поле Hash блока, что позволяет легко проверить его корректность.
using Blockchain.Algorithms; using System; using Blockchain.Exceptions; namespace Blockchain { /// <summary> /// Блок из цепочки блоков. /// </summary> public class Block : IHashable { /// <summary> /// Алгоритм хеширования. /// </summary> private IAlgorithm _algorithm = AlgorithmHelper.GetDefaultAlgorithm(); /// <summary> /// Версия спецификации блока. /// </summary> public int Version { get; private set; } /// <summary> /// Момент создания блока. /// </summary> public DateTime CreatedOn { get; private set; } /// <summary> /// Хеш блока. /// </summary> public string Hash { get; private set; } /// <summary> /// Хеш предыдущего блока. /// </summary> public string PreviousHash { get; private set; } /// <summary> /// Данные блока. /// </summary> public Data Data { get; private set; } /// <summary> /// Идентификатор пользователя, создавшего блок. /// </summary> public User User { get; private set; } /// <summary> /// Создать экземпляр блока. /// </summary> /// <param name="previousBlock">Предыдущий блок.</param> /// <param name="data">Данные, сохраняемые в блоке.</param> /// <param name="algorithm">Алгоритм хеширования.</param> /// <param name="user"> Идентификатор пользователя, создавшего блок. </param> public Block(Block previousBlock, Data data, User user, IAlgorithm algorithm = null) { #region Requires if (previousBlock == null) { throw new MethodRequiresException(nameof(previousBlock), "Предыдущий блок не может быть равен null."); } if(!previousBlock.IsCorrect()) { throw new MethodRequiresException(nameof(previousBlock), "Предыдущий блок некорректный."); } if(data == null) { throw new MethodRequiresException(nameof(data), "Данные не могут быть равны null."); } if(!data.IsCorrect()) { throw new MethodRequiresException(nameof(data), "Данные некорректные."); } if(user == null) { throw new MethodRequiresException(nameof(user), "Пользователь не может быть равен null."); } if(!user.IsCorrect()) { throw new MethodRequiresException(nameof(user), "Пользователь некорректный."); } #endregion if (algorithm != null) { _algorithm = algorithm; } Version = 1; // TODO: Вынести в конфиг файл. CreatedOn = DateTime.Now.ToUniversalTime(); PreviousHash = previousBlock.Hash; Data = data; User = user; Hash = this.GetHash(_algorithm); if (!this.IsCorrect()) { throw new MethodResultException(nameof(Block), "Ошибка создания блока. Блок некорректный."); } } /// <summary> /// Создать новый экземпляр стартового (генезис) блока. /// </summary> /// <param name="user"> Пользователь системы. </param> /// <param name="algorithm"> Алгоритм хеширования. </param> protected Block(IAlgorithm algorithm = null) { if (algorithm != null) { _algorithm = algorithm; } Version = 1; // TODO: Вынести в конфиг файл. CreatedOn = DateTime.Parse("2018-01-01T00:00:00.000+00:00").ToUniversalTime(); User = new User("admin", "admin", UserRole.Admin); PreviousHash = _algorithm.GetHash("79098738-8772-4F0A-998D-9EC7737720F4"); Data = User.GetData(); Hash = this.GetHash(_algorithm); if (!this.IsCorrect()) { throw new MethodResultException(nameof(Block), "Ошибка создания генезис блока. Блок некорректный."); } } /// <summary> /// Создание блока цепочки из блока провайдера данных. /// </summary> /// <param name="block"> Блок провайдера данных. </param> public Block(BlockchainData.Block block) { if(block == null) { throw new MethodRequiresException(nameof(block), "Блок данных провайдера данных не может быть равен null."); } Version = block.Version; CreatedOn = block.CreatedOn.ToUniversalTime(); User = User.Deserialize(block.User); PreviousHash = block.PreviousHash; Data = Data.Deserialize(block.Data); Hash = block.Hash; if (!this.IsCorrect()) { throw new MethodResultException(nameof(Block), "Ошибка создания блока из блока провайдера данных. Блок некорректный."); } } /// <summary> /// Получить начальный блок цепочки блоков. /// </summary> /// <param name="algorithm"> Алгоритм хеширования. </param> /// <returns> Стартовый блок. </returns> public static Block GetGenesisBlock(IAlgorithm algorithm = null) { if (algorithm == null) { algorithm = AlgorithmHelper.GetDefaultAlgorithm(); } var genesisBlock = new Block(algorithm); return genesisBlock; } /// <summary> /// Получить данные из объекта, на основе которых будет строиться хеш. /// </summary> /// <returns> Хешируемые данные. </returns> public string GetStringForHash() { var data = ""; data += Version; data += CreatedOn.Ticks; data += PreviousHash; data += Data.Hash; data += User.Hash; return data; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Идентификатор блока. </returns> public override string ToString() { return Hash; } } }
Реализация класса хранимых данных Data
Класс данных необходим больше для удобства работы. Он позволяет сохранять строковые данные, получать хеш хранимых данных, а также разделять хранимые данные по тип (данные о пользователях, данные о серверах, простые хранимые текстовые данные). Для корректного сохранения контента выполняется сериализация и десериализация в json формат.
Сериализация — это процесс преобразования объекта в поток байтов, десериализация — это обратный процесс, позволяющий сформировать из потока байтов готовый объект. Если сформулировать это более простым языком, мы выполняет сохранение всех значений объекта в специализированном тестовом формате json. Данная тема будет мной подробнее рассмотрена в одной из моих следующих статей.
using Blockchain.Algorithms; using Blockchain.Exceptions; using System; using System.Runtime.Serialization; namespace Blockchain { /// <summary> /// Данные хранимые в блоке из цепочки блоков. /// </summary> [DataContract] public class Data : IHashable { /// <summary> /// Алгоритм хеширования. /// </summary> private IAlgorithm _algorithm = AlgorithmHelper.GetDefaultAlgorithm(); /// <summary> /// Содержимое блока. /// </summary> [DataMember] public string Content { get; private set; } /// <summary> /// Хеш данных. /// </summary> [DataMember] public string Hash { get; private set; } /// <summary> /// Тип хранимых данных. /// </summary> [DataMember] public DataType Type { get; private set; } /// <summary> /// Создать экземпляр данных. /// </summary> /// <param name="content"> Данные. </param> /// <param name="algorithm"> Алгоритм для хеширования. </param> public Data(string content, DataType type, IAlgorithm algorithm = null) { // Проверяем предусловия. if(string.IsNullOrEmpty(content)) { throw new MethodRequiresException(nameof(content), "Данные не могут быть пустыми или равняться null"); } // Если не указан алгоритм, то берем по умолчанию. if (algorithm != null) { _algorithm = algorithm; } Content = content; Type = type; Hash = this.GetHash(_algorithm); if (!this.IsCorrect()) { throw new MethodResultException(nameof(Data), "Ошибка создания данных. Данные некорректны."); } } /// <summary> /// Десериализация данных из JSON. /// </summary> /// <param name="json"> Строка с данными в формате JSON. </param> /// <returns> Объект данных. </returns> public static Data Deserialize(string json) { var data = Helpers.Deserialize(typeof(Data), json); if(!data.IsCorrect()) { throw new MethodResultException(nameof(data), "Некорректные данные после десериализации."); } return data as Data ?? throw new FormatException("Не удалось выполнить десериализацию данных."); } /// <summary> /// Получить данные из объекта, на основе которых будет строиться хеш. /// </summary> /// <returns> Хешируемые данные. </returns> public string GetStringForHash() { var text = Content; text += (int)Type; return text; } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Хранящиеся данные. </returns> public override string ToString() { return Content; } } }
Реализация класса пользователя User
Данный класс является также необходимым только для удобства работы с пользователями системы. Он хранит важные данные о пользователях, такие как логин, хеш пароля, права пользователя. Данные о пользователях также хранятся в сериализованном json формате, что позволяет легко сохранять и восстанавливать объекты.
using Blockchain.Algorithms; using Blockchain.Exceptions; using System; using System.Runtime.Serialization; namespace Blockchain { /// <summary> /// Пользователь системы. /// </summary> [DataContract] public class User : IHashable { /// <summary> /// Алгоритм хеширования. /// </summary> private IAlgorithm _algorithm = AlgorithmHelper.GetDefaultAlgorithm(); /// <summary> /// Логин пользователя. /// </summary> [DataMember] public string Login { get; private set; } /// <summary> /// Разрешения пользователя. /// </summary> [DataMember] public UserRole Role { get; private set; } /// <summary> /// Хеш пароля пользователя. /// </summary> [DataMember] public string Password { get; private set; } /// <summary> /// Хеш пользователя. /// </summary> [DataMember] public string Hash { get; private set; } /// <summary> /// Создать новый экземпляр пользователя и правами на чтение. /// </summary> /// <param name="login"> Имя пользователя. </param> /// <param name="password"> Пароль пользователя. </param> public User(string login, string password, IAlgorithm algorithm = null) :this(login, password, UserRole.Reader, algorithm) { } /// <summary> /// Создать новый экземпляр пользователя. /// </summary> /// <param name="login"> Имя пользователя. </param> /// <param name="password"> Пароль пользователя. </param> /// <param name="role"> Права пользователя. </param> /// <param name="algorithm"> Алгоритм хеширования. </param> public User(string login, string password, UserRole role, IAlgorithm algorithm = null) { // Проверяем предусловия. if(string.IsNullOrEmpty(login)) { throw new MethodRequiresException(nameof(login), "Логин не может быть пустым или равным null."); } if(string.IsNullOrEmpty(password)) { throw new MethodRequiresException(nameof(password), "Пароль не может быть пустым или равным null."); } if(algorithm != null) { _algorithm = algorithm; } // Устанавливаем значения. Login = login; Password = password.GetHash(_algorithm); Role = role; Hash = this.GetHash(_algorithm); if(!this.IsCorrect()) { throw new MethodResultException(nameof(User), "Ошибка создания пользователя. Пользователь некорректный."); } } /// <summary> /// Создать экземпляр пользователя на основе блока. /// </summary> /// <param name="block"> Блок цепочки, содержащий информацию о пользователе. </param> public User(Block block) { if(block == null) { throw new MethodRequiresException(nameof(block), "Блок не может быть равен null"); } if(!block.IsCorrect()) { throw new MethodRequiresException(nameof(block), "Блок некорректный"); } if(block.Data == null) { throw new MethodRequiresException(nameof(block), "Данные блока не могут быть равны null"); } if(block.Data.Type != DataType.User) { throw new MethodRequiresException(nameof(block), "Некоректный тип данных блока."); } var user = Deserialize(block.Data.Content); Login = user.Login; Password = user.Password; Role = user.Role; Hash = user.Hash; if(!this.IsCorrect()) { throw new MethodResultException(nameof(User), "Не корректный пользователь."); } } /// <summary> /// Приведение объекта к строке. /// </summary> /// <returns> Имя пользователя. </returns> public override string ToString() { return Login; } /// <summary> /// Получить текущего пользователя системы. /// </summary> /// <returns> Текущий пользователь системы. </returns> public static User GetCurrentUser() { // TODO: Исправить получение текущего пользователя. return new User("admin", "admin", UserRole.Admin); } /// <summary> /// Получить данные из объекта, на основе которых будет строиться хеш. /// </summary> /// <returns> Хешируемые данные. </returns> public string GetStringForHash() { var text = Login; text += (int)Role; return text; } /// <summary> /// Получить блок для добавления пользователя в систему. /// </summary> /// <returns> Блок содержащий информацию о пользователе. </returns> public Data GetData() { var jsonString = this.GetJson(); var data = new Data(jsonString, DataType.User, _algorithm); return data; } /// <summary> /// Десериализация пользователя из JSON. /// </summary> /// <param name="json"> Строка с данными пользователя в формате JSON. </param> /// <returns> Объект пользователя. </returns> public static User Deserialize(string json) { if(string.IsNullOrEmpty(json)) { throw new MethodRequiresException(nameof(json), "Строка десеализации не может быть пустой или равной null."); } var user = Helpers.Deserialize(typeof(User), json); if(!user.IsCorrect()) { throw new MethodResultException(nameof(user), "Некоректный пользователь после десериализации."); } return user as User ?? throw new FormatException("Не удалось выполнить пользователя данных."); } } }
Интерфейсы IHashable и IAlgorithm
Обратите внимание, что важной особенностью данной реализации является использование специального интерфейса IHashable, который должны реализовывать все классы, которые подвергаются хешированию (такие как Block, User, Data). Этот интерфейс реализует всего одно свойство и один метод. Свойство строковое Hash гарантирует, что у класса будет поле, в которое будет выполнено сохранение готового хеша объекта, а метод GetStringForHash возвращает строку, в которой хранятся все тестовые данные, важные для хеширования.
Данный интерфейс используется в другом интерфейсе IAlgorithm. Данный интерфейс используется для того, чтобы предоставить возможность дальнейшего расширения возможностей приложения, путем легкого добавления дополнительных алгоритмов хеширования. Данный интерфейс определяет один перегруженный метод GetHash, которвый возвращает либо хеш стоки, либо хеш класса, реализующего интерфейс IHashable.
В данный момент реализован один алгоритм хеширования Sha256 в соответствующем классе.
Для корректности работы классов, реализующих данные интерфейсы используется проектирование по контракту. Это позволяет задать предусловия и постусловия для всех классов, которые реализуют данные интерфейсы. Благодаря этому гарантируется минимальная проверка параметров у всех наследников. Подробнее данную тему можно изучить в статье Программирование по контракту (Code Contracts) в C#.
Реализация клиент-серверной части приложения
Выше мы рассмотрели основную логику работы самой системы блокчейн и ее реализацию на языке программирования C#. Теперь быстренько пробежимся по остальным частям проекта, которые обеспечивают корректное клиент-серверное взаимодействие.
Уровень данных BlockchainData
Для хранения всех данных приложения используется отдельный проект. Это сделано с целью возможности последующего расширения возможностей системы для сохранения данных в любых системах ранения (текстовые файлы, различные реляционные и нереляционные базы данных и так далее). Для этого внедрен специальный интефейс IDataProvider, который определяет набор хранимых полей, а также методы добавления новых данных, получения всех данных и очистку всего хранилища.
Сейчас создана единственная реализация данного интерфейса с помощью работы с базой данных Microsoft SQL Server через ORM-систему EntityFramework 6. Данная реализация представлена в классе SqlDataProvider.
using System; using System.Collections.Generic; using System.Linq; namespace BlockchainData { /// <summary> /// Провайдер данных SQL. /// </summary> public class SqlDataProvider : IDataProvider { /// <summary> /// Добавление блока данных. /// </summary> /// <param name="version"> Версия блока. </param> /// <param name="createdOn"> Дата создания блока. </param> /// <param name="hash"> Хеш блока. </param> /// <param name="previousHash"> Хеш предыдущего блока. </param> /// <param name="data"> Данные блока. </param> /// <param name="user"> Данные о пользователе. </param> public void AddBlock(int version, DateTime createdOn, string hash, string previousHash, string data, string user) { using (var db = new BlockSqlContext()) { var block = new Block() { Version = version, CreatedOn = createdOn.ToLocalTime(), Hash = hash, PreviousHash = previousHash, Data = data, User = user }; db.Blocks.Add(block); db.SaveChanges(); } } /// <summary> /// Очистить хранилище. Удаление всех блоков. /// </summary> public void Crear() { using (var db = new BlockSqlContext()) { db.Blocks.RemoveRange(db.Blocks); db.SaveChanges(); } } /// <summary> /// Получить все блоки. /// </summary> /// <returns> Список всех блоков. </returns> public List<Block> GetBlocks() { using (var db = new BlockSqlContext()) { var blocks = db.Blocks.ToList(); return blocks; } } } }
Служба WCF
Основным серверном контроллером системы является WCF служба. Именно она использует логику реализованную в библиотеке классов Blockchain. Она предоставляет несколько REST API интерфейсов, к которым обращаются клиентские приложения. Запросы выполняются в асинхронном режиме, что улучшает производительность системы. На данный момент реализована передача данных через GET, что не слишком правильно, но удобно при отладке приложения. Подробнее про службу WCF можно прочитать в статье Windows Communication Foundation (WCF) служба. В этой же статье рассматриваются возможности настройки подключения клиентских приложений к службе. Очень рекомендую ознакомиться, перед продолжением чтения.
using Blockchain; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Web; using System.Threading.Tasks; namespace BlockchainService { /// <summary> /// Контракт службы. /// </summary> [ServiceContract] public interface IBlockchainService { /// <summary> /// Добавление хоста. /// </summary> /// <param name="ip"> Адрес хоста в сети. </param> /// <returns> Добавленных блок с данными о хосте. </returns> [OperationContract] [WebInvoke(Method = "GET", UriTemplate = "/AddHost/{ip}", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Task<BlockService> AddHostAsync(string ip); /// <summary> /// Добавление пользователя. /// </summary> /// <param name="login"> Логин. </param> /// <param name="password"> Пароль. </param> /// <param name="role"> Права доступа. </param> /// <returns> Добавленных блок с данными о пользователе. </returns> // TODO: Огромная дыра, нешифрованый пароль в get. Исправить. [OperationContract] [WebInvoke(Method = "GET", UriTemplate = "/AddUser/{login}&{password}&{role}", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Task<BlockService> AddUserAsync(string login, string password, string role); /// <summary> /// Добавление данных. /// </summary> /// <param name="text"> Содержимое данных. </param> /// <returns> Добавленных блок. </returns> [OperationContract] [WebInvoke(Method = "GET", UriTemplate = "/AddData/{text}", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Task<BlockService> AddDataAsync(string text); /// <summary> /// Авторизация пользователя. /// </summary> /// <param name="login"> Логин. </param> /// <param name="password"> Пароль. </param> /// <returns> Авторизованный пользователь системы. </returns> // TODO: Огромная дыра, нешифрованый пароль в get. Исправить. [OperationContract] [WebInvoke(Method = "GET", UriTemplate = "/Login/{login}&{password}", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Task<User> LoginAsync(string login, string password); /// <summary> /// Получение всех блоков цепочки. /// </summary> /// <returns> Список блоков цепочки блоков. </returns> [OperationContract] [WebInvoke(Method = "GET", UriTemplate = "/GetChain/", BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Task<List<BlockService>> GetChainAsync(); } }
В рамках службы существует единственный экземпляр цепочки блоков, реализованный с помощью паттерна Одиночка. Подробнее про этот паттерн можно прочитать в статье Паттерн Одиночка (Singleton pattern).
Служба выполняет проверку и формирование полученных данный в экземпляры классов блоков соответствующего типа, и дальнейшая логика работы передается библиотеке классов Blockchain. Таким образом, служба является своеобразным посредником между клиентскими приложениями и основной бизнес-логикой системы.
Windows Form приложение BlockchainExplorerDesktop
Для работы со службой реализовано достаточно простое приложение, которое подключается напрямую к службе и позволяет как получать все данные, так и добавлять новые. В приложении реализована работа в асинхронном режиме, благодаря чему даже при получении большого количества данных приложение не виснем полностью.
using BlockchainExplorerDesktop.BlockchainService; using System; using System.Threading.Tasks; using System.Windows.Forms; namespace BlockchainExplorerDesktop { public partial class MainForm : Form { /// <summary> /// Клиент службы. /// </summary> BlockchainServiceClient client = new BlockchainServiceClient(); /// <summary> /// Конструктор основной формы. /// </summary> public MainForm() { InitializeComponent(); blockListBox.DisplayMember = "Hash"; } /// <summary> /// Обработчик события загрузки формы. /// </summary> private async void Main_Load(object sender, EventArgs e) { await LoadAsync(); } /// <summary> /// Обработчик события двойного клика на элементе списка. /// </summary> private void BlockListBox_DoubleClick(object sender, EventArgs e) { if (blockListBox.SelectedItem is BlockService item) { textBox3.Text = item.CreatedOn.ToLocalTime().ToString("dd.MM.yyyy HH.mm.ss"); textBox4.Text = item.Hash; textBox5.Text = item.PreviousHash; textBox6.Text = item.User; textBox7.Text = item.Data; } } /// <summary> /// Обработчик события нажатия кнопки добавления блока. /// </summary> private async void AddBlockButton_Click(object sender, EventArgs e) { var block = await client.AddDataAsync(inputDataBlockTextBox.Text); blockListBox.Items.Add(block); inputDataBlockTextBox.Text = ""; } /// <summary> /// Обработчик события нажатия на ссылку помощи. /// </summary> private void HelpLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { var about = new AboutForm(); about.ShowDialog(); } /// <summary> /// Загрузка всех блоков из локального хранилища в асинхронном режиме. /// </summary> /// <returns> Успешность выполнения загрузки блоков. </returns> private async Task<bool> LoadAsync() { return await Task.Run(() => { var blocks = client.GetChain(); foreach (var block in blocks) { blockListBox.BeginInvoke((Action)delegate { blockListBox.Items.Add(block); }); } return true; }); } } }
ASP.NET MVC приложение BlockchainExplorerWeb
Также реализовано клиентское веб-приложение, для работы со службой. Оно позволяет выполнять создание дубликата существующей цепочки, и выполнять просмотр и добавление данных, а также синхронизацию между службой и веб-приложением. Взаимодействие осуществляется с помощью api-запросов.
using BlockchainExplorerWeb.Models; using System; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.Serialization.Json; using System.Text; namespace BlockchainExplorerWeb.Controllers { /// <summary> /// Класс для упрощения взаимодействия с API службы. /// </summary> public class Api { /// <summary> /// Адрес глобальной службы. /// </summary> private string _address = "http://blockchain-dev-as.azurewebsites.net"; /// <summary> /// Создать новый экземпляр цепочки блоков. /// </summary> /// <returns> Цепочка блоков. </returns> public Chain GetChain() { var json = SendRequest("getchain", ""); var chain = Deserialize<Chain>(json); return chain; } /// <summary> /// Добавить данные в цепочку блоков. /// </summary> /// <param name="data"> Данные, которые будут добавлены в цепочку блоков. </param> /// <returns> Успешность добавления данных. </returns> public bool AddData(string data) { var json = SendRequest("adddata", data); return !string.IsNullOrEmpty(json); } /// <summary> /// Отправить запрос к api службы блокчейн. /// </summary> /// <param name="method"> Вызываемый метод службы. </param> /// <param name="data"> Параметры метода передаваемые через &. </param> /// <returns> Json ответ сервера. null если нет ответа. </returns> private string SendRequest(string method, string data) { using (var client = new HttpClient()) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(20); // http://localhost:28451/BlockchainService.svc/api/getchain/ пример запроса. string repUri = $"{_address}/BlockchainService.svc/api/{method}/{data}"; var response = client.GetAsync(repUri).Result; if (response.IsSuccessStatusCode) { var result = response.Content.ReadAsStringAsync().Result; return result; } } return null; } /// <summary> /// Десериализовать json данные в объект. /// </summary> /// <typeparam name="T"> Тип данных, в который будет выполняться десериализация. </typeparam> /// <param name="json"> Json данные. </param> /// <returns> Десериализованный экземпляр объекта. </returns> public static T Deserialize<T>(string json) { using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json))) { var deserializer = new DataContractJsonSerializer(typeof(T)); var requestResult = (T)deserializer.ReadObject(ms); return requestResult; } } } }
Заключение
Исходный код проекта как всегда доступен в репозитории GitHub. Изучите проект подробнее, здесь рассмотрена только его небольшая часть. В дальнейшем я планирую вернуться к доработке этого проекта и довести его до более-менее рабочего продукта. Возможно, я напишу еще несколько статей с более детальным рассмотрением конкретных особенностей системы.
Сразу поделюсь некоторой дополнительной информацией. В качестве эксперимента, при демонстрации проекта на конкурсе я использовал Azure. Вещь очень удобная, приложения публикуются без каких-либо проблем, базы данных создаются и отлично работают, службы тоже. Но я столкнулся с двумя проблемами. Первое, разница в часовых поясах. Серверное время у службы было австралийским, из-за этого были большие проблемы при хешировании, так как время менялось. Проблему еще сильнее усугубил EntityFramework, который некорректно сохранял время в UTC. Пришлось немного поплясать с бубном. Второе, Azure очень дорогой. За 1 месяц у меня ушло около трех тысяч рублей, на базу данных, службу и веб-приложение. Кстати, конкурс я успешно выиграл.
Надеюсь, эта статья была достаточно полезной и интересной, не смотря на большой объем. По любым вопросам можете писать в комментариях любым из других представленных ниже способов. С удовольствием отвечу на ваши вопросы.
Кроме того, рекомендую прочитать статью Code Contracts C# | Программирование по контракту C#. А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.