Собственная криптовалюта: реализация блокчейна на Python

bitcoin

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

Какую структуру имеет блокчейн? Каждый блок имеет хеш-сумму, также в нем содержатся данные о транзакции и метка времени. Кроме того, в блоке есть копия хеш-суммы предыдущего блока. Выглядит все это примерно так:

Схематическое изображение блоков Биткоина

Хм, на что же это похоже? На какую структуру данных? Точно! Это же связный список!

Итак, давайте возьмем класс связного списка, который мы создали ранее (см. статью «Связный список на Python: что это такое и как его реализовать», — прим. ред. Pythonist), и превратим его в простой блокчейн. Мы внесем в него несколько простых изменений, и вы увидите, что объектно-ориентированное программирование на Python позволяет создавать крутые вещи.

Внедряем ООП

Для начала нам нужно внедрить в наш класс связного списка некоторые принципы ООП (объектно-ориентированного программирования). Да, поначалу это кажется сложным. Но если мы хотим, чтобы наш блокчейн был защищенным, нам нужно сделать кое-какие вещи приватными. Этот принцип называется инкапсуляцией. Суть его в том, что некоторые поля и методы делаются доступными только изнутри класса. Поскольку мы не хотим, чтобы кто-то мог нарушить структуру нашей криптовалюты, нам нужно ограничить возможность взаимодействия с ней.

Давайте сначала перечитаем класс Node, который мы создали ранее. Изменим его имя на Block, поскольку теперь он будет представлять один блок блокчейна. Также добавим пару импортов, которые будут использоваться в классе.

import datetime
import math
import pprint


class Block:
    def __init__(self, data):
        self.next = None
        self.data = data

    def append(self, val):
        end = Block(val)
        n = self
        while (n.next):
            n = n.next
        n.next = end

Стоит отметить, что блокчейн обладает парой отличительных свойств. Во-первых, он отличается иммутабельностью, т. е. записи в нем трудно изменить (если вообще возможно). Во-вторых, он публичный. Да, именно так. Блокчейн Bitcoin-а это публичные записи, вы можете просмотреть их хоть сейчас.

В настоящий момент данные нашего узла хранятся просто как data. При помощи дот-нотации кто угодно может получить к ним доступ и изменить их: my_block.data = "One Million Dollarssss". Нас это не устраивает. Мы можем сделать это поле приватным, добавив к имени data двойной символ подчеркивания (__data), а затем создав специальный метод, позволяющий пользователям читать данные (т. е. публичный доступ сохранится).

def __init__(self, data):
    self.next = None
    self.__data = data

Теперь давайте напишем метод, позволяющий пользователям читать значения. В ООП мы называем это get-методом, с его помощью мы просто возвращаем значение желаемого поля.

def get_data(self):
    return self.__data

Прокачиваем наш связный список

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

def __init__(self, prev_hash, transactor, amount):
    self.next = None
    self.__data = {
        "prev_hash": prev_hash,
        "time": datetime.datetime.now().time(),
        "transactor": transactor,
        "amount": amount,
        "hash": ???
    }

Как сделать хеш блока? В блокчейне это делается так: вы берете хеш предыдущего блока и видоизменяете его, используя суперсекретный алгоритм хеширования. Вы можете сделать свой алгоритм сколь угодно сложным, но он может быть и очень простым:

def make_hash(self):
    return self.__data["prev_hash"] + 1

Теперь у нас есть автоматически инкрементирующаяся SQL-база данных. Прекрасно. Давайте сделаем нечто более сложное. Возьмем сумму транзакции, возведем ее в странную степень, прибавим порядковый номер последней буквы имени транзактора, и прибавим все это к предыдущему хешу.

def make_hash(self):
    return self.__data["prev_hash"] + int(int(self.__data["amount"])**1.5) + ord(self.__data["transactor"][-1])

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

def __init__(self, prev_hash, transactor, amount):
    self.next = None
    self.__data = {
        "prev_hash": prev_hash,
        "time": datetime.datetime.now().time(),
        "transactor": transactor,
        "amount": amount,
        "hash": ""
    }
    self.__data["hash"] = self.make_hash()

Наконец, нужно обновить метод append(). Вместо data мы в качестве параметров будем передавать имя транзактора и сумму. Затем, прежде чем создать новый блок, мы получим последний хеш и передадим его в Block месте с другими данными. После этого добавим блок к концу цепочки.

def append(self, transactor, amount):
    n = self
    while (n.next):
        n = n.next
    prev_hash = n.get_data()["hash"]
    end = Block(prev_hash, transactor, amount)
    n.next = end

Делаем цепочку неизменяемой

