Return commit hash to tendermint (#1851)

* Aggregate transaction ids for commit hash

* Setup chain and return commit hash to tendermint

* Fix function naming
This commit is contained in:
Vanshdeep Singh 2017-11-15 20:11:23 +05:30 committed by vrde
parent 7dc7d745ec
commit e27c1e9cef
7 changed files with 220 additions and 5 deletions

View File

@ -4,6 +4,7 @@ from bigchaindb import backend
from bigchaindb.backend.exceptions import DuplicateKeyError from bigchaindb.backend.exceptions import DuplicateKeyError
from bigchaindb.backend.utils import module_dispatch_registrar from bigchaindb.backend.utils import module_dispatch_registrar
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
from pymongo import DESCENDING
register_query = module_dispatch_registrar(backend.query) register_query = module_dispatch_registrar(backend.query)
@ -59,3 +60,20 @@ def get_spent(conn, transaction_id, output):
{'_id': 0})) {'_id': 0}))
except IndexError: except IndexError:
pass pass
@register_query(LocalMongoDBConnection)
def get_latest_block(conn):
return conn.run(
conn.collection('blocks')
.find_one(sort=[('height', DESCENDING)]))
@register_query(LocalMongoDBConnection)
def store_block(conn, block):
try:
return conn.run(
conn.collection('blocks')
.insert_one(block))
except DuplicateKeyError:
pass

View File

@ -2,7 +2,7 @@
import logging import logging
from pymongo import ASCENDING, TEXT from pymongo import ASCENDING, DESCENDING, TEXT
from bigchaindb import backend from bigchaindb import backend
from bigchaindb.common import exceptions from bigchaindb.common import exceptions
@ -27,7 +27,7 @@ def create_database(conn, dbname):
@register_schema(LocalMongoDBConnection) @register_schema(LocalMongoDBConnection)
def create_tables(conn, dbname): def create_tables(conn, dbname):
for table_name in ['transactions', 'assets']: for table_name in ['transactions', 'assets', 'blocks']:
logger.info('Create `%s` table.', table_name) logger.info('Create `%s` table.', table_name)
# create the table # create the table
# TODO: read and write concerns can be declared here # TODO: read and write concerns can be declared here
@ -38,6 +38,7 @@ def create_tables(conn, dbname):
def create_indexes(conn, dbname): def create_indexes(conn, dbname):
create_transactions_secondary_index(conn, dbname) create_transactions_secondary_index(conn, dbname)
create_assets_secondary_index(conn, dbname) create_assets_secondary_index(conn, dbname)
create_blocks_secondary_index(conn, dbname)
@register_schema(LocalMongoDBConnection) @register_schema(LocalMongoDBConnection)
@ -80,3 +81,8 @@ def create_assets_secondary_index(conn, dbname):
# full text search index # full text search index
conn.conn[dbname]['assets'].create_index([('$**', TEXT)], name='text') conn.conn[dbname]['assets'].create_index([('$**', TEXT)], name='text')
def create_blocks_secondary_index(conn, dbname):
conn.conn[dbname]['blocks']\
.create_index([('height', DESCENDING)], name='height')

View File

