From 833b804fe8e04c8dfc1c0356af0588c4dcf958be Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 25 Apr 2016 17:09:22 +0200 Subject: [PATCH 01/59] fix yml --- docker-compose-monitor.yml | 47 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/docker-compose-monitor.yml b/docker-compose-monitor.yml index 6865931d..f32d0fa3 100644 --- a/docker-compose-monitor.yml +++ b/docker-compose-monitor.yml @@ -1,25 +1,26 @@ -influxdb: - image: tutum/influxdb - ports: - - "8083:8083" - - "8086:8086" - expose: - - "8090" - - "8099" - environment: - PRE_CREATE_DB: "telegraf" +version: '2' +services: + influxdb: + image: tutum/influxdb + ports: + - "8083:8083" + - "8086:8086" + - "8090" + - "8099" + environment: + PRE_CREATE_DB: "telegraf" -grafana: - image: bigchaindb/grafana-bigchaindb-docker - tty: true - ports: - - "3000:3000" - links: - - influxdb:localhost + grafana: + image: bigchaindb/grafana-bigchaindb-docker + tty: true + ports: + - "3000:3000" + environment: + INFLUXDB_HOST: "influxdb" -statsd: - image: bigchaindb/docker-telegraf-statsd - ports: - - "8125:8125/udp" - links: - - influxdb:localhost \ No newline at end of file + statsd: + image: bigchaindb/docker-telegraf-statsd + ports: + - "8125:8125/udp" + environment: + INFLUXDB_HOST: "influxdb" \ No newline at end of file From 821cfe2b38bac870e099cac2e52758ec64442a92 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 25 Apr 2016 17:15:13 +0200 Subject: [PATCH 02/59] fixed indentation error --- docker-compose-monitor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose-monitor.yml b/docker-compose-monitor.yml index f32d0fa3..305ade2d 100644 --- a/docker-compose-monitor.yml +++ b/docker-compose-monitor.yml @@ -2,11 +2,11 @@ version: '2' services: influxdb: image: tutum/influxdb - ports: - - "8083:8083" - - "8086:8086" - - "8090" - - "8099" + ports: + - "8083:8083" + - "8086:8086" + - "8090" + - "8099" environment: PRE_CREATE_DB: "telegraf" From 0d0fe384969561c4ac20e61aee82e08a02013e15 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 28 Apr 2016 13:45:10 +0200 Subject: [PATCH 03/59] whitespace error --- docker-compose-monitor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose-monitor.yml b/docker-compose-monitor.yml index f32d0fa3..305ade2d 100644 --- a/docker-compose-monitor.yml +++ b/docker-compose-monitor.yml @@ -2,11 +2,11 @@ version: '2' services: influxdb: image: tutum/influxdb - ports: - - "8083:8083" - - "8086:8086" - - "8090" - - "8099" + ports: + - "8083:8083" + - "8086:8086" + - "8090" + - "8099" environment: PRE_CREATE_DB: "telegraf" From dd21349df13f25384f3a61cf8abbb2e00e1838a4 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 29 Apr 2016 10:41:23 +0200 Subject: [PATCH 04/59] Added fabfile to install docker on aws --- deploy-cluster-aws/fabfile-monitor.py | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 deploy-cluster-aws/fabfile-monitor.py diff --git a/deploy-cluster-aws/fabfile-monitor.py b/deploy-cluster-aws/fabfile-monitor.py new file mode 100644 index 00000000..8e9cd939 --- /dev/null +++ b/deploy-cluster-aws/fabfile-monitor.py @@ -0,0 +1,37 @@ +from fabric.api import sudo, env +from fabric.api import task + +# Ignore known_hosts +# http://docs.fabfile.org/en/1.10/usage/env.html#disable-known-hosts +env.disable_known_hosts = True + +env.user = 'ubuntu' +env.key_filename = 'pem/bigchaindb.pem' + + +@task +def install_docker(): + """Install docker on an ec2 ubuntu 14.04 instance + + Example: + fab --fabfile=fabfile-monitor.py \ + --hosts=ec2-52-58-106-17.eu-central-1.compute.amazonaws.com \ + install_docker + """ + + # install prerequisites + sudo('apt-get update') + sudo('apt-get -y install apt-transport-https ca-certificates linux-image-extra-$(uname -r) apparmor') + + # install docker repositories + sudo('apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 \ + --recv-keys 58118E89F3A912897C070ADBF76221572C52609D') + sudo("echo 'deb https://apt.dockerproject.org/repo ubuntu-trusty main' | \ + sudo tee /etc/apt/sources.list.d/docker.list") + + # install docker engine + sudo('apt-get update') + sudo('apt-get -y install docker-engine') + + # add ubuntu user to the docker group + sudo('usermod -aG docker ubuntu') From 55e219c61c2ea5605df2650c9f2993ce492fe032 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 3 May 2016 14:37:12 +0200 Subject: [PATCH 05/59] Added fab command to install docker compose. Added fab command to run monitor --- deploy-cluster-aws/fabfile-monitor.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/deploy-cluster-aws/fabfile-monitor.py b/deploy-cluster-aws/fabfile-monitor.py index 8e9cd939..b0ff4841 100644 --- a/deploy-cluster-aws/fabfile-monitor.py +++ b/deploy-cluster-aws/fabfile-monitor.py @@ -1,5 +1,6 @@ from fabric.api import sudo, env from fabric.api import task +from fabric.operations import put, run # Ignore known_hosts # http://docs.fabfile.org/en/1.10/usage/env.html#disable-known-hosts @@ -10,7 +11,7 @@ env.key_filename = 'pem/bigchaindb.pem' @task -def install_docker(): +def install_docker_engine(): """Install docker on an ec2 ubuntu 14.04 instance Example: @@ -35,3 +36,17 @@ def install_docker(): # add ubuntu user to the docker group sudo('usermod -aG docker ubuntu') + + +@task +def install_docker_compose(): + sudo('curl -L https://github.com/docker/compose/releases/download/1.7.0/docker-compose-`uname \ + -s`-`uname -m` > /usr/local/bin/docker-compose') + sudo('chmod +x /usr/local/bin/docker-compose') + + +@task +def run_monitor(): + # copy docker-compose-monitor to the ec2 instance + put('../docker-compose-monitor.yml') + run('docker-compose -f docker-compose-monitor.yml up -d') From 4aeac23f330b3ef5a64f442de06d16d7541039b8 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 15:24:44 +0200 Subject: [PATCH 06/59] Removed last line of startup.sh --- deploy-cluster-aws/startup.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy-cluster-aws/startup.sh b/deploy-cluster-aws/startup.sh index f9ccbbe7..7529bd00 100755 --- a/deploy-cluster-aws/startup.sh +++ b/deploy-cluster-aws/startup.sh @@ -127,4 +127,3 @@ fab start_bigchaindb # cleanup rm add2known_hosts.sh -# rm -rf temp_confs \ No newline at end of file From 443589bb67ffa54f37157f2c81546489dd864c67 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 15:37:22 +0200 Subject: [PATCH 07/59] Renamed startup.sh to awsdeploy_servers.sh --- deploy-cluster-aws/{startup.sh => awsdeploy_servers.sh} | 2 +- docs/source/deploy-on-aws.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename deploy-cluster-aws/{startup.sh => awsdeploy_servers.sh} (97%) diff --git a/deploy-cluster-aws/startup.sh b/deploy-cluster-aws/awsdeploy_servers.sh similarity index 97% rename from deploy-cluster-aws/startup.sh rename to deploy-cluster-aws/awsdeploy_servers.sh index 7529bd00..538d7727 100755 --- a/deploy-cluster-aws/startup.sh +++ b/deploy-cluster-aws/awsdeploy_servers.sh @@ -6,7 +6,7 @@ set -e function printErr() { - echo "usage: ./startup.sh " + echo "usage: ./awsdeploy_servers.sh " echo "No argument $1 supplied" } diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index 49c83021..d4f25e41 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -108,19 +108,19 @@ Here's an example of how one could launch a BigchainDB cluster of three (3) node # in a Python 2.5-2.7 virtual environment where fabric, boto3, etc. are installed cd bigchaindb cd deploy-cluster-aws -./startup.sh wrigley 3 pypi +./awsdeploy_servers.sh wrigley 3 pypi ``` The `pypi` on the end means that it will install the latest (stable) `bigchaindb` package from the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB is installed using `pip install bigchaindb`. -`startup.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: +`awsdeploy_servers.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: ```text -./startup.sh +./awsdeploy_servers.sh ``` The first two arguments are self-explanatory. The third argument can be `pypi` or the name of a local Git branch (e.g. `master` or `feat/3752/quote-asimov-on-tuesdays`). If you don't include a third argument, then `pypi` will be assumed by default. -If you're curious what the `startup.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`startup.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/startup.sh) +If you're curious what the `awsdeploy_servers.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`awsdeploy_servers.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy_servers.sh) It should take a few minutes for the deployment to finish. If you run into problems, see the section on Known Deployment Issues below. From a250c6b35a7fed7d03054dbf0439fa9dcfa0dd7f Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 16:04:28 +0200 Subject: [PATCH 08/59] Auto-generate server tag for AWS nodes --- deploy-cluster-aws/awsdeploy_servers.sh | 31 +++++++++++-------------- docs/source/deploy-on-aws.md | 8 +++---- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/deploy-cluster-aws/awsdeploy_servers.sh b/deploy-cluster-aws/awsdeploy_servers.sh index 538d7727..1e053c93 100755 --- a/deploy-cluster-aws/awsdeploy_servers.sh +++ b/deploy-cluster-aws/awsdeploy_servers.sh @@ -4,32 +4,27 @@ # if any command has a non-zero exit status set -e -function printErr() - { - echo "usage: ./awsdeploy_servers.sh " - echo "No argument $1 supplied" - } +USAGE="usage: ./awsdeploy_servers.sh " + +# Auto-generate the tag to apply to all nodes in the cluster +TAG="bcdb-"`date +%m-%d@%H:%M` +echo "TAG = "$TAG if [ -z "$1" ]; then - printErr "" + echo $USAGE + echo "No first argument was specified" exit 1 +else + NUM_NODES=$1 fi -if [ -z "$2" ]; then - printErr "" - exit 1 -fi - -TAG=$1 -NUM_NODES=$2 - -# If they don't include a third argument () +# If they don't include a second argument () # then assume BRANCH = "pypi" by default -if [ -z "$3" ]; then - echo "No third argument was specified, so BigchainDB will be installed from PyPI" +if [ -z "$2" ]; then + echo "No second argument was specified, so BigchainDB will be installed from PyPI" BRANCH="pypi" else - BRANCH=$3 + BRANCH=$2 fi # Check for AWS private key file (.pem file) diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index d4f25e41..6692e5c1 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -103,22 +103,22 @@ You can look inside those files if you're curious. In step 2, they'll be modifie Step 2 is to launch the nodes ("instances") on AWS, to install all the necessary software on them, configure the software, run the software, and more. -Here's an example of how one could launch a BigchainDB cluster of three (3) nodes tagged `wrigley` on AWS: +Here's an example of how one could launch a BigchainDB cluster of three (3) nodes on AWS: ```text # in a Python 2.5-2.7 virtual environment where fabric, boto3, etc. are installed cd bigchaindb cd deploy-cluster-aws -./awsdeploy_servers.sh wrigley 3 pypi +./awsdeploy_servers.sh 3 pypi ``` The `pypi` on the end means that it will install the latest (stable) `bigchaindb` package from the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB is installed using `pip install bigchaindb`. `awsdeploy_servers.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: ```text -./awsdeploy_servers.sh +./awsdeploy_servers.sh ``` -The first two arguments are self-explanatory. The third argument can be `pypi` or the name of a local Git branch (e.g. `master` or `feat/3752/quote-asimov-on-tuesdays`). If you don't include a third argument, then `pypi` will be assumed by default. +The first argument is the number of nodes to deploy. The second argument can be `pypi` or the name of a local Git branch (e.g. `master` or `feat/3752/quote-asimov-on-tuesdays`). If you don't include a second argument, then `pypi` will be assumed by default. If you're curious what the `awsdeploy_servers.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`awsdeploy_servers.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy_servers.sh) From b82e60f074774b7f3a7a7533e74e1dde59ed98bf Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 3 May 2016 16:30:30 +0200 Subject: [PATCH 09/59] Added option to specify a mount point for influxdb. INFLUXDB_DATA=/somedir/ docker-compose -f docker-compose-monitor up --- docker-compose-monitor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose-monitor.yml b/docker-compose-monitor.yml index 305ade2d..e695387c 100644 --- a/docker-compose-monitor.yml +++ b/docker-compose-monitor.yml @@ -9,6 +9,8 @@ services: - "8099" environment: PRE_CREATE_DB: "telegraf" + volumes: + - $INFLUXDB_DATA:/data grafana: image: bigchaindb/grafana-bigchaindb-docker From 389319e497e861e860e0b525bf2a8471fa77260b Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 3 May 2016 17:06:58 +0200 Subject: [PATCH 10/59] Added fab task to run monitor Updated documentation --- deploy-cluster-aws/fabfile-monitor.py | 31 +++++++++++++++++++++++++-- docs/source/deploy-on-aws.md | 22 +++++++++++++++++++ docs/source/monitoring.md | 7 +++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/deploy-cluster-aws/fabfile-monitor.py b/deploy-cluster-aws/fabfile-monitor.py index b0ff4841..71c5b100 100644 --- a/deploy-cluster-aws/fabfile-monitor.py +++ b/deploy-cluster-aws/fabfile-monitor.py @@ -17,7 +17,7 @@ def install_docker_engine(): Example: fab --fabfile=fabfile-monitor.py \ --hosts=ec2-52-58-106-17.eu-central-1.compute.amazonaws.com \ - install_docker + install_docker_engine """ # install prerequisites @@ -40,13 +40,40 @@ def install_docker_engine(): @task def install_docker_compose(): + """Install docker-compose on an ec2 ubuntu 14.04 instance + + Example: + fab --fabfile=fabfile-monitor.py \ + --hosts=ec2-52-58-106-17.eu-central-1.compute.amazonaws.com \ + install_docker_compose + """ sudo('curl -L https://github.com/docker/compose/releases/download/1.7.0/docker-compose-`uname \ -s`-`uname -m` > /usr/local/bin/docker-compose') sudo('chmod +x /usr/local/bin/docker-compose') +@task +def install_docker(): + """Install docker and docker-compose on an ec2 ubuntu 14.04 instance + + Example: + fab --fabfile=fabfile-monitor.py \ + --hosts=ec2-52-58-106-17.eu-central-1.compute.amazonaws.com \ + install_docker + """ + install_docker_engine() + install_docker_compose() + + @task def run_monitor(): + """Run bigchaindb monitor on an ec2 ubuntu 14.04 instance + + Example: + fab --fabfile=fabfile-monitor.py \ + --hosts=ec2-52-58-106-17.eu-central-1.compute.amazonaws.com \ + run_monitor + """ # copy docker-compose-monitor to the ec2 instance put('../docker-compose-monitor.yml') - run('docker-compose -f docker-compose-monitor.yml up -d') + run('INFLUXDB_DATA=/influxdb-data docker-compose -f docker-compose-monitor.yml up -d') diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index 49c83021..0d23fdeb 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -149,6 +149,28 @@ You have 2 allocated elactic IPs which are not associated with instances (It has Domain = vpc.) ``` +## BigchainDB Monitor Deployment + +The deployment for the monitor requires an aws ec2 instance running Ubuntu 14.04 with a system folder `/influxdb-data`. +This folder can be a [mount point of an EBS volume](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html) +for data persistence. + +With the instance setup we first need to install `docker` and `docker-compose`: +```text +$ fab --fabfile=fabfile-monitor.py --hosts= install_docker +``` + +After docker is installed run the monitor with: +```text +$ fab --fabfile=fabfile-monitor.py --hosts= run_monitor +``` + +To access the monitor follow the instructions on [Monitoring](monitoring.html) replacing `localhost` with the +hostname of the ec2 instance running the monitor. + +To configure bigchaindb to start sending statistics to the monitor change statsd host in the configuration file +(in `$HOME/.bigchaindb` by default) to the hostname of the ec2 instance running the monitor. + ## Known Deployment Issues ### NetworkError diff --git a/docs/source/monitoring.md b/docs/source/monitoring.md index 4fb53072..dd3b1c6f 100644 --- a/docs/source/monitoring.md +++ b/docs/source/monitoring.md @@ -16,7 +16,12 @@ $ docker-compose -f docker-compose-monitor.yml build $ docker-compose -f docker-compose-monitor.yml up ``` -then point a browser tab to: +It is also possible to mount a host directory as a data volume for influxdb by settings the `INFLUXDB_DATA` env. +```text +$ INFLUXDB_DATA=/data docker-compose -f docker-compose-monitor up +``` + +After starting docker-compose point a browser tab to: [http://localhost:3000/dashboard/script/bigchaindb_dashboard.js](http://localhost:3000/dashboard/script/bigchaindb_dashboard.js) From 19819b880f02189713fa20324b3ab39c3bcfd2ad Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 17:16:06 +0200 Subject: [PATCH 11/59] awsdeploy_servers.sh -> awsdeploy.sh & changed its args --- .../{awsdeploy_servers.sh => awsdeploy.sh} | 29 ++++++++++++++----- docs/source/deploy-on-aws.md | 16 +++++----- 2 files changed, 30 insertions(+), 15 deletions(-) rename deploy-cluster-aws/{awsdeploy_servers.sh => awsdeploy.sh} (86%) diff --git a/deploy-cluster-aws/awsdeploy_servers.sh b/deploy-cluster-aws/awsdeploy.sh similarity index 86% rename from deploy-cluster-aws/awsdeploy_servers.sh rename to deploy-cluster-aws/awsdeploy.sh index 1e053c93..4f73b656 100755 --- a/deploy-cluster-aws/awsdeploy_servers.sh +++ b/deploy-cluster-aws/awsdeploy.sh @@ -4,29 +4,38 @@ # if any command has a non-zero exit status set -e -USAGE="usage: ./awsdeploy_servers.sh " - -# Auto-generate the tag to apply to all nodes in the cluster -TAG="bcdb-"`date +%m-%d@%H:%M` -echo "TAG = "$TAG +USAGE="usage: ./awsdeploy.sh " if [ -z "$1" ]; then echo $USAGE - echo "No first argument was specified" + echo "No first argument was specified" + echo "It should be a number like 3 or 15" exit 1 else NUM_NODES=$1 fi -# If they don't include a second argument () -# then assume BRANCH = "pypi" by default if [ -z "$2" ]; then + echo $USAGE echo "No second argument was specified, so BigchainDB will be installed from PyPI" BRANCH="pypi" else BRANCH=$2 fi +if [ -z "$3" ]; then + echo $USAGE + echo "No third argument was specified, so servers will be deployed" + WHAT_TO_DEPLOY="servers" +else + WHAT_TO_DEPLOY=$3 +fi + +if [[ ("$WHAT_TO_DEPLOY" != "servers") && ("$WHAT_TO_DEPLOY" != "clients") ]]; then + echo "The third argument, if included, must be servers or clients" + exit 1 +fi + # Check for AWS private key file (.pem file) if [ ! -f "pem/bigchaindb.pem" ]; then echo "File pem/bigchaindb.pem (AWS private key) is missing" @@ -40,6 +49,10 @@ if [ ! -d "confiles" ]; then exit 1 fi +# Auto-generate the tag to apply to all nodes in the cluster +TAG="BDB-"$WHAT_TO_DEPLOY"-"`date +%m-%d@%H:%M` +echo "TAG = "$TAG + # Change the file permissions on pem/bigchaindb.pem # so that the owner can read it, but that's all chmod 0400 pem/bigchaindb.pem diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index 6692e5c1..b5f03faf 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -108,19 +108,21 @@ Here's an example of how one could launch a BigchainDB cluster of three (3) node # in a Python 2.5-2.7 virtual environment where fabric, boto3, etc. are installed cd bigchaindb cd deploy-cluster-aws -./awsdeploy_servers.sh 3 pypi +./awsdeploy.sh 3 ``` -The `pypi` on the end means that it will install the latest (stable) `bigchaindb` package from the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB is installed using `pip install bigchaindb`. - -`awsdeploy_servers.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: +`awsdeploy.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: ```text -./awsdeploy_servers.sh +./awsdeploy.sh [pypi_or_branch] [servers_or_clients] ``` -The first argument is the number of nodes to deploy. The second argument can be `pypi` or the name of a local Git branch (e.g. `master` or `feat/3752/quote-asimov-on-tuesdays`). If you don't include a second argument, then `pypi` will be assumed by default. +The first argument is required. It's how many nodes you want to deploy. -If you're curious what the `awsdeploy_servers.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`awsdeploy_servers.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy_servers.sh) +The second argument is optional. It can be the word `pypi` or the name of a local Git branch. `pypi` means that BigchainDB will be installed from the latest `bigchaindb` package in the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB will be installed using `pip install bigchaindb`. If you don't include the second argument, then the default is `pypi`. + +The third argument is also optional (but if you want to include it, you must also include the second argument). It must be either `servers` or `clients`, depending on what you want to deploy. If you don't include the third argument, then the default is `servers`. + +If you're curious what the `awsdeploy.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`awsdeploy.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy.sh) It should take a few minutes for the deployment to finish. If you run into problems, see the section on Known Deployment Issues below. From eab54e1a0247a236bcf541019d87df7b59278d42 Mon Sep 17 00:00:00 2001 From: diminator Date: Tue, 3 May 2016 20:32:56 +0200 Subject: [PATCH 12/59] update docs/test file to avoid conflicts between testcases --- docs/source/python-server-api-examples.md | 4 +-- .../doc/run_doc_python_server_api_examples.py | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index c191fe74..f8a9c003 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -324,8 +324,8 @@ tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser b.write_transaction(tx_multisig_transfer_signed) # Check if the transaction is already in the bigchain -tx_multisig_retrieved = b.get_transaction(tx_multisig_transfer_signed['id']) -tx_multisig_retrieved +tx_multisig_transfer_retrieved = b.get_transaction(tx_multisig_transfer_signed['id']) +tx_multisig_transfer_retrieved ``` ```python diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 56a6aa31..6be75a3b 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -29,7 +29,7 @@ tx_signed = b.sign_transaction(tx, b.me_private) # included in a block, and written to the bigchain b.write_transaction(tx_signed) -sleep(10) +sleep(8) """ Read the Creation Transaction from the DB @@ -61,10 +61,12 @@ tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id # sign the transaction tx_transfer_signed = b.sign_transaction(tx_transfer, testuser1_priv) + +b.validate_transaction(tx_transfer_signed) # write the transaction b.write_transaction(tx_transfer_signed) -sleep(10) +sleep(8) # check if the transaction is already in the bigchain tx_transfer_retrieved = b.get_transaction(tx_transfer_signed['id']) @@ -95,10 +97,12 @@ tx_multisig = b.create_transaction(b.me, [testuser1_pub, testuser2_pub], None, ' # Have the federation sign the transaction tx_multisig_signed = b.sign_transaction(tx_multisig, b.me_private) + +b.validate_transaction(tx_multisig_signed) b.write_transaction(tx_multisig_signed) # wait a few seconds for the asset to appear on the blockchain -sleep(10) +sleep(8) # retrieve the transaction tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id']) @@ -111,15 +115,16 @@ tx_multisig_retrieved_id = b.get_owned_ids(testuser2_pub).pop() tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER') tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv]) +b.validate_transaction(tx_multisig_transfer_signed) b.write_transaction(tx_multisig_transfer_signed) # wait a few seconds for the asset to appear on the blockchain -sleep(10) +sleep(8) # retrieve the transaction -tx_multisig_retrieved = b.get_transaction(tx_multisig_transfer_signed['id']) - -print(json.dumps(tx_multisig_transfer_signed, sort_keys=True, indent=4, separators=(',', ':'))) +tx_multisig_transfer_retrieved = b.get_transaction(tx_multisig_transfer_signed['id']) +assert tx_multisig_transfer_retrieved is not None +print(json.dumps(tx_multisig_transfer_retrieved, sort_keys=True, indent=4, separators=(',', ':'))) """ Multiple Inputs and Outputs @@ -127,9 +132,10 @@ Multiple Inputs and Outputs for i in range(3): tx_mimo_asset = b.create_transaction(b.me, testuser1_pub, None, 'CREATE') tx_mimo_asset_signed = b.sign_transaction(tx_mimo_asset, b.me_private) + b.validate_transaction(tx_mimo_asset_signed) b.write_transaction(tx_mimo_asset_signed) -sleep(10) +sleep(8) # get inputs owned_mimo_inputs = b.get_owned_ids(testuser1_pub) @@ -137,9 +143,10 @@ print(len(owned_mimo_inputs)) # create a transaction tx_mimo = b.create_transaction(testuser1_pub, testuser2_pub, owned_mimo_inputs, 'TRANSFER') -tx_mimo_signed = b.sign_transaction(tx_mimo, testuser1_priv) +tx_mimo_signed = b.sign_transaction(tx_mimo, testuser1_priv) # write the transaction +b.validate_transaction(tx_mimo_signed) b.write_transaction(tx_mimo_signed) print(json.dumps(tx_mimo_signed, sort_keys=True, indent=4, separators=(',', ':'))) @@ -178,10 +185,11 @@ threshold_tx['id'] = util.get_hash_data(threshold_tx) # sign the transaction threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv) +b.validate_transaction(threshold_tx_signed) # write the transaction b.write_transaction(threshold_tx_signed) -sleep(10) +sleep(8) # check if the transaction is already in the bigchain tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id']) @@ -266,7 +274,7 @@ assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed b.write_transaction(hashlock_tx_signed) print(json.dumps(hashlock_tx_signed, sort_keys=True, indent=4, separators=(',', ':'))) -sleep(10) +sleep(8) hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() From d8a0765a2a856b420e6bd8bd74e828ccc5e1d28e Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 21:02:58 +0200 Subject: [PATCH 13/59] awsdeploy.sh can now deploy N servers or N clients --- deploy-cluster-aws/awsdeploy.sh | 71 +++++++++++++++++++-------------- deploy-cluster-aws/fabfile.py | 15 +++++++ 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/deploy-cluster-aws/awsdeploy.sh b/deploy-cluster-aws/awsdeploy.sh index 4f73b656..46a64928 100755 --- a/deploy-cluster-aws/awsdeploy.sh +++ b/deploy-cluster-aws/awsdeploy.sh @@ -74,15 +74,16 @@ python launch_ec2_nodes.py --tag $TAG --nodes $NUM_NODES chmod +x add2known_hosts.sh ./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 fab install_base_software -# Rollout storage backend (RethinkDB) and start it -fab install_rethinkdb +if [ "$WHAT_TO_DEPLOY" == "servers" ]; then + # (Re)create the RethinkDB configuration file conf/rethinkdb.conf + python create_rethinkdb_conf.py + # Rollout storage backend (RethinkDB) and start it + fab install_rethinkdb +fi # Rollout BigchainDB (but don't start it yet) if [ "$BRANCH" == "pypi" ]; then @@ -102,36 +103,46 @@ fi # Configure BigchainDB on all nodes -# The idea is to send a bunch of locally-created configuration -# files out to each of the instances / nodes. +if [ "$WHAT_TO_DEPLOY" == "servers" ]; then + # The idea is to send a bunch of locally-created configuration + # files out to each of the instances / nodes. -# Assume a set of $NUM_NODES BigchaindB config files -# already exists in the confiles directory. -# One can create a set using a command like -# ./make_confiles.sh confiles $NUM_NODES -# (We can't do that here now because this virtual environment -# is a Python 2 environment that may not even have -# bigchaindb installed, so bigchaindb configure can't be called) + # Assume a set of $NUM_NODES BigchaindB config files + # already exists in the confiles directory. + # One can create a set using a command like + # ./make_confiles.sh confiles $NUM_NODES + # (We can't do that here now because this virtual environment + # is a Python 2 environment that may not even have + # bigchaindb installed, so bigchaindb configure can't be called) -# Transform the config files in the confiles directory -# to have proper keyrings, api_endpoint values, etc. -python clusterize_confiles.py confiles $NUM_NODES + # Transform the config files in the confiles directory + # to have proper keyrings, api_endpoint values, etc. + python clusterize_confiles.py confiles $NUM_NODES -# Send one of the config files to each instance -for (( HOST=0 ; HOST<$NUM_NODES ; HOST++ )); do - CONFILE="bcdb_conf"$HOST - echo "Sending "$CONFILE - fab set_host:$HOST send_confile:$CONFILE -done + # Send one of the config files to each instance + for (( HOST=0 ; HOST<$NUM_NODES ; HOST++ )); do + CONFILE="bcdb_conf"$HOST + echo "Sending "$CONFILE + fab set_host:$HOST send_confile:$CONFILE + done -# Initialize BigchainDB (i.e. Create the RethinkDB database, -# the tables, the indexes, and genesis glock). Note that -# this will only be sent to one of the nodes, see the -# definition of init_bigchaindb() in fabfile.py to see why. -fab init_bigchaindb + # Initialize BigchainDB (i.e. Create the RethinkDB database, + # the tables, the indexes, and genesis glock). Note that + # this will only be sent to one of the nodes, see the + # definition of init_bigchaindb() in fabfile.py to see why. + fab init_bigchaindb -# Start BigchainDB on all the nodes using "screen" -fab start_bigchaindb + # Start BigchainDB on all the nodes using "screen" + fab start_bigchaindb +else + # Deploying clients + # The only thing to configure on clients is the api_endpoint + # It should be the public DNS name of a BigchainDB server + fab send_client_confile:client_confile + + # Start sending load from the clients to the servers + fab start_bigchaindb_load +fi # cleanup rm add2known_hosts.sh diff --git a/deploy-cluster-aws/fabfile.py b/deploy-cluster-aws/fabfile.py index 807a3536..1d481111 100644 --- a/deploy-cluster-aws/fabfile.py +++ b/deploy-cluster-aws/fabfile.py @@ -146,6 +146,15 @@ def send_confile(confile): run('bigchaindb show-config') +@task +@parallel +def send_client_confile(confile): + put(confile, 'tempfile') + run('mv tempfile ~/.bigchaindb') + print('For this node, bigchaindb show-config says:') + run('bigchaindb show-config') + + # Initialize BigchainDB # i.e. create the database, the tables, # the indexes, and the genesis block. @@ -164,6 +173,12 @@ def start_bigchaindb(): sudo('screen -d -m bigchaindb -y start &', pty=False) +@task +@parallel +def start_bigchaindb_load(): + sudo('screen -d -m bigchaindb load &', pty=False) + + # Install and run New Relic @task def install_newrelic(): From b1683ff824aabe5c55eefc4fed52bd7355954139 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 3 May 2016 21:05:38 +0200 Subject: [PATCH 14/59] Added client_confile to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 27b4f797..99e9739c 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ target/ deploy-cluster-aws/conf/rethinkdb.conf deploy-cluster-aws/hostlist.py deploy-cluster-aws/confiles/ +deploy-cluster-aws/client_confile From 4dd4de120f8296001b58079ef24c54368270b2d0 Mon Sep 17 00:00:00 2001 From: troymc Date: Wed, 4 May 2016 17:10:27 +0200 Subject: [PATCH 15/59] Edits to docs on monitoring --- docs/source/deploy-on-aws.md | 72 +++++++++++++++++++++++------------- docs/source/monitoring.md | 9 +++-- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index 0d23fdeb..a27e686a 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -83,9 +83,53 @@ Add some rules for Inbound traffic: **Note: These rules are extremely lax! They're meant to make testing easy.** You'll want to tighten them up if you intend to have a secure cluster. For example, Source = 0.0.0.0/0 is [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for "allow this traffic to come from _any_ IP address." -## AWS Deployment +## Deploy a BigchainDB Monitor -### AWS Deployment Step 1 +This step is optional. + +One way to monitor a BigchainDB cluster is to use the monitoring setup described in the [Monitoring](monitoring.html) section of this documentation. If you want to do that, then you may want to deploy the monitoring server first, so you can tell your BigchainDB nodes where to send their monitoring data. + +You can deploy a monitoring server on AWS. To do that, go to the AWS EC2 Console and launch an instance: + +1. Choose an AMI: select Ubuntu Server 14.04 LTS. +2. Choose an Instance Type: a t2.micro will suffice. +3. Configure Instance Details: you can accept the defaults, but feel free to change them. +4. Add Storage: A "Root" volume type should already be included. You _could_ store monitoring data there (e.g. in a folder named `/influxdb-data`) but we will attach another volume and store the monitoring data there instead. Select "Add New Volume" and an EBS volume type. +5. Tag Instance: give your instance a memorable name. +6. Configure Security Group: choose your bigchaindb security group. +7. Review and launch your instance. + +When it asks, choose an existing key pair: the one you created earlier (named `bigchaindb`). + +Give your instance some time to launch and become able to accept SSH connections. You can see its current status in the AWS EC2 Console (in the "Instances" section). SSH into your instance using something like: +```text +cd deploy-cluster-aws +ssh -i pem/bigchaindb.pem ubuntu@ec2-52-58-157-229.eu-central-1.compute.amazonaws.com +``` + +where `ec2-52-58-157-229.eu-central-1.compute.amazonaws.com` should be replaced by your new instance's EC2 hostname. (To get that, go to the AWS EC2 Console, select Instances, click on your newly-launched instance, and copy its "Public DNS" name.) + +Next, create a file system on the attached volume, make a directory named `/influxdb-data`, and set the attached volume's mount point to be `/influxdb-data`. For detailed instructions on how to do that, see the AWS documentation for [Making an Amazon EBS Volume Available for Use](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html). + +Then install Docker and Docker Compose: +```text +# in a Python 2.5-2.7 virtual environment where fabric, boto3, etc. are installed +fab --fabfile=fabfile-monitor.py --hosts= install_docker +``` + +After Docker is installed, we can run the monitor with: +```text +fab --fabfile=fabfile-monitor.py --hosts= run_monitor +``` + +For more information about monitoring (e.g. how to view the Grafana dashboard in your web browser), see the [Monitoring](monitoring.html) section of this documentation. + +To configure a BigchainDB node to send monitoring data to the monitoring server, change the statsd host in the configuration of the BigchainDB node. The section on [Configuring a BigchainDB Node](configuration.html) explains how you can do that. (For example, you can change the statsd host in `$HOME/.bigchaindb`.) + + +## Deploy a BigchainDB Cluster + +### Step 1 Suppose _N_ is the number of nodes you want in your BigchainDB cluster. If you already have a set of _N_ BigchainDB configuration files in the `deploy-cluster-aws/confiles` directory, then you can jump to step 2. To create such a set, you can do something like: ```text @@ -99,7 +143,7 @@ That will create three (3) _default_ BigchainDB configuration files in the `depl You can look inside those files if you're curious. In step 2, they'll be modified. For example, the default keyring is an empty list. In step 2, the deployment script automatically changes the keyring of each node to be a list of the public keys of all other nodes. Other changes are also made. -### AWS Deployment Step 2 +### Step 2 Step 2 is to launch the nodes ("instances") on AWS, to install all the necessary software on them, configure the software, run the software, and more. @@ -149,28 +193,6 @@ You have 2 allocated elactic IPs which are not associated with instances (It has Domain = vpc.) ``` -## BigchainDB Monitor Deployment - -The deployment for the monitor requires an aws ec2 instance running Ubuntu 14.04 with a system folder `/influxdb-data`. -This folder can be a [mount point of an EBS volume](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html) -for data persistence. - -With the instance setup we first need to install `docker` and `docker-compose`: -```text -$ fab --fabfile=fabfile-monitor.py --hosts= install_docker -``` - -After docker is installed run the monitor with: -```text -$ fab --fabfile=fabfile-monitor.py --hosts= run_monitor -``` - -To access the monitor follow the instructions on [Monitoring](monitoring.html) replacing `localhost` with the -hostname of the ec2 instance running the monitor. - -To configure bigchaindb to start sending statistics to the monitor change statsd host in the configuration file -(in `$HOME/.bigchaindb` by default) to the hostname of the ec2 instance running the monitor. - ## Known Deployment Issues ### NetworkError diff --git a/docs/source/monitoring.md b/docs/source/monitoring.md index dd3b1c6f..acae3fea 100644 --- a/docs/source/monitoring.md +++ b/docs/source/monitoring.md @@ -16,15 +16,18 @@ $ docker-compose -f docker-compose-monitor.yml build $ docker-compose -f docker-compose-monitor.yml up ``` -It is also possible to mount a host directory as a data volume for influxdb by settings the `INFLUXDB_DATA` env. +It is also possible to mount a host directory as a data volume for InfluxDB +by setting the `INFLUXDB_DATA` environment variable: ```text -$ INFLUXDB_DATA=/data docker-compose -f docker-compose-monitor up +$ INFLUXDB_DATA=/data docker-compose -f docker-compose-monitor.yml up ``` -After starting docker-compose point a browser tab to: +You can view the Grafana dashboard in your web browser at: [http://localhost:3000/dashboard/script/bigchaindb_dashboard.js](http://localhost:3000/dashboard/script/bigchaindb_dashboard.js) +(You may want to replace `localhost` with another hostname in that URL, e.g. the hostname of a remote monitoring server.) + The login and password are `admin` by default. If BigchainDB is running and processing transactions, you should see analytics—if not, [start BigchainDB](installing-server.html#run-bigchaindb) and load some test transactions: ```text $ bigchaindb load From 9e4cab7ffc2e60d6c529ca9aa8fe05f4d27dd7eb Mon Sep 17 00:00:00 2001 From: troymc Date: Wed, 4 May 2016 17:12:18 +0200 Subject: [PATCH 16/59] Minor stylistic/consistency edits to fabfile-monitor.py --- deploy-cluster-aws/fabfile-monitor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/deploy-cluster-aws/fabfile-monitor.py b/deploy-cluster-aws/fabfile-monitor.py index 71c5b100..4d26097b 100644 --- a/deploy-cluster-aws/fabfile-monitor.py +++ b/deploy-cluster-aws/fabfile-monitor.py @@ -1,3 +1,11 @@ +# -*- coding: utf-8 -*- +"""A Fabric fabfile with functionality to install Docker, +install Docker Compose, and run a BigchainDB monitoring server +(using the docker-compose-monitor.yml file) +""" + +from __future__ import with_statement, unicode_literals + from fabric.api import sudo, env from fabric.api import task from fabric.operations import put, run @@ -12,7 +20,7 @@ env.key_filename = 'pem/bigchaindb.pem' @task def install_docker_engine(): - """Install docker on an ec2 ubuntu 14.04 instance + """Install Docker on an EC2 Ubuntu 14.04 instance Example: fab --fabfile=fabfile-monitor.py \ @@ -40,7 +48,7 @@ def install_docker_engine(): @task def install_docker_compose(): - """Install docker-compose on an ec2 ubuntu 14.04 instance + """Install Docker Compose on an EC2 Ubuntu 14.04 instance Example: fab --fabfile=fabfile-monitor.py \ @@ -54,7 +62,7 @@ def install_docker_compose(): @task def install_docker(): - """Install docker and docker-compose on an ec2 ubuntu 14.04 instance + """Install Docker and Docker Compose on an EC2 Ubuntu 14.04 instance Example: fab --fabfile=fabfile-monitor.py \ @@ -67,7 +75,7 @@ def install_docker(): @task def run_monitor(): - """Run bigchaindb monitor on an ec2 ubuntu 14.04 instance + """Run BigchainDB monitor on an EC2 Ubuntu 14.04 instance Example: fab --fabfile=fabfile-monitor.py \ From 8f2541cc6ab6cdfd0653b75dee8a57163a4fcc75 Mon Sep 17 00:00:00 2001 From: troymc Date: Fri, 6 May 2016 10:40:29 +0200 Subject: [PATCH 17/59] Fixed docstring about where Bigchain instance param vals come from --- bigchaindb/core.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index c50ca826..b9a2d6cd 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -25,16 +25,18 @@ class Bigchain(object): consensus_plugin=None): """Initialize the Bigchain instance - There are three ways in which the Bigchain instance can get its parameters. - The order by which the parameters are chosen are: - - 1. Setting them by passing them to the `__init__` method itself. - 2. Setting them as environment variables - 3. Reading them from the `config.json` file. + A Bigchain instance has several configuration parameters (e.g. host). + If a parameter value is passed as an argument to the Bigchain + __init__ method, then that is the value it will have. + Otherwise, the parameter value will be the value from the local + configuration file. If it's not set in that file, then the value + will come from an environment variable. If that environment variable + isn't set, then the paramter will have its default value (defined in + bigchaindb.__init__). Args: - host (str): hostname where the rethinkdb is running. - port (int): port in which rethinkb is running (usually 28015). + host (str): hostname where RethinkDB is running. + port (int): port in which RethinkDB is running (usually 28015). dbname (str): the name of the database to connect to (usually bigchain). public_key (str): the base58 encoded public key for the ED25519 curve. private_key (str): the base58 encoded private key for the ED25519 curve. From be88b264a308b1c670d9423e30830c6abc5cf165 Mon Sep 17 00:00:00 2001 From: troymc Date: Fri, 6 May 2016 14:23:43 +0200 Subject: [PATCH 18/59] Move JSON Serialization docs before Cryptography docs --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ec10c228..8f3070f2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,9 +22,9 @@ Table of Contents python-driver-api-examples local-rethinkdb-cluster deploy-on-aws + json-serialization cryptography models - json-serialization developer-interface consensus monitoring From 886b6fce440681232fa674c2a1c3ee0b06f7007f Mon Sep 17 00:00:00 2001 From: troymc Date: Fri, 6 May 2016 14:31:28 +0200 Subject: [PATCH 19/59] Copy-edited Python Server API Examples section of the docs --- docs/source/python-server-api-examples.md | 53 ++++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index f8a9c003..6f477f6d 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -12,7 +12,6 @@ First, make sure you have RethinkDB and BigchainDB _installed and running_, i.e. ```text $ rethinkdb $ bigchaindb configure -$ bigchaindb init $ bigchaindb start ``` @@ -44,20 +43,20 @@ In BigchainDB, only the federation nodes are allowed to create digital assets, b ```python from bigchaindb import crypto -# create a test user +# Create a test user testuser1_priv, testuser1_pub = crypto.generate_key_pair() -# define a digital asset data payload +# Define a digital asset data payload digital_asset_payload = {'msg': 'Hello BigchainDB!'} -# a create transaction uses the operation `CREATE` and has no inputs +# 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 +# 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, +# 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) ``` @@ -66,7 +65,7 @@ b.write_transaction(tx_signed) After a couple of seconds, we can check if the transactions was included in the bigchain: ```python -# retrieve a transaction from the bigchain +# Retrieve a transaction from the bigchain tx_retrieved = b.get_transaction(tx_signed['id']) tx_retrieved ``` @@ -119,16 +118,16 @@ tx_retrieved The new owner of the digital asset is now `BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs`, 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`. +Note that the current owner (with public key `3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9`) is the federation node which 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 `cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2`. +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 `933cd83a419d2735822a2154c84176a2f419cbd449a74b94e592ab807af23861`. 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`. -Since a transaction can have multiple outputs with each their own (crypto)condition, each transaction input should also refer to the condition index `cid`. +Since a transaction can have multiple outputs with each its own (crypto)condition, each transaction input should also refer to the condition index `cid`. ![BigchainDB transactions connecting fulfillments with conditions](./_static/tx_single_condition_single_fulfillment_v1.png) @@ -230,14 +229,16 @@ DoubleSpend: input `{'cid': 0, 'txid': '933cd83a419d2735822a2154c84176a2f419cbd4 ## Multiple Owners -When creating a transaction to a group of people with shared ownership of the asset, one can simply provide a list of `new_owners`: +To create a new digital asset with _multiple_ owners, one can simply provide a list of `new_owners`: ```python # Create a new asset and assign it to multiple owners tx_multisig = b.create_transaction(b.me, [testuser1_pub, testuser2_pub], None, 'CREATE') -# Have the federation sign the transaction +# Have the federation node sign the transaction tx_multisig_signed = b.sign_transaction(tx_multisig, b.me_private) + +# Write the transaction b.write_transaction(tx_multisig_signed) # Check if the transaction is already in the bigchain @@ -320,7 +321,7 @@ tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], test # Sign with both private keys tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv]) -# Write to bigchain +# Write the transaction b.write_transaction(tx_multisig_transfer_signed) # Check if the transaction is already in the bigchain @@ -330,7 +331,6 @@ tx_multisig_transfer_retrieved ```python { - "assignee":"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9", "id":"e689e23f774e7c562eeb310c7c712b34fb6210bea5deb9175e48b68810029150", "transaction":{ "conditions":[ @@ -394,7 +394,7 @@ owned_mimo_inputs = b.get_owned_ids(testuser1_pub) # Check the number of assets print(len(owned_mimo_inputs)) -# Create a TRANSFER transaction with all the assets +# Create a signed TRANSFER transaction with all the assets tx_mimo = b.create_transaction(testuser1_pub, testuser2_pub, owned_mimo_inputs, 'TRANSFER') tx_mimo_signed = b.sign_transaction(tx_mimo, testuser1_priv) @@ -703,7 +703,6 @@ threshold_tx_transfer ```python { - "assignee":"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9", "id":"a45b2340c59df7422a5788b3c462dee708a18cdf09d1a10bd26be3f31af4b8d7", "transaction":{ "conditions":[ @@ -750,9 +749,13 @@ threshold_tx_transfer ### Hash-locked Conditions -By creating a hash of a difficult-to-guess 256-bit random or pseudo-random integer it is possible to create a condition which the creator can trivially fulfill by publishing the random value. However, for anyone else, the condition is cryptographically hard to fulfill, because they would have to find a preimage for the given condition hash. +A hash-lock condition on an asset is like a password condition: anyone with the secret preimage (like a password) can fulfill the hash-lock condition and transfer the asset to themselves. -One possible usecase might be to redeem a digital voucher when given a secret (voucher code). +Under the hood, fulfilling a hash-lock condition amounts to finding a number (a "preimage") which, when hashed, results in a given value. It's easy to verify that a given preimage hashes to the given value, but it's computationally difficult to _find_ a number which hashes to the given value. The only practical way to get a valid preimage is to get it from the original creator (possibly via intermediaries). + +One possible use case is to distribute preimages as "digital vouchers." The first person to redeem a voucher will get the associated asset. + +A federation node can create an asset with a hash-lock condition and no `new_owners`. Anyone who can fullfill the hash-lock condition can transfer the asset to themselves. ```python # Create a hash-locked asset without any new_owners @@ -771,7 +774,7 @@ hashlock_tx['transaction']['conditions'].append({ 'new_owners': None }) -# Conditions have been updated, so hash needs updating +# Conditions have been updated, so the hash needs updating hashlock_tx['id'] = util.get_hash_data(hashlock_tx) # The asset needs to be signed by the current_owner @@ -787,7 +790,6 @@ hashlock_tx_signed ```python { - "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", "id":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237", "transaction":{ "conditions":[ @@ -817,22 +819,22 @@ hashlock_tx_signed } ``` -In order to redeem the asset, one needs to create a fulfillment the correct secret as a preimage: +In order to redeem the asset, one needs to create a fulfillment with the correct secret: ```python hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() -# create hashlock fulfillment tx +# Create hashlock fulfillment tx hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_pub, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') -# provide a wrong secret +# Provide a wrong secret hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ hashlock_fulfill_tx_fulfillment.serialize_uri() assert b.is_valid_transaction(hashlock_fulfill_tx) == False -# provide the right secret +# Provide the right secret hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ hashlock_fulfill_tx_fulfillment.serialize_uri() @@ -846,7 +848,6 @@ hashlock_fulfill_tx ```python { - "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", "id":"fe6871bf3ca62eb61c52c5555cec2e07af51df817723f0cb76e5cf6248f449d2", "transaction":{ "conditions":[ From 6c4381ad1549575262e14da88a744d76c4add54d Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Fri, 6 May 2016 16:20:13 +0200 Subject: [PATCH 20/59] Small improvements on docs --- docs/source/models.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/models.md b/docs/source/models.md index 1f5f0e0c..1ae84caf 100644 --- a/docs/source/models.md +++ b/docs/source/models.md @@ -27,7 +27,7 @@ A transaction is an operation between the `current_owner` and the `new_owner` ov - `id`: sha3 hash of the transaction. The `id` is also the DB primary key. - `version`: Version of the transaction. For future compability with changes in the transaction model. - **Transaction body**: - - `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to a unspent digital asset + - `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to an unspent digital asset and a _crypto fulfillment_ that satisfies a spending condition set on the unspent digital asset. A _fulfillment_ is usually a signature proving the ownership of the digital asset. See [conditions and fulfillments](models.md#conditions-and-fulfillments) @@ -174,9 +174,9 @@ A block contains a group of transactions and includes the hash of the hash of th - `timestamp`: timestamp when the block was created. It's provided by the node that created the block. - `transactions`: the list of transactions included in the block - `node_pubkey`: the public key of the node that create the block - - `voters`: list public keys of the federation nodes. Since the size of the + - `voters`: list of public keys of the federation nodes. Since the size of the federation may change over time this will tell us how many nodes existed - in the federation when the block was created so that in a later point in + in the federation when the block was created so that at a later point in time we can check that the block received the correct number of votes. - `signature`: Signature of the block by the node that created the block (i.e. To create it, the node serialized the block contents and signed that with its private key) - `votes`: Initially an empty list. New votes are appended as they come in from the nodes. From f159b10cfa47f4cd101f15aa8063c45576d60781 Mon Sep 17 00:00:00 2001 From: troymc Date: Fri, 6 May 2016 17:31:29 +0200 Subject: [PATCH 21/59] Reformatted some docs to be more readable --- docs/source/deploy-on-aws.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index b03643f3..617b7c69 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -160,13 +160,23 @@ cd deploy-cluster-aws ./awsdeploy.sh [pypi_or_branch] [servers_or_clients] ``` -The first argument is required. It's how many nodes you want to deploy. +**** (Required) -The second argument is optional. It can be the word `pypi` or the name of a local Git branch. `pypi` means that BigchainDB will be installed from the latest `bigchaindb` package in the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB will be installed using `pip install bigchaindb`. If you don't include the second argument, then the default is `pypi`. +The number of nodes you want to deploy. Example value: 5 -The third argument is also optional (but if you want to include it, you must also include the second argument). It must be either `servers` or `clients`, depending on what you want to deploy. If you don't include the third argument, then the default is `servers`. +**[pypi_or_branch]** (Optional) -If you're curious what the `awsdeploy.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`awsdeploy.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy.sh) +Where the nodes should get their BigchainDB source code. If it's `pypi`, then BigchainDB will be installed from the latest `bigchaindb` package in the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB will be installed using `pip install bigchaindb`. You can also put the name of a local Git branch; it will be compressed and sent out to all the nodes for installation. If you don't include the second argument, then the default is `pypi`. + +**[servers_or_clients]** (Optional) + +If you want to deploy BigchainDB servers, then the third argument should be `servers`. +If you want to deploy BigchainDB clients, then the third argument should be `clients`. +The third argument is optional, but if you want to include it, you must also include the second argument. If you don't include the third argument, then the default is `servers`. + +--- + +If you're curious what the `awsdeploy.sh` script does, [the source code](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/awsdeploy.sh) has lots of explanatory comments, so it's quite easy to read. It should take a few minutes for the deployment to finish. If you run into problems, see the section on Known Deployment Issues below. From c7205d9820668991c4cf0254388e067658cb5482 Mon Sep 17 00:00:00 2001 From: Joran Kikke Date: Sat, 7 May 2016 11:56:32 +1200 Subject: [PATCH 22/59] Fix broken Readme links --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7865103d..9ab8d576 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit ## Quick Start -### [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) +### [Install and Run BigchainDB Server](http://bigchaindb.readthedocs.io/en/latest/installing-server.html) +### [Run BigchainDB with Docker](http://bigchaindb.readthedocs.io/en/latest/installing-server.html#run-bigchaindb-with-docker) +### [The Python Server API by Example](http://bigchaindb.readthedocs.io/en/latest/python-server-api-examples.html) +### [The Python Driver API by Example](http://bigchaindb.readthedocs.io/en/latest/python-driver-api-examples.html) ## Links for Everyone * [BigchainDB.com](https://www.bigchaindb.com/) - the main BigchainDB website, including newsletter signup @@ -26,7 +26,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/master/) - for developers +* [Documentation](http://bigchaindb.readthedocs.io/en/latest/) - for developers * [CONTRIBUTING.md](CONTRIBUTING.md) - how to contribute * [Community guidelines](CODE_OF_CONDUCT.md) * [Open issues](https://github.com/bigchaindb/bigchaindb/issues) From 644b15a831ad76d7ab5941cf65c9f28577e4653b Mon Sep 17 00:00:00 2001 From: vrde Date: Mon, 9 May 2016 10:07:59 +0200 Subject: [PATCH 23/59] Fix typo --- bigchaindb/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index b9a2d6cd..c463d1d3 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -31,7 +31,7 @@ class Bigchain(object): Otherwise, the parameter value will be the value from the local configuration file. If it's not set in that file, then the value will come from an environment variable. If that environment variable - isn't set, then the paramter will have its default value (defined in + isn't set, then the parameter will have its default value (defined in bigchaindb.__init__). Args: From 72bc7cb1d0116dad0f79aa857539cc69e7059524 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 9 May 2016 13:17:49 +0200 Subject: [PATCH 24/59] Don't bigchaindb start all nodes automatically once deployed --- deploy-cluster-aws/awsdeploy.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/deploy-cluster-aws/awsdeploy.sh b/deploy-cluster-aws/awsdeploy.sh index 46a64928..3ce621d9 100755 --- a/deploy-cluster-aws/awsdeploy.sh +++ b/deploy-cluster-aws/awsdeploy.sh @@ -131,9 +131,6 @@ if [ "$WHAT_TO_DEPLOY" == "servers" ]; then # this will only be sent to one of the nodes, see the # definition of init_bigchaindb() in fabfile.py to see why. fab init_bigchaindb - - # Start BigchainDB on all the nodes using "screen" - fab start_bigchaindb else # Deploying clients # The only thing to configure on clients is the api_endpoint From fd6009b4f3e8912482450ad82d0c2d266940ad68 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 9 May 2016 13:18:36 +0200 Subject: [PATCH 25/59] Updated docs to say how to start BigchainDB on all nodes --- docs/source/deploy-on-aws.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index 617b7c69..d7efd219 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -153,6 +153,7 @@ Here's an example of how one could launch a BigchainDB cluster of three (3) node cd bigchaindb cd deploy-cluster-aws ./awsdeploy.sh 3 +fab start_bigchaindb ``` `awsdeploy.sh` is a Bash script which calls some Python and Fabric scripts. The usage is: From 9f959fc6ed50cc2e6398dfd2ac8b066638ba3e56 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 9 May 2016 16:45:09 +0200 Subject: [PATCH 26/59] Add command to configure number of shards. Changed aws deployment script to automatically set the number of shards. Created tests --- bigchaindb/commands/bigchain.py | 14 ++++++++++++++ deploy-cluster-aws/awsdeploy.sh | 1 + deploy-cluster-aws/fabfile.py | 6 ++++++ docs/source/bigchaindb-cli.md | 7 +++++++ tests/test_commands.py | 22 +++++++++++++++++++++- 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index b37a3812..e6b4314d 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -13,6 +13,7 @@ import builtins import logstats +import rethinkdb as r import bigchaindb import bigchaindb.config_utils @@ -203,6 +204,12 @@ def run_load(args): workers.start() +def run_sharding(args): + b = bigchaindb.Bigchain() + r.table('bigchain').reconfigure(shards=args.num_shards, replicas=1).run(b.conn) + r.table('backlog').reconfigure(shards=args.num_shards, replicas=1).run(b.conn) + + def main(): parser = argparse.ArgumentParser( description='Control your BigchainDB node.', @@ -243,6 +250,13 @@ def main(): subparsers.add_parser('start', help='Start BigchainDB') + # parser for configuring the number of shards + sharding_parser = subparsers.add_parser('sharding', + help='Configure number of shards') + + sharding_parser.add_argument('num_shards', metavar='num_shards', type=int, default=1, + help='Number of shards') + load_parser = subparsers.add_parser('load', help='Write transactions to the backlog') diff --git a/deploy-cluster-aws/awsdeploy.sh b/deploy-cluster-aws/awsdeploy.sh index 3ce621d9..b68b0a5e 100755 --- a/deploy-cluster-aws/awsdeploy.sh +++ b/deploy-cluster-aws/awsdeploy.sh @@ -131,6 +131,7 @@ if [ "$WHAT_TO_DEPLOY" == "servers" ]; then # this will only be sent to one of the nodes, see the # definition of init_bigchaindb() in fabfile.py to see why. fab init_bigchaindb + fab configure_sharding:$NUM_NODES else # Deploying clients # The only thing to configure on clients is the api_endpoint diff --git a/deploy-cluster-aws/fabfile.py b/deploy-cluster-aws/fabfile.py index 1d481111..581a1c1b 100644 --- a/deploy-cluster-aws/fabfile.py +++ b/deploy-cluster-aws/fabfile.py @@ -166,6 +166,12 @@ def init_bigchaindb(): run('bigchaindb init', pty=False) +@task +@hosts(public_dns_names[0]) +def configure_sharding(num_shards): + run('bigchaindb sharding {}'.format(num_shards)) + + # Start BigchainDB using screen @task @parallel diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index 229d5b4c..43f8dbb0 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -43,3 +43,10 @@ This command is used to run benchmarking tests. You can learn more about it usin ```text $ bigchaindb load -h ``` + +### bigchaindb sharding + +This command is used to configure the number of shards in the underlying datastore, for example: +```text +$ bigchaindb sharding 3 +``` \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index 99a4f466..f90bc503 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,11 +1,12 @@ import json from unittest.mock import Mock, patch from argparse import Namespace -from pprint import pprint import copy import pytest +from tests.db.conftest import setup_database + @pytest.fixture def mock_run_configure(monkeypatch): @@ -225,3 +226,22 @@ def test_start_rethinkdb_exits_when_cannot_start(mock_popen): with pytest.raises(exceptions.StartupError): utils.start_rethinkdb() + +def test_configure_sharding(b): + import rethinkdb as r + from bigchaindb.commands.bigchain import run_sharding + + # change number of shards + args = Namespace(num_shards=3) + run_sharding(args) + + # retrieve table configuration + table_config = list(r.db('rethinkdb') + .table('table_config') + .filter(r.row['db'] == b.dbname) + .run(b.conn)) + + # check shard configuration + for table in table_config: + if table['name'] in ['backlog', 'bigchain']: + assert len(table['shards']) == 3 From 1d4eddf635d6f23074d3be3690a74ce6dba66287 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 9 May 2016 17:27:16 +0200 Subject: [PATCH 27/59] Created templates and util functions for benchmark testing. --- benchmarking-tests/benchmark_utils.py | 26 ++++++++++++++++++ benchmarking-tests/fabfile.py | 39 +++++++++++++++++++++++++++ benchmarking-tests/hostlist.py | 8 ++++++ benchmarking-tests/test1/README.md | 11 ++++++++ 4 files changed, 84 insertions(+) create mode 100644 benchmarking-tests/benchmark_utils.py create mode 100644 benchmarking-tests/fabfile.py create mode 100644 benchmarking-tests/hostlist.py create mode 100644 benchmarking-tests/test1/README.md diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py new file mode 100644 index 00000000..50ea586b --- /dev/null +++ b/benchmarking-tests/benchmark_utils.py @@ -0,0 +1,26 @@ +import sys +import multiprocessing as mp +import uuid +from bigchaindb import Bigchain +from bigchaindb.util import ProcessGroup + + +def create_write_transaction(tx_left): + b = Bigchain() + while tx_left > 0: + # use uuid to prevent duplicate transactions (transactions with the same hash) + tx = b.create_transaction(b.me, b.me, None, 'CREATE', payload={'msg': str(uuid.uuid4())}) + tx_signed = b.sign_transaction(tx, b.me_private) + b.write_transaction(tx_signed) + tx_left -= 1 + + +def add_to_backlog(num_transactions=10000): + tx_left = num_transactions // mp.cpu_count() + workers = ProcessGroup(target=create_write_transaction, args=(tx_left,)) + workers.start() + + +if __name__ == '__main__': + add_to_backlog(int(sys.argv[1])) + diff --git a/benchmarking-tests/fabfile.py b/benchmarking-tests/fabfile.py new file mode 100644 index 00000000..71c682c1 --- /dev/null +++ b/benchmarking-tests/fabfile.py @@ -0,0 +1,39 @@ +from __future__ import with_statement, unicode_literals + +from fabric.api import sudo, env, hosts +from fabric.api import task, parallel +from fabric.contrib.files import sed +from fabric.operations import run, put +from fabric.context_managers import settings + +from hostlist import public_dns_names + +# 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' + + +@task +@parallel +def prepare_test(): + put('benchmark_utils.py') + + +@task +@parallel +def prepare_backlog(num_transactions=10000): + run('python3 benchmark_utils.py {}'.format(num_transactions)) + + +@task +@parallel +def start_bigchaindb(): + run('screen -d -m bigchaindb start &', pty=False) diff --git a/benchmarking-tests/hostlist.py b/benchmarking-tests/hostlist.py new file mode 100644 index 00000000..00a48196 --- /dev/null +++ b/benchmarking-tests/hostlist.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""A list of the public DNS names of all the nodes in this +BigchainDB cluster/federation. +""" + +from __future__ import unicode_literals + +public_dns_names = ['ec2-52-58-162-146.eu-central-1.compute.amazonaws.com', 'ec2-52-58-15-239.eu-central-1.compute.amazonaws.com', 'ec2-52-58-160-205.eu-central-1.compute.amazonaws.com'] diff --git a/benchmarking-tests/test1/README.md b/benchmarking-tests/test1/README.md new file mode 100644 index 00000000..1b186a94 --- /dev/null +++ b/benchmarking-tests/test1/README.md @@ -0,0 +1,11 @@ +# Transactions per second + +Measure how many blocks per second are created on the _bigchain_ with a pre filled backlog. + +1. Deploy an aws cluster http://bigchaindb.readthedocs.io/en/latest/deploy-on-aws.html +2. Copy `deploy-cluster-aws/hostlist.py` to `benchmarking-tests` + +```bash +fab prepare_test +fab prepare_backlog: # wait for process to finish +fab start_bigchaindb \ No newline at end of file From f09dacdd118596cf3cbe36394a647e66459a81c0 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 9 May 2016 17:31:02 +0200 Subject: [PATCH 28/59] renamed sharding to set-shards --- bigchaindb/commands/bigchain.py | 4 ++-- tests/test_commands.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index e6b4314d..28831642 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -204,7 +204,7 @@ def run_load(args): workers.start() -def run_sharding(args): +def run_set_shards(args): b = bigchaindb.Bigchain() r.table('bigchain').reconfigure(shards=args.num_shards, replicas=1).run(b.conn) r.table('backlog').reconfigure(shards=args.num_shards, replicas=1).run(b.conn) @@ -251,7 +251,7 @@ def main(): help='Start BigchainDB') # parser for configuring the number of shards - sharding_parser = subparsers.add_parser('sharding', + sharding_parser = subparsers.add_parser('set-shards', help='Configure number of shards') sharding_parser.add_argument('num_shards', metavar='num_shards', type=int, default=1, diff --git a/tests/test_commands.py b/tests/test_commands.py index f90bc503..4b35edbb 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -229,11 +229,11 @@ def test_start_rethinkdb_exits_when_cannot_start(mock_popen): def test_configure_sharding(b): import rethinkdb as r - from bigchaindb.commands.bigchain import run_sharding + from bigchaindb.commands.bigchain import run_set_shards # change number of shards args = Namespace(num_shards=3) - run_sharding(args) + run_set_shards(args) # retrieve table configuration table_config = list(r.db('rethinkdb') From e21282818262974a9224183d9ecf607452467736 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 9 May 2016 18:13:20 +0200 Subject: [PATCH 29/59] Updated awsdeploy, fabfile, docs, test for bigchaindb set-shards --- deploy-cluster-aws/awsdeploy.sh | 2 +- deploy-cluster-aws/fabfile.py | 5 +++-- docs/source/bigchaindb-cli.md | 6 +++--- tests/test_commands.py | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/deploy-cluster-aws/awsdeploy.sh b/deploy-cluster-aws/awsdeploy.sh index b68b0a5e..6290d01e 100755 --- a/deploy-cluster-aws/awsdeploy.sh +++ b/deploy-cluster-aws/awsdeploy.sh @@ -131,7 +131,7 @@ if [ "$WHAT_TO_DEPLOY" == "servers" ]; then # this will only be sent to one of the nodes, see the # definition of init_bigchaindb() in fabfile.py to see why. fab init_bigchaindb - fab configure_sharding:$NUM_NODES + fab set_shards:$NUM_NODES else # Deploying clients # The only thing to configure on clients is the api_endpoint diff --git a/deploy-cluster-aws/fabfile.py b/deploy-cluster-aws/fabfile.py index 581a1c1b..9e4a1d47 100644 --- a/deploy-cluster-aws/fabfile.py +++ b/deploy-cluster-aws/fabfile.py @@ -166,10 +166,11 @@ def init_bigchaindb(): run('bigchaindb init', pty=False) +# Set the number of shards (in the backlog and bigchain tables) @task @hosts(public_dns_names[0]) -def configure_sharding(num_shards): - run('bigchaindb sharding {}'.format(num_shards)) +def set_shards(num_shards): + run('bigchaindb set-shards {}'.format(num_shards)) # Start BigchainDB using screen diff --git a/docs/source/bigchaindb-cli.md b/docs/source/bigchaindb-cli.md index 43f8dbb0..ad9fd5a0 100644 --- a/docs/source/bigchaindb-cli.md +++ b/docs/source/bigchaindb-cli.md @@ -44,9 +44,9 @@ This command is used to run benchmarking tests. You can learn more about it usin $ bigchaindb load -h ``` -### bigchaindb sharding +### bigchaindb set-shards -This command is used to configure the number of shards in the underlying datastore, for example: +This command is used to set the number of shards in the underlying datastore. For example, the following command will set the number of shards to four: ```text -$ bigchaindb sharding 3 +$ bigchaindb set-shards 4 ``` \ No newline at end of file diff --git a/tests/test_commands.py b/tests/test_commands.py index 4b35edbb..12c1350e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -227,11 +227,11 @@ def test_start_rethinkdb_exits_when_cannot_start(mock_popen): utils.start_rethinkdb() -def test_configure_sharding(b): +def test_set_shards(b): import rethinkdb as r from bigchaindb.commands.bigchain import run_set_shards - # change number of shards + # set the number of shards args = Namespace(num_shards=3) run_set_shards(args) @@ -241,7 +241,7 @@ def test_configure_sharding(b): .filter(r.row['db'] == b.dbname) .run(b.conn)) - # check shard configuration + # check that the number of shards got set to the correct value for table in table_config: if table['name'] in ['backlog', 'bigchain']: assert len(table['shards']) == 3 From 01c19d6f40cc8b2cdc22284177f717787645dce6 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 10:13:32 +0200 Subject: [PATCH 30/59] Simplify import statement --- deploy-cluster-aws/launch_ec2_nodes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deploy-cluster-aws/launch_ec2_nodes.py b/deploy-cluster-aws/launch_ec2_nodes.py index 868a732c..2196114c 100644 --- a/deploy-cluster-aws/launch_ec2_nodes.py +++ b/deploy-cluster-aws/launch_ec2_nodes.py @@ -18,9 +18,8 @@ import socket import argparse import botocore import boto3 -from awscommon import ( - get_naeips, -) +from awscommon import get_naeips + # First, ensure they're using Python 2.5-2.7 pyver = sys.version_info From 4b447828054b405119e0658855b7ecc265812c43 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 10:47:44 +0200 Subject: [PATCH 31/59] Docs: a preimage can be any arbitrary string --- 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 6f477f6d..aa8e9981 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -751,7 +751,7 @@ threshold_tx_transfer A hash-lock condition on an asset is like a password condition: anyone with the secret preimage (like a password) can fulfill the hash-lock condition and transfer the asset to themselves. -Under the hood, fulfilling a hash-lock condition amounts to finding a number (a "preimage") which, when hashed, results in a given value. It's easy to verify that a given preimage hashes to the given value, but it's computationally difficult to _find_ a number which hashes to the given value. The only practical way to get a valid preimage is to get it from the original creator (possibly via intermediaries). +Under the hood, fulfilling a hash-lock condition amounts to finding a string (a "preimage") which, when hashed, results in a given value. It's easy to verify that a given preimage hashes to the given value, but it's computationally difficult to _find_ a string which hashes to the given value. The only practical way to get a valid preimage is to get it from the original creator (possibly via intermediaries). One possible use case is to distribute preimages as "digital vouchers." The first person to redeem a voucher will get the associated asset. From 9b571f52ceebc9f2985125cf84f73657d099ebef Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 10:55:48 +0200 Subject: [PATCH 32/59] Added deploy-cluster-aws/keypairs.py to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 99e9739c..53a6f5b8 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ deploy-cluster-aws/conf/rethinkdb.conf deploy-cluster-aws/hostlist.py deploy-cluster-aws/confiles/ deploy-cluster-aws/client_confile +deploy-cluster-aws/keypairs.py From 37437e8298c8867d8d499bf0e4fa4718b86faa68 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 11:22:36 +0200 Subject: [PATCH 33/59] Created write_keypairs_file.py --- deploy-cluster-aws/write_keypairs_file.py | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 deploy-cluster-aws/write_keypairs_file.py diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py new file mode 100644 index 00000000..379f30de --- /dev/null +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -0,0 +1,41 @@ +"""A Python 3 script to write a file with a specified number +of keypairs, using bigchaindb.crypto.generate_key_pair() +The file is always named keypairs.py (and is Python 2). + +Usage: + python3 write_keypairs_file.py +""" + +import argparse + +from bigchaindb import crypto + + +# Parse the command-line arguments +desc = 'Write a set of keypairs to keypairs.py' +parser = argparse.ArgumentParser(description=desc) +parser.add_argument('num_pairs', + help='number of keypairs to write', + type=int) +args = parser.parse_args() +num_pairs = int(args.num_pairs) + +# Generate and write the keypairs to keypairs.py +print('Writing {} keypairs to keypairs.py...'.format(num_pairs)) +with open('keypairs.py', 'w') as f: + f.write('# -*- coding: utf-8 -*-\n') + f.write('"""A set of public/private keypairs for use in deploying\n') + f.write('BigchainDB servers with a predictable set of keys.\n') + f.write('"""\n') + f.write('\n') + f.write('from __future__ import unicode_literals\n') + f.write('\n') + f.write('keypairs_list = [') + for pair_num in range(num_pairs): + keypair = crypto.generate_key_pair() + spacer = '' if pair_num == 0 else ' ' + f.write("{}('{}',\n '{}'),\n".format( + spacer, keypair[0], keypair[1])) + f.write(' ]\n') + +print('Done.') From 49710c59588abef00771b51dd809d802f2f7e457 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 11:34:00 +0200 Subject: [PATCH 34/59] write_keypairs_file.py now writes a Python 3 script --- deploy-cluster-aws/write_keypairs_file.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py index 379f30de..719685e8 100644 --- a/deploy-cluster-aws/write_keypairs_file.py +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -1,9 +1,16 @@ """A Python 3 script to write a file with a specified number of keypairs, using bigchaindb.crypto.generate_key_pair() -The file is always named keypairs.py (and is Python 2). +The file is always named keypairs.py and it should be interpreted +as a Python 3 script. Usage: - python3 write_keypairs_file.py + $ python3 write_keypairs_file.py + +Using the list in other Python scripts: + from keypairs import keypairs_list + # keypairs_list is a list of (sk, pk) tuples + # sk = signing key (private key) + # pk = verifying key (public key) """ import argparse @@ -23,13 +30,9 @@ num_pairs = int(args.num_pairs) # Generate and write the keypairs to keypairs.py print('Writing {} keypairs to keypairs.py...'.format(num_pairs)) with open('keypairs.py', 'w') as f: - f.write('# -*- coding: utf-8 -*-\n') f.write('"""A set of public/private keypairs for use in deploying\n') f.write('BigchainDB servers with a predictable set of keys.\n') - f.write('"""\n') - f.write('\n') - f.write('from __future__ import unicode_literals\n') - f.write('\n') + f.write('"""\n\n') f.write('keypairs_list = [') for pair_num in range(num_pairs): keypair = crypto.generate_key_pair() From 6e285c95109626f28dbc676ff51c7c36e4559c90 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 10 May 2016 11:44:48 +0200 Subject: [PATCH 35/59] updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 99e9739c..b1d9188c 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ deploy-cluster-aws/conf/rethinkdb.conf deploy-cluster-aws/hostlist.py deploy-cluster-aws/confiles/ deploy-cluster-aws/client_confile +benchmarking-tests/hostlist.py From 32b5ea4cbb54f6fcc6b06f9e340731c02236d7ab Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 11:44:58 +0200 Subject: [PATCH 36/59] Minor edit to write_keypairs_file.py --- deploy-cluster-aws/write_keypairs_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py index 719685e8..92e05584 100644 --- a/deploy-cluster-aws/write_keypairs_file.py +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -30,7 +30,7 @@ num_pairs = int(args.num_pairs) # Generate and write the keypairs to keypairs.py print('Writing {} keypairs to keypairs.py...'.format(num_pairs)) with open('keypairs.py', 'w') as f: - f.write('"""A set of public/private keypairs for use in deploying\n') + f.write('"""A set of keypairs for use in deploying\n') f.write('BigchainDB servers with a predictable set of keys.\n') f.write('"""\n\n') f.write('keypairs_list = [') From e2fd574c6e8e24b442f54e7dc878502f686ce49b Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 10 May 2016 11:47:49 +0200 Subject: [PATCH 37/59] Added cli to benchmark_utils --- benchmarking-tests/benchmark_utils.py | 43 +++++++++++++++++++++++---- benchmarking-tests/fabfile.py | 16 +++++++++- benchmarking-tests/hostlist.py | 2 +- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index 50ea586b..38017e2f 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -1,8 +1,13 @@ -import sys import multiprocessing as mp import uuid +import json +import argparse + +from os.path import expanduser + from bigchaindb import Bigchain from bigchaindb.util import ProcessGroup +from bigchaindb.commands import utils def create_write_transaction(tx_left): @@ -15,12 +20,40 @@ def create_write_transaction(tx_left): tx_left -= 1 -def add_to_backlog(num_transactions=10000): - tx_left = num_transactions // mp.cpu_count() +def run_add_backlog(args): + tx_left = args.num_transactions // mp.cpu_count() workers = ProcessGroup(target=create_write_transaction, args=(tx_left,)) workers.start() -if __name__ == '__main__': - add_to_backlog(int(sys.argv[1])) +def run_update_statsd(args): + with open(expanduser('~') + '/.bigchaindb', 'r') as f: + conf = json.load(f) + + conf['statsd']['host'] = args.statsd_host + with open(expanduser('~') + '/.bigchaindb', 'w') as f: + json.dump(conf, f) + + +def main(): + parser = argparse.ArgumentParser(description='BigchainDB benchmarking utils') + subparsers = parser.add_subparsers(title='Commands', dest='command') + + # add transactions to backlog + backlog_parser = subparsers.add_parser('add-backlog', + help='Add transactions to the backlog') + backlog_parser.add_argument('num_transactions', metavar='num_transactions', type=int, default=0, + help='Number of transactions to add to the backlog') + + # update statsd configuration + statsd_parser = subparsers.add_parser('update-statsd', + help='Update statsd host') + statsd_parser.add_argument('statsd_host', metavar='statsd_host', default='localhost', + help='Hostname of the statsd server') + + utils.start(parser, globals()) + + +if __name__ == '__main__': + main() diff --git a/benchmarking-tests/fabfile.py b/benchmarking-tests/fabfile.py index 71c682c1..770ed218 100644 --- a/benchmarking-tests/fabfile.py +++ b/benchmarking-tests/fabfile.py @@ -27,13 +27,27 @@ def prepare_test(): put('benchmark_utils.py') +@task +@parallel +def update_statsd_conf(statsd_host='localhost'): + run('python3 benchmark_utils.py update-statsd {}'.format(statsd_host)) + print('update configuration') + run('bigchaindb show-config') + + @task @parallel def prepare_backlog(num_transactions=10000): - run('python3 benchmark_utils.py {}'.format(num_transactions)) + run('python3 benchmark_utils.py add-backlog {}'.format(num_transactions)) @task @parallel def start_bigchaindb(): run('screen -d -m bigchaindb start &', pty=False) + + +@task +@parallel +def kill_bigchaindb(): + run('killall bigchaindb') diff --git a/benchmarking-tests/hostlist.py b/benchmarking-tests/hostlist.py index 00a48196..4d3d1641 100644 --- a/benchmarking-tests/hostlist.py +++ b/benchmarking-tests/hostlist.py @@ -5,4 +5,4 @@ BigchainDB cluster/federation. from __future__ import unicode_literals -public_dns_names = ['ec2-52-58-162-146.eu-central-1.compute.amazonaws.com', 'ec2-52-58-15-239.eu-central-1.compute.amazonaws.com', 'ec2-52-58-160-205.eu-central-1.compute.amazonaws.com'] +public_dns_names = ['ec2-52-58-110-9.eu-central-1.compute.amazonaws.com', 'ec2-52-58-154-94.eu-central-1.compute.amazonaws.com', 'ec2-52-58-166-93.eu-central-1.compute.amazonaws.com'] From 37c34f7383a6e52c386af5abb105f0a5f334fcf3 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 10 May 2016 11:48:27 +0200 Subject: [PATCH 38/59] remove tmp file --- benchmarking-tests/hostlist.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 benchmarking-tests/hostlist.py diff --git a/benchmarking-tests/hostlist.py b/benchmarking-tests/hostlist.py deleted file mode 100644 index 4d3d1641..00000000 --- a/benchmarking-tests/hostlist.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -"""A list of the public DNS names of all the nodes in this -BigchainDB cluster/federation. -""" - -from __future__ import unicode_literals - -public_dns_names = ['ec2-52-58-110-9.eu-central-1.compute.amazonaws.com', 'ec2-52-58-154-94.eu-central-1.compute.amazonaws.com', 'ec2-52-58-166-93.eu-central-1.compute.amazonaws.com'] From d735d677fd034549beba71f086b898a06715aa00 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 10 May 2016 12:04:32 +0200 Subject: [PATCH 39/59] updated test README --- benchmarking-tests/test1/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmarking-tests/test1/README.md b/benchmarking-tests/test1/README.md index 1b186a94..4cc96d8a 100644 --- a/benchmarking-tests/test1/README.md +++ b/benchmarking-tests/test1/README.md @@ -7,5 +7,6 @@ Measure how many blocks per second are created on the _bigchain_ with a pre fill ```bash fab prepare_test +fab update_statsd_conf: fab prepare_backlog: # wait for process to finish fab start_bigchaindb \ No newline at end of file From 17cee6dcae08fccd15e6222b660bc182a489338d Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 13:27:45 +0200 Subject: [PATCH 40/59] Made keypairs.py a Python 2 script again --- deploy-cluster-aws/write_keypairs_file.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py index 92e05584..cc36ecd5 100644 --- a/deploy-cluster-aws/write_keypairs_file.py +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -1,12 +1,13 @@ """A Python 3 script to write a file with a specified number of keypairs, using bigchaindb.crypto.generate_key_pair() -The file is always named keypairs.py and it should be interpreted -as a Python 3 script. +The written file is always named keypairs.py and it should be +interpreted as a Python 2 script. Usage: $ python3 write_keypairs_file.py Using the list in other Python scripts: + # in a Python 2 script: from keypairs import keypairs_list # keypairs_list is a list of (sk, pk) tuples # sk = signing key (private key) @@ -30,9 +31,13 @@ num_pairs = int(args.num_pairs) # Generate and write the keypairs to keypairs.py print('Writing {} keypairs to keypairs.py...'.format(num_pairs)) with open('keypairs.py', 'w') as f: + f.write('# -*- coding: utf-8 -*-\n') f.write('"""A set of keypairs for use in deploying\n') f.write('BigchainDB servers with a predictable set of keys.\n') - f.write('"""\n\n') + f.write('"""\n') + f.write('\n') + f.write('from __future__ import unicode_literals\n') + f.write('\n') f.write('keypairs_list = [') for pair_num in range(num_pairs): keypair = crypto.generate_key_pair() From 1d87afab0ad5256e8c9afa55b3bba7ff6428b8f2 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 14:42:38 +0200 Subject: [PATCH 41/59] Added -k option to clusterize_confiles.py --- deploy-cluster-aws/clusterize_confiles.py | 75 +++++++++++++++++------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/deploy-cluster-aws/clusterize_confiles.py b/deploy-cluster-aws/clusterize_confiles.py index 498953e5..d1fcb6ee 100644 --- a/deploy-cluster-aws/clusterize_confiles.py +++ b/deploy-cluster-aws/clusterize_confiles.py @@ -1,58 +1,95 @@ # -*- coding: utf-8 -*- """Given a directory full of default BigchainDB config files, transform them into config files for a cluster with proper -keyrings, API endpoint values, etc. +keyrings, API endpoint values, etc. This script is meant to +be interpreted as a Python 2 script. -Note: This script assumes that there is a file named hostlist.py +Note 1: This script assumes that there is a file named hostlist.py containing public_dns_names = a list of the public DNS names of all the hosts in the cluster. +Note 2: If the optional -k argument is included, then a keypairs.py +file must exist and must have enough keypairs in it to assign one +to each of the config files in the directory of config files. +You can create a keypairs.py file using write_keypairs_file.py + Usage: - python clusterize_confiles.py + python clusterize_confiles.py [-h] [-k] dir number_of_files """ from __future__ import unicode_literals + import os import json import argparse from hostlist import public_dns_names +if os.path.isfile('keypairs.py'): + from keypairs import keypairs_list + # Parse the command-line arguments -parser = argparse.ArgumentParser() +desc = 'Transform a directory of default BigchainDB config files ' +desc += 'into config files for a cluster' +parser = argparse.ArgumentParser(description=desc) parser.add_argument('dir', help='Directory containing the config files') parser.add_argument('number_of_files', help='Number of config files expected in dir', type=int) +parser.add_argument('-k', '--use-keypairs', + action='store_true', + default=False, + help='Use public and private keys from keypairs.py') args = parser.parse_args() conf_dir = args.dir -numfiles_expected = int(args.number_of_files) +num_files_expected = int(args.number_of_files) +use_keypairs = args.use_keypairs # Check if the number of files in conf_dir is what was expected -conf_files = os.listdir(conf_dir) -numfiles = len(conf_files) -if numfiles != numfiles_expected: +conf_files = sorted(os.listdir(conf_dir)) +num_files = len(conf_files) +if num_files != num_files_expected: raise ValueError('There are {} files in {} but {} were expected'. - format(numfiles, conf_dir, numfiles_expected)) + format(num_files, conf_dir, num_files_expected)) -# Make a list containing all the public keys from -# all the config files -pubkeys = [] -for filename in conf_files: - file_path = os.path.join(conf_dir, filename) - with open(file_path, 'r') as f: - conf_dict = json.load(f) - pubkey = conf_dict['keypair']['public'] - pubkeys.append(pubkey) +# If the -k option was included, check to make sure there are enough keypairs +# in keypairs_list +num_keypairs = len(keypairs_list) +if use_keypairs: + if num_keypairs < num_files: + raise ValueError('There are {} config files in {} but ' + 'there are only {} keypairs in keypairs.py'. + format(num_files, conf_dir, num_keypairs)) + +# Make a list containing all the public keys +if use_keypairs: + print('Using keypairs from keypairs.py') + pubkeys = [keypair[1] for keypair in keypairs_list] +else: + # read the pubkeys from the config files in conf_dir + pubkeys = [] + for filename in conf_files: + file_path = os.path.join(conf_dir, filename) + with open(file_path, 'r') as f: + conf_dict = json.load(f) + pubkey = conf_dict['keypair']['public'] + pubkeys.append(pubkey) # Rewrite each config file, one at a time for i, filename in enumerate(conf_files): file_path = os.path.join(conf_dir, filename) with open(file_path, 'r') as f: conf_dict = json.load(f) + # If the -k option was included + # then replace the private and public keys + # with those from keypairs_list + if use_keypairs: + keypair = keypairs_list[i] + conf_dict['keypair']['private'] = keypair[0] + conf_dict['keypair']['public'] = keypair[1] # The keyring is the list of *all* public keys # minus the config file's own public key keyring = list(pubkeys) @@ -64,8 +101,10 @@ for i, filename in enumerate(conf_files): # Set the api_endpoint conf_dict['api_endpoint'] = 'http://' + public_dns_names[i] + \ ':9984/api/v1' + # Delete the config file os.remove(file_path) + # Write new config file with the same filename print('Rewriting {}'.format(file_path)) with open(file_path, 'w') as f2: From 11576a32a1dced19182a9a1457ef54b09162dd18 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 15:13:34 +0200 Subject: [PATCH 42/59] Minor edit to write_keypairs_file.py --- deploy-cluster-aws/write_keypairs_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py index cc36ecd5..6af96e00 100644 --- a/deploy-cluster-aws/write_keypairs_file.py +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -4,7 +4,7 @@ The written file is always named keypairs.py and it should be interpreted as a Python 2 script. Usage: - $ python3 write_keypairs_file.py + $ python3 write_keypairs_file.py num_pairs Using the list in other Python scripts: # in a Python 2 script: From 3595da4deea2b205481ae629ac79588a110b1704 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 15:15:03 +0200 Subject: [PATCH 43/59] Docs about generating and using keypairs from a file --- docs/source/deploy-on-aws.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/deploy-on-aws.md b/docs/source/deploy-on-aws.md index d7efd219..8fd9b840 100644 --- a/docs/source/deploy-on-aws.md +++ b/docs/source/deploy-on-aws.md @@ -143,6 +143,18 @@ That will create three (3) _default_ BigchainDB configuration files in the `depl You can look inside those files if you're curious. In step 2, they'll be modified. For example, the default keyring is an empty list. In step 2, the deployment script automatically changes the keyring of each node to be a list of the public keys of all other nodes. Other changes are also made. +**An Aside on Using a Standard Set of Keypairs** + +It's possible to deploy BigchainDB servers with a known set of keypairs. You can generate a set of keypairs in a file named `keypairs.py` using the `write_keypairs_file.py` script. For example: +```text +# in a Python 3 virtual environment where bigchaindb is installed +cd bigchaindb +cd deploy-cluster-aws +python3 write_keypairs_file.py 100 +``` + +The above command generates a file with 100 keypairs. (You can generate more keypairs than you need, so you can use the same list over and over again, for different numbers of servers.) To make the `awsdeploy.sh` script read all keys from `keypairs.py`, you must _edit_ the `awsdeploy.sh` script: change the line that says `python clusterize_confiles.py confiles $NUM_NODES` to `python clusterize_confiles.py -k confiles $NUM_NODES` (i.e. add the `-k` option). + ### Step 2 Step 2 is to launch the nodes ("instances") on AWS, to install all the necessary software on them, configure the software, run the software, and more. From 675a0ff30b8f0506b4205b565735a083d64c38af Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 15:53:20 +0200 Subject: [PATCH 44/59] Commit a 128-keypair keypairs.py file for testing uses --- .gitignore | 1 - deploy-cluster-aws/keypairs.py | 264 +++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 deploy-cluster-aws/keypairs.py diff --git a/.gitignore b/.gitignore index 53a6f5b8..99e9739c 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,3 @@ deploy-cluster-aws/conf/rethinkdb.conf deploy-cluster-aws/hostlist.py deploy-cluster-aws/confiles/ deploy-cluster-aws/client_confile -deploy-cluster-aws/keypairs.py diff --git a/deploy-cluster-aws/keypairs.py b/deploy-cluster-aws/keypairs.py new file mode 100644 index 00000000..f10be7b8 --- /dev/null +++ b/deploy-cluster-aws/keypairs.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +"""A set of keypairs for use in deploying +BigchainDB servers with a predictable set of keys. +""" + +from __future__ import unicode_literals + +keypairs_list = [('E72MmhHGiwywGMHdensZreNPTtAKvRxYQEyQEqUpLvXL', + 'Ar1Xt6bpmeyNnWoBUAAi8VqLPboK84bvB417FKmxcJzp'), + ('BYupx6PLnAqcTgrqsYopKeHYjmSY5F4rpSVoFv6vK3r6', + '6UkRsEhRW7RT6WYPJkW4j4aiqLiXhpyP7H1WRj2toCv3'), + ('A3FwThyWmydgjukcpF9SmTzWQ4yoRoV9jTni1t4oicz4', + '91cuZ3GvQkEkR8UVV456fVxiujBSqd9JMp7p3XaHnVUT'), + ('CkA7fS6aGmo8JPw3yuchz31AnP7KcxncQiu3pQ81X2Mj', + 'PDuBGWm4BnSSkTTxSWVd59PAzFqidaLfFo86aTLZoub'), + ('7aoKCGN4QK82yVpErN1EJyn8ciQXBUkVBe1snx7wypEf', + 'AXoar7qdJZF2kaTJb19PhqY7iSdX3AEef7GBwb9N8WT6'), + ('1GGrwAx34CbTfaV55KdCKah2G5FThjrTdTQk3gTD97x', + '853HbGVt6hT7q17cN6CtDycuTyoncUDWscdo4GUMKntp'), + ('7C6BZbk3Xi4nB1o4mUXMty4rs22CeF8dLQ2dUKhyi9qs', + 'GVu5QTqKeMGhgz8AgzfVRYP5F3HopkqgabQhRqjujEdN'), + ('2WXPBsMGmwjMv7Eg5iqgLAq2VQW1GF6AVAWuveYLXy3Z', + 'AuBnVm277newtkgfyjGQrr3X8ykKwzVzrcpqF67muQ4N'), + ('H67sRSm8W6gVmR1N3SqWXF3WTx9Arhc1RtwvLEjmhm9t', + '5SQamPP4dWUhu2L247TMnf8vX1C5vuB3jtfh1BpVSsPg'), + ('GYztiuCLEvG4wrVszbXKs9AXbKBbDZVhw35xsq8XF63S', + '6pxa9WydnD1xRFReR1yHruXL8VtFu3c6kCNBXkwAyDXA'), + ('G7x9iHnJkjKkdLEsrV2kGZ7tFBm9wj2Pive7vRZss47', + '23MvXauT6cKMLrNyxN41jnZv83aKghLP4B37bvemjENa'), + ('3MhdzHYRrFrPmQZfXfpNKLw9xEdFNZNfUcehgBmboH43', + 'Buqfw4nFfuLoHJcfYZvxXBJf5rTm5ypSyuJfL11jDFkn'), + ('B2MbWPXDPSWQAwNirepSws7a4sgKNCVtmduGh5f54Koq', + 'Cus791pRcuoVJe24WME2QYeEAX1R4uiTGNxa3HwzwtQY'), + ('7yHSmHHX4WwsZ4H6oQxxytkGRqKPiMdqftSvRqXiomYj', + '2qVE6baeD57raXJnNwyUeWi1VyfpQ21QW1J374zMGD6o'), + ('E2V7mzxce6J8PZw8rUEZXYYVnTFRkMSfTty7duohox6V', + 'HSs1oWnvTfjrMmVouRtFJYLjfgeC1uxEiA8MX9F98A34'), + ('4yP4RH18nt3DDFzhpLGborEJuS7hx4cKaz6AAe1xNChe', + 'FziConq7CF4h6TYc1W4wYtmJbhNnrAGoareRkeoRLKTi'), + ('HGgVjtNG2U6odxbAAR31UAcHknzenY88GxYhnURy2S5C', + '82miL67GzT9fTVt8hFiE2XJBRr7iNXAvFLnuiFj5HyjV'), + ('AWY2DyCDbMQqx6v5KtcoW1f9qQd5NqiibeLFpABwibEn', + '9KgHN7xTLa34hfwGq4RpW71jsKjyPKRtaAdAvjHuATtb'), + ('BYE1oV6Dyf49Qedrtf3UaVny9D7NEUhtx78pD1X38M6x', + '3ve8upjPmX9vvdEqvir7QBxnXQAyBWiZKwWyEhq47ptx'), + ('BiZLPsA8Q3faqLPxrcMP1TT77XUYd2jceAkuB9bFCzUa', + 'DrL1j2ZXLvBzk2TmA4DxsRmoR3oCSpW8YPvDCMCei1dU'), + ('FNPkTYojwJ4F4psnzbWt8XnNRBqRhwHXog8Pb8Kuuq7V', + 'FRxatYaiuuKBtvvERSADKNtSGPDY7iPzCmMaLDnPSuG8'), + ('2LiAeAJHjGsrkUmd42jdgPAyKEfzWVttrmnXu6rzfyYM', + 'FwQ3jTBnJpY62paSLdudyymZPUDSWy3827wY13jTJUmC'), + ('Gcu8TPtFM2tv9gtUo5yQYyBg7KHQWxnW9Dk3bp4zh5VC', + 'G3UrGxBB4UCUnxFNKrhmS1rpj3Z7bq484hZUUfNqprx1'), + ('HQGHpzMDdB3sYqfJJC5GuyreqaSjpqfoGytZL9NVtg8T', + 'GA9eu5RDuReBnjUcSSg9CK4X4A678YTrxHFxCpmWhDjM'), + ('2of61RBw2ARZcPD4XJFcnpx18Aj1mU4viUMVN2AXaJsE', + '3aDSyU3E5Kmn9emoUXrktFKH4i7t4uaKBmHNFFhErYU8'), + ('J8oF1sfJzXxJL1bDxPwCtDYw1FEh1prVAhWZF8dw1fRa', + '2atybus8CnehWNVj1DcCEidc3uD2Q7q4tiP5ok2CuNSD'), + ('AxMvjM1w3i3XQVjH8oVVGhiic9kgtvrrDzxnWKdwhdQo', + 'DXYvSgETSxy4vfU2rqPoZFumKw5aLWErkmEDR2w2sR7h'), + ('GBuyEpUQTf2v21NAKozUbUQnwwiugHNY9Uh2kPqBwqXK', + 'CLDPdckwDKa3qiLnoKuNFW8yHRjJdU37XE6skAmageJK'), + ('Bc8ykuXeq7HutQkveQfYXQ28BbFkjRpZCAEuRsAMtxuF', + 'B45qxKWDPnoE1C5KzunsMvfHmRgZHfz2LzxaM1LTqVwF'), + ('9H9v7uKAWScvy34ZQfWJW2NoJ3SWf2NuaqzabcaVuh4h', + '4Kj9wUpHKfgJbjyLNmMYwEwnotUmsgTDKMCusHbM5gcz'), + ('2kWx8nor8McDSZsg8vJ7hZrc3aUVtZhcVcvNeT14iSFo', + '3S9ase3dQd5oz3L7ELGivAsUyaTosK9C5X1aiZNtgcwi'), + ('ENEDnokpqJhziw9CPiGDCnNRwSDgnGjAPh1L7XABWP6s', + '2sUKDdtfVaUXZCN6V6WecweBL8ZEY5mCfPBTj4xzhQtq'), + ('FPUYgS4VvQ5WaZaQqnrJppBZQasoSMwZ4LyhUBKYnE6Q', + 'FtP6Zak6EEWpuptqxSoPAySfm4yA6rWAQqxMCi6s6RYp'), + ('FhQjcEjy36p27YGjKzWicdABNWzEYGciSU5Eht98o2eg', + '2hZ3Fby9K5jYQdtrhvehKTeJgq4NDJY46p4oBT7NUAv5'), + ('5JD7STAtYDUeMqvA75FxTGUw6mSFmFvnVMJZJkTHLafH', + 'HCGf4nWF7q4v4GBPXxTdWMjU7o3SifxfmKzTQ1dWmFqo'), + ('3VLPrCmUog6mBVqkTuSJzXP7ZABCA6ejQKu9LpzkJs6s', + 'Bap6iTjmZb781zLxSmELoqVA25mbMuL7B8WdAz5wDygG'), + ('EiQ57ZLHQvtLbEbiJ41ViZmPctFfd51EFEaK6Y3HZcYb', + '5uu84u8um1CfuT1pvpdFKMy5oWuU4BfWRbpRHzG4eW4A'), + ('3hM9hy2LSqe2SsrcE7XeNz1aPPPZgK5bnTeboyFgFsyj', + '3ptDB8YwcU9EiafreJnFSyfNuoKMMws7U7auMadhRxdr'), + ('3LoFwupCNbPk4cMYVS58UHtkDhvKpdYNmMJPEbK5hnat', + 'CQ56mX3agjJoWwt2uDSa7gtzHWyg3y4Lqp16rZL9qUdF'), + ('F9X1XsTcFwJN196D1PdCc88WrVrBGhfDgQnezeXW9Vjx', + '79cg39iLMZHPFbXrWo6aJAbsXFPk7kgqgBxijDbDLKA'), + ('Hf1XCRfcXc6sQZVchcvv54Sod8BjBFqsiU5Wu4eX6bTd', + '4o8pJV5jaNVqbQhw1u9EzoHT9m69bkfDSGVGugBYeiPC'), + ('2hamLVNSruGH8uT7YvXa3AUcsspg2ERqFqT11gbKpbUK', + '3SziPezcFQbhPrGVJrm5D8HVAZSjduBhFanaXBZcGr3s'), + ('6u92HEbihHiorTANWBs5nYsHJSJ21SfSqsD4FwZy8UZr', + '9jo5yogiEVYwxCkzYoHrn7WMnxpRqqJxbAFuMA2TuzmW'), + ('4YJJNsfEz3eiBE48w8kihENuwDXGbS1vYLi27663EDvw', + 'xcAieBttVYi8g9NQBBjf9jPoaMoWx3hA1h3iCcB11jC'), + ('CUSUaZiUyy8f9yf59RSeorwQJGGnVgR6humfvmzpBMmS', + 'EbR1dthGhu82wPJT7MmqKu84eKNKQXEuUm6Lqdf4NLXu'), + ('5RBfhrADkYu5yFKtgdVZPq1k78VcQc3VZr4kjWpXmACs', + 'Ev4PviNfb87KH5HSXEj7gN7uBYLbHWFSFqQPsoYcMHK7'), + ('4M4UiTmPLY6H4AhSkgUkKQ6cRceixyL6oT86AUvK9tTs', + '4VuGTkZ62PbgKEotnMtyG6L2M76v3qabhPZKXeJ1npca'), + ('BDAWs8i2GbRySDC5SCStzFdEvnfiqCTEbu9mpZRoKdA8', + 'FoyMqh9tcY6xCyLxdByrW8xgzAqGNJKR9dPEN7CjPmQ2'), + ('Dm1HwCxzLm76hBGAG2NEziNRiPBiYnQoKivPm5JC3477', + 'Ap747d6xaUofhsFoyXbg7SCpH53tsD8zbLY39QS2jWfC'), + ('6dRpaKGL3pzaoBX1dKsduuDkkPfUB1yBb1taCYZoNGw2', + '7PoRrQTBXmCkKuwvLxQceBbUwqo4eReNTxVaGVT6npdn'), + ('Cb6sghYERbQA5VMdxKiZx1xk6j6heJNbW1TxRTMwkquu', + 'Am8zvPbAgk2ERqmhGzJZL1NCNkEUjF6enXCwczp4d97B'), + ('EhaLhpbbRCfCuLbq3rQS1d4PfE6rHgvYA9MpTGaxACgW', + 'EfeeApbq1jBChfhe13JkEPbUfm1JYYFCdKXdtue6YrH5'), + ('353aMTUrjH628XzVnKH2oyRmMaAdJ4antn5fGNAzfqMN', + 'AqustPmyDtVpFDiUEqWfFjDeVBQhvKYZFU4wjfpXRXee'), + ('7x8v2BEkdyDvzVzbRJR9AztZHLv8kUZfwRRmcPEpHEYj', + '88MTxTfy7Btqxwdf5Xo7TmjzACeuNop8MeE63LikQn4k'), + ('2jnPZg4oeBzbqL6TdpyTdoxraqjWHqfSrzfmS5Qh8D4V', + '3GSJUg4s6ydymn9obTxdSVBkxpmWZLCGuvBK9fMECefe'), + ('N8DS5DA18i2Bh7rEz7nJSSJZycz8qKaNkPP58BCh7Zr', + 'AKjy7whpaoUnbDJXzNHbtBHawWnS7tLha3nfMPXh4Qam'), + ('DUQ3pGX5XQtvucPDcNEQPMLrqCMxCbRBuWmHHddNg83Q', + 'F3vakqePy8xmpb23psZahDVEdu4dywCPQB7fCMsP5mp3'), + ('6ABw5HQZSWWJr2Ud6KmD73azu732iNTvEfWbCotCFLrn', + 'GW9eq8JgkHDLjtENDscTK5Bj9AAC3css7SPxLZCPcS2V'), + ('ByNJL8Eo8B6kKH5UuJxiXBRRrAKfALLvQmt2Rq5JgAA4', + 'GEtT15SrZUDxVpLjS4metu4BXYw4o1NmxzH5Wr2DcqAv'), + ('F9XaoqP4A4zZoPB6phfTP8i7CQsnSakh6bk8B1CTLwqy', + '9XLZaFGco78AXQS9dmHZ6zypjtg1Z33pj4KoTtDmnLa6'), + ('ESamPv9kb87uEBZjfgarKTQwSTcEQMetBH44b8o3mPZC', + 'Nv7eXkgL84o8fQLjagN1bVj7bt5FKF6Ah1Md6WWwyLk'), + ('E43hqzYjZZ1XRSME6d37Q99UifT4d23piq1CN3fMp6cv', + 'HLMB1uPdRuYcQyM9UmY9zerxQa3cYqEaRUku3h9oRBQn'), + ('3qfPXUTeCsVRk9L68uyGF5u3XxFSVBtPkATtHayVgCGs', + 'ZEkiCeoj3FGfudrN4xgtbz1VihkKWm4cgHN9qJ4p4GH'), + ('7fxCmzKhvNGpbn9U2vih9N1aF5UXaVER6NSpwn3HPpoy', + 'CmhLU67kWqbL2kyj8bA5TNcg7HiQFJeamcJhe5BB1PAx'), + ('BhJsfuvhj9PqfvnvNGQX26fR5SXvcq7JdhhrWyZHoXT9', + 'CgMqrhrjr4mBMvTgiHLqgvf4tRzUpZuLtQnMSG1Jjgx2'), + ('GZbkL2W22Z2YwHf5SBwRfSEYQf1tquPkELDQjkwm2yU4', + 'E47ijUUheN1Zz8TWKmbcDDWz5nduBvZNtcgqbGRiiGv6'), + ('9Puc7H9PRHZ2oowzxiuGueZCzNY1X3aSuopy7k4w8TTo', + 'FTjTVxsPjiNw6TnbwBeE7WpZbvJuVEMwbdPCt1NppHhc'), + ('BczGQKaQNu8QkTc4PWmPdrbLfmXFzAqnoJ9YzHTU1vez', + '4m4xe8fjWAFHyNYLMRYDXskG2d5o9xZxgzCzca23uBBH'), + ('BZwZrE1hNzKzfnbThE9MiB5Movox67c7uGJmi9Nhef1j', + '5G6reNxH3e1gyMSgBRCYQJypFtTSBQ85r5fQGw6DfnpM'), + ('DFJxcvaR5Xk2bHiuxZzaqDxLDSq6fGSUdg54c5zAFLKz', + 'BRL9LWweehDAcEPc8MXjd3uQtAt4ZK1LY8J5KT3GeYKm'), + ('5wfyCc1mAhp2DCKMmEQG9nW4bKfaVkk8kpjuerApiFXv', + 'rdqo7bdePrF6wR8v8dzJopEHgqNgt2yNmMjxz6wMguQ'), + ('8S42sTQQqr5LJTa6jBjCfNg6xvjeL95btPJt2MPHBrDo', + '7VJjwATaownwJyTWXJxKEtJk46eEXTm9UaioPvVFD2zD'), + ('57WwYQgHHSu7SYrXXmovjiPDmc2BB25itp6xSu5KrQQn', + 'FGW86z4ymEbtqiSpp6zzpDkzdPZv9xDMCGUdGVBz8KLU'), + ('CcxnCDQ4JgH2ceTEPW75GcfW8rP7aiAT8ZuEtYbqEa7w', + '7kQdXRZNJaWo7Gj4XtT1fV4LD4ZtN8VmxdZFiJE8q8xF'), + ('8CYTgLp2kbVJKqnadQNGZorWcdWNpbaXrt6kvdzJnEjv', + '57Zwyf4FUEWTxEWrmbSb6vrcZBukHmCs7TKzKoygV6cf'), + ('4buY9tDvVRpTjfAjM8Up4vWw5yh37xWteSMAerbxpKpv', + '5FvFDCSZgtc57hSpvBqBd8VjhyAJ2a2vxTiHzg2nPyg9'), + ('5jJ8hry8Pu7rkgKkWcmZzfZ5FWk6rT3TnYGesEhfijvt', + '7hmVhrQ8vmHmNhxyvyW1cHF5N6gzRoBy7kimfj4b2uZ5'), + ('6MUnCTEZFZvsKTCW4NKDiPv4a3SRWZLS7gUNP4nXsFBh', + '5m2oXtepVwbKt9t5er72bFNioiHYMRtHcUu176DVFBQu'), + ('GXuU171dpf8JpBLiVgXksyXrdkqDqm6AWJ5A2JZLkkwV', + 'BF6xtHg3kcBKHCJ9Y6TTPyGYn3MDKLqxVDshVUbgaCAk'), + ('DoRUYrhULJbAnsGot4hYZeHUaFgXj4hwhHiGRUP3rZCj', + '8i67E6uPyrRvAN5WqSX9V7xeSGr4nPXqAgnR2pPQj3ew'), + ('At4gvM1wZt6ACte2o26Yxbn5qaodMZBd7U1WsiBwB42x', + 'GBPGPkSkkcM4KmJRqKjCPiEygVLW8LmRRarYvj967fbV'), + ('48D3mw2apqVQa6KYCjGqDFiG5cbwqZEotL2w8aPWCxtE', + '2Byvg9DGK7Axk9Bp6bmiUoBQkkLsRNrcoq2ZPZu5ZyGg'), + ('2YncoUMac2tNMcRFEGEgvuDXb58RdyqHMWmSN2QTMiCP', + 'BSNXYAX8Em2TjuCDvmLV3GgnxYT6vX68YFwoGPaPpsSa'), + ('7As7DVaC6FBqojvFoTo9jgZTcTGx9QYdVwUhNSNNvUsz', + 'E5cMypehm8j2Zxw3dCXKc1MGTCftJJm9FzqPNwnVEgQn'), + ('AAwj9V5iW88EwoZpLwuRvqwKn8c8rSgKAyZduFAfvqvV', + 'CkTks2ZGnQdM19wucQMehPe1imANjnRAAfLd1uewGUx8'), + ('axH9mijksto4vnoTjbbJJfY8iBFziJL2y39ago41WNM', + 'GJV8hxcjpieuXmVz9j5JjB2eFLXmRoBF7EYWpbMNKq7Q'), + ('6vv2FyJcTNJRnEmsRgHai5Bf7trJ8CsBMqbZckXWkbGk', + '5YXtgt3ZVKKYM3qvHXXKKSpStfH38akRYvg9shNNChWS'), + ('DKK6kfAGnLV1mowm9m52yYFViVbQfVEtmRuveiXbnC93', + 'YvrVGNzxXSTLQ5QQJ3GHWHDQJnd3qJ5npGQQvZtb4m1'), + ('4QWSQeeu9oQA3ZQG7d6LKzZLR3YZ79q999Zzb7hb2cbh', + '42ARr6nFsZXLAgGGwZ5p55kVSW5ETjrnJBUxaV6sFmzk'), + ('43oJ9CvF3Wsymj8zrkC19VfzjMiwntw3AXrTvc2UFuuf', + 'A661APGeLXuLgYUwmQjKWnuz1XmjuLNW8XVGuGjmEm76'), + ('3uN8UwhNcg219uX1GffC3a9tCZrVY327ZUk5rs3YfAR2', + 'Ca5B2Z9PAeBkEPuYeUyvs3dHhTqpAzFuXERfHZT3zxto'), + ('HuV5FPtboYQe2EEVFVhBkjRxbUBjeBCHRk2VuiNnBS7N', + '5AJCbvgfLmdGdWKjLpDBZtrrJC6NNCQJK5P9NmpvbByy'), + ('2Rbr8Lasv1CDhL2Xxu5ZfLHf4fhCfxuTr25YDB2Q5VXN', + 'FQTbtsHjw1oYyKF3pUamwubB27UqG1ista1ezL2kgF3N'), + ('CLGF2xs7YyJrNZ8ertsPwofzqTBfQiJ5cMiRNcMjgEkh', + '4uSue7UmSr1H8QCYrerRRyUh2BTqX5t5qPWRdVrcyL43'), + ('o6jUu8mqTQMaawxRBbvuWd3b7syXYEUPFWJGuNuoDs6', + '7uJuBMMZD3d6mq2ihUtJQLWsAqACAkmQSJ3gUcEgW18W'), + ('2wo2o5rqEEyijwm4MuCXHNVp2oJPEYQBF2eU6CoXYuVy', + 'AZY2HCpLGjsUgKo7PZ6gdx6btReR6gRCeE9gmzebgGZ2'), + ('Eo1z9xyGbHZxH1ezG7iLxJFhuL8YWJ6NREu4T2VtRZky', + 'GbjDtbwPBf6pcczRbANBvHeBNb3obMtEMoQTxmmafq2g'), + ('8oPaUg1Wc7293c8HR7Phs4m4DvzDjYuzFUBqffJUhJKP', + '9vJKX3jgc1K4sdhnVYLhU6iv7vf8mRygRDYr784mYUpp'), + ('K2BCZLghAwL4Y9eiPboQM2sz4GWYFM5WApZT6firnig', + '7j9QMXcyqgeVFejyNMhXszKAbZuNdECFYwZFDNCwHN3V'), + ('Dz8Ft3YeeuMcsPKMWNqDDbdx6Qo2s2H2cZNUoX2uDwgY', + '3HqEP9EvU9852orfSh9WZd54pDMJnT5nMnGkjhZibbZg'), + ('3cq9D9s9vZgyDertxiZr21etinCYKCMYcf3LXe3o8zT4', + '5174KhHkMsti7XNSYh5j1jFEv22PHQQizTXxT7gT2ZPb'), + ('5uJwmzmoZDkADaeyenBvceP4mSzBgEgbqU5cc8JQpTDE', + 'HEYiTYWaTwjXkzfbE4eZ1RL78ciJkWqEio8tDTvCXzk8'), + ('BkHzLwC5bkLVB4b5KPAqbWc4ekhqmMtk34tfYpLQ53KR', + '537uFsVdCU81kSG7eUZBFV5q3PvadsS4KgzaLuGWGzgG'), + ('3eQT6nC2BEqtXa5b5dn51cJEpj4eMHYsx7RkHXfwNEkq', + '2NV1QhXppRfj19ZemqGUgxZ9Pd5yD13aQmrcNd6g25D4'), + ('GsBGHmKMiJoYDhoXJXwUnkbH4cVWWQ7emG1t7vTFDdS', + 'CsLyGG9J9E4ZLwhpTHRgp21tvGWyPj79SaLGEpqVhHKj'), + ('ALytZ6ygpy3hqHVXGHHdNuzuQh1hSoTVU8im5C6CgTR2', + '5646BEZkpyoDWQHMscMav8bXoiAzf7giVmu8yepWsoMN'), + ('5XhJnzEfqVRM6trhL19K1AoGAQjbWC84Cv5XZ4nE9fF7', + 'BJdQwVTx2fuJWkStt3yPD2WUeopjV3yPQp1646Yi2pXL'), + ('7XLiDAjnggSU7PAvrTwsyPebC3bhuc5B2CMdiYAQBGWZ', + '8xnXGiNp1ADNfuG6uLQ91h2h1ekjuiEC5SRdw19rbpnq'), + ('7kyFUtCcaiWKfGZmWfb9kvwcYLxxmocBC7qXYwNwotgV', + '574EqNs3exLKJxgqFxKyLE5XQMBkadQf5MKQ8qpjsVJS'), + ('ESJSEPbWb13NaDkde8rEdcippc58AMCZodfmJP1SK16m', + '5iwWfDDjgyFfeLpS9EYmwszScwtxTACcgAbinCjFLZTZ'), + ('AjnWLT2vZnEmLfioGeseLuxGQFFiyoqtFJj2oEUgzax5', + '9JeUGkGHPyB7s7XVVik1aFyCxarH2tWhpSJapnRXveb8'), + ('32yM1jbRpZt7EjnH2UDimusAPfMQ83Wd1AULxLYMv2hq', + '73v6uEUhL12MEwdfFFDmqbWmSQXoC8Y3VPB9vKUYEW5X'), + ('F5DjMdHvqqym53MtBG1v9shrza74EttHn1zPFL1ic1hT', + 'FpkXbvZsW4LbU4XZYvy6euR7F9SxDMPdyVVCfJFUaT2C'), + ('3EPdMUSAXFuQLaVwq1fPHNUPzvSHXqfNupgu6kGhdEVc', + '28RxZbx71Y8ZaYt9f9D2HnAhkH2CvAPT4PXpDgCnXhVY'), + ('47YXW4Escn71q7xf6qip8NwdKTq2ScL1i4xmAnJ1RvDW', + '3NQxT4ukLvPPZV3J6qDmx5PFPa7GvaiMBwc1r47SXdfj'), + ('AiCfcc6viFsxTxfEJxo82b3GWzim2nRXvBBfB14w4dMr', + 'FBCcBLpFUss64MWjf3nuSRrLNoqnWpJGfXKJVaduPezJ'), + ('CkeGi1XM3nquJcp3osb2EhTJ99gsisPfTpnsQdYViWWa', + '4L12aHJtN96XGrYbhBFhmEQuPTnsHu95NATsz3X2Uo4Z'), + ('A78PS3MuQtWQ937ow5mzHhXUS1LNSzX2nMcmqLN57c3G', + '87T6viSDWX7Rrw2VWsqEXhwVmrsrmf2rjDHRkeUGU4rX'), + ('2SzYHP21J4KXwVgSwtNfDQKUbyC7RE8feAwfVuW7PSmD', + '4NCA5NxnhxPAAcWqyxtg4us7MJYSbn8g3Kw6v35Vmnm5'), + ('GxGuWY5A1ADiXFrdCiAcVJX4foveGxDfhcJd2Yirg3D8', + '2Jjo3w5gQ7TsQaN2N7iNejfGLjzucaNg4hYZBcwT7AzC'), + ('5dYeKTvxfH6s9Esbys8TVMDTZMCzjFJAH4xe623ykmZ2', + '5q7Le5Kcm1eBY1r8XwEseDXnEUKkZE5qtNb6p5BSSKwz'), + ('EkbeQ7eoiHxiTmq7ksw6FLvf59b3pGuoDR9LF29KYw4m', + 'CDpJ8VmgiBvYUcZMcPYr3B5UxSVEtLxRfq5dH3AxboNT'), + ('2zXT2EUMwWKPMWHK5rYvxgLNdmkoedXH754uzUBphaCE', + '5oHnEFaUaM1QRZjV48K1DrqKeEdcbmb8uG2zucTYc5qH'), + ('H6c78e97srwPEg5PsW1uuKAovSxTvmNyFt9qJwoeJP4y', + 'inwncuMiPRuw6PEucVG2Kempk91yq3dT5kpuf3Umf4j'), + ('6yJDrenNeRBpdQxqxMY3C2V6cBrfvpzYpz6MbefxuxsZ', + 'CnCjmTECDrqJP5nTPSL2NWJ9LPyyFzLmrTYiRcSjwU7e'), + ('3YTX3ntzsjG9CxbkCayToGEzmn1Fgdvw1W8gefCUTa9L', + 'FkCbQBoKRZbndsNP44CWheEchwPC65UNdrZ8FntRTyvu'), + ('8Y7xgZ5M8qBYdX5iCHe7mPQ6ZcQNXDJd28ZVDdx7FSBa', + 'AYTdxj598H36RGmBzEnR4QK8pVF6k5YTRBypxWsDkXUB'), + ('AtzLLpKuPehdP4g6x4J4BH2RjNbvXewxf8ibSgKSiJtL', + 'vC8C3u71YueJcUhtyfn9Xx5PjpJuizDZNGW23tFb5VY'), + ] From feb402ee06f73f1e4e631877acc78fbe6c245854 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 16:19:13 +0200 Subject: [PATCH 45/59] Renamed prepare_test() as put_benchmark_utils() --- benchmarking-tests/fabfile.py | 2 +- benchmarking-tests/test1/README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/benchmarking-tests/fabfile.py b/benchmarking-tests/fabfile.py index 770ed218..b6bb3c39 100644 --- a/benchmarking-tests/fabfile.py +++ b/benchmarking-tests/fabfile.py @@ -23,7 +23,7 @@ env.key_filename = 'pem/bigchaindb.pem' @task @parallel -def prepare_test(): +def put_benchmark_utils(): put('benchmark_utils.py') diff --git a/benchmarking-tests/test1/README.md b/benchmarking-tests/test1/README.md index 4cc96d8a..dd2115ef 100644 --- a/benchmarking-tests/test1/README.md +++ b/benchmarking-tests/test1/README.md @@ -6,7 +6,8 @@ Measure how many blocks per second are created on the _bigchain_ with a pre fill 2. Copy `deploy-cluster-aws/hostlist.py` to `benchmarking-tests` ```bash -fab prepare_test +fab put_benchmark_utils fab update_statsd_conf: fab prepare_backlog: # wait for process to finish -fab start_bigchaindb \ No newline at end of file +fab start_bigchaindb +``` \ No newline at end of file From be9e44140e20001c82be3f7934de7d2969f15b87 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 16:36:00 +0200 Subject: [PATCH 46/59] Renamed update-statsd command to set-statsd-host --- benchmarking-tests/benchmark_utils.py | 11 ++++++----- benchmarking-tests/fabfile.py | 4 ++-- benchmarking-tests/test1/README.md | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index 38017e2f..f05ee0f9 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -14,7 +14,8 @@ def create_write_transaction(tx_left): b = Bigchain() while tx_left > 0: # use uuid to prevent duplicate transactions (transactions with the same hash) - tx = b.create_transaction(b.me, b.me, None, 'CREATE', payload={'msg': str(uuid.uuid4())}) + tx = b.create_transaction(b.me, b.me, None, 'CREATE', + payload={'msg': str(uuid.uuid4())}) tx_signed = b.sign_transaction(tx, b.me_private) b.write_transaction(tx_signed) tx_left -= 1 @@ -26,7 +27,7 @@ def run_add_backlog(args): workers.start() -def run_update_statsd(args): +def run_set_statsd_host(args): with open(expanduser('~') + '/.bigchaindb', 'r') as f: conf = json.load(f) @@ -45,9 +46,9 @@ def main(): backlog_parser.add_argument('num_transactions', metavar='num_transactions', type=int, default=0, help='Number of transactions to add to the backlog') - # update statsd configuration - statsd_parser = subparsers.add_parser('update-statsd', - help='Update statsd host') + # set statsd host + statsd_parser = subparsers.add_parser('set-statsd-host', + help='Set statsd host') statsd_parser.add_argument('statsd_host', metavar='statsd_host', default='localhost', help='Hostname of the statsd server') diff --git a/benchmarking-tests/fabfile.py b/benchmarking-tests/fabfile.py index b6bb3c39..ddfb36dd 100644 --- a/benchmarking-tests/fabfile.py +++ b/benchmarking-tests/fabfile.py @@ -29,8 +29,8 @@ def put_benchmark_utils(): @task @parallel -def update_statsd_conf(statsd_host='localhost'): - run('python3 benchmark_utils.py update-statsd {}'.format(statsd_host)) +def set_statsd_host(statsd_host='localhost'): + run('python3 benchmark_utils.py set-statsd-host {}'.format(statsd_host)) print('update configuration') run('bigchaindb show-config') diff --git a/benchmarking-tests/test1/README.md b/benchmarking-tests/test1/README.md index dd2115ef..302c9268 100644 --- a/benchmarking-tests/test1/README.md +++ b/benchmarking-tests/test1/README.md @@ -7,7 +7,7 @@ Measure how many blocks per second are created on the _bigchain_ with a pre fill ```bash fab put_benchmark_utils -fab update_statsd_conf: +fab set_statsd_host: fab prepare_backlog: # wait for process to finish fab start_bigchaindb ``` \ No newline at end of file From c4db2c5d661214f522faf4ef59b9b7e18619e9c5 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 10 May 2016 16:46:17 +0200 Subject: [PATCH 47/59] Added README for benchmarking tests --- benchmarking-tests/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 benchmarking-tests/README.md diff --git a/benchmarking-tests/README.md b/benchmarking-tests/README.md new file mode 100644 index 00000000..3ae00969 --- /dev/null +++ b/benchmarking-tests/README.md @@ -0,0 +1,3 @@ +# Benchmarking tests + +This folder contains util files and test case folders to benchmark the performance of a BigchainDB federation. \ No newline at end of file From ca34b586295efc0763c409c5876eae99c4e069db Mon Sep 17 00:00:00 2001 From: diminator Date: Tue, 10 May 2016 17:12:38 +0200 Subject: [PATCH 48/59] rename verify_signature to validate_fulfillments --- bigchaindb/consensus.py | 10 ++++----- bigchaindb/core.py | 8 +++---- bigchaindb/util.py | 22 +++++++++---------- bigchaindb/web/views.py | 4 ++-- docs/source/consensus.md | 2 +- docs/source/python-server-api-examples.md | 2 +- tests/db/test_bigchain_api.py | 22 +++++++++---------- .../doc/run_doc_python_server_api_examples.py | 2 +- tests/test_client.py | 2 +- tests/test_util.py | 2 +- 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index 1eafe7f5..030cc289 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -77,7 +77,7 @@ class AbstractConsensusRules(metaclass=ABCMeta): raise NotImplementedError @abstractmethod - def verify_signature(signed_transaction): + def validate_fulfillments(signed_transaction): """Verify the signature of a transaction. Args: @@ -158,7 +158,7 @@ class BaseConsensusRules(AbstractConsensusRules): raise exceptions.InvalidHash() # Check signature - if not util.verify_signature(transaction): + if not util.validate_fulfillments(transaction): raise exceptions.InvalidSignature() return transaction @@ -216,10 +216,10 @@ class BaseConsensusRules(AbstractConsensusRules): return util.sign_tx(transaction, private_key) @staticmethod - def verify_signature(signed_transaction): + def validate_fulfillments(signed_transaction): """Verify the signature of a transaction. - Refer to the documentation of ``bigchaindb.util.verify_signature`` + Refer to the documentation of ``bigchaindb.util.validate_fulfillments`` """ - return util.verify_signature(signed_transaction) + return util.validate_fulfillments(signed_transaction) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index c50ca826..b1f33c63 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -86,17 +86,17 @@ class Bigchain(object): return self.consensus.sign_transaction(transaction, *args, **kwargs) - def verify_signature(self, signed_transaction, *args, **kwargs): - """Verify the signature(s) of a transaction. + def validate_fulfillments(self, signed_transaction, *args, **kwargs): + """Verify the fulfillment(s) of a transaction. Refer to the documentation of your consensus plugin. Returns: - bool: True if the transaction's required signature data is present + bool: True if the transaction's required fulfillments is present and correct, False otherwise. """ - return self.consensus.verify_signature( + return self.consensus.validate_fulfillments( signed_transaction, *args, **kwargs) def write_transaction(self, signed_transaction, durability='soft'): diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 9f0e59c3..59f658ef 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -358,7 +358,7 @@ def fulfill_simple_signature_fulfillment(fulfillment, parsed_fulfillment, fulfil Args: fulfillment (dict): BigchainDB fulfillment to fulfill. - parsed_fulfillment (object): cryptoconditions.Ed25519Fulfillment instance. + parsed_fulfillment (cryptoconditions.Ed25519Fulfillment): cryptoconditions.Ed25519Fulfillment instance. fulfillment_message (dict): message to sign. key_pairs (dict): dictionary of (public_key, private_key) pairs. @@ -380,16 +380,16 @@ def fulfill_simple_signature_fulfillment(fulfillment, parsed_fulfillment, fulfil def fulfill_threshold_signature_fulfillment(fulfillment, parsed_fulfillment, fulfillment_message, key_pairs): """Fulfill a cryptoconditions.ThresholdSha256Fulfillment - Args: - fulfillment (dict): BigchainDB fulfillment to fulfill. - parsed_fulfillment (object): cryptoconditions.ThresholdSha256Fulfillment instance. - fulfillment_message (dict): message to sign. - key_pairs (dict): dictionary of (public_key, private_key) pairs. + Args: + fulfillment (dict): BigchainDB fulfillment to fulfill. + parsed_fulfillment (cryptoconditions.ThresholdSha256Fulfillment): cryptoconditions.ThresholdSha256Fulfillment instance. + fulfillment_message (dict): message to sign. + key_pairs (dict): dictionary of (public_key, private_key) pairs. - Returns: - object: fulfilled cryptoconditions.ThresholdSha256Fulfillment + Returns: + object: fulfilled cryptoconditions.ThresholdSha256Fulfillment - """ + """ parsed_fulfillment_copy = copy.deepcopy(parsed_fulfillment) parsed_fulfillment.subconditions = [] @@ -421,11 +421,11 @@ def check_hash_and_signature(transaction): raise exceptions.InvalidHash() # Check signature - if not verify_signature(transaction): + if not validate_fulfillments(transaction): raise exceptions.InvalidSignature() -def verify_signature(signed_transaction): +def validate_fulfillments(signed_transaction): """Verify the signature of a transaction A valid transaction should have been signed `current_owner` corresponding private key. diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py index f7a14078..3199ac59 100644 --- a/bigchaindb/web/views.py +++ b/bigchaindb/web/views.py @@ -75,8 +75,8 @@ def create_transaction(): tx = util.transform_create(tx) tx = bigchain.consensus.sign_transaction(tx, private_key=bigchain.me_private) - if not bigchain.consensus.verify_signature(tx): - val['error'] = 'Invalid transaction signature' + if not bigchain.consensus.validate_fulfillments(tx): + val['error'] = 'Invalid transaction fulfillments' with monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']): val = bigchain.write_transaction(tx) diff --git a/docs/source/consensus.md b/docs/source/consensus.md index 552ce0d6..eaa16365 100644 --- a/docs/source/consensus.md +++ b/docs/source/consensus.md @@ -30,7 +30,7 @@ validate_transaction(bigchain, transaction) validate_block(bigchain, block) create_transaction(*args, **kwargs) sign_transaction(transaction, *args, **kwargs) -verify_signature(transaction) +validate_fulfillments(transaction) ``` Together, these functions are sufficient for most customizations. For example: diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index f8a9c003..db171613 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -694,7 +694,7 @@ threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = thresho # Optional validation checks assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True -assert b.verify_signature(threshold_tx_transfer) == True +assert b.validate_fulfillments(threshold_tx_transfer) == True assert b.validate_transaction(threshold_tx_transfer) b.write_transaction(threshold_tx_transfer) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index fb7f3539..0537e9ca 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -37,7 +37,7 @@ class TestBigchainApi(object): @pytest.mark.usefixtures('inputs') def test_create_transaction_transfer(self, b, user_vk, user_sk): input_tx = b.get_owned_ids(user_vk).pop() - assert b.verify_signature(b.get_transaction(input_tx['txid'])) == True + assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) == True tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER') @@ -46,8 +46,8 @@ class TestBigchainApi(object): tx_signed = b.sign_transaction(tx, user_sk) - assert b.verify_signature(tx) == False - assert b.verify_signature(tx_signed) == True + assert b.validate_fulfillments(tx) == False + assert b.validate_fulfillments(tx_signed) == True def test_transaction_hash(self, b, user_vk): payload = {'cats': 'are awesome'} @@ -73,7 +73,7 @@ class TestBigchainApi(object): tx_signed = b.sign_transaction(tx, user_sk) assert tx_signed['transaction']['fulfillments'][0]['fulfillment'] is not None - assert b.verify_signature(tx_signed) + assert b.validate_fulfillments(tx_signed) def test_serializer(self, b, user_vk): tx = b.create_transaction(user_vk, user_vk, None, 'CREATE') @@ -1218,7 +1218,7 @@ class TestCryptoconditions(object): assert fulfillment['current_owners'][0] == b.me assert fulfillment_from_uri.public_key.to_ascii().decode() == b.me - assert b.verify_signature(tx_signed) == True + assert b.validate_fulfillments(tx_signed) == True assert b.is_valid_transaction(tx_signed) == tx_signed @pytest.mark.usefixtures('inputs') @@ -1250,7 +1250,7 @@ class TestCryptoconditions(object): assert fulfillment['current_owners'][0] == user_vk assert fulfillment_from_uri.public_key.to_ascii().decode() == user_vk assert fulfillment_from_uri.condition.serialize_uri() == prev_condition['uri'] - assert b.verify_signature(tx_signed) == True + assert b.validate_fulfillments(tx_signed) == True assert b.is_valid_transaction(tx_signed) == tx_signed def test_override_condition_create(self, b, user_vk): @@ -1268,7 +1268,7 @@ class TestCryptoconditions(object): assert fulfillment['current_owners'][0] == b.me assert fulfillment_from_uri.public_key.to_ascii().decode() == b.me - assert b.verify_signature(tx_signed) == True + assert b.validate_fulfillments(tx_signed) == True assert b.is_valid_transaction(tx_signed) == tx_signed @pytest.mark.usefixtures('inputs') @@ -1290,7 +1290,7 @@ class TestCryptoconditions(object): assert fulfillment['current_owners'][0] == user_vk assert fulfillment_from_uri.public_key.to_ascii().decode() == user_vk - assert b.verify_signature(tx_signed) == True + assert b.validate_fulfillments(tx_signed) == True assert b.is_valid_transaction(tx_signed) == tx_signed def test_override_fulfillment_create(self, b, user_vk): @@ -1302,7 +1302,7 @@ class TestCryptoconditions(object): tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() - assert b.verify_signature(tx) == True + assert b.validate_fulfillments(tx) == True assert b.is_valid_transaction(tx) == tx @pytest.mark.usefixtures('inputs') @@ -1319,7 +1319,7 @@ class TestCryptoconditions(object): tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() - assert b.verify_signature(tx) == True + assert b.validate_fulfillments(tx) == True assert b.is_valid_transaction(tx) == tx @pytest.mark.usefixtures('inputs') @@ -1573,7 +1573,7 @@ class TestCryptoconditions(object): assert tx_transfer_signed['transaction']['fulfillments'][0]['fulfillment'] \ == expected_fulfillment.serialize_uri() - assert b.verify_signature(tx_transfer_signed) is True + assert b.validate_fulfillments(tx_transfer_signed) is True def test_create_asset_with_hashlock_condition(self, b): hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 6be75a3b..d4d896ed 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -233,7 +233,7 @@ assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri() -assert b.verify_signature(threshold_tx_transfer) == True +assert b.validate_fulfillments(threshold_tx_transfer) == True assert b.validate_transaction(threshold_tx_transfer) == threshold_tx_transfer diff --git a/tests/test_client.py b/tests/test_client.py index a2b7b6d5..e1e5b97f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -51,7 +51,7 @@ def test_client_can_create_assets(mock_requests_post, client): assert tx['transaction']['conditions'][0]['new_owners'][0] == client.public_key assert tx['transaction']['fulfillments'][0]['input'] is None - assert util.verify_signature(tx) + assert util.validate_fulfillments(tx) def test_client_can_transfer_assets(mock_requests_post, mock_bigchaindb_sign, client): diff --git a/tests/test_util.py b/tests/test_util.py index 92c5b7fd..aad1af40 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -34,7 +34,7 @@ def test_transform_create(b, user_sk, user_vk): assert tx['transaction']['fulfillments'][0]['current_owners'][0] == b.me assert tx['transaction']['conditions'][0]['new_owners'][0] == user_vk - assert util.verify_signature(tx) + assert util.validate_fulfillments(tx) def test_empty_pool_is_populated_with_instances(mock_queue): From a75eec9ad15da572f9f57c9f2e1d36a01302ab2f Mon Sep 17 00:00:00 2001 From: diminator Date: Tue, 10 May 2016 17:18:49 +0200 Subject: [PATCH 49/59] abstractmethod cleanup docs update --- bigchaindb/consensus.py | 16 ++++++++-------- bigchaindb/core.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index 030cc289..62131eab 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -15,6 +15,7 @@ class AbstractConsensusRules(metaclass=ABCMeta): All methods listed below must be implemented. """ + @staticmethod @abstractmethod def validate_transaction(bigchain, transaction): """Validate a transaction. @@ -31,8 +32,8 @@ class AbstractConsensusRules(metaclass=ABCMeta): Descriptive exceptions indicating the reason the transaction failed. See the `exceptions` module for bigchain-native error classes. """ - raise NotImplementedError + @staticmethod @abstractmethod def validate_block(bigchain, block): """Validate a block. @@ -49,8 +50,8 @@ class AbstractConsensusRules(metaclass=ABCMeta): Descriptive exceptions indicating the reason the block failed. See the `exceptions` module for bigchain-native error classes. """ - raise NotImplementedError + @staticmethod @abstractmethod def create_transaction(*args, **kwargs): """Create a new transaction. @@ -61,8 +62,8 @@ class AbstractConsensusRules(metaclass=ABCMeta): Returns: dict: newly constructed transaction. """ - raise NotImplementedError + @staticmethod @abstractmethod def sign_transaction(transaction, *args, **kwargs): """Sign a transaction. @@ -74,20 +75,19 @@ class AbstractConsensusRules(metaclass=ABCMeta): Returns: dict: transaction with any signatures applied. """ - raise NotImplementedError + @staticmethod @abstractmethod def validate_fulfillments(signed_transaction): - """Verify the signature of a transaction. + """Validate the fulfillments of a transaction. Args: signed_transaction (dict): signed transaction to verify Returns: - bool: True if the transaction's required signature data is present + bool: True if the transaction's required fulfillments are present and correct, False otherwise. """ - raise NotImplementedError class BaseConsensusRules(AbstractConsensusRules): @@ -217,7 +217,7 @@ class BaseConsensusRules(AbstractConsensusRules): @staticmethod def validate_fulfillments(signed_transaction): - """Verify the signature of a transaction. + """Validate the fulfillments of a transaction. Refer to the documentation of ``bigchaindb.util.validate_fulfillments`` """ diff --git a/bigchaindb/core.py b/bigchaindb/core.py index b1f33c63..329c1c54 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -87,12 +87,12 @@ class Bigchain(object): return self.consensus.sign_transaction(transaction, *args, **kwargs) def validate_fulfillments(self, signed_transaction, *args, **kwargs): - """Verify the fulfillment(s) of a transaction. + """Validate the fulfillment(s) of a transaction. Refer to the documentation of your consensus plugin. Returns: - bool: True if the transaction's required fulfillments is present + bool: True if the transaction's required fulfillments are present and correct, False otherwise. """ From da97c4c6b5de6750fa985c25796446e3575ffb35 Mon Sep 17 00:00:00 2001 From: diminator Date: Tue, 10 May 2016 17:20:11 +0200 Subject: [PATCH 50/59] docstring update --- bigchaindb/consensus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index 62131eab..29cca142 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -157,7 +157,7 @@ class BaseConsensusRules(AbstractConsensusRules): if calculated_hash != transaction['id']: raise exceptions.InvalidHash() - # Check signature + # Check fulfillments if not util.validate_fulfillments(transaction): raise exceptions.InvalidSignature() From 3636f2da9c43440424f889c9aec0544a7c397882 Mon Sep 17 00:00:00 2001 From: troymc Date: Tue, 10 May 2016 17:28:03 +0200 Subject: [PATCH 51/59] Make symbolic links to hostlist.py and pem file, rather than copy --- benchmarking-tests/test1/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/benchmarking-tests/test1/README.md b/benchmarking-tests/test1/README.md index 302c9268..12d08ce2 100644 --- a/benchmarking-tests/test1/README.md +++ b/benchmarking-tests/test1/README.md @@ -3,7 +3,15 @@ Measure how many blocks per second are created on the _bigchain_ with a pre filled backlog. 1. Deploy an aws cluster http://bigchaindb.readthedocs.io/en/latest/deploy-on-aws.html -2. Copy `deploy-cluster-aws/hostlist.py` to `benchmarking-tests` +2. Make a symbolic link to hostlist.py: `ln -s ../deploy-cluster-aws/hostlist.py .` +3. Make a symbolic link to bigchaindb.pem: +```bash +mkdir pem +cd pem +ln -s ../deploy-cluster-aws/pem/bigchaindb.pem . +``` + +Then: ```bash fab put_benchmark_utils From 69d5859add6e881ccbbe10ddadf168da537b97bb Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 08:56:47 +0200 Subject: [PATCH 52/59] Initial implementation of speed tests --- setup.py | 6 +++++- speed-tests/README.md | 3 +++ speed-tests/speed_tests.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 speed-tests/README.md create mode 100644 speed-tests/speed_tests.py diff --git a/setup.py b/setup.py index aba4de6c..3905799d 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,10 @@ docs_require = [ 'sphinx-rtd-theme>=0.1.9', ] +benchmarks_require = [ + 'line-profiler==1.0', +] + setup( name='BigchainDB', version=version['__version__'], @@ -88,7 +92,7 @@ setup( tests_require=tests_require, extras_require={ 'test': tests_require, - 'dev': dev_require + tests_require + docs_require, + 'dev': dev_require + tests_require + docs_require + benchmarks_require, 'docs': docs_require, }, ) diff --git a/speed-tests/README.md b/speed-tests/README.md new file mode 100644 index 00000000..7b07d338 --- /dev/null +++ b/speed-tests/README.md @@ -0,0 +1,3 @@ +# Speed Tests + +This folder contains tests related to the code performance of a single node. \ No newline at end of file diff --git a/speed-tests/speed_tests.py b/speed-tests/speed_tests.py new file mode 100644 index 00000000..6fb67714 --- /dev/null +++ b/speed-tests/speed_tests.py @@ -0,0 +1,21 @@ +from line_profiler import LineProfiler + +import bigchaindb + + +def speedtest_validate_transaction(): + # create a transaction + b = bigchaindb.Bigchain() + tx = b.create_transaction(b.me, b.me, None, 'CREATE') + tx_signed = b.sign_transaction(tx, b.me_private) + + # setup the profiler + profiler = LineProfiler() + profiler.enable_by_count() + profiler.add_function(bigchaindb.Bigchain.validate_transaction) + + # validate_transaction 1000 times + for i in range(1000): + b.validate_transaction(tx_signed) + + profiler.print_stats() From e944e91d53c43a42c3e1f4bd64f2ee0f4b846f4a Mon Sep 17 00:00:00 2001 From: troymc Date: Wed, 11 May 2016 10:54:33 +0200 Subject: [PATCH 53/59] Edited 2 images in Python Server API Examples --- ...x_multi_condition_multi_fulfillment_v1.png | Bin 53068 -> 27597 bytes docs/source/_static/tx_schematics.odg | Bin 18457 -> 18074 bytes ...single_condition_single_fulfillment_v1.png | Bin 25442 -> 11955 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png b/docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png index 985bdae726b3b1afa9d0338563515b3fbe95b3ac..1a38dd6d7587b4edc11c845ac7f0bba850781db7 100644 GIT binary patch literal 27597 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%0AqkqIK`x-(Y+%<(-ls#P8SfyZ>#|j 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 diff --git a/docs/source/_static/tx_schematics.odg b/docs/source/_static/tx_schematics.odg index 2a1c2fa401b3e829d7a56bc6eeb20dc06aafe373..7443eaece554ed0f2697ac2e1e701e2d72c75275 100644 GIT binary patch delta 14305 zcmc(`WpExnlP-LlnVIdFnVFfHVrFJ$k0EA?A!c^W9CKoJ%p5Z_GqZj2y!-Cf?$-C` z`*G^1YH9?n?n@(8t6QVa_zIR82Zp2~4*`h@0>Oen*;18>NJ?mbUoxH0yCZ~@AkaS# zZZeYuL1F|sK(Qk$A*$h>eU_u+PB7SZ;3r=6tNwzt2WhoQ6_ZjYwW`0_T>gH@Y=dWF zTY+%H%{AM6-`vpkJZdK*gI#uFSRj_%LlPWr8 z^b33vX+1=ol$Wvx&Qu5}I!Q(bZSJlC1rj{&SZwD7&<-$~)1FIV7{vuI&@SupfzET&jA0IWHe}i4gHASX{`d!)%dRDh4kx}b7hV0rCJMSVwqNy$>jw>)V1&L^Fj+$04e^?TXa3- zc;UfpBJ;J}sdOOYTTIE0r~NG9kEhA3tSp~g<`y#R&q)+E;|{}Mn;4s%AE+e*<8olPEG|DB2A#E`Sfe>I(UGsz?h7 zi;&I4#6+jSn=Ik?tAw2^o+lo7z@ zbOo?OUB|?rk)XaRP#Jk|g}L*M|FCc4L3`lFB7iEi!5080pu-44Ty{0i!%+^v8%&=s z`KeD8swx$w9F1vKa^=lu#Xda~Rkh(jfR7&=Q98K)CyUEdG46q1zh-I4*`SV=ATM2h z#|;BQTG?>D?Z72%?K;Bf<4?nOkHg9GL^dGQ&ugoDvX&?jJ~sC1xN-3D_^K`ZIJzRG zeQ7s&nks%>`yr?StE9mxVxZm3SnsEMfbv!(grNJ{YC zG9$)L^4<8jXPO5C7p*Kqyey`f?i{cjenqR=489>71y7^Ki1_fVzgAH9&8*L@0XvPJ z4Tp6lhavdR;QXk@v*Y93&j`3GPJB0v;z0j#cnhO|Xh1ARii(vBo8={d$%k9X?cm8> zTTxz9bMNDM5g5J2pcS7RGd99cY=R^q@Qt-$Z0t0#(#g$^Wjd~vsWuSec^cqp#s}pKK|VT-NoMY*Lxvy*o;KsqywZ8I9fK&Jfs0Jt zU2Xv(xnAb@*@Ob8K$(sTB<9g%Woc>5q7uT#Cf-KaEMp$OuYh>j-mm{DXG=lrGM8r5 zGYji?t)}LC_SXizv2ynTBLjdEb_uceQH8?+a?kHoQ%;0zxLyYGOMCaN4hC%Gx0+zC zKZv724Uk!xzq4g{3^qMzFIF})%s+7&aUe=(isAI5b}(>uryRDM!hwVF`X7uRgZS91#?!mUS62$=bgRPgRNU@J@2K_S&Alj3>fu z@Wh}rR8gxgAeAw?#|F@a(dwztKyPRcyvwR(m~_LDJD`3clvQajMLdV)9q(a%GbbKP7%OxB;?u*Ye z`e~}1`Expch0)l**2Z8RAP(R-^L=fg^2KZ$TnTdN?*L4zK?9lz?uyT z^YaVwkNlWly0QV_im1Y0*!)8Z@gio>kta=fD6a}D=LJ`|7pJa_nLOr}e$crV13!vp zoQha2_FG$>Dbpt`3SZVYE|4IfcI^dv2KTp965Ta@;Dr2KHb3>vPu15|GkNb_Q^F;0 zCqmZAH7s;k1RV72NNZOa_#OSuxKAZj=$evC)sXP`g!c>Jaw5ga^9}y4`Uqd3b|vro zFdcPg<|PJlUfdult4zRn6BKmQNax7ZYUZ}uIjSO6Pa!pDKw=_r=!wr2LQS@-7=z|t zuy(-K`x-7`?@5=achDT@T3p&uety`4FbNzSiCYP` z=pDa0uFL_!iOL2WWnQ> z^jx=DRB^qZ_ofQOKVBz4?CU=pbE_%%>IFL%;+vuTM{3jnLOxTj@WNKfrj2fNU`io%De={#IZ2y4v+s(+hJ6+Cdh=Nrcwz zNP^GglqRNn2S3Y)X+wu?xpiQ59k00tOcN_a2u1jmQV z{_nu*Q!@;gDevZoep)I{!eFg_+x5ENMaTWJo$%X{vXSo*C91QX4H35@ei%$PpU23_ z-TP&PFr=!VkCKLvyt10*Z8Rc&TD^bp@?(1D%9j{k3kvR!j$jE|^t9q^3U;Z?g9ckz z3Ie>Muj6#;Ulxq0bdnS;8?r}B*aQloO%Q+~kCh{U39 zht~^1F$M+(?8@{t#84dx$07#r$=#^_bH@a(nwp$cCQ}*VCXOz-VVS;+RZDeY-4voJ zwxQ!8Mx(n@2=IKPMK>^;Et{x6KWs8ZCm0T1f*E@c>o1z5FyAVcYtf(-dB=0~R z_j(ZZ^IUCU6Yvq3dr%@se|{DYfP1yQTgVmTqX{7ZN>!kx$LkZ1*agZx`9A7aq(%W@fm;cjVS^^>O z_47$BaN)P{GT+i!Y|*^#`=_j=?2igA2Yiym%&oN3`_}B#X)_~%IQiTCu`$3WAnJQ zRF}ht-$gEOK$G3gGYx9#C@&9(d462*a^}(W*3ahn;b1qk-iDIQIQZLnMgo^P@;~^A zNw$4kY^n?{h!+4|DgtDUF$I8*M@+00PDGCb3+@L#uBK7k)kU&sWPtz!4w{1wM~-%| zv$Jz>2)OFU+4|M}EkfB*&_<(yQLXrqNI}D}Wks-!c<)ziY+SAo?`miLC?_XZuF}o+ zF0A-NUYqx0?33`@;V(CgBMtA@L*v7+!THyt&`|ybuekf$s?VAq!v_Eim(#k29_1`# zJ3IN>Ta>$e_uKvZ!wLa@c(~X(9f@Pf`upKHBLNA?IlsEWMlW2r_{#^D(_*t3LMj^M z=4Y^vWD5B;vPbH&I;GclQ#$g$cBy59kE7%H?r<_oX;%nn6vBd!F#hIXQrk0Dl3)=m zjxW0t)QNi9Bll@@MG?SfG$Mq#+o^V);~XF$unkk_J(%R=T3B>$;l)Q9|6&vF;@H%` z6LoXI41=$)lf8U$6+ani3WZ6@h$r_T{ciry4 ziu@hGR0#OV-~E^0^Auz#2Q}U}^&E}s=^(O$Ev?}QKYM2M@x1Vv0lG7U3qF3F8^u|# zG{{l&ivDu4BtC8vxHO-un2Ra9M0V1=%2O76iF-;|DHN(PdU-?|p)Dt9PkI;xou8=< z-+>z8-Knv)f!?M9FAHbLv>0&fc2q?iVOoPN*DR5=77D*wf7{b}Rp@ew`qEM>bMx=t z7f*howL)Xk>z1m|w(U~JnYd=OTgXC>^dhLH{`qQloH=>YP|!HiR}i}WBxK)71%Z5B zqDG4e!4S1wpV|LSMW6x+2F9dW>=Q9H(qE%yNE1)=H!!OjFTo~-#}iJqRkp20*WS9E z3Zbhtl@S{s4<2!*TR7-(@)*;D5K}N(p?M3Y$VE6vB;Y-D@gV%Pn@lRwr7EJfE@&z# zmB;)mU)+HP@%9S|@Ol-e)wn*W5r|p#y`#+|zh43IF7NpKXt|M>lT!d&$@}d+XyN{V z`{Bus0Ek2E4D5SGpia>`Kvp>WGJ&aJOm{WWHu2LB<{F|$s6ox%)P+UkW;SdP& z96GchXsr;Rf=5A3nu6xu1G3AJkj@So)Q zKAS%@H6mv4fyC082LUa5x)4ulA)>2LC@SRkNEMVKnl@AJ0ij`SUlNwYB@=_XI%%v< zEs*ozn+E5e;IHM$Yk#B>oo`L9Xj7B+i8{Dzbwzt+(OwH@M6;b*UusnxJP0+NgM)2( zSr;Qt3SYaXNo7$3dZpT<+sw%aWX}18 z`v;W9+M}`YxQ~+;z{Ywxa*zsFSzKDLZ%PHwG!VBoAKI#|zFJZu6rLC6*(XAL1NxE^ zjKjP)n|*sI#j(2_&ztuRHS)B0_&-Ok%^kEF3>)QB*YG z0W6rfF%^duk>(Po+!>fuFkQ}n1C$dq)7e|U3sBTkW@xUlOO_Wb3C}WocI)GZR@>&0 zP<~I`Ntu=h#b}m(jZvZYDk`WX3G}&I8ftMfftazQP`uRmsAZgJf8Cvo7YZmjc8#ZdrZ%>p3*lwYMbH(Lv z0S7r=Y#C$igMGxCbJ|8ZIXMnaP7y>^Unl zXEnCE&pcE=bCB5&nt3c}0a1nV*ryH;BVu0r`}rq*Y%=ZUwe;uG{_{KM$8QDY^FNP| zjxNKiPYCdjp`s@#e_+(>*VRur^cI3Q*O>LsE?V0i95;VsbUTW=iWtw=uv=cWr$N5@-gd9{MPvGvs`{cwXJ%(Zu8)G> zAUsaM_I?ZTH=C!zWzfyk%_ z#=Of*po1Y0HeO<$5Xv5UfM^#Uj}%#raIwt7aB1?91!Qf}fBE@BQIhp~fd1Ey zKR!O*c-`k-TU#48clA-E2{Fn!@JSeK5JA4Ic6aO_3eZ?JB zB5U*-x`F5)oHvtAZN$YKRpzRq<(xPr&p5E>yS;KadBCK0;@PwbKYZ-)mb3AKvXTu9 z<}qM)Mk`A?VpSX9atHo{fnPx2ucIO2u(f(tc%lQ&Fz(pG{DzD&8XxVD@cw!;xnb<* zzK5eBc=|eK!9@L);?SJ(E*^X22e!i6Se};Ox&mLAdgHW+Wdkp{11K83yH@NggLr5u zsU?P_1%TsWOg4h%PCfz0}PZ(MXMGjP|odxHxl(Sw% z@@rI~w4`R@ z8z7ZQCTooLN5dMNd}%qT)_BI-XFp@Um1#B8*3xB0g4MeS{P2|~Fl^ZWeux^P;d&=Q zAO%CMG~Rznn5(|o&dk=4s>iHyq0uynbhve9FMdo3AGhJF?9g{sv|O3tfdSoxcdv(9 z|I0+IxK0JBDPOTNN^kf}9$vQe6huv>8DMi!!$EQG@z6V;iKf)*W8*!*@q-Q_cK}yU zPo0I0d+#2mkezH|-_(YU2;r^PsO#t+&W*+K0mF1_K-jv-Z|%lI z1;boyUO8wpPHFW_ZAB7rT7TIcCNG{?Qj%$Gc6nRC{(Z*tZbXM)`mZ2Xemh$uAWvU? z@a-6Q9sP-Q25FMy%!ZZmJVWvhxve$v+ZDR-VMImIEVNI?-*nmvsc75);Q z@Zz#+s2yb^8$F0R`x(TF(+B?q{p?kMuPkykihE2nYK&(y>|sDN<+k?!_Iea zYg`+aq%2y3)yHGvWt;nHi-gfnk;(73T{msN2 z%EdTbqOYWpb{le!g+nonQ^wi0=I?*2KlydRk|;0Ty=m6$3nKosZi~nF3)4@TDY*DN z$OY46pL$9fOclkyvZ~ww_t!0p7dHaH_3m)q>n=4_vdy|aY0JvnvRy40*yCHEL!UwN zoa!EB$pFkuYj-*ifrFZ^o&{Lf?>f)gIhjA+A5&FdT$p}^ndl>PC&rqKM5OFA?CkV4 zTH8AzGq9*vdh3UP{J4_Xstwz&y&qIJG0-HChRt0IdF z8`5g@%ScwXn&yt`=9vH45&GFhGhNEk41InCERYWqp8wFXEGczP+8)!wzkpSo{7mGS zsJw6fM3mn&fy{S(LGa;;Kmu4)6r1qS? zx)`8MJ>ZgbY-K?@!}xo4M?4}hhhnkGZP_nd8Tr)B?qn4 zer=6r>UD}G;ZRt0jk(m5=K@>yxE2A=>fU|Og2*fd&XY~QD2w+k^s%!h@maMN&n`5w z2}Dz1>md|7?`-JG@=DXjkUbmi!pPZlCoFtO8az-}pg`qzy2#q9R({Ag;`)A{ptco- zokUkV&X1c(BB=2Z8t2YKn46mmt>`qIxTmPSh4}=01M2+U^n?f@ce>2k-~>~%0u|fq zJj9-$;WJY=f3fJ#ch*wGIN#8F$cWg1aXbipjBwIc+%sZd$Cx)2_S-4S-{+FmW(Ct` zwgBN5hIt~I*B*@{Ck^yvi*H~-cy0PMidkGXeGaxt1Ym(Wa9n{r`bc3mS&}$Da{c_p z!F8fb2YBlrm|RLE0k_jnHwO!U^9son_+B@)By? zA7BnWLqjqa2kIu`pf7x!oF3c1qvSMk!GVKx@q(#dUeyOmFLSTM_CzeU&(7X2h90=+ zpSyl=lBRZzIGc$emONA-fKzz9=v5h)>uz<|btimhZ0u0ggA#+b7!^Tf#~4v*sXD`p z>|;lbou)B=%1-wp6Uq42q@iN3sE9P^g0*1@Gs}gG7-DSZ>|D29j+);rqEXjUZVU`R z2zi;S3OxE)ij$MGmp4@gkuxC~p5@Rs!XUWP!j!!LGkpPEf zHYe67*ez0-en@LZkLo?)gy*)<@vL8W7av)e)rdbEz!J)GY` z`#Ac)i%359E=2}=DDIVBiLN2MeiM z{5&_=^wu1EtV(9Wd&rGu=N49UOmpM-Qf!ot9up^72TaWB>dWiGI3)`cMF8>8^vLX1 z_w^k|%p!Fvr|JzGxJku^Y)-U{hSrWA{EMe^x0GOh;CZ!(zs0QWWd5@#r8R*WmQ#Y^ zX~7yFcaGub&CsXaM7ET5<`3u^rQ*t%Lj;0!CSCOYqQw|`lP6c|R0%p9R+R-60-`hC z74}dR1fGThHw#xwrf+DXeSjbr>J>j?_sZ+$+Cs8<2hz@3Dpx2o7ayfSM>Qw5(iN57 zr`-Jp-dM)2ja&sacYE7>;cVaUmG3hcRx2S*BprUISHR8GQOw}kJ+$#%0ED`tApJLKNIB+D9IMZPjgJid`MNUE_^LVl=-Rd^SB-X>&8{ zxUA2~HD$F|al(a1sn3z+aODl(`qVhR+c?F>#0Q~?k=)=}x?<_dg1z*d?_uBA(pk`U zF{CP8w`Nte=c6mC4~$t?IUc@~r{fe09johDoo!e4`Z z6Gr&)%n3l~0>D|`S~ddx*3YJK7V&@nNOP(48ahB~zZ(?Js3sIesd03C-@TdEO1SA` zU;B8R8~k{=;B&}R>;l?Sz5 zO-Nl;0H~}JsbBo#IS5LqBw1~r-gjBzRZWrw=l9w-9~7rI&^pif>Fd4tbAqYrD?=QX zV@2Cx9f5evc3Q8&B5_A`b@h)1VN?~jyh)gmi1OFXL{-|BQa!?uS-$Lkn4aVJH_p4e zAAEpP87+m~ykqx1oLX&1T}pqMZkQ@+`=Z+UBXEBh)8=ucNV`r<{X`WFO_tUWEac zF3N2@`C>ipkjz5gEYl`QDb{^~jpnsUPTwl-4uow*`u5vALOzrgjnvPx8##NFYm}}i z9y3WaiI_fzeJBxDGizzSaDnHeGY27ngWccXP%;~rI99$t`7$T7J1~ilj`Fe%n>+)& znX-4oy1c4ytaE*z)PncGU*VTmH`v~=TSf&bK#?ix`+!+PK@|mWfKd20^PRGz(aQTl zSHLiCMrWrGgc%lF2m5qNaQ=CY%8u57NGcp(ecOoAdVptN$HH@kbvyQ!hPXO2;I|J z0kRJFJoky_V#}9weR_4wQ}cEZ4^``2i92{`(pRV`(b9aB5Jn;{{NiWr=U(Mql`~Xp zZ@%=}>riE#)*79S@T;jH;P)FaF4!pO>#`hRvZ^i*(@BmYa$W6^5Z{_mzI%U&!=(S& z^H=3|%ijgPEmZQ?(-wZaopHVf@Z$1q0w&fJZX{OKHS%GCq#$lcQw^65_M86*pgoYt zV~Iq8Y5u)-lEI&872Z0iQvpg%e|KgM8VI=^>)=2O7g_cA3PSw}4F>`W?Eanmf0F_J z--@?W`TsTgcj1LUcb08aJma9hNmf!xqDIU(*i$%*; zGn_}vgcNfjPGgYmwetn?`m+34=RMaB6S zi+J`mgeJ8`?D6&~blxUF?TxNa`@k1^izj{>S9+O{tRzJ&&!7OQYxB9Mk$BFTMwgZT zb0j&U_UY-Rp*p*#T{(BJ($ex%!K${|=+gWjT_c6bMhnn7pD0c6sGsDDQu71d#OH30 z1?9|$d)tzxq_qwU%TxXZ=^2i*>SBdlGakhbPfqV-Q-9dniOmmS?T>uR0sA5$F{po(NpGc?3JQI`#n?W#m+fr3;u7OLTYx5+@Yy1&b-Ii4Arl zgEEa0D&r=+o`xj(+AWv}JFZdX^Us*!15AM5u6@*K=Fw>#Ua$Bge{u`T4^vLT+@ZqC zhh;nFf{+R=>N?UyKuDf5`(F6kK98~oy6z(GowhX;PL#;^m1n3KY`5yBW%rZIQQ@KSgM}^!@Fo`e%FdD`k2_}<} zV$azZH?%+U(W?BGyN0BlXC&@<4KAj(i&X2Yd!-cgf~z9r;9X==dOQ5G`g#OUjd3h4 zC?bp;T$uz}e2eN+Sn8%@)b8kAzv)4dGy?EZlMVg6KEVYuQmU*?s3|p%p{|X+J_zSl zr>jUCl}*Z~vMQx1**o6bv)~Q*MTigg6+gOccC!z5;h<{fd5GHGmuAHD_Ss)TAJ2h` z0Xr4nuu?-P1qWc39{u`5ylreTDZ%8s191b1za$S5N*JqIi&jUSZP!^E{Yt~QE&`C4 zxu8GL$A*}xo#BQpET4#N8V1J2APxm#-MugyX?76T`*)NU{TR+i(HJ&zO`(L&7!=8 zs9+4>gRYs@jde>l9}Rllz@h5jdcdKA`t!zxPG(YiH$U9pwfIYs84_5Z{!u@8YTEr` z-z?2#o0j={AWj%U3LnNFO|Y3x<@H1XC1%-wwf4r=`AbJiyX2{{ply$e$X%5M-yO#0 z#VB%%d~z5bdFoeo#7RJeL2ZMU)H?|7_%e5HTa-bqBT)IK0!z|fD2dcD^3x#Bvt7ll zSOgWOlyV1=&EaI%?`0s!h&|-TSmTSv_F}wdDRamnp>iWDN1WyiHlgiE(SD-Mxnl?0 zM*MB~PzI-!@h}_)I(xAV44rUX**ueda-K7yC`^JT>4hVzr%J31D#)l`>dMnZnR?ho z2`kY7(s`vj8tDUhcDkGGh%6+j`rSC(SA@(TNqt07(I(D$G!*E#i7_m6LLh}JgKiKK zR_CAGK-*g(Sv|Ph{y=*QH@UJq0v+i&BdN{B0a@_eBc<#CW zAS7ECc+7|^PV;M=$(uxDtQNPFZ8^_)a1lbk^=Li_Sm3HbJHByzS32e9F6)%3uEo&P zrRc%<3gTVCECI}_j)@~4x-t=~{&9rtXOY>rPjOzC7e2!5L! z9=j@%A3dbARB){se&0*!4LP*k($F!`et~G*tuyU@vt}v5Pq6lLK+6dh$F8N#?@}DT5P4#=|(Q5gydlIn|`zD)B@! z>}%soMB3DODD`O?c2(r%VO!o4`^BLxIh}K| zlW5|)8k(^M9{Wr;nf_o2AzZBF$nR+16W3$)8gO~N$RpWe;iL3>Xne#5prt=`Wu{{= zD|j?fQ^0NMoGN2HO>>fQP7WMm6#9$hZk_p@m78nPdXMq1lk9wvu8TDMD^GAG zgXy+QrE?xbX&!Ia{Drhb2-u=|`o&>^Y7v}7-Jg-zO?hEM9&E(BJX6Mj8n_>Sbi-tj^c^gp zL85i!(|H7i#o041V*PkmfL7cEV{l2^Bo*;Hy*>g|#l zu0Ri{ZYw`}6jg6_`g;Ov^Wr!|gUsBF=RH$mLVbkGNy1_?CsqW_l%&Smk(TezKlUi%v>9>-U8lx;B z0JRUtprt*3xo9!y22DMBn~0IU7qWy*<9Ce)ukh*n4U99R<&EMuqB6_;=2Jrl-dfvH zR>6gUazzISj_or+QOV8r^RE4pW8=~pJYGZgd_rh1FXQ%vvujXi} zPPD7dfvX6&?3}?TC`+R3`G*k)=}OM^fv&Ccu(6%mYo9w@M$dmUzSwT3Iw(D4U5)o@ zZSt6K{JN{V+gFUo)H-1x4xoi3m2AToY+(=?)u3PEMf{&-)s192ic!Q zhyZ)v3h{KI&$O{B85`vDLum9Y zp+|zIac-}^F_E0nf#+x^ai6d^AshULjvz~tu|4f9*!J-(+Z<5Fe|1D%I*+GO0 z=0AN+LQ#!>_1+RCOU{;pPtFzn4{lqc?Eh8oEQSp8?{KfbQpl1=L^1y@ku2F+8u4Ge z|IM!{aB!z%fDp z7 z68B%|MWqSAw}g@}MX{47MPQNs1Id4WgNeVic*4n7BBcMTf%k8Nx^S|;2sz#V#QqNw z@PEUh{vWWu3MbQx(k2^9A^uCQgA@VyfpBuP6xDwfiA4UwApBpF{-IMBNj4Pw-wpo& sj}l3qlA`&qarB7%g^H2PD~9lICjUA2lYeQSM3PUXsiBp`{z3JB07CoWJOBUy delta 14661 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 JqW==@{{dE`%18hJ diff --git a/docs/source/_static/tx_single_condition_single_fulfillment_v1.png b/docs/source/_static/tx_single_condition_single_fulfillment_v1.png index 4af1b22e77da89013c780c1eb0a9a3484e50eadc..0479119cf7a2be0e1ccf57ded7495a0b672c16b2 100644 GIT binary patch literal 11955 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 KBv~VF67(Nh8sG&0 From 9761bb2267a153d4b46abaeb3f6ec932c144f39d Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 11:29:53 +0200 Subject: [PATCH 54/59] Added a command to gather metrics from a cluster --- benchmarking-tests/benchmark_utils.py | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index f05ee0f9..e5cf0312 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -2,6 +2,10 @@ import multiprocessing as mp import uuid import json import argparse +import csv +import time +import logging +import rethinkdb as r from os.path import expanduser @@ -10,6 +14,10 @@ from bigchaindb.util import ProcessGroup from bigchaindb.commands import utils +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + def create_write_transaction(tx_left): b = Bigchain() while tx_left > 0: @@ -36,6 +44,38 @@ def run_set_statsd_host(args): json.dump(conf, f) +def run_gather_metrics(args): + # setup a rethinkdb connection + conn = r.connect(args.bigchaindb_host, 28015, 'bigchain') + + # setup csv writer + csv_file = open(args.csvfile, 'w') + csv_writer = csv.writer(csv_file) + + # query for the number of transactions on the backlog + num_transactions = r.table('backlog').count().run(conn) + logger.info('Starting gathering metrics. {} transasctions in the backlog'.format(num_transactions)) + + # listen to the changefeed + try: + for change in r.table('bigchain').changes().run(conn): + # check only for new blocks + if change['old_val'] is None: + block_num_transactions = len(change['new_val']['block']['transactions']) + time_now = time.time() + logger.info('{} {}'.format(time_now, block_num_transactions)) + csv_writer.writerow([str(time_now), str(block_num_transactions)]) + + num_transactions -= block_num_transactions + if num_transactions == 0: + break + except KeyboardInterrupt: + logger.info('Interrupted. Exiting early...') + + # close files + csv_file.close() + + def main(): parser = argparse.ArgumentParser(description='BigchainDB benchmarking utils') subparsers = parser.add_subparsers(title='Commands', dest='command') @@ -52,6 +92,18 @@ def main(): statsd_parser.add_argument('statsd_host', metavar='statsd_host', default='localhost', help='Hostname of the statsd server') + # metrics + metrics_parser = subparsers.add_parser('gather-metrics', + help='Gather metrics to a csv file') + + metrics_parser.add_argument('-b', '--bigchaindb-host', + required=True, + help='Bigchaindb node hostname to connect to gather cluster metrics') + + metrics_parser.add_argument('-c', '--csvfile', + required=True, + help='Filename to save the metrics') + utils.start(parser, globals()) From ced473aaa6f2f1c6f131b03e1cde5478709c0b9a Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 13:08:32 +0200 Subject: [PATCH 55/59] Added some statistics to the output of gather-metrics. --- benchmarking-tests/benchmark_utils.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index e5cf0312..a69e4f97 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -54,7 +54,15 @@ def run_gather_metrics(args): # query for the number of transactions on the backlog num_transactions = r.table('backlog').count().run(conn) + num_transactions_received = 0 + initial_time = None logger.info('Starting gathering metrics. {} transasctions in the backlog'.format(num_transactions)) + logger.info('This process should exit automatically. ' + 'If this does not happen you can exit at any time using Ctrl-C' + ' saving all the metrics gathered up to this point.') + + logger.info('\t{:<20} {:<20} {:<20} {:<20}'.format('timestamp', 'tx in block', + 'tx/s', '% complete')) # listen to the changefeed try: @@ -63,11 +71,22 @@ def run_gather_metrics(args): if change['old_val'] is None: block_num_transactions = len(change['new_val']['block']['transactions']) time_now = time.time() - logger.info('{} {}'.format(time_now, block_num_transactions)) csv_writer.writerow([str(time_now), str(block_num_transactions)]) - num_transactions -= block_num_transactions - if num_transactions == 0: + # log statistics + if initial_time is None: + initial_time = time_now + + num_transactions_received += block_num_transactions + elapsed_time = time_now - initial_time + elapsed_time = elapsed_time if elapsed_time != 0 else 1 + percent_complete = round((num_transactions_received / num_transactions) * 100) + transactions_per_second = round(num_transactions_received / elapsed_time) + + logger.info('\t{:<20} {:<20} {:<20} {:<20}'.format(time_now, block_num_transactions, + transactions_per_second, percent_complete)) + + if (num_transactions - num_transactions_received) == 0: break except KeyboardInterrupt: logger.info('Interrupted. Exiting early...') From 4c64b6642b2d8fcb5b70bcb8886b38797644f548 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 14:45:10 +0200 Subject: [PATCH 56/59] Change the block process to use all the cpus instead of only one cpu per task --- bigchaindb/block.py | 25 +++++++++++++------------ tests/db/test_bigchain_api.py | 20 +++++++++----------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/bigchaindb/block.py b/bigchaindb/block.py index 2c2e108e..651257f5 100644 --- a/bigchaindb/block.py +++ b/bigchaindb/block.py @@ -7,6 +7,7 @@ import rethinkdb as r import bigchaindb from bigchaindb import Bigchain from bigchaindb.monitor import Monitor +from bigchaindb.util import ProcessGroup logger = logging.getLogger(__name__) @@ -180,7 +181,9 @@ class Block(object): # add results to the queue for result in initial_results: q_initial.put(result) - q_initial.put('stop') + + for i in range(mp.cpu_count()): + q_initial.put('stop') return q_initial @@ -203,17 +206,21 @@ class Block(object): self._start() logger.info('exiting block module...') + def kill(self): + for i in range(mp.cpu_count()): + self.q_new_transaction.put('stop') + def _start(self): """ Initialize, spawn, and start the processes """ # initialize the processes - p_filter = mp.Process(name='filter_transactions', target=self.filter_by_assignee) - p_validate = mp.Process(name='validate_transactions', target=self.validate_transactions) - p_blocks = mp.Process(name='create_blocks', target=self.create_blocks) - p_write = mp.Process(name='write_blocks', target=self.write_blocks) - p_delete = mp.Process(name='delete_transactions', target=self.delete_transactions) + p_filter = ProcessGroup(name='filter_transactions', target=self.filter_by_assignee) + p_validate = ProcessGroup(name='validate_transactions', target=self.validate_transactions) + p_blocks = ProcessGroup(name='create_blocks', target=self.create_blocks) + p_write = ProcessGroup(name='write_blocks', target=self.write_blocks) + p_delete = ProcessGroup(name='delete_transactions', target=self.delete_transactions) # start the processes p_filter.start() @@ -222,9 +229,3 @@ class Block(object): p_write.start() p_delete.start() - # join processes - p_filter.join() - p_validate.join() - p_blocks.join() - p_write.join() - p_delete.join() diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 0537e9ca..82281a8f 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -714,8 +714,8 @@ class TestBigchainBlock(object): # run bootstrap initial_results = block.bootstrap() - # we should have gotten a queue with 100 results - assert initial_results.qsize() - 1 == 100 + # we should have gotten a queue with 100 results minus the poison pills + assert initial_results.qsize() - mp.cpu_count() == 100 def test_start(self, b, user_vk): # start with 100 transactions in the backlog and 100 in the changefeed @@ -736,7 +736,9 @@ class TestBigchainBlock(object): tx = b.sign_transaction(tx, b.me_private) b.write_transaction(tx) new_transactions.put(tx) - new_transactions.put('stop') + + for i in range(mp.cpu_count()): + new_transactions.put('stop') # create a block instance block = Block(new_transactions) @@ -744,6 +746,8 @@ class TestBigchainBlock(object): # start the block processes block.start() + time.sleep(6) + assert new_transactions.qsize() == 0 assert r.table('backlog').count() == 0 assert r.table('bigchain').count() == 2 @@ -755,20 +759,14 @@ class TestBigchainBlock(object): # create block instance block = Block(new_transactions) - # create block_process - p_block = mp.Process(target=block.start) - # start block process - p_block.start() + block.start() # wait for 6 seconds to give it time for an empty queue exception to occur time.sleep(6) - # send the poison pill - new_transactions.put('stop') - # join the process - p_block.join() + block.kill() def test_duplicated_transactions(self): pytest.skip('We may have duplicates in the initial_results and changefeed') From a8dbe60854e515f458d4270e260ac9555e782037 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 16:12:41 +0200 Subject: [PATCH 57/59] close the files in a finally clause --- benchmarking-tests/benchmark_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index a69e4f97..6adaaca9 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -90,9 +90,9 @@ def run_gather_metrics(args): break except KeyboardInterrupt: logger.info('Interrupted. Exiting early...') - - # close files - csv_file.close() + finally: + # close files + csv_file.close() def main(): From 2c58ef857c9d1f7c29d0cc1888eae9ad701f78aa Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 11 May 2016 16:20:24 +0200 Subject: [PATCH 58/59] replace tx/s by `nan` for the first result --- benchmarking-tests/benchmark_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index 6adaaca9..c19680b3 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -79,9 +79,12 @@ def run_gather_metrics(args): num_transactions_received += block_num_transactions elapsed_time = time_now - initial_time - elapsed_time = elapsed_time if elapsed_time != 0 else 1 percent_complete = round((num_transactions_received / num_transactions) * 100) - transactions_per_second = round(num_transactions_received / elapsed_time) + + if elapsed_time != 0: + transactions_per_second = round(num_transactions_received / elapsed_time) + else: + transactions_per_second = float('nan') logger.info('\t{:<20} {:<20} {:<20} {:<20}'.format(time_now, block_num_transactions, transactions_per_second, percent_complete)) From ee2fa053d9f7c8c6daaae5e7e29d9db4a20e493e Mon Sep 17 00:00:00 2001 From: vrde Date: Wed, 11 May 2016 16:18:52 +0200 Subject: [PATCH 59/59] Add lru_cache to load_consensus_plugin --- bigchaindb/config_utils.py | 2 ++ speed-tests/speed_tests.py | 4 ++++ tests/utils/test_config_utils.py | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index dc396522..c9da67b1 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -16,6 +16,7 @@ import copy import json import logging import collections +from functools import lru_cache from pkg_resources import iter_entry_points, ResolutionError @@ -218,6 +219,7 @@ def autoconfigure(filename=None, config=None, force=False): set_config(newconfig) # sets bigchaindb.config +@lru_cache() def load_consensus_plugin(name=None): """Find and load the chosen consensus plugin. diff --git a/speed-tests/speed_tests.py b/speed-tests/speed_tests.py index 6fb67714..b6a6b016 100644 --- a/speed-tests/speed_tests.py +++ b/speed-tests/speed_tests.py @@ -19,3 +19,7 @@ def speedtest_validate_transaction(): b.validate_transaction(tx_signed) profiler.print_stats() + + +if __name__ == '__main__': + speedtest_validate_transaction() diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index 0a365102..29aa5d0b 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -58,12 +58,15 @@ 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): - config_utils.load_consensus_plugin() + # 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():