Как вы помните, блокчейны отличаются неизменяемостью (иммутабельностью). То есть изменить их очень сложно. Это свойство завязано на том, что каждый хеш зависит от сохраняемых в блоке данных (по крайней мере в нашей функции). При изменении значения в блоке должны обновиться все хеши во всех последующих блоках. Если блоков всего несколько, это не займет много времени, но представьте, что у нас очень длинная цепочка, а ее обновление имеет временную сложность O(N).

Для обновления суммы транзакции мы создадим специальный метод. Блокчейн Bitcoin-а вполне может быть реализован как-то иначе, но у нас ведь просто его подобие. В ООП это называется set-методом. Он принимает новое значение и устанавливает скрытое значение data, исключая прямое вмешательство пользователя. Не забудьте вызвать функцию make_hash, чтобы получить обновленный хеш для блока.

def set_amount(self, amount):
    self.__data["amount"] = amount
    self.__data["hash"] = self.make_hash()

Но погодите, это еще не все. Нам нужно обновить хеши всех последующих блоков. Как это сделать? Конечно же путем обхода связного списка! (См. статью «Удаление дубликатов из связного списка в Python», — прим. ред. Pythonist). Как вы помните, мы начинаем со временного указателя и используем цикл while.

temp = self
while(temp.next):

Для каждого блока нам нужно получить его обновленный хеш и сохранить его как prev_hash. Затем, перейдя к следующему блоку, мы обновляем его хеши, используя хеш предыдущего блока. Мы будем делать это при помощи скрытого метода __update_hashes().

def set_amount(self, amount):
    self.__data["amount"] = amount
    self.__data["hash"] = self.make_hash()
    temp = self
    while (temp.next):
        prev_hash = temp.__data["hash"]
        temp = temp.next
        temp.__update_hashes(prev_hash)

В нашем методе __update_hashes() мы принимаем в качестве параметра предыдущий хеш, сохраняем его в __data, а затем используем функцию make_hash() для создания хеша текущего блока.

def __update_hashes(self, new_prev):
    self.__data["prev_hash"] = new_prev
    self.__data["hash"] = self.make_hash()

Вот и все! Мы создали базовый функционал нашего блокчейна.

Проверяем

Давайте построим блокчейн при помощи нашего метода Block. Следующий код вы можете просто скопировать (он позволит вывести нашу цепочку блоков).

def print_chain(chain):
    pp = pprint.PrettyPrinter(indent=4)
    node = chain
    pp.pprint(node.get_data())
    while node.next:
        node = node.next
        pp.pprint(node.get_data())

Давайте создадим первый блок. Передадим в него имя и сумму. Предыдущий хеш для первого блока в цепочке — всегда 0.

chain = Block(0, 'Tim', 120.00)

Теперь добавим еще несколько блоков.

chain.append('Jamil', 200.00)
chain.append('Carla', 123.45)
chain.append('Sarah', 450.00)

print_chain(chain)

Вы увидите следующий вывод:

{   'amount': 120.0,
    'hash': 1423,
    'prev_hash': 0,
    'time': datetime.time(0, 11, 46, 849539),
    'transactor': 'Tim'}
{   'amount': 200.0,
    'hash': 4359,
    'prev_hash': 1423,
    'time': datetime.time(0, 11, 46, 849608),
    'transactor': 'Jamil'}
{   'amount': 123.45,
    'hash': 5820,
    'prev_hash': 4359,
    'time': datetime.time(0, 11, 46, 849617),
    'transactor': 'Carla'}
{   'amount': 450.0,
    'hash': 15469,
    'prev_hash': 5820,
    'time': datetime.time(0, 11, 46, 849624),
    'transactor': 'Sarah'}

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

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

chain.next.set_amount(700)

print("UPDATE:")
print_chain(chain)

Вывод:

UPDATE:
{   'amount': 120.0,
    'hash': 1423,
    'prev_hash': 0,
    'time': datetime.time(0, 11, 46, 849539),
    'transactor': 'Tim'}
{   'amount': 700,
    'hash': 20051,
    'prev_hash': 1423,
    'time': datetime.time(0, 11, 46, 849608),
    'transactor': 'Jamil'}
{   'amount': 123.45,
    'hash': 21512,
    'prev_hash': 20051,
    'time': datetime.time(0, 11, 46, 849617),
    'transactor': 'Carla'}
{   'amount': 450.0,
    'hash': 31161,
    'prev_hash': 21512,
    'time': datetime.time(0, 11, 46, 849624),
    'transactor': 'Sarah'}

Все прошло по плану! Вы создали собственную криптовалюту! Ну ладно, нечто похожее на нее. Если вам интересно, можете сделать форк оригинального кода Bitcoin и на его основе создать что-то свое. Код доступен на GitHub.