def broadcaster(): # Give Receiver time to setup time.sleep(1) # Setup Broadcaster bcast = Broadcast() # Broadcast message bcast.broadcast("Broadcasting end")
class Blockchain: def __init__(self, port = 5000, miner = True, unitTests = False): """Init the blockchain. """ # Initialize the properties. self._master_chain = [] self._branch_list = [] self._last_hash = None self._pending_transactions = [] self._difficulty = 4 self._miner = miner #Block confirmation request self._blocks_to_confirm = [] self._block_to_mine = None self._confirm_block = False #Incoming Block confirmation flag self._block_added = False #Incoming block added flag #ip = get('https://api.ipify.org').text ip = "127.0.0.1" self._ip = "{}:{}".format(ip,port) if unitTests : self._add_genesis_block() self.broadcast = Broadcast(set(), self._ip) #Creating mining thread if self._miner: print("Create mining thread") mining_thread = threading.Thread(target = self.mine,daemon=True) mining_thread.start() def _add_genesis_block(self): """Adds the genesis block to your blockchain. """ self._master_chain.append(Block(0, [], time.time(), "0")) self._last_hash = self._master_chain[-1].compute_hash() print("Genesis block added, hash :",self._last_hash) def bootstrap(self, address): """The bootstrap address serves as the initial entry point of the bootstrapping procedure. It will contact the specified address, download the peerlist, and start the bootstrapping procedure. """ print("BOOSTRAPING -------------") if(address == self._get_ip()): # Initialize the chain with the Genesis block. self._add_genesis_block() return # Get the list of peer from bootstrap node try: result = send_to_one(address, "peers") except exceptions.RequestException: print("Unable to bootstrap (connection failed to bootstrap node)") return peers = result.json()["peers"] for peer in peers: self.add_node(peer) self.add_node(address) if(self._get_ip() in peers): peers.remove(self._get_ip()) # Get all the blocks from a non-corrupted node hashes = {} for peer in self.get_peers(): hashes[peer] = send_to_one(peer, "addNode", {"address" : self._get_ip()}) if hashes: address = get_address_best_hash(hashes) result = send_to_one(address, "blockchain") chain = result.json()["chain"] # Reconstruct the chain init_chain = [] for block in chain: block = json.loads(block) transaction = [] for t in block["_transactions"]: transaction.append(Transaction(t["key"], t["value"], t["origin"])) new_block = Block(block["_index"], transaction, block["_timestamp"], block["_previous_hash"], block["_nonce"]) print("Bootstrap BLOCK:",new_block.compute_hash()) init_chain.append(new_block) self._master_chain = init_chain self._last_hash = init_chain[-1].compute_hash() print("Bootstrap complete. Blockchain is now {} blocks long".format(len(self._master_chain))) # [print(block.__dict__) for block in self._master_chain] return def add_node(self, peer): """ Add a node to the network. """ self.broadcast.add_peer(peer) def _get_ip(self): """ Get ip address of a node. """ return self._ip def _add_block(self,new_block): """ Add a block to the blockchain if it is valid Apply longest chain rule Constraint: Discard block if the parent is more than 1 generation away from the master chain """ #Check block validity if not new_block.proof(self._difficulty): print("Block has incorrect proof") return False new_block_hash = new_block.compute_hash() #Direct successors of last block from master node #Discard block if the parent is more than 1 generation away if new_block._previous_hash == self._master_chain[-1].compute_hash(): self._branch_list.append([new_block]) print("Block ID {} hash {} added to BRANCH".format(new_block._index, new_block_hash)) return True createBranch = False for branch in self._branch_list: if createBranch: break #If parent of new_block is last block of the branch, #add it to the branch if branch[-1].compute_hash() == new_block._previous_hash: branch.append(new_block) createBranch = True print("Block ID {} hash {} added to BRANCH".format(new_block._index, new_block_hash)) break #Else, copy the branch and add the new block to the copy for k, block in enumerate(branch): if new_block._previous_hash == block.compute_hash(): new_branch = copy.deepcopy(branch[:k+1]) new_branch.append(new_block) self._branch_list.append(new_branch) print("Block ID {} hash {} added to NEW BRANCH".format(new_block._index, new_block_hash)) createBranch = True break max_len_branch = sorted(self._branch_list, key=len)[-1] max_len = len(sorted(self._branch_list, key=len)[-1]) #Longest chain rule : part of branch longer or equal to 2 blocks get added if max_len >= 2: #Add all but one element to the master chain self._last_hash = max_len_branch[-1].compute_hash() self._master_chain.extend(max_len_branch[:-1]) #Remove all but one element from the list of branches self._branch_list = [[max_len_branch[-1]]] for block in max_len_branch[:-1]: print("Block ID {} hash {} added to MASTER".format(block._index, block.compute_hash())) return True return createBranch def _proof_of_work(self): """ Implement the proof of work algorithm Also check for block confirmation request from another Node """ #Reset nonce self._block_to_mine._nonce = 0 #Get the real hash of the block computed_hash = self._block_to_mine.compute_hash() #Find the nonce that computes the right block hash while not computed_hash.startswith('0' * self._difficulty): if not self._confirm_block: self._block_to_mine._change_nonce() computed_hash = self._block_to_mine.compute_hash() if self._block_added: self._block_added = False #Discard currently mined block return False #Broadcast block to other nodes self.broadcast.broadcast("block",json.dumps(self._block_to_mine.__dict__, sort_keys=True, cls=TransactionEncoder)) print("Mined block hash",computed_hash) self._last_hash = computed_hash return True def get_blocks(self): """ Returns all blocks from the chain. """ return self._master_chain def get_last_master_hash(self): """Returns the hash of the last block. """ return self._master_chain[-1].compute_hash() def get_peers(self): """ Returns all peers of the newtork. """ return self.broadcast.get_peers() def difficulty(self): """Returns the difficulty level. """ return self._difficulty def add_transaction(self, transaction, broadcast = True): """Adds a transaction to your current list of transactions, and broadcasts it to your Blockchain network. NB : If the `mine` method is called, it will collect the current list of transactions, and attempt to mine a block with those. """ # print("Added transaction" ,transaction.__dict__) self._pending_transactions.append(transaction) if broadcast: self.broadcast.broadcast("transaction",json.dumps(transaction.__dict__,sort_keys=True)) return def confirm_block(self,foreign_block): """Pass a block to be confirmed by the blockchain. Parameters: ---------- foreign_block: Block object """ if self._miner : self._confirm_block = True print("Confirming an incoming block with hash ", foreign_block.compute_hash()) if self._add_block(foreign_block): self._block_added = True print("Block confirmed by other node") local_block_tr = self._block_to_mine.get_transactions() for tr in foreign_block.get_transactions(): # Remove the incoming block's transaction from the pool if tr in self._pending_transactions: print(tr.key, tr.value) self._pending_transactions.remove(tr) #Remove the incoming block's transaction from the locally mined block transaction list if tr in local_block_tr: local_block_tr.remove(tr) #The transactions that were not added to the chain get put back in the pool for tr in local_block_tr: if tr not in self._pending_transactions: self._pending_transactions.append(tr) self._confirm_block = False return True else: #Block is not valid, we continue mining # print("Resume mining...") #Reset block confirmation fields self._confirm_block = False return False print("Block confirmed by other node") else: self._pending_transactions = [] return self._add_block(foreign_block) def mine(self): """Implements the mining procedure. """ while(True): if not self._pending_transactions: time.sleep(1) #Wait before checking new transactions else: input_tr = copy.deepcopy(self._pending_transactions) nb_transactions = len(input_tr) self._block_to_mine = Block(index=random.randint(1, sys.maxsize), transactions=input_tr, timestamp=time.time(), previous_hash=self._last_hash) #Remove the transactions that were inserted into the block del self._pending_transactions[:nb_transactions] # print("Processed {} transaction(s) in this block, {} pending".format(nb_transactions, len(self._pending_transactions))) if self._proof_of_work(): self._add_block(self._block_to_mine) def is_valid(self): """Checks if the current state of the blockchain is valid, meaning, are the sequence of hashes, and the proofs of the blocks correct? """ chain = self._master_chain last_block = chain[-1] previous_hash = last_block._previous_hash print("Previous hash",previous_hash) it = -1 while previous_hash != "0": #Check if proof is valid and if previous hashes match if(previous_hash != chain[it-1].compute_hash() or not chain[it].proof(self._difficulty)): return False it = it - 1 previous_hash = chain[it]._previous_hash return True
class Node: def __init__(self, host): self.blockchain = [] self.current_id_count = 0 self.host = host self.wallet = Wallet() self.current_block = '' self.broadcast = Broadcast(host) # We need to store all the transactions not in a mined block # if we add them directly to the current block we have # a problem if while mining we get a transaction # We trigger the miner only when we have > BLOCK_CAPACITY pending transactions self.pending_transactions = [] # Transactions that we have not received all the inputs for self.orphan_transactions = {} # Here we store information for every node, as its id, its address (ip:port) # its public key, its balance and it's UTXOs self.ring = {} # The miner subprocess PID # If it is None the subprocess is not running self.miner_pid = None # The lock prevents multiple request threads to attempt # starting the miner self.miner_lock = threading.Lock() # Block receiver lock # We have to stop receiving blocks while running consensus # in order not to interfere with the process self.block_receiver_lock = threading.Lock() def create_new_block(self): self.current_block = Block(len(self.blockchain), self.blockchain[-1].hash, []) # Add this node to the ring, only the bootstrap node can add a node to the ring after checking his wallet and ip:port address # Bootstrap node informs all other nodes and gives the request node an id and 100 NBCs def register_node_to_ring(self, public_key, host, id=None): if id == None: self.ring[public_key] = Ring_Node(self.current_id_count, public_key, host) else: self.ring[public_key] = Ring_Node(id, public_key, host) self.current_id_count += 1 self.broadcast.add_peer(host) ''' Params: receiver: the address of the receiver to send coins to. Type <string>. Example: "id0" amount: the amount of NBC coins to send to receiver Type <int>. Example: 100 Return: create, sign and broadcast a new transaction of <amount> NBC to the address <receiver> ''' def create_transaction(self, receiver_address, amount): if not self.validate_user(receiver_address): raise Exception("I don't know the receiver!") if self.wallet.public_key == receiver_address: raise Exception("You can't send money to yourself!") if amount <= 0: raise Exception("You actually need to send some money") UTXOs = self.ring[self.wallet.public_key].UTXOs transaction_inputs = [] transaction_outputs = [] total = 0 for id, transaction in UTXOs.items(): if total < amount: transaction_inputs.append(id) total += transaction.amount if total < amount: raise Exception("You don't have enough money, unfortunately...") t = Transaction(self.wallet.public_key, receiver_address, amount, transaction_inputs) UTXO = Transaction_Output(receiver_address, amount, t.transaction_id) transaction_outputs.append(UTXO) if total > amount: UTXO = Transaction_Output(self.wallet.public_key, total - amount, t.transaction_id) transaction_outputs.append(UTXO) t.set_transaction_outputs(transaction_outputs) t.sign_transaction(self.wallet.private_key) if self.validate_transaction(t) == 'ok': self.commit_transaction(t) self.add_transaction_to_pending(t) self.broadcast_transaction(t) return t def update_balances(self): for ring_node in self.ring.values(): ring_node.update_balance() # If a transaction is valid, then commit it def commit_transaction(self, transaction): # Remove spent UTXOs for transaction_id in transaction.transaction_inputs: del self.ring[transaction.sender_address].UTXOs[transaction_id] # Add new UTXOs for UTXO in transaction.transaction_outputs: self.ring[UTXO.receiver_address].UTXOs[UTXO.transaction_id] = UTXO self.update_balances() def resolve_dependencies(self, transaction): for transaction, dependencies in self.orphan_transactions.items(): dependencies -= {transaction.transaction_id} if len(dependencies) == 0: del self.orphan_transactions[transaction] if self.validate_transaction(transaction): self.commit_transaction(transaction) self.add_transaction_to_pending(transaction) self.resolve_conflicts(transaction) # In the genesis transaction there is no sender address def commit_genesis_transaction(self, transaction): # Add new UTXOs for UTXO in transaction.transaction_outputs: self.ring[UTXO.receiver_address].UTXOs[UTXO.transaction_id] = UTXO self.update_balances() def add_transaction_to_pending(self, transaction): self.pending_transactions.append(transaction) if len(self.pending_transactions) >= BLOCK_CAPACITY: if self.request_miner_access(): self.start_miner() # If it is my block added to the chain we can delete the # pending transactions much faster! def add_block_to_chain(self, block, is_my_block=False): self.blockchain.append(block) self.create_new_block() transaction_ids = [ transaction.transaction_id for transaction in block.transactions ] self.update_pending_transactions(transaction_ids, is_my_block) # Initialization Functions def create_genesis_block(self): t = Transaction(0, self.wallet.public_key, NUMBER_OF_NODES * STARTING_NBC, []) utxo = Transaction_Output(self.wallet.public_key, NUMBER_OF_NODES * STARTING_NBC, t.transaction_id) t.set_transaction_outputs([utxo]) t.sign_transaction(self.wallet.private_key) self.commit_genesis_transaction(t) g = Block(0, 1, [t]) return g def initialize_network(self): g = self.create_genesis_block() g.setup_mined_block(0) self.broadcast_genesis_block(g) self.blockchain.append(g) self.create_new_block() for peer in self.ring.values(): if peer.public_key != self.wallet.public_key: t = self.create_transaction(peer.public_key, STARTING_NBC) # Validation Functions def validate_signature(self, transaction): public_key = transaction.sender_address public_key = RSA.importKey(public_key) verifier = PKCS1_v1_5.new(public_key) h = transaction.__hash__() return verifier.verify(h, transaction.signature) def validate_user(self, public_key): return public_key in self.ring def validate_transaction(self, transaction): try: if transaction in self.pending_transactions: raise Exception("Already have this") if not self.validate_user(transaction.sender_address): raise Exception("I don't know the sender!") if not self.validate_user(transaction.receiver_address): raise Exception("I don't know the receiver!") # If we allow a user to send transactions to himself he could flood the # network with these transactions and prevent the other transactions from # finding a place into blocks, thus adding a delay to the network if transaction.sender_address == transaction.receiver_address: raise Exception("You can't send money to yourself!") if transaction.amount <= 0: raise Exception("You actually need to send some money") if len(set(transaction.transaction_inputs)) != len( transaction.transaction_inputs): raise Exception("Duplicate transaction inputs") UTXOs = self.ring[transaction.sender_address].UTXOs for transaction_id in transaction.transaction_inputs: if not transaction_id in UTXOs: return 'orphan' in_total = sum( UTXOs[transaction_id].amount for transaction_id in transaction.transaction_inputs) if in_total < transaction.amount: raise Exception("Not enough money to commit the transaction") # conservation of money out_total = sum(UTXO.amount for UTXO in transaction.transaction_outputs) if in_total != out_total: raise Exception("Did you just give birth to money?") if not self.validate_signature(transaction): raise Exception("Invalid Signature") return 'ok' except Exception as e: print( f'Exception in transaction validation: \n{e.__class__.__name__}: {e}' ) return 'error' # TODO: Check the transactions in each block def validate_block(self, block, previous_block): try: if len(block.transactions) != BLOCK_CAPACITY: raise Exception( f'Invalid block capacity, {len(block.transactions)}') if block.hash != block.__hash__().hexdigest(): raise Exception("Invalid hash!") if not block.hash.startswith('0' * MINING_DIFFICULTY): raise Exception(f'Invalid nonce!, {block.hash}') if len( set(transaction.transaction_id for transaction in block.transactions)) != len( block.transactions): raise Exception("Duplicate transaction inputs") if block.previous_hash == previous_block.hash: # Everything seems ok return 'ok' else: for existent_block in reversed(self.blockchain[:-1]): if existent_block.hash == block.previous_hash: # The new block doesn't increase our chain since it is # a branch of an older block return 'redundant' # The block is valid but the chaining is faulty, # we probably have a fork return 'consensus' except Exception as e: print( f'Exception in block validation: \n{e.__class__.__name__}: {e}' ) return 'error' # When we adopt another chain we have to reconstruct the # state of the network as dicatetd by this chain # # Thus, starting from the genesis block and # appending each block from the chain we have to # recompute the UTXOs for every node def validate_chain(self, chain): # A blockchain with only the genesis block is valid if len(chain) == 1: return True previous_block = chain[0] for block in chain[1:]: if self.validate_block(block, previous_block) != 'ok': return False previous_block = block return True # Broadcast functions def broadcast_block(self, block): asyncio.run(self.broadcast.broadcast('receive_block', block, 'POST')) def broadcast_genesis_block(self, block): asyncio.run( self.broadcast.broadcast('receive_genesis_block', block, 'POST')) def broadcast_transaction(self, transaction): asyncio.run( self.broadcast.broadcast('receive_transaction', transaction, 'POST')) # Mining def start_miner(self): # If miner not already running # Add transactions to the block to start mining self.current_block.set_transactions( self.pending_transactions[:BLOCK_CAPACITY]) try: proc = Popen([ 'python3', 'miner.py', self.host, jsonpickle.encode({"data": self.current_block}) ]) self.miner_pid = proc.pid except Exception as e: print( f'Exception while starting the miner {e.__class__.__name__}: {e}' ) def stop_miner(self): # Kill the miner process, we lost the race try: os.kill(self.miner_pid, signal.SIGTERM) self.miner_pid = None except Exception as e: print( f'Exception in miner termination: \n{e.__class__.__name__}: {e}' ) def request_miner_access(self): with self.miner_lock: if self.miner_pid == None: self.miner_pid = 'placeholder' return True else: return False # Concensus functions # We have aquired a new block, either by the network or by us # Remove transactions that got into the block from pending def update_pending_transactions(self, transaction_ids, is_my_block): # Optimizing the deletion process for arbitrary transactions to # only check the transaction ids for equality if is_my_block: del self.pending_transactions[:BLOCK_CAPACITY] else: self.pending_transactions = list( filter( lambda transaction: transaction.transaction_id not in transaction_ids, self.pending_transactions)) # Asks each user for it's blockchain and keeps the longest valid one # # Optimization Idea: First ask every user for the length of it's blockchain # and then only ask the user with the longest blockchain # for the whole blockchain. If this blockchain is invalid greedily try # the next one etc... def resolve_conflicts(self): print("Running consensus") responses = asyncio.run( self.broadcast.broadcast('get_blockchain_length', {}, 'GET')) # Decode the response data into python objects blockchain_lengths = map(jsonpickle.decode, responses) sorted_blockchain_lengths = sorted(blockchain_lengths, key=lambda item: item['data'], reverse=True) for item in sorted_blockchain_lengths: # We are fine, we have the longest chain url = f'http://{item["host"]}/get_blockchain' headers = { 'Content-type': 'application/json', 'Accept': 'text/plain' } response = requests.get(url, headers) candidate_blockchain = jsonpickle.decode(response.json())['data'] if len(candidate_blockchain) <= len(self.blockchain): self.current_block.transactions = [] return if len(candidate_blockchain) < int(item['data']): print("You lied to me!") continue if self.valid_chain(candidate_blockchain): print("We found a valid chain to replace ours!") # Find the new veryfied transactions to remove from our pending i = 0 min_len = min(len(self.blockchain), len(candidate_blockchain)) while i < min_len and self.blockchain[ i] == candidate_blockchain[i]: i += 1 his_transactions_ids = [] for block in candidate_blockchain[i:]: for transaction in block.transactions: his_transactions_ids.append(transaction.transaction_id) self.blockchain = candidate_blockchain self.update_pending_transactions(his_transactions_ids, False) break # Executes only if we didn't break the for loop else: print("We didn't find a valid chain") self.create_new_block()