Problem: upsert-validator 'approve' command does not transfer vote to election public key (#2480)

Solution: Transfer vote to the election public key; generalize implemenation to handle election txn type

* Problem: Upsert valdiator 'new' doesn't accept public in base64 format

Solution: Tendermint stores all keys in base64 format so it would suitable to
abandon base58 encoding in favour of base64 encoding

* Problem: Not test for invalid execution of upsert-validator 'new'

Solution: Write tests to when invalid power or private key path has been supplied

* Problem: Exceptions are not informational when executing upsert-validator

Solution: generate error logs or print statement indicating success
This commit is contained in:
Vanshdeep Singh 2018-08-28 14:21:59 +02:00 committed by Lev Berman
parent dfadbff60f
commit 8e97c753eb
5 changed files with 175 additions and 52 deletions

View File

@ -16,7 +16,7 @@ import sys
from bigchaindb.utils import load_node_key
from bigchaindb.common.exceptions import (DatabaseAlreadyExists,
DatabaseDoesNotExist,
OperationError, KeypairMismatchException)
ValidationError)
import bigchaindb
from bigchaindb import (backend, ValidatorElection,
BigchainDB, ValidatorElectionVote)
@ -27,6 +27,8 @@ from bigchaindb.commands import utils
from bigchaindb.commands.utils import (configure_bigchaindb,
input_on_stderr)
from bigchaindb.log import setup_logging
from bigchaindb.tendermint_utils import public_key_from_base64
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@ -119,29 +121,36 @@ def run_upsert_validator_new(args, bigchain):
'sk': the path to the private key of the node calling the election (str)
}
:param bigchain: an instance of BigchainDB
:return: election_id (tx_id)
:raises: OperationError if the write transaction fails for any reason
:return: election_id or `False` in case of failure
"""
new_validator = {
'public_key': args.public_key,
'public_key': public_key_from_base64(args.public_key),
'power': args.power,
'node_id': args.node_id
}
key = load_node_key(args.sk)
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
voters = ValidatorElection.recipients(bigchain)
election = ValidatorElection.generate([key.public_key],
voters,
new_validator, None).sign([key.private_key])
election.validate(bigchain)
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:
raise OperationError('Failed to commit election')
logger.error('Failed to commit election proposal')
return False
def run_upsert_validator_approve(args, bigchain):
@ -153,8 +162,7 @@ def run_upsert_validator_approve(args, bigchain):
'sk': the path to the private key of the signer (str)
}
:param bigchain: an instance of BigchainDB
:return: a success message
:raises: OperationError if the write transaction fails for any reason
:return: success log message or `False` in case of error
"""
key = load_node_key(args.sk)
@ -163,22 +171,24 @@ def run_upsert_validator_approve(args, bigchain):
if len(voting_powers) > 0:
voting_power = voting_powers[0]
else:
raise KeypairMismatchException(
'The key you provided does not match any of the eligible voters in this election.'
)
logger.error('The key you provided does not match any of the eligible voters in this election.')
return False
inputs = [i for i in tx.to_inputs() if key.public_key in i.owners_before]
approval = ValidatorElectionVote.generate(inputs, [
([key.public_key], voting_power)], tx.id).sign([key.private_key])
election_pub_key = ValidatorElection.to_public_key(tx.id)
approval = ValidatorElectionVote.generate(inputs,
[([election_pub_key], voting_power)],
tx.id).sign([key.private_key])
approval.validate(bigchain)
resp = bigchain.write_transaction(approval, 'broadcast_tx_commit')
if resp == (202, ''):
print('Your vote has been submitted.')
logger.info('[SUCCESS] Your vote has been submitted')
return approval.id
else:
raise OperationError('Failed to vote for election')
logger.error('Failed to commit vote')
return False
def _run_init():

View File

