Привет, Хабр!
Меня зовут Олег, я работаю разработчиком в одной крупной IT-компании и недавно в разговоре со знакомыми логистами, я узнал, что у них в штате работает блокчейн-специалист. Для меня мир логистики был максимально далек от цепочки блоков, как и цепочка блоков от меня, поэтому я решил погрузиться в эту технологию.
Прочитав множество статей и несколько книг, я выяснил, что теория с практикой идут рядышком, но понимание того, как же блокчейн работает на самом деле, не пришло, поэтому было решено создать что-то с нуля своими ручками.
Нужно понимать, что между наработками, которые я буду рассматривать, и «настоящим» блокчейном есть большая разница: на ранних этапах разработки блокчейн-сеть может иметь низкую производительность, уязвимости и неоптимизированные механизмы консенсуса, которые улучшатся в финальной версии. Но в любом случае этот проект будет основан на ключевых принципах децентрализованных систем. Мой пост будет полезен для таких же новичков, как я, которые имеют некоторый опыт разработки, но с технологией блокчейн не были знакомы или слышали про него краем уха. В этой статье мы рассмотрим, как создать простую приватную сеть блокчейн с использованием языка Go.
Примечание *Почему Go?*
Потому что я люблю Go Go (или Golang) стал одним из самых популярных языков для разработки блокчейн-платформ благодаря своей простоте, высокой производительности и встроенной поддержке параллелизма. Именно эти особенности делают его отличным выбором для создания децентрализованных систем.
Но на самом деле эта статья будет полезна разработчикам с любыми знаниями ЯП. Здесь я разберу базовые компоненты блокчейна без привязки к какой-либо технологии. Итак, поехали.
Архитектура блокчейна
Прежде чем приступить к разработке, кратко рассмотрим, из чего состоит блокчейн:
1. Транзакции: Операции сохранения данных в блокчейне. Это основная единица работы сети, обеспечивающая перемещение активов или данных между участниками сети. Эти транзакции используют криптографию и механизмы консенсуса для обеспечения безопасности, прозрачности и неизменяемости данных.
2. Реестр: Хранилище всех операций, произведенных в сети блокчейн. Оно позволяет участникам сети вести учёт и обмениваться данными без необходимости в централизованных доверенных посредниках.
-
Блоки: Основные элементы данных, которые содержат записи транзакций. Каждый блок содержит транзакции, связанные с предыдущими блоками, и благодаря этому блокчейн остается защищенным от фальсификаций и атак.
-
Цепь блоков: Каждый блок в блокчейне содержит часть реестра, а сами блоки связаны между собой с помощью криптографических хешей, создавая тем самым непрерывную цепочку данных.
Блоки и их цепь
Механизм консенсуса 5. Механизм консенсуса: Алгоритм, который решает задачу обеспечения доверия между участниками сети, несмотря на отсутствие центрального авторитета, и гарантирует, что все копии блокчейна в сети остаются синхронизированными и идентичными.
6. Сеть взаимодействия между узлами.
Классификация блокчейна
Сначала нужно определиться, какую конкретно сеть мы хотим создать. С 2008 года и публикации работы “Bitcoin Whitepaper” различные команды в различных компаниях, да и энтузиасты одиночки, создали бесчисленное множество децентрализованных систем со схожими принципами, но с разными решениями. Блокчейн системы можно разделить на несколько видов по двум критериям: структура реестра и механизм консенсуса. Нас будет интересовать соответствие основным свойствам блокчейна: прозрачность, неизменяемость, надежность, децентрализация, а также метрики качества распределенных систем: эффективность и гибкость.
Структура реестра:
-
Классический вид реестра был описан как раз в работе Сатоши Накамото «Bitcoin Whitepaper», в которой он был представлен в виде списка блоков, доступного для всех пользователей сети. В английской литературе такие реестры называют «Global list of blocks», но узлы биткоина фактически записывают блокчейн в виде дерева блоков. Более короткие ветви, присоединенные к основной цепи, представляют альтернативные конкурирующие вариации состояния системы (вспоминаем про то, что блокам еще нужно договориться между собой и прийти к консенсусу). Однако древовидная структура данных актуальна в основном для узлов, определяющих консенсус, с точки зрения пользователя, блокчейн представляет собой список блоков.
Global List of Blocks -
Из первого пункта стало ясно, что хоть список и удобен для восприятия, он не всегда отражает верное состояние системы, а про эффективность при ожидании всех подсчетов лучше даже не думать. Поэтому был предложен вид направленного ациклического графа (DAG), на основе которого можно получить информацию о каждом объекте системы в любом состоянии.
Directed Acyclic Graph of Blocks -
Однако не все системы подразумевают общую историю транзакций, доступную для всех участников сети. Такие платформы, как Hyperledger Fabric или Corda, предоставляют возможность работы нескольким конкурирующим компаниям в одной блокчейн сети, и тогда для хранения информации о транзакциях используются несколько реестров, информация в которые попадает по так называемым “каналам”. В таких системах участники получают доступ к своему “личному” реестру, в котором содержатся транзакции только открытые для этого участника. Очевидно, что такие меры приводят к нарушению большого количества принципов распределенных систем, однако они также показывают самые выгодные показатели эффективности и гибкости.
Several Ledgers
-
В нашем случае мы будем использовать классический список блоков в сети, защищенный криптографическими методами, чтобы сохранить приватность нашего блокчейна.
Механизм консенсуса:
-
Классический общий подход называется консенсусом Накамото (Nakamoto Consensus). Он основан на том, что истинным состоянием системы считают самую длинную цепочку блоков, которая наблюдается в каждый момент времени. В Биткойне новые блоки генерируются с помощью механизма доказательства работы (Proof-of-Work). В качестве доказательства используется криптографическая головоломка, у которой легко проверить корректность решения, но решить ее сложно, и на это требуется фактически рандомное время. Те самые майнеры биткоина соревнуются в решении такой головоломки для каждого блока, используя большое количество вычислительных мощностей (и, следовательно, электроэнергии), чтобы увеличить свои шансы на победу в “соревновании за блок”. Как только головоломка решена — новый блок создан и ее создатель получает вознаграждение.
-
Доказательство владения (Proof-of-Stake) — это распространенная альтернатива механизму консенсуса Накамото, который определяет следующий блок в цепи на основе факта владения цифровой валютой сети блокчейн. Например, майнеры Peercoin должны доказать владение определенным количеством валюты Peercoin, чтобы майнить блоки.
-
Протокол Practical Byzantine Fault Tolerance (PBFT) применяется для консенсуса в приватных блокчейнах, например, в Stellar. PBFT обеспечивает консенсус, несмотря на произвольное поведение некоторой части участников. По сравнению с консенсусом Накамото, это более традиционный подход в распределенных системах. Грубо говоря, блокчейн на основе PBFT обеспечивает гораздо более сильную гарантию согласованности и меньшую задержку, но поддерживает при этом меньшее количество участников.
Consensus Mechanisms
Реализация блокчейна
Шаг 0: Структура проекта

