Реализуем свой Bitcoin на языке программирования Python

Электронная валюта уже ни для кого не новость, а вот собственная реализация валюты на Python обещает быть интересной. Создаем новый Bitcoin.

Как же создать новый Bitcoin, и что для этого нужно – рассмотрим в этой статье.

Простая монета (SimpleCoin) – простая, небезопасная и не до конца реализованная версия блокчейн криптовалюты на Python. Основной задумкой проекта была идея реализовать максимально похожую, простую и рабочую версию Bitcoin. Если вы тоже хотите создать что-то свое, вам стоит обратиться к Bitcoin Repository.

Вступление

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

С чего начать?

Первое, что необходимо сделать, – установить requirements.txt.

pip install -r requirements.txt

Новый bitcoin. НачалоВ проекте должен быть файл конфига miner_config.py с таким содержимым:

"""Этот файл нужно изменять до того, как вы запустите майнер. Для лучшего понимания смотрите в
wallet.py.
"""

# Тут указываем сгенерированный адрес. Все монеты пойдут сюда.
MINER_ADDRESS = "q3nf394hjg-random-miner-address-34nf3i4nflkn3oi"

# Тут укажите URL-адрес ноды или ее ip. Если все запущено на localhost, то пишем так:
MINER_NODE_URL = "http://localhost:5000"

# А здесь храним URL-адреса каждого узла в сети, чтобы можно было общаться с ними.
PEER_NODES = []

Далее два важных шага:

  • Запустить miner.py, чтобы создать ноду и начать майнить;
  • запустить wallet.py, чтобы стать пользователем и отправлять транзакциии (для этого нужно также запустить miner.py).

Важное замечание: не запускайте майнер в среде разработке Python, а только в консоли, т. к. он использует параллельные процессы, которые не работают в IDL-e.

Как это работает?

Самый важный файл в этом проекте – miner.py. Запустив его, вы создаете сервер, который подключается к блокчейну и обрабатывает транзакции (которые отправляют пользователи) путем майнинга. За это вы получаете несколько монет. Чем больше нод создается, тем безопаснее становится вся цепочка.

Новый bitcoin. Как это работает

miner.py запускает 2 параллельных процесса:

  • первый следит за добычей, обновляет цепочки и создает отчеты о работе;
  • второй запускает сервер, к которому могут подключаться пиры и пользователи для запроса всей цепочки, отправки транзакций и прочего.
import time
import hashlib as hasher
import json
import requests
import base64
from flask import Flask
from flask import request
from multiprocessing import Process, Pipe
import ecdsa

from miner_config import MINER_ADDRESS, MINER_NODE_URL, PEER_NODES

node = Flask(__name__)

class Block:
    def __init__(self, index, timestamp, data, previous_hash):
        """Возвращает новый объект Block. Каждый блок «привязан» к предыдущему по
         уникальному хэшу
        Аргументы:
            index (int): Номер блока.
            timestamp (int): Timestamp создания блока.
            data (str): Данные для отправки.
            previous_hash(str): Строка с хэшем предыдущего блока.
        Атрибуты:
            index (int): Номер блока.
            timestamp (int): Timestamp создания блока.
            data (str): Данные для отправки.
            previous_hash(str): Строка с хэшем предыдущего блока.
            hash(str): Хэш текущего блока.
        """
        self.index = index
        self.timestamp = timestamp
        self.data = data
        self.previous_hash = previous_hash
        self.hash = self.hash_block()
  
    def hash_block(self):
        """Создание уникального хэша для блока при помощи sha256."""
        sha = hasher.sha256()
        sha.update((str(self.index) + str(self.timestamp) + str(self.data) + /
            str(self.previous_hash)).encode('utf-8'))
        return sha.hexdigest()

def create_genesis_block():
    """Для создания нового блока. ему нужен хэш предыдущего. Первыйблок не знает хэш 
    предыдущего, поэтому его нужно создать руками (нулевой индекс и произвольный хэш)"""
    return Block(0, time.time(), 
        {"proof-of-work": 9,"transactions": None},
         "0")

# Копирование блокчейн-ноды
BLOCKCHAIN = []
BLOCKCHAIN.append(create_genesis_block())

""" Тут хранятся транзакции, которые относятся к текущей ноде. Если нода, которой была 
отправлена транзакция добавляет новый блок, он успешно принимается, но есть вероятность того,
что заявка будет отклонена и транзакция вернется """
NODE_PENDING_TRANSACTIONS = []

