Привет, Хабр! Представляю вашему вниманию перевод статьи "Code your own blockchain in less than 200 lines of Go!".
Данный урок является хорошо адаптированным постом про простое написание blockchain на Javascript. Мы портировали его на Go и добавили дополнительных фич, таких как просмотр цепочек в браузере.
Примеры в уроке будут основываться на данных сердцебиения. Мы ведь медицинская компания. Для интереса, вы можете подсчитать свой пульс (кол-во ударов в минуту) и учитывать это число во время учебного курса.
Почти каждый разработчик в мире слышал про blockchain, но большинство до сих пор не знают, как это работает. Многие слышали только про биткоин, смарт-контракты. Данный пост является попыткой развеять слухи о blockchain, помогая Вам написать свой собственный blockchain на Go менее чем в 200 строк кода! В конце данного урока Вы сможете запустить и записать данные в blockchain локально, а так же просмотреть это в браузере.
Есть ли более хороший способ узнать о blockchain, чем создать свой собственный?
Что вы сможете сделать
- Создать свой собственный blockchain
- Понять, как работает хэширование в сохранение целостности цепочки блоков
- Увидеть, как добавляются новые блоки
- Увидите, как разрешаются коллизии, когда несколько узлов генерируют блоки
- Создадите просмотр вашего blockchain в браузере
- Добавите новые блоки
- Получите базовые знания о blockchain
Что вы не сможете сделать
Что бы этот пост оставался простым, мы не будем рассматривать более совершенные концепции proof of work и proof of stake. Сетевое взаимодействие будет моделироваться, что бы Вы могли просматривать Ваш blockchain и просматривать добавленные блоки. Сетевая работа будет зарезервированная для будущих постов.
Давайте начнем!
Установка
Поскольку мы собираемся писать код на Go, мы предполагаем, что у вас уже есть опыт разработки на нем. После установки мы так же будем использовать следующие пакеты:
go get github.com/davecgh/go-spew/spew
Spew позволяет нам красиво выводить структуры и слайсы в консоль.
go get github.com/gorilla/mux
Gorilla/mux это популярный пакет для написания обработчиков запросов.
go get github.com/joho/godotenv
Gotdotenv
позволяет нам читать из файла .env
который лежит в корне каталога, поэтому нам не придется задавать в нашем коде такие параметры, как http порт.
Давайте создадим наш .env
файл в корне каталога, который будет определять порт на котором мы будем слушать HTTP запросы. Просто добавьте строку в файл:
ADDR=8080
Создайте файл main.go
. Вся реализация будет в этом файле и будет содержать менее 200 строк кода.
Импорты
Импорты пакетов, вместе с объявлением пакета:
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
Модель данных
Давайте определим структуру каждого из наших блоков, которые представляют собой blockchain. Чуть ниже мы объясним для чего необходимы все эти поля:
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
Каждый блок содержит данные, которые будут записаны в blockchain и представляет собой событие каждого замера пульса.
Index
— индекс записи данных в blockchainTimestamp
— временная метка, когда данные записываютсяBPM
— удары в минуту. Это частота вашего пульсаHash
— идентификатор SHA256, идентифицирующий текущую записьPrevHash
— идентификатор SHA256, идентифицирующий предыдущую запись в цепочке
Давайте объявим наш blockchain, который представляет собой просто слайс структур:
var Blockchain []Block
Итак, как хеширование используется в блоках и в blockchain? Мы используем хэши для определения и сохранения блоков в правильном порядке. Благодаря тому, что поле PrevHash
в каждом блоке ссылается на поле Hash
в предыдущем блоке (т.е. они равны), мы знаем правильный порядок блоков.
Хэширование и создание новых блоков
Зачем нам хэшировать? Мы получаем хэш по двум основным причинам:
- Чтобы сэкономить место. Хэши производятся из всех данных, находящихся в блоке. В нашем случае есть только несколько блоков данных, но представьте, что у нас есть данные из сотен, тысяч или миллионов предыдущих записей. Намного эффективнее хэшировать эти данные в одну строку SHA256 и хэшировать хеши, чем копировать все данные предыдущих блоков снова и снова.
- Сохранение целостности цепочки. Сохраняя предыдущие хэши, как мы делаем на диаграмме выше, мы можем гарантировать, что блоки в blockchain находятся в правильном порядке. Если злоумышленник захочет присоединитьсяФ и манипулировать данными (например, изменить сердечный ритм, что бы исправить цены на страхование жизни), хэши начнут изменяться и все будут знать, что цепочка "сломана" и все будут знать, что доверять этой цепочки нельзя.
Давайте напишем функцию, которая возьмет наши данные Block
и создаст для них хэш SHA256.
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
Функция calculateHash
объединяет в одну строку Index
, Timestamp
, BPM
, PrevHash
из структуры Block
, которая является аргументом функции и возвращается все в виде строкового представления хэша SHA256. Теперь мы можем сгенерировать новый блок со всеми необходимыми элементами с помощью новой функции generateBlock
. Для этого нам нужно будет передать предыдущий блок, что бы мы могли получить его хэш и индекс, а так же передадим новое значение частоты пульса BPM
.
func generateBlock(oldBlock Block, BPM int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1
newBlock.Timestamp = t.String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.Hash
newBlock.Hash = calculateHash(newBlock)
return newBlock, nil
}
Обратите внимание, что текущее время автоматически записывается в блок через time.Now()
. Так же обратите внимание, что была вызвана функция calculateHash
. В поле PrevHash
скопировано значение хэша из предыдущего блока. Index
просто увеличивается на единицу от значения из предыдущего блока.
Проверка блока
Теперь нам нужно написать функционал для проверки валидности предыдущих блоков. Мы делаем это проверяя Index
, что бы убедиться, что они увеличиваются так, как это ожидается. Мы так же проверяем, что бы PrevHash
действиетльно совпадал с Hash
предыдущего блока. И наконец, мы повторно вычисляем хэш текущего блока, что бы убедиться в его корректности. Давайте напишем функцию isBlockValid
, которая выполняет все эти действия и возвращает bool значение. Функция вернет true
, если все проверки пройдут верно:
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
Что, если мы столкнемся с проблемой, когда два узла нашей blockchain экосистемы добавили блоки в свои цепочки, и мы получили их оба. Какой из них мы выберем, как правильный источник? Мы выбираем наиболее длинную цепь. Это классическая проблема в blockchain.
Итак, давайте убедимся, что новая цепочка, которую мы принимаем, длиннее текущей цепи. Если это так, мы можем перезаписать нашу цепочку новой, у которой есть новый блок или блоки.
Мы просто сравним длину срезов цепей:
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
Если у Вас получилось, то можете похлопать себя по спине! Мы описали каркас функционала для нашего blockchain.
Теперь нам нужен удобный способ просмотра нашего blockchain и запись в него, в идеале в браузере, что бы мы могли похвастаться друзьям!
Web Server
Мы предполагаем, что вы уже знакомы с тем, как работают веб-серверы, и у вас есть немного опыта работы на Go.
Используем пакет Gorrila/mux
, который загрузили ранее. Создадим функцию run
для запуска сервера и вызовем ее позже.
func run() error {
mux := makeMuxRouter()
httpAddr := os.Getenv("ADDR")
log.Println("Listening on ", os.Getenv("ADDR"))
s := &http.Server{
Addr: ":" + httpAddr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
return err
}
return nil
}
Обратите внимание, что порт конфигурируется из вашего .env
-файла, который мы создали ранее. Вызовем метод log.Println
для вывода в консоль информации о запуске сервера. Мы настраиваем сервер и вызываем ListenAndServe
. Обычная практика в Go.
Теперь нам нужно написать функцию makeMuxRouter
, которая будет определять наши обработчики. Для просмотра и записи нашего blockchain в браузере нам хватит двух простых роутов. Если мы отправляем GET
запрос на localhost
, то мы просматриваем нашу цепочку. Если отправляем POST
запрос, то мы можем записывать данные.
func makeMuxRouter() http.Handler {
muxRouter := mux.NewRouter()
muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
muxRouter.HandleFunc("/", handleWriteBlock).Methods("POST")
return muxRouter
}
Обработчик GET
запроса:
func handleGetBlockchain(w http.ResponseWriter, r *http.Request) {
bytes, err := json.MarshalIndent(Blockchain, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, string(bytes))
}
Мы будем описывать blockchain в формате JSON, который можно будет просматривать в любом браузере по адресу localhost:8080
. Вы можете задать порт в файле .env
.
POST
запрос немножко сложнее и нам понадобится новая структура сообщений Message
.
type Message struct {
BPM int
}
Код для обработчика записи в blockchain.
func handleWriteBlock(w http.ResponseWriter, r *http.Request) {
var m Message
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&m); err != nil {
respondWithJSON(w, r, http.StatusBadRequest, r.Body)
return
}
defer r.Body.Close()
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], m.BPM)
if err != nil {
respondWithJSON(w, r, http.StatusInternalServerError, m)
return
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
spew.Dump(Blockchain)
}
respondWithJSON(w, r, http.StatusCreated, newBlock)
}
Причина, по которой мы использовали отдельную структуру сообщения, заключается в том, что тело POST
запроса приходит в формате JSON
и мы будем использовать его для записи новых блоков. Это позволяет нам отправить POST
запрос следующего вида и наш обработчик заполнит оставшуюся часть блока за нас:
{"BPM":50}
50
— пример частоты пульса. Можете использовать своё значение пульса.
После декодирования тела запроса в структуру var m Message
, мы создадим новый блок, передавая предыдущий бок и новое значение пульса в функцию generateBlock
, которую мы писали ранее. Проведем быструю проверку, что бы убедиться в правильности нового блока функцией isBlockValid
.
Примечания:
spew.Dump
— удобная функция, которая красиво выводит структуры в консоль. Очень помогает в отладке.- для тестирования запросов, нам нравится использовать Postman.
curl
так же хорошо справляется, если вы не можете уйти от терминала.
Хочется получать уведомление, когда наши POST
запросы успешны или завершились с ошибкой. Мы используем небольшую обертку, для получения результата. Помните, что в Go никогда не игнорируются ошибки.
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{}) {
response, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("HTTP 500: Internal Server Error"))
return
}
w.WriteHeader(code)
w.Write(response)
}
Почти готово!
Давайте соединим все наработки в одной функции main
:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
go func() {
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}()
log.Fatal(run())
}
Что здесь происходит?
godotenv.Load()
позволяет нам читать переменные из файла.env
- genesisBlock — самая важная часть основной функции
main
. Нам нужно проинициализировать первый блок, т.к. предыдущего блока еще не существует.
Все готово!
Весь код вы можете забрать с github
Давайте проверим наш код.
Запускаем в терминале наше приложение go run main.go
В терминале мы видим, что веб-сервер работает и мы получаем вывод нашего проинициализированного первого блока.
Теперь посетите localhost:8080. Как и ожидалось, мы видим первый блок.
Теперь давайте отправим POST
запросы для добавления блоков. Используя Postman, мы собираемся добавить несколько новых блоков с различными значениями BPM
.
curl команда (от переводчика):
curl -X POST http://localhost:8080/ -H 'content-type: application/json' -d '{"BPM":50}'
Обновим нашу страничку в браузере. Теперь можно увидеть новые блоки в нашей цепочке. Новые блоки содержат PrevHash
соответствуют Hash
у старых блоков, как мы и ожидали!
В дальнейшем
Поздравляем! Вы только что создали свой blockchain с правильным хэшированием и блочной проверкой. Теперь Вы можете изучать более сложные проблемы blockchain, такие, как Proof of Work, Proof of Stake, Smart Contracts, Dapps, Side Chains и другие.
Данный урок не затрагивает такие темы, как новые блоки добавляются с помощью Proof of Work. Это будет отдельный урок, но существует множество blockchain и без механизмов Proof of Work. Сейчас все моделируется путем записи и просмотра данных blockchain на веб-сервере. В этом уроке нет составляющей P2P.
Если Вы хотите, что бы мы добавили механизм Proof of Work и работу по сети, вы можете сообщить об этом в чате Telegram или подписаться на нас в Twitter! Это лучшие способы связаться с нами. Мы ждем новых отзывов и новых предложений по урокам. Мы рады услышать Вас!
Чтобы узнать больше о Coral Health и о том, как мы используем blockchain в исследовательской работе по медицине, можете посетить наш сайт.
P.S. Автор перевода будет благодарен за указанные ошибки и неточности перевода.
Комментарии (18)
Hedgehogues
31.01.2018 05:52-1Замечательно все. Но зачем для того, чтобы запомнить значение пульса, городить такой огород? Нельзя ли это в json класть или в ближайшее?
darjke
31.01.2018 07:20Очевидно что цель статьи показать как реализовать простой блокчейн, а не сохранять пульс. Поэтому и пример выбран максимально простой.
Scratch
31.01.2018 09:49В половине мест BPM, в другой BMP.
На последнем скриншоте у первого блока нет PrevHash
DROS
31.01.2018 10:42Уже который о счету тутор про «пишем блокчейн в *** строчек кода на ***» и всегда в конце приписка, мол «а про сетевое взаимодействие в следующих частях» и привет. Как правило следующие части так и остаются обещаниями.
Понятное дело, что это перевод и претензий к переводчику нет никаких, но все же. Не менее интересна реализация децентрализации.
ЗЫ.
А почему нет плашки перевод?
yellow79
31.01.2018 11:02Что-то не совсем понятно на счёт решения коллизий. Ну выбрали мы более длинную цепь, я так понимаю объединили в общую цепь. А что делать с более короткой? Куда девается она? Отбрасывается в мусор? Как быть с индексами при объединении цепей? Они же не будут совпадать с порядком индексов общей цепи.
Popik
31.01.2018 11:46По моему данный пример наглядно иллюстрирует тотальное непонимание системы блокчейн и того, зачем она нужна. В приведенном примере полностью отсутствует майнинг, который как раз гарантирует защиту децентрализованный данных при помощи PoW. Какой смысл хранить последовательные блоки, хранящие хеш предыдущего, если в такой системе можно обновить случайный блок в цепи, а потом за секунду перехешировать всю цепочку? Чем это вообще лучше обычной реляционной БД, которая уже готова, и которая предоставляет уровень «защиты от подтасовки» ровно такой же, то есть нулевой?
KosToZyB Автор
31.01.2018 12:15Да, вы правы. Авторы статьи хотели простыми словами объяснить, что такое блокчейн. А сетевое взаимодействие будет в последующих постах.
nckma
31.01.2018 12:56Как мне кажется PoW так же мало что гарантирует (но может я и ошибаюсь).
Да, каждый блок подписан «красивым хэшем», на который нужно потратить усилия.
Проблема в том, что у первых блоков не требовался «сильно красивый хэш».
У первых блоков в цепочке, защита (difficulty) была хороша на тот момент времени, когда они создавались. Иными словами, первые блоки чейна посчитанные на CPU пересчитать нынешними асик майнерами — плевое дело. Единственная но существенная проблема как изменить и пересчитать скажем только первые N блоков и потом присоединить измененные начальные блоки к последующим неизмененным (которые все труднее и труднее пересчитывать)?
Далее с течением времени производительность вычислителей скорее всего будет расти.
То что сейчас невозможно быстро пересчитать будет возможно пересчитать через некоторое время.Popik
31.01.2018 13:15Если появится принципиально новый вычислитель с мощностью более 51% всей мощности сети, и он не будет задействован в майнинге, то тогда блокчейну конец. Получится переподписать всю историю с самого начала и создать альтернативную историю. Но тут *скорее всего* сыграет роль огромное количество пользователей блокчейна, которые быстро используют тот же вычислитель в целях майнинга, и тогда такой проблемы не будет.
С точки зрения того, чтобы взять и переделать первые блоки, а потом «приклеить» их к новым, это маловероятно, пока не обнаружена уязвимость в sha256, которая позволить делать прогнозируемые коллизии (но если она будет обнаружена сломается сразу весь механизм PoW, и блокчейну опять конец). Потому что если такая возможность будет, тогда можно будет и новые блоки модифицировать так, чтобы их не перемайнивать заново. Более того «векселя» в блокчейне имеют строго определенный формат, и вероятность того, что незначительное изменение в «векселе» позволит получить хеш-коллизию близка к нулю. Такую коллизию скорее более вероятно получить на целой цепочке ранних блоков, но получается, что нужно будет переподписать раннюю цепочку миллиарды раз, чтобы получить коллизию и «приклеиться» к новым блокам.
zm_llill
31.01.2018 15:02Забавно, буквально на днях сделал тоже самое, но не описывал в статье. Все таки есть некая метафизическая связь между программистами :)
оставлю это тутBlogoslov
31.01.2018 20:07А как у вас Chain interface связан с функциями getBlock, getBlock и др? У вас функции сами по себе, а интерфейс сам по себе… это как минимум странно.
zm_llill
31.01.2018 20:27По сути, там Интерфейс не нужен, если говорить о том коде, что сейчас есть. Но есть пару нюансов: это первый проект на Го и я пробовал все, в том числе и Интерфейсы; предполагается, что еще появятся методы для работы по сети и типа Интерфейс про запас :)
pistol
31.01.2018 18:39Самое сложное в блокчейне — механизм аутентификации и авторизации каждой траты в этой цепочке блоков.
Заголовок «блокчейн в 200 строк кода» очень желтый)) Но все равно спасибо и за это.
Hisus
31.01.2018 20:08Мне кажется, или в функции isBlockValid повторно вычисленных хэш должен совпадать с уже сгенереным?
"if calculateHash(newBlock) = newBlock.Hash "KosToZyB Автор
31.01.2018 20:11Там происходит проверка если вновь сгенерированный хэш не равен хэшу этого блока, то возращаем false т.е. блок невалидный.
olku
Оригинальный пост содержит ошибку валидации двух одинаковых цепочек