Problem: The Block class is deprecated (#2368)

* Problem: docs are not building anymore
Solution: add wget to the requirements for docs

* Problem: The Block class is deprecated
Solution: remove the block class and all occurrences
This commit is contained in:
codegeschrei 2018-06-29 15:32:16 +02:00 committed by Zachary Bowen
parent 278ff1ddb2
commit af996650c3
4 changed files with 3 additions and 477 deletions

View File

@ -1,14 +1,10 @@
from copy import deepcopy
from bigchaindb.common.crypto import hash_data, PublicKey, PrivateKey
from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature,
DoubleSpend, InputDoesNotExist,
from bigchaindb.common.exceptions import (InvalidSignature, DoubleSpend,
InputDoesNotExist,
TransactionNotInValidBlock,
AssetIdMismatch, AmountError,
DuplicateTransaction)
from bigchaindb.common.transaction import Transaction
from bigchaindb.common.utils import (gen_timestamp, serialize,
validate_txn_obj, validate_key)
from bigchaindb.common.utils import (validate_txn_obj, validate_key)
from bigchaindb.common.schema import validate_transaction_schema
from bigchaindb.backend.schema import validate_language_key
@ -156,368 +152,6 @@ class Transaction(Transaction):
return cls.from_dict(tx)
# TODO: Remove node_pubkey as part of cleanup II
class Block(object):
"""Bundle a list of Transactions in a Block.
Attributes:
transaction (:obj:`list` of :class:`~.Transaction`):
Transactions to be included in the Block.
node_pubkey (str): The public key of the node creating the
Block.
timestamp (str): The Unix time a Block was created.
signature (str): A cryptographic signature ensuring the
integrity and validity of the creator of a Block.
"""
def __init__(self, transactions=None, node_pubkey=None, timestamp=None,
signature=None):
"""The Block model is mainly used for (de)serialization and integrity
checking.
Args:
transaction (:obj:`list` of :class:`~.Transaction`):
Transactions to be included in the Block.
node_pubkey (str): The public key of the node creating the
Block.
timestamp (str): The Unix time a Block was created.
signature (str): A cryptographic signature ensuring the
integrity and validity of the creator of a Block.
"""
if transactions is not None and not isinstance(transactions, list):
raise TypeError('`transactions` must be a list instance or None')
else:
self.transactions = transactions or []
if timestamp is not None:
self.timestamp = timestamp
else:
self.timestamp = gen_timestamp()
self.node_pubkey = node_pubkey
self.signature = signature
def __eq__(self, other):
try:
other = other.to_dict()
except AttributeError:
return False
return self.to_dict() == other
def validate(self, bigchain):
"""Validate the Block.
Args:
bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain
object.
Note:
The hash of the block (`id`) is validated on the `self.from_dict`
method. This is because the `from_dict` is the only method in
which we have the original json payload. The `id` provided by
this class is a mutable property that is generated on the fly.
Returns:
:class:`~.Block`: If valid, return a `Block` object. Else an
appropriate exception describing the reason of invalidity is
raised.
Raises:
ValidationError: If the block or any transaction in the block does
not validate
"""
self._validate_block(bigchain)
self._validate_block_transactions(bigchain)
return self
def _validate_block(self, bigchain):
"""Validate the Block without validating the transactions.
Args:
bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain
object.
Raises:
ValidationError: If there is a problem with the block
"""
# Check that the signature is valid
if not self.is_signature_valid():
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):
"""Validate Block transactions.
Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
Raises:
ValidationError: If an invalid transaction is found
"""
for tx in self.transactions:
# If a transaction is not valid, `validate_transactions` will
# throw an an exception and block validation will be canceled.
bigchain.validate_transaction(tx)
def sign(self, private_key):
"""Create a signature for the Block and overwrite `self.signature`.
Args:
private_key (str): A private key corresponding to
`self.node_pubkey`.
Returns:
:class:`~.Block`
"""
block_body = self.to_dict()
block_serialized = serialize(block_body['block'])
private_key = PrivateKey(private_key)
self.signature = private_key.sign(block_serialized.encode()).decode()
return self
def is_signature_valid(self):
"""Check the validity of a Block's signature.
Returns:
bool: Stating the validity of the Block's signature.
"""
block = self.to_dict()['block']
# cc only accepts bytestring messages
block_serialized = serialize(block).encode()
public_key = PublicKey(block['node_pubkey'])
try:
# NOTE: CC throws a `ValueError` on some wrong signatures
# https://github.com/bigchaindb/cryptoconditions/issues/27
return public_key.verify(block_serialized, self.signature)
except (ValueError, AttributeError):
return False
@classmethod
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`
Raises:
InvalidHash: If the block's id is not corresponding to its
data.
"""
# Validate block id
block = block_body['block']
block_serialized = serialize(block)
block_id = hash_data(block_serialized)
if block_id != block_body['id']:
raise InvalidHash()
transactions = [tx_construct(tx) for tx in block['transactions']]
signature = block_body.get('signature')
return cls(transactions, block['node_pubkey'],
block['timestamp'], signature)
@property
def id(self):
return self.to_dict()['id']
def to_dict(self):
"""Transform the Block to a Python dictionary.
Returns:
dict: The Block as a dict.
Raises:
ValueError: If the Block doesn't contain any transactions.
"""
if len(self.transactions) == 0:
raise ValueError('Empty block creation is not allowed')
block = {
'timestamp': self.timestamp,
'transactions': [tx.to_dict() for tx in self.transactions],
'node_pubkey': self.node_pubkey,
}
block_serialized = serialize(block)
block_id = hash_data(block_serialized)
return {
'id': block_id,
'block': block,
'signature': self.signature,
}
@classmethod
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
assets from the assets table and reconstructs the block.
Args:
bigchain (:class:`~bigchaindb.Bigchain`): An instance of Bigchain
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`
"""
asset_ids = cls.get_asset_ids(block_dict)
assets = bigchain.get_assets(asset_ids)
txn_ids = cls.get_txn_ids(block_dict)
metadata = bigchain.get_metadata(txn_ids)
# reconstruct block
block_dict = cls.couple_assets(block_dict, assets)
block_dict = cls.couple_metadata(block_dict, metadata)
kwargs = from_dict_kwargs or {}
return cls.from_dict(block_dict, **kwargs)
def decouple_assets(self, block_dict=None):
"""Extracts the assets from the ``CREATE`` transactions in the block.
Returns:
tuple: (assets, block) with the assets being a list of dicts and
the block being the dict of the block with no assets in the CREATE
transactions.
"""
if block_dict is None:
block_dict = deepcopy(self.to_dict())
assets = []
for transaction in block_dict['block']['transactions']:
if transaction['operation'] == Transaction.CREATE:
asset = transaction.pop('asset')
asset.update({'id': transaction['id']})
assets.append(asset)
return (assets, block_dict)
def decouple_metadata(self, block_dict=None):
"""Extracts the metadata from transactions in the block.
Returns:
tuple: (metadatas, block) with the metadatas being a list of dict/null and
the block being the dict of the block with no metadata in any transaction.
"""
if block_dict is None:
block_dict = deepcopy(self.to_dict())
metadatas = []
for transaction in block_dict['block']['transactions']:
metadata = transaction.pop('metadata')
if metadata:
metadata_new = {'id': transaction['id'],
'metadata': metadata}
metadatas.append(metadata_new)
return (metadatas, block_dict)
@staticmethod
def couple_assets(block_dict, assets):
"""Given a block_dict with no assets (as returned from a database call)
and a list of assets, reconstruct the original block by putting the
assets back into the ``CREATE`` transactions in the block.
Args:
block_dict (:obj:`dict`): The block dict as returned from a
database call.
assets (:obj:`list` of :obj:`dict`): A list of assets returned from
a database call.
Returns:
dict: The dict of the reconstructed block.
"""
# create a dict with {'<txid>': asset}
assets = {asset.pop('id'): asset for asset in assets}
# add the assets to the block transactions
for transaction in block_dict['block']['transactions']:
if transaction['operation'] == Transaction.CREATE:
transaction.update({'asset': assets.get(transaction['id'])})
return block_dict
@staticmethod
def couple_metadata(block_dict, metadatal):
"""Given a block_dict with no metadata (as returned from a database call)
and a list of metadata, reconstruct the original block by putting the
metadata of each transaction back into its original transaction.
NOTE: Till a transaction gets accepted the `metadata` of the transaction
is not moved outside of the transaction. So, if a transaction is found to
have metadata then it should not be overridden.
Args:
block_dict (:obj:`dict`): The block dict as returned from a
database call.
metadata (:obj:`list` of :obj:`dict`): A list of metadata returned from
a database call.
Returns:
dict: The dict of the reconstructed block.
"""
# create a dict with {'<txid>': metadata}
metadatal = {m.pop('id'): m.pop('metadata') for m in metadatal}
# add the metadata to their corresponding transactions
for transaction in block_dict['block']['transactions']:
metadata = metadatal.get(transaction['id'], None)
transaction.update({'metadata': metadata})
return block_dict
@staticmethod
def get_asset_ids(block_dict):
"""Given a block_dict return all the asset_ids for that block (the txid
of CREATE transactions). Useful to know which assets to retrieve
from the database to reconstruct the block.
Args:
block_dict (:obj:`dict`): The block dict as returned from a
database call.
Returns:
list: The list of asset_ids in the block.
"""
asset_ids = []
for transaction in block_dict['block']['transactions']:
if transaction['operation'] == Transaction.CREATE:
asset_ids.append(transaction['id'])
return asset_ids
@staticmethod
def get_txn_ids(block_dict):
"""Given a block_dict return all the transaction ids.
Args:
block_dict (:obj:`dict`): The block dict as returned from a
database call.
Returns:
list: The list of txn_ids in the block.
"""
txn_ids = []
for transaction in block_dict['block']['transactions']:
txn_ids.append(transaction['id'])
return txn_ids
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

View File

@ -6,7 +6,6 @@ import os.path
from bigchaindb.common.transaction import Transaction, Input, TransactionLink
from bigchaindb.core import Bigchain
from bigchaindb.models import Block
from bigchaindb.tendermint import lib
from bigchaindb.web import server

View File

@ -433,17 +433,6 @@ class TestBigchainApi(object):
assert excinfo.value.args[0] == 'Empty block creation is not allowed'
@pytest.mark.genesis
def test_get_last_voted_block_returns_genesis_if_no_votes_has_been_casted(self, b):
from bigchaindb.models import Block
from bigchaindb.backend import query
genesis = query.get_genesis_block(b.connection)
genesis = Block.from_db(b, genesis)
gb = b.get_last_voted_block()
assert gb == genesis
assert b.validate_block(gb) == gb
@pytest.mark.usefixtures('inputs')
def test_non_create_input_not_found(self, b, user_pk):
from cryptoconditions import Ed25519Sha256

View File

@ -1,26 +1,10 @@
import pytest
from bigchaindb.common.transaction import TransactionLink
from bigchaindb.models import Block, Transaction
pytestmark = pytest.mark.bdb
@pytest.fixture
def blockdata(b, user_pk, user2_pk):
txs = [Transaction.create([user_pk], [([user2_pk], 1)]),
Transaction.create([user2_pk], [([user_pk], 1)]),
Transaction.create([user_pk], [([user_pk], 1), ([user2_pk], 1)])]
blocks = []
for i in range(3):
block = Block([txs[i]])
b.write_block(block)
blocks.append(block.to_dict())
b.write_vote(b.vote(blocks[1]['id'], '', True))
b.write_vote(b.vote(blocks[2]['id'], '', False))
return blocks, [b['id'] for b in blocks]
def test_filter_valid_items(b, blockdata):
blocks, _ = blockdata
assert (b.fastquery.filter_valid_items(blocks, block_id_key=lambda b: b['id'])
@ -35,83 +19,3 @@ def test_get_outputs_by_public_key(b, user_pk, user2_pk, blockdata):
assert b.fastquery.get_outputs_by_public_key(user2_pk) == [
TransactionLink(blocks[0]['block']['transactions'][0]['id'], 0)
]
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]:
block = Block([tx])
b.write_block(block)
b.write_vote(b.vote(block.id, '', True))
# The second TRANSFER is invalid. inputs[1] remains unspent.
block = Block([tx3])
b.write_block(block)
b.write_vote(b.vote(block.id, '', False))
# The third TRANSFER is undecided. It procuces a new unspent.
block = Block([tx4])
b.write_block(block)
outputs = b.fastquery.get_outputs_by_public_key(user_pk)
unspents = b.fastquery.filter_spent_outputs(outputs)
assert set(unspents) == {
inputs[1].fulfills,
tx2.to_inputs()[0].fulfills,
tx4.to_inputs()[0].fulfills
}
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]:
block = Block([tx])
b.write_block(block)
b.write_vote(b.vote(block.id, '', True))
# The second TRANSFER is invalid. inputs[1] remains unspent.
block = Block([tx3])
b.write_block(block)
b.write_vote(b.vote(block.id, '', False))
# The third TRANSFER is undecided. It procuces a new unspent.
block = Block([tx4])
b.write_block(block)
outputs = b.fastquery.get_outputs_by_public_key(user_pk)
spents = b.fastquery.filter_unspent_outputs(outputs)
assert set(spents) == {
inputs[0].fulfills,
inputs[2].fulfills
}