diff --git a/basic_block_gp/blockchain.py b/basic_block_gp/blockchain.py index 54ead5c1..7e51bb3b 100644 --- a/basic_block_gp/blockchain.py +++ b/basic_block_gp/blockchain.py @@ -2,7 +2,7 @@ import json from time import time from uuid import uuid4 - +import random from flask import Flask, jsonify, request @@ -12,9 +12,9 @@ def __init__(self): self.current_transactions = [] # Create the genesis block - self.new_block(previous_hash=1, proof=100) + self.new_block(proof=100) - def new_block(self, proof, previous_hash=None): + def new_block(self, proof): """ Create a new Block in the Blockchain @@ -31,48 +31,26 @@ def new_block(self, proof, previous_hash=None): """ block = { - # TODO + 'index': len(self.chain) + 1, + 'timestamp': time(), + 'transactions': self.current_transactions, + 'proof': proof, + 'prev_hash': block_hash(self.last_block), } # Reset the current list of transactions # Append the chain to the block - # Return the new block - pass - - def hash(self, block): - """ - Creates a SHA-256 hash of a Block - - :param block": Block - "return": - """ - - # Use json.dumps to convert json into a string - # Use hashlib.sha256 to create a hash - # It requires a `bytes-like` object, which is what - # .encode() does. - # It converts the Python string into a byte string. - # We must make sure that the Dictionary is Ordered, - # or we'll have inconsistent hashes - - # TODO: Create the block_string - - # TODO: Hash this string using sha256 - - # By itself, the sha256 function returns the hash in a raw string - # that will likely include escaped characters. - # This can be hard to read, but .hexdigest() converts the - # hash to a string of hexadecimal characters, which is - # easier to work with and understand - - # TODO: Return the hashed block string in hexadecimal format - pass - + # Return the new block] + self.current_transactions = [] + self.chain.append(block) + @property def last_block(self): - return self.chain[-1] + if len(self.chain) > 0: + return self.chain[-1] + return 1 - def proof_of_work(self, block): + def proof_of_work(self): """ Simple Proof of Work Algorithm Stringify the block and look for a proof. @@ -80,12 +58,21 @@ def proof_of_work(self, block): in an effort to find a number that is a valid proof :return: A valid proof for the provided block """ - # TODO - pass - # return proof + + valid_proof = False + guess = 0 + + while valid_proof == False: + valid_proof = self.validate_proof(self.last_block, guess) + guess += 1 + + if valid_proof: + return guess + else: + return None @staticmethod - def valid_proof(block_string, proof): + def validate_proof(block, proof): """ Validates the Proof: Does hash(block_string, proof) contain 3 leading zeroes? Return true if the proof is valid @@ -96,9 +83,37 @@ def valid_proof(block_string, proof): correct number of leading zeroes. :return: True if the resulting hash is a valid proof, False otherwise """ - # TODO - pass - # return True or False + + block_string = json.dumps(block, sort_keys=True) + + guess_hash = str(block_hash( + f'{block_string}{proof}' + )) + return '000' == guess_hash[0:3] + + + +def block_hash(block: dict): + """ + Creates a SHA-256 hash of a Block + + :param block": Block + "return": + """ + + # Use json.dumps to convert json into a string + # Use hashlib.sha256 to create a hash + # It requires a `bytes-like` object, which is what + # .encode() does. + # It converts the Python string into a byte string. + # We must make sure that the Dictionary is Ordered, + # or we'll have inconsistent hashes + + new_hash = hashlib.sha256( + json.dumps(block, sort_keys=True).encode()).hexdigest() + # print(len(new_hash), new_hash) + return new_hash + # Instantiate our Node @@ -111,14 +126,31 @@ def valid_proof(block_string, proof): blockchain = Blockchain() + +## Routes + +@app.route('/', methods=['GET']) +def home(): + response = 'Home Page - Blockchain practice' + return response + + @app.route('/mine', methods=['GET']) def mine(): # Run the proof of work algorithm to get the next proof - # Forge the new Block by adding it to the chain with the proof + print('Begin Mining New Block') + proof = blockchain.proof_of_work() + + blockchain.new_block( + proof = proof + ) response = { - # TODO: Send a JSON response with the new block + 'message': 'New Block Found', + 'index': blockchain.last_block['index'], + 'transactions': blockchain.last_block['transactions'], + 'proof': blockchain.last_block['proof'], } return jsonify(response), 200 @@ -127,7 +159,8 @@ def mine(): @app.route('/chain', methods=['GET']) def full_chain(): response = { - # TODO: Return the chain and its current length + 'block': blockchain.chain, + 'len': len(blockchain.chain) } return jsonify(response), 200 diff --git a/basic_transactions_gp/blockchain.py b/basic_transactions_gp/blockchain.py index 5a2c7e5a..15aea919 100644 --- a/basic_transactions_gp/blockchain.py +++ b/basic_transactions_gp/blockchain.py @@ -1,2 +1,260 @@ -# Paste your version of blockchain.py from the client_mining_p -# folder here +import hashlib +import json +from time import time +from uuid import uuid4 +import random +from flask import Flask, jsonify, request + +from math import exp + + +class Blockchain(object): + def __init__(self): + self.chain = [] + self.current_transactions = [] + self.max_award = 1 + self.difficulty = 5 + + # Create the genesis block + self.new_block(proof=100) + + def new_block(self, proof): + """ + Create a new Block in the Blockchain + + A block should have: + * Index + * Timestamp + * List of current transactions + * The proof used to mine this block + * The hash of the previous block + + :param proof: The proof given by the Proof of Work algorithm + :param previous_hash: (Optional) Hash of previous Block + :return: New Block + """ + + block = { + 'index': len(self.chain) + 1, + 'timestamp': time(), + 'transactions': self.current_transactions, + 'proof': proof, + 'prev_hash': block_hash(self.last_block), + 'difficulty': self.difficulty, + } + + # Reset the current list of transactions + # Append the chain to the block + # Return the new block] + self.current_transactions = [] + self.chain.append(block) + + @property + def last_block(self): + if len(self.chain) > 0: + return self.chain[-1] + return 1 + + @property + def award(self): + return 1 / (1 - abs(self.max_award / (5 - exp(-.05*(self.last_block['index']-0))))) # 1 - logistic + + def add_transaction(self, transaction): + self.current_transactions.append(transaction) + + def proof_of_work(self): + """ + Simple Proof of Work Algorithm + Stringify the block and look for a proof. + Loop through possibilities, checking each one against `valid_proof` + in an effort to find a number that is a valid proof + :return: A valid proof for the provided block + """ + + valid_proof = False + guess = 0 + + while valid_proof == False: + valid_proof = self.validate_proof(self.last_block, guess, difficulty=self.difficulty) + guess += 1 + + if valid_proof: + return guess + else: + return None + + @staticmethod + def validate_proof(block, proof, difficulty): + """ + Validates the Proof: Does hash(block_string, proof) contain 3 + leading zeroes? Return true if the proof is valid + :param block_string: The stringified block to use to + check in combination with `proof` + :param proof: The value that when combined with the + stringified previous block results in a hash that has the + correct number of leading zeroes. + :return: True if the resulting hash is a valid proof, False otherwise + """ + + block_string = json.dumps(block, sort_keys=True) + + guess_hash = str(block_hash( + f'{block_string}{proof}' + )) + return '0'*difficulty == guess_hash[0:difficulty] + + + +def block_hash(block: dict): + """ + Creates a SHA-256 hash of a Block + + :param block": Block + "return": + """ + + # Use json.dumps to convert json into a string + # Use hashlib.sha256 to create a hash + # It requires a `bytes-like` object, which is what + # .encode() does. + # It converts the Python string into a byte string. + # We must make sure that the Dictionary is Ordered, + # or we'll have inconsistent hashes + + new_hash = hashlib.sha256( + json.dumps(block, sort_keys=True).encode()).hexdigest() + # print(len(new_hash), new_hash) + return new_hash + + + +# Instantiate our Node +app = Flask(__name__) + +# Generate a globally unique address for this node +node_identifier = str(uuid4()).replace('-', '') + +# Instantiate the Blockchain +blockchain = Blockchain() + + + +## Routes + +@app.route('/', methods=['GET']) +def home(): + response = 'Home Page - Blockchain practice' + return response + + +@app.route('/mine', methods=['GET']) +def mine(): + # Run the proof of work algorithm to get the next proof + + print('Begin Mining New Block') + proof = blockchain.proof_of_work() + + blockchain.new_block( + proof = proof + ) + + response = { + 'message': 'New Block Found', + 'index': blockchain.last_block['index'], + 'transactions': blockchain.last_block['transactions'], + 'proof': blockchain.last_block['proof'], + } + + return jsonify(response), 200 + + +@app.route('/chain', methods=['GET']) +def full_chain(): + response = { + 'block': blockchain.chain, + 'len': len(blockchain.chain) + } + return jsonify(response), 200 + + +@app.route('/last_block', methods=['GET']) +def last_block(): + response = { + 'last_block': blockchain.last_block, + 'difficulty': blockchain.last_block['difficulty'], + } + return jsonify(response), 200 + + +@app.route('/verify', methods=['POST']) +def verify(): + miner_id = request.json['miner_id'] + proof = request.json['proof'] + difficulty = blockchain.last_block['difficulty'] + + # Check proof + check = Blockchain.validate_proof( + block = blockchain.last_block, + proof = proof, + difficulty=difficulty, + ) + + # Build transaction if pass + if check: + print(f'Valid proof found! Building Transaction for {miner_id}') + new_transaction = { + 'sender': 0, + 'receiver': miner_id, + 'amount': blockchain.award, + 'message': f"Valid proof ", + } + blockchain.add_transaction(new_transaction) + blockchain.new_block(proof=proof) + else: + new_transaction = {'amount': 0} + + response = { + 'miner_id': miner_id, + 'result': check, + 'transaction': new_transaction + } + return jsonify(response) + + +@app.route('transactions/new', methods=['POST']) +def transaction_new(): + if validate_transaction_request(request): + miner_id = request.json['miner_id'] + new_transaction = build_transaction(request) + blockchain.add_transaction(new_transaction) + + response = { + 'miner_id': miner_id, + 'transaction': new_transaction + } + return jsonify(response), 200 + + return 400 + + +def validate_transaction_request(request): + # Must have the following: sender, receiver, amount + for key in ['sender', 'receiver', 'amount']: + if key not in request.json.keys(): + return False + return True + + +def build_transaction(request): + miner_id = request.json['miner_id'] + return { + 'sender': 0, + 'receiver': miner_id, + 'amount': blockchain.award, + 'message': f"Stored in ", + } + + +# Run the program on port 5000 +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/client_mining_p/blockchain.py b/client_mining_p/blockchain.py index a0a26551..356e44b5 100644 --- a/client_mining_p/blockchain.py +++ b/client_mining_p/blockchain.py @@ -1,2 +1,227 @@ -# Paste your version of blockchain.py from the basic_block_gp -# folder here +import hashlib +import json +from time import time +from uuid import uuid4 +import random +from flask import Flask, jsonify, request + +from math import exp + + +class Blockchain(object): + def __init__(self): + self.chain = [] + self.current_transactions = [] + self.max_award = 1 + self.difficulty = 5 + + # Create the genesis block + self.new_block(proof=100) + + def new_block(self, proof): + """ + Create a new Block in the Blockchain + + A block should have: + * Index + * Timestamp + * List of current transactions + * The proof used to mine this block + * The hash of the previous block + + :param proof: The proof given by the Proof of Work algorithm + :param previous_hash: (Optional) Hash of previous Block + :return: New Block + """ + + block = { + 'index': len(self.chain) + 1, + 'timestamp': time(), + 'transactions': self.current_transactions, + 'proof': proof, + 'prev_hash': block_hash(self.last_block), + 'difficulty': self.difficulty, + } + + # Reset the current list of transactions + # Append the chain to the block + # Return the new block] + self.current_transactions = [] + self.chain.append(block) + + @property + def last_block(self): + if len(self.chain) > 0: + return self.chain[-1] + return 1 + + @property + def award(self): + return 1 / (1 - abs(self.max_award / (5 - exp(-.05*(self.last_block['index']-0))))) # 1 - logistic + + def add_transaction(self, transaction): + self.current_transactions.append(transaction) + + def proof_of_work(self): + """ + Simple Proof of Work Algorithm + Stringify the block and look for a proof. + Loop through possibilities, checking each one against `valid_proof` + in an effort to find a number that is a valid proof + :return: A valid proof for the provided block + """ + + valid_proof = False + guess = 0 + + while valid_proof == False: + valid_proof = self.validate_proof(self.last_block, guess, difficulty=self.difficulty) + guess += 1 + + if valid_proof: + return guess + else: + return None + + @staticmethod + def validate_proof(block, proof, difficulty): + """ + Validates the Proof: Does hash(block_string, proof) contain 3 + leading zeroes? Return true if the proof is valid + :param block_string: The stringified block to use to + check in combination with `proof` + :param proof: The value that when combined with the + stringified previous block results in a hash that has the + correct number of leading zeroes. + :return: True if the resulting hash is a valid proof, False otherwise + """ + + block_string = json.dumps(block, sort_keys=True) + + guess_hash = str(block_hash( + f'{block_string}{proof}' + )) + return '0'*difficulty == guess_hash[0:difficulty] + + + +def block_hash(block: dict): + """ + Creates a SHA-256 hash of a Block + + :param block": Block + "return": + """ + + # Use json.dumps to convert json into a string + # Use hashlib.sha256 to create a hash + # It requires a `bytes-like` object, which is what + # .encode() does. + # It converts the Python string into a byte string. + # We must make sure that the Dictionary is Ordered, + # or we'll have inconsistent hashes + + new_hash = hashlib.sha256( + json.dumps(block, sort_keys=True).encode()).hexdigest() + # print(len(new_hash), new_hash) + return new_hash + + + +# Instantiate our Node +app = Flask(__name__) + +# Generate a globally unique address for this node +node_identifier = str(uuid4()).replace('-', '') + +# Instantiate the Blockchain +blockchain = Blockchain() + + + +## Routes + +@app.route('/', methods=['GET']) +def home(): + response = 'Home Page - Blockchain practice' + return response + + +@app.route('/mine', methods=['GET']) +def mine(): + # Run the proof of work algorithm to get the next proof + + print('Begin Mining New Block') + proof = blockchain.proof_of_work() + + blockchain.new_block( + proof = proof + ) + + response = { + 'message': 'New Block Found', + 'index': blockchain.last_block['index'], + 'transactions': blockchain.last_block['transactions'], + 'proof': blockchain.last_block['proof'], + } + + return jsonify(response), 200 + + +@app.route('/chain', methods=['GET']) +def full_chain(): + response = { + 'block': blockchain.chain, + 'len': len(blockchain.chain) + } + return jsonify(response), 200 + + +@app.route('/last_block', methods=['GET']) +def last_block(): + response = { + 'last_block': blockchain.last_block, + 'difficulty': blockchain.last_block['difficulty'], + } + return jsonify(response), 200 + + +@app.route('/verify', methods=['POST']) +def verify(): + miner_id = request.json['miner_id'] + proof = request.json['proof'] + difficulty = blockchain.last_block['difficulty'] + + # Check proof + check = Blockchain.validate_proof( + block = blockchain.last_block, + proof = proof, + difficulty=difficulty, + ) + + # Build transaction if pass + if check: + print(f'Valid proof found! Building Transaction for {miner_id}') + new_transaction = { + 'sender': 0, + 'receiver': miner_id, + 'amount': blockchain.award, + 'message': f"Valid proof ", + } + blockchain.add_transaction(new_transaction) + blockchain.new_block(proof=proof) + else: + new_transaction = {'amount': 0} + + response = { + 'miner_id': miner_id, + 'result': check, + 'transaction': new_transaction + } + return jsonify(response) + + + +# Run the program on port 5000 +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/client_mining_p/miner.py b/client_mining_p/miner.py index 8e211b88..5fb7fe42 100644 --- a/client_mining_p/miner.py +++ b/client_mining_p/miner.py @@ -4,21 +4,67 @@ import sys import json +from multiprocessing import Process, Queue, Event -def proof_of_work(block): + +### Threading Setup ### +q = Queue() +processes = [] +num_workers = 8 + + +### Local Results Store ### +class NodeInfo(): + def __init__(self, miner_id=None, server_url=None): + self.miner_id = miner_id + self.server_url = server_url + + @property + def verification_url(self): + if self.server_url is not None: + return self.server_url + '/verify' + +class Results(): + def __init__(self, results=[]): + self.proof = Queue() + + def add_result(self, result): + if result: + self.proof.put(result) + + @property + def success(self): + return not self.proof.empty() + +node_info = NodeInfo() +results = Results() + + +def block_hash(block: dict): """ - Simple Proof of Work Algorithm - Stringify the block and look for a proof. - Loop through possibilities, checking each one against `valid_proof` - in an effort to find a number that is a valid proof - :return: A valid proof for the provided block + Creates a SHA-256 hash of a Block + + :param block": Block + "return": """ - pass + # Use json.dumps to convert json into a string + # Use hashlib.sha256 to create a hash + # It requires a `bytes-like` object, which is what + # .encode() does. + # It converts the Python string into a byte string. + # We must make sure that the Dictionary is Ordered, + # or we'll have inconsistent hashes -def valid_proof(block_string, proof): + new_hash = hashlib.sha256( + json.dumps(block, sort_keys=True).encode()).hexdigest() + # print(len(new_hash), new_hash) + return new_hash + + +def validate_proof(block, proof, difficulty): """ - Validates the Proof: Does hash(block_string, proof) contain 6 + Validates the Proof: Does hash(block_string, proof) contain 3 leading zeroes? Return true if the proof is valid :param block_string: The stringified block to use to check in combination with `proof` @@ -27,44 +73,159 @@ def valid_proof(block_string, proof): correct number of leading zeroes. :return: True if the resulting hash is a valid proof, False otherwise """ - pass + + block_string = json.dumps(block, sort_keys=True) + + guess_hash = str(block_hash( + f'{block_string}{proof}' + )) + return '0'*difficulty == guess_hash[0:difficulty] + + + +def proof_of_work(block, search_space:list, difficulty: int): + """ + Modified Proof of Work Algorithm + Stringify the block and look for a proof. + Loop through defined search_space of possibilities, checking each one against `valid_proof` + in an effort to find a number that is a valid proof + :return: A valid proof for the provided block + """ + + # Validate search_space + assert len(search_space) == 2, 'Search space only accepts [start_int, end_int]' + assert type(search_space[0]) == int, 'Must provide integer start' + assert type(search_space[1]) == int, 'Must provide integer end' + + valid_proof = False + guess = search_space[0] + + while guess < search_space[1]: + valid_proof = validate_proof( + block=block, + proof=guess, + difficulty=difficulty + ) + if valid_proof: + return guess + guess += 1 + + return None + + +def request_block(url): + """ Request last block from server with given URL """ + r = requests.get(url = url) + try: + data = r.json() + except: + raise + + return(data) + + +def check_proof(): + if results.success: + print('Validating \n') + check = requests.post(url=node_info.verification_url, json = { + 'miner_id': node_info.miner_id, + 'proof': results.proof.get(), + }) + return check + + +def mine(job): + print(f"Starting to mine{job['search_space']}") + proof = proof_of_work( + block=job['block'], + search_space=job['search_space'], + difficulty=job['difficulty'] + ) + if proof is not None: + results.add_result(proof) + return True + return False + + +def miner(pquit, foundit): + while not pquit.is_set(): + if q.empty(): + break + job = q.get() + if job is None: + break + if mine(job): + print('Found proof!') + check = check_proof() + print(check.json()) + foundit.set() + break + + +def create_jobs(block_info, max_search = 2**24, num_jobs=0): + segment = max_search/num_workers + jobs = [] + + # Dynamically size number of jobs based on max_search + if num_jobs == 0: + num_jobs = int(max_search/10000) + + for i in range(num_jobs): + search_space = [ + int(i*segment), int((i+1)*segment) + ] + job = { + 'block': block_info['last_block'], + 'search_space': search_space, + 'difficulty': block_info['difficulty'], + } + jobs.append(job) + + return jobs + if __name__ == '__main__': # What is the server address? IE `python3 miner.py https://server.com/api/` if len(sys.argv) > 1: - node = sys.argv[1] + node_info.server_url = sys.argv[1] else: - node = "http://localhost:5000" + node_info.server_url = "http://localhost:5000" # Load ID f = open("my_id.txt", "r") - id = f.read() - print("ID is", id) + node_info.miner_id = f.read() + print("ID is", node_info.miner_id) f.close() - # Run forever until interrupted - while True: - r = requests.get(url=node + "/last_block") - # Handle non-json response - try: - data = r.json() - except ValueError: - print("Error: Non-json response") - print("Response returned:") - print(r) - break + # Get last block + block_info = request_block(url=node_info.server_url + '/last_block') - # TODO: Get the block from `data` and use it to look for a new proof - # new_proof = ??? + # # Search for proof (single-threaded) + # proof = proof_of_work(block=block_info['last_block'], search_space=[0, 2**256], difficulty=block_info['difficulty']) + # print('Proof:', proof) - # When found, POST it to the server {"proof": new_proof, "id": id} - post_data = {"proof": new_proof, "id": id} + # Search for proof (multi-threaded) + jobs = create_jobs(block_info=block_info) + print(jobs[0], jobs[-1]) - r = requests.post(url=node + "/mine", json=post_data) - data = r.json() + # Load queue + for job in jobs: + q.put(job) + + # Start multiple processes + pquit = Event() + foundit = Event() + for i in range(num_workers): + p = Process(target=miner, args=(pquit, foundit)) + p.start() + processes.append(p) + + foundit.wait() + pquit.set() + + for p in processes: + p.terminate() + p.join() + print('All Done') - # TODO: If the server responds with a 'message' 'New Block Forged' - # add 1 to the number of coins mined and print it. Otherwise, - # print the message from the server. - pass diff --git a/client_mining_p/my_id.txt b/client_mining_p/my_id.txt index 757227a3..ac42b42b 100644 --- a/client_mining_p/my_id.txt +++ b/client_mining_p/my_id.txt @@ -1 +1 @@ -your-name-here +test-id \ No newline at end of file