mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
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:
parent
278ff1ddb2
commit
af996650c3
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user