python

Как сделать свой блокчейн. Часть 1 — Создание, Хранение, Синхронизация, Отображение, Майнинг и Доказ

  • четверг, 23 ноября 2017 г. в 03:13:39
https://habrahabr.ru/company/otus/blog/342974/
  • Python
  • Блог компании Отус


Доброго всем! Мы тут потихоньку начали исследовать новое совсем для нас направление для обучения — блокчейны и нашли то, что оказалось интересным в рамках нашего курса по Python, в том числе. Чем, собственно, и хотим поделиться с вами.


Я могу узнать, когда у меня появился первый Bitcoin, из истории кошелька в моем аккаунте на Coinbase — входящая транзакция в 2012 году в подарок за регистрацию. Bitcoin в то время стоил около 6.50$. Если бы я сохранил те 0.1 BTC, на момент написания статьи это бы уже стоило более 500$. Если кому-то интересно, я продал их, когда Bitcoin стоил 2000$. Так что я получил только 200$ вместо ныне возможных 550$. Не стоило торопиться.

О существовании Bitcoin я знал, но особо не интересовался. Я видел взлеты и падения курса $/BTC. Я видел, как люди говорят, что за ним будущее, а видел статьи о его полной бессмысленности. Но личного мнения у меня не было — просто наблюдал со стороны.
Точно так же я почти не следил за блокчейнами. Но в последнее время мой отец несколько раз упоминал, что на CNBC и Bloomberg, которые он смотрит по утрам, часто рассказывают о блокчейнах, и он понятия не имеет, что это.

И тогда я внезапно понял, что нужно чуть глубже разобраться в этой теме. И начал с “исследования” — прочитал огромное количество статей в интернете, объясняющую их суть. Некоторые были хорошие, некоторые плохие, некоторые глубокие, а некоторые очень поверхностные.

Чтения оказалось недостаточно, а если существует одна вещь, которую я знаю наверняка, так это то, что чтение не объяснит и сотой доли того, что объяснит программирование. И так я понял, что стоит написать свой собственный локальный блокчейн.

Нужно учитывать, что есть большая разница между базовым блокчейном, который я описываю и “профессиональным” блокчейном. Эта цепь не создаст криптовалюту. Блокчейны не требуют производства монет, которые можно продавать и менять на физические деньги.
Блокчейны используются для хранения и подтверждения информации. Монеты помогают стимулирующим узлами участвовать в валидации, но они не обязаны существовать.
Я пишу пост по нескольким причинам: 1) Чтобы люди, прочитавшие его, смогли узнать больше о блокчейнах; 2) Чтобы я смог понять больше, объяснив код, а не просто написав его.
В этом посте я покажу способ хранения данных блокчейна и генерации начального блока, синхронизацию узла с локальными данными блокчейна, отображение блокчейна (что впоследствии будет использоваться для синхронизации с другими узлами), а затем, майнинг и создание валидных новых блоков. В первом посте не будет никаких других узлов. Никаких кошельков, пиров, важных данных. О них поговорим позднее.

В двух словах

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

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

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

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

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

Если хотите изучить код, посмотрите ветку part 1 на Github. Смело присылайте мне любые вопросы, комментарии, правки и похвалы (если вы в настроении сделать что-то особо хорошее), или просто пишите в твиттер.

Шаг 1 — Классы и Файлы

Первый шаг — написание класса, обрабатывающего блоки при запуске узлов. Я назову этот класс Block. Честно говоря, много делать не придется. В функции __init__ мы будем верить, что вся необходимая информация уже представлена в словаре. Для производственного блокчейна — это не самое мудрое решение, но подходит в качестве примера, потому что код пишу только я. Также я напишу метод, запаковывающий важную информацию блока в словарь, а после заведу более удобный способ для отображения информации блока при его печати в терминал.

class Block(object):
  def __init__(self, dictionary):
  '''
    We're looking for index, timestamp, data, prev_hash, nonce
  '''
  for k, v in dictionary.items():
    setattr(self, k, v)
  if not hasattr(self, 'hash'): #in creating the first block, needs to be removed in future
    self.hash = self.create_self_hash()

  def __dict__(self):
    info = {}
    info['index'] = str(self.index)
    info['timestamp'] = str(self.timestamp)
    info['prev_hash'] = str(self.prev_hash)
    info['hash'] = str(self.hash)
    info['data'] = str(self.data)
    return info

  def __str__(self):
    return "Block<prev_hash: %s,hash: %s>" % (self.prev_hash, self.hash)

