def setUp(self): super(TransactionTest, self).setUp() self.transaction = Transaction(sender=self.wallet, recipient=self.recipient, amount=self.amount) self.transaction_info = self.transaction.info self.transaction_attrs = self.transaction_info.keys()
def test_transaction_is_valid_invalid_output(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True self.transaction.output[self.wallet.address] = self._generate_float() with self.assertRaises(TransactionError) as err: Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn('Invalid transaction amount', err.message)
def test_transaction_create_invalid_schema(self, mock_is_valid_schema): err_message = 'Validation error' mock_is_valid_schema.side_effect = Mock( side_effect=TransactionError(err_message)) with self.assertRaises(TransactionError) as err: Transaction.create(**self.transaction_info) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn(err_message, err.message)
def test_blockchain_is_valid_transaction_data_multiple_rewards(self): reward_transaction_1 = Transaction.reward_mining(Wallet()) reward_transaction_2 = Transaction.reward_mining(Wallet()) self.blockchain.add_block( [reward_transaction_1.info, reward_transaction_2.info]) err_message = 'Multiple mining rewards in the same block' with self.assertRaises(BlockchainError) as err: Blockchain.is_valid_transaction_data(self.blockchain.chain) self.assertIsInstance(err, BlockchainError) self.assertIn(err_message, err.message)
def test_transaction_is_valid_invalid_signature(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True self.transaction.output[self.wallet.address] = self._generate_float() self.transaction.input['signature'] = self.wallet.sign( self.transaction.output) with self.assertRaises(TransactionError) as err: Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn('Invalid signature verification', err.message)
def test_transactions_pool_get_transaction(self): wallet = Wallet() output = {wallet.address: 10} input = {'address': wallet.address} uuid = Transaction.generate_uuid() initial_transaction = Transaction(uuid=uuid, input=input, output=output) address = initial_transaction.input.get('address') self.assertNotIn(initial_transaction.uuid, self.transactions_pool.pool.keys()) self.transactions_pool.add_transaction(initial_transaction) transaction = self.transactions_pool.get_transaction(address) self.assertEqual(transaction.uuid, initial_transaction.uuid)
def is_valid_transaction_data(chain: list): """ Perform checks to enforce the consistnecy of transactions data in the chain blocks: Each transaction mush only appear once in the chain, there can only be one mining reward per block and each transaction must be valid. :param list chain: blockchain chain of blocks. :raise BlockchainError: on invalid transaction data. """ from src.client.models.transaction import Transaction from src.client.models.wallet import Wallet transaction_uuids = set() for index, block in enumerate(chain, start=0): has_reward = False for transaction_info in block.data: try: transaction = Transaction.create(**transaction_info) Transaction.is_valid(transaction) except TransactionError as err: message = f'Invalid transaction. {err.message}.' logger.error(f'[Blockchain] Validation error. {message}') raise BlockchainError(message) if transaction.uuid in transaction_uuids: message = f'Repetead transaction uuid found: {transaction.uuid}.' logger.error(f'[Blockchain] Validation error. {message}') raise BlockchainError(message) transaction_uuids.add(transaction.uuid) address = transaction.input.get('address') if address == MINING_REWARD_INPUT.get('address'): if has_reward: message = f'Multiple mining rewards in the same block: {block}.' logger.error( f'[Blockchain] Validation error. {message}') raise BlockchainError(message) has_reward = True else: historic_blockchain = Blockchain() historic_blockchain.chain = chain[:index] historic_balance = Wallet.get_balance( historic_blockchain, address) amount = transaction.input.get('amount') if historic_balance != amount: message = f'Address {address} historic balance inconsistency: {historic_balance} ({amount}).' logger.error( f'[Blockchain] Validation error. {message}') raise BlockchainError(message)
def test_transaction_generate_output(self): output = Transaction.generate_output(self.wallet, self.recipient, self.amount) self.assertIsInstance(output, dict) self.assertTrue(all([isinstance(key, str) for key in output.keys()])) self.assertTrue( all([isinstance(value, float) for value in output.values()]))
def test_transaction_generate_output_same_address(self): output = Transaction.generate_output(self.wallet, self.wallet.address, self.amount) self.assertIsInstance(output, dict) self.assertTrue(len(output.keys()), 1) self.assertEqual(output.get(self.wallet.address), self.wallet.balance) self.assertNotEqual(output.get(self.wallet.address), self.amount)
def _generate_transaction(self, sender: Wallet = None): recipient = uuid.uuid4().hex amount = random.randint(0, 100) sender = sender or Wallet() transaction = Transaction(sender=sender, recipient=recipient, amount=amount) return transaction
async def mine_block(): logger.info('[API] GET mine. Mining block.') transaction_reward = Transaction.reward_mining(router.wallet) data = router.transactions_pool.data data.append(transaction_reward.info) block = router.blockchain.add_block(data) logger.info(f'[API] GET mine. Block mined: {block}.') await router.p2p_server.broadcast_chain() router.transactions_pool.clear_pool(router.blockchain) return {'block': block}
async def test_p2p_server_broadcast_transaction(self, mock_deserialize): transaction = Transaction(sender=Wallet(), recipient='recipient', amount=100) mock_deserialize.return_value = transaction nodes = [self.p2p_server.uri] self.p2p_server.nodes.uris.add(nodes) await self.p2p_server.start() await self.p2p_server.broadcast_transaction(transaction) self.assertTrue(mock_deserialize.called) self.p2p_server.close()
def deserialize(cls, pool: list): """ Create a new transactions pool instance from the provided serialized pool. :param list pool: serialized pool of transactions. :return TransactionsPool: transactions pool created from provided stringified pool. """ transactions = list( map(lambda transaction: Transaction.deserialize(transaction), pool)) pool = {transaction.uuid: transaction for transaction in transactions} return cls(pool)
async def _send_transaction(self, socket: Socket, transaction: Transaction): """ Send message with new transaction data over a socket connection. :param Socket socket: outgoing socket client. :param Transaction transaction: transaction instance to send. """ message = { 'channel': CHANNELS.get('transact'), 'content': transaction.serialize() } await self._send(socket, message)
def test_transaction_create_valid_schema(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True transaction = Transaction.create(**self.transaction_info) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(transaction, Transaction) self.assertTrue( all([ attr in self.transaction.info.keys() for attr in self.transaction_info.keys() ])) self.assertTrue( all([ value in self.transaction.info.values() for value in self.transaction_info.values() ]))
async def transact(data: dict): logger.info('[API] POST transact. New transaction.') recipient = data.get('recipient') amount = data.get('amount') transaction = router.transactions_pool.get_transaction( router.wallet.address) if transaction: transaction.update(router.wallet, recipient, amount) logger.info( f'[API] POST transact. Transaction updated: {transaction}.') else: transaction = Transaction.create(sender=router.wallet, recipient=recipient, amount=amount) logger.info(f'[API] POST transact. Transaction made: {transaction}.') router.transactions_pool.add_transaction(transaction) await router.p2p_server.broadcast_transaction(transaction) return {'transaction': transaction}
async def _message_handler(self, socket: Socket): """ Handle the incoming socket client and process the message data. It exits normally when the connection is closed. :param Socket socket: incoming socket client. """ async for message in socket: data = parse(message) channel = data.get('channel') if channel == CHANNELS.get('node'): uri = data.get('content') info_msg = f'Uri listed. {uri}.' logger.info(f'[P2PServer] Node received. {info_msg}') self.add_uris(uri) await self._connect_socket(self._send_chain, uri) elif channel == CHANNELS.get('sync'): uris = data.get('content') self.add_uris(uris) info_msg = f'Total uris: {self.nodes.uris.array}.' logger.info( f'[P2PServer] Synchronization finished. {info_msg}') elif channel == CHANNELS.get('chain'): chain = data.get('content') logger.info(f'[P2PServer] Chain received. {chain}.') blockchain = Blockchain.deserialize(chain) self.blockchain.set_valid_chain(blockchain.chain) self.transactions_pool.clear_pool(self.blockchain) elif channel == CHANNELS.get('transact'): transaction_info = data.get('content') logger.info( f'[P2PServer] Transaction received. {transaction_info}.') transaction = Transaction.deserialize(transaction_info) self.transactions_pool.add_transaction(transaction) else: error_msg = f'Unknown channel received: {channel}.' logger.error(f'[P2PServer] Channel error. {error_msg}')
def test_transaction_reward_mining(self): transaction = Transaction.reward_mining(self.wallet) self.assertEqual(transaction.input, MINING_REWARD_INPUT) self.assertEqual(transaction.output[self.wallet.address], MINING_REWARD)
def test_transaction_is_valid_schema(self): Transaction.is_valid_schema(self.transaction_info)
def test_transaction_deserialize(self): serialized = self.transaction.serialize() self.assertIsInstance(Transaction.deserialize(serialized), Transaction)
def test_transaction_is_valid_schema_validation_error(self): self.transaction_info.pop('input') with self.assertRaises(TransactionError) as err: Transaction.is_valid_schema(self.transaction_info) self.assertIsInstance(err, TransactionError) self.assertIn('Validation error', err.message)
def test_transaction_is_valid(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called)
def test_transaction_is_valid_mining_reward(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True wallet = Wallet() transaction = Transaction.reward_mining(wallet) Transaction.is_valid(transaction) self.assertTrue(mock_is_valid_schema.called)
class TransactionTest(ClientMixin): def setUp(self): super(TransactionTest, self).setUp() self.transaction = Transaction(sender=self.wallet, recipient=self.recipient, amount=self.amount) self.transaction_info = self.transaction.info self.transaction_attrs = self.transaction_info.keys() def test_transaction_string_representation(self): self.assertTrue( all([ attr in str(self.transaction) for attr in self.transaction_attrs ])) def test_transaction_generate_uuid(self): id = Transaction.generate_uuid() self.assertTrue(re.match(r'^[0-9]{,39}$', str(id))) self.assertIsInstance(uuid.UUID(int=id), uuid.UUID) def test_transaction_generate_output(self): output = Transaction.generate_output(self.wallet, self.recipient, self.amount) self.assertIsInstance(output, dict) self.assertTrue(all([isinstance(key, str) for key in output.keys()])) self.assertTrue( all([isinstance(value, float) for value in output.values()])) def test_transaction_generate_output_same_address(self): output = Transaction.generate_output(self.wallet, self.wallet.address, self.amount) self.assertIsInstance(output, dict) self.assertTrue(len(output.keys()), 1) self.assertEqual(output.get(self.wallet.address), self.wallet.balance) self.assertNotEqual(output.get(self.wallet.address), self.amount) def test_transaction_generate_input(self): keys = ('timestamp', 'amount', 'address', 'public_key', 'signature') input = self.transaction.generate_input(self.wallet) self.assertIsInstance(input, dict) self.assertTrue(all([key in keys for key in input.keys()])) def test_transaction_info_property(self): attrs = self.transaction.__dict__ for attr in attrs: self.assertIn(attr, self.transaction_info) def test_transaction_serialize(self): serialized = self.transaction.serialize() self.assertIsInstance(serialized, str) self.assertTrue( all([attr in serialized for attr in self.transaction_attrs])) def test_transaction_deserialize(self): serialized = self.transaction.serialize() self.assertIsInstance(Transaction.deserialize(serialized), Transaction) @patch.object(Transaction, 'is_valid_schema') def test_transaction_create_valid_schema(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True transaction = Transaction.create(**self.transaction_info) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(transaction, Transaction) self.assertTrue( all([ attr in self.transaction.info.keys() for attr in self.transaction_info.keys() ])) self.assertTrue( all([ value in self.transaction.info.values() for value in self.transaction_info.values() ])) @patch.object(Transaction, 'is_valid_schema') def test_transaction_create_invalid_schema(self, mock_is_valid_schema): err_message = 'Validation error' mock_is_valid_schema.side_effect = Mock( side_effect=TransactionError(err_message)) with self.assertRaises(TransactionError) as err: Transaction.create(**self.transaction_info) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn(err_message, err.message) def test_transaction_is_valid_schema(self): Transaction.is_valid_schema(self.transaction_info) def test_transaction_is_valid_schema_validation_error(self): self.transaction_info.pop('input') with self.assertRaises(TransactionError) as err: Transaction.is_valid_schema(self.transaction_info) self.assertIsInstance(err, TransactionError) self.assertIn('Validation error', err.message) @patch.object(Transaction, 'is_valid_schema') def test_transaction_is_valid(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called) @patch.object(Transaction, 'is_valid_schema') def test_transaction_is_valid_mining_reward(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True wallet = Wallet() transaction = Transaction.reward_mining(wallet) Transaction.is_valid(transaction) self.assertTrue(mock_is_valid_schema.called) @patch.object(Transaction, 'is_valid_schema') def test_transaction_is_valid_invalid_output(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True self.transaction.output[self.wallet.address] = self._generate_float() with self.assertRaises(TransactionError) as err: Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn('Invalid transaction amount', err.message) @patch.object(Transaction, 'is_valid_schema') def test_transaction_is_valid_invalid_signature(self, mock_is_valid_schema): mock_is_valid_schema.return_value = True self.transaction.output[self.wallet.address] = self._generate_float() self.transaction.input['signature'] = self.wallet.sign( self.transaction.output) with self.assertRaises(TransactionError) as err: Transaction.is_valid(self.transaction) self.assertTrue(mock_is_valid_schema.called) self.assertIsInstance(err, TransactionError) self.assertIn('Invalid signature verification', err.message) def test_transaction_reward_mining(self): transaction = Transaction.reward_mining(self.wallet) self.assertEqual(transaction.input, MINING_REWARD_INPUT) self.assertEqual(transaction.output[self.wallet.address], MINING_REWARD) def test_transaction_update(self): recipient = 'recipient' balance = self.wallet.balance - self.amount amount = self._generate_float(ceil=balance) self.transaction.update(self.wallet, recipient, amount) self.assertEqual(self.transaction.output[recipient], amount) self.assertEqual(self.transaction.output[self.wallet.address], balance - amount) self.assertTrue( Wallet.verify(self.transaction.input['public_key'], self.transaction.input['signature'], self.transaction.output)) def test_transaction_update_invalid_amount(self): amount = self.wallet.balance err_message = 'Invalid amount' with self.assertRaises(TransactionError) as err: self.transaction.update(self.wallet, self.recipient, amount) self.assertIn(err_message, err.message)
def _generate_transaction(self): output = {} output[self.wallet.address] = MINING_REWARD input = MINING_REWARD_INPUT id = uuid.uuid4().int return Transaction.create(uuid=id, input=input, output=output)
def test_transaction_generate_uuid(self): id = Transaction.generate_uuid() self.assertTrue(re.match(r'^[0-9]{,39}$', str(id))) self.assertIsInstance(uuid.UUID(int=id), uuid.UUID)