blockchain.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import hashlib
  2. import json
  3. from time import time
  4. from typing import Any, Dict, List, Optional
  5. from urllib.parse import urlparse
  6. from uuid import uuid4
  7. import requests
  8. from flask import Flask, jsonify, request
  9. class Blockchain:
  10. def __init__(self):
  11. self.current_transactions = []
  12. self.chain = []
  13. self.nodes = set()
  14. # Create the genesis block
  15. self.new_block(previous_hash='1', proof=100)
  16. def register_node(self, address: str) -> None:
  17. """
  18. Add a new node to the list of nodes
  19. :param address: Address of node. Eg. 'http://192.168.0.5:5000'
  20. """
  21. parsed_url = urlparse(address)
  22. self.nodes.add(parsed_url.netloc)
  23. def valid_chain(self, chain: List[Dict[str, Any]]) -> bool:
  24. """
  25. Determine if a given blockchain is valid
  26. :param chain: A blockchain
  27. :return: True if valid, False if not
  28. """
  29. last_block = chain[0]
  30. current_index = 1
  31. while current_index < len(chain):
  32. block = chain[current_index]
  33. print(f'{last_block}')
  34. print(f'{block}')
  35. print("\n-----------\n")
  36. # Check that the hash of the block is correct
  37. if block['previous_hash'] != self.hash(last_block):
  38. return False
  39. # Check that the Proof of Work is correct
  40. if not self.valid_proof(last_block['proof'], block['proof']):
  41. return False
  42. last_block = block
  43. current_index += 1
  44. return True
  45. def resolve_conflicts(self) -> bool:
  46. """
  47. This is our consensus algorithm, it resolves conflicts
  48. by replacing our chain with the longest one in the network.
  49. :return: True if our chain was replaced, False if not
  50. """
  51. neighbours = self.nodes
  52. new_chain = None
  53. # We're only looking for chains longer than ours
  54. max_length = len(self.chain)
  55. # Grab and verify the chains from all the nodes in our network
  56. for node in neighbours:
  57. response = requests.get(f'http://{node}/chain')
  58. if response.status_code == 200:
  59. length = response.json()['length']
  60. chain = response.json()['chain']
  61. # Check if the length is longer and the chain is valid
  62. if length > max_length and self.valid_chain(chain):
  63. max_length = length
  64. new_chain = chain
  65. # Replace our chain if we discovered a new, valid chain longer than ours
  66. if new_chain:
  67. self.chain = new_chain
  68. return True
  69. return False
  70. def new_block(self, proof: int, previous_hash: Optional[str]) -> Dict[str, Any]:
  71. """
  72. Create a new Block in the Blockchain
  73. :param proof: The proof given by the Proof of Work algorithm
  74. :param previous_hash: Hash of previous Block
  75. :return: New Block
  76. """
  77. block = {
  78. 'index': len(self.chain) + 1,
  79. 'timestamp': time(),
  80. 'transactions': self.current_transactions,
  81. 'proof': proof,
  82. 'previous_hash': previous_hash or self.hash(self.chain[-1]),
  83. }
  84. # Reset the current list of transactions
  85. self.current_transactions = []
  86. self.chain.append(block)
  87. print("{0}".format(block ))
  88. return block
  89. def new_transaction(self, sender: str, recipient: str, amount: int) -> int:
  90. """
  91. Creates a new transaction to go into the next mined Block
  92. :param sender: Address of the Sender
  93. :param recipient: Address of the Recipient
  94. :param amount: Amount
  95. :return: The index of the Block that will hold this transaction
  96. """
  97. self.current_transactions.append({
  98. 'sender': sender,
  99. 'recipient': recipient,
  100. 'amount': amount,
  101. })
  102. return self.last_block['index'] + 1
  103. @property
  104. def last_block(self) -> Dict[str: Any]:
  105. return self.chain[-1]
  106. @staticmethod
  107. def hash(block: Dict[str, Any]) -> str:
  108. """
  109. Creates a SHA-256 hash of a Block
  110. :param block: Block
  111. """
  112. # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
  113. block_string = json.dumps(block, sort_keys=True).encode()
  114. return hashlib.sha256(block_string).hexdigest()
  115. def proof_of_work(self, last_proof: int) -> int:
  116. """
  117. Simple Proof of Work Algorithm:
  118. - Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p'
  119. - p is the previous proof, and p' is the new proof
  120. """
  121. proof = 0
  122. while self.valid_proof(last_proof, proof) is False:
  123. proof += 1
  124. return proof
  125. @staticmethod
  126. def valid_proof(last_proof: int, proof: int) -> bool:
  127. """
  128. Validates the Proof
  129. :param last_proof: Previous Proof
  130. :param proof: Current Proof
  131. :return: True if correct, False if not.
  132. """
  133. guess = f'{last_proof}{proof}'.encode()
  134. guess_hash = hashlib.sha256(guess).hexdigest()
  135. return guess_hash[:4] == "0000"
  136. # Instantiate the Node
  137. app = Flask(__name__)
  138. # Generate a globally unique address for this node
  139. node_identifier = str(uuid4()).replace('-', '')
  140. # Instantiate the Blockchain
  141. blockchain = Blockchain()
  142. @app.route('/mine', methods=['GET'])
  143. def mine():
  144. # We run the proof of work algorithm to get the next proof...
  145. last_block = blockchain.last_block
  146. last_proof = last_block['proof']
  147. proof = blockchain.proof_of_work(last_proof)
  148. # We must receive a reward for finding the proof.
  149. # The sender is "0" to signify that this node has mined a new coin.
  150. blockchain.new_transaction(
  151. sender="0",
  152. recipient=node_identifier,
  153. amount=1,
  154. )
  155. # Forge the new Block by adding it to the chain
  156. block = blockchain.new_block(proof)
  157. response = {
  158. 'message': "New Block Forged",
  159. 'index': block['index'],
  160. 'transactions': block['transactions'],
  161. 'proof': block['proof'],
  162. 'previous_hash': block['previous_hash'],
  163. }
  164. return jsonify(response), 200
  165. @app.route('/transactions/new', methods=['POST'])
  166. def new_transaction():
  167. values = request.get_json()
  168. # Check that the required fields are in the POST'ed data
  169. required = ['sender', 'recipient', 'amount']
  170. if not all(k in values for k in required):
  171. return 'Missing values', 400
  172. # Create a new Transaction
  173. index = blockchain.new_transaction(values['sender'], values['recipient'], values['amount'])
  174. response = {'message': f'Transaction will be added to Block {index}'}
  175. return jsonify(response), 201
  176. @app.route('/chain', methods=['GET'])
  177. def full_chain():
  178. response = {
  179. 'chain': blockchain.chain,
  180. 'length': len(blockchain.chain),
  181. }
  182. return jsonify(response), 200
  183. @app.route('/nodes/register', methods=['POST'])
  184. def register_nodes():
  185. values = request.get_json()
  186. nodes = values.get('nodes')
  187. if nodes is None:
  188. return "Error: Please supply a valid list of nodes", 400
  189. for node in nodes:
  190. blockchain.register_node(node)
  191. response = {
  192. 'message': 'New nodes have been added',
  193. 'total_nodes': list(blockchain.nodes),
  194. }
  195. return jsonify(response), 201
  196. @app.route('/nodes/resolve', methods=['GET'])
  197. def consensus():
  198. replaced = blockchain.resolve_conflicts()
  199. if replaced:
  200. response = {
  201. 'message': 'Our chain was replaced',
  202. 'new_chain': blockchain.chain
  203. }
  204. else:
  205. response = {
  206. 'message': 'Our chain is authoritative',
  207. 'chain': blockchain.chain
  208. }
  209. return jsonify(response), 200
  210. if __name__ == '__main__':
  211. from argparse import ArgumentParser
  212. parser = ArgumentParser()
  213. parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on')
  214. args = parser.parse_args()
  215. port = args.port
  216. app.run(host='0.0.0.0', port=port)