diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 81690598..5f664a40 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -276,7 +276,7 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): 'new_owners': new_owners, 'condition': { 'details': json.loads(condition.serialize_json()), - 'uri': condition.condition.serialize_uri() + 'uri': condition.condition_uri }, 'cid': fulfillment['fid'] }) @@ -330,7 +330,10 @@ def sign_tx(transaction, signing_keys): for fulfillment in tx['transaction']['fulfillments']: fulfillment_message = get_fulfillment_message(transaction, fulfillment) - parsed_fulfillment = cc.Fulfillment.from_json(fulfillment_message['condition']['condition']['details']) + # TODO: avoid instantiation, pass as argument! + bigchain = bigchaindb.Bigchain() + input_condition = get_input_condition(bigchain, fulfillment) + parsed_fulfillment = cc.Fulfillment.from_json(input_condition['condition']['details']) # for the case in which the type of fulfillment is not covered by this method parsed_fulfillment_signed = parsed_fulfillment @@ -436,19 +439,22 @@ def validate_fulfillments(signed_transaction): Returns: bool: True if the signature is correct, False otherwise. """ - for fulfillment in signed_transaction['transaction']['fulfillments']: fulfillment_message = get_fulfillment_message(signed_transaction, fulfillment) try: parsed_fulfillment = cc.Fulfillment.from_uri(fulfillment['fulfillment']) except (TypeError, ValueError, ParsingError): return False + + # TODO: might already break on a False here is_valid = parsed_fulfillment.validate(serialize(fulfillment_message)) # if transaction has an input (i.e. not a `CREATE` transaction) - if fulfillment['input']: - is_valid &= parsed_fulfillment.condition.serialize_uri() == \ - fulfillment_message['condition']['condition']['uri'] + # TODO: avoid instantiation, pass as argument! + bigchain = bigchaindb.Bigchain() + input_condition = get_input_condition(bigchain, fulfillment) + is_valid = is_valid and parsed_fulfillment.condition_uri == input_condition['condition']['uri'] + if not is_valid: return False @@ -466,8 +472,6 @@ def get_fulfillment_message(transaction, fulfillment, serialized=False): Returns: str|dict: fulfillment message """ - b = bigchaindb.Bigchain() - # data to sign contains common transaction data fulfillment_message = { 'operation': transaction['transaction']['operation'], @@ -479,27 +483,48 @@ def get_fulfillment_message(transaction, fulfillment, serialized=False): # and the condition which needs to be retrieved from the output of a previous transaction # or created on the fly it this is a `CREATE` transaction fulfillment_message.update({ - 'input': fulfillment['input'], - 'condition': None, + 'fulfillment': copy.deepcopy(fulfillment), + 'condition': transaction['transaction']['conditions'][fulfillment['fid']] }) - # if `TRANSFER` transaction - if fulfillment['input']: - # get previous condition - previous_tx = b.get_transaction(fulfillment['input']['txid']) - conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) - fulfillment_message['condition'] = conditions[fulfillment['input']['cid']] - # if `CREATE` transaction - # there is no previous transaction so we need to create one on the fly - else: - current_owner = transaction['transaction']['fulfillments'][0]['current_owners'][0] - condition = json.loads(cc.Ed25519Fulfillment(public_key=current_owner).serialize_json()) - fulfillment_message['condition'] = {'condition': {'details': condition}} + # remove any fulfillment, as a fulfillment cannot sign itself + fulfillment_message['fulfillment']['fulfillment'] = None + if serialized: return serialize(fulfillment_message) return fulfillment_message +def get_input_condition(bigchain, fulfillment): + """ + + Args: + bigchain: + fulfillment: + Returns: + """ + input_tx = fulfillment['input'] + # if `TRANSFER` transaction + if input_tx: + # get previous condition + previous_tx = bigchain.get_transaction(input_tx['txid']) + conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) + return conditions[input_tx['cid']] + + # if `CREATE` transaction + # there is no previous transaction so we need to create one on the fly + else: + current_owner = fulfillment['current_owners'][0] + condition = cc.Ed25519Fulfillment(public_key=current_owner) + + return { + 'condition': { + 'details': json.loads(condition.serialize_json()), + 'uri': condition.condition_uri + } + } + + def get_hash_data(transaction): """ Get the hashed data that (should) correspond to the `transaction['id']` diff --git a/setup.py b/setup.py index 3905799d..3639bd7e 100644 --- a/setup.py +++ b/setup.py @@ -6,11 +6,28 @@ For full docs visit https://bigchaindb.readthedocs.org """ from setuptools import setup, find_packages + # get the version version = {} with open('bigchaindb/version.py') as fp: exec(fp.read(), version) + +# check if setuptools is up to date +def check_setuptools_features(): + import pkg_resources + try: + list(pkg_resources.parse_requirements('foo~=1.0')) + except ValueError: + exit('Your Python distribution comes with an incompatible version ' + 'of `setuptools`. Please run:\n' + ' $ pip3 install --upgrade setuptools\n' + 'and then run this command again') + + +check_setuptools_features() + + tests_require = [ 'pytest', 'coverage', diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index e827aa96..d541a012 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1292,6 +1292,165 @@ class TestMultipleInputs(object): assert b.get_spent(inp) is None +class TestFulfillmentMessage(object): + def test_fulfillment_message_create(self, b, user_vk): + tx = b.create_transaction(b.me, user_vk, None, 'CREATE', payload={'pay': 'load'}) + original_fulfillment = tx['transaction']['fulfillments'][0] + fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) + + assert sorted(fulfillment_message) == \ + ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] + + assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] + assert fulfillment_message['id'] == tx['id'] + assert fulfillment_message['condition'] == tx['transaction']['conditions'][0] + assert fulfillment_message['fulfillment']['current_owners'] == original_fulfillment['current_owners'] + assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] + assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] + assert fulfillment_message['operation'] == tx['transaction']['operation'] + assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] + assert fulfillment_message['version'] == tx['version'] + + @pytest.mark.usefixtures('inputs') + def test_fulfillment_message_transfer(self, b, user_vk): + input_tx = b.get_owned_ids(user_vk).pop() + assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) == True + + tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER', payload={'pay': 'load'}) + + original_fulfillment = tx['transaction']['fulfillments'][0] + fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) + + assert sorted(fulfillment_message) == \ + ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] + + assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] + assert fulfillment_message['id'] == tx['id'] + assert fulfillment_message['condition'] == tx['transaction']['conditions'][0] + assert fulfillment_message['fulfillment']['current_owners'] == original_fulfillment['current_owners'] + assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] + assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] + assert fulfillment_message['operation'] == tx['transaction']['operation'] + assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] + assert fulfillment_message['version'] == tx['version'] + + def test_fulfillment_message_multiple_current_owners_multiple_new_owners_multiple_inputs(self, b, user_vk): + # create a new users + user2_sk, user2_vk = crypto.generate_key_pair() + user3_sk, user3_vk = crypto.generate_key_pair() + user4_sk, user4_vk = crypto.generate_key_pair() + + # create inputs to spend + transactions = [] + for i in range(5): + tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') + tx_signed = b.sign_transaction(tx, b.me_private) + transactions.append(tx_signed) + block = b.create_block(transactions) + b.write_block(block, durability='hard') + + # get input + owned_inputs = b.get_owned_ids(user_vk) + inp = owned_inputs[:3] + + # create a transaction + tx = b.create_transaction([user_vk, user2_vk], [user3_vk, user4_vk], inp, 'TRANSFER', payload={'pay': 'load'}) + + for original_fulfillment in tx['transaction']['fulfillments']: + fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) + + assert sorted(fulfillment_message) == \ + ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] + + assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] + assert fulfillment_message['id'] == tx['id'] + assert fulfillment_message['condition'] == tx['transaction']['conditions'][original_fulfillment['fid']] + assert fulfillment_message['fulfillment']['current_owners'] == original_fulfillment['current_owners'] + assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] + assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] + assert fulfillment_message['operation'] == tx['transaction']['operation'] + assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] + assert fulfillment_message['version'] == tx['version'] + + +class TestTransactionMalleability(object): + @pytest.mark.usefixtures('inputs') + def test_create_transaction_transfer(self, b, user_vk, user_sk): + input_tx = b.get_owned_ids(user_vk).pop() + assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) is True + + tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER') + + tx_signed = b.sign_transaction(tx, user_sk) + + assert b.validate_fulfillments(tx_signed) is True + assert b.is_valid_transaction(tx_signed) == tx_signed + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['id'] = 'dsdasd' + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['version'] = '0' + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['operation'] = 'CREATE' + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['timestamp'] = '1463033192.123456' + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['data'] = { + "hash": "872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8", + "payload": { + "msg": "Hello BigchainDB!" + } + } + assert b.validate_fulfillments(tx_changed) == False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['fulfillments'] = [ + { + "current_owners": [ + "AFbofwJYEB7Cx2fgrPrCJzbdDVRzRKysoGXt4DsvuTGN" + ], + "fid": 0, + "fulfillment": "cf:4:iXaq3UbandDj4DgBhFDcfHjkm2639RwgLmwAHUmuDFMfMEKMZ71eQw2qCMK951kBaNNJel_FCDuYnacn_MsWzYXOUJs6DGW3lYfXI_d55xuqpH2BenvRWKNp98tRRr4B", + "input": None + } + ] + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['fulfillments'][0]['fid'] = 1 + with pytest.raises(IndexError): + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['fulfillments'][0]['current_owners'] = [ + "AFbofwJYEB7Cx2fgrPrCJzbdDVRzRKysoGXt4DsvuTGN"] + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + tx_changed = copy.deepcopy(tx_signed) + tx_changed['transaction']['fulfillments'][0]['input'] = { + "cid": 0, + "txid": "3055348675fc6f23b75f13c55db6d112b66eee068e99d30a802883d3b1784203" + } + with pytest.raises(TypeError): + assert b.validate_fulfillments(tx_changed) is False + assert b.is_valid_transaction(tx_changed) is False + + class TestCryptoconditions(object): def test_fulfillment_transaction_create(self, b, user_vk): tx = b.create_transaction(b.me, user_vk, None, 'CREATE') @@ -1396,7 +1555,7 @@ class TestCryptoconditions(object): assert b.is_valid_transaction(tx) == tx @pytest.mark.usefixtures('inputs') - def test_override_fulfillment_transfer(self, b, user_vk, user_sk): + def test_override_fulfillment_transfer(self, b, user_vk, user_sk): # create valid transaction other_sk, other_vk = crypto.generate_key_pair() prev_tx_id = b.get_owned_ids(user_vk).pop() @@ -1413,7 +1572,7 @@ class TestCryptoconditions(object): assert b.is_valid_transaction(tx) == tx @pytest.mark.usefixtures('inputs') - def test_override_condition_and_fulfillment_transfer(self, b, user_vk, user_sk): + def test_override_condition_and_fulfillment_transfer(self, b, user_vk, user_sk): other_sk, other_vk = crypto.generate_key_pair() first_input_tx = b.get_owned_ids(user_vk).pop() first_tx = b.create_transaction(user_vk, other_vk, first_input_tx, 'TRANSFER') @@ -1650,7 +1809,8 @@ class TestCryptoconditions(object): tx_transfer_signed = b.sign_transaction(tx_transfer, [user_sk, user2_sk]) # expected fulfillment - expected_fulfillment = cc.Fulfillment.from_json(tx_create['transaction']['conditions'][0]['condition']['details']) + expected_fulfillment = cc.Fulfillment.from_json( + tx_create['transaction']['conditions'][0]['condition']['details']) subfulfillment1 = expected_fulfillment.subconditions[0]['body'] subfulfillment2 = expected_fulfillment.subconditions[1]['body'] @@ -1661,7 +1821,7 @@ class TestCryptoconditions(object): subfulfillment2.sign(util.serialize(expected_fulfillment_message), crypto.SigningKey(user2_sk)) assert tx_transfer_signed['transaction']['fulfillments'][0]['fulfillment'] \ - == expected_fulfillment.serialize_uri() + == expected_fulfillment.serialize_uri() assert b.validate_fulfillments(tx_transfer_signed) is True @@ -1786,7 +1946,8 @@ class TestCryptoconditions(object): # try doublespending user2_sk, user2_vk = crypto.generate_key_pair() - hashlock_doublespend_tx = b.create_transaction(None, user2_vk, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + hashlock_doublespend_tx = b.create_transaction(None, user2_vk, {'txid': hashlock_tx['id'], 'cid': 0}, + 'TRANSFER') hashlock_doublespend_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) hashlock_doublespend_tx['transaction']['fulfillments'][0]['fulfillment'] = \