From 06a091e2157a136195873ed9170a2b8ab4f44b9a Mon Sep 17 00:00:00 2001 From: codegeschrei Date: Thu, 11 Jan 2018 13:26:06 +0100 Subject: [PATCH 1/6] add mode parameter to transaction endpoint, fixes #1995 --- bigchaindb/core.py | 2 +- bigchaindb/tendermint/lib.py | 16 +++++--- bigchaindb/web/views/parameters.py | 12 +++++- bigchaindb/web/views/transactions.py | 9 ++++- docs/server/source/http-client-server-api.rst | 23 ++++++++++++ tests/tendermint/test_lib.py | 37 ++++++++++++++++++- tests/web/test_transactions.py | 36 ++++++++++++++++++ 7 files changed, 126 insertions(+), 9 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 4fb84722..fae16e9d 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -77,7 +77,7 @@ class Bigchain(object): federation = property(lambda self: set(self.nodes_except_me + [self.me])) """ Set of federation member public keys """ - def write_transaction(self, signed_transaction): + def write_transaction(self, signed_transaction, **kwargs): """Write the transaction to bigchain. When first writing a transaction to the bigchain the transaction will be kept in a backlog until diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 37fb9d6e..3cf93ce5 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -23,11 +23,17 @@ ENDPOINT = 'http://{}:{}/'.format(TENDERMINT_HOST, TENDERMINT_PORT) class BigchainDB(Bigchain): - def post_transaction(self, transaction): + def post_transaction(self, transaction, mode): """Submit a valid transaction to the mempool.""" + mode_list = ('broadcast_tx_async', + 'broadcast_tx_sync', + 'broadcast_tx_commit') + if not mode or mode['mode'] not in mode_list: + raise ValidationError(('Mode must be one of the following {}.') + .format(', '.join(mode_list))) payload = { - 'method': 'broadcast_tx_async', + 'method': mode['mode'], 'jsonrpc': '2.0', 'params': [encode_transaction(transaction.to_dict())], 'id': str(uuid4()) @@ -35,11 +41,11 @@ class BigchainDB(Bigchain): # TODO: handle connection errors! requests.post(ENDPOINT, json=payload) - def write_transaction(self, transaction): + def write_transaction(self, transaction, **kwargs): # This method offers backward compatibility with the Web API. """Submit a valid transaction to the mempool.""" - - self.post_transaction(transaction) + mode = kwargs + self.post_transaction(transaction, mode) def store_transaction(self, transaction): """Store a valid transaction to the transactions collection.""" diff --git a/bigchaindb/web/views/parameters.py b/bigchaindb/web/views/parameters.py index 6eca4a97..58eb8a5f 100644 --- a/bigchaindb/web/views/parameters.py +++ b/bigchaindb/web/views/parameters.py @@ -29,4 +29,14 @@ def valid_operation(op): return 'CREATE' if op == 'TRANSFER': return 'TRANSFER' - raise ValueError('Operation must be "CREATE" or "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 index 39c6d529..37912140 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -55,6 +55,13 @@ class TransactionListApi(Resource): Return: A ``dict`` containing the data about the transaction. """ + parser = reqparse.RequestParser() + parser.add_argument('mode', type=parameters.valid_mode, + choices=['async', 'sync', 'commit']) + args = parser.parse_args() + if not str(args['mode']) or str(args['mode']) == 'None': + args['mode'] = 'broadcast_tx_async' + pool = current_app.config['bigchain_pool'] # `force` will try to format the body of the POST request even if the @@ -85,7 +92,7 @@ class TransactionListApi(Resource): 'Invalid transaction ({}): {}'.format(type(e).__name__, e) ) else: - bigchain.write_transaction(tx_obj) + bigchain.write_transaction(tx_obj, **args) response = jsonify(tx) response.status_code = 202 diff --git a/docs/server/source/http-client-server-api.rst b/docs/server/source/http-client-server-api.rst index 58ec5617..fce27c15 100644 --- a/docs/server/source/http-client-server-api.rst +++ b/docs/server/source/http-client-server-api.rst @@ -137,6 +137,10 @@ Transactions `_ to build a valid transaction. + A generalization of the parameter follows: + + :param mode: (Optional) Set the broadcast method. + **Example request**: .. literalinclude:: http-samples/post-tx-request.http @@ -161,6 +165,25 @@ Transactions :statuscode 202: The pushed transaction was accepted in the ``BACKLOG``, but the processing has not been completed. :statuscode 400: The transaction was malformed and not accepted in the ``BACKLOG``. +.. http:post:: /api/v1/transactions?mode={mode} + + .. note:: + + This option is only available when using BigchainDB with Tendermint. + + Tendermint offers a `broadcast API + `_ with three different modes to send transactions. + By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``broadcast_tx_async``, which + will return immediately and not wait to see if the transaction is valid. + + :query string mode: (Optional) One of the three supported modes to send a transaction: ``broadcast_tx_async``, ``broadcast_tx_sync``, ``broadcast_tx_commit``. + + The only change for a request, compared to the one above, is to set the mode + + .. sourcecode:: http + + GET /api/v1/transactions?mode={mode} HTTP/1.1 + Transaction Outputs ------------------- diff --git a/tests/tendermint/test_lib.py b/tests/tendermint/test_lib.py index 6cf28719..76c7c744 100644 --- a/tests/tendermint/test_lib.py +++ b/tests/tendermint/test_lib.py @@ -76,10 +76,45 @@ def test_write_and_post_transaction(mock_post, b): .sign([alice.private_key]).to_dict() tx = b.validate_transaction(tx) - b.write_transaction(tx) + b.write_transaction(tx, **{'mode': 'broadcast_tx_async'}) assert mock_post.called args, kwargs = mock_post.call_args assert 'broadcast_tx_async' == kwargs['json']['method'] encoded_tx = [encode_transaction(tx.to_dict())] assert encoded_tx == kwargs['json']['params'] + + +@patch('requests.post') +@pytest.mark.parametrize('mode', [ + {'mode': 'broadcast_tx_async'}, + {'mode': 'broadcast_tx_sync'}, + {'mode': 'broadcast_tx_commit'} +]) +def test_post_transaction_valid_modes(mock_post, b, mode): + from bigchaindb.models import Transaction + from bigchaindb.common.crypto import generate_key_pair + alice = generate_key_pair() + tx = Transaction.create([alice.public_key], + [([alice.public_key], 1)], + asset=None) \ + .sign([alice.private_key]).to_dict() + tx = b.validate_transaction(tx) + b.write_transaction(tx, **mode) + + args, kwargs = mock_post.call_args + assert mode['mode'] == kwargs['json']['method'] + + +def test_post_transaction_invalid_mode(b): + from bigchaindb.models import Transaction + from bigchaindb.common.crypto import generate_key_pair + from bigchaindb.common.exceptions import ValidationError + alice = generate_key_pair() + tx = Transaction.create([alice.public_key], + [([alice.public_key], 1)], + asset=None) \ + .sign([alice.private_key]).to_dict() + tx = b.validate_transaction(tx) + with pytest.raises(ValidationError): + b.write_transaction(tx, **{'mode': 'nope'}) diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index 95e1ff4b..39e1a00b 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -410,3 +410,39 @@ def test_return_only_valid_transaction(client): get_transaction_patched(Bigchain.TX_IN_BACKLOG)): url = '{}{}'.format(TX_ENDPOINT, '123') assert client.get(url).status_code == 404 + + +@pytest.mark.tendermint +@patch('requests.post') +@pytest.mark.parametrize('mode', [ + ('', 'broadcast_tx_async'), + ('?mode=async', 'broadcast_tx_async'), + ('?mode=sync', 'broadcast_tx_sync'), + ('?mode=commit', 'broadcast_tx_commit'), +]) +def test_post_transaction_valid_modes(mock_post, client, mode): + from bigchaindb.models import Transaction + from bigchaindb.common.crypto import generate_key_pair + alice = generate_key_pair() + tx = Transaction.create([alice.public_key], + [([alice.public_key], 1)], + asset=None) \ + .sign([alice.private_key]) + mode_endpoint = TX_ENDPOINT + mode[0] + client.post(mode_endpoint, data=json.dumps(tx.to_dict())) + args, kwargs = mock_post.call_args + assert mode[1] == kwargs['json']['method'] + + +@pytest.mark.tendermint +def test_post_transaction_invalid_mode(client): + from bigchaindb.models import Transaction + from bigchaindb.common.crypto import generate_key_pair + alice = generate_key_pair() + tx = Transaction.create([alice.public_key], + [([alice.public_key], 1)], + asset=None) \ + .sign([alice.private_key]) + mode_endpoint = TX_ENDPOINT + '?mode=nope' + response = client.post(mode_endpoint, data=json.dumps(tx.to_dict())) + assert '400 BAD REQUEST' in response.status From b7d235379a69126accfc37ff81d9f70c656473ab Mon Sep 17 00:00:00 2001 From: codegeschrei Date: Thu, 11 Jan 2018 13:44:54 +0100 Subject: [PATCH 2/6] fix tests --- bigchaindb/web/views/transactions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index 37912140..e49cd189 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -56,8 +56,7 @@ class TransactionListApi(Resource): A ``dict`` containing the data about the transaction. """ parser = reqparse.RequestParser() - parser.add_argument('mode', type=parameters.valid_mode, - choices=['async', 'sync', 'commit']) + parser.add_argument('mode', type=parameters.valid_mode) args = parser.parse_args() if not str(args['mode']) or str(args['mode']) == 'None': args['mode'] = 'broadcast_tx_async' From 411d1963bb1832ab73c237d04dc990109bcd7b7c Mon Sep 17 00:00:00 2001 From: codegeschrei Date: Fri, 12 Jan 2018 11:06:27 +0100 Subject: [PATCH 3/6] requested changes --- bigchaindb/tendermint/lib.py | 3 +-- bigchaindb/web/views/transactions.py | 5 ++--- tests/web/test_transactions.py | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 3cf93ce5..66b568e2 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -41,10 +41,9 @@ class BigchainDB(Bigchain): # TODO: handle connection errors! requests.post(ENDPOINT, json=payload) - def write_transaction(self, transaction, **kwargs): + def write_transaction(self, transaction, **mode): # This method offers backward compatibility with the Web API. """Submit a valid transaction to the mempool.""" - mode = kwargs self.post_transaction(transaction, mode) def store_transaction(self, transaction): diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index e49cd189..e53058b4 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -56,10 +56,9 @@ class TransactionListApi(Resource): A ``dict`` containing the data about the transaction. """ parser = reqparse.RequestParser() - parser.add_argument('mode', type=parameters.valid_mode) + parser.add_argument('mode', type=parameters.valid_mode, + default='broadcast_tx_async') args = parser.parse_args() - if not str(args['mode']) or str(args['mode']) == 'None': - args['mode'] = 'broadcast_tx_async' pool = current_app.config['bigchain_pool'] diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index 39e1a00b..a67d790e 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -446,3 +446,5 @@ def test_post_transaction_invalid_mode(client): mode_endpoint = TX_ENDPOINT + '?mode=nope' response = client.post(mode_endpoint, data=json.dumps(tx.to_dict())) assert '400 BAD REQUEST' in response.status + assert 'Mode must be "async", "sync" or "commit"' ==\ + json.loads(response.data.decode('utf8'))['message']['mode'] From 1d99d6e8a8eebb88b9168ab69e05b372ab270a94 Mon Sep 17 00:00:00 2001 From: codegeschrei Date: Mon, 15 Jan 2018 17:12:17 +0100 Subject: [PATCH 4/6] requested changes --- bigchaindb/core.py | 2 +- bigchaindb/tendermint/lib.py | 14 +++++++------- bigchaindb/web/views/transactions.py | 3 ++- tests/tendermint/test_lib.py | 14 +++++++------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index fae16e9d..4fb84722 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -77,7 +77,7 @@ class Bigchain(object): federation = property(lambda self: set(self.nodes_except_me + [self.me])) """ Set of federation member public keys """ - def write_transaction(self, signed_transaction, **kwargs): + def write_transaction(self, signed_transaction): """Write the transaction to bigchain. When first writing a transaction to the bigchain the transaction will be kept in a backlog until diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 66b568e2..61d1834b 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -19,21 +19,21 @@ logger = logging.getLogger(__name__) TENDERMINT_HOST = getenv('TENDERMINT_HOST', 'localhost') TENDERMINT_PORT = getenv('TENDERMINT_PORT', '46657') ENDPOINT = 'http://{}:{}/'.format(TENDERMINT_HOST, TENDERMINT_PORT) +MODE_LIST = ('broadcast_tx_async', + 'broadcast_tx_sync', + 'broadcast_tx_commit') class BigchainDB(Bigchain): def post_transaction(self, transaction, mode): """Submit a valid transaction to the mempool.""" - mode_list = ('broadcast_tx_async', - 'broadcast_tx_sync', - 'broadcast_tx_commit') - if not mode or mode['mode'] not in mode_list: + if not mode or mode not in MODE_LIST: raise ValidationError(('Mode must be one of the following {}.') - .format(', '.join(mode_list))) + .format(', '.join(MODE_LIST))) payload = { - 'method': mode['mode'], + 'method': mode, 'jsonrpc': '2.0', 'params': [encode_transaction(transaction.to_dict())], 'id': str(uuid4()) @@ -41,7 +41,7 @@ class BigchainDB(Bigchain): # TODO: handle connection errors! requests.post(ENDPOINT, json=payload) - def write_transaction(self, transaction, **mode): + def write_transaction(self, transaction, mode): # This method offers backward compatibility with the Web API. """Submit a valid transaction to the mempool.""" self.post_transaction(transaction, mode) diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index e53058b4..c746f2ad 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -59,6 +59,7 @@ class TransactionListApi(Resource): 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'] @@ -90,7 +91,7 @@ class TransactionListApi(Resource): 'Invalid transaction ({}): {}'.format(type(e).__name__, e) ) else: - bigchain.write_transaction(tx_obj, **args) + bigchain.write_transaction(tx_obj, mode) response = jsonify(tx) response.status_code = 202 diff --git a/tests/tendermint/test_lib.py b/tests/tendermint/test_lib.py index 76c7c744..3355683c 100644 --- a/tests/tendermint/test_lib.py +++ b/tests/tendermint/test_lib.py @@ -76,7 +76,7 @@ def test_write_and_post_transaction(mock_post, b): .sign([alice.private_key]).to_dict() tx = b.validate_transaction(tx) - b.write_transaction(tx, **{'mode': 'broadcast_tx_async'}) + b.write_transaction(tx, 'broadcast_tx_async') assert mock_post.called args, kwargs = mock_post.call_args @@ -87,9 +87,9 @@ def test_write_and_post_transaction(mock_post, b): @patch('requests.post') @pytest.mark.parametrize('mode', [ - {'mode': 'broadcast_tx_async'}, - {'mode': 'broadcast_tx_sync'}, - {'mode': 'broadcast_tx_commit'} + 'broadcast_tx_async', + 'broadcast_tx_sync', + 'broadcast_tx_commit' ]) def test_post_transaction_valid_modes(mock_post, b, mode): from bigchaindb.models import Transaction @@ -100,10 +100,10 @@ def test_post_transaction_valid_modes(mock_post, b, mode): asset=None) \ .sign([alice.private_key]).to_dict() tx = b.validate_transaction(tx) - b.write_transaction(tx, **mode) + b.write_transaction(tx, mode) args, kwargs = mock_post.call_args - assert mode['mode'] == kwargs['json']['method'] + assert mode == kwargs['json']['method'] def test_post_transaction_invalid_mode(b): @@ -117,4 +117,4 @@ def test_post_transaction_invalid_mode(b): .sign([alice.private_key]).to_dict() tx = b.validate_transaction(tx) with pytest.raises(ValidationError): - b.write_transaction(tx, **{'mode': 'nope'}) + b.write_transaction(tx, 'nope') From e74fb096fcb67fe0a6620542723b46f862dba96a Mon Sep 17 00:00:00 2001 From: codegeschrei Date: Wed, 24 Jan 2018 11:59:50 +0100 Subject: [PATCH 5/6] requested changes --- .../generate_http_server_api_documentation.py | 2 +- docs/server/source/http-client-server-api.rst | 47 ++++++++----------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/docs/server/generate_http_server_api_documentation.py b/docs/server/generate_http_server_api_documentation.py index 276c7f44..52520f47 100644 --- a/docs/server/generate_http_server_api_documentation.py +++ b/docs/server/generate_http_server_api_documentation.py @@ -58,7 +58,7 @@ Content-Type: application/json """ TPLS['post-tx-request'] = """\ -POST /api/v1/transactions/ HTTP/1.1 +POST /api/v1/transactions?mode=async HTTP/1.1 Host: example.com Content-Type: application/json diff --git a/docs/server/source/http-client-server-api.rst b/docs/server/source/http-client-server-api.rst index fce27c15..c088f926 100644 --- a/docs/server/source/http-client-server-api.rst +++ b/docs/server/source/http-client-server-api.rst @@ -124,22 +124,19 @@ Transactions :statuscode 400: The request wasn't understood by the server, e.g. the ``asset_id`` querystring was not included in the request. -.. http:post:: /api/v1/transactions +.. http:post:: /api/v1/transactions?mode={mode} - Push a new transaction. + Tendermint offers a `broadcast API + `_ with three different modes to post transactions. + By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``async``, which + will return immediately and not wait to see if the transaction is valid. The ``sync`` mode will return after the transactions is validated, while ``commit`` + returns after a transaction is committed to a block. .. note:: - The posted `transaction - `_ - should be structurally valid and not spending an already spent output. - The steps to build a valid transaction are beyond the scope of this page. - One would normally use a driver such as the `BigchainDB Python Driver - `_ - to build a valid transaction. - A generalization of the parameter follows: + This option is only available when using BigchainDB with Tendermint. - :param mode: (Optional) Set the broadcast method. + :query string mode: (Optional) One of the three supported modes to send a transaction: ``async``, ``sync``, ``commit``. **Example request**: @@ -165,25 +162,19 @@ Transactions :statuscode 202: The pushed transaction was accepted in the ``BACKLOG``, but the processing has not been completed. :statuscode 400: The transaction was malformed and not accepted in the ``BACKLOG``. -.. http:post:: /api/v1/transactions?mode={mode} + +.. http:post:: /api/v1/transactions + + The endpoint without any parameters will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used. .. note:: - - This option is only available when using BigchainDB with Tendermint. - - Tendermint offers a `broadcast API - `_ with three different modes to send transactions. - By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``broadcast_tx_async``, which - will return immediately and not wait to see if the transaction is valid. - - :query string mode: (Optional) One of the three supported modes to send a transaction: ``broadcast_tx_async``, ``broadcast_tx_sync``, ``broadcast_tx_commit``. - - The only change for a request, compared to the one above, is to set the mode - - .. sourcecode:: http - - GET /api/v1/transactions?mode={mode} HTTP/1.1 - + The posted `transaction + `_ + should be structurally valid and not spending an already spent output. + The steps to build a valid transaction are beyond the scope of this page. + One would normally use a driver such as the `BigchainDB Python Driver + `_ + to build a valid transaction. Transaction Outputs ------------------- From 328f2ed1933d760fd8f09efee0184e2d9583960a Mon Sep 17 00:00:00 2001 From: Troy McConaghy Date: Wed, 24 Jan 2018 16:43:35 +0100 Subject: [PATCH 6/6] Some minor copy-editing --- docs/server/source/http-client-server-api.rst | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/server/source/http-client-server-api.rst b/docs/server/source/http-client-server-api.rst index c088f926..2a17220d 100644 --- a/docs/server/source/http-client-server-api.rst +++ b/docs/server/source/http-client-server-api.rst @@ -129,13 +129,23 @@ Transactions Tendermint offers a `broadcast API `_ with three different modes to post transactions. By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``async``, which - will return immediately and not wait to see if the transaction is valid. The ``sync`` mode will return after the transactions is validated, while ``commit`` - returns after a transaction is committed to a block. + will return immediately and not wait to see if the transaction is valid. The ``sync`` mode will return after the transaction is validated, while ``commit`` + returns after the transaction is committed to a block. .. note:: This option is only available when using BigchainDB with Tendermint. + .. note:: + + The posted `transaction + `_ + should be structurally valid and not spending an already spent output. + The steps to build a valid transaction are beyond the scope of this page. + One would normally use a driver such as the `BigchainDB Python Driver + `_ + to build a valid transaction. + :query string mode: (Optional) One of the three supported modes to send a transaction: ``async``, ``sync``, ``commit``. **Example request**: @@ -165,16 +175,8 @@ Transactions .. http:post:: /api/v1/transactions - The endpoint without any parameters will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used. + This endpoint (without any parameters) will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used. - .. note:: - The posted `transaction - `_ - should be structurally valid and not spending an already spent output. - The steps to build a valid transaction are beyond the scope of this page. - One would normally use a driver such as the `BigchainDB Python Driver - `_ - to build a valid transaction. Transaction Outputs -------------------