Цель этого материала заключается в том, чтобы выявить отличия языков. Его можно считать Python-дополнением к исходной статье.
Несмотря на то, что исходная статья появилась на свет после того, как её автор ознакомился с примером блокчейн-разработки на Python, мне хотелось написать Python-код, который как можно более точно воспроизводит JavaScript-код из статьи. Это позволит сопоставить реализацию блокчейна на разных языках.
Я, кроме того, собираюсь сделать так, чтобы моя реализация блокчейна, как и в статье про JavaScript, тоже поместилась бы в 60 строк.
О блокчейне
Хотя я собирался повторить структуру того материала, чтобы тем же путём, что и его автор, прийти к готовому коду, я, всё же, включу сюда и кое-что своё. В частности, я предпочитаю другое определение блокчейна. Оно звучит так: «Блокчейн — это система регистрации информации, выполняемой таким способом, который усложняет или делает невозможным изменение информации, взлом системы или мошенничество с информацией».
Подготовка среды разработки
В этом проекте мы будем использовать Python, поэтому, если он у вас не установлен — найдите дистрибутив, подходящий для вашей ОС, и установите его.
Создание блока
Блок — это объект, в котором имеется некая информация. Поэтому начнём работу с создания класса Block
:
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
# В this.data должна храниться информация, вроде сведений о транзакциях.
self.data = [] if data is None else data
Определения класса Block
в Python и JavaScript получились очень похожими. В Python вместо this
используется self
, а аналогом метода constructor
является init
.
Комментарии тоже выполняются похожим образом. В Python для оформления однострочных комментариев применяется символ #
, а в JavaScript — конструкция //
.
Реализацию алгоритма sha256
я взял из библиотеки hashlib. В JS-проекте она берётся из пакета crypto
.
from hashlib import sha256
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.hash = self.getHash()
self.prevHash = None # Хеш предыдущего блока
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
return hash.hexdigest()
В методе getHash
всё начинается с пустого хеша, который мы формируем с использованием данных, хранящихся в блоке. Хеш вычисляется на основе хеша предыдущего блока, отметки времени и данных, хранящихся в блоке. Всё это преобразуется в последовательности байтов с помощью метода .encode('utf-8')
.
Блокчейн
Займёмся классом Blockchain
.
class Blockchain:
def __init__(self):
# В этом свойстве будут содержаться все блоки.
self.chain = []
Классы, представляющие собой блокчейн, похожи в обоих языках.
Для того чтобы создать первичный блок, мы просто вызываем Block
с текущей отметкой времени, для получения которой используем time()
. Для этого нам нужно импортировать библиотеку time
.
Преобразование числа в строку выполняется в Python с помощью функции str()
, а не с помощью метода toString()
, как делается в JavaScript.
from time import time
class Blockchain:
def __init__(self):
# Создаём первичный блок
self.chain = [Block(str(int(time())))]
Похоже выглядит и метод для получения самого свежего блока. Только тут, в отличие от JavaScript-проекта, для выяснения длины цепочки блоков, вместо свойства length
используется функция len()
.
def getLastBlock(self):
return self.chain[len(self.chain) - 1]
Для того чтобы добавить блок в блокчейн, мы просто вызываем метод addBlock
. Код получился почти таким же, как в JS-проекте, только тут, вместо метода push()
, используется метод append()
.
def addBlock(self, block):
# Так как мы добавляем новый блок, prevHash будет хешем предыдущего последнего блока.
block.prevHash = self.getLastBlock().hash
# Так как теперь в prevHash имеется значение, мы должны пересчитать хеш блока.
block.hash = block.getHash()
self.chain.append(block)
Проверка блокчейна
В методе, используемом для проверки блокчейна, мы пользуемся функцией range()
. В этом — серьёзное отличие нашего кода от кода JS-проекта. И, кроме того, так как мы в Python не пользуемся константами, тут мы просто применяем обычные переменные.
При проверке условий в Python, вместо ||
, используется or
.
def isValid(self):
# Перед перебором цепочки блоков нужно установить i в 1, так как до первичного блока никаких блоков нет. В результате мы начинаем со второго блока.
for i in range(1, len(self.chain)):
currentBlock = self.chain[i]
prevBlock = self.chain[i - 1]
# Проверка
if (currentBlock.hash != currentBlock.getHash() or prevBlock hash != currentBlock.prevHash):
return False
return True
Алгоритм доказательства выполнения работы
Реализовать алгоритм доказательства выполнения работы мы можем, начав с добавления в класс Block
метода mine()
и свойства nonce
. Тут стоит проявить внимательность, так как свойство nonce
должно быть объявлено до вызова метода self.getHash()
. В противном случае будет выдана ошибка AttributeError: 'Block' object has no attribute 'nonce'
.
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.prevHash = None # хеш предыдущего блока
self.nonce = 0
self.hash = self.getHash()
# Наша хеш-функция.
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
hash.update(str(self.nonce).encode('utf-8'))
return hash.hexdigest()
def mine(self, difficulty):
# Тут запускается цикл, работающий до тех пор, пока хеш не будет начинаться со строки
# 0...000 длины <difficulty>.
while self.hash[:difficulty] != '0' * difficulty:
# Инкрементируем nonce, что позволяет получить совершенно новый хеш.
self.nonce += 1
# Пересчитываем хеш блока с учётом нового значения nonce.
self.hash = self.getHash()
Создадим свойство, в котором будет храниться сложность:
self.difficulty = 1
Отредактируем метод addBlock
:
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
Тестирование блокчейна
Импортируем модуль и воспользуемся классом Blockchain
так же, как таким же классом в JS-проекте:
from blockchain import Block
from blockchain import Blockchain
from time import time
JeChain = Blockchain()
# Добавим новый блок
JeChain.addBlock(Block(str(int(time())), ({"from": "John", "to": "Bob", "amount": 100})))
# (Это - всего лишь интересный эксперимент, для создания настоящей криптовалюты обычно нужно сделать намного больше, чем сделали мы).
# Вывод обновлённого блокчейна
print(JeChain)
Результаты работы этого кода должны выглядеть примерно так:
[
{
"data": [],
"timestamp": "1636153236",
"nonce": 0,
"hash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678",
"prevHash": null
},
{
"data": {
"from": "John",
"to": "Bob",
"amount": 100
},
"timestamp": "1636153236",
"nonce": 14,
"hash": "038f82c6e6605acfcad4ade04e454eaa1cfa3d17f8c2980f1ee474eefb9613e9",
"prevHash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678"
}
]
Но заработает это всё только после добавления в класс Blockchain
метода __repr__()
:
import json
def __repr__(self):
return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Дополнение: сложность и время блока
Для настройки времени блока нам понадобится соответствующее свойство:
self.blockTime = 30000
Посмотрим на тернарный оператор, используемый в системе настройки сложности. Если переписать JS-конструкцию с тернарным оператором на Python, то получится следующее: (if_test_is_false, if_test_is_true)[test]
:
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]
Итоговый Python-код (без нормального форматирования) укладывается в обещанные 60 строк:
# -*- coding: utf-8 -*-
from hashlib import sha256
import json
from time import time
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.prevHash = None
self.nonce = 0
self.hash = self.getHash()
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
hash.update(str(self.nonce).encode('utf-8'))
return hash.hexdigest()
def mine(self, difficulty):
while self.hash[:difficulty] != '0' * difficulty:
self.nonce += 1
self.hash = self.getHash()
class Blockchain:
def __init__(self):
self.chain = [Block(str(int(time())))]
self.difficulty = 1
self.blockTime = 30000
def getLastBlock(self):
return self.chain[len(self.chain) - 1]
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]
def isValid(self):
for i in range(1, len(self.chain)):
currentBlock = self.chain[i]
prevBlock = self.chain[i - 1]
if (currentBlock.hash != currentBlock.getHash() or prevBlock.hash != currentBlock.prevHash):
return False
return True
def __repr__(self):
return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Надеюсь, вам понравились оба материала, и вы нашли в них что-то полезное.
Если бы вам понадобилось создать блокчейн-систему — какими инструментами вы воспользовались бы?
Автор:
ru_vds