Теперь мне хотелось бы рассказать, о том, как этот проект я начинал, что сейчас и опишу подробно программный код.
Это первая статья из цикла статей. Здесь я описываю сервер и протокол. На самом деле, читатель даже может написать и свои варианты этих элементов блокчейна.
А вот вторая часть — про структуры данных блокчейна и транзакций, а также о пакете реализующим взаимодействие с базой данных.
В прошлом году на хакатоне Цифровой прорыв подкинули идею, чтобы для промышленности и цифровой экономики сделать полезную систему по технологии распределенных реестров, так же на разработку был выдан грант Фондом содействия инновациям (о гранте следует мне написать отдельную статью, для тех кто стартапы только начинает делать), а теперь по-порядку.
Разработка происходит на языке 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 будет рассмотрен планировщик синхронизации, далее стековая машина обрабатывающая байткод из входов и выходов, криптография и пулы для выходов.
strcpy
В чем смысл блокчейна в данном случае? Почему не mysql?
Zolg
На mysql грант не давали
rusldv Автор
Про грант я напишу отдельную статью. Там можно любой проект подавать, хоть на PHP просто он должен быть лучше аналогов, например технически или экономически выгоднее.
proninyaroslav
Не модно же.
rusldv Автор
Если обеспечить все что касается синхронизации блоков их связывания, консенсус, то практически любая база данных подойдет. Там в пакете blockchain есть функции сохранения в БД в данном случае используется LevelDB. Об этом пакете постараюсь написать попозже.
strcpy
Зачем нужна распределенная система в данном случае?
rusldv Автор
Потому что поставщики, заказчики и потребители не доверяют как правило друг-другу. Даже если верить написанному составу никто не знает какой технологический процесс был на самом деле. Если эти данные хранить в централизованной БД, то администратор легко их сможет поправить, как надо и их достоверность не будет гарантирована. В распределенной системе данные хранятся на независимых узлах и в независимых копиях БД, что гарантирует их неизменяемость.
strcpy
Если хочется чтобы запись никто не изменил, ее можно подписать публичным ключем.
В блокчейне тот кто пишет блокчейн, имеет над ним полный контроль — это заблуждение, что блокчейн неизменен, см. историю THE DAO.