Розробка блокчейну для промисловості на Go. Частина 1

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

Розробка блокчейну для промисловості на Go. Частина 1

Це перша стаття із циклу статей. Тут я описую сервер та протокол. Насправді, читач навіть може написати свої варіанти цих елементів блокчейна.

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

Минулого року на хакатоні Цифровий прорив підкинули ідею, щоб для промисловості та цифрової економіки зробити корисну систему за технологією розподілених реєстрів, так само на розробку було видано грант Фондом сприяння інноваціям (про грант слід мені написати окрему статтю, для тих, хто стартапи тільки починає робити ), а тепер по-порядку.

Розробка відбувається мовою Go, а база даних у якій зберігаються блоки - LevelDB.
Основні частини це протокол, сервер (який запускає TCP і WebSocket — перший синхронізації блокчейна, другий для підключення клієнтів, відправки транзакцій і команд із JavaScript, наприклад.

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

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

Папки (директорії як їх називає Linux) в розумінні Go-програмістів називаються пакетами, тому на початку кожного файлу з Go-кодом з цієї директорії на початку пишуть package имя_папки_де_сидит_этот_файл. Інакше не вдасться згодувати компілятор пакет. Ну це для тих, хто знає цю мову, не секрет. Ось ці пакети:

  • Мережева взаємодія (server, client, protocol)
  • Структури даних, що зберігаються і передаються (block, transaction)
  • База даних (Blockchain)
  • Консенсус (Consensus)
  • Віртуальна машина (xvm)
  • Допоміжні (crypto, types) поки що все.

Ось посилання на github

Ця навчальна версія в ній відсутня міжпроцесна взаємодія та кілька експерементальних компонентів, за те структура відповідає тій над якою ведеться технологія. Якщо у вас буде що підказати в коментарях, я із задоволенням врахую в подальшій розробці. А тепер пояснення щодо пакетів server і протокол.

Спочатку подивимось на server.

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

Підпрограма використовує такі пакети: сервер, протокол, Типи. У самому пакеті в tcp_server.go міститься структура даних Служити.

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

Вона може приймати такі параметри:

  • Мережевий порт, яким здійснюватиметься обмін даними
  • Файл конфігурації сервера у форматі JSON
  • Прапор запуску в режимі налагодження (приватного блокчейну)

Хід виконання:

  • Зчитується конфігурація із файлу JSON
  • Перевіряється прапор режиму налагодження: якщо його встановлено, то запуск планувальника мережевої синхронізації не здійснюється і блокчейн не завантажується
  • Ініціалізація структури даних конфігурації та запуск сервера

сервер

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

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

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 налаштовані таким чином, щоб обробляли отримані дані функціями пакету протокол.

протокол

Протокол служить засобом, який представляє дані при мережному обміні.

Choice(str string) (string, error) виконує первинну обробку прийнятих сервером даних, на вхід отримує рядкове подання даних та повертає рядок підготовлений для Перекладач:

  • Вхідний рядок розбивається на head та body за допомогою ReqParseN2(str)
  • head розбивається на елементи і поміщається в слайс commands за допомогою ReqParseHead(head)
  • В switch(commands[0]) вибираємо отриману команду (cmd, key, address або спрацьовує секція дефолт)
  • У cmd перевіряється 2 команди switch(commands[1]) - length и getversion.
  • перевіряє тип даних у commands[2] і зберігає його в тип даних
  • Перевіряє що тіло містить рядкове значення
    len(body) < 1
  • Повертає рядок відповідь:
    "result:bufferlength:" + datatype + "/" + body
  • getversion повертає рядок
    return "result:version/auto"

Перекладач

Містить структуру InterpreteData і виконує вторинну обробку поверненої з Вибір рядки та формування об'єкта InterpreteData.

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

Функція

Interprete(str string) (*InterpreteData, error)

приймає рядок результат та створює повертає посилання на об'єкт InterpreteData.

Хід виконання:

  • аналогічно Вибір виймається head та body за допомогою ReqParseN2(str)
  • head розбивається на елементи за допомогою ReqParseHead(head)
  • Ініціалізується об'єкт InterpreteData і повертається покажчик на нього:

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

Цей об'єкт використовується в server.go пакету main.

Клієнт

Пакет 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]))
  • Якщо передано корисне навантаження то передає його
    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 буде розглянуто планувальник синхронізації, далі стекова машина обробляє байткод із входів і виходів, криптографія та пули для виходів.

Джерело: habr.com

Додати коментар або відгук