diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 37fb9d6e..61d1834b 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -19,15 +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): + def post_transaction(self, transaction, mode): """Submit a valid transaction to the mempool.""" + if not mode or 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, 'jsonrpc': '2.0', 'params': [encode_transaction(transaction.to_dict())], 'id': str(uuid4()) @@ -35,11 +41,10 @@ class BigchainDB(Bigchain): # TODO: handle connection errors! requests.post(ENDPOINT, json=payload) - def write_transaction(self, transaction): + 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) + 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..c746f2ad 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -55,6 +55,12 @@ class TransactionListApi(Resource): 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 @@ -85,7 +91,7 @@ class TransactionListApi(Resource): 'Invalid transaction ({}): {}'.format(type(e).__name__, e) ) else: - bigchain.write_transaction(tx_obj) + bigchain.write_transaction(tx_obj, mode) response = jsonify(tx) response.status_code = 202 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 58ec5617..2a17220d 100644 --- a/docs/server/source/http-client-server-api.rst +++ b/docs/server/source/http-client-server-api.rst @@ -124,11 +124,20 @@ 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 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. @@ -137,6 +146,8 @@ Transactions `_ to build a valid transaction. + :query string mode: (Optional) One of the three supported modes to send a transaction: ``async``, ``sync``, ``commit``. + **Example request**: .. literalinclude:: http-samples/post-tx-request.http @@ -162,6 +173,11 @@ Transactions :statuscode 400: The transaction was malformed and not accepted in the ``BACKLOG``. +.. http:post:: /api/v1/transactions + + This endpoint (without any parameters) will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used. + + Transaction Outputs ------------------- diff --git a/tests/tendermint/test_lib.py b/tests/tendermint/test_lib.py index 6cf28719..3355683c 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, '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', [ + 'broadcast_tx_async', + 'broadcast_tx_sync', + '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 == 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, 'nope') diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index 95e1ff4b..a67d790e 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -410,3 +410,41 @@ 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 + assert 'Mode must be "async", "sync" or "commit"' ==\ + json.loads(response.data.decode('utf8'))['message']['mode']