import logging import unittest from aucoin import consensus, dsa, util from aucoin.block import Block from aucoin.exceptions import InvalidTransactionException, InvalidBlockException from aucoin.transactions import Transaction, Input, Output, CoinbaseTransaction from aucoin.validation import SyntaxChecker from aucoin.wallet import public_bytes from tests import helpers logging.basicConfig(level=logging.DEBUG) class TestCheckTransaction(unittest.TestCase): def setUp(self): # A valid transaction used for testing. self.transaction = Transaction( inputs=[ Input( prev_tx_hash=b"hash1", txout_index=2 ), Input( prev_tx_hash=b"hash2", txout_index=0 ), ], outputs=[ Output( value=50, address=b"addr1" ), Output( value=150, address=b"addr2" ) ] ) def test_valid(self): SyntaxChecker.check_transaction(self.transaction) def test_reject_empty_input_list(self): self.transaction.inputs = [] self.assertRaisesRegex(InvalidTransactionException, "Input list is empty", SyntaxChecker.check_transaction, self.transaction) def test_reject_empty_output_list(self): self.transaction.outputs = [] self.assertRaisesRegex(InvalidTransactionException, "Output list is empty", SyntaxChecker.check_transaction, self.transaction) def test_reject_zero_output_value(self): self.transaction.outputs[1].value = 0 self.assertRaisesRegex(InvalidTransactionException, "Negative or zero output value for one or more outputs", SyntaxChecker.check_transaction, self.transaction) def test_reject_size_too_large(self): self.transaction.outputs[1].address = bytes(consensus.block_max_size + 1) self.assertRaisesRegex(InvalidTransactionException, "Transaction-size larger than max block size", SyntaxChecker.check_transaction, self.transaction) def test_reject_negative_output_value(self): self.transaction.outputs[1].value = -10 self.assertRaisesRegex(InvalidTransactionException, "Negative or zero output value for one or more outputs", SyntaxChecker.check_transaction, self.transaction) def test_reject_duplicate_inputs(self): self.transaction.inputs.append(self.transaction.inputs[1]) self.assertRaisesRegex(InvalidTransactionException, "Transaction contains duplicate inputs", SyntaxChecker.check_transaction, self.transaction) def test_reject_prev_hash_equals_zero(self): self.transaction.inputs[1].prev_tx_hash = bytes(32) self.assertRaisesRegex(InvalidTransactionException, "prev_tx_hash is 0x00...00 for one or more inputs in non-coinbase tx", SyntaxChecker.check_transaction, self.transaction) class TestCheckCoinbaseTransaction(unittest.TestCase): def setUp(self): # A valid CoinbaseTransaction used for testing. self.transaction = CoinbaseTransaction( address=b"addr1", block_height=1, coinbase=b"coinbasedata" ) def test_valid(self): SyntaxChecker.check_transaction(self.transaction) def test_reject_multiple_inputs(self): self.transaction.inputs.append(Input(b"prev_tx_hash", 10)) self.assertRaisesRegex(InvalidTransactionException, "Coinbase transactions may only have one input", SyntaxChecker.check_transaction, self.transaction) def test_reject_multiple_outputs(self): self.transaction.outputs.append(Output(100, b"address")) self.assertRaisesRegex(InvalidTransactionException, "Coinbase transactions may only have one output", SyntaxChecker.check_transaction, self.transaction) def test_reject_prev_hash_nonzero(self): self.transaction.inputs[0].prev_tx_hash = b"not zero" self.assertRaisesRegex(InvalidTransactionException, "Coinbase transaction must have prev_tx_hash = 0x00...00", SyntaxChecker.check_transaction, self.transaction) def test_reject_txout_index_nonzero(self): self.transaction.inputs[0].txout_index = 1 self.assertRaisesRegex(InvalidTransactionException, "Coinbase transaction must have txout_index = 0", SyntaxChecker.check_transaction, self.transaction) def test_reject_coinbase_too_large(self): self.transaction.inputs[0].coinbase = bytes(consensus.tx_coinbase_max_size + 1) self.assertRaisesRegex(InvalidTransactionException, "Size of coinbase parameter exceeds coinbase_max_size", SyntaxChecker.check_transaction, self.transaction) class TestCheckBlock(unittest.TestCase): def setUp(self): # A valid block used for testing. self.private_key, self.public_key = dsa.generate_keypair() block = Block( target=bytes.fromhex("0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), public_key=public_bytes(self.public_key), transactions=[ CoinbaseTransaction( address=util.address(public_bytes(self.public_key)), block_height=1, coinbase=b"coinbasedata" ), Transaction( inputs=[ Input( prev_tx_hash=b"hash1", txout_index=2 ) ], outputs=[ Output( value=50, address=b"addr1" ) ] ) ] ) self.block = helpers.mine(block, self.private_key) # calculate correct signature def test_valid(self): SyntaxChecker.check_block(self.block) def test_reject_unsatisfactory_hash(self): self.block.target = bytes(32) self.assertRaisesRegex(InvalidBlockException, "Block hash doesn't satisfy claimed target proof of work", SyntaxChecker.check_block, self.block) def test_reject_empty_transaction_list(self): self.block.transactions = [] self.assertRaisesRegex(InvalidBlockException, "Transaction list must be non-empty", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_wrong_merkle(self): self.block.transactions.pop() self.assertRaisesRegex(InvalidBlockException, "Incorrect Merkle root hash", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_size_too_large(self): self.block.transactions[0].inputs[0].coinbase = bytes(consensus.block_max_size + 1) self.block.calculate_merkle() self.assertRaisesRegex(InvalidBlockException, "Block-size larger than max block size", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_first_transaction_is_not_coinbase(self): del self.block.transactions[0] self.block.calculate_merkle() self.assertRaisesRegex(InvalidBlockException, "The first transaction must be coinbase", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_non_first_transaction_is_coinbase(self): self.block.transactions.append(self.block.transactions[0]) self.block.calculate_merkle() self.assertRaisesRegex(InvalidBlockException, "Only the first transaction may be coinbase", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_invalid_signature(self): # A small "mining algorithm" that changes the block's hash by changing the signature (like normally in sign to # mine), but this signature is always invalid to provoke exception. nonce = 0 while True: self.block.signature = nonce.to_bytes(8, "big") if self.block.hash <= self.block.target: break nonce += 1 self.assertRaisesRegex(InvalidBlockException, "Invalid block signature", SyntaxChecker.check_block, self.block) def test_reject_wrong_public_key(self): self.block.transactions[0].outputs[0].address = b"Wrong" self.block.calculate_merkle() self.assertRaisesRegex(InvalidBlockException, "Public key doesn't match output address of the coinbase", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) def test_reject_any_invalid_transaction(self): self.block.transactions[1].inputs = [] self.block.calculate_merkle() self.assertRaisesRegex(InvalidTransactionException, "Input list is empty", SyntaxChecker.check_block, helpers.mine(self.block, self.private_key)) if __name__ == '__main__': unittest.main()