Problem: utxoset needs to be updated

Solution: update utxoset via store_transaction
This commit is contained in:
Sylvain Bellemare 2018-02-08 14:53:20 +01:00
parent 00ec69a4bd
commit 51d4f0f2d9
6 changed files with 279 additions and 21 deletions

View File

@ -256,4 +256,5 @@ def delete_unspent_outputs(conn, *unspent_outputs):
def get_unspent_outputs(conn, *, query=None): def get_unspent_outputs(conn, *, query=None):
if query is None: if query is None:
query = {} query = {}
return conn.run(conn.collection('utxos').find(query)) return conn.run(conn.collection('utxos').find(query,
projection={'_id': False}))

View File

@ -61,6 +61,7 @@ class BigchainDB(Bigchain):
def store_transaction(self, transaction): def store_transaction(self, transaction):
"""Store a valid transaction to the transactions collection.""" """Store a valid transaction to the transactions collection."""
self.update_utxoset(transaction)
transaction = deepcopy(transaction.to_dict()) transaction = deepcopy(transaction.to_dict())
if transaction['operation'] == 'CREATE': if transaction['operation'] == 'CREATE':
asset = transaction.pop('asset') asset = transaction.pop('asset')
@ -81,6 +82,7 @@ class BigchainDB(Bigchain):
assets = [] assets = []
txn_metadatas = [] txn_metadatas = []
for transaction in transactions: for transaction in transactions:
self.update_utxoset(transaction)
transaction = transaction.to_dict() transaction = transaction.to_dict()
if transaction['operation'] == 'CREATE': if transaction['operation'] == 'CREATE':
asset = transaction.pop('asset') asset = transaction.pop('asset')
@ -98,6 +100,56 @@ class BigchainDB(Bigchain):
backend.query.store_assets(self.connection, assets) backend.query.store_assets(self.connection, assets)
return backend.query.store_transactions(self.connection, txns) return backend.query.store_transactions(self.connection, txns)
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_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 get_transaction(self, transaction_id, include_status=False): def get_transaction(self, transaction_id, include_status=False):
transaction = backend.query.get_transaction(self.connection, transaction_id) transaction = backend.query.get_transaction(self.connection, transaction_id)
asset = backend.query.get_asset(self.connection, transaction_id) asset = backend.query.get_asset(self.connection, transaction_id)

View File

@ -6,23 +6,6 @@ import pymongo
pytestmark = [pytest.mark.tendermint, pytest.mark.localmongodb, pytest.mark.bdb] pytestmark = [pytest.mark.tendermint, pytest.mark.localmongodb, pytest.mark.bdb]
@pytest.fixture
def dummy_unspent_outputs():
return [
{'transaction_id': 'a', 'output_index': 0},
{'transaction_id': 'a', 'output_index': 1},
{'transaction_id': 'b', 'output_index': 0},
]
@pytest.fixture
def utxoset(dummy_unspent_outputs, utxo_collection):
insert_res = utxo_collection.insert_many(deepcopy(dummy_unspent_outputs))
assert insert_res.acknowledged
assert len(insert_res.inserted_ids) == 3
return dummy_unspent_outputs, utxo_collection
def test_get_txids_filtered(signed_create_tx, signed_transfer_tx): def test_get_txids_filtered(signed_create_tx, signed_transfer_tx):
from bigchaindb.backend import connect, query from bigchaindb.backend import connect, query
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
@ -286,7 +269,6 @@ def test_get_unspent_outputs(db_context, utxoset):
assert cursor.count() == 3 assert cursor.count() == 3
retrieved_utxoset = list(cursor) retrieved_utxoset = list(cursor)
unspent_outputs, utxo_collection = utxoset unspent_outputs, utxo_collection = utxoset
assert retrieved_utxoset == list(utxo_collection.find()) assert retrieved_utxoset == list(
for utxo in retrieved_utxoset: utxo_collection.find(projection={'_id': False}))
del utxo['_id']
assert retrieved_utxoset == unspent_outputs assert retrieved_utxoset == unspent_outputs

View File

