diff --git a/bigchaindb/core.py b/bigchaindb/core.py index e8e673ce..7e395614 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -188,7 +188,8 @@ class Bigchain(object): return self.validate_transaction(transaction) except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist, exceptions.TransactionOwnerError, exceptions.DoubleSpend, - exceptions.InvalidHash, exceptions.InvalidSignature): + exceptions.InvalidHash, exceptions.InvalidSignature, + exceptions.FulfillmentNotInValidBlock): return False def get_transaction(self, txid, include_status=False): diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 6855787e..e31d1a5d 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -1,7 +1,8 @@ from bigchaindb_common.crypto import hash_data, VerifyingKey, SigningKey from bigchaindb_common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, - TransactionDoesNotExist) + TransactionDoesNotExist, + FulfillmentNotInValidBlock) from bigchaindb_common.transaction import Transaction from bigchaindb_common.util import gen_timestamp, serialize @@ -45,11 +46,18 @@ class Transaction(Transaction): for ffill in self.fulfillments: input_txid = ffill.tx_input.txid input_cid = ffill.tx_input.cid - input_tx = bigchain.get_transaction(input_txid) + input_tx, status = bigchain.\ + get_transaction(input_txid, include_status=True) + if input_tx is None: raise TransactionDoesNotExist("input `{}` doesn't exist" .format(input_txid)) + if status != bigchain.TX_VALID: + raise FulfillmentNotInValidBlock( + 'input `{}` does not exist in a valid block'.format( + input_txid)) + spent = bigchain.get_spent(input_txid, ffill.tx_input.cid) if spent and spent.id != self.id: raise DoubleSpend('input `{}` was already spent' diff --git a/tests/db/conftest.py b/tests/db/conftest.py index c6bcbe44..3cd9ae49 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -11,7 +11,9 @@ import rethinkdb as r from bigchaindb import Bigchain from bigchaindb.db import get_conn +from bigchaindb_common import crypto +USER2_SK, USER2_VK = crypto.generate_key_pair() @pytest.fixture(autouse=True) def restore_config(request, node_config): @@ -104,11 +106,12 @@ def inputs(user_vk): # 1. create the genesis block b = Bigchain() try: - b.create_genesis_block() + g = b.create_genesis_block() except GenesisBlockAlreadyExistsError: pass - # 2. create block with transactions for `USER` to spend + # 2. create blocks with transactions for `USER` to spend + prev_block_id = g.id for block in range(4): transactions = [ Transaction.create( @@ -117,3 +120,46 @@ def inputs(user_vk): ] block = b.create_block(transactions) b.write_block(block, durability='hard') + + # 3. vote the blocks valid, so that the inputs are valid + vote = b.vote(block.id, prev_block_id, True) + prev_block_id = block.id + b.write_vote(vote) + + +@pytest.fixture +def user2_sk(): + return USER2_SK + + +@pytest.fixture +def user2_vk(): + return USER2_VK + + +@pytest.fixture +def inputs_shared(user_vk, user2_vk): + from bigchaindb.models import Transaction + from bigchaindb_common.exceptions import GenesisBlockAlreadyExistsError + # 1. create the genesis block + b = Bigchain() + try: + g = b.create_genesis_block() + except GenesisBlockAlreadyExistsError: + pass + + # 2. create blocks with transactions for `USER` to spend + prev_block_id = g.id + for block in range(4): + transactions = [ + Transaction.create( + [b.me], [user_vk, user2_vk], payload={'i': i}).sign([b.me_private]) + for i in range(10) + ] + block = b.create_block(transactions) + b.write_block(block, durability='hard') + + # 3. vote the blocks valid, so that the inputs are valid + vote = b.vote(block.id, prev_block_id, True) + prev_block_id = block.id + b.write_vote(vote) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 01944b6e..7b57049c 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1,3 +1,5 @@ +from time import sleep + import pytest @@ -556,6 +558,7 @@ class TestTransactionValidation(object): with pytest.raises(InvalidSignature): b.validate_transaction(tx) + @pytest.mark.usefixtures('inputs') def test_non_create_double_spend(self, b, signed_create_tx, signed_transfer_tx): from bigchaindb_common.exceptions import DoubleSpend @@ -563,10 +566,20 @@ class TestTransactionValidation(object): block1 = b.create_block([signed_create_tx]) b.write_block(block1) + # vote block valid + vote = b.vote(block1.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + b.write_transaction(signed_transfer_tx) block = b.create_block([signed_transfer_tx]) b.write_block(block, durability='hard') + # vote block valid + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + sleep(1) + signed_transfer_tx.timestamp = 123 # FIXME: https://github.com/bigchaindb/bigchaindb/issues/592 with pytest.raises(DoubleSpend): @@ -595,6 +608,32 @@ class TestTransactionValidation(object): # bigchain assert transfer_tx == b.validate_transaction(transfer_tx) + @pytest.mark.usefixtures('inputs') + def test_fulfillment_not_in_valid_block(self, b, user_vk, user_sk): + from bigchaindb.models import Transaction + from bigchaindb_common.exceptions import FulfillmentNotInValidBlock + + input_tx = b.get_owned_ids(user_vk).pop() + input_tx = b.get_transaction(input_tx.txid) + inputs = input_tx.to_inputs() + + # create a transaction that's valid but not in a voted valid block + transfer_tx = Transaction.transfer(inputs, [user_vk]) + transfer_tx = transfer_tx.sign([user_sk]) + + assert transfer_tx == b.validate_transaction(transfer_tx) + + # create block + block = b.create_block([transfer_tx]) + b.write_block(block, durability='hard') + + # create transaction with the undecided input + tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), [user_vk]) + tx_invalid = tx_invalid.sign([user_sk]) + + with pytest.raises(FulfillmentNotInValidBlock): + b.validate_transaction(tx_invalid) + class TestBlockValidation(object): @pytest.mark.skipif(reason='Separated tx validation from block creation.') @@ -695,67 +734,52 @@ class TestMultipleInputs(object): assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 + @pytest.mark.usefixtures('inputs') def test_transfer_single_owners_multiple_inputs(self, b, user_sk, user_vk): from bigchaindb_common import crypto from bigchaindb.models import Transaction user2_sk, user2_vk = crypto.generate_key_pair() - # TODO: Make this a fixture - transactions = [] - for i in range(3): - tx = Transaction.create([user_vk], [user_vk]) - tx = tx.sign([user_sk]) - transactions.append(tx) - b.write_transaction(tx) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - # get inputs owned_inputs = b.get_owned_ids(user_vk) input_txs = [b.get_transaction(tx_link.txid) for tx_link in owned_inputs] inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, 3 * [[user_vk]]) + tx = Transaction.transfer(inputs, len(inputs) * [[user_vk]]) tx = tx.sign([user_sk]) assert b.validate_transaction(tx) == tx - assert len(tx.fulfillments) == 3 - assert len(tx.conditions) == 3 + assert len(tx.fulfillments) == len(inputs) + assert len(tx.conditions) == len(inputs) + @pytest.mark.usefixtures('inputs') def test_transfer_single_owners_single_input_from_multiple_outputs(self, b, user_sk, user_vk): - import random from bigchaindb_common import crypto from bigchaindb.models import Transaction user2_sk, user2_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([user_vk], [user_vk], None, 'CREATE', payload) - tx = tx.sign([user_sk]) - transactions.append(tx) - b.write_transaction(tx) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - # get inputs owned_inputs = b.get_owned_ids(user_vk) input_txs = [b.get_transaction(tx_link.txid) for tx_link in owned_inputs] inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, 3 * [[user2_vk]]) + tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) tx = tx.sign([user_sk]) # create block with the transaction block = b.create_block([tx]) b.write_block(block, durability='hard') + # vote block valid + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + # get inputs from user2 owned_inputs = b.get_owned_ids(user2_vk) - assert len(owned_inputs) == 3 + assert len(owned_inputs) == len(inputs) # create a transaction with a single input from a multiple output transaction tx_link = owned_inputs.pop() @@ -787,40 +811,38 @@ class TestMultipleInputs(object): assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 + @pytest.mark.usefixtures('inputs') def test_single_owner_before_multiple_owners_after_multiple_inputs(self, b, user_sk, user_vk): - import random from bigchaindb_common import crypto from bigchaindb.models import Transaction user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([user_vk], [user_vk], None, 'CREATE', - payload) - tx = tx.sign([user_sk]) - transactions.append(tx) - b.write_transaction(tx) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - owned_inputs = b.get_owned_ids(user_vk) input_txs = [b.get_transaction(tx_link.txid) for tx_link in owned_inputs] inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, 3 * [[user2_vk, user3_vk]]) + tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk, user3_vk]]) tx = tx.sign([user_sk]) + # create block with the transaction + block = b.create_block([tx]) + b.write_block(block, durability='hard') + + # vote block valid + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + # validate transaction assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == 3 - assert len(tx.conditions) == 3 + assert len(tx.fulfillments) == len(inputs) + assert len(tx.conditions) == len(inputs) + @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_single_owner_after_single_input(self, b, user_sk, user_vk): @@ -835,6 +857,10 @@ class TestMultipleInputs(object): block = b.create_block([tx]) b.write_block(block, durability='hard') + # vote block valid + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + owned_input = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(owned_input.txid) inputs = input_tx.to_inputs() @@ -847,23 +873,15 @@ class TestMultipleInputs(object): assert len(transfer_tx.fulfillments) == 1 assert len(transfer_tx.conditions) == 1 + @pytest.mark.usefixtures('inputs_shared') def test_multiple_owners_before_single_owner_after_multiple_inputs(self, b, - user_sk, - user_vk): + user_sk, user_vk, user2_vk, user2_sk): from bigchaindb_common import crypto from bigchaindb.models import Transaction - user2_sk, user2_vk = crypto.generate_key_pair() + # create a new users user3_sk, user3_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - tx = Transaction.create([b.me], [user_vk, user2_vk]) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - tx_links = b.get_owned_ids(user_vk) inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link in tx_links], []) @@ -872,9 +890,10 @@ class TestMultipleInputs(object): tx = tx.sign([user_sk, user2_sk]) assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == 3 - assert len(tx.conditions) == 3 + assert len(tx.fulfillments) == len(inputs) + assert len(tx.conditions) == len(inputs) + @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_multiple_owners_after_single_input(self, b, user_sk, user_vk): @@ -890,6 +909,11 @@ class TestMultipleInputs(object): block = b.create_block([tx]) b.write_block(block, durability='hard') + # vote block valid + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + # get input tx_link = b.get_owned_ids(user_vk).pop() tx_input = b.get_transaction(tx_link.txid).to_inputs() @@ -900,25 +924,17 @@ class TestMultipleInputs(object): assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, - b, - user_sk, - user_vk): + @pytest.mark.usefixtures('inputs_shared') + def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, b, + user_sk, user_vk, + user2_sk, user2_vk): from bigchaindb_common import crypto from bigchaindb.models import Transaction - user2_sk, user2_vk = crypto.generate_key_pair() + # create a new users user3_sk, user3_vk = crypto.generate_key_pair() user4_sk, user4_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - tx = Transaction.create([b.me], [user_vk, user2_vk]) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - tx_links = b.get_owned_ids(user_vk) inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link in tx_links], []) @@ -927,8 +943,8 @@ class TestMultipleInputs(object): tx = tx.sign([user_sk, user2_sk]) assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == 3 - assert len(tx.conditions) == 3 + assert len(tx.fulfillments) == len(inputs) + assert len(tx.conditions) == len(inputs) def test_get_owned_ids_single_tx_single_output(self, b, user_sk, user_vk): from bigchaindb_common import crypto diff --git a/tests/pipelines/test_vote.py b/tests/pipelines/test_vote.py index 4ecd69a2..3ac431b7 100644 --- a/tests/pipelines/test_vote.py +++ b/tests/pipelines/test_vote.py @@ -1,3 +1,5 @@ +import time + from unittest.mock import patch import rethinkdb as r @@ -285,9 +287,10 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) - inpipe.put(block.to_dict()) - inpipe.put(block2.to_dict()) vote_pipeline.start() + inpipe.put(block.to_dict()) + time.sleep(1) + inpipe.put(block2.to_dict()) vote_out = outpipe.get() vote2_out = outpipe.get() vote_pipeline.terminate()