def non_redundant_transaction(transaction, chain): """ Check to make sure a transaction isn't already on the chain. :param transaction: a pending transaction. :param chain: a blockchain to search for the transaction. :return: True if not redundant, else false. """ transaction_time = transaction['timestamp'] sender = transaction['sender'] recipient = transaction['recipient'] transaction_seen = False for block in chain: if block['transactions']: for other_transaction in block['transactions']: other_time = other_transaction['timestamp'] other_sender = other_transaction['sender'] other_recipient = other_transaction['recipient'] if transaction_time == other_time and sender == other_sender and recipient == other_recipient: # If we've already seen this transaction, it is redundant. If not, mark it as seen. if transaction_seen: # Transaction is redundant. log("TRANSACTION IS REDUNDANT") return False transaction_seen = True return True
def new_block(self, proof, previous_hash): """ Create a new block in the blockchain. Make sure not to add transaction to a block that already exist on the blockchain. Make sure that a would be transferer has sufficient balance to make a transfer. Check that the key used to request each transaction is correct. :param proof: The proof given by the proof of work algorithm :param previous_hash: Hash of previous block :return: A new block """ valid_transactions = [] for transaction in self.current_transactions: # Ensure proper vote signature and sufficient balance. if self.valid_transaction( transaction, self.chain) and self.valid_balance(transaction): self.update_wallets(transaction) valid_transactions.append(transaction) log("TRANSACTION ADDED TO NEW BLOCK.") block = { 'index': len(self.chain), 'timestamp': time(), 'transactions': valid_transactions, 'proof': proof, 'previous_hash': previous_hash or self.hash(self.chain[-1]) } # Reset the current list of transactions self.current_transactions = [] self.chain.append(block) log("NEW BLOCK ADDED TO CHAIN.") return block
def valid_transaction(self, transaction, chain): """ Checks the validity of a requested transaction by: Ensuring transfer amount is positive. Checking that the key used to request transaction is correct. :return: True if valid, else false """ # Ensure that transaction is not redundant: if not self.non_redundant_transaction(transaction, chain): return False # Ensure transfer amount is positive. amount = transaction['amount'] if amount < 0: return False sender = transaction['sender'] if sender == "0": # Original vote producer node. return True # If not an original vote producer vote, then the vote is being # transferred, e.g. being cast. # Ensure that the vote number is the correct one for this vote: vote_number = transaction['vote_number'] if len(chain) < vote_number: return False target_node_transactions = chain[vote_number]['transactions'] if len(target_node_transactions) != 1: # The initial vote nodes only have one transaction per block. return False referenced_voter = target_node_transactions[0]['recipient'] if sender != referenced_voter: return False # We now know that someone is trying to cast a vote that is indeed available # to be cast. Now we need to verify the signature that the voter provided in # order to ascertain that the transaction is actually valid. The signature is # the phrase "NO COLLUSION" encrypted with a private RSA key that corresponds # to the public key in the 'sender' field. voter_private_key = cryptfuncs.import_key(transaction['signature']) # A voters signature is the private key that is assigned to their vote. # Once used, it is no longer usable. Thus, it doesn't matter that users # are publicizing their private key, something one wouldn't want to do # in most contexts. voter_public_key = cryptfuncs.import_key(sender) verification_message = "NO COLLUSION" signature = cryptfuncs.sign(verification_message, voter_private_key) verification = cryptfuncs.verify(verification_message, signature, voter_public_key) log("{} TRANSACTION".format("GOOD" if verification else "BAD")) if not verification: log("THE FOLLOWING TRANSACTION WAS NOT VALID: {}".format( transaction)) return verification
def chain_transactions_valid(self, chain): """ Checks the validity of a the transactions in a chain. Make sure transaction is not redundant in its chain. Make sure transaction key is valid. :param chain: The chain to be checked. :return: True if valid, else false """ log("CHECKING VALIDITY OF TRANSACTIONS.") for block in chain: for transaction in block['transactions']: if not self.valid_transaction(transaction, chain): return False return True
def resolve_conflicts(self): """ Resolves conflicts by replacing our chain with the longest one in the network. :return: True if chain was replaced, False if not. """ log("RESOLVING CONFCLICTS.") neighbours = list(self.nodes) new_chain = None new_wallets = self.wallets new_chain_value = self.total_value # Only looking for longer chains: max_length = len(self.chain) # Grab and verify the chains from all the nodes in the network for node in neighbours: response = None for i in range(5): try: response = requests.get(f'http://{node}/chain/') if response: break except: if i == 4: # Remove unresponsive nodes. self.nodes.discard(node) continue if response: if response.status_code == 200: length = response.json()['length'] chain = response.json()['chain'] # Check if the length is longer and the chain is valid log("CHECKING CHAIN.") if length > max_length and self.valid_chain(chain): log("CHAIN IS VALID. CHECKING BALANCES.") valid_wallets, new_wallets, new_chain_value = self.valid_wallets( chain) # If all blocks have correct hashes, and all blocks have valid # transactions, and the new chain leads to valid wallets. if valid_wallets: max_length = length new_chain = chain # Replace this node's chain if a new, valid, longer chain is discovered: if new_chain: log("REPLACING THIS NODE'S CHAIN WITH NEW ONE.") self.chain = new_chain self.wallets = new_wallets self.total_value = new_chain_value return True log("KEEPING CURRENT CHAIN.") return False
def valid_chain(self, chain): """ Determine if a given blockchain is valid. :param chain: A blockchain :return: True if valid, False if not """ last_block = chain[0] for current_index in range(1, len(chain)): block = chain[current_index] # Check that the hash of the block is correct last_block_hash = self.hash(last_block) if block['previous_hash'] != last_block_hash: return False # Check that the proof of work is correct if not self.valid_proof(last_block['proof'], block['proof'], last_block_hash): return False last_block = block # Return true only if all transactions on the chain are valid: log("CHAIN HASHES ARE CORRECT. CHECKING FOR VALID TRANSACTIONS.") return self.chain_transactions_valid(chain)
def cache(func, *args): """ Put expensive calculatinos in cache on file that can be retrieved even after program has terminated @param: obj - object to be cached value - name of object @return: return value args - optional arguments given to the object # >>> a = binary_search(1, 12, 1) # >>> cache_put(a, [1, 12, 1]) >>> cache(binary_search, 1, 12, 1) #doctest: +NORMALIZE_WHITESPACE 3 >>> cache(binary_search, 2, 12, 1) #doctest: +SKIP 3 """ global CACHE_HIT _initialize() # Hash is function name + values cache_index = hash(str(func.func_name) + str(list(args))) cache_name = os.path.join(SIMPLECACHE_DIR, str(cache_index)) # If we find file, load it if os.path.exists(cache_name): log("cache hit") with open(cache_name, "rb") as fh: r = pickle.load(fh) CACHE_HIT = True return r # Object doesn't exist, put it in cache else: r = func(*args) with open(cache_name, "wb") as fh: log("cache miss") pickle.dump(r, fh, pickle.HIGHEST_PROTOCOL) CACHE_HIT = False return r
def cache(func, *args): """ Put expensive calculatinos in cache on file that can be retrieved even after program has terminated @param: obj - object to be cached value - name of object @return: return value args - optional arguments given to the object # >>> a = binary_search(1, 12, 1) # >>> cache_put(a, [1, 12, 1]) >>> cache(binary_search, 1, 12, 1) #doctest: +NORMALIZE_WHITESPACE 3 >>> cache(binary_search, 2, 12, 1) #doctest: +SKIP 3 """ global CACHE_HIT _initialize() # Hash is function name + values cache_index = hash(str(func.func_name) + str(list(args))) cache_name = os.path.join(SIMPLECACHE_DIR, str(cache_index)) # If we find file, load it if (os.path.exists(cache_name)): log('cache hit') with open(cache_name, "rb") as fh: r = pickle.load(fh) CACHE_HIT = True return r # Object doesn't exist, put it in cache else: r = func(*args) with open(cache_name, "wb") as fh: log('cache miss') pickle.dump(r, fh, pickle.HIGHEST_PROTOCOL) CACHE_HIT = False return r
def valid_balance(self, transaction): """ Checks to see if a sender has sufficient balance (a vote left to cast) in order to make a transaction. :param transaction: A transaction with a sender, reciever, and amount. :return: True if balance is sufficient, else false. """ log("CHECKING BALANCE.") amount = transaction['amount'] sender = transaction['sender'] if sender == "0" and not self.lock: # "0" sender, is the original source, and is allowed return True if sender in self.wallets: log("SENDER FOUND IN WALLET.") if self.wallets[sender] >= amount: log("BALANCE SUFFICIENT.") return True log("{} DOES NOT HAVE A SUFFICIENT BALANCE OR DOES NOT EXIST.".format( sender)) return False