Integrate blocks API (#1970)

* Integrate blocks api

* Update docs

* Fix docs

* Fixed mismatch between code and documentation

* Fixed docs
This commit is contained in:
Vanshdeep Singh 2018-01-31 19:12:41 +05:30 committed by vrde
parent c156d0bfe8
commit e25d365828
13 changed files with 214 additions and 101 deletions

View File

@ -32,6 +32,17 @@ def get_transaction(conn, transaction_id):
pass pass
@register_query(LocalMongoDBConnection)
def get_transactions(conn, transaction_ids):
try:
return conn.run(
conn.collection('transactions')
.find({'id': {'$in': transaction_ids}},
projection={'_id': False}))
except IndexError:
pass
@register_query(LocalMongoDBConnection) @register_query(LocalMongoDBConnection)
def store_metadata(conn, metadata): def store_metadata(conn, metadata):
try: try:
@ -153,3 +164,11 @@ def get_spending_transactions(conn, inputs):
{'$project': {'_id': False}} {'$project': {'_id': False}}
])) ]))
return cursor return cursor
@register_query(LocalMongoDBConnection)
def get_block(conn, block_id):
return conn.run(
conn.collection('blocks')
.find_one({'height': block_id},
projection={'_id': False}))

View File

@ -68,6 +68,20 @@ def get_transaction(connection, transaction_id):
raise NotImplementedError raise NotImplementedError
@singledispatch
def get_transactions(connection, transaction_ids):
"""Get transactions from the transactions table.
Args:
transaction_ids (list): list of transaction ids to fetch
Returns:
The result of the operation.
"""
raise NotImplementedError
@singledispatch @singledispatch
def get_asset(connection, asset_id): def get_asset(connection, asset_id):
"""Get a transaction from the transactions table. """Get a transaction from the transactions table.

View File

@ -96,7 +96,7 @@ class Transaction(Transaction):
return super().from_dict(tx_body) return super().from_dict(tx_body)
@classmethod @classmethod
def from_db(cls, bigchain, tx_dict): def from_db(cls, bigchain, tx_dict_list):
"""Helper method that reconstructs a transaction dict that was returned """Helper method that reconstructs a transaction dict that was returned
from the database. It checks what asset_id to retrieve, retrieves the from the database. It checks what asset_id to retrieve, retrieves the
asset from the asset table and reconstructs the transaction. asset from the asset table and reconstructs the transaction.
@ -104,29 +104,46 @@ class Transaction(Transaction):
Args: Args:
bigchain (:class:`~bigchaindb.Bigchain`): An instance of Bigchain bigchain (:class:`~bigchaindb.Bigchain`): An instance of Bigchain
used to perform database queries. used to perform database queries.
tx_dict (:obj:`dict`): The transaction dict as returned from the tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or
database. list of transaction dict as returned from the database.
Returns: Returns:
:class:`~Transaction` :class:`~Transaction`
""" """
if tx_dict['operation'] in [Transaction.CREATE, Transaction.GENESIS]: return_list = True
# TODO: Maybe replace this call to a call to get_asset_by_id if isinstance(tx_dict_list, dict):
asset = list(bigchain.get_assets([tx_dict['id']]))[0] tx_dict_list = [tx_dict_list]
return_list = False
tx_map = {}
tx_ids = []
for tx in tx_dict_list:
tx.update({'metadata': None})
tx_map[tx['id']] = tx
if tx['operation'] in [Transaction.CREATE, Transaction.GENESIS]:
tx_ids.append(tx['id'])
assets = list(bigchain.get_assets(tx_ids))
for asset in assets:
tx = tx_map[asset['id']]
del asset['id'] del asset['id']
tx_dict.update({'asset': asset}) tx.update({'asset': asset})
# get metadata of the transaction tx_ids = list(tx_map.keys())
metadata = list(bigchain.get_metadata([tx_dict['id']])) metadata_list = list(bigchain.get_metadata(tx_ids))
if 'metadata' not in tx_dict: for metadata in metadata_list:
metadata = metadata[0] if metadata else None tx = tx_map[metadata['id']]
if metadata: tx.update({'metadata': metadata.get('metadata')})
metadata = metadata.get('metadata')
tx_dict.update({'metadata': metadata}) if return_list:
tx_list = []
return cls.from_dict(tx_dict) for tx_id, tx in tx_map.items():
tx_list.append(cls.from_dict(tx))
return tx_list
else:
tx = list(tx_map.values())[0]
return cls.from_dict(tx)
class Block(object): class Block(object):

View File

