Пишем свой блокчейн

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

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

Прежде, чем начать

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

Если вы не знаете, что такое хэш, вот объяснение.

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

Что мне нужно? Убедитесь, что установлен Python 3.6 + (вместе с pip). Вам также потребуется установить Flask и замечательную библиотеку requests:

 pip install Flask==0.12.2 requests==2.18.4 

Вам также понадобится HTTP-клиент, например Postman или cURL. Но все подойдет.

Исходный код доступен здесь.

Шаг 1. Создание блокчейна

Откройте свой любимый текстовый редактор или IDE, лично я люблю PyCharm. Создайте новый файл с именем blockchain.py. Мы будем использовать только один файл, но, если вы потеряетесь, всегда можете обратиться к исходному коду.

Представление блокчейна

Мы создадим класс Blockchain, конструктор которого создает начальный пустой список (для хранения нашей цепочки блоков), другой - для хранения транзакций. Вот план нашего класса:

class Blockchain(object):
    def __init__(self):
        self.chain = []
        self.current_transactions = []
        
    def new_block(self):
        # Creates a new Block and adds it to the chain
        pass
    
    def new_transaction(self):
        # Adds a new transaction to the list of transactions
        pass
    
    @staticmethod
    def hash(block):
        # Hashes a Block
        pass

    @property
    def last_block(self):
        # Returns the last Block in the chain
        pass

Наш класс Blockchain отвечает за управление цепочкой. Он будет хранить транзакции и иметь несколько вспомогательных методов для добавления новых блоков в цепочку. Давайте начнем конкретизировать некоторые методы.

Как выглядит блок?

Каждый блок имеет индекс, временную метку (во времени Unix), список транзакций, доказательство (подробнее об этом позже) и хэш предыдущего блока.

Вот пример того, как выглядит отдельный блок:

block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

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

Есть смысл? Если нет, привыкните - это основная идея, лежащая в основе блокчейна.

Добавление транзакций в блок

Нам понадобится способ добавления транзакций в блок. За это отвечает наш метод new_transaction(), и он довольно прост:

class Blockchain(object):
    ...
    
    def new_transaction(self, sender, recipient, amount):
        """
        Creates a new transaction to go into the next mined Block
        :param sender: <str> Address of the Sender
        :param recipient: <str> Address of the Recipient
        :param amount: <int> Amount
        :return: <int> The index of the Block that will hold this transaction
        """

        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

После добавления транзакции в список он возвращает индекс блока, в который будет добавлена ​​транзакция - следующего блока, который будет добыт. Это будет полезно позже, для пользователя, отправляющего транзакцию.

Создание новых блоков

Когда наш экземпляр Blockchain будет создан, нам нужно будет засеять его генезисным блоком - блоком без предшественников. Нам также нужно будет добавить «доказательство» к нашему генезис-блоку, которое является результатом майнинга (или доказательства работы). Подробнее о майнинге поговорим позже.

Помимо создания генезис-блока в нашем конструкторе, мы также конкретизируем методы для new_block(), new_transaction() и hash():

import hashlib
import json
from time import time


class Blockchain(object):
    def __init__(self):
        self.current_transactions = []
        self.chain = []

        # Create the genesis block
        self.new_block(previous_hash=1, proof=100)

    def new_block(self, proof, previous_hash=None):
        """
        Create a new Block in the Blockchain
        :param proof: <int> The proof given by the Proof of Work algorithm
        :param previous_hash: (Optional) <str> Hash of previous Block
        :return: <dict> New Block
        """

        block = {
            'index': len(self.chain) + 1,
            'timestamp': time(),
            'transactions': self.current_transactions,
            'proof': proof,
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        # Reset the current list of transactions
        self.current_transactions = []

        self.chain.append(block)
        return block

    def new_transaction(self, sender, recipient, amount):
        """
        Creates a new transaction to go into the next mined Block
        :param sender: <str> Address of the Sender
        :param recipient: <str> Address of the Recipient
        :param amount: <int> Amount
        :return: <int> The index of the Block that will hold this transaction
        """
        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):
        """
        Creates a SHA-256 hash of a Block
        :param block: <dict> Block
        :return: <str>
        """

        # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
        block_string = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()
  

Вышеупомянутое должно быть понятно - я добавил несколько комментариев и строк документации. Мы почти закончили с представлением нашей цепочки блоков. Но вам должно быть интересно, как создаются или добываются новые блоки.