В дальнейшей работе я буду ссылаться на файловую структуру своего проекта. Она поможет чуть лучше понять то, о чем я буду говорить ниже.
Шаг 1: Создание сети
Предлагаю начать построение нашего решения с сети для взаимодействия будущих блоков. Протокол взаимодействия можно выбрать любой или даже реализовать несколько различных интерфейсов для передачи различных типов информации в сети.
Мы остановимся на rpc решении, так как оно хорошо подходит для передачи данных разного типа, а также гибко настраивается. С этой целью создадим саму структуру взаимодействия RPC в файле rpc.go. Не думаю, что имеет смысл ее усложнять, поэтому добавим в нее только поля адресанта и сообщения.
type RPC struct { From NetAddr // Отправитель нашего сообщения - по сути адрес-строка Payload io.Reader // Полученная информация - считывает поток отправленных сообщений } type NetAddr string
Любую транзакцию в сети блокчейн можно назвать сообщением между блоками с некоторым названием и содержанием. Соответственно создадим следующую структуру Message:
type Message struct { Header MessageType // Заголовок сообщения - набор байт для определения типа сообщения Data []byte // Зашифрованные данные, переданные в сообщении - слайс (массив) байтов } type MessageType byte
Всю информацию, хранящуюся в нашей сети, мы хотим оберегать, поэтому атрибут Data является байтовым закодированным представлением внутренней информации. А декодированное сообщение DecodedMessage в свою очередь будет иметь вид:
type DecodedMessage struct { From NetAddr // Отправитель нашего сообщения - по сути адрес-строка Data any // Данные, переданные в сообщении - любая информация любого типа }
Для работы с шифрованием сообщений пользователей я буду использовать встроенную библиотеку «encoding».
func DefaultRPCDecodeFunc (rpc RPC) (*DecodedMessage, error) { msg := Message{} if err := gob.NewDecoder(rpc.Payload).Decode(&msg); err != nil { return nil, fmt.Errorf("failed to decode message from %s: %s", rpc.From, err) } switch msg.Header { case MessageTypeTx: tx := new(core.Transaction) if err := tx.Decode(core.NewGobTxDecoder(bytes.NewReader(msg.Data))); err != nil { return nil, err } return &DecodedMessage{ From: rpc.From, Data: tx, }, nil default: return nil, fmt.Errorf("invalid message header %x", msg.Header) } }
Код выше не делает никаких магических преобразований, а лишь считывает байтовое представление сообщения в сети и на основе полученного заголовка расшифровывает его.
Сеть выполняет транспортную функцию — под этим можно понимать подключение блоков, получение сообщений из внешней сети, а также распространение информации внутри блокчейна. Для транспортного уровня создадим интерфейс Transport в файле transport.go.
type Transport interface { Consume() <-chan RPC // Метод получения сообщения Connect(Transport) error // Метод подключения к сети SendMessage(NetAddr,[]byte) error // Метод отправки зашифрованного сообщения Broadcast([]byte) error // Метод распространения сообщений по сети Addr() NetAddr // Метод получения адреса в сети }
Шаг 1.5: Создание локального транспортного слоя
Для того, чтобы тестирование, отладка и вообще функционирование проекта не требовало подготовки инфраструктуры или владения несколькими пк, я решил создать своего рода эмулятор взаимодействия блоков LocalTransport, который реализует интерфейс Transport. Важно, что для предотвращения конкурентных обращений к одному блоку нам потребуется мьютекс на чтение/запись, а также явно указать канал, по которому сообщения будут переданы.
type LocalTransport struct { addr NetAddr // Адрес локального участника сети - строка consumeCh chan RPC // Канал, по которому участник будет получать сообщения lock sync.RWMutex // Мьютекс на чтение и запись для избежания последствий конкурентного доступа peers map[NetAddr]*LocalTransport // Другие участники сети для обмена информацией - мапа, связывающая адрес участника с транспортным слоем }
func (t *LocalTransport) Consume() <-chan RPC { return t.consumeCh } func (t *LocalTransport) Connect(tr Transport) error { trans := tr.(*LocalTransport) t.lock.Lock() defer t.lock.Unlock() t.peers[tr.Addr()] = trans return nil } func (t *LocalTransport) SendMessage(to NetAddr, payload []byte) error { t.lock.RLock() defer t.lock.RUnlock() peer, ok := t.peers[to] if !ok { return fmt.Errorf("%s: could not send message to %s", t.addr, to) } peer.consumeCh <- RPC{ From: t.addr, Payload: bytes.NewReader(payload), } return nil } func (t *LocalTransport) Broadcast(payload []byte) error { for _, peer := range t.peers { if err := t.SendMessage(peer.Addr(), payload); err != nil { return err } } return nil } func (t *LocalTransport) Addr() NetAddr { return t.addr }
Шаг 2: Создание транзакции
Теперь перейдем к тому, что же будем передавать в нашей сети. Для того, чтобы в нашу сеть не попадали случайные транзакции злоумышленников, обязательной частью будет являться электронная подпись этой операции. Также для описания транзакции мы будем хранить в ней некоторую информацию и адресанта в нашей сети, идентифицируемого по приватному ключу. Чтобы была возможность быстро находить операцию и работать с хранилищем, одним из полей нашей структуры будет являться хеш операции. Также, как мы уже знаем, новая транзакция и транзакция, уже зарегистрированная в реестре, провоцируют разное поведение системы, соответственно нам нужен указатель на самый первый блок, который провалидировал данную операцию. Прежде чем блоки подтвердят корректность операции, они будут помещены в так называемый пул транзакций, где будут ждать своей очереди.
Для этого создадим структуру Transaction в файле transaction.go, а также реализуем методы подписи и проверки транзакции.
type Transaction struct { Data []byte // Зашифрованные данные, переданные в транзакции - слайс (массив) байтов From crypto.PublicKey // Инициатор транзакции в нашей сети - публичный ключ, который идентифицирует данного пользователя Signature *crypto.Signature // Подпись инициатора транзакции - указатель на big int комбинацию параметров пользователя hash types.Hash // Хеш для идентификации и быстрого доступа к транзакции firstSeen int64 // Указатель на первое вхождение транзакции в блокчейн - является номером блока-обработчика } type TxPool struct { transactions map[types.Hash]*core.Transaction // Список инициированных, но не обработанных транзакций - мапа, где в соответствие ставится хеш транзакции и указатель на нее } type Hash [32]uint8 func (tx *Transaction) Sign(privKey crypto.PrivateKey) error { sig, err := privKey.Sign(tx.Data) if err != nil { return err } tx.From = privKey.PublicKey() tx.Signature = sig return nil } func (tx *Transaction) Verify() error { if tx.Signature == nil { return fmt.Errorf("transaction has no signature") } if !tx.Signature.Verify(tx.From, tx.Data) { return fmt.Errorf("invalid transaction signature") } return nil }
Шаг 3: Создание структуры блока
Как уже было сказано, блоки будут хранить в себе транзакции, но также важно хранить информацию о самом блоке в сети — всю эту информацию мы будем хранить в заголовке нашего блока Header в файле block.go.Кроме этого мы все еще хотим сделать нашу приватную сеть максимально безопасной, поэтому мы также подпишем блоки и добавим валидацию этих подписей.
type Header struct { Version uint32 // Версия данного блока - дефолтное значение 1 DataHash types.Hash // Хеш информации, хранящейся в блоке PrevBlockHash types.Hash // Хеш предыдущего блока для поддержания связности Timestamp int64 // Временная метка создания блока - имеет значение Unix Nano Height uint32 // Высота блока - значение указывает на глубину дерева блоков от Genesis Block } type Block struct { *Header Transactions []Transaction // Список транзакций, записанных на блок - слайс (массив) транзакций Validator crypto.PublicKey // Значение публичного ключа, относительно которого будем валидировать подписанный блок Signature *crypto.Signature // Подпись пользователя, предлагающего блок hash types.Hash // Хеш блока для идентификации } func (b *Block) Sign(privKey crypto.PrivateKey) error { sig, err := privKey.Sign(b.Header.Bytes()) if err != nil { return err } b.Validator = privKey.PublicKey() b.Signature = sig return nil } func (b *Block) Verify() error { if b.Signature == nil { return fmt.Errorf("block has no signature") } if !b.Signature.Verify(b.Validator, b.Header.Bytes()) { return fmt.Errorf("block has invalid signature") } for _, tx := range b.Transactions { if err := tx.Verify(); err != nil { return err } } return nil }
Шаг 4: Реализация цепочки блоков
Теперь когда блоки готовы появляться в сети, создадим долгожданный блокчейн в файле blockchain.go.В блокчейне мы будем хранить не целиком блоки, а все те же заголовки, которые по сути являются идентификаторами наших блоков. Как я уже говорил, нам нужно хранилище, нам нужен мьютекс для предотвращения одновременной работы с блокчейном и валидация блоков для безопасности нашей сети. Проверка корректности предложенного блока делится на несколько этапов:
-
Проверка на уникальность блока в сети
-
Проверка на корректное расположение блока в графе
-
Проверка на связность блока с предшественником
-
Проверка подписи
type Blockchain struct { store Storage // Хранилище блоков в сети блокчейн для дальнейшего обращения к ним lock sync.RWMutex // Мьютекс на чтение и запись для избежания последствий конкурентного доступа headers []*Header // Список блоков в сети - слайс (массив) заголовков validator Validator // Валидатор при добавлении блоков } type Storage interface{ Put(*Block) error } func (s *MemoryStore) Put(b *Block) error { return nil } type Validator interface{ ValidateBlock(*Block) error } func (v *BlockValidator) ValidateBlock(b *Block) error{ if v.bc.HasBlock(b.Height){ return fmt.Errorf("chain already contains block (%d) with hash (%s)", b.Height, b.Hash(BlockHasher{})) } if b.Height != v.bc.Height()+1 { return fmt.Errorf("block (%s) too high", b.Hash(BlockHasher{})) } prevHeader, err := v.bc.GetHeader(b.Height - 1) if err != nil { return err } hash := BlockHasher{}.Hash(prevHeader) if hash != b.PrevBlockHash { return fmt.Errorf("the hash of the previous block (%s) is invalid", b.PrevBlockHash) } if err := b.Verify(); err != nil { return err } return nil } func (bc *Blockchain) addBlockWithoutValidation(b *Block) error { bc.lock.Lock() bc.headers = append(bc.headers, b.Header) bc.lock.Unlock() logrus.WithFields(logrus.Fields{ "height": b.Height, "hash": b.Hash(BlockHasher{}), }).Info("adding new block") return bc.store.Put(b) } func (bc *Blockchain) AddBlock(b *Block) error { if err := bc.validator.ValidateBlock(b); err != nil { return err } bc.addBlockWithoutValidation(b) return nil }
Шаг 5: Запуск и тестирование
Когда все основные уровни матрешки блокчейна были нами описаны в коде, пришла пора тестировать, что же получилось. Для этого создадим сервер в файле server.go, который будет использовать LocalTransport и поддерживать саму сеть доступной. Структура Server определяет сам сервер, а ServerOpts — его настройки.
type Server struct { ServerOpts memPool *TxPool // Пул транзакций isValidator bool // Флаг наличия ключа для проверки корректности блоков rpcCh chan RPC // Канал для получения сообщений по RPC quitCh chan struct{} // Канал для поддержания graceful shutdown сервера - при получении сообщения в этом канале сервер отключается } type ServerOpts struct{ ID string // ID сервера в сети Logger log.Logger RPCDecodeFunc RPCDecodeFunc // Алгоритм декодирования сообщений в сети RPCProcessor RPCProcessor // Алгоритм обработки сообщений в сети Transports []Transport // Виды поддерживаемого транспорта BlockTime time.Duration // Время, необходимое для создания блока PrivateKey *crypto.PrivateKey // Приватный ключ для валидации объектов в сети } func NewServer(opts ServerOpts) *Server { if opts.BlockTime == time.Duration(0) { opts.BlockTime = defaultBlockTime } if opts.RPCDecodeFunc == nil { opts.RPCDecodeFunc = DefaultRPCDecodeFunc } if opts.Logger == nil { opts.Logger = log.NewLogfmtLogger(os.Stderr) opts.Logger = log.With(opts.Logger, "ID", opts.ID) } s := &Server{ ServerOpts: opts, memPool: NewTxPool(), isValidator: opts.PrivateKey != nil, rpcCh: make(chan RPC), quitCh: make(chan struct{}, 1), } if s.RPCProcessor == nil { s.RPCProcessor = s } if s.isValidator { go s.validatorLoop() } return s } func (s *Server) Start(){ s.initTransports() free: for { select{ case rpc := <- s.rpcCh: msg, err := s.RPCDecodeFunc(rpc) if err != nil { s.Logger.Log("error", err) } if err := s.RPCProcessor.ProcessMessage(msg); err != nil { s.Logger.Log("error", err) } case <-s.quitCh: break free } } s.Logger.Log("msg", "Server shutdown") } func (s *Server) validatorLoop() { ticker := time.NewTicker(s.BlockTime) s.Logger.Log("msg", "Starting validator loop", "blocktime", s.BlockTime) for { <-ticker.C s.createNewBlock() } } func (s *Server) ProcessMessage(msg *DecodedMessage) error { switch t := msg.Data.(type) { case *core.Transaction: return s.processTransaction(t) } return nil } func (s *Server) broadcast(payload []byte) error { for _, tr := range s.Transports { if err := tr.Broadcast(payload); err != nil { return err } } return nil } func (s *Server) processTransaction (tx *core.Transaction) error { hash := tx.Hash(core.TxHasher{}) if s.memPool.Has(hash) { return nil } if err := tx.Verify(); err != nil { return err } tx.SetFirstSeen(time.Now().UnixNano()) logrus.WithFields(logrus.Fields{ "hash": hash, "mempool length": s.memPool.Len(), }).Info("adding new tx to the mempool") s.Logger.Log("msg", "adding new tx to mempool", "hash", hash, "mempoolLength", s.memPool.Len()) go s.broadcastTx(tx) return s.memPool.Add(tx) } func (s *Server) broadcastTx(tx *core.Transaction) error { buf := &bytes.Buffer{} if err := tx.Encode(core.NewGobTxEncoder(buf)); err != nil { return err } msg := NewMessage(MessageTypeTx, buf.Bytes()) return s.broadcast(msg.Bytes()) } func (s *Server) createNewBlock() error { fmt.Println("creating a new block") return nil } func (s *Server) initTransports(){ for _, tr := range s.Transports{ go func(tr Transport) { for rpc := range tr.Consume(){ s.rpcCh <- rpc } }(tr) } }
Заключение
Итак, после большого количества слов и кода, пора подводить итоги, что же мы сделали. Во-первых, постарались разобраться в базовых элементах блокчейн сети, узнали, чем блокчейны различаются и на каких общих китах стоят. Также сделали простую систему, которая содержит все элементы децентрализованной системы, эмулирует поведение работы блоков и будет в дальнейшем расширяться. Во второй части я покажу, как на основе такой простой системы создать платформу для взаимодействия нескольких пользователей, площадку для реализации smart контрактов и как все это обезопасить от внешних угроз. Буду рад услышать обратную связь, почитать комменты и понять, что ж я все-таки неправильно понял.
Всем большое спасибо и до скорого!
ссылка на оригинал статьи https://habr.com/ru/articles/874758/
Добавить комментарий