@ -11,6 +11,7 @@ import random
from collections import namedtuple from collections import namedtuple
import pytest import pytest
from pymongo import MongoClient
from logging import getLogger from logging import getLogger
from logging.config import dictConfig from logging.config import dictConfig
@ -675,3 +676,30 @@ def unspent_output_2():
@pytest.fixture @pytest.fixture
def unspent_outputs(unspent_output_0, unspent_output_1, unspent_output_2): def unspent_outputs(unspent_output_0, unspent_output_1, unspent_output_2):
return unspent_output_0, unspent_output_1, unspent_output_2 return unspent_output_0, unspent_output_1, unspent_output_2
@pytest.fixture
def mongo_client(db_context):
return MongoClient(host=db_context.host, port=db_context.port)
@pytest.fixture
def utxo_collection(db_context, mongo_client):
return mongo_client[db_context.name].utxos
@pytest.fixture
def dummy_unspent_outputs():
return [
{'transaction_id': 'a', 'output_index': 0},
{'transaction_id': 'a', 'output_index': 1},
{'transaction_id': 'b', 'output_index': 0},
]
@pytest.fixture
def utxoset(dummy_unspent_outputs, utxo_collection):
res = utxo_collection.insert_many(copy.deepcopy(dummy_unspent_outputs))
assert res.acknowledged
assert len(res.inserted_ids) == 3
return dummy_unspent_outputs, utxo_collection

View File

@ -66,6 +66,12 @@ def test_deliver_tx__valid_create_updates_db(b):
app.end_block(99) app.end_block(99)
app.commit() app.commit()
assert b.get_transaction(tx.id).id == tx.id assert b.get_transaction(tx.id).id == tx.id
unspent_outputs = b.get_unspent_outputs()
unspent_output = next(unspent_outputs)
expected_unspent_output = next(tx.unspent_outputs)._asdict()
assert unspent_output == expected_unspent_output
with pytest.raises(StopIteration):
next(unspent_outputs)
def test_deliver_tx__double_spend_fails(b): def test_deliver_tx__double_spend_fails(b):

View File

