
Содержание статьи
от Sandeep Panda

В Hashnode мы много экспериментировали с блокчейном и их вариантами использования. Мы сами руководим сообществом разработчиков, и идея «децентрализованных сообществ» меня очень увлекает. Тот факт, что каждый владеет данными и контролирует платформу, может породить новые типы социальных приложений и нарушить традиционный способ создания онлайн-сообществ.
Такие платформы, как Steemit, доказали, что можно создавать такие сообщества и вознаграждать пользователей за их вклад. Но как кому-то подойти к копированию и запуску собственной децентрализованной социальной платформы на основе блокчейна?
Чтобы ответить на этот вопрос, я принялся за создание децентрализованной версии HackerNews.
В ходе этого процесса я оценил несколько платформ и, наконец, остановился на протоколе под названием Tendermint. Используя Tendermint, я создал прототип под названием Mint, который может служить шаблоном для создания социальных приложений на основе блокчейна.
Кодовая база находится на GitHub. Вы можете просмотреть следующие ссылки для кода и демонстрации:
Итак, что нужно, чтобы построить социальное сообщество на основе блокчейна, где данные, созданные пользователями, децентрализованы? Если вы ищете ответ, вы попали в нужное место. Читайте дальше.
Предварительные наблюдения
Сначала я думал об использовании существующей платформы для создания программы. Платформы Smart Contract, как Ethereum, NEM, NEOи так далее предлагают хранение активов, но они не предназначены для хранения большого количества данных.
HyperLedger Fabric является убедительным, но разработан для развертывания в частных сетях блокчейн. Hashgraph звучит интересно, но сейчас он экспериментален.
Другие потенциальные решения были: Лиск Sidechains, Loom Network, и BigChainDB. Первые два находятся в приватной альфа-версии (только приглашение), тогда как BigChainDB работает на Tendermint.
Поэтому вместо использования BigChainDB я решил поиграть прямо с Tendermint и посмотреть, что возможно.
Почему Tendermint
Tendermint — это протокол, заботящийся об уровне консенсуса с помощью алгоритма BFT, а вы только сосредоточены на написании бизнес-логики.
Прелесть протокола заключается в том, что вы буквально свободны выбирать любой язык программирования для создания интерфейса (Application Blockchain Interface или просто ABCI), взаимодействующего с блокчейном.
Tendermint обрабатывает самые сложные аспекты блокчейна, такие как циклы производства блоков, одноранговое подключение, сплетни о новых блоках, обработка транзакций и т.д. Он сохраняет транзакции на диске с помощью LevelDB, а также доставляет подтвержденную транзакцию на ваш сервер ABCI, чтобы вы могли создать глобальное состояние из нее.
Звучит интересно? Давайте посмотрим, как создать блокчейн-приложение, хранящее данные в цепочке с помощью Tendermint.
Что нужно?
Вот что вам понадобится:
- Сервер Macbook/Ubuntu
- Голанг
- Tendermint
- MongoDB
- И пиво… (Кофеманы могут заменить это кофе)
Настройка машины
Tendermint написан на Go. Итак, сначала нам нужно установить язык Go. Перейдите по этой ссылке, чтобы просмотреть несколько вариантов загрузки. Если вы используете Ubuntu, вы можете следовать этому руководству.
По умолчанию Go выбирает $HOME/go
как ваше рабочее пространство. Если вы хотите использовать другое местоположение в качестве рабочей области, вы можете установить GOPATH
переменная в ~/.profile
. Теперь мы будем называть это место как GOPATH
.
Во как ~/.profile
файл смотрится на моей машине:
export GOPATH="$HOME/go" export PATH=~/.yarn/bin:$GOPATH/bin:$PATHexport GOBIN="$GOPATH/bin"
Не забудьте установить GOBIN
переменной, как показано выше. Здесь будут установлены двоичные файлы Go.
Не забудьте запустить source ~/.profile после обновления файла.
Теперь мы можем установить Tendermint. Вот шаги:
cd $GOPATH/src/github.com
mkdir tendermint
cd tendermint
И, наконец,
git clone https://github.com/tendermint/tendermint
Это установит последнюю версию Tendermint. Поскольку я проверил свой код v0.19.7
Давайте проверим конкретный выпуск.
cd tendermintgit checkout v0.19.7
Это поставит вас на v0.19.7. Чтобы продолжить установку, выполните следующие команды:
make get_tools make get_vendor_depsmake install
Поздравляю! Вы успешно установили Tendermint. Если все было установлено по назначению, команда tendermint version
распечатает версию Tendermint.
Теперь вам нужно установить MongoDB.
Кодирование блокчейна
Если вы хотите понять, как работает Tendermint, просмотрите это руководство. Вам также может быть полезна следующая диаграмма.

