This commit is contained in:
Sylvain Bellemare 2017-11-25 01:04:50 +01:00
parent e377fb57af
commit 9dd2e026b0
15 changed files with 383 additions and 109 deletions

View File

@ -13,7 +13,9 @@ required:
- version
properties:
id:
"$ref": "#/definitions/sha3_hexdigest"
anyOf:
- "$ref": "#/definitions/sha3_hexdigest"
- type: 'null'
operation:
"$ref": "#/definitions/operation"
asset:

View File

@ -112,7 +112,7 @@ class Input(object):
InvalidSignature: If an Input's URI couldn't be parsed.
"""
fulfillment = data['fulfillment']
if not isinstance(fulfillment, Fulfillment):
if not isinstance(fulfillment, (Fulfillment, type(None))):
try:
fulfillment = Fulfillment.from_uri(data['fulfillment'])
except ASN1DecodeError:
@ -477,7 +477,7 @@ class Transaction(object):
VERSION = '1.0'
def __init__(self, operation, asset, inputs=None, outputs=None,
metadata=None, version=None):
metadata=None, version=None, hash_id=None):
"""The constructor allows to create a customizable Transaction.
Note:
@ -495,6 +495,7 @@ class Transaction(object):
metadata (dict): Metadata to be stored along with the
Transaction.
version (string): Defines the version number of a Transaction.
hash_id (string): Hash id of the transaction.
"""
if operation not in Transaction.ALLOWED_OPERATIONS:
allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS)
@ -528,6 +529,14 @@ class Transaction(object):
self.inputs = inputs or []
self.outputs = outputs or []
self.metadata = metadata
self._id = hash_id
@property
def serialized(self):
return Transaction._to_str(self.to_dict())
def _hash(self):
self._id = hash_data(self.serialized)
@classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None):
@ -756,6 +765,9 @@ class Transaction(object):
tx_serialized = Transaction._to_str(tx_dict)
for i, input_ in enumerate(self.inputs):
self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs)
self._hash()
return self
@classmethod
@ -907,6 +919,7 @@ class Transaction(object):
tx_dict = self.to_dict()
tx_dict = Transaction._remove_signatures(tx_dict)
tx_dict['id'] = None
tx_serialized = Transaction._to_str(tx_dict)
def validate(i, output_condition_uri=None):
@ -965,22 +978,16 @@ class Transaction(object):
Returns:
dict: The Transaction as an alternative serialization format.
"""
tx = {
return {
'inputs': [input_.to_dict() for input_ in self.inputs],
'outputs': [output.to_dict() for output in self.outputs],
'operation': str(self.operation),
'metadata': self.metadata,
'asset': self.asset,
'version': self.version,
'id': self._id,
}
tx_no_signatures = Transaction._remove_signatures(tx)
tx_serialized = Transaction._to_str(tx_no_signatures)
tx_id = Transaction._to_hash(tx_serialized)
tx['id'] = tx_id
return tx
@staticmethod
# TODO: Remove `_dict` prefix of variable.
def _remove_signatures(tx_dict):
@ -1010,7 +1017,7 @@ class Transaction(object):
@property
def id(self):
return self.to_hash()
return self._id
def to_hash(self):
return self.to_dict()['id']
@ -1069,12 +1076,13 @@ class Transaction(object):
# NOTE: Remove reference to avoid side effects
tx_body = deepcopy(tx_body)
try:
proposed_tx_id = tx_body.pop('id')
proposed_tx_id = tx_body['id']
except KeyError:
raise InvalidHash('No transaction id found!')
tx_body_no_signatures = Transaction._remove_signatures(tx_body)
tx_body_serialized = Transaction._to_str(tx_body_no_signatures)
tx_body['id'] = None
tx_body_serialized = Transaction._to_str(tx_body)
valid_tx_id = Transaction._to_hash(tx_body_serialized)
if proposed_tx_id != valid_tx_id:
@ -1092,8 +1100,7 @@ class Transaction(object):
Returns:
:class:`~bigchaindb.common.transaction.Transaction`
"""
cls.validate_id(tx)
inputs = [Input.from_dict(input_) for input_ in tx['inputs']]
outputs = [Output.from_dict(output) for output in tx['outputs']]
return cls(tx['operation'], tx['asset'], inputs, outputs,
tx['metadata'], tx['version'])
tx['metadata'], tx['version'], hash_id=tx['id'])

View File

@ -84,6 +84,7 @@ class Transaction(Transaction):
@classmethod
def from_dict(cls, tx_body):
super().validate_id(tx_body)
validate_transaction_schema(tx_body)
validate_txn_obj('asset', tx_body['asset'], 'data', validate_key)
validate_txn_obj('metadata', tx_body, 'metadata', validate_key)

View File

@ -71,8 +71,10 @@ def test_asset_id_mismatch(b, user_pk):
tx1 = Transaction.create([b.me], [([user_pk], 1)],
metadata={'msg': random.random()})
tx1.sign([b.me_private])
tx2 = Transaction.create([b.me], [([user_pk], 1)],
metadata={'msg': random.random()})
tx2.sign([b.me_private])
with pytest.raises(AssetIdMismatch):
Transaction.get_asset_id([tx1, tx2])

View File

@ -308,14 +308,13 @@ def test_count_blocks(signed_create_tx):
assert query.count_blocks(conn) == 1
def test_count_backlog(signed_create_tx):
def test_count_backlog(signed_create_tx, signed_transfer_tx):
from bigchaindb.backend import connect, query
conn = connect()
# create and insert some transations
conn.db.backlog.insert_one(signed_create_tx.to_dict())
signed_create_tx.metadata = {'msg': 'aaa'}
conn.db.backlog.insert_one(signed_create_tx.to_dict())
conn.db.backlog.insert_one(signed_transfer_tx.to_dict())
assert query.count_backlog(conn) == 2
@ -437,17 +436,21 @@ def test_get_new_blocks_feed(b, create_tx):
assert list(feed) == [b3]
def test_get_spending_transactions(user_pk):
def test_get_spending_transactions(user_pk, user_sk):
from bigchaindb.backend import connect, query
from bigchaindb.models import Block, Transaction
conn = connect()
out = [([user_pk], 1)]
tx1 = Transaction.create([user_pk], out * 3)
tx1.sign([user_sk])
inputs = tx1.to_inputs()
tx2 = Transaction.transfer([inputs[0]], out, tx1.id)
tx2.sign([user_sk])
tx3 = Transaction.transfer([inputs[1]], out, tx1.id)
tx3.sign([user_sk])
tx4 = Transaction.transfer([inputs[2]], out, tx1.id)
tx4.sign([user_sk])
block = Block([tx1, tx2, tx3, tx4])
conn.db.bigchain.insert_one(block.to_dict())

View File

@ -202,3 +202,149 @@ def dummy_transaction():
}],
'version': '1.0'
}
@pytest.fixture
def unfulfilled_transaction():
return {
'asset': {
'data': {
'msg': 'Hello BigchainDB!',
}
},
'id': None,
'inputs': [{
# XXX This could be None, see #1925
# https://github.com/bigchaindb/bigchaindb/issues/1925
'fulfillment': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'
},
'fulfills': None,
'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'metadata': None,
'operation': 'CREATE',
'outputs': [{
'amount': '1',
'condition': {
'details': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'
},
'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'},
'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'version': '1.0'
}
@pytest.fixture
def fulfilled_transaction():
return {
'asset': {
'data': {
'msg': 'Hello BigchainDB!',
}
},
'id': None,
'inputs': [{
'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g'
'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k'
'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN'),
'fulfills': None,
'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'metadata': None,
'operation': 'CREATE',
'outputs': [{
'amount': '1',
'condition': {
'details': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'
},
'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'},
'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'version': '1.0'
}
@pytest.fixture
def fulfilled_and_hashed_transaction():
return {
'asset': {
'data': {
'msg': 'Hello BigchainDB!',
}
},
'id': '7a7c827cf4ef7985f08f4e9d16f5ffc58ca4e82271921dfbed32e70cb462485f',
'inputs': [{
'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g'
'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k'
'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN'),
'fulfills': None,
'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'metadata': None,
'operation': 'CREATE',
'outputs': [{
'amount': '1',
'condition': {
'details': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'
},
'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'},
'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'version': '1.0'
}
# TODO For reviewers: Pick which approach you like best: parametrized or not?
@pytest.fixture(params=(
{'id': None,
'fulfillment': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'}},
{'id': None,
'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g'
'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k'
'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN')},
{'id': '7a7c827cf4ef7985f08f4e9d16f5ffc58ca4e82271921dfbed32e70cb462485f',
'fulfillment': ('pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g'
'UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k'
'jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN')},
))
def tri_state_transaction(request):
tx = {
'asset': {
'data': {
'msg': 'Hello BigchainDB!',
}
},
'id': None,
'inputs': [{
'fulfillment': None,
'fulfills': None,
'owners_before': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'metadata': None,
'operation': 'CREATE',
'outputs': [{
'amount': '1',
'condition': {
'details': {
'public_key': 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE',
'type': 'ed25519-sha-256'
},
'uri': 'ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072'},
'public_keys': ['JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE']
}],
'version': '1.0'
}
tx['id'] = request.param['id']
tx['inputs'][0]['fulfillment'] = request.param['fulfillment']
return tx

View File

@ -1,10 +1,12 @@
"""These are tests of the API of the Transaction class and associated classes.
Tests for transaction validation are separate.
"""
import json
from copy import deepcopy
from base58 import b58encode, b58decode
from pytest import raises
from pytest import mark, raises
from sha3 import sha3_256
def test_input_serialization(ffill_uri, user_pub):
@ -35,6 +37,7 @@ def test_input_deserialization_with_uri(ffill_uri, user_pub):
assert input == expected
@mark.skip(reason='None is tolerated because it is None before fulfilling.')
def test_input_deserialization_with_invalid_input(user_pub):
from bigchaindb.common.transaction import Input
@ -303,10 +306,8 @@ def test_create_default_asset_on_tx_initialization(asset_definition):
def test_transaction_serialization(user_input, user_output, data):
from bigchaindb.common.transaction import Transaction
tx_id = 'l0l'
expected = {
'id': tx_id,
'id': None,
'version': Transaction.VERSION,
# NOTE: This test assumes that Inputs and Outputs can
# successfully be serialized
@ -322,37 +323,14 @@ def test_transaction_serialization(user_input, user_output, data):
tx = Transaction(Transaction.CREATE, {'data': data}, [user_input],
[user_output])
tx_dict = tx.to_dict()
tx_dict['id'] = tx_id
assert tx_dict == expected
def test_transaction_deserialization(user_input, user_output, data):
def test_transaction_deserialization(tri_state_transaction):
from bigchaindb.common.transaction import Transaction
from .utils import validate_transaction_model
expected_asset = {'data': data}
expected = Transaction(Transaction.CREATE, expected_asset, [user_input],
[user_output], None, Transaction.VERSION)
tx = {
'version': Transaction.VERSION,
# NOTE: This test assumes that Inputs and Outputs can
# successfully be serialized
'inputs': [user_input.to_dict()],
'outputs': [user_output.to_dict()],
'operation': Transaction.CREATE,
'metadata': None,
'asset': {
'data': data,
}
}
tx_no_signatures = Transaction._remove_signatures(tx)
tx['id'] = Transaction._to_hash(Transaction._to_str(tx_no_signatures))
tx = Transaction.from_dict(tx)
assert tx == expected
tx = Transaction.from_dict(tri_state_transaction)
validate_transaction_model(tx)
@ -543,7 +521,6 @@ def test_validate_input_with_invalid_parameters(utx):
input_conditions = [out.fulfillment.condition_uri for out in utx.outputs]
tx_dict = utx.to_dict()
tx_dict = Transaction._remove_signatures(tx_dict)
tx_serialized = Transaction._to_str(tx_dict)
valid = utx._input_valid(utx.inputs[0], tx_serialized, input_conditions)
assert not valid
@ -834,6 +811,7 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub,
from .utils import validate_transaction_model
expected = {
'id': None,
'outputs': [user2_output.to_dict()],
'metadata': None,
'asset': {
@ -861,7 +839,6 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub,
transfer_tx = transfer_tx.to_dict()
expected_input = deepcopy(inputs[0])
expected['id'] = transfer_tx['id']
expected_input.fulfillment.sign(
serialize(expected).encode(), b58decode(user_priv))
expected_ffill = expected_input.fulfillment.serialize_uri()
@ -971,6 +948,35 @@ def test_cant_add_empty_input():
tx.add_input(None)
def test_unfulfilled_transaction_serialized(unfulfilled_transaction):
from bigchaindb.common.transaction import Transaction
tx_obj = Transaction.from_dict(unfulfilled_transaction)
expected = json.dumps(unfulfilled_transaction, sort_keys=True,
separators=(',', ':'), ensure_ascii=True)
assert tx_obj.serialized == expected
def test_fulfilled_transaction_serialized(fulfilled_transaction):
from bigchaindb.common.transaction import Transaction
tx_obj = Transaction.from_dict(fulfilled_transaction)
expected = json.dumps(fulfilled_transaction, sort_keys=True,
separators=(',', ':'), ensure_ascii=True)
assert tx_obj.serialized == expected
def test_transaction_hash(fulfilled_transaction):
from bigchaindb.common.transaction import Transaction
tx_obj = Transaction.from_dict(fulfilled_transaction)
assert tx_obj._id is None
assert tx_obj.id is None
thing_to_hash = json.dumps(fulfilled_transaction, sort_keys=True,
separators=(',', ':'), ensure_ascii=True)
expected_hash_id = sha3_256(thing_to_hash.encode()).hexdigest()
tx_obj._hash()
assert tx_obj._id == expected_hash_id
assert tx_obj.id == expected_hash_id
def test_output_from_dict_invalid_amount(user_output):
from bigchaindb.common.transaction import Output
from bigchaindb.common.exceptions import AmountError

View File

@ -335,6 +335,15 @@ def signed_transfer_tx(signed_create_tx, user_pk, user_sk):
return tx.sign([user_sk])
@pytest.fixture
def double_spend_tx(signed_create_tx, carol_pubkey, user_sk):
from bigchaindb.models import Transaction
inputs = signed_create_tx.to_inputs()
tx = Transaction.transfer(
inputs, [([carol_pubkey], 1)], asset_id=signed_create_tx.id)
return tx.sign([user_sk])
@pytest.fixture
def structurally_valid_vote():
return {

View File

@ -635,7 +635,7 @@ class TestTransactionValidation(object):
@pytest.mark.usefixtures('inputs')
def test_non_create_double_spend(self, b, signed_create_tx,
signed_transfer_tx):
signed_transfer_tx, double_spend_tx):
from bigchaindb.common.exceptions import DoubleSpend
block1 = b.create_block([signed_create_tx])
@ -655,10 +655,8 @@ class TestTransactionValidation(object):
sleep(1)
signed_transfer_tx.metadata = {'different': 1}
# FIXME: https://github.com/bigchaindb/bigchaindb/issues/592
with pytest.raises(DoubleSpend):
b.validate_transaction(signed_transfer_tx)
b.validate_transaction(double_spend_tx)
@pytest.mark.usefixtures('inputs')
def test_valid_non_create_transaction_after_block_creation(self, b,

View File

@ -18,6 +18,7 @@ def test_check_for_quorum_invalid(b, user_pk):
# create blocks with transactions
tx1 = Transaction.create([b.me], [([user_pk], 1)])
tx1.sign([b.me_private])
test_block = b.create_block([tx1])
# simulate a federation with four voters
@ -51,6 +52,7 @@ def test_check_for_quorum_invalid_prev_node(b, user_pk):
# create blocks with transactions
tx1 = Transaction.create([b.me], [([user_pk], 1)])
tx1.sign([b.me_private])
test_block = b.create_block([tx1])
# simulate a federation with four voters
@ -94,6 +96,7 @@ def test_check_for_quorum_valid(b, user_pk):
# create blocks with transactions
tx1 = Transaction.create([b.me], [([user_pk], 1)])
tx1.sign([b.me_private])
test_block = b.create_block([tx1])
# add voters to block and write
@ -129,6 +132,7 @@ def test_check_requeue_transaction(b, user_pk):
# create blocks with transactions
tx1 = Transaction.create([b.me], [([user_pk], 1)])
tx1.sign([b.me_private])
test_block = b.create_block([tx1])
e.requeue_transactions(test_block)

View File

@ -378,6 +378,10 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch,
vote2_doc['signature']) is True
@pytest.mark.skip(
reason=('Needs important modification following issue #1891:'
'https://github.com/bigchaindb/bigchaindb/issues/1891')
)
@pytest.mark.bdb
def test_unsigned_tx_in_block_voting(monkeypatch, b, user_pk, genesis_block):
from bigchaindb.backend import query

View File

@ -57,14 +57,15 @@ class TestBlockModel(object):
from bigchaindb.common.utils import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [([b.me], 1)])]
transaction = Transaction.create([b.me], [([b.me], 1)])
transaction.sign([b.me_private])
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected = Block(transactions, b.me, timestamp, voters)
expected = Block([transaction], b.me, timestamp, voters)
block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'transactions': [transaction.to_dict()],
'node_pubkey': b.me,
'voters': voters,
}
@ -97,12 +98,13 @@ class TestBlockModel(object):
from bigchaindb.common.utils import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [([b.me], 1)])]
transaction = Transaction.create([b.me], [([b.me], 1)])
transaction.sign([b.me_private])
timestamp = gen_timestamp()
block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'transactions': [transaction.to_dict()],
'node_pubkey': b.me,
'voters': list(b.federation),
}
@ -168,12 +170,14 @@ class TestBlockModel(object):
# create 3 assets
for asset in assets:
tx = Transaction.create([b.me], [([b.me], 1)], asset=asset)
tx.sign([b.me_private])
txs.append(tx)
# create a `TRANSFER` transaction.
# the asset in `TRANSFER` transactions is not extracted
tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)],
asset_id=txs[0].id)
tx.sign([b.me_private])
txs.append(tx)
# create the block
@ -203,12 +207,14 @@ class TestBlockModel(object):
# create 3 assets
for asset in assets:
tx = Transaction.create([b.me], [([b.me], 1)], asset=asset)
tx.sign([b.me_private])
txs.append(tx)
# create a `TRANSFER` transaction.
# the asset in `TRANSFER` transactions is not extracted
tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)],
asset_id=txs[0].id)
tx.sign([b.me_private])
txs.append(tx)
# create the block
@ -236,12 +242,14 @@ class TestBlockModel(object):
# create 3 assets
for asset in assets:
tx = Transaction.create([b.me], [([b.me], 1)], asset=asset)
tx.sign([b.me_private])
txs.append(tx)
# create a `TRANSFER` transaction.
# the asset in `TRANSFER` transactions is not extracted
tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)],
asset_id=txs[0].id)
tx.sign([b.me_private])
txs.append(tx)
# create the block
@ -268,12 +276,14 @@ class TestBlockModel(object):
# create 3 assets
for asset in assets:
tx = Transaction.create([b.me], [([b.me], 1)], asset=asset)
tx.sign([b.me_private])
txs.append(tx)
# create a `TRANSFER` transaction.
# the asset in `TRANSFER` transactions is not extracted
tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)],
asset_id=txs[0].id)
tx.sign([b.me_private])
txs.append(tx)
# create the block

View File

@ -49,17 +49,20 @@ def test_get_outputs_by_public_key(b, user_pk, user2_pk, blockdata):
]
def test_filter_spent_outputs(b, user_pk):
def test_filter_spent_outputs(b, user_pk, user_sk):
out = [([user_pk], 1)]
tx1 = Transaction.create([user_pk], out * 3)
tx1.sign([user_sk])
# There are 3 inputs
inputs = tx1.to_inputs()
# Each spent individually
tx2 = Transaction.transfer([inputs[0]], out, tx1.id)
tx2.sign([user_sk])
tx3 = Transaction.transfer([inputs[1]], out, tx1.id)
tx3.sign([user_sk])
tx4 = Transaction.transfer([inputs[2]], out, tx1.id)
tx4.sign([user_sk])
# The CREATE and first TRANSFER are valid. tx2 produces a new unspent.
for tx in [tx1, tx2]:
@ -86,17 +89,21 @@ def test_filter_spent_outputs(b, user_pk):
}
def test_filter_unspent_outputs(b, user_pk):
def test_filter_unspent_outputs(b, user_pk, user_sk):
out = [([user_pk], 1)]
tx1 = Transaction.create([user_pk], out * 3)
tx1.sign([user_sk])
# There are 3 inputs
inputs = tx1.to_inputs()
# Each spent individually
tx2 = Transaction.transfer([inputs[0]], out, tx1.id)
tx2.sign([user_sk])
tx3 = Transaction.transfer([inputs[1]], out, tx1.id)
tx3.sign([user_sk])
tx4 = Transaction.transfer([inputs[2]], out, tx1.id)
tx4.sign([user_sk])
# The CREATE and first TRANSFER are valid. tx2 produces a new unspent.
for tx in [tx1, tx2]:

View File

@ -2,11 +2,13 @@
structural / schematic issues are caught when reading a transaction
(ie going from dict -> transaction).
"""
import json
import pytest
import sha3
from unittest.mock import MagicMock
from bigchaindb.common.exceptions import (AmountError, InvalidHash,
from bigchaindb.common.exceptions import (AmountError,
SchemaValidationError,
ThresholdTooDeep)
from bigchaindb.models import Transaction
@ -28,137 +30,175 @@ def validate_raises(tx, exc=SchemaValidationError):
# We should test that validation works when we expect it to
def test_validation_passes(create_tx):
validate(create_tx)
def test_validation_passes(signed_create_tx):
Transaction.from_dict(signed_create_tx.to_dict())
################################################################################
# ID
def test_tx_serialization_hash_function(create_tx):
import sha3
import json
tx = create_tx.to_dict()
tx['inputs'][0]['fulfillment'] = None
del tx['id']
def test_tx_serialization_hash_function(signed_create_tx):
tx = signed_create_tx.to_dict()
tx['id'] = None
payload = json.dumps(tx, skipkeys=False, sort_keys=True,
separators=(',', ':'))
assert sha3.sha3_256(payload.encode()).hexdigest() == create_tx.id
assert sha3.sha3_256(payload.encode()).hexdigest() == signed_create_tx.id
def test_tx_serialization_with_incorrect_hash(create_tx):
tx = create_tx.to_dict()
def test_tx_serialization_with_incorrect_hash(signed_create_tx):
from bigchaindb.common.transaction import Transaction
from bigchaindb.common.exceptions import InvalidHash
tx = signed_create_tx.to_dict()
tx['id'] = 'a' * 64
validate_raises(tx, InvalidHash)
with pytest.raises(InvalidHash):
Transaction.validate_id(tx)
def test_tx_serialization_with_no_hash(create_tx):
tx = create_tx.to_dict()
def test_tx_serialization_with_no_hash(signed_create_tx):
from bigchaindb.common.exceptions import InvalidHash
tx = signed_create_tx.to_dict()
del tx['id']
validate_raises(tx)
with pytest.raises(InvalidHash):
Transaction.from_dict(tx)
################################################################################
# Operation
def test_validate_invalid_operation(create_tx):
def test_validate_invalid_operation(b, create_tx):
create_tx.operation = 'something invalid'
validate_raises(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate_raises(signed_tx)
################################################################################
# Metadata
def test_validate_fails_metadata_empty_dict(create_tx):
def test_validate_fails_metadata_empty_dict(b, create_tx):
create_tx.metadata = {'a': 1}
validate(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate(signed_tx)
create_tx._id = None
create_tx.fulfillment = None
create_tx.metadata = None
validate(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate(signed_tx)
create_tx._id = None
create_tx.fulfillment = None
create_tx.metadata = {}
validate_raises(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate_raises(signed_tx)
################################################################################
# Asset
def test_transfer_asset_schema(signed_transfer_tx):
def test_transfer_asset_schema(user_sk, signed_transfer_tx):
from bigchaindb.common.transaction import Transaction
tx = signed_transfer_tx.to_dict()
validate(tx)
tx['id'] = None
tx['asset']['data'] = {}
tx = Transaction.from_dict(tx).sign([user_sk]).to_dict()
validate_raises(tx)
tx['id'] = None
del tx['asset']['data']
tx['asset']['id'] = 'b' * 63
tx = Transaction.from_dict(tx).sign([user_sk]).to_dict()
validate_raises(tx)
def test_create_tx_no_asset_id(create_tx):
def test_create_tx_no_asset_id(b, create_tx):
create_tx.asset['id'] = 'b' * 64
validate_raises(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate_raises(signed_tx)
def test_create_tx_asset_type(create_tx):
def test_create_tx_asset_type(b, create_tx):
create_tx.asset['data'] = 'a'
validate_raises(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate_raises(signed_tx)
def test_create_tx_no_asset_data(create_tx):
def test_create_tx_no_asset_data(b, create_tx):
tx_body = create_tx.to_dict()
del tx_body['asset']['data']
tx_body_no_signatures = Transaction._remove_signatures(tx_body)
tx_body_serialized = Transaction._to_str(tx_body_no_signatures)
tx_body['id'] = Transaction._to_hash(tx_body_serialized)
tx_serialized = json.dumps(
tx_body, skipkeys=False, sort_keys=True, separators=(',', ':'))
tx_body['id'] = sha3.sha3_256(tx_serialized.encode()).hexdigest()
validate_raises(tx_body)
################################################################################
# Inputs
def test_no_inputs(create_tx):
def test_no_inputs(b, create_tx):
create_tx.inputs = []
validate_raises(create_tx)
signed_tx = create_tx.sign([b.me_private])
validate_raises(signed_tx)
def test_create_single_input(create_tx):
def test_create_single_input(b, create_tx):
from bigchaindb.common.transaction import Transaction
tx = create_tx.to_dict()
tx['inputs'] += tx['inputs']
tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict()
validate_raises(tx)
tx['id'] = None
tx['inputs'] = []
tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict()
validate_raises(tx)
def test_create_tx_no_fulfills(create_tx):
def test_create_tx_no_fulfills(b, create_tx):
from bigchaindb.common.transaction import Transaction
tx = create_tx.to_dict()
tx['inputs'][0]['fulfills'] = {'tx': 'a' * 64, 'output': 0}
tx['inputs'][0]['fulfills'] = {'transaction_id': 'a' * 64,
'output_index': 0}
tx = Transaction.from_dict(tx).sign([b.me_private]).to_dict()
validate_raises(tx)
def test_transfer_has_inputs(signed_transfer_tx):
def test_transfer_has_inputs(user_sk, signed_transfer_tx):
signed_transfer_tx.inputs = []
signed_transfer_tx._id = None
signed_transfer_tx.sign([user_sk])
validate_raises(signed_transfer_tx)
################################################################################
# Outputs
def test_low_amounts(create_tx, signed_transfer_tx):
for tx in [create_tx, signed_transfer_tx]:
def test_low_amounts(b, user_sk, create_tx, signed_transfer_tx):
for sk, tx in [(b.me_private, create_tx), (user_sk, signed_transfer_tx)]:
tx.outputs[0].amount = 0
tx._id = None
tx.sign([sk])
validate_raises(tx, AmountError)
tx.outputs[0].amount = -1
tx._id = None
tx.sign([sk])
validate_raises(tx)
def test_high_amounts(create_tx):
def test_high_amounts(b, create_tx):
# Should raise a SchemaValidationError - don't want to allow ridiculously
# large numbers to get converted to int
create_tx.outputs[0].amount = 10 ** 21
create_tx.sign([b.me_private])
validate_raises(create_tx)
# Should raise AmountError
create_tx.outputs[0].amount = 9 * 10 ** 18 + 1
create_tx._id = None
create_tx.sign([b.me_private])
validate_raises(create_tx, AmountError)
# Should pass
create_tx.outputs[0].amount -= 1
create_tx._id = None
create_tx.sign([b.me_private])
validate(create_tx)
@ -196,10 +236,17 @@ def test_unsupported_condition_type():
################################################################################
# Version
def test_validate_version(create_tx):
def test_validate_version(b, create_tx):
create_tx.version = '1.0'
create_tx.sign([b.me_private])
validate(create_tx)
create_tx.version = '0.10'
create_tx._id = None
create_tx.sign([b.me_private])
validate_raises(create_tx)
create_tx.version = '110'
create_tx._id = None
create_tx.sign([b.me_private])
validate_raises(create_tx)

View File

@ -1,7 +1,11 @@
import json
from unittest.mock import patch
import base58
import pytest
from cryptoconditions import Ed25519Sha256
from sha3 import sha3_256
from bigchaindb.common import crypto
@ -165,9 +169,16 @@ def test_post_create_transaction_with_invalid_signature(mock_logger,
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
tx = Transaction.create([user_pub], [([user_pub], 1)])
tx = tx.sign([user_priv]).to_dict()
tx = Transaction.create([user_pub], [([user_pub], 1)]).to_dict()
tx['inputs'][0]['fulfillment'] = 64 * '0'
tx['id'] = sha3_256(
json.dumps(
tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
).encode(),
).hexdigest()
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
expected_status_code = 400
@ -202,9 +213,25 @@ def test_post_create_transaction_with_invalid_structure(client):
def test_post_create_transaction_with_invalid_schema(mock_logger, client):
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
tx = Transaction.create(
[user_pub], [([user_pub], 1)]).sign([user_priv]).to_dict()
tx = Transaction.create([user_pub], [([user_pub], 1)]).to_dict()
del tx['version']
ed25519 = Ed25519Sha256(public_key=base58.b58decode(user_pub))
message = json.dumps(
tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
).encode()
ed25519.sign(message, base58.b58decode(user_priv))
tx['inputs'][0]['fulfillment'] = ed25519.serialize_uri()
tx['id'] = sha3_256(
json.dumps(
tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
).encode(),
).hexdigest()
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
expected_status_code = 400
expected_error_message = (
@ -308,6 +335,7 @@ def test_post_invalid_transfer_transaction_returns_400(b, client, user_pk):
transfer_tx = Transaction.transfer(create_tx.to_inputs(),
[([user_pub], 1)],
asset_id=create_tx.id)
transfer_tx._hash()
res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict()))
expected_status_code = 400