diff --git a/bigchaindb/util.py b/bigchaindb/util.py index bb7af6cf..9f0e59c3 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -203,11 +203,20 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): }, } """ - # validate arguments (owners and inputs should be lists) + # validate arguments (owners and inputs should be lists or None) + + # The None case appears on fulfilling a hashlock + if current_owners is None: + current_owners = [] if not isinstance(current_owners, list): current_owners = [current_owners] + + # The None case appears on assigning a hashlock + if new_owners is None: + new_owners = [] if not isinstance(new_owners, list): new_owners = [new_owners] + if not isinstance(inputs, list): inputs = [inputs] @@ -247,20 +256,30 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): # handle outputs conditions = [] for fulfillment in fulfillments: + + # threshold condition if len(new_owners) > 1: condition = cc.ThresholdSha256Fulfillment(threshold=len(new_owners)) for new_owner in new_owners: condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=new_owner)) + + # simple signature condition elif len(new_owners) == 1: condition = cc.Ed25519Fulfillment(public_key=new_owners[0]) - conditions.append({ - 'new_owners': new_owners, - 'condition': { - 'details': json.loads(condition.serialize_json()), - 'uri': condition.condition.serialize_uri() - }, - 'cid': fulfillment['fid'] - }) + + # to be added later (hashlock conditions) + else: + condition = None + + if condition: + conditions.append({ + 'new_owners': new_owners, + 'condition': { + 'details': json.loads(condition.serialize_json()), + 'uri': condition.condition.serialize_uri() + }, + 'cid': fulfillment['fid'] + }) tx = { 'fulfillments': fulfillments, diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 15ee08f7..c191fe74 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -746,3 +746,142 @@ threshold_tx_transfer "version":1 } ``` + + +### Hash-locked Conditions + +By creating a hash of a difficult-to-guess 256-bit random or pseudo-random integer it is possible to create a condition which the creator can trivially fulfill by publishing the random value. However, for anyone else, the condition is cryptographically hard to fulfill, because they would have to find a preimage for the given condition hash. + +One possible usecase might be to redeem a digital voucher when given a secret (voucher code). + +```python +# Create a hash-locked asset without any new_owners +hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + +# Define a secret that will be hashed - fulfillments need to guess the secret +secret = b'much secret! wow!' +first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + +# The conditions list is empty, so we need to append a new condition +hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# Conditions have been updated, so hash needs updating +hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + +# The asset needs to be signed by the current_owner +hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + +# Some validations +assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed +assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + +b.write_transaction(hashlock_tx_signed) +hashlock_tx_signed +``` + +```python +{ + "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", + "id":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "uri":"cc:0:3:nsW2IiYgk9EUtsg4uBe3pBnOgRoAEX2IIsPgjqZz47U:17" + }, + "new_owners":None + } + ], + "data":None, + "fulfillments":[ + { + "current_owners":[ + "FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2" + ], + "fid":0, + "fulfillment":"cf:4:21-D-LfNhIQhvY5914ArFTUGpgPKc7EVC1ZtJqqOTHGx1p9FuRr9tRfkbdqtX2MZWh7sRVUmMnwp7I1-xZbCnCkeADf69IwDHbZvNS6aTr1CpekREsV9ZG8m_wjlZiUN", + "input":None + } + ], + "operation":"CREATE", + "timestamp":"1461250387.910102" + }, + "version":1 +} +``` + +In order to redeem the asset, one needs to create a fulfillment the correct secret as a preimage: + +```python +hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() + +# create hashlock fulfillment tx +hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_pub, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + +# provide a wrong secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.is_valid_transaction(hashlock_fulfill_tx) == False + +# provide the right secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx +assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + +b.write_transaction(hashlock_fulfill_tx) +hashlock_fulfill_tx +``` + +```python +{ + "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", + "id":"fe6871bf3ca62eb61c52c5555cec2e07af51df817723f0cb76e5cf6248f449d2", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":32, + "public_key":"EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb", + "signature":None, + "type":"fulfillment", + "type_id":4 + }, + "uri":"cc:4:20:y9884Md2YI_wdnGSTJGhwvFaNsKLe8sqwimqk-2JLSI:96" + }, + "new_owners":[ + "EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb" + ] + } + ], + "data":None, + "fulfillments":[ + { + "current_owners":[], + "fid":0, + "fulfillment":"cf:0:bXVjaCBzZWNyZXQhIHdvdyE", + "input":{ + "cid":0, + "txid":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237" + } + } + ], + "operation":"TRANSFER", + "timestamp":"1461250397.944510" + }, + "version":1 +} +``` \ No newline at end of file diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 71ad97a2..07479944 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1544,6 +1544,136 @@ class TestCryptoconditions(object): assert b.verify_signature(tx_transfer_signed) is True + def test_create_asset_with_hashlock_condition(self, b): + hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + @pytest.mark.usefixtures('inputs') + def test_transfer_asset_with_hashlock_condition(self, b, user_vk, user_sk): + first_input_tx = b.get_owned_ids(user_vk).pop() + + hashlock_tx = b.create_transaction(user_vk, None, first_input_tx, 'TRANSFER') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, user_sk) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert len(b.get_owned_ids(user_vk)) == 1 + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(user_vk)) == 0 + + def test_create_and_fulfill_asset_with_hashlock_condition(self, b, user_vk): + hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(b.me)) == 0 + + # create hashlock fulfillment tx + hashlock_fulfill_tx = b.create_transaction(None, user_vk, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + + hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') + hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + + with pytest.raises(exceptions.InvalidSignature): + b.validate_transaction(hashlock_fulfill_tx) + assert b.is_valid_transaction(hashlock_fulfill_tx) == False + + hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) + hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + + assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + + b.write_transaction(hashlock_fulfill_tx) + + # create and write block to bigchain + block = b.create_block([hashlock_fulfill_tx]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(b.me)) == 0 + assert len(b.get_owned_ids(user_vk)) == 1 + + # 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_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) + hashlock_doublespend_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_doublespend_tx_fulfillment.serialize_uri() + + with pytest.raises(exceptions.DoubleSpend): + b.validate_transaction(hashlock_doublespend_tx) + def test_get_subcondition_from_vk(self, b, user_sk, user_vk): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 3500d6ab..56a6aa31 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -233,4 +233,60 @@ b.write_transaction(threshold_tx_transfer) print(json.dumps(threshold_tx_transfer, sort_keys=True, indent=4, separators=(',', ':'))) +""" +Hashlocked Conditions +""" +# Create a hash-locked asset without any new_owners +hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + +# Define a secret that will be hashed - fulfillments need to guess the secret +secret = b'much secret! wow!' +first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + +# The conditions list is empty, so we need to append a new condition +hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# Conditions have been updated, so hash needs updating +hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + +# The asset needs to be signed by the current_owner +hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + +# Some validations +assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed +assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + +b.write_transaction(hashlock_tx_signed) +print(json.dumps(hashlock_tx_signed, sort_keys=True, indent=4, separators=(',', ':'))) + +sleep(10) + +hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() + +# create hashlock fulfillment tx +hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_priv, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + +# try a wrong secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.is_valid_transaction(hashlock_fulfill_tx) == False + +# provide the right secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx +assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + +b.write_transaction(hashlock_fulfill_tx) +print(json.dumps(hashlock_fulfill_tx, sort_keys=True, indent=4, separators=(',', ':')))