diff --git a/bigchaindb/core.py b/bigchaindb/core.py index f7418eac..8b624a7b 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -72,106 +72,17 @@ class Bigchain(object): """Create a new transaction Refer to the documentation of ``bigchaindb.util.create_tx`` - A transaction in the bigchain is a transfer of a digital asset between two entities represented - by public keys. - - Currently the bigchain supports two types of operations: - - `CREATE` - Only federation nodes are allowed to use this operation. In a create operation - a federation node creates a digital asset in the bigchain and assigns that asset to a public - key. The owner of the private key can then decided to transfer this digital asset by using the - `transaction id` of the transaction as an input in a `TRANSFER` transaction. - - `TRANSFER` - A transfer operation allows for a transfer of the digital assets between entities. - - Args: - current_owners (list): base58 encoded public keys of all current owners of the asset. - new_owners (list): base58 encoded public keys of all new owners of the digital asset. - tx_input (str): id of the transaction to use as input. - operation (str): Either `CREATE` or `TRANSFER` operation. - payload (Optional[dict]): dictionary with information about asset. - - Returns: - dict: unsigned transaction. - - - Raises: - TypeError: if the optional ``payload`` argument is not a ``dict``. """ - data = None - if payload is not None: - if isinstance(payload, dict): - hash_payload = hash_data(self.serialize(payload)) - data = { - 'hash': hash_payload, - 'payload': payload - } - else: - raise TypeError('`payload` must be an dict instance') - hash_payload = hash_data(self.serialize(payload)) - data = { - 'hash': hash_payload, - 'payload': payload - } - - tx = { - 'current_owners': current_owners if isinstance(current_owners, list) else [current_owners], - 'new_owners': new_owners if isinstance(new_owners, list) else [new_owners], - 'input': tx_input, - 'operation': operation, - 'timestamp': self.timestamp(), - 'data': data - } - - # serialize and convert to bytes - tx_serialized = self.serialize(tx) - tx_hash = hash_data(tx_serialized) - - # create the transaction - transaction = { - 'id': tx_hash, - 'transaction': tx - } - - return transaction - return util.create_tx(current_owner, new_owner, tx_input, operation, payload) + return util.create_tx(current_owners, new_owners, tx_input, operation, payload) def sign_transaction(self, transaction, private_key, public_key=None): """Sign a transaction Refer to the documentation of ``bigchaindb.util.sign_tx`` - A transaction signed with the `current_owner` corresponding private key. - - Args: - transaction (dict): transaction to sign. - private_key (str): base58 encoded private key to create a signature of the transaction. - public_key (str): (optional) base58 encoded public key to identify each signature of a multisig transaction. - - Returns: - dict: transaction with the `signature` field included. - """ - # return util.sign_tx(transaction, private_key) - private_key = PrivateKey(private_key) - if len(transaction['transaction']['current_owners']) == 1: - signatures_updated = private_key.sign(self.serialize(transaction)) - else: - # multisig, sign for each input and store {pub_key: signature_for_priv_key} - if public_key is None: - raise ValueError('public_key must be provided for signing multisig transactions') - transaction_without_signatures = transaction.copy() - signatures = transaction_without_signatures.pop('signatures') \ - if 'signatures' in transaction_without_signatures else [] - signatures_updated = signatures.copy() - signatures_updated = [s for s in signatures_updated if not s['public_key'] == public_key] - signatures_updated.append({'public_key': public_key, - 'signature': private_key.sign(self.serialize(transaction_without_signatures))}) - - signed_transaction = transaction.copy() - signed_transaction.update({'signatures': signatures_updated}) - return signed_transaction + return util.sign_tx(transaction, private_key, public_key) def verify_signature(self, signed_transaction): """Verify the signature of a transaction. @@ -179,29 +90,7 @@ class Bigchain(object): Refer to the documentation of ``bigchaindb.crypto.verify_signature`` """ - data = signed_transaction.copy() - - # if assignee field in the transaction, remove it - if 'assignee' in data: - data.pop('assignee') - - signatures = data.pop('signatures') - for public_key_base58 in signed_transaction['transaction']['current_owners']: - public_key = PublicKey(public_key_base58) - - if isinstance(signatures, list): - try: - signature = [s['signature'] for s in signatures if s['public_key'] == public_key_base58] - except KeyError: - return False - if not len(signature) == 1: - return False - signature = signature[0] - else: - signature = signatures - if not public_key.verify(self.serialize(data), signature): - return False - return True + return util.verify_signature(signed_transaction) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) def write_transaction(self, signed_transaction, durability='soft'): diff --git a/bigchaindb/util.py b/bigchaindb/util.py index d8ddc9ce..78644036 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_owners, new_owners, tx_input, operation, payload=None): """Create a new transaction A transaction in the bigchain is a transfer of a digital asset between two entities represented @@ -92,8 +92,8 @@ def create_tx(current_owner, new_owner, tx_input, 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 keys of all current owners of the asset. + new_owners (list): base58 encoded public keys of all new owners of the digital asset. tx_input (str): id of the transaction to use as input. operation (str): Either `CREATE` or `TRANSFER` operation. payload (Optional[dict]): dictionary with information about asset. @@ -124,8 +124,8 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): } tx = { - 'current_owner': current_owner, - 'new_owner': new_owner, + 'current_owners': current_owners if isinstance(current_owners, list) else [current_owners], + 'new_owners': new_owners if isinstance(new_owners, list) else [new_owners], 'input': tx_input, 'operation': operation, 'timestamp': timestamp(), @@ -145,7 +145,7 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None): return transaction -def sign_tx(transaction, private_key): +def sign_tx(transaction, private_key, public_key=None): """Sign a transaction A transaction signed with the `current_owner` corresponding private key. @@ -153,15 +153,29 @@ def sign_tx(transaction, private_key): Args: transaction (dict): transaction to sign. private_key (str): base58 encoded private key to create a signature of the transaction. + public_key (str): (optional) base58 encoded public key to identify each signature of a multisig transaction. Returns: dict: transaction with the `signature` field included. """ private_key = PrivateKey(private_key) - signature = private_key.sign(serialize(transaction)) + if len(transaction['transaction']['current_owners']) == 1: + signatures_updated = private_key.sign(serialize(transaction)) + else: + # multisig, sign for each input and store {pub_key: signature_for_priv_key} + if public_key is None: + raise ValueError('public_key must be provided for signing multisig transactions') + transaction_without_signatures = transaction.copy() + signatures = transaction_without_signatures.pop('signatures') \ + if 'signatures' in transaction_without_signatures else [] + signatures_updated = signatures.copy() + signatures_updated = [s for s in signatures_updated if not s['public_key'] == public_key] + signatures_updated.append({'public_key': public_key, + 'signature': private_key.sign(serialize(transaction_without_signatures))}) + signed_transaction = transaction.copy() - signed_transaction.update({'signature': signature}) + signed_transaction.update({'signatures': signatures_updated}) return signed_transaction @@ -199,10 +213,23 @@ def verify_signature(signed_transaction): if 'assignee' in data: data.pop('assignee') - signature = data.pop('signature') - public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = PublicKey(public_key_base58) - return public_key.verify(serialize(data), signature) + signatures = data.pop('signatures') + for public_key_base58 in signed_transaction['transaction']['current_owners']: + public_key = PublicKey(public_key_base58) + + if isinstance(signatures, list): + try: + signature = [s['signature'] for s in signatures if s['public_key'] == public_key_base58] + except KeyError: + return False + if not len(signature) == 1: + return False + signature = signature[0] + else: + signature = signatures + if not public_key.verify(serialize(data), signature): + return False + return True def transform_create(tx): @@ -215,6 +242,6 @@ def transform_create(tx): payload = None if transaction['data'] and 'payload' in transaction['data']: payload = transaction['data']['payload'] - new_tx = create_tx(b.me, transaction['current_owner'], None, 'CREATE', payload=payload) + new_tx = create_tx(b.me, transaction['current_owners'], None, 'CREATE', payload=payload) return new_tx diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index bf7e5eae..d4b307e9 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -50,8 +50,8 @@ class TestBigchainApi(object): payload = {'cats': 'are awesome'} tx = b.create_transaction('a', 'b', 'c', 'd', payload) tx_calculated = { - 'current_owner': 'a', - 'new_owner': 'b', + 'current_owners': ['a'], + 'new_owners': ['b'], 'input': 'c', 'operation': 'd', 'timestamp': tx['transaction']['timestamp'], @@ -67,7 +67,8 @@ class TestBigchainApi(object): sk, vk = generate_key_pair() tx = b.create_transaction(vk, 'b', 'c', 'd') - assert b.verify_signature(tx) is False + with pytest.raises(KeyError) as excinfo: + b.verify_signature(tx) tx_signed = b.sign_transaction(tx, sk) assert 'signatures' in tx_signed @@ -77,13 +78,18 @@ class TestBigchainApi(object): num_current_owners = 42 sk, vk = [], [] for _ in range(num_current_owners): - sk_, vk_ = b.generate_keys() + sk_, vk_ = generate_key_pair() sk.append(sk_) vk.append(vk_) tx = b.create_transaction(vk, 'b', 'c', 'd') tx_signed = tx + + with pytest.raises(KeyError) as excinfo: + b.verify_signature(tx_signed) + for i in range(num_current_owners): - assert b.verify_signature(tx_signed) is False + if i > 0: + assert b.verify_signature(tx_signed) is False tx_signed = b.sign_transaction(tx_signed, sk[i], vk[i]) assert 'signatures' in tx_signed diff --git a/tests/test_client.py b/tests/test_client.py index f5e15cad..2ea317c1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -39,8 +39,8 @@ def test_client_can_create_assets(mock_requests_post, client): # `current_owner` will be overwritten with the public key of the node in the federation # that will create the real transaction. `signature` will be overwritten with the new signature. # Note that this scenario is ignored by this test. - assert tx['transaction']['current_owner'] == client.public_key - assert tx['transaction']['new_owner'] == client.public_key + assert tx['transaction']['current_owners'] == [client.public_key] + assert tx['transaction']['new_owners'] == [client.public_key] assert tx['transaction']['input'] == None assert util.verify_signature(tx) @@ -51,8 +51,8 @@ def test_client_can_transfer_assets(mock_requests_post, client): tx = client.transfer('a', 123) - assert tx['transaction']['current_owner'] == client.public_key - assert tx['transaction']['new_owner'] == 'a' + assert tx['transaction']['current_owners'] == [client.public_key] + assert tx['transaction']['new_owners'] == ['a'] assert tx['transaction']['input'] == 123 assert util.verify_signature(tx) diff --git a/tests/test_util.py b/tests/test_util.py index f4708b59..1bdae2bd 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,7 +6,7 @@ def test_transform_create(b, user_private_key, user_public_key): tx = util.transform_create(tx) tx = util.sign_tx(tx, b.me_private) - assert tx['transaction']['current_owner'] == b.me - assert tx['transaction']['new_owner'] == user_public_key + assert tx['transaction']['current_owners'] == [b.me] + assert tx['transaction']['new_owners'] == [user_public_key] assert util.verify_signature(tx) diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 04a1c292..cfbf4890 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -22,8 +22,8 @@ def test_post_create_transaction_endpoint(b, client): tx = util.create_and_sign_tx(keypair[0], keypair[1], keypair[1], None, 'CREATE') res = client.post(TX_ENDPOINT, data=json.dumps(tx)) - assert res.json['transaction']['current_owner'] == b.me - assert res.json['transaction']['new_owner'] == keypair[1] + assert res.json['transaction']['current_owners'] == [b.me] + assert res.json['transaction']['new_owners'] == [keypair[1]] def test_post_transfer_transaction_endpoint(b, client): @@ -37,6 +37,6 @@ def test_post_transfer_transaction_endpoint(b, client): transfer = util.create_and_sign_tx(from_keypair[0], from_keypair[1], to_keypair[1], tx_id) res = client.post(TX_ENDPOINT, data=json.dumps(transfer)) - assert res.json['transaction']['current_owner'] == from_keypair[1] - assert res.json['transaction']['new_owner'] == to_keypair[1] + assert res.json['transaction']['current_owners'] == [from_keypair[1]] + assert res.json['transaction']['new_owners'] == [to_keypair[1]]