Понимание доказательства работы

Алгоритм Proof of Work (PoW) - это то, как новые блоки создаются или добываются в цепочке блоков. Цель PoW - найти число, которое решает проблему. Число должно быть трудно найти, но легко проверить - с точки зрения вычислений - кем угодно в сети. Это основная идея Proof of Work.

Мы рассмотрим очень простой пример, который поможет понять это.

Давайте решим, что хэш одного целого числа, x умноженного на другое, y должен заканчиваться на 0. Так, hash(x * y) = ac23dc...0. И для этого упрощенного примера давайте примем x = 5. Реализация этого в Python:

from hashlib import sha256
x = 5
y = 0  # We don't know what y should be yet...
while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0":
    y += 1
print(f'The solution is y = {y}')

Решение здесь y = 21. Поскольку созданный хэш заканчивается на 0:

hash(5 * 21) = 1253e9373e...5e3600155e860

В Биткойне алгоритм Proof of Work называется Hashcash. И он не слишком отличается от нашего базового примера выше. Это алгоритм, который майнеры пытаются решить, чтобы создать новый блок. Как правило, сложность определяется количеством символов, которые ищутся в строке. Затем майнеры награждаются за свое решение получением монеты - в транзакции.

Сеть способна легко проверить их решение.

Внедрение базового доказательства работы

Давайте реализуем аналогичный алгоритм для нашего блокчейна. Наше правило будет аналогично приведенному выше примеру:

Найдите число p, при хешировании которого с решением предыдущего блока получается хэш с 4 ведущими 0.

import hashlib
import json

from time import time
from uuid import uuid4


