From 30a68e483f218a9cdcab62a36f5cc2ab39877620 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 24 Feb 2016 19:38:52 +0100 Subject: [PATCH] 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)