Added ability to CREATE divisible assets

This commit is contained in:
Rodolphe Marques 2016-11-03 15:57:05 +01:00
parent d47ed214cd
commit dd382ee4e6
4 changed files with 114 additions and 18 deletions

View File

@ -9,7 +9,8 @@ from cryptoconditions.exceptions import ParsingError
from bigchaindb.common.crypto import SigningKey, hash_data
from bigchaindb.common.exceptions import (KeypairMismatchException,
InvalidHash, InvalidSignature)
InvalidHash, InvalidSignature,
AmountError)
from bigchaindb.common.util import serialize, gen_timestamp
@ -268,7 +269,7 @@ class Condition(object):
return cond
@classmethod
def generate(cls, owners_after):
def generate(cls, owners_after, amount=1):
"""Generates a Condition from a specifically formed tuple or list.
Note:
@ -306,6 +307,8 @@ class Condition(object):
else:
threshold = len(owners_after)
if not isinstance(amount, int):
raise TypeError('`amount` must be a int')
if not isinstance(owners_after, list):
raise TypeError('`owners_after` must be an instance of list')
if len(owners_after) == 0:
@ -316,12 +319,12 @@ class Condition(object):
ffill = Ed25519Fulfillment(public_key=owners_after[0])
except TypeError:
ffill = owners_after[0]
return cls(ffill, owners_after)
return cls(ffill, owners_after, amount=amount)
else:
initial_cond = ThresholdSha256Fulfillment(threshold=threshold)
threshold_cond = reduce(cls._gen_condition, owners_after,
initial_cond)
return cls(threshold_cond, owners_after)
return cls(threshold_cond, owners_afteri, amount=amount)
@classmethod
def _gen_condition(cls, initial, current):
@ -466,7 +469,7 @@ class Asset(object):
"""Generates a unqiue uuid for an Asset"""
return str(uuid4())
def _validate_asset(self):
def _validate_asset(self, amount=None):
"""Validates the asset"""
if self.data is not None and not isinstance(self.data, dict):
raise TypeError('`data` must be a dict instance or None')
@ -477,6 +480,29 @@ class Asset(object):
if not isinstance(self.updatable, bool):
raise TypeError('`updatable` must be a boolean')
if self.refillable:
raise NotImplementedError('Refillable assets are not yet'
' implemented')
if self.updatable:
raise NotImplementedError('Updatable assets are not yet'
' implemented')
# If the amount is supplied we can perform extra validations to
# the asset
if amount is not None:
if not isinstance(amount, int):
raise TypeError('`amount` must be an int')
if self.divisible is False and amount != 1:
raise AmountError('non divisible assets always have'
' amount equal to one')
# Since refillable assets are not yet implemented this should
# raise and exception
if self.divisible is True and amount < 2:
raise AmountError('divisible assets must have an amount'
' greater than one')
class Metadata(object):
"""Metadata is used to store a dictionary and its hash in a Transaction."""
@ -621,6 +647,7 @@ class Transaction(object):
if conditions is not None and not isinstance(conditions, list):
raise TypeError('`conditions` must be a list instance or None')
# TODO: Check if there is a case in which conditions may be None
elif conditions is None:
self.conditions = []
else:
@ -628,6 +655,7 @@ class Transaction(object):
if fulfillments is not None and not isinstance(fulfillments, list):
raise TypeError('`fulfillments` must be a list instance or None')
# TODO: Check if there is a case in which fulfillments may be None
elif fulfillments is None:
self.fulfillments = []
else:
@ -638,9 +666,16 @@ class Transaction(object):
else:
self.metadata = metadata
# validate asset
# we know that each transaction relates to a single asset
# we can sum the amount of all the conditions
amount = sum([condition.amount for condition in self.conditions])
self.asset._validate_asset(amount=amount)
@classmethod
def create(cls, owners_before, owners_after, metadata=None, asset=None,
secret=None, time_expire=None):
secret=None, time_expire=None, amount=1):
"""A simple way to generate a `CREATE` transaction.
Note:
@ -675,6 +710,8 @@ class Transaction(object):
raise TypeError('`owners_before` must be a list instance')
if not isinstance(owners_after, list):
raise TypeError('`owners_after` must be a list instance')
if not isinstance(amount, int):
raise TypeError('`amount` must be a int')
metadata = Metadata(metadata)
if len(owners_before) == len(owners_after) and len(owners_after) == 1:
@ -683,7 +720,7 @@ class Transaction(object):
# fulfillment for the fulfillment and condition.
ffill = Ed25519Fulfillment(public_key=owners_before[0])
ffill_tx = Fulfillment(ffill, owners_before)
cond_tx = Condition.generate(owners_after)
cond_tx = Condition.generate(owners_after, amount=amount)
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)
elif len(owners_before) == len(owners_after) and len(owners_after) > 1:
@ -693,7 +730,8 @@ class Transaction(object):
ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before),
[owner_before])
for owner_before in owners_before]
conds = [Condition.generate(owners) for owners in owners_after]
conds = [Condition.generate(owners, amount=amount)
for owners in owners_after]
return cls(cls.CREATE, asset, ffills, conds, metadata)
elif len(owners_before) == 1 and len(owners_after) > 1:
@ -707,7 +745,7 @@ class Transaction(object):
secret is not None):
# NOTE: Hashlock condition case
hashlock = PreimageSha256Fulfillment(preimage=secret)
cond_tx = Condition(hashlock.condition_uri)
cond_tx = Condition(hashlock.condition_uri, amount=amount)
ffill = Ed25519Fulfillment(public_key=owners_before[0])
ffill_tx = Fulfillment(ffill, owners_before)
return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata)

