Adjust multiple elections conclusion.

- Do not conclude migration election if there is a migration in progress.
- Rewrite election tests to not use mocks and assert many different things.
- Record concluded elections in the `election` collection.
This commit is contained in:
Lev Berman 2018-09-18 11:18:59 +02:00
parent e6893632dc
commit 113065b9f6
7 changed files with 236 additions and 147 deletions

View File

@ -283,12 +283,11 @@ def store_validator_set(conn, validators_update):
@register_query(LocalMongoDBConnection)
def store_election_results(conn, election):
height = election['height']
return conn.run(
conn.collection('elections').replace_one(
{'height': height},
{'election_id': election['election_id']},
election,
upsert=True
upsert=True,
)
)

View File

@ -186,27 +186,26 @@ class Election(Transaction):
election_pk))
return self.count_votes(election_pk, txns, dict.get)
@classmethod
def has_concluded(cls, bigchain, election_id, current_votes=[], height=None):
"""Check if the given `election_id` can be concluded or not
NOTE:
* Election is concluded iff the current validator set is exactly equal
to the validator set encoded in election outputs
* Election can concluded only if the current votes achieves a supermajority
def has_concluded(self, bigchain, current_votes=[], height=None):
"""Check if the election can be concluded or not.
* Elections can only be concluded if the current validator set
is exactly equal to the validator set encoded in the election outputs.
* Elections can be concluded only if the current votes form a supermajority.
Custom elections may override this function and introduce additional checks.
"""
election = bigchain.get_transaction(election_id)
if election:
election_pk = election.to_public_key(election.id)
votes_committed = election.get_commited_votes(bigchain, election_pk)
votes_current = election.count_votes(election_pk, current_votes)
current_validators = election.get_validators(bigchain, height)
election_pk = self.to_public_key(self.id)
votes_committed = self.get_commited_votes(bigchain, election_pk)
votes_current = self.count_votes(election_pk, current_votes)
current_validators = self.get_validators(bigchain, height)
if election.is_same_topology(current_validators, election.outputs):
total_votes = sum(current_validators.values())
if (votes_committed < (2/3)*total_votes) and \
(votes_committed + votes_current >= (2/3)*total_votes):
return election
if self.is_same_topology(current_validators, self.outputs):
total_votes = sum(current_validators.values())
if (votes_committed < (2/3) * total_votes) and \
(votes_committed + votes_current >= (2/3)*total_votes):
return True
return False
def get_status(self, bigchain):
@ -255,9 +254,11 @@ class Election(Transaction):
validator_set_updated = False
validator_set_change = []
for election_id, votes in elections.items():
election = Election.has_concluded(bigchain, election_id, votes, new_height)
election = bigchain.get_transaction(election_id)
if election is None:
continue
if not election:
if not election.has_concluded(bigchain, votes, new_height):
continue
if election.makes_validator_set_change():
@ -267,6 +268,7 @@ class Election(Transaction):
validator_set_updated = True
election.on_approval(bigchain, election, new_height)
election.store_election_results(bigchain, election, new_height)
return validator_set_change

View File

@ -10,6 +10,15 @@ class ChainMigrationElection(Election):
TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION
CHANGES_VALIDATOR_SET = False
def has_concluded(self, bigchaindb, *args, **kwargs):
chain = bigchaindb.get_latest_abci_chain()
if chain is not None and not chain['is_synced']:
# do not conclude the migration election if
# there is another migration in progress
return False
return super().has_concluded(bigchaindb, *args, **kwargs)
@classmethod
def on_approval(cls, bigchain, election, new_height):
bigchain.migrate_abci_chain()

View File

@ -23,7 +23,6 @@ from pymongo import MongoClient
from bigchaindb import ValidatorElection
from bigchaindb.common import crypto
from bigchaindb.log import setup_logging
from bigchaindb.migrations.chain_migration_election import ChainMigrationElection
from bigchaindb.tendermint_utils import key_from_base64
from bigchaindb.backend import schema, query
from bigchaindb.common.crypto import (key_pair_from_ed25519_key,
@ -714,22 +713,6 @@ def valid_upsert_validator_election_2(b_mock, node_key, new_validator):
new_validator, None).sign([node_key.private_key])
@pytest.fixture
def valid_chain_migration_election(b_mock, node_key):
voters = ChainMigrationElection.recipients(b_mock)
return ChainMigrationElection.generate([node_key.public_key],
voters,
{}, None).sign([node_key.private_key])
@pytest.fixture
def valid_chain_migration_election_2(b_mock, node_key):
voters = ChainMigrationElection.recipients(b_mock)
return ChainMigrationElection.generate([node_key.public_key],
voters,
{}, None).sign([node_key.private_key])
@pytest.fixture
def ongoing_validator_election(b, valid_upsert_validator_election, ed25519_node_keys):
validators = b.get_validators(height=1)
@ -758,24 +741,6 @@ def ongoing_validator_election_2(b, valid_upsert_validator_election_2, ed25519_n
return valid_upsert_validator_election_2
@pytest.fixture
def ongoing_chain_migration_election(b, valid_chain_migration_election, ed25519_node_keys):
b.store_bulk_transactions([valid_chain_migration_election])
block_1 = Block(app_hash='hash_1', height=1, transactions=[valid_chain_migration_election.id])
b.store_block(block_1._asdict())
return valid_chain_migration_election
@pytest.fixture
def ongoing_chain_migration_election_2(b, valid_chain_migration_election_2, ed25519_node_keys):
b.store_bulk_transactions([valid_chain_migration_election_2])
block_1 = Block(app_hash='hash_2', height=1, transactions=[valid_chain_migration_election_2.id])
b.store_block(block_1._asdict())
return valid_chain_migration_election_2
@pytest.fixture
def validator_election_votes(b_mock, ongoing_validator_election, ed25519_node_keys):
voters = ValidatorElection.recipients(b_mock)
@ -790,23 +755,9 @@ def validator_election_votes_2(b_mock, ongoing_validator_election_2, ed25519_nod
return votes
@pytest.fixture
def chain_migration_election_votes(b_mock, ongoing_chain_migration_election, ed25519_node_keys):
voters = ChainMigrationElection.recipients(b_mock)
votes = generate_votes(ongoing_chain_migration_election, voters, ed25519_node_keys)
return votes
@pytest.fixture
def chain_migration_election_votes_2(b_mock, ongoing_chain_migration_election_2, ed25519_node_keys):
voters = ChainMigrationElection.recipients(b_mock)
votes = generate_votes(ongoing_chain_migration_election_2, voters, ed25519_node_keys)
return votes
def generate_votes(election, voters, keys):
votes = []
for voter in range(len(voters)):
for voter, _ in enumerate(voters):
v = gen_vote(election, voter, keys)
votes.append(v)
return votes

View File

@ -1,64 +1,143 @@
from unittest.mock import MagicMock
import pytest
from tests.utils import generate_election, generate_validators
from bigchaindb.lib import Block
from bigchaindb.elections.election import Election
from bigchaindb.migrations.chain_migration_election import ChainMigrationElection
from bigchaindb.upsert_validator.validator_election import ValidatorElection
@pytest.mark.bdb
def test_approved_elections_one_migration_one_upsert(
b,
ongoing_validator_election, validator_election_votes,
ongoing_chain_migration_election, chain_migration_election_votes
):
txns = validator_election_votes + \
chain_migration_election_votes
mock_chain_migration, mock_store_validator = run_approved_elections(b, txns)
mock_chain_migration.assert_called_once()
mock_store_validator.assert_called_once()
def test_approved_elections_concludes_all_elections(b):
validators = generate_validators([1] * 4)
b.store_validator_set(1, [v['storage'] for v in validators])
new_validator = generate_validators([1])[0]
public_key = validators[0]['public_key']
private_key = validators[0]['private_key']
election, votes = generate_election(b,
ValidatorElection,
public_key, private_key,
new_validator['election'])
txs = [election]
total_votes = votes
election, votes = generate_election(b,
ChainMigrationElection,
public_key, private_key,
{})
txs += [election]
total_votes += votes
b.store_abci_chain(1, 'chain-X')
b.store_block(Block(height=1,
transactions=[tx.id for tx in txs],
app_hash='')._asdict())
b.store_bulk_transactions(txs)
Election.approved_elections(b, 1, total_votes)
validators = b.get_validators()
assert len(validators) == 5
assert new_validator['storage'] in validators
chain = b.get_latest_abci_chain()
assert chain
assert chain == {
'height': 2,
'is_synced': False,
'chain_id': 'chain-X-migrated-at-height-1',
}
for tx in txs:
election = b.get_election(tx.id)
assert election
@pytest.mark.bdb
def test_approved_elections_one_migration_two_upsert(
b,
ongoing_validator_election, validator_election_votes,
ongoing_validator_election_2, validator_election_votes_2,
ongoing_chain_migration_election, chain_migration_election_votes
):
txns = validator_election_votes + \
validator_election_votes_2 + \
chain_migration_election_votes
mock_chain_migration, mock_store_validator = run_approved_elections(b, txns)
mock_chain_migration.assert_called_once()
mock_store_validator.assert_called_once()
def test_approved_elections_applies_only_one_validator_update(b):
validators = generate_validators([1] * 4)
b.store_validator_set(1, [v['storage'] for v in validators])
new_validator = generate_validators([1])[0]
public_key = validators[0]['public_key']
private_key = validators[0]['private_key']
election, votes = generate_election(b,
ValidatorElection,
public_key, private_key,
new_validator['election'])
txs = [election]
total_votes = votes
another_validator = generate_validators([1])[0]
election, votes = generate_election(b,
ValidatorElection,
public_key, private_key,
another_validator['election'])
txs += [election]
total_votes += votes
b.store_block(Block(height=1,
transactions=[tx.id for tx in txs],
app_hash='')._asdict())
b.store_bulk_transactions(txs)
Election.approved_elections(b, 1, total_votes)
validators = b.get_validators()
assert len(validators) == 5
assert new_validator['storage'] in validators
assert another_validator['storage'] not in validators
assert b.get_election(txs[0].id)
assert not b.get_election(txs[1].id)
@pytest.mark.bdb
def test_approved_elections_two_migrations_one_upsert(
b,
ongoing_validator_election, validator_election_votes,
ongoing_chain_migration_election, chain_migration_election_votes,
ongoing_chain_migration_election_2, chain_migration_election_votes_2
):
txns = validator_election_votes + \
chain_migration_election_votes + \
chain_migration_election_votes_2
mock_chain_migration, mock_store_validator = run_approved_elections(b, txns)
assert mock_chain_migration.call_count == 2
mock_store_validator.assert_called_once()
def test_approved_elections_applies_only_one_migration(b):
validators = generate_validators([1] * 4)
b.store_validator_set(1, [v['storage'] for v in validators])
public_key = validators[0]['public_key']
private_key = validators[0]['private_key']
election, votes = generate_election(b,
ChainMigrationElection,
public_key, private_key,
{})
txs = [election]
total_votes = votes
election, votes = generate_election(b,
ChainMigrationElection,
public_key, private_key,
{})
txs += [election]
total_votes += votes
b.store_abci_chain(1, 'chain-X')
b.store_block(Block(height=1,
transactions=[tx.id for tx in txs],
app_hash='')._asdict())
b.store_bulk_transactions(txs)
Election.approved_elections(b, 1, total_votes)
chain = b.get_latest_abci_chain()
assert chain
assert chain == {
'height': 2,
'is_synced': False,
'chain_id': 'chain-X-migrated-at-height-1',
}
assert b.get_election(txs[0].id)
assert not b.get_election(txs[1].id)
def test_approved_elections_no_elections(b):
txns = []
mock_chain_migration, mock_store_validator = run_approved_elections(b, txns)
mock_chain_migration.assert_not_called()
mock_store_validator.assert_not_called()
def run_approved_elections(bigchain, txns):
mock_chain_migration = MagicMock()
mock_store_validator = MagicMock()
bigchain.migrate_abci_chain = mock_chain_migration
bigchain.store_validator_set = mock_store_validator
Election.approved_elections(bigchain, 1, txns)
return mock_chain_migration, mock_store_validator
def test_approved_elections_gracefully_handles_empty_block(b):
Election.approved_elections(b, 1, [])

View File

@ -168,14 +168,14 @@ def test_valid_election_conclude(b_mock, valid_upsert_validator_election, ed2551
# store election
b_mock.store_bulk_transactions([valid_upsert_validator_election])
# cannot conclude election as not votes exist
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
assert not valid_upsert_validator_election.has_concluded(b_mock)
# validate vote
assert tx_vote0.validate(b_mock)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote0])
assert not valid_upsert_validator_election.has_concluded(b_mock, [tx_vote0])
b_mock.store_bulk_transactions([tx_vote0])
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
assert not valid_upsert_validator_election.has_concluded(b_mock)
# Node 1: cast vote
tx_vote1 = gen_vote(valid_upsert_validator_election, 1, ed25519_node_keys)
@ -187,31 +187,31 @@ def test_valid_election_conclude(b_mock, valid_upsert_validator_election, ed2551
tx_vote3 = gen_vote(valid_upsert_validator_election, 3, ed25519_node_keys)
assert tx_vote1.validate(b_mock)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote1])
assert not valid_upsert_validator_election.has_concluded(b_mock, [tx_vote1])
# 2/3 is achieved in the same block so the election can be.has_concludedd
assert ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote1, tx_vote2])
assert valid_upsert_validator_election.has_concluded(b_mock, [tx_vote1, tx_vote2])
b_mock.store_bulk_transactions([tx_vote1])
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
assert not valid_upsert_validator_election.has_concluded(b_mock)
assert tx_vote2.validate(b_mock)
assert tx_vote3.validate(b_mock)
# conclusion can be triggered my different votes in the same block
assert ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote2])
assert ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote2, tx_vote3])
assert valid_upsert_validator_election.has_concluded(b_mock, [tx_vote2])
assert valid_upsert_validator_election.has_concluded(b_mock, [tx_vote2, tx_vote3])
b_mock.store_bulk_transactions([tx_vote2])
# Once the blockchain records >2/3 of the votes the election is assumed to be.has_concludedd
# so any invocation of `.has_concluded` for that election should return False
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
assert not valid_upsert_validator_election.has_concluded(b_mock)
# Vote is still valid but the election cannot be.has_concludedd as it it assmed that it has
# been.has_concludedd before
assert tx_vote3.validate(b_mock)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote3])
assert not valid_upsert_validator_election.has_concluded(b_mock, [tx_vote3])
@pytest.mark.abci
@ -285,9 +285,9 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
tx_vote1 = gen_vote(election, 1, ed25519_node_keys)
tx_vote2 = gen_vote(election, 2, ed25519_node_keys)
assert not ValidatorElection.has_concluded(b, election.id, [tx_vote0])
assert not ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1])
assert ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1, tx_vote2])
assert not election.has_concluded(b, [tx_vote0])
assert not election.has_concluded(b, [tx_vote0, tx_vote1])
assert election.has_concluded(b, [tx_vote0, tx_vote1, tx_vote2])
assert Election.approved_elections(b, 4, [tx_vote0]) == []
assert Election.approved_elections(b, 4, [tx_vote0, tx_vote1]) == []
@ -297,13 +297,6 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert update_public_key == public_key64
b.store_bulk_transactions([tx_vote0, tx_vote1])
update = Election.approved_elections(b, 4, [tx_vote2])
assert len(update) == 1
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert update_public_key == public_key64
# remove validator
power = 0
new_validator = {'public_key': {'value': public_key, 'type': 'ed25519-base16'},

View File

@ -2,12 +2,17 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
import base58
import base64
import random
from functools import singledispatch
from bigchaindb import Vote
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
from bigchaindb.backend.schema import TABLES
from bigchaindb.elections.election import Election
from bigchaindb.common import crypto
from bigchaindb.elections.election import Election, Vote
from bigchaindb.tendermint_utils import key_to_base64
@singledispatch
@ -24,7 +29,6 @@ def flush_localmongo_db(connection, dbname):
def generate_block(bigchain):
from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.models import Transaction
import time
alice = generate_key_pair()
tx = Transaction.create([alice.public_key],
@ -34,7 +38,6 @@ def generate_block(bigchain):
code, message = bigchain.write_transaction(tx, 'broadcast_tx_commit')
assert code == 202
time.sleep(2)
def to_inputs(election, i, ed25519_node_keys):
@ -52,3 +55,56 @@ def gen_vote(election, i, ed25519_node_keys):
[([election_pub_key], votes_i)],
election_id=election.id)\
.sign([key_i.private_key])
def generate_validators(powers):
"""Generates an arbitrary number of validators with random public keys.
The object under the `storage` key is in the format expected by DB.
The object under the `eleciton` key is in the format expected by
the upsert validator election.
`public_key`, `private_key` are in the format used for signing transactions.
Args:
powers: A list of intergers representing the voting power to
assign to the corresponding validators.
"""
validators = []
for power in powers:
kp = crypto.generate_key_pair()
validators.append({
'storage': {
'public_key': {
'value': key_to_base64(base58.b58decode(kp.public_key).hex()),
'type': 'ed25519-base64',
},
'voting_power': power,
},
'election': {
'node_id': f'node-{random.choice(range(100))}',
'power': power,
'public_key': {
'value': base64.b16encode(base58.b58decode(kp.public_key)).decode('utf-8'),
'type': 'ed25519-base16',
},
},
'public_key': kp.public_key,
'private_key': kp.private_key,
})
return validators
def generate_election(b, cls, public_key, private_key, asset_data):
voters = cls.recipients(b)
election = cls.generate([public_key],
voters,
asset_data,
None).sign([private_key])
votes = [Vote.generate([election.to_inputs()[i]],
[([Election.to_public_key(election.id)], power)],
election.id) for i, (_, power) in enumerate(voters)]
return election, votes