From ed1d4b82ad27a3b10c0e7daa070165feda422e92 Mon Sep 17 00:00:00 2001 From: Sangat Das Date: Thu, 13 Jan 2022 16:29:35 +0000 Subject: [PATCH 1/4] Initial Migration --- .ci/entrypoint.sh | 14 + .ci/travis-after-success.sh | 12 + .ci/travis-before-install.sh | 16 + .ci/travis-before-script.sh | 18 + .ci/travis-install.sh | 21 + .ci/travis_script.sh | 18 + .dockerignore | 9 + .github/CONTRIBUTING.md | 14 + .github/ISSUE_TEMPLATE.md | 20 + .github/ISSUE_TEMPLATE/bug_report.md | 30 + .github/PULL_REQUEST_TEMPLATE.md | 18 + .gitignore | 92 +- .pre-commit-config.yaml | 25 + .readthedocs.yml | 11 + .travis.yml | 77 + CHANGELOG.md | 1162 ++++++++++++++ CODE_OF_CONDUCT.md | 57 + Dockerfile | 24 + Dockerfile-all-in-one | 51 + Dockerfile-alpine | 30 + Dockerfile-dev | 35 + LICENSE | 862 +++-------- LICENSES.md | 24 + Makefile | 144 ++ PYTHON_STYLE_GUIDE.md | 97 ++ README.md | 78 +- README_cn.md | 77 + README_kor.md | 65 + RELEASE_PROCESS.md | 101 ++ ROADMAP.md | 14 + acceptance/README.md | 27 + acceptance/python/.gitignore | 1 + acceptance/python/Dockerfile | 9 + acceptance/python/src/test_basic.py | 125 ++ acceptance/python/src/test_divisible_asset.py | 181 +++ acceptance/python/src/test_double_spend.py | 48 + acceptance/python/src/test_multiple_owners.py | 126 ++ acceptance/python/src/test_naughty_strings.py | 101 ++ acceptance/python/src/test_stream.py | 132 ++ bigchaindb/README.md | 42 + bigchaindb/__init__.py | 106 ++ bigchaindb/backend/README.md | 51 + bigchaindb/backend/__init__.py | 17 + bigchaindb/backend/connection.py | 170 +++ bigchaindb/backend/exceptions.py | 22 + bigchaindb/backend/localmongodb/__init__.py | 28 + bigchaindb/backend/localmongodb/connection.py | 136 ++ bigchaindb/backend/localmongodb/query.py | 377 +++++ bigchaindb/backend/localmongodb/schema.py | 90 ++ bigchaindb/backend/query.py | 430 ++++++ bigchaindb/backend/schema.py | 128 ++ bigchaindb/backend/utils.py | 39 + bigchaindb/commands/__init__.py | 0 bigchaindb/commands/bigchaindb.py | 398 +++++ bigchaindb/commands/election_types.py | 31 + bigchaindb/commands/utils.py | 161 ++ bigchaindb/common/__init__.py | 0 bigchaindb/common/crypto.py | 55 + bigchaindb/common/exceptions.py | 115 ++ bigchaindb/common/memoize.py | 58 + bigchaindb/common/schema/README.md | 54 + bigchaindb/common/schema/__init__.py | 82 + ...saction_chain_migration_election_v2.0.yaml | 45 + .../schema/transaction_create_v1.0.yaml | 35 + .../schema/transaction_create_v2.0.yaml | 35 + .../schema/transaction_transfer_v1.0.yaml | 34 + .../schema/transaction_transfer_v2.0.yaml | 34 + .../common/schema/transaction_v1.0.yaml | 168 +++ .../common/schema/transaction_v2.0.yaml | 170 +++ .../transaction_validator_election_v2.0.yaml | 68 + .../common/schema/transaction_vote_v2.0.yaml | 34 + bigchaindb/common/transaction.py | 1330 +++++++++++++++++ bigchaindb/common/transaction_mode_types.py | 8 + bigchaindb/common/utils.py | 165 ++ bigchaindb/config_utils.py | 308 ++++ bigchaindb/core.py | 271 ++++ bigchaindb/elections/__init__.py | 0 bigchaindb/elections/election.py | 355 +++++ bigchaindb/elections/vote.py | 64 + bigchaindb/events.py | 109 ++ bigchaindb/exceptions.py | 12 + bigchaindb/fastquery.py | 50 + bigchaindb/lib.py | 514 +++++++ bigchaindb/log.py | 130 ++ bigchaindb/migrations/__init__.py | 0 .../migrations/chain_migration_election.py | 48 + bigchaindb/models.py | 75 + bigchaindb/parallel_validation.py | 124 ++ bigchaindb/start.py | 90 ++ bigchaindb/tendermint_utils.py | 100 ++ bigchaindb/upsert_validator/__init__.py | 7 + .../upsert_validator/validator_election.py | 68 + .../upsert_validator/validator_utils.py | 85 ++ bigchaindb/utils.py | 209 +++ bigchaindb/validation.py | 25 + bigchaindb/version.py | 10 + bigchaindb/web/__init__.py | 0 bigchaindb/web/routes.py | 48 + bigchaindb/web/server.py | 118 ++ .../web/strip_content_type_middleware.py | 32 + bigchaindb/web/views/__init__.py | 0 bigchaindb/web/views/assets.py | 55 + bigchaindb/web/views/base.py | 46 + bigchaindb/web/views/blocks.py | 58 + bigchaindb/web/views/info.py | 57 + bigchaindb/web/views/metadata.py | 55 + bigchaindb/web/views/outputs.py | 31 + bigchaindb/web/views/parameters.py | 51 + bigchaindb/web/views/transactions.py | 108 ++ bigchaindb/web/views/validators.py | 23 + bigchaindb/web/websocket_server.py | 184 +++ codecov.yml | 35 + docker-compose.yml | 105 ++ docs/README.md | 51 + docs/root/Makefile | 225 +++ .../generate_http_server_api_documentation.py | 207 +++ docs/root/make.bat | 263 ++++ docs/root/requirements.txt | 9 + .../_static/CREATE_and_TRANSFER_example.png | Bin 0 -> 4149 bytes docs/root/source/_static/CREATE_example.png | Bin 0 -> 1806 bytes docs/root/source/_static/schemaDB.png | Bin 0 -> 169926 bytes docs/root/source/about-bigchaindb.rst | 130 ++ docs/root/source/basic-usage.md | 131 ++ docs/root/source/conf.py | 409 +++++ .../cross-project-policies/code-of-conduct.md | 57 + .../cross-project-policies/index.rst | 18 + .../python-style-guide.md | 97 ++ .../cross-project-policies/release-process.md | 101 ++ .../index.rst | 17 + .../run-dev-network-ansible.md | 170 +++ .../run-dev-network-stack.md | 324 ++++ .../run-node-as-processes.md | 138 ++ .../run-node-with-docker-compose.md | 108 ++ .../write-code.rst | 157 ++ docs/root/source/contributing/index.rst | 29 + .../contributing/ways-to-contribute/index.rst | 14 + .../ways-to-contribute/report-a-bug.md | 44 + .../ways-to-contribute/write-docs.md | 30 + docs/root/source/drivers/index.rst | 31 + docs/root/source/index.rst | 33 + .../_static/Conditions_Circuit_Diagram.png | Bin 0 -> 5751 bytes .../installation/_static/Node-components.png | Bin 0 -> 17894 bytes .../root/source/installation/_static/arch.jpg | Bin 0 -> 53244 bytes .../_static/cc_escrow_execute_abort.png | Bin 0 -> 53825 bytes .../installation/_static/models_diagrams.odg | Bin 0 -> 21227 bytes .../_static/mongodb_cloud_manager_1.png | Bin 0 -> 12196 bytes .../_static/monitoring_system_diagram.png | Bin 0 -> 43931 bytes .../installation/_static/stories_3_assets.png | Bin 0 -> 27686 bytes .../_static/tx_escrow_execute_abort.png | Bin 0 -> 61740 bytes ...x_multi_condition_multi_fulfillment_v1.png | Bin 0 -> 27597 bytes .../installation/_static/tx_schematics.odg | Bin 0 -> 18296 bytes ...single_condition_single_fulfillment_v1.png | Bin 0 -> 11955 bytes .../api/http-client-server-api.rst | 737 +++++++++ .../api/http-samples/api-index-response.http | 13 + .../api/http-samples/get-block-request.http | 3 + .../api/http-samples/get-block-response.http | 45 + .../http-samples/get-block-txid-request.http | 3 + .../http-samples/get-block-txid-response.http | 6 + .../http-samples/get-tx-by-asset-request.http | 3 + .../get-tx-by-asset-response.http | 79 + .../api/http-samples/get-tx-id-request.http | 3 + .../api/http-samples/get-tx-id-response.http | 40 + .../api/http-samples/index-response.http | 20 + .../api/http-samples/post-tx-request.http | 41 + .../api/http-samples/post-tx-response.http | 40 + docs/root/source/installation/api/index.rst | 16 + .../api/websocket-event-stream-api.rst | 110 ++ .../installation/appendices/cryptography.rst | 14 + .../installation/appendices/firewall-notes.md | 79 + .../appendices/generate-key-pair-for-ssh.md | 41 + .../source/installation/appendices/index.rst | 19 + .../installation/appendices/licenses.md | 10 + .../installation/appendices/log-rotation.md | 50 + .../installation/appendices/ntp-notes.md | 59 + .../commands-and-backend/backend.rst | 53 + .../commands-and-backend/commands.rst | 24 + .../commands-and-backend/index.rst | 26 + .../the-bigchaindb-class.rst | 11 + docs/root/source/installation/index.rst | 20 + .../network-setup/bigchaindb-node-ansible.md | 7 + .../installation/network-setup/index.rst | 19 + .../k8s-deployment-template/architecture.rst | 228 +++ .../bigchaindb-network-on-kubernetes.rst | 542 +++++++ .../ca-installation.rst | 101 ++ .../client-tls-certificate.rst | 111 ++ .../k8s-deployment-template/cloud-manager.rst | 68 + .../k8s-deployment-template/easy-rsa.rst | 98 ++ .../k8s-deployment-template/index.rst | 48 + .../k8s-deployment-template/log-analytics.rst | 343 +++++ .../node-config-map-and-secrets.rst | 124 ++ .../node-on-kubernetes.rst | 769 ++++++++++ .../revoke-tls-certificate.rst | 49 + .../server-tls-certificate.rst | 102 ++ .../tectonic-azure.rst | 149 ++ .../template-kubernetes-azure.rst | 271 ++++ .../k8s-deployment-template/troubleshoot.rst | 147 ++ .../upgrade-on-kubernetes.rst | 122 ++ .../k8s-deployment-template/workflow.rst | 162 ++ .../network-setup/network-setup.md | 208 +++ .../installation/network-setup/networks.md | 44 + .../node-setup/all-in-one-bigchaindb.md | 92 ++ .../installation/node-setup/aws-setup.md | 65 + .../installation/node-setup/bigchaindb-cli.md | 206 +++ .../node-setup/bigchaindb-node-ansible.md | 7 + .../installation/node-setup/configuration.md | 398 +++++ .../node-setup/deploy-a-machine.md | 64 + .../source/installation/node-setup/index.rst | 25 + .../node-setup/production-node/index.rst | 17 + .../production-node/node-assumptions.md | 25 + .../production-node/node-components.md | 30 + .../production-node/node-requirements.md | 22 + .../node-security-and-privacy.md | 18 + .../production-node/reverse-proxy-notes.md | 58 + .../installation/node-setup/release-notes.md | 16 + .../installation/node-setup/set-up-nginx.md | 43 + .../node-setup/set-up-node-software.md | 112 ++ .../node-setup/troubleshooting.md | 95 ++ docs/root/source/installation/quickstart.md | 91 ++ .../_static/CREATE_and_TRANSFER_example.png | Bin 0 -> 4149 bytes .../source/korean/_static/CREATE_example.png | Bin 0 -> 1806 bytes docs/root/source/korean/_static/schemaDB.png | Bin 0 -> 169926 bytes docs/root/source/korean/assets_ko.md | 21 + docs/root/source/korean/bft-ko.md | 13 + docs/root/source/korean/decentralized_kor.md | 24 + docs/root/source/korean/diversity-ko.md | 18 + docs/root/source/korean/immutable-ko.md | 27 + docs/root/source/korean/index.rst | 98 ++ docs/root/source/korean/permissions-ko.md | 59 + docs/root/source/korean/private-data-ko.md | 102 ++ .../source/korean/production-ready_kor.md | 12 + docs/root/source/korean/query-ko.md | 202 +++ docs/root/source/korean/smart-contracts_ko.md | 17 + docs/root/source/korean/store-files_ko.md | 14 + docs/root/source/korean/terminology_kor.md | 26 + .../source/korean/transaction-concepts_ko.md | 61 + docs/root/source/properties.md | 60 + docs/root/source/query.rst | 229 +++ docs/root/source/terminology.md | 81 + docs/upgrade-guides/v0.10-v1.0.md | 439 ++++++ k8s/bigchaindb/bigchaindb-ext-conn-svc.yaml | 27 + k8s/bigchaindb/bigchaindb-pv.yaml | 46 + k8s/bigchaindb/bigchaindb-pvc.yaml | 36 + k8s/bigchaindb/bigchaindb-sc.yaml | 36 + k8s/bigchaindb/bigchaindb-ss.yaml | 295 ++++ k8s/bigchaindb/bigchaindb-svc.yaml | 42 + k8s/bigchaindb/nginx_container/Dockerfile | 12 + k8s/bigchaindb/nginx_container/README.md | 26 + .../docker_build_and_push.bash | 10 + .../nginx_container/nginx.conf.template | 10 + .../nginx_container/nginx_entrypoint.bash | 26 + .../tendermint_container/Dockerfile | 10 + k8s/bigchaindb/tendermint_container/README.md | 36 + .../docker_build_and_push.bash | 10 + .../genesis.json.template | 6 + .../tendermint_entrypoint.bash | 141 ++ k8s/configuration/config-map.yaml | 179 +++ k8s/configuration/secret.yaml | 118 ++ k8s/dev-setup/bigchaindb.yaml | 98 ++ k8s/dev-setup/mongo.yaml | 54 + k8s/dev-setup/nginx-http.yaml | 88 ++ k8s/dev-setup/nginx-https.yaml | 126 ++ k8s/dev-setup/nginx-openresty.yaml | 88 ++ k8s/logging-and-monitoring/analyze.py | 82 + .../log_analytics_oms.json | 49 + .../log_analytics_oms.parameters.json | 15 + k8s/logging-and-monitoring/oms-daemonset.yaml | 35 + .../container/Dockerfile | 58 + .../container/docker_build_and_push.bash | 10 + .../mongodb_mon_agent_entrypoint.bash | 59 + .../mongo-mon-dep.yaml | 70 + k8s/mongodb/configure_mdb.sh | 24 + k8s/mongodb/container/Dockerfile | 13 + k8s/mongodb/container/README.md | 35 + .../container/configure_mdb_users.template.js | 58 + .../container/docker_build_and_push.bash | 9 + k8s/mongodb/container/mongod.conf.template | 95 ++ k8s/mongodb/container/mongod_entrypoint.bash | 147 ++ k8s/mongodb/mongo-ext-conn-svc.yaml | 18 + k8s/mongodb/mongo-pv.yaml | 46 + k8s/mongodb/mongo-pvc.yaml | 36 + k8s/mongodb/mongo-sc.yaml | 36 + k8s/mongodb/mongo-ss.yaml | 134 ++ k8s/mongodb/mongo-svc.yaml | 22 + k8s/nginx-http/container/Dockerfile | 11 + k8s/nginx-http/container/README.md | 22 + .../container/docker_build_and_push.bash | 10 + k8s/nginx-http/container/nginx.conf.template | 170 +++ .../container/nginx_entrypoint.bash | 79 + k8s/nginx-http/nginx-http-dep.yaml | 102 ++ k8s/nginx-http/nginx-http-svc.yaml | 33 + k8s/nginx-https-web-proxy/README.md | 40 + .../container/Dockerfile | 8 + .../container/docker_build_and_push.bash | 10 + .../container/nginx.conf.template | 139 ++ .../container/nginx_entrypoint.bash | 79 + .../nginx-https-web-proxy-conf.yaml | 70 + .../nginx-https-web-proxy-dep.yaml | 109 ++ .../nginx-https-web-proxy-svc.yaml | 21 + k8s/nginx-https/container/Dockerfile | 12 + k8s/nginx-https/container/README.md | 22 + .../container/docker_build_and_push.bash | 10 + k8s/nginx-https/container/nginx.conf.template | 201 +++ .../container/nginx.conf.threescale.template | 198 +++ .../container/nginx_entrypoint.bash | 102 ++ k8s/nginx-https/nginx-https-dep.yaml | 135 ++ k8s/nginx-https/nginx-https-svc.yaml | 41 + k8s/nginx-openresty/LICENSE.md | 70 + k8s/nginx-openresty/container/Dockerfile | 15 + k8s/nginx-openresty/container/README.md | 60 + .../container/docker_build_and_push.bash | 10 + .../container/nginx.conf.template | 197 +++ .../container/nginx.lua.template | 416 ++++++ .../container/nginx_openresty_entrypoint.bash | 62 + k8s/nginx-openresty/nginx-openresty-dep.yaml | 69 + k8s/nginx-openresty/nginx-openresty-svc.yaml | 27 + k8s/scripts/functions | 407 +++++ k8s/scripts/generate_configs.sh | 109 ++ k8s/scripts/vars | 53 + k8s/toolbox/Dockerfile | 16 + k8s/toolbox/README.md | 21 + nginx/nginx.conf | 140 ++ pkg/configuration/bigchaindb-start.yml | 31 + pkg/configuration/bigchaindb-stop.yml | 20 + pkg/configuration/group_vars/all | 5 + pkg/configuration/host_vars/bdb-node-01 | 5 + pkg/configuration/hosts/all | 8 + .../roles/bigchaindb/defaults/main.yml | 48 + .../roles/bigchaindb/tasks/centos.yml | 24 + .../roles/bigchaindb/tasks/common.yml | 46 + .../roles/bigchaindb/tasks/debian.yml | 13 + .../roles/bigchaindb/tasks/fedora.yml | 12 + .../roles/bigchaindb/tasks/main.yml | 26 + .../roles/bigchaindb/tasks/start.yml | 62 + .../roles/bigchaindb/tasks/stop.yml | 39 + .../roles/docker/defaults/main.yml | 31 + .../roles/docker/tasks/centos.yml | 71 + .../roles/docker/tasks/debian.yml | 57 + .../roles/docker/tasks/fedora.yml | 39 + .../roles/docker/tasks/macos.yml | 29 + pkg/configuration/roles/docker/tasks/main.yml | 79 + .../roles/mongodb/defaults/main.yml | 40 + .../roles/mongodb/tasks/centos.yml | 55 + .../roles/mongodb/tasks/debian.yml | 48 + .../roles/mongodb/tasks/fedora.yml | 24 + .../roles/mongodb/tasks/main.yml | 24 + .../roles/mongodb/tasks/start.yml | 61 + .../roles/mongodb/tasks/stop.yml | 46 + pkg/configuration/roles/py36/tasks/centos.yml | 26 + pkg/configuration/roles/py36/tasks/debian.yml | 31 + pkg/configuration/roles/py36/tasks/fedora.yml | 44 + pkg/configuration/roles/py36/tasks/main.yml | 16 + .../roles/py36/templates/install_pip36.j2 | 8 + .../roles/py36/templates/install_py36.j2 | 34 + .../roles/tendermint/defaults/main.yml | 33 + .../roles/tendermint/files/Dockerfile | 7 + .../tendermint/files/access_pub_key.conf | 10 + .../roles/tendermint/files/config.toml | 169 +++ .../roles/tendermint/files/genesis.json | 6 + .../roles/tendermint/tasks/centos.yml | 16 + .../roles/tendermint/tasks/common.yml | 57 + .../roles/tendermint/tasks/debian.yml | 17 + .../roles/tendermint/tasks/fedora.yml | 26 + .../roles/tendermint/tasks/main.yml | 24 + .../roles/tendermint/tasks/start.yml | 90 ++ .../roles/tendermint/tasks/stop.yml | 62 + .../tendermint/templates/start_tendermint.j2 | 98 ++ pkg/configuration/vars/stack-config.yml | 0 pkg/scripts/Vagrantfile | 102 ++ pkg/scripts/all-in-one.bash | 19 + pkg/scripts/bigchaindb-monit-config | 199 +++ pkg/scripts/bootstrap.sh | 101 ++ pkg/scripts/bootstrap_constants.sh | 13 + pkg/scripts/bootstrap_helper.sh | 136 ++ pkg/scripts/functions-common | 360 +++++ pkg/scripts/stack.sh | 262 ++++ pkg/scripts/tm_config_gen | 39 + pkg/scripts/tm_start | 36 + pkg/scripts/unstack.sh | 265 ++++ proposals/extend-post-txn.md | 76 + proposals/integration-test-cases.md | 109 ++ proposals/integration-testing.md | 117 ++ proposals/migrate-cli.md | 169 +++ pytest.ini | 5 + run-acceptance-test.sh | 37 + setup.cfg | 9 + setup.py | 149 ++ snap/README.md | 19 + snap/snapcraft.yaml | 29 + tests/README.md | 17 + tests/__init__.py | 0 tests/assets/__init__.py | 0 tests/assets/test_digital_assets.py | 73 + tests/assets/test_divisible_assets.py | 593 ++++++++ tests/backend/__init__.py | 0 tests/backend/localmongodb/__init__.py | 0 tests/backend/localmongodb/conftest.py | 17 + tests/backend/localmongodb/test_connection.py | 111 ++ tests/backend/localmongodb/test_queries.py | 484 ++++++ tests/backend/localmongodb/test_schema.py | 76 + tests/backend/test_connection.py | 22 + tests/backend/test_generics.py | 37 + tests/backend/test_utils.py | 77 + tests/commands/__init__.py | 0 tests/commands/conftest.py | 63 + tests/commands/test_commands.py | 639 ++++++++ tests/commands/test_utils.py | 133 ++ tests/common/__init__.py | 0 tests/common/conftest.py | 307 ++++ tests/common/test_memoize.py | 93 ++ tests/common/test_schema.py | 131 ++ tests/common/test_transaction.py | 1057 +++++++++++++ tests/common/utils.py | 15 + tests/conftest.py | 768 ++++++++++ tests/db/__init__.py | 0 tests/db/test_bigchain_api.py | 533 +++++++ tests/elections/__init__.py | 0 tests/elections/test_election.py | 264 ++++ tests/migrations/test_migration_election.py | 9 + tests/tendermint/__init__.py | 0 tests/tendermint/conftest.py | 23 + tests/tendermint/test_core.py | 544 +++++++ tests/tendermint/test_fastquery.py | 120 ++ tests/tendermint/test_integration.py | 157 ++ tests/tendermint/test_lib.py | 490 ++++++ tests/tendermint/test_utils.py | 71 + tests/test_config_utils.py | 332 ++++ tests/test_core.py | 118 ++ tests/test_docs.py | 14 + tests/test_events.py | 75 + tests/test_parallel_validation.py | 134 ++ tests/test_txlist.py | 48 + tests/test_utils.py | 173 +++ tests/upsert_validator/__init__.py | 0 tests/upsert_validator/conftest.py | 46 + .../test_upsert_validator_vote.py | 340 +++++ .../test_validator_election.py | 180 +++ tests/utils.py | 114 ++ .../validation/test_transaction_structure.py | 259 ++++ tests/web/__init__.py | 0 tests/web/conftest.py | 19 + tests/web/test_assets.py | 72 + tests/web/test_block_tendermint.py | 74 + tests/web/test_blocks.py | 55 + tests/web/test_content_type_middleware.py | 45 + tests/web/test_info.py | 52 + tests/web/test_metadata.py | 75 + tests/web/test_outputs.py | 140 ++ tests/web/test_parameters.py | 80 + tests/web/test_server.py | 15 + tests/web/test_transactions.py | 459 ++++++ tests/web/test_validators.py | 23 + tests/web/test_websocket_server.py | 211 +++ tmdata/config.toml | 18 + tox.ini | 36 + 454 files changed, 42542 insertions(+), 723 deletions(-) create mode 100755 .ci/entrypoint.sh create mode 100755 .ci/travis-after-success.sh create mode 100755 .ci/travis-before-install.sh create mode 100755 .ci/travis-before-script.sh create mode 100755 .ci/travis-install.sh create mode 100755 .ci/travis_script.sh create mode 100644 .dockerignore create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Dockerfile create mode 100644 Dockerfile-all-in-one create mode 100644 Dockerfile-alpine create mode 100644 Dockerfile-dev create mode 100644 LICENSES.md create mode 100644 Makefile create mode 100644 PYTHON_STYLE_GUIDE.md create mode 100644 README_cn.md create mode 100644 README_kor.md create mode 100644 RELEASE_PROCESS.md create mode 100644 ROADMAP.md create mode 100644 acceptance/README.md create mode 100644 acceptance/python/.gitignore create mode 100644 acceptance/python/Dockerfile create mode 100644 acceptance/python/src/test_basic.py create mode 100644 acceptance/python/src/test_divisible_asset.py create mode 100644 acceptance/python/src/test_double_spend.py create mode 100644 acceptance/python/src/test_multiple_owners.py create mode 100644 acceptance/python/src/test_naughty_strings.py create mode 100644 acceptance/python/src/test_stream.py create mode 100644 bigchaindb/README.md create mode 100644 bigchaindb/__init__.py create mode 100644 bigchaindb/backend/README.md create mode 100644 bigchaindb/backend/__init__.py create mode 100644 bigchaindb/backend/connection.py create mode 100644 bigchaindb/backend/exceptions.py create mode 100644 bigchaindb/backend/localmongodb/__init__.py create mode 100644 bigchaindb/backend/localmongodb/connection.py create mode 100644 bigchaindb/backend/localmongodb/query.py create mode 100644 bigchaindb/backend/localmongodb/schema.py create mode 100644 bigchaindb/backend/query.py create mode 100644 bigchaindb/backend/schema.py create mode 100644 bigchaindb/backend/utils.py create mode 100644 bigchaindb/commands/__init__.py create mode 100644 bigchaindb/commands/bigchaindb.py create mode 100644 bigchaindb/commands/election_types.py create mode 100644 bigchaindb/commands/utils.py create mode 100644 bigchaindb/common/__init__.py create mode 100644 bigchaindb/common/crypto.py create mode 100644 bigchaindb/common/exceptions.py create mode 100644 bigchaindb/common/memoize.py create mode 100644 bigchaindb/common/schema/README.md create mode 100644 bigchaindb/common/schema/__init__.py create mode 100644 bigchaindb/common/schema/transaction_chain_migration_election_v2.0.yaml create mode 100644 bigchaindb/common/schema/transaction_create_v1.0.yaml create mode 100644 bigchaindb/common/schema/transaction_create_v2.0.yaml create mode 100644 bigchaindb/common/schema/transaction_transfer_v1.0.yaml create mode 100644 bigchaindb/common/schema/transaction_transfer_v2.0.yaml create mode 100644 bigchaindb/common/schema/transaction_v1.0.yaml create mode 100644 bigchaindb/common/schema/transaction_v2.0.yaml create mode 100644 bigchaindb/common/schema/transaction_validator_election_v2.0.yaml create mode 100644 bigchaindb/common/schema/transaction_vote_v2.0.yaml create mode 100644 bigchaindb/common/transaction.py create mode 100644 bigchaindb/common/transaction_mode_types.py create mode 100644 bigchaindb/common/utils.py create mode 100644 bigchaindb/config_utils.py create mode 100644 bigchaindb/core.py create mode 100644 bigchaindb/elections/__init__.py create mode 100644 bigchaindb/elections/election.py create mode 100644 bigchaindb/elections/vote.py create mode 100644 bigchaindb/events.py create mode 100644 bigchaindb/exceptions.py create mode 100644 bigchaindb/fastquery.py create mode 100644 bigchaindb/lib.py create mode 100644 bigchaindb/log.py create mode 100644 bigchaindb/migrations/__init__.py create mode 100644 bigchaindb/migrations/chain_migration_election.py create mode 100644 bigchaindb/models.py create mode 100644 bigchaindb/parallel_validation.py create mode 100644 bigchaindb/start.py create mode 100644 bigchaindb/tendermint_utils.py create mode 100644 bigchaindb/upsert_validator/__init__.py create mode 100644 bigchaindb/upsert_validator/validator_election.py create mode 100644 bigchaindb/upsert_validator/validator_utils.py create mode 100644 bigchaindb/utils.py create mode 100644 bigchaindb/validation.py create mode 100644 bigchaindb/version.py create mode 100644 bigchaindb/web/__init__.py create mode 100644 bigchaindb/web/routes.py create mode 100644 bigchaindb/web/server.py create mode 100644 bigchaindb/web/strip_content_type_middleware.py create mode 100644 bigchaindb/web/views/__init__.py create mode 100644 bigchaindb/web/views/assets.py create mode 100644 bigchaindb/web/views/base.py create mode 100644 bigchaindb/web/views/blocks.py create mode 100644 bigchaindb/web/views/info.py create mode 100644 bigchaindb/web/views/metadata.py create mode 100644 bigchaindb/web/views/outputs.py create mode 100644 bigchaindb/web/views/parameters.py create mode 100644 bigchaindb/web/views/transactions.py create mode 100644 bigchaindb/web/views/validators.py create mode 100644 bigchaindb/web/websocket_server.py create mode 100644 codecov.yml create mode 100644 docker-compose.yml create mode 100644 docs/README.md create mode 100644 docs/root/Makefile create mode 100644 docs/root/generate_http_server_api_documentation.py create mode 100644 docs/root/make.bat create mode 100644 docs/root/requirements.txt create mode 100644 docs/root/source/_static/CREATE_and_TRANSFER_example.png create mode 100644 docs/root/source/_static/CREATE_example.png create mode 100644 docs/root/source/_static/schemaDB.png create mode 100644 docs/root/source/about-bigchaindb.rst create mode 100644 docs/root/source/basic-usage.md create mode 100644 docs/root/source/conf.py create mode 100644 docs/root/source/contributing/cross-project-policies/code-of-conduct.md create mode 100644 docs/root/source/contributing/cross-project-policies/index.rst create mode 100644 docs/root/source/contributing/cross-project-policies/python-style-guide.md create mode 100644 docs/root/source/contributing/cross-project-policies/release-process.md create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/index.rst create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/run-dev-network-ansible.md create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/run-dev-network-stack.md create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/run-node-as-processes.md create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/run-node-with-docker-compose.md create mode 100644 docs/root/source/contributing/dev-setup-coding-and-contribution-process/write-code.rst create mode 100644 docs/root/source/contributing/index.rst create mode 100644 docs/root/source/contributing/ways-to-contribute/index.rst create mode 100644 docs/root/source/contributing/ways-to-contribute/report-a-bug.md create mode 100644 docs/root/source/contributing/ways-to-contribute/write-docs.md create mode 100644 docs/root/source/drivers/index.rst create mode 100644 docs/root/source/index.rst create mode 100644 docs/root/source/installation/_static/Conditions_Circuit_Diagram.png create mode 100644 docs/root/source/installation/_static/Node-components.png create mode 100644 docs/root/source/installation/_static/arch.jpg create mode 100644 docs/root/source/installation/_static/cc_escrow_execute_abort.png create mode 100644 docs/root/source/installation/_static/models_diagrams.odg create mode 100644 docs/root/source/installation/_static/mongodb_cloud_manager_1.png create mode 100644 docs/root/source/installation/_static/monitoring_system_diagram.png create mode 100644 docs/root/source/installation/_static/stories_3_assets.png create mode 100644 docs/root/source/installation/_static/tx_escrow_execute_abort.png create mode 100644 docs/root/source/installation/_static/tx_multi_condition_multi_fulfillment_v1.png create mode 100644 docs/root/source/installation/_static/tx_schematics.odg create mode 100644 docs/root/source/installation/_static/tx_single_condition_single_fulfillment_v1.png create mode 100644 docs/root/source/installation/api/http-client-server-api.rst create mode 100644 docs/root/source/installation/api/http-samples/api-index-response.http create mode 100644 docs/root/source/installation/api/http-samples/get-block-request.http create mode 100644 docs/root/source/installation/api/http-samples/get-block-response.http create mode 100644 docs/root/source/installation/api/http-samples/get-block-txid-request.http create mode 100644 docs/root/source/installation/api/http-samples/get-block-txid-response.http create mode 100644 docs/root/source/installation/api/http-samples/get-tx-by-asset-request.http create mode 100644 docs/root/source/installation/api/http-samples/get-tx-by-asset-response.http create mode 100644 docs/root/source/installation/api/http-samples/get-tx-id-request.http create mode 100644 docs/root/source/installation/api/http-samples/get-tx-id-response.http create mode 100644 docs/root/source/installation/api/http-samples/index-response.http create mode 100644 docs/root/source/installation/api/http-samples/post-tx-request.http create mode 100644 docs/root/source/installation/api/http-samples/post-tx-response.http create mode 100644 docs/root/source/installation/api/index.rst create mode 100644 docs/root/source/installation/api/websocket-event-stream-api.rst create mode 100644 docs/root/source/installation/appendices/cryptography.rst create mode 100644 docs/root/source/installation/appendices/firewall-notes.md create mode 100644 docs/root/source/installation/appendices/generate-key-pair-for-ssh.md create mode 100755 docs/root/source/installation/appendices/index.rst create mode 100644 docs/root/source/installation/appendices/licenses.md create mode 100644 docs/root/source/installation/appendices/log-rotation.md create mode 100644 docs/root/source/installation/appendices/ntp-notes.md create mode 100644 docs/root/source/installation/commands-and-backend/backend.rst create mode 100644 docs/root/source/installation/commands-and-backend/commands.rst create mode 100644 docs/root/source/installation/commands-and-backend/index.rst create mode 100644 docs/root/source/installation/commands-and-backend/the-bigchaindb-class.rst create mode 100644 docs/root/source/installation/index.rst create mode 100644 docs/root/source/installation/network-setup/bigchaindb-node-ansible.md create mode 100644 docs/root/source/installation/network-setup/index.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/architecture.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/bigchaindb-network-on-kubernetes.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/ca-installation.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/client-tls-certificate.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/cloud-manager.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/easy-rsa.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/index.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/log-analytics.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/node-config-map-and-secrets.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/node-on-kubernetes.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/revoke-tls-certificate.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/server-tls-certificate.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/tectonic-azure.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/template-kubernetes-azure.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/troubleshoot.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/upgrade-on-kubernetes.rst create mode 100644 docs/root/source/installation/network-setup/k8s-deployment-template/workflow.rst create mode 100644 docs/root/source/installation/network-setup/network-setup.md create mode 100644 docs/root/source/installation/network-setup/networks.md create mode 100644 docs/root/source/installation/node-setup/all-in-one-bigchaindb.md create mode 100644 docs/root/source/installation/node-setup/aws-setup.md create mode 100644 docs/root/source/installation/node-setup/bigchaindb-cli.md create mode 100644 docs/root/source/installation/node-setup/bigchaindb-node-ansible.md create mode 100644 docs/root/source/installation/node-setup/configuration.md create mode 100644 docs/root/source/installation/node-setup/deploy-a-machine.md create mode 100644 docs/root/source/installation/node-setup/index.rst create mode 100644 docs/root/source/installation/node-setup/production-node/index.rst create mode 100644 docs/root/source/installation/node-setup/production-node/node-assumptions.md create mode 100644 docs/root/source/installation/node-setup/production-node/node-components.md create mode 100644 docs/root/source/installation/node-setup/production-node/node-requirements.md create mode 100644 docs/root/source/installation/node-setup/production-node/node-security-and-privacy.md create mode 100644 docs/root/source/installation/node-setup/production-node/reverse-proxy-notes.md create mode 100644 docs/root/source/installation/node-setup/release-notes.md create mode 100644 docs/root/source/installation/node-setup/set-up-nginx.md create mode 100644 docs/root/source/installation/node-setup/set-up-node-software.md create mode 100644 docs/root/source/installation/node-setup/troubleshooting.md create mode 100644 docs/root/source/installation/quickstart.md create mode 100644 docs/root/source/korean/_static/CREATE_and_TRANSFER_example.png create mode 100644 docs/root/source/korean/_static/CREATE_example.png create mode 100644 docs/root/source/korean/_static/schemaDB.png create mode 100644 docs/root/source/korean/assets_ko.md create mode 100644 docs/root/source/korean/bft-ko.md create mode 100644 docs/root/source/korean/decentralized_kor.md create mode 100644 docs/root/source/korean/diversity-ko.md create mode 100644 docs/root/source/korean/immutable-ko.md create mode 100644 docs/root/source/korean/index.rst create mode 100644 docs/root/source/korean/permissions-ko.md create mode 100644 docs/root/source/korean/private-data-ko.md create mode 100644 docs/root/source/korean/production-ready_kor.md create mode 100644 docs/root/source/korean/query-ko.md create mode 100644 docs/root/source/korean/smart-contracts_ko.md create mode 100644 docs/root/source/korean/store-files_ko.md create mode 100644 docs/root/source/korean/terminology_kor.md create mode 100644 docs/root/source/korean/transaction-concepts_ko.md create mode 100644 docs/root/source/properties.md create mode 100644 docs/root/source/query.rst create mode 100644 docs/root/source/terminology.md create mode 100644 docs/upgrade-guides/v0.10-v1.0.md create mode 100644 k8s/bigchaindb/bigchaindb-ext-conn-svc.yaml create mode 100644 k8s/bigchaindb/bigchaindb-pv.yaml create mode 100644 k8s/bigchaindb/bigchaindb-pvc.yaml create mode 100644 k8s/bigchaindb/bigchaindb-sc.yaml create mode 100644 k8s/bigchaindb/bigchaindb-ss.yaml create mode 100644 k8s/bigchaindb/bigchaindb-svc.yaml create mode 100644 k8s/bigchaindb/nginx_container/Dockerfile create mode 100644 k8s/bigchaindb/nginx_container/README.md create mode 100755 k8s/bigchaindb/nginx_container/docker_build_and_push.bash create mode 100644 k8s/bigchaindb/nginx_container/nginx.conf.template create mode 100755 k8s/bigchaindb/nginx_container/nginx_entrypoint.bash create mode 100644 k8s/bigchaindb/tendermint_container/Dockerfile create mode 100644 k8s/bigchaindb/tendermint_container/README.md create mode 100755 k8s/bigchaindb/tendermint_container/docker_build_and_push.bash create mode 100644 k8s/bigchaindb/tendermint_container/genesis.json.template create mode 100755 k8s/bigchaindb/tendermint_container/tendermint_entrypoint.bash create mode 100644 k8s/configuration/config-map.yaml create mode 100644 k8s/configuration/secret.yaml create mode 100644 k8s/dev-setup/bigchaindb.yaml create mode 100644 k8s/dev-setup/mongo.yaml create mode 100644 k8s/dev-setup/nginx-http.yaml create mode 100644 k8s/dev-setup/nginx-https.yaml create mode 100644 k8s/dev-setup/nginx-openresty.yaml create mode 100644 k8s/logging-and-monitoring/analyze.py create mode 100644 k8s/logging-and-monitoring/log_analytics_oms.json create mode 100644 k8s/logging-and-monitoring/log_analytics_oms.parameters.json create mode 100644 k8s/logging-and-monitoring/oms-daemonset.yaml create mode 100644 k8s/mongodb-monitoring-agent/container/Dockerfile create mode 100755 k8s/mongodb-monitoring-agent/container/docker_build_and_push.bash create mode 100755 k8s/mongodb-monitoring-agent/container/mongodb_mon_agent_entrypoint.bash create mode 100644 k8s/mongodb-monitoring-agent/mongo-mon-dep.yaml create mode 100755 k8s/mongodb/configure_mdb.sh create mode 100644 k8s/mongodb/container/Dockerfile create mode 100644 k8s/mongodb/container/README.md create mode 100644 k8s/mongodb/container/configure_mdb_users.template.js create mode 100755 k8s/mongodb/container/docker_build_and_push.bash create mode 100644 k8s/mongodb/container/mongod.conf.template create mode 100755 k8s/mongodb/container/mongod_entrypoint.bash create mode 100644 k8s/mongodb/mongo-ext-conn-svc.yaml create mode 100644 k8s/mongodb/mongo-pv.yaml create mode 100644 k8s/mongodb/mongo-pvc.yaml create mode 100644 k8s/mongodb/mongo-sc.yaml create mode 100644 k8s/mongodb/mongo-ss.yaml create mode 100644 k8s/mongodb/mongo-svc.yaml create mode 100644 k8s/nginx-http/container/Dockerfile create mode 100644 k8s/nginx-http/container/README.md create mode 100755 k8s/nginx-http/container/docker_build_and_push.bash create mode 100644 k8s/nginx-http/container/nginx.conf.template create mode 100755 k8s/nginx-http/container/nginx_entrypoint.bash create mode 100644 k8s/nginx-http/nginx-http-dep.yaml create mode 100644 k8s/nginx-http/nginx-http-svc.yaml create mode 100644 k8s/nginx-https-web-proxy/README.md create mode 100644 k8s/nginx-https-web-proxy/container/Dockerfile create mode 100755 k8s/nginx-https-web-proxy/container/docker_build_and_push.bash create mode 100644 k8s/nginx-https-web-proxy/container/nginx.conf.template create mode 100755 k8s/nginx-https-web-proxy/container/nginx_entrypoint.bash create mode 100644 k8s/nginx-https-web-proxy/nginx-https-web-proxy-conf.yaml create mode 100644 k8s/nginx-https-web-proxy/nginx-https-web-proxy-dep.yaml create mode 100644 k8s/nginx-https-web-proxy/nginx-https-web-proxy-svc.yaml create mode 100644 k8s/nginx-https/container/Dockerfile create mode 100644 k8s/nginx-https/container/README.md create mode 100755 k8s/nginx-https/container/docker_build_and_push.bash create mode 100644 k8s/nginx-https/container/nginx.conf.template create mode 100644 k8s/nginx-https/container/nginx.conf.threescale.template create mode 100755 k8s/nginx-https/container/nginx_entrypoint.bash create mode 100644 k8s/nginx-https/nginx-https-dep.yaml create mode 100644 k8s/nginx-https/nginx-https-svc.yaml create mode 100644 k8s/nginx-openresty/LICENSE.md create mode 100644 k8s/nginx-openresty/container/Dockerfile create mode 100644 k8s/nginx-openresty/container/README.md create mode 100755 k8s/nginx-openresty/container/docker_build_and_push.bash create mode 100644 k8s/nginx-openresty/container/nginx.conf.template create mode 100644 k8s/nginx-openresty/container/nginx.lua.template create mode 100755 k8s/nginx-openresty/container/nginx_openresty_entrypoint.bash create mode 100644 k8s/nginx-openresty/nginx-openresty-dep.yaml create mode 100644 k8s/nginx-openresty/nginx-openresty-svc.yaml create mode 100755 k8s/scripts/functions create mode 100755 k8s/scripts/generate_configs.sh create mode 100644 k8s/scripts/vars create mode 100644 k8s/toolbox/Dockerfile create mode 100644 k8s/toolbox/README.md create mode 100644 nginx/nginx.conf create mode 100644 pkg/configuration/bigchaindb-start.yml create mode 100644 pkg/configuration/bigchaindb-stop.yml create mode 100644 pkg/configuration/group_vars/all create mode 100644 pkg/configuration/host_vars/bdb-node-01 create mode 100644 pkg/configuration/hosts/all create mode 100644 pkg/configuration/roles/bigchaindb/defaults/main.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/centos.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/common.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/debian.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/fedora.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/main.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/start.yml create mode 100644 pkg/configuration/roles/bigchaindb/tasks/stop.yml create mode 100644 pkg/configuration/roles/docker/defaults/main.yml create mode 100644 pkg/configuration/roles/docker/tasks/centos.yml create mode 100644 pkg/configuration/roles/docker/tasks/debian.yml create mode 100644 pkg/configuration/roles/docker/tasks/fedora.yml create mode 100644 pkg/configuration/roles/docker/tasks/macos.yml create mode 100644 pkg/configuration/roles/docker/tasks/main.yml create mode 100644 pkg/configuration/roles/mongodb/defaults/main.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/centos.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/debian.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/fedora.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/main.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/start.yml create mode 100644 pkg/configuration/roles/mongodb/tasks/stop.yml create mode 100644 pkg/configuration/roles/py36/tasks/centos.yml create mode 100644 pkg/configuration/roles/py36/tasks/debian.yml create mode 100644 pkg/configuration/roles/py36/tasks/fedora.yml create mode 100644 pkg/configuration/roles/py36/tasks/main.yml create mode 100644 pkg/configuration/roles/py36/templates/install_pip36.j2 create mode 100644 pkg/configuration/roles/py36/templates/install_py36.j2 create mode 100644 pkg/configuration/roles/tendermint/defaults/main.yml create mode 100644 pkg/configuration/roles/tendermint/files/Dockerfile create mode 100644 pkg/configuration/roles/tendermint/files/access_pub_key.conf create mode 100644 pkg/configuration/roles/tendermint/files/config.toml create mode 100644 pkg/configuration/roles/tendermint/files/genesis.json create mode 100644 pkg/configuration/roles/tendermint/tasks/centos.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/common.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/debian.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/fedora.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/main.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/start.yml create mode 100644 pkg/configuration/roles/tendermint/tasks/stop.yml create mode 100644 pkg/configuration/roles/tendermint/templates/start_tendermint.j2 create mode 100644 pkg/configuration/vars/stack-config.yml create mode 100644 pkg/scripts/Vagrantfile create mode 100755 pkg/scripts/all-in-one.bash create mode 100644 pkg/scripts/bigchaindb-monit-config create mode 100755 pkg/scripts/bootstrap.sh create mode 100755 pkg/scripts/bootstrap_constants.sh create mode 100755 pkg/scripts/bootstrap_helper.sh create mode 100755 pkg/scripts/functions-common create mode 100755 pkg/scripts/stack.sh create mode 100755 pkg/scripts/tm_config_gen create mode 100755 pkg/scripts/tm_start create mode 100755 pkg/scripts/unstack.sh create mode 100644 proposals/extend-post-txn.md create mode 100644 proposals/integration-test-cases.md create mode 100644 proposals/integration-testing.md create mode 100644 proposals/migrate-cli.md create mode 100644 pytest.ini create mode 100755 run-acceptance-test.sh create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 snap/README.md create mode 100644 snap/snapcraft.yaml create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/assets/__init__.py create mode 100644 tests/assets/test_digital_assets.py create mode 100644 tests/assets/test_divisible_assets.py create mode 100644 tests/backend/__init__.py create mode 100644 tests/backend/localmongodb/__init__.py create mode 100644 tests/backend/localmongodb/conftest.py create mode 100644 tests/backend/localmongodb/test_connection.py create mode 100644 tests/backend/localmongodb/test_queries.py create mode 100644 tests/backend/localmongodb/test_schema.py create mode 100644 tests/backend/test_connection.py create mode 100644 tests/backend/test_generics.py create mode 100644 tests/backend/test_utils.py create mode 100644 tests/commands/__init__.py create mode 100644 tests/commands/conftest.py create mode 100644 tests/commands/test_commands.py create mode 100644 tests/commands/test_utils.py create mode 100644 tests/common/__init__.py create mode 100644 tests/common/conftest.py create mode 100644 tests/common/test_memoize.py create mode 100644 tests/common/test_schema.py create mode 100644 tests/common/test_transaction.py create mode 100644 tests/common/utils.py create mode 100644 tests/conftest.py create mode 100644 tests/db/__init__.py create mode 100644 tests/db/test_bigchain_api.py create mode 100644 tests/elections/__init__.py create mode 100644 tests/elections/test_election.py create mode 100644 tests/migrations/test_migration_election.py create mode 100644 tests/tendermint/__init__.py create mode 100644 tests/tendermint/conftest.py create mode 100644 tests/tendermint/test_core.py create mode 100644 tests/tendermint/test_fastquery.py create mode 100644 tests/tendermint/test_integration.py create mode 100644 tests/tendermint/test_lib.py create mode 100644 tests/tendermint/test_utils.py create mode 100644 tests/test_config_utils.py create mode 100644 tests/test_core.py create mode 100644 tests/test_docs.py create mode 100644 tests/test_events.py create mode 100644 tests/test_parallel_validation.py create mode 100644 tests/test_txlist.py create mode 100644 tests/test_utils.py create mode 100644 tests/upsert_validator/__init__.py create mode 100644 tests/upsert_validator/conftest.py create mode 100644 tests/upsert_validator/test_upsert_validator_vote.py create mode 100644 tests/upsert_validator/test_validator_election.py create mode 100644 tests/utils.py create mode 100644 tests/validation/test_transaction_structure.py create mode 100644 tests/web/__init__.py create mode 100644 tests/web/conftest.py create mode 100644 tests/web/test_assets.py create mode 100644 tests/web/test_block_tendermint.py create mode 100644 tests/web/test_blocks.py create mode 100644 tests/web/test_content_type_middleware.py create mode 100644 tests/web/test_info.py create mode 100644 tests/web/test_metadata.py create mode 100644 tests/web/test_outputs.py create mode 100644 tests/web/test_parameters.py create mode 100644 tests/web/test_server.py create mode 100644 tests/web/test_transactions.py create mode 100644 tests/web/test_validators.py create mode 100644 tests/web/test_websocket_server.py create mode 100644 tmdata/config.toml create mode 100644 tox.ini diff --git a/.ci/entrypoint.sh b/.ci/entrypoint.sh new file mode 100755 index 0000000..6a96dd7 --- /dev/null +++ b/.ci/entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +set -e -x + +if [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then + sleep 3600 +else + bigchaindb -l DEBUG start +fi diff --git a/.ci/travis-after-success.sh b/.ci/travis-after-success.sh new file mode 100755 index 0000000..af77412 --- /dev/null +++ b/.ci/travis-after-success.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +set -e -x + +if [[ -z ${TOXENV} ]] && [[ ${PLANETMINT_CI_ABCI} != 'enable' ]] && [[ ${PLANETMINT_ACCEPTANCE_TEST} != 'enable' ]]; then + codecov -v -f htmlcov/coverage.xml +fi diff --git a/.ci/travis-before-install.sh b/.ci/travis-before-install.sh new file mode 100755 index 0000000..384dcc2 --- /dev/null +++ b/.ci/travis-before-install.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +if [[ -z ${TOXENV} ]]; then + sudo apt-get update + sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce + + sudo rm /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose + chmod +x docker-compose + sudo mv docker-compose /usr/local/bin +fi diff --git a/.ci/travis-before-script.sh b/.ci/travis-before-script.sh new file mode 100755 index 0000000..0bf784b --- /dev/null +++ b/.ci/travis-before-script.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +set -e -x + +if [[ -z ${TOXENV} ]]; then + + if [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then + docker-compose up -d bigchaindb + else + docker-compose up -d bdb + fi + +fi diff --git a/.ci/travis-install.sh b/.ci/travis-install.sh new file mode 100755 index 0000000..1462505 --- /dev/null +++ b/.ci/travis-install.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +set -e -x + +pip install --upgrade pip + +if [[ -n ${TOXENV} ]]; then + pip install --upgrade tox +elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then + docker-compose build --no-cache --build-arg abci_status=enable bigchaindb +elif [[ $PLANETMINT_INTEGRATION_TEST == 'enable' ]]; then + docker-compose build bigchaindb python-driver +else + docker-compose build --no-cache bigchaindb + pip install --upgrade codecov +fi diff --git a/.ci/travis_script.sh b/.ci/travis_script.sh new file mode 100755 index 0000000..a6af3b9 --- /dev/null +++ b/.ci/travis_script.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +set -e -x + +if [[ -n ${TOXENV} ]]; then + tox -e ${TOXENV} +elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then + docker-compose exec bigchaindb pytest -v -m abci +elif [[ ${PLANETMINT_ACCEPTANCE_TEST} == 'enable' ]]; then + ./run-acceptance-test.sh +else + docker-compose exec bigchaindb pytest -v --cov=bigchaindb --cov-report xml:htmlcov/coverage.xml +fi diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..441eb44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.cache/ +.coverage +.eggs/ +.git/ +.gitignore +.ropeproject/ +.travis.yml +Planetmint.egg-info/ +dist/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e78d110 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,14 @@ + + +# How to Contribute to the Planetmint Project + +There are many ways you can contribute to the Planetmint project, some very easy and others more involved. + +All of that is documented elsewhere: go to the "[Contributing to Planetmint" docs on ReadTheDocs](https://docs.bigchaindb.com/projects/contributing/en/latest/index.html). + +Note: GitHub automatically links to this file (`.github/CONTRIBUTING.md`) when a contributor creates a new issue or pull request, so you shouldn't delete it. Just use it to point people to full and proper help elsewhere. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..cf0aff9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ +# Do you want to: + +- make a bug report? Then read below about what should go in a bug report. +- make a feature request or proposal? Then read [the page about how to make a feature request or proposal](https://docs.bigchaindb.com/projects/contributing/en/latest/ways-can-contribute/make-a-feature-request-or-proposal.html). +- ask a question about Planetmint? Then [go to Gitter](https://gitter.im/bigchaindb/bigchaindb) (our chat room) and ask it there. +- share your neat idea or realization? Then [go to Gitter](https://gitter.im/bigchaindb/bigchaindb) (our chat room) and share it there. + +# What Should Go in a Bug Report + +- What computer are you on (hardware)? +- What operating system are you using, including version. e.g. Ubuntu 14.04? Fedora 23? +- What version of Planetmint software were you using? Is that the latest version? +- What, exactly, did you do to get to the point where you got stuck? Describe all the steps so we can get there too. Show screenshots or copy-and-paste text to GitHub. +- Show what actually happened. +- Say what you tried to do to resolve the problem. +- Provide details to convince us that it matters to you. Is it for a school project, a job, a contract with a deadline, a child who needs it for Christmas? + +We will do our best but please understand that we don't have time to help everyone, especially people who don't care to help us help them. "It doesn't work." is not going to get any reaction from us. We need _details_. + +Tip: Use Github code block formatting to make code render pretty in GitHub. To do that, put three backticks followed by a string to set the type of code (e.g. `Python`), then the code, and then end with three backticks. There's more information about [inserting code blocks](https://help.github.com/articles/creating-and-highlighting-code-blocks/) in the GitHub help pages. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8c269fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs or terminal output** +If applicable, add add textual content to help explain your problem. + +**Desktop (please complete the following information):** + - Distribution: [e.g. Ubuntu 18.04] + - Bigchaindb version: + - Tendermint version: + - Mongodb version: +- Python full version: [e.g. Python 3.6.6] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9305a8a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +Make sure the title of this pull request has the form: + +**Problem: A short statement of the problem.** + +## Solution + +A short statement about how this PR solves the **Problem**. + +## Issues Resolved + +What issues does this PR resolve, if any? Please include lines like the following (i.e. "Resolves #NNNN), so that when this PR gets merged, GitHub will automatically close those issues. + +Resolves #NNNN +Resolves #MMMM + +## BEPs Implemented + +What [BEPs](https://github.com/bigchaindb/beps) does this pull request implement, if any? diff --git a/.gitignore b/.gitignore index b6e4761..1e0fb31 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,24 @@ __pycache__/ # C extensions *.so +# Swap -- copypasta from https://github.com/github/gitignore/blob/master/Global/Vim.gitignore +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags + # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -19,13 +35,9 @@ lib64/ parts/ sdist/ var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -40,16 +52,14 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache +.pytest_cache/ nosetests.xml coverage.xml *.cover -*.py,cover .hypothesis/ -.pytest_cache/ # Translations *.mo @@ -58,72 +68,32 @@ coverage.xml # Django stuff: *.log local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy # Sphinx documentation -docs/_build/ +docs/build/ # PyBuilder target/ -# Jupyter Notebook +# Ipython Notebook .ipynb_checkpoints -# IPython -profile_default/ -ipython_config.py - # pyenv .python-version -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock +# Just in time documentation +docs/server/source/http-samples -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ +# Terraform state files +# See https://stackoverflow.com/a/41482391 +terraform.tfstate +terraform.tfstate.backup -# Celery stuff -celerybeat-schedule -celerybeat.pid +# tendermint data +tmdata/data +network/*/data -# SageMath parsed files -*.sage.py +# Docs that are fetched at build time +docs/contributing/source/cross-project-policies/*.md -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..32ba096 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +repos: +- repo: git://github.com/pre-commit/pre-commit-hooks + sha: v1.1.1 + hooks: + - id: trailing-whitespace + args: ['--no-markdown-linebreak-ext'] + - id: check-merge-conflict + - id: debug-statements + - id: check-added-large-files + - id: flake8 + +- repo: git://github.com/chewse/pre-commit-mirrors-pydocstyle + sha: v2.1.1 + hooks: + - id: pydocstyle + # list of error codes to check, see: http://www.pydocstyle.org/en/latest/error_codes.html + args: ['--select=D204,D201,D209,D210,D212,D300,D403'] + +# negate the exclude to only apply the hooks to 'bigchaindb' and 'tests' folder +exclude: '^(?!bigchaindb/)(?!tests/)(?!acceptance/)' diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..15aeb2c --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,11 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +build: + image: latest + +python: + version: 3.6 + pip_install: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e94eb1c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,77 @@ +# Copyright © 2020, 2021 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +sudo: required + +dist: focal + +services: + - docker + +language: python +cache: pip + +python: + - 3.6 + - 3.7 + - 3.8 + +env: + global: + - DOCKER_COMPOSE_VERSION=1.29.2 + matrix: + - TOXENV=flake8 + - TOXENV=docsroot + +matrix: + fast_finish: true + include: + - python: 3.6 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - python: 3.6 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - PLANETMINT_CI_ABCI=enable + - python: 3.6 + env: + - PLANETMINT_ACCEPTANCE_TEST=enable + - python: 3.7 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - python: 3.7 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - PLANETMINT_CI_ABCI=enable + - python: 3.7 + env: + - PLANETMINT_ACCEPTANCE_TEST=enable + - python: 3.8 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - python: 3.8 + env: + - PLANETMINT_DATABASE_BACKEND=localmongodb + - PLANETMINT_DATABASE_SSL= + - PLANETMINT_CI_ABCI=enable + - python: 3.8 + env: + - PLANETMINT_ACCEPTANCE_TEST=enable + + +before_install: sudo .ci/travis-before-install.sh + +install: .ci/travis-install.sh + +before_script: .ci/travis-before-script.sh + +script: .ci/travis_script.sh + +after_success: .ci/travis-after-success.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ad3583a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1162 @@ + + +# Change Log (Release Notes) + +All _notable_ changes to this project will be documented in this file (`CHANGELOG.md`). + +This project adheres to [the Python form of Semantic Versioning](https://packaging.python.org/tutorials/distributing-packages/#choosing-a-versioning-scheme) (or at least we try). The Planetmint public API _was_ defined in this file but that definition was moved and can now be found in [BEP-7](https://github.com/bigchaindb/BEPs/tree/master/7). + +Contributors to this file, please follow the guidelines on [keepachangelog.com](http://keepachangelog.com/). Note that each version (or "release") is the name of a [Git _tag_](https://git-scm.com/book/en/v2/Git-Basics-Tagging) of a particular commit, so the associated date and time are the date and time of that commit (as reported by GitHub), _not_ the "Uploaded on" date listed on PyPI (which may differ). + +For reference, the possible headings are: + +* **Added** for new features. +* **Changed** for changes in existing functionality. +* **Deprecated** for once-stable features removed in upcoming releases. +* **Removed** for deprecated features removed in this release. +* **Fixed** for any bug fixes. +* **Security** to invite users to upgrade in case of vulnerabilities. +* **External Contributors** to list contributors outside of Planetmint GmbH. +* **Known Issues** +* **Notes** + +## [2.2.2] - 2020-08-12 + +### Security + +Several dependencies updated including Flask that had vulnerability. + +### Fixed + +* Updated priv_validator key format in stack script (#2707) + +### External Contributors + +* @aostrun - [#2708](https://github.com/bigchaindb/bigchaindb/pull/2708) + +## [2.2.1] - 2020-04-14 + +### Fixed + +Gevent library API update is incompatible with bigchaindb-abci 1.0.1 version. +Updated bigchaindb-abci. + +## [2.2.0] - 2020-02-20 + +### Added + +Support for multiple ABCI versions. + +## [2.1.0] - 2019-11-06 + +### Added + +Option for last transaction retrieval added. + +## [2.0] - 2019-09-26 + +### Changed + +Migrated from Tendermint 0.22.8 to 0.31.5. + +## [2.0 Beta 9] - 2018-11-27 + +### Changed + +Removed support for TLSv1 and TLSv1.1 in all NGINX config files. Kept support for TLSv1.2 and added support for TLSv1.3. [Pull Request #2601](https://github.com/bigchaindb/bigchaindb/pull/2601) + +### Fixed + +Fixed two issues with schema validation. Pull requests [#2606](https://github.com/bigchaindb/bigchaindb/pull/2606) & [#2607](https://github.com/bigchaindb/bigchaindb/pull/2607) + +### External Contributors + +[@gamjapark](https://github.com/gamjapark) and team translated all the [Planetmint root docs](https://docs.bigchaindb.com/en/latest/korean/index.html) into Korean. [Pull Request #2603](https://github.com/bigchaindb/bigchaindb/pull/2603) + +## [2.0 Beta 8] - 2018-11-03 + +### Changed + +* Revised the [Simple Deployment Template](http://docs.bigchaindb.com/projects/server/en/latest/simple-deployment-template/index.html) in the docs. Added NGINX to the mix. Pull Requests [#2578](https://github.com/bigchaindb/bigchaindb/pull/2578) and [#2579](https://github.com/bigchaindb/bigchaindb/pull/2579) +* Revised `nginx/nginx.conf` to enable CORS. [Pull Request #2580](https://github.com/bigchaindb/bigchaindb/pull/2580) + +### Fixed + +* Fixed a typo in the Kubernetes ConfigMap template. [Pull Request #2583](https://github.com/bigchaindb/bigchaindb/pull/2583) + +### External Contributors + +[@gamjapark](https://github.com/gamjapark) translated the main `README.md` file into Korean. [Pull Request #2592](https://github.com/bigchaindb/bigchaindb/pull/2592) + +## [2.0 Beta 7] - 2018-09-28 + +Tag name: v2.0.0b7 + +### Added + +Completed the implementation of chain-migration elections (BEP-42). Pull requests [#2553](https://github.com/bigchaindb/bigchaindb/pull/2553), [#2556](https://github.com/bigchaindb/bigchaindb/pull/2556), [#2558](https://github.com/bigchaindb/bigchaindb/pull/2558), [#2563](https://github.com/bigchaindb/bigchaindb/pull/2563) and [#2566](https://github.com/bigchaindb/bigchaindb/pull/2566) + +### Changed + +* Code that used the Python driver's (deprecated) transactions.send() method now uses its transactions.send_commit() method instead. [Pull request #2547](https://github.com/bigchaindb/bigchaindb/pull/2547) +* Code that implied pluggable "consensus" now implies pluggable transaction "validation" (a more accurate word). [Pull request #2561](https://github.com/bigchaindb/bigchaindb/pull/2561) + +### Removed + +Benchmark logs. [Pull request #2565](https://github.com/bigchaindb/bigchaindb/pull/2565) + +### Fixed + +A bug caused by an incorrect MongoDB query. [Pull request #2567](https://github.com/bigchaindb/bigchaindb/pull/2567) + +### Notes + +There's now better documentation about logs, log rotation, and the `server.bind` config setting. Pull requests [#2546](https://github.com/bigchaindb/bigchaindb/pull/2546) and [#2575](https://github.com/bigchaindb/bigchaindb/pull/2575) + +## [2.0 Beta 6] - 2018-09-17 + +Tag name: v2.0.0b6 + +### Added + +* [New documentation about privacy and handling private data](https://docs.bigchaindb.com/en/latest/private-data.html). [Pull request #2437](https://github.com/bigchaindb/bigchaindb/pull/2437) +* New documentation about log rotation. Also rotate Tendermint logs if started using Monit. [Pull request #2528](https://github.com/bigchaindb/bigchaindb/pull/2528) +* Began implementing one of the migration strategies outlined in [BEP-42](https://github.com/bigchaindb/BEPs/tree/master/42). That involved creating a more general-purpose election process and commands. Pull requests [#2488](https://github.com/bigchaindb/bigchaindb/pull/2488), [#2495](https://github.com/bigchaindb/bigchaindb/pull/2495), [#2498](https://github.com/bigchaindb/bigchaindb/pull/2498), [#2515](https://github.com/bigchaindb/bigchaindb/pull/2515), [#2535](https://github.com/bigchaindb/bigchaindb/pull/2535) +* Used memoization to avoid doing some validation checks multiple times. [Pull request #2490](https://github.com/bigchaindb/bigchaindb/pull/2490) +* Created an all-in-one Docker image containing Planetmint Server, Tendermint and MongoDB. It was created for a particular user and is not recommended for production use unless you really know what you're doing. [Pull request #2424](https://github.com/bigchaindb/bigchaindb/pull/2424) + +### Changed + +* The supported versions of Tendermint are now hard-wired into Planetmint Server: it checks to see what version the connected Tendermint has, and if it's not compatible, Planetmint Server exits with an error message. [Pull request #2541](https://github.com/bigchaindb/bigchaindb/pull/2541) +* The docs no longer say to install the highest version of Tendermint: they say to install a specific version. [Pull request #2524](https://github.com/bigchaindb/bigchaindb/pull/2524) +* The setup docs include more recommended settings for `config.toml`. [Pull request #2516](https://github.com/bigchaindb/bigchaindb/pull/2516) +* The process to add, remove or update the voting power of a validator at run time (using the `bigchaindb upsert-validator` subcommands) was completely changed and is now fully working. See [issue #2372](https://github.com/bigchaindb/bigchaindb/issues/2372) and all the pull requests it references. Pull requests [#2439](https://github.com/bigchaindb/bigchaindb/pull/2439) and [#2440](https://github.com/bigchaindb/bigchaindb/pull/2440) +* The license on the documentation was changed from CC-BY-SA-4 to CC-BY-4. [Pull request #2427](https://github.com/bigchaindb/bigchaindb/pull/2427) +* Re-activated and/or updated some unit tests that had been deacivated during the migration to Tendermint. Pull requests [#2390](https://github.com/bigchaindb/bigchaindb/pull/2390), [#2415](https://github.com/bigchaindb/bigchaindb/pull/2415), [#2452](https://github.com/bigchaindb/bigchaindb/pull/24), [#2456](https://github.com/bigchaindb/bigchaindb/pull/2456) +* Updated RapidJSON to a newer, faster version. [Pull request #2470](https://github.com/bigchaindb/bigchaindb/pull/2470) +* The Java driver is now officially supported. [Pull request #2478](https://github.com/bigchaindb/bigchaindb/pull/2478) +* The MongoDB indexes on transaction id and block height were changed to be [unique indexes](https://docs.mongodb.com/manual/core/index-unique/). [Pull request #2492](https://github.com/bigchaindb/bigchaindb/pull/2492) +* Updated the required `cryptoconditions` package to a newer one. [Pull request #2494](https://github.com/bigchaindb/bigchaindb/pull/2494) + +### Removed + +* Removed some old code and tests. Pull requests + [#2374](https://github.com/bigchaindb/bigchaindb/pull/2374), + [#2452](https://github.com/bigchaindb/bigchaindb/pull/2452), + [#2474](https://github.com/bigchaindb/bigchaindb/pull/2474), + [#2476](https://github.com/bigchaindb/bigchaindb/pull/2476), + [#2491](https://github.com/bigchaindb/bigchaindb/pull/2491) + +### Fixed + +* Fixed the Events API so that it only sends valid transactions to subscribers. Also changed how it works internally, so now it is more reliable. [Pull request #2529](https://github.com/bigchaindb/bigchaindb/pull/2529) +* Fixed a bug where MongoDB database initialization would abort if a collection already existed. [Pull request #2520](https://github.com/bigchaindb/bigchaindb/pull/2520) +* Fixed a unit test that was failing randomly. [Pull request #2423](https://github.com/bigchaindb/bigchaindb/pull/2423) +* Fixed the validator curl port. [Pull request #2447](https://github.com/bigchaindb/bigchaindb/pull/2447) +* Fixed an error in the docs about the HTTP POST /transactions endpoint. [Pull request #2481](https://github.com/bigchaindb/bigchaindb/pull/2481) +* Fixed a unit test that could loop forever. [Pull requqest #2486](https://github.com/bigchaindb/bigchaindb/pull/2486) +* Fixed a bug when validating a CREATE + TRANSFER. [Pull request #2487](https://github.com/bigchaindb/bigchaindb/pull/2487) +* Fixed the HTTP response when posting a transaction in commit mode. [Pull request #2510](https://github.com/bigchaindb/bigchaindb/pull/2510) +* Fixed a crash that happened when attempting to restart Planetmint at Tendermint block height 1. [Pull request#2519](https://github.com/bigchaindb/bigchaindb/pull/2519) + +### External Contributors + +@danacr - [Pull request #2447](https://github.com/bigchaindb/bigchaindb/pull/2447) + +### Notes + +The docs section titled "Production Deployment Template" was renamed to "Kubernetes Deployment Template" and we no longer consider it the go-to deployment template. The "Simple Deployment Template" is simpler, easier to understand, and less expensive (unless you are with an organization that already has a big Kubernetes cluster). + +## [2.0 Beta 5] - 2018-08-01 + +Tag name: v2.0.0b5 + +### Changed + +* Supported version of Tendermint `0.22.3` -> `0.22.8`. [Pull request #2429](https://github.com/bigchaindb/bigchaindb/pull/2429). + +### Fixed + +* Stateful validation raises a DoubleSpend exception if there is any other transaction that spends the same output(s) even if it has the same transaction ID. [Pull request #2422](https://github.com/bigchaindb/bigchaindb/pull/2422). + +## [2.0 Beta 4] - 2018-07-30 + +Tag name: v2.0.0b4 + +### Added + +- Added scripts for creating a configuration to manage processes with Monit. [Pull request #2410](https://github.com/bigchaindb/bigchaindb/pull/2410). + +### Fixed + +- Redundant asset and metadata queries were removed. [Pull request #2409](https://github.com/bigchaindb/bigchaindb/pull/2409). +- Signal handling was fixed for Planetmint processes. [Pull request #2395](https://github.com/bigchaindb/bigchaindb/pull/2395). +- Some of the abruptly closed sockets that used to stay in memory are being cleaned up now. [Pull request 2408](https://github.com/bigchaindb/bigchaindb/pull/2408). +- Fixed the bug when WebSockets powering Events API became unresponsive. [Pull request #2413](https://github.com/bigchaindb/bigchaindb/pull/2413). + +### Notes: + +* The instructions on how to write a BEP were simplified. [Pull request #2347](https://github.com/bigchaindb/bigchaindb/pull/2347). +* A section about troubleshooting was added to the network setup guide. [Pull request #2398](https://github.com/bigchaindb/bigchaindb/pull/2398). +* Some of the core code was given a better package structure. [Pull request #2401](https://github.com/bigchaindb/bigchaindb/pull/2401). +* Some of the previously disabled unit tests were re-enabled and updated. Pull requests [#2404](https://github.com/bigchaindb/bigchaindb/pull/2404) and [#2402](https://github.com/bigchaindb/bigchaindb/pull/2402). +* Some building blocks for dynamically adding new validators were introduced. [Pull request #2392](https://github.com/bigchaindb/bigchaindb/pull/2392). + +## [2.0 Beta 3] - 2018-07-18 + +Tag name: v2.0.0b3 + +### Fixed + +Fixed a bug in transaction validation. For some more-complex situations, it would say that a valid transaction was invalid. This bug was actually fixed before; it was [issue #1271](https://github.com/bigchaindb/bigchaindb/issues/1271). The unit test for it was turned off while we integrated Tendermint. Then the query implementation code got changed, reintroducing the bug, but the unit test was off so the bug wasn't caught. When we turned the test back on, shortly after releasing Beta 2, it failed, unveiling the bug. [Pull request #2389](https://github.com/bigchaindb/bigchaindb/pull/2389) + +## [2.0 Beta 2] - 2018-07-16 + +Tag name: v2.0.0b2 + +### Added + +* Added new configuration settings `tendermint.host` and `tendermint.port`. [Pull request #2342](https://github.com/bigchaindb/bigchaindb/pull/2342) +* Added tests to ensure that Planetmint gracefully handles "nasty" strings in keys and values. [Pull request #2334](https://github.com/bigchaindb/bigchaindb/pull/2334) +* Added a new logging handler to capture benchmark stats to a separate file. [Pull request #2349](https://github.com/bigchaindb/bigchaindb/pull/2349) + +### Changed + +* Changed the names of Planetmint processes (Python processes) to include 'bigchaindb', so they are easier to spot and find. [Pull request #2354](https://github.com/bigchaindb/bigchaindb/pull/2354) +* Updated all code to support the latest version of Tendermint. Note that the Planetmint ABCI server now listens to port 26657 instead of 46657. Pull requests [#2375](https://github.com/bigchaindb/bigchaindb/pull/2375) and [#2380](https://github.com/bigchaindb/bigchaindb/pull/2380) + +### Removed + +Removed all support and code for the old backlog_reassign_delay setting. [Pull request #2332](https://github.com/bigchaindb/bigchaindb/pull/2332) + +### Fixed + +* Fixed a bug that sometimes arose when using Docker Compose. (Tendermint would freeze.) [Pull request #2341](https://github.com/bigchaindb/bigchaindb/pull/2341) +* Fixed a bug in the code that creates a MongoDB index for the "id" in the transactions collection. It works now, and performance is improved. [Pull request #2378](https://github.com/bigchaindb/bigchaindb/pull/2378) +* The logging server would keep runnning in some tear-down scenarios. It doesn't do that any more. [Pull request #2304](https://github.com/bigchaindb/bigchaindb/pull/2304) + +### External Contributors + +@hrntknr - [Pull request #2331](https://github.com/bigchaindb/bigchaindb/pull/2331) + +### Known Issues + +The `bigchaindb upsert-validator` subcommand is not working yet, but a solution ([BEP-21](https://github.com/bigchaindb/BEPs/tree/master/21)) has been finalized and will be implemented before we release the final Planetmint 2.0. + +### Notes + +* A lot of old/dead code was deleted. Pull requests +[#2319](https://github.com/bigchaindb/bigchaindb/pull/2319), +[#2338](https://github.com/bigchaindb/bigchaindb/pull/2338), +[#2357](https://github.com/bigchaindb/bigchaindb/pull/2357), +[#2365](https://github.com/bigchaindb/bigchaindb/pull/2365), +[#2366](https://github.com/bigchaindb/bigchaindb/pull/2366), +[#2368](https://github.com/bigchaindb/bigchaindb/pull/2368) and +[#2374](https://github.com/bigchaindb/bigchaindb/pull/2374) +* Improved the documentation page "How to setup a Planetmint Network". [Pull Request #2312](https://github.com/bigchaindb/bigchaindb/pull/2312) + +## [2.0 Beta 1] - 2018-06-01 + +Tag name: v2.0.0b1 + +### Fixed + +* Fixed a bug that arose with some code that treated transactions-waiting-for-block-inclusion as if they were already stored in MongoDB (i.e. already in a block). [Pull request #2318](https://github.com/bigchaindb/bigchaindb/pull/2318) +* If a user asked for a block and it happened to be an empty block, Planetmint returned 404 Not Found, as if the block did not exist. Now it returns a 200 OK with a block containing no transactions, i.e. an empty block. [Pull request #2321](https://github.com/bigchaindb/bigchaindb/pull/2321) + +### Known Issues + +* An issue was found with the `bigchaindb upsert-validator` command. A solution was proposed in [BEP-19](https://github.com/bigchaindb/BEPs/pull/45) and is being implemented in [pull request #2314](https://github.com/bigchaindb/bigchaindb/pull/2314) +* If you run Planetmint locally using `make start` (i.e. using Docker Compose) and then you put the node under heavy write load, Tendermint can become unresponsive and running `make stop` can hang. +* There seems to be one or more issues with Tendermint when it is put under heavy load (i.e. even when Planetmint isn't involved). See Tendermint issues [#1394](https://github.com/tendermint/tendermint/issues/1394), [#1642](https://github.com/tendermint/tendermint/issues/1642) and [#1661](https://github.com/tendermint/tendermint/issues/1661) + +### Notes + +* There's a [new docs page](https://docs.bigchaindb.com/projects/server/en/v2.0.0b1/simple-network-setup.html) about how to set up a network where each node uses a single virtual machine. +* We checked, and Planetmint 2.0 Beta 1 works with MongoDB 3.6 (and 3.4). +* Support for RethinkDB is completely gone. + +## [2.0 Alpha 6] - 2018-05-17 + +Tag name: v2.0.0a6 + +### Changed + +Upgraded PyMongo to version 3.6 (which is compatible with MongoDB 3.6, 3.4 [and more](https://docs.mongodb.com/ecosystem/drivers/driver-compatibility-reference/#python-driver-compatibility)). [Pull request #2298](https://github.com/bigchaindb/bigchaindb/pull/2298) + +### Fixed + +When deploying a node using our Docker Compose file, it didn't expose port 46656, which is used by Tendermint for inter-node communications, so the node couldn't communicate with other nodes. We fixed that in [pull request #2299](https://github.com/bigchaindb/bigchaindb/pull/2299) + +### Notes + +We ran all our tests using MongoDB 3.6 and they all passed, so it seems safe to use Planetmint with MongoDB 3.6 from now on. + +## [2.0 Alpha 5] - 2018-05-11 + +Tag name: v2.0.0a5 + +### Changed + +To resolve [issue #2279](https://github.com/bigchaindb/bigchaindb/issues/2279), we made some changes to the `bigchaindb-abci` package (which is our fork of `py-abci`) and told Planetmint to use the new version (`bigchaindb-abci==0.4.5`). [Pull request #2281](https://github.com/bigchaindb/bigchaindb/pull/2281). + +## [2.0 Alpha 4] - 2018-05-09 + +Tag name: v2.0.0a4 + +### Changed + +The Kubernetes liveness probe for the Planetmint StatefulSet was improved to check the Tendermint /status endpoint in addition to the Tendermint /abci_info endpoint. [Pull request #2275](https://github.com/bigchaindb/bigchaindb/pull/2275) + +### Fixed + +[Pull request #2270](https://github.com/bigchaindb/bigchaindb/pull/2270) resolved [issue #2269](https://github.com/bigchaindb/bigchaindb/issues/2269). + +### Notes + +There's a new [page in the docs about storing files in Planetmint](https://docs.bigchaindb.com/en/latest/store-files.html). [Pull request #2259](https://github.com/bigchaindb/bigchaindb/pull/2259) + +## [2.0 Alpha 3] - 2018-05-03 + +Tag name: v2.0.0a3 + +### Changed + +* Upgraded Planetmint Server code to use the latest version of Tendermint: version 0.19.2. Pull requests [#2249](https://github.com/bigchaindb/bigchaindb/pull/2249), [#2252](https://github.com/bigchaindb/bigchaindb/pull/2252) and [#2253](https://github.com/bigchaindb/bigchaindb/pull/2253) +* Made some fixes to `py-abci` (an external Python package) and used our fixed version with Planetmint. Those fixes resolved several known issues, including [issue #2182](https://github.com/bigchaindb/bigchaindb/issues/2182) and issues with large transactions in general. Note: At the time of writing, our fixes to `py-abci` hadn't been merged into the main `py-abci` repository or its latest release on PyPI; we were using our own special `bigchaindb-abci` package (which included our fixes). Pull requests [#2250](https://github.com/bigchaindb/bigchaindb/pull/2250) and [#2261](https://github.com/bigchaindb/bigchaindb/pull/2261) +* If Planetmint Server crashes and then comes back, Tendermint Core doesn't try to reconnect to it. That's just how Tendermint Core works. We revised our Kubernetes-based production deployment template to resolve that issue: Planetmint Server and Tendermint Core are now in the same Kubernetes StatefulSet; if the connection between them ever goes down, then Kubernetes restarts the whole StatefulSet. [Pull request #2242](https://github.com/bigchaindb/bigchaindb/pull/2242) + +### Fixed + +Re-enabled multi-threading. [Pull request #2258](https://github.com/bigchaindb/bigchaindb/pull/2258) + +### Known Issues + +Tendermint changed how it responds to a request to store data (via the [Tendermint Broadcast API](https://tendermint.com/docs/tendermint-core/using-tendermint.html#broadcast-api)) between version 0.12 and 0.19.2. We started modifying the code of Planetmint Server to account for those changes in responses (in [pull request #2239](https://github.com/bigchaindb/bigchaindb/pull/2239)), but we found that there's a difference between what the Tendermint documentation _says_ about those responses and how Tendermint actually responds. We need to determine Tendermint's intent before we can finalize that pull request. + +### Notes + +We were focused on getting the public Planetmint Testnet stable during the development of Planetmint 2.0 Alpha 3, and we think we largely succeeded. Because of that focus, we delayed the deployment of an internal test network until later. It would have had the same instabilities as the public Planetmint Testnet anyway. In the future, we'll always test a new version of Planetmint on our internal test network before deploying it on the public Planetmint Testnet. (That wasn't possible this time around, because there was no old/stable version of Planetmint 2.n to run on the public Planetmint Testnet while we tested Planetmint 2.[n+1] internally.) + +## [2.0 Alpha 2] - 2018-04-18 + +Tag name: v2.0.0a2 + +### Added + +An implementation of [BEP-8 (Planetmint Enhancement Proposal #8)](https://github.com/bigchaindb/BEPs/tree/master/8), which makes sure a node can recover from a system fault (e.g. a crash) into a consistent state, i.e. a state where the data in the node's local MongoDB database is consistent with the data stored in the blockchain. Pull requests [#2135](https://github.com/bigchaindb/bigchaindb/pull/2135) and [#2207](https://github.com/bigchaindb/bigchaindb/pull/2207) + +### Changed + +When someone uses the HTTP API to send a new transaction to a Planetmint network using the [POST /api/v1/transactions?mode={mode}](https://docs.bigchaindb.com/projects/server/en/master/http-client-server-api.html#post--api-v1-transactions?mode=mode) endpoint, they now get back a more informative HTTP response, so they can better-understand what happened. This is only when mode is `commit` or `sync`, because `async` _means_ that the response is immediate, without waiting to see what happened to the transaction. [Pull request #2198](https://github.com/bigchaindb/bigchaindb/pull/2198) + +### Known Issues + +* If Planetmint Server crashes and then is restarted, Tendermint Core won't try to reconnect to Planetmint Server and so all operations requiring that connection won't work. We only understood this recently. We'll write a blog post explaining what we intend to do about it. +* The known issues in 2.0 Alpha (listed below) are still there. + +## [2.0 Alpha] - 2018-04-03 + +Tag name: v2.0.0a1 + +There were _many_ changes between 1.3 and 2.0 Alpha, too many to list here. We wrote a series of blog posts to summarize most changes, especially those that affect end users and application developers: + +* [Planetmint 2.0 is Byzantine Fault Tolerant](https://blog.bigchaindb.com/bigchaindb-2-0-is-byzantine-fault-tolerant-5ffdac96bc44) +* [Some HTTP API Changes in the Next Release](https://blog.bigchaindb.com/some-http-api-changes-in-the-next-release-49612a537b0c) +* [Three Transaction Model Changes in the Next Release](https://blog.bigchaindb.com/three-transaction-model-changes-in-the-next-release-dadbac50094a) +* [Changes to the Server Command Line Interface in Planetmint 2.0](https://blog.bigchaindb.com/changes-to-the-server-command-line-interface-in-bigchaindb-2-0-e1d6576e7155) +* _A forthcoming post about changes in Planetmint Server configuration settings_ + +### External Contributors + +* @r7vme contributed [pull request #1984](https://github.com/bigchaindb/bigchaindb/pull/1984) which fixed a bug in our Kubernetes-based production deployment template. + +### Known Issues + +We intend to resolve these issues before releasing the final Planetmint 2.0: + +* There's a known Heisenbug that (sometimes) arises and we found that making the Planetmint webserver single-threaded prevents that bug from causing problems. We intend to resolve that bug, but in the meantime our temporary workaround is to change the _default_ webserver configuration settings to single-threaded mode, i.e. `PLANETMINT_SERVER_WORKERS=1` and `PLANETMINT_SERVER_THREADS=1`. +* Issues sometimes happen when a large transaction is sent to a Planetmint network. + +## [1.3] - 2017-11-21 +Tag name: v1.3.0 + +### Added +* Metadata full-text search. [Pull request #1812](https://github.com/bigchaindb/bigchaindb/pull/1812) + +### Notes +* Improved documentation about blocks and votes. [Pull request #1855](https://github.com/bigchaindb/bigchaindb/pull/1855) + + +## [1.2] - 2017-11-13 +Tag name: v1.2.0 + +### Added +* New and improved installation setup docs and code. Pull requests [#1775](https://github.com/bigchaindb/bigchaindb/pull/1775) and [#1785](https://github.com/bigchaindb/bigchaindb/pull/1785) +* New Planetmint configuration setting to set the port number of the log server: `log.port`. [Pull request #1796](https://github.com/bigchaindb/bigchaindb/pull/1796) +* New secondary index on `id` in the bigchain table. That will make some queries execute faster. [Pull request #1803](https://github.com/bigchaindb/bigchaindb/pull/1803) +* When using MongoDB, there are some restrictions on allowed names for keys (JSON keys). Those restrictions were always there but now Planetmint checks key names explicitly, rather than leaving that to MongoDB. Pull requests [#1807](https://github.com/bigchaindb/bigchaindb/pull/1807) and [#1811](https://github.com/bigchaindb/bigchaindb/pull/1811) +* When using MongoDB, there are some restrictions on the allowed values of "language" (if that key is used in the values of `metadata` or `asset.data`). Those restrictions were always there but now Planetmint checks the values explicitly, rather than leaving that to MongoDB. Pull requests [#1806](https://github.com/bigchaindb/bigchaindb/pull/1806) and [#1811](https://github.com/bigchaindb/bigchaindb/pull/1811) +* There's a new page in the root docs about permissions in Planetmint. [Pull request #1788](https://github.com/bigchaindb/bigchaindb/pull/1788) +* There's a new option in the `bigchaindb start` command: `bigchaindb start --no-init` will avoid doing `bigchaindb init` if it wasn't done already. [Pull request #1814](https://github.com/bigchaindb/bigchaindb/pull/1814) + +### Fixed +* Fixed a bug where setting the log level in a Planetmint config file didn't have any effect. It does now. [Pull request #1797](https://github.com/bigchaindb/bigchaindb/pull/1797) +* The docs were wrong about there being no Ping/Pong support in the Events API. There is, so the docs were fixed. [Pull request #1799](https://github.com/bigchaindb/bigchaindb/pull/1799) +* Fixed an issue with closing WebSocket connections properly. [Pull request #1819](https://github.com/bigchaindb/bigchaindb/pull/1819) + +### Notes +* Many changes were made to the Kubernetes-based production deployment template and code. + + +## [1.1] - 2017-09-26 +Tag name: v1.1.0 + +### Added +* Support for server-side plugins that can add support for alternate event consumers (other than the WebSocket API). [Pull request #1707](https://github.com/bigchaindb/bigchaindb/pull/1707) +* New configuration settings to set the *advertised* wsserver scheme, host and port. (The *advertised* ones are the ones that external users use to connect to the WebSocket API.) [Pull request #1703](https://github.com/bigchaindb/bigchaindb/pull/1703) +* Support for secure (TLS) WebSocket connections. [Pull request #1619](https://github.com/bigchaindb/bigchaindb/pull/1619) +* A new page of documentation about the contents of a condition (inside a transaction). [Pull request #1668](https://github.com/bigchaindb/bigchaindb/pull/1668) + +### Changed +* We updated our definition of the **public API** (at the top of this document). [Pull request #1700](https://github.com/bigchaindb/bigchaindb/pull/1700) +* The HTTP API Logger now logs the request path and method as well. [Pull request #1644](https://github.com/bigchaindb/bigchaindb/pull/1644) + +### External Contributors +* @carchrae - [Pull request #1731](https://github.com/bigchaindb/bigchaindb/pull/1731) +* @ivanbakel - [Pull request #1706](https://github.com/bigchaindb/bigchaindb/pull/1706) +* @ketanbhatt - Pull requests [#1643](https://github.com/bigchaindb/bigchaindb/pull/1643) and [#1644](https://github.com/bigchaindb/bigchaindb/pull/1644) + +### Notes +* New drivers & tools from our community: + * [Java driver](https://github.com/authenteq/java-bigchaindb-driver), by [Authenteq](https://authenteq.com/) + * [Ruby library](https://rubygems.org/gems/bigchaindb), by @nileshtrivedi +* Many improvements to our production deployment template (which uses Kubernetes). +* The production deployment template for the multi-node case was out of date. We updated that and verified it. [Pull request #1713](https://github.com/bigchaindb/bigchaindb/pull/1713) + + +## [1.0.1] - 2017-07-13 +Tag name: v1.0.1 + +### Fixed +* Various issues in the Quickstart page. Pull requests + [#1641](https://github.com/bigchaindb/bigchaindb/pull/1641) and + [#1648](https://github.com/bigchaindb/bigchaindb/pull/1648). +* Changefeed hanging when MongoDB primary node is turned off. + [Pull request #1638](https://github.com/bigchaindb/bigchaindb/pull/1638). +* Missing `assets` tables for RethinkDB backend. + [Pull request #1646](https://github.com/bigchaindb/bigchaindb/pull/1646). +* Cryptoconditions version mismatch. + [Pull request #1659](https://github.com/bigchaindb/bigchaindb/pull/1659). + + +## [1.0.0] - 2017-07-05 +Tag name: v1.0.0 + +**This just reports the changes since the release of 1.0.0rc1. If you want the full picture of all changes since 0.10, then read the 1.0.0rc1 change log below as well as the upgrade guide.** + +### Changed +* The file name of the upgrade guide changed from `docs/upgrade-guides/v0.10-->v1.0.md` to `docs/upgrade-guides/v0.10-v1.0.md`. +* In `transaction.inputs[n].fulfills`, `output` was renamed to `output_index`. [Pull Request #1596](https://github.com/bigchaindb/bigchaindb/pull/1596) +* In `transaction.outputs[n].condition.details`, 1) `signature` was removed (from signature conditions) and 2) `subfulfillments` was renamed to `subconditions` (in threshold conditions). [Pull Request #1589](https://github.com/bigchaindb/bigchaindb/pull/1589) +* Refined transaction schema validation to check that the `transaction.outputs[n].condition.uri` corresponds to a condition that Planetmint Server 1.0.0 actually supports. [Pull Request #1597](https://github.com/bigchaindb/bigchaindb/pull/1597) +* Before, GET requests (to the HTTP API) with header `Content-Type: 'application/json'` would get a response with the message, "The browser (or proxy) sent a request that this server could not understand." Now, if a GET request includes a `Content-Type` header, that header gets deleted (i.e. ignored). [Pull Request #1630](https://github.com/bigchaindb/bigchaindb/pull/1630) + +### Fixed +* If an end user sends a transaction with `operation` equal to `GENESIS`, it will be rejected as invalid. [Pull Request #1612](https://github.com/bigchaindb/bigchaindb/pull/1612) + + +## [1.0.0rc1] - 2017-06-23 +Tag name: v1.0.0rc1 + +### Added +* Support for secure TLS/SSL communication between MongoDB and {Planetmint, MongoDB Backup Agent, MongoDB Monitoring Agent}. Pull Requests +[#1456](https://github.com/bigchaindb/bigchaindb/pull/1456), +[#1497](https://github.com/bigchaindb/bigchaindb/pull/1497), +[#1510](https://github.com/bigchaindb/bigchaindb/pull/1510), +[#1536](https://github.com/bigchaindb/bigchaindb/pull/1536), +[#1551](https://github.com/bigchaindb/bigchaindb/pull/1551) and +[#1552](https://github.com/bigchaindb/bigchaindb/pull/1552). +* Text search support (only if using MongoDB). Pull Requests [#1469](https://github.com/bigchaindb/bigchaindb/pull/1469) and [#1471](https://github.com/bigchaindb/bigchaindb/pull/1471) +* The `database.connection_timeout` configuration setting now works with RethinkDB too. [#1512](https://github.com/bigchaindb/bigchaindb/pull/1512) +* New code and tools for benchmarking CREATE transactions. [Pull Request #1511](https://github.com/bigchaindb/bigchaindb/pull/1511) + +### Changed +* There's an upgrade guide in `docs/upgrade-guides/v0.10-->v1.0.md`. It only covers changes to the transaction model and HTTP API. If that file hasn't been merged yet, see [Pull Request #1547](https://github.com/bigchaindb/bigchaindb/pull/1547) +* Cryptographic signatures now sign the whole (serialized) transaction body, including the transaction ID, but with all `"fulfillment"` values changed to `None`. [Pull Request #1225](https://github.com/bigchaindb/bigchaindb/pull/1225) +* In transactions, the value of `"amount"` must be a string. (Before, it was supposed to be a number.) [Pull Request #1286](https://github.com/bigchaindb/bigchaindb/pull/1286) +* In `setup.py`, the "Development Status" (as reported on PyPI) was changed from Alpha to Beta. [Pull Request #1437](https://github.com/bigchaindb/bigchaindb/pull/1437) +* If you explicitly specify a config file, e.g. `bigchaindb -c path/to/config start` and that file can't be found, then Planetmint Server will fail with a helpful error message. [Pull Request #1486](https://github.com/bigchaindb/bigchaindb/pull/1486) +* Reduced the response time on the HTTP API endpoint to get all the unspent outputs associated with a given public key (a.k.a. "fast unspents"). [Pull Request #1411](https://github.com/bigchaindb/bigchaindb/pull/1411) +* Internally, the value of an asset's `"data"` is now stored in a separate assets table. This enabled the new text search. Each asset data is stored along with the associated CREATE transaction ID (asset ID). That data gets written when the containing block gets written to the bigchain table. [Pull Request #1460](https://github.com/bigchaindb/bigchaindb/pull/1460) +* Schema validation was sped up by switching to `rapidjson-schema`. [Pull Request #1494](https://github.com/bigchaindb/bigchaindb/pull/1494) +* If a node comes back from being down for a while, it will resume voting on blocks in the order determined by the MongoDB oplog, in the case of MongoDB. (In the case of RethinkDB, blocks missed in the changefeed will not be voted on.) [Pull Request #1389](https://github.com/bigchaindb/bigchaindb/pull/1389) +* Parallelized transaction schema validation in the vote pipeline. [Pull Request #1492](https://github.com/bigchaindb/bigchaindb/pull/1492) +* `asset.data` or `asset.id` are now *required* in a CREATE or TRANSFER transaction, respectively. [Pull Request #1518](https://github.com/bigchaindb/bigchaindb/pull/1518) +* The HTTP response body, in the response to the `GET /` and the `GET /api/v1` endpoints, was changed substantially. [Pull Request #1529](https://github.com/bigchaindb/bigchaindb/pull/1529) +* Changed the HTTP `GET /api/v1/transactions/{transaction_id}` endpoint. It now only returns the transaction if it's in a valid block. It also returns a new header with a relative link to a status monitor. [Pull Request #1543](https://github.com/bigchaindb/bigchaindb/pull/1543) +* All instances of `txid` and `tx_id` were replaced with `transaction_id`, in the transaction model and the HTTP API. [Pull Request #1532](https://github.com/bigchaindb/bigchaindb/pull/1532) +* The hostname and port were removed from all URLs in all HTTP API responses. [Pull Request #1538](https://github.com/bigchaindb/bigchaindb/pull/1538) +* Relative links were replaced with JSON objects in HTTP API responses. [Pull Request #1541](https://github.com/bigchaindb/bigchaindb/pull/1541) +* In the outputs endpoint of the HTTP API, the query parameter `unspent` was changed to `spent` (so no more double negatives). If that query parameter isn't included, then all outputs matching the specificed public key will be returned. If `spent=true`, then only the spent outputs will be returned. If `spent=false`, then only the unspent outputs will be returned. [Pull Request #1545](https://github.com/bigchaindb/bigchaindb/pull/1545) +* The supported crypto-conditions changed from version 01 of the crypto-conditions spec to version 02. [Pull Request #1562](https://github.com/bigchaindb/bigchaindb/pull/1562) +* The value of "version" inside a transaction must now be "1.0". (Before, it could be "0.anything".) [Pull Request #1574](https://github.com/bigchaindb/bigchaindb/pull/1574) + +### Removed +* The `server.threads` configuration setting (for the Gunicorn HTTP server) was removed from the default set of Planetmint configuration settings. [Pull Request #1488](https://github.com/bigchaindb/bigchaindb/pull/1488) + +### Fixed +* The `GET /api/v1/outputs` endpoint was failing for some transactions with threshold conditions. Fixed in [Pull Request #1450](https://github.com/bigchaindb/bigchaindb/pull/1450) + +### External Contributors +* @elopio - Pull Requests [#1415](https://github.com/bigchaindb/bigchaindb/pull/1415) and [#1491](https://github.com/bigchaindb/bigchaindb/pull/1491) +* @CsterKuroi - [Pull Request #1447](https://github.com/bigchaindb/bigchaindb/pull/1447) +* @tdsgit - [Pull Request #1512](https://github.com/bigchaindb/bigchaindb/pull/1512) +* @lavinasachdev3 - [Pull Request #1357](https://github.com/bigchaindb/bigchaindb/pull/1357) + +### Notes +* We dropped support for Python 3.4. [Pull Request #1564](https://github.com/bigchaindb/bigchaindb/pull/1564) +* There were many improvements to our Kubernetes-based production deployment template (and the associated documentaiton). +* There is now a [Planetmint Ruby driver](https://github.com/LicenseRocks/bigchaindb_ruby), created by @addywaddy at [license.rocks](https://github.com/bigchaindb/bigchaindb/pull/1437). +* The [Planetmint JavaScript driver](https://github.com/bigchaindb/js-bigchaindb-driver) was moved to a different GitHub repo and is now officially maintained by the Planetmint team. +* We continue to recommend using MongoDB. + +## [0.10.3] - 2017-06-29 +Tag name: v0.10.3 + +## Fixed +* Pin minor+ version of `cryptoconditions` to avoid upgrading to a non + compatible version. +[commit 97268a5](https://github.com/bigchaindb/bigchaindb/commit/97268a577bf27942a87d8eb838f4816165c84fd5) + +## [0.10.2] - 2017-05-16 +Tag name: v0.10.2 + +### Added +* Add Cross Origin Resource Sharing (CORS) support for the HTTP API. + [Commit 6cb7596](https://github.com/bigchaindb/bigchaindb/commit/6cb75960b05403c77bdae0fd327612482589efcb) + +### Fixed +* Fixed `streams_v1` API link in response to `GET /api/v1`. + [Pull Request #1466](https://github.com/bigchaindb/bigchaindb/pull/1466) +* Fixed mismatch between docs and implementation for `GET /blocks?status=` + endpoint. The `status` query parameter is now case insensitive. + [Pull Request #1464](https://github.com/bigchaindb/bigchaindb/pull/1464) + +## [0.10.1] - 2017-04-19 +Tag name: v0.10.1 + +### Added +* Documentation for the Planetmint settings `wsserver.host` and `wsserver.port`. [Pull Request #1408](https://github.com/bigchaindb/bigchaindb/pull/1408) + +### Fixed +* Fixed `Dockerfile`, which was failing to build. It now starts `FROM python:3.6` (instead of `FROM ubuntu:xenial`). [Pull Request #1410](https://github.com/bigchaindb/bigchaindb/pull/1410) +* Fixed the `Makefile` so that `release` depends on `dist`. [Pull Request #1405](https://github.com/bigchaindb/bigchaindb/pull/1405) + +## [0.10.0] - 2017-04-18 +Tag name: v0.10.0 + +### Added +* Improved logging. Added logging to file. Added `--log-level` option to `bigchaindb start` command. Added new logging configuration settings. Pull Requests +[#1285](https://github.com/bigchaindb/bigchaindb/pull/1285), +[#1307](https://github.com/bigchaindb/bigchaindb/pull/1307), +[#1324](https://github.com/bigchaindb/bigchaindb/pull/1324), +[#1326](https://github.com/bigchaindb/bigchaindb/pull/1326), +[#1327](https://github.com/bigchaindb/bigchaindb/pull/1327), +[#1330](https://github.com/bigchaindb/bigchaindb/pull/1330), +[#1365](https://github.com/bigchaindb/bigchaindb/pull/1365), +[#1394](https://github.com/bigchaindb/bigchaindb/pull/1394), +[#1396](https://github.com/bigchaindb/bigchaindb/pull/1396), +[#1398](https://github.com/bigchaindb/bigchaindb/pull/1398) and +[#1402](https://github.com/bigchaindb/bigchaindb/pull/1402) +* Events API using WebSocket protocol. Pull Requests +[#1086](https://github.com/bigchaindb/bigchaindb/pull/1086), +[#1347](https://github.com/bigchaindb/bigchaindb/pull/1347), +[#1349](https://github.com/bigchaindb/bigchaindb/pull/1349), +[#1356](https://github.com/bigchaindb/bigchaindb/pull/1356), +[#1368](https://github.com/bigchaindb/bigchaindb/pull/1368), +[#1401](https://github.com/bigchaindb/bigchaindb/pull/1401) and +[#1403](https://github.com/bigchaindb/bigchaindb/pull/1403) +* Initial support for using SSL with MongoDB (work in progress). Pull Requests +[#1299](https://github.com/bigchaindb/bigchaindb/pull/1299) and +[#1348](https://github.com/bigchaindb/bigchaindb/pull/1348) + +### Changed +* The main Planetmint Dockerfile (and its generated Docker image) now contains only Planetmint Server. (It used to contain both Planetmint Server and RethinkDB.) You must now run MongoDB or RethinkDB in a separate Docker container. [Pull Request #1174](https://github.com/bigchaindb/bigchaindb/pull/1174) +* Made separate schemas for CREATE and TRANSFER transactions. [Pull Request #1257](https://github.com/bigchaindb/bigchaindb/pull/1257) +* When signing transactions with threshold conditions, we now sign all subconditions for a public key. [Pull Request #1294](https://github.com/bigchaindb/bigchaindb/pull/1294) +* Many changes to the voting-related code, including how we validate votes and prevent duplicate votes by the same node. Pull Requests [#1215](https://github.com/bigchaindb/bigchaindb/pull/1215) and [#1258](https://github.com/bigchaindb/bigchaindb/pull/1258) + +### Removed +* Removed the `bigchaindb load` command. Pull Requests +[#1261](https://github.com/bigchaindb/bigchaindb/pull/1261), +[#1273](https://github.com/bigchaindb/bigchaindb/pull/1273) and +[#1301](https://github.com/bigchaindb/bigchaindb/pull/1301) +* Removed old `/speed-tests` and `/benchmarking-tests` directories. [Pull Request #1359](https://github.com/bigchaindb/bigchaindb/pull/1359) + +### Fixed +* Fixed the URL of the Planetmint docs returned by the HTTP API. [Pull Request #1178](https://github.com/bigchaindb/bigchaindb/pull/1178) +* Fixed the MongoDB changefeed: it wasn't reporting update operations. [Pull Request #1193](https://github.com/bigchaindb/bigchaindb/pull/1193) +* Fixed the block-creation process: it wasn't checking if the transaction was previously included in: + * a valid block. [Pull Request #1208](https://github.com/bigchaindb/bigchaindb/pull/1208) + * the block-under-construction. Pull Requests [#1237](https://github.com/bigchaindb/bigchaindb/issues/1237) and [#1377](https://github.com/bigchaindb/bigchaindb/issues/1377) + +### External Contributors +In alphabetical order by GitHub username: +* @anryko - [Pull Request #1277](https://github.com/bigchaindb/bigchaindb/pull/1277) +* @anujism - [Pull Request #1366](https://github.com/bigchaindb/bigchaindb/pull/1366) +* @jackric - [Pull Request #1365](https://github.com/bigchaindb/bigchaindb/pull/1365) +* @lavinasachdev3 - [Pull Request #1358](https://github.com/bigchaindb/bigchaindb/pull/1358) +* @morrme - [Pull Request #1340](https://github.com/bigchaindb/bigchaindb/pull/1340) +* @tomconte - [Pull Request #1299](https://github.com/bigchaindb/bigchaindb/pull/1299) +* @tymlez - Pull Requests [#1108](https://github.com/bigchaindb/bigchaindb/pull/1108) & [#1209](https://github.com/bigchaindb/bigchaindb/pull/1209) + +### Notes +* MongoDB is now the recommended database backend (not RethinkDB). +* There are some initial docs about how to deploy a Planetmint node on Kubernetes. It's work in progress. + + +## [0.9.5] - 2017-03-29 +Tag name: v0.9.5 + +### Fixed +Upgrade `python-rapidjson` to `0.0.11`(fixes #1350 - thanks to @ferOnti for +reporting). + +## [0.9.4] - 2017-03-16 +Tag name: v0.9.4 + +### Fixed +Fixed #1271 (false double spend error). Thanks to @jmduque for reporting the +problem along with a very detailed diagnosis and useful recommendations. + +## [0.9.3] - 2017-03-06 +Tag name: v0.9.3 + +### Fixed +Fixed HTTP API 500 error on `GET /outputs`: issues #1200 and #1231. + +## [0.9.2] - 2017-03-02 +Tag name: v0.9.2 + +### Fixed +Pin `python-rapidjson` library in `setup.py` to prevent `bigchaindb`'s +installation to fail due to +https://github.com/python-rapidjson/python-rapidjson/issues/62. + +## [0.9.1] - 2017-02-06 +Tag name: v0.9.1 + +### Fixed +* Fixed bug in how the transaction `VERSION` string was calculated from the Planetmint Server `__short_version__` string. [Pull Request #1160](https://github.com/bigchaindb/bigchaindb/pull/1160) + + +## [0.9.0] - 2017-02-06 +Tag name: v0.9.0 + +It has been more than two months since the v0.8.0 release, so there have been _many_ changes. We decided to describe them in broad strokes, with links to more details elsewhere. + +### Added +- Support for MongoDB as a backend database. +- Some configuration settings and `bigchaindb` command-line commands were added. In particular, one can specify the database backend (`rethinkdb` or `mongodb`). For MongoDB, one can specify the name of the replicaset. Also for MongoDB, there are new command-line commands to add and remove hosts from the replicaset. See [the Settings & CLI docs](https://docs.bigchaindb.com/projects/server/en/v0.9.0/server-reference/index.html). +- Transaction schema validation. The transaction schema is also used to auto-generate some docs. [Pull Request #880](https://github.com/bigchaindb/bigchaindb/pull/880) +- Vote schema validation. The vote schema is also used to auto-generate some docs. [Pull Request #865](https://github.com/bigchaindb/bigchaindb/pull/865) +- New `ENABLE_WEB_ADMIN` setting in the AWS deployment configuration file. [Pull Request #1015](https://github.com/bigchaindb/bigchaindb/pull/1015) + +### Changed +- The transaction model has changed substantially. @libscott wrote a blog post about the changes and it will be published soon on [the Planetmint Blog](https://blog.bigchaindb.com/). Also, see [the docs about the transaction model](https://docs.bigchaindb.com/projects/server/en/v0.9.0/data-models/transaction-model.html). +- The HTTP API has changed substantially. @diminator wrote a blog post about the changes and it will be published soon on [the Planetmint Blog](https://blog.bigchaindb.com/). Also, see [the docs about the vote model](https://docs.bigchaindb.com/projects/server/en/v0.9.0/data-models/vote-model.html). +- All RethinkDB-specific database calls were replaced with abstract calls to a backend database. +- Some improvements to the Dockerfile, e.g. Pull Requests [#1011](https://github.com/bigchaindb/bigchaindb/pull/1011) and [#1121](https://github.com/bigchaindb/bigchaindb/pull/1121) +- Many improvements to the tests +- We standardized on supporting Ubuntu 16.04 for now (but Planetmint Server also works on similar Linux distros). + +### Removed +- `api_endpoint` was removed from the Planetmint configuration settings. (It was never used anyway.) [Pull Request #821](https://github.com/bigchaindb/bigchaindb/pull/821) +- Removed all remaining StatsD monitoring code, configuration settings, docs, etc. (We'll add another monitoring solution in the future.) [Pull Request #1138](https://github.com/bigchaindb/bigchaindb/pull/1138) + +### Fixed +- Fixed a memory (RAM) overflow problem when under heavy load by bounding the size of the queue at the entrance to the block pipeline. [Pull Request #908](https://github.com/bigchaindb/bigchaindb/pull/908) +- Fixed some logic in block validation. [Pull Request #1130](https://github.com/bigchaindb/bigchaindb/pull/1130) + +### External Contributors +- @amirelemam - [Pull Request #762](https://github.com/bigchaindb/bigchaindb/pull/762) (closed but used as the basis for [Pull Request #1074](https://github.com/bigchaindb/bigchaindb/pull/1074)) +- @utarl - [Pull Request #1019](https://github.com/bigchaindb/bigchaindb/pull/1019) + +### Notes +- There were many additions and changes to the documentation. Fun fact: The JSON in the HTTP API docs is now auto-generated to be consistent with the current code. +- There's a draft spec for a Planetmint Event Stream API and we welcome your feedback. See [Pull Request #1086](https://github.com/bigchaindb/bigchaindb/pull/1086) + + +## [0.8.2] - 2017-01-27 +Tag name: v0.8.2 + +### Fixed +- Fix spend input twice in same transaction + (https://github.com/bigchaindb/bigchaindb/issues/1099). + + +## [0.8.1] - 2017-01-16 +Tag name: v0.8.1 + +### Changed +- Upgrade pysha3 to 1.0.0 (supports official NIST standard). + +### Fixed +- Workaround for rapidjson problem with package metadata extraction + (https://github.com/kenrobbins/python-rapidjson/pull/52). + + +## [0.8.0] - 2016-11-29 +Tag name: v0.8.0 + +### Added +- The big new thing in version 0.8.0 is support for divisible assets, i.e. assets like carrots or thumbtacks, where the initial CREATE transaction can register/create some amount (e.g. 542 carrots), the first TRANSFER transaction can split that amount across multiple owners, and so on. [Pull Request #794](https://github.com/bigchaindb/bigchaindb/pull/794) +- Wrote a formal schema for the JSON structure of transactions. [Pull Request #798](https://github.com/bigchaindb/bigchaindb/pull/798) +- New configuration parameter: `backlog_reassign_delay`. [Pull Request #883](https://github.com/bigchaindb/bigchaindb/pull/883) + +### Changed +- CREATE transactions must now be signed by all `owners_before` (rather than by a federation node). [Pull Request #794](https://github.com/bigchaindb/bigchaindb/pull/794) +- The user-provided timestamp was removed from the transaction data model (schema). [Pull Request #817](https://github.com/bigchaindb/bigchaindb/pull/817) +- `get_transaction()` will now return a transaction from the backlog, even if there are copies of the transaction in invalid blocks. [Pull Request #793](https://github.com/bigchaindb/bigchaindb/pull/793) +- Several pull requests to introduce a generalized database interface, to move RethinkDB calls into a separate implementation of that interface, and to work on a new MongoDB implementation of that interface. Pull Requests +[#754](https://github.com/bigchaindb/bigchaindb/pull/754), +[#783](https://github.com/bigchaindb/bigchaindb/pull/783), +[#799](https://github.com/bigchaindb/bigchaindb/pull/799), +[#806](https://github.com/bigchaindb/bigchaindb/pull/806), +[#809](https://github.com/bigchaindb/bigchaindb/pull/809), +[#853](https://github.com/bigchaindb/bigchaindb/pull/853) +- Renamed "verifying key" to "public key". Renamed "signing key" to "private key". Renamed "vk" to "pk". [Pull Request #807](https://github.com/bigchaindb/bigchaindb/pull/807) +- `get_transaction_by_asset_id` now ignores invalid transactions. [Pull Request #810](https://github.com/bigchaindb/bigchaindb/pull/810) +- `get_transaction_by_metadata_id` now ignores invalid transactions. [Pull Request #811](https://github.com/bigchaindb/bigchaindb/pull/811) +- Updates to the configs and scripts for deploying a test network on AWS. The example config file deploys virtual machines running Ubuntu 16.04 now. Pull Requests +[#771](https://github.com/bigchaindb/bigchaindb/pull/771), +[#813](https://github.com/bigchaindb/bigchaindb/pull/813) +- Changed logging of transactions on block creation so now it just says the length of the list of transactions, rather than listing all the transactions. [Pull Request #861](https://github.com/bigchaindb/bigchaindb/pull/861) + +### Fixed +- Equality checks with AssetLinks. [Pull Request #825](https://github.com/bigchaindb/bigchaindb/pull/825) +- Bug in `bigchaindb load`. [Pull Request #824](https://github.com/bigchaindb/bigchaindb/pull/824) +- Two issues found with timestamp indexes. [Pull Request #816](https://github.com/bigchaindb/bigchaindb/pull/816) +- Hard-coded `backlog_reassign_delay`. [Pull Request #854](https://github.com/bigchaindb/bigchaindb/pull/854) +- Race condition in `test_stale_monitor.py`. [Pull Request #846](https://github.com/bigchaindb/bigchaindb/pull/846) +- When creating a signed vote, decode the vote signature to a `str`. [Pull Request #869](https://github.com/bigchaindb/bigchaindb/pull/869) +- Bug in AWS deployment scripts. Setting `BIND_HTTP_TO_LOCALHOST` to `False` didn't actually work. It does now. [Pull Request #870](https://github.com/bigchaindb/bigchaindb/pull/870) + +### External Contributors +- @najlachamseddine - [Pull Request #528](https://github.com/bigchaindb/bigchaindb/pull/528) +- @ChristianGaertner - [Pull Request #659](https://github.com/bigchaindb/bigchaindb/pull/659) +- @MinchinWeb - [Pull Request #695](https://github.com/bigchaindb/bigchaindb/pull/695) +- @ckeyer - [Pull Request #785](https://github.com/bigchaindb/bigchaindb/pull/785) + +### Notes +- @ChristianGaertner added a Python style checker (Flake8) to Travis CI, so external contributors should be aware that the Python code in their pull requests will be checked. See [our Python Style Guide](PYTHON_STYLE_GUIDE.md). +- Several additions and changes to the documentation, e.g. Pull Requests +[#690](https://github.com/bigchaindb/bigchaindb/pull/690), +[#764](https://github.com/bigchaindb/bigchaindb/pull/764), +[#766](https://github.com/bigchaindb/bigchaindb/pull/766), +[#769](https://github.com/bigchaindb/bigchaindb/pull/769), +[#777](https://github.com/bigchaindb/bigchaindb/pull/777), +[#800](https://github.com/bigchaindb/bigchaindb/pull/800), +[#801](https://github.com/bigchaindb/bigchaindb/pull/801), +[#802](https://github.com/bigchaindb/bigchaindb/pull/802), +[#803](https://github.com/bigchaindb/bigchaindb/pull/803), +[#819](https://github.com/bigchaindb/bigchaindb/pull/819), +[#827](https://github.com/bigchaindb/bigchaindb/pull/827), +[#859](https://github.com/bigchaindb/bigchaindb/pull/859), +[#872](https://github.com/bigchaindb/bigchaindb/pull/872), +[#882](https://github.com/bigchaindb/bigchaindb/pull/882), +[#883](https://github.com/bigchaindb/bigchaindb/pull/883) + + +## [0.7.0] - 2016-10-28 +Tag name: v0.7.0 += commit: 2dd7f1af27478c529e6d2d916f64daa3fbda3885 +committed: Oct 28, 2016, 4:00 PM GMT+2 + +### Added +- Stale transactions in the `backlog` table now get reassigned to another node (for inclusion in a new block): [Pull Request #359](https://github.com/bigchaindb/bigchaindb/pull/359) +- Many improvements to make the database connection more robust: [Pull Request #623](https://github.com/bigchaindb/bigchaindb/pull/623) +- The new `--dev-allow-temp-keypair` option on `bigchaindb start` will generate a temporary keypair if no keypair is found. The `Dockerfile` was updated to use this. [Pull Request #635](https://github.com/bigchaindb/bigchaindb/pull/635) +- The AWS deployment scripts now allow you to: + - specify the AWS security group as a configuration parameter: [Pull Request #620](https://github.com/bigchaindb/bigchaindb/pull/620) + - tell RethinkDB to bind HTTP to localhost (a more secure setup; now the default in the example config file): [Pull Request #666](https://github.com/bigchaindb/bigchaindb/pull/666) + +### Changed +- Integrated the new `Transaction` model. This was a **big** change; 49 files changed. [Pull Request #641](https://github.com/bigchaindb/bigchaindb/pull/641) +- Merged "common" code (used by Planetmint Server and the Python driver), which used to be in its own repository (`bigchaindb/bigchaindb-common`), into the main `bigchaindb/bigchaindb` repository (this one): [Pull Request #742](https://github.com/bigchaindb/bigchaindb/pull/742) +- Integrated the new digital asset model. This changed the data structure of a transaction and will make it easier to support divisible assets in the future. [Pull Request #680](https://github.com/bigchaindb/bigchaindb/pull/680) +- Transactions are now deleted from the `backlog` table _after_ a block containing them is written to the `bigchain` table: [Pull Request #609](https://github.com/bigchaindb/bigchaindb/pull/609) +- Changed the example AWS deployment configuration file: [Pull Request #665](https://github.com/bigchaindb/bigchaindb/pull/665) +- Support for version 0.5.0 of the `cryptoconditions` Python package. Note that this means you must now install `ffi.h` (e.g. `sudo apt-get install libffi-dev` on Ubuntu). See Pull Requests [#685](https://github.com/bigchaindb/bigchaindb/pull/685) and [#698](https://github.com/bigchaindb/bigchaindb/pull/698) +- Updated some database access code: Pull Requests [#676](https://github.com/bigchaindb/bigchaindb/pull/676) and [#701](https://github.com/bigchaindb/bigchaindb/pull/701) + +### Fixed +- Internally, when a transaction is in the `backlog` table, it carries some extra book-keeping fields: + 1. an `assignment_timestamp` (i.e. the time when it was assigned to a node), which is used to determine if it has gone stale. + 2. an `assignee`: the public key of the node it was assigned to. +- The `assignment_timestamp` wasn't removed before writing the transaction to a block. That was fixed in [Pull Request #627](https://github.com/bigchaindb/bigchaindb/pull/627) +- The `assignment_timestamp` and `assignee` weren't removed in the response to an HTTP API request sent to the `/api/v1/transactions/` endpoint. That was fixed in [Pull Request #646](https://github.com/bigchaindb/bigchaindb/pull/646) +- When validating a TRANSFER transaction, if any fulfillment refers to a transaction that's _not_ in a valid block, then the transaction isn't valid. This wasn't checked before but it is now. [Pull Request #629](https://github.com/bigchaindb/bigchaindb/pull/629) + +### External Contributors +- @MinchinWeb - [Pull Request #696](https://github.com/bigchaindb/bigchaindb/pull/696) + +### Notes +- We made a small change to how we do version labeling. Going forward, we will have the version label set to 0.X.Y.dev in the master branch as we work on what will eventually be released as version 0.X.Y. The version number will only be changed to 0.X.Y just before the release. This version labeling scheme began with [Pull Request #752](https://github.com/bigchaindb/bigchaindb/pull/752) +- Several additions and changes to the documentation, e.g. Pull Requests +[#618](https://github.com/bigchaindb/bigchaindb/pull/618), +[#621](https://github.com/bigchaindb/bigchaindb/pull/621), +[#625](https://github.com/bigchaindb/bigchaindb/pull/625), +[#645](https://github.com/bigchaindb/bigchaindb/pull/645), +[#647](https://github.com/bigchaindb/bigchaindb/pull/647), +[#648](https://github.com/bigchaindb/bigchaindb/pull/648), +[#650](https://github.com/bigchaindb/bigchaindb/pull/650), +[#651](https://github.com/bigchaindb/bigchaindb/pull/651), +[#653](https://github.com/bigchaindb/bigchaindb/pull/653), +[#655](https://github.com/bigchaindb/bigchaindb/pull/655), +[#656](https://github.com/bigchaindb/bigchaindb/pull/656), +[#657](https://github.com/bigchaindb/bigchaindb/pull/657), +[#667](https://github.com/bigchaindb/bigchaindb/pull/667), +[#668](https://github.com/bigchaindb/bigchaindb/pull/668), +[#669](https://github.com/bigchaindb/bigchaindb/pull/669), +[#673](https://github.com/bigchaindb/bigchaindb/pull/673), +[#678](https://github.com/bigchaindb/bigchaindb/pull/678), +[#684](https://github.com/bigchaindb/bigchaindb/pull/684), +[#688](https://github.com/bigchaindb/bigchaindb/pull/688), +[#699](https://github.com/bigchaindb/bigchaindb/pull/699), +[#705](https://github.com/bigchaindb/bigchaindb/pull/705), +[#737](https://github.com/bigchaindb/bigchaindb/pull/737), +[#748](https://github.com/bigchaindb/bigchaindb/pull/748), +[#753](https://github.com/bigchaindb/bigchaindb/pull/753), +[#757](https://github.com/bigchaindb/bigchaindb/pull/757), +[#759](https://github.com/bigchaindb/bigchaindb/pull/759), and more + + +## [0.6.0] - 2016-09-01 +Tag name: v0.6.0 += commit: bfc86e0295c7d1ef0acd3c275c125798bd5b0dfd +committed: Sep 1, 2016, 2:15 PM GMT+2 + +### Added +- Support for multiple operations in the ChangeFeed class: [Pull Request #569](https://github.com/bigchaindb/bigchaindb/pull/569) +- Instructions, templates and code for deploying a starter node on AWS using Terraform and Ansible: Pull Requests +[#572](https://github.com/bigchaindb/bigchaindb/pull/572), +[#589](https://github.com/bigchaindb/bigchaindb/pull/589), +[#600](https://github.com/bigchaindb/bigchaindb/pull/600), +[#605](https://github.com/bigchaindb/bigchaindb/pull/605), +[#610](https://github.com/bigchaindb/bigchaindb/pull/610) +- Check that the majority of votes on a block agree on the previous block. If they don't, consider the block invalid. [Pull Request #565](https://github.com/bigchaindb/bigchaindb/pull/565) + +### Changed +- Set RethinkDB `read-mode='majority'` everywhere: [Pull Request #497](https://github.com/bigchaindb/bigchaindb/pull/497) +- Ported election logic and voting logic to the new pipeline architecture: Pull Requests [#510](https://github.com/bigchaindb/bigchaindb/pull/510) and [#515](https://github.com/bigchaindb/bigchaindb/pull/515) +- Moved the transaction (model) `version` inside the `transaction` (in the transaction data model): [Pull Request #518](https://github.com/bigchaindb/bigchaindb/pull/518) +- Changed how the Planetmint config file (JSON) gets written so it's easier for humans to read: [Pull Request #522](https://github.com/bigchaindb/bigchaindb/pull/522) +- Improved and expanded the GET/POST endpoints for transactions (in the HTTP API): [Pull Request #563](https://github.com/bigchaindb/bigchaindb/pull/563) +- Changed the AWS cluster deployment scripts so that the deployer now generates their keypair locally, rather than having Amazon do it: [Pull Request #567](https://github.com/bigchaindb/bigchaindb/pull/567) +- When a transaction is retrieved by `get_transaction`, a `validity` field is added with a value one of `valid`, `undecided`, or `backlog`: [Pull Request #574](https://github.com/bigchaindb/bigchaindb/pull/574) +- Renamed `current_owners` and `new_owners` (in the data models) to `owners_before` and `owners_after`, respectively (i.e. before/after *the transaction*): [Pull Request #578](https://github.com/bigchaindb/bigchaindb/pull/578) +- Use `flask_restful` and class-based views for realizing the HTTP API: [Pull Request #588](https://github.com/bigchaindb/bigchaindb/pull/588) + +### Fixed +- Fixed the confusing error message when there was a syntax error in the Planetmint config file: [Pull Request #531](https://github.com/bigchaindb/bigchaindb/pull/531) +- Fixed `write_transaction` so it no longer has the side effect of adding `assignee` to a transaction that is being processed: [Pull Request #606](https://github.com/bigchaindb/bigchaindb/pull/606) + +### External Contributors +- @eladve - [Pull Request #518](https://github.com/bigchaindb/bigchaindb/pull/518) +- @d01phin - Pull Requests [#522](https://github.com/bigchaindb/bigchaindb/pull/522) and [#531](https://github.com/bigchaindb/bigchaindb/pull/531) +- @Kentoseth - [Pull Request #537](https://github.com/bigchaindb/bigchaindb/pull/537) + +### Notes +- Several additions and changes to the documentation, e.g. Pull Requests +[#523](https://github.com/bigchaindb/bigchaindb/pull/523), +[#532](https://github.com/bigchaindb/bigchaindb/pull/532), +[#537](https://github.com/bigchaindb/bigchaindb/pull/537), +[#539](https://github.com/bigchaindb/bigchaindb/pull/539), +[#610](https://github.com/bigchaindb/bigchaindb/pull/610), and more + + +## [0.5.1] - 2016-07-29 +Tag name: v0.5.1 += commit: ff042b5954abe48c7264d43128d52584eab2a806 +committed: Jul 29, 2016, 2:38 PM GMT+2 + +### Added +- New third table, the 'votes' table: [Pull Request #379](https://github.com/bigchaindb/bigchaindb/pull/379) +- Added `get_tx_by_payload_uuid()` including index: [Pull Request #411](https://github.com/bigchaindb/bigchaindb/pull/411) +- Ability to deploy a test cluster on AWS using Amazon Elastic Block Store (EBS) for storage: [Pull Request #469](https://github.com/bigchaindb/bigchaindb/pull/469) +- Ability to add different-size payloads to transactions when filling the backlog for benchmarking: [Pull Request #273](https://github.com/bigchaindb/bigchaindb/pull/273) + +### Changed +- Votes are no longer appended to the blocks inside the 'bigchain' table. They're now written to their own table, the 'votes' table: [Pull Request #379](https://github.com/bigchaindb/bigchaindb/pull/379) +- Refactored how blocks get constructed using the new approach to doing multiprocessing, using the `multipipes` package: [Pull Request #484](https://github.com/bigchaindb/bigchaindb/pull/484) +- Changed all queries to use `read_mode='majority'`: [Pull Request #497](https://github.com/bigchaindb/bigchaindb/pull/497) +- Revised how base software gets deployed on AWS instances: [Pull Request #478](https://github.com/bigchaindb/bigchaindb/pull/478) +- Refactored `db.utils.init()`: [Pull Request #471](https://github.com/bigchaindb/bigchaindb/pull/471) + +### External Contributors +- @shauns - [Pull Request #411](https://github.com/bigchaindb/bigchaindb/pull/411) +- @lonelypeanut - [Pull Request #479](https://github.com/bigchaindb/bigchaindb/pull/479) +- @lluminita - Pull Requests [#435](https://github.com/bigchaindb/bigchaindb/pull/435) & [#471](https://github.com/bigchaindb/bigchaindb/pull/471) + +### Notes +- Several additions and changes to the documentation: Pull Requests +[#416](https://github.com/bigchaindb/bigchaindb/pull/416), +[#417](https://github.com/bigchaindb/bigchaindb/pull/417), +[#418](https://github.com/bigchaindb/bigchaindb/pull/418), +[#420](https://github.com/bigchaindb/bigchaindb/pull/420), +[#421](https://github.com/bigchaindb/bigchaindb/pull/421), +[#422](https://github.com/bigchaindb/bigchaindb/pull/422), +[#423](https://github.com/bigchaindb/bigchaindb/pull/423), +[#425](https://github.com/bigchaindb/bigchaindb/pull/425), +[#428](https://github.com/bigchaindb/bigchaindb/pull/428), +[#430](https://github.com/bigchaindb/bigchaindb/pull/430), +[#431](https://github.com/bigchaindb/bigchaindb/pull/431), +[#435](https://github.com/bigchaindb/bigchaindb/pull/435), +[#442](https://github.com/bigchaindb/bigchaindb/pull/442), +[#472](https://github.com/bigchaindb/bigchaindb/pull/472), +[#481](https://github.com/bigchaindb/bigchaindb/pull/481) + + +## [0.5.0] - 2016-07-04 +Tag name: v0.5.0 += commit: 38329531304952128b48f2e5603db5fa08069c26 +committed: July 4, 2016, 1:07 PM GMT+2 + +### Added +- New `bigchaindb set-replicas` subcommand: [Pull Request #392](https://github.com/bigchaindb/bigchaindb/pull/392) +- Informative JSON message when one makes a request to the root endpoint of the HTTP client-server API: [Pull Request #367](https://github.com/bigchaindb/bigchaindb/pull/367) +- Return HTTP response code 404 when a transaction is not found: [Pull Request #369](https://github.com/bigchaindb/bigchaindb/pull/369) + +### Changed +- Changed the order in which configuration settings get their values. If a setting is set by an environment variable, then that value will be _the_ value, regardless of whether another value is set in a local config file. Also added a method to programattically update the config settings. [Pull Request #395](https://github.com/bigchaindb/bigchaindb/pull/395) +- Changed the definition of `util.sign_tx()`. It now has a third, optional argument: a Bigchain instance. [Pull Request #410](https://github.com/bigchaindb/bigchaindb/pull/410) + +### Notes +- Several additions and changes to the documentation: Pull Requests +[#388](https://github.com/bigchaindb/bigchaindb/pull/388), +[#393](https://github.com/bigchaindb/bigchaindb/pull/393), +[#397](https://github.com/bigchaindb/bigchaindb/pull/397), +[#402](https://github.com/bigchaindb/bigchaindb/pull/402), +[#403](https://github.com/bigchaindb/bigchaindb/pull/403), +[#406](https://github.com/bigchaindb/bigchaindb/pull/406), +[#408](https://github.com/bigchaindb/bigchaindb/pull/408) + + +## [0.4.2] - 2016-06-15 +Tag name: v0.4.2 += commit: 7ce6c3980cf70437d7ce716a67f069afa8ecb79e +committed: June 15, 2016, 1:42 PM GMT+2 + +### Added +- Report the Planetmint version number when starting Planetmint: [Pull Request #385](https://github.com/bigchaindb/bigchaindb/pull/385) + +### Changed +- Round timestamps to a precision of one second, and replace payload hash with payload UUID in transactions: [Pull Request #384](https://github.com/bigchaindb/bigchaindb/pull/384) +- Updated cryptoconditions API usage: [Pull Request #373](https://github.com/bigchaindb/bigchaindb/pull/373) + + +## [0.4.1] - 2016-06-13 +Tag name: v0.4.1 += commit: 9c4aa987bcbc294b6a5c3069e6c45a7ed77a4068 +committed: June 13, 2016, 9:52 AM GMT+2 + +### Added +- Revert `bigchain` deletes: [Pull Request #330](https://github.com/bigchaindb/bigchaindb/pull/330) + +### Changed +- Use inverted threshold condition instead of negative weights for escrow: [Pull Request #355](https://github.com/bigchaindb/bigchaindb/pull/355) + +### Fixed +- Removed duplicate `pytest` in `setup.py`: [Pull Request #365](https://github.com/bigchaindb/bigchaindb/pull/365) + +### Notes +- There were several additions and changes to the documentation: Pull Requests +[#343](https://github.com/bigchaindb/bigchaindb/pull/343), +[#363](https://github.com/bigchaindb/bigchaindb/pull/363), +[#364](https://github.com/bigchaindb/bigchaindb/pull/364), +[#366](https://github.com/bigchaindb/bigchaindb/pull/366), +[#370](https://github.com/bigchaindb/bigchaindb/pull/370), +[#372](https://github.com/bigchaindb/bigchaindb/pull/372) + + +## [0.4.0] - 2016-05-27 +Tag name: v0.4.0 += commit: a89399c4f9fcdf82df73e0d8191af9e539d8d081 +committed: May 27, 2016, 1:42 PM GMT+2 + +### Added +- Support for escrow (possible because of two other new things: cryptoconditions with inverters, and a timeout condition): [Pull Request #329](https://github.com/bigchaindb/bigchaindb/pull/329) +- Caching of calls to `load_consensus_plugin()`, using [`@lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache). This speeds up the instantiation of `Bigchain` objects and greatly improves overall performance. [Pull Request #271](https://github.com/bigchaindb/bigchaindb/pull/271) +- New `Dockerfile-dev` Docker file to make it easier for developers to _develop_ Planetmint with Docker. One can run all unit tests with Docker again. [Pull Request #313](https://github.com/bigchaindb/bigchaindb/pull/313) +- Transactions in invalid blocks are copied to the backlog: [Pull Request #221](https://github.com/bigchaindb/bigchaindb/pull/221). +- Queries to the bigchain table now ignore invalid blocks: [Pull Request #324](https://github.com/bigchaindb/bigchaindb/issues/324) +- Use secondary index on get_transaction: [Pull Request #324](https://github.com/bigchaindb/bigchaindb/issues/324) +- New `bigchaindb` command to set the number of RethinkDB shards (in both tables): [Pull Request #258](https://github.com/bigchaindb/bigchaindb/pull/258) +- Better handling of an outdated `setuptools`: [Pull Request #279](https://github.com/bigchaindb/bigchaindb/pull/279) + +### Changed +- The block processes now use GroupProcess: [Pull Request #267](https://github.com/bigchaindb/bigchaindb/pull/267) +- Replaced the `json` Python package with `rapidjson` (a Python wrapper for a fast JSON parser/generator written in C++), to speed up JSON serialization and deserialization: [Pull Request #318](https://github.com/bigchaindb/bigchaindb/pull/318) +- Overhauled `ROADMAP.md` and moved it to [the bigchaindb/org repository](https://github.com/bigchaindb/org): Pull Requests +[#282](https://github.com/bigchaindb/bigchaindb/pull/282), +[#306](https://github.com/bigchaindb/bigchaindb/pull/306), +[#308](https://github.com/bigchaindb/bigchaindb/pull/308), +[#325](https://github.com/bigchaindb/bigchaindb/pull/325) +- AWS deployment has better support for [New Relic Server Monitoring](https://newrelic.com/server-monitoring): [Pull Request #316](https://github.com/bigchaindb/bigchaindb/pull/316) +- AWS deployment script now reads from a configuration file: [Pull Request #291](https://github.com/bigchaindb/bigchaindb/pull/291) +- AWS deployment script doesn't auto-start the Planetmint servers anymore: [Pull Request #257](https://github.com/bigchaindb/bigchaindb/pull/257) + +### Fixed +- Bug related to transaction malleability: [Pull Request #281](https://github.com/bigchaindb/bigchaindb/pull/281) + +### Notes +You can now see a big-picture view of all Planetmint repositories on [a waffle.io board](https://waffle.io/bigchaindb/org). + + +## [0.3.0] - 2016-05-03 +Tag name: v0.3.0 += commit: a97c54e82be954a1411e5bfe0f09a9c631309f1e +committed: May 3, 2016, 11:52 AM GMT+2 + +### Added +- Crypto-conditions specs according to the Interledger protocol: [Pull Request #174](https://github.com/bigchaindb/bigchaindb/pull/174) +- Added support for anonymous hashlocked conditions and fulfillments: [Pull Request #211](https://github.com/bigchaindb/bigchaindb/pull/211) + +### Changed +- Several improvements to the AWS deployment scripts: [Pull Request #227](https://github.com/bigchaindb/bigchaindb/pull/227) + +### Fixed +- Bug related to block validation: [Pull Request #233](https://github.com/bigchaindb/bigchaindb/pull/233) + +### Notes +This release completely refactored the structure of the transactions and broke compatibility with older versions +of Planetmint. The refactor of the transactions was made in order to add support for multiple inputs/outputs and +the crypto-conditions specs from the Interledger protocol. + +We also updated the RethinkDB Python drivers so you need to upgrade to RethinkDB v2.3+ + + +## [0.2.0] - 2016-04-26 +Tag name: v0.2.0 += commit: 0c4a2b380aabdcf50fa2d7fb351c290aaedc3db7 +committed: April 26, 2016, 11:09 AM GMT+2 + +### Added +- Ability to use environment variables to set (or partially set) configuration settings: [Pull Request #153](https://github.com/bigchaindb/bigchaindb/pull/153) +- `bigchaindb --export-my-pubkey`: [Pull Request #186](https://github.com/bigchaindb/bigchaindb/pull/186) +- `bigchaindb --version`, and one central source for the current version (`version.py`): [Pull Request #208](https://github.com/bigchaindb/bigchaindb/pull/208) +- AWS deployment scripts: Pull Requests +[#160](https://github.com/bigchaindb/bigchaindb/pull/160), +[#166](https://github.com/bigchaindb/bigchaindb/pull/166), +[#172](https://github.com/bigchaindb/bigchaindb/pull/172), +[#203](https://github.com/bigchaindb/bigchaindb/pull/203) +- `codecov.yml`: [Pull Request #161](https://github.com/bigchaindb/bigchaindb/pull/161) +- `CHANGELOG.md` (this file): [Pull Request #117](https://github.com/bigchaindb/bigchaindb/pull/117) +- Signatures using Ed25519: Pull Requests +[#138](https://github.com/bigchaindb/bigchaindb/pull/138), +[#152](https://github.com/bigchaindb/bigchaindb/pull/152) +- Multisig support: [Pull Request #107](https://github.com/bigchaindb/bigchaindb/pull/107) +- HTTP Server & Web API: Pull Requests +[#102](https://github.com/bigchaindb/bigchaindb/pull/102), +[#150](https://github.com/bigchaindb/bigchaindb/pull/150), +[#155](https://github.com/bigchaindb/bigchaindb/pull/155), +[#183](https://github.com/bigchaindb/bigchaindb/pull/183) +- Python driver/SDK/API: [Pull Request #102](https://github.com/bigchaindb/bigchaindb/pull/102) +- Python Style Guide: [Pull Request #89](https://github.com/bigchaindb/bigchaindb/pull/89) +- Monitoring & dashboard tools: Pull Requests +[#72](https://github.com/bigchaindb/bigchaindb/pull/72), +[#181](https://github.com/bigchaindb/bigchaindb/pull/181) + +### Changed +- Rewrote [`README.md`](README.md) into four sets of links: Pull Requests [#80](https://github.com/bigchaindb/bigchaindb/pull/80) and [#115](https://github.com/bigchaindb/bigchaindb/pull/115) + +### Fixed +- Bug related to config overwrite: [Pull Request #97](https://github.com/bigchaindb/bigchaindb/pull/97) +- Bug related to running the `bigchaindb-benchmark load` on docker [Pull Request #225](https://github.com/bigchaindb/bigchaindb/pull/225) + +## External Contributors +- [@thedoctor](https://github.com/thedoctor): Pull Requests +[#99](https://github.com/bigchaindb/bigchaindb/pull/99), +[#136](https://github.com/bigchaindb/bigchaindb/pull/136) +- [@roderik](https://github.com/roderik): [Pull Request #162](https://github.com/bigchaindb/bigchaindb/pull/162) + + +## [0.1.5] - 2016-04-20 +Tag name: v0.1.5 += commit: 9f62cddbaf44167692cfee71db707bce93e3395f +committed: April 20, 2016, 3:31 PM GMT+2 + +### Fixed +- [Issue #71](https://github.com/bigchaindb/bigchaindb/issues/71) (Voter is not validating blocks correctly when checking for double spends) in [Pull Request #76](https://github.com/bigchaindb/bigchaindb/pull/76) + + +## [0.1.4] - 2016-02-22 +Tag name: v0.1.4 += commit: c4c850f480bc9ae72df2a54f81c0825b6fb4ed62 +committed: Feb 22, 2016, 11:51 AM GMT+1 + +### Added +- Added to `classifiers` to setup.py + +### Changed +- Allow running pytest tests in parallel (using [xdist](http://pytest.org/latest/xdist.html)): [Pull Request #65](https://github.com/bigchaindb/bigchaindb/pull/65) +- Allow non-interactive first start: [Pull Request #64](https://github.com/bigchaindb/bigchaindb/pull/64) to resolve [Issue #58](https://github.com/bigchaindb/bigchaindb/issues/58) + + +## [0.1.3] - 2016-02-16 +Tag name: v0.1.3 += commit 8926e3216c1ee39b9bc332e5ef1df2a8901262dd +committed Feb 16, 2016, 11:37 AM GMT+1 + +### Changed +- Changed from using Git Flow to GitHub flow (but with `develop` as the default branch). + + +## [0.1.2] - 2016-02-15 +Tag name: v0.1.2 += commit d2ff24166d69dda68dd7b4a24a88279b1d37e222 +committed Feb 15, 2016, 2:23 PM GMT+1 + +### Added +- Various tests + +### Fixed +- Fix exception when running `start`: [Pull Request #32](https://github.com/bigchaindb/bigchaindb/pull/32) resolved [Issue #35] + +## [0.1.1] - 2016-02-15 +Tag name: v0.1.1 += commit 2a025448b29fe7056760de1039c73bbcfe992461 +committed Feb 15, 2016, 10:48 AM GMT+1 + +### Added +- "release 0.1.1": [Pull Request #37](https://github.com/bigchaindb/bigchaindb/pull/37) + +### Removed +- `tox.ini` [Pull Request #18](https://github.com/bigchaindb/bigchaindb/pull/18) +- `requirements.txt` in the root directory, and the entire `requirements/` directory: [Pull Request #14](https://github.com/bigchaindb/bigchaindb/pull/14) + +### Fixed +- Hotfix for AttributeError, fixed [Issue #27](https://github.com/bigchaindb/bigchaindb/issues/27) + + +## [0.1.0] - 2016-02-10 +Tag name: v0.1.0 += commit 8539e8dc2d036a4e0a866a3fb9e55889503254d5 +committed Feb 10, 2016, 10:04 PM GMT+1 + +The first public release of Planetmint, including: + +- Initial Planetmint Server code, including many tests and some code for benchmarking. +- Initial documentation (in `bigchaindb/docs`). +- Initial `README.md`, `ROADMAP.md`, `CODE_OF_CONDUCT.md`, and `CONTRIBUTING.md`. +- Packaging for PyPI, including `setup.py` and `setup.cfg`. +- Initial `Dockerfile` and `docker-compose.yml` (for deployment using Docker and Docker Compose). +- Initial `.gitignore` (list of things for git to ignore). +- Initial `.travis.yml` (used by Travis CI). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bca9eab --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,57 @@ + + +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute to the project. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, nationality, or species--no picking on Wrigley for being a buffalo! + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Deliberate intimidation +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior directed at yourself or another community member may be +reported by contacting a project maintainer at [contact@bigchaindb.com](mailto:contact@bigchaindb.com). All +complaints will be reviewed and investigated and will result in a response that +is appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..677aa1c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.6 +LABEL maintainer "contact@ipdb.global" +RUN mkdir -p /usr/src/app +COPY . /usr/src/app/ +WORKDIR /usr/src/app +RUN apt-get -qq update \ + && apt-get -y upgrade \ + && apt-get install -y jq \ + && pip install . \ + && apt-get autoremove \ + && apt-get clean + +VOLUME ["/data", "/certs"] + +ENV PYTHONUNBUFFERED 0 +ENV PLANETMINT_CONFIG_PATH /data/.bigchaindb +ENV PLANETMINT_SERVER_BIND 0.0.0.0:9984 +ENV PLANETMINT_WSSERVER_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_SCHEME ws +ENV PLANETMINT_WSSERVER_ADVERTISED_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_ADVERTISED_SCHEME ws +ENV PLANETMINT_WSSERVER_ADVERTISED_PORT 9985 +ENTRYPOINT ["bigchaindb"] +CMD ["start"] diff --git a/Dockerfile-all-in-one b/Dockerfile-all-in-one new file mode 100644 index 0000000..11d8d20 --- /dev/null +++ b/Dockerfile-all-in-one @@ -0,0 +1,51 @@ +FROM alpine:3.9 +LABEL maintainer "contact@ipdb.global" + +ARG TM_VERSION=v0.31.5 +RUN mkdir -p /usr/src/app +ENV HOME /root +COPY . /usr/src/app/ +WORKDIR /usr/src/app + +RUN apk --update add sudo bash \ + && apk --update add python3 openssl ca-certificates git \ + && apk --update add --virtual build-dependencies python3-dev \ + libffi-dev openssl-dev build-base jq \ + && apk add --no-cache libstdc++ dpkg gnupg \ + && pip3 install --upgrade pip cffi \ + && pip install -e . \ + && apk del build-dependencies \ + && rm -f /var/cache/apk/* + +# Install mongodb and monit +RUN apk --update add mongodb monit + +# Install Tendermint +RUN wget https://github.com/tendermint/tendermint/releases/download/${TM_VERSION}/tendermint_${TM_VERSION}_linux_amd64.zip \ + && unzip tendermint_${TM_VERSION}_linux_amd64.zip \ + && mv tendermint /usr/local/bin/ \ + && rm tendermint_${TM_VERSION}_linux_amd64.zip + +ENV TMHOME=/tendermint + +# Set permissions required for mongodb +RUN mkdir -p /data/db /data/configdb \ + && chown -R mongodb:mongodb /data/db /data/configdb + +# Planetmint enviroment variables +ENV PLANETMINT_DATABASE_PORT 27017 +ENV PLANETMINT_DATABASE_BACKEND localmongodb +ENV PLANETMINT_SERVER_BIND 0.0.0.0:9984 +ENV PLANETMINT_WSSERVER_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_SCHEME ws + +ENV PLANETMINT_WSSERVER_ADVERTISED_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_ADVERTISED_SCHEME ws +ENV PLANETMINT_TENDERMINT_PORT 26657 + +VOLUME /data/db /data/configdb /tendermint + +EXPOSE 27017 28017 9984 9985 26656 26657 26658 + +WORKDIR $HOME +ENTRYPOINT ["/usr/src/app/pkg/scripts/all-in-one.bash"] diff --git a/Dockerfile-alpine b/Dockerfile-alpine new file mode 100644 index 0000000..6663deb --- /dev/null +++ b/Dockerfile-alpine @@ -0,0 +1,30 @@ +FROM alpine:latest +LABEL maintainer "contact@ipdb.global" +RUN mkdir -p /usr/src/app +COPY . /usr/src/app/ +WORKDIR /usr/src/app +RUN apk --update add sudo \ + && apk --update add python3 py-pip openssl ca-certificates git\ + && apk --update add --virtual build-dependencies python3-dev \ + libffi-dev openssl-dev build-base \ + && apk add --no-cache libstdc++ \ + && pip3 install --upgrade pip cffi \ + && pip install -e . \ + && apk del build-dependencies \ + && rm -f /var/cache/apk/* +# When developing with Python in a docker container, we are using PYTHONBUFFERED +# to force stdin, stdout and stderr to be totally unbuffered and to capture logs/outputs +ENV PYTHONUNBUFFERED 0 + +ENV PLANETMINT_DATABASE_PORT 27017 +ENV PLANETMINT_DATABASE_BACKEND $backend +ENV PLANETMINT_SERVER_BIND 0.0.0.0:9984 +ENV PLANETMINT_WSSERVER_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_SCHEME ws + +ENV PLANETMINT_WSSERVER_ADVERTISED_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_ADVERTISED_SCHEME ws + +ENV PLANETMINT_TENDERMINT_PORT 26657 +ARG backend +RUN bigchaindb -y configure "$backend" \ No newline at end of file diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 0000000..fbe11e6 --- /dev/null +++ b/Dockerfile-dev @@ -0,0 +1,35 @@ +ARG python_version=3.6 +FROM python:${python_version} +LABEL maintainer "contact@ipdb.global" + +RUN apt-get update \ + && apt-get install -y git \ + && pip install -U pip \ + && apt-get autoremove \ + && apt-get clean + +ARG backend +ARG abci_status + +# When developing with Python in a docker container, we are using PYTHONBUFFERED +# to force stdin, stdout and stderr to be totally unbuffered and to capture logs/outputs +ENV PYTHONUNBUFFERED 0 + +ENV PLANETMINT_DATABASE_PORT 27017 +ENV PLANETMINT_DATABASE_BACKEND $backend +ENV PLANETMINT_SERVER_BIND 0.0.0.0:9984 +ENV PLANETMINT_WSSERVER_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_SCHEME ws + +ENV PLANETMINT_WSSERVER_ADVERTISED_HOST 0.0.0.0 +ENV PLANETMINT_WSSERVER_ADVERTISED_SCHEME ws + +ENV PLANETMINT_TENDERMINT_PORT 26657 + +ENV PLANETMINT_CI_ABCI ${abci_status} + +RUN mkdir -p /usr/src/app +COPY . /usr/src/app/ +WORKDIR /usr/src/app +RUN pip install -e .[dev] +RUN bigchaindb -y configure diff --git a/LICENSE b/LICENSE index 0ad25db..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,201 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES.md b/LICENSES.md new file mode 100644 index 0000000..5dc2736 --- /dev/null +++ b/LICENSES.md @@ -0,0 +1,24 @@ +# Copyrights and Licenses + +## Copyrights + +For all the code and documentation in this repository, the copyright is owned by one or more of the following: + +- Planetmint GmbH +- A Planetmint contributor who agreed to a Planetmint Contributor License Agreement (CLA) with Planetmint GmbH. (See [BEP-16](https://github.com/bigchaindb/BEPs/tree/master/16).) +- A Planetmint contributor who signed off on the Developer Certificate of Origin (DCO) for all their contributions. (See [BEP-24](https://github.com/bigchaindb/BEPs/tree/master/24).) +- (Rarely, see the **Exceptions Section** below) A third pary who licensed the code in question under an open source license. + +## Code Licenses + +All code in this repository, including short code snippets in the documentation, but not including the **Exceptions** noted below, is licensed under the Apache License, Version 2.0, the full text of which can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). + +For the licenses on all other Planetmint-related code (i.e. in other repositories), see the LICENSE file in the associated repository. + +## Documentation Licenses + +The official Planetmint documentation, _except for the short code snippets embedded within it_, is licensed under a Creative Commons Attribution 4.0 International license, the full text of which can be found at [http://creativecommons.org/licenses/by/4.0/legalcode](http://creativecommons.org/licenses/by/4.0/legalcode). + +## Exceptions + +The contents of the `k8s/nginx-openresty/` directory are licensed as described in the `LICENSE.md` file in that directory. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0195f87 --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +.PHONY: help run start stop logs test test-unit test-unit-watch test-acceptance cov doc doc-acceptance clean reset release dist check-deps clean-build clean-pyc clean-test + +.DEFAULT_GOAL := help + + +############################# +# Open a URL in the browser # +############################# +define BROWSER_PYSCRIPT +import os, webbrowser, sys +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + + +################################## +# Display help for this makefile # +################################## +define PRINT_HELP_PYSCRIPT +import re, sys + +print("Planetmint 2.0 developer toolbox") +print("--------------------------------") +print("Usage: make COMMAND") +print("") +print("Commands:") +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print(" %-16s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +################## +# Basic commands # +################## +DOCKER := docker +DC := docker-compose +BROWSER := python -c "$$BROWSER_PYSCRIPT" +HELP := python -c "$$PRINT_HELP_PYSCRIPT" +ECHO := /usr/bin/env echo + +IS_DOCKER_COMPOSE_INSTALLED := $(shell command -v docker-compose 2> /dev/null) + +################ +# Main targets # +################ + +help: ## Show this help + @$(HELP) < $(MAKEFILE_LIST) + +run: check-deps ## Run Planetmint from source (stop it with ctrl+c) + # although bigchaindb has tendermint and mongodb in depends_on, + # launch them first otherwise tendermint will get stuck upon sending yet another log + # due to some docker-compose issue; does not happen when containers are run as daemons + @$(DC) up --no-deps mongodb tendermint bigchaindb + +start: check-deps ## Run Planetmint from source and daemonize it (stop with `make stop`) + @$(DC) up -d bigchaindb + +stop: check-deps ## Stop Planetmint + @$(DC) stop + +logs: check-deps ## Attach to the logs + @$(DC) logs -f bigchaindb + +test: check-deps test-unit test-acceptance ## Run unit and acceptance tests + +test-unit: check-deps ## Run all tests once + @$(DC) up -d bdb + @$(DC) exec bigchaindb pytest + +test-unit-watch: check-deps ## Run all tests and wait. Every time you change code, tests will be run again + @$(DC) run --rm --no-deps bigchaindb pytest -f + +test-acceptance: check-deps ## Run all acceptance tests + @./run-acceptance-test.sh + +cov: check-deps ## Check code coverage and open the result in the browser + @$(DC) run --rm bigchaindb pytest -v --cov=bigchaindb --cov-report html + $(BROWSER) htmlcov/index.html + +doc: check-deps ## Generate HTML documentation and open it in the browser + @$(DC) run --rm --no-deps bdocs make -C docs/root html + @$(DC) run --rm --no-deps bdocs make -C docs/server html + @$(DC) run --rm --no-deps bdocs make -C docs/contributing html + $(BROWSER) docs/root/build/html/index.html + +doc-acceptance: check-deps ## Create documentation for acceptance tests + @$(DC) run --rm python-acceptance pycco -i -s /src -d /docs + $(BROWSER) acceptance/python/docs/index.html + +clean: clean-build clean-pyc clean-test ## Remove all build, test, coverage and Python artifacts + @$(ECHO) "Cleaning was successful." + +reset: check-deps ## Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint. + @$(DC) down + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source (and not for now, wheel package) + python setup.py sdist + # python setup.py bdist_wheel + ls -l dist + +############### +# Sub targets # +############### + +check-deps: +ifndef IS_DOCKER_COMPOSE_INSTALLED + @$(ECHO) "Error: docker-compose is not installed" + @$(ECHO) + @$(ECHO) "You need docker-compose to run this command. Check out the official docs on how to install it in your system:" + @$(ECHO) "- https://docs.docker.com/compose/install/" + @$(ECHO) + @$(DC) # docker-compose is not installed, so we call it to generate an error and exit +endif + +clean-build: # Remove build artifacts + @rm -fr build/ + @rm -fr dist/ + @rm -fr .eggs/ + @find . -name '*.egg-info' -exec rm -fr {} + + @find . -name '*.egg' -exec rm -f {} + + +clean-pyc: # Remove Python file artifacts + @find . -name '*.pyc' -exec rm -f {} + + @find . -name '*.pyo' -exec rm -f {} + + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +clean-test: # Remove test and coverage artifacts + @find . -name '.pytest_cache' -exec rm -fr {} + + @rm -fr .tox/ + @rm -f .coverage + @rm -fr htmlcov/ diff --git a/PYTHON_STYLE_GUIDE.md b/PYTHON_STYLE_GUIDE.md new file mode 100644 index 0000000..b84b40b --- /dev/null +++ b/PYTHON_STYLE_GUIDE.md @@ -0,0 +1,97 @@ + + +# Python Style Guide + +This guide starts out with our general Python coding style guidelines and ends with a section on how we write & run (Python) tests. + +## General Python Coding Style Guidelines + +Our starting point is [PEP8](https://www.python.org/dev/peps/pep-0008/), the standard "Style Guide for Python Code." Many Python IDEs will check your code against PEP8. (Note that PEP8 isn't frozen; it actually changes over time, but slowly.) + +Planetmint uses Python 3.5+, so you can ignore all PEP8 guidelines specific to Python 2. + +We use [pre-commit](http://pre-commit.com/) to check some of the rules below before every commit but not everything is realized yet. +The hooks we use can be found in the [.pre-commit-config.yaml](https://github.com/bigchaindb/bigchaindb/blob/master/.pre-commit-config.yaml) file. + +### Python Docstrings + +PEP8 says some things about docstrings, but not what to put in them or how to structure them. [PEP257](https://www.python.org/dev/peps/pep-0257/) was one proposal for docstring conventions, but we prefer [Google-style docstrings](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) instead: they're easier to read and the [napoleon extension](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) for Sphinx lets us turn them into nice-looking documentation. Here are some references on Google-style docstrings: + +* [Google's docs on Google-style docstrings](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) +* [napoleon's docs include an overview of Google-style docstrings](http://sphinxcontrib-napoleon.readthedocs.org/en/latest/index.html) +* [Example Google-style docstrings](http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html) (from napoleon's docs) + +### Maximum Line Length + +PEP8 has some [maximum line length guidelines](https://www.python.org/dev/peps/pep-0008/#id17), starting with "Limit all lines to a maximum of 79 characters" but "for flowing long blocks of text with fewer structural restrictions (docstrings or comments), the line length should be limited to 72 characters." + +We discussed this at length, and it seems that the consensus is: _try_ to keep line lengths less than 79/72 characters, unless you have a special situation where longer lines would improve readability. (The basic reason is that 79/72 works for everyone, and Planetmint is an open source project.) As a hard limit, keep all lines less than 119 characters (which is the width of GitHub code review). + +### Single or Double Quotes? + +Python lets you use single or double quotes. PEP8 says you can use either, as long as you're consistent. We try to stick to using single quotes, except in cases where using double quotes is more readable. For example: +```python +print('This doesn\'t look so nice.') +print("Doesn't this look nicer?") +``` + +### Breaking Strings Across Multiple Lines + +Should we use parentheses or slashes (`\`) to break strings across multiple lines, i.e. +```python +my_string = ('This is a very long string, so long that it will not fit into just one line ' + 'so it must be split across multiple lines.') +# or +my_string = 'This is a very long string, so long that it will not fit into just one line ' \ + 'so it must be split across multiple lines.' +``` + +It seems the preference is for slashes, but using parentheses is okay too. (There are good arguments either way. Arguing about it seems like a waste of time.) + +### How to Format Long import Statements + +If you need to `import` lots of names from a module or package, and they won't all fit in one line (without making the line too long), then use parentheses to spread the names across multiple lines, like so: +```python +from Tkinter import ( + Tk, Frame, Button, Entry, Canvas, Text, + LEFT, DISABLED, NORMAL, RIDGE, END, +) + +# Or + +from Tkinter import (Tk, Frame, Button, Entry, Canvas, Text, + LEFT, DISABLED, NORMAL, RIDGE, END) +``` + +For the rationale, see [PEP 328](https://www.python.org/dev/peps/pep-0328/#rationale-for-parentheses). + +### Using the % operator or `format()` to Format Strings + +Given the choice: +```python +x = 'name: %s; score: %d' % (name, n) +# or +x = 'name: {}; score: {}'.format(name, n) +``` + +we use the `format()` version. The [official Python documentation says](https://docs.python.org/2/library/stdtypes.html#str.format), "This method of string formatting is the new standard in Python 3, and should be preferred to the % formatting described in String Formatting Operations in new code." + + +## Running the Flake8 Style Checker + +We use [Flake8](http://flake8.pycqa.org/en/latest/index.html) to check our Python code style. Once you have it installed, you can run it using: +```text +flake8 --max-line-length 119 bigchaindb/ +``` + + +## Writing and Running (Python) Tests + +The content of this section was moved to [`bigchaindb/tests/README.md`](https://github.com/bigchaindb/bigchaindb/blob/master/tests/README.md). + +Note: We automatically run all tests on all pull requests (using Travis CI), so you should definitely run all tests locally before you submit a pull request. See the above-linked README file for instructions. diff --git a/README.md b/README.md index 6ba72de..ac7b9b5 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# planetmint \ No newline at end of file + + + + +[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) +[![Latest release](https://img.shields.io/github/release/bigchaindb/bigchaindb/all.svg)](https://github.com/bigchaindb/bigchaindb/releases) +[![Status on PyPI](https://img.shields.io/pypi/status/bigchaindb.svg)](https://pypi.org/project/Planetmint/) +[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.com/bigchaindb/bigchaindb) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb-server/badge/?version=latest)](https://docs.bigchaindb.com/projects/server/en/latest/) +[![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +# Planetmint Server + +Planetmint is the blockchain database. This repository is for _BigchainDB Server_. + +## The Basics + +* [Try the Quickstart](https://docs.bigchaindb.com/projects/server/en/latest/quickstart.html) +* [Read the Planetmint 2.0 whitepaper](https://www.bigchaindb.com/whitepaper/) +* [Check out the _Hitchiker's Guide to BigchainDB_](https://www.bigchaindb.com/developers/guide/) + +## Run and Test Planetmint Server from the `master` Branch + +Running and testing the latest version of Planetmint Server is easy. Make sure you have a recent version of [Docker Compose](https://docs.docker.com/compose/install/) installed. When you are ready, fire up a terminal and run: + +```text +git clone https://github.com/bigchaindb/bigchaindb.git +cd bigchaindb +make run +``` + +Planetmint should be reachable now on `http://localhost:9984/`. + +There are also other commands you can execute: + +* `make start`: Run Planetmint from source and daemonize it (stop it with `make stop`). +* `make stop`: Stop Planetmint. +* `make logs`: Attach to the logs. +* `make test`: Run all unit and acceptance tests. +* `make test-unit-watch`: Run all tests and wait. Every time you change code, tests will be run again. +* `make cov`: Check code coverage and open the result in the browser. +* `make doc`: Generate HTML documentation and open it in the browser. +* `make clean`: Remove all build, test, coverage and Python artifacts. +* `make reset`: Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint. + +To view all commands available, run `make`. + +## Links for Everyone + +* [Planetmint.com](https://www.bigchaindb.com/) - the main Planetmint website, including newsletter signup +* [Roadmap](https://github.com/bigchaindb/org/blob/master/ROADMAP.md) +* [Blog](https://medium.com/the-bigchaindb-blog) +* [Twitter](https://twitter.com/Planetmint) + +## Links for Developers + +* [All Planetmint Documentation](https://docs.bigchaindb.com/en/latest/) +* [Planetmint Server Documentation](https://docs.bigchaindb.com/projects/server/en/latest/index.html) +* [CONTRIBUTING.md](.github/CONTRIBUTING.md) - how to contribute +* [Community guidelines](CODE_OF_CONDUCT.md) +* [Open issues](https://github.com/bigchaindb/bigchaindb/issues) +* [Open pull requests](https://github.com/bigchaindb/bigchaindb/pulls) +* [Gitter chatroom](https://gitter.im/bigchaindb/bigchaindb) + +## Legal + +* [Licenses](LICENSES.md) - open source & open content +* [Imprint](https://www.bigchaindb.com/imprint/) +* [Contact Us](https://www.bigchaindb.com/contact/) diff --git a/README_cn.md b/README_cn.md new file mode 100644 index 0000000..8385ace --- /dev/null +++ b/README_cn.md @@ -0,0 +1,77 @@ + + + + +[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) +[![Latest release](https://img.shields.io/github/release/bigchaindb/bigchaindb/all.svg)](https://github.com/bigchaindb/bigchaindb/releases) +[![Status on PyPI](https://img.shields.io/pypi/status/bigchaindb.svg)](https://pypi.org/project/Planetmint/) +[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.com/bigchaindb/bigchaindb) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb-server/badge/?version=latest)](https://docs.bigchaindb.com/projects/server/en/latest/) +[![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +# Planetmint 服务器 + +Planetmint 是区块链数据库. 这是 _BigchainDB 服务器_ 的仓库. + +## 基础知识 + +* [尝试快速开始](https://docs.bigchaindb.com/projects/server/en/latest/quickstart.html) +* [阅读 Planetmint 2.0 白皮书](https://www.bigchaindb.com/whitepaper/) +* [查阅漫游指南](https://www.bigchaindb.com/developers/guide/) + +## 运行和测试 `master` 分支的 Planetmint 服务器 + +运行和测试最新版本的 Planetmint 服务器非常简单. 确认你有安装最新版本的 [Docker Compose](https://docs.docker.com/compose/install/). 当你准备好了, 打开一个终端并运行: + +```text +git clone https://github.com/bigchaindb/bigchaindb.git +cd bigchaindb +make run +``` + +Planetmint 应该可以通过 `http://localhost:9984/` 访问. + +这里也有一些其他的命令你可以运行: + +* `make start`: 通过源码和守护进程的方式运行 Planetmint (通过 `make stop` 停止). +* `make stop`: 停止运行 Planetmint. +* `make logs`: 附在日志上. +* `make test`: 运行所有单元和验收测试. +* `make test-unit-watch`: 运行所有测试并等待. 每次更改代码时都会再次运行测试. +* `make cov`: 检查代码覆盖率并在浏览器中打开结果. +* `make doc`: 生成 HTML 文档并在浏览器中打开它. +* `make clean`: 删除所有构建, 测试, 覆盖和 Python 生成物. +* `make reset`: 停止并移除所有容器. 警告: 您将丢失存储在 Planetmint 中的所有数据. + +查看所有可用命令, 请运行 `make`. + +## 一般人员链接 + +* [Planetmint.com](https://www.bigchaindb.com/) - Planetmint 主网站, 包括新闻订阅 +* [路线图](https://github.com/bigchaindb/org/blob/master/ROADMAP.md) +* [博客](https://medium.com/the-bigchaindb-blog) +* [推特](https://twitter.com/Planetmint) + +## 开发人员链接 + +* [所有的 Planetmint 文档](https://docs.bigchaindb.com/en/latest/) +* [Planetmint 服务器 文档](https://docs.bigchaindb.com/projects/server/en/latest/index.html) +* [CONTRIBUTING.md](.github/CONTRIBUTING.md) - how to contribute +* [社区指南](CODE_OF_CONDUCT.md) +* [公开问题](https://github.com/bigchaindb/bigchaindb/issues) +* [公开的 pull request](https://github.com/bigchaindb/bigchaindb/pulls) +* [Gitter 聊天室](https://gitter.im/bigchaindb/bigchaindb) + +## 法律声明 + +* [许可](LICENSES.md) - 开源代码 & 开源内容 +* [印记](https://www.bigchaindb.com/imprint/) +* [联系我们](https://www.bigchaindb.com/contact/) diff --git a/README_kor.md b/README_kor.md new file mode 100644 index 0000000..c4f440f --- /dev/null +++ b/README_kor.md @@ -0,0 +1,65 @@ +[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) +[![Latest release](https://img.shields.io/github/release/bigchaindb/bigchaindb/all.svg)](https://github.com/bigchaindb/bigchaindb/releases) +[![Status on PyPI](https://img.shields.io/pypi/status/bigchaindb.svg)](https://pypi.org/project/Planetmint/) +[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb-server/badge/?version=latest)](https://docs.bigchaindb.com/projects/server/en/latest/) +[![Join the chat at https://gitter.im/bigchaindb/bigchaindb](https://badges.gitter.im/bigchaindb/bigchaindb.svg)](https://gitter.im/bigchaindb/bigchaindb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +# Planetmint 서버 + +BigchaingDB는 블록체인 데이터베이스입니다. 이 저장소는 _BigchaingDB 서버_를 위한 저장소입니다. + +### 기본 사항 + +* [빠른 시작 사용해보기](https://docs.bigchaindb.com/projects/server/en/latest/quickstart.html) +* [Planetmint 2.0 백서 읽기](https://www.bigchaindb.com/whitepaper/) +* [BigchainDB에 대한 _Hitchiker's Guide_를 확인십시오.](https://www.bigchaindb.com/developers/guide/) + +### `master` Branch에서 Planetmint 서버 실행 및 테스트 + +BigchaingDB 서버의 최신 버전을 실행하고 테스트하는 것은 어렵지 않습니다. [Docker Compose](https://docs.docker.com/compose/install/)의 최신 버전이 설치되어 있는지 확인하십시오. 준비가 되었다면, 터미널에서 다음을 실행하십시오. + +```text +git clone https://github.com/bigchaindb/bigchaindb.git +cd bigchaindb +make run +``` + +이제 BigchainDB는 `http://localhost:9984/`에 연결되어야 합니다. + +또한, 실행시키기 위한 다른 명령어들도 있습니다. + +* `make start` : 소스로부터 BigchainDB를 실행하고 데몬화합니다. \(이는 `make stop` 을 하면 중지합니다.\) +* `make stop` : BigchainDB를 중지합니다. +* `make logs` : 로그에 첨부합니다. +* `make text` : 모든 유닛과 허가 테스트를 실행합니다. +* `make test-unit-watch` : 모든 테스트를 수행하고 기다립니다. 코드를 변경할 때마다 테스트는 다시 실행될 것입니다. +* `make cov` : 코드 커버리지를 확인하고 브라우저에서 결과를 엽니다. +* `make doc` : HTML 문서를 만들고, 브라우저에서 엽니다. +* `make clean` : 모든 빌드와 테스트, 커버리지 및 파이썬 아티팩트를 제거합니다. +* `make reset` : 모든 컨테이너들을 중지하고 제거합니다. 경고 : BigchainDB에 저장된 모든 데이터를 잃을 수 있습니다. + +사용 가능한 모든 명령어를 보기 위해서는 `make` 를 실행하십시오. + +### 모두를 위한 링크들 + +* [Planetmint.com ](https://www.bigchaindb.com/)- 뉴스 레터 가입을 포함하는 Planetmint 주요 웹 사이트 +* [로드맵](https://github.com/bigchaindb/org/blob/master/ROADMAP.md) +* [블로그](https://medium.com/the-bigchaindb-blog) +* [트위터](https://twitter.com/Planetmint) + +### 개발자들을 위한 링크들 + +* [모든 Planetmint 문서](https://docs.bigchaindb.com/en/latest/) +* [Planetmint 서버 문서](https://docs.bigchaindb.com/projects/server/en/latest/index.html) +* [CONTRIBUTING.md](https://github.com/bigchaindb/bigchaindb/blob/master/.github/CONTRIBUTING.md) - 기여를 하는 방법 +* [커뮤니티 가이드라인](https://github.com/bigchaindb/bigchaindb/blob/master/CODE_OF_CONDUCT.md) +* [이슈 작성](https://github.com/bigchaindb/bigchaindb/issues) +* [pull request 하기](https://github.com/bigchaindb/bigchaindb/pulls) +* [Gitter 채팅방](https://gitter.im/bigchaindb/bigchaindb) + +### 합법 + +* [라이선스](https://github.com/bigchaindb/bigchaindb/blob/master/LICENSES.md) - 오픈 소스 & 오픈 콘텐츠 +* [발행](https://www.bigchaindb.com/imprint/) +* [연락처](https://www.bigchaindb.com/contact/) diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md new file mode 100644 index 0000000..117e407 --- /dev/null +++ b/RELEASE_PROCESS.md @@ -0,0 +1,101 @@ + + +# Our Release Process + +## Notes + +Planetmint follows +[the Python form of Semantic Versioning](https://packaging.python.org/tutorials/distributing-packages/#choosing-a-versioning-scheme) +(i.e. MAJOR.MINOR.PATCH), +which is almost identical +to [regular semantic versioning](http://semver.org/), but there's no hyphen, e.g. + +- `0.9.0` for a typical final release +- `4.5.2a1` not `4.5.2-a1` for the first Alpha release +- `3.4.5rc2` not `3.4.5-rc2` for Release Candidate 2 + +**Note 1:** For Git tags (which are used to identify releases on GitHub), we append a `v` in front. For example, the Git tag for version `2.0.0a1` was `v2.0.0a1`. + +**Note 2:** For Docker image tags (e.g. on Docker Hub), we use longer version names for Alpha, Beta and Release Candidate releases. For example, the Docker image tag for version `2.0.0a2` was `2.0.0-alpha2`. + +We use `0.9` and `0.9.0` as example version and short-version values below. You should replace those with the correct values for your new version. + +We follow [BEP-1](https://github.com/bigchaindb/BEPs/tree/master/1), which is our variant of C4, the Collective Code Construction Contract, so a release is just a [tagged commit](https://git-scm.com/book/en/v2/Git-Basics-Tagging) on the `master` branch, i.e. a label for a particular Git commit. + +The following steps are what we do to release a new version of _BigchainDB Server_. The steps to release the Python Driver are similar but not the same. + +## Steps + +1. Create a pull request where you make the following changes: + + - Update `CHANGELOG.md` + - Update all Docker image tags in all Kubernetes YAML files (in the `k8s/` directory). + For example, in the files: + + - `k8s/bigchaindb/bigchaindb-ss.yaml` and + - `k8s/dev-setup/bigchaindb.yaml` + + find the line of the form `image: bigchaindb/bigchaindb:0.8.1` and change the version number to the new version number, e.g. `0.9.0`. (This is the Docker image that Kubernetes should pull from Docker Hub.) + Keep in mind that this is a _Docker image tag_ so our naming convention is + a bit different; see Note 2 in the **Notes** section above. + - In `bigchaindb/version.py`: + - update `__version__` to e.g. `0.9.0` (with no `.dev` on the end) + - update `__short_version__` to e.g. `0.9` (with no `.dev` on the end) + - In the docs about installing Planetmint (and Tendermint), and in the associated scripts, recommend/install a version of Tendermint that _actually works_ with the soon-to-be-released version of Planetmint. You can find all such references by doing a search for the previously-recommended version number, such as `0.31.5`. + - In `setup.py`, _maybe_ update the development status item in the `classifiers` list. For example, one allowed value is `"Development Status :: 5 - Production/Stable"`. The [allowed values are listed at pypi.python.org](https://pypi.python.org/pypi?%3Aaction=list_classifiers). + +2. **Wait for all the tests to pass!** +3. Merge the pull request into the `master` branch. +4. Go to the [bigchaindb/bigchaindb Releases page on GitHub](https://github.com/bigchaindb/bigchaindb/releases) + and click the "Draft a new release" button. +5. Fill in the details: + - **Tag version:** version number preceded by `v`, e.g. `v0.9.1` + - **Target:** the last commit that was just merged. In other words, that commit will get a Git tag with the value given for tag version above. + - **Title:** Same as tag version above, e.g `v0.9.1` + - **Description:** The body of the changelog entry (Added, Changed, etc.) +6. Click "Publish release" to publish the release on GitHub. +7. On your local computer, make sure you're on the `master` branch and that it's up-to-date with the `master` branch in the bigchaindb/bigchaindb repository (e.g. `git pull upstream master`). We're going to use that to push a new `bigchaindb` package to PyPI. +8. Make sure you have a `~/.pypirc` file containing credentials for PyPI. +9. Do `make release` to build and publish the new `bigchaindb` package on PyPI. For this step you need to have `twine` installed. If you get an error like `Makefile:135: recipe for target 'clean-pyc' failed` then try doing + ```text + sudo chown -R $(whoami):$(whoami) . + ``` +10. [Log in to readthedocs.org](https://readthedocs.org/accounts/login/) and go to the **Planetmint Server** project, then: + - Click on "Builds", select "latest" from the drop-down menu, then click the "Build Version:" button. + - Wait for the build of "latest" to finish. This can take a few minutes. + - Go to Admin --> Advanced Settings + and make sure that "Default branch:" (i.e. what "latest" points to) + is set to the new release's tag, e.g. `v0.9.1`. + (It won't be an option if you didn't wait for the build of "latest" to finish.) + Then scroll to the bottom and click "Save". + - Go to Admin --> Versions + and under **Choose Active Versions**, do these things: + 1. Make sure that the new version's tag is "Active" and "Public" + 2. Make sure the **stable** branch is _not_ active. + 3. Scroll to the bottom of the page and click "Save". +11. Go to [Docker Hub](https://hub.docker.com/) and sign in, then: + - Click on "Organizations" + - Click on "bigchaindb" + - Click on "bigchaindb/bigchaindb" + - Click on "Build Settings" + - Find the row where "Docker Tag Name" equals `latest` + and change the value of "Name" to the name (Git tag) + of the new release, e.g. `v0.9.0`. + - If the release is an Alpha, Beta or Release Candidate release, + then a new row must be added. + You can do that by clicking the green "+" (plus) icon. + The contents of the new row should be similar to the existing rows + of previous releases like that. + - Click on "Tags" + - Delete the "latest" tag (so we can rebuild it) + - Click on "Build Settings" again + - Click on the "Trigger" button for the "latest" tag and make sure it worked by clicking on "Tags" again + - If the release is an Alpha, Beta or Release Candidate release, + then click on the "Trigger" button for that tag as well. + +Congratulations, you have released a new version of Planetmint Server! diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ba00fd0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,14 @@ + + +# Planetmint Roadmap + +We moved the Planetmint Roadmap to the bigchaindb/org repository; see: + +[https://github.com/bigchaindb/org/blob/master/ROADMAP.md](https://github.com/bigchaindb/org/blob/master/ROADMAP.md) + +(We kept this file here to avoid breaking some links.) diff --git a/acceptance/README.md b/acceptance/README.md new file mode 100644 index 0000000..56264d3 --- /dev/null +++ b/acceptance/README.md @@ -0,0 +1,27 @@ + + +# Acceptance test suite +This directory contains the acceptance test suite for Planetmint. + +The suite uses Docker Compose to set up a single Planetmint node, run all tests, and finally stop the node. In the future we will add support for a four node network setup. + +## Running the tests +It should be as easy as `make test-acceptance`. + +Note that `make test-acceptance` will take some time to start the node and shutting it down. If you are developing a test, or you wish to run a specific test in the acceptance test suite, first start the node with `make start`. After the node is running, you can run `pytest` inside the `python-acceptance` container with: + +```bash +docker-compose run --rm python-acceptance pytest +``` + +## Writing and documenting the tests +Tests are sometimes difficult to read. For acceptance tests, we try to be really explicit on what the test is doing, so please write code that is *simple* and easy to understand. We decided to use literate-programming documentation. To generate the documentation run: + +```bash +make doc-acceptance +``` diff --git a/acceptance/python/.gitignore b/acceptance/python/.gitignore new file mode 100644 index 0000000..d8f8d46 --- /dev/null +++ b/acceptance/python/.gitignore @@ -0,0 +1 @@ +docs diff --git a/acceptance/python/Dockerfile b/acceptance/python/Dockerfile new file mode 100644 index 0000000..144fe64 --- /dev/null +++ b/acceptance/python/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.6.3 + +RUN mkdir -p /src +RUN pip install --upgrade \ + pycco \ + websocket-client~=0.47.0 \ + pytest~=3.0 \ + bigchaindb-driver~=0.6.2 \ + blns diff --git a/acceptance/python/src/test_basic.py b/acceptance/python/src/test_basic.py new file mode 100644 index 0000000..263dc1b --- /dev/null +++ b/acceptance/python/src/test_basic.py @@ -0,0 +1,125 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# # Basic Acceptance Test +# Here we check that the primitives of the system behave as expected. +# As you will see, this script tests basic stuff like: +# +# - create a transaction +# - check if the transaction is stored +# - check for the outputs of a given public key +# - transfer the transaction to another key +# +# We run a series of checks for each steps, that is retrieving the transaction from +# the remote system, and also checking the `outputs` of a given public key. +# +# This acceptance test is a rip-off of our +# [tutorial](https://docs.bigchaindb.com/projects/py-driver/en/latest/usage.html). + +# ## Imports +# We need some utils from the `os` package, we will interact with +# env variables. +import os + +# For this test case we import and use the Python Driver. +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair + + +def test_basic(): + # ## Set up a connection to Planetmint + # To use BighainDB we need a connection. Here we create one. By default we + # connect to localhost, but you can override this value using the env variable + # called `PLANETMINT_ENDPOINT`, a valid value must include the schema: + # `https://example.com:9984` + bdb = BigchainDB(os.environ.get('PLANETMINT_ENDPOINT')) + + # ## Create keypairs + # This test requires the interaction between two actors with their own keypair. + # The two keypairs will be called—drum roll—Alice and Bob. + alice, bob = generate_keypair(), generate_keypair() + + # ## Alice registers her bike in Planetmint + # Alice has a nice bike, and here she creates the "digital twin" + # of her bike. + bike = {'data': {'bicycle': {'serial_number': 420420}}} + + # She prepares a `CREATE` transaction... + prepared_creation_tx = bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + asset=bike) + + # ... and she fulfills it with her private key. + fulfilled_creation_tx = bdb.transactions.fulfill( + prepared_creation_tx, + private_keys=alice.private_key) + + # We will use the `id` of this transaction several time, so we store it in + # a variable with a short and easy name + bike_id = fulfilled_creation_tx['id'] + + # Now she is ready to send it to the Planetmint Network. + sent_transfer_tx = bdb.transactions.send_commit(fulfilled_creation_tx) + + # And just to be 100% sure, she also checks if she can retrieve + # it from the Planetmint node. + assert bdb.transactions.retrieve(bike_id), 'Cannot find transaction {}'.format(bike_id) + + # Alice is now the proud owner of one unspent asset. + assert len(bdb.outputs.get(alice.public_key, spent=False)) == 1 + assert bdb.outputs.get(alice.public_key)[0]['transaction_id'] == bike_id + + # ## Alice transfers her bike to Bob + # After registering her bike, Alice is ready to transfer it to Bob. + # She needs to create a new `TRANSFER` transaction. + + # A `TRANSFER` transaction contains a pointer to the original asset. The original asset + # is identified by the `id` of the `CREATE` transaction that defined it. + transfer_asset = {'id': bike_id} + + # Alice wants to spend the one and only output available, the one with index `0`. + output_index = 0 + output = fulfilled_creation_tx['outputs'][output_index] + + # Here, she defines the `input` of the `TRANSFER` transaction. The `input` contains + # several keys: + # + # - `fulfillment`, taken from the previous `CREATE` transaction. + # - `fulfills`, that specifies which condition she is fulfilling. + # - `owners_before`. + transfer_input = {'fulfillment': output['condition']['details'], + 'fulfills': {'output_index': output_index, + 'transaction_id': fulfilled_creation_tx['id']}, + 'owners_before': output['public_keys']} + + # Now that all the elements are set, she creates the actual transaction... + prepared_transfer_tx = bdb.transactions.prepare( + operation='TRANSFER', + asset=transfer_asset, + inputs=transfer_input, + recipients=bob.public_key) + + # ... and signs it with her private key. + fulfilled_transfer_tx = bdb.transactions.fulfill( + prepared_transfer_tx, + private_keys=alice.private_key) + + # She finally sends the transaction to a Planetmint node. + sent_transfer_tx = bdb.transactions.send_commit(fulfilled_transfer_tx) + + # And just to be 100% sure, she also checks if she can retrieve + # it from the Planetmint node. + assert bdb.transactions.retrieve(fulfilled_transfer_tx['id']) == sent_transfer_tx + + # Now Alice has zero unspent transactions. + assert len(bdb.outputs.get(alice.public_key, spent=False)) == 0 + + # While Bob has one. + assert len(bdb.outputs.get(bob.public_key, spent=False)) == 1 + + # Bob double checks what he got was the actual bike. + bob_tx_id = bdb.outputs.get(bob.public_key, spent=False)[0]['transaction_id'] + assert bdb.transactions.retrieve(bob_tx_id) == sent_transfer_tx diff --git a/acceptance/python/src/test_divisible_asset.py b/acceptance/python/src/test_divisible_asset.py new file mode 100644 index 0000000..d02ac2b --- /dev/null +++ b/acceptance/python/src/test_divisible_asset.py @@ -0,0 +1,181 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# # Divisible assets integration testing +# This test checks if we can successfully divide assets. +# The script tests various things like: +# +# - create a transaction with a divisible asset and issue them to someone +# - check if the transaction is stored and has the right amount of tokens +# - spend some tokens +# - try to spend more tokens than available +# +# We run a series of checks for each step, that is retrieving +# the transaction from the remote system, and also checking the `amount` +# of a given transaction. +# +# This integration test is a rip-off of our +# [tutorial](https://docs.bigchaindb.com/projects/py-driver/en/latest/usage.html). + +# ## Imports +# We need some utils from the `os` package, we will interact with +# env variables. +# We need the `pytest` package to catch the `BadRequest` exception properly. +# And of course, we also need the `BadRequest`. +import os +import pytest +from bigchaindb_driver.exceptions import BadRequest + +# For this test case we import and use the Python Driver. +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair + + +def test_divisible_assets(): + # ## Set up a connection to Planetmint + # Check [test_basic.py](./test_basic.html) to get some more details + # about the endpoint. + bdb = BigchainDB(os.environ.get('PLANETMINT_ENDPOINT')) + + # Oh look, it is Alice again and she brought her friend Bob along. + alice, bob = generate_keypair(), generate_keypair() + + # ## Alice creates a time sharing token + # Alice wants to go on vacation, while Bobs bike just broke down. + # Alice decides to rent her bike to Bob while she is gone. + # So she prepares a `CREATE` transaction to issues 10 tokens. + # First, she prepares an asset for a time sharing token. As you can see in + # the description, Bob and Alice agree that each token can be used to ride + # the bike for one hour. + + bike_token = { + 'data': { + 'token_for': { + 'bike': { + 'serial_number': 420420 + } + }, + 'description': 'Time share token. Each token equals one hour of riding.', + }, + } + + # She prepares a `CREATE` transaction and issues 10 tokens. + # Here, Alice defines in a tuple that she wants to assign + # these 10 tokens to Bob. + prepared_token_tx = bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + recipients=[([bob.public_key], 10)], + asset=bike_token) + + # She fulfills and sends the transaction. + fulfilled_token_tx = bdb.transactions.fulfill( + prepared_token_tx, + private_keys=alice.private_key) + + bdb.transactions.send_commit(fulfilled_token_tx) + + # We store the `id` of the transaction to use it later on. + bike_token_id = fulfilled_token_tx['id'] + + # Let's check if the transaction was successful. + assert bdb.transactions.retrieve(bike_token_id), \ + 'Cannot find transaction {}'.format(bike_token_id) + + # Bob owns 10 tokens now. + assert bdb.transactions.retrieve(bike_token_id)['outputs'][0][ + 'amount'] == '10' + + # ## Bob wants to use the bike + # Now that Bob got the tokens and the sun is shining, he wants to get out + # with the bike for three hours. + # To use the bike he has to send the tokens back to Alice. + # To learn about the details of transferring a transaction check out + # [test_basic.py](./test_basic.html) + transfer_asset = {'id': bike_token_id} + + output_index = 0 + output = fulfilled_token_tx['outputs'][output_index] + transfer_input = {'fulfillment': output['condition']['details'], + 'fulfills': {'output_index': output_index, + 'transaction_id': fulfilled_token_tx[ + 'id']}, + 'owners_before': output['public_keys']} + + # To use the tokens Bob has to reassign 7 tokens to himself and the + # amount he wants to use to Alice. + prepared_transfer_tx = bdb.transactions.prepare( + operation='TRANSFER', + asset=transfer_asset, + inputs=transfer_input, + recipients=[([alice.public_key], 3), ([bob.public_key], 7)]) + + # He signs and sends the transaction. + fulfilled_transfer_tx = bdb.transactions.fulfill( + prepared_transfer_tx, + private_keys=bob.private_key) + + sent_transfer_tx = bdb.transactions.send_commit(fulfilled_transfer_tx) + + # First, Bob checks if the transaction was successful. + assert bdb.transactions.retrieve( + fulfilled_transfer_tx['id']) == sent_transfer_tx + # There are two outputs in the transaction now. + # The first output shows that Alice got back 3 tokens... + assert bdb.transactions.retrieve( + fulfilled_transfer_tx['id'])['outputs'][0]['amount'] == '3' + + # ... while Bob still has 7 left. + assert bdb.transactions.retrieve( + fulfilled_transfer_tx['id'])['outputs'][1]['amount'] == '7' + + # ## Bob wants to ride the bike again + # It's been a week and Bob wants to right the bike again. + # Now he wants to ride for 8 hours, that's a lot Bob! + # He prepares the transaction again. + + transfer_asset = {'id': bike_token_id} + # This time we need an `output_index` of 1, since we have two outputs + # in the `fulfilled_transfer_tx` we created before. The first output with + # index 0 is for Alice and the second output is for Bob. + # Since Bob wants to spend more of his tokens he has to provide the + # correct output with the correct amount of tokens. + output_index = 1 + + output = fulfilled_transfer_tx['outputs'][output_index] + + transfer_input = {'fulfillment': output['condition']['details'], + 'fulfills': {'output_index': output_index, + 'transaction_id': fulfilled_transfer_tx['id']}, + 'owners_before': output['public_keys']} + + # This time Bob only provides Alice in the `recipients` because he wants + # to spend all his tokens + prepared_transfer_tx = bdb.transactions.prepare( + operation='TRANSFER', + asset=transfer_asset, + inputs=transfer_input, + recipients=[([alice.public_key], 8)]) + + fulfilled_transfer_tx = bdb.transactions.fulfill( + prepared_transfer_tx, + private_keys=bob.private_key) + + # Oh Bob, what have you done?! You tried to spend more tokens than you had. + # Remember Bob, last time you spent 3 tokens already, + # so you only have 7 left. + with pytest.raises(BadRequest) as error: + bdb.transactions.send_commit(fulfilled_transfer_tx) + + # Now Bob gets an error saying that the amount he wanted to spent is + # higher than the amount of tokens he has left. + assert error.value.args[0] == 400 + message = 'Invalid transaction (AmountError): The amount used in the ' \ + 'inputs `7` needs to be same as the amount used in the ' \ + 'outputs `8`' + assert error.value.args[2]['message'] == message + + # We have to stop this test now, I am sorry, but Bob is pretty upset + # about his mistake. See you next time :) diff --git a/acceptance/python/src/test_double_spend.py b/acceptance/python/src/test_double_spend.py new file mode 100644 index 0000000..66a2467 --- /dev/null +++ b/acceptance/python/src/test_double_spend.py @@ -0,0 +1,48 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# # Double Spend testing +# This test challenge the system with double spends. + +import os +from uuid import uuid4 +from threading import Thread +import queue + +import bigchaindb_driver.exceptions +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair + + +def test_double_create(): + bdb = BigchainDB(os.environ.get('PLANETMINT_ENDPOINT')) + alice = generate_keypair() + + results = queue.Queue() + + tx = bdb.transactions.fulfill( + bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + asset={'data': {'uuid': str(uuid4())}}), + private_keys=alice.private_key) + + def send_and_queue(tx): + try: + bdb.transactions.send_commit(tx) + results.put('OK') + except bigchaindb_driver.exceptions.TransportError as e: + results.put('FAIL') + + t1 = Thread(target=send_and_queue, args=(tx, )) + t2 = Thread(target=send_and_queue, args=(tx, )) + + t1.start() + t2.start() + + results = [results.get(timeout=2), results.get(timeout=2)] + + assert results.count('OK') == 1 + assert results.count('FAIL') == 1 diff --git a/acceptance/python/src/test_multiple_owners.py b/acceptance/python/src/test_multiple_owners.py new file mode 100644 index 0000000..f407c7d --- /dev/null +++ b/acceptance/python/src/test_multiple_owners.py @@ -0,0 +1,126 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# # Multiple owners integration testing +# This test checks if we can successfully create and transfer a transaction +# with multiple owners. +# The script tests various things like: +# +# - create a transaction with multiple owners +# - check if the transaction is stored and has the right amount of public keys +# - transfer the transaction to a third person +# +# We run a series of checks for each step, that is retrieving +# the transaction from the remote system, and also checking the public keys +# of a given transaction. +# +# This integration test is a rip-off of our +# [tutorial](https://docs.bigchaindb.com/projects/py-driver/en/latest/usage.html). + +# ## Imports +# We need some utils from the `os` package, we will interact with +# env variables. +import os + +# For this test case we import and use the Python Driver. +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair + + +def test_multiple_owners(): + # ## Set up a connection to Planetmint + # Check [test_basic.py](./test_basic.html) to get some more details + # about the endpoint. + bdb = BigchainDB(os.environ.get('PLANETMINT_ENDPOINT')) + + # Hey Alice and Bob, nice to see you again! + alice, bob = generate_keypair(), generate_keypair() + + # ## Alice and Bob create a transaction + # Alice and Bob just moved into a shared flat, no one can afford these + # high rents anymore. Bob suggests to get a dish washer for the + # kitchen. Alice agrees and here they go, creating the asset for their + # dish washer. + dw_asset = { + 'data': { + 'dish washer': { + 'serial_number': 1337 + } + } + } + + # They prepare a `CREATE` transaction. To have multiple owners, both + # Bob and Alice need to be the recipients. + prepared_dw_tx = bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + recipients=(alice.public_key, bob.public_key), + asset=dw_asset) + + # Now they both sign the transaction by providing their private keys. + # And send it afterwards. + fulfilled_dw_tx = bdb.transactions.fulfill( + prepared_dw_tx, + private_keys=[alice.private_key, bob.private_key]) + + bdb.transactions.send_commit(fulfilled_dw_tx) + + # We store the `id` of the transaction to use it later on. + dw_id = fulfilled_dw_tx['id'] + + # Let's check if the transaction was successful. + assert bdb.transactions.retrieve(dw_id), \ + 'Cannot find transaction {}'.format(dw_id) + + # The transaction should have two public keys in the outputs. + assert len( + bdb.transactions.retrieve(dw_id)['outputs'][0]['public_keys']) == 2 + + # ## Alice and Bob transfer a transaction to Carol. + # Alice and Bob save a lot of money living together. They often go out + # for dinner and don't cook at home. But now they don't have any dishes to + # wash, so they decide to sell the dish washer to their friend Carol. + + # Hey Carol, nice to meet you! + carol = generate_keypair() + + # Alice and Bob prepare the transaction to transfer the dish washer to + # Carol. + transfer_asset = {'id': dw_id} + + output_index = 0 + output = fulfilled_dw_tx['outputs'][output_index] + transfer_input = {'fulfillment': output['condition']['details'], + 'fulfills': {'output_index': output_index, + 'transaction_id': fulfilled_dw_tx[ + 'id']}, + 'owners_before': output['public_keys']} + + # Now they create the transaction... + prepared_transfer_tx = bdb.transactions.prepare( + operation='TRANSFER', + asset=transfer_asset, + inputs=transfer_input, + recipients=carol.public_key) + + # ... and sign it with their private keys, then send it. + fulfilled_transfer_tx = bdb.transactions.fulfill( + prepared_transfer_tx, + private_keys=[alice.private_key, bob.private_key]) + + sent_transfer_tx = bdb.transactions.send_commit(fulfilled_transfer_tx) + + # They check if the transaction was successful. + assert bdb.transactions.retrieve( + fulfilled_transfer_tx['id']) == sent_transfer_tx + + # The owners before should include both Alice and Bob. + assert len( + bdb.transactions.retrieve(fulfilled_transfer_tx['id'])['inputs'][0][ + 'owners_before']) == 2 + + # While the new owner is Carol. + assert bdb.transactions.retrieve(fulfilled_transfer_tx['id'])[ + 'outputs'][0]['public_keys'][0] == carol.public_key diff --git a/acceptance/python/src/test_naughty_strings.py b/acceptance/python/src/test_naughty_strings.py new file mode 100644 index 0000000..f0e92d4 --- /dev/null +++ b/acceptance/python/src/test_naughty_strings.py @@ -0,0 +1,101 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# ## Testing potentially hazardous strings +# This test uses a library of `naughty` strings (code injections, weird unicode chars., etc.) as both keys and values. +# We look for either a successful tx, or in the case that we use a naughty string as a key, and it violates some key +# constraints, we expect to receive a well formatted error message. + +# ## Imports +# We need some utils from the `os` package, we will interact with +# env variables. +import os + +# Since the naughty strings get encoded and decoded in odd ways, +# we'll use a regex to sweep those details under the rug. +import re + +# We'll use a nice library of naughty strings... +from blns import blns + +# And parameterize our test so each one is treated as a separate test case +import pytest + +# For this test case we import and use the Python Driver. +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair +from bigchaindb_driver.exceptions import BadRequest + +naughty_strings = blns.all() + + +# This is our base test case, but we'll reuse it to send naughty strings as both keys and values. +def send_naughty_tx(asset, metadata): + # ## Set up a connection to Planetmint + # Check [test_basic.py](./test_basic.html) to get some more details + # about the endpoint. + bdb = BigchainDB(os.environ.get('PLANETMINT_ENDPOINT')) + + # Here's Alice. + alice = generate_keypair() + + # Alice is in a naughty mood today, so she creates a tx with some naughty strings + prepared_transaction = bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + asset=asset, + metadata=metadata) + + # She fulfills the transaction + fulfilled_transaction = bdb.transactions.fulfill( + prepared_transaction, + private_keys=alice.private_key) + + # The fulfilled tx gets sent to the BDB network + try: + sent_transaction = bdb.transactions.send_commit(fulfilled_transaction) + except BadRequest as e: + sent_transaction = e + + # If her key contained a '.', began with a '$', or contained a NUL character + regex = '.*\..*|\$.*|.*\x00.*' + key = next(iter(metadata)) + if re.match(regex, key): + # Then she expects a nicely formatted error code + status_code = sent_transaction.status_code + error = sent_transaction.error + regex = ( + r'\{\s*\n*' + r'\s*"message":\s*"Invalid transaction \(ValidationError\):\s*' + r'Invalid key name.*The key name cannot contain characters.*\n*' + r'\s*"status":\s*400\n*' + r'\s*\}\n*') + assert status_code == 400 + assert re.fullmatch(regex, error), sent_transaction + # Otherwise, she expects to see her transaction in the database + elif 'id' in sent_transaction.keys(): + tx_id = sent_transaction['id'] + assert bdb.transactions.retrieve(tx_id) + # If neither condition was true, then something weird happened... + else: + raise TypeError(sent_transaction) + + +@pytest.mark.parametrize("naughty_string", naughty_strings, ids=naughty_strings) +def test_naughty_keys(naughty_string): + + asset = {'data': {naughty_string: 'nice_value'}} + metadata = {naughty_string: 'nice_value'} + + send_naughty_tx(asset, metadata) + + +@pytest.mark.parametrize("naughty_string", naughty_strings, ids=naughty_strings) +def test_naughty_values(naughty_string): + + asset = {'data': {'nice_key': naughty_string}} + metadata = {'nice_key': naughty_string} + + send_naughty_tx(asset, metadata) diff --git a/acceptance/python/src/test_stream.py b/acceptance/python/src/test_stream.py new file mode 100644 index 0000000..c82f43d --- /dev/null +++ b/acceptance/python/src/test_stream.py @@ -0,0 +1,132 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# # Stream Acceptance Test +# This test checks if the event stream works correctly. The basic idea of this +# test is to generate some random **valid** transaction, send them to a +# Planetmint node, and expect those transactions to be returned by the valid +# transactions Stream API. During this test, two threads work together, +# sharing a queue to exchange events. +# +# - The *main thread* first creates and sends the transactions to Planetmint; +# then it run through all events in the shared queue to check if all +# transactions sent have been validated by Planetmint. +# - The *listen thread* listens to the events coming from Planetmint and puts +# them in a queue shared with the main thread. + +import os +import queue +import json +from threading import Thread, Event +from uuid import uuid4 + +# For this script, we need to set up a websocket connection, that's the reason +# we import the +# [websocket](https://github.com/websocket-client/websocket-client) module +from websocket import create_connection + +from bigchaindb_driver import BigchainDB +from bigchaindb_driver.crypto import generate_keypair + + +def test_stream(): + # ## Set up the test + # We use the env variable `BICHAINDB_ENDPOINT` to know where to connect. + # Check [test_basic.py](./test_basic.html) for more information. + BDB_ENDPOINT = os.environ.get('PLANETMINT_ENDPOINT') + + # *That's pretty bad, but let's do like this for now.* + WS_ENDPOINT = 'ws://{}:9985/api/v1/streams/valid_transactions'.format(BDB_ENDPOINT.rsplit(':')[0]) + + bdb = BigchainDB(BDB_ENDPOINT) + + # Hello to Alice again, she is pretty active in those tests, good job + # Alice! + alice = generate_keypair() + + # We need few variables to keep the state, specifically we need `sent` to + # keep track of all transactions Alice sent to Planetmint, while `received` + # are the transactions Planetmint validated and sent back to her. + sent = [] + received = queue.Queue() + + # In this test we use a websocket. The websocket must be started **before** + # sending transactions to Planetmint, otherwise we might lose some + # transactions. The `ws_ready` event is used to synchronize the main thread + # with the listen thread. + ws_ready = Event() + + # ## Listening to events + # This is the function run by the complementary thread. + def listen(): + # First we connect to the remote endpoint using the WebSocket protocol. + ws = create_connection(WS_ENDPOINT) + + # After the connection has been set up, we can signal the main thread + # to proceed (continue reading, it should make sense in a second.) + ws_ready.set() + + # It's time to consume all events coming from the Planetmint stream API. + # Every time a new event is received, it is put in the queue shared + # with the main thread. + while True: + result = ws.recv() + received.put(result) + + # Put `listen` in a thread, and start it. Note that `listen` is a local + # function and it can access all variables in the enclosing function. + t = Thread(target=listen, daemon=True) + t.start() + + # ## Pushing the transactions to Planetmint + # After starting the listen thread, we wait for it to connect, and then we + # proceed. + ws_ready.wait() + + # Here we prepare, sign, and send ten different `CREATE` transactions. To + # make sure each transaction is different from the other, we generate a + # random `uuid`. + for _ in range(10): + tx = bdb.transactions.fulfill( + bdb.transactions.prepare( + operation='CREATE', + signers=alice.public_key, + asset={'data': {'uuid': str(uuid4())}}), + private_keys=alice.private_key) + # We don't want to wait for each transaction to be in a block. By using + # `async` mode, we make sure that the driver returns as soon as the + # transaction is pushed to the Planetmint API. Remember: we expect all + # transactions to be in the shared queue: this is a two phase test, + # first we send a bunch of transactions, then we check if they are + # valid (and, in this case, they should). + bdb.transactions.send_async(tx) + + # The `id` of every sent transaction is then stored in a list. + sent.append(tx['id']) + + # ## Check the valid transactions coming from Planetmint + # Now we are ready to check if Planetmint did its job. A simple way to + # check if all sent transactions have been processed is to **remove** from + # `sent` the transactions we get from the *listen thread*. At one point in + # time, `sent` should be empty, and we exit the test. + while sent: + # To avoid waiting forever, we have an arbitrary timeout of 5 + # seconds: it should be enough time for Planetmint to create + # blocks, in fact a new block is created every second. If we hit + # the timeout, then game over ¯\\\_(ツ)\_/¯ + try: + event = received.get(timeout=5) + txid = json.loads(event)['transaction_id'] + except queue.Empty: + assert False, 'Did not receive all expected transactions' + + # Last thing is to try to remove the `txid` from the set of sent + # transactions. If this test is running in parallel with others, we + # might get a transaction id of another test, and `remove` can fail. + # It's OK if this happens. + try: + sent.remove(txid) + except ValueError: + pass diff --git a/bigchaindb/README.md b/bigchaindb/README.md new file mode 100644 index 0000000..9988929 --- /dev/null +++ b/bigchaindb/README.md @@ -0,0 +1,42 @@ + + +# Overview + +A high-level description of the files and subdirectories of Planetmint. + +## Files + +### [`lib.py`](lib.py) + +The `Planetmint` class is defined here. Most node-level operations and database interactions are found in this file. This is the place to start if you are interested in implementing a server API, since many of these class methods concern Planetmint interacting with the outside world. + +### [`models.py`](./models.py) + +`Block`, `Transaction`, and `Asset` classes are defined here. The classes mirror the block and transaction structure from the [documentation](https://docs.bigchaindb.com/projects/server/en/latest/data-models/index.html), but also include methods for validation and signing. + +### [`validation.py`](./validation.py) + +Base class for validation methods (verification of votes, blocks, and transactions). The actual logic is mostly found in `transaction` and `block` models, defined in [`models.py`](./models.py). + +### [`processes.py`](./processes.py) + +Entry point for the Planetmint process, after initialization. All subprocesses are started here: processes to handle new blocks, votes, etc. + +### [`config_utils.py`](./config_utils.py) + +Methods for managing the configuration, including loading configuration files, automatically generating the configuration, and keeping the configuration consistent across Planetmint instances. + +## Folders + +### [`commands`](./commands) + +Contains code for the [CLI](https://docs.bigchaindb.com/projects/server/en/latest/server-reference/bigchaindb-cli.html) for Planetmint. + +### [`db`](./db) + +Code for building the database connection, creating indexes, and other database setup tasks. diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py new file mode 100644 index 0000000..6bba680 --- /dev/null +++ b/bigchaindb/__init__.py @@ -0,0 +1,106 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import copy +import logging + +from bigchaindb.log import DEFAULT_LOGGING_CONFIG as log_config +from bigchaindb.lib import BigchainDB # noqa +from bigchaindb.migrations.chain_migration_election import ChainMigrationElection +from bigchaindb.version import __version__ # noqa +from bigchaindb.core import App # noqa + +# from functools import reduce +# PORT_NUMBER = reduce(lambda x, y: x * y, map(ord, 'BigchainDB')) % 2**16 +# basically, the port number is 9984 + +# The following variable is used by `bigchaindb configure` to +# prompt the user for database values. We cannot rely on +# _base_database_localmongodb.keys() because dicts are unordered. +# I tried to configure + +_database_keys_map = { + 'localmongodb': ('host', 'port', 'name'), +} + +_base_database_localmongodb = { + 'host': 'localhost', + 'port': 27017, + 'name': 'bigchain', + 'replicaset': None, + 'login': None, + 'password': None, +} + +_database_localmongodb = { + 'backend': 'localmongodb', + 'connection_timeout': 5000, + 'max_tries': 3, + 'ssl': False, + 'ca_cert': None, + 'certfile': None, + 'keyfile': None, + 'keyfile_passphrase': None, + 'crlfile': None, +} +_database_localmongodb.update(_base_database_localmongodb) + +_database_map = { + 'localmongodb': _database_localmongodb, +} + +config = { + 'server': { + # Note: this section supports all the Gunicorn settings: + # - http://docs.gunicorn.org/en/stable/settings.html + 'bind': 'localhost:9984', + 'loglevel': logging.getLevelName( + log_config['handlers']['console']['level']).lower(), + 'workers': None, # if None, the value will be cpu_count * 2 + 1 + }, + 'wsserver': { + 'scheme': 'ws', + 'host': 'localhost', + 'port': 9985, + 'advertised_scheme': 'ws', + 'advertised_host': 'localhost', + 'advertised_port': 9985, + }, + 'tendermint': { + 'host': 'localhost', + 'port': 26657, + 'version': 'v0.31.5', # look for __tm_supported_versions__ + }, + # FIXME: hardcoding to localmongodb for now + 'database': _database_map['localmongodb'], + 'log': { + 'file': log_config['handlers']['file']['filename'], + 'error_file': log_config['handlers']['errors']['filename'], + 'level_console': logging.getLevelName( + log_config['handlers']['console']['level']).lower(), + 'level_logfile': logging.getLevelName( + log_config['handlers']['file']['level']).lower(), + 'datefmt_console': log_config['formatters']['console']['datefmt'], + 'datefmt_logfile': log_config['formatters']['file']['datefmt'], + 'fmt_console': log_config['formatters']['console']['format'], + 'fmt_logfile': log_config['formatters']['file']['format'], + 'granular_levels': {}, + }, +} + +# We need to maintain a backup copy of the original config dict in case +# the user wants to reconfigure the node. Check ``bigchaindb.config_utils`` +# for more info. +_config = copy.deepcopy(config) +from bigchaindb.common.transaction import Transaction # noqa +from bigchaindb import models # noqa +from bigchaindb.upsert_validator import ValidatorElection # noqa +from bigchaindb.elections.vote import Vote # noqa + +Transaction.register_type(Transaction.CREATE, models.Transaction) +Transaction.register_type(Transaction.TRANSFER, models.Transaction) +Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection) +Transaction.register_type(ChainMigrationElection.OPERATION, ChainMigrationElection) +Transaction.register_type(Vote.OPERATION, Vote) diff --git a/bigchaindb/backend/README.md b/bigchaindb/backend/README.md new file mode 100644 index 0000000..885cb9e --- /dev/null +++ b/bigchaindb/backend/README.md @@ -0,0 +1,51 @@ + + +# Backend Interfaces + +## Structure + +- [`connection.py`](./connection.py): Database connection-related interfaces +- [`query.py`](./query.py): Database query-related interfaces, dispatched through single-dispatch +- [`schema.py`](./schema.py): Database setup and schema-related interfaces, dispatched through + single-dispatch + +Built-in implementations (e.g. [MongoDB's](./localmongodb)) are provided in sub-directories and +have their connection type's location exposed as `BACKENDS` in [`connection.py`](./connection.py). + +## Single-Dispatched Interfaces + +The architecture of this module is based heavily upon Python's newly-introduced [single-dispatch +generic functions](https://www.python.org/dev/peps/pep-0443/). Single-dispatch is convenient, +because it allows Python, rather than something we design ourselves, to manage the dispatching of +generic functions based on certain conditions being met (e.g. the database backend to use). + +To see what this looks like in Planetmint, first note that our backend interfaces have been +configured to dispatch based on a backend's **connection type**. + +Call `bigchaindb.backend.connect()` to create an instance of a `Connection`: + +```python +from bigchaindb.backend import connect +connection = connect() # By default, uses the current configuration for backend, host, port, etc. +``` + +Then, we can call a backend function by directly calling its interface: + +```python +from bigchaindb.backend import query +query.write_transaction(connection, ...) +``` + +Notice that we don't need to care about which backend implementation to use or how to access it. +Code can simply call the base interface function with a `Connection` instance, and single-dispatch +will handle routing the call to the actual implementation. + +Planetmint will load and register the configured backend's implementation automatically (see +`bigchaindb.backend.connect()`), so you should always just be able to call an interface function if +you have a `Connection` instance. A few helper utilities (see [`backend/utils.py`](./utils.py)) are +also provided to make registering new backend implementations easier. diff --git a/bigchaindb/backend/__init__.py b/bigchaindb/backend/__init__.py new file mode 100644 index 0000000..edc9473 --- /dev/null +++ b/bigchaindb/backend/__init__.py @@ -0,0 +1,17 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Generic backend database interfaces expected by Planetmint. + +The interfaces in this module allow Planetmint to be agnostic about its +database backend. One can configure Planetmint to use different databases as +its data store by setting the ``database.backend`` property in the +configuration or the ``PLANETMINT_DATABASE_BACKEND`` environment variable. +""" + +# Include the backend interfaces +from bigchaindb.backend import schema, query # noqa + +from bigchaindb.backend.connection import connect # noqa diff --git a/bigchaindb/backend/connection.py b/bigchaindb/backend/connection.py new file mode 100644 index 0000000..0a529f0 --- /dev/null +++ b/bigchaindb/backend/connection.py @@ -0,0 +1,170 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import logging +from importlib import import_module +from itertools import repeat + +import bigchaindb +from bigchaindb.backend.exceptions import ConnectionError +from bigchaindb.backend.utils import get_bigchaindb_config_value, get_bigchaindb_config_value_or_key_error +from bigchaindb.common.exceptions import ConfigurationError + +BACKENDS = { + 'localmongodb': 'bigchaindb.backend.localmongodb.connection.LocalMongoDBConnection', +} + +logger = logging.getLogger(__name__) + + +def connect(backend=None, host=None, port=None, name=None, max_tries=None, + connection_timeout=None, replicaset=None, ssl=None, login=None, password=None, + ca_cert=None, certfile=None, keyfile=None, keyfile_passphrase=None, + crlfile=None): + """Create a new connection to the database backend. + + All arguments default to the current configuration's values if not + given. + + Args: + backend (str): the name of the backend to use. + host (str): the host to connect to. + port (int): the port to connect to. + name (str): the name of the database to use. + replicaset (str): the name of the replica set (only relevant for + MongoDB connections). + + Returns: + An instance of :class:`~bigchaindb.backend.connection.Connection` + based on the given (or defaulted) :attr:`backend`. + + Raises: + :exc:`~ConnectionError`: If the connection to the database fails. + :exc:`~ConfigurationError`: If the given (or defaulted) :attr:`backend` + is not supported or could not be loaded. + :exc:`~AuthenticationError`: If there is a OperationFailure due to + Authentication failure after connecting to the database. + """ + + backend = backend or get_bigchaindb_config_value_or_key_error('backend') + host = host or get_bigchaindb_config_value_or_key_error('host') + port = port or get_bigchaindb_config_value_or_key_error('port') + dbname = name or get_bigchaindb_config_value_or_key_error('name') + # Not sure how to handle this here. This setting is only relevant for + # mongodb. + # I added **kwargs for both RethinkDBConnection and MongoDBConnection + # to handle these these additional args. In case of RethinkDBConnection + # it just does not do anything with it. + # + # UPD: RethinkDBConnection is not here anymore cause we no longer support RethinkDB. + # The problem described above might be reconsidered next time we introduce a backend, + # if it ever happens. + replicaset = replicaset or get_bigchaindb_config_value('replicaset') + ssl = ssl if ssl is not None else get_bigchaindb_config_value('ssl', False) + login = login or get_bigchaindb_config_value('login') + password = password or get_bigchaindb_config_value('password') + ca_cert = ca_cert or get_bigchaindb_config_value('ca_cert') + certfile = certfile or get_bigchaindb_config_value('certfile') + keyfile = keyfile or get_bigchaindb_config_value('keyfile') + keyfile_passphrase = keyfile_passphrase or get_bigchaindb_config_value('keyfile_passphrase', None) + crlfile = crlfile or get_bigchaindb_config_value('crlfile') + + try: + module_name, _, class_name = BACKENDS[backend].rpartition('.') + Class = getattr(import_module(module_name), class_name) + except KeyError: + raise ConfigurationError('Backend `{}` is not supported. ' + 'Planetmint currently supports {}'.format(backend, BACKENDS.keys())) + except (ImportError, AttributeError) as exc: + raise ConfigurationError('Error loading backend `{}`'.format(backend)) from exc + + logger.debug('Connection: {}'.format(Class)) + return Class(host=host, port=port, dbname=dbname, + max_tries=max_tries, connection_timeout=connection_timeout, + replicaset=replicaset, ssl=ssl, login=login, password=password, + ca_cert=ca_cert, certfile=certfile, keyfile=keyfile, + keyfile_passphrase=keyfile_passphrase, crlfile=crlfile) + + +class Connection: + """Connection class interface. + + All backend implementations should provide a connection class that inherits + from and implements this class. + """ + + def __init__(self, host=None, port=None, dbname=None, + connection_timeout=None, max_tries=None, + **kwargs): + """Create a new :class:`~.Connection` instance. + + Args: + host (str): the host to connect to. + port (int): the port to connect to. + dbname (str): the name of the database to use. + connection_timeout (int, optional): the milliseconds to wait + until timing out the database connection attempt. + Defaults to 5000ms. + max_tries (int, optional): how many tries before giving up, + if 0 then try forever. Defaults to 3. + **kwargs: arbitrary keyword arguments provided by the + configuration's ``database`` settings + """ + + dbconf = bigchaindb.config['database'] + + self.host = host or dbconf['host'] + self.port = port or dbconf['port'] + self.dbname = dbname or dbconf['name'] + self.connection_timeout = connection_timeout if connection_timeout is not None \ + else dbconf['connection_timeout'] + self.max_tries = max_tries if max_tries is not None else dbconf['max_tries'] + self.max_tries_counter = range(self.max_tries) if self.max_tries != 0 else repeat(0) + self._conn = None + + @property + def conn(self): + if self._conn is None: + self.connect() + return self._conn + + def run(self, query): + """Run a query. + + Args: + query: the query to run + Raises: + :exc:`~DuplicateKeyError`: If the query fails because of a + duplicate key constraint. + :exc:`~OperationFailure`: If the query fails for any other + reason. + :exc:`~ConnectionError`: If the connection to the database + fails. + """ + + raise NotImplementedError() + + def connect(self): + """Try to connect to the database. + + Raises: + :exc:`~ConnectionError`: If the connection to the database + fails. + """ + + attempt = 0 + for i in self.max_tries_counter: + attempt += 1 + try: + self._conn = self._connect() + except ConnectionError as exc: + logger.warning('Attempt %s/%s. Connection to %s:%s failed after %sms.', + attempt, self.max_tries if self.max_tries != 0 else '∞', + self.host, self.port, self.connection_timeout) + if attempt == self.max_tries: + logger.critical('Cannot connect to the Database. Giving up.') + raise ConnectionError() from exc + else: + break diff --git a/bigchaindb/backend/exceptions.py b/bigchaindb/backend/exceptions.py new file mode 100644 index 0000000..d524597 --- /dev/null +++ b/bigchaindb/backend/exceptions.py @@ -0,0 +1,22 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from bigchaindb.exceptions import BigchainDBError + + +class BackendError(BigchainDBError): + """Top level exception for any backend exception.""" + + +class ConnectionError(BackendError): + """Exception raised when the connection to the backend fails.""" + + +class OperationError(BackendError): + """Exception raised when a backend operation fails.""" + + +class DuplicateKeyError(OperationError): + """Exception raised when an insert fails because the key is not unique""" diff --git a/bigchaindb/backend/localmongodb/__init__.py b/bigchaindb/backend/localmongodb/__init__.py new file mode 100644 index 0000000..4e44f18 --- /dev/null +++ b/bigchaindb/backend/localmongodb/__init__.py @@ -0,0 +1,28 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""MongoDB backend implementation. + +Contains a MongoDB-specific implementation of the +:mod:`~bigchaindb.backend.schema` and :mod:`~bigchaindb.backend.query` interfaces. + +You can specify Planetmint to use MongoDB as its database backend by either +setting ``database.backend`` to ``'localmongodb'`` in your configuration file, or +setting the ``PLANETMINT_DATABASE_BACKEND`` environment variable to +``'localmongodb'``. + +MongoDB is the default database backend for Planetmint. + +If configured to use MongoDB, Planetmint will automatically return instances +of :class:`~bigchaindb.backend.localmongodb.LocalMongoDBConnection` for +:func:`~bigchaindb.backend.connection.connect` and dispatch calls of the +generic backend interfaces to the implementations in this module. +""" + +# Register the single dispatched modules on import. +from bigchaindb.backend.localmongodb import schema, query # noqa + +# MongoDBConnection should always be accessed via +# ``bigchaindb.backend.connect()``. diff --git a/bigchaindb/backend/localmongodb/connection.py b/bigchaindb/backend/localmongodb/connection.py new file mode 100644 index 0000000..6ac660b --- /dev/null +++ b/bigchaindb/backend/localmongodb/connection.py @@ -0,0 +1,136 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import logging +from ssl import CERT_REQUIRED + +import pymongo + +from bigchaindb.backend.connection import Connection +from bigchaindb.backend.exceptions import (DuplicateKeyError, + OperationError, + ConnectionError) +from bigchaindb.backend.utils import get_bigchaindb_config_value +from bigchaindb.common.exceptions import ConfigurationError +from bigchaindb.utils import Lazy + +logger = logging.getLogger(__name__) + + +class LocalMongoDBConnection(Connection): + + def __init__(self, replicaset=None, ssl=None, login=None, password=None, + ca_cert=None, certfile=None, keyfile=None, + keyfile_passphrase=None, crlfile=None, **kwargs): + """Create a new Connection instance. + + Args: + replicaset (str, optional): the name of the replica set to + connect to. + **kwargs: arbitrary keyword arguments provided by the + configuration's ``database`` settings + """ + + super().__init__(**kwargs) + self.replicaset = replicaset or get_bigchaindb_config_value('replicaset') + self.ssl = ssl if ssl is not None else get_bigchaindb_config_value('ssl', False) + self.login = login or get_bigchaindb_config_value('login') + self.password = password or get_bigchaindb_config_value('password') + self.ca_cert = ca_cert or get_bigchaindb_config_value('ca_cert') + self.certfile = certfile or get_bigchaindb_config_value('certfile') + self.keyfile = keyfile or get_bigchaindb_config_value('keyfile') + self.keyfile_passphrase = keyfile_passphrase or get_bigchaindb_config_value('keyfile_passphrase') + self.crlfile = crlfile or get_bigchaindb_config_value('crlfile') + + @property + def db(self): + return self.conn[self.dbname] + + def query(self): + return Lazy() + + def collection(self, name): + """Return a lazy object that can be used to compose a query. + + Args: + name (str): the name of the collection to query. + """ + return self.query()[self.dbname][name] + + def run(self, query): + try: + try: + return query.run(self.conn) + except pymongo.errors.AutoReconnect: + logger.warning('Lost connection to the database, ' + 'retrying query.') + return query.run(self.conn) + except pymongo.errors.AutoReconnect as exc: + raise ConnectionError from exc + except pymongo.errors.DuplicateKeyError as exc: + raise DuplicateKeyError from exc + except pymongo.errors.OperationFailure as exc: + print(f'DETAILS: {exc.details}') + raise OperationError from exc + + def _connect(self): + """Try to connect to the database. + + Raises: + :exc:`~ConnectionError`: If the connection to the database + fails. + :exc:`~AuthenticationError`: If there is a OperationFailure due to + Authentication failure after connecting to the database. + :exc:`~ConfigurationError`: If there is a ConfigurationError while + connecting to the database. + """ + + try: + # FYI: the connection process might raise a + # `ServerSelectionTimeoutError`, that is a subclass of + # `ConnectionFailure`. + # The presence of ca_cert, certfile, keyfile, crlfile implies the + # use of certificates for TLS connectivity. + if self.ca_cert is None or self.certfile is None or \ + self.keyfile is None or self.crlfile is None: + client = pymongo.MongoClient(self.host, + self.port, + replicaset=self.replicaset, + serverselectiontimeoutms=self.connection_timeout, + ssl=self.ssl, + **MONGO_OPTS) + if self.login is not None and self.password is not None: + client[self.dbname].authenticate(self.login, self.password) + else: + logger.info('Connecting to MongoDB over TLS/SSL...') + client = pymongo.MongoClient(self.host, + self.port, + replicaset=self.replicaset, + serverselectiontimeoutms=self.connection_timeout, + ssl=self.ssl, + ssl_ca_certs=self.ca_cert, + ssl_certfile=self.certfile, + ssl_keyfile=self.keyfile, + ssl_pem_passphrase=self.keyfile_passphrase, + ssl_crlfile=self.crlfile, + ssl_cert_reqs=CERT_REQUIRED, + **MONGO_OPTS) + if self.login is not None: + client[self.dbname].authenticate(self.login, + mechanism='MONGODB-X509') + + return client + + except (pymongo.errors.ConnectionFailure, + pymongo.errors.OperationFailure) as exc: + logger.info('Exception in _connect(): {}'.format(exc)) + raise ConnectionError(str(exc)) from exc + except pymongo.errors.ConfigurationError as exc: + raise ConfigurationError from exc + + +MONGO_OPTS = { + 'socketTimeoutMS': 20000, +} diff --git a/bigchaindb/backend/localmongodb/query.py b/bigchaindb/backend/localmongodb/query.py new file mode 100644 index 0000000..b63f038 --- /dev/null +++ b/bigchaindb/backend/localmongodb/query.py @@ -0,0 +1,377 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Query implementation for MongoDB""" + +from pymongo import DESCENDING + +from bigchaindb import backend +from bigchaindb.backend.exceptions import DuplicateKeyError +from bigchaindb.backend.utils import module_dispatch_registrar +from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection +from bigchaindb.common.transaction import Transaction + +register_query = module_dispatch_registrar(backend.query) + + +@register_query(LocalMongoDBConnection) +def store_transactions(conn, signed_transactions): + return conn.run(conn.collection('transactions') + .insert_many(signed_transactions)) + + +@register_query(LocalMongoDBConnection) +def get_transaction(conn, transaction_id): + return conn.run( + conn.collection('transactions') + .find_one({'id': transaction_id}, {'_id': 0})) + + +@register_query(LocalMongoDBConnection) +def get_transactions(conn, transaction_ids): + try: + return conn.run( + conn.collection('transactions') + .find({'id': {'$in': transaction_ids}}, + projection={'_id': False})) + except IndexError: + pass + + +@register_query(LocalMongoDBConnection) +def store_metadatas(conn, metadata): + return conn.run( + conn.collection('metadata') + .insert_many(metadata, ordered=False)) + + +@register_query(LocalMongoDBConnection) +def get_metadata(conn, transaction_ids): + return conn.run( + conn.collection('metadata') + .find({'id': {'$in': transaction_ids}}, + projection={'_id': False})) + + +@register_query(LocalMongoDBConnection) +def store_asset(conn, asset): + try: + return conn.run( + conn.collection('assets') + .insert_one(asset)) + except DuplicateKeyError: + pass + + +@register_query(LocalMongoDBConnection) +def store_assets(conn, assets): + return conn.run( + conn.collection('assets') + .insert_many(assets, ordered=False)) + + +@register_query(LocalMongoDBConnection) +def get_asset(conn, asset_id): + try: + return conn.run( + conn.collection('assets') + .find_one({'id': asset_id}, {'_id': 0, 'id': 0})) + except IndexError: + pass + + +@register_query(LocalMongoDBConnection) +def get_assets(conn, asset_ids): + return conn.run( + conn.collection('assets') + .find({'id': {'$in': asset_ids}}, + projection={'_id': False})) + + +@register_query(LocalMongoDBConnection) +def get_spent(conn, transaction_id, output): + query = {'inputs': + {'$elemMatch': + {'$and': [{'fulfills.transaction_id': transaction_id}, + {'fulfills.output_index': output}]}}} + + return conn.run( + conn.collection('transactions') + .find(query, {'_id': 0})) + + +@register_query(LocalMongoDBConnection) +def get_latest_block(conn): + return conn.run( + conn.collection('blocks') + .find_one(projection={'_id': False}, + sort=[('height', DESCENDING)])) + + +@register_query(LocalMongoDBConnection) +def store_block(conn, block): + try: + return conn.run( + conn.collection('blocks') + .insert_one(block)) + except DuplicateKeyError: + pass + + +@register_query(LocalMongoDBConnection) +def get_txids_filtered(conn, asset_id, operation=None, last_tx=None): + + match = { + Transaction.CREATE: {'operation': 'CREATE', 'id': asset_id}, + Transaction.TRANSFER: {'operation': 'TRANSFER', 'asset.id': asset_id}, + None: {'$or': [{'asset.id': asset_id}, {'id': asset_id}]}, + }[operation] + + cursor = conn.run(conn.collection('transactions').find(match)) + + if last_tx: + cursor = cursor.sort([('$natural', DESCENDING)]).limit(1) + + return (elem['id'] for elem in cursor) + + +@register_query(LocalMongoDBConnection) +def text_search(conn, search, *, language='english', case_sensitive=False, + diacritic_sensitive=False, text_score=False, limit=0, table='assets'): + cursor = conn.run( + conn.collection(table) + .find({'$text': { + '$search': search, + '$language': language, + '$caseSensitive': case_sensitive, + '$diacriticSensitive': diacritic_sensitive}}, + {'score': {'$meta': 'textScore'}, '_id': False}) + .sort([('score', {'$meta': 'textScore'})]) + .limit(limit)) + + if text_score: + return cursor + + return (_remove_text_score(obj) for obj in cursor) + + +def _remove_text_score(asset): + asset.pop('score', None) + return asset + + +@register_query(LocalMongoDBConnection) +def get_owned_ids(conn, owner): + cursor = conn.run( + conn.collection('transactions').aggregate([ + {'$match': {'outputs.public_keys': owner}}, + {'$project': {'_id': False}} + ])) + return cursor + + +@register_query(LocalMongoDBConnection) +def get_spending_transactions(conn, inputs): + transaction_ids = [i['transaction_id'] for i in inputs] + output_indexes = [i['output_index'] for i in inputs] + query = {'inputs': + {'$elemMatch': + {'$and': + [ + {'fulfills.transaction_id': {'$in': transaction_ids}}, + {'fulfills.output_index': {'$in': output_indexes}} + ]}}} + + cursor = conn.run( + conn.collection('transactions').find(query, {'_id': False})) + return cursor + + +@register_query(LocalMongoDBConnection) +def get_block(conn, block_id): + return conn.run( + conn.collection('blocks') + .find_one({'height': block_id}, + projection={'_id': False})) + + +@register_query(LocalMongoDBConnection) +def get_block_with_transaction(conn, txid): + return conn.run( + conn.collection('blocks') + .find({'transactions': txid}, + projection={'_id': False, 'height': True})) + + +@register_query(LocalMongoDBConnection) +def delete_transactions(conn, txn_ids): + conn.run(conn.collection('assets').delete_many({'id': {'$in': txn_ids}})) + conn.run(conn.collection('metadata').delete_many({'id': {'$in': txn_ids}})) + conn.run(conn.collection('transactions').delete_many({'id': {'$in': txn_ids}})) + + +@register_query(LocalMongoDBConnection) +def store_unspent_outputs(conn, *unspent_outputs): + if unspent_outputs: + try: + return conn.run( + conn.collection('utxos').insert_many( + unspent_outputs, + ordered=False, + ) + ) + except DuplicateKeyError: + # TODO log warning at least + pass + + +@register_query(LocalMongoDBConnection) +def delete_unspent_outputs(conn, *unspent_outputs): + if unspent_outputs: + return conn.run( + conn.collection('utxos').delete_many({ + '$or': [{ + '$and': [ + {'transaction_id': unspent_output['transaction_id']}, + {'output_index': unspent_output['output_index']}, + ], + } for unspent_output in unspent_outputs] + }) + ) + + +@register_query(LocalMongoDBConnection) +def get_unspent_outputs(conn, *, query=None): + if query is None: + query = {} + return conn.run(conn.collection('utxos').find(query, + projection={'_id': False})) + + +@register_query(LocalMongoDBConnection) +def store_pre_commit_state(conn, state): + return conn.run( + conn.collection('pre_commit') + .replace_one({}, state, upsert=True) + ) + + +@register_query(LocalMongoDBConnection) +def get_pre_commit_state(conn): + return conn.run(conn.collection('pre_commit').find_one()) + + +@register_query(LocalMongoDBConnection) +def store_validator_set(conn, validators_update): + height = validators_update['height'] + return conn.run( + conn.collection('validators').replace_one( + {'height': height}, + validators_update, + upsert=True + ) + ) + + +@register_query(LocalMongoDBConnection) +def delete_validator_set(conn, height): + return conn.run( + conn.collection('validators').delete_many({'height': height}) + ) + + +@register_query(LocalMongoDBConnection) +def store_election(conn, election_id, height, is_concluded): + return conn.run( + conn.collection('elections').replace_one( + {'election_id': election_id, + 'height': height}, + {'election_id': election_id, + 'height': height, + 'is_concluded': is_concluded}, + upsert=True, + ) + ) + + +@register_query(LocalMongoDBConnection) +def store_elections(conn, elections): + return conn.run( + conn.collection('elections').insert_many(elections) + ) + + +@register_query(LocalMongoDBConnection) +def delete_elections(conn, height): + return conn.run( + conn.collection('elections').delete_many({'height': height}) + ) + + +@register_query(LocalMongoDBConnection) +def get_validator_set(conn, height=None): + query = {} + if height is not None: + query = {'height': {'$lte': height}} + + cursor = conn.run( + conn.collection('validators') + .find(query, projection={'_id': False}) + .sort([('height', DESCENDING)]) + .limit(1) + ) + + return next(cursor, None) + + +@register_query(LocalMongoDBConnection) +def get_election(conn, election_id): + query = {'election_id': election_id} + + return conn.run( + conn.collection('elections') + .find_one(query, projection={'_id': False}, + sort=[('height', DESCENDING)]) + ) + + +@register_query(LocalMongoDBConnection) +def get_asset_tokens_for_public_key(conn, asset_id, public_key): + query = {'outputs.public_keys': [public_key], + 'asset.id': asset_id} + + cursor = conn.run( + conn.collection('transactions').aggregate([ + {'$match': query}, + {'$project': {'_id': False}} + ])) + return cursor + + +@register_query(LocalMongoDBConnection) +def store_abci_chain(conn, height, chain_id, is_synced=True): + return conn.run( + conn.collection('abci_chains').replace_one( + {'height': height}, + {'height': height, 'chain_id': chain_id, + 'is_synced': is_synced}, + upsert=True, + ) + ) + + +@register_query(LocalMongoDBConnection) +def delete_abci_chain(conn, height): + return conn.run( + conn.collection('abci_chains').delete_many({'height': height}) + ) + + +@register_query(LocalMongoDBConnection) +def get_latest_abci_chain(conn): + return conn.run( + conn.collection('abci_chains') + .find_one(projection={'_id': False}, sort=[('height', DESCENDING)]) + ) diff --git a/bigchaindb/backend/localmongodb/schema.py b/bigchaindb/backend/localmongodb/schema.py new file mode 100644 index 0000000..d520521 --- /dev/null +++ b/bigchaindb/backend/localmongodb/schema.py @@ -0,0 +1,90 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Utils to initialize and drop the database.""" + +import logging + +from pymongo import ASCENDING, DESCENDING, TEXT +from pymongo.errors import CollectionInvalid + +from bigchaindb import backend +from bigchaindb.backend.utils import module_dispatch_registrar +from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection + + +logger = logging.getLogger(__name__) +register_schema = module_dispatch_registrar(backend.schema) + + +INDEXES = { + 'transactions': [ + ('id', dict(unique=True, name='transaction_id')), + ('asset.id', dict(name='asset_id')), + ('outputs.public_keys', dict(name='outputs')), + ([('inputs.fulfills.transaction_id', ASCENDING), + ('inputs.fulfills.output_index', ASCENDING)], dict(name='inputs')), + ], + 'assets': [ + ('id', dict(name='asset_id', unique=True)), + ([('$**', TEXT)], dict(name='text')), + ], + 'blocks': [ + ([('height', DESCENDING)], dict(name='height', unique=True)), + ], + 'metadata': [ + ('id', dict(name='transaction_id', unique=True)), + ([('$**', TEXT)], dict(name='text')), + ], + 'utxos': [ + ([('transaction_id', ASCENDING), + ('output_index', ASCENDING)], dict(name='utxo', unique=True)), + ], + 'pre_commit': [ + ('height', dict(name='height', unique=True)), + ], + 'elections': [ + ([('height', DESCENDING), ('election_id', ASCENDING)], + dict(name='election_id_height', unique=True)), + ], + 'validators': [ + ('height', dict(name='height', unique=True)), + ], + 'abci_chains': [ + ('height', dict(name='height', unique=True)), + ('chain_id', dict(name='chain_id', unique=True)), + ], +} + + +@register_schema(LocalMongoDBConnection) +def create_database(conn, dbname): + logger.info('Create database `%s`.', dbname) + # TODO: read and write concerns can be declared here + conn.conn.get_database(dbname) + + +@register_schema(LocalMongoDBConnection) +def create_tables(conn, dbname): + for table_name in backend.schema.TABLES: + # create the table + # TODO: read and write concerns can be declared here + try: + logger.info(f'Create `{table_name}` table.') + conn.conn[dbname].create_collection(table_name) + except CollectionInvalid: + logger.info(f'Collection {table_name} already exists.') + create_indexes(conn, dbname, table_name, INDEXES[table_name]) + + +def create_indexes(conn, dbname, collection, indexes): + logger.info(f'Ensure secondary indexes for `{collection}`.') + for fields, kwargs in indexes: + conn.conn[dbname][collection].create_index(fields, **kwargs) + + +@register_schema(LocalMongoDBConnection) +def drop_database(conn, dbname): + conn.conn.drop_database(dbname) diff --git a/bigchaindb/backend/query.py b/bigchaindb/backend/query.py new file mode 100644 index 0000000..184f99f --- /dev/null +++ b/bigchaindb/backend/query.py @@ -0,0 +1,430 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Query interfaces for backends.""" + +from functools import singledispatch + +from bigchaindb.backend.exceptions import OperationError + + +@singledispatch +def store_asset(connection, asset): + """Write an asset to the asset table. + + Args: + asset (dict): the asset. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def store_assets(connection, assets): + """Write a list of assets to the assets table. + + Args: + assets (list): a list of assets to write. + + Returns: + The database response. + """ + + raise NotImplementedError + + +@singledispatch +def store_metadatas(connection, metadata): + """Write a list of metadata to metadata table. + + Args: + metadata (list): list of metadata. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def store_transactions(connection, signed_transactions): + """Store the list of transactions.""" + + raise NotImplementedError + + +@singledispatch +def get_transaction(connection, transaction_id): + """Get a transaction from the transactions table. + + Args: + transaction_id (str): the id of the transaction. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def get_transactions(connection, transaction_ids): + """Get transactions from the transactions table. + + Args: + transaction_ids (list): list of transaction ids to fetch + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def get_asset(connection, asset_id): + """Get a transaction from the transactions table. + + Args: + asset_id (str): the id of the asset + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def get_spent(connection, transaction_id, condition_id): + """Check if a `txid` was already used as an input. + + A transaction can be used as an input for another transaction. Bigchain + needs to make sure that a given `txid` is only used once. + + Args: + transaction_id (str): The id of the transaction. + condition_id (int): The index of the condition in the respective + transaction. + + Returns: + The transaction that used the `txid` as an input else `None` + """ + + raise NotImplementedError + + +@singledispatch +def get_spending_transactions(connection, inputs): + """Return transactions which spend given inputs + + Args: + inputs (list): list of {txid, output} + + Returns: + Iterator of (block_ids, transaction) for transactions that + spend given inputs. + """ + raise NotImplementedError + + +@singledispatch +def get_owned_ids(connection, owner): + """Retrieve a list of `txids` that can we used has inputs. + + Args: + owner (str): base58 encoded public key. + + Returns: + Iterator of (block_id, transaction) for transactions + that list given owner in conditions. + """ + raise NotImplementedError + + +@singledispatch +def get_block(connection, block_id): + """Get a block from the bigchain table. + + Args: + block_id (str): block id of the block to get + + Returns: + block (dict): the block or `None` + """ + + raise NotImplementedError + + +@singledispatch +def get_block_with_transaction(connection, txid): + """Get a block containing transaction id `txid` + + Args: + txid (str): id of transaction to be searched. + + Returns: + block_id (int): the block id or `None` + """ + + raise NotImplementedError + + +@singledispatch +def get_metadata(connection, transaction_ids): + """Get a list of metadata from the metadata table. + + Args: + transaction_ids (list): a list of ids for the metadata to be retrieved from + the database. + + Returns: + metadata (list): the list of returned metadata. + """ + raise NotImplementedError + + +@singledispatch +def get_assets(connection, asset_ids): + """Get a list of assets from the assets table. + Args: + asset_ids (list): a list of ids for the assets to be retrieved from + the database. + Returns: + assets (list): the list of returned assets. + """ + raise NotImplementedError + + +@singledispatch +def get_txids_filtered(connection, asset_id, operation=None): + """Return all transactions for a particular asset id and optional operation. + + Args: + asset_id (str): ID of transaction that defined the asset + operation (str) (optional): Operation to filter on + """ + + raise NotImplementedError + + +@singledispatch +def text_search(conn, search, *, language='english', case_sensitive=False, + diacritic_sensitive=False, text_score=False, limit=0, table=None): + """Return all the assets that match the text search. + + The results are sorted by text score. + For more information about the behavior of text search on MongoDB see + https://docs.mongodb.com/manual/reference/operator/query/text/#behavior + + Args: + search (str): Text search string to query the text index + language (str, optional): The language for the search and the rules for + stemmer and tokenizer. If the language is ``None`` text search uses + simple tokenization and no stemming. + case_sensitive (bool, optional): Enable or disable case sensitive + search. + diacritic_sensitive (bool, optional): Enable or disable case sensitive + diacritic search. + text_score (bool, optional): If ``True`` returns the text score with + each document. + limit (int, optional): Limit the number of returned documents. + + Returns: + :obj:`list` of :obj:`dict`: a list of assets + + Raises: + OperationError: If the backend does not support text search + """ + + raise OperationError('This query is only supported when running ' + 'Planetmint with MongoDB as the backend.') + + +@singledispatch +def get_latest_block(conn): + """Get the latest commited block i.e. block with largest height""" + + raise NotImplementedError + + +@singledispatch +def store_block(conn, block): + """Write a new block to the `blocks` table + + Args: + block (dict): block with current height and block hash. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def store_unspent_outputs(connection, unspent_outputs): + """Store unspent outputs in ``utxo_set`` table.""" + + raise NotImplementedError + + +@singledispatch +def delete_unspent_outputs(connection, unspent_outputs): + """Delete unspent outputs in ``utxo_set`` table.""" + + raise NotImplementedError + + +@singledispatch +def delete_transactions(conn, txn_ids): + """Delete transactions from database + + Args: + txn_ids (list): list of transaction ids + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def get_unspent_outputs(connection, *, query=None): + """Retrieves unspent outputs. + + Args: + query (dict): An optional parameter to filter the result set. + Defaults to ``None``, which means that all UTXO records + will be returned. + + Returns: + Generator yielding unspent outputs (UTXO set) according to the + given query. + """ + + raise NotImplementedError + + +@singledispatch +def store_pre_commit_state(connection, state): + """Store pre-commit state. + + Args: + state (dict): pre-commit state. + + Returns: + The result of the operation. + """ + + raise NotImplementedError + + +@singledispatch +def get_pre_commit_state(connection): + """Get pre-commit state. + + Returns: + Document representing the pre-commit state. + """ + + raise NotImplementedError + + +@singledispatch +def store_validator_set(conn, validator_update): + """Store updated validator set""" + + raise NotImplementedError + + +@singledispatch +def delete_validator_set(conn, height): + """Delete the validator set at the given height.""" + + raise NotImplementedError + + +@singledispatch +def store_election(conn, election_id, height, is_concluded): + """Store election record""" + + raise NotImplementedError + + +@singledispatch +def store_elections(conn, elections): + """Store election records in bulk""" + + raise NotImplementedError + + +@singledispatch +def delete_elections(conn, height): + """Delete all election records at the given height""" + + raise NotImplementedError + + +@singledispatch +def get_validator_set(conn, height): + """Get validator set for a given `height`, if `height` is not specified + then return the latest validator set + """ + + raise NotImplementedError + + +@singledispatch +def get_election(conn, election_id): + """Return the election record + """ + + raise NotImplementedError + + +@singledispatch +def get_asset_tokens_for_public_key(connection, asset_id, public_key): + """Retrieve a list of tokens of type `asset_id` that are owned by the `public_key`. + Args: + asset_id (str): Id of the token. + public_key (str): base58 encoded public key + Returns: + Iterator of transaction that list given owner in conditions. + """ + raise NotImplementedError + + +@singledispatch +def store_abci_chain(conn, height, chain_id, is_synced=True): + """Create or update an ABCI chain at the given height. + Usually invoked in the beginning of the ABCI communications (height=0) + or when ABCI client (like Tendermint) is migrated (any height). + + Args: + is_synced: True if the chain is known by both ABCI client and server + """ + + raise NotImplementedError + + +@singledispatch +def delete_abci_chain(conn, height): + """Delete the ABCI chain at the given height.""" + + raise NotImplementedError + + +@singledispatch +def get_latest_abci_chain(conn): + """Returns the ABCI chain stored at the biggest height, if any, + None otherwise. + """ + raise NotImplementedError diff --git a/bigchaindb/backend/schema.py b/bigchaindb/backend/schema.py new file mode 100644 index 0000000..3856b1b --- /dev/null +++ b/bigchaindb/backend/schema.py @@ -0,0 +1,128 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Database creation and schema-providing interfaces for backends.""" + +from functools import singledispatch +import logging + +import bigchaindb +from bigchaindb.backend.connection import connect +from bigchaindb.common.exceptions import ValidationError +from bigchaindb.common.utils import validate_all_values_for_key_in_obj, validate_all_values_for_key_in_list + +logger = logging.getLogger(__name__) + +# Tables/collections that every backend database must create +TABLES = ('transactions', 'blocks', 'assets', 'metadata', + 'validators', 'elections', 'pre_commit', 'utxos', 'abci_chains') + +VALID_LANGUAGES = ('danish', 'dutch', 'english', 'finnish', 'french', 'german', + 'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian', + 'russian', 'spanish', 'swedish', 'turkish', 'none', + 'da', 'nl', 'en', 'fi', 'fr', 'de', 'hu', 'it', 'nb', 'pt', + 'ro', 'ru', 'es', 'sv', 'tr') + + +@singledispatch +def create_database(connection, dbname): + """Create database to be used by Planetmint. + + Args: + dbname (str): the name of the database to create. + """ + + raise NotImplementedError + + +@singledispatch +def create_tables(connection, dbname): + """Create the tables to be used by Planetmint. + + Args: + dbname (str): the name of the database to create tables for. + """ + + raise NotImplementedError + + +@singledispatch +def drop_database(connection, dbname): + """Drop the database used by Planetmint. + + Args: + dbname (str): the name of the database to drop. + + Raises: + :exc:`~DatabaseDoesNotExist`: If the given :attr:`dbname` does not + exist as a database. + """ + + raise NotImplementedError + + +def init_database(connection=None, dbname=None): + """Initialize the configured backend for use with Planetmint. + + Creates a database with :attr:`dbname` with any required tables + and supporting indexes. + + Args: + connection (:class:`~bigchaindb.backend.connection.Connection`): an + existing connection to use to initialize the database. + Creates one if not given. + dbname (str): the name of the database to create. + Defaults to the database name given in the Planetmint + configuration. + """ + + connection = connection or connect() + dbname = dbname or bigchaindb.config['database']['name'] + + create_database(connection, dbname) + create_tables(connection, dbname) + + +def validate_language_key(obj, key): + """Validate all nested "language" key in `obj`. + + Args: + obj (dict): dictionary whose "language" key is to be validated. + + Returns: + None: validation successful + + Raises: + ValidationError: will raise exception in case language is not valid. + """ + backend = bigchaindb.config['database']['backend'] + + if backend == 'localmongodb': + data = obj.get(key, {}) + if isinstance(data, dict): + validate_all_values_for_key_in_obj(data, 'language', validate_language) + elif isinstance(data, list): + validate_all_values_for_key_in_list(data, 'language', validate_language) + + +def validate_language(value): + """Check if `value` is a valid language. + https://docs.mongodb.com/manual/reference/text-search-languages/ + + Args: + value (str): language to validated + + Returns: + None: validation successful + + Raises: + ValidationError: will raise exception in case language is not valid. + """ + if value not in VALID_LANGUAGES: + error_str = ('MongoDB does not support text search for the ' + 'language "{}". If you do not understand this error ' + 'message then please rename key/field "language" to ' + 'something else like "lang".').format(value) + raise ValidationError(error_str) diff --git a/bigchaindb/backend/utils.py b/bigchaindb/backend/utils.py new file mode 100644 index 0000000..9bca8ac --- /dev/null +++ b/bigchaindb/backend/utils.py @@ -0,0 +1,39 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import bigchaindb + + +class ModuleDispatchRegistrationError(Exception): + """Raised when there is a problem registering dispatched functions for a + module + """ + + +def module_dispatch_registrar(module): + def dispatch_wrapper(obj_type): + def wrapper(func): + func_name = func.__name__ + try: + dispatch_registrar = getattr(module, func_name) + return dispatch_registrar.register(obj_type)(func) + except AttributeError as ex: + raise ModuleDispatchRegistrationError( + ('`{module}` does not contain a single-dispatchable ' + 'function named `{func}`. The module being registered ' + 'was not implemented correctly!').format( + func=func_name, module=module.__name__)) from ex + + return wrapper + + return dispatch_wrapper + + +def get_bigchaindb_config_value(key, default_value=None): + return bigchaindb.config['database'].get(key, default_value) + + +def get_bigchaindb_config_value_or_key_error(key): + return bigchaindb.config['database'][key] diff --git a/bigchaindb/commands/__init__.py b/bigchaindb/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/commands/bigchaindb.py b/bigchaindb/commands/bigchaindb.py new file mode 100644 index 0000000..8801986 --- /dev/null +++ b/bigchaindb/commands/bigchaindb.py @@ -0,0 +1,398 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Implementation of the `bigchaindb` command, +the command-line interface (CLI) for Planetmint Server. +""" + +import os +import logging +import argparse +import copy +import json +import sys + +from bigchaindb.core import rollback +from bigchaindb.migrations.chain_migration_election import ChainMigrationElection +from bigchaindb.utils import load_node_key +from bigchaindb.common.transaction_mode_types import BROADCAST_TX_COMMIT +from bigchaindb.common.exceptions import (DatabaseDoesNotExist, + ValidationError) +from bigchaindb.elections.vote import Vote +import bigchaindb +from bigchaindb import (backend, ValidatorElection, + BigchainDB) +from bigchaindb.backend import schema +from bigchaindb.commands import utils +from bigchaindb.commands.utils import (configure_bigchaindb, + input_on_stderr) +from bigchaindb.log import setup_logging +from bigchaindb.tendermint_utils import public_key_from_base64 +from bigchaindb.commands.election_types import elections +from bigchaindb.version import __tm_supported_versions__ + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# Note about printing: +# We try to print to stdout for results of a command that may be useful to +# someone (or another program). Strictly informational text, or errors, +# should be printed to stderr. + + +@configure_bigchaindb +def run_show_config(args): + """Show the current configuration""" + # TODO Proposal: remove the "hidden" configuration. Only show config. If + # the system needs to be configured, then display information on how to + # configure the system. + config = copy.deepcopy(bigchaindb.config) + del config['CONFIGURED'] + print(json.dumps(config, indent=4, sort_keys=True)) + + +@configure_bigchaindb +def run_configure(args): + """Run a script to configure the current node.""" + config_path = args.config or bigchaindb.config_utils.CONFIG_DEFAULT_PATH + + config_file_exists = False + # if the config path is `-` then it's stdout + if config_path != '-': + config_file_exists = os.path.exists(config_path) + + if config_file_exists and not args.yes: + want = input_on_stderr('Config file `{}` exists, do you want to ' + 'override it? (cannot be undone) [y/N]: '.format(config_path)) + if want != 'y': + return + + conf = copy.deepcopy(bigchaindb.config) + + # select the correct config defaults based on the backend + print('Generating default configuration for backend {}' + .format(args.backend), file=sys.stderr) + database_keys = bigchaindb._database_keys_map[args.backend] + conf['database'] = bigchaindb._database_map[args.backend] + + if not args.yes: + for key in ('bind', ): + val = conf['server'][key] + conf['server'][key] = input_on_stderr('API Server {}? (default `{}`): '.format(key, val), val) + + for key in ('scheme', 'host', 'port'): + val = conf['wsserver'][key] + conf['wsserver'][key] = input_on_stderr('WebSocket Server {}? (default `{}`): '.format(key, val), val) + + for key in database_keys: + val = conf['database'][key] + conf['database'][key] = input_on_stderr('Database {}? (default `{}`): '.format(key, val), val) + + for key in ('host', 'port'): + val = conf['tendermint'][key] + conf['tendermint'][key] = input_on_stderr('Tendermint {}? (default `{}`)'.format(key, val), val) + + if config_path != '-': + bigchaindb.config_utils.write_config(conf, config_path) + else: + print(json.dumps(conf, indent=4, sort_keys=True)) + print('Configuration written to {}'.format(config_path), file=sys.stderr) + print('Ready to go!', file=sys.stderr) + + +@configure_bigchaindb +def run_election(args): + """Initiate and manage elections""" + + b = BigchainDB() + + # Call the function specified by args.action, as defined above + globals()[f'run_election_{args.action}'](args, b) + + +def run_election_new(args, bigchain): + election_type = args.election_type.replace('-', '_') + globals()[f'run_election_new_{election_type}'](args, bigchain) + + +def create_new_election(sk, bigchain, election_class, data): + try: + key = load_node_key(sk) + voters = election_class.recipients(bigchain) + election = election_class.generate([key.public_key], + voters, + data, None).sign([key.private_key]) + election.validate(bigchain) + except ValidationError as e: + logger.error(e) + return False + except FileNotFoundError as fd_404: + logger.error(fd_404) + return False + + resp = bigchain.write_transaction(election, BROADCAST_TX_COMMIT) + if resp == (202, ''): + logger.info('[SUCCESS] Submitted proposal with id: {}'.format(election.id)) + return election.id + else: + logger.error('Failed to commit election proposal') + return False + + +def run_election_new_upsert_validator(args, bigchain): + """Initiates an election to add/update/remove a validator to an existing Planetmint network + + :param args: dict + args = { + 'public_key': the public key of the proposed peer, (str) + 'power': the proposed validator power for the new peer, (str) + 'node_id': the node_id of the new peer (str) + 'sk': the path to the private key of the node calling the election (str) + } + :param bigchain: an instance of Planetmint + :return: election_id or `False` in case of failure + """ + + new_validator = { + 'public_key': {'value': public_key_from_base64(args.public_key), + 'type': 'ed25519-base16'}, + 'power': args.power, + 'node_id': args.node_id + } + + return create_new_election(args.sk, bigchain, ValidatorElection, new_validator) + + +def run_election_new_chain_migration(args, bigchain): + """Initiates an election to halt block production + + :param args: dict + args = { + 'sk': the path to the private key of the node calling the election (str) + } + :param bigchain: an instance of Planetmint + :return: election_id or `False` in case of failure + """ + + return create_new_election(args.sk, bigchain, ChainMigrationElection, {}) + + +def run_election_approve(args, bigchain): + """Approve an election + + :param args: dict + args = { + 'election_id': the election_id of the election (str) + 'sk': the path to the private key of the signer (str) + } + :param bigchain: an instance of Planetmint + :return: success log message or `False` in case of error + """ + + key = load_node_key(args.sk) + tx = bigchain.get_transaction(args.election_id) + voting_powers = [v.amount for v in tx.outputs if key.public_key in v.public_keys] + if len(voting_powers) > 0: + voting_power = voting_powers[0] + else: + logger.error('The key you provided does not match any of the eligible voters in this election.') + return False + + inputs = [i for i in tx.to_inputs() if key.public_key in i.owners_before] + election_pub_key = ValidatorElection.to_public_key(tx.id) + approval = Vote.generate(inputs, + [([election_pub_key], voting_power)], + tx.id).sign([key.private_key]) + approval.validate(bigchain) + + resp = bigchain.write_transaction(approval, BROADCAST_TX_COMMIT) + + if resp == (202, ''): + logger.info('[SUCCESS] Your vote has been submitted') + return approval.id + else: + logger.error('Failed to commit vote') + return False + + +def run_election_show(args, bigchain): + """Retrieves information about an election + + :param args: dict + args = { + 'election_id': the transaction_id for an election (str) + } + :param bigchain: an instance of Planetmint + """ + + election = bigchain.get_transaction(args.election_id) + if not election: + logger.error(f'No election found with election_id {args.election_id}') + return + + response = election.show_election(bigchain) + + logger.info(response) + + return response + + +def _run_init(): + bdb = bigchaindb.BigchainDB() + + schema.init_database(connection=bdb.connection) + + +@configure_bigchaindb +def run_init(args): + """Initialize the database""" + _run_init() + + +@configure_bigchaindb +def run_drop(args): + """Drop the database""" + dbname = bigchaindb.config['database']['name'] + + if not args.yes: + response = input_on_stderr('Do you want to drop `{}` database? [y/n]: '.format(dbname)) + if response != 'y': + return + + conn = backend.connect() + try: + schema.drop_database(conn, dbname) + except DatabaseDoesNotExist: + print("Cannot drop '{name}'. The database does not exist.".format(name=dbname), file=sys.stderr) + + +def run_recover(b): + rollback(b) + + +@configure_bigchaindb +def run_start(args): + """Start the processes to run the node""" + + # Configure Logging + setup_logging() + + logger.info('Planetmint Version %s', bigchaindb.__version__) + run_recover(bigchaindb.lib.BigchainDB()) + + if not args.skip_initialize_database: + logger.info('Initializing database') + _run_init() + + logger.info('Starting Planetmint main process.') + from bigchaindb.start import start + start(args) + + +def run_tendermint_version(args): + """Show the supported Tendermint version(s)""" + supported_tm_ver = { + 'description': 'Planetmint supports the following Tendermint version(s)', + 'tendermint': __tm_supported_versions__, + } + print(json.dumps(supported_tm_ver, indent=4, sort_keys=True)) + + +def create_parser(): + parser = argparse.ArgumentParser( + description='Control your Planetmint node.', + parents=[utils.base_parser]) + + # all the commands are contained in the subparsers object, + # the command selected by the user will be stored in `args.command` + # that is used by the `main` function to select which other + # function to call. + subparsers = parser.add_subparsers(title='Commands', + dest='command') + + # parser for writing a config file + config_parser = subparsers.add_parser('configure', + help='Prepare the config file.') + + config_parser.add_argument('backend', + choices=['localmongodb'], + default='localmongodb', + const='localmongodb', + nargs='?', + help='The backend to use. It can only be ' + '"localmongodb", currently.') + + # parser for managing elections + election_parser = subparsers.add_parser('election', + help='Manage elections.') + + election_subparser = election_parser.add_subparsers(title='Action', + dest='action') + + new_election_parser = election_subparser.add_parser('new', + help='Calls a new election.') + + new_election_subparser = new_election_parser.add_subparsers(title='Election_Type', + dest='election_type') + + # Parser factory for each type of new election, so we get a bunch of commands that look like this: + # election new ... + for name, data in elections.items(): + args = data['args'] + generic_parser = new_election_subparser.add_parser(name, help=data['help']) + for arg, kwargs in args.items(): + generic_parser.add_argument(arg, **kwargs) + + approve_election_parser = election_subparser.add_parser('approve', + help='Approve the election.') + approve_election_parser.add_argument('election_id', + help='The election_id of the election.') + approve_election_parser.add_argument('--private-key', + dest='sk', + required=True, + help='Path to the private key of the election initiator.') + + show_election_parser = election_subparser.add_parser('show', + help='Provides information about an election.') + + show_election_parser.add_argument('election_id', + help='The transaction id of the election you wish to query.') + + # parsers for showing/exporting config values + subparsers.add_parser('show-config', + help='Show the current configuration') + + # parser for database-level commands + subparsers.add_parser('init', + help='Init the database') + + subparsers.add_parser('drop', + help='Drop the database') + + # parser for starting Planetmint + start_parser = subparsers.add_parser('start', + help='Start Planetmint') + + start_parser.add_argument('--no-init', + dest='skip_initialize_database', + default=False, + action='store_true', + help='Skip database initialization') + + subparsers.add_parser('tendermint-version', + help='Show the Tendermint supported versions') + + start_parser.add_argument('--experimental-parallel-validation', + dest='experimental_parallel_validation', + default=False, + action='store_true', + help='💀 EXPERIMENTAL: parallelize validation for better throughput 💀') + + return parser + + +def main(): + utils.start(create_parser(), sys.argv[1:], globals()) diff --git a/bigchaindb/commands/election_types.py b/bigchaindb/commands/election_types.py new file mode 100644 index 0000000..cfa58b9 --- /dev/null +++ b/bigchaindb/commands/election_types.py @@ -0,0 +1,31 @@ +elections = { + 'upsert-validator': { + 'help': 'Propose a change to the validator set', + 'args': { + 'public_key': { + 'help': 'Public key of the validator to be added/updated/removed.' + }, + 'power': { + 'type': int, + 'help': 'The proposed power for the validator. Setting to 0 will remove the validator.'}, + 'node_id': { + 'help': 'The node_id of the validator.' + }, + '--private-key': { + 'dest': 'sk', + 'required': True, + 'help': 'Path to the private key of the election initiator.' + } + } + }, + 'chain-migration': { + 'help': 'Call for a halt to block production to allow for a version change across breaking changes.', + 'args': { + '--private-key': { + 'dest': 'sk', + 'required': True, + 'help': 'Path to the private key of the election initiator.' + } + } + } +} diff --git a/bigchaindb/commands/utils.py b/bigchaindb/commands/utils.py new file mode 100644 index 0000000..6adb56a --- /dev/null +++ b/bigchaindb/commands/utils.py @@ -0,0 +1,161 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Utility functions and basic common arguments +for ``argparse.ArgumentParser``. +""" + +import argparse +import builtins +import functools +import multiprocessing as mp +import sys + +import bigchaindb +import bigchaindb.config_utils +from bigchaindb.version import __version__ + + +def configure_bigchaindb(command): + """Decorator to be used by command line functions, such that the + configuration of bigchaindb is performed before the execution of + the command. + + Args: + command: The command to decorate. + + Returns: + The command wrapper function. + + """ + @functools.wraps(command) + def configure(args): + config_from_cmdline = None + try: + if args.log_level is not None: + config_from_cmdline = { + 'log': { + 'level_console': args.log_level, + 'level_logfile': args.log_level, + }, + 'server': {'loglevel': args.log_level}, + } + except AttributeError: + pass + bigchaindb.config_utils.autoconfigure( + filename=args.config, config=config_from_cmdline, force=True) + command(args) + + return configure + + +def _convert(value, default=None, convert=None): + def convert_bool(value): + if value.lower() in ('true', 't', 'yes', 'y'): + return True + if value.lower() in ('false', 'f', 'no', 'n'): + return False + raise ValueError('{} cannot be converted to bool'.format(value)) + + if value == '': + value = None + + if convert is None: + if default is not None: + convert = type(default) + else: + convert = str + + if convert == bool: + convert = convert_bool + + if value is None: + return default + else: + return convert(value) + + +# We need this because `input` always prints on stdout, while it should print +# to stderr. It's a very old bug, check it out here: +# - https://bugs.python.org/issue1927 +def input_on_stderr(prompt='', default=None, convert=None): + """Output a string to stderr and wait for input. + + Args: + prompt (str): the message to display. + default: the default value to return if the user + leaves the field empty + convert (callable): a callable to be used to convert + the value the user inserted. If None, the type of + ``default`` will be used. + """ + + print(prompt, end='', file=sys.stderr) + value = builtins.input() + return _convert(value, default, convert) + + +def start(parser, argv, scope): + """Utility function to execute a subcommand. + + The function will look up in the ``scope`` + if there is a function called ``run_`` + and will run it using ``parser.args`` as first positional argument. + + Args: + parser: an ArgumentParser instance. + argv: the list of command line arguments without the script name. + scope (dict): map containing (eventually) the functions to be called. + + Raises: + NotImplementedError: if ``scope`` doesn't contain a function called + ``run_``. + """ + args = parser.parse_args(argv) + + if not args.command: + parser.print_help() + raise SystemExit() + + # look up in the current scope for a function called 'run_' + # replacing all the dashes '-' with the lowercase character '_' + func = scope.get('run_' + args.command.replace('-', '_')) + + # if no command has been found, raise a `NotImplementedError` + if not func: + raise NotImplementedError('Command `{}` not yet implemented'. + format(args.command)) + + args.multiprocess = getattr(args, 'multiprocess', False) + + if args.multiprocess is False: + args.multiprocess = 1 + elif args.multiprocess is None: + args.multiprocess = mp.cpu_count() + + return func(args) + + +base_parser = argparse.ArgumentParser(add_help=False, prog='bigchaindb') + +base_parser.add_argument('-c', '--config', + help='Specify the location of the configuration file ' + '(use "-" for stdout)') + +# NOTE: this flag should not have any default value because that will override +# the environment variables provided to configure the logger. +base_parser.add_argument('-l', '--log-level', + type=str.upper, # convert to uppercase for comparison to choices + choices=['DEBUG', 'BENCHMARK', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], + help='Log level') + +base_parser.add_argument('-y', '--yes', '--yes-please', + action='store_true', + help='Assume "yes" as answer to all prompts and run ' + 'non-interactively') + +base_parser.add_argument('-v', '--version', + action='version', + version='%(prog)s {}'.format(__version__)) diff --git a/bigchaindb/common/__init__.py b/bigchaindb/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/common/crypto.py b/bigchaindb/common/crypto.py new file mode 100644 index 0000000..2bf6940 --- /dev/null +++ b/bigchaindb/common/crypto.py @@ -0,0 +1,55 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +# Separate all crypto code so that we can easily test several implementations +from collections import namedtuple + +try: + from hashlib import sha3_256 +except ImportError: + from sha3 import sha3_256 + +from cryptoconditions import crypto + + +CryptoKeypair = namedtuple('CryptoKeypair', ('private_key', 'public_key')) + + +def hash_data(data): + """Hash the provided data using SHA3-256""" + return sha3_256(data.encode()).hexdigest() + + +def generate_key_pair(): + """Generates a cryptographic key pair. + + Returns: + :class:`~bigchaindb.common.crypto.CryptoKeypair`: A + :obj:`collections.namedtuple` with named fields + :attr:`~bigchaindb.common.crypto.CryptoKeypair.private_key` and + :attr:`~bigchaindb.common.crypto.CryptoKeypair.public_key`. + + """ + # TODO FOR CC: Adjust interface so that this function becomes unnecessary + return CryptoKeypair( + *(k.decode() for k in crypto.ed25519_generate_key_pair())) + + +PrivateKey = crypto.Ed25519SigningKey +PublicKey = crypto.Ed25519VerifyingKey + + +def key_pair_from_ed25519_key(hex_private_key): + """Generate base58 encode public-private key pair from a hex encoded private key""" + priv_key = crypto.Ed25519SigningKey(bytes.fromhex(hex_private_key)[:32], encoding='bytes') + public_key = priv_key.get_verifying_key() + return CryptoKeypair(private_key=priv_key.encode(encoding='base58').decode('utf-8'), + public_key=public_key.encode(encoding='base58').decode('utf-8')) + + +def public_key_from_ed25519_key(hex_public_key): + """Generate base58 public key from hex encoded public key""" + public_key = crypto.Ed25519VerifyingKey(bytes.fromhex(hex_public_key), encoding='bytes') + return public_key.encode(encoding='base58').decode('utf-8') diff --git a/bigchaindb/common/exceptions.py b/bigchaindb/common/exceptions.py new file mode 100644 index 0000000..750da36 --- /dev/null +++ b/bigchaindb/common/exceptions.py @@ -0,0 +1,115 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Custom exceptions used in the `bigchaindb` package. +""" +from bigchaindb.exceptions import BigchainDBError + + +class ConfigurationError(BigchainDBError): + """Raised when there is a problem with server configuration""" + + +class DatabaseDoesNotExist(BigchainDBError): + """Raised when trying to delete the database but the db is not there""" + + +class StartupError(BigchainDBError): + """Raised when there is an error starting up the system""" + + +class CyclicBlockchainError(BigchainDBError): + """Raised when there is a cycle in the blockchain""" + + +class KeypairMismatchException(BigchainDBError): + """Raised if the private key(s) provided for signing don't match any of the + current owner(s) + """ + + +class OperationError(BigchainDBError): + """Raised when an operation cannot go through""" + + +################################################################################ +# Validation errors +# +# All validation errors (which are handleable errors, not faults) should +# subclass ValidationError. However, where possible they should also have their +# own distinct type to differentiate them from other validation errors, +# especially for the purposes of testing. + + +class ValidationError(BigchainDBError): + """Raised if there was an error in validation""" + + +class DoubleSpend(ValidationError): + """Raised if a double spend is found""" + + +class InvalidHash(ValidationError): + """Raised if there was an error checking the hash for a particular + operation + """ + + +class SchemaValidationError(ValidationError): + """Raised if there was any error validating an object's schema""" + + +class InvalidSignature(ValidationError): + """Raised if there was an error checking the signature for a particular + operation + """ + + +class AssetIdMismatch(ValidationError): + """Raised when multiple transaction inputs related to different assets""" + + +class AmountError(ValidationError): + """Raised when there is a problem with a transaction's output amounts""" + + +class InputDoesNotExist(ValidationError): + """Raised if a transaction input does not exist""" + + +class TransactionOwnerError(ValidationError): + """Raised if a user tries to transfer a transaction they don't own""" + + +class DuplicateTransaction(ValidationError): + """Raised if a duplicated transaction is found""" + + +class ThresholdTooDeep(ValidationError): + """Raised if threshold condition is too deep""" + + +class MultipleValidatorOperationError(ValidationError): + """Raised when a validator update pending but new request is submited""" + + +class MultipleInputsError(ValidationError): + """Raised if there were multiple inputs when only one was expected""" + + +class InvalidProposer(ValidationError): + """Raised if the public key is not a part of the validator set""" + + +class UnequalValidatorSet(ValidationError): + """Raised if the validator sets differ""" + + +class InvalidPowerChange(ValidationError): + """Raised if proposed power change in validator set is >=1/3 total power""" + + +class InvalidPublicKey(ValidationError): + """Raised if public key doesn't match the encoding type""" diff --git a/bigchaindb/common/memoize.py b/bigchaindb/common/memoize.py new file mode 100644 index 0000000..b814e51 --- /dev/null +++ b/bigchaindb/common/memoize.py @@ -0,0 +1,58 @@ +import functools +import codecs +from functools import lru_cache + + +class HDict(dict): + def __hash__(self): + return hash(codecs.decode(self['id'], 'hex')) + + +@lru_cache(maxsize=16384) +def from_dict(func, *args, **kwargs): + return func(*args, **kwargs) + + +def memoize_from_dict(func): + + @functools.wraps(func) + def memoized_func(*args, **kwargs): + + if args[1].get('id', None): + args = list(args) + args[1] = HDict(args[1]) + new_args = tuple(args) + return from_dict(func, *new_args, **kwargs) + else: + return func(*args, **kwargs) + + return memoized_func + + +class ToDictWrapper(): + def __init__(self, tx): + self.tx = tx + + def __eq__(self, other): + return self.tx.id == other.tx.id + + def __hash__(self): + return hash(self.tx.id) + + +@lru_cache(maxsize=16384) +def to_dict(func, tx_wrapped): + return func(tx_wrapped.tx) + + +def memoize_to_dict(func): + + @functools.wraps(func) + def memoized_func(*args, **kwargs): + + if args[0].id: + return to_dict(func, ToDictWrapper(args[0])) + else: + return func(*args, **kwargs) + + return memoized_func diff --git a/bigchaindb/common/schema/README.md b/bigchaindb/common/schema/README.md new file mode 100644 index 0000000..b06e438 --- /dev/null +++ b/bigchaindb/common/schema/README.md @@ -0,0 +1,54 @@ + + +# Introduction + +This directory contains the schemas for the different JSON documents Planetmint uses. + +The aim is to provide: + +- a strict definition of the data structures used in Planetmint, +- a language-independent tool to validate the structure of incoming/outcoming + data. (There are several ready to use + [implementations](http://json-schema.org/implementations.html) written in + different languages.) + +## Sources + +The files defining the JSON Schema for transactions (`transaction_*.yaml`) +are based on the [Planetmint Transactions Specs](https://github.com/bigchaindb/BEPs/tree/master/tx-specs). +If you want to add a new transaction version, +you must write a spec for it first. +(You can't change the JSON Schema files for old versions. +Those were used to validate old transactions +and are needed to re-check those transactions.) + +There used to be a file defining the JSON Schema for votes, named `vote.yaml`. +It was used by Planetmint version 1.3.0 and earlier. +If you want a copy of the latest `vote.yaml` file, +then you can get it from the version 1.3.0 release on GitHub, at +[https://github.com/bigchaindb/bigchaindb/blob/v1.3.0/bigchaindb/common/schema/vote.yaml](https://github.com/bigchaindb/bigchaindb/blob/v1.3.0/bigchaindb/common/schema/vote.yaml). + +## Learn about JSON Schema + +A good resource is [Understanding JSON Schema](http://spacetelescope.github.io/understanding-json-schema/index.html). +It provides a *more accessible documentation for JSON schema* than the [specs](http://json-schema.org/documentation.html). + +## If it's supposed to be JSON, why's everything in YAML D:? + +YAML is great for its conciseness and friendliness towards human-editing in comparision to JSON. + +Although YAML is a superset of JSON, at the end of the day, JSON Schema processors, like +[json-schema](http://python-jsonschema.readthedocs.io/en/latest/), take in a native object (e.g. +Python dicts or JavaScript objects) as the schema used for validation. As long as we can serialize +the YAML into what the JSON Schema processor expects (almost always as simple as loading the YAML +like you would with a JSON file), it's the same as using JSON. + +Specific advantages of using YAML: + - Legibility, especially when nesting + - Multi-line string literals, that make it easy to include descriptions that can be [auto-generated + into Sphinx documentation](/docs/server/generate_schema_documentation.py) diff --git a/bigchaindb/common/schema/__init__.py b/bigchaindb/common/schema/__init__.py new file mode 100644 index 0000000..8343fa8 --- /dev/null +++ b/bigchaindb/common/schema/__init__.py @@ -0,0 +1,82 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Schema validation related functions and data""" +import os.path +import logging + +import jsonschema +import yaml +import rapidjson + +from bigchaindb.common.exceptions import SchemaValidationError + + +logger = logging.getLogger(__name__) + + +def _load_schema(name, path=__file__): + """Load a schema from disk""" + path = os.path.join(os.path.dirname(path), name + '.yaml') + with open(path) as handle: + schema = yaml.safe_load(handle) + fast_schema = rapidjson.Validator(rapidjson.dumps(schema)) + return path, (schema, fast_schema) + + +TX_SCHEMA_VERSION = 'v2.0' + +TX_SCHEMA_PATH, TX_SCHEMA_COMMON = _load_schema('transaction_' + + TX_SCHEMA_VERSION) +_, TX_SCHEMA_CREATE = _load_schema('transaction_create_' + + TX_SCHEMA_VERSION) +_, TX_SCHEMA_TRANSFER = _load_schema('transaction_transfer_' + + TX_SCHEMA_VERSION) + +_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' + + TX_SCHEMA_VERSION) + +_, TX_SCHEMA_CHAIN_MIGRATION_ELECTION = _load_schema('transaction_chain_migration_election_' + + TX_SCHEMA_VERSION) + +_, TX_SCHEMA_VOTE = _load_schema('transaction_vote_' + TX_SCHEMA_VERSION) + + +def _validate_schema(schema, body): + """Validate data against a schema""" + + # Note + # + # Schema validation is currently the major CPU bottleneck of + # Planetmint. the `jsonschema` library validates python data structures + # directly and produces nice error messages, but validation takes 4+ ms + # per transaction which is pretty slow. The rapidjson library validates + # much faster at 1.5ms, however it produces _very_ poor error messages. + # For this reason we use both, rapidjson as an optimistic pathway and + # jsonschema as a fallback in case there is a failure, so we can produce + # a helpful error message. + + try: + schema[1](rapidjson.dumps(body)) + except ValueError as exc: + try: + jsonschema.validate(body, schema[0]) + except jsonschema.ValidationError as exc2: + raise SchemaValidationError(str(exc2)) from exc2 + logger.warning('code problem: jsonschema did not raise an exception, wheras rapidjson raised %s', exc) + raise SchemaValidationError(str(exc)) from exc + + +def validate_transaction_schema(tx): + """Validate a transaction dict. + + TX_SCHEMA_COMMON contains properties that are common to all types of + transaction. TX_SCHEMA_[TRANSFER|CREATE] add additional constraints on top. + """ + _validate_schema(TX_SCHEMA_COMMON, tx) + if tx['operation'] == 'TRANSFER': + _validate_schema(TX_SCHEMA_TRANSFER, tx) + else: + _validate_schema(TX_SCHEMA_CREATE, tx) diff --git a/bigchaindb/common/schema/transaction_chain_migration_election_v2.0.yaml b/bigchaindb/common/schema/transaction_chain_migration_election_v2.0.yaml new file mode 100644 index 0000000..d5c5f4a --- /dev/null +++ b/bigchaindb/common/schema/transaction_chain_migration_election_v2.0.yaml @@ -0,0 +1,45 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Chain Migration Election Schema - Propose a halt in block production to allow for a version change +required: +- operation +- asset +- outputs +properties: + operation: + type: string + value: "CHAIN_MIGRATION_ELECTION" + asset: + additionalProperties: false + properties: + data: + additionalProperties: false + properties: + seed: + type: string + required: + - data + outputs: + type: array + items: + "$ref": "#/definitions/output" +definitions: + output: + type: object + properties: + condition: + type: object + required: + - uri + properties: + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" diff --git a/bigchaindb/common/schema/transaction_create_v1.0.yaml b/bigchaindb/common/schema/transaction_create_v1.0.yaml new file mode 100644 index 0000000..d43b543 --- /dev/null +++ b/bigchaindb/common/schema/transaction_create_v1.0.yaml @@ -0,0 +1,35 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Transaction Schema - CREATE/GENESIS specific constraints +required: +- asset +- inputs +properties: + asset: + additionalProperties: false + properties: + data: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + required: + - data + inputs: + type: array + title: "Transaction inputs" + maxItems: 1 + minItems: 1 + items: + type: "object" + required: + - fulfills + properties: + fulfills: + type: "null" diff --git a/bigchaindb/common/schema/transaction_create_v2.0.yaml b/bigchaindb/common/schema/transaction_create_v2.0.yaml new file mode 100644 index 0000000..d3c7ea2 --- /dev/null +++ b/bigchaindb/common/schema/transaction_create_v2.0.yaml @@ -0,0 +1,35 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Transaction Schema - CREATE specific constraints +required: +- asset +- inputs +properties: + asset: + additionalProperties: false + properties: + data: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + required: + - data + inputs: + type: array + title: "Transaction inputs" + maxItems: 1 + minItems: 1 + items: + type: "object" + required: + - fulfills + properties: + fulfills: + type: "null" diff --git a/bigchaindb/common/schema/transaction_transfer_v1.0.yaml b/bigchaindb/common/schema/transaction_transfer_v1.0.yaml new file mode 100644 index 0000000..0ac4023 --- /dev/null +++ b/bigchaindb/common/schema/transaction_transfer_v1.0.yaml @@ -0,0 +1,34 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Transaction Schema - TRANSFER specific properties +required: +- asset +properties: + asset: + additionalProperties: false + properties: + id: + "$ref": "#/definitions/sha3_hexdigest" + required: + - id + inputs: + type: array + title: "Transaction inputs" + minItems: 1 + items: + type: "object" + required: + - fulfills + properties: + fulfills: + type: "object" +definitions: + sha3_hexdigest: + pattern: "[0-9a-f]{64}" + type: string diff --git a/bigchaindb/common/schema/transaction_transfer_v2.0.yaml b/bigchaindb/common/schema/transaction_transfer_v2.0.yaml new file mode 100644 index 0000000..0ac4023 --- /dev/null +++ b/bigchaindb/common/schema/transaction_transfer_v2.0.yaml @@ -0,0 +1,34 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Transaction Schema - TRANSFER specific properties +required: +- asset +properties: + asset: + additionalProperties: false + properties: + id: + "$ref": "#/definitions/sha3_hexdigest" + required: + - id + inputs: + type: array + title: "Transaction inputs" + minItems: 1 + items: + type: "object" + required: + - fulfills + properties: + fulfills: + type: "object" +definitions: + sha3_hexdigest: + pattern: "[0-9a-f]{64}" + type: string diff --git a/bigchaindb/common/schema/transaction_v1.0.yaml b/bigchaindb/common/schema/transaction_v1.0.yaml new file mode 100644 index 0000000..3546d78 --- /dev/null +++ b/bigchaindb/common/schema/transaction_v1.0.yaml @@ -0,0 +1,168 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +additionalProperties: false +title: Transaction Schema +required: +- id +- inputs +- outputs +- operation +- metadata +- asset +- version +properties: + id: + anyOf: + - "$ref": "#/definitions/sha3_hexdigest" + - type: 'null' + operation: + "$ref": "#/definitions/operation" + asset: + "$ref": "#/definitions/asset" + inputs: + type: array + title: "Transaction inputs" + items: + "$ref": "#/definitions/input" + outputs: + type: array + items: + "$ref": "#/definitions/output" + metadata: + "$ref": "#/definitions/metadata" + version: + type: string + pattern: "^1\\.0$" +definitions: + offset: + type: integer + minimum: 0 + base58: + pattern: "[1-9a-zA-Z^OIl]{43,44}" + type: string + public_keys: + anyOf: + - type: array + items: + "$ref": "#/definitions/base58" + - type: 'null' + sha3_hexdigest: + pattern: "[0-9a-f]{64}" + type: string + uuid4: + pattern: "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}" + type: string + operation: + type: string + enum: + - CREATE + - TRANSFER + - GENESIS + asset: + type: object + additionalProperties: false + properties: + id: + "$ref": "#/definitions/sha3_hexdigest" + data: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + output: + type: object + additionalProperties: false + required: + - amount + - condition + - public_keys + properties: + amount: + type: string + pattern: "^[0-9]{1,20}$" + condition: + type: object + additionalProperties: false + required: + - details + - uri + properties: + details: + "$ref": "#/definitions/condition_details" + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=(ed25519|threshold)-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" + public_keys: + "$ref": "#/definitions/public_keys" + input: + type: "object" + additionalProperties: false + required: + - owners_before + - fulfillment + properties: + owners_before: + "$ref": "#/definitions/public_keys" + fulfillment: + anyOf: + - type: string + pattern: "^[a-zA-Z0-9_-]*$" + - "$ref": "#/definitions/condition_details" + fulfills: + anyOf: + - type: 'object' + additionalProperties: false + required: + - output_index + - transaction_id + properties: + output_index: + "$ref": "#/definitions/offset" + transaction_id: + "$ref": "#/definitions/sha3_hexdigest" + - type: 'null' + metadata: + anyOf: + - type: object + additionalProperties: true + minProperties: 1 + - type: 'null' + condition_details: + anyOf: + - type: object + additionalProperties: false + required: + - type + - public_key + properties: + type: + type: string + pattern: "^ed25519-sha-256$" + public_key: + "$ref": "#/definitions/base58" + - type: object + additionalProperties: false + required: + - type + - threshold + - subconditions + properties: + type: + type: "string" + pattern: "^threshold-sha-256$" + threshold: + type: integer + minimum: 1 + maximum: 100 + subconditions: + type: array + items: + "$ref": "#/definitions/condition_details" diff --git a/bigchaindb/common/schema/transaction_v2.0.yaml b/bigchaindb/common/schema/transaction_v2.0.yaml new file mode 100644 index 0000000..604302f --- /dev/null +++ b/bigchaindb/common/schema/transaction_v2.0.yaml @@ -0,0 +1,170 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +additionalProperties: false +title: Transaction Schema +required: +- id +- inputs +- outputs +- operation +- metadata +- asset +- version +properties: + id: + anyOf: + - "$ref": "#/definitions/sha3_hexdigest" + - type: 'null' + operation: + "$ref": "#/definitions/operation" + asset: + "$ref": "#/definitions/asset" + inputs: + type: array + title: "Transaction inputs" + items: + "$ref": "#/definitions/input" + outputs: + type: array + items: + "$ref": "#/definitions/output" + metadata: + "$ref": "#/definitions/metadata" + version: + type: string + pattern: "^2\\.0$" +definitions: + offset: + type: integer + minimum: 0 + base58: + pattern: "[1-9a-zA-Z^OIl]{43,44}" + type: string + public_keys: + anyOf: + - type: array + items: + "$ref": "#/definitions/base58" + - type: 'null' + sha3_hexdigest: + pattern: "[0-9a-f]{64}" + type: string + uuid4: + pattern: "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}" + type: string + operation: + type: string + enum: + - CREATE + - TRANSFER + - VALIDATOR_ELECTION + - CHAIN_MIGRATION_ELECTION + - VOTE + asset: + type: object + additionalProperties: false + properties: + id: + "$ref": "#/definitions/sha3_hexdigest" + data: + anyOf: + - type: object + additionalProperties: true + - type: 'null' + output: + type: object + additionalProperties: false + required: + - amount + - condition + - public_keys + properties: + amount: + type: string + pattern: "^[0-9]{1,20}$" + condition: + type: object + additionalProperties: false + required: + - details + - uri + properties: + details: + "$ref": "#/definitions/condition_details" + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=(ed25519|threshold)-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" + public_keys: + "$ref": "#/definitions/public_keys" + input: + type: "object" + additionalProperties: false + required: + - owners_before + - fulfillment + properties: + owners_before: + "$ref": "#/definitions/public_keys" + fulfillment: + anyOf: + - type: string + pattern: "^[a-zA-Z0-9_-]*$" + - "$ref": "#/definitions/condition_details" + fulfills: + anyOf: + - type: 'object' + additionalProperties: false + required: + - output_index + - transaction_id + properties: + output_index: + "$ref": "#/definitions/offset" + transaction_id: + "$ref": "#/definitions/sha3_hexdigest" + - type: 'null' + metadata: + anyOf: + - type: object + additionalProperties: true + minProperties: 1 + - type: 'null' + condition_details: + anyOf: + - type: object + additionalProperties: false + required: + - type + - public_key + properties: + type: + type: string + pattern: "^ed25519-sha-256$" + public_key: + "$ref": "#/definitions/base58" + - type: object + additionalProperties: false + required: + - type + - threshold + - subconditions + properties: + type: + type: "string" + pattern: "^threshold-sha-256$" + threshold: + type: integer + minimum: 1 + maximum: 100 + subconditions: + type: array + items: + "$ref": "#/definitions/condition_details" diff --git a/bigchaindb/common/schema/transaction_validator_election_v2.0.yaml b/bigchaindb/common/schema/transaction_validator_election_v2.0.yaml new file mode 100644 index 0000000..f93353c --- /dev/null +++ b/bigchaindb/common/schema/transaction_validator_election_v2.0.yaml @@ -0,0 +1,68 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Validator Election Schema - Propose a change to validator set +required: +- operation +- asset +- outputs +properties: + operation: + type: string + value: "VALIDATOR_ELECTION" + asset: + additionalProperties: false + properties: + data: + additionalProperties: false + properties: + node_id: + type: string + seed: + type: string + public_key: + type: object + additionalProperties: false + required: + - value + - type + properties: + value: + type: string + type: + type: string + enum: + - ed25519-base16 + - ed25519-base32 + - ed25519-base64 + power: + "$ref": "#/definitions/positiveInteger" + required: + - node_id + - public_key + - power + required: + - data + outputs: + type: array + items: + "$ref": "#/definitions/output" +definitions: + output: + type: object + properties: + condition: + type: object + required: + - uri + properties: + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" diff --git a/bigchaindb/common/schema/transaction_vote_v2.0.yaml b/bigchaindb/common/schema/transaction_vote_v2.0.yaml new file mode 100644 index 0000000..64ed6ee --- /dev/null +++ b/bigchaindb/common/schema/transaction_vote_v2.0.yaml @@ -0,0 +1,34 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Vote Schema - Vote on an election +required: +- operation +- outputs +properties: + operation: + type: string + value: "VOTE" + outputs: + type: array + items: + "$ref": "#/definitions/output" +definitions: + output: + type: object + properties: + condition: + type: object + required: + - uri + properties: + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py new file mode 100644 index 0000000..432757c --- /dev/null +++ b/bigchaindb/common/transaction.py @@ -0,0 +1,1330 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Transaction related models to parse and construct transaction +payloads. + +Attributes: + UnspentOutput (namedtuple): Object holding the information + representing an unspent output. + +""" +from collections import namedtuple +from copy import deepcopy +from functools import reduce, lru_cache +import rapidjson + +import base58 +from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256 +from cryptoconditions.exceptions import ( + ParsingError, ASN1DecodeError, ASN1EncodeError, UnsupportedTypeError) +try: + from hashlib import sha3_256 +except ImportError: + from sha3 import sha3_256 + +from bigchaindb.common.crypto import PrivateKey, hash_data +from bigchaindb.common.exceptions import (KeypairMismatchException, + InputDoesNotExist, DoubleSpend, + InvalidHash, InvalidSignature, + AmountError, AssetIdMismatch, + ThresholdTooDeep) +from bigchaindb.common.utils import serialize +from .memoize import memoize_from_dict, memoize_to_dict + + +UnspentOutput = namedtuple( + 'UnspentOutput', ( + # TODO 'utxo_hash': sha3_256(f'{txid}{output_index}'.encode()) + # 'utxo_hash', # noqa + 'transaction_id', + 'output_index', + 'amount', + 'asset_id', + 'condition_uri', + ) +) + + +class Input(object): + """A Input is used to spend assets locked by an Output. + + Wraps around a Crypto-condition Fulfillment. + + Attributes: + fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment + to be signed with a private key. + owners_before (:obj:`list` of :obj:`str`): A list of owners after a + Transaction was confirmed. + fulfills (:class:`~bigchaindb.common.transaction. TransactionLink`, + optional): A link representing the input of a `TRANSFER` + Transaction. + """ + + def __init__(self, fulfillment, owners_before, fulfills=None): + """Create an instance of an :class:`~.Input`. + + Args: + fulfillment (:class:`cryptoconditions.Fulfillment`): A + Fulfillment to be signed with a private key. + owners_before (:obj:`list` of :obj:`str`): A list of owners + after a Transaction was confirmed. + fulfills (:class:`~bigchaindb.common.transaction. + TransactionLink`, optional): A link representing the input + of a `TRANSFER` Transaction. + """ + if fulfills is not None and not isinstance(fulfills, TransactionLink): + raise TypeError('`fulfills` must be a TransactionLink instance') + if not isinstance(owners_before, list): + raise TypeError('`owners_before` must be a list instance') + + self.fulfillment = fulfillment + self.fulfills = fulfills + self.owners_before = owners_before + + def __eq__(self, other): + # TODO: If `other !== Fulfillment` return `False` + return self.to_dict() == other.to_dict() + + # NOTE: This function is used to provide a unique key for a given + # Input to suppliment memoization + def __hash__(self): + return hash((self.fulfillment, self.fulfills)) + + def to_dict(self): + """Transforms the object to a Python dictionary. + + Note: + If an Input hasn't been signed yet, this method returns a + dictionary representation. + + Returns: + dict: The Input as an alternative serialization format. + """ + try: + fulfillment = self.fulfillment.serialize_uri() + except (TypeError, AttributeError, ASN1EncodeError, ASN1DecodeError): + fulfillment = _fulfillment_to_details(self.fulfillment) + + try: + # NOTE: `self.fulfills` can be `None` and that's fine + fulfills = self.fulfills.to_dict() + except AttributeError: + fulfills = None + + input_ = { + 'owners_before': self.owners_before, + 'fulfills': fulfills, + 'fulfillment': fulfillment, + } + return input_ + + @classmethod + def generate(cls, public_keys): + # TODO: write docstring + # The amount here does not really matter. It is only use on the + # output data model but here we only care about the fulfillment + output = Output.generate(public_keys, 1) + return cls(output.fulfillment, public_keys) + + @classmethod + def from_dict(cls, data): + """Transforms a Python dictionary to an Input object. + + Note: + Optionally, this method can also serialize a Cryptoconditions- + Fulfillment that is not yet signed. + + Args: + data (dict): The Input to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.Input` + + Raises: + InvalidSignature: If an Input's URI couldn't be parsed. + """ + fulfillment = data['fulfillment'] + if not isinstance(fulfillment, (Fulfillment, type(None))): + try: + fulfillment = Fulfillment.from_uri(data['fulfillment']) + except ASN1DecodeError: + # TODO Remove as it is legacy code, and simply fall back on + # ASN1DecodeError + raise InvalidSignature("Fulfillment URI couldn't been parsed") + except TypeError: + # NOTE: See comment about this special case in + # `Input.to_dict` + fulfillment = _fulfillment_from_details(data['fulfillment']) + fulfills = TransactionLink.from_dict(data['fulfills']) + return cls(fulfillment, data['owners_before'], fulfills) + + +def _fulfillment_to_details(fulfillment): + """Encode a fulfillment as a details dictionary + + Args: + fulfillment: Crypto-conditions Fulfillment object + """ + + if fulfillment.type_name == 'ed25519-sha-256': + return { + 'type': 'ed25519-sha-256', + 'public_key': base58.b58encode(fulfillment.public_key).decode(), + } + + if fulfillment.type_name == 'threshold-sha-256': + subconditions = [ + _fulfillment_to_details(cond['body']) + for cond in fulfillment.subconditions + ] + return { + 'type': 'threshold-sha-256', + 'threshold': fulfillment.threshold, + 'subconditions': subconditions, + } + + raise UnsupportedTypeError(fulfillment.type_name) + + +def _fulfillment_from_details(data, _depth=0): + """Load a fulfillment for a signing spec dictionary + + Args: + data: tx.output[].condition.details dictionary + """ + if _depth == 100: + raise ThresholdTooDeep() + + if data['type'] == 'ed25519-sha-256': + public_key = base58.b58decode(data['public_key']) + return Ed25519Sha256(public_key=public_key) + + if data['type'] == 'threshold-sha-256': + threshold = ThresholdSha256(data['threshold']) + for cond in data['subconditions']: + cond = _fulfillment_from_details(cond, _depth+1) + threshold.add_subfulfillment(cond) + return threshold + + raise UnsupportedTypeError(data.get('type')) + + +class TransactionLink(object): + """An object for unidirectional linking to a Transaction's Output. + + Attributes: + txid (str, optional): A Transaction to link to. + output (int, optional): An output's index in a Transaction with id + `txid`. + """ + + def __init__(self, txid=None, output=None): + """Create an instance of a :class:`~.TransactionLink`. + + Note: + In an IPLD implementation, this class is not necessary anymore, + as an IPLD link can simply point to an object, as well as an + objects properties. So instead of having a (de)serializable + class, we can have a simple IPLD link of the form: + `//transaction/outputs//`. + + Args: + txid (str, optional): A Transaction to link to. + output (int, optional): An Outputs's index in a Transaction with + id `txid`. + """ + self.txid = txid + self.output = output + + def __bool__(self): + return self.txid is not None and self.output is not None + + def __eq__(self, other): + # TODO: If `other !== TransactionLink` return `False` + return self.to_dict() == other.to_dict() + + def __hash__(self): + return hash((self.txid, self.output)) + + @classmethod + def from_dict(cls, link): + """Transforms a Python dictionary to a TransactionLink object. + + Args: + link (dict): The link to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.TransactionLink` + """ + try: + return cls(link['transaction_id'], link['output_index']) + except TypeError: + return cls() + + def to_dict(self): + """Transforms the object to a Python dictionary. + + Returns: + (dict|None): The link as an alternative serialization format. + """ + if self.txid is None and self.output is None: + return None + else: + return { + 'transaction_id': self.txid, + 'output_index': self.output, + } + + def to_uri(self, path=''): + if self.txid is None and self.output is None: + return None + return '{}/transactions/{}/outputs/{}'.format(path, self.txid, + self.output) + + +class Output(object): + """An Output is used to lock an asset. + + Wraps around a Crypto-condition Condition. + + Attributes: + fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment + to extract a Condition from. + public_keys (:obj:`list` of :obj:`str`, optional): A list of + owners before a Transaction was confirmed. + """ + + MAX_AMOUNT = 9 * 10 ** 18 + + def __init__(self, fulfillment, public_keys=None, amount=1): + """Create an instance of a :class:`~.Output`. + + Args: + fulfillment (:class:`cryptoconditions.Fulfillment`): A + Fulfillment to extract a Condition from. + public_keys (:obj:`list` of :obj:`str`, optional): A list of + owners before a Transaction was confirmed. + amount (int): The amount of Assets to be locked with this + Output. + + Raises: + TypeError: if `public_keys` is not instance of `list`. + """ + if not isinstance(public_keys, list) and public_keys is not None: + raise TypeError('`public_keys` must be a list instance or None') + if not isinstance(amount, int): + raise TypeError('`amount` must be an int') + if amount < 1: + raise AmountError('`amount` must be greater than 0') + if amount > self.MAX_AMOUNT: + raise AmountError('`amount` must be <= %s' % self.MAX_AMOUNT) + + self.fulfillment = fulfillment + self.amount = amount + self.public_keys = public_keys + + def __eq__(self, other): + # TODO: If `other !== Condition` return `False` + return self.to_dict() == other.to_dict() + + def to_dict(self): + """Transforms the object to a Python dictionary. + + Note: + A dictionary serialization of the Input the Output was + derived from is always provided. + + Returns: + dict: The Output as an alternative serialization format. + """ + # TODO FOR CC: It must be able to recognize a hashlock condition + # and fulfillment! + condition = {} + try: + condition['details'] = _fulfillment_to_details(self.fulfillment) + except AttributeError: + pass + + try: + condition['uri'] = self.fulfillment.condition_uri + except AttributeError: + condition['uri'] = self.fulfillment + + output = { + 'public_keys': self.public_keys, + 'condition': condition, + 'amount': str(self.amount), + } + return output + + @classmethod + def generate(cls, public_keys, amount): + """Generates a Output from a specifically formed tuple or list. + + Note: + If a ThresholdCondition has to be generated where the threshold + is always the number of subconditions it is split between, a + list of the following structure is sufficient: + + [(address|condition)*, [(address|condition)*, ...], ...] + + Args: + public_keys (:obj:`list` of :obj:`str`): The public key of + the users that should be able to fulfill the Condition + that is being created. + amount (:obj:`int`): The amount locked by the Output. + + Returns: + An Output that can be used in a Transaction. + + Raises: + TypeError: If `public_keys` is not an instance of `list`. + ValueError: If `public_keys` is an empty list. + """ + threshold = len(public_keys) + if not isinstance(amount, int): + raise TypeError('`amount` must be a int') + if amount < 1: + raise AmountError('`amount` needs to be greater than zero') + if not isinstance(public_keys, list): + raise TypeError('`public_keys` must be an instance of list') + if len(public_keys) == 0: + raise ValueError('`public_keys` needs to contain at least one' + 'owner') + elif len(public_keys) == 1 and not isinstance(public_keys[0], list): + if isinstance(public_keys[0], Fulfillment): + ffill = public_keys[0] + else: + ffill = Ed25519Sha256( + public_key=base58.b58decode(public_keys[0])) + return cls(ffill, public_keys, amount=amount) + else: + initial_cond = ThresholdSha256(threshold=threshold) + threshold_cond = reduce(cls._gen_condition, public_keys, + initial_cond) + return cls(threshold_cond, public_keys, amount=amount) + + @classmethod + def _gen_condition(cls, initial, new_public_keys): + """Generates ThresholdSha256 conditions from a list of new owners. + + Note: + This method is intended only to be used with a reduce function. + For a description on how to use this method, see + :meth:`~.Output.generate`. + + Args: + initial (:class:`cryptoconditions.ThresholdSha256`): + A Condition representing the overall root. + new_public_keys (:obj:`list` of :obj:`str`|str): A list of new + owners or a single new owner. + + Returns: + :class:`cryptoconditions.ThresholdSha256`: + """ + try: + threshold = len(new_public_keys) + except TypeError: + threshold = None + + if isinstance(new_public_keys, list) and len(new_public_keys) > 1: + ffill = ThresholdSha256(threshold=threshold) + reduce(cls._gen_condition, new_public_keys, ffill) + elif isinstance(new_public_keys, list) and len(new_public_keys) <= 1: + raise ValueError('Sublist cannot contain single owner') + else: + try: + new_public_keys = new_public_keys.pop() + except AttributeError: + pass + # NOTE: Instead of submitting base58 encoded addresses, a user + # of this class can also submit fully instantiated + # Cryptoconditions. In the case of casting + # `new_public_keys` to a Ed25519Fulfillment with the + # result of a `TypeError`, we're assuming that + # `new_public_keys` is a Cryptocondition then. + if isinstance(new_public_keys, Fulfillment): + ffill = new_public_keys + else: + ffill = Ed25519Sha256( + public_key=base58.b58decode(new_public_keys)) + initial.add_subfulfillment(ffill) + return initial + + @classmethod + def from_dict(cls, data): + """Transforms a Python dictionary to an Output object. + + Note: + To pass a serialization cycle multiple times, a + Cryptoconditions Fulfillment needs to be present in the + passed-in dictionary, as Condition URIs are not serializable + anymore. + + Args: + data (dict): The dict to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.Output` + """ + try: + fulfillment = _fulfillment_from_details(data['condition']['details']) + except KeyError: + # NOTE: Hashlock condition case + fulfillment = data['condition']['uri'] + try: + amount = int(data['amount']) + except ValueError: + raise AmountError('Invalid amount: %s' % data['amount']) + return cls(fulfillment, data['public_keys'], amount) + + +class Transaction(object): + """A Transaction is used to create and transfer assets. + + Note: + For adding Inputs and Outputs, this class provides methods + to do so. + + Attributes: + operation (str): Defines the operation of the Transaction. + inputs (:obj:`list` of :class:`~bigchaindb.common. + transaction.Input`, optional): Define the assets to + spend. + outputs (:obj:`list` of :class:`~bigchaindb.common. + transaction.Output`, optional): Define the assets to lock. + asset (dict): Asset payload for this Transaction. ``CREATE`` + Transactions require a dict with a ``data`` + property while ``TRANSFER`` Transactions require a dict with a + ``id`` property. + metadata (dict): + Metadata to be stored along with the Transaction. + version (string): Defines the version number of a Transaction. + """ + + CREATE = 'CREATE' + TRANSFER = 'TRANSFER' + ALLOWED_OPERATIONS = (CREATE, TRANSFER) + VERSION = '2.0' + + def __init__(self, operation, asset, inputs=None, outputs=None, + metadata=None, version=None, hash_id=None, tx_dict=None): + """The constructor allows to create a customizable Transaction. + + Note: + When no `version` is provided, one is being + generated by this method. + + Args: + operation (str): Defines the operation of the Transaction. + asset (dict): Asset payload for this Transaction. + inputs (:obj:`list` of :class:`~bigchaindb.common. + transaction.Input`, optional): Define the assets to + outputs (:obj:`list` of :class:`~bigchaindb.common. + transaction.Output`, optional): Define the assets to + lock. + metadata (dict): Metadata to be stored along with the + Transaction. + version (string): Defines the version number of a Transaction. + hash_id (string): Hash id of the transaction. + """ + if operation not in self.ALLOWED_OPERATIONS: + allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS) + raise ValueError('`operation` must be one of {}' + .format(allowed_ops)) + + # Asset payloads for 'CREATE' operations must be None or + # dicts holding a `data` property. Asset payloads for 'TRANSFER' + # operations must be dicts holding an `id` property. + if (operation == self.CREATE and + asset is not None and not (isinstance(asset, dict) and 'data' in asset)): + raise TypeError(('`asset` must be None or a dict holding a `data` ' + " property instance for '{}' Transactions".format(operation))) + elif (operation == self.TRANSFER and + not (isinstance(asset, dict) and 'id' in asset)): + raise TypeError(('`asset` must be a dict holding an `id` property ' + 'for \'TRANSFER\' Transactions')) + + if outputs and not isinstance(outputs, list): + raise TypeError('`outputs` must be a list instance or None') + + if inputs and not isinstance(inputs, list): + raise TypeError('`inputs` must be a list instance or None') + + if metadata is not None and not isinstance(metadata, dict): + raise TypeError('`metadata` must be a dict or None') + + self.version = version if version is not None else self.VERSION + self.operation = operation + self.asset = asset + self.inputs = inputs or [] + self.outputs = outputs or [] + self.metadata = metadata + self._id = hash_id + self.tx_dict = tx_dict + + @property + def unspent_outputs(self): + """UnspentOutput: The outputs of this transaction, in a data + structure containing relevant information for storing them in + a UTXO set, and performing validation. + """ + if self.operation == self.CREATE: + self._asset_id = self._id + elif self.operation == self.TRANSFER: + self._asset_id = self.asset['id'] + return (UnspentOutput( + transaction_id=self._id, + output_index=output_index, + amount=output.amount, + asset_id=self._asset_id, + condition_uri=output.fulfillment.condition_uri, + ) for output_index, output in enumerate(self.outputs)) + + @property + def spent_outputs(self): + """Tuple of :obj:`dict`: Inputs of this transaction. Each input + is represented as a dictionary containing a transaction id and + output index. + """ + return ( + input_.fulfills.to_dict() + for input_ in self.inputs if input_.fulfills + ) + + @property + def serialized(self): + return Transaction._to_str(self.to_dict()) + + def _hash(self): + self._id = hash_data(self.serialized) + + @classmethod + def validate_create(cls, tx_signers, recipients, asset, metadata): + if not isinstance(tx_signers, list): + raise TypeError('`tx_signers` must be a list instance') + if not isinstance(recipients, list): + raise TypeError('`recipients` must be a list instance') + if len(tx_signers) == 0: + raise ValueError('`tx_signers` list cannot be empty') + if len(recipients) == 0: + raise ValueError('`recipients` list cannot be empty') + if not (asset is None or isinstance(asset, dict)): + raise TypeError('`asset` must be a dict or None') + if not (metadata is None or isinstance(metadata, dict)): + raise TypeError('`metadata` must be a dict or None') + + inputs = [] + outputs = [] + + # generate_outputs + for recipient in recipients: + if not isinstance(recipient, tuple) or len(recipient) != 2: + raise ValueError(('Each `recipient` in the list must be a' + ' tuple of `([],' + ' )`')) + pub_keys, amount = recipient + outputs.append(Output.generate(pub_keys, amount)) + + # generate inputs + inputs.append(Input.generate(tx_signers)) + + return (inputs, outputs) + + @classmethod + def create(cls, tx_signers, recipients, metadata=None, asset=None): + """A simple way to generate a `CREATE` transaction. + + Note: + This method currently supports the following Cryptoconditions + use cases: + - Ed25519 + - ThresholdSha256 + + Additionally, it provides support for the following Planetmint + use cases: + - Multiple inputs and outputs. + + Args: + tx_signers (:obj:`list` of :obj:`str`): A list of keys that + represent the signers of the CREATE Transaction. + recipients (:obj:`list` of :obj:`tuple`): A list of + ([keys],amount) that represent the recipients of this + Transaction. + metadata (dict): The metadata to be stored along with the + Transaction. + asset (dict): The metadata associated with the asset that will + be created in this Transaction. + + Returns: + :class:`~bigchaindb.common.transaction.Transaction` + """ + + (inputs, outputs) = cls.validate_create(tx_signers, recipients, asset, metadata) + return cls(cls.CREATE, {'data': asset}, inputs, outputs, metadata) + + @classmethod + def validate_transfer(cls, inputs, recipients, asset_id, metadata): + if not isinstance(inputs, list): + raise TypeError('`inputs` must be a list instance') + if len(inputs) == 0: + raise ValueError('`inputs` must contain at least one item') + if not isinstance(recipients, list): + raise TypeError('`recipients` must be a list instance') + if len(recipients) == 0: + raise ValueError('`recipients` list cannot be empty') + + outputs = [] + for recipient in recipients: + if not isinstance(recipient, tuple) or len(recipient) != 2: + raise ValueError(('Each `recipient` in the list must be a' + ' tuple of `([],' + ' )`')) + pub_keys, amount = recipient + outputs.append(Output.generate(pub_keys, amount)) + + if not isinstance(asset_id, str): + raise TypeError('`asset_id` must be a string') + + return (deepcopy(inputs), outputs) + + @classmethod + def transfer(cls, inputs, recipients, asset_id, metadata=None): + """A simple way to generate a `TRANSFER` transaction. + + Note: + Different cases for threshold conditions: + + Combining multiple `inputs` with an arbitrary number of + `recipients` can yield interesting cases for the creation of + threshold conditions we'd like to support. The following + notation is proposed: + + 1. The index of a `recipient` corresponds to the index of + an input: + e.g. `transfer([input1], [a])`, means `input1` would now be + owned by user `a`. + + 2. `recipients` can (almost) get arbitrary deeply nested, + creating various complex threshold conditions: + e.g. `transfer([inp1, inp2], [[a, [b, c]], d])`, means + `a`'s signature would have a 50% weight on `inp1` + compared to `b` and `c` that share 25% of the leftover + weight respectively. `inp2` is owned completely by `d`. + + Args: + inputs (:obj:`list` of :class:`~bigchaindb.common.transaction. + Input`): Converted `Output`s, intended to + be used as inputs in the transfer to generate. + recipients (:obj:`list` of :obj:`tuple`): A list of + ([keys],amount) that represent the recipients of this + Transaction. + asset_id (str): The asset ID of the asset to be transferred in + this Transaction. + metadata (dict): Python dictionary to be stored along with the + Transaction. + + Returns: + :class:`~bigchaindb.common.transaction.Transaction` + """ + (inputs, outputs) = cls.validate_transfer(inputs, recipients, asset_id, metadata) + return cls(cls.TRANSFER, {'id': asset_id}, inputs, outputs, metadata) + + def __eq__(self, other): + try: + other = other.to_dict() + except AttributeError: + return False + return self.to_dict() == other + + def to_inputs(self, indices=None): + """Converts a Transaction's outputs to spendable inputs. + + Note: + Takes the Transaction's outputs and derives inputs + from that can then be passed into `Transaction.transfer` as + `inputs`. + A list of integers can be passed to `indices` that + defines which outputs should be returned as inputs. + If no `indices` are passed (empty list or None) all + outputs of the Transaction are returned. + + Args: + indices (:obj:`list` of int): Defines which + outputs should be returned as inputs. + + Returns: + :obj:`list` of :class:`~bigchaindb.common.transaction. + Input` + """ + # NOTE: If no indices are passed, we just assume to take all outputs + # as inputs. + indices = indices or range(len(self.outputs)) + return [ + Input(self.outputs[idx].fulfillment, + self.outputs[idx].public_keys, + TransactionLink(self.id, idx)) + for idx in indices + ] + + def add_input(self, input_): + """Adds an input to a Transaction's list of inputs. + + Args: + input_ (:class:`~bigchaindb.common.transaction. + Input`): An Input to be added to the Transaction. + """ + if not isinstance(input_, Input): + raise TypeError('`input_` must be a Input instance') + self.inputs.append(input_) + + def add_output(self, output): + """Adds an output to a Transaction's list of outputs. + + Args: + output (:class:`~bigchaindb.common.transaction. + Output`): An Output to be added to the + Transaction. + """ + if not isinstance(output, Output): + raise TypeError('`output` must be an Output instance or None') + self.outputs.append(output) + + def sign(self, private_keys): + """Fulfills a previous Transaction's Output by signing Inputs. + + Note: + This method works only for the following Cryptoconditions + currently: + - Ed25519Fulfillment + - ThresholdSha256 + Furthermore, note that all keys required to fully sign the + Transaction have to be passed to this method. A subset of all + will cause this method to fail. + + Args: + private_keys (:obj:`list` of :obj:`str`): A complete list of + all private keys needed to sign all Fulfillments of this + Transaction. + + Returns: + :class:`~bigchaindb.common.transaction.Transaction` + """ + # TODO: Singing should be possible with at least one of all private + # keys supplied to this method. + if private_keys is None or not isinstance(private_keys, list): + raise TypeError('`private_keys` must be a list instance') + + # NOTE: Generate public keys from private keys and match them in a + # dictionary: + # key: public_key + # value: private_key + def gen_public_key(private_key): + # TODO FOR CC: Adjust interface so that this function becomes + # unnecessary + + # cc now provides a single method `encode` to return the key + # in several different encodings. + public_key = private_key.get_verifying_key().encode() + # Returned values from cc are always bytestrings so here we need + # to decode to convert the bytestring into a python str + return public_key.decode() + + key_pairs = {gen_public_key(PrivateKey(private_key)): + PrivateKey(private_key) for private_key in private_keys} + + tx_dict = self.to_dict() + tx_dict = Transaction._remove_signatures(tx_dict) + tx_serialized = Transaction._to_str(tx_dict) + for i, input_ in enumerate(self.inputs): + self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs) + + self._hash() + + return self + + @classmethod + def _sign_input(cls, input_, message, key_pairs): + """Signs a single Input. + + Note: + This method works only for the following Cryptoconditions + currently: + - Ed25519Fulfillment + - ThresholdSha256. + + Args: + input_ (:class:`~bigchaindb.common.transaction. + Input`) The Input to be signed. + message (str): The message to be signed + key_pairs (dict): The keys to sign the Transaction with. + """ + if isinstance(input_.fulfillment, Ed25519Sha256): + return cls._sign_simple_signature_fulfillment(input_, message, + key_pairs) + elif isinstance(input_.fulfillment, ThresholdSha256): + return cls._sign_threshold_signature_fulfillment(input_, message, + key_pairs) + else: + raise ValueError( + 'Fulfillment couldn\'t be matched to ' + 'Cryptocondition fulfillment type.') + + @classmethod + def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs): + """Signs a Ed25519Fulfillment. + + Args: + input_ (:class:`~bigchaindb.common.transaction. + Input`) The input to be signed. + message (str): The message to be signed + key_pairs (dict): The keys to sign the Transaction with. + """ + # NOTE: To eliminate the dangers of accidentally signing a condition by + # reference, we remove the reference of input_ here + # intentionally. If the user of this class knows how to use it, + # this should never happen, but then again, never say never. + input_ = deepcopy(input_) + public_key = input_.owners_before[0] + message = sha3_256(message.encode()) + if input_.fulfills: + message.update('{}{}'.format( + input_.fulfills.txid, input_.fulfills.output).encode()) + + try: + # cryptoconditions makes no assumptions of the encoding of the + # message to sign or verify. It only accepts bytestrings + input_.fulfillment.sign( + message.digest(), base58.b58decode(key_pairs[public_key].encode())) + except KeyError: + raise KeypairMismatchException('Public key {} is not a pair to ' + 'any of the private keys' + .format(public_key)) + return input_ + + @classmethod + def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs): + """Signs a ThresholdSha256. + + Args: + input_ (:class:`~bigchaindb.common.transaction. + Input`) The Input to be signed. + message (str): The message to be signed + key_pairs (dict): The keys to sign the Transaction with. + """ + input_ = deepcopy(input_) + message = sha3_256(message.encode()) + if input_.fulfills: + message.update('{}{}'.format( + input_.fulfills.txid, input_.fulfills.output).encode()) + + for owner_before in set(input_.owners_before): + # TODO: CC should throw a KeypairMismatchException, instead of + # our manual mapping here + + # TODO FOR CC: Naming wise this is not so smart, + # `get_subcondition` in fact doesn't return a + # condition but a fulfillment + + # TODO FOR CC: `get_subcondition` is singular. One would not + # expect to get a list back. + ccffill = input_.fulfillment + subffills = ccffill.get_subcondition_from_vk( + base58.b58decode(owner_before)) + if not subffills: + raise KeypairMismatchException('Public key {} cannot be found ' + 'in the fulfillment' + .format(owner_before)) + try: + private_key = key_pairs[owner_before] + except KeyError: + raise KeypairMismatchException('Public key {} is not a pair ' + 'to any of the private keys' + .format(owner_before)) + + # cryptoconditions makes no assumptions of the encoding of the + # message to sign or verify. It only accepts bytestrings + for subffill in subffills: + subffill.sign( + message.digest(), base58.b58decode(private_key.encode())) + return input_ + + def inputs_valid(self, outputs=None): + """Validates the Inputs in the Transaction against given + Outputs. + + Note: + Given a `CREATE` Transaction is passed, + dummy values for Outputs are submitted for validation that + evaluate parts of the validation-checks to `True`. + + Args: + outputs (:obj:`list` of :class:`~bigchaindb.common. + transaction.Output`): A list of Outputs to check the + Inputs against. + + Returns: + bool: If all Inputs are valid. + """ + if self.operation == self.CREATE: + # NOTE: Since in the case of a `CREATE`-transaction we do not have + # to check for outputs, we're just submitting dummy + # values to the actual method. This simplifies it's logic + # greatly, as we do not have to check against `None` values. + return self._inputs_valid(['dummyvalue' + for _ in self.inputs]) + elif self.operation == self.TRANSFER: + return self._inputs_valid([output.fulfillment.condition_uri + for output in outputs]) + else: + allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS) + raise TypeError('`operation` must be one of {}' + .format(allowed_ops)) + + def _inputs_valid(self, output_condition_uris): + """Validates an Input against a given set of Outputs. + + Note: + The number of `output_condition_uris` must be equal to the + number of Inputs a Transaction has. + + Args: + output_condition_uris (:obj:`list` of :obj:`str`): A list of + Outputs to check the Inputs against. + + Returns: + bool: If all Outputs are valid. + """ + + if len(self.inputs) != len(output_condition_uris): + raise ValueError('Inputs and ' + 'output_condition_uris must have the same count') + + tx_dict = self.tx_dict if self.tx_dict else self.to_dict() + tx_dict = Transaction._remove_signatures(tx_dict) + tx_dict['id'] = None + tx_serialized = Transaction._to_str(tx_dict) + + def validate(i, output_condition_uri=None): + """Validate input against output condition URI""" + return self._input_valid(self.inputs[i], self.operation, + tx_serialized, output_condition_uri) + + return all(validate(i, cond) + for i, cond in enumerate(output_condition_uris)) + + @lru_cache(maxsize=16384) + def _input_valid(self, input_, operation, message, output_condition_uri=None): + """Validates a single Input against a single Output. + + Note: + In case of a `CREATE` Transaction, this method + does not validate against `output_condition_uri`. + + Args: + input_ (:class:`~bigchaindb.common.transaction. + Input`) The Input to be signed. + operation (str): The type of Transaction. + message (str): The fulfillment message. + output_condition_uri (str, optional): An Output to check the + Input against. + + Returns: + bool: If the Input is valid. + """ + ccffill = input_.fulfillment + try: + parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri()) + except (TypeError, ValueError, + ParsingError, ASN1DecodeError, ASN1EncodeError): + return False + + if operation == self.CREATE: + # NOTE: In the case of a `CREATE` transaction, the + # output is always valid. + output_valid = True + else: + output_valid = output_condition_uri == ccffill.condition_uri + + message = sha3_256(message.encode()) + if input_.fulfills: + message.update('{}{}'.format( + input_.fulfills.txid, input_.fulfills.output).encode()) + + # NOTE: We pass a timestamp to `.validate`, as in case of a timeout + # condition we'll have to validate against it + + # cryptoconditions makes no assumptions of the encoding of the + # message to sign or verify. It only accepts bytestrings + ffill_valid = parsed_ffill.validate(message=message.digest()) + return output_valid and ffill_valid + + # This function is required by `lru_cache` to create a key for memoization + def __hash__(self): + return hash(self.id) + + @memoize_to_dict + def to_dict(self): + """Transforms the object to a Python dictionary. + + Returns: + dict: The Transaction as an alternative serialization format. + """ + return { + 'inputs': [input_.to_dict() for input_ in self.inputs], + 'outputs': [output.to_dict() for output in self.outputs], + 'operation': str(self.operation), + 'metadata': self.metadata, + 'asset': self.asset, + 'version': self.version, + 'id': self._id, + } + + @staticmethod + # TODO: Remove `_dict` prefix of variable. + def _remove_signatures(tx_dict): + """Takes a Transaction dictionary and removes all signatures. + + Args: + tx_dict (dict): The Transaction to remove all signatures from. + + Returns: + dict + + """ + # NOTE: We remove the reference since we need `tx_dict` only for the + # transaction's hash + tx_dict = deepcopy(tx_dict) + for input_ in tx_dict['inputs']: + # NOTE: Not all Cryptoconditions return a `signature` key (e.g. + # ThresholdSha256), so setting it to `None` in any + # case could yield incorrect signatures. This is why we only + # set it to `None` if it's set in the dict. + input_['fulfillment'] = None + return tx_dict + + @staticmethod + def _to_hash(value): + return hash_data(value) + + @property + def id(self): + return self._id + + def to_hash(self): + return self.to_dict()['id'] + + @staticmethod + def _to_str(value): + return serialize(value) + + # TODO: This method shouldn't call `_remove_signatures` + def __str__(self): + tx = Transaction._remove_signatures(self.to_dict()) + return Transaction._to_str(tx) + + @classmethod + def get_asset_id(cls, transactions): + """Get the asset id from a list of :class:`~.Transactions`. + + This is useful when we want to check if the multiple inputs of a + transaction are related to the same asset id. + + Args: + transactions (:obj:`list` of :class:`~bigchaindb.common. + transaction.Transaction`): A list of Transactions. + Usually input Transactions that should have a matching + asset ID. + + Returns: + str: ID of the asset. + + Raises: + :exc:`AssetIdMismatch`: If the inputs are related to different + assets. + """ + + if not isinstance(transactions, list): + transactions = [transactions] + + # create a set of the transactions' asset ids + asset_ids = {tx.id if tx.operation == tx.CREATE + else tx.asset['id'] + for tx in transactions} + + # check that all the transasctions have the same asset id + if len(asset_ids) > 1: + raise AssetIdMismatch(('All inputs of all transactions passed' + ' need to have the same asset id')) + return asset_ids.pop() + + @staticmethod + def validate_id(tx_body): + """Validate the transaction ID of a transaction + + Args: + tx_body (dict): The Transaction to be transformed. + """ + # NOTE: Remove reference to avoid side effects + # tx_body = deepcopy(tx_body) + tx_body = rapidjson.loads(rapidjson.dumps(tx_body)) + + try: + proposed_tx_id = tx_body['id'] + except KeyError: + raise InvalidHash('No transaction id found!') + + tx_body['id'] = None + + tx_body_serialized = Transaction._to_str(tx_body) + valid_tx_id = Transaction._to_hash(tx_body_serialized) + + if proposed_tx_id != valid_tx_id: + err_msg = ("The transaction's id '{}' isn't equal to " + "the hash of its body, i.e. it's not valid.") + raise InvalidHash(err_msg.format(proposed_tx_id)) + + @classmethod + @memoize_from_dict + def from_dict(cls, tx, skip_schema_validation=True): + """Transforms a Python dictionary to a Transaction object. + + Args: + tx_body (dict): The Transaction to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.Transaction` + """ + operation = tx.get('operation', Transaction.CREATE) if isinstance(tx, dict) else Transaction.CREATE + cls = Transaction.resolve_class(operation) + + if not skip_schema_validation: + cls.validate_id(tx) + cls.validate_schema(tx) + + inputs = [Input.from_dict(input_) for input_ in tx['inputs']] + outputs = [Output.from_dict(output) for output in tx['outputs']] + return cls(tx['operation'], tx['asset'], inputs, outputs, + tx['metadata'], tx['version'], hash_id=tx['id'], tx_dict=tx) + + @classmethod + def from_db(cls, bigchain, tx_dict_list): + """Helper method that reconstructs a transaction dict that was returned + from the database. It checks what asset_id to retrieve, retrieves the + asset from the asset table and reconstructs the transaction. + + Args: + bigchain (:class:`~bigchaindb.tendermint.BigchainDB`): An instance + of Planetmint used to perform database queries. + tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or + list of transaction dict as returned from the database. + + Returns: + :class:`~Transaction` + + """ + return_list = True + if isinstance(tx_dict_list, dict): + tx_dict_list = [tx_dict_list] + return_list = False + + tx_map = {} + tx_ids = [] + for tx in tx_dict_list: + tx.update({'metadata': None}) + tx_map[tx['id']] = tx + tx_ids.append(tx['id']) + + assets = list(bigchain.get_assets(tx_ids)) + for asset in assets: + if asset is not None: + tx = tx_map[asset['id']] + del asset['id'] + tx['asset'] = asset + + tx_ids = list(tx_map.keys()) + metadata_list = list(bigchain.get_metadata(tx_ids)) + for metadata in metadata_list: + tx = tx_map[metadata['id']] + tx.update({'metadata': metadata.get('metadata')}) + + if return_list: + tx_list = [] + for tx_id, tx in tx_map.items(): + tx_list.append(cls.from_dict(tx)) + return tx_list + else: + tx = list(tx_map.values())[0] + return cls.from_dict(tx) + + type_registry = {} + + @staticmethod + def register_type(tx_type, tx_class): + Transaction.type_registry[tx_type] = tx_class + + def resolve_class(operation): + """For the given `tx` based on the `operation` key return its implementation class""" + + create_txn_class = Transaction.type_registry.get(Transaction.CREATE) + return Transaction.type_registry.get(operation, create_txn_class) + + @classmethod + def validate_schema(cls, tx): + pass + + def validate_transfer_inputs(self, bigchain, current_transactions=[]): + # store the inputs so that we can check if the asset ids match + input_txs = [] + input_conditions = [] + for input_ in self.inputs: + input_txid = input_.fulfills.txid + input_tx = bigchain.get_transaction(input_txid) + + if input_tx is None: + for ctxn in current_transactions: + if ctxn.id == input_txid: + input_tx = ctxn + + if input_tx is None: + raise InputDoesNotExist("input `{}` doesn't exist" + .format(input_txid)) + + spent = bigchain.get_spent(input_txid, input_.fulfills.output, + current_transactions) + if spent: + raise DoubleSpend('input `{}` was already spent' + .format(input_txid)) + + output = input_tx.outputs[input_.fulfills.output] + input_conditions.append(output) + input_txs.append(input_tx) + + # Validate that all inputs are distinct + links = [i.fulfills.to_uri() for i in self.inputs] + if len(links) != len(set(links)): + raise DoubleSpend('tx "{}" spends inputs twice'.format(self.id)) + + # validate asset id + asset_id = self.get_asset_id(input_txs) + if asset_id != self.asset['id']: + raise AssetIdMismatch(('The asset id of the input does not' + ' match the asset id of the' + ' transaction')) + + input_amount = sum([input_condition.amount for input_condition in input_conditions]) + output_amount = sum([output_condition.amount for output_condition in self.outputs]) + + if output_amount != input_amount: + raise AmountError(('The amount used in the inputs `{}`' + ' needs to be same as the amount used' + ' in the outputs `{}`') + .format(input_amount, output_amount)) + + if not self.inputs_valid(input_conditions): + raise InvalidSignature('Transaction signature is invalid.') + + return True diff --git a/bigchaindb/common/transaction_mode_types.py b/bigchaindb/common/transaction_mode_types.py new file mode 100644 index 0000000..840dff7 --- /dev/null +++ b/bigchaindb/common/transaction_mode_types.py @@ -0,0 +1,8 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +BROADCAST_TX_COMMIT = 'broadcast_tx_commit' +BROADCAST_TX_ASYNC = 'broadcast_tx_async' +BROADCAST_TX_SYNC = 'broadcast_tx_sync' diff --git a/bigchaindb/common/utils.py b/bigchaindb/common/utils.py new file mode 100644 index 0000000..ecb03a0 --- /dev/null +++ b/bigchaindb/common/utils.py @@ -0,0 +1,165 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import time +import re +import rapidjson + +import bigchaindb +from bigchaindb.common.exceptions import ValidationError + + +def gen_timestamp(): + """The Unix time, rounded to the nearest second. + See https://en.wikipedia.org/wiki/Unix_time + + Returns: + str: the Unix time + """ + return str(round(time.time())) + + +def serialize(data): + """Serialize a dict into a JSON formatted string. + + This function enforces rules like the separator and order of keys. + This ensures that all dicts are serialized in the same way. + + This is specially important for hashing data. We need to make sure that + everyone serializes their data in the same way so that we do not have + hash mismatches for the same structure due to serialization + differences. + + Args: + data (dict): dict to serialize + + Returns: + str: JSON formatted string + + """ + return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False, + sort_keys=True) + + +def deserialize(data): + """Deserialize a JSON formatted string into a dict. + + Args: + data (str): JSON formatted string. + + Returns: + dict: dict resulting from the serialization of a JSON formatted + string. + """ + return rapidjson.loads(data) + + +def validate_txn_obj(obj_name, obj, key, validation_fun): + """Validate value of `key` in `obj` using `validation_fun`. + + Args: + obj_name (str): name for `obj` being validated. + obj (dict): dictionary object. + key (str): key to be validated in `obj`. + validation_fun (function): function used to validate the value + of `key`. + + Returns: + None: indicates validation successful + + Raises: + ValidationError: `validation_fun` will raise exception on failure + """ + backend = bigchaindb.config['database']['backend'] + + if backend == 'localmongodb': + data = obj.get(key, {}) + if isinstance(data, dict): + validate_all_keys_in_obj(obj_name, data, validation_fun) + elif isinstance(data, list): + validate_all_items_in_list(obj_name, data, validation_fun) + + +def validate_all_items_in_list(obj_name, data, validation_fun): + for item in data: + if isinstance(item, dict): + validate_all_keys_in_obj(obj_name, item, validation_fun) + elif isinstance(item, list): + validate_all_items_in_list(obj_name, item, validation_fun) + + +def validate_all_keys_in_obj(obj_name, obj, validation_fun): + """Validate all (nested) keys in `obj` by using `validation_fun`. + + Args: + obj_name (str): name for `obj` being validated. + obj (dict): dictionary object. + validation_fun (function): function used to validate the value + of `key`. + + Returns: + None: indicates validation successful + + Raises: + ValidationError: `validation_fun` will raise this error on failure + """ + for key, value in obj.items(): + validation_fun(obj_name, key) + if isinstance(value, dict): + validate_all_keys_in_obj(obj_name, value, validation_fun) + elif isinstance(value, list): + validate_all_items_in_list(obj_name, value, validation_fun) + + +def validate_all_values_for_key_in_obj(obj, key, validation_fun): + """Validate value for all (nested) occurrence of `key` in `obj` + using `validation_fun`. + + Args: + obj (dict): dictionary object. + key (str): key whose value is to be validated. + validation_fun (function): function used to validate the value + of `key`. + + Raises: + ValidationError: `validation_fun` will raise this error on failure + """ + for vkey, value in obj.items(): + if vkey == key: + validation_fun(value) + elif isinstance(value, dict): + validate_all_values_for_key_in_obj(value, key, validation_fun) + elif isinstance(value, list): + validate_all_values_for_key_in_list(value, key, validation_fun) + + +def validate_all_values_for_key_in_list(input_list, key, validation_fun): + for item in input_list: + if isinstance(item, dict): + validate_all_values_for_key_in_obj(item, key, validation_fun) + elif isinstance(item, list): + validate_all_values_for_key_in_list(item, key, validation_fun) + + +def validate_key(obj_name, key): + """Check if `key` contains ".", "$" or null characters. + + https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names + + Args: + obj_name (str): object name to use when raising exception + key (str): key to validated + + Returns: + None: validation successful + + Raises: + ValidationError: will raise exception in case of regex match. + """ + if re.search(r'^[$]|\.|\x00', key): + error_str = ('Invalid key name "{}" in {} object. The ' + 'key name cannot contain characters ' + '".", "$" or null characters').format(key, obj_name) + raise ValidationError(error_str) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py new file mode 100644 index 0000000..aa330f4 --- /dev/null +++ b/bigchaindb/config_utils.py @@ -0,0 +1,308 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Utils for reading and setting configuration settings. + +The value of each Planetmint Server configuration setting is +determined according to the following rules: + +* If it's set by an environment variable, then use that value +* Otherwise, if it's set in a local config file, then use that + value +* Otherwise, use the default value (contained in + ``bigchaindb.__init__``) +""" + + +import os +import copy +import json +import logging +import collections.abc +from functools import lru_cache + +from pkg_resources import iter_entry_points, ResolutionError + +from bigchaindb.common import exceptions + +import bigchaindb + +from bigchaindb.validation import BaseValidationRules + +# TODO: move this to a proper configuration file for logging +logging.getLogger('requests').setLevel(logging.WARNING) +logger = logging.getLogger(__name__) + +CONFIG_DEFAULT_PATH = os.environ.setdefault( + 'PLANETMINT_CONFIG_PATH', + os.path.join(os.path.expanduser('~'), '.bigchaindb'), +) + +CONFIG_PREFIX = 'PLANETMINT' +CONFIG_SEP = '_' + + +def map_leafs(func, mapping): + """Map a function to the leafs of a mapping.""" + + def _inner(mapping, path=None): + if path is None: + path = [] + + for key, val in mapping.items(): + if isinstance(val, collections.abc.Mapping): + _inner(val, path + [key]) + else: + mapping[key] = func(val, path=path+[key]) + + return mapping + + return _inner(copy.deepcopy(mapping)) + + +# Thanks Alex <3 +# http://stackoverflow.com/a/3233356/597097 +def update(d, u): + """Recursively update a mapping (i.e. a dict, list, set, or tuple). + + Conceptually, d and u are two sets trees (with nodes and edges). + This function goes through all the nodes of u. For each node in u, + if d doesn't have that node yet, then this function adds the node from u, + otherwise this function overwrites the node already in d with u's node. + + Args: + d (mapping): The mapping to overwrite and add to. + u (mapping): The mapping to read for changes. + + Returns: + mapping: An updated version of d (updated by u). + """ + for k, v in u.items(): + if isinstance(v, collections.abc.Mapping): + r = update(d.get(k, {}), v) + d[k] = r + else: + d[k] = u[k] + return d + + +def file_config(filename=None): + """Returns the config values found in a configuration file. + + Args: + filename (str): the JSON file with the configuration values. + If ``None``, CONFIG_DEFAULT_PATH will be used. + + Returns: + dict: The config values in the specified config file (or the + file at CONFIG_DEFAULT_PATH, if filename == None) + """ + logger.debug('On entry into file_config(), filename = {}'.format(filename)) + + if filename is None: + filename = CONFIG_DEFAULT_PATH + + logger.debug('file_config() will try to open `{}`'.format(filename)) + with open(filename) as f: + try: + config = json.load(f) + except ValueError as err: + raise exceptions.ConfigurationError( + 'Failed to parse the JSON configuration from `{}`, {}'.format(filename, err) + ) + + logger.info('Configuration loaded from `{}`'.format(filename)) + + return config + + +def env_config(config): + """Return a new configuration with the values found in the environment. + + The function recursively iterates over the config, checking if there is + a matching env variable. If an env variable is found, the func updates + the configuration with that value. + + The name of the env variable is built combining a prefix (``PLANETMINT``) + with the path to the value. If the ``config`` in input is: + ``{'database': {'host': 'localhost'}}`` + this function will try to read the env variable ``PLANETMINT_DATABASE_HOST``. + """ + + def load_from_env(value, path): + var_name = CONFIG_SEP.join([CONFIG_PREFIX] + list(map(lambda s: s.upper(), path))) + + return os.environ.get(var_name, value) + + return map_leafs(load_from_env, config) + + +def update_types(config, reference, list_sep=':'): + """Return a new configuration where all the values types + are aligned with the ones in the default configuration + """ + + def _coerce(current, value): + # Coerce a value to the `current` type. + try: + # First we try to apply current to the value, since it + # might be a function + return current(value) + except TypeError: + # Then we check if current is a list AND if the value + # is a string. + if isinstance(current, list) and isinstance(value, str): + # If so, we use the colon as the separator + return value.split(list_sep) + + try: + # If we are here, we should try to apply the type + # of `current` to the value + return type(current)(value) + except TypeError: + # Worst case scenario we return the value itself. + return value + + def _update_type(value, path): + current = reference + + for elem in path: + try: + current = current[elem] + except KeyError: + return value + + return _coerce(current, value) + + return map_leafs(_update_type, config) + + +def set_config(config): + """Set bigchaindb.config equal to the default config dict, + then update that with whatever is in the provided config dict, + and then set bigchaindb.config['CONFIGURED'] = True + + Args: + config (dict): the config dict to read for changes + to the default config + + Note: + Any previous changes made to ``bigchaindb.config`` will be lost. + """ + # Deep copy the default config into bigchaindb.config + bigchaindb.config = copy.deepcopy(bigchaindb._config) + # Update the default config with whatever is in the passed config + update(bigchaindb.config, update_types(config, bigchaindb.config)) + bigchaindb.config['CONFIGURED'] = True + + +def update_config(config): + """Update bigchaindb.config with whatever is in the provided config dict, + and then set bigchaindb.config['CONFIGURED'] = True + + Args: + config (dict): the config dict to read for changes + to the default config + """ + + # Update the default config with whatever is in the passed config + update(bigchaindb.config, update_types(config, bigchaindb.config)) + bigchaindb.config['CONFIGURED'] = True + + +def write_config(config, filename=None): + """Write the provided configuration to a specific location. + + Args: + config (dict): a dictionary with the configuration to load. + filename (str): the name of the file that will store the new configuration. Defaults to ``None``. + If ``None``, the HOME of the current user and the string ``.bigchaindb`` will be used. + """ + if not filename: + filename = CONFIG_DEFAULT_PATH + + with open(filename, 'w') as f: + json.dump(config, f, indent=4) + + +def is_configured(): + return bool(bigchaindb.config.get('CONFIGURED')) + + +def autoconfigure(filename=None, config=None, force=False): + """Run ``file_config`` and ``env_config`` if the module has not + been initialized. + """ + if not force and is_configured(): + logger.debug('System already configured, skipping autoconfiguration') + return + + # start with the current configuration + newconfig = bigchaindb.config + + # update configuration from file + try: + newconfig = update(newconfig, file_config(filename=filename)) + except FileNotFoundError as e: + if filename: + raise + else: + logger.info('Cannot find config file `%s`.' % e.filename) + + # override configuration with env variables + newconfig = env_config(newconfig) + if config: + newconfig = update(newconfig, config) + set_config(newconfig) # sets bigchaindb.config + + +@lru_cache() +def load_validation_plugin(name=None): + """Find and load the chosen validation plugin. + + Args: + name (string): the name of the entry_point, as advertised in the + setup.py of the providing package. + + Returns: + an uninstantiated subclass of ``bigchaindb.validation.AbstractValidationRules`` + """ + if not name: + return BaseValidationRules + + # TODO: This will return the first plugin with group `bigchaindb.validation` + # and name `name` in the active WorkingSet. + # We should probably support Requirements specs in the config, e.g. + # validation_plugin: 'my-plugin-package==0.0.1;default' + plugin = None + for entry_point in iter_entry_points('bigchaindb.validation', name): + plugin = entry_point.load() + + # No matching entry_point found + if not plugin: + raise ResolutionError( + 'No plugin found in group `bigchaindb.validation` with name `{}`'. + format(name)) + + # Is this strictness desireable? + # It will probably reduce developer headaches in the wild. + if not issubclass(plugin, (BaseValidationRules,)): + raise TypeError('object of type "{}" does not implement `bigchaindb.' + 'validation.BaseValidationRules`'.format(type(plugin))) + + return plugin + + +def load_events_plugins(names=None): + plugins = [] + + if names is None: + return plugins + + for name in names: + for entry_point in iter_entry_points('bigchaindb.events', name): + plugins.append((name, entry_point.load())) + + return plugins diff --git a/bigchaindb/core.py b/bigchaindb/core.py new file mode 100644 index 0000000..fe4e0d6 --- /dev/null +++ b/bigchaindb/core.py @@ -0,0 +1,271 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module contains all the goodness to integrate Planetmint +with Tendermint. +""" +import logging +import sys + +from abci.application import BaseApplication +from abci import CodeTypeOk + +from bigchaindb import BigchainDB +from bigchaindb.elections.election import Election +from bigchaindb.version import __tm_supported_versions__ +from bigchaindb.utils import tendermint_version_is_compatible +from bigchaindb.tendermint_utils import (decode_transaction, + calculate_hash) +from bigchaindb.lib import Block +import bigchaindb.upsert_validator.validator_utils as vutils +from bigchaindb.events import EventTypes, Event + + +CodeTypeError = 1 +logger = logging.getLogger(__name__) + + +class App(BaseApplication): + """Bridge between Planetmint and Tendermint. + + The role of this class is to expose the Planetmint + transaction logic to Tendermint Core. + """ + + def __init__(self, abci, bigchaindb=None, events_queue=None,): + super().__init__(abci) + self.events_queue = events_queue + self.bigchaindb = bigchaindb or BigchainDB() + self.block_txn_ids = [] + self.block_txn_hash = '' + self.block_transactions = [] + self.validators = None + self.new_height = None + self.chain = self.bigchaindb.get_latest_abci_chain() + + def log_abci_migration_error(self, chain_id, validators): + logger.error('An ABCI chain migration is in process. ' + 'Download the new ABCI client and configure it with ' + f'chain_id={chain_id} and validators={validators}.') + + def abort_if_abci_chain_is_not_synced(self): + if self.chain is None or self.chain['is_synced']: + return + + validators = self.bigchaindb.get_validators() + self.log_abci_migration_error(self.chain['chain_id'], validators) + sys.exit(1) + + def init_chain(self, genesis): + """Initialize chain upon genesis or a migration""" + + app_hash = '' + height = 0 + + known_chain = self.bigchaindb.get_latest_abci_chain() + if known_chain is not None: + chain_id = known_chain['chain_id'] + + if known_chain['is_synced']: + msg = (f'Got invalid InitChain ABCI request ({genesis}) - ' + f'the chain {chain_id} is already synced.') + logger.error(msg) + sys.exit(1) + + if chain_id != genesis.chain_id: + validators = self.bigchaindb.get_validators() + self.log_abci_migration_error(chain_id, validators) + sys.exit(1) + + # set migration values for app hash and height + block = self.bigchaindb.get_latest_block() + app_hash = '' if block is None else block['app_hash'] + height = 0 if block is None else block['height'] + 1 + + known_validators = self.bigchaindb.get_validators() + validator_set = [vutils.decode_validator(v) + for v in genesis.validators] + + if known_validators and known_validators != validator_set: + self.log_abci_migration_error(known_chain['chain_id'], + known_validators) + sys.exit(1) + + block = Block(app_hash=app_hash, height=height, transactions=[]) + self.bigchaindb.store_block(block._asdict()) + self.bigchaindb.store_validator_set(height + 1, validator_set) + abci_chain_height = 0 if known_chain is None else known_chain['height'] + self.bigchaindb.store_abci_chain(abci_chain_height, + genesis.chain_id, True) + self.chain = {'height': abci_chain_height, 'is_synced': True, + 'chain_id': genesis.chain_id} + return self.abci.ResponseInitChain() + + def info(self, request): + """Return height of the latest committed block.""" + + self.abort_if_abci_chain_is_not_synced() + + # Check if Planetmint supports the Tendermint version + if not (hasattr(request, 'version') and tendermint_version_is_compatible(request.version)): + logger.error(f'Unsupported Tendermint version: {getattr(request, "version", "no version")}.' + f' Currently, Planetmint only supports {__tm_supported_versions__}. Exiting!') + sys.exit(1) + + logger.info(f"Tendermint version: {request.version}") + + r = self.abci.ResponseInfo() + block = self.bigchaindb.get_latest_block() + if block: + chain_shift = 0 if self.chain is None else self.chain['height'] + r.last_block_height = block['height'] - chain_shift + r.last_block_app_hash = block['app_hash'].encode('utf-8') + else: + r.last_block_height = 0 + r.last_block_app_hash = b'' + return r + + def check_tx(self, raw_transaction): + """Validate the transaction before entry into + the mempool. + + Args: + raw_tx: a raw string (in bytes) transaction. + """ + + self.abort_if_abci_chain_is_not_synced() + + logger.debug('check_tx: %s', raw_transaction) + transaction = decode_transaction(raw_transaction) + if self.bigchaindb.is_valid_transaction(transaction): + logger.debug('check_tx: VALID') + return self.abci.ResponseCheckTx(code=CodeTypeOk) + else: + logger.debug('check_tx: INVALID') + return self.abci.ResponseCheckTx(code=CodeTypeError) + + def begin_block(self, req_begin_block): + """Initialize list of transaction. + Args: + req_begin_block: block object which contains block header + and block hash. + """ + self.abort_if_abci_chain_is_not_synced() + + chain_shift = 0 if self.chain is None else self.chain['height'] + logger.debug('BEGIN BLOCK, height:%s, num_txs:%s', + req_begin_block.header.height + chain_shift, + req_begin_block.header.num_txs) + + self.block_txn_ids = [] + self.block_transactions = [] + return self.abci.ResponseBeginBlock() + + def deliver_tx(self, raw_transaction): + """Validate the transaction before mutating the state. + + Args: + raw_tx: a raw string (in bytes) transaction. + """ + + self.abort_if_abci_chain_is_not_synced() + + logger.debug('deliver_tx: %s', raw_transaction) + transaction = self.bigchaindb.is_valid_transaction( + decode_transaction(raw_transaction), self.block_transactions) + + if not transaction: + logger.debug('deliver_tx: INVALID') + return self.abci.ResponseDeliverTx(code=CodeTypeError) + else: + logger.debug('storing tx') + self.block_txn_ids.append(transaction.id) + self.block_transactions.append(transaction) + return self.abci.ResponseDeliverTx(code=CodeTypeOk) + + def end_block(self, request_end_block): + """Calculate block hash using transaction ids and previous block + hash to be stored in the next block. + + Args: + height (int): new height of the chain. + """ + + self.abort_if_abci_chain_is_not_synced() + + chain_shift = 0 if self.chain is None else self.chain['height'] + + height = request_end_block.height + chain_shift + self.new_height = height + + # store pre-commit state to recover in case there is a crash during + # `end_block` or `commit` + logger.debug(f'Updating pre-commit state: {self.new_height}') + pre_commit_state = dict(height=self.new_height, + transactions=self.block_txn_ids) + self.bigchaindb.store_pre_commit_state(pre_commit_state) + + block_txn_hash = calculate_hash(self.block_txn_ids) + block = self.bigchaindb.get_latest_block() + + if self.block_txn_ids: + self.block_txn_hash = calculate_hash([block['app_hash'], block_txn_hash]) + else: + self.block_txn_hash = block['app_hash'] + + validator_update = Election.process_block(self.bigchaindb, + self.new_height, + self.block_transactions) + + return self.abci.ResponseEndBlock(validator_updates=validator_update) + + def commit(self): + """Store the new height and along with block hash.""" + + self.abort_if_abci_chain_is_not_synced() + + data = self.block_txn_hash.encode('utf-8') + + # register a new block only when new transactions are received + if self.block_txn_ids: + self.bigchaindb.store_bulk_transactions(self.block_transactions) + + block = Block(app_hash=self.block_txn_hash, + height=self.new_height, + transactions=self.block_txn_ids) + # NOTE: storing the block should be the last operation during commit + # this effects crash recovery. Refer BEP#8 for details + self.bigchaindb.store_block(block._asdict()) + + logger.debug('Commit-ing new block with hash: apphash=%s ,' + 'height=%s, txn ids=%s', data, self.new_height, + self.block_txn_ids) + + if self.events_queue: + event = Event(EventTypes.BLOCK_VALID, { + 'height': self.new_height, + 'transactions': self.block_transactions + }) + self.events_queue.put(event) + + return self.abci.ResponseCommit(data=data) + + +def rollback(b): + pre_commit = b.get_pre_commit_state() + + if pre_commit is None: + # the pre_commit record is first stored in the first `end_block` + return + + latest_block = b.get_latest_block() + if latest_block is None: + logger.error('Found precommit state but no blocks!') + sys.exit(1) + + # NOTE: the pre-commit state is always at most 1 block ahead of the commited state + if latest_block['height'] < pre_commit['height']: + Election.rollback(b, pre_commit['height'], pre_commit['transactions']) + b.delete_transactions(pre_commit['transactions']) diff --git a/bigchaindb/elections/__init__.py b/bigchaindb/elections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/elections/election.py b/bigchaindb/elections/election.py new file mode 100644 index 0000000..5ed91c7 --- /dev/null +++ b/bigchaindb/elections/election.py @@ -0,0 +1,355 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 +from collections import OrderedDict + +import base58 +from uuid import uuid4 + +from bigchaindb import backend +from bigchaindb.elections.vote import Vote +from bigchaindb.common.exceptions import (InvalidSignature, + MultipleInputsError, + InvalidProposer, + UnequalValidatorSet, + DuplicateTransaction) +from bigchaindb.tendermint_utils import key_from_base64, public_key_to_base64 +from bigchaindb.common.crypto import (public_key_from_ed25519_key) +from bigchaindb.common.transaction import Transaction +from bigchaindb.common.schema import (_validate_schema, + TX_SCHEMA_COMMON, + TX_SCHEMA_CREATE) + + +class Election(Transaction): + """Represents election transactions. + + To implement a custom election, create a class deriving from this one + with OPERATION set to the election operation, ALLOWED_OPERATIONS + set to (OPERATION,), CREATE set to OPERATION. + """ + + OPERATION = None + # Custom validation schema + TX_SCHEMA_CUSTOM = None + # Election Statuses: + ONGOING = 'ongoing' + CONCLUDED = 'concluded' + INCONCLUSIVE = 'inconclusive' + # Vote ratio to approve an election + ELECTION_THRESHOLD = 2 / 3 + + @classmethod + def get_validator_change(cls, bigchain): + """Return the validator set from the most recent approved block + + :return: { + 'height': , + 'validators': + } + """ + latest_block = bigchain.get_latest_block() + if latest_block is None: + return None + return bigchain.get_validator_change(latest_block['height']) + + @classmethod + def get_validators(cls, bigchain, height=None): + """Return a dictionary of validators with key as `public_key` and + value as the `voting_power` + """ + validators = {} + for validator in bigchain.get_validators(height): + # NOTE: we assume that Tendermint encodes public key in base64 + public_key = public_key_from_ed25519_key(key_from_base64(validator['public_key']['value'])) + validators[public_key] = validator['voting_power'] + + return validators + + @classmethod + def recipients(cls, bigchain): + """Convert validator dictionary to a recipient list for `Transaction`""" + + recipients = [] + for public_key, voting_power in cls.get_validators(bigchain).items(): + recipients.append(([public_key], voting_power)) + + return recipients + + @classmethod + def is_same_topology(cls, current_topology, election_topology): + voters = {} + for voter in election_topology: + if len(voter.public_keys) > 1: + return False + + [public_key] = voter.public_keys + voting_power = voter.amount + voters[public_key] = voting_power + + # Check whether the voters and their votes is same to that of the + # validators and their voting power in the network + return current_topology == voters + + def validate(self, bigchain, current_transactions=[]): + """Validate election transaction + + NOTE: + * A valid election is initiated by an existing validator. + + * A valid election is one where voters are validators and votes are + allocated according to the voting power of each validator node. + + Args: + :param bigchain: (Planetmint) an instantiated bigchaindb.lib.BigchainDB object. + :param current_transactions: (list) A list of transactions to be validated along with the election + + Returns: + Election: a Election object or an object of the derived Election subclass. + + Raises: + ValidationError: If the election is invalid + """ + input_conditions = [] + + duplicates = any(txn for txn in current_transactions if txn.id == self.id) + if bigchain.is_committed(self.id) or duplicates: + raise DuplicateTransaction('transaction `{}` already exists' + .format(self.id)) + + if not self.inputs_valid(input_conditions): + raise InvalidSignature('Transaction signature is invalid.') + + current_validators = self.get_validators(bigchain) + + # NOTE: Proposer should be a single node + if len(self.inputs) != 1 or len(self.inputs[0].owners_before) != 1: + raise MultipleInputsError('`tx_signers` must be a list instance of length one') + + # NOTE: Check if the proposer is a validator. + [election_initiator_node_pub_key] = self.inputs[0].owners_before + if election_initiator_node_pub_key not in current_validators.keys(): + raise InvalidProposer('Public key is not a part of the validator set') + + # NOTE: Check if all validators have been assigned votes equal to their voting power + if not self.is_same_topology(current_validators, self.outputs): + raise UnequalValidatorSet('Validator set much be exactly same to the outputs of election') + + return self + + @classmethod + def generate(cls, initiator, voters, election_data, metadata=None): + # Break symmetry in case we need to call an election with the same properties twice + uuid = uuid4() + election_data['seed'] = str(uuid) + + (inputs, outputs) = cls.validate_create(initiator, voters, election_data, metadata) + election = cls(cls.OPERATION, {'data': election_data}, inputs, outputs, metadata) + cls.validate_schema(election.to_dict()) + return election + + @classmethod + def validate_schema(cls, tx): + """Validate the election transaction. Since `ELECTION` extends `CREATE` transaction, all the validations for + `CREATE` transaction should be inherited + """ + _validate_schema(TX_SCHEMA_COMMON, tx) + _validate_schema(TX_SCHEMA_CREATE, tx) + if cls.TX_SCHEMA_CUSTOM: + _validate_schema(cls.TX_SCHEMA_CUSTOM, tx) + + @classmethod + def create(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError + + @classmethod + def transfer(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError + + @classmethod + def to_public_key(cls, election_id): + return base58.b58encode(bytes.fromhex(election_id)).decode() + + @classmethod + def count_votes(cls, election_pk, transactions, getter=getattr): + votes = 0 + for txn in transactions: + if getter(txn, 'operation') == Vote.OPERATION: + for output in getter(txn, 'outputs'): + # NOTE: We enforce that a valid vote to election id will have only + # election_pk in the output public keys, including any other public key + # along with election_pk will lead to vote being not considered valid. + if len(getter(output, 'public_keys')) == 1 and [election_pk] == getter(output, 'public_keys'): + votes = votes + int(getter(output, 'amount')) + return votes + + def get_commited_votes(self, bigchain, election_pk=None): + if election_pk is None: + election_pk = self.to_public_key(self.id) + txns = list(backend.query.get_asset_tokens_for_public_key(bigchain.connection, + self.id, + election_pk)) + return self.count_votes(election_pk, txns, dict.get) + + def has_concluded(self, bigchain, current_votes=[]): + """Check if the election can be concluded or not. + + * Elections can only be concluded if the validator set has not changed + since the election was initiated. + * Elections can be concluded only if the current votes form a supermajority. + + Custom elections may override this function and introduce additional checks. + """ + if self.has_validator_set_changed(bigchain): + return False + + election_pk = self.to_public_key(self.id) + votes_committed = self.get_commited_votes(bigchain, election_pk) + votes_current = self.count_votes(election_pk, current_votes) + + total_votes = sum(output.amount for output in self.outputs) + if (votes_committed < (2/3) * total_votes) and \ + (votes_committed + votes_current >= (2/3)*total_votes): + return True + + return False + + def get_status(self, bigchain): + election = self.get_election(self.id, bigchain) + if election and election['is_concluded']: + return self.CONCLUDED + + return self.INCONCLUSIVE if self.has_validator_set_changed(bigchain) else self.ONGOING + + def has_validator_set_changed(self, bigchain): + latest_change = self.get_validator_change(bigchain) + if latest_change is None: + return False + + latest_change_height = latest_change['height'] + + election = self.get_election(self.id, bigchain) + + return latest_change_height > election['height'] + + def get_election(self, election_id, bigchain): + return bigchain.get_election(election_id) + + def store(self, bigchain, height, is_concluded): + bigchain.store_election(self.id, height, is_concluded) + + def show_election(self, bigchain): + data = self.asset['data'] + if 'public_key' in data.keys(): + data['public_key'] = public_key_to_base64(data['public_key']['value']) + response = '' + for k, v in data.items(): + if k != 'seed': + response += f'{k}={v}\n' + response += f'status={self.get_status(bigchain)}' + + return response + + @classmethod + def _get_initiated_elections(cls, height, txns): + elections = [] + for tx in txns: + if not isinstance(tx, Election): + continue + + elections.append({'election_id': tx.id, 'height': height, + 'is_concluded': False}) + return elections + + @classmethod + def _get_votes(cls, txns): + elections = OrderedDict() + for tx in txns: + if not isinstance(tx, Vote): + continue + + election_id = tx.asset['id'] + if election_id not in elections: + elections[election_id] = [] + elections[election_id].append(tx) + return elections + + @classmethod + def process_block(cls, bigchain, new_height, txns): + """Looks for election and vote transactions inside the block, records + and processes elections. + + Every election is recorded in the database. + + Every vote has a chance to conclude the corresponding election. When + an election is concluded, the corresponding database record is + marked as such. + + Elections and votes are processed in the order in which they + appear in the block. Elections are concluded in the order of + appearance of their first votes in the block. + + For every election concluded in the block, calls its `on_approval` + method. The returned value of the last `on_approval`, if any, + is a validator set update to be applied in one of the following blocks. + + `on_approval` methods are implemented by elections of particular type. + The method may contain side effects but should be idempotent. To account + for other concluded elections, if it requires so, the method should + rely on the database state. + """ + # elections initiated in this block + initiated_elections = cls._get_initiated_elections(new_height, txns) + + if initiated_elections: + bigchain.store_elections(initiated_elections) + + # elections voted for in this block and their votes + elections = cls._get_votes(txns) + + validator_update = None + for election_id, votes in elections.items(): + election = bigchain.get_transaction(election_id) + if election is None: + continue + + if not election.has_concluded(bigchain, votes): + continue + + validator_update = election.on_approval(bigchain, new_height) + election.store(bigchain, new_height, is_concluded=True) + + return [validator_update] if validator_update else [] + + @classmethod + def rollback(cls, bigchain, new_height, txn_ids): + """Looks for election and vote transactions inside the block and + cleans up the database artifacts possibly created in `process_blocks`. + + Part of the `end_block`/`commit` crash recovery. + """ + + # delete election records for elections initiated at this height and + # elections concluded at this height + bigchain.delete_elections(new_height) + + txns = [bigchain.get_transaction(tx_id) for tx_id in txn_ids] + + elections = cls._get_votes(txns) + for election_id in elections: + election = bigchain.get_transaction(election_id) + election.on_rollback(bigchain, new_height) + + def on_approval(self, bigchain, new_height): + """Override to update the database state according to the + election rules. Consider the current database state to account for + other concluded elections, if required. + """ + raise NotImplementedError + + def on_rollback(self, bigchain, new_height): + """Override to clean up the database artifacts possibly created + in `on_approval`. Part of the `end_block`/`commit` crash recovery. + """ + raise NotImplementedError diff --git a/bigchaindb/elections/vote.py b/bigchaindb/elections/vote.py new file mode 100644 index 0000000..1bc7c56 --- /dev/null +++ b/bigchaindb/elections/vote.py @@ -0,0 +1,64 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from bigchaindb.common.transaction import Transaction +from bigchaindb.common.schema import (_validate_schema, + TX_SCHEMA_COMMON, + TX_SCHEMA_TRANSFER, + TX_SCHEMA_VOTE) + + +class Vote(Transaction): + + OPERATION = 'VOTE' + # NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is + # overriden to re-use methods from parent class + TRANSFER = OPERATION + ALLOWED_OPERATIONS = (OPERATION,) + # Custom validation schema + TX_SCHEMA_CUSTOM = TX_SCHEMA_VOTE + + def validate(self, bigchain, current_transactions=[]): + """Validate election vote transaction + NOTE: There are no additional validity conditions on casting votes i.e. + a vote is just a valid TRANFER transaction + + For more details refer BEP-21: https://github.com/bigchaindb/BEPs/tree/master/21 + + Args: + bigchain (Planetmint): an instantiated bigchaindb.lib.BigchainDB object. + + Returns: + Vote: a Vote object + + Raises: + ValidationError: If the election vote is invalid + """ + self.validate_transfer_inputs(bigchain, current_transactions) + return self + + @classmethod + def generate(cls, inputs, recipients, election_id, metadata=None): + (inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata) + election_vote = cls(cls.OPERATION, {'id': election_id}, inputs, outputs, metadata) + cls.validate_schema(election_vote.to_dict()) + return election_vote + + @classmethod + def validate_schema(cls, tx): + """Validate the validator election vote transaction. Since `VOTE` extends `TRANSFER` + transaction, all the validations for `CREATE` transaction should be inherited + """ + _validate_schema(TX_SCHEMA_COMMON, tx) + _validate_schema(TX_SCHEMA_TRANSFER, tx) + _validate_schema(cls.TX_SCHEMA_CUSTOM, tx) + + @classmethod + def create(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError + + @classmethod + def transfer(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError diff --git a/bigchaindb/events.py b/bigchaindb/events.py new file mode 100644 index 0000000..b3e7f21 --- /dev/null +++ b/bigchaindb/events.py @@ -0,0 +1,109 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from queue import Empty +from collections import defaultdict +from multiprocessing import Queue + + +POISON_PILL = 'POISON_PILL' + + +class EventTypes: + """Container class that holds all the possible + events Planetmint manages. + """ + + # If you add a new Event Type, make sure to add it + # to the docs in docs/server/source/event-plugin-api.rst + ALL = ~0 + BLOCK_VALID = 1 + BLOCK_INVALID = 2 + # NEW_EVENT = 4 + # NEW_EVENT = 8 + # NEW_EVENT = 16... + + +class Event: + """An Event.""" + + def __init__(self, event_type, event_data): + """Creates a new event. + + Args: + event_type (int): the type of the event, see + :class:`~bigchaindb.events.EventTypes` + event_data (obj): the data of the event. + """ + + self.type = event_type + self.data = event_data + + +class Exchange: + """Dispatch events to subscribers.""" + + def __init__(self): + self.publisher_queue = Queue() + self.started_queue = Queue() + + # Map queues> + self.queues = defaultdict(list) + + def get_publisher_queue(self): + """Get the queue used by the publisher. + + Returns: + a :class:`multiprocessing.Queue`. + """ + + return self.publisher_queue + + def get_subscriber_queue(self, event_types=None): + """Create a new queue for a specific combination of event types + and return it. + + Returns: + a :class:`multiprocessing.Queue`. + Raises: + RuntimeError if called after `run` + """ + + try: + self.started_queue.get(timeout=1) + raise RuntimeError('Cannot create a new subscriber queue while Exchange is running.') + except Empty: + pass + + if event_types is None: + event_types = EventTypes.ALL + + queue = Queue() + self.queues[event_types].append(queue) + return queue + + def dispatch(self, event): + """Given an event, send it to all the subscribers. + + Args + event (:class:`~bigchaindb.events.EventTypes`): the event to + dispatch to all the subscribers. + """ + + for event_types, queues in self.queues.items(): + if event.type & event_types: + for queue in queues: + queue.put(event) + + def run(self): + """Start the exchange""" + self.started_queue.put('STARTED') + + while True: + event = self.publisher_queue.get() + if event == POISON_PILL: + return + else: + self.dispatch(event) diff --git a/bigchaindb/exceptions.py b/bigchaindb/exceptions.py new file mode 100644 index 0000000..9e12b7c --- /dev/null +++ b/bigchaindb/exceptions.py @@ -0,0 +1,12 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +class BigchainDBError(Exception): + """Base class for Planetmint exceptions.""" + + +class CriticalDoubleSpend(BigchainDBError): + """Data integrity error that requires attention""" diff --git a/bigchaindb/fastquery.py b/bigchaindb/fastquery.py new file mode 100644 index 0000000..93068f0 --- /dev/null +++ b/bigchaindb/fastquery.py @@ -0,0 +1,50 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from bigchaindb.utils import condition_details_has_owner +from bigchaindb.backend import query +from bigchaindb.common.transaction import TransactionLink + + +class FastQuery(): + """Database queries that join on block results from a single node.""" + + def __init__(self, connection): + self.connection = connection + + def get_outputs_by_public_key(self, public_key): + """Get outputs for a public key""" + txs = list(query.get_owned_ids(self.connection, public_key)) + return [TransactionLink(tx['id'], index) + for tx in txs + for index, output in enumerate(tx['outputs']) + if condition_details_has_owner(output['condition']['details'], + public_key)] + + def filter_spent_outputs(self, outputs): + """Remove outputs that have been spent + + Args: + outputs: list of TransactionLink + """ + links = [o.to_dict() for o in outputs] + txs = list(query.get_spending_transactions(self.connection, links)) + spends = {TransactionLink.from_dict(input_['fulfills']) + for tx in txs + for input_ in tx['inputs']} + return [ff for ff in outputs if ff not in spends] + + def filter_unspent_outputs(self, outputs): + """Remove outputs that have not been spent + + Args: + outputs: list of TransactionLink + """ + links = [o.to_dict() for o in outputs] + txs = list(query.get_spending_transactions(self.connection, links)) + spends = {TransactionLink.from_dict(input_['fulfills']) + for tx in txs + for input_ in tx['inputs']} + return [ff for ff in outputs if ff in spends] diff --git a/bigchaindb/lib.py b/bigchaindb/lib.py new file mode 100644 index 0000000..3033d79 --- /dev/null +++ b/bigchaindb/lib.py @@ -0,0 +1,514 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Module containing main contact points with Tendermint and +MongoDB. + +""" +import logging +from collections import namedtuple +from uuid import uuid4 + +import rapidjson + +try: + from hashlib import sha3_256 +except ImportError: + # NOTE: needed for Python < 3.6 + from sha3 import sha3_256 + +import requests + +import bigchaindb +from bigchaindb import backend, config_utils, fastquery +from bigchaindb.models import Transaction +from bigchaindb.common.exceptions import (SchemaValidationError, + ValidationError, + DoubleSpend) +from bigchaindb.common.transaction_mode_types import (BROADCAST_TX_COMMIT, + BROADCAST_TX_ASYNC, + BROADCAST_TX_SYNC) +from bigchaindb.tendermint_utils import encode_transaction, merkleroot +from bigchaindb import exceptions as core_exceptions +from bigchaindb.validation import BaseValidationRules + + +logger = logging.getLogger(__name__) + + +class BigchainDB(object): + """Bigchain API + + Create, read, sign, write transactions to the database + """ + + def __init__(self, connection=None): + """Initialize the Bigchain instance + + 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 come from an environment variable. + If that environment variable isn't set, then the value + will come from the local configuration file. And if that variable + isn't in the local configuration file, then the parameter will have + its default value (defined in bigchaindb.__init__). + + Args: + connection (:class:`~bigchaindb.backend.connection.Connection`): + A connection to the database. + """ + config_utils.autoconfigure() + self.mode_commit = BROADCAST_TX_COMMIT + self.mode_list = (BROADCAST_TX_ASYNC, + BROADCAST_TX_SYNC, + self.mode_commit) + self.tendermint_host = bigchaindb.config['tendermint']['host'] + self.tendermint_port = bigchaindb.config['tendermint']['port'] + self.endpoint = 'http://{}:{}/'.format(self.tendermint_host, self.tendermint_port) + + validationPlugin = bigchaindb.config.get('validation_plugin') + + if validationPlugin: + self.validation = config_utils.load_validation_plugin(validationPlugin) + else: + self.validation = BaseValidationRules + + self.connection = connection if connection else backend.connect(**bigchaindb.config['database']) + + def post_transaction(self, transaction, mode): + """Submit a valid transaction to the mempool.""" + if not mode or mode not in self.mode_list: + raise ValidationError('Mode must be one of the following {}.' + .format(', '.join(self.mode_list))) + + tx_dict = transaction.tx_dict if transaction.tx_dict else transaction.to_dict() + payload = { + 'method': mode, + 'jsonrpc': '2.0', + 'params': [encode_transaction(tx_dict)], + 'id': str(uuid4()) + } + # TODO: handle connection errors! + return requests.post(self.endpoint, json=payload) + + def write_transaction(self, transaction, mode): + # This method offers backward compatibility with the Web API. + """Submit a valid transaction to the mempool.""" + response = self.post_transaction(transaction, mode) + return self._process_post_response(response.json(), mode) + + def _process_post_response(self, response, mode): + logger.debug(response) + + error = response.get('error') + if error: + status_code = 500 + message = error.get('message', 'Internal Error') + data = error.get('data', '') + + if 'Tx already exists in cache' in data: + status_code = 400 + + return (status_code, message + ' - ' + data) + + result = response['result'] + if mode == self.mode_commit: + check_tx_code = result.get('check_tx', {}).get('code', 0) + deliver_tx_code = result.get('deliver_tx', {}).get('code', 0) + error_code = check_tx_code or deliver_tx_code + else: + error_code = result.get('code', 0) + + if error_code: + return (500, 'Transaction validation failed') + + return (202, '') + + def store_bulk_transactions(self, transactions): + txns = [] + assets = [] + txn_metadatas = [] + for t in transactions: + transaction = t.tx_dict if t.tx_dict else rapidjson.loads(rapidjson.dumps(t.to_dict())) + if transaction['operation'] == t.CREATE: + asset = transaction.pop('asset') + asset['id'] = transaction['id'] + assets.append(asset) + + metadata = transaction.pop('metadata') + txn_metadatas.append({'id': transaction['id'], + 'metadata': metadata}) + txns.append(transaction) + + backend.query.store_metadatas(self.connection, txn_metadatas) + if assets: + backend.query.store_assets(self.connection, assets) + return backend.query.store_transactions(self.connection, txns) + + def delete_transactions(self, txs): + return backend.query.delete_transactions(self.connection, txs) + + def update_utxoset(self, transaction): + """Update the UTXO set given ``transaction``. That is, remove + the outputs that the given ``transaction`` spends, and add the + outputs that the given ``transaction`` creates. + + Args: + transaction (:obj:`~bigchaindb.models.Transaction`): A new + transaction incoming into the system for which the UTXO + set needs to be updated. + """ + spent_outputs = [ + spent_output for spent_output in transaction.spent_outputs + ] + if spent_outputs: + self.delete_unspent_outputs(*spent_outputs) + self.store_unspent_outputs( + *[utxo._asdict() for utxo in transaction.unspent_outputs] + ) + + def store_unspent_outputs(self, *unspent_outputs): + """Store the given ``unspent_outputs`` (utxos). + + Args: + *unspent_outputs (:obj:`tuple` of :obj:`dict`): Variable + length tuple or list of unspent outputs. + """ + if unspent_outputs: + return backend.query.store_unspent_outputs( + self.connection, *unspent_outputs) + + def get_utxoset_merkle_root(self): + """Returns the merkle root of the utxoset. This implies that + the utxoset is first put into a merkle tree. + + For now, the merkle tree and its root will be computed each + time. This obviously is not efficient and a better approach + that limits the repetition of the same computation when + unnecesary should be sought. For instance, future optimizations + could simply re-compute the branches of the tree that were + affected by a change. + + The transaction hash (id) and output index should be sufficient + to uniquely identify a utxo, and consequently only that + information from a utxo record is needed to compute the merkle + root. Hence, each node of the merkle tree should contain the + tuple (txid, output_index). + + .. important:: The leaves of the tree will need to be sorted in + some kind of lexicographical order. + + Returns: + str: Merkle root in hexadecimal form. + """ + utxoset = backend.query.get_unspent_outputs(self.connection) + # TODO Once ready, use the already pre-computed utxo_hash field. + # See common/transactions.py for details. + hashes = [ + sha3_256( + '{}{}'.format(utxo['transaction_id'], utxo['output_index']).encode() + ).digest() for utxo in utxoset + ] + # TODO Notice the sorted call! + return merkleroot(sorted(hashes)) + + def get_unspent_outputs(self): + """Get the utxoset. + + Returns: + generator of unspent_outputs. + """ + cursor = backend.query.get_unspent_outputs(self.connection) + return (record for record in cursor) + + def delete_unspent_outputs(self, *unspent_outputs): + """Deletes the given ``unspent_outputs`` (utxos). + + Args: + *unspent_outputs (:obj:`tuple` of :obj:`dict`): Variable + length tuple or list of unspent outputs. + """ + if unspent_outputs: + return backend.query.delete_unspent_outputs( + self.connection, *unspent_outputs) + + def is_committed(self, transaction_id): + transaction = backend.query.get_transaction(self.connection, transaction_id) + return bool(transaction) + + def get_transaction(self, transaction_id): + transaction = backend.query.get_transaction(self.connection, transaction_id) + + if transaction: + asset = backend.query.get_asset(self.connection, transaction_id) + metadata = backend.query.get_metadata(self.connection, [transaction_id]) + if asset: + transaction['asset'] = asset + + if 'metadata' not in transaction: + metadata = metadata[0] if metadata else None + if metadata: + metadata = metadata.get('metadata') + + transaction.update({'metadata': metadata}) + + transaction = Transaction.from_dict(transaction) + + return transaction + + def get_transactions(self, txn_ids): + return backend.query.get_transactions(self.connection, txn_ids) + + def get_transactions_filtered(self, asset_id, operation=None, last_tx=None): + """Get a list of transactions filtered on some criteria + """ + txids = backend.query.get_txids_filtered(self.connection, asset_id, + operation, last_tx) + for txid in txids: + yield self.get_transaction(txid) + + def get_outputs_filtered(self, owner, spent=None): + """Get a list of output links filtered on some criteria + + Args: + owner (str): base58 encoded public_key. + spent (bool): If ``True`` return only the spent outputs. If + ``False`` return only unspent outputs. If spent is + not specified (``None``) return all outputs. + + Returns: + :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s + pointing to another transaction's condition + """ + outputs = self.fastquery.get_outputs_by_public_key(owner) + if spent is None: + return outputs + elif spent is True: + return self.fastquery.filter_unspent_outputs(outputs) + elif spent is False: + return self.fastquery.filter_spent_outputs(outputs) + + def get_spent(self, txid, output, current_transactions=[]): + transactions = backend.query.get_spent(self.connection, txid, + output) + transactions = list(transactions) if transactions else [] + if len(transactions) > 1: + raise core_exceptions.CriticalDoubleSpend( + '`{}` was spent more than once. There is a problem' + ' with the chain'.format(txid)) + + current_spent_transactions = [] + for ctxn in current_transactions: + for ctxn_input in ctxn.inputs: + if ctxn_input.fulfills and\ + ctxn_input.fulfills.txid == txid and\ + ctxn_input.fulfills.output == output: + current_spent_transactions.append(ctxn) + + transaction = None + if len(transactions) + len(current_spent_transactions) > 1: + raise DoubleSpend('tx "{}" spends inputs twice'.format(txid)) + elif transactions: + transaction = Transaction.from_db(self, transactions[0]) + elif current_spent_transactions: + transaction = current_spent_transactions[0] + + return transaction + + def store_block(self, block): + """Create a new block.""" + + return backend.query.store_block(self.connection, block) + + def get_latest_block(self): + """Get the block with largest height.""" + + return backend.query.get_latest_block(self.connection) + + def get_block(self, block_id): + """Get the block with the specified `block_id`. + + Returns the block corresponding to `block_id` or None if no match is + found. + + Args: + block_id (int): block id of the block to get. + """ + + block = backend.query.get_block(self.connection, block_id) + latest_block = self.get_latest_block() + latest_block_height = latest_block['height'] if latest_block else 0 + + if not block and block_id > latest_block_height: + return + + result = {'height': block_id, + 'transactions': []} + + if block: + transactions = backend.query.get_transactions(self.connection, block['transactions']) + result['transactions'] = [t.to_dict() for t in Transaction.from_db(self, transactions)] + + return result + + def get_block_containing_tx(self, txid): + """Retrieve the list of blocks (block ids) containing a + transaction with transaction id `txid` + + Args: + txid (str): transaction id of the transaction to query + + Returns: + Block id list (list(int)) + """ + blocks = list(backend.query.get_block_with_transaction(self.connection, txid)) + if len(blocks) > 1: + logger.critical('Transaction id %s exists in multiple blocks', txid) + + return [block['height'] for block in blocks] + + def validate_transaction(self, tx, current_transactions=[]): + """Validate a transaction against the current status of the database.""" + + transaction = tx + + # CLEANUP: The conditional below checks for transaction in dict format. + # It would be better to only have a single format for the transaction + # throught the code base. + if isinstance(transaction, dict): + try: + transaction = Transaction.from_dict(tx) + except SchemaValidationError as e: + logger.warning('Invalid transaction schema: %s', e.__cause__.message) + return False + except ValidationError as e: + logger.warning('Invalid transaction (%s): %s', type(e).__name__, e) + return False + return transaction.validate(self, current_transactions) + + def is_valid_transaction(self, tx, current_transactions=[]): + # NOTE: the function returns the Transaction object in case + # the transaction is valid + try: + return self.validate_transaction(tx, current_transactions) + except ValidationError as e: + logger.warning('Invalid transaction (%s): %s', type(e).__name__, e) + return False + + def text_search(self, search, *, limit=0, table='assets'): + """Return an iterator of assets that match the text search + + Args: + search (str): Text search string to query the text index + limit (int, optional): Limit the number of returned documents. + + Returns: + iter: An iterator of assets that match the text search. + """ + return backend.query.text_search(self.connection, search, limit=limit, + table=table) + + def get_assets(self, asset_ids): + """Return a list of assets that match the asset_ids + + Args: + asset_ids (:obj:`list` of :obj:`str`): A list of asset_ids to + retrieve from the database. + + Returns: + list: The list of assets returned from the database. + """ + return backend.query.get_assets(self.connection, asset_ids) + + def get_metadata(self, txn_ids): + """Return a list of metadata that match the transaction ids (txn_ids) + + Args: + txn_ids (:obj:`list` of :obj:`str`): A list of txn_ids to + retrieve from the database. + + Returns: + list: The list of metadata returned from the database. + """ + return backend.query.get_metadata(self.connection, txn_ids) + + @property + def fastquery(self): + return fastquery.FastQuery(self.connection) + + def get_validator_change(self, height=None): + return backend.query.get_validator_set(self.connection, height) + + def get_validators(self, height=None): + result = self.get_validator_change(height) + return [] if result is None else result['validators'] + + def get_election(self, election_id): + return backend.query.get_election(self.connection, election_id) + + def get_pre_commit_state(self): + return backend.query.get_pre_commit_state(self.connection) + + def store_pre_commit_state(self, state): + return backend.query.store_pre_commit_state(self.connection, state) + + def store_validator_set(self, height, validators): + """Store validator set at a given `height`. + NOTE: If the validator set already exists at that `height` then an + exception will be raised. + """ + return backend.query.store_validator_set(self.connection, {'height': height, + 'validators': validators}) + + def delete_validator_set(self, height): + return backend.query.delete_validator_set(self.connection, height) + + def store_abci_chain(self, height, chain_id, is_synced=True): + return backend.query.store_abci_chain(self.connection, height, + chain_id, is_synced) + + def delete_abci_chain(self, height): + return backend.query.delete_abci_chain(self.connection, height) + + def get_latest_abci_chain(self): + return backend.query.get_latest_abci_chain(self.connection) + + def migrate_abci_chain(self): + """Generate and record a new ABCI chain ID. New blocks are not + accepted until we receive an InitChain ABCI request with + the matching chain ID and validator set. + + Chain ID is generated based on the current chain and height. + `chain-X` => `chain-X-migrated-at-height-5`. + `chain-X-migrated-at-height-5` => `chain-X-migrated-at-height-21`. + + If there is no known chain (we are at genesis), the function returns. + """ + latest_chain = self.get_latest_abci_chain() + if latest_chain is None: + return + + block = self.get_latest_block() + + suffix = '-migrated-at-height-' + chain_id = latest_chain['chain_id'] + block_height_str = str(block['height']) + new_chain_id = chain_id.split(suffix)[0] + suffix + block_height_str + + self.store_abci_chain(block['height'] + 1, new_chain_id, False) + + def store_election(self, election_id, height, is_concluded): + return backend.query.store_election(self.connection, election_id, + height, is_concluded) + + def store_elections(self, elections): + return backend.query.store_elections(self.connection, elections) + + def delete_elections(self, height): + return backend.query.delete_elections(self.connection, height) + + +Block = namedtuple('Block', ('app_hash', 'height', 'transactions')) diff --git a/bigchaindb/log.py b/bigchaindb/log.py new file mode 100644 index 0000000..54755bb --- /dev/null +++ b/bigchaindb/log.py @@ -0,0 +1,130 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import bigchaindb +import logging + +from bigchaindb.common.exceptions import ConfigurationError +from logging.config import dictConfig as set_logging_config +import os + + +DEFAULT_LOG_DIR = os.getcwd() + +DEFAULT_LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'console': { + 'class': 'logging.Formatter', + 'format': ('[%(asctime)s] [%(levelname)s] (%(name)s) ' + '%(message)s (%(processName)-10s - pid: %(process)d)'), + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + 'file': { + 'class': 'logging.Formatter', + 'format': ('[%(asctime)s] [%(levelname)s] (%(name)s) ' + '%(message)s (%(processName)-10s - pid: %(process)d)'), + 'datefmt': '%Y-%m-%d %H:%M:%S', + } + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'console', + 'level': logging.INFO, + }, + 'file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(DEFAULT_LOG_DIR, 'bigchaindb.log'), + 'mode': 'w', + 'maxBytes': 209715200, + 'backupCount': 5, + 'formatter': 'file', + 'level': logging.INFO, + }, + 'errors': { + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(DEFAULT_LOG_DIR, 'bigchaindb-errors.log'), + 'mode': 'w', + 'maxBytes': 209715200, + 'backupCount': 5, + 'formatter': 'file', + 'level': logging.ERROR, + } + }, + 'loggers': {}, + 'root': { + 'level': logging.DEBUG, + 'handlers': ['console', 'file', 'errors'], + }, +} + + +def _normalize_log_level(level): + try: + return level.upper() + except AttributeError as exc: + raise ConfigurationError('Log level must be a string!') from exc + + +def setup_logging(): + """Function to configure log hadlers. + + .. important:: + + Configuration, if needed, should be applied before invoking this + decorator, as starting the subscriber process for logging will + configure the root logger for the child process based on the + state of :obj:`bigchaindb.config` at the moment this decorator + is invoked. + + """ + + logging_configs = DEFAULT_LOGGING_CONFIG + new_logging_configs = bigchaindb.config['log'] + + if 'file' in new_logging_configs: + filename = new_logging_configs['file'] + logging_configs['handlers']['file']['filename'] = filename + + if 'error_file' in new_logging_configs: + error_filename = new_logging_configs['error_file'] + logging_configs['handlers']['errors']['filename'] = error_filename + + if 'level_console' in new_logging_configs: + level = _normalize_log_level(new_logging_configs['level_console']) + logging_configs['handlers']['console']['level'] = level + + if 'level_logfile' in new_logging_configs: + level = _normalize_log_level(new_logging_configs['level_logfile']) + logging_configs['handlers']['file']['level'] = level + + if 'fmt_console' in new_logging_configs: + fmt = new_logging_configs['fmt_console'] + logging_configs['formatters']['console']['format'] = fmt + + if 'fmt_logfile' in new_logging_configs: + fmt = new_logging_configs['fmt_logfile'] + logging_configs['formatters']['file']['format'] = fmt + + if 'datefmt_console' in new_logging_configs: + fmt = new_logging_configs['datefmt_console'] + logging_configs['formatters']['console']['datefmt'] = fmt + + if 'datefmt_logfile' in new_logging_configs: + fmt = new_logging_configs['datefmt_logfile'] + logging_configs['formatters']['file']['datefmt'] = fmt + + log_levels = new_logging_configs.get('granular_levels', {}) + + for logger_name, level in log_levels.items(): + level = _normalize_log_level(level) + try: + logging_configs['loggers'][logger_name]['level'] = level + except KeyError: + logging_configs['loggers'][logger_name] = {'level': level} + + set_logging_config(logging_configs) diff --git a/bigchaindb/migrations/__init__.py b/bigchaindb/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/migrations/chain_migration_election.py b/bigchaindb/migrations/chain_migration_election.py new file mode 100644 index 0000000..119392c --- /dev/null +++ b/bigchaindb/migrations/chain_migration_election.py @@ -0,0 +1,48 @@ +import json + +from bigchaindb.common.schema import TX_SCHEMA_CHAIN_MIGRATION_ELECTION +from bigchaindb.elections.election import Election + + +class ChainMigrationElection(Election): + + OPERATION = 'CHAIN_MIGRATION_ELECTION' + CREATE = OPERATION + ALLOWED_OPERATIONS = (OPERATION,) + TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION + + def has_concluded(self, bigchaindb, *args, **kwargs): + chain = bigchaindb.get_latest_abci_chain() + if chain is not None and not chain['is_synced']: + # do not conclude the migration election if + # there is another migration in progress + return False + + return super().has_concluded(bigchaindb, *args, **kwargs) + + def on_approval(self, bigchain, *args, **kwargs): + bigchain.migrate_abci_chain() + + def show_election(self, bigchain): + output = super().show_election(bigchain) + chain = bigchain.get_latest_abci_chain() + if chain is None or chain['is_synced']: + return output + + output += f'\nchain_id={chain["chain_id"]}' + block = bigchain.get_latest_block() + output += f'\napp_hash={block["app_hash"]}' + validators = [ + { + 'pub_key': { + 'type': 'tendermint/PubKeyEd25519', + 'value': k, + }, + 'power': v, + } for k, v in self.get_validators(bigchain).items() + ] + output += f'\nvalidators={json.dumps(validators, indent=4)}' + return output + + def on_rollback(self, bigchain, new_height): + bigchain.delete_abci_chain(new_height) diff --git a/bigchaindb/models.py b/bigchaindb/models.py new file mode 100644 index 0000000..4a0c30e --- /dev/null +++ b/bigchaindb/models.py @@ -0,0 +1,75 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from bigchaindb.backend.schema import validate_language_key +from bigchaindb.common.exceptions import (InvalidSignature, + DuplicateTransaction) +from bigchaindb.common.schema import validate_transaction_schema +from bigchaindb.common.transaction import Transaction +from bigchaindb.common.utils import (validate_txn_obj, validate_key) + + +class Transaction(Transaction): + ASSET = 'asset' + METADATA = 'metadata' + DATA = 'data' + + def validate(self, bigchain, current_transactions=[]): + """Validate transaction spend + Args: + bigchain (Planetmint): an instantiated bigchaindb.Planetmint object. + Returns: + The transaction (Transaction) if the transaction is valid else it + raises an exception describing the reason why the transaction is + invalid. + Raises: + ValidationError: If the transaction is invalid + """ + input_conditions = [] + + if self.operation == Transaction.CREATE: + duplicates = any(txn for txn in current_transactions if txn.id == self.id) + if bigchain.is_committed(self.id) or duplicates: + raise DuplicateTransaction('transaction `{}` already exists' + .format(self.id)) + + if not self.inputs_valid(input_conditions): + raise InvalidSignature('Transaction signature is invalid.') + + elif self.operation == Transaction.TRANSFER: + self.validate_transfer_inputs(bigchain, current_transactions) + + return self + + @classmethod + def from_dict(cls, tx_body): + return super().from_dict(tx_body, False) + + @classmethod + def validate_schema(cls, tx_body): + validate_transaction_schema(tx_body) + validate_txn_obj(cls.ASSET, tx_body[cls.ASSET], cls.DATA, validate_key) + validate_txn_obj(cls.METADATA, tx_body, cls.METADATA, validate_key) + validate_language_key(tx_body[cls.ASSET], cls.DATA) + validate_language_key(tx_body, cls.METADATA) + + +class FastTransaction: + """A minimal wrapper around a transaction dictionary. This is useful for + when validation is not required but a routine expects something that looks + like a transaction, for example during block creation. + + Note: immutability could also be provided + """ + + def __init__(self, tx_dict): + self.data = tx_dict + + @property + def id(self): + return self.data['id'] + + def to_dict(self): + return self.data diff --git a/bigchaindb/parallel_validation.py b/bigchaindb/parallel_validation.py new file mode 100644 index 0000000..ae45798 --- /dev/null +++ b/bigchaindb/parallel_validation.py @@ -0,0 +1,124 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import multiprocessing as mp +from collections import defaultdict + +from bigchaindb import App, BigchainDB +from bigchaindb.tendermint_utils import decode_transaction +from abci import CodeTypeOk + + +class ParallelValidationApp(App): + def __init__(self, bigchaindb=None, events_queue=None, abci=None): + super().__init__(bigchaindb, events_queue, abci=abci) + self.parallel_validator = ParallelValidator() + self.parallel_validator.start() + + def check_tx(self, raw_transaction): + return self.abci.ResponseCheckTx(code=CodeTypeOk) + + def deliver_tx(self, raw_transaction): + self.parallel_validator.validate(raw_transaction) + return self.abci.ResponseDeliverTx(code=CodeTypeOk) + + def end_block(self, request_end_block): + result = self.parallel_validator.result(timeout=30) + for transaction in result: + if transaction: + self.block_txn_ids.append(transaction.id) + self.block_transactions.append(transaction) + + return super().end_block(request_end_block) + + +RESET = 'reset' +EXIT = 'exit' + + +class ParallelValidator: + def __init__(self, number_of_workers=mp.cpu_count()): + self.number_of_workers = number_of_workers + self.transaction_index = 0 + self.routing_queues = [mp.Queue() for _ in range(self.number_of_workers)] + self.workers = [] + self.results_queue = mp.Queue() + + def start(self): + for routing_queue in self.routing_queues: + worker = ValidationWorker(routing_queue, self.results_queue) + process = mp.Process(target=worker.run) + process.start() + self.workers.append(process) + + def stop(self): + for routing_queue in self.routing_queues: + routing_queue.put(EXIT) + + def validate(self, raw_transaction): + dict_transaction = decode_transaction(raw_transaction) + index = int(dict_transaction['id'], 16) % self.number_of_workers + self.routing_queues[index].put((self.transaction_index, dict_transaction)) + self.transaction_index += 1 + + def result(self, timeout=None): + result_buffer = [None] * self.transaction_index + for _ in range(self.transaction_index): + index, transaction = self.results_queue.get(timeout=timeout) + result_buffer[index] = transaction + self.transaction_index = 0 + for routing_queue in self.routing_queues: + routing_queue.put(RESET) + return result_buffer + + +class ValidationWorker: + """Run validation logic in a loop. This Worker is suitable for a Process + life: no thrills, just a queue to get some values, and a queue to return results. + + Note that a worker is expected to validate multiple transactions in + multiple rounds, and it needs to keep in memory all transactions already + validated, until a new round starts. To trigger a new round of validation, + a ValidationWorker expects a `RESET` message. To exit the infinite loop the + worker is in, it expects an `EXIT` message. + """ + + def __init__(self, in_queue, results_queue): + self.in_queue = in_queue + self.results_queue = results_queue + self.bigchaindb = BigchainDB() + self.reset() + + def reset(self): + # We need a place to store already validated transactions, + # in case of dependant transactions in the same block. + # `validated_transactions` maps an `asset_id` with the list + # of all other transactions sharing the same asset. + self.validated_transactions = defaultdict(list) + + def validate(self, dict_transaction): + try: + asset_id = dict_transaction['asset']['id'] + except KeyError: + asset_id = dict_transaction['id'] + + transaction = self.bigchaindb.is_valid_transaction( + dict_transaction, + self.validated_transactions[asset_id]) + + if transaction: + self.validated_transactions[asset_id].append(transaction) + return transaction + + def run(self): + while True: + message = self.in_queue.get() + if message == RESET: + self.reset() + elif message == EXIT: + return + else: + index, transaction = message + self.results_queue.put((index, self.validate(transaction))) diff --git a/bigchaindb/start.py b/bigchaindb/start.py new file mode 100644 index 0000000..d9b7935 --- /dev/null +++ b/bigchaindb/start.py @@ -0,0 +1,90 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import logging +import setproctitle + +from abci import TmVersion, ABCI + +import bigchaindb +from bigchaindb.lib import BigchainDB +from bigchaindb.core import App +from bigchaindb.parallel_validation import ParallelValidationApp +from bigchaindb.web import server, websocket_server +from bigchaindb.events import Exchange, EventTypes +from bigchaindb.utils import Process + + +logger = logging.getLogger(__name__) + +BANNER = """ +**************************************************************************** +* * +* Planetmint 2.2.2 * +* codename "jumping sloth" * +* Initialization complete. Planetmint Server is ready and waiting. * +* * +* You can send HTTP requests via the HTTP API documented in the * +* Planetmint Server docs at: * +* https://bigchaindb.com/http-api * +* * +* Listening to client connections on: {:<15} * +* * +**************************************************************************** +""" + + +def start(args): + # Exchange object for event stream api + logger.info('Starting Planetmint') + exchange = Exchange() + # start the web api + app_server = server.create_server( + settings=bigchaindb.config['server'], + log_config=bigchaindb.config['log'], + bigchaindb_factory=BigchainDB) + p_webapi = Process(name='bigchaindb_webapi', target=app_server.run, daemon=True) + p_webapi.start() + + logger.info(BANNER.format(bigchaindb.config['server']['bind'])) + + # start websocket server + p_websocket_server = Process(name='bigchaindb_ws', + target=websocket_server.start, + daemon=True, + args=(exchange.get_subscriber_queue(EventTypes.BLOCK_VALID),)) + p_websocket_server.start() + + p_exchange = Process(name='bigchaindb_exchange', target=exchange.run, daemon=True) + p_exchange.start() + + # We need to import this after spawning the web server + # because import ABCIServer will monkeypatch all sockets + # for gevent. + from abci.server import ABCIServer + + setproctitle.setproctitle('bigchaindb') + + # Start the ABCIServer + abci = ABCI(TmVersion(bigchaindb.config['tendermint']['version'])) + if args.experimental_parallel_validation: + app = ABCIServer( + app=ParallelValidationApp( + abci=abci.types, + events_queue=exchange.get_publisher_queue(), + ) + ) + else: + app = ABCIServer( + app=App( + abci=abci.types, + events_queue=exchange.get_publisher_queue(), + ) + ) + app.run() + + +if __name__ == '__main__': + start() diff --git a/bigchaindb/tendermint_utils.py b/bigchaindb/tendermint_utils.py new file mode 100644 index 0000000..f613b63 --- /dev/null +++ b/bigchaindb/tendermint_utils.py @@ -0,0 +1,100 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import base64 +import hashlib +import json +from binascii import hexlify + +try: + from hashlib import sha3_256 +except ImportError: + from sha3 import sha3_256 + + +def encode_transaction(value): + """Encode a transaction (dict) to Base64.""" + + return base64.b64encode(json.dumps(value).encode('utf8')).decode('utf8') + + +def decode_transaction(raw): + """Decode a transaction from bytes to a dict.""" + + return json.loads(raw.decode('utf8')) + + +def decode_transaction_base64(value): + """Decode a transaction from Base64.""" + + return json.loads(base64.b64decode(value.encode('utf8')).decode('utf8')) + + +def calculate_hash(key_list): + if not key_list: + return '' + + full_hash = sha3_256() + for key in key_list: + full_hash.update(key.encode('utf8')) + + return full_hash.hexdigest() + + +def merkleroot(hashes): + """Computes the merkle root for a given list. + + Args: + hashes (:obj:`list` of :obj:`bytes`): The leaves of the tree. + + Returns: + str: Merkle root in hexadecimal form. + + """ + # XXX TEMPORARY -- MUST REVIEW and possibly CHANGE + # The idea here is that the UTXO SET would be empty and this function + # would be invoked to compute the merkle root, and since there is nothing, + # i.e. an empty list, then the hash of the empty string is returned. + # This seems too easy but maybe that is good enough? TO REVIEW! + if not hashes: + return sha3_256(b'').hexdigest() + # XXX END TEMPORARY -- MUST REVIEW ... + if len(hashes) == 1: + return hexlify(hashes[0]).decode() + if len(hashes) % 2 == 1: + hashes.append(hashes[-1]) + parent_hashes = [ + sha3_256(hashes[i] + hashes[i+1]).digest() + for i in range(0, len(hashes)-1, 2) + ] + return merkleroot(parent_hashes) + + +def public_key64_to_address(base64_public_key): + """Note this only compatible with Tendermint 0.19.x""" + ed25519_public_key = public_key_from_base64(base64_public_key) + encoded_public_key = amino_encoded_public_key(ed25519_public_key) + return hashlib.new('ripemd160', encoded_public_key).hexdigest().upper() + + +def public_key_from_base64(base64_public_key): + return key_from_base64(base64_public_key) + + +def key_from_base64(base64_key): + return base64.b64decode(base64_key).hex().upper() + + +def public_key_to_base64(ed25519_public_key): + return key_to_base64(ed25519_public_key) + + +def key_to_base64(ed25519_key): + ed25519_key = bytes.fromhex(ed25519_key) + return base64.b64encode(ed25519_key).decode('utf-8') + + +def amino_encoded_public_key(ed25519_public_key): + return bytes.fromhex('1624DE6220{}'.format(ed25519_public_key)) diff --git a/bigchaindb/upsert_validator/__init__.py b/bigchaindb/upsert_validator/__init__.py new file mode 100644 index 0000000..903bb6b --- /dev/null +++ b/bigchaindb/upsert_validator/__init__.py @@ -0,0 +1,7 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py new file mode 100644 index 0000000..703eb89 --- /dev/null +++ b/bigchaindb/upsert_validator/validator_election.py @@ -0,0 +1,68 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from bigchaindb.common.exceptions import InvalidPowerChange +from bigchaindb.elections.election import Election +from bigchaindb.common.schema import TX_SCHEMA_VALIDATOR_ELECTION +from .validator_utils import (new_validator_set, encode_validator, validate_asset_public_key) + + +class ValidatorElection(Election): + + OPERATION = 'VALIDATOR_ELECTION' + # NOTE: this transaction class extends create so the operation inheritence is achieved + # by renaming CREATE to VALIDATOR_ELECTION + CREATE = OPERATION + ALLOWED_OPERATIONS = (OPERATION,) + TX_SCHEMA_CUSTOM = TX_SCHEMA_VALIDATOR_ELECTION + + def validate(self, bigchain, current_transactions=[]): + """For more details refer BEP-21: https://github.com/bigchaindb/BEPs/tree/master/21 + """ + + current_validators = self.get_validators(bigchain) + + super(ValidatorElection, self).validate(bigchain, current_transactions=current_transactions) + + # NOTE: change more than 1/3 of the current power is not allowed + if self.asset['data']['power'] >= (1/3)*sum(current_validators.values()): + raise InvalidPowerChange('`power` change must be less than 1/3 of total power') + + return self + + @classmethod + def validate_schema(cls, tx): + super(ValidatorElection, cls).validate_schema(tx) + validate_asset_public_key(tx['asset']['data']['public_key']) + + def has_concluded(self, bigchain, *args, **kwargs): + latest_block = bigchain.get_latest_block() + if latest_block is not None: + latest_block_height = latest_block['height'] + latest_validator_change = bigchain.get_validator_change()['height'] + + # TODO change to `latest_block_height + 3` when upgrading to Tendermint 0.24.0. + if latest_validator_change == latest_block_height + 2: + # do not conclude the election if there is a change assigned already + return False + + return super().has_concluded(bigchain, *args, **kwargs) + + def on_approval(self, bigchain, new_height): + validator_updates = [self.asset['data']] + curr_validator_set = bigchain.get_validators(new_height) + updated_validator_set = new_validator_set(curr_validator_set, + validator_updates) + + updated_validator_set = [v for v in updated_validator_set + if v['voting_power'] > 0] + + # TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0. + bigchain.store_validator_set(new_height + 1, updated_validator_set) + return encode_validator(self.asset['data']) + + def on_rollback(self, bigchaindb, new_height): + # TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0. + bigchaindb.delete_validator_set(new_height + 1) diff --git a/bigchaindb/upsert_validator/validator_utils.py b/bigchaindb/upsert_validator/validator_utils.py new file mode 100644 index 0000000..30bec43 --- /dev/null +++ b/bigchaindb/upsert_validator/validator_utils.py @@ -0,0 +1,85 @@ +import base64 +import binascii +import codecs + +import bigchaindb +from abci import types_v0_22_8, types_v0_31_5, TmVersion +from bigchaindb.common.exceptions import InvalidPublicKey, BigchainDBError + + +def encode_validator(v): + ed25519_public_key = v['public_key']['value'] + # NOTE: tendermint expects public to be encoded in go-amino format + try: + version = TmVersion(bigchaindb.config["tendermint"]["version"]) + except ValueError: + raise BigchainDBError('Invalid tendermint version, ' + 'check Planetmint configuration file') + + validator_update_t, pubkey_t = { + TmVersion.v0_22_8: (types_v0_22_8.Validator, types_v0_22_8.PubKey), + TmVersion.v0_31_5: (types_v0_31_5.ValidatorUpdate, types_v0_31_5.PubKey) + }[version] + pub_key = pubkey_t(type='ed25519', data=bytes.fromhex(ed25519_public_key)) + + return validator_update_t(pub_key=pub_key, power=v['power']) + + +def decode_validator(v): + return {'public_key': {'type': 'ed25519-base64', + 'value': codecs.encode(v.pub_key.data, 'base64').decode().rstrip('\n')}, + 'voting_power': v.power} + + +def new_validator_set(validators, updates): + validators_dict = {} + for v in validators: + validators_dict[v['public_key']['value']] = v + + updates_dict = {} + for u in updates: + decoder = get_public_key_decoder(u['public_key']) + public_key64 = base64.b64encode(decoder(u['public_key']['value'])).decode('utf-8') + updates_dict[public_key64] = {'public_key': {'type': 'ed25519-base64', + 'value': public_key64}, + 'voting_power': u['power']} + + new_validators_dict = {**validators_dict, **updates_dict} + return list(new_validators_dict.values()) + + +def encode_pk_to_base16(validator): + pk = validator['public_key'] + decoder = get_public_key_decoder(pk) + public_key16 = base64.b16encode(decoder(pk['value'])).decode('utf-8') + + validator['public_key']['value'] = public_key16 + return validator + + +def validate_asset_public_key(pk): + pk_binary = pk['value'].encode('utf-8') + decoder = get_public_key_decoder(pk) + try: + pk_decoded = decoder(pk_binary) + if len(pk_decoded) != 32: + raise InvalidPublicKey('Public key should be of size 32 bytes') + + except binascii.Error: + raise InvalidPublicKey('Invalid `type` specified for public key `value`') + + +def get_public_key_decoder(pk): + encoding = pk['type'] + decoder = base64.b64decode + + if encoding == 'ed25519-base16': + decoder = base64.b16decode + elif encoding == 'ed25519-base32': + decoder = base64.b32decode + elif encoding == 'ed25519-base64': + decoder = base64.b64decode + else: + raise InvalidPublicKey('Invalid `type` specified for public key `value`') + + return decoder diff --git a/bigchaindb/utils.py b/bigchaindb/utils.py new file mode 100644 index 0000000..789afed --- /dev/null +++ b/bigchaindb/utils.py @@ -0,0 +1,209 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import contextlib +import threading +import queue +import multiprocessing as mp +import json + +import setproctitle +from packaging import version +from bigchaindb.version import __tm_supported_versions__ +from bigchaindb.tendermint_utils import key_from_base64 +from bigchaindb.common.crypto import key_pair_from_ed25519_key + + +class ProcessGroup(object): + + def __init__(self, concurrency=None, group=None, target=None, name=None, + args=None, kwargs=None, daemon=None): + self.concurrency = concurrency or mp.cpu_count() + self.group = group + self.target = target + self.name = name + self.args = args or () + self.kwargs = kwargs or {} + self.daemon = daemon + self.processes = [] + + def start(self): + for i in range(self.concurrency): + proc = mp.Process(group=self.group, target=self.target, + name=self.name, args=self.args, + kwargs=self.kwargs, daemon=self.daemon) + proc.start() + self.processes.append(proc) + + +class Process(mp.Process): + """Wrapper around multiprocessing.Process that uses + setproctitle to set the name of the process when running + the target task. + """ + + def run(self): + setproctitle.setproctitle(self.name) + super().run() + + +# Inspired by: +# - http://stackoverflow.com/a/24741694/597097 +def pool(builder, size, timeout=None): + """Create a pool that imposes a limit on the number of stored + instances. + + Args: + builder: a function to build an instance. + size: the size of the pool. + timeout(Optional[float]): the seconds to wait before raising + a ``queue.Empty`` exception if no instances are available + within that time. + Raises: + If ``timeout`` is defined but the request is taking longer + than the specified time, the context manager will raise + a ``queue.Empty`` exception. + + Returns: + A context manager that can be used with the ``with`` + statement. + + """ + + lock = threading.Lock() + local_pool = queue.Queue() + current_size = 0 + + @contextlib.contextmanager + def pooled(): + nonlocal current_size + instance = None + + # If we still have free slots, then we have room to create new + # instances. + if current_size < size: + with lock: + # We need to check again if we have slots available, since + # the situation might be different after acquiring the lock + if current_size < size: + current_size += 1 + instance = builder() + + # Watchout: current_size can be equal to size if the previous part of + # the function has been executed, that's why we need to check if the + # instance is None. + if instance is None: + instance = local_pool.get(timeout=timeout) + + yield instance + + local_pool.put(instance) + + return pooled + + +# TODO: Rename this function, it's handling fulfillments not conditions +def condition_details_has_owner(condition_details, owner): + """Check if the public_key of owner is in the condition details + as an Ed25519Fulfillment.public_key + + Args: + condition_details (dict): dict with condition details + owner (str): base58 public key of owner + + Returns: + bool: True if the public key is found in the condition details, False otherwise + + """ + if 'subconditions' in condition_details: + result = condition_details_has_owner(condition_details['subconditions'], owner) + if result: + return True + + elif isinstance(condition_details, list): + for subcondition in condition_details: + result = condition_details_has_owner(subcondition, owner) + if result: + return True + else: + if 'public_key' in condition_details \ + and owner == condition_details['public_key']: + return True + return False + + +class Lazy: + """Lazy objects are useful to create chains of methods to + execute later. + + A lazy object records the methods that has been called, and + replay them when the :py:meth:`run` method is called. Note that + :py:meth:`run` needs an object `instance` to replay all the + methods that have been recorded. + """ + + def __init__(self): + """Instantiate a new Lazy object.""" + self.stack = [] + + def __getattr__(self, name): + self.stack.append(name) + return self + + def __call__(self, *args, **kwargs): + self.stack.append((args, kwargs)) + return self + + def __getitem__(self, key): + self.stack.append('__getitem__') + self.stack.append(([key], {})) + return self + + def run(self, instance): + """Run the recorded chain of methods on `instance`. + + Args: + instance: an object. + """ + + last = instance + + for item in self.stack: + if isinstance(item, str): + last = getattr(last, item) + else: + last = last(*item[0], **item[1]) + + self.stack = [] + return last + + +# Load Tendermint's public and private key from the file path +def load_node_key(path): + with open(path) as json_data: + priv_validator = json.load(json_data) + priv_key = priv_validator['priv_key']['value'] + hex_private_key = key_from_base64(priv_key) + return key_pair_from_ed25519_key(hex_private_key) + + +def tendermint_version_is_compatible(running_tm_ver): + """ + Check Tendermint compatability with Planetmint server + + :param running_tm_ver: Version number of the connected Tendermint instance + :type running_tm_ver: str + :return: True/False depending on the compatability with Planetmint server + :rtype: bool + """ + + # Splitting because version can look like this e.g. 0.22.8-40d6dc2e + tm_ver = running_tm_ver.split('-') + if not tm_ver: + return False + for ver in __tm_supported_versions__: + if version.parse(ver) == version.parse(tm_ver[0]): + return True + return False diff --git a/bigchaindb/validation.py b/bigchaindb/validation.py new file mode 100644 index 0000000..f5ed2db --- /dev/null +++ b/bigchaindb/validation.py @@ -0,0 +1,25 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + + +class BaseValidationRules(): + """Base validation rules for Planetmint. + + A validation plugin must expose a class inheriting from this one via an entry_point. + + All methods listed below must be implemented. + """ + + @staticmethod + def validate_transaction(bigchaindb, transaction): + """See :meth:`bigchaindb.models.Transaction.validate` + for documentation. + """ + return transaction.validate(bigchaindb) + + @staticmethod + def validate_block(bigchaindb, block): + """See :meth:`bigchaindb.models.Block.validate` for documentation.""" + return block.validate(bigchaindb) diff --git a/bigchaindb/version.py b/bigchaindb/version.py new file mode 100644 index 0000000..d4a311d --- /dev/null +++ b/bigchaindb/version.py @@ -0,0 +1,10 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +__version__ = '2.2.2' +__short_version__ = '2.2' + +# Supported Tendermint versions +__tm_supported_versions__ = ["0.31.5", "0.22.8"] diff --git a/bigchaindb/web/__init__.py b/bigchaindb/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/web/routes.py b/bigchaindb/web/routes.py new file mode 100644 index 0000000..62f0369 --- /dev/null +++ b/bigchaindb/web/routes.py @@ -0,0 +1,48 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""API routes definition""" +from flask_restful import Api +from bigchaindb.web.views import ( + assets, + metadata, + blocks, + info, + transactions as tx, + outputs, + validators, +) + + +def add_routes(app): + """Add the routes to an app""" + for (prefix, routes) in API_SECTIONS: + api = Api(app, prefix=prefix) + for ((pattern, resource, *args), kwargs) in routes: + kwargs.setdefault('strict_slashes', False) + api.add_resource(resource, pattern, *args, **kwargs) + + +def r(*args, **kwargs): + return (args, kwargs) + + +ROUTES_API_V1 = [ + r('/', info.ApiV1Index), + r('assets/', assets.AssetListApi), + r('metadata/', metadata.MetadataApi), + r('blocks/', blocks.BlockApi), + r('blocks/', blocks.BlockListApi), + r('transactions/', tx.TransactionApi), + r('transactions', tx.TransactionListApi), + r('outputs/', outputs.OutputListApi), + r('validators/', validators.ValidatorsApi), +] + + +API_SECTIONS = [ + (None, [r('/', info.RootIndex)]), + ('/api/v1/', ROUTES_API_V1), +] diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py new file mode 100644 index 0000000..c525ccd --- /dev/null +++ b/bigchaindb/web/server.py @@ -0,0 +1,118 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module contains basic functions to instantiate the Planetmint API. + +The application is implemented in Flask and runs using Gunicorn. +""" + +import copy +import multiprocessing + +from flask import Flask +from flask_cors import CORS +import gunicorn.app.base + +from bigchaindb import utils +from bigchaindb import BigchainDB +from bigchaindb.web.routes import add_routes +from bigchaindb.web.strip_content_type_middleware import StripContentTypeMiddleware + + +# TODO: Figure out if we do we need all this boilerplate. +class StandaloneApplication(gunicorn.app.base.BaseApplication): + """Run a **wsgi** app wrapping it in a Gunicorn Base Application. + + Adapted from: + - http://docs.gunicorn.org/en/latest/custom.html + """ + + def __init__(self, app, *, options=None): + """Initialize a new standalone application. + + Args: + app: A wsgi Python application. + options (dict): the configuration. + + """ + self.options = options or {} + self.application = app + super().__init__() + + def load_config(self): + # find a better way to pass this such that + # the custom logger class can access it. + custom_log_config = self.options.get('custom_log_config') + self.cfg.env_orig['custom_log_config'] = custom_log_config + + config = dict((key, value) for key, value in self.options.items() + if key in self.cfg.settings and value is not None) + + config['default_proc_name'] = 'bigchaindb_gunicorn' + for key, value in config.items(): + # not sure if we need the `key.lower` here, will just keep + # keep it for now. + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + + +def create_app(*, debug=False, threads=1, bigchaindb_factory=None): + """Return an instance of the Flask application. + + Args: + debug (bool): a flag to activate the debug mode for the app + (default: False). + threads (int): number of threads to use + Return: + an instance of the Flask application. + """ + + if not bigchaindb_factory: + bigchaindb_factory = Planetmint + + app = Flask(__name__) + app.wsgi_app = StripContentTypeMiddleware(app.wsgi_app) + + CORS(app) + + app.debug = debug + + app.config['bigchain_pool'] = utils.pool(bigchaindb_factory, size=threads) + + add_routes(app) + + return app + + +def create_server(settings, log_config=None, bigchaindb_factory=None): + """Wrap and return an application ready to be run. + + Args: + settings (dict): a dictionary containing the settings, more info + here http://docs.gunicorn.org/en/latest/settings.html + + Return: + an initialized instance of the application. + """ + + settings = copy.deepcopy(settings) + + if not settings.get('workers'): + settings['workers'] = (multiprocessing.cpu_count() * 2) + 1 + + if not settings.get('threads'): + # Note: Threading is not recommended currently, as the frontend workload + # is largely CPU bound and parallisation across Python threads makes it + # slower. + settings['threads'] = 1 + + settings['custom_log_config'] = log_config + app = create_app(debug=settings.get('debug', False), + threads=settings['threads'], + bigchaindb_factory=bigchaindb_factory) + standalone = StandaloneApplication(app, options=settings) + return standalone diff --git a/bigchaindb/web/strip_content_type_middleware.py b/bigchaindb/web/strip_content_type_middleware.py new file mode 100644 index 0000000..026b96b --- /dev/null +++ b/bigchaindb/web/strip_content_type_middleware.py @@ -0,0 +1,32 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import logging + +logger = logging.getLogger(__name__) + + +class StripContentTypeMiddleware: + """WSGI middleware to strip Content-Type header for GETs.""" + + def __init__(self, app): + """Create the new middleware. + + Args: + app: a flask application + """ + self.app = app + + def __call__(self, environ, start_response): + """Run the middleware and then call the original WSGI application.""" + + if environ['REQUEST_METHOD'] == 'GET': + try: + del environ['CONTENT_TYPE'] + except KeyError: + pass + else: + logger.debug('Remove header "Content-Type" from GET request') + return self.app(environ, start_response) diff --git a/bigchaindb/web/views/__init__.py b/bigchaindb/web/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bigchaindb/web/views/assets.py b/bigchaindb/web/views/assets.py new file mode 100644 index 0000000..d97e7d0 --- /dev/null +++ b/bigchaindb/web/views/assets.py @@ -0,0 +1,55 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module provides the blueprint for some basic API endpoints. + +For more information please refer to the documentation: http://bigchaindb.com/http-api +""" +import logging + +from flask_restful import reqparse, Resource +from flask import current_app + +from bigchaindb.backend.exceptions import OperationError +from bigchaindb.web.views.base import make_error + +logger = logging.getLogger(__name__) + + +class AssetListApi(Resource): + def get(self): + """API endpoint to perform a text search on the assets. + + Args: + search (str): Text search string to query the text index + limit (int, optional): Limit the number of returned documents. + + Return: + A list of assets that match the query. + """ + parser = reqparse.RequestParser() + parser.add_argument('search', type=str, required=True) + parser.add_argument('limit', type=int) + args = parser.parse_args() + + if not args['search']: + return make_error(400, 'text_search cannot be empty') + if not args['limit']: + # if the limit is not specified do not pass None to `text_search` + del args['limit'] + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + assets = bigchain.text_search(**args) + + try: + # This only works with MongoDB as the backend + return list(assets) + except OperationError as e: + return make_error( + 400, + '({}): {}'.format(type(e).__name__, e) + ) diff --git a/bigchaindb/web/views/base.py b/bigchaindb/web/views/base.py new file mode 100644 index 0000000..a0153b5 --- /dev/null +++ b/bigchaindb/web/views/base.py @@ -0,0 +1,46 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""Common classes and methods for API handlers +""" +import logging + +from flask import jsonify, request + +from bigchaindb import config + + +logger = logging.getLogger(__name__) + + +def make_error(status_code, message=None): + if status_code == 404 and message is None: + message = 'Not found' + + response_content = {'status': status_code, 'message': message} + request_info = {'method': request.method, 'path': request.path} + request_info.update(response_content) + + logger.error('HTTP API error: %(status)s - %(method)s:%(path)s - %(message)s', request_info) + + response = jsonify(response_content) + response.status_code = status_code + return response + + +def base_ws_uri(): + """Base websocket URL that is advertised to external clients. + + Useful when the websocket URL advertised to the clients needs to be + customized (typically when running behind NAT, firewall, etc.) + """ + + config_wsserver = config['wsserver'] + + scheme = config_wsserver['advertised_scheme'] + host = config_wsserver['advertised_host'] + port = config_wsserver['advertised_port'] + + return '{}://{}:{}'.format(scheme, host, port) diff --git a/bigchaindb/web/views/blocks.py b/bigchaindb/web/views/blocks.py new file mode 100644 index 0000000..b2986cf --- /dev/null +++ b/bigchaindb/web/views/blocks.py @@ -0,0 +1,58 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module provides the blueprint for the blocks API endpoints. + +For more information please refer to the documentation: http://bigchaindb.com/http-api +""" +from flask import current_app +from flask_restful import Resource, reqparse + +from bigchaindb.web.views.base import make_error + + +class BlockApi(Resource): + def get(self, block_id): + """API endpoint to get details about a block. + + Args: + block_id (str): the id of the block. + + Return: + A JSON string containing the data about the block. + """ + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + block = bigchain.get_block(block_id=block_id) + + if not block: + return make_error(404) + + return block + + +class BlockListApi(Resource): + def get(self): + """API endpoint to get the related blocks for a transaction. + + Return: + A ``list`` of ``block_id``s that contain the given transaction. The + list may be filtered when provided a status query parameter: + "valid", "invalid", "undecided". + """ + parser = reqparse.RequestParser() + parser.add_argument('transaction_id', type=str, required=True) + + args = parser.parse_args(strict=True) + tx_id = args['transaction_id'] + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + blocks = bigchain.get_block_containing_tx(tx_id) + + return blocks diff --git a/bigchaindb/web/views/info.py b/bigchaindb/web/views/info.py new file mode 100644 index 0000000..6bf4439 --- /dev/null +++ b/bigchaindb/web/views/info.py @@ -0,0 +1,57 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""API Index endpoint""" + +import flask +from flask_restful import Resource + +from bigchaindb.web.views.base import base_ws_uri +from bigchaindb import version +from bigchaindb.web.websocket_server import EVENTS_ENDPOINT + + +class RootIndex(Resource): + def get(self): + docs_url = [ + 'https://docs.bigchaindb.com/projects/server/en/v', + version.__version__ + '/' + ] + return flask.jsonify({ + 'api': { + 'v1': get_api_v1_info('/api/v1/') + }, + 'docs': ''.join(docs_url), + 'software': 'Planetmint', + 'version': version.__version__, + }) + + +class ApiV1Index(Resource): + def get(self): + return flask.jsonify(get_api_v1_info('/')) + + +def get_api_v1_info(api_prefix): + """Return a dict with all the information specific for the v1 of the + api. + """ + websocket_root = base_ws_uri() + EVENTS_ENDPOINT + docs_url = [ + 'https://docs.bigchaindb.com/projects/server/en/v', + version.__version__, + '/http-client-server-api.html', + ] + + return { + 'docs': ''.join(docs_url), + 'transactions': '{}transactions/'.format(api_prefix), + 'blocks': '{}blocks/'.format(api_prefix), + 'assets': '{}assets/'.format(api_prefix), + 'outputs': '{}outputs/'.format(api_prefix), + 'streams': websocket_root, + 'metadata': '{}metadata/'.format(api_prefix), + 'validators': '{}validators'.format(api_prefix), + } diff --git a/bigchaindb/web/views/metadata.py b/bigchaindb/web/views/metadata.py new file mode 100644 index 0000000..aeaa6e4 --- /dev/null +++ b/bigchaindb/web/views/metadata.py @@ -0,0 +1,55 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module provides the blueprint for some basic API endpoints. + +For more information please refer to the documentation: http://bigchaindb.com/http-api +""" +import logging + +from flask_restful import reqparse, Resource +from flask import current_app + +from bigchaindb.backend.exceptions import OperationError +from bigchaindb.web.views.base import make_error + +logger = logging.getLogger(__name__) + + +class MetadataApi(Resource): + def get(self): + """API endpoint to perform a text search on transaction metadata. + + Args: + search (str): Text search string to query the text index + limit (int, optional): Limit the number of returned documents. + + Return: + A list of metadata that match the query. + """ + parser = reqparse.RequestParser() + parser.add_argument('search', type=str, required=True) + parser.add_argument('limit', type=int) + args = parser.parse_args() + + if not args['search']: + return make_error(400, 'text_search cannot be empty') + if not args['limit']: + del args['limit'] + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + args['table'] = 'metadata' + metadata = bigchain.text_search(**args) + + try: + # This only works with MongoDB as the backend + return list(metadata) + except OperationError as e: + return make_error( + 400, + '({}): {}'.format(type(e).__name__, e) + ) diff --git a/bigchaindb/web/views/outputs.py b/bigchaindb/web/views/outputs.py new file mode 100644 index 0000000..f8cd298 --- /dev/null +++ b/bigchaindb/web/views/outputs.py @@ -0,0 +1,31 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from flask import current_app +from flask_restful import reqparse, Resource + +from bigchaindb.web.views import parameters + + +class OutputListApi(Resource): + def get(self): + """API endpoint to retrieve a list of links to transaction + outputs. + + Returns: + A :obj:`list` of :cls:`str` of links to outputs. + """ + parser = reqparse.RequestParser() + parser.add_argument('public_key', type=parameters.valid_ed25519, + required=True) + parser.add_argument('spent', type=parameters.valid_bool) + args = parser.parse_args(strict=True) + + pool = current_app.config['bigchain_pool'] + with pool() as bigchain: + outputs = bigchain.get_outputs_filtered(args['public_key'], + args['spent']) + return [{'transaction_id': output.txid, 'output_index': output.output} + for output in outputs] diff --git a/bigchaindb/web/views/parameters.py b/bigchaindb/web/views/parameters.py new file mode 100644 index 0000000..8013680 --- /dev/null +++ b/bigchaindb/web/views/parameters.py @@ -0,0 +1,51 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +import re + +from bigchaindb.common.transaction_mode_types import (BROADCAST_TX_COMMIT, + BROADCAST_TX_ASYNC, + BROADCAST_TX_SYNC) + + +def valid_txid(txid): + if re.match('^[a-fA-F0-9]{64}$', txid): + return txid.lower() + raise ValueError('Invalid hash') + + +def valid_bool(val): + val = val.lower() + if val == 'true': + return True + if val == 'false': + return False + raise ValueError('Boolean value must be "true" or "false" (lowercase)') + + +def valid_ed25519(key): + if (re.match('^[1-9a-zA-Z]{43,44}$', key) and not + re.match('.*[Il0O]', key)): + return key + raise ValueError('Invalid base58 ed25519 key') + + +def valid_operation(op): + op = op.upper() + if op == 'CREATE': + return 'CREATE' + if op == 'TRANSFER': + return 'TRANSFER' + raise ValueError('Operation must be "CREATE" or "TRANSFER"') + + +def valid_mode(mode): + if mode == 'async': + return BROADCAST_TX_ASYNC + if mode == 'sync': + return BROADCAST_TX_SYNC + if mode == 'commit': + return BROADCAST_TX_COMMIT + raise ValueError('Mode must be "async", "sync" or "commit"') diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py new file mode 100644 index 0000000..4a371db --- /dev/null +++ b/bigchaindb/web/views/transactions.py @@ -0,0 +1,108 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""This module provides the blueprint for some basic API endpoints. + +For more information please refer to the documentation: http://bigchaindb.com/http-api +""" +import logging + +from flask import current_app, request, jsonify +from flask_restful import Resource, reqparse + +from bigchaindb.common.transaction_mode_types import BROADCAST_TX_ASYNC +from bigchaindb.common.exceptions import SchemaValidationError, ValidationError +from bigchaindb.web.views.base import make_error +from bigchaindb.web.views import parameters +from bigchaindb.models import Transaction + + +logger = logging.getLogger(__name__) + + +class TransactionApi(Resource): + def get(self, tx_id): + """API endpoint to get details about a transaction. + + Args: + tx_id (str): the id of the transaction. + + Return: + A JSON string containing the data about the transaction. + """ + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + tx = bigchain.get_transaction(tx_id) + + if not tx: + return make_error(404) + + return tx.to_dict() + + +class TransactionListApi(Resource): + def get(self): + parser = reqparse.RequestParser() + parser.add_argument('operation', type=parameters.valid_operation) + parser.add_argument('asset_id', type=parameters.valid_txid, + required=True) + parser.add_argument('last_tx', type=parameters.valid_bool, + required=False) + args = parser.parse_args() + with current_app.config['bigchain_pool']() as bigchain: + txs = bigchain.get_transactions_filtered(**args) + + return [tx.to_dict() for tx in txs] + + def post(self): + """API endpoint to push transactions to the Federation. + + Return: + A ``dict`` containing the data about the transaction. + """ + parser = reqparse.RequestParser() + parser.add_argument('mode', type=parameters.valid_mode, + default=BROADCAST_TX_ASYNC) + args = parser.parse_args() + mode = str(args['mode']) + + pool = current_app.config['bigchain_pool'] + + # `force` will try to format the body of the POST request even if the + # `content-type` header is not set to `application/json` + tx = request.get_json(force=True) + + try: + tx_obj = Transaction.from_dict(tx) + except SchemaValidationError as e: + return make_error( + 400, + message='Invalid transaction schema: {}'.format( + e.__cause__.message) + ) + except ValidationError as e: + return make_error( + 400, + 'Invalid transaction ({}): {}'.format(type(e).__name__, e) + ) + + with pool() as bigchain: + try: + bigchain.validate_transaction(tx_obj) + except ValidationError as e: + return make_error( + 400, + 'Invalid transaction ({}): {}'.format(type(e).__name__, e) + ) + else: + status_code, message = bigchain.write_transaction(tx_obj, mode) + + if status_code == 202: + response = jsonify(tx) + response.status_code = 202 + return response + else: + return make_error(status_code, message) diff --git a/bigchaindb/web/views/validators.py b/bigchaindb/web/views/validators.py new file mode 100644 index 0000000..7c3fcee --- /dev/null +++ b/bigchaindb/web/views/validators.py @@ -0,0 +1,23 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +from flask import current_app +from flask_restful import Resource + + +class ValidatorsApi(Resource): + def get(self): + """API endpoint to get validators set. + + Return: + A JSON string containing the validator set of the current node. + """ + + pool = current_app.config['bigchain_pool'] + + with pool() as bigchain: + validators = bigchain.get_validators() + + return validators diff --git a/bigchaindb/web/websocket_server.py b/bigchaindb/web/websocket_server.py new file mode 100644 index 0000000..2d8f621 --- /dev/null +++ b/bigchaindb/web/websocket_server.py @@ -0,0 +1,184 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +"""WebSocket server for the Planetmint Event Stream API.""" + +# NOTE +# +# This module contains some functions and utilities that might belong to other +# modules. For now, I prefer to keep everything in this module. Why? Because +# those functions are needed only here. +# +# When we will extend this part of the project and we find that we need those +# functionalities elsewhere, we can start creating new modules and organizing +# things in a better way. + + +import json +import asyncio +import logging +import threading +from uuid import uuid4 +from concurrent.futures import CancelledError + +import aiohttp +from aiohttp import web + +from bigchaindb import config +from bigchaindb.events import EventTypes + + +logger = logging.getLogger(__name__) +POISON_PILL = 'POISON_PILL' +EVENTS_ENDPOINT = '/api/v1/streams/valid_transactions' + + +def _multiprocessing_to_asyncio(in_queue, out_queue, loop): + """Bridge between a synchronous multiprocessing queue + and an asynchronous asyncio queue. + + Args: + in_queue (multiprocessing.Queue): input queue + out_queue (asyncio.Queue): output queue + """ + + while True: + value = in_queue.get() + loop.call_soon_threadsafe(out_queue.put_nowait, value) + + +def eventify_block(block): + for tx in block['transactions']: + if tx.asset: + asset_id = tx.asset.get('id', tx.id) + else: + asset_id = tx.id + yield {'height': block['height'], + 'asset_id': asset_id, + 'transaction_id': tx.id} + + +class Dispatcher: + """Dispatch events to websockets. + + This class implements a simple publish/subscribe pattern. + """ + + def __init__(self, event_source): + """Create a new instance. + + Args: + event_source: a source of events. Elements in the queue + should be strings. + """ + + self.event_source = event_source + self.subscribers = {} + + def subscribe(self, uuid, websocket): + """Add a websocket to the list of subscribers. + + Args: + uuid (str): a unique identifier for the websocket. + websocket: the websocket to publish information. + """ + + self.subscribers[uuid] = websocket + + def unsubscribe(self, uuid): + """Remove a websocket from the list of subscribers. + + Args: + uuid (str): a unique identifier for the websocket. + """ + + del self.subscribers[uuid] + + async def publish(self): + """Publish new events to the subscribers.""" + + while True: + event = await self.event_source.get() + str_buffer = [] + + if event == POISON_PILL: + return + + if isinstance(event, str): + str_buffer.append(event) + + elif event.type == EventTypes.BLOCK_VALID: + str_buffer = map(json.dumps, eventify_block(event.data)) + + for str_item in str_buffer: + for _, websocket in self.subscribers.items(): + await websocket.send_str(str_item) + + +async def websocket_handler(request): + """Handle a new socket connection.""" + + logger.debug('New websocket connection.') + websocket = web.WebSocketResponse() + await websocket.prepare(request) + uuid = uuid4() + request.app['dispatcher'].subscribe(uuid, websocket) + + while True: + # Consume input buffer + try: + msg = await websocket.receive() + except RuntimeError as e: + logger.debug('Websocket exception: %s', str(e)) + break + except CancelledError: + logger.debug('Websocket closed') + break + if msg.type == aiohttp.WSMsgType.CLOSED: + logger.debug('Websocket closed') + break + elif msg.type == aiohttp.WSMsgType.ERROR: + logger.debug('Websocket exception: %s', websocket.exception()) + break + + request.app['dispatcher'].unsubscribe(uuid) + return websocket + + +def init_app(event_source, *, loop=None): + """Init the application server. + + Return: + An aiohttp application. + """ + + dispatcher = Dispatcher(event_source) + + # Schedule the dispatcher + loop.create_task(dispatcher.publish()) + + app = web.Application(loop=loop) + app['dispatcher'] = dispatcher + app.router.add_get(EVENTS_ENDPOINT, websocket_handler) + return app + + +def start(sync_event_source, loop=None): + """Create and start the WebSocket server.""" + + if not loop: + loop = asyncio.get_event_loop() + + event_source = asyncio.Queue(loop=loop) + + bridge = threading.Thread(target=_multiprocessing_to_asyncio, + args=(sync_event_source, event_source, loop), + daemon=True) + bridge.start() + + app = init_app(event_source, loop=loop) + aiohttp.web.run_app(app, + host=config['wsserver']['host'], + port=config['wsserver']['port']) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..6966641 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,35 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +codecov: + branch: master # the branch to show by default + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + target: auto + if_no_uploads: error + + patch: + default: + target: "80%" + if_no_uploads: error + + ignore: # files and folders that will be removed during processing + - "docs/*" + - "tests/*" + - "bigchaindb/version.py" + - "k8s/*" + +comment: + # @stevepeak (from codecov.io) suggested we change 'suggestions' to 'uncovered' + # in the following line. Thanks Steve! + layout: "header, diff, changes, sunburst, uncovered" + behavior: default diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a5e6ab0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,105 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +version: '2.2' + +services: + # Build: docker-compose build -d bigchaindb + # Run: docker-compose run -d bdb + mongodb: + image: mongo:3.6 + ports: + - "27017:27017" + command: mongod + restart: always + bigchaindb: + depends_on: + - mongodb + - tendermint + build: + context: . + dockerfile: Dockerfile-dev + volumes: + - ./bigchaindb:/usr/src/app/bigchaindb + - ./tests:/usr/src/app/tests + - ./docs:/usr/src/app/docs + - ./htmlcov:/usr/src/app/htmlcov + - ./setup.py:/usr/src/app/setup.py + - ./setup.cfg:/usr/src/app/setup.cfg + - ./pytest.ini:/usr/src/app/pytest.ini + - ./tox.ini:/usr/src/app/tox.ini + environment: + PLANETMINT_DATABASE_BACKEND: localmongodb + PLANETMINT_DATABASE_HOST: mongodb + PLANETMINT_DATABASE_PORT: 27017 + PLANETMINT_SERVER_BIND: 0.0.0.0:9984 + PLANETMINT_WSSERVER_HOST: 0.0.0.0 + PLANETMINT_WSSERVER_ADVERTISED_HOST: bigchaindb + PLANETMINT_TENDERMINT_HOST: tendermint + PLANETMINT_TENDERMINT_PORT: 26657 + ports: + - "9984:9984" + - "9985:9985" + - "26658" + healthcheck: + test: ["CMD", "bash", "-c", "curl http://bigchaindb:9984 && curl http://tendermint:26657/abci_query"] + interval: 3s + timeout: 5s + retries: 3 + command: '.ci/entrypoint.sh' + restart: always + tendermint: + image: tendermint/tendermint:v0.31.5 + # volumes: + # - ./tmdata:/tendermint + entrypoint: '' + ports: + - "26656:26656" + - "26657:26657" + command: sh -c "tendermint init && tendermint node --consensus.create_empty_blocks=false --proxy_app=tcp://bigchaindb:26658" + restart: always + bdb: + image: busybox + depends_on: + bigchaindb: + condition: service_healthy + + + # curl client to check the health of development env + curl-client: + image: appropriate/curl + command: /bin/sh -c "curl -s http://bigchaindb:9984/ > /dev/null && curl -s http://tendermint:26657/ > /dev/null" + + # Planetmint setup to do acceptance testing with Python + python-acceptance: + build: + context: . + dockerfile: ./acceptance/python/Dockerfile + volumes: + - ./acceptance/python/docs:/docs + - ./acceptance/python/src:/src + environment: + - PLANETMINT_ENDPOINT=bigchaindb + + # Build docs only + # docker-compose build bdocs + # docker-compose up -d bdocs + bdocs: + depends_on: + - vdocs + build: + context: . + dockerfile: Dockerfile-dev + args: + backend: localmongodb + volumes: + - .:/usr/src/app/ + command: make -C docs/root html + vdocs: + image: nginx + ports: + - '33333:80' + volumes: + - ./docs/root/build/html:/usr/share/nginx/html diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..15c3175 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,51 @@ + + +- [Documentation on ReadTheDocs](http://bigchaindb.readthedocs.org/) +- [Planetmint Upgrade Guides](upgrade-guides/) + +# The Planetmint Documentation Strategy + +* Include explanatory comments and docstrings in your code. Write [Google style docstrings](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) with a maximum line width of 119 characters. +* For quick overview and help documents, feel free to create `README.md` or other `X.md` files, written using [GitHub-flavored Markdown](https://help.github.com/categories/writing-on-github/). Markdown files render nicely on GitHub. We might auto-convert some .md files into a format that can be included in the long-form documentation. +* We use [Sphinx](http://www.sphinx-doc.org/en/stable/) to generate the long-form documentation in various formats (e.g. HTML, PDF). +* We also use [Sphinx](http://www.sphinx-doc.org/en/stable/) to generate Python code documentation (from docstrings and possibly other sources). +* We also use Sphinx to document all REST APIs, with the help of [the `httpdomain` extension](https://pythonhosted.org/sphinxcontrib-httpdomain/). + +# How to Generate the HTML Version of the Long-Form Documentation + +If you want to generate the HTML version of the long-form documentation on your local machine, you need to have Sphinx and some Sphinx-contrib packages installed. To do that, go to a subdirectory of `docs` (e.g. `docs/server`) and do: +```bash +pip install -r requirements.txt +``` + +If you're building the *Server* docs (in `docs/server`) then you must also do: +```bash +pip install -e ../../ +``` + +Note: Don't put `-e ../../` in the `requirements.txt` file. That will work locally +but not on ReadTheDocs. + +You can then generate the HTML documentation _in that subdirectory_ by doing: +```bash +make html +``` + +It should tell you where the generated documentation (HTML files) can be found. You can view it in your web browser. +# Building Docs with Docker Compose + +You can also use [Docker Compose](https://docs.docker.com/compose/) to build and host docs. + +```text +$ docker-compose up -d bdocs +``` + +The docs will be hosted on port **33333**, and can be accessed over [localhost](http://localhost:33333), [127.0.0.1](http://127.0.0.1:33333) +OR http:/HOST_IP:33333. + + diff --git a/docs/root/Makefile b/docs/root/Makefile new file mode 100644 index 0000000..597a65f --- /dev/null +++ b/docs/root/Makefile @@ -0,0 +1,225 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -W +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Planetmint.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Planetmint.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Planetmint" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Planetmint" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/root/generate_http_server_api_documentation.py b/docs/root/generate_http_server_api_documentation.py new file mode 100644 index 0000000..f5cbf25 --- /dev/null +++ b/docs/root/generate_http_server_api_documentation.py @@ -0,0 +1,207 @@ +# Copyright © 2020 Interplanetary Database Association e.V., +# Planetmint and IPDB software contributors. +# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) +# Code is Apache-2.0 and docs are CC-BY-4.0 + +""" Script to build http examples for http server api docs """ + +import json +import os +import os.path + +from bigchaindb.common.transaction import Transaction, Input, TransactionLink +from bigchaindb import lib +from bigchaindb.web import server + + +TPLS = {} + + +TPLS['index-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +%(index)s +""" + +TPLS['api-index-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +%(api_index)s +""" + +TPLS['get-tx-id-request'] = """\ +GET /api/v1/transactions/%(txid)s HTTP/1.1 +Host: example.com + +""" + + +TPLS['get-tx-id-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +%(tx)s +""" + + +TPLS['get-tx-by-asset-request'] = """\ +GET /api/v1/transactions?operation=TRANSFER&asset_id=%(txid)s HTTP/1.1 +Host: example.com + +""" + + +TPLS['get-tx-by-asset-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +[%(tx_transfer)s, +%(tx_transfer_last)s] +""" + +TPLS['post-tx-request'] = """\ +POST /api/v1/transactions?mode=async HTTP/1.1 +Host: example.com +Content-Type: application/json + +%(tx)s +""" + + +TPLS['post-tx-response'] = """\ +HTTP/1.1 202 Accepted +Content-Type: application/json + +%(tx)s +""" + + +TPLS['get-block-request'] = """\ +GET /api/v1/blocks/%(blockid)s HTTP/1.1 +Host: example.com + +""" + + +TPLS['get-block-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +%(block)s +""" + + +TPLS['get-block-txid-request'] = """\ +GET /api/v1/blocks?transaction_id=%(txid)s HTTP/1.1 +Host: example.com + +""" + + +TPLS['get-block-txid-response'] = """\ +HTTP/1.1 200 OK +Content-Type: application/json + +%(block_list)s +""" + + +def main(): + """ Main function """ + + ctx = {} + + def pretty_json(data): + return json.dumps(data, indent=2, sort_keys=True) + + client = server.create_app().test_client() + + host = 'example.com:9984' + + # HTTP Index + res = client.get('/', environ_overrides={'HTTP_HOST': host}) + res_data = json.loads(res.data.decode()) + ctx['index'] = pretty_json(res_data) + + # API index + res = client.get('/api/v1/', environ_overrides={'HTTP_HOST': host}) + ctx['api_index'] = pretty_json(json.loads(res.data.decode())) + + # tx create + privkey = 'CfdqtD7sS7FgkMoGPXw55MVGGFwQLAoHYTcBhZDtF99Z' + pubkey = '4K9sWUMFwTgaDGPfdynrbxWqWS6sWmKbZoTjxLtVUibD' + asset = {'msg': 'Hello Planetmint!'} + tx = Transaction.create([pubkey], [([pubkey], 1)], asset=asset, metadata={'sequence': 0}) + tx = tx.sign([privkey]) + ctx['tx'] = pretty_json(tx.to_dict()) + ctx['public_keys'] = tx.outputs[0].public_keys[0] + ctx['txid'] = tx.id + + # tx transfer + privkey_transfer = '3AeWpPdhEZzWLYfkfYHBfMFC2r1f8HEaGS9NtbbKssya' + pubkey_transfer = '3yfQPHeWAa1MxTX9Zf9176QqcpcnWcanVZZbaHb8B3h9' + + cid = 0 + input_ = Input(fulfillment=tx.outputs[cid].fulfillment, + fulfills=TransactionLink(txid=tx.id, output=cid), + owners_before=tx.outputs[cid].public_keys) + tx_transfer = Transaction.transfer([input_], [([pubkey_transfer], 1)], asset_id=tx.id, metadata={'sequence': 1}) + tx_transfer = tx_transfer.sign([privkey]) + ctx['tx_transfer'] = pretty_json(tx_transfer.to_dict()) + ctx['public_keys_transfer'] = tx_transfer.outputs[0].public_keys[0] + ctx['tx_transfer_id'] = tx_transfer.id + + # privkey_transfer_last = 'sG3jWDtdTXUidBJK53ucSTrosktG616U3tQHBk81eQe' + pubkey_transfer_last = '3Af3fhhjU6d9WecEM9Uw5hfom9kNEwE7YuDWdqAUssqm' + + cid = 0 + input_ = Input(fulfillment=tx_transfer.outputs[cid].fulfillment, + fulfills=TransactionLink(txid=tx_transfer.id, output=cid), + owners_before=tx_transfer.outputs[cid].public_keys) + tx_transfer_last = Transaction.transfer([input_], [([pubkey_transfer_last], 1)], + asset_id=tx.id, metadata={'sequence': 2}) + tx_transfer_last = tx_transfer_last.sign([privkey_transfer]) + ctx['tx_transfer_last'] = pretty_json(tx_transfer_last.to_dict()) + ctx['tx_transfer_last_id'] = tx_transfer_last.id + ctx['public_keys_transfer_last'] = tx_transfer_last.outputs[0].public_keys[0] + + # block + node_private = "5G2kE1zJAgTajkVSbPAQWo4c2izvtwqaNHYsaNpbbvxX" + node_public = "DngBurxfeNVKZWCEcDnLj1eMPAS7focUZTE5FndFGuHT" + signature = "53wxrEQDYk1dXzmvNSytbCfmNVnPqPkDQaTnAe8Jf43s6ssejPxezkCvUnGTnduNUmaLjhaan1iRLi3peu6s5DzA" + + app_hash = 'f6e0c49c6d94d6924351f25bb334cf2a99af4206339bf784e741d1a5ab599056' + block = lib.Block(height=1, transactions=[tx.to_dict()], app_hash=app_hash) + block_dict = block._asdict() + block_dict.pop('app_hash') + ctx['block'] = pretty_json(block_dict) + ctx['blockid'] = block.height + + # block status + block_list = [ + block.height + ] + ctx['block_list'] = pretty_json(block_list) + + + base_path = os.path.join(os.path.dirname(__file__), + 'source/installation/api/http-samples') + if not os.path.exists(base_path): + os.makedirs(base_path) + + for name, tpl in TPLS.items(): + path = os.path.join(base_path, name + '.http') + code = tpl % ctx + with open(path, 'w') as handle: + handle.write(code) + + +def setup(*_): + """ Fool sphinx into think it's an extension muahaha """ + main() + + +if __name__ == '__main__': + main() diff --git a/docs/root/make.bat b/docs/root/make.bat new file mode 100644 index 0000000..7f41e02 --- /dev/null +++ b/docs/root/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\bigchaindb.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\bigchaindb.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/root/requirements.txt b/docs/root/requirements.txt new file mode 100644 index 0000000..81df20d --- /dev/null +++ b/docs/root/requirements.txt @@ -0,0 +1,9 @@ +Sphinx~=1.0 +recommonmark>=0.4.0 +sphinx-rtd-theme>=0.1.9 +sphinxcontrib-napoleon>=0.4.4 +sphinxcontrib-httpdomain>=1.5.0 +pyyaml>=4.2b1 +aafigure>=0.6 +packaging~=18.0 +wget diff --git a/docs/root/source/_static/CREATE_and_TRANSFER_example.png b/docs/root/source/_static/CREATE_and_TRANSFER_example.png new file mode 100644 index 0000000000000000000000000000000000000000..f9ef1ee07e01f4c24831228ff0b7e4d089c398f6 GIT binary patch literal 4149 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