Merge 4039310d439f72f128a7d2d809e601a9ba14a52e into 55ad60097da46da44a33892d9c4f1f8ce797e774

This commit is contained in:
Dimitri De Jonghe 2016-03-18 19:22:10 +00:00
commit 68ad188b63
6 changed files with 105 additions and 47 deletions

View File

@ -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'])

View File

@ -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']
signatures = data.pop('signatures')
for public_key_base58 in signed_transaction['transaction']['current_owners']:
public_key = PublicKey(public_key_base58)
return public_key.verify(serialize(data), signature)
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

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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]]