Чтобы создать первый блок, запустим этот простой код:

def create_first_block():
  # index zero and arbitrary previous hash
  block_data = {}
  block_data['index'] = 0
  block_data['timestamp'] = date.datetime.now()
  block_data['data'] = 'First block data'
  block_data['prev_hash'] = None
  block = Block(block_data)
  return block

Отлично. Последний вопрос в этой части — где хранить данные в файловой системе. Это необходимо, если мы не хотим потерять локальные данные блока при отключении узла.
Я назову папку с данными ‘chaindata’, в какой-то степени подражая схеме папок Etherium Mist. Каждому блоку теперь присвоен отдельный файл, названный по его индексу. Нужно убедиться, что имена файлов содержат в начале достаточное количество нулей, чтобы блоки перечислялись по порядку.

С учетом кода выше, нужно написать следующее для создание первого блока:

#check if chaindata folder exists.
chaindata_dir = 'chaindata'
if not os.path.exists(chaindata_dir):
  #make chaindata dir
  os.mkdir(chaindata_dir)
  #check if dir is empty from just creation, or empty before
if os.listdir(chaindata_dir) == []:
  #create first block
  first_block = create_first_block()
  first_block.self_save()

Шаг 2 — Синхронизация блокчейна, локально

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

def sync():
  node_blocks = []
  #We're assuming that the folder and at least initial block exists
  chaindata_dir = 'chaindata'
  if os.path.exists(chaindata_dir):
    for filename in os.listdir(chaindata_dir):
      if filename.endswith('.json'): #.DS_Store sometimes screws things up
        filepath = '%s/%s' % (chaindata_dir, filename)
        with open(filepath, 'r') as block_file:
          block_info = json.load(block_file)
          block_object = Block(block_info) #since we can init a Block object with just a dict
          node_blocks.append(block_object)
return node_blocks

Пока просто и красиво. Чтение строк из файлов их загрузка в структуры данных не требуют чрезмерно сложного кода. Пока это работает. Но в будущих постах, где я буду писать о возможностях коммуникации разных узлов, эта функция sync станет значительно сложнее.

Шаг 3 — Отображение блокчейна

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

Для этого я использую Flask — у него низкий порог вхождения, и я решил, что он подходит для наших целей.

Ниже представлен код для отображения json блокчейна. Я проигнорирую импорты для экономии места.

node = Flask(__name__)

node_blocks = sync.sync() #inital blocks that are synced

@node.route('/blockchain.json', methods=['GET'])
def blockchain():
  '''
  Shoots back the blockchain, which in our case, is a json list of hashes
  with the block information which is:
  index
  timestamp
  data
  hash
  prev_hash
  '''
  node_blocks = sync.sync() #regrab the nodes if they've changed
  # Convert our blocks into dictionaries
  # so we can send them as json objects later
  python_blocks = []
  for block in node_blocks:
    python_blocks.append(block.__dict__())
  json_blocks = json.dumps(python_blocks)
  return json_blocks

if __name__ == '__main__':
  node.run()

Запустите этот код, зайдите на localhost:3000/blockchain.json и увидите текущий блок.

Шаг 4 — “Майнинг”, также известный как создание блока

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

Сатоши описывает это следующим образом в Bitcoin whitepaper. Учтите, что “timestamp сервер” назван “узлом”.

“Начнем описание нашего решения с timestamp сервера. Его работа заключается в хэшировании блока данных, на который нужно поставить timestamp, и открытой публикации этого хэша… Timestamp показывает, что в данный момент конкретные данные существовали и потому попали в хэш блока. Каждый хэш включает в себя предыдущий timestamp: так выстраивается цепь, где очередное звено укрепляет все предыдущие.”

Скриншот изображения, прикрепленного под описанием:



Основная идея раздела — при необходимости соединить блоки, мы создаем хэш информации о новом блоке, включая время создания блока, хэш предыдущего блока и информацию в самом блоке. Я буду называть всю эту информацию “хедером” блока. Таким образом, мы можем проверить корректность блока, посчитав все хэши перед ним, подтвердив последовательность.

В данном случае хедер, который я создаю, объединяет значения строки в одну огромную строку. Я включил следующие данные:

  1. Индекс, показывающий каким по счету является блок;
  2. Хэш предыдущего блока;
  3. Данные — просто случайные строки. Для bitcoin они называются Merkle root и содержат информацию о транзакциях;
  4. Timestamp майнинга этого блока.