@ -220,6 +220,7 @@ class ValidatorElection(Transaction):
updated_validator_set = new_validator_set(curr_validator_set,
new_height, 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 []

View File

@ -83,44 +83,59 @@ configuration file as documented under
## bigchaindb upsert-validator
**This is an experimental feature. Users are advised not to use it in production.**
Manage elections to add, update, or remove a validator from the validators set of the local node. The upsert-validator subcommands implement [BEP-21](https://github.com/bigchaindb/BEPs/tree/master/21). Check it out if you need more details on how this is orchestrated.
Manage elections to add, update, or remove a validator from the validators set. The upsert-validator subcommands implement [BEP-21](https://github.com/bigchaindb/BEPs/tree/master/21), please refer it for more details.
Election management is broken into several subcommands. Below is the command line syntax for each,
#### upsert-validator new
Calls a new election, proposing a change to the validator set.
Create a new election which proposes a change to the validator set. An election can be used to add/update/remove a validator from the validator set.
Below is the command line syntax and the return value,
```bash
$ bigchaindb upsert-validator new E_PUBKEY E_POWER E_NODE_ID --private-key PATH_TO_YOUR_PRIVATE_KEY
<election_id>
[SUCCESS] Submitted proposal with id: <election_id>
```
Here, `E_PUBKEY`, `E_POWER`, and `E_NODE_ID` are the public key, proposed power, and node id of the validator being voted on. `--private-key` should be the path to wherever the private key for your validator node is stored, (*not* the private key itself.). 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`.
- `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.
NOTE: A change to the validator set can only be proposed by one of the exisitng validators.
Example usage,
```bash
$ bigchaindb upsert-validator new B0E42D2589A455EAD339A035D6CE1C8C3E25863F268120AA0162AD7D003A4014 1 12345 --private-key /home/user/.tendermint/config/priv_validator.json
$ bigchaindb upsert-validator new HHG0IQRybpT6nJMIWWFWhMczCLHt6xcm7eP52GnGuPY= 1 fb7140f03a4ffad899fabbbf655b97e0321add66 --private-key /home/user/.tendermint/config/priv_validator.json
[SUCCESS] Submitted proposal with id: 04a067582cf03eba2b53b82e4adb5ece424474cbd4f7183780855a93ac5e3caa
```
If the command succeeds, it will create an election and return an `election_id`. Elections consist of one vote token per voting power, issued to the members of the validator set. Validators can cast their votes to approve the change to the validator set by spending their vote tokens. The status of the election can be monitored by providing the `election_id` to the `show` subcommand.
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.
**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.
#### upsert-validator approve
Approve an election by voting for it.
Below is the command line syntax and the return value,
Approve an election by voting for it. The propsal generated by executing `bigchaindb upsert-valdiator approve ...` can approved by the validators using this command. The validator who is approving the proposal will spend all their votes i.e. if the validator has a network power of `10` then they will cast `10` votes for the proposal.`
Below is the command line syntax and the return value,
```bash
$ bigchaindb upsert-validator approve <election_id> --private-key PATH_TO_YOUR_PRIVATE_KEY
[SUCCESS] Your vote has been submitted
```
Here, `<election_id>` is the transaction id of the election the approval should be given for. `--private-key` should be the path to Tendermint's private key which can be generally found at `/home/user/.tendermint/config/priv_validator.json`.
- `election_id` is the transaction id of the election the approval should be given for.
- `--private-key` should be the path to Tendermint's private key which can be generally found at `/home/user/.tendermint/config/priv_validator.json`.
Example usage,
```bash
$ bigchaindb upsert-validator approve 04a067582cf03eba2b53b82e4adb5ece424474cbd4f7183780855a93ac5e3caa --private-key /home/user/.tendermint/config/priv_validator.json
[SUCCESS] Your vote has been submitted
```
If the command succeeds, a message will be returned, that the vote was submitted successfully.
If the command succeeds a message will be returned stating that the vote was submitted successfully. Once a proposal has been approved by sufficent validators (more than `2/3` of the total voting power) then the proposed change is applied to the network. For example, consider a network wherein the total power is `90` then the proposed changed applied only after `60` (`2/3 * 90`) have been received.

View File

@ -3,6 +3,7 @@
# Code is Apache-2.0 and docs are CC-BY-4.0
import json
import logging
from unittest.mock import Mock, patch
from argparse import Namespace
@ -10,7 +11,6 @@ from argparse import Namespace
import pytest
from bigchaindb import ValidatorElection
from bigchaindb.common.exceptions import KeypairMismatchException
from tests.conftest import node_keys
@ -379,7 +379,7 @@ def test_upsert_validator_new_with_tendermint(b, priv_validator_path, user_sk, m
@pytest.mark.tendermint
@pytest.mark.bdb
def test_upsert_validator_new_without_tendermint(b, priv_validator_path, user_sk):
def test_upsert_validator_new_without_tendermint(caplog, b, priv_validator_path, user_sk):
from bigchaindb.commands.bigchaindb import run_upsert_validator_new
def mock_write(tx, mode):
@ -392,24 +392,67 @@ def test_upsert_validator_new_without_tendermint(b, priv_validator_path, user_sk
args = Namespace(action='new',
public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=',
power=1,
node_id='12345',
node_id='fb7140f03a4ffad899fabbbf655b97e0321add66',
sk=priv_validator_path,
config={})
resp = run_upsert_validator_new(args, b)
assert b.get_transaction(resp)
with caplog.at_level(logging.INFO):
election_id = run_upsert_validator_new(args, b)
assert caplog.records[0].msg == '[SUCCESS] Submitted proposal with id: ' + election_id
assert b.get_transaction(election_id)
@pytest.mark.tendermint
@pytest.mark.bdb
def test_upsert_validator_new_invalid_election(caplog, b, priv_validator_path, user_sk):
from bigchaindb.commands.bigchaindb import run_upsert_validator_new
args = Namespace(action='new',
public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=',
power=10,
node_id='fb7140f03a4ffad899fabbbf655b97e0321add66',
sk='/tmp/invalid/path/key.json',
config={})
with caplog.at_level(logging.ERROR):
assert not run_upsert_validator_new(args, b)
assert caplog.records[0].msg.__class__ == FileNotFoundError
@pytest.mark.tendermint
@pytest.mark.bdb
def test_upsert_validator_new_election_invalid_power(caplog, b, priv_validator_path, user_sk):
from bigchaindb.commands.bigchaindb import run_upsert_validator_new
from bigchaindb.common.exceptions import InvalidPowerChange
def mock_write(tx, mode):
b.store_bulk_transactions([tx])
return (400, '')
b.write_transaction = mock_write
b.get_validators = mock_get
args = Namespace(action='new',
public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=',
power=10,
node_id='fb7140f03a4ffad899fabbbf655b97e0321add66',
sk=priv_validator_path,
config={})
with caplog.at_level(logging.ERROR):
assert not run_upsert_validator_new(args, b)
assert caplog.records[0].msg.__class__ == InvalidPowerChange
@pytest.mark.abci
def test_upsert_validator_approve_with_tendermint(b, priv_validator_path, user_sk, validators):
from bigchaindb.commands.bigchaindb import run_upsert_validator_new, \
run_upsert_validator_approve
from bigchaindb.commands.bigchaindb import (run_upsert_validator_new,
run_upsert_validator_approve)
public_key = '8eJ8q9ZQpReWyQT5aFCiwtZ5wDZC4eDnCen88p3tQ6ie'
public_key = 'CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg='
new_args = Namespace(action='new',
public_key=public_key,
power=1,
node_id='12345',
node_id='fb7140f03a4ffad899fabbbf655b97e0321add66',
sk=priv_validator_path,
config={})
@ -426,7 +469,7 @@ def test_upsert_validator_approve_with_tendermint(b, priv_validator_path, user_s
@pytest.mark.bdb
@pytest.mark.tendermint
def test_upsert_validator_approve_without_tendermint(b, priv_validator_path, new_validator, node_key):
def test_upsert_validator_approve_without_tendermint(caplog, b, priv_validator_path, new_validator, node_key):
from bigchaindb.commands.bigchaindb import run_upsert_validator_approve
from argparse import Namespace
@ -438,15 +481,41 @@ def test_upsert_validator_approve_without_tendermint(b, priv_validator_path, new
sk=priv_validator_path,
config={})
approval_id = run_upsert_validator_approve(args, b)
# assert returned id is in the db
assert b.get_transaction(approval_id)
with caplog.at_level(logging.INFO):
approval_id = run_upsert_validator_approve(args, b)
assert caplog.records[0].msg == '[SUCCESS] Your vote has been submitted'
assert b.get_transaction(approval_id)
@pytest.mark.bdb
@pytest.mark.tendermint
def test_upsert_validator_approve_called_with_bad_key(b, bad_validator_path, new_validator, node_key):
@pytest.mark.bdb
def test_upsert_validator_approve_failure(caplog, b, priv_validator_path, new_validator, node_key):
from bigchaindb.commands.bigchaindb import run_upsert_validator_approve
from argparse import Namespace
b, election_id = call_election(b, new_validator, node_key)
def mock_write(tx, mode):
b.store_bulk_transactions([tx])
return (400, '')
b.write_transaction = mock_write
# call run_upsert_validator_approve with args that point to the election
args = Namespace(action='approve',
election_id=election_id,
sk=priv_validator_path,
config={})
with caplog.at_level(logging.ERROR):
assert not run_upsert_validator_approve(args, b)
assert caplog.records[0].msg == 'Failed to commit vote'
@pytest.mark.tendermint
@pytest.mark.bdb
def test_upsert_validator_approve_called_with_bad_key(caplog, b, bad_validator_path, new_validator, node_key):
from bigchaindb.commands.bigchaindb import run_upsert_validator_approve
from argparse import Namespace
@ -458,8 +527,10 @@ def test_upsert_validator_approve_called_with_bad_key(b, bad_validator_path, new
sk=bad_validator_path,
config={})
with pytest.raises(KeypairMismatchException):
run_upsert_validator_approve(args, b)
with caplog.at_level(logging.ERROR):
assert not run_upsert_validator_approve(args, b)
assert caplog.records[0].msg == 'The key you provided does not match any of '\
'the eligible voters in this election.'
def mock_get(height):

View File

@ -310,11 +310,37 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.get_validator_update(b, 4, [tx_vote2])
print('update', update)
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert len(update) == 1
assert update_public_key == public_key64
# remove validator
power = 0
new_validator = {'public_key': public_key,
'node_id': 'some_node_id',
'power': power}
voters = ValidatorElection.recipients(b)
election = ValidatorElection.generate([node_key.public_key],
voters,
new_validator).sign([node_key.private_key])
# store election
b.store_bulk_transactions([election])
tx_vote0 = gen_vote(election, 0, ed25519_node_keys)
tx_vote1 = gen_vote(election, 1, ed25519_node_keys)
tx_vote2 = gen_vote(election, 2, ed25519_node_keys)
b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.get_validator_update(b, 9, [tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert len(update) == 1
assert update_public_key == public_key64
# assert that the public key is not a part of the current validator set
for v in b.get_validators(10):
assert not v['pub_key']['data'] == public_key64
# ============================================================================
# Helper functions