Самый быстрый способ узнать, как работает блокчейн — это создать его.
Вы здесь, потому что, как и я, немного помешаны на криптовалюте. Кроме этого, вы явно хотите узнать, как работает блокчейн — фундаментальная технология, связанная с криптовалютой.
Однако, понимание блокчейн — не самое простое дело, по крайней мере, так было в моем случае. Я прошел через тонны видеороликов, изучал руководства и постоянно разочаровывался из-за слишком маленького количества примеров.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Я люблю учиться на практике. Это лучше всего показывает суть дела на уровне кода, что запоминается лучше всего. Если у вас похожее мнение, то в конце статьи вы получите рабочий блокчейн с твердым пониманием того, как они работают.
Перед тем, как начать…
Помните, что блокчейн — это неизменная, последовательная цепочка записей, каждая часть этой цепочки называется блоками. Они могут содержать транзакции, файлы или любой вид данных, который вам угоден. Однако важный момент заключается в том, что они связаны вместе хешами.
Если вы не знаете, что такое хеш, то вот вам статьи:
На кого нацелена данная статья?
В целом, вы уже должны более-менее свободно читать и писать основы Python, наряду с пониманием работы запросов HTTP, так как мы будем говорить о блокчейне на HTTP.
Что нам нужно? Убедитесь, что у вас установлен Python 3.6+ (а также pip). Вам также нужно будет установить Flask и замечательную библиотеку Requests:
1 |
pip install Flask==0.12.2 requests==2.18.4 |
И да, вам также нужен будет HTTP клиент, такой как Postman или cURL. В целом, что-нибудь подойдет.
Исходный код статьи
Вот здесь вы можете ознакомиться с исходным: https://github.com/dvf/blockchain
Шаг 1: Создание блокчейна
Открывайте свой любимый редактор текста, или IDE, лично я предпочитаю PyCharm. Создайте новый файл под названием blockchain.py. Мы используем только один файл, но если вы запутаетесь, вы всегда можете пройтись по исходному коду.
Скелет блокчейна
Мы создадим класс блокчейна, чей конструктор создает начальный пустой лист (для хранения нашего блокчейна), и еще один — для хранения транзакций. Вот чертёж нашего класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Blockchain(object): def __init__(self): self.chain = [] self.current_transactions = [] def new_block(self): # Создает новый блок и вносит его в цепь pass def new_transaction(self): # Вносит новую транзакцию в список транзакций pass @staticmethod def hash(block): # Хеширует блок pass @property def last_block(self): # Возвращает последний блок в цепочке pass |
Наш класс Blockchain отвечает за управление цепью. Он будет хранить транзакции, а также иметь несколько вспомогательных методов для внесения новых блоков в цепь. Начнем с работы с несколькими методами.
Как выглядит блок?
Каждый блок содержит индекс, временной штамп (время unix), список транзакций, доказательство (об этом позже) и хеш предыдущего блока.
Вот пример того, как выглядит один блок:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
block = { 'index': 1, 'timestamp': 1506057125.900785, 'transactions': [ { 'sender': "8527147fe1f5426f9dd545de4b27ee00", 'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f", 'amount': 5, } ], 'proof': 324984774000, 'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" } |
С этого момента, понимание цепи должно быть раздельным — каждый новый блок содержит внутри себя хеш предыдущего блока. Это принципиально важно, так как этим обеспечивается неизменность блокчейна: если злоумышленник взломает предыдущий блок, то все остальные блоки будут содержать неправильные хеши.
Имеет ли это смысл? Если нет — то вам нужно уделить время, чтобы понять и осознать это, так как мы говорим о фундаментальном принципе работы блокчейна.
Внесение транзакций в блок
Нам нужен будет способом внесения транзакций в блок. Наш метод new_transaction() отвечает за это, и он достаточно прямолинейный:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Blockchain(object): ... def new_transaction(self, sender, recipient, amount): """ Направляет новую транзакцию в следующий блок :param sender: <str> Адрес отправителя :param recipient: <str> Адрес получателя :param amount: <int> Сумма :return: <int> Индекс блока, который будет хранить эту транзакцию """ self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 |
После того, как new_transaction() внесет транзакцию в список, он вернет индекс блока, в которой должна будет быть внесена транзакция — а именно следующая. В будущем, это будет полезно для пользователя, отправляющего транзакцию.
Создание новых блоков
После того, как мы получили экземпляр блокчейна, нам нужно посадить в него блок генезиса — первый блок без предшественников. Нам также нужно внести “пруф” в наш блок генезиса, который представляет собой результат майнинга (доказательства проведенной работы). Мы рассмотрим майнинг позже.
В дополнению к созданию блока генезиса в конструкторе, мы также выкатим методы для new_block(), new_transaction() и hash():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
import hashlib import json from time import time class Blockchain(object): def __init__(self): self.current_transactions = [] self.chain = [] # Создание блока генезиса self.new_block(previous_hash=1, proof=100) def new_block(self, proof, previous_hash=None): """ Создание нового блока в блокчейне :param proof: <int> Доказательства проведенной работы :param previous_hash: (Опционально) хеш предыдущего блока :return: <dict> Новый блок """ block = { 'index': len(self.chain) + 1, 'timestamp': time(), 'transactions': self.current_transactions, 'proof': proof, 'previous_hash': previous_hash or self.hash(self.chain[-1]), } # Перезагрузка текущего списка транзакций self.current_transactions = [] self.chain.append(block) return block def new_transaction(self, sender, recipient, amount): """ Направляет новую транзакцию в следующий блок :param sender: <str> Адрес отправителя :param recipient: <str> Адрес получателя :param amount: <int> Сумма :return: <int> Индекс блока, который будет хранить эту транзакцию """ self.current_transactions.append({ 'sender': sender, 'recipient': recipient, 'amount': amount, }) return self.last_block['index'] + 1 @property def last_block(self): return self.chain[-1] @staticmethod def hash(block): """ Создает хэш SHA-256 блока :param block: <dict> Блок :return: <str> """ # Мы должны убедиться в том, что словарь упорядочен, иначе у нас будут непоследовательные хеши block_string = json.dumps(block, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest() |
Код выше должен быть достаточно ясным — я внес несколько комментариев и документацию, чтобы все было понятно. Структура данных будет в json. Мы почти закончили с скелетом нашего блокчейна. Однако на данный момент, вам наверное интересно, как создаются новые блоки?
Понимание подтверждения работы
Алгоритм пруфа работы (Proof of Work, PoW) — это то, как новые блоки созданы или майнятся в блокчейне. Цель PoW — это найти число, которое решает проблему. Число должно быть таким, чтобы его тяжело было найти, но легко подтвердить (говоря о вычислениях) кем угодно в интернете. Это главная задача алгоритма.
Рассмотрим простой пример, чтобы получить лучшее представление.
Скажем, что хеш того или иного числа х, умноженного на другое число должен заканчиваться нулем. Таким образом, hash(x * y) = ac23dc…0. Для этого упрощенного примера, представим что x = 5. Как это работает в Python:
1 2 3 4 5 6 7 8 |
from hashlib import sha256 x = 5 y = 0 # Мы еще не знаем, чему равен y... while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0": y += 1 print(f'The solution is y = {y}') |
Решение здесь следующее: y = 21, так как созданный хеш заканчивается нулем:
1 |
hash(5 * 21) = 1253e9373e...5e3600155e860 |
В биткоине, такой алгоритм называется Hashcash. И он особо не отличается от приведенного выше примера. Это алгоритм, который поколение майнеров (читай, отдельная раса) пытается решить, чтобы создать новый блок. В целом, сложность определяется количеством символом, которые рассматриваются в строке. Майнеры неплохо вознаграждаются за решение задачи получением коина в транзакции.
Реализация базового PoW
Давайте реализуем аналогичный алгоритм для нашего блокчейна. Наше правило будет аналогично указанному ранее:
Найдите число «p«, которое хешировано с предыдущим созданным решением блока с хешем содержащим 4 заглавных нуля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import hashlib import json from time import time from uuid import uuid4 class Blockchain(object): ... def proof_of_work(self, last_proof): """ Простая проверка алгоритма: - Поиска числа p`, так как hash(pp`) содержит 4 заглавных нуля, где p - предыдущий - p является предыдущим доказательством, а p` - новым :param last_proof: <int> :return: <int> """ proof = 0 while self.valid_proof(last_proof, proof) is False: proof += 1 return proof @staticmethod def valid_proof(last_proof, proof): """ Подтверждение доказательства: Содержит ли hash(last_proof, proof) 4 заглавных нуля? :param last_proof: <int> Предыдущее доказательство :param proof: <int> Текущее доказательство :return: <bool> True, если правильно, False, если нет. """ guess = f'{last_proof}{proof}'.encode() guess_hash = hashlib.sha256(guess).hexdigest() return guess_hash[:4] == "0000" |
Чтобы скорректировать сложность алгоритма, мы можем изменить количество заглавных нулей. В нашем случае, 4 — достаточно. Вы узнаете, что внесение одного ведущего нуля создает колоссальную разницу во времени, необходимом для поиска решения (майнинга).
Наш класс практически готов, так что мы можем начать взаимодействовать с ним через HTTP запросы.
Шаг 2: Блокчейн как API
Здесь мы задействуем фреймворк под названием Flask. Это макро-фреймворк, который заметно упрощает сопоставление конечных точек с функциями Python. Это позволяет нам взаимодействовать с нашим блокчейном в интернете при помощиHTTP-запросов.
Мы создадим три метода:
- /transactions/new для создания новой транзакции в блоке;
- /mine, чтобы указать серверу, что нужно майнить новый блок;
- /chain для возвращения всего блокчейна
Настройка Flask
Наш “сервер” сформирует единый узел в нашей сети блокчейна. Давайте создадим шаблонный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import hashlib import json from textwrap import dedent from time import time from uuid import uuid4 from flask import Flask class Blockchain(object): ... # Создаем экземпляр узла app = Flask(__name__) # Генерируем уникальный на глобальном уровне адрес для этого узла node_identifier = str(uuid4()).replace('-', '') # Создаем экземпляр блокчейна blockchain = Blockchain() @app.route('/mine', methods=['GET']) def mine(): return "We'll mine a new Block" @app.route('/transactions/new', methods=['POST']) def new_transaction(): return "We'll add a new transaction" @app.route('/chain', methods=['GET']) def full_chain(): response = { 'chain': blockchain.chain, 'length': len(blockchain.chain), } return jsonify(response), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) |
Краткое объяснение того, что мы только что добавили:
- Строка 15: Создание экземпляра узла. Можете больше узнать о Flask здесь;
- Строка 18: Создание случайного имени нашего узла;
- Строка 21: Создание экземпляра класса Blockchain;
- Строки 24-26: Создание конечной точки /mine, которая является GET-запросом;
- Строки 28-30: Создание конечной точки /transactions/new, которая являетсяPOST-запросом, так как мы будем отправлять туда данные;
- Строки 32-38: Создание конечной точки /chain, которая возвращает весь блокчейн;
- Строки 40-41: Запускает сервер на порт: 5000.
Конечная точка транзакций
Вот так запрос транзакции должен будет выглядеть. Это то, что пользователь отправляет в сервер:
1 2 3 4 5 |
{ "sender": "my address", "recipient": "someone else's address", "amount": 5 } |
Так как мы уже обладаем методом класса для добавления транзакций в блок, дело осталось за малым. Давайте напишем функцию для внесения транзакций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import hashlib import json from textwrap import dedent from time import time from uuid import uuid4 from flask import Flask, jsonify, request ... @app.route('/transactions/new', methods=['POST']) def new_transaction(): values = request.get_json() # Убедитесь в том, что необходимые поля находятся среди POST-данных required = ['sender', 'recipient', 'amount'] if not all(k in values for k in required): return 'Missing values', 400 # Создание новой транзакции index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount']) response = {'message': f'Transaction will be added to Block {index}'} return jsonify(response), 201 |
Конечная точка майнинга
Конечная точка майнинга — это часть, где происходит магия, и это просто! Для этого нужно сделать три вещи:
- Подсчитать PoW;
- Наградить майнера (нас), добавив транзакцию, дающую нам 1 коин;
- Слепить следующий блок, внеся его в цепь.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import hashlib import json from time import time from uuid import uuid4 from flask import Flask, jsonify, request ... @app.route('/mine', methods=['GET']) def mine(): # Мы запускаем алгоритм подтверждения работы, чтобы получить следующее подтверждение… last_block = blockchain.last_block last_proof = last_block['proof'] proof = blockchain.proof_of_work(last_proof) # Мы должны получить вознаграждение за найденное подтверждение # Отправитель “0” означает, что узел заработал крипто-монету blockchain.new_transaction( sender="0", recipient=node_identifier, amount=1, ) # Создаем новый блок, путем внесения его в цепь previous_hash = blockchain.hash(last_block) block = blockchain.new_block(proof, previous_hash) response = { 'message': "New Block Forged", 'index': block['index'], 'transactions': block['transactions'], 'proof': block['proof'], 'previous_hash': block['previous_hash'], } return jsonify(response), 200 |
Обратите внимание на то, что получатель замайненого блока — это адрес нашего узла. Большая часть того, что мы здесь сделали, это просто взаимодействие с методами в нашем классе Blockchain. С этого момента, мы закончили, и можем начать взаимодействовать с нашим blockchain на Python.
Шаг 3: Взаимодействие с нашим блокчейном
Вы можете использовать старый добрый cURL или Postman для взаимодействия с нашим API в сети.
Запускаем сервер:
1 2 |
$ python blockchain.py * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) |
Давайте попробуем майнить блок, создав GET-запрос к узлу http://localhost:5000/mine:
1 |
curl http://localhost:5000/mine |
Теперь, давайте создадим новую транзакцию, отправив POST-запрос к узлу http://localhost:5000/transactions/new с телом, содержащим структуру нашей транзакции:
Если вы не пользуетесь Postman, тогда вы можете создать аналогичный запрос при помощи cURL:
1 2 3 4 5 |
$ curl -X POST -H "Content-Type: application/json" -d '{ "sender": "d4ee26eee15148ee92c6cd394edd974e", "recipient": "someone-other-address", "amount": 5 }' "http://localhost:5000/transactions/new" |
Я перезапустил свой сервер и замайнил два блока, итого их количество 3. Давайте проверим всю цепочку, выполнив запрос к узлу http://localhost:5000/chain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
{ "chain": [ { "index": 1, "previous_hash": 1, "proof": 100, "timestamp": 1506280650.770839, "transactions": [] }, { "index": 2, "previous_hash": "c099bc...bfb7", "proof": 35293, "timestamp": 1506280664.717925, "transactions": [ { "amount": 1, "recipient": "8bbcb347e0634905b0cac7955bae152b", "sender": "0" } ] }, { "index": 3, "previous_hash": "eff91a...10f2", "proof": 35089, "timestamp": 1506280666.1086972, "transactions": [ { "amount": 1, "recipient": "8bbcb347e0634905b0cac7955bae152b", "sender": "0" } ] } ], "length": 3 } |
Шаг 4: Консенсус
Пока всё идет очень здорово. У нас есть базовый blockchain, который принимает транзакции и дает возможность майнить новые блоки. Но вся суть блокчейна в том, что они должны быть децентрализованными. А если они децентрализованы, каким образом мы можем гарантировать, все они отображают одну цепочку?
Это называется проблемой Консенсуса, так что нам нужно реализовать алгоритм Консенсуса, если нам нужно больше одного узла в нашей цепи.
Регистрация новых узлов
Чтобы мы смогли реализовать алгоритм Консенсуса, нам нужно найти способом дать узлу знать о существовании соседних узлов в цепи. Каждый узел в нашей цепи должен содержать регистр других узлов в цепи. Следовательно, нам понадобиться больше конечных точек:
- /nodes/register для принятия список новых узлов в форме URL-ов;
- /nodes/resolve для реализации нашего алгоритма Консенсуса, который решает любые конфликты, связанные с подтверждением того, что узел находиться в своей цепи.
Нам нужно будет изменить конструктор нашего Blockchain и привнести метод для регистрации узлов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
... from urllib.parse import urlparse ... class Blockchain(object): def __init__(self): ... self.nodes = set() ... def register_node(self, address): """ Вносим новый узел в список узлов :param address: <str> адрес узла , другими словами: 'http://192.168.0.5:5000' :return: None """ parsed_url = urlparse(address) self.nodes.add(parsed_url.netloc) |
Обратите внимание на то, что мы использовали set() для хранения списка узлов. Это легкий способ убедиться в том, что внесение новых узлов является идемпотентным — это означает, что вне зависимости от того, сколько раз мы внесем определенный узел, он возникнет только один раз.
Реализация алгоритма Консенсуса
Как мы уже знаем, конфликт заключается в том, что один узел имеет другую цепь, связанную с другим узлом. Чтобы решить это, мы введем правило, где самая длинная и валидная цена является авторитетной. Другими словами, длиннейшая цепь сети де-факто является единственной. Используясь этот алгоритм, мы достигнем Консенсуса среди узлов в нашей сети.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
... import requests class Blockchain(object) ... def valid_chain(self, chain): """ Проверяем, является ли внесенный в блок хеш корректным :param chain: <list> blockchain :return: <bool> True если она действительна, False, если нет """ last_block = chain[0] current_index = 1 while current_index < len(chain): block = chain[current_index] print(f'{last_block}') print(f'{block}') print("\n-----------\n") # Проверьте правильность хеша блока if block['previous_hash'] != self.hash(last_block): return False # Проверяем, является ли подтверждение работы корректным if not self.valid_proof(last_block['proof'], block['proof']): return False last_block = block current_index += 1 return True def resolve_conflicts(self): """ Это наш алгоритм Консенсуса, он разрешает конфликты, заменяя нашу цепь на самую длинную в цепи :return: <bool> True, если бы наша цепь была заменена, False, если нет. """ neighbours = self.nodes new_chain = None # Ищем только цепи, длиннее нашей max_length = len(self.chain) # Захватываем и проверяем все цепи из всех узлов сети for node in neighbours: response = requests.get(f'http://{node}/chain') if response.status_code == 200: length = response.json()['length'] chain = response.json()['chain'] # Проверяем, является ли длина самой длинной, а цепь - валидной if length > max_length and self.valid_chain(chain): max_length = length new_chain = chain # Заменяем нашу цепь, если найдем другую валидную и более длинную if new_chain: self.chain = new_chain return True return False |
Первый метод valid_chain() отвечает за проверку того, является ли цепь валидной, запустив цикл через каждый блок и проводя верификацию как хеша, так и пруфа.
Метод resolve_conflicts(), который запускает цикл через все наши соседние узлы, загружает их цепи и проводит проверку, как и в предыдущем методе. Если валидная цепь, длина которой больше, чем наша, мы заменяем нашу.
Давайте зарегистрируем две конечные точки нашего API, одну для внесения соседних узлов, а вторую — для решения конфликтов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@app.route('/nodes/register', methods=['POST']) def register_nodes(): values = request.get_json() nodes = values.get('nodes') if nodes is None: return "Error: Please supply a valid list of nodes", 400 for node in nodes: blockchain.register_node(node) response = { 'message': 'New nodes have been added', 'total_nodes': list(blockchain.nodes), } return jsonify(response), 201 @app.route('/nodes/resolve', methods=['GET']) def consensus(): replaced = blockchain.resolve_conflicts() if replaced: response = { 'message': 'Our chain was replaced', 'new_chain': blockchain.chain } else: response = { 'message': 'Our chain is authoritative', 'chain': blockchain.chain } return jsonify(response), 200 |
Теперь вы можете сесть за другой компьютер (если хотите), и развернуть разные узлы в своей сети. Также вы можете развернуть процессы при помощи различных портов на одном и том же компьютере. Я развернул еще один узел на своем компьютере, но на другом порте и зарегистрировал его при помощи моего текущего узла. Таким образом, я получил два узла: http://localhost:5000 и http://localhost:5001.
После этого я получил два новых блока в узле 2, чтобы убедиться в том, что цепь была длиннее. После этого, я вызвал GET /nodes/resolve в узле 1, где цепь была заменена нашим алгоритмом консенсуса:
И это была обертка… Найдите друзей и вместе попробуйте протестировать ваш блокчейн!
Подведем итоги
Надеюсь, эта статья вдохновила вас на что-нибудь новое. Я в восторге от криптовалют, так как я верю, что блокчейн радикально изменят наше представление об экономике, правительстве и учетных записях!
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»