View File

@ -183,7 +183,7 @@ class Bigchain(object):
except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist,
exceptions.TransactionOwnerError, exceptions.DoubleSpend,
exceptions.InvalidHash, exceptions.InvalidSignature,
exceptions.FulfillmentNotInValidBlock):
exceptions.FulfillmentNotInValidBlock, exceptions.AmountError):
return False
def get_transaction(self, txid, include_status=False):

View File

@ -1,4 +1,6 @@
import pytest
from unittest.mock import patch
from ..db.conftest import inputs
@ -161,3 +163,47 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk):
assert tx_transfer.id in [t.id for t in txs]
assert asset_id == txs[0].asset.data_id
assert asset_id == txs[1].asset.data_id
def test_create_invalid_divisible_asset(b, user_vk, user_sk):
from bigchaindb.models import Transaction, Asset
from bigchaindb.common.exceptions import AmountError
# non divisible assets cannot have amount > 1
# Transaction.__init__ should raise an exception
asset = Asset(divisible=False)
with pytest.raises(AmountError):
Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
# divisible assets need to have an amount > 1
# Transaction.__init__ should raise an exception
asset = Asset(divisible=True)
with pytest.raises(AmountError):
Transaction.create([user_vk], [user_vk], asset=asset, amount=1)
# even if a transaction is badly constructed the server should raise the
# exception
asset = Asset(divisible=False)
with patch.object(Asset, '_validate_asset', return_value=None):
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
tx_signed = tx.sign([user_sk])
with pytest.raises(AmountError):
tx_signed.validate(b)
assert b.is_valid_transaction(tx_signed) is False
asset = Asset(divisible=True)
with patch.object(Asset, '_validate_asset', return_value=None):
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=1)
tx_signed = tx.sign([user_sk])
with pytest.raises(AmountError):
tx_signed.validate(b)
assert b.is_valid_transaction(tx_signed) is False
def test_create_valid_divisible_asset(b, user_vk, user_sk):
from bigchaindb.models import Transaction, Asset
asset = Asset(divisible=True)
tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2)
tx_signed = tx.sign([user_sk])
assert b.is_valid_transaction(tx_signed)

View File

@ -22,6 +22,7 @@ def test_asset_creation_with_data(data):
def test_asset_invalid_asset_initialization():
from bigchaindb.common.transaction import Asset
# check types
with raises(TypeError):
Asset(data='some wrong type')
with raises(TypeError):
@ -31,6 +32,12 @@ def test_asset_invalid_asset_initialization():
with raises(TypeError):
Asset(updatable=1)
# check for features that are not yet implemented
with raises(NotImplementedError):
Asset(updatable=True)
with raises(NotImplementedError):
Asset(refillable=True)
def test_invalid_asset_comparison(data, data_id):
from bigchaindb.common.transaction import Asset
@ -69,12 +76,17 @@ def test_asset_deserialization(data, data_id):
def test_validate_asset():
from bigchaindb.common.transaction import Asset
from bigchaindb.common.exceptions import AmountError
# test amount errors
asset = Asset(divisible=False)
with raises(AmountError):
asset._validate_asset(amount=2)
asset = Asset(divisible=True)
with raises(AmountError):
asset._validate_asset(amount=1)
asset = Asset()
with raises(TypeError):
Asset(divisible=1)
with raises(TypeError):
Asset(refillable=1)
with raises(TypeError):
Asset(updatable=1)
with raises(TypeError):
Asset(data='we need more lemon pledge')
asset._validate_asset(amount='a')