From 30a68e483f218a9cdcab62a36f5cc2ab39877620 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 24 Feb 2016 19:38:52 +0100 Subject: [PATCH 01/11] bigchain/core: multisig interledger: user, escrow, connector --- bigchaindb/core.py | 65 ++++++++++----- interledger/__init__.py | 0 interledger/core.py | 120 ++++++++++++++++++++++++++++ interledger/tests/__init__.py | 0 interledger/tests/bigchain.json | 1 + interledger/tests/megachain.json | 1 + interledger/tests/test_connector.py | 53 ++++++++++++ 7 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 interledger/__init__.py create mode 100644 interledger/core.py create mode 100644 interledger/tests/__init__.py create mode 100644 interledger/tests/bigchain.json create mode 100644 interledger/tests/megachain.json create mode 100644 interledger/tests/test_connector.py diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 76d90a58..f82a8ce6 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -68,7 +68,7 @@ class Bigchain(object): def reconnect(self): return r.connect(host=self.host, port=self.port, db=self.dbname) - 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 A transaction in the bigchain is a transfer of a digital asset between two entities represented @@ -84,8 +84,8 @@ class Bigchain(object): `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. @@ -115,8 +115,8 @@ class Bigchain(object): } 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': self.timestamp(), @@ -135,7 +135,7 @@ class Bigchain(object): return transaction - def sign_transaction(self, transaction, private_key): + def sign_transaction(self, transaction, private_key, public_key=None): """Sign a transaction A transaction signed with the `current_owner` corresponding private key. @@ -143,15 +143,28 @@ class Bigchain(object): 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(self.serialize(transaction)) + 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({'signature': signature}) + signed_transaction.update({'signatures': signatures_updated}) return signed_transaction def verify_signature(self, signed_transaction): @@ -172,10 +185,23 @@ class Bigchain(object): 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(self.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(self.serialize(data), signature): + return False + return True def write_transaction(self, signed_transaction): """Write the transaction to bigchain. @@ -184,7 +210,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 @@ -299,9 +325,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 = [] @@ -336,7 +363,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: @@ -349,9 +376,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 spent = self.get_spent(tx_input['id']) @@ -496,13 +523,11 @@ class Bigchain(object): # 2. create the block with one transaction # 3. write the block to the bigchain - blocks_count = r.table('bigchain').count().run(self.conn) if blocks_count: raise GenesisBlockAlreadyExistsError('Cannot create the Genesis block') - payload = {'message': 'Hello World from the Bigchain'} transaction = self.create_transaction(self.me, self.me, None, 'GENESIS', payload=payload) transaction_signed = self.sign_transaction(transaction, self.me_private) diff --git a/interledger/__init__.py b/interledger/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/interledger/core.py b/interledger/core.py new file mode 100644 index 00000000..598038d2 --- /dev/null +++ b/interledger/core.py @@ -0,0 +1,120 @@ + + +class User: + def __init__(self, ledger): + self.ledger = ledger + self.private, self.public = ledger.generate_keys() + self.assets = [] + + def create_asset(self): + tx = self.ledger.create_transaction(self.ledger.me, self.public, None, 'CREATE') + tx_signed = self.ledger.sign_transaction(tx, self.ledger.me_private) + self.ledger.validate_transaction(tx_signed) + self.ledger.write_transaction(tx_signed) + self.assets.append(tx_signed) + + def create_assets(self, amount=1): + for i in range(amount): + self.create_asset() + + +class Escrow(User): + def __init__(self, ledger=None, current_owner=None, new_owner=None, + asset_id=None, condition_func=None, payload=None): + User.__init__(self, ledger) + self.condition_func = condition_func if condition_func else lambda proof: True + self.new_owner = new_owner + tx = self.ledger.create_transaction(current_owner, + [current_owner, self.public], + asset_id, + 'TRANSFER', + payload) + self.assets = tx + + def release(self, receipt=None): + if not self.validate(receipt): + raise Exception + tx = self.ledger.create_transaction(self.assets['transaction']['new_owners'], + self.new_owner, + self.assets['id'], + 'TRANSFER', + self.assets['transaction']['data']['payload']) + return self.ledger.sign_transaction(tx, self.private, self.public) + + def validate(self, receipt): + return self.condition_func(receipt) + + +class LedgerConnection(User): + def __init__(self, ledger): + self._escrow = None + User.__init__(self, ledger) + + def escrow(self, current_owner=None, new_owner=None, condition_func=None, asset_id=None, payload=None): + self._escrow = Escrow(ledger=self.ledger, + current_owner=current_owner if current_owner else self.public, + new_owner=new_owner if new_owner else self.public, + asset_id=asset_id if asset_id else self.assets[0]['id'], + condition_func=condition_func, + payload=payload) + if not current_owner: + tx_connector_signed = self.ledger.sign_transaction(self._escrow.assets, self.private) + self.ledger.validate_transaction(tx_connector_signed) + self._escrow.assets = tx_connector_signed + self.ledger.write_transaction(tx_connector_signed) + + def release(self, condition): + return self._escrow.release(condition) + + +class Connector: + def __init__(self, ledger=None): + self.ledger_connections = [] + if ledger: + self.add_ledger(ledger) + + def public(self, ledger=None): + return self.get_ledger_connection(ledger).public if self.get_ledger_connection(ledger) else None + + def private(self, ledger=None): + return self.get_ledger_connection(ledger).private if self.get_ledger_connection(ledger) else None + + def get_assets(self, ledger=None): + ledger_connection = self.get_ledger_connection(ledger) + return ledger_connection.assets if ledger_connection else None + + def create_assets(self, amount=1, ledger=None): + ledger_connection = self.get_ledger_connection(ledger) + if ledger_connection: + ledger_connection.create_assets(amount) + + def get_ledger_connection(self, ledger=None): + if not ledger: + return self.ledger_connections[0] + # TODO: yield + ledger_connection = [l for l in self.ledger_connections if l.ledger == ledger] + return ledger_connection[0] if ledger_connection else None + + def add_ledger(self, ledger): + if self.can_add_ledger_connection(ledger): + self.ledger_connections.append(LedgerConnection(ledger)) + + def can_add_ledger_connection(self, ledger): + return False if self.get_ledger_connection(ledger) else True + + def connect(self, user_from=None, ledger_from=None, user_to=None, ledger_to=None, + condition_func=None, asset_id=None, payload=None): + connection_from = self.get_ledger_connection(ledger_from) + connection_to = self.get_ledger_connection(ledger_to) + connection_from.escrow(current_owner=user_from, + condition_func=condition_func, + asset_id=asset_id, + payload=payload) + connection_to.escrow(new_owner=user_to, + condition_func=condition_func, + payload=payload) + return connection_from._escrow.assets + + def release(self, ledger=None, receipt=None): + connection = self.get_ledger_connection(ledger) + return connection.release(receipt) \ No newline at end of file diff --git a/interledger/tests/__init__.py b/interledger/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/interledger/tests/bigchain.json b/interledger/tests/bigchain.json new file mode 100644 index 00000000..0ed6b50a --- /dev/null +++ b/interledger/tests/bigchain.json @@ -0,0 +1 @@ +{"database": {"host": "localhost", "name": "bigchain", "port": 28015}, "keyring": [], "keypair": {"public": "jhpCiHPiMmHGFQvjqn1LdLddYhYFd63ywUbi3tFCW31f", "private": "5vbka7oLMf2UTNV1sq3nyKJXTERCPWvD8Pf4KnGYNBtA"}} \ No newline at end of file diff --git a/interledger/tests/megachain.json b/interledger/tests/megachain.json new file mode 100644 index 00000000..fe0c6a4d --- /dev/null +++ b/interledger/tests/megachain.json @@ -0,0 +1 @@ +{"keyring": [], "database": {"host": "localhost", "port": 28015, "name": "megachain"}, "keypair": {"private": "DGKiLSdLsZofmzZMv3B9KZVj79wrq4PZ3PsMmk8Hu8KK", "public": "gMGJ9j15qyBVC1t8ceiDgJLLaRBgvrFchyzB1GgWk3Fa"}} \ No newline at end of file diff --git a/interledger/tests/test_connector.py b/interledger/tests/test_connector.py new file mode 100644 index 00000000..79b21750 --- /dev/null +++ b/interledger/tests/test_connector.py @@ -0,0 +1,53 @@ +import json +from time import sleep + +from bigchaindb import Bigchain +from interledger.core import User, Connector + +config_bigchain = json.load(open('interledger/tests/bigchain.json', 'r')) +config_megachain = json.load(open('interledger/tests/megachain.json', 'r')) + +bigchain = Bigchain(dbname=config_bigchain['database']['name'], + public_key=config_bigchain['keypair']['public'], + private_key=config_bigchain['keypair']['private']) + +megachain = Bigchain(dbname=config_megachain['database']['name'], + public_key=config_megachain['keypair']['public'], + private_key=config_megachain['keypair']['private']) + +alice = User(bigchain) +bob = User(megachain) + +connector = Connector(bigchain) +connector.add_ledger(megachain) + +# create assets +alice.create_assets(amount=2) +connector.create_assets(amount=2, ledger=bigchain) +connector.create_assets(amount=2, ledger=megachain) + +sleep(6) +# transfer asset to escrow +tx_alice = connector.connect(user_from=alice.public, + ledger_from=alice.ledger, + user_to=bob.public, + ledger_to=bob.ledger, + condition_func=lambda proof: True, + asset_id=alice.assets[0]['id'], + payload={'what': 'ever'}) + +tx_alice_signed = alice.ledger.sign_transaction(tx_alice, alice.private) +alice.ledger.validate_transaction(tx_alice_signed) +alice.ledger.write_transaction(tx_alice_signed) + +sleep(6) +# release asset from escrow +tx_bob = connector.release(ledger=bob.ledger, receipt=None) +tx_bob_signed = bob.ledger.sign_transaction(tx_bob, connector.private(bob.ledger), connector.public(bob.ledger)) +bob.ledger.validate_transaction(tx_bob_signed) +bob.ledger.write_transaction(tx_bob_signed) + +tx_connector = connector.release(ledger=alice.ledger, receipt=None) +tx_connector_signed = alice.ledger.sign_transaction(tx_connector, alice.private, alice.public) +alice.ledger.validate_transaction(tx_connector_signed) +alice.ledger.write_transaction(tx_connector_signed) From 21baf7d5767e0ff1b8568bad426cc095c1063712 Mon Sep 17 00:00:00 2001 From: Dimitri De Jonghe Date: Thu, 25 Feb 2016 10:22:28 +0100 Subject: [PATCH 02/11] Update README.md --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 948b879c..34deba89 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# BigchainDB +# BigchainDB - Interledger fork [![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) @@ -6,11 +6,26 @@ [![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/develop.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=develop) [![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=develop)](http://bigchaindb.readthedocs.org/en/develop/?badge=develop) +This fork provides basic functionality for supporting the interledger protocol, see http://interledger.org/ + +The edits are found under interledger/core.py and interledger/tests/test_connector.py +To run the interledger test, [install bigchaindb](#gettingstarted) +``` +``` + +- [x] multisig +- [x] escrow +- [x] connectors +- [ ] signed receipts +- [ ] receipt propagation and listeners +- [ ] proper asset conversion +- [ ] multi-(big)chain(db) instantiation + network path optimization + ## Documentation Documentation is available at https://bigchaindb.readthedocs.org/ -## Getting started +## Getting started ### Install RethinkDB From a683a4283e8ec05d2ab9441219bf93f9871841b1 Mon Sep 17 00:00:00 2001 From: Dimitri De Jonghe Date: Thu, 25 Feb 2016 10:24:52 +0100 Subject: [PATCH 03/11] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 34deba89..e55af809 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ This fork provides basic functionality for supporting the interledger protocol, The edits are found under interledger/core.py and interledger/tests/test_connector.py To run the interledger test, [install bigchaindb](#gettingstarted) ``` +$0> rethinkdb & +$1> bigchaindb -c interledger/tests/bigchain.json start +$2> bigchaindb -c interledger/tests/megachain.json start +$3> python3 interledger/tests/test_connector.py ``` - [x] multisig @@ -19,13 +23,15 @@ To run the interledger test, [install bigchaindb](#gettingstarted) - [ ] signed receipts - [ ] receipt propagation and listeners - [ ] proper asset conversion +- [ ] RESTful API wrapper - [ ] multi-(big)chain(db) instantiation + network path optimization + ## Documentation Documentation is available at https://bigchaindb.readthedocs.org/ -## Getting started +## Getting started ### Install RethinkDB From 90ab22047a6104d0cf13cd1361847896852e3545 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 2 Mar 2016 17:55:03 +0100 Subject: [PATCH 04/11] fix multisig test owner => owners --- tests/db/test_bigchain_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index f956f1e2..ddbdcd24 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -52,7 +52,7 @@ 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): From 3d423ba44bdf4d067b4f3cd0460a6a2003cb7ab9 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 2 Mar 2016 18:05:16 +0100 Subject: [PATCH 05/11] fix tests for multisig --- tests/db/test_bigchain_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index ddbdcd24..aadf9147 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -81,7 +81,7 @@ class TestBigchainApi(object): tx = b.create_transaction(vk, 'b', 'c', 'd') 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_serializer(self, b): @@ -289,7 +289,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') From 516cf2791d63f13e89bde87ceb3be516f3cf846a Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 2 Mar 2016 18:15:27 +0100 Subject: [PATCH 06/11] reorganized under examples --- README.md | 6 +-- {interledger => examples}/__init__.py | 0 examples/accounts.py | 45 ++++++++++++++++++ .../interledger}/__init__.py | 0 .../interledger/connector.py | 46 +------------------ examples/interledger/tests/__init__.py | 0 .../interledger}/tests/bigchain.json | 0 .../interledger}/tests/megachain.json | 0 .../interledger}/tests/test_connector.py | 7 +-- 9 files changed, 53 insertions(+), 51 deletions(-) rename {interledger => examples}/__init__.py (100%) create mode 100644 examples/accounts.py rename {interledger/tests => examples/interledger}/__init__.py (100%) rename interledger/core.py => examples/interledger/connector.py (63%) create mode 100644 examples/interledger/tests/__init__.py rename {interledger => examples/interledger}/tests/bigchain.json (100%) rename {interledger => examples/interledger}/tests/megachain.json (100%) rename {interledger => examples/interledger}/tests/test_connector.py (90%) diff --git a/README.md b/README.md index 9d220d16..baedc2e9 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ The edits are found under interledger/core.py and interledger/tests/test_connect To run the interledger test, [install bigchaindb](#gettingstarted) ``` $0> rethinkdb & -$1> bigchaindb -c interledger/tests/bigchain.json start -$2> bigchaindb -c interledger/tests/megachain.json start -$3> python3 interledger/tests/test_connector.py +$1> bigchaindb -c examples/interledger/tests/bigchain.json start +$2> bigchaindb -c examples/interledger/tests/megachain.json start +$3> python3 examples/interledger/tests/test_connector.py ``` - [x] multisig diff --git a/interledger/__init__.py b/examples/__init__.py similarity index 100% rename from interledger/__init__.py rename to examples/__init__.py diff --git a/examples/accounts.py b/examples/accounts.py new file mode 100644 index 00000000..daebb006 --- /dev/null +++ b/examples/accounts.py @@ -0,0 +1,45 @@ + + +class User: + def __init__(self, ledger): + self.ledger = ledger + self.private, self.public = ledger.generate_keys() + self.assets = [] + + def create_asset(self): + tx = self.ledger.create_transaction(self.ledger.me, self.public, None, 'CREATE') + tx_signed = self.ledger.sign_transaction(tx, self.ledger.me_private) + self.ledger.validate_transaction(tx_signed) + self.ledger.write_transaction(tx_signed) + self.assets.append(tx_signed) + + def create_assets(self, amount=1): + for i in range(amount): + self.create_asset() + + +class Escrow(User): + def __init__(self, ledger=None, current_owner=None, new_owner=None, + asset_id=None, condition_func=None, payload=None): + User.__init__(self, ledger) + self.condition_func = condition_func if condition_func else lambda proof: True + self.new_owner = new_owner + tx = self.ledger.create_transaction(current_owner, + [current_owner, self.public], + asset_id, + 'TRANSFER', + payload) + self.assets = tx + + def release(self, receipt=None): + if not self.validate(receipt): + raise Exception + tx = self.ledger.create_transaction(self.assets['transaction']['new_owners'], + self.new_owner, + self.assets['id'], + 'TRANSFER', + self.assets['transaction']['data']['payload']) + return self.ledger.sign_transaction(tx, self.private, self.public) + + def validate(self, receipt): + return self.condition_func(receipt) diff --git a/interledger/tests/__init__.py b/examples/interledger/__init__.py similarity index 100% rename from interledger/tests/__init__.py rename to examples/interledger/__init__.py diff --git a/interledger/core.py b/examples/interledger/connector.py similarity index 63% rename from interledger/core.py rename to examples/interledger/connector.py index 598038d2..5d26a6b6 100644 --- a/interledger/core.py +++ b/examples/interledger/connector.py @@ -1,48 +1,4 @@ - - -class User: - def __init__(self, ledger): - self.ledger = ledger - self.private, self.public = ledger.generate_keys() - self.assets = [] - - def create_asset(self): - tx = self.ledger.create_transaction(self.ledger.me, self.public, None, 'CREATE') - tx_signed = self.ledger.sign_transaction(tx, self.ledger.me_private) - self.ledger.validate_transaction(tx_signed) - self.ledger.write_transaction(tx_signed) - self.assets.append(tx_signed) - - def create_assets(self, amount=1): - for i in range(amount): - self.create_asset() - - -class Escrow(User): - def __init__(self, ledger=None, current_owner=None, new_owner=None, - asset_id=None, condition_func=None, payload=None): - User.__init__(self, ledger) - self.condition_func = condition_func if condition_func else lambda proof: True - self.new_owner = new_owner - tx = self.ledger.create_transaction(current_owner, - [current_owner, self.public], - asset_id, - 'TRANSFER', - payload) - self.assets = tx - - def release(self, receipt=None): - if not self.validate(receipt): - raise Exception - tx = self.ledger.create_transaction(self.assets['transaction']['new_owners'], - self.new_owner, - self.assets['id'], - 'TRANSFER', - self.assets['transaction']['data']['payload']) - return self.ledger.sign_transaction(tx, self.private, self.public) - - def validate(self, receipt): - return self.condition_func(receipt) +from examples.accounts import User, Escrow class LedgerConnection(User): diff --git a/examples/interledger/tests/__init__.py b/examples/interledger/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/interledger/tests/bigchain.json b/examples/interledger/tests/bigchain.json similarity index 100% rename from interledger/tests/bigchain.json rename to examples/interledger/tests/bigchain.json diff --git a/interledger/tests/megachain.json b/examples/interledger/tests/megachain.json similarity index 100% rename from interledger/tests/megachain.json rename to examples/interledger/tests/megachain.json diff --git a/interledger/tests/test_connector.py b/examples/interledger/tests/test_connector.py similarity index 90% rename from interledger/tests/test_connector.py rename to examples/interledger/tests/test_connector.py index 79b21750..701f8b8b 100644 --- a/interledger/tests/test_connector.py +++ b/examples/interledger/tests/test_connector.py @@ -2,10 +2,11 @@ import json from time import sleep from bigchaindb import Bigchain -from interledger.core import User, Connector +from examples.accounts import User +from examples.interledger.connector import Connector -config_bigchain = json.load(open('interledger/tests/bigchain.json', 'r')) -config_megachain = json.load(open('interledger/tests/megachain.json', 'r')) +config_bigchain = json.load(open('bigchain.json', 'r')) +config_megachain = json.load(open('megachain.json', 'r')) bigchain = Bigchain(dbname=config_bigchain['database']['name'], public_key=config_bigchain['keypair']['public'], From 8a9ea97b69a0364c580cb6458db3c8011de8d9d2 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 2 Mar 2016 18:26:33 +0100 Subject: [PATCH 07/11] update readme --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index baedc2e9..8b5c9e8b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ -# BigchainDB - Interledger fork +# BigchainDB - Integration Examples A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whitepaper/) explains what that means. [![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) -[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/develop.svg)](https://travis-ci.org/bigchaindb/bigchaindb) -[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/develop.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=develop) -[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=develop)](http://bigchaindb.readthedocs.org/en/develop/?badge=develop) +[![Travis branch](https://img.shields.io/travis/diminator/bigchaindb/develop.svg)](https://travis-ci.org/diminator/bigchaindb) +[![Codecov branch](https://img.shields.io/codecov/c/github/diminator/bigchaindb/develop.svg)](https://codecov.io/github/diminator/bigchaindb?branch=develop) +## Interledger This fork provides basic functionality for supporting the interledger protocol, see http://interledger.org/ From 9a1a3bdc2d53c2820f1b7788f99a4b7a58183f3e Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 2 Mar 2016 18:36:02 +0100 Subject: [PATCH 08/11] restructure example --- README.md | 9 ++++++--- examples/interledger/{tests => }/bigchain.json | 0 examples/interledger/{tests => }/megachain.json | 0 ...t_connector.py => run_cross_ledger_payment_simple.py} | 0 4 files changed, 6 insertions(+), 3 deletions(-) rename examples/interledger/{tests => }/bigchain.json (100%) rename examples/interledger/{tests => }/megachain.json (100%) rename examples/interledger/{tests/test_connector.py => run_cross_ledger_payment_simple.py} (100%) diff --git a/README.md b/README.md index 8b5c9e8b..99c0952f 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,12 @@ The edits are found under interledger/core.py and interledger/tests/test_connect To run the interledger test, [install bigchaindb](#gettingstarted) ``` $0> rethinkdb & -$1> bigchaindb -c examples/interledger/tests/bigchain.json start -$2> bigchaindb -c examples/interledger/tests/megachain.json start -$3> python3 examples/interledger/tests/test_connector.py + +$1> bigchaindb -c examples/interledger/bigchain.json start +$2> bigchaindb -c examples/interledger/megachain.json start + +$3> cd examples/interledger +$3> python3 run_cross_ledger_payment_simple.py ``` - [x] multisig diff --git a/examples/interledger/tests/bigchain.json b/examples/interledger/bigchain.json similarity index 100% rename from examples/interledger/tests/bigchain.json rename to examples/interledger/bigchain.json diff --git a/examples/interledger/tests/megachain.json b/examples/interledger/megachain.json similarity index 100% rename from examples/interledger/tests/megachain.json rename to examples/interledger/megachain.json diff --git a/examples/interledger/tests/test_connector.py b/examples/interledger/run_cross_ledger_payment_simple.py similarity index 100% rename from examples/interledger/tests/test_connector.py rename to examples/interledger/run_cross_ledger_payment_simple.py From dca74116aba80376c8cb9d16f603b6f0afc15cb3 Mon Sep 17 00:00:00 2001 From: diminator Date: Thu, 3 Mar 2016 11:59:03 +0100 Subject: [PATCH 09/11] added tests for multisig fixed bug in validate_transaction if no signature --- bigchaindb/core.py | 2 ++ tests/db/test_bigchain_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 14892e95..dfc363b0 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -189,6 +189,8 @@ class Bigchain(object): # if assignee field in the transaction, remove it if 'assignee' in data: data.pop('assignee') + if 'signatures' not in data: + return False signatures = data.pop('signatures') for public_key_base58 in signed_transaction['transaction']['current_owners']: diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index aadf9147..1af1d874 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -59,6 +59,18 @@ class TestBigchainApi(object): 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) @@ -79,11 +91,32 @@ class TestBigchainApi(object): def test_transaction_signature(self, b): sk, vk = b.generate_keys() tx = b.create_transaction(vk, 'b', 'c', 'd') + + assert b.verify_signature(tx) is False tx_signed = b.sign_transaction(tx, sk) 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_ = b.generate_keys() + sk.append(sk_) + vk.append(vk_) + tx = b.create_transaction(vk, 'b', 'c', 'd') + tx_signed = tx + for i in range(num_current_owners): + 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): tx = b.create_transaction('a', 'b', 'c', 'd') assert b.deserialize(b.serialize(tx)) == tx From f42f79e3f136827236390c8e05ad5d372d5db22d Mon Sep 17 00:00:00 2001 From: diminator Date: Thu, 3 Mar 2016 12:35:15 +0100 Subject: [PATCH 10/11] only multisig changes --- README.md | 36 ++------- examples/__init__.py | 0 examples/accounts.py | 45 ----------- examples/interledger/__init__.py | 0 examples/interledger/bigchain.json | 1 - examples/interledger/connector.py | 76 ------------------- examples/interledger/megachain.json | 1 - .../run_cross_ledger_payment_simple.py | 54 ------------- examples/interledger/tests/__init__.py | 0 9 files changed, 6 insertions(+), 207 deletions(-) delete mode 100644 examples/__init__.py delete mode 100644 examples/accounts.py delete mode 100644 examples/interledger/__init__.py delete mode 100644 examples/interledger/bigchain.json delete mode 100644 examples/interledger/connector.py delete mode 100644 examples/interledger/megachain.json delete mode 100644 examples/interledger/run_cross_ledger_payment_simple.py delete mode 100644 examples/interledger/tests/__init__.py diff --git a/README.md b/README.md index 99c0952f..86a198e0 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,14 @@ -# BigchainDB - Integration Examples +# BigchainDB A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whitepaper/) explains what that means. [![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Travis branch](https://img.shields.io/travis/diminator/bigchaindb/develop.svg)](https://travis-ci.org/diminator/bigchaindb) -[![Codecov branch](https://img.shields.io/codecov/c/github/diminator/bigchaindb/develop.svg)](https://codecov.io/github/diminator/bigchaindb?branch=develop) +[![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) +[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/develop.svg)](https://travis-ci.org/bigchaindb/bigchaindb) +[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/develop.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=develop) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=develop)](http://bigchaindb.readthedocs.org/en/develop/?badge=develop) -## Interledger - -This fork provides basic functionality for supporting the interledger protocol, see http://interledger.org/ - -The edits are found under interledger/core.py and interledger/tests/test_connector.py -To run the interledger test, [install bigchaindb](#gettingstarted) -``` -$0> rethinkdb & - -$1> bigchaindb -c examples/interledger/bigchain.json start -$2> bigchaindb -c examples/interledger/megachain.json start - -$3> cd examples/interledger -$3> python3 run_cross_ledger_payment_simple.py -``` - -- [x] multisig -- [x] escrow -- [x] connectors -- [ ] signed receipts -- [ ] receipt propagation and listeners -- [ ] proper asset conversion -- [ ] RESTful API wrapper -- [ ] multi-(big)chain(db) instantiation + network path optimization - - -## Quick Start +## Quick Start ### [Install & Run BigchainDB](http://bigchaindb.readthedocs.org/en/develop/installing.html) ### [Run BigchainDB with Docker](http://bigchaindb.readthedocs.org/en/develop/installing.html#run-bigchaindb-with-docker) diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/accounts.py b/examples/accounts.py deleted file mode 100644 index daebb006..00000000 --- a/examples/accounts.py +++ /dev/null @@ -1,45 +0,0 @@ - - -class User: - def __init__(self, ledger): - self.ledger = ledger - self.private, self.public = ledger.generate_keys() - self.assets = [] - - def create_asset(self): - tx = self.ledger.create_transaction(self.ledger.me, self.public, None, 'CREATE') - tx_signed = self.ledger.sign_transaction(tx, self.ledger.me_private) - self.ledger.validate_transaction(tx_signed) - self.ledger.write_transaction(tx_signed) - self.assets.append(tx_signed) - - def create_assets(self, amount=1): - for i in range(amount): - self.create_asset() - - -class Escrow(User): - def __init__(self, ledger=None, current_owner=None, new_owner=None, - asset_id=None, condition_func=None, payload=None): - User.__init__(self, ledger) - self.condition_func = condition_func if condition_func else lambda proof: True - self.new_owner = new_owner - tx = self.ledger.create_transaction(current_owner, - [current_owner, self.public], - asset_id, - 'TRANSFER', - payload) - self.assets = tx - - def release(self, receipt=None): - if not self.validate(receipt): - raise Exception - tx = self.ledger.create_transaction(self.assets['transaction']['new_owners'], - self.new_owner, - self.assets['id'], - 'TRANSFER', - self.assets['transaction']['data']['payload']) - return self.ledger.sign_transaction(tx, self.private, self.public) - - def validate(self, receipt): - return self.condition_func(receipt) diff --git a/examples/interledger/__init__.py b/examples/interledger/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/interledger/bigchain.json b/examples/interledger/bigchain.json deleted file mode 100644 index 0ed6b50a..00000000 --- a/examples/interledger/bigchain.json +++ /dev/null @@ -1 +0,0 @@ -{"database": {"host": "localhost", "name": "bigchain", "port": 28015}, "keyring": [], "keypair": {"public": "jhpCiHPiMmHGFQvjqn1LdLddYhYFd63ywUbi3tFCW31f", "private": "5vbka7oLMf2UTNV1sq3nyKJXTERCPWvD8Pf4KnGYNBtA"}} \ No newline at end of file diff --git a/examples/interledger/connector.py b/examples/interledger/connector.py deleted file mode 100644 index 5d26a6b6..00000000 --- a/examples/interledger/connector.py +++ /dev/null @@ -1,76 +0,0 @@ -from examples.accounts import User, Escrow - - -class LedgerConnection(User): - def __init__(self, ledger): - self._escrow = None - User.__init__(self, ledger) - - def escrow(self, current_owner=None, new_owner=None, condition_func=None, asset_id=None, payload=None): - self._escrow = Escrow(ledger=self.ledger, - current_owner=current_owner if current_owner else self.public, - new_owner=new_owner if new_owner else self.public, - asset_id=asset_id if asset_id else self.assets[0]['id'], - condition_func=condition_func, - payload=payload) - if not current_owner: - tx_connector_signed = self.ledger.sign_transaction(self._escrow.assets, self.private) - self.ledger.validate_transaction(tx_connector_signed) - self._escrow.assets = tx_connector_signed - self.ledger.write_transaction(tx_connector_signed) - - def release(self, condition): - return self._escrow.release(condition) - - -class Connector: - def __init__(self, ledger=None): - self.ledger_connections = [] - if ledger: - self.add_ledger(ledger) - - def public(self, ledger=None): - return self.get_ledger_connection(ledger).public if self.get_ledger_connection(ledger) else None - - def private(self, ledger=None): - return self.get_ledger_connection(ledger).private if self.get_ledger_connection(ledger) else None - - def get_assets(self, ledger=None): - ledger_connection = self.get_ledger_connection(ledger) - return ledger_connection.assets if ledger_connection else None - - def create_assets(self, amount=1, ledger=None): - ledger_connection = self.get_ledger_connection(ledger) - if ledger_connection: - ledger_connection.create_assets(amount) - - def get_ledger_connection(self, ledger=None): - if not ledger: - return self.ledger_connections[0] - # TODO: yield - ledger_connection = [l for l in self.ledger_connections if l.ledger == ledger] - return ledger_connection[0] if ledger_connection else None - - def add_ledger(self, ledger): - if self.can_add_ledger_connection(ledger): - self.ledger_connections.append(LedgerConnection(ledger)) - - def can_add_ledger_connection(self, ledger): - return False if self.get_ledger_connection(ledger) else True - - def connect(self, user_from=None, ledger_from=None, user_to=None, ledger_to=None, - condition_func=None, asset_id=None, payload=None): - connection_from = self.get_ledger_connection(ledger_from) - connection_to = self.get_ledger_connection(ledger_to) - connection_from.escrow(current_owner=user_from, - condition_func=condition_func, - asset_id=asset_id, - payload=payload) - connection_to.escrow(new_owner=user_to, - condition_func=condition_func, - payload=payload) - return connection_from._escrow.assets - - def release(self, ledger=None, receipt=None): - connection = self.get_ledger_connection(ledger) - return connection.release(receipt) \ No newline at end of file diff --git a/examples/interledger/megachain.json b/examples/interledger/megachain.json deleted file mode 100644 index fe0c6a4d..00000000 --- a/examples/interledger/megachain.json +++ /dev/null @@ -1 +0,0 @@ -{"keyring": [], "database": {"host": "localhost", "port": 28015, "name": "megachain"}, "keypair": {"private": "DGKiLSdLsZofmzZMv3B9KZVj79wrq4PZ3PsMmk8Hu8KK", "public": "gMGJ9j15qyBVC1t8ceiDgJLLaRBgvrFchyzB1GgWk3Fa"}} \ No newline at end of file diff --git a/examples/interledger/run_cross_ledger_payment_simple.py b/examples/interledger/run_cross_ledger_payment_simple.py deleted file mode 100644 index 701f8b8b..00000000 --- a/examples/interledger/run_cross_ledger_payment_simple.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -from time import sleep - -from bigchaindb import Bigchain -from examples.accounts import User -from examples.interledger.connector import Connector - -config_bigchain = json.load(open('bigchain.json', 'r')) -config_megachain = json.load(open('megachain.json', 'r')) - -bigchain = Bigchain(dbname=config_bigchain['database']['name'], - public_key=config_bigchain['keypair']['public'], - private_key=config_bigchain['keypair']['private']) - -megachain = Bigchain(dbname=config_megachain['database']['name'], - public_key=config_megachain['keypair']['public'], - private_key=config_megachain['keypair']['private']) - -alice = User(bigchain) -bob = User(megachain) - -connector = Connector(bigchain) -connector.add_ledger(megachain) - -# create assets -alice.create_assets(amount=2) -connector.create_assets(amount=2, ledger=bigchain) -connector.create_assets(amount=2, ledger=megachain) - -sleep(6) -# transfer asset to escrow -tx_alice = connector.connect(user_from=alice.public, - ledger_from=alice.ledger, - user_to=bob.public, - ledger_to=bob.ledger, - condition_func=lambda proof: True, - asset_id=alice.assets[0]['id'], - payload={'what': 'ever'}) - -tx_alice_signed = alice.ledger.sign_transaction(tx_alice, alice.private) -alice.ledger.validate_transaction(tx_alice_signed) -alice.ledger.write_transaction(tx_alice_signed) - -sleep(6) -# release asset from escrow -tx_bob = connector.release(ledger=bob.ledger, receipt=None) -tx_bob_signed = bob.ledger.sign_transaction(tx_bob, connector.private(bob.ledger), connector.public(bob.ledger)) -bob.ledger.validate_transaction(tx_bob_signed) -bob.ledger.write_transaction(tx_bob_signed) - -tx_connector = connector.release(ledger=alice.ledger, receipt=None) -tx_connector_signed = alice.ledger.sign_transaction(tx_connector, alice.private, alice.public) -alice.ledger.validate_transaction(tx_connector_signed) -alice.ledger.write_transaction(tx_connector_signed) diff --git a/examples/interledger/tests/__init__.py b/examples/interledger/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 From 4039310d439f72f128a7d2d809e601a9ba14a52e Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 9 Mar 2016 13:46:14 +0100 Subject: [PATCH 11/11] fix merge + tests --- bigchaindb/core.py | 117 +--------------------------------- bigchaindb/util.py | 53 +++++++++++---- tests/db/test_bigchain_api.py | 16 +++-- tests/test_client.py | 8 +-- tests/test_util.py | 4 +- tests/web/test_basic_views.py | 8 +-- 6 files changed, 64 insertions(+), 142 deletions(-) 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]]