mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Create dynamic upsert validator commands (#2446)
* Problem: Need a method to initiate a new upsert-validator election Solution: Added subcommand `new` to `upsert_validator` * Problem: Changes to upsert-validator needed to be reflected in the docs Solution: Wrote a section for `upsert-validator new`
This commit is contained in:
parent
3011548317
commit
3760824261
@ -34,4 +34,3 @@ COPY . /usr/src/app/
|
||||
WORKDIR /usr/src/app
|
||||
RUN pip install --no-cache-dir --process-dependency-links -e .[dev]
|
||||
RUN bigchaindb -y configure
|
||||
|
||||
|
@ -9,19 +9,19 @@ import copy
|
||||
import json
|
||||
import sys
|
||||
|
||||
from bigchaindb.utils import load_node_key
|
||||
from bigchaindb.common.exceptions import (DatabaseAlreadyExists,
|
||||
DatabaseDoesNotExist,
|
||||
MultipleValidatorOperationError)
|
||||
OperationError)
|
||||
import bigchaindb
|
||||
from bigchaindb import backend
|
||||
from bigchaindb import backend, ValidatorElection, BigchainDB
|
||||
from bigchaindb.backend import schema
|
||||
from bigchaindb.backend import query
|
||||
from bigchaindb.backend.query import VALIDATOR_UPDATE_ID, PRE_COMMIT_ID
|
||||
from bigchaindb.backend.query import PRE_COMMIT_ID
|
||||
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__)
|
||||
@ -95,21 +95,48 @@ def run_configure(args):
|
||||
|
||||
@configure_bigchaindb
|
||||
def run_upsert_validator(args):
|
||||
"""Store validators which should be synced with Tendermint"""
|
||||
"""Initiate and manage elections to change the validator set"""
|
||||
|
||||
b = bigchaindb.BigchainDB()
|
||||
public_key = public_key_from_base64(args.public_key)
|
||||
validator = {'pub_key': {'type': 'ed25519',
|
||||
'data': public_key},
|
||||
'power': args.power}
|
||||
validator_update = {'validator': validator,
|
||||
'update_id': VALIDATOR_UPDATE_ID}
|
||||
try:
|
||||
query.store_validator_update(b.connection, validator_update)
|
||||
except MultipleValidatorOperationError:
|
||||
logger.error('A validator update is pending to be applied. '
|
||||
'Please re-try after the current update has '
|
||||
'been processed.')
|
||||
b = BigchainDB()
|
||||
|
||||
# Call the function specified by args.action, as defined above
|
||||
globals()[f'run_upsert_validator_{args.action}'](args, b)
|
||||
|
||||
|
||||
def run_upsert_validator_new(args, bigchain):
|
||||
"""Initiates an election to add/update/remove a validator to an existing BigchainDB network
|
||||
|
||||
:param args: dict
|
||||
args = {
|
||||
'public_key': the public key of the proposed peer, (str)
|
||||
'power': the proposed validator power for the new peer, (str)
|
||||
'node_id': the node_id of the new peer (str)
|
||||
'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
|
||||
"""
|
||||
|
||||
new_validator = {
|
||||
'public_key': args.public_key,
|
||||
'power': args.power,
|
||||
'node_id': args.node_id
|
||||
}
|
||||
|
||||
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)
|
||||
resp = bigchain.write_transaction(election, 'broadcast_tx_commit')
|
||||
if resp == (202, ''):
|
||||
return election.id
|
||||
else:
|
||||
raise OperationError('Failed to commit election')
|
||||
|
||||
|
||||
def _run_init():
|
||||
@ -208,16 +235,30 @@ def create_parser():
|
||||
help='The backend to use. It can only be '
|
||||
'"localmongodb", currently.')
|
||||
|
||||
# parser for managing validator elections
|
||||
validator_parser = subparsers.add_parser('upsert-validator',
|
||||
help='Add/update/delete a validator')
|
||||
help='Add/update/delete a validator.')
|
||||
|
||||
validator_parser.add_argument('public_key',
|
||||
help='Public key of the validator.')
|
||||
validator_subparser = validator_parser.add_subparsers(title='Action',
|
||||
dest='action')
|
||||
|
||||
validator_parser.add_argument('power',
|
||||
type=int,
|
||||
help='Voting power of the validator. '
|
||||
'Setting it to 0 will delete the validator.')
|
||||
new_election_parser = validator_subparser.add_parser('new',
|
||||
help='Calls a new election.')
|
||||
|
||||
new_election_parser.add_argument('public_key',
|
||||
help='Public key of the validator to be added/updated/removed.')
|
||||
|
||||
new_election_parser.add_argument('power',
|
||||
type=int,
|
||||
help='The proposed power for the validator. '
|
||||
'Setting to 0 will remove the validator.')
|
||||
|
||||
new_election_parser.add_argument('node_id',
|
||||
help='The node_id of the validator.')
|
||||
|
||||
new_election_parser.add_argument('--private-key',
|
||||
dest='sk',
|
||||
help='Path to the private key of the election initiator.')
|
||||
|
||||
# parsers for showing/exporting config values
|
||||
subparsers.add_parser('show-config',
|
||||
|
@ -440,6 +440,7 @@ class BigchainDB(object):
|
||||
validators = result['validators']
|
||||
for v in validators:
|
||||
v.pop('address')
|
||||
v['voting_power'] = int(v['voting_power'])
|
||||
|
||||
return validators
|
||||
|
||||
|
@ -2,9 +2,13 @@ import contextlib
|
||||
import threading
|
||||
import queue
|
||||
import multiprocessing as mp
|
||||
import json
|
||||
|
||||
import setproctitle
|
||||
|
||||
from bigchaindb.tendermint_utils import key_from_base64
|
||||
from bigchaindb.common.crypto import key_pair_from_ed25519_key
|
||||
|
||||
|
||||
class ProcessGroup(object):
|
||||
|
||||
@ -31,7 +35,8 @@ class ProcessGroup(object):
|
||||
class Process(mp.Process):
|
||||
"""Wrapper around multiprocessing.Process that uses
|
||||
setproctitle to set the name of the process when running
|
||||
the target task."""
|
||||
the target task.
|
||||
"""
|
||||
|
||||
def run(self):
|
||||
setproctitle.setproctitle(self.name)
|
||||
@ -167,3 +172,12 @@ class Lazy:
|
||||
|
||||
self.stack = []
|
||||
return last
|
||||
|
||||
|
||||
# Load Tendermint's public and private key from the file path
|
||||
def load_node_key(path):
|
||||
with open(path) as json_data:
|
||||
priv_validator = json.load(json_data)
|
||||
priv_key = priv_validator['priv_key']['value']
|
||||
hex_private_key = key_from_base64(priv_key)
|
||||
return key_pair_from_ed25519_key(hex_private_key)
|
||||
|
@ -80,22 +80,27 @@ configuration file as documented under
|
||||
**This is an experimental feature. Users are advised not to use it in production.**
|
||||
|
||||
|
||||
Add, update, or remove a validator from the validators set of the local node. The command implements [3/UPSERT-VALIDATORS](https://github.com/bigchaindb/BEPs/tree/master/3), 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 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.
|
||||
|
||||
Below is the command line syntax,
|
||||
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.
|
||||
|
||||
Below is the command line syntax and the return value,
|
||||
|
||||
```bash
|
||||
$ bigchaindb upsert-validator PUBLIC_KEY_OF_VALIDATOR POWER
|
||||
$ bigchaindb upsert-validator new E_PUBKEY E_POWER E_NODE_ID --private-key PATH_TO_YOUR_PRIVATE_KEY
|
||||
<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`.
|
||||
|
||||
Example usage,
|
||||
|
||||
```bash
|
||||
$ bigchaindb upsert-validator B0E42D2589A455EAD339A035D6CE1C8C3E25863F268120AA0162AD7D003A4014 10
|
||||
$ bigchaindb upsert-validator new B0E42D2589A455EAD339A035D6CE1C8C3E25863F268120AA0162AD7D003A4014 1 12345 --private-key /home/user/.tendermint/config/priv_validator.json
|
||||
```
|
||||
|
||||
If the command is returns without any error then a request to update the validator set has been successfully submitted. So, even if the command has been successfully executed it doesn't imply that the validator set has been updated. In order to check whether the change has been applied, the node operator can execute `curl http://node_ip:9984/api/v1/validators` which will list the current validators set. Refer to the [validators](/http-client-server-api.html#validators) section of the HTTP API docs for more detail.
|
||||
|
||||
Note:
|
||||
- When `POWER`is set to `0` then the validator will be removed from the validator set.
|
||||
- Upsert requests are handled once per block i.e. the validators set is updated once a new block is committed. So, the node operator is not allowed to submit a new upsert request until the current request has been processed. Furthermore, if Tendermint is started with `--consensus.create_empty_blocks=false`, and there are no new incoming transactions then the validators set update is delayed until any new transactions are received and a new block can be committed.
|
||||
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.
|
||||
|
@ -18,7 +18,8 @@ def test_make_sure_we_dont_remove_any_command():
|
||||
assert parser.parse_args(['init']).command
|
||||
assert parser.parse_args(['drop']).command
|
||||
assert parser.parse_args(['start']).command
|
||||
assert parser.parse_args(['upsert-validator', 'TEMP_PUB_KEYPAIR', '10']).command
|
||||
assert parser.parse_args(['upsert-validator', 'new', 'TEMP_PUB_KEYPAIR', '10', 'TEMP_NODE_ID',
|
||||
'--private-key', 'TEMP_PATH_TO_PRIVATE_KEY']).command
|
||||
|
||||
|
||||
@pytest.mark.tendermint
|
||||
@ -341,15 +342,70 @@ class MockResponse():
|
||||
return {'result': {'latest_block_height': self.height}}
|
||||
|
||||
|
||||
# @pytest.mark.execute
|
||||
# @patch('bigchaindb.lib.BigchainDB.get_validators')
|
||||
# @pytest.mark.abci
|
||||
@pytest.mark.skip
|
||||
@patch('bigchaindb.config_utils.autoconfigure')
|
||||
@patch('bigchaindb.backend.query.store_validator_update')
|
||||
def test_upsert_validator_new_with_tendermint(b, priv_validator_path, user_sk, monkeypatch):
|
||||
"""WIP: Will be fixed and activated in the next PR
|
||||
"""
|
||||
from bigchaindb.commands.bigchaindb import run_upsert_validator_new
|
||||
import time
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
def mock_get():
|
||||
return [
|
||||
{'pub_key': {'value': 'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=',
|
||||
'type': 'tendermint/PubKeyEd25519'},
|
||||
'voting_power': 10}
|
||||
]
|
||||
|
||||
# b.get_validators = mock_get
|
||||
# mock_get_validators = mock_get
|
||||
# monkeypatch.setattr('requests.get', mock_get)
|
||||
|
||||
proposer_key = b.get_validators()[0]['pub_key']['value']
|
||||
|
||||
args = Namespace(action='new',
|
||||
public_key=proposer_key,
|
||||
power=1,
|
||||
node_id='12345',
|
||||
sk=priv_validator_path,
|
||||
config={})
|
||||
resp = run_upsert_validator_new(args, b)
|
||||
time.sleep(3)
|
||||
|
||||
assert b.get_transaction(resp)
|
||||
|
||||
|
||||
@pytest.mark.tendermint
|
||||
def test_upsert_validator(mock_autoconfigure, mock_store_validator_update):
|
||||
from bigchaindb.commands.bigchaindb import run_upsert_validator
|
||||
@pytest.mark.bdb
|
||||
def test_upsert_validator_new_without_tendermint(b, priv_validator_path, user_sk, monkeypatch):
|
||||
from bigchaindb.commands.bigchaindb import run_upsert_validator_new
|
||||
|
||||
args = Namespace(public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=',
|
||||
power='10', config={})
|
||||
run_upsert_validator(args)
|
||||
def mock_get():
|
||||
return [
|
||||
{'pub_key': {'value': 'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=',
|
||||
'type': 'tendermint/PubKeyEd25519'},
|
||||
'voting_power': 10}
|
||||
]
|
||||
|
||||
assert mock_store_validator_update.called
|
||||
def mock_write(tx, mode):
|
||||
b.store_transaction(tx)
|
||||
return (202, '')
|
||||
|
||||
b.get_validators = mock_get
|
||||
b.write_transaction = mock_write
|
||||
|
||||
monkeypatch.setattr('requests.get', mock_get)
|
||||
|
||||
args = Namespace(action='new',
|
||||
public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=',
|
||||
power=1,
|
||||
node_id='12345',
|
||||
sk=priv_validator_path,
|
||||
config={})
|
||||
resp = run_upsert_validator_new(args, b)
|
||||
|
||||
assert b.get_transaction(resp)
|
||||
|
@ -4,10 +4,11 @@ Tasks:
|
||||
1. setup test database before starting the tests
|
||||
2. delete test database after running the tests
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import copy
|
||||
import random
|
||||
import tempfile
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
from logging.config import dictConfig
|
||||
@ -668,3 +669,27 @@ def node_keys():
|
||||
'83VINXdj2ynOHuhvSZz5tGuOE5oYzIi0mEximkX1KYMlt/Csu8JUjA4+by2Pz3fqSLshhuYYeM+IpvqcBl6BEA==',
|
||||
'PecJ58SaNRsWJZodDmqjpCWqG6btdwXFHLyE40RYlYM=':
|
||||
'uz8bYgoL4rHErWT1gjjrnA+W7bgD/uDQWSRKDmC8otc95wnnxJo1GxYlmh0OaqOkJaobpu13BcUcvITjRFiVgw=='}
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def priv_validator_path(node_keys):
|
||||
(public_key, private_key) = list(node_keys.items())[0]
|
||||
priv_validator = {
|
||||
'address': '84F787D95E196DC5DE5F972666CFECCA36801426',
|
||||
'pub_key': {
|
||||
'type': 'AC26791624DE60',
|
||||
'value': public_key
|
||||
},
|
||||
'last_height': 0,
|
||||
'last_round': 0,
|
||||
'last_step': 0,
|
||||
'priv_key': {
|
||||
'type': '954568A3288910',
|
||||
'value': private_key
|
||||
}
|
||||
}
|
||||
fd, path = tempfile.mkstemp()
|
||||
socket = os.fdopen(fd, 'w')
|
||||
json.dump(priv_validator, socket)
|
||||
socket.close()
|
||||
return path
|
||||
|
Loading…
x
Reference in New Issue
Block a user