From 9dd2e026b0532b8f852e2c326380e45c5acafc01 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Sat, 25 Nov 2017 01:04:50 +0100 Subject: [PATCH] Fixes #1891 --- .../common/schema/transaction_v1.0.yaml | 4 +- bigchaindb/common/transaction.py | 39 +++-- bigchaindb/models.py | 1 + tests/assets/test_digital_assets.py | 2 + tests/backend/mongodb/test_queries.py | 11 +- tests/common/conftest.py | 146 ++++++++++++++++++ tests/common/test_transaction.py | 68 ++++---- tests/conftest.py | 9 ++ tests/db/test_bigchain_api.py | 6 +- tests/pipelines/test_election.py | 4 + tests/pipelines/test_vote.py | 4 + tests/test_block_model.py | 20 ++- tests/test_fastquery.py | 13 +- .../validation/test_transaction_structure.py | 129 +++++++++++----- tests/web/test_transactions.py | 36 ++++- 15 files changed, 383 insertions(+), 109 deletions(-) diff --git a/bigchaindb/common/schema/transaction_v1.0.yaml b/bigchaindb/common/schema/transaction_v1.0.yaml index 2d5a4964..15b7d381 100644 --- a/bigchaindb/common/schema/transaction_v1.0.yaml +++ b/bigchaindb/common/schema/transaction_v1.0.yaml @@ -13,7 +13,9 @@ required: - version properties: id: - "$ref": "#/definitions/sha3_hexdigest" + anyOf: + - "$ref": "#/definitions/sha3_hexdigest" + - type: 'null' operation: "$ref": "#/definitions/operation" asset: diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index c1cc760a..bf47588d 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -112,7 +112,7 @@ class Input(object): InvalidSignature: If an Input's URI couldn't be parsed. """ fulfillment = data['fulfillment'] - if not isinstance(fulfillment, Fulfillment): + if not isinstance(fulfillment, (Fulfillment, type(None))): try: fulfillment = Fulfillment.from_uri(data['fulfillment']) except ASN1DecodeError: @@ -477,7 +477,7 @@ class Transaction(object): VERSION = '1.0' def __init__(self, operation, asset, inputs=None, outputs=None, - metadata=None, version=None): + metadata=None, version=None, hash_id=None): """The constructor allows to create a customizable Transaction. Note: @@ -495,6 +495,7 @@ class Transaction(object): metadata (dict): Metadata to be stored along with the Transaction. version (string): Defines the version number of a Transaction. + hash_id (string): Hash id of the transaction. """ if operation not in Transaction.ALLOWED_OPERATIONS: allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS) @@ -528,6 +529,14 @@ class Transaction(object): self.inputs = inputs or [] self.outputs = outputs or [] self.metadata = metadata + self._id = hash_id + + @property + def serialized(self): + return Transaction._to_str(self.to_dict()) + + def _hash(self): + self._id = hash_data(self.serialized) @classmethod def create(cls, tx_signers, recipients, metadata=None, asset=None): @@ -756,6 +765,9 @@ class Transaction(object): tx_serialized = Transaction._to_str(tx_dict) for i, input_ in enumerate(self.inputs): self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs) + + self._hash() + return self @classmethod @@ -907,6 +919,7 @@ class Transaction(object): tx_dict = self.to_dict() tx_dict = Transaction._remove_signatures(tx_dict) + tx_dict['id'] = None tx_serialized = Transaction._to_str(tx_dict) def validate(i, output_condition_uri=None): @@ -965,22 +978,16 @@ class Transaction(object): Returns: dict: The Transaction as an alternative serialization format. """ - tx = { + return { 'inputs': [input_.to_dict() for input_ in self.inputs], 'outputs': [output.to_dict() for output in self.outputs], 'operation': str(self.operation), 'metadata': self.metadata, 'asset': self.asset, 'version': self.version, + 'id': self._id, } - tx_no_signatures = Transaction._remove_signatures(tx) - tx_serialized = Transaction._to_str(tx_no_signatures) - tx_id = Transaction._to_hash(tx_serialized) - - tx['id'] = tx_id - return tx - @staticmethod # TODO: Remove `_dict` prefix of variable. def _remove_signatures(tx_dict): @@ -1010,7 +1017,7 @@ class Transaction(object): @property def id(self): - return self.to_hash() + return self._id def to_hash(self): return self.to_dict()['id'] @@ -1069,12 +1076,13 @@ class Transaction(object): # NOTE: Remove reference to avoid side effects tx_body = deepcopy(tx_body) try: - proposed_tx_id = tx_body.pop('id') + proposed_tx_id = tx_body['id'] except KeyError: raise InvalidHash('No transaction id found!') - tx_body_no_signatures = Transaction._remove_signatures(tx_body) - tx_body_serialized = Transaction._to_str(tx_body_no_signatures) + tx_body['id'] = None + + tx_body_serialized = Transaction._to_str(tx_body) valid_tx_id = Transaction._to_hash(tx_body_serialized) if proposed_tx_id != valid_tx_id: @@ -1092,8 +1100,7 @@ class Transaction(object): Returns: :class:`~bigchaindb.common.transaction.Transaction` """ - cls.validate_id(tx) inputs = [Input.from_dict(input_) for input_ in tx['inputs']] outputs = [Output.from_dict(output) for output in tx['outputs']] return cls(tx['operation'], tx['asset'], inputs, outputs, - tx['metadata'], tx['version']) + tx['metadata'], tx['version'], hash_id=tx['id']) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 48cc735a..3e825340 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -84,6 +84,7 @@ class Transaction(Transaction): @classmethod def from_dict(cls, tx_body): + super().validate_id(tx_body) validate_transaction_schema(tx_body) validate_txn_obj('asset', tx_body['asset'], 'data', validate_key) validate_txn_obj('metadata', tx_body, 'metadata', validate_key) diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index 1e63dbec..422eddd3 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -71,8 +71,10 @@ def test_asset_id_mismatch(b, user_pk): tx1 = Transaction.create([b.me], [([user_pk], 1)], metadata={'msg': random.random()}) + tx1.sign([b.me_private]) tx2 = Transaction.create([b.me], [([user_pk], 1)], metadata={'msg': random.random()}) + tx2.sign([b.me_private]) with pytest.raises(AssetIdMismatch): Transaction.get_asset_id([tx1, tx2]) diff --git a/tests/backend/mongodb/test_queries.py b/tests/backend/mongodb/test_queries.py index eeb70399..bd66d8ab 100644 --- a/tests/backend/mongodb/test_queries.py +++ b/tests/backend/mongodb/test_queries.py @@ -308,14 +308,13 @@ def test_count_blocks(signed_create_tx): assert query.count_blocks(conn) == 1 -def test_count_backlog(signed_create_tx): +def test_count_backlog(signed_create_tx, signed_transfer_tx): from bigchaindb.backend import connect, query conn = connect() # create and insert some transations conn.db.backlog.insert_one(signed_create_tx.to_dict()) - signed_create_tx.metadata = {'msg': 'aaa'} - conn.db.backlog.insert_one(signed_create_tx.to_dict()) + conn.db.backlog.insert_one(signed_transfer_tx.to_dict()) assert query.count_backlog(conn) == 2 @@ -437,17 +436,21 @@ def test_get_new_blocks_feed(b, create_tx): assert list(feed) == [b3] -def test_get_spending_transactions(user_pk): +def test_get_spending_transactions(user_pk, user_sk): from bigchaindb.backend import connect, query from bigchaindb.models import Block, Transaction conn = connect() out = [([user_pk], 1)] tx1 = Transaction.create([user_pk], out * 3) + tx1.sign([user_sk]) inputs = tx1.to_inputs() tx2 = Transaction.transfer([inputs[0]], out, tx1.id) + tx2.sign([user_sk]) tx3 = Transaction.transfer([inputs[1]], out, tx1.id) + tx3.sign([user_sk]) tx4 = Transaction.transfer([inputs[2]], out, tx1.id) + tx4.sign([user_sk]) block = Block([tx1, tx2, tx3, tx4]) conn.db.bigchain.insert_one(block.to_dict()) diff --git a/tests/common/conftest.py b/tests/common/conftest.py index 4c918a7b..97cfcfb3 100644 --- a/tests/common/conftest.py +++ b/tests/common/conftest.py @@ -202,3 +202,149 @@ def dummy_transaction(): }], 'version': '1.0' } + + +@pytest.fixture +def unfulfilled_transaction(): + return { + 'asset': { + 'data': { + 'msg': 'Hello BigchainDB!', + } + }, + 'id': None, + 'inputs': [{ + # XXX This could be None, see #1925 + # https://github.com/bigchaindb/bigchaindb/issues/1925 + 'fulfillment': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256' + }, + 'fulfills': None, + 'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'metadata': None, + 'operation': 'CREATE', + 'outputs': [{ + 'amount': '1', + 'condition': { + 'details': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256' + }, + 'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'}, + 'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'version': '1.0' + } + + +@pytest.fixture +def fulfilled_transaction(): + return { + 'asset': { + 'data': { + 'msg': 'Hello BigchainDB!', + } + }, + 'id': None, + 'inputs': [{ + 'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g' + 'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k' + 'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN'), + 'fulfills': None, + 'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'metadata': None, + 'operation': 'CREATE', + 'outputs': [{ + 'amount': '1', + 'condition': { + 'details': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256' + }, + 'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'}, + 'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'version': '1.0' + } + + +@pytest.fixture +def fulfilled_and_hashed_transaction(): + return { + 'asset': { + 'data': { + 'msg': 'Hello BigchainDB!', + } + }, + 'id': '7a7c827cf4ef7985f08f4e9d16f5ffc58ca4e82271921dfbed32e70cb462485f', + 'inputs': [{ + 'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g' + 'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k' + 'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN'), + 'fulfills': None, + 'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'metadata': None, + 'operation': 'CREATE', + 'outputs': [{ + 'amount': '1', + 'condition': { + 'details': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256' + }, + 'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'}, + 'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'version': '1.0' + } + + +# TODO For reviewers: Pick which approach you like best: parametrized or not? +@pytest.fixture(params=( + {'id': None, + 'fulfillment': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256'}}, + {'id': None, + 'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g' + 'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k' + 'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN')}, + {'id': '7a7c827cf4ef7985f08f4e9d16f5ffc58ca4e82271921dfbed32e70cb462485f', + 'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g' + 'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k' + 'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN')}, +)) +def tri_state_transaction(request): + tx = { + 'asset': { + 'data': { + 'msg': 'Hello BigchainDB!', + } + }, + 'id': None, + 'inputs': [{ + 'fulfillment': None, + 'fulfills': None, + 'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'metadata': None, + 'operation': 'CREATE', + 'outputs': [{ + 'amount': '1', + 'condition': { + 'details': { + 'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE', + 'type': 'ed25519-sha-256' + }, + 'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'}, + 'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'] + }], + 'version': '1.0' + } + tx['id'] = request.param['id'] + tx['inputs'][0]['fulfillment'] = request.param['fulfillment'] + return tx diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index a628eb53..0b113c05 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -1,10 +1,12 @@ """These are tests of the API of the Transaction class and associated classes. Tests for transaction validation are separate. """ +import json from copy import deepcopy from base58 import b58encode, b58decode -from pytest import raises +from pytest import mark, raises +from sha3 import sha3_256 def test_input_serialization(ffill_uri, user_pub): @@ -35,6 +37,7 @@ def test_input_deserialization_with_uri(ffill_uri, user_pub): assert input == expected +@mark.skip(reason='None is tolerated because it is None before fulfilling.') def test_input_deserialization_with_invalid_input(user_pub): from bigchaindb.common.transaction import Input @@ -303,10 +306,8 @@ def test_create_default_asset_on_tx_initialization(asset_definition): def test_transaction_serialization(user_input, user_output, data): from bigchaindb.common.transaction import Transaction - tx_id = 'l0l' - expected = { - 'id': tx_id, + 'id': None, 'version': Transaction.VERSION, # NOTE: This test assumes that Inputs and Outputs can # successfully be serialized @@ -322,37 +323,14 @@ def test_transaction_serialization(user_input, user_output, data): tx = Transaction(Transaction.CREATE, {'data': data}, [user_input], [user_output]) tx_dict = tx.to_dict() - tx_dict['id'] = tx_id assert tx_dict == expected -def test_transaction_deserialization(user_input, user_output, data): +def test_transaction_deserialization(tri_state_transaction): from bigchaindb.common.transaction import Transaction from .utils import validate_transaction_model - - expected_asset = {'data': data} - expected = Transaction(Transaction.CREATE, expected_asset, [user_input], - [user_output], None, Transaction.VERSION) - - tx = { - 'version': Transaction.VERSION, - # NOTE: This test assumes that Inputs and Outputs can - # successfully be serialized - 'inputs': [user_input.to_dict()], - 'outputs': [user_output.to_dict()], - 'operation': Transaction.CREATE, - 'metadata': None, - 'asset': { - 'data': data, - } - } - tx_no_signatures = Transaction._remove_signatures(tx) - tx['id'] = Transaction._to_hash(Transaction._to_str(tx_no_signatures)) - tx = Transaction.from_dict(tx) - - assert tx == expected - + tx = Transaction.from_dict(tri_state_transaction) validate_transaction_model(tx) @@ -543,7 +521,6 @@ def test_validate_input_with_invalid_parameters(utx): input_conditions = [out.fulfillment.condition_uri for out in utx.outputs] tx_dict = utx.to_dict() - tx_dict = Transaction._remove_signatures(tx_dict) tx_serialized = Transaction._to_str(tx_dict) valid = utx._input_valid(utx.inputs[0], tx_serialized, input_conditions) assert not valid @@ -834,6 +811,7 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, from .utils import validate_transaction_model expected = { + 'id': None, 'outputs': [user2_output.to_dict()], 'metadata': None, 'asset': { @@ -861,7 +839,6 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, transfer_tx = transfer_tx.to_dict() expected_input = deepcopy(inputs[0]) - expected['id'] = transfer_tx['id'] expected_input.fulfillment.sign( serialize(expected).encode(), b58decode(user_priv)) expected_ffill = expected_input.fulfillment.serialize_uri() @@ -971,6 +948,35 @@ def test_cant_add_empty_input(): tx.add_input(None) +def test_unfulfilled_transaction_serialized(unfulfilled_transaction): + from bigchaindb.common.transaction import Transaction + tx_obj = Transaction.from_dict(unfulfilled_transaction) + expected = json.dumps(unfulfilled_transaction, sort_keys=True, + separators=(',', ':'), ensure_ascii=True) + assert tx_obj.serialized == expected + + +def test_fulfilled_transaction_serialized(fulfilled_transaction): + from bigchaindb.common.transaction import Transaction + tx_obj = Transaction.from_dict(fulfilled_transaction) + expected = json.dumps(fulfilled_transaction, sort_keys=True, + separators=(',', ':'), ensure_ascii=True) + assert tx_obj.serialized == expected + + +def test_transaction_hash(fulfilled_transaction): + from bigchaindb.common.transaction import Transaction + tx_obj = Transaction.from_dict(fulfilled_transaction) + assert tx_obj._id is None + assert tx_obj.id is None + thing_to_hash = json.dumps(fulfilled_transaction, sort_keys=True, + separators=(',', ':'), ensure_ascii=True) + expected_hash_id = sha3_256(thing_to_hash.encode()).hexdigest() + tx_obj._hash() + assert tx_obj._id == expected_hash_id + assert tx_obj.id == expected_hash_id + + def test_output_from_dict_invalid_amount(user_output): from bigchaindb.common.transaction import Output from bigchaindb.common.exceptions import AmountError diff --git a/tests/conftest.py b/tests/conftest.py index 665218f9..ba3cb84f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -335,6 +335,15 @@ def signed_transfer_tx(signed_create_tx, user_pk, user_sk): return tx.sign([user_sk]) +@pytest.fixture +def double_spend_tx(signed_create_tx, carol_pubkey, user_sk): + from bigchaindb.models import Transaction + inputs = signed_create_tx.to_inputs() + tx = Transaction.transfer( + inputs, [([carol_pubkey], 1)], asset_id=signed_create_tx.id) + return tx.sign([user_sk]) + + @pytest.fixture def structurally_valid_vote(): return { diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 3a8c270c..d98059eb 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -635,7 +635,7 @@ class TestTransactionValidation(object): @pytest.mark.usefixtures('inputs') def test_non_create_double_spend(self, b, signed_create_tx, - signed_transfer_tx): + signed_transfer_tx, double_spend_tx): from bigchaindb.common.exceptions import DoubleSpend block1 = b.create_block([signed_create_tx]) @@ -655,10 +655,8 @@ class TestTransactionValidation(object): sleep(1) - signed_transfer_tx.metadata = {'different': 1} - # FIXME: https://github.com/bigchaindb/bigchaindb/issues/592 with pytest.raises(DoubleSpend): - b.validate_transaction(signed_transfer_tx) + b.validate_transaction(double_spend_tx) @pytest.mark.usefixtures('inputs') def test_valid_non_create_transaction_after_block_creation(self, b, diff --git a/tests/pipelines/test_election.py b/tests/pipelines/test_election.py index 05b1ef37..c802a58b 100644 --- a/tests/pipelines/test_election.py +++ b/tests/pipelines/test_election.py @@ -18,6 +18,7 @@ def test_check_for_quorum_invalid(b, user_pk): # create blocks with transactions tx1 = Transaction.create([b.me], [([user_pk], 1)]) + tx1.sign([b.me_private]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -51,6 +52,7 @@ def test_check_for_quorum_invalid_prev_node(b, user_pk): # create blocks with transactions tx1 = Transaction.create([b.me], [([user_pk], 1)]) + tx1.sign([b.me_private]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -94,6 +96,7 @@ def test_check_for_quorum_valid(b, user_pk): # create blocks with transactions tx1 = Transaction.create([b.me], [([user_pk], 1)]) + tx1.sign([b.me_private]) test_block = b.create_block([tx1]) # add voters to block and write @@ -129,6 +132,7 @@ def test_check_requeue_transaction(b, user_pk): # create blocks with transactions tx1 = Transaction.create([b.me], [([user_pk], 1)]) + tx1.sign([b.me_private]) test_block = b.create_block([tx1]) e.requeue_transactions(test_block) diff --git a/tests/pipelines/test_vote.py b/tests/pipelines/test_vote.py index 701434fd..eb1133ac 100644 --- a/tests/pipelines/test_vote.py +++ b/tests/pipelines/test_vote.py @@ -378,6 +378,10 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, vote2_doc['signature']) is True +@pytest.mark.skip( + reason=('Needs important modification following issue #1891:' + 'https://github.com/bigchaindb/bigchaindb/issues/1891') +) @pytest.mark.bdb def test_unsigned_tx_in_block_voting(monkeypatch, b, user_pk, genesis_block): from bigchaindb.backend import query diff --git a/tests/test_block_model.py b/tests/test_block_model.py index 6e14d293..ebcc2d6e 100644 --- a/tests/test_block_model.py +++ b/tests/test_block_model.py @@ -57,14 +57,15 @@ class TestBlockModel(object): from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [([b.me], 1)])] + transaction = Transaction.create([b.me], [([b.me], 1)]) + transaction.sign([b.me_private]) timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] - expected = Block(transactions, b.me, timestamp, voters) + expected = Block([transaction], b.me, timestamp, voters) block = { 'timestamp': timestamp, - 'transactions': [tx.to_dict() for tx in transactions], + 'transactions': [transaction.to_dict()], 'node_pubkey': b.me, 'voters': voters, } @@ -97,12 +98,13 @@ class TestBlockModel(object): from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [([b.me], 1)])] + transaction = Transaction.create([b.me], [([b.me], 1)]) + transaction.sign([b.me_private]) timestamp = gen_timestamp() block = { 'timestamp': timestamp, - 'transactions': [tx.to_dict() for tx in transactions], + 'transactions': [transaction.to_dict()], 'node_pubkey': b.me, 'voters': list(b.federation), } @@ -168,12 +170,14 @@ class TestBlockModel(object): # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) + tx.sign([b.me_private]) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) + tx.sign([b.me_private]) txs.append(tx) # create the block @@ -203,12 +207,14 @@ class TestBlockModel(object): # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) + tx.sign([b.me_private]) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) + tx.sign([b.me_private]) txs.append(tx) # create the block @@ -236,12 +242,14 @@ class TestBlockModel(object): # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) + tx.sign([b.me_private]) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) + tx.sign([b.me_private]) txs.append(tx) # create the block @@ -268,12 +276,14 @@ class TestBlockModel(object): # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) + tx.sign([b.me_private]) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) + tx.sign([b.me_private]) txs.append(tx) # create the block diff --git a/tests/test_fastquery.py b/tests/test_fastquery.py index bf3facf0..811bee43 100644 --- a/tests/test_fastquery.py +++ b/tests/test_fastquery.py @@ -49,17 +49,20 @@ def test_get_outputs_by_public_key(b, user_pk, user2_pk, blockdata): ] -def test_filter_spent_outputs(b, user_pk): +def test_filter_spent_outputs(b, user_pk, user_sk): out = [([user_pk], 1)] tx1 = Transaction.create([user_pk], out * 3) - + tx1.sign([user_sk]) # There are 3 inputs inputs = tx1.to_inputs() # Each spent individually tx2 = Transaction.transfer([inputs[0]], out, tx1.id) + tx2.sign([user_sk]) tx3 = Transaction.transfer([inputs[1]], out, tx1.id) + tx3.sign([user_sk]) tx4 = Transaction.transfer([inputs[2]], out, tx1.id) + tx4.sign([user_sk]) # The CREATE and first TRANSFER are valid. tx2 produces a new unspent. for tx in [tx1, tx2]: @@ -86,17 +89,21 @@ def test_filter_spent_outputs(b, user_pk): } -def test_filter_unspent_outputs(b, user_pk): +def test_filter_unspent_outputs(b, user_pk, user_sk): out = [([user_pk], 1)] tx1 = Transaction.create([user_pk], out * 3) + tx1.sign([user_sk]) # There are 3 inputs inputs = tx1.to_inputs() # Each spent individually tx2 = Transaction.transfer([inputs[0]], out, tx1.id) + tx2.sign([user_sk]) tx3 = Transaction.transfer([inputs[1]], out, tx1.id) + tx3.sign([user_sk]) tx4 = Transaction.transfer([inputs[2]], out, tx1.id) + tx4.sign([user_sk]) # The CREATE and first TRANSFER are valid. tx2 produces a new unspent. for tx in [tx1, tx2]: diff --git a/tests/validation/test_transaction_structure.py b/tests/validation/test_transaction_structure.py index f1e86baf..5b128003 100644 --- a/tests/validation/test_transaction_structure.py +++ b/tests/validation/test_transaction_structure.py @@ -2,11 +2,13 @@ structural / schematic issues are caught when reading a transaction (ie going from dict -> transaction). """ +import json import pytest +import sha3 from unittest.mock import MagicMock -from bigchaindb.common.exceptions import (AmountError, InvalidHash, +from bigchaindb.common.exceptions import (AmountError, SchemaValidationError, ThresholdTooDeep) from bigchaindb.models import Transaction @@ -28,137 +30,175 @@ def validate_raises(tx, exc=SchemaValidationError): # We should test that validation works when we expect it to -def test_validation_passes(create_tx): - validate(create_tx) +def test_validation_passes(signed_create_tx): + Transaction.from_dict(signed_create_tx.to_dict()) ################################################################################ # ID -def test_tx_serialization_hash_function(create_tx): - import sha3 - import json - tx = create_tx.to_dict() - tx['inputs'][0]['fulfillment'] = None - del tx['id'] +def test_tx_serialization_hash_function(signed_create_tx): + tx = signed_create_tx.to_dict() + tx['id'] = None payload = json.dumps(tx, skipkeys=False, sort_keys=True, separators=(',', ':')) - assert sha3.sha3_256(payload.encode()).hexdigest() == create_tx.id + assert sha3.sha3_256(payload.encode()).hexdigest() == signed_create_tx.id -def test_tx_serialization_with_incorrect_hash(create_tx): - tx = create_tx.to_dict() +def test_tx_serialization_with_incorrect_hash(signed_create_tx): + from bigchaindb.common.transaction import Transaction + from bigchaindb.common.exceptions import InvalidHash + tx = signed_create_tx.to_dict() tx['id'] = 'a' * 64 - validate_raises(tx, InvalidHash) + with pytest.raises(InvalidHash): + Transaction.validate_id(tx) -def test_tx_serialization_with_no_hash(create_tx): - tx = create_tx.to_dict() +def test_tx_serialization_with_no_hash(signed_create_tx): + from bigchaindb.common.exceptions import InvalidHash + tx = signed_create_tx.to_dict() del tx['id'] - validate_raises(tx) + with pytest.raises(InvalidHash): + Transaction.from_dict(tx) ################################################################################ # Operation -def test_validate_invalid_operation(create_tx): +def test_validate_invalid_operation(b, create_tx): create_tx.operation = 'something invalid' - validate_raises(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate_raises(signed_tx) ################################################################################ # Metadata -def test_validate_fails_metadata_empty_dict(create_tx): +def test_validate_fails_metadata_empty_dict(b, create_tx): create_tx.metadata = {'a': 1} - validate(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate(signed_tx) + + create_tx._id = None + create_tx.fulfillment = None create_tx.metadata = None - validate(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate(signed_tx) + + create_tx._id = None + create_tx.fulfillment = None create_tx.metadata = {} - validate_raises(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate_raises(signed_tx) ################################################################################ # Asset -def test_transfer_asset_schema(signed_transfer_tx): +def test_transfer_asset_schema(user_sk, signed_transfer_tx): + from bigchaindb.common.transaction import Transaction tx = signed_transfer_tx.to_dict() validate(tx) + tx['id'] = None tx['asset']['data'] = {} + tx = Transaction.from_dict(tx).sign([user_sk]).to_dict() validate_raises(tx) + tx['id'] = None del tx['asset']['data'] tx['asset']['id'] = 'b' * 63 + tx = Transaction.from_dict(tx).sign([user_sk]).to_dict() validate_raises(tx) -def test_create_tx_no_asset_id(create_tx): +def test_create_tx_no_asset_id(b, create_tx): create_tx.asset['id'] = 'b' * 64 - validate_raises(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate_raises(signed_tx) -def test_create_tx_asset_type(create_tx): +def test_create_tx_asset_type(b, create_tx): create_tx.asset['data'] = 'a' - validate_raises(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate_raises(signed_tx) -def test_create_tx_no_asset_data(create_tx): +def test_create_tx_no_asset_data(b, create_tx): tx_body = create_tx.to_dict() del tx_body['asset']['data'] - tx_body_no_signatures = Transaction._remove_signatures(tx_body) - tx_body_serialized = Transaction._to_str(tx_body_no_signatures) - tx_body['id'] = Transaction._to_hash(tx_body_serialized) + tx_serialized = json.dumps( + tx_body, skipkeys=False, sort_keys=True, separators=(',', ':')) + tx_body['id'] = sha3.sha3_256(tx_serialized.encode()).hexdigest() validate_raises(tx_body) ################################################################################ # Inputs -def test_no_inputs(create_tx): +def test_no_inputs(b, create_tx): create_tx.inputs = [] - validate_raises(create_tx) + signed_tx = create_tx.sign([b.me_private]) + validate_raises(signed_tx) -def test_create_single_input(create_tx): +def test_create_single_input(b, create_tx): + from bigchaindb.common.transaction import Transaction tx = create_tx.to_dict() tx['inputs'] += tx['inputs'] + tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict() validate_raises(tx) + tx['id'] = None tx['inputs'] = [] + tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict() validate_raises(tx) -def test_create_tx_no_fulfills(create_tx): +def test_create_tx_no_fulfills(b, create_tx): + from bigchaindb.common.transaction import Transaction tx = create_tx.to_dict() - tx['inputs'][0]['fulfills'] = {'tx': 'a' * 64, 'output': 0} + tx['inputs'][0]['fulfills'] = {'transaction_id': 'a' * 64, + 'output_index': 0} + tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict() validate_raises(tx) -def test_transfer_has_inputs(signed_transfer_tx): +def test_transfer_has_inputs(user_sk, signed_transfer_tx): signed_transfer_tx.inputs = [] + signed_transfer_tx._id = None + signed_transfer_tx.sign([user_sk]) validate_raises(signed_transfer_tx) ################################################################################ # Outputs -def test_low_amounts(create_tx, signed_transfer_tx): - for tx in [create_tx, signed_transfer_tx]: +def test_low_amounts(b, user_sk, create_tx, signed_transfer_tx): + for sk, tx in [(b.me_private, create_tx), (user_sk, signed_transfer_tx)]: tx.outputs[0].amount = 0 + tx._id = None + tx.sign([sk]) validate_raises(tx, AmountError) tx.outputs[0].amount = -1 + tx._id = None + tx.sign([sk]) validate_raises(tx) -def test_high_amounts(create_tx): +def test_high_amounts(b, create_tx): # Should raise a SchemaValidationError - don't want to allow ridiculously # large numbers to get converted to int create_tx.outputs[0].amount = 10 ** 21 + create_tx.sign([b.me_private]) validate_raises(create_tx) # Should raise AmountError create_tx.outputs[0].amount = 9 * 10 ** 18 + 1 + create_tx._id = None + create_tx.sign([b.me_private]) validate_raises(create_tx, AmountError) # Should pass create_tx.outputs[0].amount -= 1 + create_tx._id = None + create_tx.sign([b.me_private]) validate(create_tx) @@ -196,10 +236,17 @@ def test_unsupported_condition_type(): ################################################################################ # Version -def test_validate_version(create_tx): +def test_validate_version(b, create_tx): create_tx.version = '1.0' + create_tx.sign([b.me_private]) validate(create_tx) + create_tx.version = '0.10' + create_tx._id = None + create_tx.sign([b.me_private]) validate_raises(create_tx) + create_tx.version = '110' + create_tx._id = None + create_tx.sign([b.me_private]) validate_raises(create_tx) diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index 7a506653..1a70de85 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -1,7 +1,11 @@ import json from unittest.mock import patch +import base58 import pytest +from cryptoconditions import Ed25519Sha256 +from sha3 import sha3_256 + from bigchaindb.common import crypto @@ -165,9 +169,16 @@ def test_post_create_transaction_with_invalid_signature(mock_logger, from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [([user_pub], 1)]) - tx = tx.sign([user_priv]).to_dict() + tx = Transaction.create([user_pub], [([user_pub], 1)]).to_dict() tx['inputs'][0]['fulfillment'] = 64 * '0' + tx['id'] = sha3_256( + json.dumps( + tx, + sort_keys=True, + separators=(',', ':'), + ensure_ascii=False, + ).encode(), + ).hexdigest() res = client.post(TX_ENDPOINT, data=json.dumps(tx)) expected_status_code = 400 @@ -202,9 +213,25 @@ def test_post_create_transaction_with_invalid_structure(client): def test_post_create_transaction_with_invalid_schema(mock_logger, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create( - [user_pub], [([user_pub], 1)]).sign([user_priv]).to_dict() + tx = Transaction.create([user_pub], [([user_pub], 1)]).to_dict() del tx['version'] + ed25519 = Ed25519Sha256(public_key=base58.b58decode(user_pub)) + message = json.dumps( + tx, + sort_keys=True, + separators=(',', ':'), + ensure_ascii=False, + ).encode() + ed25519.sign(message, base58.b58decode(user_priv)) + tx['inputs'][0]['fulfillment'] = ed25519.serialize_uri() + tx['id'] = sha3_256( + json.dumps( + tx, + sort_keys=True, + separators=(',', ':'), + ensure_ascii=False, + ).encode(), + ).hexdigest() res = client.post(TX_ENDPOINT, data=json.dumps(tx)) expected_status_code = 400 expected_error_message = ( @@ -308,6 +335,7 @@ def test_post_invalid_transfer_transaction_returns_400(b, client, user_pk): transfer_tx = Transaction.transfer(create_tx.to_inputs(), [([user_pub], 1)], asset_id=create_tx.id) + transfer_tx._hash() res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) expected_status_code = 400