Merge pull request #1492 from bigchaindb/pipeline-fast-transaction

Transaction schema validation pipeline tweaks
This commit is contained in:
libscott 2017-06-01 14:25:34 +02:00 committed by GitHub
commit 30bf7c9b4e
3 changed files with 68 additions and 33 deletions

View File

@ -267,11 +267,12 @@ class Block(object):
return False
@classmethod
def from_dict(cls, block_body):
def from_dict(cls, block_body, tx_construct=Transaction.from_dict):
"""Transform a Python dictionary to a Block object.
Args:
block_body (dict): A block dictionary to be transformed.
tx_construct (functions): Function to instantiate Transaction instance
Returns:
:class:`~Block`
@ -288,8 +289,7 @@ class Block(object):
if block_id != block_body['id']:
raise InvalidHash()
transactions = [Transaction.from_dict(tx) for tx
in block['transactions']]
transactions = [tx_construct(tx) for tx in block['transactions']]
signature = block_body.get('signature')
@ -328,7 +328,7 @@ class Block(object):
}
@classmethod
def from_db(cls, bigchain, block_dict):
def from_db(cls, bigchain, block_dict, from_dict_kwargs=None):
"""
Helper method that reconstructs a block_dict that was returned from
the database. It checks what asset_ids to retrieve, retrieves the
@ -339,6 +339,7 @@ class Block(object):
used to perform database queries.
block_dict(:obj:`dict`): The block dict as returned from the
database.
from_dict_kwargs (:obj:`dict`): additional kwargs to pass to from_dict
Returns:
:class:`~Block`
@ -347,7 +348,8 @@ class Block(object):
asset_ids = cls.get_asset_ids(block_dict)
assets = bigchain.get_assets(asset_ids)
block_dict = cls.couple_assets(block_dict, assets)
return cls.from_dict(block_dict)
kwargs = from_dict_kwargs or {}
return cls.from_dict(block_dict, **kwargs)
def decouple_assets(self):
"""
@ -419,3 +421,22 @@ class Block(object):
def to_str(self):
return serialize(self.to_dict())
class FastTransaction:
"""
A minimal wrapper around a transaction dictionary. This is useful for
when validation is not required but a routine expects something that looks
like a transaction, for example during block creation.
Note: immutability could also be provided
"""
def __init__(self, tx_dict):
self.data = tx_dict
@property
def id(self):
return self.data['id']
def to_dict(self):
return self.data

View File

@ -10,8 +10,8 @@ from collections import Counter
from multipipes import Pipeline, Node
from bigchaindb import Bigchain, backend
from bigchaindb.models import Transaction, Block
from bigchaindb import backend, Bigchain
from bigchaindb.models import Transaction, Block, FastTransaction
from bigchaindb.common import exceptions
@ -41,20 +41,23 @@ class Vote:
self.counters = Counter()
self.validity = {}
self.invalid_dummy_tx = Transaction.create([self.bigchain.me],
[([self.bigchain.me], 1)])
dummy_tx = Transaction.create([self.bigchain.me],
[([self.bigchain.me], 1)]).to_dict()
self.invalid_dummy_tx = dummy_tx
def validate_block(self, block):
if not self.bigchain.has_previous_vote(block['id']):
def validate_block(self, block_dict):
if not self.bigchain.has_previous_vote(block_dict['id']):
try:
block = Block.from_db(self.bigchain, block)
block = Block.from_db(self.bigchain, block_dict, from_dict_kwargs={
'tx_construct': FastTransaction
})
except (exceptions.InvalidHash):
# XXX: if a block is invalid we should skip the `validate_tx`
# step, but since we are in a pipeline we cannot just jump to
# another function. Hackish solution: generate an invalid
# transaction and propagate it to the next steps of the
# pipeline.
return block['id'], [self.invalid_dummy_tx]
return block_dict['id'], [self.invalid_dummy_tx]
try:
block._validate_block(self.bigchain)
except exceptions.ValidationError:
@ -64,14 +67,14 @@ class Vote:
# transaction and propagate it to the next steps of the
# pipeline.
return block.id, [self.invalid_dummy_tx]
return block.id, block.transactions
return block.id, block_dict['block']['transactions']
def ungroup(self, block_id, transactions):
"""Given a block, ungroup the transactions in it.
Args:
block_id (str): the id of the block in progress.
transactions (list(Transaction)): transactions of the block in
transactions (list(dict)): transactions of the block in
progress.
Returns:
@ -84,12 +87,12 @@ class Vote:
for tx in transactions:
yield tx, block_id, num_tx
def validate_tx(self, tx, block_id, num_tx):
def validate_tx(self, tx_dict, block_id, num_tx):
"""Validate a transaction. Transaction must also not be in any VALID
block.
Args:
tx (dict): the transaction to validate
tx_dict (dict): the transaction to validate
block_id (str): the id of block containing the transaction
num_tx (int): the total number of transactions to process
@ -97,16 +100,17 @@ class Vote:
Three values are returned, the validity of the transaction,
``block_id``, ``num_tx``.
"""
new = self.bigchain.is_new_transaction(tx.id, exclude_block_id=block_id)
if not new:
return False, block_id, num_tx
try:
tx = Transaction.from_dict(tx_dict)
new = self.bigchain.is_new_transaction(tx.id, exclude_block_id=block_id)
if not new:
raise exceptions.ValidationError('Tx already exists, %s', tx.id)
tx.validate(self.bigchain)
valid = True
except exceptions.ValidationError as e:
logger.warning('Invalid tx: %s', e)
valid = False
logger.warning('Invalid tx: %s', e)
return valid, block_id, num_tx