def proof_of_work(last_proof,blockchain):
  # Создаем переменную, которая будет использоваться для проверки работы
  incrementor = last_proof + 1
  # Получаем время начала
  start_time = time.time()
  # Продолжаем увеличивать инкрементатор до тех пор, пока он не будет равен числу, которое 
  # делится на 9, и доказательству работы предыдущего блока 
  while not (incrementor % 7919 == 0 and incrementor % last_proof == 0):
    incrementor += 1
    start_time = time.time()
    # Каждые 60сек проверяем, нашла ли нода подтверждение работы
    if (int((time.time()-start_time)%60)==0):
        # Если нашла - прекращаем проверку
        new_blockchain = consensus(blockchain)
        if new_blockchain != False:
            #(False:другая нода первая нашла подтверждение работы)
            return (False,new_blockchain)
  # Как только число найдено, можно вернуть его как доказательство
  return (incrementor,blockchain)


def mine(a,blockchain,node_pending_transactions):
    BLOCKCHAIN = blockchain
    NODE_PENDING_TRANSACTIONS = node_pending_transactions
    while True:
        """Майнинг - единственный способ создания новых монет.
         Чтобы предотвратить создание большого количества монет, процесс
         замедляется с помощью алгоритма доказательства работы.
        """
        # Получаем последнее доказательство
        last_block = BLOCKCHAIN[len(BLOCKCHAIN) - 1]
        last_proof = last_block.data['proof-of-work']
        # Ищем доказательство работы в текущем блоке
        # Программа будет ждать пока новое подтверждение не будет найдено
        proof = proof_of_work(last_proof, BLOCKCHAIN)
        # Если доказательство не нашлось - начинаем майнить опять
        if proof[0] == False:
            # Обновляем блокчейн и сохраняемся в файл
            BLOCKCHAIN = proof[1]
            a.send(BLOCKCHAIN)
            continue
        else:
            # Как только мы найдем действительное доказательство работы, мы можем разбить блок,
            # и добавить транзакцию
            # Загружаем все ожидающие транзакции и отправляем их на сервер
            NODE_PENDING_TRANSACTIONS = requests.get(MINER_NODE_URL + "
              /txion?update=" + MINER_ADDRESS).content
            NODE_PENDING_TRANSACTIONS = json.loads(NODE_PENDING_TRANSACTIONS)
            # Затем добавляется вознаграждение за майнинг
            NODE_PENDING_TRANSACTIONS.append(
            { "from": "network",
              "to": MINER_ADDRESS,
              "amount": 1 }
            )
            # Теперь мы можем собрать данные, необходимые для создания нового блока
            new_block_data = {
            "proof-of-work": proof[0],
            "transactions": list(NODE_PENDING_TRANSACTIONS)
            }
            new_block_index = last_block.index + 1
            new_block_timestamp = time.time()
            last_block_hash = last_block.hash
            # Список пустых транзакций
            NODE_PENDING_TRANSACTIONS = []
            # Теперь создаем новый блок
            mined_block = Block(new_block_index, new_block_timestamp, new_block_data, 
                 last_block_hash)
            BLOCKCHAIN.append(mined_block)
            # Сообщаем клиентам, что нода готова майнить
              print(json.dumps({
              "index": new_block_index,
              "timestamp": str(new_block_timestamp),
              "data": new_block_data,
              "hash": last_block_hash
            }) + "\n")
            a.send(BLOCKCHAIN)
            requests.get(MINER_NODE_URL + "/blocks?update=" + MINER_ADDRESS)


def find_new_chains():
    # Получаем данные о других нодах
    other_chains = []
    for node_url in PEER_NODES:
        # Получаем их цепочки GET-запросом
        block = requests.get(node_url + "/blocks").content
        # Конвертим объект JSON в словарь Python
        block = json.loads(block)
        # Проверяем, чтобы другая нода была корректной
        validated = validate_blockchain(block)
        if validated == True:
            # Добавляем ее в наш список
            other_chains.append(block)
    return other_chains

def consensus(blockchain):
    # Получаем блоки из других нод
    other_chains = find_new_chains()
    # Если наша цепочка не самая длинная, то мы сохраняем самую длинную цепочку
    BLOCKCHAIN = blockchain
    longest_chain = BLOCKCHAIN
    for chain in other_chains:
        if len(longest_chain) < len(chain):
            longest_chain = chain
    # Если самая длинная цепочка не наша, делаем ее самой длинной
    if longest_chain == BLOCKCHAIN:
        # Продолжаем искать подтверждение
        return False
    else:
        # Сдаемся, обновляем цепочку и ищем снова
        BLOCKCHAIN = longest_chain
        return BLOCKCHAIN


def validate_blockchain(block):
    """Проверяем отправленную цепочку. Если хэши неверны, возвращаем false
    block(str): json
    """
    return True


@node.route('/blocks', methods=['GET'])
def get_blocks():
    # Загружаем текущий блокчейн.
    if request.args.get("update") == MINER_ADDRESS:
        global BLOCKCHAIN
        BLOCKCHAIN = b.recv()
        chain_to_send = BLOCKCHAIN
    else:
        # Любая нода, которая будет подключаться, будет делать так:
        chain_to_send = BLOCKCHAIN
    # Конвертим наши блоки в словари и можем отправить им json объект
    chain_to_send_json = []
    for block in chain_to_send:
        block = {
            "index": str(block.index),
            "timestamp": str(block.timestamp),
            "data": str(block.data),
            "hash": block.hash
        }
        chain_to_send_json.append(block)

    # Отправляем нашу цепочку тому, кто попросил
    chain_to_send = json.dumps(chain_to_send_json)
    return chain_to_send


@node.route('/txion', methods=['GET','POST'])
def transaction():
    """Каждая отправленная транзакция в эту ноду проверяется и отправляется.
    Потом она ждет добавления в блокчейн. Транзакции не создают новые монеты, а только 
    перемещают их.
    """
    if request.method == 'POST':
        # При каждом новом POST-запросе мы извлекаем данные транзакции
        new_txion = request.get_json()
        # Добавляем транзакцию в список
        if validate_signature(new_txion['from'],new_txion['signature'],new_txion['message']):
            NODE_PENDING_TRANSACTIONS.append(new_txion)
            # Транзакция успешно отправлена - сообщаем это в консоль
            print("New transaction")
            print("FROM: {0}".format(new_txion['from']))
            print("TO: {0}".format(new_txion['to']))
            print("AMOUNT: {0}\n".format(new_txion['amount']))
            return "Transaction submission successful\n"
        else:
            return "Transaction submission failed. Wrong signature\n"
    # Отправляем ожидающие транзакции майнеру
    elif request.method == 'GET' and request.args.get("update") == MINER_ADDRESS:
        pending = json.dumps(NODE_PENDING_TRANSACTIONS)
        NODE_PENDING_TRANSACTIONS[:] = []
        return pending

def validate_signature(public_key,signature,message):
    """Проверяем правильность подписи. Это используется для доказательства того, что это вы
    (а не кто-то еще), пытающийся совершить транзакцию за вас. Вызывается, когда пользователь 
    пытается отправить новую транзакцию.
    """
    public_key = (base64.b64decode(public_key)).hex()
    signature = base64.b64decode(signature)
    vk = ecdsa.VerifyingKey.from_string(bytes.fromhex(public_key), curve=ecdsa.SECP256k1)
    try:
        return(vk.verify(signature, message.encode()))
    except:
        return False

def welcome_msg():
    print("""       =========================================\n
        SIMPLE COIN v1.0.0 - BLOCKCHAIN SYSTEM\n
       =========================================\n\n
        You can find more help at: https://github.com/cosme12/SimpleCoin\n
        Make sure you are using the latest version or you may end in
        a parallel chain.\n\n\n""")


if __name__ == '__main__':
    welcome_msg()
    # Запускаем майнинг
    a,b=Pipe()
    p1 = Process(target = mine, args=(a,BLOCKCHAIN,NODE_PENDING_TRANSACTIONS))
    p1.start()
    # Запускаем сервер для приема транзакций
    p2 = Process(target = node.run(), args=b)
    p2.start()

wallet.py используется для пользователей. Запуск этого файла позволит вам генерировать новые адреса, отправлять монеты и проверять историю транзакций. Помните, что если вы его запускаете на локальном сервере, вам нужен процесс miner.py.

Новый bitcoin. Заключение

"""Это ваш кошелек. Здесь вы можете сделать несколько вещей:
- Создать новый адрес (открытый и закрытый ключ). Вы будете использовать этот адрес 
(открытый ключ) для отправки или получения любых транзакций. У вас может быть столько адресов, 
сколько пожелаете, но если вы потеряете доступ - восстановить его вы уже не сможете.
- Отправлять монеты на другой адрес.
- Извлекать целую цепочку и проверять баланс.
Если вы впервые используете этот скрипт, не забудьте сгенерировать
новый адрес и отредактируйте файл конфигурации miner.
Временная метка захэширована. Когда вы отправляете транзакцию, она будет получена
несколькими узлами. Если какой-либо узел майнит блок, ваша транзакция будет добавлена в
blockchain, а другие узлы будут ожидать. Если какой-либо узел видит, что ваша
транзакция с той же меткой времени, они должны удалить ее из
node_pending_transactions, чтобы избежать ее обработки более 1 раза.
"""

import requests
import time
import base64
import ecdsa


def welcome_msg():
    print("""       =========================================\n
        SIMPLE COIN v1.0.0 - BLOCKCHAIN SYSTEM\n
       =========================================\n\n
        You can find more help at: https://github.com/cosme12/SimpleCoin\n
        Make sure you are using the latest version or you may end in
        a parallel chain.\n\n\n""")


def wallet():
    response = False
    while response not in ["1","2","3"]:
        response = input("""What do you want to do?
        1. Generate new wallet
        2. Send coins to another wallet
        3. Check transactions\n""")
    if response in "1":
        # Создаем новый кошелек
        print("""=========================================\n
IMPORTANT: save this credentials or you won't be able to recover your wallet\n
=========================================\n""")
        generate_ECDSA_keys()        
    elif response in "2":
        addr_from = input("From: introduce your wallet address (public key)\n")
        private_key = input("Introduce your private key\n")
        addr_to = input("To: introduce destination wallet address\n")
        amount = input("Amount: number stating how much do you want to send\n")
        print("=========================================\n\n")
        print("Is everything correct?\n")
        print("From: {0}\nPrivate Key: {1}\nTo: {2}\nAmount: {3}\n".format
            (addr_from,private_key,addr_to,amount))
        response = input("y/n\n")
        if response.lower() == "y":
            send_transaction(addr_from,private_key,addr_to,amount)
    elif response == "3":
        check_transactions()


def send_transaction(addr_from,private_key,addr_to,amount):
    """Отправляем транзакцию на разные узлы. Как только главная нода начнет майнить блок,
    транзакция добавляется в блокчейн. Несмотря на это, существует небольшая вероятность того,
    что ваша транзакция будет отменена из-за других узлов, имеющих более длинную цепочку. 
    Поэтому убедитесь, что ваша транзакция глубоко в цепочке, прежде чем утверждать, 
    что она одобрена!
    """
    if len(private_key) == 64:
        signature,message = sign_ECDSA_msg(private_key)
        url     = 'http://localhost:5000/txion'
        payload = {"from": addr_from, "to": addr_to, "amount": amount, "signature": /
            signature.decode(), "message": message}
        headers = {"Content-Type": "application/json"}

        res = requests.post(url, json=payload, headers=headers)
        print(res.text)
    else:
        print("Wrong address or key length! Verify and try again.")

def check_transactions():
    """Извлекаем весь блокчейн. Тут вы можете проверить свой баланс. Если блокчейн очень 
       длинный, загрузка может занять время.
    """
    res = requests.get('http://localhost:5000/blocks')
    print(res.text)

def generate_ECDSA_keys():
    """Эта функция следит за созданием вашего private и public ключа. Очень важно не потерять
    ни один из них т.к. доступ к кошельку будет потерян. Если кто-то получит доступ к вашему
    кошельку, вы рискуете потерять свои монеты.
    private_key: str
    public_ley: base64
    """
    sk = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1) # private ключ
    private_key = sk.to_string().hex() # конвертим private ключ в hex
    vk = sk.get_verifying_key() # public ключ
    public_key = vk.to_string().hex()
    print("Private key: {0}".format(private_key))
    # кодируем public ключ, чтобы сделать его короче
    public_key = base64.b64encode(bytes.fromhex(public_key))
    # используем decode(), чтобы удалить b'' из строки
    print("Wallet address / Public key: {0}".format(public_key.decode()))


def sign_ECDSA_msg(private_key):
    """Подписываем сообщение для отправки
    private ключ должен быть hex
    return
    signature: base64
    message: str
    """
    # получаем timestamp, округляем, переводим в строку и кодируем
    message=str(round(time.time()))
    bmessage = message.encode()
    sk = ecdsa.SigningKey.from_string(bytes.fromhex(private_key), curve=ecdsa.SECP256k1)
    signature = base64.b64encode(sk.sign(bmessage))
    return signature,message


if __name__ == '__main__':
    welcome_msg()
    wallet()
    input("Press any key to exit...")

Заключение

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

Оригинал

Другие материалы по теме:

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик С#
от 200000 RUB до 400000 RUB
Java Team Lead
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