def test_merkle_happy_path(): w1 = Wallet(test=True) w2 = Wallet(test=True) transactions = [ SignedRawTransaction( details=Details( sender=w1.address, recipient=w2.address, amount=4.5, nonce=0, timestamp=datetime.utcnow(), public_key=w1.public_key.hex(), ), signature="sig", ), SignedRawTransaction( details=Details( sender=w2.address, recipient=w1.address, amount=1.87, nonce=0, timestamp=datetime.utcnow(), public_key=w2.public_key.hex(), ), signature="sig", ), ] m = convert_to_merkle(transactions) assert m.is_ready print(m.get_merkle_root()) print(m.get_proof(0)) SignedRawTransaction.ParseFromHex(m.get_proof(0)[0]["right"]) print(m.get_proof(1)) SignedRawTransaction.ParseFromHex(m.get_proof(1)[0]["left"])
def sign_transaction(self, details: Details) -> SignedRawTransaction: """ Sign a transaction and return the signature A signature is generated using the contents of the rest of the transaction. This means that the signature will always be able to be decoded and will match the transaction. """ logger.info("Signing transaction") if self.private_key is None: message = "Unable to sign transaction without a private key" logger.error(message) raise ValueError(message) d = details.SerializeToString() signature = self.private_key.sign(d) expected_nonce = self.get_nonce() if details.nonce != expected_nonce: raise InvalidNonceError( details.sender, details.nonce, expected_nonce, "The transaction nonce must match the stored nonce before signing the transaction", ) self.save_new_nonce(expected_nonce) logger.debug("Transaction signed successfully") return SignedRawTransaction(details=details, signature=signature.hex())
def test_transaction_fails_validation(): w = Wallet(test=True) w2 = Wallet(test=True) timestamp = datetime.utcfromtimestamp(0) d = Details( sender=w.address, recipient=w2.address, amount=4.5, nonce=0, timestamp=timestamp, public_key=w.public_key.hex(), ) t = w.sign_transaction(d) t_hex = t.SerializeToHex() parsed_t = SignedRawTransaction.ParseFromHex(t_hex) assert parsed_t == t assert w.verify_transaction(t, return_test_nonce) t.details.amount = 2.5 try: assert w.verify_transaction(t, return_test_nonce) raise Exception("Expected to fail but did not") except ecdsa.keys.BadSignatureError: assert True
def __broadcast_transaction(self, transaction: SignedRawTransaction, type_: str) -> None: """ Broadcast the current transaction to all nodes on the network that this node is aware of. This ensures synchronicity across all nodes on the network. """ for node in self.nodes: url = f"{node}/broadcast-transaction" try: logging.debug("Broadcasting new transaction %s to %s", transaction, url) response = requests.post( url, json={ "transaction": transaction.SerializeToHex(), "type": type_ }, ) if response.status_code == 400 or response.status_code == 500: logger.error("Transaction declined, needs resolving: %s", response.json()) except requests.exceptions.ConnectionError: continue
def hash_transaction(transaction: SignedRawTransaction) -> str: """ First, convert the transaction to byte array Then hash the transaction using SHA256 """ hashable_transaction = transaction.SerializeToString() return Verification.hash_bytes_256(hashable_transaction)
def new_transaction(): # pylint: disable=unused-variable values = request.get_json() # Check for required fields required = ["transaction"] if not values or not all(k in values for k in required): return "Missing values", 400 details = values["transaction"]["details"] # Create a new Transaction index = blockchain.add_transaction( SignedRawTransaction( details=Details( sender=details["sender"], recipient=details["recipient"], amount=details["amount"], nonce=details["nonce"], timestamp=details["timestamp"], public_key=details["public_key"], ), signature=values["transaction"]["signature"], ), "open", ) response = {"message": f"Transaction will be added to Block {index}"} return jsonify(response), 201
def test_correct_nonce(): timestamp = datetime.utcfromtimestamp(0) block_one = Block( index=0, block_hash="", size=0, header=Header( timestamp=timestamp, transaction_merkle_root="", nonce=100, previous_hash="", difficulty=4, version=1, ), transaction_count=0, transactions=[], ) previous_hash = Verification.hash_block_header(block_one.header) open_transactions = [ SignedRawTransaction( details=Details( sender="test2", recipient="test", amount=2.5, nonce=0, timestamp=timestamp, public_key="pub_key", ), signature="sig", ) ] block_header = Header( version=1, difficulty=4, timestamp=datetime.utcfromtimestamp(1), transaction_merkle_root=get_merkle_root(open_transactions), previous_hash=previous_hash, nonce=0, ) block_header = Verification.proof_of_work(block_header) block_two = Block( index=1, block_hash="", size=0, header=block_header, transaction_count=len(open_transactions), transactions=[ Verification.hash_transaction(t) for t in open_transactions ], ) assert Verification.valid_nonce(block_two.header)
def broadcast_transaction(): # pylint: disable=unused-variable """ Broadcasts a new transaction to all the nodes Returns a status message Methods ----- POST Parameters ----- transaction : SignedRawTransaction as hex Returns application/json ----- Return code : 201, 400, 500 Response : message : str transaction : optional Transaction as Dict """ values = request.get_json() if not values: response = {"message": "No data found."} return jsonify(response), 400 required = ["transaction", "type"] if not all(key in values for key in required): response = {"message": "Some data is missing."} return jsonify(response), 400 t = SignedRawTransaction.ParseFromHex(values["transaction"]) try: if values["type"] == "mining" or values["type"] == "confirmed": tx = FinalTransaction( transaction_hash=Verification.hash_transaction(t), transaction_id=Verification.hash_transaction(t), signed_transaction=t, ) FinalTransaction.SaveTransaction(blockchain.data_location, tx, values["type"]) response = { "message": f"Successfully saved {values['type']} transaction.", "transaction": values["transaction"], } return jsonify(response), 201 block_index = blockchain.add_transaction(t, is_receiving=True) response = { "message": "Successfully added transaction.", "transaction": values["transaction"], "block": block_index, } return jsonify(response), 201 except ValueError as e: response = { "message": "Creating a transaction failed.", "error": str(e) } return jsonify(response), 500
def test_transaction_to_protobuf_and_back(): timestamp = datetime.utcfromtimestamp(0) t = SignedRawTransaction( details=Details( sender="test", recipient="test2", amount=4.5, nonce=0, timestamp=timestamp, public_key="pub_key", ), signature="sig", ) t_hex = t.SerializeToHex() assert ( t_hex == "0a230a04746573741205746573743219000000000000124020002a0032077075625f6b65791203736967" ) parsed_t = SignedRawTransaction.ParseFromHex(t_hex) assert parsed_t == t
def test_transaction_with_signature_to_protobuf_and_back(): w = Wallet(test=True) w2 = Wallet(test=True) timestamp = datetime.utcfromtimestamp(0) d = Details( sender=w.address, recipient=w2.address, amount=4.5, nonce=0, timestamp=timestamp, public_key=w.public_key.hex(), ) t = w.sign_transaction(d) t_hex = t.SerializeToHex() parsed_t = SignedRawTransaction.ParseFromHex(t_hex) assert parsed_t == t assert w.verify_transaction(t, return_test_nonce)
def test_block_hash_mutliple_transaction_field_order_doesnt_matter(): timestamp = datetime.utcfromtimestamp(0) transactions = [ SignedRawTransaction( details=Details( sender="test", recipient="test2", amount=5.0, nonce=0, timestamp=timestamp, public_key="pub_key", ), signature="sig", ), SignedRawTransaction( details=Details( sender="test2", recipient="test", amount=2.5, nonce=0, timestamp=timestamp, public_key="pub_key", ), signature="sig", ), ] tx_merkle_root = get_merkle_root(transactions) block = Block( index=0, block_hash="", size=0, header=Header( timestamp=timestamp, transaction_merkle_root=tx_merkle_root, nonce=100, previous_hash="", difficulty=4, version=1, ), transaction_count=2, transactions=[Verification.hash_transaction(t) for t in transactions], ) first_hash = Verification.hash_block_header(block.header) block = Block( index=0, block_hash="", size=0, header=Header( timestamp=timestamp, transaction_merkle_root=tx_merkle_root, nonce=100, previous_hash="", difficulty=4, version=1, ), transaction_count=2, transactions=[Verification.hash_transaction(t) for t in transactions], ) second_hash = Verification.hash_block_header(block.header) assert first_hash == second_hash
def test_block_to_protobuf_and_back_with_transactions(): timestamp = datetime.utcfromtimestamp(0) transactions = [ FinalTransaction( transaction_hash="tx_hash_1", transaction_id="tx_hash_1", signed_transaction=SignedRawTransaction( details=Details( sender="test", recipient="test2", amount=4.5, nonce=0, timestamp=timestamp, public_key="pub_key", ), signature="sig", ), ) ] header = Header( version=1, previous_hash="", timestamp=timestamp, transaction_merkle_root=get_merkle_root( [t.signed_transaction for t in transactions]), difficulty=4, nonce=100, ) block = Block( index=0, block_hash="", size=0, header=header, transaction_count=len(transactions), transactions=[t.transaction_hash for t in transactions], ) p_block = block.SerializeToHex() assert ( p_block == "080010001a002260080112001a543061323330613034373436353733373431323035373436353733373433323139303030303030303030303030313234303230303032613030333230373730373536323566366236353739313230333733363936372200280430642801320974785f686173685f31" # noqa: E501 ) og_block = Block.ParseFromHex(p_block) assert og_block == Block( index=0, block_hash="", size=0, header=Header( version=1, previous_hash="", timestamp=timestamp, transaction_merkle_root=get_merkle_root( [t.signed_transaction for t in transactions]), difficulty=4, nonce=100, ), transaction_count=len(transactions), transactions=[t.transaction_hash for t in transactions], )
def mine_block( self, address: Optional[str] = None, difficulty: Optional[int] = None, version: Optional[int] = None, ) -> Optional[Block]: """ The current node runs the mining protocol, and depending on the difficulty, this could take a lot of processing power. Once the nonce is discovered, or "mined", the reward transaction is created. Then all of the open transactions are validated and verified, ensuring that the senders in all of the transactions have enough coin to conduct the transaction. Once the transactions are validated, the reward block is added to the list of open transactions. This is because Mining transactions do not need to be validated since they are created by the node itself. The block is then added directy to the node's chain and the open_transactions is cleared and ready for a new block to be mined Finally, the new block is broadcasted to all connected nodes. """ if not address: address = self.address if not address: return None difficulty = difficulty if difficulty is not None else self.difficulty version = version if version is not None else self.version last_block = self.last_block transaction_merkle_root = get_merkle_root( [tx.signed_transaction for tx in self.get_open_transactions]) previous_hash = Verification.hash_block_header(last_block.header) block_header = Header( version=version, difficulty=difficulty, timestamp=datetime.utcnow(), transaction_merkle_root=transaction_merkle_root, previous_hash=previous_hash, nonce=0, ) # We run the PoW algorithm to get the next nonce and return an updated block_header block_header = Verification.proof_of_work(block_header) # Create the transaction that will be rewarded to the miners for their work # The sender is "0" or "Mining" to signify that this node has mined a new coin. reward_signed = SignedRawTransaction( details=Details( sender="0", recipient=address, nonce=0, amount=MINING_REWARD, timestamp=datetime.utcnow(), public_key="coinbase", ), signature="coinbase", ) reward_transaction = FinalTransaction( transaction_hash=Verification.hash_transaction(reward_signed), transaction_id=Verification.hash_transaction(reward_signed), signed_transaction=reward_signed, ) # Copy transactions instead of manipulating the original open_transactions list # This ensures that if for some reason the mining should fail, # we don't have the reward transaction stored in the pending transactions copied_open_transactions = self.get_open_transactions for tx in copied_open_transactions: if not Wallet.verify_transaction(tx.signed_transaction, self.get_last_tx_nonce, exclude_from_open=True): return None FinalTransaction.SaveTransaction(self.data_location, reward_transaction, "mining") self.__broadcast_transaction(reward_transaction.signed_transaction, "mining") for t in copied_open_transactions: self.__broadcast_transaction(t.signed_transaction, "confirmed") copied_open_transactions.append(reward_transaction) block = Block( index=self.next_index, header=block_header, block_hash=Verification.hash_block_header(block_header), size=len(str(block_header)), transaction_count=len(copied_open_transactions), transactions=[ t.transaction_hash for t in copied_open_transactions ], ) # Add the block to the node's chain self.add_block_to_chain(block) # Reset the open list of transactions logger.info("Moving open transaction to confirmed storage at %s", self.data_location) FinalTransaction.MoveOpenTransactions(self.data_location) self.__open_transactions = [] self.save_data() self.__broadcast_block(block) return block