[Перевод] Блокчейн на Python

Когда я читал статью про блокчейн на JavaScript, мне было интересно познакомиться с идеями о блокчейн-разработке, которые отличаются от тех, что мне уже известны. А как только я начал читать код, мне захотелось сопоставить его с аналогичным Python-кодом, чтобы ещё и разобраться с его отличиями от кода, написанного на JavaScript.

Цель этого материала заключается в том, чтобы выявить отличия языков. Его можно считать Python-дополнением к исходной статье.

u112prjefaec_cvnyuk_sb6oxqy.jpeg

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


Надеюсь, вам понравились оба материала, и вы нашли в них что-то полезное.

Если бы вам понадобилось создать блокчейн-систему — какими инструментами вы воспользовались бы?

image-loader.svg

© Habrahabr.ru