mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
consensus plugin
decompose validate consensus per asset type
This commit is contained in:
parent
391713ad6e
commit
e394b07f05
@ -61,6 +61,7 @@ config = {
|
||||
# 'bichaindb.core': 'info',
|
||||
# },
|
||||
},
|
||||
'consensus_plugin': None
|
||||
}
|
||||
|
||||
# We need to maintain a backup copy of the original config dict in case
|
||||
|
0
bigchaindb/common/asset/__init__.py
Normal file
0
bigchaindb/common/asset/__init__.py
Normal file
104
bigchaindb/common/asset/consensus.py
Normal file
104
bigchaindb/common/asset/consensus.py
Normal file
@ -0,0 +1,104 @@
|
||||
from bigchaindb.common.exceptions import ValidationError
|
||||
from bigchaindb.consensus import BaseConsensusRules
|
||||
from bigchaindb.models import Transaction
|
||||
|
||||
ASSET_TYPE_MIX = 'mix'
|
||||
ASSET_TYPE_PURE = 'pure'
|
||||
ASSET_TYPE_COMPOSITION = 'composition'
|
||||
|
||||
ASSET_TYPES = [
|
||||
ASSET_TYPE_MIX,
|
||||
ASSET_TYPE_PURE,
|
||||
ASSET_TYPE_COMPOSITION
|
||||
]
|
||||
|
||||
|
||||
class AssetCompositionConsensusRules(BaseConsensusRules):
|
||||
|
||||
@staticmethod
|
||||
def validate_transaction(bigchain, transaction):
|
||||
|
||||
input_txs = None
|
||||
if transaction.operation == Transaction.TRANSFER:
|
||||
input_txs = transaction.get_input_txs(bigchain)
|
||||
|
||||
AssetCompositionConsensusRules\
|
||||
.validate_asset(bigchain, transaction, input_txs)
|
||||
|
||||
return transaction.validate(bigchain, input_txs)
|
||||
|
||||
@staticmethod
|
||||
def validate_asset(bigchain, transaction, input_txs):
|
||||
assets = AssetCompositionConsensusRules\
|
||||
.resolve_asset(bigchain, transaction, input_txs)
|
||||
|
||||
asset_types = set([asset['data']['type']
|
||||
for asset in assets
|
||||
if 'data' in asset
|
||||
and 'type' in asset['data']
|
||||
and asset['data']['type'] in ASSET_TYPES])
|
||||
|
||||
asset_type = ASSET_TYPE_PURE
|
||||
if len(asset_types) == 1:
|
||||
asset_type = asset_types.pop()
|
||||
if len(asset_types) > 1:
|
||||
raise ValidationError('Cannot mix assets')
|
||||
|
||||
if asset_type == ASSET_TYPE_PURE:
|
||||
return AssetCompositionConsensusRules\
|
||||
.validate_pure(bigchain, transaction, input_txs)
|
||||
elif asset_type == ASSET_TYPE_MIX:
|
||||
return AssetCompositionConsensusRules\
|
||||
.validate_mix(bigchain, transaction, input_txs)
|
||||
elif asset_type == ASSET_TYPE_COMPOSITION:
|
||||
return AssetCompositionConsensusRules\
|
||||
.validate_composition(bigchain, transaction, input_txs)
|
||||
|
||||
@staticmethod
|
||||
def validate_pure(bigchain, transaction, input_txs):
|
||||
transaction.validate_asset(
|
||||
bigchain,
|
||||
[input_tx
|
||||
for (input_, input_tx, status)
|
||||
in input_txs if input_tx is not None])
|
||||
|
||||
if transaction.operation == Transaction.TRANSFER:
|
||||
AssetCompositionConsensusRules\
|
||||
.validate_amount_conservation(transaction, input_txs)
|
||||
|
||||
return transaction
|
||||
|
||||
@staticmethod
|
||||
def validate_mix(bigchain, transaction, input_txs):
|
||||
if transaction.operation == Transaction.TRANSFER:
|
||||
pass
|
||||
return transaction
|
||||
|
||||
@staticmethod
|
||||
def validate_composition(bigchain, transaction, input_txs):
|
||||
if transaction.operation == Transaction.TRANSFER:
|
||||
AssetCompositionConsensusRules \
|
||||
.validate_amount_conservation(transaction, input_txs)
|
||||
|
||||
return transaction
|
||||
|
||||
@staticmethod
|
||||
def validate_amount_conservation(transaction, input_txs):
|
||||
transaction.validate_amount(
|
||||
[input_tx.outputs[input_.fulfills.output]
|
||||
for (input_, input_tx, status)
|
||||
in input_txs if input_tx is not None])
|
||||
|
||||
@staticmethod
|
||||
def resolve_asset(bigchain, transaction, input_txs):
|
||||
if not hasattr(transaction, 'asset'):
|
||||
raise ValidationError('Asset not found in transaction {}'.format(transaction))
|
||||
|
||||
if transaction.operation == Transaction.CREATE:
|
||||
return [transaction.asset]
|
||||
elif transaction.operation == Transaction.TRANSFER:
|
||||
asset_ids = transaction.get_asset_ids(
|
||||
[input_tx
|
||||
for (input_, input_tx, status)
|
||||
in input_txs if input_tx is not None])
|
||||
return [bigchain.get_transaction(asset_id).asset for asset_id in asset_ids]
|
@ -974,6 +974,21 @@ class Transaction(object):
|
||||
tx = Transaction._remove_signatures(self.to_dict())
|
||||
return Transaction._to_str(tx)
|
||||
|
||||
@staticmethod
|
||||
def get_asset_ids(transactions):
|
||||
if not isinstance(transactions, list):
|
||||
transactions = [transactions]
|
||||
|
||||
if not len(transactions):
|
||||
return []
|
||||
|
||||
# create a set of the transactions' asset ids
|
||||
asset_ids = {tx.id if tx.operation == Transaction.CREATE
|
||||
else tx.asset['id']
|
||||
for tx in transactions}
|
||||
|
||||
return asset_ids
|
||||
|
||||
@staticmethod
|
||||
def get_asset_id(transactions):
|
||||
"""Get the asset id from a list of :class:`~.Transactions`.
|
||||
@ -994,14 +1009,10 @@ class Transaction(object):
|
||||
:exc:`AssetIdMismatch`: If the inputs are related to different
|
||||
assets.
|
||||
"""
|
||||
asset_ids = Transaction.get_asset_ids(transactions)
|
||||
|
||||
if not isinstance(transactions, list):
|
||||
transactions = [transactions]
|
||||
|
||||
# create a set of the transactions' asset ids
|
||||
asset_ids = {tx.id if tx.operation == Transaction.CREATE
|
||||
else tx.asset['id']
|
||||
for tx in transactions}
|
||||
if len(asset_ids) == 0:
|
||||
return None
|
||||
|
||||
# check that all the transasctions have the same asset id
|
||||
if len(asset_ids) > 1:
|
||||
|
@ -16,6 +16,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import collections
|
||||
import importlib.util
|
||||
from functools import lru_cache
|
||||
|
||||
from pkg_resources import iter_entry_points, ResolutionError
|
||||
@ -249,6 +250,16 @@ def autoconfigure(filename=None, config=None, force=False):
|
||||
set_config(newconfig) # sets bigchaindb.config
|
||||
|
||||
|
||||
def load_from_path(path):
|
||||
module_path, object_name = path.split(':')
|
||||
module_name = os.path.basename(module_path).split('.').pop(0)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return getattr(module, object_name)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def load_consensus_plugin(name=None):
|
||||
"""Find and load the chosen consensus plugin.
|
||||
@ -271,6 +282,12 @@ def load_consensus_plugin(name=None):
|
||||
for entry_point in iter_entry_points('bigchaindb.consensus', name):
|
||||
plugin = entry_point.load()
|
||||
|
||||
if not plugin and name:
|
||||
try:
|
||||
plugin = load_from_path(name)
|
||||
except:
|
||||
raise ResolutionError('Invalid consensus plugin {}'.format(name))
|
||||
|
||||
# No matching entry_point found
|
||||
if not plugin:
|
||||
raise ResolutionError(
|
||||
|
@ -1,3 +1,4 @@
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.voting import Voting
|
||||
|
||||
|
||||
@ -15,9 +16,28 @@ class BaseConsensusRules():
|
||||
def validate_transaction(bigchain, transaction):
|
||||
"""See :meth:`bigchaindb.models.Transaction.validate`
|
||||
for documentation."""
|
||||
return transaction.validate(bigchain)
|
||||
input_txs = None
|
||||
|
||||
result = transaction.validate(bigchain, input_txs)
|
||||
|
||||
if transaction.operation == Transaction.TRANSFER:
|
||||
input_txs = transaction.get_input_txs(bigchain)
|
||||
|
||||
transaction.validate_asset(
|
||||
bigchain,
|
||||
[input_tx
|
||||
for (input_, input_tx, status)
|
||||
in input_txs if input_tx is not None])
|
||||
|
||||
transaction.validate_amount(
|
||||
[input_tx.outputs[input_.fulfills.output]
|
||||
for (input_, input_tx, status)
|
||||
in input_txs if input_tx is not None])
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def validate_block(bigchain, block):
|
||||
"""See :meth:`bigchaindb.models.Block.validate` for documentation."""
|
||||
return block.validate(bigchain)
|
||||
|
||||
|
@ -28,7 +28,7 @@ class Bigchain(object):
|
||||
# return if transaction is in backlog
|
||||
TX_IN_BACKLOG = 'backlog'
|
||||
|
||||
def __init__(self, public_key=None, private_key=None, keyring=[], connection=None, backlog_reassign_delay=None):
|
||||
def __init__(self, public_key=None, private_key=None, keyring=[], connection=None, backlog_reassign_delay=None, consensusPlugin=None):
|
||||
"""Initialize the Bigchain instance
|
||||
|
||||
A Bigchain instance has several configuration parameters (e.g. host).
|
||||
@ -58,7 +58,8 @@ class Bigchain(object):
|
||||
backlog_reassign_delay = bigchaindb.config['backlog_reassign_delay']
|
||||
self.backlog_reassign_delay = backlog_reassign_delay
|
||||
|
||||
consensusPlugin = bigchaindb.config.get('consensus_plugin')
|
||||
if consensusPlugin is None:
|
||||
consensusPlugin = bigchaindb.config.get('consensus_plugin')
|
||||
|
||||
if consensusPlugin:
|
||||
self.consensus = config_utils.load_consensus_plugin(consensusPlugin)
|
||||
|
@ -11,7 +11,7 @@ from bigchaindb.common.schema import validate_transaction_schema
|
||||
|
||||
|
||||
class Transaction(Transaction):
|
||||
def validate(self, bigchain):
|
||||
def validate(self, bigchain, input_txs=None):
|
||||
"""Validate transaction spend
|
||||
|
||||
Args:
|
||||
@ -29,17 +29,17 @@ class Transaction(Transaction):
|
||||
|
||||
if self.operation == Transaction.TRANSFER:
|
||||
# store the inputs so that we can check if the asset ids match
|
||||
input_txs = []
|
||||
for input_ in self.inputs:
|
||||
if not input_txs:
|
||||
input_txs = self.get_input_txs(bigchain)
|
||||
|
||||
for input_, input_tx, input_status in input_txs:
|
||||
input_txid = input_.fulfills.txid
|
||||
input_tx, status = bigchain.\
|
||||
get_transaction(input_txid, include_status=True)
|
||||
|
||||
if input_tx is None:
|
||||
raise InputDoesNotExist("input `{}` doesn't exist"
|
||||
.format(input_txid))
|
||||
|
||||
if status != bigchain.TX_VALID:
|
||||
if input_status != bigchain.TX_VALID:
|
||||
raise TransactionNotInValidBlock(
|
||||
'input `{}` does not exist in a valid block'.format(
|
||||
input_txid))
|
||||
@ -51,34 +51,46 @@ class Transaction(Transaction):
|
||||
|
||||
output = input_tx.outputs[input_.fulfills.output]
|
||||
input_conditions.append(output)
|
||||
input_txs.append(input_tx)
|
||||
|
||||
# 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
|
||||
asset_id = Transaction.get_asset_id(input_txs)
|
||||
if asset_id != self.asset['id']:
|
||||
raise AssetIdMismatch(('The asset id of the input does not'
|
||||
' match the asset id of the'
|
||||
' transaction'))
|
||||
|
||||
input_amount = sum([input_condition.amount for input_condition in input_conditions])
|
||||
output_amount = sum([output_condition.amount for output_condition in self.outputs])
|
||||
|
||||
if output_amount != input_amount:
|
||||
raise AmountError(('The amount used in the inputs `{}`'
|
||||
' needs to be same as the amount used'
|
||||
' in the outputs `{}`')
|
||||
.format(input_amount, output_amount))
|
||||
|
||||
if not self.inputs_valid(input_conditions):
|
||||
raise InvalidSignature('Transaction signature is invalid.')
|
||||
|
||||
return self
|
||||
|
||||
def get_input_txs(self, bigchain):
|
||||
input_txs = []
|
||||
for input_ in self.inputs:
|
||||
input_txid = input_.fulfills.txid
|
||||
input_tx, status = bigchain. \
|
||||
get_transaction(input_txid, include_status=True)
|
||||
input_txs.append((input_, input_tx, status))
|
||||
return input_txs
|
||||
|
||||
def validate_asset(self, bigchain, input_txs=None):
|
||||
# validate asset id
|
||||
asset_id = Transaction.get_asset_id(input_txs)
|
||||
if asset_id != self.asset['id']:
|
||||
raise AssetIdMismatch(('The asset id of the input does not'
|
||||
' match the asset id of the'
|
||||
' transaction'))
|
||||
return self
|
||||
|
||||
def validate_amount(self, input_conditions):
|
||||
input_amount = sum([input_condition.amount for input_condition in input_conditions])
|
||||
output_amount = sum([output_condition.amount for output_condition in self.outputs])
|
||||
|
||||
if output_amount != input_amount:
|
||||
raise AmountError(('The amount used in the inputs `{}`'
|
||||
' needs to be same as the amount used'
|
||||
' in the outputs `{}`')
|
||||
.format(input_amount, output_amount))
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, tx_body):
|
||||
validate_transaction_schema(tx_body)
|
||||
|
22
tests/assets/conftest.py
Normal file
22
tests/assets/conftest.py
Normal file
@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(request):
|
||||
from bigchaindb.web import server
|
||||
app = server.create_app(debug=True)
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plugin():
|
||||
import os
|
||||
plugin_path = 'bigchaindb/common/asset/consensus.py:AssetCompositionConsensusRules'
|
||||
return '{}/{}'.format(os.getcwd().split('/tests')[0], plugin_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def b_consensus(plugin):
|
||||
from bigchaindb import Bigchain
|
||||
return Bigchain(consensusPlugin=plugin)
|
||||
|
104
tests/assets/test_asset_types.py
Normal file
104
tests/assets/test_asset_types.py
Normal file
@ -0,0 +1,104 @@
|
||||
import json
|
||||
|
||||
from bigchaindb.common import crypto
|
||||
from bigchaindb.common.exceptions import ValidationError
|
||||
import pytest
|
||||
import random
|
||||
|
||||
TX_ENDPOINT = '/api/v1/transactions/'
|
||||
|
||||
|
||||
def post_tx(b, client, tx):
|
||||
class Response():
|
||||
status_code = None
|
||||
|
||||
response = Response()
|
||||
try:
|
||||
b.validate_transaction(tx)
|
||||
response.status_code = 202
|
||||
except ValidationError:
|
||||
response.status_code = 400
|
||||
|
||||
if response.status_code == 202:
|
||||
mine(b, [tx])
|
||||
return response
|
||||
|
||||
|
||||
def mine(b, tx_list):
|
||||
block = b.create_block(tx_list)
|
||||
b.write_block(block)
|
||||
|
||||
# vote the block valid
|
||||
vote = b.vote(block.id, b.get_last_voted_block().id, True)
|
||||
b.write_vote(vote)
|
||||
|
||||
return block, vote
|
||||
|
||||
|
||||
def create_simple_tx(user_pub, user_priv, asset=None, metadata=None):
|
||||
from bigchaindb.models import Transaction
|
||||
create_tx = Transaction.create([user_pub], [([user_pub], 1)], asset=asset, metadata=metadata)
|
||||
create_tx = create_tx.sign([user_priv])
|
||||
return create_tx
|
||||
|
||||
|
||||
def transfer_simple_tx(user_pub, user_priv, input_tx, metadata=None):
|
||||
from bigchaindb.models import Transaction
|
||||
|
||||
asset_id = input_tx.id if input_tx.operation == 'CREATE' else input_tx.asset['id']
|
||||
|
||||
transfer_tx = Transaction.transfer(input_tx.to_inputs(),
|
||||
[([user_pub], 1)],
|
||||
asset_id=asset_id,
|
||||
metadata=metadata)
|
||||
transfer_tx = transfer_tx.sign([user_priv])
|
||||
|
||||
return transfer_tx
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
@pytest.mark.usefixtures('inputs')
|
||||
def test_asset_type_mix(b_consensus, client):
|
||||
from bigchaindb.models import Transaction
|
||||
|
||||
alice_priv, alice_pub = crypto.generate_key_pair()
|
||||
|
||||
create_a = create_simple_tx(
|
||||
alice_pub, alice_priv,
|
||||
asset={
|
||||
'type': 'mix',
|
||||
'data': {
|
||||
'material': 'secret sauce'
|
||||
}
|
||||
})
|
||||
response = post_tx(b_consensus, client, create_a)
|
||||
assert response.status_code == 202
|
||||
|
||||
transfer_a = transfer_simple_tx(alice_pub, alice_priv, create_a)
|
||||
response = post_tx(b_consensus, client, transfer_a)
|
||||
assert response.status_code == 202
|
||||
|
||||
bob_priv, bob_pub = crypto.generate_key_pair()
|
||||
tx_b = create_simple_tx(
|
||||
bob_pub,
|
||||
bob_priv,
|
||||
asset={
|
||||
'type': 'mix',
|
||||
'data': {
|
||||
'material': 'bulk'
|
||||
}
|
||||
})
|
||||
response = post_tx(b_consensus, client, tx_b)
|
||||
assert response.status_code == 202
|
||||
|
||||
carly_priv, carly_pub = crypto.generate_key_pair()
|
||||
|
||||
tx_mix = Transaction.transfer(
|
||||
transfer_a.to_inputs() + tx_b.to_inputs(),
|
||||
[([carly_pub], 1)],
|
||||
transfer_a.id
|
||||
)
|
||||
|
||||
tx_mix_signed = tx_mix.sign([alice_priv, bob_priv])
|
||||
response = post_tx(b_consensus, client, tx_mix_signed)
|
||||
assert response.status_code == 202
|
@ -44,7 +44,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_pk, user_sk):
|
||||
tx_transfer.asset['id'] = 'aaa'
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk])
|
||||
with pytest.raises(AssetIdMismatch):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_get_asset_id_create_transaction(b, user_pk):
|
||||
|
@ -538,7 +538,7 @@ def test_amount_error_transfer(b, user_pk, user_sk):
|
||||
asset_id=tx_create.id)
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk])
|
||||
with pytest.raises(AmountError):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
# TRANSFER
|
||||
# output amount greater than input amount
|
||||
@ -546,7 +546,7 @@ def test_amount_error_transfer(b, user_pk, user_sk):
|
||||
asset_id=tx_create.id)
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk])
|
||||
with pytest.raises(AmountError):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='Figure out how to handle this case')
|
||||
|
@ -204,6 +204,7 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch, request):
|
||||
'keyring': KEYRING.split(':'),
|
||||
'backlog_reassign_delay': 5,
|
||||
'log': {},
|
||||
'consensus_plugin': None
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user