@ -438,3 +438,24 @@ def text_search(conn, search, *, language='english', case_sensitive=False,
raise OperationError('This query is only supported when running ' raise OperationError('This query is only supported when running '
'BigchainDB with MongoDB as the backend.') 'BigchainDB with MongoDB as the backend.')
@singledispatch
def get_latest_block(conn):
"""Get the latest commited block i.e. block with largest height """
raise NotImplementedError
@singledispatch
def store_block(conn, block):
"""Write a new block to the `blocks` table
Args:
block (dict): block with current height and block hash.
Returns:
The result of the operation.
"""
raise NotImplementedError

View File

@ -1,11 +1,12 @@
"""This module contains all the goodness to integrate BigchainDB """This module contains all the goodness to integrate BigchainDB
with Tendermint.""" with Tendermint."""
from abci import BaseApplication, Result from abci import BaseApplication, Result
from abci.types_pb2 import ResponseEndBlock, ResponseInfo
from bigchaindb.tendermint import BigchainDB from bigchaindb.tendermint import BigchainDB
from bigchaindb.tendermint.utils import decode_transaction from bigchaindb.tendermint.utils import decode_transaction, calculate_hash
from bigchaindb.tendermint.lib import Block
class App(BaseApplication): class App(BaseApplication):
@ -19,6 +20,29 @@ class App(BaseApplication):
if not bigchaindb: if not bigchaindb:
bigchaindb = BigchainDB() bigchaindb = BigchainDB()
self.bigchaindb = bigchaindb self.bigchaindb = bigchaindb
self.block_txn_ids = []
self.block_txn_hash = ''
self.validators = None
self.new_height = None
def init_chain(self, validators):
"""Initialize chain with block of height 0"""
block = Block(hash='', height=0)
self.bigchaindb.store_block(block.to_dict())
def info(self):
"""Return height of the latest committed block."""
r = ResponseInfo()
block = self.bigchaindb.get_latest_block()
if block:
r.last_block_height = block['height']
r.last_block_app_hash = block['hash'].encode('utf-8')
else:
r.last_block_height = 0
r.last_block_app_hash = b''
return r
def check_tx(self, raw_transaction): def check_tx(self, raw_transaction):
"""Validate the transaction before entry into """Validate the transaction before entry into
@ -33,12 +57,16 @@ class App(BaseApplication):
else: else:
return Result.error() return Result.error()
def begin_block(self, block_hash, header):
"""Initialize list of transaction."""
self.block_txn_ids = []
def deliver_tx(self, raw_transaction): def deliver_tx(self, raw_transaction):
"""Validate the transaction before mutating the state. """Validate the transaction before mutating the state.
Args: Args:
raw_tx: a raw string (in bytes) transaction.""" raw_tx: a raw string (in bytes) transaction."""
transaction = self.bigchaindb.validate_transaction( transaction = self.bigchaindb.validate_transaction(
decode_transaction(raw_transaction)) decode_transaction(raw_transaction))
@ -46,4 +74,34 @@ class App(BaseApplication):
return Result.error(log='Invalid transaction') return Result.error(log='Invalid transaction')
else: else:
self.bigchaindb.store_transaction(transaction) self.bigchaindb.store_transaction(transaction)
self.block_txn_ids.append(transaction.id)
return Result.ok() return Result.ok()
def end_block(self, height):
"""Calculate block hash using transaction ids and previous block
hash to be stored in the next block.
Args:
height (int): new height of the chain."""
self.new_height = height
block_txn_hash = calculate_hash(self.block_txn_ids)
block = self.bigchaindb.get_latest_block()
if self.block_txn_ids:
self.block_txn_hash = calculate_hash([block['hash'], block_txn_hash])
else:
self.block_txn_hash = block['hash']
return ResponseEndBlock()
def commit(self):
"""Store the new height and along with block hash."""
# register a new block only when new transactions are received
if self.block_txn_ids:
block = Block(hash=self.block_txn_hash, height=self.new_height)
self.bigchaindb.store_block(block.to_dict())
data = self.block_txn_hash.encode('utf-8')
return Result.ok(data=data)

View File

@ -70,6 +70,16 @@ class BigchainDB(Bigchain):
output) output)
return Transaction.from_dict(transaction) return Transaction.from_dict(transaction)
def store_block(self, block):
"""Create a new block."""
return backend.query.store_block(self.connection, block)
def get_latest_block(self):
"""Get the block with largest height."""
return backend.query.get_latest_block(self.connection)
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."""
@ -90,3 +100,15 @@ class BigchainDB(Bigchain):
logger.warning('Invalid transaction (%s): %s', type(e).__name__, e) logger.warning('Invalid transaction (%s): %s', type(e).__name__, e)
return False return False
return transaction return transaction
class Block(object):
def __init__(self, hash='', height=0):
self.hash = hash
self.height = height
def to_dict(self):
block = {'hash': self.hash,
'height': self.height}
return block

View File

@ -1,5 +1,6 @@
import base64 import base64
import json import json
import sha3
def encode_transaction(value): def encode_transaction(value):
@ -12,3 +13,14 @@ def decode_transaction(raw):
"""Decode a transaction from Base64 to a dict.""" """Decode a transaction from Base64 to a dict."""
return json.loads(raw.decode('utf8')) return json.loads(raw.decode('utf8'))
def calculate_hash(key_list):
if not key_list:
return ''
full_hash = sha3.sha3_256()
for key in key_list:
full_hash.update(key.encode('utf8'))
return full_hash.hexdigest()

View File

@ -0,0 +1,78 @@
import json
import pytest
@pytest.mark.bdb
@pytest.mark.tapp
def test_app(b):
from bigchaindb.tendermint import App
from bigchaindb.tendermint.utils import calculate_hash
from abci.server import ProtocolHandler
from io import BytesIO
import abci.types_pb2 as types
from abci.wire import read_message
from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.models import Transaction
from abci.messages import to_request_deliver_tx, to_request_check_tx
app = App(b)
p = ProtocolHandler(app)
data = p.process('info', None)
res, err = read_message(BytesIO(data), types.Response)
assert res
assert res.info.last_block_app_hash == b''
assert res.info.last_block_height == 0
assert not b.get_latest_block()
p.process('init_chain', None)
block0 = b.get_latest_block()
assert block0
assert block0['height'] == 0
assert block0['hash'] == ''
alice = generate_key_pair()
bob = generate_key_pair()
tx = Transaction.create([alice.public_key],
[([bob.public_key], 1)])\
.sign([alice.private_key])
etxn = json.dumps(tx.to_dict()).encode('utf8')
r = to_request_check_tx(etxn)
data = p.process('check_tx', r)
res, err = read_message(BytesIO(data), types.Response)
assert res
assert res.check_tx.code == 0
r = types.Request()
r.begin_block.hash = b''
p.process('begin_block', r)
r = to_request_deliver_tx(etxn)
data = p.process('deliver_tx', r)
res, err = read_message(BytesIO(data), types.Response)
assert res
assert res.deliver_tx.code == 0
assert b.get_transaction(tx.id).id == tx.id
new_block_txn_hash = calculate_hash([tx.id])
r = types.Request()
r.end_block.height = 1
data = p.process('end_block', r)
res, err = read_message(BytesIO(data), types.Response)
assert res
assert 'end_block' == res.WhichOneof("value")
new_block_hash = calculate_hash([block0['hash'], new_block_txn_hash])
data = p.process('commit', None)
res, err = read_message(BytesIO(data), types.Response)
assert res
assert res.commit.code == 0
assert res.commit.data == new_block_hash.encode('utf-8')
block0 = b.get_latest_block()
assert block0
assert block0['height'] == 1
assert block0['hash'] == new_block_hash