diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f44483..f886d3dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,29 @@ For reference, the possible headings are: * **External Contributors** to list contributors outside of ascribe GmbH. +## [0.3.0] - 2016-05-03 +Tag name: v0.3.0 += commit: +committed: + +### 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 BigchainDB. 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 diff --git a/PYTHON_STYLE_GUIDE.md b/PYTHON_STYLE_GUIDE.md index 61863860..ca94c197 100644 --- a/PYTHON_STYLE_GUIDE.md +++ b/PYTHON_STYLE_GUIDE.md @@ -43,6 +43,16 @@ my_string = 'This is a very long string, so long that it will not fit into just 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) +``` + +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: diff --git a/README.md b/README.md index 5c38f639..7865103d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB) [![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb) [![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master) -[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=stable)](https://bigchaindb.readthedocs.org/en/stable/) +[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=latest)](https://bigchaindb.readthedocs.org/en/latest/) # BigchainDB diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index a308a0e5..e5236129 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -176,7 +176,6 @@ class BaseConsensusRules(AbstractConsensusRules): return transaction - # TODO: Unsure if a bigchain parameter is really necessary here? @staticmethod def validate_block(bigchain, block): """Validate a block. @@ -198,6 +197,15 @@ class BaseConsensusRules(AbstractConsensusRules): if calculated_hash != block['id']: raise exceptions.InvalidHash() + # Check if the block was created by a federation node + if block['block']['node_pubkey'] not in (bigchain.federation_nodes + [bigchain.me]): + raise exceptions.OperationError('Only federation nodes can create blocks') + + # Check if block signature is valid + verifying_key = crypto.VerifyingKey(block['block']['node_pubkey']) + if not verifying_key.verify(util.serialize(block['block']), block['signature']): + raise exceptions.InvalidSignature('Invalid block signature') + return block @staticmethod diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 34a684e0..35b0aa77 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -203,11 +203,20 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): }, } """ - # validate arguments (owners and inputs should be lists) + # validate arguments (owners and inputs should be lists or None) + + # The None case appears on fulfilling a hashlock + if current_owners is None: + current_owners = [] if not isinstance(current_owners, list): current_owners = [current_owners] + + # The None case appears on assigning a hashlock + if new_owners is None: + new_owners = [] if not isinstance(new_owners, list): new_owners = [new_owners] + if not isinstance(inputs, list): inputs = [inputs] @@ -247,20 +256,30 @@ def create_tx(current_owners, new_owners, inputs, operation, payload=None): # handle outputs conditions = [] for fulfillment in fulfillments: + + # threshold condition if len(new_owners) > 1: condition = cc.ThresholdSha256Fulfillment(threshold=len(new_owners)) for new_owner in new_owners: condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=new_owner)) + + # simple signature condition elif len(new_owners) == 1: condition = cc.Ed25519Fulfillment(public_key=new_owners[0]) - conditions.append({ - 'new_owners': new_owners, - 'condition': { - 'details': json.loads(condition.serialize_json()), - 'uri': condition.condition.serialize_uri() - }, - 'cid': fulfillment['fid'] - }) + + # to be added later (hashlock conditions) + else: + condition = None + + if condition: + conditions.append({ + 'new_owners': new_owners, + 'condition': { + 'details': json.loads(condition.serialize_json()), + 'uri': condition.condition.serialize_uri() + }, + 'cid': fulfillment['fid'] + }) tx = { 'fulfillments': fulfillments, diff --git a/bigchaindb/version.py b/bigchaindb/version.py index 3af9f911..793d1adc 100644 --- a/bigchaindb/version.py +++ b/bigchaindb/version.py @@ -1,2 +1,2 @@ -__version__ = '0.2.0' -__short_version__ = '0.2' \ No newline at end of file +__version__ = '0.3.0' +__short_version__ = '0.3' \ No newline at end of file diff --git a/codecov.yml b/codecov.yml index d3956b6c..71dc2177 100644 --- a/codecov.yml +++ b/codecov.yml @@ -26,6 +26,7 @@ coverage: - "deploy-cluster-aws/*" - "docs/*" - "tests/*" + - "bigchaindb/version.py" comment: # @stevepeak (from codecov.io) suggested we change 'suggestions' to 'uncovered' diff --git a/docs/source/python-server-api-examples.md b/docs/source/python-server-api-examples.md index 15ee08f7..c191fe74 100644 --- a/docs/source/python-server-api-examples.md +++ b/docs/source/python-server-api-examples.md @@ -746,3 +746,142 @@ threshold_tx_transfer "version":1 } ``` + + +### Hash-locked Conditions + +By creating a hash of a difficult-to-guess 256-bit random or pseudo-random integer it is possible to create a condition which the creator can trivially fulfill by publishing the random value. However, for anyone else, the condition is cryptographically hard to fulfill, because they would have to find a preimage for the given condition hash. + +One possible usecase might be to redeem a digital voucher when given a secret (voucher code). + +```python +# Create a hash-locked asset without any new_owners +hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + +# Define a secret that will be hashed - fulfillments need to guess the secret +secret = b'much secret! wow!' +first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + +# The conditions list is empty, so we need to append a new condition +hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# Conditions have been updated, so hash needs updating +hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + +# The asset needs to be signed by the current_owner +hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + +# Some validations +assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed +assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + +b.write_transaction(hashlock_tx_signed) +hashlock_tx_signed +``` + +```python +{ + "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", + "id":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "uri":"cc:0:3:nsW2IiYgk9EUtsg4uBe3pBnOgRoAEX2IIsPgjqZz47U:17" + }, + "new_owners":None + } + ], + "data":None, + "fulfillments":[ + { + "current_owners":[ + "FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2" + ], + "fid":0, + "fulfillment":"cf:4:21-D-LfNhIQhvY5914ArFTUGpgPKc7EVC1ZtJqqOTHGx1p9FuRr9tRfkbdqtX2MZWh7sRVUmMnwp7I1-xZbCnCkeADf69IwDHbZvNS6aTr1CpekREsV9ZG8m_wjlZiUN", + "input":None + } + ], + "operation":"CREATE", + "timestamp":"1461250387.910102" + }, + "version":1 +} +``` + +In order to redeem the asset, one needs to create a fulfillment the correct secret as a preimage: + +```python +hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() + +# create hashlock fulfillment tx +hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_pub, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + +# provide a wrong secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.is_valid_transaction(hashlock_fulfill_tx) == False + +# provide the right secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx +assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + +b.write_transaction(hashlock_fulfill_tx) +hashlock_fulfill_tx +``` + +```python +{ + "assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2", + "id":"fe6871bf3ca62eb61c52c5555cec2e07af51df817723f0cb76e5cf6248f449d2", + "transaction":{ + "conditions":[ + { + "cid":0, + "condition":{ + "details":{ + "bitmask":32, + "public_key":"EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb", + "signature":None, + "type":"fulfillment", + "type_id":4 + }, + "uri":"cc:4:20:y9884Md2YI_wdnGSTJGhwvFaNsKLe8sqwimqk-2JLSI:96" + }, + "new_owners":[ + "EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb" + ] + } + ], + "data":None, + "fulfillments":[ + { + "current_owners":[], + "fid":0, + "fulfillment":"cf:0:bXVjaCBzZWNyZXQhIHdvdyE", + "input":{ + "cid":0, + "txid":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237" + } + } + ], + "operation":"TRANSFER", + "timestamp":"1461250397.944510" + }, + "version":1 +} +``` \ No newline at end of file diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 71ad97a2..fb7f3539 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -433,6 +433,37 @@ class TestBlockValidation(object): assert block == b.validate_block(block) assert b.is_valid_block(block) + def test_invalid_signature(self, b): + # create a valid block + block = b.create_block([]) + + # replace the block signature with an invalid one + block['signature'] = crypto.SigningKey(b.me_private).sign(b'wrongdata') + + # check that validate_block raises an InvalidSignature exception + with pytest.raises(exceptions.InvalidSignature): + b.validate_block(block) + + def test_invalid_node_pubkey(self, b): + # blocks can only be created by a federation node + # create a valid block + block = b.create_block([]) + + # create some temp keys + tmp_sk, tmp_vk = crypto.generate_key_pair() + + # change the block node_pubkey + block['block']['node_pubkey'] = tmp_vk + + # just to make sure lets re-hash the block and create a valid signature + # from a non federation node + block['id'] = crypto.hash_data(util.serialize(block['block'])) + block['signature'] = crypto.SigningKey(tmp_sk).sign(util.serialize(block['block'])) + + # check that validate_block raises an OperationError + with pytest.raises(exceptions.OperationError): + b.validate_block(block) + class TestBigchainVoter(object): def test_valid_block_voting(self, b): @@ -1544,6 +1575,136 @@ class TestCryptoconditions(object): assert b.verify_signature(tx_transfer_signed) is True + def test_create_asset_with_hashlock_condition(self, b): + hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + @pytest.mark.usefixtures('inputs') + def test_transfer_asset_with_hashlock_condition(self, b, user_vk, user_sk): + first_input_tx = b.get_owned_ids(user_vk).pop() + + hashlock_tx = b.create_transaction(user_vk, None, first_input_tx, 'TRANSFER') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, user_sk) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert len(b.get_owned_ids(user_vk)) == 1 + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(user_vk)) == 0 + + def test_create_and_fulfill_asset_with_hashlock_condition(self, b, user_vk): + hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + + secret = b'much secret! wow!' + first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + + hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'details': json.loads(first_tx_condition.serialize_json()), + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None + }) + # conditions have been updated, so hash needs updating + hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + + hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + + assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed + assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + + b.write_transaction(hashlock_tx_signed) + + # create and write block to bigchain + block = b.create_block([hashlock_tx_signed]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(b.me)) == 0 + + # create hashlock fulfillment tx + hashlock_fulfill_tx = b.create_transaction(None, user_vk, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + + hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') + hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + + with pytest.raises(exceptions.InvalidSignature): + b.validate_transaction(hashlock_fulfill_tx) + assert b.is_valid_transaction(hashlock_fulfill_tx) == False + + hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) + hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + + assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + + b.write_transaction(hashlock_fulfill_tx) + + # create and write block to bigchain + block = b.create_block([hashlock_fulfill_tx]) + b.write_block(block, durability='hard') + + assert len(b.get_owned_ids(b.me)) == 0 + assert len(b.get_owned_ids(user_vk)) == 1 + + # try doublespending + user2_sk, user2_vk = crypto.generate_key_pair() + hashlock_doublespend_tx = b.create_transaction(None, user2_vk, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + + hashlock_doublespend_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) + hashlock_doublespend_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_doublespend_tx_fulfillment.serialize_uri() + + with pytest.raises(exceptions.DoubleSpend): + b.validate_transaction(hashlock_doublespend_tx) + def test_get_subcondition_from_vk(self, b, user_sk, user_vk): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 3500d6ab..56a6aa31 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -233,4 +233,60 @@ b.write_transaction(threshold_tx_transfer) print(json.dumps(threshold_tx_transfer, sort_keys=True, indent=4, separators=(',', ':'))) +""" +Hashlocked Conditions +""" +# Create a hash-locked asset without any new_owners +hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') + +# Define a secret that will be hashed - fulfillments need to guess the secret +secret = b'much secret! wow!' +first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) + +# The conditions list is empty, so we need to append a new condition +hashlock_tx['transaction']['conditions'].append({ + 'condition': { + 'uri': first_tx_condition.condition.serialize_uri() + }, + 'cid': 0, + 'new_owners': None +}) + +# Conditions have been updated, so hash needs updating +hashlock_tx['id'] = util.get_hash_data(hashlock_tx) + +# The asset needs to be signed by the current_owner +hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) + +# Some validations +assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed +assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed + +b.write_transaction(hashlock_tx_signed) +print(json.dumps(hashlock_tx_signed, sort_keys=True, indent=4, separators=(',', ':'))) + +sleep(10) + +hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair() + +# create hashlock fulfillment tx +hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_priv, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') + +# try a wrong secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.is_valid_transaction(hashlock_fulfill_tx) == False + +# provide the right secret +hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) +hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ + hashlock_fulfill_tx_fulfillment.serialize_uri() + +assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx +assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx + +b.write_transaction(hashlock_fulfill_tx) +print(json.dumps(hashlock_fulfill_tx, sort_keys=True, indent=4, separators=(',', ':')))