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

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 будет рассмотрен планировщик синхронизации, далее стековая машина обрабатывающая байткод из входов и выходов, криптография и пулы для выходов.