├── .gitignore ├── README.md ├── blockchain_stuff.py ├── config_peers.py ├── generate_wallet.py ├── main.py ├── requirements.txt ├── templates └── make_transaction.html └── wallet.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | 4 | # Environments 5 | env/ 6 | venv/ 7 | ENV/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blockchain_implementation 2 | 3 | Simple blockchain implemetation with python! Though it won't transfer anything actual to a given address but it implements all the basic concepts of blockchain like 4 | proof of work, mining, consensus, generating and validating signature on transaction etc. The best way of understanding the blockchain is by building one! 5 | 6 | ## Instructions to run: 7 | First download the repo on your local machine using git clone: 8 | ``` 9 | git clone https://github.com/Adiprogrammer7/blockchain_implementation.git 10 | ``` 11 | Then navigate to repo and install dependencies: 12 | ``` 13 | pip install -r requirements.txt 14 | ``` 15 | In the ```config_peers.py``` add the peers you wish to have while running the network. By default we have some peers added to file like: 16 | ``` 17 | # store all url's running on the network here in string format, so that they can communicate 18 | # for example: 'http://127.0.0.1:5000/' 19 | peers = {'http://127.0.0.1:5000/', 'http://127.0.0.1:5001/'} 20 | ``` 21 | We will need a valid private key and public key pair to be able to sign the transaction and make a valid transaction. For that you can run ```generate_wallet.py``` file 22 | which will give you a valid private key and public key pair. 23 | 24 | Now to run, open two terminals and navigate both to repo. Use following commands to run instances of our application: 25 | For the first instance on port 5000: 26 | ``` 27 | set FLASK_APP=main.py 28 | flask run --port 5000 --debugger --reload 29 | ``` 30 | For the second instance on port 5001: 31 | ``` 32 | set FLASK_APP=main.py 33 | flask run --port 5001 --debugger --reload 34 | ``` 35 | Now we will have two instances running on http://localhost:5000 and http://localhost:5001. 36 |  37 | 38 | 39 | ## How it works: 40 | You can find private key and public key pair in ```wallet.txt```, which we generated earlier by running ```generate_wallet.py```. Using that you can make a valid transaction: 41 |  42 | 43 | That transaction now can be mined by ```/mine```. For example, http://localhost:5001/mine: 44 |  45 | 46 | Just like this you can make multiple transactions and mining will put them in a block which will be added to blockchain of each peer. 47 | You can view the entire blockchain on ```/chain``` view. For example, http://localhost:5000/chain: 48 |  49 | 50 | Also there are other views like ```/peers```, ```/consensus```, ```/unconfirmed_transactions```, etc. 51 | 52 | ### Open to any useful contribution :) -------------------------------------------------------------------------------- /blockchain_stuff.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | from datetime import datetime 3 | from ecdsa import SigningKey, SECP256k1, VerifyingKey 4 | import json 5 | import requests 6 | from flask import request 7 | 8 | class Block: 9 | def __init__(self, index, block_timestamp, transactions, prev_hash, proof_of_work=0): 10 | self.index = index 11 | self.block_timestamp = block_timestamp 12 | self.transactions = transactions 13 | self.prev_hash = prev_hash 14 | self.proof_of_work = proof_of_work 15 | 16 | # hash of complete block along with the proof of work using sha256. 17 | @property 18 | def hash(self): 19 | block_string = str(self.index) + str(self.block_timestamp) + str(self.transactions) + str(self.prev_hash) + str(self.proof_of_work) 20 | return sha256(block_string.encode()).hexdigest() 21 | 22 | 23 | class Blockchain: 24 | def __init__(self): 25 | #hash should start with this no. of zeros for proof_of_work 26 | self.zeros_difficulty = 2 27 | self.unconfirmed_transactions = [] 28 | self.chain = [] 29 | self.genesis_block() #genesis block will be created with initialization. 30 | 31 | # genesis block is the first block in a blockchain, its prev_hash would be 0 32 | def genesis_block(self): 33 | g_block = Block(0, str(0), [], 0, 0) 34 | self.chain.append(g_block) 35 | 36 | @property 37 | def last_block(self): 38 | if self.chain: 39 | return self.chain[-1] 40 | else: 41 | return False 42 | 43 | # the block should have valid prev_hash and valid proof_of_work to be added in blockchain. 44 | def add_block(self, block): 45 | if self.last_block and self.last_block.hash == block.prev_hash: 46 | if self.is_valid_proof(block): 47 | self.chain.append(block) 48 | return True 49 | return False 50 | 51 | def mine(self): 52 | '''this adds all unconfirmed transactions into a block and 53 | finds valid proof_of_work for that block in order to 54 | add it to blockchain''' 55 | if not self.unconfirmed_transactions: 56 | return False 57 | else: 58 | # to consider only valid transaction by verifying signature. 59 | for transaction in self.unconfirmed_transactions: 60 | if not self.is_valid_transaction(transaction): 61 | self.unconfirmed_transactions.remove(transaction) 62 | 63 | new_block = Block(index= self.last_block.index + 1, block_timestamp= str(datetime.now()), 64 | transactions= self.unconfirmed_transactions, prev_hash= self.last_block.hash) 65 | 66 | #Calulates proof_of_work 67 | while not new_block.hash.startswith('0' * self.zeros_difficulty): 68 | new_block.proof_of_work += 1 69 | 70 | self.add_block(new_block) 71 | self.unconfirmed_transactions = [] 72 | return new_block 73 | return True 74 | 75 | # to verify signature of transaction. 76 | def is_valid_transaction(self, transaction_dict): 77 | signature = transaction_dict['signature'] 78 | signature = bytes.fromhex(signature) #converting hex string back to bytes to be able to verify. 79 | public_key = transaction_dict['message']['from_addr'] 80 | public_key = VerifyingKey.from_string(bytes.fromhex(public_key), curve=SECP256k1) #getting public key in bytes from public key in hex string format. 81 | msg = json.dumps(transaction_dict['message']).encode() #converting msg back in bytes format. 82 | if public_key.verify(signature, msg): 83 | return True 84 | return False 85 | 86 | # checking if block hash(calculated with proof_of_work) starts with given no. of zeros_difficulty 87 | def is_valid_proof(self, block): 88 | if block.hash.startswith('0' * self.zeros_difficulty): 89 | return True 90 | 91 | # validates entire blockchain except genesis block. 92 | def is_valid_chain(self): 93 | for i in range(1, len(self.chain)): 94 | if self.is_valid_proof(self.chain[i]): 95 | if self.chain[i].prev_hash == self.chain[i-1].hash: 96 | return True 97 | return False 98 | 99 | # creates Blockchain obj. based on list received. 100 | def create_temp_chain(self, blockchain_list): 101 | temp_blockchain = Blockchain() 102 | for block in blockchain_list[1:]: #because genesis block would be already there. 103 | temp_block = Block(block['index'], block['block_timestamp'], block['transactions'], block['prev_hash'], block['proof_of_work']) 104 | temp_blockchain.add_block(temp_block) 105 | return temp_blockchain 106 | 107 | # to find the longest valid chain among all peers. 108 | def consensus(self, peers): 109 | longest_chain = self.chain 110 | for peer in peers: 111 | if peer != request.host_url: #to check others chain, not current url's chain. 112 | response = requests.get(peer+'chain') 113 | chain = response.json()['blockchain'] 114 | # creating temp Blockchain obj. from json response to be able to use Blockchain's methods. 115 | temp_blockchain = self.create_temp_chain(chain) 116 | if len(temp_blockchain.chain) > len(longest_chain) and temp_blockchain.is_valid_chain(): 117 | longest_chain = temp_blockchain.chain 118 | 119 | if longest_chain != self.chain: #means longest chain is not of current peer's. 120 | self.chain = longest_chain 121 | return True 122 | return False 123 | 124 | # announce block to other peers 125 | def announce_block(self, peers, block_obj): 126 | for peer in peers: 127 | if peer != request.host_url: 128 | response = requests.post(peer+'add_block', json= block_obj.__dict__) 129 | 130 | def generate_signature(self, readable_sk, msg): 131 | # msg is just a transaction dict. 132 | # converting from readable format to SigningKey object. 133 | sk = SigningKey.from_string(bytes.fromhex(readable_sk), curve=SECP256k1) 134 | msg = json.dumps(msg).encode() #to convert msg dict to bytes like object 135 | return sk.sign(msg) 136 | 137 | def announce_transaction(self, peers, transaction_dict): 138 | for peer in peers: 139 | response = requests.post(peer+'add_transaction', json= transaction_dict) 140 | -------------------------------------------------------------------------------- /config_peers.py: -------------------------------------------------------------------------------- 1 | # store all url's running on the network here in string format, so that they can communicate 2 | # for example: 'http://127.0.0.1:5000/' 3 | peers = {'http://127.0.0.1:5000/', 'http://127.0.0.1:5001/'} -------------------------------------------------------------------------------- /generate_wallet.py: -------------------------------------------------------------------------------- 1 | from ecdsa import SigningKey, SECP256k1 2 | 3 | def generate_wallet(): 4 | '''- private/secret/signing key(sk) will be used to generate signature which can be verified with 5 | the public key(pk) associated with that private key only. 6 | - to_string() will give bytes format then hex() will give hexcode in string format.''' 7 | 8 | sk = SigningKey.generate(curve= SECP256k1) #gives SigningKey object 9 | readable_sk = sk.to_string().hex() 10 | pk = sk.get_verifying_key() #public key corresponding to private key 11 | readable_pk = pk.to_string().hex() 12 | 13 | print('Private Key: {} \nPublic Key: {}'.format(readable_sk, readable_pk)) 14 | 15 | # saving in file 16 | with open('wallet.txt', 'w') as file: 17 | file.write("Private Key/Signing Key: {}\nPublic Key/Wallet Address: {}".format(readable_sk, readable_pk)) 18 | print("Your credentials saved in 'wallet.txt' file!") 19 | 20 | generate_wallet() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, url_for, render_template 2 | from blockchain_stuff import Block, Blockchain 3 | from config_peers import peers 4 | from datetime import datetime 5 | 6 | app = Flask(__name__) 7 | app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True #to enable pretty printing with jsonify. 8 | app.config['JSON_SORT_KEYS'] = False #to not sort while returning json response. 9 | 10 | blockchain = Blockchain() 11 | 12 | # form to create new transaction 13 | @app.route('/', methods= ['GET', 'POST']) 14 | def index(): 15 | if request.method == 'POST': 16 | return url_for('process_transaction') 17 | else: 18 | return render_template('make_transaction.html') 19 | 20 | # to view entire blockchain 21 | @app.route('/chain', methods=['GET']) 22 | def display_chain(): 23 | blocks = [] 24 | for each_block in blockchain.chain: 25 | blocks.append(each_block.__dict__) 26 | return jsonify({'blockchain': blocks, 27 | 'chain_length': len(blockchain.chain)}) 28 | 29 | # mine and announce block 30 | @app.route('/mine', methods=['GET']) 31 | def mining(): 32 | mined_block = blockchain.mine() 33 | if mined_block: 34 | blockchain.announce_block(peers, mined_block) 35 | return jsonify({'mined_block': mined_block.__dict__}) 36 | else: 37 | return "Nothing to mine!!" 38 | 39 | # to check if larger chain exits on network and if yes then switch to it. 40 | @app.route('/consensus', methods= ['GET']) 41 | def chain_conflict(): 42 | if blockchain.consensus(peers): 43 | return "Conflict detected, Switched to longest valid chain on the network!" 44 | else: 45 | return "We are good, no conflict in blockchain!" 46 | 47 | # announced block gets added in blockchain 48 | @app.route('/add_block', methods=['POST']) 49 | def add_block(): 50 | block_data = request.get_json() 51 | # clearing the blockchain.unconfirmed_transaction after block having those transaction is already mined. 52 | if block_data['transactions'] == blockchain.unconfirmed_transactions: 53 | blockchain.unconfirmed_transactions = [] 54 | 55 | block = Block(block_data['index'], block_data['block_timestamp'], block_data['transactions'], block_data['prev_hash'], block_data['proof_of_work']) 56 | added = blockchain.add_block(block) 57 | if not added: 58 | return "The block was discarded by the node", 400 59 | return "Block added to the chain", 201 60 | 61 | # receives form data from '/', generates signature and announces transaction. 62 | @app.route('/process_transaction', methods= ['POST']) 63 | def process_transaction(): 64 | readable_pk = request.form.get('pk') 65 | to_addr = request.form.get('to_addr') 66 | amount = request.form.get('amount') 67 | readable_sk = request.form.get('sk') 68 | timestamp = str(datetime.now()) 69 | msg = {'transaction_timestamp': timestamp, 'from_addr': readable_pk, 'to_addr': to_addr, 'amount': amount} 70 | signature = blockchain.generate_signature(readable_sk, msg) 71 | signature = signature.hex() #converting bytes type to hex string, so it will be accepted by json. 72 | blockchain.announce_transaction(peers, {'message': msg, 'signature': signature}) 73 | return "Transaction has been made!" 74 | 75 | # announced transaction gets added in unconfirmed_transactions list. 76 | @app.route('/add_transaction', methods= ['POST']) 77 | def add_transaction(): 78 | transaction_dict = request.get_json() 79 | blockchain.unconfirmed_transactions.append(transaction_dict) 80 | return "Transaction added to unconfirmed_transactions and is ready to be mined!" 81 | 82 | # to view peers(all host url's from 'config_peers.py') 83 | @app.route('/peers', methods= ["GET"]) 84 | def display_peers(): 85 | return jsonify({ 86 | 'peers': str(peers), 87 | 'count': len(peers) 88 | }) 89 | 90 | # to view unconfirmed_transactions list, which can be mined into a block 91 | @app.route('/unconfirmed_transactions', methods= ["GET"]) 92 | def display_unconfirmed_transactions(): 93 | return jsonify({ 94 | 'unconfirmed_transactions': blockchain.unconfirmed_transactions 95 | }) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | Flask==1.1.0 3 | ecdsa==0.16.1 4 | -------------------------------------------------------------------------------- /templates/make_transaction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |