Merge pull request #1377 from bigchaindb/bug/1343/double-spent-critical-on-voting

Bug/1343/double spent critical on voting
This commit is contained in:
Rodolphe Marques 2017-04-06 16:36:17 +02:00 committed by GitHub
commit b90766f2c5
4 changed files with 56 additions and 29 deletions

View File

@ -324,43 +324,57 @@ class Bigchain(object):
def get_spent(self, txid, output): def get_spent(self, txid, output):
"""Check if a `txid` was already used as an input. """Check if a `txid` was already used as an input.
A transaction can be used as an input for another transaction. Bigchain needs to make sure that a A transaction can be used as an input for another transaction. Bigchain
given `txid` is only used once. needs to make sure that a given `(txid, output)` is only used once.
This method will check if the `(txid, output)` has already been
spent in a transaction that is in either the `VALID`, `UNDECIDED` or
`BACKLOG` state.
Args: Args:
txid (str): The id of the transaction txid (str): The id of the transaction
output (num): the index of the output in the respective transaction output (num): the index of the output in the respective transaction
Returns: Returns:
The transaction (Transaction) that used the `txid` as an input else The transaction (Transaction) that used the `(txid, output)` as an
`None` input else `None`
Raises:
CriticalDoubleSpend: If the given `(txid, output)` was spent in
more than one valid transaction.
""" """
# checks if an input was already spent # checks if an input was already spent
# checks if the bigchain has any transaction with input {'txid': ..., # checks if the bigchain has any transaction with input {'txid': ...,
# 'output': ...} # 'output': ...}
transactions = list(backend.query.get_spent(self.connection, txid, output)) transactions = list(backend.query.get_spent(self.connection, txid,
output))
# a transaction_id should have been spent at most one time # a transaction_id should have been spent at most one time
if transactions: # determine if these valid transactions appear in more than one valid
# determine if these valid transactions appear in more than one valid block # block
num_valid_transactions = 0 num_valid_transactions = 0
for transaction in transactions: non_invalid_transactions = []
# ignore invalid blocks for transaction in transactions:
# FIXME: Isn't there a faster solution than doing I/O again? # ignore transactions in invalid blocks
if self.get_transaction(transaction['id']): # FIXME: Isn't there a faster solution than doing I/O again?
num_valid_transactions += 1 _, status = self.get_transaction(transaction['id'],
if num_valid_transactions > 1: include_status=True)
raise core_exceptions.CriticalDoubleSpend( if status == self.TX_VALID:
'`{}` was spent more than once. There is a problem' num_valid_transactions += 1
' with the chain'.format(txid)) # `txid` can only have been spent in at most on valid block.
if num_valid_transactions > 1:
raise core_exceptions.CriticalDoubleSpend(
'`{}` was spent more than once. There is a problem'
' with the chain'.format(txid))
# if its not and invalid transaction
if status is not None:
non_invalid_transactions.append(transaction)
if num_valid_transactions: if non_invalid_transactions:
return Transaction.from_dict(transactions[0]) return Transaction.from_dict(non_invalid_transactions[0])
else:
# all queried transactions were invalid # Either no transaction was returned spending the `(txid, output)` as
return None # input or the returned transactions are not valid.
else:
return None
def get_outputs(self, owner): def get_outputs(self, owner):
"""Retrieve a list of links to transaction outputs for a given public """Retrieve a list of links to transaction outputs for a given public

View File

@ -187,6 +187,11 @@ class Block(object):
if not self.is_signature_valid(): if not self.is_signature_valid():
raise InvalidSignature('Invalid block signature') raise InvalidSignature('Invalid block signature')
# Check that the block contains no duplicated transactions
txids = [tx.id for tx in self.transactions]
if len(txids) != len(set(txids)):
raise DuplicateTransaction('Block has duplicate transaction')
def _validate_block_transactions(self, bigchain): def _validate_block_transactions(self, bigchain):
"""Validate Block transactions. """Validate Block transactions.
@ -196,10 +201,6 @@ class Block(object):
Raises: Raises:
ValidationError: If an invalid transaction is found ValidationError: If an invalid transaction is found
""" """
txids = [tx.id for tx in self.transactions]
if len(txids) != len(set(txids)):
raise DuplicateTransaction('Block has duplicate transaction')
for tx in self.transactions: for tx in self.transactions:
# If a transaction is not valid, `validate_transactions` will # If a transaction is not valid, `validate_transactions` will
# throw an an exception and block validation will be canceled. # throw an an exception and block validation will be canceled.

View File

@ -111,6 +111,18 @@ def test_validate_block_with_invalid_id(b):
assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx] assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx]
@pytest.mark.genesis
def test_validate_block_with_duplicated_transactions(b):
from bigchaindb.pipelines import vote
tx = dummy_tx(b)
block = b.create_block([tx, tx]).to_dict()
vote_obj = vote.Vote()
block_id, invalid_dummy_tx = vote_obj.validate_block(block)
assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx]
@pytest.mark.genesis @pytest.mark.genesis
def test_validate_block_with_invalid_signature(b): def test_validate_block_with_invalid_signature(b):
from bigchaindb.pipelines import vote from bigchaindb.pipelines import vote

View File

@ -152,4 +152,4 @@ class TestBlockModel(object):
tx = Transaction.create([b.me], [([b.me], 1)]) tx = Transaction.create([b.me], [([b.me], 1)])
block = b.create_block([tx, tx]) block = b.create_block([tx, tx])
with raises(DuplicateTransaction): with raises(DuplicateTransaction):
block._validate_block_transactions(b) block._validate_block(b)