diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py
index e678f4e9..87a25d3f 100644
--- a/bigchaindb/config_utils.py
+++ b/bigchaindb/config_utils.py
@@ -16,11 +16,15 @@ import copy
import json
import logging
import collections
+from functools import lru_cache
+
+from pkg_resources import iter_entry_points, ResolutionError
from bigchaindb.common import exceptions
import bigchaindb
+from bigchaindb.consensus import BaseConsensusRules
# TODO: move this to a proper configuration file for logging
logging.getLogger('requests').setLevel(logging.WARNING)
@@ -240,3 +244,40 @@ def autoconfigure(filename=None, config=None, force=False):
newconfig = update(newconfig, config)
set_config(newconfig) # sets bigchaindb.config
+
+
+@lru_cache()
+def load_consensus_plugin(name=None):
+ """Find and load the chosen consensus plugin.
+
+ Args:
+ name (string): the name of the entry_point, as advertised in the
+ setup.py of the providing package.
+
+ Returns:
+ an uninstantiated subclass of ``bigchaindb.consensus.AbstractConsensusRules``
+ """
+ if not name:
+ return BaseConsensusRules
+
+ # TODO: This will return the first plugin with group `bigchaindb.consensus`
+ # and name `name` in the active WorkingSet.
+ # We should probably support Requirements specs in the config, e.g.
+ # consensus_plugin: 'my-plugin-package==0.0.1;default'
+ plugin = None
+ for entry_point in iter_entry_points('bigchaindb.consensus', name):
+ plugin = entry_point.load()
+
+ # No matching entry_point found
+ if not plugin:
+ raise ResolutionError(
+ 'No plugin found in group `bigchaindb.consensus` with name `{}`'.
+ format(name))
+
+ # Is this strictness desireable?
+ # It will probably reduce developer headaches in the wild.
+ if not issubclass(plugin, (BaseConsensusRules,)):
+ raise TypeError('object of type "{}" does not implement `bigchaindb.'
+ 'consensus.BaseConsensusRules`'.format(type(plugin)))
+
+ return plugin
diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py
index 1cc6e9ec..0e7dc4bd 100644
--- a/bigchaindb/consensus.py
+++ b/bigchaindb/consensus.py
@@ -10,6 +10,11 @@ logger = logging.getLogger(__name__)
class BaseConsensusRules():
"""Base consensus rules for Bigchain.
+
+ A consensus plugin must expose a class inheriting from this one via an entry_point.
+
+ All methods listed below must be implemented.
+
"""
@staticmethod
diff --git a/bigchaindb/core.py b/bigchaindb/core.py
index e6e3863f..b082eac4 100644
--- a/bigchaindb/core.py
+++ b/bigchaindb/core.py
@@ -56,10 +56,18 @@ class Bigchain(object):
self.me = public_key or bigchaindb.config['keypair']['public']
self.me_private = private_key or bigchaindb.config['keypair']['private']
self.nodes_except_me = keyring or bigchaindb.config['keyring']
+
if backlog_reassign_delay is None:
backlog_reassign_delay = bigchaindb.config['backlog_reassign_delay']
self.backlog_reassign_delay = backlog_reassign_delay
- self.consensus = BaseConsensusRules
+
+ consensusPlugin = bigchaindb.config.get('consensus_plugin')
+
+ if consensusPlugin:
+ self.consensus = config_utils.load_consensus_plugin(consensusPlugin)
+ else:
+ self.consensus = BaseConsensusRules
+
self.connection = connection if connection else backend.connect(**bigchaindb.config['database'])
if not self.me or not self.me_private:
raise exceptions.KeypairNotFoundException()
diff --git a/bigchaindb/pipelines/vote.py b/bigchaindb/pipelines/vote.py
index 5f385f53..8d4f4386 100644
--- a/bigchaindb/pipelines/vote.py
+++ b/bigchaindb/pipelines/vote.py
@@ -13,6 +13,7 @@ from multipipes import Pipeline, Node
import bigchaindb
from bigchaindb import Bigchain
from bigchaindb import backend
+from bigchaindb import config_utils
from bigchaindb.backend.changefeed import ChangeFeed
from bigchaindb.consensus import BaseConsensusRules
from bigchaindb.models import Transaction, Block
@@ -35,10 +36,17 @@ class Vote:
# Since cannot share a connection to RethinkDB using multiprocessing,
# we need to create a temporary instance of BigchainDB that we use
# only to query RethinkDB
- self.consensus = BaseConsensusRules
+
+ consensusPlugin = bigchaindb.config.get('consensus_plugin')
+
+ if consensusPlugin:
+ self.consensus = config_utils.load_consensus_plugin(consensusPlugin)
+ else:
+ self.consensus = BaseConsensusRules
# This is the Bigchain instance that will be "shared" (aka: copied)
# by all the subprocesses
+
self.bigchain = Bigchain()
self.last_voted_id = Bigchain().get_last_voted_block().id
diff --git a/docs/server/source/server-reference/configuration.md b/docs/server/source/server-reference/configuration.md
index 29431e82..f12b8247 100644
--- a/docs/server/source/server-reference/configuration.md
+++ b/docs/server/source/server-reference/configuration.md
@@ -21,6 +21,7 @@ For convenience, here's a list of all the relevant environment variables (docume
`BIGCHAINDB_SERVER_THREADS`
`BIGCHAINDB_CONFIG_PATH`
`BIGCHAINDB_BACKLOG_REASSIGN_DELAY`
+`BIGCHAINDB_CONSENSUS_PLUGIN`
The local config file is `$HOME/.bigchaindb` by default (a file which might not even exist), but you can tell BigchainDB to use a different file by using the `-c` command-line option, e.g. `bigchaindb -c path/to/config_file.json start`
or using the `BIGCHAINDB_CONFIG_PATH` environment variable, e.g. `BIGHAINDB_CONFIG_PATH=.my_bigchaindb_config bigchaindb start`.
@@ -54,7 +55,7 @@ Internally (i.e. in the Python code), both keys have a default value of `None`,
## keyring
-A list of the public keys of all the nodes in the cluster, excluding the public key of this node.
+A list of the public keys of all the nodes in the cluster, excluding the public key of this node.
**Example using an environment variable**
```text
@@ -65,7 +66,7 @@ Note how the keys in the list are separated by colons.
**Example config file snippet**
```js
-"keyring": ["BnCsre9MPBeQK8QZBFznU2dJJ2GwtvnSMdemCmod2XPB",
+"keyring": ["BnCsre9MPBeQK8QZBFznU2dJJ2GwtvnSMdemCmod2XPB",
"4cYQHoQrvPiut3Sjs8fVR1BMZZpJjMTC4bsMTt9V71aQ"]
```
@@ -152,9 +153,23 @@ Specifies how long, in seconds, transactions can remain in the backlog before be
**Example using environment variables**
```text
export BIGCHAINDB_BACKLOG_REASSIGN_DELAY=30
-```
+```
**Default value (from a config file)**
```js
-"backlog_reassign_delay": 120
+"backlog_reassign_delay": 120
+```
+
+## consensus_plugin
+
+The [consensus plugin](../appendices/consensus.html) to use.
+
+**Example using an environment variable**
+```text
+export BIGCHAINDB_CONSENSUS_PLUGIN=default
+```
+
+**Example config file snippet: the default**
+```js
+"consensus_plugin": "default"
```
diff --git a/tests/test_config_utils.py b/tests/test_config_utils.py
index d69b789a..602e9b57 100644
--- a/tests/test_config_utils.py
+++ b/tests/test_config_utils.py
@@ -48,6 +48,36 @@ def test_bigchain_instance_raises_when_not_configured(request, monkeypatch):
bigchaindb.Bigchain()
+def test_load_consensus_plugin_loads_default_rules_without_name():
+ from bigchaindb import config_utils
+ from bigchaindb.consensus import BaseConsensusRules
+
+ assert config_utils.load_consensus_plugin() == BaseConsensusRules
+
+
+def test_load_consensus_plugin_raises_with_unknown_name():
+ from pkg_resources import ResolutionError
+ from bigchaindb import config_utils
+
+ with pytest.raises(ResolutionError):
+ config_utils.load_consensus_plugin('bogus')
+
+
+def test_load_consensus_plugin_raises_with_invalid_subclass(monkeypatch):
+ # Monkeypatch entry_point.load to return something other than a
+ # ConsensusRules instance
+ from bigchaindb import config_utils
+ import time
+ monkeypatch.setattr(config_utils,
+ 'iter_entry_points',
+ lambda *args: [type('entry_point', (object), {'load': lambda: object})])
+
+ with pytest.raises(TypeError):
+ # Since the function is decorated with `lru_cache`, we need to
+ # "miss" the cache using a name that has not been used previously
+ config_utils.load_consensus_plugin(str(time.time()))
+
+
def test_map_leafs_iterator():
from bigchaindb import config_utils