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.exceptions import (InvalidSignature, DoubleSpend,
InputDoesNotExist,
from bigchaindb.common.crypto import hash_data, PublicKey, PrivateKey
from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature,
DoubleSpend, InputDoesNotExist,
TransactionNotInValidBlock, TransactionNotInValidBlock,
AssetIdMismatch, AmountError, AssetIdMismatch, AmountError,
DuplicateTransaction) DuplicateTransaction)
from bigchaindb.common.transaction import Transaction from bigchaindb.common.transaction import Transaction
from bigchaindb.common.utils import (gen_timestamp, serialize, from bigchaindb.common.utils import (validate_txn_obj, validate_key)
validate_txn_obj, validate_key)
from bigchaindb.common.schema import validate_transaction_schema from bigchaindb.common.schema import validate_transaction_schema
from bigchaindb.backend.schema import validate_language_key from bigchaindb.backend.schema import validate_language_key
@ -156,368 +152,6 @@ class Transaction(Transaction):
return cls.from_dict(tx) 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: class FastTransaction:
"""A minimal wrapper around a transaction dictionary. This is useful for """A minimal wrapper around a transaction dictionary. This is useful for
when validation is not required but a routine expects something that looks 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.common.transaction import Transaction, Input, TransactionLink
from bigchaindb.core import Bigchain from bigchaindb.core import Bigchain
from bigchaindb.models import Block
from bigchaindb.tendermint import lib from bigchaindb.tendermint import lib
from bigchaindb.web import server 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' 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') @pytest.mark.usefixtures('inputs')
def test_non_create_input_not_found(self, b, user_pk): def test_non_create_input_not_found(self, b, user_pk):
from cryptoconditions import Ed25519Sha256 from cryptoconditions import Ed25519Sha256

View File

@ -1,26 +1,10 @@
import pytest import pytest
from bigchaindb.common.transaction import TransactionLink from bigchaindb.common.transaction import TransactionLink
from bigchaindb.models import Block, Transaction
pytestmark = pytest.mark.bdb 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): def test_filter_valid_items(b, blockdata):
blocks, _ = blockdata blocks, _ = blockdata
assert (b.fastquery.filter_valid_items(blocks, block_id_key=lambda b: b['id']) 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) == [ assert b.fastquery.get_outputs_by_public_key(user2_pk) == [
TransactionLink(blocks[0]['block']['transactions'][0]['id'], 0) 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
}