@ -31,7 +31,7 @@ class App(BaseApplication):
def init_chain(self, validators): def init_chain(self, validators):
"""Initialize chain with block of height 0""" """Initialize chain with block of height 0"""
block = Block(app_hash='', height=0) block = Block(app_hash='', height=0, transactions=[])
self.bigchaindb.store_block(block._asdict()) self.bigchaindb.store_block(block._asdict())
def info(self): def info(self):
@ -112,7 +112,9 @@ class App(BaseApplication):
# register a new block only when new transactions are received # register a new block only when new transactions are received
if self.block_txn_ids: if self.block_txn_ids:
block = Block(app_hash=self.block_txn_hash, height=self.new_height) block = Block(app_hash=self.block_txn_hash,
height=self.new_height,
transactions=self.block_txn_ids)
self.bigchaindb.store_block(block._asdict()) self.bigchaindb.store_block(block._asdict())
data = self.block_txn_hash.encode('utf-8') data = self.block_txn_hash.encode('utf-8')

View File

@ -116,6 +116,42 @@ class BigchainDB(Bigchain):
return backend.query.get_latest_block(self.connection) return backend.query.get_latest_block(self.connection)
def get_block(self, block_id, include_status=False):
"""Get the block with the specified `block_id` (and optionally its status)
Returns the block corresponding to `block_id` or None if no match is
found.
Args:
block_id (str): block id of the block to get
include_status (bool): also return the status of the block
the return value is then a tuple: (block, status)
"""
# get block from database
if isinstance(block_id, str):
block_id = int(block_id)
block = backend.query.get_block(self.connection, block_id)
if block:
transactions = backend.query.get_transactions(self.connection, block['transactions'])
transactions = Transaction.from_db(self, transactions)
block = {'height': block['height'],
'transactions': []}
block_txns = block['transactions']
for txn in transactions:
block_txns.append(txn.to_dict())
status = None
if include_status:
# NOTE: (In Tendermint) a block is an abstract entity which
# exists only after it has been validated
if block:
status = self.BLOCK_VALID
return block, status
else:
return block
def validate_transaction(self, tx): def validate_transaction(self, tx):
"""Validate a transaction against the current status of the database.""" """Validate a transaction against the current status of the database."""
@ -142,4 +178,4 @@ class BigchainDB(Bigchain):
return fastquery.FastQuery(self.connection, self.me) return fastquery.FastQuery(self.connection, self.me)
Block = namedtuple('Block', ('app_hash', 'height')) Block = namedtuple('Block', ('app_hash', 'height', 'transactions'))

View File

@ -7,6 +7,7 @@ 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.models import Block
from bigchaindb.tendermint import lib
from bigchaindb.web import server from bigchaindb.web import server
@ -242,18 +243,24 @@ def main():
node_private = "5G2kE1zJAgTajkVSbPAQWo4c2izvtwqaNHYsaNpbbvxX" node_private = "5G2kE1zJAgTajkVSbPAQWo4c2izvtwqaNHYsaNpbbvxX"
node_public = "DngBurxfeNVKZWCEcDnLj1eMPAS7focUZTE5FndFGuHT" node_public = "DngBurxfeNVKZWCEcDnLj1eMPAS7focUZTE5FndFGuHT"
signature = "53wxrEQDYk1dXzmvNSytbCfmNVnPqPkDQaTnAe8Jf43s6ssejPxezkCvUnGTnduNUmaLjhaan1iRLi3peu6s5DzA" signature = "53wxrEQDYk1dXzmvNSytbCfmNVnPqPkDQaTnAe8Jf43s6ssejPxezkCvUnGTnduNUmaLjhaan1iRLi3peu6s5DzA"
block = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
ctx['block'] = pretty_json(block.to_dict())
ctx['blockid'] = block.id
app_hash = 'f6e0c49c6d94d6924351f25bb334cf2a99af4206339bf784e741d1a5ab599056'
block = lib.Block(height=1, transactions=[tx.to_dict()], app_hash=app_hash)
block_dict = block._asdict()
block_dict.pop('app_hash')
ctx['block'] = pretty_json(block_dict)
ctx['blockid'] = block.height
block = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
block_transfer = Block(transactions=[tx_transfer], node_pubkey=node_public, block_transfer = Block(transactions=[tx_transfer], node_pubkey=node_public,
voters=[node_public], signature=signature) voters=[node_public], signature=signature)
ctx['block_transfer'] = pretty_json(block.to_dict()) ctx['block_transfer'] = pretty_json(block_transfer.to_dict())
# vote # vote
vblock = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
DUMMY_SHA3 = '0123456789abcdef' * 4 DUMMY_SHA3 = '0123456789abcdef' * 4
b = Bigchain(public_key=node_public, private_key=node_private) b = Bigchain(public_key=node_public, private_key=node_private)
vote = b.vote(block.id, DUMMY_SHA3, True) vote = b.vote(vblock.id, DUMMY_SHA3, True)
ctx['vote'] = pretty_json(vote) ctx['vote'] = pretty_json(vote)
# block status # block status

