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

Такие платформы, как Steemit, доказали, что можно создавать такие сообщества и вознаграждать пользователей за их вклад. Но как кто-то должен воспроизвести это и запустить свою собственную децентрализованную социальную платформу, работающую на блокчейне?

Чтобы ответить на этот вопрос, я взялся за создание децентрализованной версии HackerNews.

В процессе я оценил несколько платформ и, наконец, остановился на протоколе под названием Tendermint. Используя Tendermint, я создал прототип под названием Mint, который может служить шаблоном для создания социальных приложений на основе блокчейна.

Кодовая база находится на GitHub. Вы можете просмотреть код и демонстрацию по следующим ссылкам:

Итак, что нужно для создания социального сообщества на основе блокчейна, в котором данные, генерируемые пользователями, децентрализованы? Если вы ищете ответ, вы попали в нужное место. Читать дальше.

Предварительные наблюдения

Изначально я думал об использовании существующей платформы для создания приложения. Платформы смарт-контрактов, такие как Ethereum, NEM, NEO и т. Д., Предлагают хранение активов, но они не предназначены для хранения больших объемов данных. .

HyperLedger Fabric впечатляет, но спроектирован для развертывания в частных блокчейн-сетях. Хешграф звучит интересно, но на данный момент он экспериментальный.

Другими потенциальными решениями были: боковые цепи Lisk, Loom Network и BigChainDB. Первые два находятся в закрытой альфа-версии (только по приглашениям), а BigChainDB работает на Tendermint.

Поэтому вместо использования BigChainDB я решил поиграть непосредственно с Tendermint и посмотреть, что возможно.

Почему нежная мята

Tendermint - это протокол, который заботится об уровне консенсуса с использованием алгоритма BFT, в то время как вы просто сосредотачиваетесь на написании бизнес-логики.

Прелесть протокола в том, что вы буквально свободны выбирать любой язык программирования для создания интерфейса (Application Blockchain Interface или просто ABCI), который взаимодействует с блокчейном.

Tendermint обрабатывает самые сложные аспекты цепочки блоков, такие как этапы производства блоков, одноранговое соединение, сплетни о новых блоках, обработка транзакций и многое другое. Он хранит транзакции на диске с помощью LevelDB, а также доставляет подтвержденную транзакцию на ваш сервер ABCI, чтобы вы могли создать из него глобальное состояние.

Звучит интересно? Давайте посмотрим, как создать приложение блокчейн, которое хранит данные в сети с помощью Tendermint.

Что нужно?

Вот что вам понадобится:

  • Сервер Macbook / Ubuntu
  • Голанг
  • Нежная мята
  • MongoDB
  • И пиво… (Любители кофе могут заменить его на кофе)

Настройка машины

Tendermint написан Go. Итак, нам нужно сначала установить язык Go. Посетите эту ссылку, чтобы проверить несколько вариантов загрузки. Если вы используете Ubuntu, вы можете следовать этому руководству.

По умолчанию Go выбирает $HOME/go в качестве рабочего пространства. Если вы хотите использовать другое место в качестве рабочего пространства, вы можете установить переменную GOPATH в ~/.profile. С этого момента мы будем называть это место GOPATH.

Вот как файл ~/.profile выглядит на моей машине:

export GOPATH="$HOME/go" 
export PATH=~/.yarn/bin:$GOPATH/bin:$PATH
export 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 tendermint
git checkout v0.19.7

Это переведет вас на v0.19.7. Чтобы продолжить установку, выполните следующие команды:

make get_tools 
make get_vendor_deps
make 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 (Proof of State), в дополнение к консенсусу BFT. В этом случае мы просто будем придерживаться простого консенсуса BFT. Я оставлю добавление PoS на ваше усмотрение.

Я предлагаю вам клонировать сервер ABCI блокчейна (кодовое название mint) с GitHub. Но прежде чем мы продолжим, нам нужно установить инструмент управления зависимостями под названием dep.

Если у вас Mac, вы можете просто запустить brew install dep. Для Ubuntu выполните следующую команду.

curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

Теперь вы можете клонировать кодовую базу монетного двора.

cd $GOPATH/src
git clone https://github.com/Hashnode/mint
cd mint
dep ensure
go 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.

Теперь посмотрим на jsonstorepackage.

Бизнес-логика

Вот репо 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 Time
func 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 определены различные коды, такие как code.CodeTypeBadSignature. Это просто целые числа. Просто помните, что если вы хотите отклонить транзакцию, вы должны вернуть ненулевой код. В нашем случае, если мы обнаруживаем, что подпись сообщения недействительна, мы возвращаем 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. Мы делаем это для того, чтобы пользователи нашего веб-сайта могли легко читать данные.

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

Во-первых, мы продолжим и демаршалируем txdata в объект 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 Post
post.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 User
err := 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 rank
post.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 UserPostVote
document.ID = bson.NewObjectId()
document.UserID = user.ID
document.PostID = post.ID
db.C("userpostvotes").Insert(document)

Фактический блок кода имеет оператор switch, который обрабатывает каждый тип транзакции по-разному. Не стесняйтесь проверить код и поиграться. Если что-то непонятно, не стесняйтесь писать свои запросы в комментариях ниже.

Теперь, когда мы изучили два важных аспекта сервера ABCI, давайте попробуем запустить ядро ​​Tendermint и наш сервер и посмотрим, как отправлять транзакции.

Чтобы запустить приложение, выполните следующие команды с двух разных терминалов.

Первый забег:

mint

Если команда завершится успешно, вы увидите следующий вывод в терминале:

Перед запуском mint убедитесь, что MongoDB уже запущена. Если ваш терминал не может распознать команду 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: "http://localhost:46657",
    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 https://github.com/Hashnode/Uphack
cd Uphack
yarn
gulp less // make sure gulp is installed globally
node server.js

Вы можете получить доступ к веб-сайту по http://localhost:3000. На моей машине это выглядит так:

Поскольку у вас еще нет данных, сначала он будет выглядеть пустым. Не стесняйтесь зарегистрируйте учетную запись и отправьте несколько сообщений, чтобы визуализировать процесс.

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

Подробное описание Uphack выходит за рамки этого руководства. Однако, как упоминалось выше, код Uphack (клиент) имеет открытый исходный код и логика проста. Если вы пройдетесь по кодовой базе и изучите различные конечные точки, вы сможете лучше понять весь процесс.

Подведение итогов

Подводя итог, мы создали блокчейн, который хранит данные 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.

В заключение я хотел бы прояснить, что я не эксперт по блокчейнам. Я учусь и просто делюсь тем, что мне интересно. Меня увлекает хранение данных в цепочке, и я считаю, что децентрализованные социальные сообщества являются одним из основных вариантов использования блокчейна.

Если вы заметили какие-либо неточности в кодовой базе или в статье, не стесняйтесь указать на них. Буду признателен, если вы воспользуетесь мятой и Uphack и оставите свой отзыв. Всегда приветствуются пиарщики!

Дайте мне знать, что вы думаете, в комментариях ниже!