Здесь я очеркну несколько важных понятий:
- Ядро Tendermint обрабатывает консенсусную часть.
- Вам нужно написать сервер ABCI, обрабатывающий бизнес-логику, проверки и т.д. Хотя вы можете написать это на любом языке, на нашем языке будет Go.
- Ядро Tendermint будет взаимодействовать с вашим сервером ABCI через соединение сокетов.
- Сервер ABCI имеет множество методов (разработчики JS могут рассматривать их как обратные вызовы), которые будут вызываться ядром Tendermint во время различных событий.
- Два важных метода:
CheckTx
иDeliverTx
. Первый вызывается для проверки транзакции, а второй вызывается, когдаTx
подтверждается. DeliverTx
помогает выполнять необходимые действия на основе подтвержденных транзакций. В нашем случае мы будем использовать это для создания и обновления нашего глобального состояния, хранящегося в MongoDB.- Tendermint использует консенсус BFT. Это означает, что более 2/3 валидаторов должны иметь консенсус для осуществления транзакции. Таким образом, даже если 1/3 валидатора станут неисправными, блокчейн все равно будет работать.
- В реальном мире (по крайней мере в публичном развертывании) вы, скорее всего, добавите какой-нибудь консенсус, такой как PoS (доказательство состояния) в дополнение к консенсусу BFT. В этом случае мы просто продлим простой консенсус BFT. Добавление PoS я оставлю вам.
Я предлагаю вам клонировать сервер ABCI blockchain (под кодовым названием mint) из GitHub. Но прежде чем продолжить, нам нужно установить инструмент управления зависимостями под названием dep.
Если вы используете Mac, вы можете просто запустить brew install dep
. Для Ubuntu выполните следующую команду.
curl | sh
Теперь можно клонировать кодовую базу mint.
cd $GOPATH/srcgit clone mintdep ensurego install mint
Сладкий! Теперь вы установили mint, являющийся сервером ABCI и работающий вместе с ядром Tendermint.
Теперь позвольте мне провести вас через все настройки и весь код.
Точка входа
Вы можете найти код (и точку входа) на GitHub здесь.
Точкой входа программы является mint.go
. Важнейшей частью файла является следующий раздел:
app = jsonstore.NewJSONStoreApplication(db)srv, err := server.NewServer("tcp://0.0.0.0:46658", "socket", app) if err != nil { return err }
Вся бизнес-логика, методы и т.п. jsonstore
. Приведенный выше код просто создает сервер TCP на порту 46658
принимающий подключение сокетов от ядра Tendermint.
Теперь рассмотрим jsonstore
пакет.
Бизнес-логика
Вот jsonstore
репо.
Наш сервер ABCI выполняет две важные вещи:
- Проверка входных транзакций. Если транзакция недействительна, она возвращает код ошибки и транзакция отклоняется.
- После того, как транзакция фиксируется (подтверждается > 2/3 валидаторов) и сохраняется в LevelDB, сервер ABCI обновляет свое глобальное состояние, хранящееся в MongoDB.
Мы собираемся использовать mgo для взаимодействия с MongoDB. Поэтому, jsonstore.go
определяет 5 моделей, соответствующих 5 разным коллекциям MongoDB.
Код выглядит следующим образом:
// Post ...type Post struct { ID bson.ObjectId `bson:"_id" json:"_id"` Title string `bson:"title" json:"title"` URL string `bson:"url" json:"url"` Text string `bson:"text" json:"text"` Author bson.ObjectId `bson:"author" json:"author"` Upvotes int `bson:"upvotes" json:"upvotes"` Date time.Time `bson:"date" json:"date"` Score float64 `bson:"score" json:"score"` NumComments int `bson:"numComments" json:"numComments"` AskUH bool `bson:"askUH" json:"askUH"` ShowUH bool `bson:"showUH" json:"showUH"` Spam bool `bson:"spam" json:"spam"`}
// Comment ...type Comment struct { ID bson.ObjectId `bson:"_id" json:"_id"` Content string `bson:"content" json:"content"` Author bson.ObjectId `bson:"author" json:"author"` Upvotes int `bson:"upvotes" json:"upvotes"` Score float64 `bson:"score" json:"score"` Date time.Time PostID bson.ObjectId `bson:"postID" json:"postID"` ParentCommentID bson.ObjectId `bson:"parentCommentId,omitempty" json:"parentCommentId"`}
// User ...type User struct { ID bson.ObjectId `bson:"_id" json:"_id"` Name string `bson:"name" json:"name"` Username string `bson:"username" json:"username"` PublicKey string `bson:"publicKey" json:"publicKey"`}
// UserPostVote ...type UserPostVote struct { ID bson.ObjectId `bson:"_id" json:"_id"` UserID bson.ObjectId `bson:"userID" json:"userID"` PostID bson.ObjectId `bson:"postID" json:"postID"`}
// UserCommentVote ...type UserCommentVote struct { ID bson.ObjectId `bson:"_id" json:"_id"` UserID bson.ObjectId `bson:"userID" json:"userID"` CommentID bson.ObjectId `bson:"commentID" json:"commentID"`}
Мы также определяем несколько вспомогательных функций, например:
func byteToHex(input []byte) string { var hexValue string for _, v := range input { hexValue += fmt.Sprintf("%02x", v) } return hexValue}
func findTotalDocuments(db *mgo.Database) int64 { collections := [5]string{"posts", "comments", "users", "userpostvotes", "usercommentvotes"} var sum int64
for _, collection := range collections { count, _ := db.C(collection).Find(nil).Count() sum += int64(count) }
return sum}
func hotScore(votes int, date time.Time) float64 { gravity := 1.8 hoursAge := float64(date.Unix() * 3600) return float64(votes-1) / math.Pow(hoursAge+2, gravity)}
// FindTimeFromObjectID ... Convert ObjectID string to Timefunc FindTimeFromObjectID(id string) time.Time { ts, _ := strconv.ParseInt(id[0:8], 16, 64) return time.Unix(ts, 0)}
Они будут использованы впоследствии в коде.
Внутри CheckTx
Теперь перейдем к части проверки. Как мы принимаем или отклоняем транзакцию? Скажем, кто пытается зарегистрироваться, но не выбирает действительное имя пользователя. Как наша программа может это проверить?
Это делается через CheckTx
функция. Подпись выглядит следующим образом:
func (app *JSONStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx {
// ... Validation logic}
Когда узел Tendermint получает транзакцию, он вызываетCheckTx
сервера ABCI и проходы tx
данные как а byte
аргумент массива. Если CheckTx
возвращает ненулевой код, транзакция отклоняется.
В нашем случае клиенты посылают объекты JSON с кодировкой Base64 в узел Tendermint по запросу RPC. Итак, наша работа состоит в том, чтобы декодировать tx и расмаршировать строку в объекте JSON.
Делается это так:
var temp interface{}err := json.Unmarshal(tx, &temp)if err != nil { panic(err)}message := temp.(map[string]interface{})
message
Объект обычно выглядит следующим образом:
{ body: {... Message body}, publicKey: <Public Key of Sender>, signature: <message.body is signed with the Private Key>}
Во-первых, мы должны в этом убедиться сказал лицо действительно прислал транзакцию в блокчейн, а не кто-то другой, выдающий себя за этого человека.
Лучший способ проверки — попросить клиентов подписать тело сообщения с помощью частного пользовательского ключа и присоединить открытый ключ и подпись к полезной нагрузке. Мы воспользуемся ed25519
алгоритм для создания ключей и подписания сообщения в браузере и нажатия конечной точки RPC. В CheckTx
функцию, которую мы снова воспользуемся ed25519
и проверить сообщение с помощью открытого пользовательского ключа.
Делается это так:
pubKeyBytes, err := base64.StdEncoding.DecodeString(message["publicKey"].(string))
sigBytes, err := hex.DecodeString(message["signature"].(string))
messageBytes := []byte(message["body"].(string))isCorrect := ed25519.Verify(pubKeyBytes, messageBytes, sigBytes)if isCorrect != true { return types.ResponseCheckTx{Code: code.CodeTypeBadSignature}}
В приведенном выше примере мы используем ed25519
пакет для проверки сообщения. Различные коды, такие как code.CodeTypeBadSignature
определяются внутри code
пакет. Это просто целые числа. Просто помните, что если вы хотите отклонить транзакцию, вы должны вернуть ненулевой код. В нашем случае, если мы обнаруживаем, что подпись сообщения недействительна, мы возвращаем CodeTypeBadSignature
какой есть 4
.
Следующий раздел о CheckTx
имеет дело с разными проверками данных, такими как:
- Если пользователь отправляет любую транзакцию, кроме createUser (Sign up), мы сначала проверяем, есть ли открытый ключ пользователя в нашей базе данных.
- Если пользователь пытается создать сообщение или комментарий, он должен иметь действительные данные, например, не пуст
title
,content
и так дальше. - Если пользователь пытается зарегистрироваться, то имя пользователя должно содержать допустимые символы.
Код выглядит следующим образом:
// ==== Does the user really exist? ======if body["type"] != "createUser" { publicKey := strings.ToUpper(byteToHex(pubKeyBytes))
count, _ := db.C("users").Find(bson.M{"publicKey": publicKey}).Count()
if count == 0 { return types.ResponseCheckTx{Code: code.CodeTypeBadData} }}// ==== Does the user really exist? ======
codeType := code.CodeTypeOK
// ===== Data Validation =======switch body["type"] {case "createPost": entity := body["entity"].(map[string]interface{})
if (entity["id"] == nil) || (bson.IsObjectIdHex(entity["id"]. (string)) != true) { codeType = code.CodeTypeBadData break }
if entity["title"] == nil || strings.TrimSpace(entity["title"].(string)) == "" { codeType = code.CodeTypeBadData break }
if (entity["url"] != nil) && (strings.TrimSpace(entity["url"].(string)) != "") { _, err := url.ParseRequestURI(entity["url"].(string)) if err != nil { codeType = code.CodeTypeBadData break } }case "createUser": entity := body["entity"].(map[string]interface{})
if (entity["id"] == nil) || (bson.IsObjectIdHex(entity["id"].(string)) != true) { codeType = code.CodeTypeBadData break }
r, _ := regexp.Compile("^[A-Za-z_0-9]+$")
if (entity["username"] == nil) || (strings.TrimSpace(entity["username"].(string)) == "") || (r.MatchString(entity["username"].(string)) != true) { codeType = code.CodeTypeBadData break }
if (entity["name"] == nil) || (strings.TrimSpace(entity["name"].(string)) == "") { codeType = code.CodeTypeBadData break }case "createComment": entity := body["entity"].(map[string]interface{})
if (entity["id"] == nil) || (bson.IsObjectIdHex(entity["id"].(string)) != true) { codeType = code.CodeTypeBadData break }
if (entity["postId"] == nil) || (bson.IsObjectIdHex(entity["postId"].(string)) != true) { codeType = code.CodeTypeBadData break }
if (entity["content"] == nil) || (strings.TrimSpace(entity["content"].(string)) == "") { codeType = code.CodeTypeBadData break }}
// ===== Data Validation =======return types.ResponseCheckTx{Code: codeType}
Код действительно прост и достаточно ясен. Поэтому я не буду вдаваться в детали, а оставлю вам читать и исследовать дальше.
Внутри DeliverTx
После подтверждения транзакции и ее применения к блокчейну, ядро Tendermint звонит DeliverTx
и передает транзакцию как массив б. Сигнатура функции выглядит так:
func (app *JSONStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { // ... Code goes here}
Мы будем использовать эту функцию для создания глобального положения на основе MongoDB. Мы делаем это, чтобы пользователи нашего сайта могли легко читать данные.
Эта функция обширна и имеет несколько случаев. В этой главе я расскажу только об одном случае, который называется «После создания». Поскольку остальные коды похожи, я оставлю вам возможность копнуть глубже и исследовать полный код.
Во-первых, мы продолжим распоряжаться tx
данные в объекте JSON:
var temp interface{}err := json.Unmarshal(tx, &temp)
if err != nil { panic(err)}
message := temp.(map[string]interface{})
var bodyTemp interface{}
errBody := json.Unmarshal([]byte(message["body"].(string)), &bodyTemp)
if errBody != nil { panic(errBody)}
body := bodyTemp.(map[string]interface{})
Для создания публикации объект сообщения выглядит следующим образом:
{body: { type: "createPost", entity: { id: id, title: title, url: url, text: text, author: author }},signature: signature,publicKey: publicKey}
А вот как DeliverTx
функция создает новую запись в базе данных, когда выполняется транзакция «createPost»:
entity := body["entity"].(map[string]interface{})
var post Postpost.ID = bson.ObjectIdHex(entity["id"].(string))post.Title = entity["title"].(string)
if entity["url"] != nil { post.URL = entity["url"].(string)}if entity["text"] != nil { post.Text = entity["text"].(string)}
if strings.Index(post.Title, "Show UH:") == 0 { post.ShowUH = true} else if strings.Index(post.Title, "Ask UH:") == 0 { post.AskUH = true}
pubKeyBytes, errDecode := base64.StdEncoding.DecodeString(message["publicKey"].(string))
if errDecode != nil { panic(errDecode)}
publicKey := strings.ToUpper(byteToHex(pubKeyBytes))
var user Usererr := db.C("users").Find(bson.M{"publicKey": publicKey}).One(&user)if err != nil { panic(err)}post.Author = user.ID
post.Date = FindTimeFromObjectID(post.ID.Hex())
post.Upvotes = 1
post.NumComments = 0
// Calculate hot rankpost.Score = hotScore(post.Upvotes, post.Date)
// While replaying the transaction, check if it has been marked as spam
spamCount, _ := db.C("spams").Find(bson.M{"postID": post.ID}).Count()
if spamCount > 0 { post.Spam = true}
dbErr := db.C("posts").Insert(post)
if dbErr != nil { panic(dbErr)}
var document UserPostVotedocument.ID = bson.NewObjectId()document.UserID = user.IDdocument.PostID = post.ID
db.C("userpostvotes").Insert(document)
Фактический блок кода имеет оператор switch, обрабатывающий каждый тип транзакции по-разному. Не стесняйтесь проверить код и поиграть. Если что-то неясно, не стесняйтесь писать свои вопросы в комментариях ниже.
Теперь, когда мы рассмотрели два важных аспекта сервера ABCI, давайте попробуем запустить как ядро Tendermint, так и наш сервер и посмотрим, как отправлять транзакции.
Чтобы запустить программу, выполните следующие команды из двух разных терминалов.
Сначала запустите:
mint
Если команда выполнена успешно, вы увидите следующий вывод в терминале:

Перед запуском убедитесь, что MongoDB уже запущен mint
. Если ваш терминал не может распознать mint
команду, обязательно запустите source ~/.profile
.
Затем запустите Tendermint в другом терминале:
tendermint node --consensus.create_empty_blocks=false
По умолчанию Tendermint создает новые блоки каждые 3 секунды, даже если нет транзакций.
Чтобы предотвратить это, мы используем флаг:
consensus.create_empty_blocks=false
Теперь, когда Tendermint запущен, вы можете начать отправлять ему транзакции. Вам нужен клиент, который может генерировать ed25519
ключи, подпишите свои запросы и нажмите конечную точку RPC, открывшуюся Tendermint.
Пример запроса (Node.js) выглядит так:
const base64Data = req.body.base64Data;
let headers = { 'Content-Type': 'text/plain', 'Accept':'application/json-rpc'}
let options = { url: " method: 'POST', headers: headers, json: true, body: {"jsonrpc":"2.0","method":"broadcast_tx_commit","params": { "tx" : base64Data } ,"id":"something"}}
request(options, function (error, response, body) { res.json({ body: response.body });});
Обратите внимание, что конечная точка RPC доступна на порту 46657
.
Формирование и подписание запросов вручную может быть утомительным. Поэтому я предлагаю вам использовать Uphack (веб-сайт в стиле HackerNews, взаимодействующий с блокчейном), чтобы получить полную картину.
Чтобы установить Uphack, выполните следующие действия.
git clone Uphackyarngulp less // make sure gulp is installed globallynode server.js
Вы можете получить доступ к веб-сайту на . На моей машине это выглядит так:

Поскольку у вас нет данных, сначала они будут выглядеть пустыми. Не стесняйтесь зарегистрировать аккаунт и отправить несколько публикаций, чтобы представить процесс.
При использовании приложения откройте вкладку сети в браузере и просмотрите раздел XHR. The /rpc
URL принимает данные base64 и запрашивает конечную точку RPC Tendermint на стороне сервера. Вы можете скопировать данные base64 и вставить их в декодер base64, чтобы увидеть фактические данные, которые отправляются.

Притворство в детали Uphack выходит за рамки этого учебника. Однако, как упоминалось выше, код для Uphack (клиент) является открытым исходным кодом, и логика проста. Если вы просмотрите кодовую базу и изучите различные конечные точки, вы лучше поймете весь процесс.
Подведению
Подводя итог, мы построили a блокчейн который хранит данные JSON в цепи и принимает транзакции в форме base64. Чтобы продемонстрировать использование, мы также кратко рассмотрели Uphack, веб-сайт в стиле HackerNews который взаимодействует с блокчейном.
Однако вот несколько вещей, которые вы должны знать:
- Вы создали сеть с одним узлом. Это означает, что вы единственный валидатор. Если вам интересно многоузловое развертывание, просмотрите документацию Mint. Пока мы развернули 4-узловую сеть, и если вы хотите стать валидатором и поиграть с блокчейном, не стесняйтесь обращаться ко мне.
- Эта договоренность использует консенсус BFT. В реальном мире вам понадобится определенный алгоритм консенсуса, например, Proof of Stake, Delegated Proof of Stake и т.д.
- Конечная точка RPC блокчейну слушает
46657
и сервер ABCI работает46658
. В любое время вы можете проверить состояние блокчейна, посетив страницуlocalhost:46657/status
. - На данный момент нет стимула стать валидатором и создавать блоки. В настройках (D)PoS производители блоков должны получать определенный токен каждый раз, когда они предлагают блок. Это осталось для вас как упражнение.
- Сервер ABCI может быть написан на любом языке. К примеру, проверьте js-abci.
В заключение я хотел бы объяснить, что я не специалист по блокчейну. Я учусь и просто делюсь тем, что мне интересно. Хранение данных на цепочке меня увлекает, и я верю децентрализованные социальные сообщества является одним из основных вариантов использования блокчейна.
Если вы заметили какие-либо неточности в кодовой базе или статье, не стесняйтесь указать на это. Я буду благодарен, если вы воспользуетесь mint & Uphack и предоставите свой отзыв. PR всегда рады!
Дайте мне знать, что вы думаете в комментариях ниже!