Create migration election class (#2535).

* Problem: We need a way to synchronize a halt to block production to allow for upgrades across breaking changes.

* Solution: Created `MigrationElection`.

* Problem: Need documentation for `migration` elections.

* Solution: Updated the docs.

* Problem: `MigrationElection` needs 'new' CLI method.

* Solution: Updated the definition of `election` to include the new `migration` type.

* Problem: The way `end_block` checks for concluded elections assumes there is only one type of election (so we can't conclude an `upsert-validator` and a `chain-migration` at the same height).

* Solution: Re-engineered the code in `Elections` to conclude multiple elections in the same block. If more than one election change the validator set, only one of them is applied.

* Problem: Tendermint change to store validator changes at height h+2 will break `Election.get_status`.

* Solution: Reworked `get_validator_change` to look at only the latest block height or less.
This commit is contained in:
Zachary Bowen
2018-09-17 13:59:57 +02:00
committed by Lev Berman
parent 3cb0f8e2ab
commit e6893632dc
20 changed files with 551 additions and 223 deletions

View File

@@ -7,6 +7,7 @@ import logging
from bigchaindb.log import DEFAULT_LOGGING_CONFIG as log_config
from bigchaindb.lib import BigchainDB # noqa
from bigchaindb.migrations.chain_migration_election import ChainMigrationElection
from bigchaindb.version import __version__ # noqa
from bigchaindb.core import App # noqa
@@ -99,4 +100,5 @@ from bigchaindb.elections.vote import Vote # noqa
Transaction.register_type(Transaction.CREATE, models.Transaction)
Transaction.register_type(Transaction.TRANSFER, models.Transaction)
Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection)
Transaction.register_type(ChainMigrationElection.OPERATION, ChainMigrationElection)
Transaction.register_type(Vote.OPERATION, Vote)

View File

@@ -13,6 +13,7 @@ import copy
import json
import sys
from bigchaindb.migrations.chain_migration_election import ChainMigrationElection
from bigchaindb.utils import load_node_key
from bigchaindb.common.exceptions import (DatabaseDoesNotExist,
ValidationError)
@@ -115,6 +116,31 @@ def run_election_new(args, bigchain):
globals()[f'run_election_new_{args.election_type}'](args, bigchain)
def create_new_election(sk, bigchain, election_class, data):
try:
key = load_node_key(sk)
voters = election_class.recipients(bigchain)
election = election_class.generate([key.public_key],
voters,
data, None).sign([key.private_key])
election.validate(bigchain)
except ValidationError as e:
logger.error(e)
return False
except FileNotFoundError as fd_404:
logger.error(fd_404)
return False
resp = bigchain.write_transaction(election, 'broadcast_tx_commit')
if resp == (202, ''):
logger.info('[SUCCESS] Submitted proposal with id: {}'.format(election.id))
return election.id
else:
logger.error('Failed to commit election proposal')
return False
def run_election_new_upsert_validator(args, bigchain):
"""Initiates an election to add/update/remove a validator to an existing BigchainDB network
@@ -136,27 +162,21 @@ def run_election_new_upsert_validator(args, bigchain):
'node_id': args.node_id
}
try:
key = load_node_key(args.sk)
voters = ValidatorElection.recipients(bigchain)
election = ValidatorElection.generate([key.public_key],
voters,
new_validator, None).sign([key.private_key])
election.validate(bigchain)
except ValidationError as e:
logger.error(e)
return False
except FileNotFoundError as fd_404:
logger.error(fd_404)
return False
return create_new_election(args.sk, bigchain, ValidatorElection, new_validator)
resp = bigchain.write_transaction(election, 'broadcast_tx_commit')
if resp == (202, ''):
logger.info('[SUCCESS] Submitted proposal with id: {}'.format(election.id))
return election.id
else:
logger.error('Failed to commit election proposal')
return False
def run_election_new_chain_migration(args, bigchain):
"""Initiates an election to halt block production
:param args: dict
args = {
'sk': the path to the private key of the node calling the election (str)
}
:param bigchain: an instance of BigchainDB
:return: election_id or `False` in case of failure
"""
return create_new_election(args.sk, bigchain, ChainMigrationElection, {})
def run_election_approve(args, bigchain):

View File

@@ -16,5 +16,14 @@ elections = {
'help': 'Path to the private key of the election initiator.'
}
}
},
'chain-migration': {
'help': 'Call for a halt to block production to allow for a version change across breaking changes.',
'args': {
'--private-key': {
'dest': 'sk',
'help': 'Path to the private key of the election initiator.'
}
}
}
}

View File

@@ -37,6 +37,9 @@ _, TX_SCHEMA_TRANSFER = _load_schema('transaction_transfer_' +
_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' +
TX_SCHEMA_VERSION)
_, TX_SCHEMA_CHAIN_MIGRATION_ELECTION = _load_schema('transaction_chain_migration_election_' +
TX_SCHEMA_VERSION)
_, TX_SCHEMA_VOTE = _load_schema('transaction_vote_' + TX_SCHEMA_VERSION)

View File

@@ -0,0 +1,44 @@
# Copyright BigchainDB GmbH and BigchainDB contributors
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
---
"$schema": "http://json-schema.org/draft-04/schema#"
type: object
title: Chain Migration Election Schema - Propose a halt in block production to allow for a version change
required:
- operation
- asset
- outputs
properties:
operation:
type: string
value: "CHAIN_MIGRATION_ELECTION"
asset:
additionalProperties: false
properties:
data:
additionalProperties: false
properties:
seed:
type: string
required:
- data
outputs:
type: array
items:
"$ref": "#/definitions/output"
definitions:
output:
type: object
properties:
condition:
type: object
required:
- uri
properties:
uri:
type: string
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
subtypes=ed25519-sha-256(&)?){2,3}$"

View File

@@ -63,6 +63,7 @@ definitions:
- CREATE
- TRANSFER
- VALIDATOR_ELECTION
- CHAIN_MIGRATION_ELECTION
- VOTE
asset:
type: object

View File

@@ -20,13 +20,13 @@ from abci.types_pb2 import (
)
from bigchaindb import BigchainDB
from bigchaindb.elections.election import Election
from bigchaindb.version import __tm_supported_versions__
from bigchaindb.utils import tendermint_version_is_compatible
from bigchaindb.tendermint_utils import (decode_transaction,
calculate_hash)
from bigchaindb.lib import Block, PreCommitState
from bigchaindb.backend.query import PRE_COMMIT_ID
from bigchaindb.upsert_validator import ValidatorElection
import bigchaindb.upsert_validator.validator_utils as vutils
from bigchaindb.events import EventTypes, Event
@@ -219,15 +219,12 @@ class App(BaseApplication):
else:
self.block_txn_hash = block['app_hash']
# Check if the current block concluded any validator elections and
# update the locally tracked validator set
validator_update = ValidatorElection.approved_update(self.bigchaindb,
self.new_height,
self.block_transactions)
update = [validator_update] if validator_update else []
# Process all concluded elections in the current block and get any update to the validator set
update = Election.approved_elections(self.bigchaindb,
self.new_height,
self.block_transactions)
# Store pre-commit state to recover in case there is a crash
# during `commit`
# Store pre-commit state to recover in case there is a crash during `commit`
pre_commit_state = PreCommitState(commit_id=PRE_COMMIT_ID,
height=self.new_height,
transactions=self.block_txn_ids)

View File

@@ -1,6 +1,7 @@
# Copyright BigchainDB GmbH and BigchainDB contributors
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
from collections import defaultdict
import base58
from uuid import uuid4
@@ -33,17 +34,18 @@ class Election(Transaction):
INCONCLUSIVE = 'inconclusive'
# Vote ratio to approve an election
ELECTION_THRESHOLD = 2 / 3
CHANGES_VALIDATOR_SET = True
@classmethod
def get_validator_change(cls, bigchain, height=None):
"""Return the latest change to the validator set
def get_validator_change(cls, bigchain):
"""Return the validator set from the most recent approved block
:return: {
'height': <block_height>,
'validators': <validator_set>,
'election_id': <election_id_that_approved_the_change>
'validators': <validator_set>
}
"""
height = bigchain.get_latest_block()['height']
return bigchain.get_validator_change(height)
@classmethod
@@ -242,24 +244,41 @@ class Election(Transaction):
return response
@classmethod
def approved_update(cls, bigchain, new_height, txns):
votes = {}
for txn in txns:
if not isinstance(txn, Vote):
def approved_elections(cls, bigchain, new_height, txns):
elections = defaultdict(list)
for tx in txns:
if not isinstance(tx, Vote):
continue
election_id = tx.asset['id']
elections[election_id].append(tx)
validator_set_updated = False
validator_set_change = []
for election_id, votes in elections.items():
election = Election.has_concluded(bigchain, election_id, votes, new_height)
if not election:
continue
election_id = txn.asset['id']
election_votes = votes.get(election_id, [])
election_votes.append(txn)
votes[election_id] = election_votes
if election.makes_validator_set_change():
if validator_set_updated:
continue
validator_set_change.append(election.get_validator_set_change(bigchain, new_height))
validator_set_updated = True
election = cls.has_concluded(bigchain, election_id, election_votes, new_height)
# Once an election concludes any other conclusion for the same
# or any other election is invalidated
if election:
cls.store_election_results(bigchain, election, new_height)
return cls.on_approval(bigchain, election, new_height)
return None
election.on_approval(bigchain, election, new_height)
return validator_set_change
def makes_validator_set_change(self):
return self.CHANGES_VALIDATOR_SET
def get_validator_set_change(self, bigchain, new_height):
if self.makes_validator_set_change():
return self.change_validator_set(bigchain, new_height)
def change_validator_set(self, bigchain, new_height):
raise NotImplementedError
@classmethod
def on_approval(cls, bigchain, election, new_height):

View File

@@ -0,0 +1,15 @@
from bigchaindb.common.schema import TX_SCHEMA_CHAIN_MIGRATION_ELECTION
from bigchaindb.elections.election import Election
class ChainMigrationElection(Election):
OPERATION = 'CHAIN_MIGRATION_ELECTION'
CREATE = OPERATION
ALLOWED_OPERATIONS = (OPERATION,)
TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION
CHANGES_VALIDATOR_SET = False
@classmethod
def on_approval(cls, bigchain, election, new_height):
bigchain.migrate_abci_chain()

View File

@@ -4,7 +4,7 @@
from bigchaindb.common.exceptions import InvalidPowerChange
from bigchaindb.elections.election import Election
from bigchaindb.common.schema import (TX_SCHEMA_VALIDATOR_ELECTION)
from bigchaindb.common.schema import TX_SCHEMA_VALIDATOR_ELECTION
from .validator_utils import (new_validator_set, encode_validator, validate_asset_public_key)
@@ -36,14 +36,18 @@ class ValidatorElection(Election):
super(ValidatorElection, cls).validate_schema(tx)
validate_asset_public_key(tx['asset']['data']['public_key'])
@classmethod
def on_approval(cls, bigchain, election, new_height):
def change_validator_set(self, bigchain, new_height):
# The new validator set comes into effect from height = new_height+1
validator_updates = [election.asset['data']]
# (upcoming changes to Tendermint will change this to height = new_height+2)
validator_updates = [self.asset['data']]
curr_validator_set = bigchain.get_validators(new_height)
updated_validator_set = new_validator_set(curr_validator_set,
validator_updates)
updated_validator_set = [v for v in updated_validator_set if v['voting_power'] > 0]
bigchain.store_validator_set(new_height+1, updated_validator_set)
return encode_validator(election.asset['data'])
return encode_validator(self.asset['data'])
@classmethod
def on_approval(cls, bigchain, election, new_height):
pass

View File

@@ -91,6 +91,10 @@ Election management is broken into several subcommands. Below is the command lin
Create a new election which proposes a change to your BigChainDB network.
If the command succeeds, it will create an election and return an `election_id`.
**NOTE**: The election proposal consists of vote tokens allocated to each current validator as per their voting power. Validators then cast their votes to approve the election by spending their vote tokens, (see the documentation on `election approve`).
There are multiple types of election, which each take different parameters. Below is a short description of each type of election, as well as their command line syntax and the return value.
###### election new upsert-validator
@@ -106,7 +110,9 @@ $ bigchaindb election new upsert-validator E_PUBKEY E_POWER E_NODE_ID --private-
- `E_PUBKEY`: Public key of the node to be added/updated/removed.
- `E_POWER`: The new power for the `E_PUBKEY`. NOTE, if power is set to `0` then `E_PUBKEY` will be removed from the validator set when the election concludes.
- `E_NODE_ID`: Node id of `E_PUBKEY`. The node operator of `E_PUBKEY` can generate the node id via `tendermint show_node_id`.
- `--private-key`: The path to Tendermint's private key which can be generally found at `/home/user/.tendermint/config/priv_validator.json`. For example, to add a new validator, provide the public key and node id for some node not already in the validator set, along with whatever voting power you'd like them to have. To remove an existing validator, provide their public key and node id, and set `E_POWER` to `0`. Please note that the private key provided here is of the node which is generating this election i.e.
- `--private-key`: The path to Tendermint's private key which can be generally found at `/home/user/.tendermint/config/priv_validator.json`.
For example, to add a new validator, provide the public key and node id for some node not already in the validator set, along with whatever voting power you'd like them to have. To remove an existing validator, provide their public key and node id, and set `E_POWER` to `0`. Please note that the private key provided here is of the node which is generating this election i.e.
NOTE: A change to the validator set can only be proposed by one of the exisitng validators.
@@ -118,10 +124,29 @@ $ bigchaindb election new upsert-validator HHG0IQRybpT6nJMIWWFWhMczCLHt6xcm7eP52
[SUCCESS] Submitted proposal with id: 04a067582cf03eba2b53b82e4adb5ece424474cbd4f7183780855a93ac5e3caa
```
If the command succeeds, it will create an election and return an `election_id`. A successful execution of the above command **doesn't** imply that the validator set will be immediately updated but rather it means the proposal has been succcessfully accepted by the network. Once the `election_id` has been generated the node operator should share this `election_id` with other validators in the network and urge them to approve the proposal. Note that the node operator should themsleves also approve the proposal.
A successful execution of the above command **doesn't** imply that the validator set will be immediately updated but rather it means the proposal has been succcessfully accepted by the network. Once the `election_id` has been generated the node operator should share this `election_id` with other validators in the network and urge them to approve the proposal. Note that the node operator should themsleves also approve the proposal.
**NOTE**: The election proposal consists of vote tokens allocated to each current validator as per their voting power. Validators then cast their votes to approve the change to the validator set by spending their vote tokens.
###### election new migration
Create an election to halt block production, to allow for a version change across breaking changes.
```bash
$ bigchaindb election new migration --private-key PATH_TO_YOUR_PRIVATE_KEY
[SUCCESS] Submitted proposal with id: <election_id>
```
- `--private-key`: The path to Tendermint's private key which can be generally found at `/home/user/.tendermint/config/priv_validator.json`.
Example usage,
```bash
$ bigchaindb election new migration --private-key /home/user/.tendermint/config/priv_validator.json
[SUCCESS] Submitted proposal with id: 04a067582cf03eba2b53b82e4adb5ece424474cbd4f7183780855a93ac5e3caa
```
**NOTE** `migration` elections will halt block production at whichever blockheight they are approved. Once the election is concluded, the validators will need to restart their systems with a new `chain_id` to resume normal operations.
#### election approve

View File

@@ -26,6 +26,8 @@ def test_make_sure_we_dont_remove_any_command():
assert parser.parse_args(['start']).command
assert parser.parse_args(['election', 'new', 'upsert-validator', 'TEMP_PUB_KEYPAIR', '10', 'TEMP_NODE_ID',
'--private-key', 'TEMP_PATH_TO_PRIVATE_KEY']).command
assert parser.parse_args(['election', 'new', 'chain-migration',
'--private-key', 'TEMP_PATH_TO_PRIVATE_KEY']).command
assert parser.parse_args(['election', 'approve', 'ELECTION_ID', '--private-key',
'TEMP_PATH_TO_PRIVATE_KEY']).command
assert parser.parse_args(['election', 'show', 'ELECTION_ID']).command
@@ -341,6 +343,42 @@ def test_election_new_upsert_validator_without_tendermint(caplog, b, priv_valida
assert b.get_transaction(election_id)
@pytest.mark.abci
def test_election_new_chain_migration_with_tendermint(b, priv_validator_path, user_sk, validators):
from bigchaindb.commands.bigchaindb import run_election_new_chain_migration
new_args = Namespace(action='new',
election_type='migration',
sk=priv_validator_path,
config={})
election_id = run_election_new_chain_migration(new_args, b)
assert b.get_transaction(election_id)
@pytest.mark.bdb
def test_election_new_chain_migration_without_tendermint(caplog, b, priv_validator_path, user_sk):
from bigchaindb.commands.bigchaindb import run_election_new_chain_migration
def mock_write(tx, mode):
b.store_bulk_transactions([tx])
return (202, '')
b.get_validators = mock_get_validators
b.write_transaction = mock_write
args = Namespace(action='new',
election_type='migration',
sk=priv_validator_path,
config={})
with caplog.at_level(logging.INFO):
election_id = run_election_new_chain_migration(args, b)
assert caplog.records[0].msg == '[SUCCESS] Submitted proposal with id: ' + election_id
assert b.get_transaction(election_id)
@pytest.mark.bdb
def test_election_new_upsert_validator_invalid_election(caplog, b, priv_validator_path, user_sk):
from bigchaindb.commands.bigchaindb import run_election_new_upsert_validator
@@ -415,7 +453,7 @@ def test_election_approve_without_tendermint(caplog, b, priv_validator_path, new
b, election_id = call_election(b, new_validator, node_key)
# call run_upsert_validator_approve with args that point to the election
# call run_election_approve with args that point to the election
args = Namespace(action='approve',
election_id=election_id,
sk=priv_validator_path,

View File

@@ -20,15 +20,17 @@ from logging.config import dictConfig
import pytest
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
from bigchaindb.backend import schema, query
from bigchaindb.common.crypto import (key_pair_from_ed25519_key,
public_key_from_ed25519_key)
from bigchaindb.common.exceptions import DatabaseDoesNotExist
from bigchaindb.lib import Block
from tests.utils import gen_vote
TEST_DB_NAME = 'bigchain_test'
@@ -242,6 +244,26 @@ def b():
return BigchainDB()
@pytest.fixture
def b_mock(b, network_validators):
b.get_validators = mock_get_validators(network_validators)
return b
def mock_get_validators(network_validators):
def validator_set(height):
validators = []
for public_key, power in network_validators.items():
validators.append({
'public_key': {'type': 'ed25519-base64', 'value': public_key},
'voting_power': power
})
return validators
return validator_set
@pytest.fixture
def create_tx(alice, user_pk):
from bigchaindb.models import Transaction
@@ -674,3 +696,117 @@ def new_validator():
'type': 'ed25519-base16'},
'power': power,
'node_id': node_id}
@pytest.fixture
def valid_upsert_validator_election(b_mock, node_key, new_validator):
voters = ValidatorElection.recipients(b_mock)
return ValidatorElection.generate([node_key.public_key],
voters,
new_validator, None).sign([node_key.private_key])
@pytest.fixture
def valid_upsert_validator_election_2(b_mock, node_key, new_validator):
voters = ValidatorElection.recipients(b_mock)
return ValidatorElection.generate([node_key.public_key],
voters,
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)
genesis_validators = {'validators': validators,
'height': 0,
'election_id': None}
query.store_validator_set(b.connection, genesis_validators)
b.store_bulk_transactions([valid_upsert_validator_election])
block_1 = Block(app_hash='hash_1', height=1, transactions=[valid_upsert_validator_election.id])
b.store_block(block_1._asdict())
return valid_upsert_validator_election
@pytest.fixture
def ongoing_validator_election_2(b, valid_upsert_validator_election_2, ed25519_node_keys):
validators = b.get_validators(height=1)
genesis_validators = {'validators': validators,
'height': 0,
'election_id': None}
query.store_validator_set(b.connection, genesis_validators)
b.store_bulk_transactions([valid_upsert_validator_election_2])
block_1 = Block(app_hash='hash_2', height=1, transactions=[valid_upsert_validator_election_2.id])
b.store_block(block_1._asdict())
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)
votes = generate_votes(ongoing_validator_election, voters, ed25519_node_keys)
return votes
@pytest.fixture
def validator_election_votes_2(b_mock, ongoing_validator_election_2, ed25519_node_keys):
voters = ValidatorElection.recipients(b_mock)
votes = generate_votes(ongoing_validator_election_2, voters, ed25519_node_keys)
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)):
v = gen_vote(election, voter, keys)
votes.append(v)
return votes

View File

View File

@@ -0,0 +1,64 @@
from unittest.mock import MagicMock
import pytest
from bigchaindb.elections.election import Election
@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()
@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()
@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_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

View File

@@ -0,0 +1,9 @@
from bigchaindb.migrations.chain_migration_election import ChainMigrationElection
def test_valid_migration_election(b_mock, node_key):
voters = ChainMigrationElection.recipients(b_mock)
election = ChainMigrationElection.generate([node_key.public_key],
voters,
{}, None).sign([node_key.private_key])
assert election.validate(b_mock)

View File

@@ -5,42 +5,12 @@ from unittest.mock import patch
import pytest
from bigchaindb import Vote
from bigchaindb.backend.localmongodb import query
from bigchaindb.lib import Block
from bigchaindb.upsert_validator import ValidatorElection
@pytest.fixture
def b_mock(b, network_validators):
b.get_validators = mock_get_validators(network_validators)
return b
def mock_get_validators(network_validators):
def validator_set(height):
validators = []
for public_key, power in network_validators.items():
validators.append({
'public_key': {'type': 'ed25519-base64', 'value': public_key},
'voting_power': power
})
return validators
return validator_set
@pytest.fixture
def valid_election(b_mock, node_key, new_validator):
voters = ValidatorElection.recipients(b_mock)
return ValidatorElection.generate([node_key.public_key],
voters,
new_validator, None).sign([node_key.private_key])
@pytest.fixture
def valid_election_b(b, node_key, new_validator):
def valid_upsert_validator_election_b(b, node_key, new_validator):
voters = ValidatorElection.recipients(b)
return ValidatorElection.generate([node_key.public_key],
voters,
@@ -57,30 +27,16 @@ def fixed_seed_election(b_mock, node_key, new_validator):
@pytest.fixture
def ongoing_election(b, valid_election, ed25519_node_keys):
validators = b.get_validators(height=1)
genesis_validators = {'validators': validators,
'height': 0,
'election_id': None}
query.store_validator_set(b.connection, genesis_validators)
b.store_bulk_transactions([valid_election])
block_1 = Block(app_hash='hash_1', height=1, transactions=[valid_election.id])
b.store_block(block_1._asdict())
return valid_election
@pytest.fixture
def concluded_election(b, ongoing_election, ed25519_node_keys):
def concluded_election(b, ongoing_validator_election, ed25519_node_keys):
election_result = {'height': 2,
'election_id': ongoing_election.id}
'election_id': ongoing_validator_election.id}
query.store_election_results(b.connection, election_result)
return ongoing_election
return ongoing_validator_election
@pytest.fixture
def inconclusive_election(b, ongoing_election, new_validator):
def inconclusive_election(b, ongoing_validator_election, new_validator):
validators = b.get_validators(height=1)
validators[0]['voting_power'] = 15
validator_update = {'validators': validators,
@@ -88,20 +44,4 @@ def inconclusive_election(b, ongoing_election, new_validator):
'election_id': 'some_other_election'}
query.store_validator_set(b.connection, validator_update)
return ongoing_election
def vote(election, voter, keys, b):
election_input = election.to_inputs()[voter]
votes = election.outputs[voter].amount
public_key = election_input.owners_before[0]
key = keys[public_key]
election_pub_key = ValidatorElection.to_public_key(election.id)
v = Vote.generate([election_input],
[([election_pub_key], votes)],
election_id=election.id)\
.sign([key.private_key])
b.store_bulk_transactions([v])
return v
return ongoing_validator_election

View File

@@ -5,104 +5,105 @@
import pytest
import codecs
from bigchaindb.elections.election import Election
from bigchaindb.tendermint_utils import public_key_to_base64
from bigchaindb.upsert_validator import ValidatorElection
from bigchaindb.common.exceptions import AmountError
from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.common.exceptions import ValidationError
from bigchaindb.elections.vote import Vote
from tests.utils import generate_block
from tests.utils import generate_block, gen_vote
pytestmark = [pytest.mark.execute]
@pytest.mark.bdb
def test_upsert_validator_valid_election_vote(b_mock, valid_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_election])
def test_upsert_validator_valid_election_vote(b_mock, valid_upsert_validator_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_upsert_validator_election])
input0 = valid_election.to_inputs()[0]
votes = valid_election.outputs[0].amount
input0 = valid_upsert_validator_election.to_inputs()[0]
votes = valid_upsert_validator_election.outputs[0].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
election_pub_key = ValidatorElection.to_public_key(valid_election.id)
election_pub_key = ValidatorElection.to_public_key(valid_upsert_validator_election.id)
vote = Vote.generate([input0],
[([election_pub_key], votes)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
assert vote.validate(b_mock)
@pytest.mark.bdb
def test_upsert_validator_valid_non_election_vote(b_mock, valid_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_election])
def test_upsert_validator_valid_non_election_vote(b_mock, valid_upsert_validator_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_upsert_validator_election])
input0 = valid_election.to_inputs()[0]
votes = valid_election.outputs[0].amount
input0 = valid_upsert_validator_election.to_inputs()[0]
votes = valid_upsert_validator_election.outputs[0].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
election_pub_key = ValidatorElection.to_public_key(valid_election.id)
election_pub_key = ValidatorElection.to_public_key(valid_upsert_validator_election.id)
# Ensure that threshold conditions are now allowed
with pytest.raises(ValidationError):
Vote.generate([input0],
[([election_pub_key, key0.public_key], votes)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
@pytest.mark.bdb
def test_upsert_validator_delegate_election_vote(b_mock, valid_election, ed25519_node_keys):
def test_upsert_validator_delegate_election_vote(b_mock, valid_upsert_validator_election, ed25519_node_keys):
alice = generate_key_pair()
b_mock.store_bulk_transactions([valid_election])
b_mock.store_bulk_transactions([valid_upsert_validator_election])
input0 = valid_election.to_inputs()[0]
votes = valid_election.outputs[0].amount
input0 = valid_upsert_validator_election.to_inputs()[0]
votes = valid_upsert_validator_election.outputs[0].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
delegate_vote = Vote.generate([input0],
[([alice.public_key], 3), ([key0.public_key], votes-3)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
assert delegate_vote.validate(b_mock)
b_mock.store_bulk_transactions([delegate_vote])
election_pub_key = ValidatorElection.to_public_key(valid_election.id)
election_pub_key = ValidatorElection.to_public_key(valid_upsert_validator_election.id)
alice_votes = delegate_vote.to_inputs()[0]
alice_casted_vote = Vote.generate([alice_votes],
[([election_pub_key], 3)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([alice.private_key])
assert alice_casted_vote.validate(b_mock)
key0_votes = delegate_vote.to_inputs()[1]
key0_casted_vote = Vote.generate([key0_votes],
[([election_pub_key], votes-3)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
assert key0_casted_vote.validate(b_mock)
@pytest.mark.bdb
def test_upsert_validator_invalid_election_vote(b_mock, valid_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_election])
def test_upsert_validator_invalid_election_vote(b_mock, valid_upsert_validator_election, ed25519_node_keys):
b_mock.store_bulk_transactions([valid_upsert_validator_election])
input0 = valid_election.to_inputs()[0]
votes = valid_election.outputs[0].amount
input0 = valid_upsert_validator_election.to_inputs()[0]
votes = valid_upsert_validator_election.outputs[0].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
election_pub_key = ValidatorElection.to_public_key(valid_election.id)
election_pub_key = ValidatorElection.to_public_key(valid_upsert_validator_election.id)
vote = Vote.generate([input0],
[([election_pub_key], votes+1)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
with pytest.raises(AmountError):
@@ -110,113 +111,111 @@ def test_upsert_validator_invalid_election_vote(b_mock, valid_election, ed25519_
@pytest.mark.bdb
def test_valid_election_votes_received(b_mock, valid_election, ed25519_node_keys):
def test_valid_election_votes_received(b_mock, valid_upsert_validator_election, ed25519_node_keys):
alice = generate_key_pair()
b_mock.store_bulk_transactions([valid_election])
assert valid_election.get_commited_votes(b_mock) == 0
b_mock.store_bulk_transactions([valid_upsert_validator_election])
assert valid_upsert_validator_election.get_commited_votes(b_mock) == 0
input0 = valid_election.to_inputs()[0]
votes = valid_election.outputs[0].amount
input0 = valid_upsert_validator_election.to_inputs()[0]
votes = valid_upsert_validator_election.outputs[0].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
# delegate some votes to alice
delegate_vote = Vote.generate([input0],
[([alice.public_key], 4), ([key0.public_key], votes-4)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
b_mock.store_bulk_transactions([delegate_vote])
assert valid_election.get_commited_votes(b_mock) == 0
assert valid_upsert_validator_election.get_commited_votes(b_mock) == 0
election_public_key = ValidatorElection.to_public_key(valid_election.id)
election_public_key = ValidatorElection.to_public_key(valid_upsert_validator_election.id)
alice_votes = delegate_vote.to_inputs()[0]
key0_votes = delegate_vote.to_inputs()[1]
alice_casted_vote = Vote.generate([alice_votes],
[([election_public_key], 2), ([alice.public_key], 2)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([alice.private_key])
assert alice_casted_vote.validate(b_mock)
b_mock.store_bulk_transactions([alice_casted_vote])
# Check if the delegated vote is count as valid vote
assert valid_election.get_commited_votes(b_mock) == 2
assert valid_upsert_validator_election.get_commited_votes(b_mock) == 2
key0_casted_vote = Vote.generate([key0_votes],
[([election_public_key], votes-4)],
election_id=valid_election.id)\
election_id=valid_upsert_validator_election.id)\
.sign([key0.private_key])
assert key0_casted_vote.validate(b_mock)
b_mock.store_bulk_transactions([key0_casted_vote])
assert valid_election.get_commited_votes(b_mock) == votes-2
assert valid_upsert_validator_election.get_commited_votes(b_mock) == votes - 2
@pytest.mark.bdb
def test_valid_election_conclude(b_mock, valid_election, ed25519_node_keys):
def test_valid_election_conclude(b_mock, valid_upsert_validator_election, ed25519_node_keys):
# Node 0: cast vote
tx_vote0 = gen_vote(valid_election, 0, ed25519_node_keys)
tx_vote0 = gen_vote(valid_upsert_validator_election, 0, ed25519_node_keys)
# check if the vote is valid even before the election doesn't exist
with pytest.raises(ValidationError):
assert tx_vote0.validate(b_mock)
# store election
b_mock.store_bulk_transactions([valid_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_election.id)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
# validate vote
assert tx_vote0.validate(b_mock)
assert not ValidatorElection.has_concluded(b_mock, valid_election.id, [tx_vote0])
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote0])
b_mock.store_bulk_transactions([tx_vote0])
assert not ValidatorElection.has_concluded(b_mock, valid_election.id)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
# Node 1: cast vote
tx_vote1 = gen_vote(valid_election, 1, ed25519_node_keys)
tx_vote1 = gen_vote(valid_upsert_validator_election, 1, ed25519_node_keys)
# Node 2: cast vote
tx_vote2 = gen_vote(valid_election, 2, ed25519_node_keys)
tx_vote2 = gen_vote(valid_upsert_validator_election, 2, ed25519_node_keys)
# Node 3: cast vote
tx_vote3 = gen_vote(valid_election, 3, ed25519_node_keys)
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_election.id, [tx_vote1])
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote1])
# 2/3 is achieved in the same block so the election can be.has_concludedd
assert ValidatorElection.has_concluded(b_mock, valid_election.id, [tx_vote1, tx_vote2])
assert ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote1, tx_vote2])
b_mock.store_bulk_transactions([tx_vote1])
assert not ValidatorElection.has_concluded(b_mock, valid_election.id)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
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_election.id, [tx_vote2])
assert ValidatorElection.has_concluded(b_mock, valid_election.id, [tx_vote2, tx_vote3])
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])
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_election.id)
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id)
# 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_election.id, [tx_vote3])
assert not ValidatorElection.has_concluded(b_mock, valid_upsert_validator_election.id, [tx_vote3])
@pytest.mark.abci
def test_upsert_validator(b, node_key, node_keys, ed25519_node_keys):
import time
import requests
if b.get_latest_block()['height'] == 0:
generate_block(b)
@@ -244,20 +243,17 @@ def test_upsert_validator(b, node_key, node_keys, ed25519_node_keys):
new_validator, None).sign([node_key.private_key])
code, message = b.write_transaction(election, 'broadcast_tx_commit')
assert code == 202
time.sleep(3)
assert b.get_transaction(election.id)
tx_vote = gen_vote(election, 0, ed25519_node_keys)
assert tx_vote.validate(b)
code, message = b.write_transaction(tx_vote, 'broadcast_tx_commit')
assert code == 202
time.sleep(3)
resp = requests.get(b.endpoint + 'validators')
resp = b.get_validators()
validator_pub_keys = []
for v in resp.json()['result']['validators']:
validator_pub_keys.append(v['pub_key']['value'])
for v in resp:
validator_pub_keys.append(v['public_key']['value'])
assert (public_key64 in validator_pub_keys)
new_validator_set = b.get_validators()
@@ -293,19 +289,19 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
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 ValidatorElection.approved_update(b, 4, [tx_vote0])
assert not ValidatorElection.approved_update(b, 4, [tx_vote0, tx_vote1])
assert Election.approved_elections(b, 4, [tx_vote0]) == []
assert Election.approved_elections(b, 4, [tx_vote0, tx_vote1]) == []
update = ValidatorElection.approved_update(b, 4, [tx_vote0, tx_vote1, tx_vote2])
assert update
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
update = Election.approved_elections(b, 4, [tx_vote0, tx_vote1, 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
b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.approved_update(b, 4, [tx_vote2])
assert update
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
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
@@ -326,10 +322,9 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.approved_update(b, 9, [tx_vote2])
if update:
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update
update = Election.approved_elections(b, 9, [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
# assert that the public key is not a part of the current validator set
@@ -340,22 +335,6 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
# ============================================================================
# Helper functions
# ============================================================================
def to_inputs(election, i, ed25519_node_keys):
input0 = election.to_inputs()[i]
votes = election.outputs[i].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
return (input0, votes, key0)
def gen_vote(election, i, ed25519_node_keys):
(input_i, votes_i, key_i) = to_inputs(election, i, ed25519_node_keys)
election_pub_key = ValidatorElection.to_public_key(election.id)
return Vote.generate([input_i],
[([election_pub_key], votes_i)],
election_id=election.id)\
.sign([key_i.private_key])
def reset_validator_set(b, node_keys, height):
validators = []

View File

@@ -111,9 +111,9 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixe
tx_election.validate(b_mock)
def test_get_status_ongoing(b, ongoing_election, new_validator):
def test_get_status_ongoing(b, ongoing_validator_election, new_validator):
status = ValidatorElection.ONGOING
resp = ongoing_election.get_status(b)
resp = ongoing_validator_election.get_status(b)
assert resp == status
@@ -124,6 +124,9 @@ def test_get_status_concluded(b, concluded_election, new_validator):
def test_get_status_inconclusive(b, inconclusive_election, new_validator):
def set_block_height_to_3():
return {'height': 3}
def custom_mock_get_validators(height):
if height >= 3:
return [{'pub_key': {'data': 'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=',
@@ -153,18 +156,19 @@ def test_get_status_inconclusive(b, inconclusive_election, new_validator):
'voting_power': 8}]
b.get_validators = custom_mock_get_validators
b.get_latest_block = set_block_height_to_3
status = ValidatorElection.INCONCLUSIVE
resp = inconclusive_election.get_status(b)
assert resp == status
def test_upsert_validator_show(caplog, ongoing_election, b):
def test_upsert_validator_show(caplog, ongoing_validator_election, b):
from bigchaindb.commands.bigchaindb import run_election_show
election_id = ongoing_election.id
public_key = public_key_to_base64(ongoing_election.asset['data']['public_key']['value'])
power = ongoing_election.asset['data']['power']
node_id = ongoing_election.asset['data']['node_id']
election_id = ongoing_validator_election.id
public_key = public_key_to_base64(ongoing_validator_election.asset['data']['public_key']['value'])
power = ongoing_validator_election.asset['data']['power']
node_id = ongoing_validator_election.asset['data']['node_id']
status = ValidatorElection.ONGOING
show_args = Namespace(action='show',

View File

@@ -4,8 +4,10 @@
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
@singledispatch
@@ -33,3 +35,20 @@ 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):
input0 = election.to_inputs()[i]
votes = election.outputs[i].amount
public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0]
return (input0, votes, key0)
def gen_vote(election, i, ed25519_node_keys):
(input_i, votes_i, key_i) = to_inputs(election, i, ed25519_node_keys)
election_pub_key = Election.to_public_key(election.id)
return Vote.generate([input_i],
[([election_pub_key], votes_i)],
election_id=election.id)\
.sign([key_i.private_key])