Merge branch 'master' into implement-connection-run-for-mongodb

This commit is contained in:
vrde 2017-01-30 23:45:44 +01:00
commit d8ba1f8f67
No known key found for this signature in database
GPG Key ID: 6581C7C39B3D397D
14 changed files with 168 additions and 107 deletions

View File

@ -16,6 +16,27 @@ For reference, the possible headings are:
* **Notes** * **Notes**
## [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
= commit:
committed:
### 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 ## [0.8.0] - 2016-11-29
Tag name: v0.8.0 Tag name: v0.8.0
= commit: = commit:

View File

@ -1,6 +1,7 @@
"""Query implementation for MongoDB""" """Query implementation for MongoDB"""
from time import time from time import time
from itertools import chain
from pymongo import ReturnDocument from pymongo import ReturnDocument
from pymongo import errors from pymongo import errors
@ -8,6 +9,7 @@ from pymongo import errors
from bigchaindb import backend from bigchaindb import backend
from bigchaindb.common.exceptions import CyclicBlockchainError from bigchaindb.common.exceptions import CyclicBlockchainError
from bigchaindb.common.transaction import Transaction
from bigchaindb.backend.utils import module_dispatch_registrar from bigchaindb.backend.utils import module_dispatch_registrar
from bigchaindb.backend.mongodb.connection import MongoDBConnection, collection from bigchaindb.backend.mongodb.connection import MongoDBConnection, collection
@ -97,6 +99,43 @@ def get_blocks_status_from_transaction(conn, transaction_id):
projection=['id', 'block.voters'])) projection=['id', 'block.voters']))
@register_query(MongoDBConnection)
def get_txids_filtered(conn, asset_id, operation=None):
parts = []
if operation in (Transaction.CREATE, None):
# get the txid of the create transaction for asset_id
cursor = conn.db['bigchain'].aggregate([
{'$match': {
'block.transactions.id': asset_id,
'block.transactions.operation': 'CREATE'
}},
{'$unwind': '$block.transactions'},
{'$match': {
'block.transactions.id': asset_id,
'block.transactions.operation': 'CREATE'
}},
{'$project': {'block.transactions.id': True}}
])
parts.append(elem['block']['transactions']['id'] for elem in cursor)
if operation in (Transaction.TRANSFER, None):
# get txids of transfer transaction with asset_id
cursor = conn.db['bigchain'].aggregate([
{'$match': {
'block.transactions.asset.id': asset_id
}},
{'$unwind': '$block.transactions'},
{'$match': {
'block.transactions.asset.id': asset_id
}},
{'$project': {'block.transactions.id': True}}
])
parts.append(elem['block']['transactions']['id'] for elem in cursor)
return chain(*parts)
@register_query(MongoDBConnection) @register_query(MongoDBConnection)
def get_asset_by_id(conn, asset_id): def get_asset_by_id(conn, asset_id):
cursor = conn.run( cursor = conn.run(
@ -253,37 +292,18 @@ def get_last_voted_block(conn, node_pubkey):
@register_query(MongoDBConnection) @register_query(MongoDBConnection)
def get_unvoted_blocks(conn, node_pubkey): def get_unvoted_blocks(conn, node_pubkey):
return conn.run( return conn.db['bigchain'].aggregate([
collection('bigchain').aggregate([ {'$lookup': {
{'$lookup': { 'from': 'votes',
'from': 'votes', 'localField': 'id',
'localField': 'id', 'foreignField': 'vote.voting_for_block',
'foreignField': 'vote.voting_for_block', 'as': 'votes'
'as': 'votes' }},
}}, {'$match': {
{'$match': { 'votes.node_pubkey': {'$ne': node_pubkey},
'votes.node_pubkey': {'$ne': node_pubkey}, 'block.transactions.operation': {'$ne': 'GENESIS'}
'block.transactions.operation': {'$ne': 'GENESIS'} }},
}}, {'$project': {
{'$project': { 'votes': False, '_id': False
'votes': False, '_id': False }}
}} ])
]))
@register_query(MongoDBConnection)
def get_txids_filtered(conn, asset_id, operation=None):
match = {'block.transactions.asset.id': asset_id}
if operation:
match['block.transactions.operation'] = operation
cursor = conn.run(
collection('bigchain')
.aggregate([
{'$match': match},
{'$unwind': '$block.transactions'},
{'$match': match},
{'$project': {'block.transactions.id': True}}
]))
return (r['block']['transactions']['id'] for r in cursor)

View File

@ -77,3 +77,5 @@ class RethinkDBConnection(Connection):
wait_time = 2**i wait_time = 2**i
logging.debug('Error connecting to database, waiting %ss', wait_time) logging.debug('Error connecting to database, waiting %ss', wait_time)
time.sleep(wait_time) time.sleep(wait_time)
else:
break

View File

@ -1,9 +1,11 @@
from itertools import chain
from time import time from time import time
import rethinkdb as r import rethinkdb as r
from bigchaindb import backend, utils from bigchaindb import backend, utils
from bigchaindb.common import exceptions from bigchaindb.common import exceptions
from bigchaindb.common.transaction import Transaction
from bigchaindb.backend.utils import module_dispatch_registrar from bigchaindb.backend.utils import module_dispatch_registrar
from bigchaindb.backend.rethinkdb.connection import RethinkDBConnection from bigchaindb.backend.rethinkdb.connection import RethinkDBConnection
@ -71,6 +73,30 @@ def get_blocks_status_from_transaction(connection, transaction_id):
.pluck('votes', 'id', {'block': ['voters']})) .pluck('votes', 'id', {'block': ['voters']}))
@register_query(RethinkDBConnection)
def get_txids_filtered(connection, asset_id, operation=None):
# here we only want to return the transaction ids since later on when
# we are going to retrieve the transaction with status validation
parts = []
if operation in (Transaction.CREATE, None):
# First find the asset's CREATE transaction
parts.append(connection.run(
_get_asset_create_tx_query(asset_id).get_field('id')))
if operation in (Transaction.TRANSFER, None):
# Then find any TRANSFER transactions related to the asset
parts.append(connection.run(
r.table('bigchain')
.get_all(asset_id, index='asset_id')
.concat_map(lambda block: block['block']['transactions'])
.filter(lambda transaction: transaction['asset']['id'] == asset_id)
.get_field('id')))
return chain(*parts)
@register_query(RethinkDBConnection) @register_query(RethinkDBConnection)
def get_asset_by_id(connection, asset_id): def get_asset_by_id(connection, asset_id):
return connection.run(_get_asset_create_tx_query(asset_id).pluck('asset')) return connection.run(_get_asset_create_tx_query(asset_id).pluck('asset'))
@ -233,20 +259,3 @@ def get_unvoted_blocks(connection, node_pubkey):
# database level. Solving issue #444 can help untangling the situation # database level. Solving issue #444 can help untangling the situation
unvoted_blocks = filter(lambda block: not utils.is_genesis_block(block), unvoted) unvoted_blocks = filter(lambda block: not utils.is_genesis_block(block), unvoted)
return unvoted_blocks return unvoted_blocks
@register_query(RethinkDBConnection)
def get_txids_filtered(connection, asset_id, operation=None):
# here we only want to return the transaction ids since later on when
# we are going to retrieve the transaction with status validation
tx_filter = r.row['asset']['id'] == asset_id
if operation:
tx_filter &= r.row['operation'] == operation
return connection.run(
r.table('bigchain')
.get_all(asset_id, index='asset_id')
.concat_map(lambda block: block['block']['transactions'])
.filter(tx_filter)
.get_field('id'))