View File

@ -9,82 +9,23 @@ A block must contain the following JSON keys
.. code-block:: json .. code-block:: json
{ {
"id": "<ID of the block>", "height": "<Height of the block>",
"block": { "transactions": ["<List of transactions>"]
"timestamp": "<Block-creation timestamp>",
"transactions": ["<List of transactions>"],
"node_pubkey": "<Public key of the node which created the block>",
"voters": ["<List of public keys of all nodes in the cluster>"]
},
"signature": "<Signature of inner block object>"
} }
The JSON Keys in a Block The JSON Keys in a Block
------------------------ ------------------------
**id** **height**
The transaction ID and also the SHA3-256 hash The block ``"height"`` (``integer``) denotes the height of the blockchain when the given block was committed.
of the inner ``block`` object, loosely speaking. Since the blockchain height increases monotonically the height of block can be regarded as its id.
It's a string.
To compute it, 1) construct an :term:`associative array` ``d`` containing **NOTE**: The genesis block has height ``0``
``block.timestamp``, ``block.transactions``, ``block.node_pubkey``,
``block.voters``, and their values. 2) compute ``id = hash_of_aa(d)``.
There's pseudocode for the ``hash_of_aa()`` function
in the `IPDB Transaction Spec page about cryptographic hashes
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-hashes.html#computing-the-hash-of-an-associative-array>`_.
The result (``id``) is a string: the block ID.
An example is ``"b60adf655932bf47ef58c0bfb2dd276d4795b94346b36cbb477e10d7eb02cea8"``
**block.timestamp** **transactions**
The `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_
when the block was created, according to the node which created it.
It's a string representation of an integer.
An example is ``"1507294217"``.
**block.transactions**
A list of the :ref:`transactions <The Transaction Model>` included in the block. A list of the :ref:`transactions <The Transaction Model>` included in the block.
(Each transaction is a JSON object.) (Each transaction is a JSON object.)
**block.node_pubkey**
The public key of the node that created the block.
It's a string.
See the `IPDB Transaction Spec page about cryptographic keys & signatures
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-keys-and-sigs.html>`_.
**block.voters**
A list of the public keys of all cluster nodes at the time the block was created.
It's a list of strings.
This list can change from block to block, as nodes join and leave the cluster.
**signature**
The cryptographic signature of the inner ``block``
by the node that created the block
(i.e. the node with public key ``node_pubkey``).
To compute that:
#. Construct an :term:`associative array` ``d`` containing the contents
of the inner ``block``
(i.e. ``block.timestamp``, ``block.transactions``, ``block.node_pubkey``,
``block.voters``, and their values).
#. Compute ``signature = sig_of_aa(d, private_key)``,
where ``private_key`` is the node's private key
(i.e. ``node_pubkey`` and ``private_key`` are a key pair). There's pseudocode
for the ``sig_of_aa()`` function
on `the IPDB Transaction Spec page about cryptographic keys and signatures
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-keys-and-sigs.html#computing-the-signature-of-an-associative-array>`_.
.. note::
The ``d_bytes`` computed when computing the block ID will be the *same* as the ``d_bytes`` computed when computing the block signature. This can be used to avoid redundant calculations.

View File

@ -597,14 +597,12 @@ The `votes endpoint <#votes>`_ contains all the voting information for a specifi
Blocks Blocks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. http:get:: /api/v1/blocks/{block_id} .. http:get:: /api/v1/blocks/{block_height}
Get the block with the ID ``block_id``. Any blocks, be they ``VALID``, ``UNDECIDED`` or ``INVALID`` will be Get the block with the height ``block_height``.
returned. To check a block's status independently, use the `Statuses endpoint <#status>`_.
To check the votes on a block, have a look at the `votes endpoint <#votes>`_.
:param block_id: block ID :param block_height: block ID
:type block_id: hex string :type block_height: integer
**Example request**: **Example request**:
@ -620,7 +618,7 @@ Blocks
:resheader Content-Type: ``application/json`` :resheader Content-Type: ``application/json``
:statuscode 200: A block with that ID was found. :statuscode 200: A block with that ID was found.
:statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks`` without the ``block_id``. :statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks`` without the ``block_height``.
:statuscode 404: A block with that ID was not found. :statuscode 404: A block with that ID was not found.