View File

@ -94,7 +94,7 @@ def test_vote_validate_block(b):
validation = vote_obj.validate_block(block_dict)
assert validation[0] == block.id
for tx1, tx2 in zip(validation[1], block.transactions):
assert tx1 == tx2
assert tx1 == tx2.to_dict()
block = b.create_block([tx])
# NOTE: Setting a blocks signature to `None` invalidates it.
@ -152,7 +152,7 @@ def test_vote_validate_transaction(b):
from bigchaindb.pipelines import vote
from bigchaindb.common.exceptions import ValidationError
tx = dummy_tx(b)
tx = dummy_tx(b).to_dict()
vote_obj = vote.Vote()
validation = vote_obj.validate_tx(tx, 123, 1)
assert validation == (True, 123, 1)
@ -175,15 +175,13 @@ def test_vote_accumulates_transactions(b):
vote_obj = vote.Vote()
for _ in range(10):
tx = dummy_tx(b)
tx = dummy_tx(b)
tx = tx
validation = vote_obj.validate_tx(tx, 123, 1)
validation = vote_obj.validate_tx(tx.to_dict(), 123, 1)
assert validation == (True, 123, 1)
tx.inputs[0].fulfillment.signature = None
validation = vote_obj.validate_tx(tx, 456, 10)
validation = vote_obj.validate_tx(tx.to_dict(), 456, 10)
assert validation == (False, 456, 10)
@ -195,16 +193,17 @@ def test_valid_block_voting_sequential(b, genesis_block, monkeypatch):
monkeypatch.setattr('time.time', lambda: 1111111111)
vote_obj = vote.Vote()
block = dummy_block(b)
block = dummy_block(b).to_dict()
txs = block['block']['transactions']
for tx, block_id, num_tx in vote_obj.ungroup(block.id, block.transactions):
for tx, block_id, num_tx in vote_obj.ungroup(block['id'], txs):
last_vote = vote_obj.vote(*vote_obj.validate_tx(tx, block_id, num_tx))
vote_obj.write_vote(last_vote)
vote_rs = query.get_votes_by_block_id_and_voter(b.connection, block_id, b.me)
vote_doc = vote_rs.next()
assert vote_doc['vote'] == {'voting_for_block': block.id,
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis_block.id,
'is_block_valid': True,
'invalid_reason': None,
@ -578,9 +577,20 @@ def test_vote_no_double_inclusion(b):
tx = dummy_tx(b)
block = b.create_block([tx])
r = vote.Vote().validate_tx(tx, block.id, 1)
r = vote.Vote().validate_tx(tx.to_dict(), block.id, 1)
assert r == (True, block.id, 1)
b.write_block(block)
r = vote.Vote().validate_tx(tx, 'other_block_id', 1)
r = vote.Vote().validate_tx(tx.to_dict(), 'other_block_id', 1)
assert r == (False, 'other_block_id', 1)
@pytest.mark.genesis
def test_duplicate_transaction(signed_create_tx):
from bigchaindb.pipelines import vote
with patch('bigchaindb.core.Bigchain.is_new_transaction') as is_new:
is_new.return_value = False
res = vote.Vote().validate_tx(signed_create_tx.to_dict(), 'a', 1)
assert res == (False, 'a', 1)
assert is_new.call_count == 1