class Blockchain(object):
    ...
        
    def proof_of_work(self, last_proof):
        """
        Simple Proof of Work Algorithm:
         - Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p'
         - p is the previous proof, and p' is the new proof
        :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):
        """
        Validates the Proof: Does hash(last_proof, proof) contain 4 leading zeroes?
        :param last_proof: <int> Previous Proof
        :param proof: <int> Current Proof
        :return: <bool> True if correct, False if not.
        """

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()
        return guess_hash[:4] == "0000"

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

Наш класс почти готов, и мы готовы начать с ним взаимодействовать с помощью HTTP-запросов.

Шаг 2: Блокчейн как API

Мы собираемся использовать Python Flask Framework. Это фреймворк, который упрощает сопоставление конечных точек с функциями Python. Это позволяет нам общаться с нашей цепочкой блоков через Интернет, используя HTTP-запросы.

Мы создадим три метода:

/transactions/new создать новую транзакцию в блоке

/mine чтобы сказать нашему серверу майнить новый блок.

/chain чтобы вернуть полную цепочку блоков.

Настройка Flask

Наш «сервер» сформирует единый узел в нашей сети блокчейнов. Создадим шаблонный код:

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask


class Blockchain(object):
    ...


# Instantiate our Node
app = Flask(__name__)

# Generate a globally unique address for this node
node_identifier = str(uuid4()).replace('-', '')

# Instantiate the Blockchain
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.

Конечная точка транзакций

Так будет выглядеть запрос на транзакцию. Это то, что пользователь отправляет на сервер:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

Поскольку у нас уже есть метод класса для добавления транзакций в блок, остальное легко. Напишем функцию добавления транзакций:

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()

    # Check that the required fields are in the POST'ed data
    required = ['sender', 'recipient', 'amount']
    if not all(k in values for k in required):
        return 'Missing values', 400

    # Create a new Transaction
    index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])

    response = {'message': f'Transaction will be added to Block {index}'}
    return jsonify(response), 201

Конечная точка майнинга

Наша конечная точка майнинга - это место, где происходит волшебство, и это легко. Она должна делать три вещи:

1.      Рассчитать Proof of Work

2.      Наградить майнера (нас), добавив транзакцию, дающую нам 1 монету.

3.      Создать новый блок, добавив его в цепочку

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():
    # We run the proof of work algorithm to get the next proof...
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # We must receive a reward for finding the proof.
    # The sender is "0" to signify that this node has mined a new coin.
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # Forge the new Block by adding it to the chain
    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. На этом мы закончили и можем начать взаимодействие с нашей цепочкой блоков.

Шаг 3: Взаимодействие с нашей цепочкой блоков

Вы можете использовать старый простой cURL или Postman для взаимодействия с нашим API по сети.

Запустите сервер:

$ python blockchain.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Попробуем добыть блок, запросив http://localhost:5000/mine:

Используем Postman отправить GET-запрос
Используем Postman отправить GET-запрос

Давайте создадим новую транзакцию, сделав POST-запрос к http://localhost:5000/transactions/new, содержащему нашу структуру транзакции:

Используем Postman отправить POST-запрос
Используем Postman отправить POST-запрос

Если вы не используете Postman, вы можете сделать аналогичный запрос с помощью cURL:

$ 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:

{
  "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: консенсус

Это очень круто. У нас есть базовая цепочка блоков, которая принимает транзакции и позволяет нам добывать новые блоки. Но весь смысл блокчейна в том, что они должны быть децентрализованными . И если они децентрализованы, как мы можем гарантировать, что все они отражают одну и ту же цепочку? Это называется проблемой консенсуса, и нам придется реализовать алгоритм консенсуса, если мы хотим, чтобы в нашей сети было более одного узла.

Регистрация новых узлов

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

/nodes/register принять список новых узлов в виде URL-адресов.

/nodes/resolve для реализации нашего алгоритма консенсуса, который разрешает любые конфликты, чтобы гарантировать, что узел имеет правильную цепочку.

Нам нужно будет изменить конструктор нашей цепочки блоков и предоставить метод для регистрации узлов:

...
from urllib.parse import urlparse
...


class Blockchain(object):
    def __init__(self):
        ...
        self.nodes = set()
        ...

    def register_node(self, address):
        """
        Add a new node to the list of nodes
        :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000'
        :return: None
        """

        parsed_url = urlparse(address)
        self.nodes.add(parsed_url.netloc)

Обратите внимание, что мы использовали set() для хранения списка узлов. Это дешевый способ гарантировать, что добавление новых узлов идемпотентно, то есть независимо от того, сколько раз мы добавляем конкретный узел, он появляется только один раз.

Реализация алгоритма консенсуса

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

...
import requests


class Blockchain(object)
    ...
    
    def valid_chain(self, chain):
        """
        Determine if a given blockchain is valid
        :param chain: <list> A blockchain
        :return: <bool> True if valid, False if not
        """

        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")
            # Check that the hash of the block is correct
            if block['previous_hash'] != self.hash(last_block):
                return False

            # Check that the Proof of Work is correct
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self):
        """
        This is our Consensus Algorithm, it resolves conflicts
        by replacing our chain with the longest one in the network.
        :return: <bool> True if our chain was replaced, False if not
        """

        neighbours = self.nodes
        new_chain = None

        # We're only looking for chains longer than ours
        max_length = len(self.chain)

        # Grab and verify the chains from all the nodes in our network
        for node in neighbours:
            response = requests.get(f'http://{node}/chain')

            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                # Check if the length is longer and the chain is valid
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain

        # Replace our chain if we discovered a new, valid chain longer than ours
        if new_chain:
            self.chain = new_chain
            return True

        return False

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

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

Давайте зарегистрируем две конечные точки в нашем API, одну для добавления соседних узлов, а другую для разрешения конфликтов:

@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, чтобы цепочка была длиннее. После этого я вызвал /nodes/resolve узел 1, где цепочка была заменена алгоритмом консенсуса:

Алгоритм консенсуса
Алгоритм консенсуса

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

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

Источник: https://habr.com/ru/post/583606/


Интересные статьи

Интересные статьи

Привет, Хабр. По мере написания библиотеки в этой статье я хочу продолжить объяснять математику, лежащей в основе работы дополненной реальности. Результатом будет пример на игровом дви...
Компания «Деловой разговор» — Титановый партнер 3СХ — осуществила расширенную интеграцию IP-АТС 3CX с Битрикс 24. Ранее уже существовали отдельные модули, решающие конкретные задачи, напр...
Наш сегодняшний перевод посвящен Data Science. Аналитик данных из Дублина рассказал, как искал себе жилье на рынке с высоким спросом и низким предложением. Я всегда завидовал тем про...
Данная статья — четвертая в серии. Ссылки на предыдущие статьи: первая, вторая, третья 4.1 Структуры данных Структура данных — это представление того, как организованы отдельные данны...
Приветствую всех читателей "Хабра". Дисклеймер Статья получилась довольно длинная и тем кто не хочет читать предысторию, а хочет перейти сразу к сути прошу прямиком к главе "Решение" Вступлени...