View File

@ -152,3 +152,31 @@ def test_get_spending_transactions(user_pk, user_sk):
# tx3 not a member because input 1 not asked for # tx3 not a member because input 1 not asked for
assert txns == [tx2.to_dict(), tx4.to_dict()] assert txns == [tx2.to_dict(), tx4.to_dict()]
def test_store_block():
from bigchaindb.backend import connect, query
from bigchaindb.tendermint.lib import Block
conn = connect()
block = Block(app_hash='random_utxo',
height=3,
transactions=[])
query.store_block(conn, block._asdict())
cursor = conn.db.blocks.find({}, projection={'_id': False})
assert cursor.count() == 1
def test_get_block():
from bigchaindb.backend import connect, query
from bigchaindb.tendermint.lib import Block
conn = connect()
block = Block(app_hash='random_utxo',
height=3,
transactions=[])
conn.db.blocks.insert_one(block._asdict())
block = dict(query.get_block(conn, 3))
assert block['height'] == 3

View File

@ -12,12 +12,13 @@ pytestmark = pytest.mark.tendermint
@pytest.mark.bdb @pytest.mark.bdb
def test_app(b): def test_app(tb):
from bigchaindb.tendermint import App from bigchaindb.tendermint import App
from bigchaindb.tendermint.utils import calculate_hash from bigchaindb.tendermint.utils import calculate_hash
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
b = tb
app = App(b) app = App(b)
p = ProtocolHandler(app) p = ProtocolHandler(app)

View File

@ -37,12 +37,15 @@ def test_asset_is_separated_from_transaciton(b):
assert b.get_transaction(tx.id) == tx assert b.get_transaction(tx.id) == tx
def test_get_latest_block(b): def test_get_latest_block(tb):
from bigchaindb.tendermint.lib import Block from bigchaindb.tendermint.lib import Block
b = tb
for i in range(10): for i in range(10):
app_hash = os.urandom(16).hex() app_hash = os.urandom(16).hex()
block = Block(app_hash=app_hash, height=i)._asdict() txn_id = os.urandom(16).hex()
block = Block(app_hash=app_hash, height=i,
transactions=[txn_id])._asdict()
b.store_block(block) b.store_block(block)
block = b.get_latest_block() block = b.get_latest_block()

View File

@ -4,6 +4,7 @@ import rethinkdb as r
from bigchaindb.backend.mongodb.connection import MongoDBConnection from bigchaindb.backend.mongodb.connection import MongoDBConnection
from bigchaindb.backend.rethinkdb.connection import RethinkDBConnection from bigchaindb.backend.rethinkdb.connection import RethinkDBConnection
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
@singledispatch @singledispatch
@ -47,6 +48,15 @@ def flush_mongo_db(connection, dbname):
connection.conn[dbname].metadata.delete_many({}) connection.conn[dbname].metadata.delete_many({})
@flush_db.register(LocalMongoDBConnection)
def flush_localmongo_db(connection, dbname):
connection.conn[dbname].bigchain.delete_many({})
connection.conn[dbname].blocks.delete_many({})
connection.conn[dbname].transactions.delete_many({})
connection.conn[dbname].assets.delete_many({})
connection.conn[dbname].metadata.delete_many({})
@singledispatch @singledispatch
def update_table_config(connection, table, **kwrgas): def update_table_config(connection, table, **kwrgas):
raise NotImplementedError raise NotImplementedError

View File

@ -0,0 +1,37 @@
import pytest
from bigchaindb.models import Transaction
from bigchaindb.tendermint.lib import Block
BLOCKS_ENDPOINT = '/api/v1/blocks/'
pytestmark = pytest.mark.tendermint
@pytest.mark.bdb
@pytest.mark.usefixtures('inputs')
def test_get_block_endpoint(tb, client):
b = tb
tx = Transaction.create([b.me], [([b.me], 1)], asset={'cycle': 'hero'})
tx = tx.sign([b.me_private])
b.store_transaction(tx)
block = Block(app_hash='random_utxo',
height=31,
transactions=[tx.id])
b.store_block(block._asdict())
res = client.get(BLOCKS_ENDPOINT + str(block.height))
expected_response = {'height': block.height, 'transactions': [tx.to_dict()]}
assert res.json == expected_response
assert res.status_code == 200
@pytest.mark.bdb
@pytest.mark.usefixtures('inputs')
def test_get_block_returns_404_if_not_found(client):
res = client.get(BLOCKS_ENDPOINT + '123')
assert res.status_code == 404
res = client.get(BLOCKS_ENDPOINT + '123/')
assert res.status_code == 404