Skip to content

Commit

Permalink
Problem: Validator set not tracked by BigchainDB (#2436)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kansi authored and muawiakh committed Aug 6, 2018
1 parent e067630 commit 2e9a9b1
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 99 deletions.
22 changes: 11 additions & 11 deletions bigchaindb/backend/localmongodb/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from bigchaindb.backend.utils import module_dispatch_registrar
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
from bigchaindb.common.transaction import Transaction
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID

register_query = module_dispatch_registrar(backend.query)

Expand Down Expand Up @@ -279,7 +278,7 @@ def get_pre_commit_state(conn, commit_id):


@register_query(LocalMongoDBConnection)
def store_validator_update(conn, validator_update):
def store_validator_set(conn, validator_update):
try:
return conn.run(
conn.collection('validators')
Expand All @@ -289,15 +288,16 @@ def store_validator_update(conn, validator_update):


@register_query(LocalMongoDBConnection)
def get_validator_update(conn, update_id=VALIDATOR_UPDATE_ID):
return conn.run(
conn.collection('validators')
.find_one({'update_id': update_id}, projection={'_id': False}))
def get_validator_set(conn, height=None):
query = {}
if height is not None:
query = {'height': {'$lte': height}}


@register_query(LocalMongoDBConnection)
def delete_validator_update(conn, update_id=VALIDATOR_UPDATE_ID):
return conn.run(
cursor = conn.run(
conn.collection('validators')
.delete_one({'update_id': update_id})
.find(query, projection={'_id': False})
.sort([('height', DESCENDING)])
.limit(1)
)

return list(cursor)[0]
4 changes: 2 additions & 2 deletions bigchaindb/backend/localmongodb/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,6 @@ def create_pre_commit_secondary_index(conn, dbname):
def create_validators_secondary_index(conn, dbname):
logger.info('Create `validators` secondary index.')

conn.conn[dbname]['validators'].create_index('update_id',
name='update_id',
conn.conn[dbname]['validators'].create_index('height',
name='height',
unique=True,)
16 changes: 5 additions & 11 deletions bigchaindb/backend/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,6 @@ def store_pre_commit_state(connection, commit_id, state):
raise NotImplementedError


@singledispatch
def store_validator_update(conn, validator_update):
"""Store a update for the validator set"""

raise NotImplementedError


@singledispatch
def get_pre_commit_state(connection, commit_id):
"""Get pre-commit state where `id` is `commit_id`.
Expand All @@ -362,14 +355,15 @@ def get_pre_commit_state(connection, commit_id):


@singledispatch
def get_validator_update(conn):
"""Get validator updates which are not synced"""
def store_validator_set(conn, validator_update):
"""Store updated validator set"""

raise NotImplementedError


@singledispatch
def delete_validator_update(conn, id):
"""Set the sync status for validator update documents"""
def get_validator_set(conn, height):
"""Get validator set for a given `height`, if `height` is not specified
then return the latest validator set"""

raise NotImplementedError
22 changes: 16 additions & 6 deletions bigchaindb/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""This module contains all the goodness to integrate BigchainDB
with Tendermint."""
import logging
import codecs

from abci.application import BaseApplication
from abci.types_pb2 import (
Expand Down Expand Up @@ -42,11 +43,13 @@ def __init__(self, bigchaindb=None):
self.validators = None
self.new_height = None

def init_chain(self, validators):
def init_chain(self, genesis):
"""Initialize chain with block of height 0"""

validator_set = [decode_validator(v) for v in genesis.validators]
block = Block(app_hash='', height=0, transactions=[])
self.bigchaindb.store_block(block._asdict())
self.bigchaindb.store_validator_set(1, validator_set)
return ResponseInitChain()

def info(self, request):
Expand Down Expand Up @@ -129,11 +132,11 @@ def end_block(self, request_end_block):
else:
self.block_txn_hash = block['app_hash']

validator_updates = self.bigchaindb.get_validator_update()
validator_updates = [encode_validator(v) for v in validator_updates]

# set sync status to true
self.bigchaindb.delete_validator_update()
# TODO: calculate if an election has concluded
# NOTE: ensure the local validator set is updated
# validator_updates = self.bigchaindb.get_validator_update()
# validator_updates = [encode_validator(v) for v in validator_updates]
validator_updates = []

# Store pre-commit state to recover in case there is a crash
# during `commit`
Expand Down Expand Up @@ -176,3 +179,10 @@ def encode_validator(v):
return Validator(pub_key=pub_key,
address=b'',
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}
26 changes: 14 additions & 12 deletions bigchaindb/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,19 +460,13 @@ def get_metadata(self, txn_ids):
def fastquery(self):
return fastquery.FastQuery(self.connection)

def get_validators(self):
try:
resp = requests.get('{}validators'.format(self.endpoint))
validators = resp.json()['result']['validators']
for v in validators:
v.pop('accum')
v.pop('address')

return validators
def get_validators(self, height=None):
result = backend.query.get_validator_set(self.connection, height)
validators = result['validators']
for v in validators:
v.pop('address')

except requests.exceptions.RequestException as e:
logger.error('Error while connecting to Tendermint HTTP API')
raise e
return validators

def get_validator_update(self):
update = backend.query.get_validator_update(self.connection)
Expand All @@ -484,6 +478,14 @@ def delete_validator_update(self):
def store_pre_commit_state(self, 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'))

Expand Down
25 changes: 13 additions & 12 deletions tests/backend/localmongodb/test_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,22 +370,23 @@ def test_get_pre_commit_state(db_context):
assert resp == state._asdict()


def test_store_validator_update():
def test_validator_update():
from bigchaindb.backend import connect, query
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID
from bigchaindb.common.exceptions import MultipleValidatorOperationError

conn = connect()

validator_update = {'validator': {'key': 'value'},
'update_id': VALIDATOR_UPDATE_ID}
query.store_validator_update(conn, deepcopy(validator_update))
def gen_validator_update(height):
return {'data': 'somedata', 'height': height}

with pytest.raises(MultipleValidatorOperationError):
query.store_validator_update(conn, deepcopy(validator_update))
for i in range(1, 100, 10):
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
assert query.delete_validator_update(conn, VALIDATOR_UPDATE_ID)
assert not query.get_validator_update(conn, VALIDATOR_UPDATE_ID)
v41 = query.get_validator_set(conn, 50)
assert v41['height'] == 41

v91 = query.get_validator_set(conn)
assert v91['height'] == 91
2 changes: 1 addition & 1 deletion tests/backend/localmongodb/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_init_creates_db_tables_and_indexes():
assert set(indexes) == {'_id_', 'pre_commit_id'}

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():
Expand Down
1 change: 1 addition & 0 deletions tests/commands/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ def json(self):
return {'result': {'latest_block_height': self.height}}


@pytest.mark.skip
@patch('bigchaindb.config_utils.autoconfigure')
@patch('bigchaindb.backend.query.store_validator_update')
@pytest.mark.tendermint
Expand Down
13 changes: 13 additions & 0 deletions tests/tendermint/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest
import codecs

import abci.types_pb2 as types


@pytest.fixture
Expand All @@ -10,3 +13,13 @@ def b():
@pytest.fixture
def validator_pub_key():
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])
23 changes: 13 additions & 10 deletions tests/tendermint/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_check_tx__unsigned_create_is_error(b):


@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.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
Expand All @@ -64,8 +64,9 @@ def test_deliver_tx__valid_create_updates_db(b):

app = App(b)

app.init_chain(init_chain_request)

begin_block = RequestBeginBlock()
app.init_chain(['ignore'])
app.begin_block(begin_block)

result = app.deliver_tx(encode_tx_to_bytes(tx))
Expand All @@ -83,7 +84,7 @@ def test_deliver_tx__valid_create_updates_db(b):
# 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.models import Transaction
from bigchaindb.common.crypto import generate_key_pair
Expand All @@ -96,7 +97,7 @@ def test_deliver_tx__double_spend_fails(b):
.sign([alice.private_key])

app = App(b)
app.init_chain(['ignore'])
app.init_chain(init_chain_request)

begin_block = RequestBeginBlock()
app.begin_block(begin_block)
Expand All @@ -112,13 +113,13 @@ def test_deliver_tx__double_spend_fails(b):
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.models import Transaction
from bigchaindb.common.crypto import generate_key_pair

app = App(b)
app.init_chain(['ignore'])
app.init_chain(init_chain_request)

begin_block = RequestBeginBlock()
app.begin_block(begin_block)
Expand Down Expand Up @@ -156,14 +157,16 @@ def test_deliver_transfer_tx__double_spend_fails(b):
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.backend import query
from bigchaindb.core import encode_validator
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID

app = App(b)
app.init_chain(['ignore'])
app.init_chain(init_chain_request)

begin_block = RequestBeginBlock()
app.begin_block(begin_block)
Expand All @@ -182,7 +185,7 @@ def test_end_block_return_validator_updates(b):
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.backend import query
from bigchaindb.models import Transaction
Expand All @@ -194,7 +197,7 @@ def test_store_pre_commit_state_in_end_block(b, alice):
.sign([alice.private_key])

app = App(b)
app.init_chain(['ignore'])
app.init_chain(init_chain_request)

begin_block = RequestBeginBlock()
app.begin_block(begin_block)
Expand Down
12 changes: 10 additions & 2 deletions tests/tendermint/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import codecs

import abci.types_pb2 as types
import json
import pytest
Expand All @@ -11,7 +13,7 @@

@pytest.mark.tendermint
@pytest.mark.bdb
def test_app(tb):
def test_app(tb, init_chain_request):
from bigchaindb import App
from bigchaindb.tendermint_utils import calculate_hash
from bigchaindb.common.crypto import generate_key_pair
Expand All @@ -28,12 +30,17 @@ def test_app(tb):
assert res.info.last_block_height == 0
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()
assert block0
assert block0['height'] == 0
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()
bob = generate_key_pair()
tx = Transaction.create([alice.public_key],
Expand Down Expand Up @@ -98,6 +105,7 @@ def test_app(tb):
assert block0['app_hash'] == new_block_hash


@pytest.mark.skip
@pytest.mark.abci
def test_upsert_validator(b, alice):
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID
Expand Down
1 change: 1 addition & 0 deletions tests/tendermint/test_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def test_post_transaction_invalid_mode(b):
b.write_transaction(tx, 'nope')


@pytest.mark.skip
@pytest.mark.bdb
def test_validator_updates(b, validator_pub_key):
from bigchaindb.backend import query
Expand Down

0 comments on commit 2e9a9b1

Please sign in to comment.