Problem: Validator set not tracked by BigchainDB (#2436)

* Problem: Validator set not tracked by BigchainDB

Solution: BigchainDB depends on tendermint's RPC API to get the validator set
which is not avaiable during replay so the validators set should be tracked
inside BigchainDB

* Problem: Unclear code and documentation

Solution: Fix decode_validator and docs strings

* Problem: Doc strings missing

Solution: Add doc string for store_validato_set
This commit is contained in:
Vanshdeep Singh 2018-08-06 11:37:43 +02:00 committed by Muawia Khan
parent e0676306b7
commit 2e9a9b1121
13 changed files with 105 additions and 99 deletions

View File

@ -8,7 +8,6 @@ from bigchaindb.common.exceptions import MultipleValidatorOperationError
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 bigchaindb.common.transaction import Transaction from bigchaindb.common.transaction import Transaction
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID
register_query = module_dispatch_registrar(backend.query) register_query = module_dispatch_registrar(backend.query)
@ -279,7 +278,7 @@ def get_pre_commit_state(conn, commit_id):
@register_query(LocalMongoDBConnection) @register_query(LocalMongoDBConnection)
def store_validator_update(conn, validator_update): def store_validator_set(conn, validator_update):
try: try:
return conn.run( return conn.run(
conn.collection('validators') conn.collection('validators')
@ -289,15 +288,16 @@ def store_validator_update(conn, validator_update):
@register_query(LocalMongoDBConnection) @register_query(LocalMongoDBConnection)
def get_validator_update(conn, update_id=VALIDATOR_UPDATE_ID): def get_validator_set(conn, height=None):
return conn.run( query = {}
conn.collection('validators') if height is not None:
.find_one({'update_id': update_id}, projection={'_id': False})) query = {'height': {'$lte': height}}
cursor = conn.run(
@register_query(LocalMongoDBConnection)
def delete_validator_update(conn, update_id=VALIDATOR_UPDATE_ID):
return conn.run(
conn.collection('validators') conn.collection('validators')
.delete_one({'update_id': update_id}) .find(query, projection={'_id': False})
.sort([('height', DESCENDING)])
.limit(1)
) )
return list(cursor)[0]

View File

@ -126,6 +126,6 @@ def create_pre_commit_secondary_index(conn, dbname):
def create_validators_secondary_index(conn, dbname): def create_validators_secondary_index(conn, dbname):
logger.info('Create `validators` secondary index.') logger.info('Create `validators` secondary index.')
conn.conn[dbname]['validators'].create_index('update_id', conn.conn[dbname]['validators'].create_index('height',
name='update_id', name='height',
unique=True,) unique=True,)

View File

@ -340,13 +340,6 @@ def store_pre_commit_state(connection, commit_id, state):
raise NotImplementedError raise NotImplementedError
@singledispatch
def store_validator_update(conn, validator_update):
"""Store a update for the validator set"""
raise NotImplementedError
@singledispatch @singledispatch
def get_pre_commit_state(connection, commit_id): def get_pre_commit_state(connection, commit_id):
"""Get pre-commit state where `id` is `commit_id`. """Get pre-commit state where `id` is `commit_id`.
@ -362,14 +355,15 @@ def get_pre_commit_state(connection, commit_id):
@singledispatch @singledispatch
def get_validator_update(conn): def store_validator_set(conn, validator_update):
"""Get validator updates which are not synced""" """Store updated validator set"""
raise NotImplementedError raise NotImplementedError
@singledispatch @singledispatch
def delete_validator_update(conn, id): def get_validator_set(conn, height):
"""Set the sync status for validator update documents""" """Get validator set for a given `height`, if `height` is not specified
then return the latest validator set"""
raise NotImplementedError raise NotImplementedError

View File

@ -1,6 +1,7 @@
"""This module contains all the goodness to integrate BigchainDB """This module contains all the goodness to integrate BigchainDB
with Tendermint.""" with Tendermint."""
import logging import logging
import codecs
from abci.application import BaseApplication from abci.application import BaseApplication
from abci.types_pb2 import ( from abci.types_pb2 import (
@ -42,11 +43,13 @@ class App(BaseApplication):
self.validators = None self.validators = None
self.new_height = None self.new_height = None
def init_chain(self, validators): def init_chain(self, genesis):
"""Initialize chain with block of height 0""" """Initialize chain with block of height 0"""
validator_set = [decode_validator(v) for v in genesis.validators]
block = Block(app_hash='', height=0, transactions=[]) block = Block(app_hash='', height=0, transactions=[])
self.bigchaindb.store_block(block._asdict()) self.bigchaindb.store_block(block._asdict())
self.bigchaindb.store_validator_set(1, validator_set)
return ResponseInitChain() return ResponseInitChain()
def info(self, request): def info(self, request):
@ -129,11 +132,11 @@ class App(BaseApplication):
else: else:
self.block_txn_hash = block['app_hash'] self.block_txn_hash = block['app_hash']
validator_updates = self.bigchaindb.get_validator_update() # TODO: calculate if an election has concluded
validator_updates = [encode_validator(v) for v in validator_updates] # NOTE: ensure the local validator set is updated
# validator_updates = self.bigchaindb.get_validator_update()
# set sync status to true # validator_updates = [encode_validator(v) for v in validator_updates]
self.bigchaindb.delete_validator_update() validator_updates = []
# Store pre-commit state to recover in case there is a crash # Store pre-commit state to recover in case there is a crash
# during `commit` # during `commit`
@ -176,3 +179,10 @@ def encode_validator(v):
return Validator(pub_key=pub_key, return Validator(pub_key=pub_key,
address=b'', address=b'',
power=v['power']) power=v['power'])
def decode_validator(v):
return {'address': codecs.encode(v.address, 'hex').decode().upper().rstrip('\n'),
'pub_key': {'type': v.pub_key.type,
'data': codecs.encode(v.pub_key.data, 'base64').decode().rstrip('\n')},
'voting_power': v.power}

View File

@ -460,19 +460,13 @@ class BigchainDB(object):
def fastquery(self): def fastquery(self):
return fastquery.FastQuery(self.connection) return fastquery.FastQuery(self.connection)
def get_validators(self): def get_validators(self, height=None):
try: result = backend.query.get_validator_set(self.connection, height)
resp = requests.get('{}validators'.format(self.endpoint)) validators = result['validators']
validators = resp.json()['result']['validators'] for v in validators:
for v in validators: v.pop('address')
v.pop('accum')
v.pop('address')
return validators return validators
except requests.exceptions.RequestException as e:
logger.error('Error while connecting to Tendermint HTTP API')
raise e
def get_validator_update(self): def get_validator_update(self):
update = backend.query.get_validator_update(self.connection) update = backend.query.get_validator_update(self.connection)
@ -484,6 +478,14 @@ class BigchainDB(object):
def store_pre_commit_state(self, state): def store_pre_commit_state(self, state):
return backend.query.store_pre_commit_state(self.connection, state) return backend.query.store_pre_commit_state(self.connection, state)
def store_validator_set(self, height, validators):
"""Store validator set at a given `height`.
NOTE: If the validator set already exists at that `height` then an
exception will be raised.
"""
return backend.query.store_validator_set(self.connection, {'height': height,
'validators': validators})
Block = namedtuple('Block', ('app_hash', 'height', 'transactions')) Block = namedtuple('Block', ('app_hash', 'height', 'transactions'))

View File

@ -370,22 +370,23 @@ def test_get_pre_commit_state(db_context):
assert resp == state._asdict() assert resp == state._asdict()
def test_store_validator_update(): def test_validator_update():
from bigchaindb.backend import connect, query from bigchaindb.backend import connect, query
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID
from bigchaindb.common.exceptions import MultipleValidatorOperationError
conn = connect() conn = connect()
validator_update = {'validator': {'key': 'value'}, def gen_validator_update(height):
'update_id': VALIDATOR_UPDATE_ID} return {'data': 'somedata', 'height': height}
query.store_validator_update(conn, deepcopy(validator_update))
with pytest.raises(MultipleValidatorOperationError): for i in range(1, 100, 10):
query.store_validator_update(conn, deepcopy(validator_update)) value = gen_validator_update(i)
query.store_validator_set(conn, value)
resp = query.get_validator_update(conn, VALIDATOR_UPDATE_ID) v1 = query.get_validator_set(conn, 8)
assert v1['height'] == 1
assert resp == validator_update v41 = query.get_validator_set(conn, 50)
assert query.delete_validator_update(conn, VALIDATOR_UPDATE_ID) assert v41['height'] == 41
assert not query.get_validator_update(conn, VALIDATOR_UPDATE_ID)
v91 = query.get_validator_set(conn)
assert v91['height'] == 91

View File

@ -40,7 +40,7 @@ def test_init_creates_db_tables_and_indexes():
assert set(indexes) == {'_id_', 'pre_commit_id'} assert set(indexes) == {'_id_', 'pre_commit_id'}
indexes = conn.conn[dbname]['validators'].index_information().keys() indexes = conn.conn[dbname]['validators'].index_information().keys()
assert set(indexes) == {'_id_', 'update_id'} assert set(indexes) == {'_id_', 'height'}
def test_init_database_fails_if_db_exists(): def test_init_database_fails_if_db_exists():

View File

@ -341,6 +341,7 @@ class MockResponse():
return {'result': {'latest_block_height': self.height}} return {'result': {'latest_block_height': self.height}}
@pytest.mark.skip
@patch('bigchaindb.config_utils.autoconfigure') @patch('bigchaindb.config_utils.autoconfigure')
@patch('bigchaindb.backend.query.store_validator_update') @patch('bigchaindb.backend.query.store_validator_update')
@pytest.mark.tendermint @pytest.mark.tendermint

View File

@ -1,4 +1,7 @@
import pytest import pytest
import codecs
import abci.types_pb2 as types
@pytest.fixture @pytest.fixture
@ -10,3 +13,13 @@ def b():
@pytest.fixture @pytest.fixture
def validator_pub_key(): def validator_pub_key():
return 'B0E42D2589A455EAD339A035D6CE1C8C3E25863F268120AA0162AD7D003A4014' return 'B0E42D2589A455EAD339A035D6CE1C8C3E25863F268120AA0162AD7D003A4014'
@pytest.fixture
def init_chain_request():
addr = codecs.decode(b'9FD479C869C7D7E7605BF99293457AA5D80C3033', 'hex')
pk = codecs.decode(b'VAgFZtYw8bNR5TMZHFOBDWk9cAmEu3/c6JgRBmddbbI=', 'base64')
val_a = types.Validator(address=addr, power=10,
pub_key=types.PubKey(type='ed25519', data=pk))
return types.RequestInitChain(validators=[val_a])

View File

@ -50,7 +50,7 @@ def test_check_tx__unsigned_create_is_error(b):
@pytest.mark.bdb @pytest.mark.bdb
def test_deliver_tx__valid_create_updates_db(b): def test_deliver_tx__valid_create_updates_db(b, init_chain_request):
from bigchaindb import App from bigchaindb import App
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
@ -64,8 +64,9 @@ def test_deliver_tx__valid_create_updates_db(b):
app = App(b) app = App(b)
app.init_chain(init_chain_request)
begin_block = RequestBeginBlock() begin_block = RequestBeginBlock()
app.init_chain(['ignore'])
app.begin_block(begin_block) app.begin_block(begin_block)
result = app.deliver_tx(encode_tx_to_bytes(tx)) result = app.deliver_tx(encode_tx_to_bytes(tx))
@ -83,7 +84,7 @@ def test_deliver_tx__valid_create_updates_db(b):
# next(unspent_outputs) # next(unspent_outputs)
def test_deliver_tx__double_spend_fails(b): def test_deliver_tx__double_spend_fails(b, init_chain_request):
from bigchaindb import App from bigchaindb import App
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
@ -96,7 +97,7 @@ def test_deliver_tx__double_spend_fails(b):
.sign([alice.private_key]) .sign([alice.private_key])
app = App(b) app = App(b)
app.init_chain(['ignore']) app.init_chain(init_chain_request)
begin_block = RequestBeginBlock() begin_block = RequestBeginBlock()
app.begin_block(begin_block) app.begin_block(begin_block)
@ -112,13 +113,13 @@ def test_deliver_tx__double_spend_fails(b):
assert result.code == CodeTypeError assert result.code == CodeTypeError
def test_deliver_transfer_tx__double_spend_fails(b): def test_deliver_transfer_tx__double_spend_fails(b, init_chain_request):
from bigchaindb import App from bigchaindb import App
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
app = App(b) app = App(b)
app.init_chain(['ignore']) app.init_chain(init_chain_request)
begin_block = RequestBeginBlock() begin_block = RequestBeginBlock()
app.begin_block(begin_block) app.begin_block(begin_block)
@ -156,14 +157,16 @@ def test_deliver_transfer_tx__double_spend_fails(b):
assert result.code == CodeTypeError assert result.code == CodeTypeError
def test_end_block_return_validator_updates(b): # The test below has to re-written one election conclusion logic has been implemented
@pytest.mark.skip
def test_end_block_return_validator_updates(b, init_chain_request):
from bigchaindb import App from bigchaindb import App
from bigchaindb.backend import query from bigchaindb.backend import query
from bigchaindb.core import encode_validator from bigchaindb.core import encode_validator
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID from bigchaindb.backend.query import VALIDATOR_UPDATE_ID
app = App(b) app = App(b)
app.init_chain(['ignore']) app.init_chain(init_chain_request)
begin_block = RequestBeginBlock() begin_block = RequestBeginBlock()
app.begin_block(begin_block) app.begin_block(begin_block)
@ -182,7 +185,7 @@ def test_end_block_return_validator_updates(b):
assert updates == [] assert updates == []
def test_store_pre_commit_state_in_end_block(b, alice): def test_store_pre_commit_state_in_end_block(b, alice, init_chain_request):
from bigchaindb import App from bigchaindb import App
from bigchaindb.backend import query from bigchaindb.backend import query
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
@ -194,7 +197,7 @@ def test_store_pre_commit_state_in_end_block(b, alice):
.sign([alice.private_key]) .sign([alice.private_key])
app = App(b) app = App(b)
app.init_chain(['ignore']) app.init_chain(init_chain_request)
begin_block = RequestBeginBlock() begin_block = RequestBeginBlock()
app.begin_block(begin_block) app.begin_block(begin_block)

View File

@ -1,3 +1,5 @@
import codecs
import abci.types_pb2 as types import abci.types_pb2 as types
import json import json
import pytest import pytest
@ -11,7 +13,7 @@ from io import BytesIO
@pytest.mark.tendermint @pytest.mark.tendermint
@pytest.mark.bdb @pytest.mark.bdb
def test_app(tb): def test_app(tb, init_chain_request):
from bigchaindb import App from bigchaindb 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
@ -28,12 +30,17 @@ def test_app(tb):
assert res.info.last_block_height == 0 assert res.info.last_block_height == 0
assert not b.get_latest_block() assert not b.get_latest_block()
p.process('init_chain', types.Request(init_chain=types.RequestInitChain())) p.process('init_chain', types.Request(init_chain=init_chain_request))
block0 = b.get_latest_block() block0 = b.get_latest_block()
assert block0 assert block0
assert block0['height'] == 0 assert block0['height'] == 0
assert block0['app_hash'] == '' assert block0['app_hash'] == ''
pk = codecs.encode(init_chain_request.validators[0].pub_key.data, 'base64').decode().strip('\n')
[validator] = b.get_validators(height=1)
assert validator['pub_key']['data'] == pk
assert validator['voting_power'] == 10
alice = generate_key_pair() alice = generate_key_pair()
bob = generate_key_pair() bob = generate_key_pair()
tx = Transaction.create([alice.public_key], tx = Transaction.create([alice.public_key],
@ -98,6 +105,7 @@ def test_app(tb):
assert block0['app_hash'] == new_block_hash assert block0['app_hash'] == new_block_hash
@pytest.mark.skip
@pytest.mark.abci @pytest.mark.abci
def test_upsert_validator(b, alice): def test_upsert_validator(b, alice):
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID from bigchaindb.backend.query import VALIDATOR_UPDATE_ID

View File

@ -139,6 +139,7 @@ def test_post_transaction_invalid_mode(b):
b.write_transaction(tx, 'nope') b.write_transaction(tx, 'nope')
@pytest.mark.skip
@pytest.mark.bdb @pytest.mark.bdb
def test_validator_updates(b, validator_pub_key): def test_validator_updates(b, validator_pub_key):
from bigchaindb.backend import query from bigchaindb.backend import query

View File

@ -1,49 +1,22 @@
import pytest import pytest
from requests.exceptions import RequestException
pytestmark = pytest.mark.tendermint pytestmark = pytest.mark.tendermint
VALIDATORS_ENDPOINT = '/api/v1/validators/' VALIDATORS_ENDPOINT = '/api/v1/validators/'
def test_get_validators_endpoint(b, client, monkeypatch): def test_get_validators_endpoint(b, client, monkeypatch):
validator_set = [{'address': 'F5426F0980E36E03044F74DD414248D29ABCBDB2',
def mock_get(uri): 'pub_key': {'data': '4E2685D9016126864733225BE00F005515200727FBAB1312FC78C8B76831255A',
return MockResponse() 'type': 'ed25519'},
monkeypatch.setattr('requests.get', mock_get) 'voting_power': 10}]
b.store_validator_set(23, validator_set)
res = client.get(VALIDATORS_ENDPOINT) res = client.get(VALIDATORS_ENDPOINT)
assert is_validator(res.json[0]) assert is_validator(res.json[0])
assert res.status_code == 200 assert res.status_code == 200
def test_get_validators_500_endpoint(b, client, monkeypatch):
def mock_get(uri):
raise RequestException
monkeypatch.setattr('requests.get', mock_get)
with pytest.raises(RequestException):
client.get(VALIDATORS_ENDPOINT)
# Helper # Helper
def is_validator(v): def is_validator(v):
return ('pub_key' in v) and ('voting_power' in v) return ('pub_key' in v) and ('voting_power' in v)
class MockResponse():
def json(self):
return {'id': '',
'jsonrpc': '2.0',
'result':
{'block_height': 5,
'validators': [
{'accum': 0,
'address': 'F5426F0980E36E03044F74DD414248D29ABCBDB2',
'pub_key': {'data': '4E2685D9016126864733225BE00F005515200727FBAB1312FC78C8B76831255A',
'type': 'ed25519'},
'voting_power': 10}]}}