Разработка блокчейна для промышленности на Go. Часть 1

    Вот уже четыре месяца я занимаюсь проектом, который назвал «Разработка средств защиты и управления данными в государственных и промышленных сферах на основе блокчейна».
    Теперь мне хотелось бы рассказать, о том, как этот проект я начинал, что сейчас и опишу подробно программный код.

    image

    Это первая статья из цикла статей. Здесь я описываю сервер и протокол. На самом деле, читатель даже может написать и свои варианты этих элементов блокчейна.

    А вот вторая часть — про структуры данных блокчейна и транзакций, а также о пакете реализующим взаимодействие с базой данных.

    В прошлом году на хакатоне Цифровой прорыв подкинули идею, чтобы для промышленности и цифровой экономики сделать полезную систему по технологии распределенных реестров, так же на разработку был выдан грант Фондом содействия инновациям (о гранте следует мне написать отдельную статью, для тех кто стартапы только начинает делать), а теперь по-порядку.

    Разработка происходит на языке Go, а база данных в которой хранятся блоки — LevelDB.
    Основные части это протокол, сервер (который запускает TCP и WebSocket — первый для синхронизации блокчейна, в второй для подключения клиентов, отправки транзакций и комманд из JavaScript, например.

    Как было упомянуто этот блокчейн нужен в первую очередь для автоматизации и защиты обмена продукцией между поставщиками и заказчиками, либо и теми и другими в одном лице. Доверять друг-другу такие не спешат. Но задача не только сделать «чековую книжку» со встроенным калькулятором, а систему с автоматизацией большинства рутинных задач, которые возникают при работе с жизненным циклом продукции. Байт-код, который за это дело отвечает как и принято у блокчейнов хранится во входах и выходах транзакций (сами транзакции сидят в блоках, блоки в LevelDB предварительно закодированные в формат GOB). Для начала давайте расскажу про протокол и сервер (он же нода).

    Протокол работает не сложно, весь его смысл в том чтобы в ответ на специальную строку-команду переключиться в режим загрузки каких-то данных, обычно блока или транзакции, а еще он нужен для обмена инвентарем, это чтобы нода знала к кому она подключена и как у них дела (подключенные для сессии синхронизации ноды еще называют «соседними» потому что известны их IP и хранятся данные их состояния в памяти).

    Папки (дирректории как их называет Linux) в понимании Go-программистов называются пакетами, поэтому в начале каждого файла с Go-кодом из этой дирректории в начале пишут package имя_папки_где_сидит_этот_файл. Иначе не получится скормить компилятору пакет. Ну это для знающих этот язык не секрет. Вот эти пакеты:

    • Сетевое взаимодействие (server, client, protocol)
    • Структуры хранимых и передаваемых данных (block, transaction)
    • База данных (blockchain)
    • Консенсус (consensus)
    • Стековая виртуальная машина (xvm)
    • Вспомогательные (crypto, types) пока всё.

    Вот ссылка на github

    Это учебная версия в ней отсутствует межпроцессное взаимодействие и несколько эксперементальных компонентов, за то структура соответствует той над которой ведется разработка. Если у вас будет что подсказать в комментариях я с удовольствием учту в дальнейшей разработке. А теперь пояснения по пакетам server и protocol.

    Сначала посмотрим на server.

    Подпрограмма server выполняет функции сервера данных, работающего поверх протокола TCP с использованием структур данных из пакета protocol.

    Подпрограмма использует следующие пакеты: server, protocol, types. В самом пакете в tcp_server.go содержится структура данных Serve.

    type Serve struct {
    	Port string
    	BufSize int
    	ST *types.Settings
    }

    Она может принимать следующие параметры:

    • Сетевой порт, по которому будет осуществляться обмен данными
    • Файл конфигурации сервера в формате JSON
    • Флаг запуска в режиме отладки (приватного блокчейна)

    Ход выполнения:

    • Считывается конфигурация из файла JSON
    • Проверяется флаг режима отладки: если он установлен, то запуск планировщика сетевой синхронизации не осуществляется и блокчейн не загружается
    • Инициализация структуры данных конфигурации и запуск сервера

    Server


    • Осуществляет зпуск TCP сервера и сетевое взаимодействие в соответствии с протоколом.
    • В нем имеется структура данных Serve состоящая из номера порта, размера буфера и указателя на структуру types.Settings
    • Метод Run запускает сетевое взаимодействие (прослушивание входящих соединений на заданном порту, при получении нового соединения его обработка передается в приватный метод handle в новом потоке)
    • В handle данные из соединения считываются в буфер, преобразуются в строковое представление и передаются в protocol.Choice
    • protocol.Choice возвращает result либо вызывает ошибку. result затем передается в protocol.Interprete, который возвращает intrpr — объект типа InterpreteData, либо вызывает ошибку обработки результата выбора
    • Затем выполняется switch по intrpr.Commands[0] в котором проверяется одно из: result, inv, error и есть секция default
    • В секции result находится switch по значению intrpr.Commands[1] который проверяет значения bufferlength и version (в каждом кейсе вызывается соответствующая функция)

    Функции GetVersion и BufferLength находятся в файле srvlib.go пакета server.

    GetVersion(conn net.Conn, version string)

    просто печатает в консоль и отправляет клиенту переданную в параметре версию:

    conn.Write([]byte("result:" + version))
    .
    Функция

    BufferLength(conn net.Conn, intrpr *protocol.InterpreteData)

    выполняет загрузку блока, транзакции либо иных определенных данных следующим образом:

    • Печатает на консоли указанный в протоколе тип данных, которые нужно принят:

      fmt.Println("DataType:", intrpr.Commands[2])
    • Считывает значение intrpr.Body в числовую переменную buf_len
    • Создает буфер newbuf указанного размера:

      make([]byte, buf_len)
    • Отправляет ответ ok:

      conn.Write([]byte("result:ok"))
    • Производит полное заполнение буфера из считываемого потока:

      io.ReadFull(conn, newbuf)
      .
    • Выводит в консоль содержимое буфера

      fmt.Println(string(newbuf))

      и количество прочитанных байтов

      fmt.Println("Bytes length:", n)
    • Отправляет ответ ok:

      conn.Write([]byte("result:ok"))

    Методы из пакета server настроены таким образом, чтобы обрабатывали полученные данные функциями из пакета protocol.

    Protocol


    Протокол служит средством, которое представляет данные при сетевом обмене.

    Choice(str string) (string, error) выполняет первичную обработку принятых сервером данных, на вход получает строковое представление данных и возвращает строку подготовленную для Interprete:

    • Входная строка разбивается на head и body с помощью ReqParseN2(str)
    • head разбивается на элементы и помещается в слайс commands с помощью ReqParseHead(head)
    • В switch(commands[0]) выбираем полученную команду (cmd, key, address либо срабатывает секция default)
    • В cmd проверяется 2 команды switch(commands[1]) — length и getversion.
    • length проверяет тип данных в commands[2] и сохраняет его в datatype
    • Проверяет что body содержит строковое значение

      len(body) < 1
    • Возвращает строку ответ:

      "result:bufferlength:" + datatype + "/" + body
    • getversion возвращает строку

      return "result:version/auto"

    Interprete


    Содержит структуру InterpreteData и выполняет вторичную обработку возвращенной из Choice строки и формирование объекта InterpreteData.

    type InterpreteData struct {
    	Head string
    	Commands []string
    	Body string
    	IsErr bool
    	ErrCode int 
    	ErrMessage string
    }

    Функция

    Interprete(str string) (*InterpreteData, error)

    принимает строку result и создает возвращает ссылку на объект InterpreteData.

    Ход выполнения:

    • Аналогично Choice извлекается head и body с помощью ReqParseN2(str)
    • head разбивается на элементы с помощью ReqParseHead(head)
    • Инициализируется объект InterpreteData и возвращается указатель на него:

    res := &InterpreteData{
    	Head: head,
    	Commands: commands,
    	Body: body,
    }
    return res, nil

    Этот объект используется в server.go пакета main.

    Client


    Пакет client содержит функции TCPConnect и TCPResponseData.

    Функция

    TCPConnect(s *types.Settings, data []byte, payload []byte)

    работает следующим образом:

    • Выполняется подключение к указанному в переданном объекте настроек соединению

      net.Dial("tcp", s.Host + ":" + s.Port)
    • Передаются данные, переданные в параметре data:

      conn.Write(data)
    • Считывается ответ

      resp, n, _ := TCPResponseData(conn, s.BufSize)

      и печатается на консоли

      fmt.Println(string(resp[:n]))
    • Если передан payload то передает его

      conn.Write(payload)

      и также считывает ответ сервера, печатая его на консоли

    Функция

     TCPResponseData(conn net.Conn, bufsiz int) ([]byte, int, error)

    создает буфер указанного размера, читает туда ответ сервера и возвращает этот буфер и количество считанных байтов, а также объект ошибки.

    Подпрограмма client


    Служит для передачи команд серверам нод, а также получения краткой статистики и тестирования.

    Может принимать следующие параметры: файл конфигурации в формате JSON, данные для передаче серверу в виде строки, путь к файлу для передачи его в payload, флаг эмуляции планировщика ноды, тип передаваемых данных в виде числового значения.

    • Получаем конфигурацию

      st := types.ParseConfig(*config)
    • Если передан флаг emu запускается sheduler
    • Если предан флаг f с указанием пути к файлу, то загружаем его данные в fdb и выполняется отправка содержимого серверу

      client.TCPConnect(st, []byte(CMD_BUFFER_LENGTH + ":" + strconv.Itoa(*t) + "/" + strconv.Itoa(fdblen)), fdb)
    • Если файл не задан, то просто отправляются данные из флага -d:

      client.TCPConnect(st, []byte(*data), nil)

    Все это упрощенное представление показывающее структуру протокола. При разработке в его структуру добавляется необходимый функционал.

    Во второй части я расскажу о структурах данных для блоков и транзакций, в 3 о WebSocket сервере для подключения из JavaScript, в 4 будет рассмотрен планировщик синхронизации, далее стековая машина обрабатывающая байткод из входов и выходов, криптография и пулы для выходов.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

      +2
      В чем смысл блокчейна в данном случае? Почему не mysql?
        +2

        На mysql грант не давали

          0
          Про грант я напишу отдельную статью. Там можно любой проект подавать, хоть на PHP просто он должен быть лучше аналогов, например технически или экономически выгоднее.
          +1

          Не модно же.

            0
            Если обеспечить все что касается синхронизации блоков их связывания, консенсус, то практически любая база данных подойдет. Там в пакете blockchain есть функции сохранения в БД в данном случае используется LevelDB. Об этом пакете постараюсь написать попозже.
              0
              Зачем нужна распределенная система в данном случае?
                0
                Потому что поставщики, заказчики и потребители не доверяют как правило друг-другу. Даже если верить написанному составу никто не знает какой технологический процесс был на самом деле. Если эти данные хранить в централизованной БД, то администратор легко их сможет поправить, как надо и их достоверность не будет гарантирована. В распределенной системе данные хранятся на независимых узлах и в независимых копиях БД, что гарантирует их неизменяемость.
                  0
                  Если хочется чтобы запись никто не изменил, ее можно подписать публичным ключем.
                  В блокчейне тот кто пишет блокчейн, имеет над ним полный контроль — это заблуждение, что блокчейн неизменен, см. историю THE DAO.
            +1

            А чем готовые решения не подошли? Cosmos sdk, substrate?

              0
              Их можно использовать как часть функционала, на равне с IPFS, однако сервер WebSocket-a, виртуальная стековая машина (в пакете xvm) все-равно же разрабатываются как самостоятельные элементы, без них смысл затеи теряется. А такие ВМ как EVN, TVM не очень подходят, там нету такого понятия как идентификатор состояния, который был придуман в рамках этого проекта.
                0

                А зачем свою стековую машину делать? wasm? Что-то нужно особенное — пишите отдельный функционал. Какой выигрыш в своем vm?

                  0
                  Ну давайте рассмотрим это на примере TON, возьмем допустим вот эту статью. Когда проектировался TON то предполагалась возможность совместимости с EVM. Так же можно сделать и совместимость с wasm. Если бы веб-ассемблер был бы общим стандартом, тогда конечно изобретать велосипед никто бы не стал
                    0

                    И? Так почему велосипед?

                      0
                      Тут 2 причины. 1. Если мы говорим о совместимости с EVM и TON, то придется делать транслитератор, что сложнее, чем просто добавить совместимые байтовые коды. 2. Если мы пишем свой высокоуровневый язык, то для wasm его сделать сложнее, учитывая что там всякие s-выражения, которые нужно как-то связать с инструкциями такого языка.
                        +1

                        А если мы не пишем свой язык? Пока не понятно, зачем нам это вообще делать для решения задачи.

                          0
                          Ну скажем пока для первой стадии проектирования так проще. Вот как можно отправить транзакцию в этот блокчейн:
                          function SendTx() {
                          	var tx = new Transaction(1);
                          	tx.addIn("thathash1", 0, [0, 0, 0, 0, 0]);
                          	tx.addOut(1230, [0, 8, 3, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 4, 2, 12]);
                          	console.log(tx.getHash());
                          	console.log(tx.getBase64());
                          	console.log(tx.getSendingTx());
                          	ws.send(tx.getSendingTx());
                          }

                          Этот код складывает 4 и 5. Согласитесь проще чем wasm. Если понадобится можно и его поддержку в дальнейшем добавить))
                            +1

                            Не проще, потому что напрямую на wasm вы писать не будете: rust, c++, golang, nim, .net и дюжина вариантов (https://github.com/appcypher/awesome-wasm-langs/) к вашим услугам.

                              0
                              Да вы правы. Добавляю в проект поддержку WASM :)

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

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