diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 3b2a29d1..8b624a7b 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -68,21 +68,21 @@ class Bigchain(object): return r.connect(host=self.host, port=self.port, db=self.dbname) @monitor.timer('create_transaction', rate=bigchaindb.config['statsd']['rate']) - def create_transaction(self, current_owner, new_owner, tx_input, operation, payload=None): + def create_transaction(self, current_owners, new_owners, tx_input, operation, payload=None): """Create a new transaction Refer to the documentation of ``bigchaindb.util.create_tx`` """ - 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): + def sign_transaction(self, transaction, private_key, public_key=None): """Sign a transaction Refer to the documentation of ``bigchaindb.util.sign_tx`` """ - return util.sign_tx(transaction, private_key) + return util.sign_tx(transaction, private_key, public_key) def verify_signature(self, signed_transaction): """Verify the signature of a transaction. @@ -90,16 +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') - - signature = data.pop('signature') - public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = crypto.PublicKey(public_key_base58) - return public_key.verify(util.serialize(data), signature) + return util.verify_signature(signed_transaction) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) def write_transaction(self, signed_transaction, durability='soft'): @@ -109,7 +100,7 @@ class Bigchain(object): it has been validated by the nodes of the federation. Args: - singed_transaction (dict): transaction with the `signature` included. + signed_transaction (dict): transaction with the `signature` included. Returns: dict: database response @@ -222,9 +213,10 @@ class Bigchain(object): list: list of `txids` currently owned by `owner` """ + # TODO: fix for multisig. new_owners is a list! response = r.table('bigchain')\ .concat_map(lambda doc: doc['block']['transactions'])\ - .filter({'transaction': {'new_owner': owner}})\ + .filter({'transaction': {'new_owners': owner if isinstance(owner, list) else [owner]}})\ .pluck('id')['id']\ .run(self.conn) owned = [] @@ -261,7 +253,7 @@ class Bigchain(object): if transaction['transaction']['operation'] == 'CREATE': if transaction['transaction']['input']: raise ValueError('A CREATE operation has no inputs') - if transaction['transaction']['current_owner'] not in self.federation_nodes + [self.me]: + if not(set(transaction['transaction']['current_owners']) <= set(self.federation_nodes + [self.me])): raise exceptions.OperationError('Only federation nodes can use the operation `CREATE`') else: @@ -274,9 +266,9 @@ class Bigchain(object): raise exceptions.TransactionDoesNotExist('input `{}` does not exist in the bigchain'.format( transaction['transaction']['input'])) - if tx_input['transaction']['new_owner'] != transaction['transaction']['current_owner']: + if tx_input['transaction']['new_owners'] != transaction['transaction']['current_owners']: raise exceptions.TransactionOwnerError('current_owner `{}` does not own the input `{}`'.format( - transaction['transaction']['current_owner'], transaction['transaction']['input'])) + transaction['transaction']['current_owners'], transaction['transaction']['input'])) # check if the input was already spent by a transaction other then this one. spent = self.get_spent(tx_input['id']) 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 1fed8800..d4b307e9 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -27,19 +27,31 @@ class TestBigchainApi(object): tx = b.create_transaction('a', 'b', 'c', 'd') assert sorted(tx) == sorted(['id', 'transaction']) - assert sorted(tx['transaction']) == sorted(['current_owner', 'new_owner', 'input', 'operation', + assert sorted(tx['transaction']) == sorted(['current_owners', 'new_owners', 'input', 'operation', 'timestamp', 'data']) def test_create_transaction_with_unsupported_payload_raises(self, b): with pytest.raises(TypeError): b.create_transaction('a', 'b', 'c', 'd', payload=[]) + def test_create_transaction_with_multiple_owners(self, b): + num_current_owners = 42 + num_new_owners = 73 + tx = b.create_transaction(['a']*num_current_owners, ['b']*num_new_owners, 'd', 'e') + + assert sorted(tx) == sorted(['id', 'transaction']) + assert sorted(tx['transaction']) == sorted(['current_owners', 'new_owners', 'input', 'operation', + 'timestamp', 'data']) + + assert len(tx['transaction']['current_owners']) == num_current_owners + assert len(tx['transaction']['new_owners']) == num_new_owners + def test_transaction_hash(self, b): 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'], @@ -54,9 +66,36 @@ class TestBigchainApi(object): def test_transaction_signature(self, b): sk, vk = generate_key_pair() tx = b.create_transaction(vk, 'b', 'c', 'd') + + with pytest.raises(KeyError) as excinfo: + b.verify_signature(tx) tx_signed = b.sign_transaction(tx, sk) - assert 'signature' in tx_signed + assert 'signatures' in tx_signed + assert b.verify_signature(tx_signed) + + def test_transaction_signature_multiple_owners(self, b): + num_current_owners = 42 + sk, vk = [], [] + for _ in range(num_current_owners): + 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): + 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 + assert 'public_key' in tx_signed['signatures'][0] + assert 'signature' in tx_signed['signatures'][0] + assert len(tx_signed['signatures']) == num_current_owners assert b.verify_signature(tx_signed) def test_serializer(self, b): @@ -264,7 +303,7 @@ class TestTransactionValidation(object): with pytest.raises(exceptions.TransactionOwnerError) as excinfo: b.validate_transaction(tx) - assert excinfo.value.args[0] == 'current_owner `a` does not own the input `{}`'.format(valid_input) + assert excinfo.value.args[0] == 'current_owner `[\'a\']` does not own the input `{}`'.format(valid_input) assert b.is_valid_transaction(tx) is False @pytest.mark.usefixtures('inputs') 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]]