From 194bf8c6bdc13cde734b4b9feab3c1fc95a52fe8 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 22 Mar 2016 19:30:53 +0100 Subject: [PATCH 1/3] initial implementation of multi input support --- bigchaindb/consensus.py | 42 +++++++++++++++++------------------ bigchaindb/core.py | 2 +- bigchaindb/util.py | 10 ++++++--- tests/db/test_bigchain_api.py | 13 +++++++++++ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index b65ab9ad..f0ee601e 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -119,7 +119,7 @@ class BaseConsensusRules(AbstractConsensusRules): # If the operation is CREATE the transaction should have no inputs and # should be signed by a federation node if transaction['transaction']['operation'] == 'CREATE': - if transaction['transaction']['input']: + if transaction['transaction']['inputs']: raise ValueError('A CREATE operation has no inputs') if transaction['transaction']['current_owner'] not in ( bigchain.federation_nodes + [bigchain.me]): @@ -128,32 +128,32 @@ class BaseConsensusRules(AbstractConsensusRules): else: # check if the input exists, is owned by the current_owner - if not transaction['transaction']['input']: + if not transaction['transaction']['inputs']: raise ValueError( 'Only `CREATE` transactions can have null inputs') - tx_input = bigchain.get_transaction( - transaction['transaction']['input']) + # check inputs + for inp in transaction['transaction']['inputs']: + tx_input = bigchain.get_transaction(inp) - if not tx_input: - raise exceptions.TransactionDoesNotExist( - 'input `{}` does not exist in the bigchain'.format( - transaction['transaction']['input'])) + if not tx_input: + raise exceptions.TransactionDoesNotExist( + 'input `{}` does not exist in the bigchain'.format( + transaction['transaction']['input'])) - if (tx_input['transaction']['new_owner'] != - transaction['transaction']['current_owner']): - raise exceptions.TransactionOwnerError( - 'current_owner `{}` does not own the input `{}`'.format( - transaction['transaction']['current_owner'], - transaction['transaction']['input'])) + if (tx_input['transaction']['new_owner'] != + transaction['transaction']['current_owner']): + raise exceptions.TransactionOwnerError( + 'current_owner `{}` does not own the input `{}`'.format( + transaction['transaction']['current_owner'], + transaction['transaction']['input'])) - # check if the input was already spent by a transaction other than - # this one. - spent = bigchain.get_spent(tx_input['id']) - if spent and spent['id'] != transaction['id']: - raise exceptions.DoubleSpend( - 'input `{}` was already spent'.format( - transaction['transaction']['input'])) + # check if the input was already spent by a transaction other than + # this one. + spent = bigchain.get_spent(tx_input['id']) + if spent and spent['id'] != transaction['id']: + raise exceptions.DoubleSpend( + 'input `{}` was already spent'.format(inp)) # Check hash of the transaction calculated_hash = hash_data(util.serialize( diff --git a/bigchaindb/core.py b/bigchaindb/core.py index f76dd2d6..99131055 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -203,7 +203,7 @@ class Bigchain(object): # checks if an input was already spent # checks if the bigchain has any transaction with input `transaction_id` response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\ - .filter(lambda transaction: transaction['transaction']['input'] == txid).run(self.conn) + .filter(lambda transaction: transaction['transaction']['inputs'].contains(txid)).run(self.conn) # a transaction_id should have been spent at most one time transactions = list(response) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index d8ddc9ce..999805fe 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -76,7 +76,7 @@ def timestamp(): return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6) -def create_tx(current_owner, new_owner, tx_input, operation, payload=None): +def create_tx(current_owner, new_owner, inputs, operation, payload=None): """Create a new transaction A transaction in the bigchain is a transfer of a digital asset between two entities represented @@ -94,7 +94,7 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): Args: current_owner (str): base58 encoded public key of the current owner of the asset. new_owner (str): base58 encoded public key of the new owner of the digital asset. - tx_input (str): id of the transaction to use as input. + inputs (list): id of the transaction to use as input. operation (str): Either `CREATE` or `TRANSFER` operation. payload (Optional[dict]): dictionary with information about asset. @@ -106,6 +106,7 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): TypeError: if the optional ``payload`` argument is not a ``dict``. """ + # handle payload data = None if payload is not None: if isinstance(payload, dict): @@ -123,10 +124,13 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): 'payload': payload } + if inputs == []: + inputs = None + tx = { 'current_owner': current_owner, 'new_owner': new_owner, - 'input': tx_input, + 'inputs': inputs, 'operation': operation, 'timestamp': timestamp(), 'data': data diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 1fed8800..6349a824 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -756,3 +756,16 @@ class TestBigchainBlock(object): def test_duplicated_transactions(self): pytest.skip('We may have duplicates in the initial_results and changefeed') + + +class TestMultipleInputs(object): + + def test_transfer_transaction_multiple(self, b): + pass + + def test_transfer_single_input_from_multi_input(self, b): + pass + + def test_get_spent(self, b): + pass + From dd3bc54c620a11d69f6692911f11ae3d863cd0b0 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 5 Apr 2016 14:36:23 +0200 Subject: [PATCH 2/3] changed structure of transactions --- bigchaindb/util.py | 114 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 999805fe..4f1fe45f 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -76,7 +76,7 @@ def timestamp(): return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6) -def create_tx(current_owner, new_owner, inputs, operation, payload=None): +def create_tx(current_owners, new_owners, inputs, operation, payload=None): """Create a new transaction A transaction in the bigchain is a transfer of a digital asset between two entities represented @@ -104,6 +104,42 @@ def create_tx(current_owner, new_owner, inputs, operation, payload=None): Raises: TypeError: if the optional ``payload`` argument is not a ``dict``. + + Reference: + { + "id": "", + "version": "transaction version number", + "transaction": { + "fulfillments": [ + { + "current_owners": ["list of "], + "input": { + "txid": "", + "cid": "condition index" + }, + "fulfillment": "fulfillement of condition cid", + "fid": "fulfillment index" + } + ], + "conditions": [ + { + "new_owners": ["list of "], + "condition": "condition to be met", + "cid": "condition index (1-to-1 mapping with fid)" + } + ], + "operation": "", + "timestamp": "", + "data": { + "hash": "", + "payload": { + "title": "The Winds of Plast", + "creator": "Johnathan Plunkett", + "IPFS_key": "QmfQ5QAjvg4GtA3wg3adpnDJug8ktA1BxurVqBD8rtgVjP" + } + } + }, + } """ # handle payload @@ -124,13 +160,38 @@ def create_tx(current_owner, new_owner, inputs, operation, payload=None): 'payload': payload } - if inputs == []: - inputs = None + # handle inputs + fulfillments = [] + # transfer + if inputs: + for fid, inp in enumerate(inputs): + fulfillments.append({ + 'current_owners': current_owners, + 'input': inp, + 'fulfillment': None, + 'fid': fid + }) + # create + else: + fulfillments.append({ + 'current_owners': current_owners, + 'input': None, + 'fulfillment': None, + 'fid': 0 + }) + + # handle outputs + conditions = [] + for fulfillment in fulfillments: + conditions.append({ + 'new_owners': new_owners, + 'condition': None, + 'cid': fulfillment['fid'] + }) tx = { - 'current_owner': current_owner, - 'new_owner': new_owner, - 'inputs': inputs, + 'fulfillments': fulfillments, + 'conditions': conditions, 'operation': operation, 'timestamp': timestamp(), 'data': data @@ -143,12 +204,14 @@ def create_tx(current_owner, new_owner, inputs, operation, payload=None): # create the transaction transaction = { 'id': tx_hash, + 'version': 1, 'transaction': tx } return transaction +#TODO: Change sign_tx to populate the fulfillments def sign_tx(transaction, private_key): """Sign a transaction @@ -159,14 +222,43 @@ def sign_tx(transaction, private_key): private_key (str): base58 encoded private key to create a signature of the transaction. Returns: - dict: transaction with the `signature` field included. + dict: transaction with the `fulfillment` fields populated. """ + b = bigchaindb.Bigchain() private_key = PrivateKey(private_key) - signature = private_key.sign(serialize(transaction)) - signed_transaction = transaction.copy() - signed_transaction.update({'signature': signature}) - return signed_transaction + + common_data = { + 'operation': transaction['transaction']['operation'], + 'timestamp': transaction['transaction']['timestamp'], + 'data': transaction['transaction']['data'], + 'version': transaction['version'], + 'id': transaction['id'] + } + + for fulfillment in transaction['transaction']['fulfillments']: + fulfillment_message = common_data.copy() + if transaction['transaction']['operation'] == 'CREATE': + fulfillment_message.update({ + 'input': None, + 'condition': None + }) + else: + # get previous condition + previous_tx = b.get_transaction(fulfillment['input']['txid']) + conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) + + # update the fulfillment message + fulfillment_message.update({ + 'input': fulfillment['input'], + 'condition': conditions[fulfillment['cid']] + }) + + # sign the fulfillment message + fulfillment_message_signature = private_key.sign(serialize(fulfillment_message)) + fulfillment.update({'fulfillment': fulfillment_message_signature}) + + return transaction def create_and_sign_tx(private_key, current_owner, new_owner, tx_input, operation='TRANSFER', payload=None): From aa626a768d1d1c2b71edea74b1f6dfe3b49db15d Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 6 Apr 2016 11:50:46 +0200 Subject: [PATCH 3/3] fixed payload handling --- bigchaindb/util.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 4f1fe45f..a171f195 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -76,6 +76,7 @@ def timestamp(): return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6) +# TODO: Consider remove the operation (if there are no inputs CREATE else TRANSFER) def create_tx(current_owners, new_owners, inputs, operation, payload=None): """Create a new transaction @@ -92,8 +93,8 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): `TRANSFER` - A transfer operation allows for a transfer of the digital assets between entities. Args: - current_owner (str): base58 encoded public key of the current owner of the asset. - new_owner (str): base58 encoded public key of the new owner of the digital asset. + current_owners (list): base58 encoded public key of the current owners of the asset. + new_owners (list): base58 encoded public key of the new owners of the digital asset. inputs (list): id of the transaction to use as input. operation (str): Either `CREATE` or `TRANSFER` operation. payload (Optional[dict]): dictionary with information about asset. @@ -154,12 +155,6 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): else: raise TypeError('`payload` must be an dict instance') - hash_payload = hash_data(serialize(payload)) - data = { - 'hash': hash_payload, - 'payload': payload - } - # handle inputs fulfillments = [] # transfer