View File

@ -103,8 +103,8 @@ definitions:
description: | description: |
Description of the asset being transacted. In the case of a ``TRANSFER`` Description of the asset being transacted. In the case of a ``TRANSFER``
transaction, this field contains only the ID of asset. In the case transaction, this field contains only the ID of asset. In the case
of a ``CREATE`` transaction, this field contains the user-defined of a ``CREATE`` transaction, this field contains only the user-defined
payload and the asset ID (duplicated from the Transaction ID). payload.
additionalProperties: false additionalProperties: false
properties: properties:
id: id:

View File

@ -444,7 +444,6 @@ class Transaction(object):
asset is not None and not (isinstance(asset, dict) and 'data' in asset)): 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` ' raise TypeError(('`asset` must be None or a dict holding a `data` '
" property instance for '{}' Transactions".format(operation))) " property instance for '{}' Transactions".format(operation)))
asset.pop('id', None) # Remove duplicated asset ID if there is one
elif (operation == Transaction.TRANSFER and elif (operation == Transaction.TRANSFER and
not (isinstance(asset, dict) and 'id' in asset)): not (isinstance(asset, dict) and 'id' in asset)):
raise TypeError(('`asset` must be a dict holding an `id` property ' raise TypeError(('`asset` must be a dict holding an `id` property '
@ -927,11 +926,9 @@ class Transaction(object):
tx_no_signatures = Transaction._remove_signatures(tx) tx_no_signatures = Transaction._remove_signatures(tx)
tx_serialized = Transaction._to_str(tx_no_signatures) tx_serialized = Transaction._to_str(tx_no_signatures)
tx['id'] = Transaction._to_hash(tx_serialized) tx_id = Transaction._to_hash(tx_serialized)
if self.operation == Transaction.CREATE:
# Duplicate asset into asset for consistency with TRANSFER tx['id'] = tx_id
# transactions
tx['asset']['id'] = tx['id']
return tx return tx
@staticmethod @staticmethod
@ -955,9 +952,6 @@ class Transaction(object):
# case could yield incorrect signatures. This is why we only # case could yield incorrect signatures. This is why we only
# set it to `None` if it's set in the dict. # set it to `None` if it's set in the dict.
input_['fulfillment'] = None input_['fulfillment'] = None
# Pop duplicated asset_id from CREATE tx
if tx_dict['operation'] == Transaction.CREATE:
tx_dict['asset'].pop('id', None)
return tx_dict return tx_dict
@staticmethod @staticmethod
@ -1037,10 +1031,6 @@ class Transaction(object):
"the hash of its body, i.e. it's not valid.") "the hash of its body, i.e. it's not valid.")
raise InvalidHash(err_msg.format(proposed_tx_id)) raise InvalidHash(err_msg.format(proposed_tx_id))
if tx_body.get('operation') == Transaction.CREATE:
if proposed_tx_id != tx_body['asset'].get('id'):
raise InvalidHash('CREATE tx has wrong asset_id')
@classmethod @classmethod
def from_dict(cls, tx): def from_dict(cls, tx):
"""Transforms a Python dictionary to a Transaction object. """Transforms a Python dictionary to a Transaction object.

View File

@ -88,6 +88,11 @@ class Transaction(Transaction):
if output.amount < 1: if output.amount < 1:
raise AmountError('`amount` needs to be greater than zero') raise AmountError('`amount` needs to be greater than zero')
# 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 # validate asset id
asset_id = Transaction.get_asset_id(input_txs) asset_id = Transaction.get_asset_id(input_txs)
if asset_id != self.asset['id']: if asset_id != self.asset['id']:

View File

@ -8,6 +8,7 @@ def valid_txid(txid):
def valid_bool(val): def valid_bool(val):
val = val.lower()
if val == 'true': if val == 'true':
return True return True
if val == 'false': if val == 'false':
@ -23,6 +24,7 @@ def valid_ed25519(key):
def valid_operation(op): def valid_operation(op):
op = op.upper()
if op == 'CREATE': if op == 'CREATE':
return 'CREATE' return 'CREATE'
if op == 'TRANSFER': if op == 'TRANSFER':

View File

@ -1,11 +1,10 @@
# The Digital Asset Model # The Digital Asset Model
The asset ID is the same as the ID of the CREATE transaction that defined the asset. To avoid redundant data in transactions, the digital asset model is different for `CREATE` and `TRANSFER` transactions.
In the case of a CREATE transaction, the transaction ID is duplicated into the asset object for clarity and consistency in the database. The CREATE transaction also contains a user definable payload to describe the asset: A digital asset's properties are defined in a `CREATE` transaction with the following model:
```json ```json
{ {
"id": "<same as transaction ID (sha3-256 hash)>",
"data": "<json document>" "data": "<json document>"
} }
``` ```

View File

@ -165,7 +165,7 @@ not already been spent.
Returns a list of links to transaction outputs. Returns a list of links to transaction outputs.
:param public_key: Base58 encoded public key associated with output ownership. This parameter is mandatory and without it the endpoint will return a ``400`` response code. :param public_key: Base58 encoded public key associated with output ownership. This parameter is mandatory and without it the endpoint will return a ``400`` response code.
:param unspent: Boolean value ("true" or "false") indicating if the result set should be limited to outputs that are available to spend. :param unspent: Boolean value ("true" or "false") indicating if the result set should be limited to outputs that are available to spend. Defaults to "false".
**Example request**: **Example request**:

View File

@ -1,6 +1,7 @@
import time import time
import multiprocessing as mp import multiprocessing as mp
from threading import Thread from threading import Thread
from unittest.mock import patch
import pytest import pytest
import rethinkdb as r import rethinkdb as r
@ -118,3 +119,15 @@ def test_changefeed_reconnects_when_connection_lost(monkeypatch):
fact = changefeed.outqueue.get()['fact'] fact = changefeed.outqueue.get()['fact']
assert fact == 'Cats sleep 70% of their lives.' assert fact == 'Cats sleep 70% of their lives.'
@patch('rethinkdb.connect')
def test_connection_happens_one_time_if_successful(mock_connect):
from bigchaindb.backend import connect
query = r.expr('1')
conn = connect('rethinkdb', 'localhost', 1337, 'whatev')
conn.run(query)
mock_connect.assert_called_once_with(host='localhost',
port=1337,
db='whatev')

View File

@ -300,7 +300,6 @@ def test_transaction_serialization(user_input, user_output, data):
'operation': Transaction.CREATE, 'operation': Transaction.CREATE,
'metadata': None, 'metadata': None,
'asset': { 'asset': {
'id': tx_id,
'data': data, 'data': data,
} }
} }
@ -308,7 +307,7 @@ def test_transaction_serialization(user_input, user_output, data):
tx = Transaction(Transaction.CREATE, {'data': data}, [user_input], tx = Transaction(Transaction.CREATE, {'data': data}, [user_input],
[user_output]) [user_output])
tx_dict = tx.to_dict() tx_dict = tx.to_dict()
tx_dict['id'] = tx_dict['asset']['id'] = tx_id tx_dict['id'] = tx_id
assert tx_dict == expected assert tx_dict == expected
@ -335,7 +334,6 @@ def test_transaction_deserialization(user_input, user_output, data):
} }
tx_no_signatures = Transaction._remove_signatures(tx) tx_no_signatures = Transaction._remove_signatures(tx)
tx['id'] = Transaction._to_hash(Transaction._to_str(tx_no_signatures)) tx['id'] = Transaction._to_hash(Transaction._to_str(tx_no_signatures))
tx['asset']['id'] = tx['id']
tx = Transaction.from_dict(tx) tx = Transaction.from_dict(tx)
assert tx == expected assert tx == expected
@ -691,7 +689,6 @@ def test_create_create_transaction_single_io(user_output, user_pub, data):
tx_dict = tx.to_dict() tx_dict = tx.to_dict()
tx_dict['inputs'][0]['fulfillment'] = None tx_dict['inputs'][0]['fulfillment'] = None
tx_dict.pop('id') tx_dict.pop('id')
tx_dict['asset'].pop('id')
assert tx_dict == expected assert tx_dict == expected
@ -775,7 +772,6 @@ def test_create_create_transaction_threshold(user_pub, user2_pub, user3_pub,
metadata=data, asset=data) metadata=data, asset=data)
tx_dict = tx.to_dict() tx_dict = tx.to_dict()
tx_dict.pop('id') tx_dict.pop('id')
tx_dict['asset'].pop('id')
tx_dict['inputs'][0]['fulfillment'] = None tx_dict['inputs'][0]['fulfillment'] = None
assert tx_dict == expected assert tx_dict == expected
@ -989,25 +985,3 @@ def test_validate_version(utx):
utx.version = '1.0.0' utx.version = '1.0.0'
with raises(SchemaValidationError): with raises(SchemaValidationError):
validate_transaction_model(utx) validate_transaction_model(utx)
def test_create_tx_has_asset_id(tx):
tx = tx.to_dict()
assert tx['id'] == tx['asset']['id']
def test_create_tx_validates_asset_id(tx):
from bigchaindb.common.transaction import Transaction
from bigchaindb.common.exceptions import InvalidHash
tx = tx.to_dict()
# Test fails with wrong asset_id
tx['asset']['id'] = tx['asset']['id'][::-1]
with raises(InvalidHash):
Transaction.from_dict(tx)
# Test fails with no asset_id
tx['asset'].pop('id')
with raises(InvalidHash):
Transaction.from_dict(tx)

View File

@ -1192,3 +1192,33 @@ def test_get_outputs_filtered():
get_outputs.assert_called_once_with('abc') get_outputs.assert_called_once_with('abc')
get_spent.assert_not_called() get_spent.assert_not_called()
assert out == get_outputs.return_value assert out == get_outputs.return_value
@pytest.mark.bdb
def test_cant_spend_same_input_twice_in_tx(b, genesis_block):
"""
Recreate duplicated fulfillments bug
https://github.com/bigchaindb/bigchaindb/issues/1099
"""
from bigchaindb.models import Transaction
from bigchaindb.common.exceptions import DoubleSpend
# create a divisible asset
tx_create = Transaction.create([b.me], [([b.me], 100)])
tx_create_signed = tx_create.sign([b.me_private])
assert b.validate_transaction(tx_create_signed) == tx_create_signed
# create a block and valid vote
block = b.create_block([tx_create_signed])
b.write_block(block)
vote = b.vote(block.id, genesis_block.id, True)
b.write_vote(vote)
# Create a transfer transaction with duplicated fulfillments
dup_inputs = tx_create.to_inputs() + tx_create.to_inputs()
tx_transfer = Transaction.transfer(dup_inputs, [([b.me], 200)],
asset_id=tx_create.id)
tx_transfer_signed = tx_transfer.sign([b.me_private])
assert b.is_valid_transaction(tx_transfer_signed) is False
with pytest.raises(DoubleSpend):
tx_transfer_signed.validate(b)

View File

@ -24,11 +24,9 @@ def test_valid_bool():
assert valid_bool('true') is True assert valid_bool('true') is True
assert valid_bool('false') is False assert valid_bool('false') is False
assert valid_bool('tRUE') is True
assert valid_bool('fALSE') is False
with pytest.raises(ValueError):
valid_bool('TRUE')
with pytest.raises(ValueError):
valid_bool('FALSE')
with pytest.raises(ValueError): with pytest.raises(ValueError):
valid_bool('0') valid_bool('0')
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -64,13 +62,11 @@ def test_valid_ed25519():
def test_valid_operation(): def test_valid_operation():
from bigchaindb.web.views.parameters import valid_operation from bigchaindb.web.views.parameters import valid_operation
assert valid_operation('CREATE') == 'CREATE' assert valid_operation('create') == 'CREATE'
assert valid_operation('TRANSFER') == 'TRANSFER' assert valid_operation('transfer') == 'TRANSFER'
assert valid_operation('CREATe') == 'CREATE'
assert valid_operation('TRANSFEr') == 'TRANSFER'
with pytest.raises(ValueError):
valid_operation('create')
with pytest.raises(ValueError):
valid_operation('transfer')
with pytest.raises(ValueError): with pytest.raises(ValueError):
valid_operation('GENESIS') valid_operation('GENESIS')
with pytest.raises(ValueError): with pytest.raises(ValueError):