Реализуем свой Bitcoin на языке программирования Python
Электронная валюта уже ни для кого не новость, а вот собственная реализация валюты на Python обещает быть интересной. Создаем новый Bitcoin.
Как же создать новый Bitcoin, и что для этого нужно – рассмотрим в этой статье.
Простая монета (SimpleCoin) – простая, небезопасная и не до конца реализованная версия блокчейн криптовалюты на Python. Основной задумкой проекта была идея реализовать максимально похожую, простую и рабочую версию Bitcoin. Если вы тоже хотите создать что-то свое, вам стоит обратиться к Bitcoin Repository.
Вступление
Понятие блокчейн уже не раз рассматривалось, но повторение – мать учения. Блокчейн – это база транзакций, совместно используемая всеми узлами, участвующими в системе на основе биткойн-протокола. Полная копия цепочки блоков валюты содержит каждую транзакцию, когда-либо выполняемую в валюте. С помощью этой информации можно узнать, какое значение принадлежит каждому адресу в любой точке истории.
С чего начать?
Первое, что необходимо сделать, – установить requirements.txt.
pip install -r requirements.txt
В проекте должен быть файл конфига
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
. Запустив его, вы создаете сервер, который подключается к блокчейну и обрабатывает транзакции (которые отправляют пользователи) путем майнинга. За это вы получаете несколько монет. Чем больше нод создается, тем безопаснее становится вся цепочка.
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
.
"""Это ваш кошелек. Здесь вы можете сделать несколько вещей: - Создать новый адрес (открытый и закрытый ключ). Вы будете использовать этот адрес (открытый ключ) для отправки или получения любых транзакций. У вас может быть столько адресов, сколько пожелаете, но если вы потеряете доступ - восстановить его вы уже не сможете. - Отправлять монеты на другой адрес. - Извлекать целую цепочку и проверять баланс. Если вы впервые используете этот скрипт, не забудьте сгенерировать новый адрес и отредактируйте файл конфигурации 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...")
Заключение
Автор призывает участвовать всех желающих в этом проекте. Главная задача – упрощение кода и повышение его читабельности. Перевод на русский осуществлен Библиотекой Программиста.