consensus plugin

decompose validate
consensus per asset type
This commit is contained in:
diminator 2017-03-23 23:36:16 +01:00
parent 391713ad6e
commit e394b07f05
No known key found for this signature in database
GPG Key ID: C3D8590E6D0D439A
13 changed files with 329 additions and 36 deletions

View File

@ -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

View File

View 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]

View File

@ -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:

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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
View 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)

View 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

View File

@ -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):

View File

@ -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')

View File

@ -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
}