def generate_header(index, prev_hash, data, timestamp):
  return str(index) + prev_hash + data + str(timestamp)

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

Хедер Bitcoin значительно сложнее объединения строк. Он использует хэши данных и времени и завязан на то, как данные расположены в памяти. Но в нашем случае объединения строк достаточно.

Теперь у нас есть хедер и можно вычислить валидность хэша. Я буду использовать метод, отличающийся от метода Bitcoin, но все равно запущу хедер блока через функцию sha256.

def calculate_hash(index, prev_hash, data, timestamp, nonce):
  header_string = generate_header(index, prev_hash, data, timestamp, nonce)
  sha = hashlib.sha256()
  sha.update(header_string)
  return sha.hexdigest()

Для майнинга блока мы используем функцию выше, чтобы получить хэш, положить его в новый блок и сохранить этот блок в директории chaindata.

node_blocks = sync.sync()

def mine(last_block):
  index = int(last_block.index) + 1
  timestamp = date.datetime.now()
  data = "I block #%s" % (int(last_block.index) + 1) #random string for now, not transactions
  prev_hash = last_block.hash
  block_hash = calculate_hash(index, prev_hash, data, timestamp)

  block_data = {}
  block_data['index'] = int(last_block.index) + 1
  block_data['timestamp'] = date.datetime.now()
  block_data['data'] = "I block #%s" % last_block.index
  block_data['prev_hash'] = last_block.hash
  block_data['hash'] = block_hash
  return Block(block_data)

def save_block(block):
  chaindata_dir = 'chaindata'
  filename = '%s/%s.json' % (chaindata_dir, block.index)
  with open(filename, 'w') as block_file:
    print new_block.__dict__()
    json.dump(block.__dict__(), block_file)

if __name__ == '__main__':
  last_block = node_blocks[-1]
  new_block = mine(last_block)
  save_block(new_block)

Готово! Но при таком типе создания блока кто угодно с самым быстрым CPU сможет создавать самые длинные цепи, которые другие узлы посчитают корректными. Нужен способ снизить скорость создания блока и подтверждение до перехода к следующему блоку.

Шаг 5 — Доказательство выполнения работы

Для снижения скорость я использую Доказательство выполнения работы, как и Bitcoin. Доказательство доли владения — другой способ, используемый в блокчейнах для достижения консенсуса, но в этом случае я воспользуюсь работой.

Способ сделать это — установить требования к структуре хэша блока. Как и в случае с bitcoin, необходимо убедиться, что хэш начинается с определенного количества нулей, перед тем, как перейти к следующему. А для этого нужно добавить в хедер дополнительную информацию — случайно перебираемое число (nonce).

def generate_header(index, prev_hash, data, timestamp, nonce):
  return str(index) + prev_hash + data + str(timestamp) + str(nonce)

Теперь функция майнинга настроена для создания хэша, но если хэш блока не содержит достаточного количества нулей, мы увеличиваем значение nonce, создаем новый хедер, вычисляем новый хэш и проверяем хватает ли нулей.

NUM_ZEROS = 4

def mine(last_block):
  index = int(last_block.index) + 1
  timestamp = date.datetime.now()
  data = "I block #%s" % (int(last_block.index) + 1) #random string for now, not transactions
  prev_hash = last_block.hash
  nonce = 0

  block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)
  while str(block_hash[0:NUM_ZEROS]) != '0' * NUM_ZEROS:
    nonce += 1
    block_hash = calculate_hash(index, prev_hash, data, timestamp, nonce)
  block_data = {}
  block_data['index'] = int(last_block.index) + 1
  block_data['timestamp'] = date.datetime.now()
  block_data['data'] = "I block #%s" % last_block.index
  block_data['prev_hash'] = last_block.hash
  block_data['hash'] = block_hash
  block_data['nonce'] = nonce
  return Block(block_data)

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

Заключение

На этом все! Пока что. Осталось еще много вопросов и фичей в блокчейнах, которые я не объяснил.

Например, как задействовать другие узлы? Как узлы передают данные, когда включаются в блок? Существуют ли иные способы хранения данных кроме огромных строк данных?
Ответы на эти вопросы можно будет найти в следующих частях этой серии постов, как только я сам найду на них ответы. Пожелания по содержанию можно писать мне в твиттер, в комментарии к посту или через форму обратной связи!

Спасибо моей сестре Саре за уточняющие вопросы о блокчейнах и помощь в редактировании поста!

THE END

Комментарии, вопросы, как всегда, приветствуются и тут, и на дне открытых дверей.