import logging import unittest from unittest.mock import patch from aucoin import dsa, util from aucoin.block import Block from aucoin.blockchain import Blockchain from aucoin.database import session_scope from aucoin.exceptions import InvalidTransactionException, OrphanException from aucoin.mempool import Mempool from aucoin.network import Network from aucoin.transactions import Transaction, Input, Output, CoinbaseTransaction from aucoin.validation import Validator from aucoin.wallet import Wallet, public_bytes from tests import helpers from tests.helpers import mine logging.basicConfig(level=logging.DEBUG) class TestAddTransaction(unittest.TestCase): def setUp(self): self.blockchain = Blockchain(reset=True) self.mempool = Mempool() self.wallet = Wallet(self.blockchain, self.mempool) self.network = Network(self.blockchain, self.mempool, max_peers=0) self.validator = Validator(helpers.Core(), self.blockchain, self.mempool, self.network) # add a block with an unspent transaction output we can reference with session_scope() as session: genesis = self.blockchain.genesis_block(session) address = self.wallet.new_address() self.private_key, self.public_key = self.wallet.keys[address] self.block = Block( hash_prev_block=genesis.hash, target=genesis.target, public_key=public_bytes(self.public_key), transactions=[ CoinbaseTransaction( address=address, value=100, block_height=1 ) ] ) self.validator.add_block(mine(self.block, self.private_key)) # the transaction which we will be modifying to provoke validation exceptions self.transaction = Transaction( inputs=[ Input( prev_tx_hash=self.block.transactions[0].hash, txout_index=0 ) ], outputs=[ Output( value=50, address=b"some_address" ) ] ) self.wallet.sign(self.transaction, session) def test_valid(self): self.validator.add_transaction(self.transaction) def test_reject_invalid_syntax(self): self.transaction.inputs = [] self.assertRaisesRegex(InvalidTransactionException, "Input list is empty", self.validator.add_transaction, self.transaction) def test_reject_coinbase_transaction(self): coinbase_transaction = CoinbaseTransaction(b"some_address") self.assertRaisesRegex(InvalidTransactionException, "Standalone transaction cannot be coinbase", self.validator.add_transaction, coinbase_transaction) def test_reject_duplicate_in_blockchain(self): with session_scope() as session: block = Block( hash_prev_block=self.block.hash, target=self.block.target, transactions=[ CoinbaseTransaction( address=self.wallet.new_address(), value=100 ), self.transaction ] ) self.blockchain.add(block, session, main_branch=True) self.assertRaisesRegex(InvalidTransactionException, "Transaction already exists in blockchain's main branch", self.validator.add_transaction, self.transaction) def test_reject_duplicate_mempool(self): self.mempool[self.transaction.hash] = self.transaction self.assertRaisesRegex(InvalidTransactionException, "Transaction already exists in mempool", self.validator.add_transaction, self.transaction) def test_reject_mempool_conflict(self): # a transaction that spends the same input as self.transaction: transaction = Transaction( inputs=[ Input( prev_tx_hash=self.block.transactions[0].hash, txout_index=0 ) ], outputs=[ Output( value=100, address=b"another_address" ) ] ) self.assertNotEqual(transaction.hash, self.transaction.hash) self.assertEqual((self.transaction.inputs[0].prev_tx_hash, self.transaction.inputs[0].txout_index), (transaction.inputs[0].prev_tx_hash, transaction.inputs[0].txout_index)) self.mempool[transaction.hash] = transaction self.assertRaisesRegex(InvalidTransactionException, "Transaction conflicts with mempool: one or more input's referenced output is spent by another transaction in the mempool", self.validator.add_transaction, self.transaction) def test_reject_orphan(self): input = Input( prev_tx_hash=b"nonexistent_tx", txout_index=123 ) self.transaction.inputs.append(input) self.assertRaises(OrphanException, self.validator.add_transaction, self.transaction) def test_reject_spent(self): address = self.wallet.new_address() private_key, public_key = self.wallet.keys[address] block = Block( hash_prev_block=self.block.hash, target=self.block.target, public_key=public_bytes(public_key), transactions=[ CoinbaseTransaction( address=address, value=100, block_height=2 ) ] + [self.transaction] ) self.validator.add_block(mine(block, private_key)) # a transaction that spends the same input as self.transaction: transaction = Transaction( inputs=[ Input( prev_tx_hash=self.block.transactions[0].hash, txout_index=0 ) ], outputs=[ Output( value=20, address=b"other_address" ) ] ) self.assertRaisesRegex(InvalidTransactionException, "Referenced output already spent \(not in blockchain/mempool UTXO\) for one or more inputs", self.validator.add_transaction, transaction) def test_reject_negative_fee(self): output = Output( value=10000, address=b"doesnt_matter" ) self.transaction.outputs.append(output) self.assertRaisesRegex(InvalidTransactionException, "Sum of input values < sum of output values \(negative fee\)", self.validator.add_transaction, self.transaction) def test_reject_too_low_fee(self): with patch("aucoin.consensus.tx_min_fee", 51): # fee of self.transaction is 50 self.assertRaisesRegex(InvalidTransactionException, "Transaction fee too low", self.validator.add_transaction, self.transaction) def test_reject_invalid_signature(self): self.transaction.inputs[0].signature = b"Wrong" self.assertRaisesRegex(InvalidTransactionException, "Invalid signature for one or more inputs", self.validator.add_transaction, self.transaction) def test_reject_wrong_public_key(self): with session_scope() as session: for input, copy_hash in self.transaction.truncated_copies(self.blockchain, self.mempool, session): keypair = dsa.generate_keypair() # generate new, wrong, keypair input.public_key = public_bytes(keypair.public) input.signature = dsa.sign(keypair.private, copy_hash) self.assertRaisesRegex(InvalidTransactionException, "Public key doesn't match address of the output it is spending for one or more inputs", self.validator.add_transaction, self.transaction) if __name__ == '__main__': unittest.main()