@ -2,6 +2,7 @@ import os
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from pymongo import MongoClient
from bigchaindb import backend from bigchaindb import backend
@ -9,6 +10,7 @@ from bigchaindb import backend
pytestmark = pytest.mark.tendermint pytestmark = pytest.mark.tendermint
@pytest.mark.bdb
def test_asset_is_separated_from_transaciton(b): def test_asset_is_separated_from_transaciton(b):
from bigchaindb.models import Transaction from bigchaindb.models import Transaction
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
@ -122,3 +124,190 @@ def test_post_transaction_invalid_mode(b):
tx = b.validate_transaction(tx) tx = b.validate_transaction(tx)
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
b.write_transaction(tx, 'nope') b.write_transaction(tx, 'nope')
@pytest.mark.bdb
def test_update_utxoset(tb, signed_create_tx, signed_transfer_tx, db_context):
mongo_client = MongoClient(host=db_context.host, port=db_context.port)
tb.update_utxoset(signed_create_tx)
utxoset = mongo_client[db_context.name]['utxos']
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_create_tx.id
assert utxo['output_index'] == 0
tb.update_utxoset(signed_transfer_tx)
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_transfer_tx.id
assert utxo['output_index'] == 0
@pytest.mark.bdb
def test_store_transaction(mocker, tb, signed_create_tx,
signed_transfer_tx, db_context):
mocked_store_asset = mocker.patch('bigchaindb.backend.query.store_asset')
mocked_store_metadata = mocker.patch(
'bigchaindb.backend.query.store_metadata')
mocked_store_transaction = mocker.patch(
'bigchaindb.backend.query.store_transaction')
mongo_client = MongoClient(host=db_context.host, port=db_context.port)
tb.store_transaction(signed_create_tx)
utxoset = mongo_client[db_context.name]['utxos']
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_create_tx.id
assert utxo['output_index'] == 0
mocked_store_asset.assert_called_once_with(
tb.connection,
{'id': signed_create_tx.id, 'data': signed_create_tx.asset['data']},
)
mocked_store_metadata.assert_called_once_with(
tb.connection,
[{'id': signed_create_tx.id, 'metadata': signed_create_tx.metadata}],
)
mocked_store_transaction.assert_called_once_with(
tb.connection,
{k: v for k, v in signed_create_tx.to_dict().items()
if k not in ('asset', 'metadata')},
)
mocked_store_asset.reset_mock()
mocked_store_metadata.reset_mock()
mocked_store_transaction.reset_mock()
tb.store_transaction(signed_transfer_tx)
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_transfer_tx.id
assert utxo['output_index'] == 0
assert not mocked_store_asset.called
mocked_store_metadata.asser_called_once_with(
tb.connection,
{'id': signed_transfer_tx.id, 'metadata': signed_transfer_tx.metadata},
)
mocked_store_transaction.assert_called_once_with(
tb.connection,
{k: v for k, v in signed_transfer_tx.to_dict().items()
if k != 'metadata'},
)
@pytest.mark.bdb
def test_store_bulk_transaction(mocker, tb, signed_create_tx,
signed_transfer_tx, db_context):
mocked_store_assets = mocker.patch(
'bigchaindb.backend.query.store_assets')
mocked_store_metadata = mocker.patch(
'bigchaindb.backend.query.store_metadatas')
mocked_store_transactions = mocker.patch(
'bigchaindb.backend.query.store_transactions')
mongo_client = MongoClient(host=db_context.host, port=db_context.port)
tb.store_bulk_transactions((signed_create_tx,))
utxoset = mongo_client[db_context.name]['utxos']
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_create_tx.id
assert utxo['output_index'] == 0
mocked_store_assets.assert_called_once_with(
tb.connection,
[{'id': signed_create_tx.id, 'data': signed_create_tx.asset['data']}],
)
mocked_store_metadata.assert_called_once_with(
tb.connection,
[{'id': signed_create_tx.id, 'metadata': signed_create_tx.metadata}],
)
mocked_store_transactions.assert_called_once_with(
tb.connection,
[{k: v for k, v in signed_create_tx.to_dict().items()
if k not in ('asset', 'metadata')}],
)
mocked_store_assets.reset_mock()
mocked_store_metadata.reset_mock()
mocked_store_transactions.reset_mock()
tb.store_bulk_transactions((signed_transfer_tx,))
assert utxoset.count() == 1
utxo = utxoset.find_one()
assert utxo['transaction_id'] == signed_transfer_tx.id
assert utxo['output_index'] == 0
assert not mocked_store_assets.called
mocked_store_metadata.asser_called_once_with(
tb.connection,
[{'id': signed_transfer_tx.id,
'metadata': signed_transfer_tx.metadata}],
)
mocked_store_transactions.assert_called_once_with(
tb.connection,
[{k: v for k, v in signed_transfer_tx.to_dict().items()
if k != 'metadata'}],
)
@pytest.mark.bdb
def test_delete_zero_unspent_outputs(b, utxoset):
unspent_outputs, utxo_collection = utxoset
delete_res = b.delete_unspent_outputs()
assert delete_res is None
assert utxo_collection.count() == 3
assert utxo_collection.find(
{'$or': [
{'transaction_id': 'a', 'output_index': 0},
{'transaction_id': 'b', 'output_index': 0},
{'transaction_id': 'a', 'output_index': 1},
]}
).count() == 3
@pytest.mark.bdb
def test_delete_one_unspent_outputs(b, utxoset):
unspent_outputs, utxo_collection = utxoset
delete_res = b.delete_unspent_outputs(unspent_outputs[0])
assert delete_res['n'] == 1
assert utxo_collection.find(
{'$or': [
{'transaction_id': 'a', 'output_index': 1},
{'transaction_id': 'b', 'output_index': 0},
]}
).count() == 2
assert utxo_collection.find(
{'transaction_id': 'a', 'output_index': 0}).count() == 0
@pytest.mark.bdb
def test_delete_many_unspent_outputs(b, utxoset):
unspent_outputs, utxo_collection = utxoset
delete_res = b.delete_unspent_outputs(*unspent_outputs[::2])
assert delete_res['n'] == 2
assert utxo_collection.find(
{'$or': [
{'transaction_id': 'a', 'output_index': 0},
{'transaction_id': 'b', 'output_index': 0},
]}
).count() == 0
assert utxo_collection.find(
{'transaction_id': 'a', 'output_index': 1}).count() == 1
@pytest.mark.bdb
def test_store_zero_unspent_output(b, utxo_collection):
res = b.store_unspent_outputs()
assert res is None
assert utxo_collection.count() == 0
@pytest.mark.bdb
def test_store_one_unspent_output(b, unspent_output_1, utxo_collection):
res = b.store_unspent_outputs(unspent_output_1)
assert res.acknowledged
assert len(res.inserted_ids) == 1
assert utxo_collection.find(
{'transaction_id': unspent_output_1['transaction_id'],
'output_index': unspent_output_1['output_index']}
).count() == 1
@pytest.mark.bdb
def test_store_many_unspent_outputs(b, unspent_outputs, utxo_collection):
res = b.store_unspent_outputs(*unspent_outputs)
assert res.acknowledged
assert len(res.inserted_ids) == 3
assert utxo_collection.find(
{'transaction_id': unspent_outputs[0]['transaction_id']}
).count() == 3