diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 47ed6dce..e062d490 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -238,23 +238,6 @@ class Bigchain(object): .run(self.conn) owned = [] - def owner_in_subfulfillments(condition_details, _owner): - if 'subfulfillments' in condition_details: - result = owner_in_subfulfillments(condition_details['subfulfillments'], _owner) - if result: - return True - - elif isinstance(condition_details, list): - for _subfulfillment in condition_details: - result = owner_in_subfulfillments(_subfulfillment, _owner) - if result: - return True - else: - if 'public_key' in condition_details \ - and _owner == condition_details['public_key']: - return True - return False - for tx in response: # a transaction can contain multiple outputs (conditions) so we need to iterate over all of them # to get a list of outputs available to spend @@ -268,7 +251,7 @@ class Bigchain(object): # for transactions with multiple `new_owners` there will be several subfulfillments nested # in the condition. We need to iterate the subfulfillments to make sure there is a # subfulfillment for `owner` - if owner_in_subfulfillments(condition['condition']['details'], owner): + if util.condition_details_has_owner(condition['condition']['details'], owner): tx_input = {'txid': tx['id'], 'cid': condition['cid']} # check if input was already spent if not self.get_spent(tx_input): diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 70db8814..195eb35b 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -526,6 +526,37 @@ def get_input_condition(bigchain, fulfillment): } +def condition_details_has_owner(condition_details, owner): + """ + + Check if the public_key of owner is in the condition details + as an Ed25519Fulfillment.public_key + + Args: + condition_details (dict): dict with condition details + owner (str): base58 public key of owner + + Returns: + bool: True if the public key is found in the condition details, False otherwise + + """ + if 'subfulfillments' in condition_details: + result = condition_details_has_owner(condition_details['subfulfillments'], owner) + if result: + return True + + elif isinstance(condition_details, list): + for subcondition in condition_details: + result = condition_details_has_owner(subcondition, owner) + if result: + return True + else: + if 'public_key' in condition_details \ + and owner == condition_details['public_key']: + return True + return False + + def get_hash_data(transaction): """ Get the hashed data that (should) correspond to the `transaction['id']` diff --git a/docs/source/_static/cc_escrow_execute_abort.png b/docs/source/_static/cc_escrow_execute_abort.png new file mode 100644 index 00000000..2d4a98e1 Binary files /dev/null and b/docs/source/_static/cc_escrow_execute_abort.png differ diff --git a/docs/source/_static/tx_escrow_execute_abort.png b/docs/source/_static/tx_escrow_execute_abort.png new file mode 100644 index 00000000..bb425aa6 Binary files /dev/null and b/docs/source/_static/tx_escrow_execute_abort.png differ diff --git a/docs/source/_static/tx_schematics.odg b/docs/source/_static/tx_schematics.odg index 7443eaec..b66b138f 100644 Binary files a/docs/source/_static/tx_schematics.odg and b/docs/source/_static/tx_schematics.odg differ diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 611a4755..36f9716b 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -885,4 +885,204 @@ hashlock_fulfill_tx }, "version":1 } +``` + +### Timeout Conditions + +Timeout conditions allow assets to expire after a certain time. +The primary use case of timeout conditions is to enable Escrow (see further). + +The condition can only be fulfilled before the expiry time. +Once expired, the asset is lost and cannot be fulfilled by anyone. + +__Note__: The timeout conditions are BigchainDB-specific and not (yet) supported by the ILP standard. + +```python +# Create a timeout asset without any new_owners +timeout_tx = b.create_transaction(b.me, None, None, 'CREATE') + +# Set expiry time - the asset needs to be transfered before expiration +time_sleep = 12 +time_expire = str(float(util.timestamp()) + time_sleep) # 12 secs from now +condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) + +# The conditions list is empty, so we need to append a new condition +tx_timeout['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(condition_timeout.serialize_json()), + 'uri': condition_timeout.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# Conditions have been updated, so the hash needs updating +tx_timeout['id'] = util.get_hash_data(tx_timeout) + +# The asset needs to be signed by the current_owner +tx_timeout_signed = b.sign_transaction(tx_timeout, b.me_private) + +# Some validations +assert b.validate_transaction(tx_timeout_signed) == tx_timeout_signed +assert b.is_valid_transaction(tx_timeout_signed) == tx_timeout_signed + +b.write_transaction(tx_timeout_signed) +tx_timeout_signed +``` + +```python +{ + "id":"78145396cd368f7168fb01c97aaf1df6f85244d7b544073dfcb42397dae38f90", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":9, + "expire_time":"1464167910.643431", + "type":"fulfillment", + "type_id":99 + }, + "uri":"cc:63:9:sceU_NZc3cAjAvaR1TVmgj7am5y8hJEBoqLm-tbqGbQ:17" + }, + "new_owners":null + } + ], + "data":null, + "fulfillments":[ + { + "current_owners":[ + "FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2" + ], + "fid":0, + "fulfillment":null, + "input":null + } + ], + "operation":"CREATE", + "timestamp":"1464167898.643353" + }, + "version":1 +} +``` + +The following demonstrates that the transaction invalidates once the timeout occurs: + +```python +# Create a timeout fulfillment tx +tx_timeout_transfer = b.create_transaction(None, testuser1_pub, {'txid': tx_timeout['id'], 'cid': 0}, 'TRANSFER') + +# Parse the timeout condition and create the corresponding fulfillment +timeout_fulfillment = cc.Fulfillment.from_json( + tx_timeout['transaction']['conditions'][0]['condition']['details']) +tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_fulfillment.serialize_uri() + +# No need to sign transaction, like with hashlocks + +# Small test to see the state change +for i in range(time_sleep - 4): + tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer + seconds_to_timeout = int(float(time_expire) - float(util.timestamp())) + print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout)) + sleep(1) +``` + +If you were fast enough, you should see the following output: + +```python +tx_timeout valid: True (3s to timeout) +tx_timeout valid: True (2s to timeout) +tx_timeout valid: True (1s to timeout) +tx_timeout valid: True (0s to timeout) +tx_timeout valid: False (0s to timeout) +tx_timeout valid: False (-1s to timeout) +tx_timeout valid: False (-2s to timeout) +tx_timeout valid: False (-3s to timeout) +``` + +## Escrow + +Escrow is a mechanism for conditional release of assets. + +This means that a the assets are locked up by a trusted party until an `execute` condition is presented. In order not to tie up the assets forever, the escrow foresees an `abort` condition, which is typically an expiry time. + +BigchainDB and cryptoconditions provides escrow out-of-the-box, without the need of a trusted party. + +A threshold condition is used to represent the escrow, since BigchainDB transactions cannot have a _pending_ state. + +![BigchainDB escrow conditions and fulfillments](./_static/tx_escrow_execute_abort.png) + +The logic for switching between `execute` and `abort` conditions is conceptually simple: + +```python +if timeout_condition.validate(utcnow()): + execute_fulfillment.validate(msg) == True + abort_fulfillment.validate(msg) == False +else: + execute_fulfillment.validate(msg) == False + abort_fulfillment.validate(msg) == True +``` + +The above switch can be implemented as follows using threshold cryptoconditions: + +![Cryptoconditions escrow conditions and fulfillments](./_static/cc_escrow_execute_abort.png) + +The small circle on the threshold conditions denotes an inversion of the fulfillment: + +```python +inverted_fulfillment.validate(msg) == not fulfillment.validate(msg) +``` + +An inverted input to a threshold condition is simply obtained by using negative weights. + +__Note__: negative weights are BigchainDB-specific and not (yet) supported by the ILP standard. + +The following code snippet shows how to create an escrow condition + +```python +# Retrieve the last transaction of testuser2_pub +tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop() + +# Create a base template with the execute and abort address +tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub], tx_retrieved_id, 'TRANSFER') + +# Set expiry time - the execute address needs to fulfill before expiration +time_sleep = 12 +time_expire = str(float(util.timestamp()) + time_sleep) # 12 secs from now + +# Create the escrow and timeout condition +condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate +condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) # only valid if now() <= time_expire + +# Create the execute branch +condition_execute = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate +condition_execute.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser1_pub)) # execute address +condition_execute.add_subfulfillment(condition_timeout) # federation checks on expiry +condition_escrow.add_subfulfillment(condition_execute) + +# Create the abort branch +condition_abort = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate +condition_abort.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser2_pub)) # abort address +condition_abort.add_subfulfillment(condition_timeout, weight=-1) # the negative weight inverts the condition +condition_escrow.add_subfulfillment(condition_abort) + +# Update the condition in the newly created transaction +tx_escrow['transaction']['conditions'][0]['condition'] = { + 'details': json.loads(condition_escrow.serialize_json()), + 'uri': condition_escrow.condition.serialize_uri() +} + +# Conditions have been updated, so the hash needs updating +tx_escrow['id'] = util.get_hash_data(tx_escrow) + +# The asset needs to be signed by the current_owner +tx_escrow_signed = b.sign_transaction(tx_escrow, testuser2_priv) + +# Some validations +assert b.validate_transaction(tx_escrow_signed) == tx_escrow_signed +assert b.is_valid_transaction(tx_escrow_signed) == tx_escrow_signed + +b.write_transaction(tx_escrow_signed) +tx_escrow_signed ``` \ No newline at end of file diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index c0afcce3..1648e060 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1884,6 +1884,7 @@ class TestCryptoconditions(object): first_input_tx = b.get_owned_ids(user_vk).pop() user2_sk, user2_vk = crypto.generate_key_pair() + # ESCROW escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER') time_sleep = 3 @@ -1926,12 +1927,10 @@ class TestCryptoconditions(object): block = b.create_block([escrow_tx_signed]) b.write_block(block, durability='hard') - # create hashlock fulfillment tx # Retrieve the last transaction of thresholduser1_pub tx_retrieved_id = b.get_owned_ids(user2_vk).pop() # EXECUTE - # Create a base template for output transaction escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER') @@ -1973,7 +1972,6 @@ class TestCryptoconditions(object): assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer # ABORT - # Create a base template for output transaction escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER') @@ -2013,6 +2011,7 @@ class TestCryptoconditions(object): first_input_tx = b.get_owned_ids(user_vk).pop() user2_sk, user2_vk = crypto.generate_key_pair() + # ESCROW escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER') time_sleep = 3 @@ -2055,12 +2054,10 @@ class TestCryptoconditions(object): block = b.create_block([escrow_tx_signed]) b.write_block(block, durability='hard') - # create hashlock fulfillment tx # Retrieve the last transaction of thresholduser1_pub tx_retrieved_id = b.get_owned_ids(user2_vk).pop() # EXECUTE - # Create a base template for output transaction escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER') @@ -2108,7 +2105,6 @@ class TestCryptoconditions(object): assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer # ABORT - # Create a base template for output transaction escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER') @@ -2124,13 +2120,13 @@ class TestCryptoconditions(object): escrow_tx_abort['transaction']['fulfillments'][0], serialized=True) escrow_fulfillment.subconditions = [] - # fulfill execute branch + # do not fulfill execute branch fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) fulfillment_and_execute.add_subfulfillment(subfulfillment_user2) fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) escrow_fulfillment.add_subcondition(fulfillment_and_execute.condition) - # do not fulfill abort branch + # fulfill abort branch fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) subfulfillment_user.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user_sk)) fulfillment_and_abort.add_subfulfillment(subfulfillment_user) diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index d4d896ed..ee95ef3a 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -106,6 +106,7 @@ sleep(8) # retrieve the transaction tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id']) +assert tx_multisig_retrieved is not None print(json.dumps(tx_multisig_retrieved, sort_keys=True, indent=4, separators=(',', ':'))) @@ -115,7 +116,11 @@ tx_multisig_retrieved_id = b.get_owned_ids(testuser2_pub).pop() tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER') tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv]) -b.validate_transaction(tx_multisig_transfer_signed) +try: + b.validate_transaction(tx_multisig_transfer_signed) +except exceptions.InvalidSignature: + import ipdb; ipdb.set_trace() + b.validate_transaction(tx_multisig_transfer_signed) b.write_transaction(tx_multisig_transfer_signed) # wait a few seconds for the asset to appear on the blockchain @@ -151,6 +156,8 @@ b.write_transaction(tx_mimo_signed) print(json.dumps(tx_mimo_signed, sort_keys=True, indent=4, separators=(',', ':'))) +sleep(8) + """ Threshold Conditions """ @@ -298,3 +305,181 @@ 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=(',', ':'))) + + +""" +Timeout Conditions +""" +# Create transaction template +tx_timeout = b.create_transaction(b.me, None, None, 'CREATE') + +# Set expiry time (12 secs from now) +time_sleep = 12 +time_expire = str(float(util.timestamp()) + time_sleep) + +# only valid if the server time <= time_expire +condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) + +# The conditions list is empty, so we need to append a new condition +tx_timeout['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(condition_timeout.serialize_json()), + 'uri': condition_timeout.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# conditions have been updated, so hash needs updating +tx_timeout['id'] = util.get_hash_data(tx_timeout) + +# sign transaction +tx_timeout_signed = b.sign_transaction(tx_timeout, b.me_private) + +b.write_transaction(tx_timeout_signed) +print(json.dumps(tx_timeout, sort_keys=True, indent=4, separators=(',', ':'))) +sleep(8) + +# Retrieve the transaction id of tx_timeout +tx_timeout_id = {'txid': tx_timeout['id'], 'cid': 0} + +# Create a template to transfer the tx_timeout +tx_timeout_transfer = b.create_transaction(None, testuser1_pub, tx_timeout_id, 'TRANSFER') + +# Parse the threshold cryptocondition +timeout_fulfillment = cc.Fulfillment.from_json( + tx_timeout['transaction']['conditions'][0]['condition']['details']) + +tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_fulfillment.serialize_uri() + +# no need to sign transaction, like with hashlocks +for i in range(time_sleep - 4): + tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer + seconds_to_timeout = int(float(time_expire) - float(util.timestamp())) + print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout)) + sleep(1) + +""" +Escrow Conditions +""" +# retrieve the last transaction of testuser2 +tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop() + +# Create escrow template with the execute and abort address +tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub], tx_retrieved_id, 'TRANSFER') + +# Set expiry time (12 secs from now) +time_sleep = 12 +time_expire = str(float(util.timestamp()) + time_sleep) + +# Create escrow and timeout condition +condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate +condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) # only valid if now() <= time_expire + +# Create execute branch +condition_execute = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate +condition_execute.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser1_pub)) # execute address +condition_execute.add_subfulfillment(condition_timeout) # federation checks on expiry +condition_escrow.add_subfulfillment(condition_execute) + +# Create abort branch +condition_abort = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate +condition_abort.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser2_pub)) # abort address +condition_abort.add_subfulfillment(condition_timeout, weight=-1) # the negative weight inverts the condition +condition_escrow.add_subfulfillment(condition_abort) + +# Update the condition in the newly created transaction +tx_escrow['transaction']['conditions'][0]['condition'] = { + 'details': json.loads(condition_escrow.serialize_json()), + 'uri': condition_escrow.condition.serialize_uri() +} + +# conditions have been updated, so hash needs updating +tx_escrow['id'] = util.get_hash_data(tx_escrow) + +# sign transaction +tx_escrow_signed = b.sign_transaction(tx_escrow, testuser2_priv) + +# some checks +assert b.validate_transaction(tx_escrow_signed) == tx_escrow_signed +assert b.is_valid_transaction(tx_escrow_signed) == tx_escrow_signed + +print(json.dumps(tx_escrow_signed, sort_keys=True, indent=4, separators=(',', ':'))) +b.write_transaction(tx_escrow_signed) +sleep(8) + +# Retrieve the last transaction of thresholduser1_pub +tx_escrow_id = {'txid': tx_escrow_signed['id'], 'cid': 0} + +# Create a base template for output transaction +tx_escrow_execute = b.create_transaction([testuser2_pub, testuser1_pub], testuser1_pub, tx_escrow_id, 'TRANSFER') + +# Parse the threshold cryptocondition +escrow_fulfillment = cc.Fulfillment.from_json( + tx_escrow['transaction']['conditions'][0]['condition']['details']) + +subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0] +subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0] +subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body'] + +# Get the fulfillment message to sign +tx_escrow_execute_fulfillment_message = \ + util.get_fulfillment_message(tx_escrow_execute, + tx_escrow_execute['transaction']['fulfillments'][0], + serialized=True) + +escrow_fulfillment.subconditions = [] + +# fulfill execute branch +fulfillment_execute = cc.ThresholdSha256Fulfillment(threshold=2) +subfulfillment_testuser1.sign(tx_escrow_execute_fulfillment_message, crypto.SigningKey(testuser1_priv)) +fulfillment_execute.add_subfulfillment(subfulfillment_testuser1) +fulfillment_execute.add_subfulfillment(subfulfillment_timeout) +escrow_fulfillment.add_subfulfillment(fulfillment_execute) + +# do not fulfill abort branch +condition_abort = cc.ThresholdSha256Fulfillment(threshold=2) +condition_abort.add_subfulfillment(subfulfillment_testuser2) +condition_abort.add_subfulfillment(subfulfillment_timeout, weight=-1) +escrow_fulfillment.add_subcondition(condition_abort.condition) + +# create fulfillment and append to transaction +tx_escrow_execute['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() + +# Time has expired, hence the abort branch can redeem +tx_escrow_abort = b.create_transaction([testuser2_pub, testuser1_pub], testuser2_pub, tx_escrow_id, 'TRANSFER') + +# Parse the threshold cryptocondition +escrow_fulfillment = cc.Fulfillment.from_json( + tx_escrow['transaction']['conditions'][0]['condition']['details']) + +subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0] +subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0] +subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body'] + +tx_escrow_abort_fulfillment_message = \ + util.get_fulfillment_message(tx_escrow_abort, + tx_escrow_abort['transaction']['fulfillments'][0], + serialized=True) +escrow_fulfillment.subconditions = [] + +# Do not fulfill execute branch +condition_execute = cc.ThresholdSha256Fulfillment(threshold=2) +condition_execute.add_subfulfillment(subfulfillment_testuser1) +condition_execute.add_subfulfillment(subfulfillment_timeout) +escrow_fulfillment.add_subcondition(condition_execute.condition) + +# Fulfill abort branch +fulfillment_abort = cc.ThresholdSha256Fulfillment(threshold=2) +subfulfillment_testuser2.sign(tx_escrow_abort_fulfillment_message, crypto.SigningKey(testuser2_priv)) +fulfillment_abort.add_subfulfillment(subfulfillment_testuser2) +fulfillment_abort.add_subfulfillment(subfulfillment_timeout, weight=-1) +escrow_fulfillment.add_subfulfillment(fulfillment_abort) + +tx_escrow_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() + +for i in range(time_sleep - 4): + valid_execute = b.is_valid_transaction(tx_escrow_execute) == tx_escrow_execute + valid_abort = b.is_valid_transaction(tx_escrow_abort) == tx_escrow_abort + print('execute: {} - abort {}'.format(valid_execute, valid_abort)) + sleep(1)