215 lines
9.4 KiB
Python
215 lines
9.4 KiB
Python
|
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()
|