From e4d3915533a4de730975474fbdc131a1f54219dd Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 24 Mar 2016 01:41:00 +0100 Subject: [PATCH 01/39] Add type conversion and docs --- bigchaindb/__init__.py | 43 +++------ bigchaindb/commands/bigchain.py | 2 +- bigchaindb/config_utils.py | 149 +++++++++++++++++++++++++++---- docs/source/bigchaindb-cli.md | 65 +++++++++++++- tests/test_commands.py | 1 + tests/utils/test_config_utils.py | 76 +++++++++++++++- 6 files changed, 283 insertions(+), 53 deletions(-) diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index 9283913f..4ed6d0bd 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -1,47 +1,24 @@ -import os import copy - -def e(key, default=None, conv=None): - '''Get the environment variable `key`, fallback to `default` - if nothing is found. - - Keyword arguments: - key -- the key to look for in the environment - default -- the default value if nothing is found (default: None) - conv -- a callable used to convert the value (default: use the type of the - default value) - ''' - - val = os.environ.get(key, default) - - if conv or default is not None: - conv = conv or type(default) - return conv(val) - - return val - - config = { 'database': { - 'host': e('BIGCHAIN_DATABASE_HOST', default='localhost'), - 'port': e('BIGCHAIN_DATABASE_PORT', default=28015), - 'name': e('BIGCHAIN_DATABASE_NAME', default='bigchain') + 'host': 'localhost', + 'port': 28015, + 'name': 'bigchain', }, 'keypair': { - 'public': e('BIGCHAIN_KEYPAIR_PUBLIC'), - 'private': e('BIGCHAIN_KEYPAIR_PRIVATE') + 'public': None, + 'private': None, }, - 'keyring': [ - ], + 'keyring': [], 'statsd': { - 'host': e('BIGCHAIN_STATSD_HOST', default='localhost'), - 'port': e('BIGCHAIN_STATSD_PORT', default=8125), - 'rate': e('BIGCHAIN_STATSD_SAMPLERATE', default=0.01) + 'host': 'localhost', + 'port': 8125, + 'rate': 0.01, }, 'api_endpoint': 'http://localhost:8008/api/v1', - 'consensus_plugin': e('BIGCHAIN_CONSENSUS_PLUGIN', default='default') + 'consensus_plugin': 'default', } # We need to maintain a backup copy of the original config dict in case diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 35b2de2f..40f0cfa5 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -88,7 +88,7 @@ def run_drop(args): def run_start(args): """Start the processes to run the node""" - run_configure(args, skip_if_exists=True) + # run_configure(args, skip_if_exists=True) bigchaindb.config_utils.file_config(args.config) try: db.init() diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index 645780eb..7ab81250 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -5,10 +5,46 @@ By calling `file_config`, the global configuration (stored in configuration file. Note that there is a precedence in reading configuration values: - - [not yet] command line; + - (**not yet**) command line; - local config file; - environment vars; - - default config file (contained in `bigchain.__init__`). + - default config file (contained in ``bigchaindb.__init__``). + +This means that if the default configuration contains an entry that is: + +``` +{... + "database": {"host": "localhost", + "port": 28015} +...} +``` + +while your local file `local.json` contains: +``` +{... + "database": {"host": "whatever"} +...} +``` + +and you run this command: +``` +$ BIGCHAINDB_DATABASE_HOST=foobar BIGCHAINDB_DATABASE_PORT=42000 bigchaindb -c local.json show-config +``` + +You will get: +``` +INFO:bigchaindb.config_utils:Configuration loaded from `local.json` +{'CONFIGURED': True, + 'api_endpoint': 'http://localhost:8008/api/v1', + 'consensus_plugin': 'default', + 'database': {'host': 'localhost', 'name': 'bigchain', 'port': 28015}, + 'keypair': {'private': 'Hbqvh...', + 'public': '2Bi5NU...'}, + 'keyring': [], + 'statsd': {'host': 'localhost', 'port': 8125, 'rate': 0.01}} + +``` + """ import os @@ -23,11 +59,40 @@ import bigchaindb from bigchaindb.consensus import AbstractConsensusRules logger = logging.getLogger(__name__) + CONFIG_DEFAULT_PATH = os.environ.setdefault( 'BIGCHAINDB_CONFIG_PATH', os.path.join(os.path.expanduser('~'), '.bigchaindb'), ) +CONFIG_PREFIX = 'BIGCHAINDB' +CONFIG_SEP = '_' + + +def map_leafs(func, mapping): + """Map a function to the leafs of a mapping.""" + + def _inner(mapping, path=None): + if path is None: + path = [] + + for key, val in mapping.items(): + if isinstance(val, collections.Mapping): + _inner(val, path + [key]) + else: + mapping[key] = func(val, path=path+[key]) + + return mapping + + return _inner(copy.deepcopy(mapping)) + + +def get_casted_value(original, to_convert): + try: + return original(to_convert) + except TypeError: + return type(original)(to_convert) + # Thanks Alex <3 # http://stackoverflow.com/a/3233356/597097 @@ -43,7 +108,7 @@ def update(d, u): def file_config(filename=None): - """Read a configuration file and merge it with the default configuration. + """Returns the values found in a configuration file. Args: filename (str): the JSON file with the configuration. Defaults to ``None``. @@ -57,13 +122,57 @@ def file_config(filename=None): filename = CONFIG_DEFAULT_PATH with open(filename) as f: - newconfig = json.load(f) + config = json.load(f) - dict_config(newconfig) logger.info('Configuration loaded from `{}`'.format(filename)) + return config -def dict_config(newconfig): + +def env_config(config): + """Return a new configuration with the values found in the environment. + + The function recursively iterates over the config, checking if there is + a matching env variable. If an env variable is found, the func updates + the configuration with that value. + + The name of the env variable is built combining a prefix (``BIGCHAINDB``) + with the path to the value. If the ``config`` in input is: + ``{'database': {'host': 'localhost'}}`` + this function will try to read the env variable ``BIGCHAINDB_DATABASE_HOST``. + """ + + def load_from_env(value, path): + var_name = CONFIG_SEP.join([CONFIG_PREFIX] + list(map(lambda s: s.upper(), path))) + return os.environ.get(var_name, value) + + return map_leafs(load_from_env, config) + + +def update_types(config): + """Return a new configuration where all the values types + are aligned with the ones in the default configuration""" + + reference = bigchaindb.config + + def _update_type(value, path): + ref = reference + + for elem in path: + ref = ref[elem] + + try: + return ref(value) + except TypeError: + try: + return type(ref)(value) + except TypeError: + return value + + return map_leafs(_update_type, config) + + +def dict_config(config): """Merge the provided configuration with the default one. Args: @@ -74,11 +183,11 @@ def dict_config(newconfig): update made to ``bigchaindb.config`` will be lost. """ bigchaindb.config = copy.deepcopy(bigchaindb._config) - update(bigchaindb.config, newconfig) + update(bigchaindb.config, update_types(config)) bigchaindb.config['CONFIGURED'] = True -def write_config(newconfig, filename=None): +def write_config(config, filename=None): """Write the provided configuration to a specific location. Args: @@ -90,18 +199,28 @@ def write_config(newconfig, filename=None): filename = CONFIG_DEFAULT_PATH with open(filename, 'w') as f: - json.dump(newconfig, f) + json.dump(config, f) -def autoconfigure(): - """Run ``file_config`` if the module has not been initialized. - """ - if bigchaindb.config.get('CONFIGURED'): +def autoconfigure(filename=None, config=None, force=False): + """Run ``file_config`` and ``env_config`` if the module has not + been initialized.""" + + if not force and bigchaindb.config.get('CONFIGURED'): return + + newconfig = env_config(bigchaindb.config) + + if config: + newconfig = update(newconfig, config) + try: - file_config() + newconfig = update(newconfig, file_config()) except FileNotFoundError: - logger.warning('Cannot find your config file. Run `bigchaindb configure` to create one') + logger.warning('Cannot find your config file.') + + dict_config(newconfig) + return newconfig def load_consensus_plugin(name=None): diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index 108e5846..53ef986c 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -2,10 +2,11 @@ BigchainDB has some Command Line Interfaces (CLIs). One of them is the `bigchaindb` command which we already saw when we first started BigchainDB using: ```text +$ bigchaindb configure $ bigchaindb start ``` -The fist time you run `bigchaindb start`, it creates a default configuration file in `$HOME/.bigchaindb`. You can check that configuration using: +When you run `bigchaindb configure`, it creates a default configuration file in `$HOME/.bigchaindb`. You can check that configuration using: ```text $ bigchaindb show-config ``` @@ -20,3 +21,65 @@ There's another command named `bigchaindb-benchmark`. It's used to run benchmark $ bigchaindb-benchmark -h $ bigchaindb-benchmark load -h ``` + +Note that you can always start `bigchaindb` using a different config file using the `-c` option. +For more information check the help with `bigchaindb -h`. + + +# Precedence in reading configuration values + +Note that there is a precedence in reading configuration values: + - (**not yet**) command line; + - local config file; + - environment vars; + - default config file (contained in ``bigchaindb.__init__``). + +This means that if the default configuration contains an entry that is: + +``` +{... + "database": {"host": "localhost", + "port": 28015} +...} +``` + +while your local file `local.json` contains: +``` +{... + "database": {"host": "whatever"} +...} +``` + +and you run this command: +``` +$ BIGCHAINDB_DATABASE_HOST=foobar BIGCHAINDB_DATABASE_PORT=42000 bigchaindb -c local.json show-config +``` + +you will get: +``` +INFO:bigchaindb.config_utils:Configuration loaded from `local.json` +{'CONFIGURED': True, + 'api_endpoint': 'http://localhost:8008/api/v1', + 'consensus_plugin': 'default', + 'database': {'host': 'localhost', 'name': 'bigchain', 'port': 28015}, + 'keypair': {'private': 'Hbqvh...', + 'public': '2Bi5NU...'}, + 'keyring': [], + 'statsd': {'host': 'localhost', 'port': 8125, 'rate': 0.01}} + +``` + +# List of env variables + +``` +- BIGCHAINDB_DATABASE_HOST +- BIGCHAINDB_DATABASE_PORT +- BIGCHAINDB_DATABASE_NAME +- BIGCHAINDB_KEYPAIR_PUBLIC +- BIGCHAINDB_KEYPAIR_PRIVATE +- BIGCHAINDB_STATSD_HOST +- BIGCHAINDB_STATSD_PORT +- BIGCHAINDB_STATSD_RATE +- BIGCHAINDB_API_ENDPOINT +- BIGCHAINDB_CONSENSUS_PLUGIN +``` diff --git a/tests/test_commands.py b/tests/test_commands.py index 6e731c13..1a6db1b1 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -72,6 +72,7 @@ def test_bigchain_run_start(mock_run_configure, mock_file_config, run_start(args) +@pytest.mark.skipif(reason="BigchainDB doesn't support the automatic creation of a config file anymore") def test_bigchain_run_start_assume_yes_create_default_config(monkeypatch, mock_processes_start, mock_generate_key_pair, mock_db_init_with_existing_db): import bigchaindb diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index fa63158d..c6224c2a 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -60,9 +60,79 @@ def test_load_consensus_plugin_raises_with_invalid_subclass(monkeypatch): from bigchaindb import config_utils monkeypatch.setattr(config_utils, 'iter_entry_points', - lambda *args: [ type('entry_point', - (object), - {'load': lambda: object}) ]) + lambda *args: [type('entry_point', (object), {'load': lambda: object})]) with pytest.raises(TypeError): config_utils.load_consensus_plugin() + + +def test_map_leafs_iterator(): + from bigchaindb import config_utils + + mapping = { + 'a': {'b': {'c': 1}, + 'd': {'z': 44}}, + 'b': {'d': 2}, + 'c': 3 + } + + result = config_utils.map_leafs(lambda x, path: x * 2, mapping) + assert result == { + 'a': {'b': {'c': 2}, + 'd': {'z': 88}}, + 'b': {'d': 4}, + 'c': 6 + } + + result = config_utils.map_leafs(lambda x, path: path, mapping) + assert result == { + 'a': {'b': {'c': ['a', 'b', 'c']}, + 'd': {'z': ['a', 'd', 'z']}}, + 'b': {'d': ['b', 'd']}, + 'c': ['c'] + } + + +def test_env_config(monkeypatch): + monkeypatch.setattr('os.environ', {'BIGCHAINDB_DATABASE_HOST': 'test-host', + 'BIGCHAINDB_DATABASE_PORT': 'test-port'}) + + from bigchaindb import config_utils + + result = config_utils.env_config({'database': {'host': None, 'port': None}}) + expected = {'database': {'host': 'test-host', 'port': 'test-port'}} + + assert result == expected + + +def test_autoconfigure_read_both_from_file_and_env(monkeypatch): + monkeypatch.setattr('bigchaindb.config_utils.file_config', lambda: {}) + monkeypatch.setattr('os.environ', {'BIGCHAINDB_DATABASE_HOST': 'test-host', + 'BIGCHAINDB_DATABASE_PORT': '4242'}) + + import bigchaindb + from bigchaindb import config_utils + config_utils.autoconfigure() + + assert bigchaindb.config['database']['host'] == 'test-host' + assert bigchaindb.config['database']['port'] == 4242 + assert bigchaindb.config == { + 'CONFIGURED': True, + 'database': { + 'host': 'test-host', + 'port': 4242, + 'name': 'bigchain', + }, + 'keypair': { + 'public': None, + 'private': None, + }, + 'keyring': [], + 'statsd': { + 'host': 'localhost', + 'port': 8125, + 'rate': 0.01, + }, + 'api_endpoint': 'http://localhost:8008/api/v1', + 'consensus_plugin': 'default', + } From 804365db24c23a604dc5fa75b5da1fc47ee89269 Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 31 Mar 2016 11:37:09 +0200 Subject: [PATCH 02/39] Improve config, tests and docs Add better support to env variables. --- bigchaindb/commands/bigchain.py | 2 +- bigchaindb/config_utils.py | 42 +++++++++++++++++++++++--------- docs/source/bigchaindb-cli.md | 24 ++++++++++++------ tests/test_commands.py | 2 +- tests/utils/test_config_utils.py | 16 ++++++------ 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 40f0cfa5..fbcc3f31 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -26,7 +26,7 @@ def run_show_config(args): # TODO Proposal: remove the "hidden" configuration. Only show config. If # the system needs to be configured, then display information on how to # configure the system. - bigchaindb.config_utils.file_config(args.config) + bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) pprint(bigchaindb.config) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index 7ab81250..b1e729d2 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -155,20 +155,38 @@ def update_types(config): reference = bigchaindb.config + def _coerce(current, value): + # Coerce a value to the `current` type. + try: + # First we try to apply current to the value, since it + # might be a function + return current(value) + except TypeError: + # Then we check if current is a list AND if the value + # is a string. + if isinstance(current, list) and isinstance(value, str): + # If so, we use the colon as the separator + return value.split(':') + + try: + # If we are here, we should try to apply the type + # of `current` to the value + return type(current)(value) + except TypeError: + # Worst case scenario we return the value itself. + return value + def _update_type(value, path): - ref = reference + current = reference for elem in path: - ref = ref[elem] - - try: - return ref(value) - except TypeError: try: - return type(ref)(value) - except TypeError: + current = current[elem] + except KeyError: return value + return _coerce(current, value) + return map_leafs(_update_type, config) @@ -207,6 +225,7 @@ def autoconfigure(filename=None, config=None, force=False): been initialized.""" if not force and bigchaindb.config.get('CONFIGURED'): + logger.info('System already configured, skipping autoconfiguration') return newconfig = env_config(bigchaindb.config) @@ -215,9 +234,10 @@ def autoconfigure(filename=None, config=None, force=False): newconfig = update(newconfig, config) try: - newconfig = update(newconfig, file_config()) - except FileNotFoundError: - logger.warning('Cannot find your config file.') + # import pdb; pdb.set_trace() + newconfig = update(newconfig, file_config(filename=filename)) + except FileNotFoundError as e: + logger.warning('Cannot find config file `%s`.' % e.filename) dict_config(newconfig) return newconfig diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index 53ef986c..dce80671 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -29,7 +29,6 @@ For more information check the help with `bigchaindb -h`. # Precedence in reading configuration values Note that there is a precedence in reading configuration values: - - (**not yet**) command line; - local config file; - environment vars; - default config file (contained in ``bigchaindb.__init__``). @@ -46,29 +45,37 @@ This means that if the default configuration contains an entry that is: while your local file `local.json` contains: ``` {... - "database": {"host": "whatever"} + "database": {"host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com"} ...} ``` and you run this command: ``` -$ BIGCHAINDB_DATABASE_HOST=foobar BIGCHAINDB_DATABASE_PORT=42000 bigchaindb -c local.json show-config +$ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ + BIGCHAINDB_DATABASE_PORT=4242 \ + BIGCHAINDB_KEYRING=pubkey0:pubkey1 \ + bigchaindb -c local.json show-config ``` you will get: ``` +Cannot find config file `/home/vrde/.bigchaindb`. INFO:bigchaindb.config_utils:Configuration loaded from `local.json` {'CONFIGURED': True, 'api_endpoint': 'http://localhost:8008/api/v1', 'consensus_plugin': 'default', - 'database': {'host': 'localhost', 'name': 'bigchain', 'port': 28015}, - 'keypair': {'private': 'Hbqvh...', - 'public': '2Bi5NU...'}, - 'keyring': [], + 'database': {'host': 'ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com', + 'name': 'bigchain', + 'port': 4242}, + 'keypair': {'private': None, 'public': None}, + 'keyring': ['pubkey0', 'pubkey1'], 'statsd': {'host': 'localhost', 'port': 8125, 'rate': 0.01}} - ``` +Note that the type of `keyring` is a list. If you want to pass a list as an +environ variable you need to use colon (`:`) as separator. + + # List of env variables ``` @@ -77,6 +84,7 @@ INFO:bigchaindb.config_utils:Configuration loaded from `local.json` - BIGCHAINDB_DATABASE_NAME - BIGCHAINDB_KEYPAIR_PUBLIC - BIGCHAINDB_KEYPAIR_PRIVATE +- BIGCHAINDB_KEYRING - BIGCHAINDB_STATSD_HOST - BIGCHAINDB_STATSD_PORT - BIGCHAINDB_STATSD_RATE diff --git a/tests/test_commands.py b/tests/test_commands.py index 1a6db1b1..e1fbe14a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -14,7 +14,7 @@ def mock_run_configure(monkeypatch): @pytest.fixture def mock_file_config(monkeypatch): from bigchaindb import config_utils - monkeypatch.setattr(config_utils, 'file_config', lambda *args: None) + monkeypatch.setattr(config_utils, 'file_config', lambda *args, **kwargs: {}) @pytest.fixture diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index c6224c2a..9defc601 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -106,28 +106,30 @@ def test_env_config(monkeypatch): def test_autoconfigure_read_both_from_file_and_env(monkeypatch): - monkeypatch.setattr('bigchaindb.config_utils.file_config', lambda: {}) - monkeypatch.setattr('os.environ', {'BIGCHAINDB_DATABASE_HOST': 'test-host', - 'BIGCHAINDB_DATABASE_PORT': '4242'}) + file_config = { + 'database': {'host': 'test-host'} + } + monkeypatch.setattr('bigchaindb.config_utils.file_config', lambda *args, **kwargs: file_config) + monkeypatch.setattr('os.environ', {'BIGCHAINDB_DATABASE_NAME': 'test-dbname', + 'BIGCHAINDB_DATABASE_PORT': '4242', + 'BIGCHAINDB_KEYRING': 'pubkey_0:pubkey_1:pubkey_2'}) import bigchaindb from bigchaindb import config_utils config_utils.autoconfigure() - assert bigchaindb.config['database']['host'] == 'test-host' - assert bigchaindb.config['database']['port'] == 4242 assert bigchaindb.config == { 'CONFIGURED': True, 'database': { 'host': 'test-host', 'port': 4242, - 'name': 'bigchain', + 'name': 'test-dbname', }, 'keypair': { 'public': None, 'private': None, }, - 'keyring': [], + 'keyring': ['pubkey_0', 'pubkey_1', 'pubkey_2'], 'statsd': { 'host': 'localhost', 'port': 8125, From 75a4bf65dd37ca807556165dc5cde31d0c870efd Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 31 Mar 2016 11:44:33 +0200 Subject: [PATCH 03/39] Remove reference to command line params --- bigchaindb/config_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index b1e729d2..e4e23144 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -5,7 +5,6 @@ By calling `file_config`, the global configuration (stored in configuration file. Note that there is a precedence in reading configuration values: - - (**not yet**) command line; - local config file; - environment vars; - default config file (contained in ``bigchaindb.__init__``). From 5864c34115e4110c1784bc83a013a0b61cebd7d9 Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 31 Mar 2016 11:50:48 +0200 Subject: [PATCH 04/39] Remove redundant documentation --- bigchaindb/config_utils.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index e4e23144..f165334a 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -8,44 +8,9 @@ Note that there is a precedence in reading configuration values: - local config file; - environment vars; - default config file (contained in ``bigchaindb.__init__``). - -This means that if the default configuration contains an entry that is: - -``` -{... - "database": {"host": "localhost", - "port": 28015} -...} -``` - -while your local file `local.json` contains: -``` -{... - "database": {"host": "whatever"} -...} -``` - -and you run this command: -``` -$ BIGCHAINDB_DATABASE_HOST=foobar BIGCHAINDB_DATABASE_PORT=42000 bigchaindb -c local.json show-config -``` - -You will get: -``` -INFO:bigchaindb.config_utils:Configuration loaded from `local.json` -{'CONFIGURED': True, - 'api_endpoint': 'http://localhost:8008/api/v1', - 'consensus_plugin': 'default', - 'database': {'host': 'localhost', 'name': 'bigchain', 'port': 28015}, - 'keypair': {'private': 'Hbqvh...', - 'public': '2Bi5NU...'}, - 'keyring': [], - 'statsd': {'host': 'localhost', 'port': 8125, 'rate': 0.01}} - -``` - """ + import os import copy import json From aec3b3db6c48346c15fe48d3d3a16fa8cdef8295 Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 16:14:43 +0200 Subject: [PATCH 05/39] Hide private key when showing configuration --- bigchaindb/commands/bigchain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index fbcc3f31..6e2bf1d3 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -27,6 +27,7 @@ def run_show_config(args): # the system needs to be configured, then display information on how to # configure the system. bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) + bigchaindb.config['keypair']['private'] = 'x' * 45 pprint(bigchaindb.config) From 4b32e8e01a627a75f0d6f5cc0de41e325065b2f6 Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 16:15:17 +0200 Subject: [PATCH 06/39] Use autoconfigure on every command --- bigchaindb/commands/bigchain.py | 6 +++--- bigchaindb/config_utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 6e2bf1d3..fddcce52 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -70,7 +70,7 @@ def run_configure(args, skip_if_exists=False): def run_init(args): """Initialize the database""" - bigchaindb.config_utils.file_config(args.config) + bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) # TODO Provide mechanism to: # 1. prompt the user to inquire whether they wish to drop the db # 2. force the init, (e.g., via -f flag) @@ -83,14 +83,14 @@ def run_init(args): def run_drop(args): """Drop the database""" - bigchaindb.config_utils.file_config(args.config) + bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) db.drop(assume_yes=args.yes) def run_start(args): """Start the processes to run the node""" # run_configure(args, skip_if_exists=True) - bigchaindb.config_utils.file_config(args.config) + bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) try: db.init() except DatabaseAlreadyExists: diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index f165334a..19caef90 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -189,7 +189,7 @@ def autoconfigure(filename=None, config=None, force=False): been initialized.""" if not force and bigchaindb.config.get('CONFIGURED'): - logger.info('System already configured, skipping autoconfiguration') + logger.debug('System already configured, skipping autoconfiguration') return newconfig = env_config(bigchaindb.config) From c5cefd6ad5c818ca6fbf23c5b60c6e6242a93e57 Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 16:15:38 +0200 Subject: [PATCH 07/39] Remove useless import --- bigchaindb/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index f76dd2d6..48838adb 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,6 +1,5 @@ import rethinkdb as r import random -import json import rapidjson From a56aa992fa6dc1501e5ac702ca2eeff33e31cb94 Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 16:36:12 +0200 Subject: [PATCH 08/39] Do not hide empty privkeys --- bigchaindb/commands/bigchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index fddcce52..70554ad3 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -27,7 +27,8 @@ def run_show_config(args): # the system needs to be configured, then display information on how to # configure the system. bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) - bigchaindb.config['keypair']['private'] = 'x' * 45 + private_key = bigchaindb.config['keypair']['private'] + bigchaindb.config['keypair']['private'] = 'x' * 45 if private_key else None pprint(bigchaindb.config) From 3c31a8684ffa2b878f1708abc253c1ee87d0366f Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 16:50:34 +0200 Subject: [PATCH 09/39] Remove `CONFIGURED` key from show-config --- bigchaindb/commands/bigchain.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 70554ad3..e7cb933f 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -27,9 +27,11 @@ def run_show_config(args): # the system needs to be configured, then display information on how to # configure the system. bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) - private_key = bigchaindb.config['keypair']['private'] - bigchaindb.config['keypair']['private'] = 'x' * 45 if private_key else None - pprint(bigchaindb.config) + config = copy.deepcopy(bigchaindb.config) + del config['CONFIGURED'] + private_key = config['keypair']['private'] + config['keypair']['private'] = 'x' * 45 if private_key else None + pprint(config) def run_configure(args, skip_if_exists=False): From 959320cdb9603edeae6552a5e33b1971ac8f1df3 Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 6 Apr 2016 17:39:44 +0200 Subject: [PATCH 10/39] Remove `CONFIGURED` from tests as well :sweat: --- tests/test_commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_commands.py b/tests/test_commands.py index e1fbe14a..895efc07 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -107,6 +107,8 @@ def test_bigchain_show_config(capsys, mock_file_config): _, _ = capsys.readouterr() run_show_config(args) output_config, _ = capsys.readouterr() + config['keypair']['private'] = 'x' * 45 + del config['CONFIGURED'] pprint(config) expected_outout_config, _ = capsys.readouterr() assert output_config == expected_outout_config From 06f45f1dc71fe4387d4e3fc7a8e7c3d6276e38be Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 7 Apr 2016 14:15:29 +0200 Subject: [PATCH 11/39] Change server port to 9984 --- bigchaindb/__init__.py | 12 ++++++++++-- bigchaindb/web/server.py | 3 +++ docs/source/bigchaindb-cli.md | 3 +++ docs/source/http-client-server-api.md | 13 +++++++++---- docs/source/installing-server.md | 10 ++++++++-- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index f1dc0f2f..6a462ab7 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -1,9 +1,17 @@ import copy +# from functools import reduce +# PORT_NUMBER = reduce(lambda x, y: x * y, map(ord, 'BigchainDB')) % 2**16 +# basically, the port number is 9984 + config = { 'server': { - 'bind': '0.0.0.0:5000', + # Note: this section supports all the Gunicorn settings: + # - http://docs.gunicorn.org/en/stable/settings.html + 'bind': '0.0.0.0:9984', + 'workers': None, # if none, the value will be cpu_count * 2 + 1 + 'threads': None, # if none, the value will be cpu_count * 2 + 1 }, 'database': { 'host': 'localhost', @@ -20,7 +28,7 @@ config = { 'port': 8125, 'rate': 0.01, }, - 'api_endpoint': 'http://localhost:8008/api/v1', + 'api_endpoint': 'http://localhost:9984/api/v1', 'consensus_plugin': 'default', } diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py index 208f2458..021ceab4 100644 --- a/bigchaindb/web/server.py +++ b/bigchaindb/web/server.py @@ -76,6 +76,9 @@ def create_server(settings): if not settings.get('workers'): settings['workers'] = (multiprocessing.cpu_count() * 2) + 1 + if not settings.get('threads'): + settings['threads'] = (multiprocessing.cpu_count() * 2) + 1 + debug = settings.pop('debug', False) app = create_app(debug) standalone = StandaloneApplication(app, settings) diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index dce80671..a1b9e778 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -90,4 +90,7 @@ environ variable you need to use colon (`:`) as separator. - BIGCHAINDB_STATSD_RATE - BIGCHAINDB_API_ENDPOINT - BIGCHAINDB_CONSENSUS_PLUGIN +- BIGCHAINDB_SERVER_BIND +- BIGCHAINDB_SERVER_WORKERS +- BIGCHAINDB_SERVER_THREADS ``` diff --git a/docs/source/http-client-server-api.md b/docs/source/http-client-server-api.md index 1520b046..da045fa3 100644 --- a/docs/source/http-client-server-api.md +++ b/docs/source/http-client-server-api.md @@ -2,10 +2,15 @@ When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at: -[http://localhost:5000/api/v1/](http://localhost:5000/api/v1/) +- [http://localhost:9984/api/v1/](http://localhost:9984/api/v1/) -Right now, that API can only be accessed from localhost (i.e. not remotely). In the future, we'll enable remote access and explain how that works. See [Issue #149](https://github.com/bigchaindb/bigchaindb/issues/149) on GitHub. -The HTTP API currently exposes two endpoints, one to get information about a specific transaction id, and one to push a transaction to the BigchainDB cluster. Those endpoints are documented at: +Please note that by default the server binds to `0.0.0.0:9984`, hence the API +is exposed to the world. + +The HTTP API currently exposes two endpoints, one to get information about a +specific transaction id, and one to push a transaction to the BigchainDB +cluster. Those endpoints are documented at: + +- [http://docs.bigchaindb.apiary.io/](http://docs.bigchaindb.apiary.io/) -[http://docs.bigchaindb.apiary.io/](http://docs.bigchaindb.apiary.io/) diff --git a/docs/source/installing-server.md b/docs/source/installing-server.md index c168f62d..4047f817 100644 --- a/docs/source/installing-server.md +++ b/docs/source/installing-server.md @@ -94,10 +94,16 @@ $ rethinkdb Then open a different terminal and run: ```text -$ bigchaindb start +$ bigchaindb init ``` -During its first run, BigchainDB Server takes care of configuring a single node environment. +During its first run, the command takes care of configuring a single node environment. + + +After configuring the node, you can start it with: +```text +$ bigchaindb start +``` ## Run BigchainDB with Docker From 0692fe04d0175162409e7f440d3994cb6c1b393f Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 7 Apr 2016 14:16:01 +0200 Subject: [PATCH 12/39] Print config using json --- bigchaindb/commands/bigchain.py | 5 +-- docs/source/bigchaindb-cli.md | 65 ++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 1832b99f..0b82986a 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -5,6 +5,7 @@ import os import logging import argparse import copy +import json import bigchaindb import bigchaindb.config_utils @@ -21,8 +22,6 @@ logger = logging.getLogger(__name__) def run_show_config(args): """Show the current configuration""" - from pprint import pprint - # TODO Proposal: remove the "hidden" configuration. Only show config. If # the system needs to be configured, then display information on how to # configure the system. @@ -31,7 +30,7 @@ def run_show_config(args): del config['CONFIGURED'] private_key = config['keypair']['private'] config['keypair']['private'] = 'x' * 45 if private_key else None - pprint(config) + print(json.dumps(config, indent=4, sort_keys=True)) def run_configure(args, skip_if_exists=False): diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index a1b9e778..ddb04e1c 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -35,18 +35,25 @@ Note that there is a precedence in reading configuration values: This means that if the default configuration contains an entry that is: -``` -{... - "database": {"host": "localhost", - "port": 28015} -...} +```json +{ + + "database": { + "host": "localhost", + "port": 28015 + } + +} ``` while your local file `local.json` contains: -``` -{... - "database": {"host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com"} -...} +```json +{ + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com" + } +} + ``` and you run this command: @@ -58,18 +65,34 @@ $ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ ``` you will get: -``` -Cannot find config file `/home/vrde/.bigchaindb`. -INFO:bigchaindb.config_utils:Configuration loaded from `local.json` -{'CONFIGURED': True, - 'api_endpoint': 'http://localhost:8008/api/v1', - 'consensus_plugin': 'default', - 'database': {'host': 'ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com', - 'name': 'bigchain', - 'port': 4242}, - 'keypair': {'private': None, 'public': None}, - 'keyring': ['pubkey0', 'pubkey1'], - 'statsd': {'host': 'localhost', 'port': 8125, 'rate': 0.01}} +```json +{ + "api_endpoint": "http://localhost:8008/api/v1", + "consensus_plugin": "default", + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com", + "name": "bigchain", + "port": 4242 + }, + "keypair": { + "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "public": "nJq6EmdUkvFjQRB5hFvDmvZtv1deb3W3RgmiAq6dyygC" + }, + "keyring": [ + "pubkey0", + "pubkey1" + ], + "server": { + "bind": "0.0.0.0:9984", + "threads": null, + "workers": null + }, + "statsd": { + "host": "localhost", + "port": 8125, + "rate": 0.01 + } +} ``` Note that the type of `keyring` is a list. If you want to pass a list as an From ae641d6a9611877992ae526b71c433c46db233df Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 7 Apr 2016 14:31:34 +0200 Subject: [PATCH 13/39] Fix tests --- tests/test_commands.py | 5 ++--- tests/utils/test_config_utils.py | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index 895efc07..1ad96d45 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,3 +1,4 @@ +import json from argparse import Namespace from pprint import pprint import copy @@ -109,9 +110,7 @@ def test_bigchain_show_config(capsys, mock_file_config): output_config, _ = capsys.readouterr() config['keypair']['private'] = 'x' * 45 del config['CONFIGURED'] - pprint(config) - expected_outout_config, _ = capsys.readouterr() - assert output_config == expected_outout_config + assert output_config.strip() == json.dumps(config, indent=4, sort_keys=True) def test_bigchain_run_init_when_db_exists(mock_file_config, mock_db_init_with_existing_db): diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index 9defc601..eafcef08 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -120,6 +120,11 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch): assert bigchaindb.config == { 'CONFIGURED': True, + 'server': { + 'bind': '0.0.0.0:9984', + 'workers': None, + 'threads': None, + }, 'database': { 'host': 'test-host', 'port': 4242, @@ -135,6 +140,6 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch): 'port': 8125, 'rate': 0.01, }, - 'api_endpoint': 'http://localhost:8008/api/v1', + 'api_endpoint': 'http://localhost:9984/api/v1', 'consensus_plugin': 'default', } From c9cd8421513798fbfbb567101096cb8511463f89 Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 7 Apr 2016 17:54:36 +0200 Subject: [PATCH 14/39] Avoid loading the config located in the home of the current user --- bigchaindb/commands/bigchain.py | 2 +- tests/conftest.py | 12 ++++++++++++ tests/test_commands.py | 16 ++++------------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 0b82986a..c460ae15 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -58,7 +58,7 @@ def run_configure(args, skip_if_exists=False): conf['keypair']['private'], conf['keypair']['public'] = crypto.generate_key_pair() if not args.yes: - for key in ('host', 'port'): + for key in ('bind', ): val = conf['server'][key] conf['server'][key] = input('API Server {}? (default `{}`): '.format(key, val)) or val diff --git a/tests/conftest.py b/tests/conftest.py index 3781b2b6..f73fc63c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,18 @@ USER_PRIVATE_KEY = 'GmRZxQdQv7tooMijXytQkexKuFN6mJocciJarAmMwTX2' USER_PUBLIC_KEY = 'r3cEu8GNoz8rYpNJ61k7GqfR8VEvdUbtyHce8u1kaYwh' +# We need this function to avoid loading an existing +# conf file located in the home of the user running +# the tests. If it's too aggressive we can change it +# later. +@pytest.fixture(scope='function', autouse=True) +def ignore_local_config_file(monkeypatch): + def mock_file_config(filename=None): + raise FileNotFoundError() + + monkeypatch.setattr('bigchaindb.config_utils.file_config', mock_file_config) + + @pytest.fixture def restore_config(request, node_config): from bigchaindb import config_utils diff --git a/tests/test_commands.py b/tests/test_commands.py index 1ad96d45..64918cd4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -12,12 +12,6 @@ def mock_run_configure(monkeypatch): monkeypatch.setattr(bigchain, 'run_configure', lambda *args, **kwargs: None) -@pytest.fixture -def mock_file_config(monkeypatch): - from bigchaindb import config_utils - monkeypatch.setattr(config_utils, 'file_config', lambda *args, **kwargs: {}) - - @pytest.fixture def mock_write_config(monkeypatch): from bigchaindb import config_utils @@ -66,8 +60,7 @@ def mock_bigchaindb_backup_config(monkeypatch): monkeypatch.setattr('bigchaindb._config', config) -def test_bigchain_run_start(mock_run_configure, mock_file_config, - mock_processes_start, mock_db_init_with_existing_db): +def test_bigchain_run_start(mock_run_configure, mock_processes_start, mock_db_init_with_existing_db): from bigchaindb.commands.bigchain import run_start args = Namespace(config=None, yes=True) run_start(args) @@ -101,25 +94,24 @@ def test_bigchain_run_start_assume_yes_create_default_config(monkeypatch, mock_p # TODO Please beware, that if debugging, the "-s" switch for pytest will # interfere with capsys. # See related issue: https://github.com/pytest-dev/pytest/issues/128 -def test_bigchain_show_config(capsys, mock_file_config): +def test_bigchain_show_config(capsys): from bigchaindb import config from bigchaindb.commands.bigchain import run_show_config args = Namespace(config=None) _, _ = capsys.readouterr() run_show_config(args) output_config, _ = capsys.readouterr() - config['keypair']['private'] = 'x' * 45 del config['CONFIGURED'] assert output_config.strip() == json.dumps(config, indent=4, sort_keys=True) -def test_bigchain_run_init_when_db_exists(mock_file_config, mock_db_init_with_existing_db): +def test_bigchain_run_init_when_db_exists(mock_db_init_with_existing_db): from bigchaindb.commands.bigchain import run_init args = Namespace(config=None) run_init(args) -def test_drop_existing_db(mock_file_config, mock_rethink_db_drop): +def test_drop_existing_db(mock_rethink_db_drop): from bigchaindb.commands.bigchain import run_drop args = Namespace(config=None, yes=True) run_drop(args) From 1ddfa2becbbbfc5c0578f569c98d88df246db44e Mon Sep 17 00:00:00 2001 From: vrde Date: Thu, 7 Apr 2016 18:06:04 +0200 Subject: [PATCH 15/39] Add explicit tests for update_types --- bigchaindb/config_utils.py | 8 +++----- tests/utils/test_config_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index 19caef90..df63e7c2 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -113,12 +113,10 @@ def env_config(config): return map_leafs(load_from_env, config) -def update_types(config): +def update_types(config, reference, list_sep=':'): """Return a new configuration where all the values types are aligned with the ones in the default configuration""" - reference = bigchaindb.config - def _coerce(current, value): # Coerce a value to the `current` type. try: @@ -130,7 +128,7 @@ def update_types(config): # is a string. if isinstance(current, list) and isinstance(value, str): # If so, we use the colon as the separator - return value.split(':') + return value.split(list_sep) try: # If we are here, we should try to apply the type @@ -165,7 +163,7 @@ def dict_config(config): update made to ``bigchaindb.config`` will be lost. """ bigchaindb.config = copy.deepcopy(bigchaindb._config) - update(bigchaindb.config, update_types(config)) + update(bigchaindb.config, update_types(config, bigchaindb.config)) bigchaindb.config['CONFIGURED'] = True diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index eafcef08..cec7743f 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -93,6 +93,27 @@ def test_map_leafs_iterator(): } +def test_update_types(): + from bigchaindb import config_utils + + raw = { + 'a_string': 'test', + 'an_int': '42', + 'a_float': '3.14', + 'a_list': 'a:b:c', + } + + reference = { + 'a_string': 'test', + 'an_int': 42, + 'a_float': 3.14, + 'a_list': ['a', 'b', 'c'], + } + + result = config_utils.update_types(raw, reference) + assert result == reference + + def test_env_config(monkeypatch): monkeypatch.setattr('os.environ', {'BIGCHAINDB_DATABASE_HOST': 'test-host', 'BIGCHAINDB_DATABASE_PORT': 'test-port'}) @@ -143,3 +164,4 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch): 'api_endpoint': 'http://localhost:9984/api/v1', 'consensus_plugin': 'default', } + From 32e08b7c63edab652d8b2a3e94ce8446b4de268e Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 8 Apr 2016 10:14:14 +0200 Subject: [PATCH 16/39] Remove useless func --- bigchaindb/config_utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index df63e7c2..0ce41360 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -51,13 +51,6 @@ def map_leafs(func, mapping): return _inner(copy.deepcopy(mapping)) -def get_casted_value(original, to_convert): - try: - return original(to_convert) - except TypeError: - return type(original)(to_convert) - - # Thanks Alex <3 # http://stackoverflow.com/a/3233356/597097 def update(d, u): From d2b44890f26646ab61c1a0567a827323d0ae6b6f Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 8 Apr 2016 14:04:17 +0200 Subject: [PATCH 17/39] changed bigchaindb-benchmark to use the new configuration loader --- bigchaindb/commands/bigchain_benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigchaindb/commands/bigchain_benchmark.py b/bigchaindb/commands/bigchain_benchmark.py index 96ba0a5b..2bac4c08 100644 --- a/bigchaindb/commands/bigchain_benchmark.py +++ b/bigchaindb/commands/bigchain_benchmark.py @@ -34,7 +34,7 @@ def _run_load(tx_left, stats): def run_load(args): - bigchaindb.config_utils.file_config(args.config) + bigchaindb.config_utils.autoconfigure(filename=args.config, force=True) logger.info('Starting %s processes', args.multiprocess) stats = logstats.Logstats() logstats.thread.start(stats) From f90254c3c92b4a454d1dab1ac2e51965413dba33 Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 8 Apr 2016 15:14:14 +0200 Subject: [PATCH 18/39] Update documentation --- docs/source/bigchaindb-cli.md | 92 ------------- docs/source/configuration.md | 247 ++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 92 deletions(-) create mode 100644 docs/source/configuration.md diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index ddb04e1c..4186426e 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -25,95 +25,3 @@ $ bigchaindb-benchmark load -h Note that you can always start `bigchaindb` using a different config file using the `-c` option. For more information check the help with `bigchaindb -h`. - -# Precedence in reading configuration values - -Note that there is a precedence in reading configuration values: - - local config file; - - environment vars; - - default config file (contained in ``bigchaindb.__init__``). - -This means that if the default configuration contains an entry that is: - -```json -{ - - "database": { - "host": "localhost", - "port": 28015 - } - -} -``` - -while your local file `local.json` contains: -```json -{ - "database": { - "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com" - } -} - -``` - -and you run this command: -``` -$ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ - BIGCHAINDB_DATABASE_PORT=4242 \ - BIGCHAINDB_KEYRING=pubkey0:pubkey1 \ - bigchaindb -c local.json show-config -``` - -you will get: -```json -{ - "api_endpoint": "http://localhost:8008/api/v1", - "consensus_plugin": "default", - "database": { - "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com", - "name": "bigchain", - "port": 4242 - }, - "keypair": { - "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "public": "nJq6EmdUkvFjQRB5hFvDmvZtv1deb3W3RgmiAq6dyygC" - }, - "keyring": [ - "pubkey0", - "pubkey1" - ], - "server": { - "bind": "0.0.0.0:9984", - "threads": null, - "workers": null - }, - "statsd": { - "host": "localhost", - "port": 8125, - "rate": 0.01 - } -} -``` - -Note that the type of `keyring` is a list. If you want to pass a list as an -environ variable you need to use colon (`:`) as separator. - - -# List of env variables - -``` -- BIGCHAINDB_DATABASE_HOST -- BIGCHAINDB_DATABASE_PORT -- BIGCHAINDB_DATABASE_NAME -- BIGCHAINDB_KEYPAIR_PUBLIC -- BIGCHAINDB_KEYPAIR_PRIVATE -- BIGCHAINDB_KEYRING -- BIGCHAINDB_STATSD_HOST -- BIGCHAINDB_STATSD_PORT -- BIGCHAINDB_STATSD_RATE -- BIGCHAINDB_API_ENDPOINT -- BIGCHAINDB_CONSENSUS_PLUGIN -- BIGCHAINDB_SERVER_BIND -- BIGCHAINDB_SERVER_WORKERS -- BIGCHAINDB_SERVER_THREADS -``` diff --git a/docs/source/configuration.md b/docs/source/configuration.md new file mode 100644 index 00000000..6a906b96 --- /dev/null +++ b/docs/source/configuration.md @@ -0,0 +1,247 @@ +# Configuring a BigchainDB Node + +The standard way to configure a BigchainDB node is to run the command `configure`: + +```text +$ bigchaindb configure +``` + +This command will generate a new keypair and will guide you through the +configuration of the system. By default keypair and settings will be saved in the +`$HOME/.bigchaindb` file. + + +## Using a different path for the configuration + +By default the configuration is stored in `$HOME/.bigchaindb`, if you want to +specify a different path for your configuration you can use the `-c` parameter. +This works for every subcommand under the `bigchaindb` executable. + +For example, if you want to **generate** a new configuration file under a +specific path, you can run: + +``` +$ bigchaindb -c local.json configure +$ bigchaindb -c test.json configure +``` + +This will create two new files called `local.json` and `test.json` in your +current working directory. + +From now on, you can refer to those configuration files using the `-c` +parameter, for example: + +``` +$ bigchaindb -c local.json show-config +``` + +Will show the configuration for `local.json`. + +If you want to **start** BigchainDB with the `test.json` configuration, you can +try: + +``` +$ bigchaindb -c test.json start +``` + + +## Using environ variables to configure the node + +Sometimes it's more convenient to use environment variables to configure the +system, for example when using Docker or Heroku. Another use case is to have a +*volatile*, throw away configuration you need to test something quickly. In +those cases you can configure the system using environment variables. + +Every configuration key can be mapped to an environment variable. The +environment variables available are: +``` +- BIGCHAINDB_DATABASE_HOST +- BIGCHAINDB_DATABASE_PORT +- BIGCHAINDB_DATABASE_NAME +- BIGCHAINDB_KEYPAIR_PUBLIC +- BIGCHAINDB_KEYPAIR_PRIVATE +- BIGCHAINDB_KEYRING +- BIGCHAINDB_STATSD_HOST +- BIGCHAINDB_STATSD_PORT +- BIGCHAINDB_STATSD_RATE +- BIGCHAINDB_API_ENDPOINT +- BIGCHAINDB_CONSENSUS_PLUGIN +- BIGCHAINDB_SERVER_BIND +- BIGCHAINDB_SERVER_WORKERS +- BIGCHAINDB_SERVER_THREADS +``` + +As an example, let's assume we **don't** have any configuration file stored in +the default location `$HOME/.bigchaindb`. + +As you can see, `show-config` displays the default configuration (and a +warning): +``` +$ bigchaindb show-config +WARNING:bigchaindb.config_utils:Cannot find config file `/home/vrde/.bigchaindb`. +{ + "api_endpoint": "http://localhost:9984/api/v1", + "consensus_plugin": "default", + "database": { + "host": "localhost", + "name": "bigchain", + "port": 28015 + }, + "keypair": { + "private": null, + "public": null + }, + "keyring": [], + "server": { + "bind": "0.0.0.0:9984", + "threads": null, + "workers": null + }, + "statsd": { + "host": "localhost", + "port": 8125, + "rate": 0.01 + } +} +``` + +If we try to run the node, the command will fail: + +``` +$ bigchaindb start +WARNING:bigchaindb.config_utils:Cannot find config file `/home/vrde/.bigchaindb`. +INFO:bigchaindb.db.utils:Create: +INFO:bigchaindb.db.utils: - database `bigchain` +INFO:bigchaindb.db.utils: - tables +INFO:bigchaindb.db.utils: - indexes +INFO:bigchaindb.db.utils: - genesis block +Traceback (most recent call last): + File "/home/vrde/.local/share/virtualenvs/bigchain/bin/bigchaindb", line 9, in + load_entry_point('BigchainDB', 'console_scripts', 'bigchaindb')() + File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/bigchain.py", line 137, in main + start(parser, globals()) + File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/utils.py", line 41, in start + func(args) + File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/bigchain.py", line 101, in run_start + db.init() + File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/db/utils.py", line 62, in init + b = bigchaindb.Bigchain() + File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/core.py", line 58, in __init__ + raise exceptions.KeypairNotFoundException() +bigchaindb.exceptions.KeypairNotFoundException +``` + +This is failing as expected: a BigchainDB node needs at least a key pair to work. +We can pass the key pair using environment variables: +``` +$ BIGCHAINDB_KEYPAIR_PUBLIC=26y9EuyGP44JXxqcvF8GbCJGqkiqFXddZzxVjLU3rWbHp \ + BIGCHAINDB_KEYPAIR_PRIVATE=9PkLfHbzXnSSNnb1sSBL73C2MydzKLs5fAHoA4Q7otrG \ + bigchaindb start +``` + +We can also run `show-config` to see how the configuration looks like: +``` +$ BIGCHAINDB_KEYPAIR_PUBLIC=26y9EuyGP44JXxqcvF8GbCJGqkiqFXddZzxVjLU3rWbHp \ + BIGCHAINDB_KEYPAIR_PRIVATE=9PkLfHbzXnSSNnb1sSBL73C2MydzKLs5fAHoA4Q7otrG \ + bigchaindb show-config + +WARNING:bigchaindb.config_utils:Cannot find config file `/home/vrde/.bigchaindb`. +{ + "api_endpoint": "http://localhost:9984/api/v1", + "consensus_plugin": "default", + "database": { + "host": "localhost", + "name": "bigchain", + "port": 28015 + }, + "keypair": { + "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "public": "26y9EuyGP44JXxqcvF8GbCJGqkiqFXddZzxVjLU3rWbHp" + }, + "keyring": [], + "server": { + "bind": "0.0.0.0:9984", + "threads": null, + "workers": null + }, + "statsd": { + "host": "localhost", + "port": 8125, + "rate": 0.01 + } +} +``` + + +# Precedence in reading configuration values + +Note that there is a precedence in reading configuration values: + - local config file; + - environment vars; + - default config file (contained in ``bigchaindb.__init__``). + +This means that if the default configuration contains an entry that is: + +```json +{ + + "database": { + "host": "localhost", + "port": 28015 + } + +} +``` + +while your local file `local.json` contains: +```json +{ + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com" + } +} + +``` + +and you run this command: +``` +$ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ + BIGCHAINDB_DATABASE_PORT=4242 \ + BIGCHAINDB_KEYRING=pubkey0:pubkey1 \ + bigchaindb -c local.json show-config +``` + +you will get: +```json +{ + "api_endpoint": "http://localhost:8008/api/v1", + "consensus_plugin": "default", + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com", + "name": "bigchain", + "port": 4242 + }, + "keypair": { + "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "public": "nJq6EmdUkvFjQRB5hFvDmvZtv1deb3W3RgmiAq6dyygC" + }, + "keyring": [ + "pubkey0", + "pubkey1" + ], + "server": { + "bind": "0.0.0.0:9984", + "threads": null, + "workers": null + }, + "statsd": { + "host": "localhost", + "port": 8125, + "rate": 0.01 + } +} +``` + +Note that the type of `keyring` is a list. If you want to pass a list as an +environ variable you need to use colon (`:`) as separator. + From 82155764975e6d62401b3cc4b8f95468ab525aa3 Mon Sep 17 00:00:00 2001 From: vrde Date: Fri, 8 Apr 2016 15:56:10 +0200 Subject: [PATCH 19/39] Remove exception stacktrace when starting without keypair --- bigchaindb/commands/bigchain.py | 6 +++++- bigchaindb/db/utils.py | 4 +++- docs/source/configuration.md | 20 +------------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index c460ae15..912e5ed7 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -10,7 +10,7 @@ import json import bigchaindb import bigchaindb.config_utils from bigchaindb import db -from bigchaindb.exceptions import DatabaseAlreadyExists +from bigchaindb.exceptions import DatabaseAlreadyExists, KeypairNotFoundException from bigchaindb.commands.utils import base_parser, start from bigchaindb.processes import Processes from bigchaindb import crypto @@ -101,6 +101,10 @@ def run_start(args): db.init() except DatabaseAlreadyExists: pass + except KeypairNotFoundException: + print('Cannot start BigchainDB, no keypair found. Did you run `bigchaindb configure`?') + exit(1) + processes = Processes() logger.info('Start bigchaindb main process') processes.start() diff --git a/bigchaindb/db/utils.py b/bigchaindb/db/utils.py index d5551833..b0a44d6a 100644 --- a/bigchaindb/db/utils.py +++ b/bigchaindb/db/utils.py @@ -19,6 +19,9 @@ def get_conn(): def init(): + # Try to access the keypair, throws an exception if it does not exist + b = bigchaindb.Bigchain() + conn = get_conn() dbname = bigchaindb.config['database']['name'] @@ -59,7 +62,6 @@ def init(): r.db(dbname).table('bigchain').index_wait().run(conn) logger.info(' - genesis block') - b = bigchaindb.Bigchain() b.create_genesis_block() logger.info('Done, have fun!') diff --git a/docs/source/configuration.md b/docs/source/configuration.md index 6a906b96..efb71cc4 100644 --- a/docs/source/configuration.md +++ b/docs/source/configuration.md @@ -110,25 +110,7 @@ If we try to run the node, the command will fail: ``` $ bigchaindb start WARNING:bigchaindb.config_utils:Cannot find config file `/home/vrde/.bigchaindb`. -INFO:bigchaindb.db.utils:Create: -INFO:bigchaindb.db.utils: - database `bigchain` -INFO:bigchaindb.db.utils: - tables -INFO:bigchaindb.db.utils: - indexes -INFO:bigchaindb.db.utils: - genesis block -Traceback (most recent call last): - File "/home/vrde/.local/share/virtualenvs/bigchain/bin/bigchaindb", line 9, in - load_entry_point('BigchainDB', 'console_scripts', 'bigchaindb')() - File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/bigchain.py", line 137, in main - start(parser, globals()) - File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/utils.py", line 41, in start - func(args) - File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/commands/bigchain.py", line 101, in run_start - db.init() - File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/db/utils.py", line 62, in init - b = bigchaindb.Bigchain() - File "/home/vrde/ascribe/repos/bigchaindb/bigchaindb/core.py", line 58, in __init__ - raise exceptions.KeypairNotFoundException() -bigchaindb.exceptions.KeypairNotFoundException +Cannot start BigchainDB, no keypair found. Did you run `bigchaindb configure`? ``` This is failing as expected: a BigchainDB node needs at least a key pair to work. From 9e7c55a08331629bea8c3acf6a9c009df19bbaea Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 11 Apr 2016 09:43:17 +0200 Subject: [PATCH 20/39] Add docs for config when using docker (#169) --- docs/source/installing-server.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/source/installing-server.md b/docs/source/installing-server.md index 4047f817..6b39c5a2 100644 --- a/docs/source/installing-server.md +++ b/docs/source/installing-server.md @@ -128,8 +128,22 @@ then do a one-time configuration step to create the config file; it will be stored on your host machine under ` ~/.bigchaindb_docker/config`: ```text $ docker-compose run --rm bigchaindb bigchaindb configure +Starting bigchaindb_rethinkdb-data_1 +Generating keypair +API Server bind? (default `0.0.0.0:9984`): +Database host? (default `localhost`): rethinkdb +Database port? (default `28015`): +Database name? (default `bigchain`): +Statsd host? (default `localhost`): statsd +Statsd port? (default `8125`): +Statsd rate? (default `0.01`): +Ready to go! ``` +As shown above, make sure that you set the database and statsd hosts to their +corresponding service names (`rethinkdb`, `statsd`), defined in`docker-compose.yml` +and `docker-compose-monitor.yml`. + You can then start it up (in the background, as a daemon) using: ```text $ docker-compose up -d From a59797c491fb6461ecd39bda8edf459cc03e7fd4 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 11:26:58 +0200 Subject: [PATCH 21/39] Add more docs to config --- docs/source/configuration.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/source/configuration.md b/docs/source/configuration.md index efb71cc4..64369384 100644 --- a/docs/source/configuration.md +++ b/docs/source/configuration.md @@ -54,22 +54,23 @@ those cases you can configure the system using environment variables. Every configuration key can be mapped to an environment variable. The environment variables available are: -``` -- BIGCHAINDB_DATABASE_HOST -- BIGCHAINDB_DATABASE_PORT -- BIGCHAINDB_DATABASE_NAME -- BIGCHAINDB_KEYPAIR_PUBLIC -- BIGCHAINDB_KEYPAIR_PRIVATE -- BIGCHAINDB_KEYRING -- BIGCHAINDB_STATSD_HOST -- BIGCHAINDB_STATSD_PORT -- BIGCHAINDB_STATSD_RATE -- BIGCHAINDB_API_ENDPOINT -- BIGCHAINDB_CONSENSUS_PLUGIN -- BIGCHAINDB_SERVER_BIND -- BIGCHAINDB_SERVER_WORKERS -- BIGCHAINDB_SERVER_THREADS -``` + +- `BIGCHAINDB_DATABASE_HOST` defines the database hostname to connect to. +- `BIGCHAINDB_DATABASE_PORT` defines the database port to connect to. +- `BIGCHAINDB_DATABASE_NAME` defines the database name to use. +- `BIGCHAINDB_KEYPAIR_PUBLIC` defines the public key of the BigchainDB node. +- `BIGCHAINDB_KEYPAIR_PRIVATE` defines the private key of the BigchainDB noce. +- `BIGCHAINDB_KEYRING` is a colon-separated list of public keys +- `BIGCHAINDB_STATSD_HOST` defines the host of the statsd server for [monitoring](monitoring.html). +- `BIGCHAINDB_STATSD_PORT` defines the port of the statsd server for monitoring. +- `BIGCHAINDB_STATSD_RATE` is a float between `0` and `1` that defines the fraction of transaction operations sampled. +- `BIGCHAINDB_API_ENDPOINT` defines the API endpoint to use. +- `BIGCHAINDB_CONSENSUS_PLUGIN` defines the name of the [consensus plugin](consensus.html) to use. +- `BIGCHAINDB_SERVER_BIND` defines where to bind the server socket, the format is `addr:port`. +- `BIGCHAINDB_SERVER_WORKERS defines the [number of workers](http://docs.gunicorn.org/en/stable/settings.html#workers) + to start for the server API. +- `BIGCHAINDB_SERVER_THREADS defines the [number of threads](http://docs.gunicorn.org/en/stable/settings.html#threads) + to start for the server API. As an example, let's assume we **don't** have any configuration file stored in the default location `$HOME/.bigchaindb`. From ab8113a91cb9040a10d73fd6789ac5268646cb5a Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 11:29:42 +0200 Subject: [PATCH 22/39] Use `sys.exit` instead of `exit` --- bigchaindb/commands/bigchain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 912e5ed7..15092db5 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -2,6 +2,7 @@ import os +import sys import logging import argparse import copy @@ -102,8 +103,7 @@ def run_start(args): except DatabaseAlreadyExists: pass except KeypairNotFoundException: - print('Cannot start BigchainDB, no keypair found. Did you run `bigchaindb configure`?') - exit(1) + sys.exit('Cannot start BigchainDB, no keypair found. Did you run `bigchaindb configure`?') processes = Processes() logger.info('Start bigchaindb main process') From bdc2cf21c47d48aff14734ff0c8a7f6c39a2e0c0 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 11:31:33 +0200 Subject: [PATCH 23/39] Add missing backtick --- docs/source/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration.md b/docs/source/configuration.md index 64369384..b9c3c724 100644 --- a/docs/source/configuration.md +++ b/docs/source/configuration.md @@ -67,9 +67,9 @@ environment variables available are: - `BIGCHAINDB_API_ENDPOINT` defines the API endpoint to use. - `BIGCHAINDB_CONSENSUS_PLUGIN` defines the name of the [consensus plugin](consensus.html) to use. - `BIGCHAINDB_SERVER_BIND` defines where to bind the server socket, the format is `addr:port`. -- `BIGCHAINDB_SERVER_WORKERS defines the [number of workers](http://docs.gunicorn.org/en/stable/settings.html#workers) +- `BIGCHAINDB_SERVER_WORKERS` defines the [number of workers](http://docs.gunicorn.org/en/stable/settings.html#workers) to start for the server API. -- `BIGCHAINDB_SERVER_THREADS defines the [number of threads](http://docs.gunicorn.org/en/stable/settings.html#threads) +- `BIGCHAINDB_SERVER_THREADS` defines the [number of threads](http://docs.gunicorn.org/en/stable/settings.html#threads) to start for the server API. As an example, let's assume we **don't** have any configuration file stored in From e09a2896494b4ca2ea1c1585eadb5ed96242cf52 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 11 Apr 2016 11:58:14 +0200 Subject: [PATCH 24/39] several AWS deployment improvements --- deploy-cluster-aws/create_rethinkdb_conf.py | 4 +- deploy-cluster-aws/fab_prepare_chain.py | 27 ----- deploy-cluster-aws/fabfile.py | 117 +++++++++++++------- deploy-cluster-aws/launch_ec2_nodes.py | 15 ++- deploy-cluster-aws/startup.sh | 34 +++--- 5 files changed, 107 insertions(+), 90 deletions(-) delete mode 100644 deploy-cluster-aws/fab_prepare_chain.py diff --git a/deploy-cluster-aws/create_rethinkdb_conf.py b/deploy-cluster-aws/create_rethinkdb_conf.py index 268dd710..4a11b462 100644 --- a/deploy-cluster-aws/create_rethinkdb_conf.py +++ b/deploy-cluster-aws/create_rethinkdb_conf.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals import os import os.path import shutil -from hostlist import hosts_dev +from hostlist import public_dns_names # cwd = current working directory old_cwd = os.getcwd() @@ -22,7 +22,7 @@ shutil.copy2('rethinkdb.conf.template', 'rethinkdb.conf') # Append additional lines to rethinkdb.conf with open('rethinkdb.conf', 'a') as f: f.write('## The host:port of a node that RethinkDB will connect to\n') - for public_dns_name in hosts_dev: + for public_dns_name in public_dns_names: f.write('join=' + public_dns_name + ':29015\n') os.chdir(old_cwd) diff --git a/deploy-cluster-aws/fab_prepare_chain.py b/deploy-cluster-aws/fab_prepare_chain.py deleted file mode 100644 index caa827ad..00000000 --- a/deploy-cluster-aws/fab_prepare_chain.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -""" Generating genesis block -""" - -from __future__ import with_statement, unicode_literals - -from fabric import colors as c -from fabric.api import * -from fabric.api import local, puts, settings, hide, abort, lcd, prefix -from fabric.api import run, sudo, cd, get, local, lcd, env, hide -from fabric.api import task, parallel -from fabric.contrib import files -from fabric.contrib.files import append, exists -from fabric.contrib.console import confirm -from fabric.contrib.project import rsync_project -from fabric.operations import run, put -from fabric.context_managers import settings -from fabric.decorators import roles -from fabtools import * - -env.user = 'ubuntu' -env.key_filename = 'pem/bigchaindb.pem' - -@task -def init_bigchaindb(): - run('bigchaindb -y start &', pty = False) diff --git a/deploy-cluster-aws/fabfile.py b/deploy-cluster-aws/fabfile.py index 22af0f04..5aba7a83 100644 --- a/deploy-cluster-aws/fabfile.py +++ b/deploy-cluster-aws/fabfile.py @@ -1,47 +1,58 @@ # -*- coding: utf-8 -*- - -"""A fabfile with functionality to prepare, install, and configure -bigchaindb, including its storage backend. +"""A Fabric fabfile with functionality to prepare, install, and configure +BigchainDB, including its storage backend (RethinkDB). """ from __future__ import with_statement, unicode_literals -import requests -from time import * -import os -from datetime import datetime, timedelta -import json -from pprint import pprint - -from fabric import colors as c -from fabric.api import * -from fabric.api import local, puts, settings, hide, abort, lcd, prefix -from fabric.api import run, sudo, cd, get, local, lcd, env, hide +from fabric.api import sudo, env from fabric.api import task, parallel -from fabric.contrib import files -from fabric.contrib.files import append, exists -from fabric.contrib.console import confirm -from fabric.contrib.project import rsync_project +from fabric.contrib.files import sed from fabric.operations import run, put from fabric.context_managers import settings -from fabric.decorators import roles -from fabtools import * -from hostlist import hosts_dev +from hostlist import public_dns_names -env.hosts = hosts_dev -env.roledefs = { - "role1": hosts_dev, - "role2": [hosts_dev[0]], - } -env.roles = ["role1"] +# Ignore known_hosts +# http://docs.fabfile.org/en/1.10/usage/env.html#disable-known-hosts +env.disable_known_hosts = True + +# What remote servers should Fabric connect to? With what usernames? env.user = 'ubuntu' +env.hosts = public_dns_names + +# SSH key files to try when connecting: +# http://docs.fabfile.org/en/1.10/usage/env.html#key-filename env.key_filename = 'pem/bigchaindb.pem' +newrelic_license_key = 'you_need_a_real_license_key' + ###################################################################### -# base software rollout +# DON'T PUT @parallel +@task +def set_hosts(hosts): + """A helper function to change env.hosts from the + command line. + + Args: + hosts (str): 'one_node' or 'two_nodes' + + Example: + fab set_hosts:one_node init_bigchaindb + """ + if hosts == 'one_node': + env.hosts = public_dns_names[0] + elif hosts == 'two_nodes': + env.hosts = public_dns_names[0:1] + else: + raise ValueError('Invalid input to set_hosts.' + ' Expected one_node or two_nodes.' + ' Got {}'.format(hosts)) + + +# Install base software @task @parallel def install_base_software(): @@ -59,7 +70,7 @@ def install_base_software(): python3-pip ipython3 sysstat s3cmd') -# RethinkDB +# Install RethinkDB @task @parallel def install_rethinkdb(): @@ -67,7 +78,7 @@ def install_rethinkdb(): with settings(warn_only=True): # preparing filesystem sudo("mkdir -p /data") - # Locally mounted storage (m3.2xlarge, aber auch c3.xxx) + # Locally mounted storage (m3.2xlarge, but also c3.xxx) try: sudo("umount /mnt") sudo("mkfs -t ext4 /dev/xvdb") @@ -91,27 +102,48 @@ def install_rethinkdb(): sudo('chown -R rethinkdb:rethinkdb /data') # copy config file to target system put('conf/rethinkdb.conf', - '/etc/rethinkdb/instances.d/instance1.conf', mode=0600, use_sudo=True) + '/etc/rethinkdb/instances.d/instance1.conf', + mode=0600, + use_sudo=True) # initialize data-dir sudo('rm -rf /data/*') # finally restart instance sudo('/etc/init.d/rethinkdb restart') -# bigchaindb deployment +# Install BigchainDB (from PyPI) @task @parallel def install_bigchaindb(): sudo('python3 -m pip install bigchaindb') -# startup all nodes of bigchaindb in cluster +# Configure BigchainDB @task @parallel -def start_bigchaindb_nodes(): +def configure_bigchaindb(): + run('bigchaindb -y configure', pty=False) + + +# Initialize BigchainDB +# i.e. create the database, the tables, +# the indexes, and the genesis block. +# (This only needs to be run on one node.) +# Call using: +# fab set_hosts:one_node init_bigchaindb +@task +def init_bigchaindb(): + run('bigchaindb init', pty=False) + + +# Start BigchainDB using screen +@task +@parallel +def start_bigchaindb(): sudo('screen -d -m bigchaindb -y start &', pty=False) +# Install and run New Relic @task def install_newrelic(): with settings(warn_only=True): @@ -119,18 +151,18 @@ def install_newrelic(): # sudo('apt-key adv --keyserver hkp://subkeys.pgp.net --recv-keys 548C16BF') sudo('apt-get update') sudo('apt-get -y --force-yes install newrelic-sysmond') - sudo('nrsysmond-config --set license_key=c88af00c813983f8ee12e9b455aa13fde1cddaa8') + sudo('nrsysmond-config --set license_key=' + newrelic_license_key) sudo('/etc/init.d/newrelic-sysmond restart') -############################### -# Security / FirewallStuff next -############################### +########################### +# Security / Firewall Stuff +########################### @task def harden_sshd(): - """Security harden sshd.""" - + """Security harden sshd. + """ # Disable password authentication sed('/etc/ssh/sshd_config', '#PasswordAuthentication yes', @@ -147,7 +179,8 @@ def harden_sshd(): def disable_root_login(): """Disable `root` login for even more security. Access to `root` account is now possible by first connecting with your dedicated maintenance - account and then running ``sudo su -``.""" + account and then running ``sudo su -``. + """ sudo('passwd --lock root') @@ -172,7 +205,7 @@ def set_fw(): ######################################################### -# some helper-functions to handle bad behavior of cluster +# Some helper-functions to handle bad behavior of cluster ######################################################### # rebuild indexes diff --git a/deploy-cluster-aws/launch_ec2_nodes.py b/deploy-cluster-aws/launch_ec2_nodes.py index c531821b..9ebf3026 100644 --- a/deploy-cluster-aws/launch_ec2_nodes.py +++ b/deploy-cluster-aws/launch_ec2_nodes.py @@ -166,26 +166,31 @@ for i, instance in enumerate(instances_with_tag): format(instance.instance_id)) # Get a list of the pubic DNS names of the instances_with_tag -hosts_dev = [] +public_dns_names = [] for instance in instances_with_tag: public_dns_name = getattr(instance, 'public_dns_name', None) if public_dns_name is not None: - hosts_dev.append(public_dns_name) + public_dns_names.append(public_dns_name) # Write a shellscript to add remote keys to ~/.ssh/known_hosts print('Preparing shellscript to add remote keys to known_hosts') with open('add2known_hosts.sh', 'w') as f: f.write('#!/bin/bash\n') - for public_dns_name in hosts_dev: + for public_dns_name in public_dns_names: f.write('ssh-keyscan ' + public_dns_name + ' >> ~/.ssh/known_hosts\n') -# Create a file named hostlist.py containing hosts_dev. +# Create a file named hostlist.py containing public_dns_names. # If a hostlist.py already exists, it will be overwritten. print('Writing hostlist.py') with open('hostlist.py', 'w') as f: f.write('# -*- coding: utf-8 -*-\n') + f.write('"""A list of the public DNS names of all the nodes in this\n') + f.write('BigchainDB cluster/federation.\n') + f.write('"""\n') + f.write('\n') f.write('from __future__ import unicode_literals\n') - f.write('hosts_dev = {}\n'.format(hosts_dev)) + f.write('\n') + f.write('public_dns_names = {}\n'.format(public_dns_names)) # Wait wait_time = 45 diff --git a/deploy-cluster-aws/startup.sh b/deploy-cluster-aws/startup.sh index 84d420cb..9a73f7f8 100755 --- a/deploy-cluster-aws/startup.sh +++ b/deploy-cluster-aws/startup.sh @@ -55,27 +55,33 @@ chmod +x add2known_hosts.sh # (Re)create the RethinkDB configuration file conf/rethinkdb.conf python create_rethinkdb_conf.py -# rollout base packages (dependencies) needed before -# storage backend (rethinkdb) and bigchaindb can be rolled out +# Rollout base packages (dependencies) needed before +# storage backend (RethinkDB) and BigchainDB can be rolled out fab install_base_software -# rollout storage backend (rethinkdb) +# Rollout storage backend (RethinkDB) and start it fab install_rethinkdb -# rollout bigchaindb +# Rollout BigchainDB (but don't start it yet) fab install_bigchaindb -# generate genesis block -# HORST is the last public_dns_name listed in conf/rethinkdb.conf -# For example: -# ec2-52-58-86-145.eu-central-1.compute.amazonaws.com -HORST=`tail -1 conf/rethinkdb.conf|cut -d: -f1|cut -d= -f2` -fab -H $HORST -f fab_prepare_chain.py init_bigchaindb +# Configure BigchainDB on all nodes +fab configure_bigchaindb -# initiate sharding -fab start_bigchaindb_nodes +# TODO Get public keys from all nodes +# using e.g. bigchaindb export-pubkey + +# TODO Add list of public keys to keyring of all nodes +# using e.g. bigchaindb import-pubkey + +# Send a "bigchaindb init" command to one node +# to initialize the BigchainDB database +# i.e. create the database, the tables, +# the indexes, and the genesis block. +fab set_hosts:one_node init_bigchaindb + +# Start BigchainDB on all the nodes using "screen" +fab start_bigchaindb # cleanup rm add2known_hosts.sh - -# DONE From fb6ba306580dc368614d5a3500c300eff0639dc5 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 14:47:16 +0200 Subject: [PATCH 25/39] Move refs from develop to master --- CONTRIBUTING.md | 19 ++++++++----------- PYTHON_STYLE_GUIDE.md | 2 +- README.md | 16 ++++++++-------- codecov.yml | 4 ++-- docs/source/introduction.md | 2 +- docs/source/licenses.md | 2 +- docs/source/release-notes.md | 4 ++-- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index acd281ca..c5c0ccbf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,11 +23,8 @@ Familiarize yourself with how we do coding and documentation in the BigchainDB p * the GitHub Flow (workflow) * [GitHub Guide: Understanding the GitHub Flow](https://guides.github.com/introduction/flow/) * [Scott Chacon's blog post about GitHub Flow](http://scottchacon.com/2011/08/31/github-flow.html) - * Note that we call the main branch `develop` rather than `master` * [semantic versioning](http://semver.org/) -Note: We have a slight variation on the GitHub Flow: we call the default branch `develop` rather than `master`. - ### Step 1 - Fork bigchaindb on GitHub In your web browser, go to [the BigchainDB repository on GitHub](https://github.com/bigchaindb/bigchaindb) and click the `Fork` button in the top right corner. This creates a new Git repository named `bigchaindb` in _your_ GitHub account. @@ -41,13 +38,13 @@ cd bigchaindb git add upstream git@github.com:bigchaindb/bigchaindb.git ``` -### Step 3 - Fetch and Merge the Latest from `upstream/develop` +### Step 3 - Fetch and Merge the Latest from `upstream/master` -Switch to the `develop` branch locally, fetch all `upstream` branches, and merge the just-fetched `upstream/develop` branch with the local `develop` branch: +Switch to the `master` branch locally, fetch all `upstream` branches, and merge the just-fetched `upstream/master` branch with the local `master` branch: ```text -git checkout develop +git checkout master git fetch upstream -git merge upstream/develop +git merge upstream/master ``` ### Step 4 - Create a New Branch for Each Bug/Feature @@ -79,12 +76,12 @@ git commit -m "Short description of new or changed things" You will want to merge changes from upstream (i.e. the original repository) into your new branch from time to time, using something like: ```text git fetch upstream -git merge upstream/develop +git merge upstream/master ``` Once you're done commiting a set of new things and you're ready to submit them for inclusion, please be sure to run all the tests (as per the instructions at the end of our [Python Style Guide](PYTHON_STYLE_GUIDE.md)). -If your addition or change is substantial, then please add a line or two to the [CHANGELOG.md file](https://github.com/bigchaindb/bigchaindb/blob/develop/CHANGELOG.md), following the guidelines given at the top of that file. +If your addition or change is substantial, then please add a line or two to the [CHANGELOG.md file](https://github.com/bigchaindb/bigchaindb/blob/master/CHANGELOG.md), following the guidelines given at the top of that file. (When you submit your pull request [following the instructions below], we run all the tests automatically, so we will see if some are failing. If you don't know why some tests are failing, you can still submit your pull request, but be sure to note the failing tests and to ask for help with resolving them.) @@ -99,7 +96,7 @@ git push origin new-branch-name Go to the GitHub website and to _your_ remote bigchaindb repository (i.e. something like https://github.com/your-user-name/bigchaindb). -See [GitHub's documentation on how to initiate and send a pull request](https://help.github.com/articles/using-pull-requests/). Note that the destination repository should be `bigchaindb/bigchaindb` and the destination branch will be `develop` (usually, and if it's not, then we can change that if necessary). +See [GitHub's documentation on how to initiate and send a pull request](https://help.github.com/articles/using-pull-requests/). Note that the destination repository should be `bigchaindb/bigchaindb` and the destination branch will be `master` (usually, and if it's not, then we can change that if necessary). If this is the first time you've submitted a pull request to BigchainDB, then you must read and accept the Contributor License Agreement (CLA) before we can merge your contributions. That can be found at [https://www.bigchaindb.com/cla](https://www.bigchaindb.com/cla). @@ -115,4 +112,4 @@ Someone will then merge your branch or suggest changes. If we suggsest changes, * [BigchainDB Licenses](./LICENSES.md) * [Contributor License Agreement](https://www.bigchaindb.com/cla) -(Note: GitHub automatically links to CONTRIBUTING.md when a contributor creates an Issue or opens a Pull Request.) \ No newline at end of file +(Note: GitHub automatically links to CONTRIBUTING.md when a contributor creates an Issue or opens a Pull Request.) diff --git a/PYTHON_STYLE_GUIDE.md b/PYTHON_STYLE_GUIDE.md index 4d4d4888..536ed321 100644 --- a/PYTHON_STYLE_GUIDE.md +++ b/PYTHON_STYLE_GUIDE.md @@ -61,6 +61,6 @@ We write unit tests for our Python code using the [pytest](http://pytest.org/lat All tests go in the `bigchaindb/tests` directory or one of its subdirectories. You can use the tests already in there as templates or examples. -The BigchainDB Documentation has a [section explaining how to run all unit tests](http://bigchaindb.readthedocs.org/en/develop/running-unit-tests.html). +The BigchainDB Documentation has a [section explaining how to run all unit tests](http://bigchaindb.readthedocs.org/en/master/running-unit-tests.html). **Automated testing of pull requests.** We use [Travis CI](https://travis-ci.com/), so that whenever someone creates a new BigchainDB pull request on GitHub, Travis CI gets the new code and does _a bunch of stuff_. You can find out what we tell Travis CI to do in [the `.travis.yml` file](.travis.yml): it tells Travis CI how to install BigchainDB, how to run all the tests, and what to do "after success" (e.g. run `codecov`). (We use [Codecov](https://codecov.io/) to get a rough estimate of our test coverage.) diff --git a/README.md b/README.md index e72a55a8..3577a02c 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit [![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) -[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/develop.svg)](https://travis-ci.org/bigchaindb/bigchaindb) -[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/develop.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=develop) -[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=develop)](http://bigchaindb.readthedocs.org/en/develop/?badge=develop) +[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) +[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=master)](http://bigchaindb.readthedocs.org/en/master/?badge=master) ## Quick Start -### [Install and Run BigchainDB Server](http://bigchaindb.readthedocs.org/en/develop/installing-server.html) -### [Run BigchainDB with Docker](http://bigchaindb.readthedocs.org/en/develop/installing-server.html#run-bigchaindb-with-docker) -### [The Python Server API by Example](http://bigchaindb.readthedocs.org/en/develop/python-server-api-examples.html) -### [The Python Driver API by Example](http://bigchaindb.readthedocs.org/en/develop/python-driver-api-examples.html) +### [Install and Run BigchainDB Server](http://bigchaindb.readthedocs.org/en/master/installing-server.html) +### [Run BigchainDB with Docker](http://bigchaindb.readthedocs.org/en/master/installing-server.html#run-bigchaindb-with-docker) +### [The Python Server API by Example](http://bigchaindb.readthedocs.org/en/master/python-server-api-examples.html) +### [The Python Driver API by Example](http://bigchaindb.readthedocs.org/en/master/python-driver-api-examples.html) ## Links for Everyone * [BigchainDB.com](https://www.bigchaindb.com/) - the main BigchainDB website, including newsletter signup @@ -24,7 +24,7 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit * [Google Group](https://groups.google.com/forum/#!forum/bigchaindb) ## Links for Developers -* [Documentation](http://bigchaindb.readthedocs.org/en/develop/#) - for developers +* [Documentation](http://bigchaindb.readthedocs.org/en/master/) - for developers * [CONTRIBUTING.md](CONTRIBUTING.md) - how to contribute * [Community guidelines](CODE_OF_CONDUCT.md) * [Open issues](https://github.com/bigchaindb/bigchaindb/issues) diff --git a/codecov.yml b/codecov.yml index 490d0805..27507adb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,5 +1,5 @@ codecov: - branch: develop # the branch to show by default + branch: master # the branch to show by default # The help text for bot says: # "the username that will consume any oauth requests @@ -29,4 +29,4 @@ coverage: comment: layout: "header, diff, changes, sunburst, suggestions" - behavior: default \ No newline at end of file + behavior: default diff --git a/docs/source/introduction.md b/docs/source/introduction.md index 3c1230ad..bb7f4e70 100644 --- a/docs/source/introduction.md +++ b/docs/source/introduction.md @@ -7,4 +7,4 @@ BigchainDB is a scalable blockchain database. You can read about its motivations 3. Developers of BigchainDB driver software (SDKs used to develop client software). 4. App developers who are developing client apps to talk to one or more live, operational BigchainDB clusters. They would use one of the BigchainDB drivers. -If you're curious about what's in our roadmap, see [the ROADMAP.md file](https://github.com/bigchaindb/bigchaindb/blob/develop/ROADMAP.md) and [the list of open issues](https://github.com/bigchaindb/bigchaindb/issues). If you want to request a feature, file a bug report, or make a pull request, see [the CONTRIBUTING.md file](https://github.com/bigchaindb/bigchaindb/blob/develop/CONTRIBUTING.md). +If you're curious about what's in our roadmap, see [the ROADMAP.md file](https://github.com/bigchaindb/bigchaindb/blob/master/ROADMAP.md) and [the list of open issues](https://github.com/bigchaindb/bigchaindb/issues). If you want to request a feature, file a bug report, or make a pull request, see [the CONTRIBUTING.md file](https://github.com/bigchaindb/bigchaindb/blob/master/CONTRIBUTING.md). diff --git a/docs/source/licenses.md b/docs/source/licenses.md index aa60e2bc..97699c56 100644 --- a/docs/source/licenses.md +++ b/docs/source/licenses.md @@ -1,3 +1,3 @@ # Licenses -Information about how the BigchainDB code and documentation are licensed can be found in [the LICENSES.md file](https://github.com/bigchaindb/bigchaindb/blob/develop/LICENSES.md) (in the root directory of the repository). \ No newline at end of file +Information about how the BigchainDB code and documentation are licensed can be found in [the LICENSES.md file](https://github.com/bigchaindb/bigchaindb/blob/master/LICENSES.md) (in the root directory of the repository). diff --git a/docs/source/release-notes.md b/docs/source/release-notes.md index 1a80c120..5b4f1e88 100644 --- a/docs/source/release-notes.md +++ b/docs/source/release-notes.md @@ -4,6 +4,6 @@ You can find a list of all BigchainDB releases and release notes on GitHub at: [https://github.com/bigchaindb/bigchaindb/releases](https://github.com/bigchaindb/bigchaindb/releases) -The [CHANGELOG.md file](https://github.com/bigchaindb/bigchaindb/blob/develop/CHANGELOG.md) contains much the same information, but it also has notes about what to expect in the _next_ release. +The [CHANGELOG.md file](https://github.com/bigchaindb/bigchaindb/blob/master/CHANGELOG.md) contains much the same information, but it also has notes about what to expect in the _next_ release. -We also have [a roadmap document in ROADMAP.md](https://github.com/bigchaindb/bigchaindb/blob/develop/ROADMAP.md). +We also have [a roadmap document in ROADMAP.md](https://github.com/bigchaindb/bigchaindb/blob/master/ROADMAP.md). From ff2e36a44e1bdf657ac6cb22ef666012522e33d7 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 11 Apr 2016 14:51:26 +0200 Subject: [PATCH 26/39] Edits to configuration.md and related docs --- docs/source/configuration.md | 202 +++++++++++----------- docs/source/index.rst | 1 + docs/source/installing-server.md | 11 +- docs/source/python-server-api-examples.md | 2 + 4 files changed, 110 insertions(+), 106 deletions(-) diff --git a/docs/source/configuration.md b/docs/source/configuration.md index b9c3c724..b5d538e1 100644 --- a/docs/source/configuration.md +++ b/docs/source/configuration.md @@ -1,20 +1,21 @@ # Configuring a BigchainDB Node -The standard way to configure a BigchainDB node is to run the command `configure`: +The BigchainDB configuration settings for a particular node are stored on that node in a configuration file at `$HOME/.bigchaindb`. That file doesn't exist by default. (It's not created when installing BigchainDB.) One could create it using a text editor, but it's easiest to use the `bigchaindb configure` command: ```text $ bigchaindb configure ``` -This command will generate a new keypair and will guide you through the -configuration of the system. By default keypair and settings will be saved in the -`$HOME/.bigchaindb` file. +It will ask some questions and generate a new keypair (i.e. a private key and corresponding public key for the node). See below for some additional explanation of the settings and their meanings. To accept a suggested default value, press Enter or Return. If you want to accept all the default values, use the `-y` option when running the command, that is: +```text +$ bigchaindb -y configure +``` -## Using a different path for the configuration +## Using a Different Path for the Configuration File -By default the configuration is stored in `$HOME/.bigchaindb`, if you want to -specify a different path for your configuration you can use the `-c` parameter. +By default, the configuration settings are stored in `$HOME/.bigchaindb`. If you want to +specify a different path for your configuration file, you can use the `-c` parameter. This works for every subcommand under the `bigchaindb` executable. For example, if you want to **generate** a new configuration file under a @@ -25,57 +26,126 @@ $ bigchaindb -c local.json configure $ bigchaindb -c test.json configure ``` -This will create two new files called `local.json` and `test.json` in your +This will create two new files named `local.json` and `test.json` in your current working directory. From now on, you can refer to those configuration files using the `-c` -parameter, for example: +parameter; for example: ``` $ bigchaindb -c local.json show-config ``` -Will show the configuration for `local.json`. +will show the configuration for `local.json`. -If you want to **start** BigchainDB with the `test.json` configuration, you can -try: +If you want to **start** BigchainDB with the `test.json` configuration file, you can use: ``` $ bigchaindb -c test.json start ``` -## Using environ variables to configure the node +## Using Environment Variables to Configure the Node Sometimes it's more convenient to use environment variables to configure the -system, for example when using Docker or Heroku. Another use case is to have a -*volatile*, throw away configuration you need to test something quickly. In -those cases you can configure the system using environment variables. +system, for example when using Docker or Heroku. In +that case you can configure the system using environment variables. -Every configuration key can be mapped to an environment variable. The +Every configuration parameter can be mapped to an environment variable. The environment variables available are: -- `BIGCHAINDB_DATABASE_HOST` defines the database hostname to connect to. -- `BIGCHAINDB_DATABASE_PORT` defines the database port to connect to. -- `BIGCHAINDB_DATABASE_NAME` defines the database name to use. +- `BIGCHAINDB_DATABASE_HOST` defines the RethinkDB database hostname to connect to. +- `BIGCHAINDB_DATABASE_PORT` defines the RethinkDB database port to connect to. +- `BIGCHAINDB_DATABASE_NAME` defines the RethinkDB database name to use. - `BIGCHAINDB_KEYPAIR_PUBLIC` defines the public key of the BigchainDB node. -- `BIGCHAINDB_KEYPAIR_PRIVATE` defines the private key of the BigchainDB noce. -- `BIGCHAINDB_KEYRING` is a colon-separated list of public keys -- `BIGCHAINDB_STATSD_HOST` defines the host of the statsd server for [monitoring](monitoring.html). +- `BIGCHAINDB_KEYPAIR_PRIVATE` defines the private key of the BigchainDB node. +- `BIGCHAINDB_KEYRING` is a colon-separated list of the public keys of all _other_ nodes in the cluster. +- `BIGCHAINDB_STATSD_HOST` defines the hostname of the statsd server for [monitoring](monitoring.html). - `BIGCHAINDB_STATSD_PORT` defines the port of the statsd server for monitoring. - `BIGCHAINDB_STATSD_RATE` is a float between `0` and `1` that defines the fraction of transaction operations sampled. -- `BIGCHAINDB_API_ENDPOINT` defines the API endpoint to use. +- `BIGCHAINDB_API_ENDPOINT` defines the API endpoint to use (e.g. `http://localhost:9984/api/v1`). - `BIGCHAINDB_CONSENSUS_PLUGIN` defines the name of the [consensus plugin](consensus.html) to use. -- `BIGCHAINDB_SERVER_BIND` defines where to bind the server socket, the format is `addr:port`. +- `BIGCHAINDB_SERVER_BIND` defines where to bind the server socket, the format is `addr:port` (e.g. `0.0.0.0:9984`). - `BIGCHAINDB_SERVER_WORKERS` defines the [number of workers](http://docs.gunicorn.org/en/stable/settings.html#workers) to start for the server API. - `BIGCHAINDB_SERVER_THREADS` defines the [number of threads](http://docs.gunicorn.org/en/stable/settings.html#threads) to start for the server API. -As an example, let's assume we **don't** have any configuration file stored in -the default location `$HOME/.bigchaindb`. -As you can see, `show-config` displays the default configuration (and a +## Order of Precedence in Determining Configuration Values + +All configuration values start with their default values (defined in `bigchaindb.__init__`), but a default value can be overriden by an environment variable, and a value set by an environment variable can be overriden by a value in a local configuration file (`$HOME/.bigchaindb` or the location specified by the `-c` command-line option). + +In summary, there is an order of precedence in reading configuration values: +1. local configuration file +2. environment variables +3. default configuration file (defined in ``bigchaindb.__init__``) + +This means that if the default configuration contains: + +```json +{ + "database": { + "host": "localhost", + "port": 28015 + } +} +``` + +while the local file `local.json` contains: +```json +{ + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com" + } +} + +``` + +and you run this command: +``` +$ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ + BIGCHAINDB_DATABASE_PORT=4242 \ + BIGCHAINDB_KEYRING=pubkey0:pubkey1 \ + bigchaindb -c local.json show-config +``` + +you will get the following values for all the configuration settings: +```json +{ + "api_endpoint": "http://localhost:8008/api/v1", + "consensus_plugin": "default", + "database": { + "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com", + "name": "bigchain", + "port": 4242 + }, + "keypair": { + "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "public": "nJq6EmdUkvFjQRB5hFvDmvZtv1deb3W3RgmiAq6dyygC" + }, + "keyring": [ + "pubkey0", + "pubkey1" + ], + "server": { + "bind": "0.0.0.0:9984", + "threads": null, + "workers": null + }, + "statsd": { + "host": "localhost", + "port": 8125, + "rate": 0.01 + } +} +``` + + +## Another Example + +As another example, let's assume we **don't** have any configuration file stored in +`$HOME/.bigchaindb`. As you can see, `show-config` displays the default configuration (and a warning): ``` $ bigchaindb show-config @@ -122,7 +192,7 @@ $ BIGCHAINDB_KEYPAIR_PUBLIC=26y9EuyGP44JXxqcvF8GbCJGqkiqFXddZzxVjLU3rWbHp \ bigchaindb start ``` -We can also run `show-config` to see how the configuration looks like: +We can also run `show-config` to see how the configuration looks: ``` $ BIGCHAINDB_KEYPAIR_PUBLIC=26y9EuyGP44JXxqcvF8GbCJGqkiqFXddZzxVjLU3rWbHp \ BIGCHAINDB_KEYPAIR_PRIVATE=9PkLfHbzXnSSNnb1sSBL73C2MydzKLs5fAHoA4Q7otrG \ @@ -154,77 +224,3 @@ WARNING:bigchaindb.config_utils:Cannot find config file `/home/vrde/.bigchaindb` } } ``` - - -# Precedence in reading configuration values - -Note that there is a precedence in reading configuration values: - - local config file; - - environment vars; - - default config file (contained in ``bigchaindb.__init__``). - -This means that if the default configuration contains an entry that is: - -```json -{ - - "database": { - "host": "localhost", - "port": 28015 - } - -} -``` - -while your local file `local.json` contains: -```json -{ - "database": { - "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com" - } -} - -``` - -and you run this command: -``` -$ BIGCHAINDB_DATABASE_HOST=anotherhost.com \ - BIGCHAINDB_DATABASE_PORT=4242 \ - BIGCHAINDB_KEYRING=pubkey0:pubkey1 \ - bigchaindb -c local.json show-config -``` - -you will get: -```json -{ - "api_endpoint": "http://localhost:8008/api/v1", - "consensus_plugin": "default", - "database": { - "host": "ec2-xx-xx-xxx-xxx.eu-central-1.compute.amazonaws.com", - "name": "bigchain", - "port": 4242 - }, - "keypair": { - "private": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "public": "nJq6EmdUkvFjQRB5hFvDmvZtv1deb3W3RgmiAq6dyygC" - }, - "keyring": [ - "pubkey0", - "pubkey1" - ], - "server": { - "bind": "0.0.0.0:9984", - "threads": null, - "workers": null - }, - "statsd": { - "host": "localhost", - "port": 8125, - "rate": 0.01 - } -} -``` - -Note that the type of `keyring` is a list. If you want to pass a list as an -environ variable you need to use colon (`:`) as separator. - diff --git a/docs/source/index.rst b/docs/source/index.rst index 8a75833f..ec10c228 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,6 +15,7 @@ Table of Contents introduction installing-server running-unit-tests + configuration python-server-api-examples bigchaindb-cli http-client-server-api diff --git a/docs/source/installing-server.md b/docs/source/installing-server.md index 6b39c5a2..7da2b1c4 100644 --- a/docs/source/installing-server.md +++ b/docs/source/installing-server.md @@ -80,6 +80,11 @@ Install from the source: $ python setup.py install ``` +If you want to update BigchainDB to reflect the latest local source code changes, you can use: +```text +$ pip install -e . +``` + ### How to Install BigchainDB on a VM with Vagrant One of our community members ([@Mec-Is](https://github.com/Mec-iS)) wrote [a page about how to install BigchainDB on a VM with Vagrant](https://gist.github.com/Mec-iS/b84758397f1b21f21700). @@ -94,13 +99,13 @@ $ rethinkdb Then open a different terminal and run: ```text +$ bigchaindb -y configure $ bigchaindb init ``` -During its first run, the command takes care of configuring a single node environment. +That creates a configuration file in `$HOME/.bigchaindb` (documented in [the section on configuration](configuration.html)), initializes the database, creates the tables, creates the indexes, and generates the genesis block. - -After configuring the node, you can start it with: +You can start BigchainDB Server using: ```text $ bigchaindb start ``` diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 25fc5b19..2b2bfd86 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -11,6 +11,8 @@ We create a digital asset, sign it, write it to a BigchainDB Server instance, re First, make sure you have RethinkDB and BigchainDB _installed and running_, i.e. you [installed them](installing-server.html) and you ran: ```text $ rethinkdb +$ bigchaindb configure +$ bigchaindb init $ bigchaindb start ``` From 4db0c07ea1269014a67bcd3a901ad90289432713 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 15:50:30 +0200 Subject: [PATCH 27/39] Move badges to top --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3577a02c..a1495584 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# BigchainDB - -A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whitepaper/) explains what that means. - [![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) [![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) [![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) [![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=master)](http://bigchaindb.readthedocs.org/en/master/?badge=master) +# BigchainDB + +A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whitepaper/) explains what that means. + + ## Quick Start ### [Install and Run BigchainDB Server](http://bigchaindb.readthedocs.org/en/master/installing-server.html) From c73ed7974f10ddde61a246cde05ac2346c0b3e03 Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 16:16:36 +0200 Subject: [PATCH 28/39] Fix badge for RTD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3577a02c..783ed9ad 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) [![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) [![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) -[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=master)](http://bigchaindb.readthedocs.org/en/master/?badge=master) +[![Documentation Status](http://bigchaindb.readthedocs.org/en/stable/?badge=stable)](http://bigchaindb.readthedocs.org/en/stable/?badge=stable) ## Quick Start From 708d10e166f800daddd73b3d6c9fe08b45fcb20c Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 11 Apr 2016 16:19:20 +0200 Subject: [PATCH 29/39] Fix badge for RTD --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 783ed9ad..2d4ebed8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) [![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) [![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) -[![Documentation Status](http://bigchaindb.readthedocs.org/en/stable/?badge=stable)](http://bigchaindb.readthedocs.org/en/stable/?badge=stable) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=stable)](https://bigchaindb.readthedocs.org/en/stable/) + ## Quick Start From 61c0da8212af3588f29de264bb71326a069d43d9 Mon Sep 17 00:00:00 2001 From: diminator Date: Tue, 12 Apr 2016 13:20:34 +0200 Subject: [PATCH 31/39] docs with code examples --- docs/source/python-server-api-examples.md | 303 ++++++++++++++++-- tests/doc/__init__.py | 0 .../doc/run_doc_python_server_api_examples.py | 133 ++++++++ 3 files changed, 415 insertions(+), 21 deletions(-) create mode 100644 tests/doc/__init__.py create mode 100644 tests/doc/run_doc_python_server_api_examples.py diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 2b2bfd86..85a3809e 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -69,17 +69,52 @@ After a couple of seconds, we can check if the transactions was included in the # retrieve a transaction from the bigchain tx_retrieved = b.get_transaction(tx_signed['id']) - 'id': '6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd', - 'transaction': { 'current_owner': 'pvGtcm5dvwWMzCqagki1N6CDKYs2J1cCwTNw8CqJic3Q', - 'data': { 'hash': '872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8', - 'payload': {'msg': 'Hello BigchainDB!'}}, - 'input': None, - 'new_owner': 'ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA', - 'operation': 'CREATE', - 'timestamp': '1455108421.753908'}} +{ + "id": "cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2", + "transaction": { + "conditions": [ + { + "cid": 0, + "condition": { + "details": { + "bitmask": 32, + "public_key": "DTJCqP3sNkZcpoSA8bCtGwZ4ASfRLsMFXZDCmMHzCoeJ", + "signature": null, + "type": "fulfillment", + "type_id": 4 + }, + "uri": "cc:1:20:uQjL_E_uT1yUsJpVi1X7x2G7B15urzIlKN5fUufehTM:98" + }, + "new_owners": [ + "DTJCqP3sNkZcpoSA8bCtGwZ4ASfRLsMFXZDCmMHzCoeJ" + ] + } + ], + "data": { + "hash": "872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8", + "payload": { + "msg": "Hello BigchainDB!" + } + }, + "fulfillments": [ + { + "current_owners": [ + "3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9" + ], + "fid": 0, + "fulfillment": "cf:1:4:ICKvgXHM8K2jNlKRfkwz3cCvH0OiF5A_-riWsQWXffOMQCyqbFgSDfKTaKRQHypHr5z5jsXzCQ4dKgYkmUo55CMxYs3TT2OxGiV0bZ7Tzn1lcLhpyutGZWm8xIyJKJmmSQQ", + "input": null + } + ], + "operation": "CREATE", + "timestamp": "1460450439.267737" + }, + "version": 1 +} + ``` -The new owner of the digital asset is now `ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA`, which is the public key of `testuser1`. +The new owner of the digital asset is now `ACJyBLfeLNpCPrGWYoPYvnQ2MAC8BFukBko4hxtW9YoH`, which is the public key of `testuser1`. ## Transfer the Digital Asset @@ -87,10 +122,16 @@ Now that `testuser1` has a digital asset assigned to him, he can transfer it to ```python # create a second testuser -testuser2_priv, testuser2_pub = b.generate_keys() +testuser2_priv, testuser2_pub = crypto.generate_key_pair() + +# retrieve the transaction with condition id +tx_retrieved_id = b.get_owned_ids(testuser1_pub).pop() + {'cid': 0, + 'txid': 'cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2'} + # create a transfer transaction -tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved['id'], 'TRANSFER') +tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER') # sign the transaction tx_transfer_signed = b.sign_transaction(tx_transfer, testuser1_priv) @@ -101,14 +142,47 @@ b.write_transaction(tx_transfer_signed) # check if the transaction is already in the bigchain tx_transfer_retrieved = b.get_transaction(tx_transfer_signed['id']) -{ 'id': '1b78c313257540189f27da480152ed8c0b758569cdadd123d9810c057da408c3', - 'signature': '3045022056166de447001db8ef024cfa1eecdba4306f92688920ac24325729d5a5068d47022100fbd495077cb1040c48bd7dc050b2515b296ca215cb5ce3369f094928e31955f6', - 'transaction': { 'current_owner': 'ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA', - 'data': None, - 'input': '6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd', - 'new_owner': 'zVzophT73m4Wvf3f8gFYokddkYe3b9PbaMzobiUK7fmP', - 'operation': 'TRANSFER', - 'timestamp': '1455109497.480323'}} +{ + "id": "86ce10d653c69acf422a6d017a4ccd27168cdcdac99a49e4a38fb5e0d280c579", + "transaction": { + "conditions": [ + { + "cid": 0, + "condition": { + "details": { + "bitmask": 32, + "public_key": "7MUjLUFEu12Hk5jb8BZEFgM5JWgSya47SVbqzDqF6ZFQ", + "signature": null, + "type": "fulfillment", + "type_id": 4 + }, + "uri": "cc:1:20:XmUXkarmpe3n17ITJpi-EFy40qvGZ1C9aWphiiRfjOs:98" + }, + "new_owners": [ + "7MUjLUFEu12Hk5jb8BZEFgM5JWgSya47SVbqzDqF6ZFQ" + ] + } + ], + "data": null, + "fulfillments": [ + { + "current_owners": [ + "DTJCqP3sNkZcpoSA8bCtGwZ4ASfRLsMFXZDCmMHzCoeJ" + ], + "fid": 0, + "fulfillment": "cf:1:4:ILkIy_xP7k9clLCaVYtV-8dhuwdebq8yJSjeX1Ln3oUzQPKxMGutQV0EIRYxg81_Z6gdUHQYHkEyTKxwN7zRFjHNAnIdyU1NxqqohhFQSR-qYho-L-uqPRJcAed-SI7xwAI", + "input": { + "cid": 0, + "txid": "cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2" + } + } + ], + "operation": "TRANSFER", + "timestamp": "1460450449.289641" + }, + "version": 1 +} + ``` ## Double Spends @@ -119,12 +193,199 @@ If we try to create another transaction with the same input as before, the trans ```python # create another transfer transaction with the same input -tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved['id'], 'TRANSFER') +tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER') # sign the transaction tx_transfer_signed2 = b.sign_transaction(tx_transfer2, testuser1_priv) # check if the transaction is valid b.validate_transaction(tx_transfer_signed2) -Exception: input `6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd` was already spent +DoubleSpend: input `cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2` was already spent ``` + +## Crypto Conditions + +### Introduction + + +### Threshold Signatures + +```python +import copy +import json + +from cryptoconditions import Ed25519Fulfillment, ThresholdSha256Fulfillment +from bigchaindb import util, crypto + +# create some new testusers +thresholduser1_priv, thresholduser1_pub = crypto.generate_key_pair() +thresholduser2_priv, thresholduser2_pub = crypto.generate_key_pair() + +# retrieve the last transaction of testuser2 +tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop() + +# create a base template for a 1-input/2-output transaction +threshold_tx = b.create_transaction(testuser2_pub, [thresholduser1_pub, thresholduser2_pub], tx_retrieved_id, 'TRANSFER') + +# create a Threshold Cryptocondition +threshold_condition = ThresholdSha256Fulfillment(threshold=2) +threshold_condition.add_subfulfillment(Ed25519Fulfillment(public_key=thresholduser1_pub)) +threshold_condition.add_subfulfillment(Ed25519Fulfillment(public_key=thresholduser2_pub)) + +# update the condition in the newly created transaction +threshold_tx['transaction']['conditions'][0]['condition'] = { + 'details': json.loads(threshold_condition.serialize_json()), + 'uri': threshold_condition.condition.serialize_uri() +} + +# conditions have been updated, so hash needs updating +threshold_tx_data = copy.deepcopy(threshold_tx) +for fulfillment in threshold_tx_data['transaction']['fulfillments']: + fulfillment['fulfillment'] = None + +threshold_tx['id'] = crypto.hash_data(util.serialize(threshold_tx_data['transaction'])) + +# sign the transaction +threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv) + +# write the transaction +b.write_transaction(threshold_tx_signed) + +# check if the transaction is already in the bigchain +tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id']) + +{ + "id": "f0ea4a96afb3b8cafd6336aa3c4b44d1bb0f2b801f61fcb6a44eea4b870ff2e2", + "transaction": { + "conditions": [ + { + "cid": 0, + "condition": { + "details": { + "bitmask": 41, + "subfulfillments": [ + { + "bitmask": 32, + "public_key": "3tuSZ4FitNVWRgK7bGe6pEia7ERmxHmhCxFfFEVbD7g4", + "signature": null, + "type": "fulfillment", + "type_id": 4, + "weight": 1 + }, + { + "bitmask": 32, + "public_key": "8CvrriTsPZULEXTZW2Hnmg7najZsvXzgTi9NKpJaUdHS", + "signature": null, + "type": "fulfillment", + "type_id": 4, + "weight": 1 + } + ], + "threshold": 2, + "type": "fulfillment", + "type_id": 2 + }, + "uri": "cc:1:29:kiQHpdEiRe24L62KzwQgLu7dxCHaLBfEFLr_xzlswT4:208" + }, + "new_owners": [ + "3tuSZ4FitNVWRgK7bGe6pEia7ERmxHmhCxFfFEVbD7g4", + "8CvrriTsPZULEXTZW2Hnmg7najZsvXzgTi9NKpJaUdHS" + ] + } + ], + "data": null, + "fulfillments": [ + { + "current_owners": [ + "7MUjLUFEu12Hk5jb8BZEFgM5JWgSya47SVbqzDqF6ZFQ" + ], + "fid": 0, + "fulfillment": "cf:1:4:IF5lF5Gq5qXt59eyEyaYvhBcuNKrxmdQvWlqYYokX4zrQDSWz8yxBCFaYFKZOLai5ZCoVq28LVoiQ7TL5zkajG-I-BYH2NaKj7CfPBIZHWkMGWfd_UuQWkbhyx07MJ_1Jww", + "input": { + "cid": 0, + "txid": "86ce10d653c69acf422a6d017a4ccd27168cdcdac99a49e4a38fb5e0d280c579" + } + } + ], + "operation": "TRANSFER", + "timestamp": "1460450459.321600" + }, + "version": 1 +} + +``` + +```python +from cryptoconditions.fulfillment import Fulfillment + +thresholduser3_priv, thresholduser3_pub = crypto.generate_key_pair() + +# retrieve the last transaction of thresholduser1_pub +tx_retrieved_id = b.get_owned_ids(thresholduser1_pub).pop() + +# create a base template for a 2-input/1-output transaction +threshold_tx_transfer = b.create_transaction([thresholduser1_pub, thresholduser2_pub], thresholduser3_pub, tx_retrieved_id, 'TRANSFER') + +# parse the threshold cryptocondition +threshold_fulfillment = Fulfillment.from_json(threshold_tx['transaction']['conditions'][0]['condition']['details']) +subfulfillment1 = threshold_fulfillment.subconditions[0]['body'] +subfulfillment2 = threshold_fulfillment.subconditions[1]['body'] + +# get the fulfillment message to sign +threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer, + threshold_tx_transfer['transaction']['fulfillments'][0]) + +# sign the subconditions +subfulfillment1.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser1_priv)) +subfulfillment2.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser2_priv)) + +threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri() + +b.write_transaction(threshold_tx_transfer) + +{ + "id": "27d1e780526e172fdafb6cfec24b43878b0f8a2c34e962546ba4932ef7662646", + "transaction": { + "conditions": [ + { + "cid": 0, + "condition": { + "details": { + "bitmask": 32, + "public_key": "4SwVNiYRykGw1ixgKH75k97ipCnmm5QpwNwzQdCKLCzH", + "signature": null, + "type": "fulfillment", + "type_id": 4 + }, + "uri": "cc:1:20:MzgxMS8Zt2XZrSA_dFk1d64nwUz16knOeKkxc5LyIv4:98" + }, + "new_owners": [ + "4SwVNiYRykGw1ixgKH75k97ipCnmm5QpwNwzQdCKLCzH" + ] + } + ], + "data": null, + "fulfillments": [ + { + "current_owners": [ + "3tuSZ4FitNVWRgK7bGe6pEia7ERmxHmhCxFfFEVbD7g4", + "8CvrriTsPZULEXTZW2Hnmg7najZsvXzgTi9NKpJaUdHS" + ], + "fid": 0, + "fulfillment": "cf:1:2:AgIBYwQgKwNKM5oJUhL3lUJ3Xj0dzePTH_1BOxcIry5trRxnNXFANabre0P23pzs3liGozZ-cua3zLZuZIc4UA-2Eb_3oi0zFZKHlL6_PrfxpZFp4Mafsl3Iz1yGVz8s-x5jcbahDwABYwQgaxAYvRMOihIk-M4AZYFB2mlf4XjEqhiOaWpqinOYiXFAuQm7AMeXDs4NCeFI4P6YeL3RqNZqyTr9OsNHZ9JgJLZ2ER1nFpwsLhOt4TJZ01Plon7r7xA2GFKFkw511bRWAQA", + "input": { + "cid": 0, + "txid": "f0ea4a96afb3b8cafd6336aa3c4b44d1bb0f2b801f61fcb6a44eea4b870ff2e2" + } + } + ], + "operation": "TRANSFER", + "timestamp": "1460450469.337543" + }, + "version": 1 +} + +``` + + +### Merkle Trees diff --git a/tests/doc/__init__.py b/tests/doc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py new file mode 100644 index 00000000..77a7e272 --- /dev/null +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -0,0 +1,133 @@ +import copy +import json +from time import sleep + +from cryptoconditions import Ed25519Fulfillment, ThresholdSha256Fulfillment +from cryptoconditions.fulfillment import Fulfillment + +from bigchaindb import Bigchain, util, crypto, exceptions + + +b = Bigchain() + +# create a test user +testuser1_priv, testuser1_pub = crypto.generate_key_pair() + +# define a digital asset data payload +digital_asset_payload = {'msg': 'Hello BigchainDB!'} + +# a create transaction uses the operation `CREATE` and has no inputs +tx = b.create_transaction(b.me, testuser1_pub, None, 'CREATE', payload=digital_asset_payload) + +# all transactions need to be signed by the user creating the transaction +tx_signed = b.sign_transaction(tx, b.me_private) + +# write the transaction to the bigchain +# the transaction will be stored in a backlog where it will be validated, +# included in a block, and written to the bigchain +b.write_transaction(tx_signed) + +sleep(10) + +tx_retrieved = b.get_transaction(tx_signed['id']) + +# create a second testuser +testuser2_priv, testuser2_pub = crypto.generate_key_pair() + +# retrieve the transaction with condition id +tx_retrieved_id = b.get_owned_ids(testuser1_pub).pop() + +# create a transfer transaction +tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER') + +# sign the transaction +tx_transfer_signed = b.sign_transaction(tx_transfer, testuser1_priv) + +# write the transaction +b.write_transaction(tx_transfer_signed) + +sleep(10) + +# check if the transaction is already in the bigchain +tx_transfer_retrieved = b.get_transaction(tx_transfer_signed['id']) + +# create another transfer transaction with the same input +tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER') + +# sign the transaction +tx_transfer_signed2 = b.sign_transaction(tx_transfer2, testuser1_priv) + +# check if the transaction is valid +try: + b.validate_transaction(tx_transfer_signed2) +except exceptions.DoubleSpend as e: + print(e) + +# create some new testusers +thresholduser1_priv, thresholduser1_pub = crypto.generate_key_pair() +thresholduser2_priv, thresholduser2_pub = crypto.generate_key_pair() + +# retrieve the last transaction of testuser2 +tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop() + +# create a base template for a 1-input/2-output transaction +threshold_tx = b.create_transaction(testuser2_pub, [thresholduser1_pub, thresholduser2_pub], tx_retrieved_id, 'TRANSFER') + +# create a Threshold Cryptocondition +threshold_condition = ThresholdSha256Fulfillment(threshold=2) +threshold_condition.add_subfulfillment(Ed25519Fulfillment(public_key=thresholduser1_pub)) +threshold_condition.add_subfulfillment(Ed25519Fulfillment(public_key=thresholduser2_pub)) + +# update the condition in the newly created transaction +threshold_tx['transaction']['conditions'][0]['condition'] = { + 'details': json.loads(threshold_condition.serialize_json()), + 'uri': threshold_condition.condition.serialize_uri() +} + +# conditions have been updated, so hash needs updating +threshold_tx_data = copy.deepcopy(threshold_tx) +for fulfillment in threshold_tx_data['transaction']['fulfillments']: + fulfillment['fulfillment'] = None + +threshold_tx['id'] = crypto.hash_data(util.serialize(threshold_tx_data['transaction'])) + +# sign the transaction +threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv) + +# write the transaction +b.write_transaction(threshold_tx_signed) + +sleep(10) + +# check if the transaction is already in the bigchain +tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id']) + +thresholduser3_priv, thresholduser3_pub = crypto.generate_key_pair() + +# retrieve the last transaction of thresholduser1_pub +tx_retrieved_id = b.get_owned_ids(thresholduser1_pub).pop() + +# create a base template for a 2-input/1-output transaction +threshold_tx_transfer = b.create_transaction([thresholduser1_pub, thresholduser2_pub], thresholduser3_pub, tx_retrieved_id, 'TRANSFER') + +# parse the threshold cryptocondition +threshold_fulfillment = Fulfillment.from_json(threshold_tx['transaction']['conditions'][0]['condition']['details']) +subfulfillment1 = threshold_fulfillment.subconditions[0]['body'] +subfulfillment2 = threshold_fulfillment.subconditions[1]['body'] + +# get the fulfillment message to sign +threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer, + threshold_tx_transfer['transaction']['fulfillments'][0]) + +# sign the subconditions +subfulfillment1.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser1_priv)) +subfulfillment2.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser2_priv)) + +threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri() + +b.verify_signature(threshold_tx_transfer) + +b.validate_transaction(threshold_tx_transfer) + +b.write_transaction(threshold_tx_transfer) + From f8e0c41c9e00d7fe6bd3dd13bcef3a2cb71ec3c7 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 12 Apr 2016 13:37:45 +0200 Subject: [PATCH 32/39] CONTRIBUTING.md: a PR should resolve an existing issue --- CONTRIBUTING.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5c0ccbf..525d68f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,17 @@ If you want to file a bug report, suggest a feature, or ask a code-related quest ## How to Contribute Code or Documentation -### Step 0 - Prepare and Familiarize Yourself +### Step 0 - Decide on an Issue to Resolve, or Create One + +We want you to feel like your contributions (pull requests) are welcome, but if you contribute something unnecessary, unwanted, or perplexing, then your experience may be unpleasant. Your pull request may sit gathering dust as everyon scratches their head wondering what to do with it. + +To prevent that situation, we ask that all pull requests should resolve, address, or fix an existing issue. If there is no existing issue, then you should create one first. That way there can be commentary and discussion first, and you can have a better idea of what to expect when you create a corresponding pull request. + +When you submit a pull request, please mention the issue (or issues) that it resolves, e.g. "Resolves #123". + +Exception: hotfixes and minor changes don't require a pre-existing issue, but please write a thorough pull request description. + +### Step 1 - Prepare and Familiarize Yourself To contribute code or documentation, you need a [GitHub account](https://github.com/signup/free). @@ -25,11 +35,11 @@ Familiarize yourself with how we do coding and documentation in the BigchainDB p * [Scott Chacon's blog post about GitHub Flow](http://scottchacon.com/2011/08/31/github-flow.html) * [semantic versioning](http://semver.org/) -### Step 1 - Fork bigchaindb on GitHub +### Step 2 - Fork bigchaindb on GitHub In your web browser, go to [the BigchainDB repository on GitHub](https://github.com/bigchaindb/bigchaindb) and click the `Fork` button in the top right corner. This creates a new Git repository named `bigchaindb` in _your_ GitHub account. -### Step 2 - Clone Your Fork +### Step 3 - Clone Your Fork (This only has to be done once.) In your local terminal, use Git to clone _your_ `bigchaindb` repository to your local computer. Also add the original GitHub bigchaindb/bigchaindb repository as a remote named `upstream` (a convention): ```text @@ -38,7 +48,7 @@ cd bigchaindb git add upstream git@github.com:bigchaindb/bigchaindb.git ``` -### Step 3 - Fetch and Merge the Latest from `upstream/master` +### Step 4 - Fetch and Merge the Latest from `upstream/master` Switch to the `master` branch locally, fetch all `upstream` branches, and merge the just-fetched `upstream/master` branch with the local `master` branch: ```text @@ -47,7 +57,7 @@ git fetch upstream git merge upstream/master ``` -### Step 4 - Create a New Branch for Each Bug/Feature +### Step 5 - Create a New Branch for Each Bug/Feature If your new branch is to **fix a bug** identified in a specific GitHub Issue with number `ISSNO`, then name your new branch `bug/ISSNO/short-description-here`. For example, `bug/67/fix-leap-year-crash`. @@ -58,7 +68,7 @@ Otherwise, please give your new branch a short, descriptive, all-lowercase name. git checkout -b new-branch-name ``` -### Step 5 - Make Edits, git add, git commit +### Step 6 - Make Edits, git add, git commit With your new branch checked out locally, make changes or additions to the code or documentation. Remember to: @@ -85,14 +95,14 @@ If your addition or change is substantial, then please add a line or two to the (When you submit your pull request [following the instructions below], we run all the tests automatically, so we will see if some are failing. If you don't know why some tests are failing, you can still submit your pull request, but be sure to note the failing tests and to ask for help with resolving them.) -### Step 6 - Push Your New Branch to origin +### Step 7 - Push Your New Branch to origin Make sure you've commited all the additions or changes you want to include in your pull request. Then push your new branch to origin (i.e. _your_ remote bigchaindb repository). ```text git push origin new-branch-name ``` -### Step 7 - Create a Pull Request +### Step 8 - Create a Pull Request Go to the GitHub website and to _your_ remote bigchaindb repository (i.e. something like https://github.com/your-user-name/bigchaindb). From 3adf62d0f248e32b8b355dd07e7bfe06ff1598e0 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 12 Apr 2016 14:05:21 +0200 Subject: [PATCH 33/39] Fixed typo --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 525d68f3..65fdb205 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ If you want to file a bug report, suggest a feature, or ask a code-related quest ### Step 0 - Decide on an Issue to Resolve, or Create One -We want you to feel like your contributions (pull requests) are welcome, but if you contribute something unnecessary, unwanted, or perplexing, then your experience may be unpleasant. Your pull request may sit gathering dust as everyon scratches their head wondering what to do with it. +We want you to feel like your contributions (pull requests) are welcome, but if you contribute something unnecessary, unwanted, or perplexing, then your experience may be unpleasant. Your pull request may sit gathering dust as everyone scratches their heads wondering what to do with it. To prevent that situation, we ask that all pull requests should resolve, address, or fix an existing issue. If there is no existing issue, then you should create one first. That way there can be commentary and discussion first, and you can have a better idea of what to expect when you create a corresponding pull request. From 22923ee9d3046cbf08d447228018246cb2a3002c Mon Sep 17 00:00:00 2001 From: vrde Date: Tue, 12 Apr 2016 16:23:09 +0200 Subject: [PATCH 34/39] Force a config reset before every test --- bigchaindb/config_utils.py | 7 +++---- tests/conftest.py | 1 + tests/test_commands.py | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index 0ce41360..53eb7954 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -185,15 +185,14 @@ def autoconfigure(filename=None, config=None, force=False): newconfig = env_config(bigchaindb.config) - if config: - newconfig = update(newconfig, config) - try: - # import pdb; pdb.set_trace() newconfig = update(newconfig, file_config(filename=filename)) except FileNotFoundError as e: logger.warning('Cannot find config file `%s`.' % e.filename) + if config: + newconfig = update(newconfig, config) + dict_config(newconfig) return newconfig diff --git a/tests/conftest.py b/tests/conftest.py index 40500a9f..44de6316 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,7 @@ def ignore_local_config_file(monkeypatch): @pytest.fixture +@pytest.fixture(scope='function', autouse=True) def restore_config(request, node_config): from bigchaindb import config_utils config_utils.dict_config(node_config) diff --git a/tests/test_commands.py b/tests/test_commands.py index 64918cd4..c289f279 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -94,15 +94,18 @@ def test_bigchain_run_start_assume_yes_create_default_config(monkeypatch, mock_p # TODO Please beware, that if debugging, the "-s" switch for pytest will # interfere with capsys. # See related issue: https://github.com/pytest-dev/pytest/issues/128 +@pytest.mark.usefixtures('restore_config') def test_bigchain_show_config(capsys): from bigchaindb import config from bigchaindb.commands.bigchain import run_show_config + args = Namespace(config=None) _, _ = capsys.readouterr() run_show_config(args) - output_config, _ = capsys.readouterr() + output_config = json.loads(capsys.readouterr()[0]) del config['CONFIGURED'] - assert output_config.strip() == json.dumps(config, indent=4, sort_keys=True) + config['keypair']['private'] = 'x' * 45 + assert output_config == config def test_bigchain_run_init_when_db_exists(mock_db_init_with_existing_db): From a4cf5500ed7de914cde3f21383fa6d9a350c4420 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 12 Apr 2016 17:55:48 +0200 Subject: [PATCH 35/39] Fixed slice range bug in fabfile.py --- deploy-cluster-aws/fabfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy-cluster-aws/fabfile.py b/deploy-cluster-aws/fabfile.py index 5aba7a83..f385641b 100644 --- a/deploy-cluster-aws/fabfile.py +++ b/deploy-cluster-aws/fabfile.py @@ -43,9 +43,9 @@ def set_hosts(hosts): fab set_hosts:one_node init_bigchaindb """ if hosts == 'one_node': - env.hosts = public_dns_names[0] + env.hosts = public_dns_names[:1] elif hosts == 'two_nodes': - env.hosts = public_dns_names[0:1] + env.hosts = public_dns_names[:2] else: raise ValueError('Invalid input to set_hosts.' ' Expected one_node or two_nodes.' From 8775e7e8d7fcdba779eba563eb26bfb5a419074d Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 13 Apr 2016 10:36:46 +0200 Subject: [PATCH 36/39] added get_hash_data() --- bigchaindb/consensus.py | 7 +---- bigchaindb/util.py | 19 ++++++++++--- ...x_multi_condition_multi_fulfillment_v1.png | Bin 0 -> 53068 bytes docs/source/_static/tx_schematics.odg | Bin 0 -> 18457 bytes ...single_condition_single_fulfillment_v1.png | Bin 0 -> 25442 bytes docs/source/python-server-api-examples.md | 26 ++++++++++++++++-- tests/db/test_bigchain_api.py | 16 ++--------- 7 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png create mode 100644 docs/source/_static/tx_schematics.odg create mode 100644 docs/source/_static/tx_single_condition_single_fulfillment_v1.png diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index 3c622280..593aea9f 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -153,12 +153,7 @@ class BaseConsensusRules(AbstractConsensusRules): 'input `{}` was already spent'.format(fulfillment['input'])) # Check hash of the transaction - # remove the fulfillment messages (signatures) - transaction_data = copy.deepcopy(transaction) - for fulfillment in transaction_data['transaction']['fulfillments']: - fulfillment['fulfillment'] = None - - calculated_hash = crypto.hash_data(util.serialize(transaction_data['transaction'])) + calculated_hash = util.get_hash_data(transaction) if calculated_hash != transaction['id']: raise exceptions.InvalidHash() diff --git a/bigchaindb/util.py b/bigchaindb/util.py index bd17a670..7fed1f64 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -217,8 +217,7 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): } # serialize and convert to bytes - tx_serialized = serialize(tx) - tx_hash = crypto.hash_data(tx_serialized) + tx_hash = get_hash_data(tx) # create the transaction transaction = { @@ -268,7 +267,7 @@ def create_and_sign_tx(private_key, current_owner, new_owner, tx_input, operatio def check_hash_and_signature(transaction): # Check hash of the transaction - calculated_hash = crypto.hash_data(serialize(transaction['transaction'])) + calculated_hash = get_hash_data(transaction) if calculated_hash != transaction['id']: raise exceptions.InvalidHash() @@ -331,10 +330,22 @@ def get_fulfillment_message(transaction, fulfillment): # get previous condition previous_tx = b.get_transaction(fulfillment['input']['txid']) conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) - fulfillment_message['condition'] = conditions[fulfillment['fid']] + fulfillment_message['condition'] = conditions[fulfillment['input']['cid']] return fulfillment_message +def get_hash_data(transaction): + tx = copy.deepcopy(transaction) + if 'transaction' in tx: + tx = tx['transaction'] + + # remove the fulfillment messages (signatures) + for fulfillment in tx['fulfillments']: + fulfillment['fulfillment'] = None + + return crypto.hash_data(serialize(tx)) + + def transform_create(tx): """Change the owner and signature for a ``CREATE`` transaction created by a node""" diff --git a/docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png b/docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png new file mode 100644 index 0000000000000000000000000000000000000000..985bdae726b3b1afa9d0338563515b3fbe95b3ac GIT binary patch literal 53068 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%0AqkqIK`x-(Y+%<(-ls#P8SfyZ>#|j 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/tx_schematics.odg b/docs/source/_static/tx_schematics.odg new file mode 100644 index 0000000000000000000000000000000000000000..2a1c2fa401b3e829d7a56bc6eeb20dc06aafe373 GIT binary patch literal 18457 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 KBv~VF67(Nh8sG&0 literal 0 HcmV?d00001 diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 85a3809e..7ed19493 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -114,11 +114,15 @@ tx_retrieved = b.get_transaction(tx_signed['id']) ``` -The new owner of the digital asset is now `ACJyBLfeLNpCPrGWYoPYvnQ2MAC8BFukBko4hxtW9YoH`, which is the public key of `testuser1`. +The new owner of the digital asset is now `DTJCqP3sNkZcpoSA8bCtGwZ4ASfRLsMFXZDCmMHzCoeJ`, which is the public key of `testuser1`. + +Note that the current owner with public key `3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9` refers to one of the federation nodes that actually created the asset and assigned it to `testuser1`. ## Transfer the Digital Asset -Now that `testuser1` has a digital asset assigned to him, he can transfer it to another user. Transfer transactions require an input. The input will be the transaction id of a digital asset that was assigned to `testuser1`, which in our case is `6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd`. +Now that `testuser1` has a digital asset assigned to him, he can transfer it to another user. Transfer transactions require an input. The input will be the transaction id of a digital asset that was assigned to `testuser1`, which in our case is `cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2`. + +Since a transaction can have multiple outputs with each their own (crypto)condition, each transaction input should also refer to the condition index `cid`. ```python # create a second testuser @@ -203,13 +207,29 @@ b.validate_transaction(tx_transfer_signed2) DoubleSpend: input `cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2` was already spent ``` -## Crypto Conditions +## Crypto-Conditions + +BigchainDB makes use of the crypto-conditions library to both cryptographically lock and unlock transactions. +The locking script is refered to as a `condition` and a corresponding `fulfillment` unlocks the condition of the `input_tx`. + +![BigchainDB transactions connecting fulfillments with conditions](./_static/tx_single_condition_single_fulfillment_v1.png) + ### Introduction +Crypto-conditions provide a mechanism to describe a signed message such that multiple actors in a distributed system can all verify the same signed message and agree on whether it matches the description. + +This provides a useful primitive for event-based systems that are distributed on the Internet since we can describe events in a standard deterministic manner (represented by signed messages) and therefore define generic authenticated event handlers. + +Crypto-conditions are part of the Interledger protocol and the full specification can be found [here](https://interledger.org/five-bells-condition/spec.html). + +Implementations of the crypto-conditions are available in [Python](https://github.com/bigchaindb/cryptoconditions) and [JavaScript](https://github.com/interledger/five-bells-condition). + ### Threshold Signatures +MultiSig, m-of-n signatures + ```python import copy import json diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 47d02376..4a44a037 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -940,14 +940,8 @@ class TestCryptoconditions(object): 'details': json.loads(first_tx_condition.serialize_json()), 'uri': first_tx_condition.condition.serialize_uri() } - # conditions have been updated, so hash needs updating - transaction_data = copy.deepcopy(first_tx) - for fulfillment in transaction_data['transaction']['fulfillments']: - fulfillment['fulfillment'] = None - - calculated_hash = crypto.hash_data(util.serialize(transaction_data['transaction'])) - first_tx['id'] = calculated_hash + first_tx['id'] = util.get_hash_data(first_tx) first_tx_signed = b.sign_transaction(first_tx, user_sk) @@ -994,14 +988,8 @@ class TestCryptoconditions(object): 'details': json.loads(first_tx_condition.serialize_json()), 'uri': first_tx_condition.condition.serialize_uri() } - # conditions have been updated, so hash needs updating - transaction_data = copy.deepcopy(first_tx) - for fulfillment in transaction_data['transaction']['fulfillments']: - fulfillment['fulfillment'] = None - - calculated_hash = crypto.hash_data(util.serialize(transaction_data['transaction'])) - first_tx['id'] = calculated_hash + first_tx['id'] = util.get_hash_data(first_tx) first_tx_signed = b.sign_transaction(first_tx, user_sk) From 197954576f6dcc4739b54a44eed7d1f7a9f855aa Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 13 Apr 2016 11:10:32 +0200 Subject: [PATCH 37/39] threshold conditions documentation --- docs/source/python-server-api-examples.md | 32 +++++++++++++------ .../doc/run_doc_python_server_api_examples.py | 6 +--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 7ed19493..e59b2df1 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -226,9 +226,19 @@ Crypto-conditions are part of the Interledger protocol and the full specificatio Implementations of the crypto-conditions are available in [Python](https://github.com/bigchaindb/cryptoconditions) and [JavaScript](https://github.com/interledger/five-bells-condition). -### Threshold Signatures +### Threshold Conditions -MultiSig, m-of-n signatures +Threshold conditions introduce multi-signatures, m-of-n signatures or even more complex binary Merkle trees to BigchainDB. + +Setting up a generic threshold condition is a bit more elaborate than regular transaction signing but allow for flexible signing between multiple parties or groups. + +The basic workflow for creating a more complex cryptocondition is the following: + +1. Create a transaction template that include the public key of all (nested) parties as `new_owners` +2. Set up the threshold condition using the [cryptocondition library](https://github.com/bigchaindb/cryptoconditions) +3. Update the condition and hash in the transaction template + +We'll illustrate this by an example: ```python import copy @@ -259,11 +269,7 @@ threshold_tx['transaction']['conditions'][0]['condition'] = { } # conditions have been updated, so hash needs updating -threshold_tx_data = copy.deepcopy(threshold_tx) -for fulfillment in threshold_tx_data['transaction']['fulfillments']: - fulfillment['fulfillment'] = None - -threshold_tx['id'] = crypto.hash_data(util.serialize(threshold_tx_data['transaction'])) +threshold_tx['id'] = util.get_hash_data(threshold_tx) # sign the transaction threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv) @@ -335,6 +341,15 @@ tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id']) ``` +The transaction can now be transfered by fulfilling the threshold condition. + +The fulfillement involves: + +1. Create a transaction template that include the public key of all (nested) parties as `current_owners` +2. Parsing the threshold condition into a fulfillment using the [cryptocondition library](https://github.com/bigchaindb/cryptoconditions) +3. Signing all necessary subfulfillments and updating the fulfillment field in the transaction + + ```python from cryptoconditions.fulfillment import Fulfillment @@ -406,6 +421,3 @@ b.write_transaction(threshold_tx_transfer) } ``` - - -### Merkle Trees diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 77a7e272..add483d8 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -85,11 +85,7 @@ threshold_tx['transaction']['conditions'][0]['condition'] = { } # conditions have been updated, so hash needs updating -threshold_tx_data = copy.deepcopy(threshold_tx) -for fulfillment in threshold_tx_data['transaction']['fulfillments']: - fulfillment['fulfillment'] = None - -threshold_tx['id'] = crypto.hash_data(util.serialize(threshold_tx_data['transaction'])) +threshold_tx['id'] = util.get_hash_data(threshold_tx) # sign the transaction threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv) From 565fc8a1f76682137767cb898fae37e077b3b02f Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 13 Apr 2016 11:13:04 +0200 Subject: [PATCH 38/39] fix typo --- docs/source/python-server-api-examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index e59b2df1..6236e467 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -343,7 +343,7 @@ tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id']) The transaction can now be transfered by fulfilling the threshold condition. -The fulfillement involves: +The fulfillment involves: 1. Create a transaction template that include the public key of all (nested) parties as `current_owners` 2. Parsing the threshold condition into a fulfillment using the [cryptocondition library](https://github.com/bigchaindb/cryptoconditions) From 87592ee004018ca4586bccefbbf6132284c6fc82 Mon Sep 17 00:00:00 2001 From: diminator Date: Wed, 13 Apr 2016 11:34:01 +0200 Subject: [PATCH 39/39] docs for get_hash_data and get_fulfillment_message added serialized=False arg to get_fulfillment_message --- bigchaindb/util.py | 22 ++++++++++++++- docs/source/python-server-api-examples.md | 13 +++++++-- tests/db/test_bigchain_api.py | 28 +++++++++---------- .../doc/run_doc_python_server_api_examples.py | 13 +++++---- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 7fed1f64..b3df9d68 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -308,7 +308,17 @@ def verify_signature(signed_transaction): return True -def get_fulfillment_message(transaction, fulfillment): +def get_fulfillment_message(transaction, fulfillment, serialized=False): + """Get the fulfillment message for signing a specific fulfillment in a transaction + + Args: + transaction (dict): a transaction + fulfillment (dict): a specific fulfillment (for a condition index) within the transaction + serialized (bool): False returns a dict, True returns a serialized string + + Returns: + str|dict: fulfillment message + """ b = bigchaindb.Bigchain() common_data = { @@ -331,10 +341,20 @@ def get_fulfillment_message(transaction, fulfillment): previous_tx = b.get_transaction(fulfillment['input']['txid']) conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) fulfillment_message['condition'] = conditions[fulfillment['input']['cid']] + if serialized: + return serialize(fulfillment_message) return fulfillment_message def get_hash_data(transaction): + """ Get the hashed data that (should) correspond to the `transaction['id']` + + Args: + transaction (dict): the transaction to be hashed + + Returns: + str: the hash of the transaction + """ tx = copy.deepcopy(transaction) if 'transaction' in tx: tx = tx['transaction'] diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 6236e467..72dc4329 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -368,14 +368,21 @@ subfulfillment2 = threshold_fulfillment.subconditions[1]['body'] # get the fulfillment message to sign threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer, - threshold_tx_transfer['transaction']['fulfillments'][0]) + threshold_tx_transfer['transaction']['fulfillments'][0], + serialized=True) # sign the subconditions -subfulfillment1.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser1_priv)) -subfulfillment2.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser2_priv)) +subfulfillment1.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser1_priv)) +subfulfillment2.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser2_priv)) +# update the fulfillment threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri() +# optional validation checks +assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True +assert b.verify_signature(threshold_tx_transfer) == True +assert b.validate_transaction(threshold_tx_transfer) == True + b.write_transaction(threshold_tx_transfer) { diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 4a44a037..0c99a2c9 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -858,9 +858,9 @@ class TestCryptoconditions(object): def test_override_fulfillment_create(self, b, user_vk): tx = b.create_transaction(b.me, user_vk, None, 'CREATE') original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) + fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment, serialized=True) fulfillment = Ed25519Fulfillment(public_key=b.me) - fulfillment.sign(util.serialize(fulfillment_message), crypto.SigningKey(b.me_private)) + fulfillment.sign(fulfillment_message, crypto.SigningKey(b.me_private)) tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() @@ -875,9 +875,9 @@ class TestCryptoconditions(object): tx = b.create_transaction(user_vk, other_vk, prev_tx_id, 'TRANSFER') original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) + fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment, serialized=True) fulfillment = Ed25519Fulfillment(public_key=user_vk) - fulfillment.sign(util.serialize(fulfillment_message), crypto.SigningKey(user_sk)) + fulfillment.sign(fulfillment_message, crypto.SigningKey(user_sk)) tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() @@ -897,9 +897,9 @@ class TestCryptoconditions(object): } first_tx_fulfillment = first_tx['transaction']['fulfillments'][0] - first_tx_fulfillment_message = util.get_fulfillment_message(first_tx, first_tx_fulfillment) + first_tx_fulfillment_message = util.get_fulfillment_message(first_tx, first_tx_fulfillment, serialized=True) first_tx_fulfillment = Ed25519Fulfillment(public_key=user_vk) - first_tx_fulfillment.sign(util.serialize(first_tx_fulfillment_message), crypto.SigningKey(user_sk)) + first_tx_fulfillment.sign(first_tx_fulfillment_message, crypto.SigningKey(user_sk)) first_tx['transaction']['fulfillments'][0]['fulfillment'] = first_tx_fulfillment.serialize_uri() assert b.validate_transaction(first_tx) @@ -916,9 +916,9 @@ class TestCryptoconditions(object): next_tx = b.create_transaction(other_vk, user_vk, next_input_tx, 'TRANSFER') next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment) + next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) next_tx_fulfillment = Ed25519Fulfillment(public_key=other_vk) - next_tx_fulfillment.sign(util.serialize(next_tx_fulfillment_message), crypto.SigningKey(other_sk)) + next_tx_fulfillment.sign(next_tx_fulfillment_message, crypto.SigningKey(other_sk)) next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() assert b.validate_transaction(next_tx) @@ -959,13 +959,13 @@ class TestCryptoconditions(object): next_tx = b.create_transaction([other1_vk, other2_vk], user_vk, next_input_tx, 'TRANSFER') next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment) + next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) next_tx_fulfillment = ThresholdSha256Fulfillment(threshold=2) next_tx_subfulfillment1 = Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment1.sign(util.serialize(next_tx_fulfillment_message), crypto.SigningKey(other1_sk)) + next_tx_subfulfillment1.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment1) next_tx_subfulfillment2 = Ed25519Fulfillment(public_key=other2_vk) - next_tx_subfulfillment2.sign(util.serialize(next_tx_fulfillment_message), crypto.SigningKey(other2_sk)) + next_tx_subfulfillment2.sign(next_tx_fulfillment_message, crypto.SigningKey(other2_sk)) next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment2) next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() @@ -1007,15 +1007,15 @@ class TestCryptoconditions(object): next_tx = b.create_transaction([other1_vk, other2_vk], user_vk, next_input_tx, 'TRANSFER') next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment) + next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) next_tx_fulfillment = ThresholdSha256Fulfillment(threshold=2) next_tx_subfulfillment1 = Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment1.sign(util.serialize(next_tx_fulfillment_message), crypto.SigningKey(other1_sk)) + next_tx_subfulfillment1.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment1) # Wrong signing happens here next_tx_subfulfillment2 = Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment2.sign(util.serialize(next_tx_fulfillment_message), crypto.SigningKey(other1_sk)) + next_tx_subfulfillment2.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment2) next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index add483d8..9d46e938 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -113,17 +113,20 @@ subfulfillment2 = threshold_fulfillment.subconditions[1]['body'] # get the fulfillment message to sign threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer, - threshold_tx_transfer['transaction']['fulfillments'][0]) + threshold_tx_transfer['transaction']['fulfillments'][0], + serialized=True) # sign the subconditions -subfulfillment1.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser1_priv)) -subfulfillment2.sign(util.serialize(threshold_tx_fulfillment_message), crypto.SigningKey(thresholduser2_priv)) +subfulfillment1.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser1_priv)) +subfulfillment2.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser2_priv)) + +assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri() -b.verify_signature(threshold_tx_transfer) +assert b.verify_signature(threshold_tx_transfer) == True -b.validate_transaction(threshold_tx_transfer) +assert b.validate_transaction(threshold_tx_transfer) == True b.write_transaction(threshold_tx_transfer)