Rebase/feat/586/integrate tx model (#641)

* Adjust imports to bigchaindb_common

* Adjust get_spent function signature

* Adjust block serialization

* Fix BigchainApi Test

* Fix TestTransactionValidation tests

* Fix TestBlockValidation tests

* WIP: TestMultipleInputs

* Adjust tests to tx-model interface changes

- Fix old tests
- Fix tests in TestMultipleInputs class

* Remove fulfillment message tests

* Fix TransactionMalleability tests

* Remove Cryptoconditions tests

* Remove create_transaction

* Remove signing logic

* Remove consensus plugin

* Fix block_creation pipeline

* Fix election pipeline

* Replace some util functions with bdb_common ones

- timestamp ==> gen_timestamp
- serialize.

* Implement Block model

* Simplify function signatures for vote functions

Change parameter interface for the following functions:

- has_previous_vote
- verify_vote_signature
- block_election_status

so that they take a block's id and voters instead of a fake block.

* Integrate Block and Transaction model

* Fix leftover tests and cleanup conftest

* Add bigchaindb-common to install_requires

* Delete transactions after block is written (#609)

* delete transactions after block is written

* cleanup transaction_exists

* check for duplicate transactions

* delete invalid tx from backlog

* test duplicate transaction

* Remove dead code

* Test processes.py

* Test invalid tx in on server

* Fix tests for core.py

* Fix models tests

* Test commands main fn

* Add final coverage to vote pipeline

* Add more tests to voting pipeline

* Remove consensus plugin docs and misc

* Post rebase fixes

* Fix rebase mess

* Remove extra blank line

* Improve docstring

* Remove comment

handled in bigchaindb/cryptoconditions#27;
see https://github.com/bigchaindb/cryptoconditions/issues/27

* Fix block serialization in block creation

* Add signed_ prefix to transfer_tx

* Improve docs

* Add library documentation page on pipelines

* PR feedback for models.py

* Impr. readability of get_last_voted_block

* Use dict comprehension

* Add docker-compose file to build and serve docs

locally for development purposes

* Change private_key for signing_key

* Improve docstrings

* Remove consensus docs

* Document new consensus module

* Create different transactions for the block

* Cleanup variable names in block.py

* Create different transactions for the block

* Cleanup variable names in block.py
This commit is contained in:
Sylvain Bellemare 2016-09-29 10:29:41 +02:00 committed by GitHub
parent e74b4ee528
commit 50b0b3cef2
49 changed files with 1921 additions and 3316 deletions

View File

@ -8,6 +8,7 @@ import logging
import rethinkdb as r
from os.path import expanduser
from bigchaindb_common.transaction import Transaction
from bigchaindb import Bigchain
from bigchaindb.util import ProcessGroup
@ -33,10 +34,9 @@ def create_write_transaction(tx_left, payload_filler):
# Include a random uuid string in the payload to prevent duplicate
# transactions (i.e. transactions with the same hash)
payload_dict['msg'] = str(uuid.uuid4())
tx = b.create_transaction(b.me, b.me, None, 'CREATE',
payload=payload_dict)
tx_signed = b.sign_transaction(tx, b.me_private)
b.write_transaction(tx_signed)
tx = Transaction.create([b.me], [b.me], payload=payload_dict)
tx = tx.sign([b.me_private])
b.write_transaction(tx)
tx_left -= 1

View File

@ -30,7 +30,6 @@ config = {
'rate': 0.01,
},
'api_endpoint': 'http://localhost:9984/api/v1',
'consensus_plugin': 'default',
'backlog_reassign_delay': 30
}

View File

@ -1,115 +0,0 @@
import requests
import bigchaindb
from bigchaindb import config_utils
from bigchaindb import exceptions
from bigchaindb import crypto
class Client:
"""Client for BigchainDB.
A Client is initialized with a keypair and is able to create, sign, and submit transactions to a Node
in the Federation. At the moment, a Client instance is bounded to a specific ``host`` in the Federation.
In the future, a Client might connect to >1 hosts.
"""
def __init__(self, public_key=None, private_key=None, api_endpoint=None,
consensus_plugin=None):
"""Initialize the Client instance
There are three ways in which the Client instance can get its parameters.
The order by which the parameters are chosen are:
1. Setting them by passing them to the `__init__` method itself.
2. Setting them as environment variables
3. Reading them from the `config.json` file.
Args:
public_key (str): the base58 encoded public key for the ED25519 curve.
private_key (str): the base58 encoded private key for the ED25519 curve.
api_endpoint (str): a URL where rethinkdb is running.
format: scheme://hostname:port
consensus_plugin (str): the registered name of your installed
consensus plugin. The `core` plugin is built into BigchainDB;
others must be installed via pip.
"""
config_utils.autoconfigure()
self.public_key = public_key or bigchaindb.config['keypair']['public']
self.private_key = private_key or bigchaindb.config['keypair']['private']
self.api_endpoint = api_endpoint or bigchaindb.config['api_endpoint']
self.consensus = config_utils.load_consensus_plugin(consensus_plugin)
if not self.public_key or not self.private_key:
raise exceptions.KeypairNotFoundException()
def create(self, payload=None):
"""Issue a transaction to create an asset.
Args:
payload (dict): the payload for the transaction.
Return:
The transaction pushed to the Federation.
"""
tx = self.consensus.create_transaction(
owner_before=self.public_key,
owner_after=self.public_key,
tx_input=None,
operation='CREATE',
payload=payload)
signed_tx = self.consensus.sign_transaction(
tx, private_key=self.private_key)
return self._push(signed_tx)
def transfer(self, owner_after, tx_input, payload=None):
"""Issue a transaction to transfer an asset.
Args:
owner_after (str): the public key of the new owner
tx_input (str): the id of the transaction to use as input
payload (dict, optional): the payload for the transaction.
Return:
The transaction pushed to the Federation.
"""
tx = self.consensus.create_transaction(
owner_before=self.public_key,
owner_after=owner_after,
tx_input=tx_input,
operation='TRANSFER',
payload=payload)
signed_tx = self.consensus.sign_transaction(
tx, private_key=self.private_key)
return self._push(signed_tx)
def _push(self, tx):
"""Submit a transaction to the Federation.
Args:
tx (dict): the transaction to be pushed to the Federation.
Return:
The transaction pushed to the Federation.
"""
res = requests.post(self.api_endpoint + '/transactions/', json=tx)
return res.json()
def temp_client():
"""Create a new temporary client.
Return:
A client initialized with a keypair generated on the fly.
"""
private_key, public_key = crypto.generate_key_pair()
return Client(private_key=private_key, public_key=public_key, api_endpoint=bigchaindb.config['api_endpoint'])

View File

@ -13,19 +13,19 @@ import builtins
import logstats
from bigchaindb_common import crypto
from bigchaindb_common.exceptions import (StartupError,
DatabaseAlreadyExists,
KeypairNotFoundException)
import rethinkdb as r
import bigchaindb
import bigchaindb.config_utils
from bigchaindb.models import Transaction
from bigchaindb.util import ProcessGroup
from bigchaindb.client import temp_client
from bigchaindb import db
from bigchaindb.exceptions import (StartupError,
DatabaseAlreadyExists,
KeypairNotFoundException)
from bigchaindb.commands import utils
from bigchaindb import processes
from bigchaindb import crypto
logging.basicConfig(level=logging.INFO)
@ -193,10 +193,12 @@ def run_start(args):
def _run_load(tx_left, stats):
logstats.thread.start(stats)
client = temp_client()
b = bigchaindb.Bigchain()
while True:
tx = client.create()
tx = Transaction.create([b.me], [b.me])
tx = tx.sign([b.me_private])
b.write_transaction(tx)
stats['transactions'] += 1

View File

@ -3,17 +3,18 @@ for ``argparse.ArgumentParser``.
"""
import argparse
from bigchaindb_common.exceptions import StartupError
import multiprocessing as mp
import subprocess
import rethinkdb as r
import bigchaindb
from bigchaindb.exceptions import StartupError
from bigchaindb import db
from bigchaindb.version import __version__
def start_rethinkdb():
"""Start RethinkDB as a child process and wait for it to be
available.
@ -24,7 +25,7 @@ def start_rethinkdb():
starting the db
Raises:
``bigchaindb.exceptions.StartupError`` if RethinkDB cannot
``bigchaindb_common.exceptions.StartupError`` if RethinkDB cannot
be started.
"""

View File

@ -16,13 +16,11 @@ import copy
import json
import logging
import collections
from functools import lru_cache
from pkg_resources import iter_entry_points, ResolutionError
from bigchaindb_common import exceptions
import bigchaindb
from bigchaindb.consensus import AbstractConsensusRules
from bigchaindb import exceptions
# TODO: move this to a proper configuration file for logging
logging.getLogger('requests').setLevel(logging.WARNING)
@ -242,40 +240,3 @@ def autoconfigure(filename=None, config=None, force=False):
newconfig = update(newconfig, config)
set_config(newconfig) # sets bigchaindb.config
@lru_cache()
def load_consensus_plugin(name=None):
"""Find and load the chosen consensus plugin.
Args:
name (string): the name of the entry_point, as advertised in the
setup.py of the providing package.
Returns:
an uninstantiated subclass of ``bigchaindb.consensus.AbstractConsensusRules``
"""
if not name:
name = bigchaindb.config.get('consensus_plugin', 'default')
# TODO: This will return the first plugin with group `bigchaindb.consensus`
# and name `name` in the active WorkingSet.
# We should probably support Requirements specs in the config, e.g.
# consensus_plugin: 'my-plugin-package==0.0.1;default'
plugin = None
for entry_point in iter_entry_points('bigchaindb.consensus', name):
plugin = entry_point.load()
# No matching entry_point found
if not plugin:
raise ResolutionError(
'No plugin found in group `bigchaindb.consensus` with name `{}`'.
format(name))
# Is this strictness desireable?
# It will probably reduce developer headaches in the wild.
if not issubclass(plugin, (AbstractConsensusRules)):
raise TypeError("object of type '{}' does not implement `bigchaindb."
"consensus.AbstractConsensusRules`".format(type(plugin)))
return plugin

View File

@ -1,244 +1,28 @@
from abc import ABCMeta, abstractmethod
from bigchaindb import crypto, exceptions, util
from bigchaindb.util import verify_vote_signature
class AbstractConsensusRules(metaclass=ABCMeta):
"""Abstract base class for Bigchain plugins which implement consensus logic.
A consensus plugin must expose a class inheriting from this one via an
entry_point.
All methods listed below must be implemented.
"""
@staticmethod
@abstractmethod
def validate_transaction(bigchain, transaction):
"""Validate a transaction.
Args:
bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object.
transaction (dict): transaction to validate.
Returns:
The transaction if the transaction is valid else it raises an
exception describing the reason why the transaction is invalid.
Raises:
Descriptive exceptions indicating the reason the transaction failed.
See the `exceptions` module for bigchain-native error classes.
"""
@staticmethod
@abstractmethod
def validate_block(bigchain, block):
"""Validate a block.
Args:
bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object.
block (dict): block to validate.
Returns:
The block if the block is valid else it raises an exception
describing the reason why the block is invalid.
Raises:
Descriptive exceptions indicating the reason the block failed.
See the `exceptions` module for bigchain-native error classes.
"""
@staticmethod
@abstractmethod
def create_transaction(*args, **kwargs):
"""Create a new transaction.
Args:
The signature of this method is left to plugin authors to decide.
Returns:
dict: newly constructed transaction.
"""
@staticmethod
@abstractmethod
def sign_transaction(transaction, *args, **kwargs):
"""Sign a transaction.
Args:
transaction (dict): transaction to sign.
any other arguments are left to plugin authors to decide.
Returns:
dict: transaction with any signatures applied.
"""
@staticmethod
@abstractmethod
def validate_fulfillments(signed_transaction):
"""Validate the fulfillments of a transaction.
Args:
signed_transaction (dict): signed transaction to verify
Returns:
bool: True if the transaction's required fulfillments are present
and correct, False otherwise.
"""
@abstractmethod
def verify_vote_signature(block, signed_vote):
"""Verify a cast vote.
Args:
block (dict): block under election
signed_vote (dict): signed vote to verify
Returns:
bool: True if the votes's required signature data is present
and correct, False otherwise.
"""
class BaseConsensusRules(AbstractConsensusRules):
class BaseConsensusRules():
"""Base consensus rules for Bigchain.
This class can be copied or overridden to write your own consensus rules!
"""
@staticmethod
def validate_transaction(bigchain, transaction):
"""Validate a transaction.
"""See :meth:`bigchaindb.models.Transaction.validate`
for documentation.
Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
transaction (dict): transaction to validate.
Returns:
The transaction if the transaction is valid else it raises an
exception describing the reason why the transaction is invalid.
Raises:
OperationError: if the transaction operation is not supported
TransactionDoesNotExist: if the input of the transaction is not found
TransactionOwnerError: if the new transaction is using an input it doesn't own
DoubleSpend: if the transaction is a double spend
InvalidHash: if the hash of the transaction is wrong
InvalidSignature: if the signature of the transaction is wrong
"""
# If the operation is CREATE the transaction should have no inputs and
# should be signed by a federation node
if transaction['transaction']['operation'] in ('CREATE', 'GENESIS'):
# TODO: for now lets assume a CREATE transaction only has one fulfillment
if transaction['transaction']['fulfillments'][0]['input']:
raise ValueError('A CREATE operation has no inputs')
# TODO: for now lets assume a CREATE transaction only has one owner_before
if transaction['transaction']['fulfillments'][0]['owners_before'][0] not in (
bigchain.nodes_except_me + [bigchain.me]):
raise exceptions.OperationError(
'Only federation nodes can use the operation `CREATE`')
else:
# check if the input exists, is owned by the owner_before
if not transaction['transaction']['fulfillments']:
raise ValueError('Transaction contains no fulfillments')
# check inputs
for fulfillment in transaction['transaction']['fulfillments']:
if not fulfillment['input']:
raise ValueError('Only `CREATE` transactions can have null inputs')
tx_input = bigchain.get_transaction(fulfillment['input']['txid'])
if not tx_input:
raise exceptions.TransactionDoesNotExist(
'input `{}` does not exist in the bigchain'.format(
fulfillment['input']['txid']))
# TODO: check if current owners own tx_input (maybe checked by InvalidSignature)
# check if the input was already spent by a transaction other than
# this one.
spent = bigchain.get_spent(fulfillment['input'])
if spent and spent['id'] != transaction['id']:
raise exceptions.DoubleSpend(
'input `{}` was already spent'.format(fulfillment['input']))
# Check hash of the transaction
calculated_hash = util.get_hash_data(transaction)
if calculated_hash != transaction['id']:
raise exceptions.InvalidHash()
# Check fulfillments
if not util.validate_fulfillments(transaction):
raise exceptions.InvalidSignature()
return transaction
return transaction.validate(bigchain)
@staticmethod
def validate_block(bigchain, block):
"""Validate a block.
Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
block (dict): block to validate.
Returns:
The block if the block is valid else it raises an exception
describing the reason why the block is invalid.
Raises:
InvalidHash: if the hash of the block is wrong.
"""
# Check if current hash is correct
calculated_hash = crypto.hash_data(util.serialize(block['block']))
if calculated_hash != block['id']:
raise exceptions.InvalidHash()
# Check if the block was created by a federation node
if block['block']['node_pubkey'] not in (bigchain.nodes_except_me + [bigchain.me]):
raise exceptions.OperationError('Only federation nodes can create blocks')
# Check if block signature is valid
verifying_key = crypto.VerifyingKey(block['block']['node_pubkey'])
if not verifying_key.verify(util.serialize(block['block']), block['signature']):
raise exceptions.InvalidSignature('Invalid block signature')
return block
"""See :meth:`bigchaindb.models.Block.validate` for documentation."""
return block.validate(bigchain)
@staticmethod
def create_transaction(owner_before, owner_after, tx_input, operation,
payload=None):
"""Create a new transaction
Refer to the documentation of ``bigchaindb.util.create_tx``
"""
return util.create_tx(owner_before, owner_after, tx_input, operation,
payload)
@staticmethod
def sign_transaction(transaction, private_key, bigchain=None):
"""Sign a transaction
Refer to the documentation of ``bigchaindb.util.sign_tx``
"""
return util.sign_tx(transaction, private_key, bigchain=bigchain)
@staticmethod
def validate_fulfillments(signed_transaction):
"""Validate the fulfillments of a transaction.
Refer to the documentation of ``bigchaindb.util.validate_fulfillments``
"""
return util.validate_fulfillments(signed_transaction)
@staticmethod
def verify_vote_signature(block, signed_vote):
def verify_vote_signature(voters, signed_vote):
"""Verify the signature of a vote.
Refer to the documentation of ``bigchaindb.util.verify_signature``
Refer to the documentation of
:func:`bigchaindb.util.verify_signature`.
"""
return util.verify_vote_signature(block, signed_vote)
return verify_vote_signature(voters, signed_vote)

View File

@ -1,16 +1,20 @@
import random
import math
import collections
from copy import deepcopy
from time import time
from itertools import compress
from bigchaindb_common import crypto, exceptions
from bigchaindb_common.util import gen_timestamp, serialize
from bigchaindb_common.transaction import TransactionLink
import rethinkdb as r
import rapidjson
import bigchaindb
from bigchaindb.db.utils import Connection
from bigchaindb import config_utils, crypto, exceptions, util
from bigchaindb import config_utils, util
from bigchaindb.consensus import BaseConsensusRules
from bigchaindb.models import Block, Transaction
class Bigchain(object):
@ -30,7 +34,7 @@ class Bigchain(object):
def __init__(self, host=None, port=None, dbname=None,
public_key=None, private_key=None, keyring=[],
consensus_plugin=None, backlog_reassign_delay=None):
backlog_reassign_delay=None):
"""Initialize the Bigchain instance
A Bigchain instance has several configuration parameters (e.g. host).
@ -59,7 +63,7 @@ class Bigchain(object):
self.me_private = private_key or bigchaindb.config['keypair']['private']
self.nodes_except_me = keyring or bigchaindb.config['keyring']
self.backlog_reassign_delay = backlog_reassign_delay or bigchaindb.config['backlog_reassign_delay']
self.consensus = config_utils.load_consensus_plugin(consensus_plugin)
self.consensus = BaseConsensusRules
# change RethinkDB read mode to majority. This ensures consistency in query results
self.read_mode = 'majority'
@ -78,41 +82,6 @@ class Bigchain(object):
def reconnect(self):
return r.connect(host=self.host, port=self.port, db=self.dbname)
def create_transaction(self, *args, **kwargs):
"""Create a new transaction
Refer to the documentation of your consensus plugin.
Returns:
dict: newly constructed transaction.
"""
return self.consensus.create_transaction(*args, **kwargs)
def sign_transaction(self, transaction, *args, **kwargs):
"""Sign a transaction
Refer to the documentation of your consensus plugin.
Returns:
dict: transaction with any signatures applied.
"""
return self.consensus.sign_transaction(transaction, *args, bigchain=self, **kwargs)
def validate_fulfillments(self, signed_transaction, *args, **kwargs):
"""Validate the fulfillment(s) of a transaction.
Refer to the documentation of your consensus plugin.
Returns:
bool: True if the transaction's required fulfillments are present
and correct, False otherwise.
"""
return self.consensus.validate_fulfillments(
signed_transaction, *args, **kwargs)
def write_transaction(self, signed_transaction, durability='soft'):
"""Write the transaction to bigchain.
@ -120,25 +89,21 @@ class Bigchain(object):
it has been validated by the nodes of the federation.
Args:
signed_transaction (dict): transaction with the `signature` included.
signed_transaction (Transaction): transaction with the `signature` included.
Returns:
dict: database response
"""
signed_transaction = signed_transaction.to_dict()
# we will assign this transaction to `one` node. This way we make sure that there are no duplicate
# transactions on the bigchain
if self.nodes_except_me:
assignee = random.choice(self.nodes_except_me)
else:
# I am the only node
assignee = self.me
# We copy the transaction here to not add `assignee` to the transaction
# dictionary passed to this method (as it would update by reference).
signed_transaction = deepcopy(signed_transaction)
# update the transaction
signed_transaction.update({'assignee': assignee})
signed_transaction.update({'assignment_timestamp': time()})
@ -190,6 +155,42 @@ class Bigchain(object):
.filter(lambda tx: time() - tx['assignment_timestamp'] >
self.backlog_reassign_delay).run(self.conn)
def validate_transaction(self, transaction):
"""Validate a transaction.
Args:
transaction (Transaction): transaction to validate.
Returns:
The transaction if the transaction is valid else it raises an
exception describing the reason why the transaction is invalid.
"""
return self.consensus.validate_transaction(self, transaction)
def is_valid_transaction(self, transaction):
"""Check whether a transaction is valid or invalid.
Similar to :meth:`~bigchaindb.Bigchain.validate_transaction`
but never raises an exception. It returns :obj:`False` if
the transaction is invalid.
Args:
transaction (:Class:`~bigchaindb.models.Transaction`): transaction
to check.
Returns:
The :class:`~bigchaindb.models.Transaction` instance if valid,
otherwise :obj:`False`.
"""
try:
return self.validate_transaction(transaction)
except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist,
exceptions.TransactionOwnerError, exceptions.DoubleSpend,
exceptions.InvalidHash, exceptions.InvalidSignature):
return False
def get_transaction(self, txid, include_status=False):
"""Retrieve a transaction with `txid` from bigchain.
@ -244,6 +245,9 @@ class Bigchain(object):
if response:
tx_status = self.TX_IN_BACKLOG
if response:
response = Transaction.from_dict(response)
if include_status:
return response, tx_status
else:
@ -271,7 +275,7 @@ class Bigchain(object):
index (str): name of a secondary index, e.g. 'transaction_id'
Returns:
A list of blocks with with only election information
:obj:`list` of :obj:`dict`: A list of blocks with with only election information
"""
# First, get information on all blocks which contain this transaction
response = self.connection.run(
@ -296,17 +300,25 @@ class Bigchain(object):
# First, get information on all blocks which contain this transaction
blocks = self.search_block_election_on_index(txid, 'transaction_id')
if blocks:
# Determine the election status of each block
validity = {block['id']: self.block_election_status(block) for block in blocks}
validity = {
block['id']: self.block_election_status(
block['id'],
block['block']['voters']
) for block in blocks
}
# If there are multiple valid blocks with this transaction, something has gone wrong
# NOTE: If there are multiple valid blocks with this transaction,
# something has gone wrong
if list(validity.values()).count(Bigchain.BLOCK_VALID) > 1:
block_ids = str([block for block in validity
if validity[block] == Bigchain.BLOCK_VALID])
raise Exception('Transaction {tx} is present in multiple valid blocks: {block_ids}'
.format(tx=txid, block_ids=block_ids))
if validity[block] == Bigchain.BLOCK_VALID])
raise exceptions.DoubleSpend('Transaction {tx} is present in '
'multiple valid blocks: '
'{block_ids}'
.format(tx=txid,
block_ids=block_ids))
return validity
@ -319,7 +331,7 @@ class Bigchain(object):
When creating a transaction one of the optional arguments is the `payload`. The payload is a generic
dict that contains information about the digital asset.
To make it easy to query the bigchain for that digital asset we create a UUID for the payload and
To make it easy to query BigchainDB for that digital asset we create a UUID for the payload and
store it with the transaction. This makes it easy for developers to keep track of their digital
assets in bigchain.
@ -337,19 +349,21 @@ class Bigchain(object):
.filter(lambda transaction: transaction['transaction']['data']['uuid'] == payload_uuid))
transactions = list(cursor)
return transactions
return [Transaction.from_dict(tx) for tx in transactions]
def get_spent(self, tx_input):
def get_spent(self, txid, cid):
"""Check if a `txid` was already used as an input.
A transaction can be used as an input for another transaction. Bigchain needs to make sure that a
given `txid` is only used once.
Args:
tx_input (dict): Input of a transaction in the form `{'txid': 'transaction id', 'cid': 'condition id'}`
txid (str): The id of the transaction
cid (num): the index of the condition in the respective transaction
Returns:
The transaction that used the `txid` as an input if it exists else it returns `None`
The transaction (Transaction) that used the `txid` as an input else
`None`
"""
# checks if an input was already spent
# checks if the bigchain has any transaction with input {'txid': ..., 'cid': ...}
@ -357,7 +371,7 @@ class Bigchain(object):
r.table('bigchain', read_mode=self.read_mode)
.concat_map(lambda doc: doc['block']['transactions'])
.filter(lambda transaction: transaction['transaction']['fulfillments']
.contains(lambda fulfillment: fulfillment['input'] == tx_input)))
.contains(lambda fulfillment: fulfillment['input'] == {'txid': txid, 'cid': cid})))
transactions = list(response)
@ -367,14 +381,15 @@ class Bigchain(object):
num_valid_transactions = 0
for transaction in transactions:
# ignore invalid blocks
# FIXME: Isn't there a faster solution than doing I/O again?
if self.get_transaction(transaction['id']):
num_valid_transactions += 1
if num_valid_transactions > 1:
raise exceptions.DoubleSpend('`{}` was spent more then once. There is a problem with the chain'.format(
tx_input['txid']))
txid))
if num_valid_transactions:
return transactions[0]
return Transaction.from_dict(transactions[0])
else:
# all queried transactions were invalid
return None
@ -388,7 +403,8 @@ class Bigchain(object):
owner (str): base58 encoded public key.
Returns:
list: list of `txids` currently owned by `owner`
list (TransactionLink): list of `txid`s and `cid`s pointing to
another transaction's condition
"""
# get all transactions in which owner is in the `owners_after` list
@ -407,95 +423,49 @@ class Bigchain(object):
if Bigchain.BLOCK_UNDECIDED not in validity.values():
continue
# NOTE: It's OK to not serialize the transaction here, as we do not
# use it after the execution of this function.
# a transaction can contain multiple outputs (conditions) so we need to iterate over all of them
# to get a list of outputs available to spend
for condition in tx['transaction']['conditions']:
for index, cond in enumerate(tx['transaction']['conditions']):
# for simple signature conditions there are no subfulfillments
# check if the owner is in the condition `owners_after`
if len(condition['owners_after']) == 1:
if condition['condition']['details']['public_key'] == owner:
tx_input = {'txid': tx['id'], 'cid': condition['cid']}
if len(cond['owners_after']) == 1:
if cond['condition']['details']['public_key'] == owner:
tx_link = TransactionLink(tx['id'], index)
else:
# for transactions with multiple `owners_after` there will be several subfulfillments nested
# in the condition. We need to iterate the subfulfillments to make sure there is a
# subfulfillment for `owner`
if util.condition_details_has_owner(condition['condition']['details'], owner):
tx_input = {'txid': tx['id'], 'cid': condition['cid']}
if util.condition_details_has_owner(cond['condition']['details'], owner):
tx_link = TransactionLink(tx['id'], index)
# check if input was already spent
if not self.get_spent(tx_input):
owned.append(tx_input)
if not self.get_spent(tx_link.txid, tx_link.cid):
owned.append(tx_link)
return owned
def validate_transaction(self, transaction):
"""Validate a transaction.
Args:
transaction (dict): transaction to validate.
Returns:
The transaction if the transaction is valid else it raises an
exception describing the reason why the transaction is invalid.
"""
return self.consensus.validate_transaction(self, transaction)
def is_valid_transaction(self, transaction):
"""Check whether a transacion is valid or invalid.
Similar to `validate_transaction` but never raises an exception.
It returns `False` if the transaction is invalid.
Args:
transaction (dict): transaction to check.
Returns:
`transaction` if the transaction is valid, `False` otherwise
"""
try:
self.validate_transaction(transaction)
return transaction
except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist,
exceptions.TransactionOwnerError, exceptions.DoubleSpend,
exceptions.InvalidHash, exceptions.InvalidSignature):
return False
def create_block(self, validated_transactions):
"""Creates a block given a list of `validated_transactions`.
Note that this method does not validate the transactions. Transactions should be validated before
calling create_block.
Note that this method does not validate the transactions. Transactions
should be validated before calling create_block.
Args:
validated_transactions (list): list of validated transactions.
validated_transactions (list(Transaction)): list of validated
transactions.
Returns:
dict: created block.
Block: created block.
"""
# Prevent the creation of empty blocks
if len(validated_transactions) == 0:
raise exceptions.OperationError('Empty block creation is not allowed')
raise exceptions.OperationError('Empty block creation is not '
'allowed')
# Create the new block
block = {
'timestamp': util.timestamp(),
'transactions': validated_transactions,
'node_pubkey': self.me,
'voters': self.nodes_except_me + [self.me]
}
# Calculate the hash of the new block
block_data = util.serialize(block)
block_hash = crypto.hash_data(block_data)
block_signature = crypto.SigningKey(self.me_private).sign(block_data)
block = {
'id': block_hash,
'block': block,
'signature': block_signature,
}
voters = self.nodes_except_me + [self.me]
block = Block(validated_transactions, self.me, gen_timestamp(), voters)
block = block.sign(self.me_private)
return block
@ -504,33 +474,20 @@ class Bigchain(object):
"""Validate a block.
Args:
block (dict): block to validate.
block (Block): block to validate.
Returns:
The block if the block is valid else it raises and exception
describing the reason why the block is invalid.
"""
# First, make sure this node hasn't already voted on this block
if self.has_previous_vote(block):
return block
return self.consensus.validate_block(self, block)
# Run the plugin block validation logic
self.consensus.validate_block(self, block)
# Finally: Tentative assumption that every blockchain will want to
# validate all transactions in each block
for transaction in block['block']['transactions']:
if not self.is_valid_transaction(transaction):
# this will raise the exception
self.validate_transaction(transaction)
return block
def has_previous_vote(self, block):
def has_previous_vote(self, block_id, voters):
"""Check for previous votes from this node
Args:
block (dict): block to check.
block_id (str): the id of the block to check
voters (list(str)): the voters of the block to check
Returns:
bool: :const:`True` if this block already has a
@ -543,50 +500,31 @@ class Bigchain(object):
"""
votes = list(self.connection.run(
r.table('votes', read_mode=self.read_mode)
.get_all([block['id'], self.me], index='block_and_voter')))
.get_all([block_id, self.me], index='block_and_voter')))
if len(votes) > 1:
raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes from public key {me}'
.format(block_id=block['id'], n_votes=str(len(votes)), me=self.me))
.format(block_id=block_id, n_votes=str(len(votes)), me=self.me))
has_previous_vote = False
if votes:
if util.verify_vote_signature(block, votes[0]):
if util.verify_vote_signature(voters, votes[0]):
has_previous_vote = True
else:
raise exceptions.ImproperVoteError('Block {block_id} already has an incorrectly signed vote '
'from public key {me}'.format(block_id=block['id'], me=self.me))
'from public key {me}'.format(block_id=block_id, me=self.me))
return has_previous_vote
def is_valid_block(self, block):
"""Check whether a block is valid or invalid.
Similar to `validate_block` but does not raise an exception if the block is invalid.
Args:
block (dict): block to check.
Returns:
bool: `True` if the block is valid, `False` otherwise.
"""
try:
self.validate_block(block)
return True
except Exception:
return False
def write_block(self, block, durability='soft'):
"""Write a block to bigchain.
Args:
block (dict): block to write to bigchain.
block (Block): block to write to bigchain.
"""
block_serialized = rapidjson.dumps(block)
self.connection.run(
r.table('bigchain')
.insert(r.json(block_serialized), durability=durability))
.insert(r.json(block.to_str()), durability=durability))
def transaction_exists(self, transaction_id):
response = self.connection.run(
@ -598,14 +536,16 @@ class Bigchain(object):
"""Prepare a genesis block."""
payload = {'message': 'Hello World from the BigchainDB'}
transaction = self.create_transaction([self.me], [self.me], None, 'GENESIS', payload=payload)
transaction_signed = self.sign_transaction(transaction, self.me_private)
transaction = Transaction.create([self.me], [self.me], payload=payload)
# NOTE: The transaction model doesn't expose an API to generate a
# GENESIS transaction, as this is literally the only usage.
transaction.operation = 'GENESIS'
transaction = transaction.sign([self.me_private])
# create the block
return self.create_block([transaction_signed])
return self.create_block([transaction])
# TODO: Unless we prescribe the signature of create_transaction, this will
# also need to be moved into the plugin API.
def create_genesis_block(self):
"""Create the genesis block
@ -649,10 +589,10 @@ class Bigchain(object):
'previous_block': previous_block_id,
'is_block_valid': decision,
'invalid_reason': invalid_reason,
'timestamp': util.timestamp()
'timestamp': gen_timestamp()
}
vote_data = util.serialize(vote)
vote_data = serialize(vote)
signature = crypto.SigningKey(self.me_private).sign(vote_data)
vote_signed = {
@ -687,9 +627,11 @@ class Bigchain(object):
except r.ReqlNonExistenceError:
# return last vote if last vote exists else return Genesis block
return list(self.connection.run(
res = self.connection.run(
r.table('bigchain', read_mode=self.read_mode)
.filter(util.is_genesis_block)))[0]
.filter(util.is_genesis_block))
block = list(res)[0]
return Block.from_dict(block)
# Now the fun starts. Since the resolution of timestamp is a second,
# we might have more than one vote per timestamp. If this is the case
@ -725,10 +667,14 @@ class Bigchain(object):
r.table('bigchain', read_mode=self.read_mode)
.get(last_block_id))
return res
return Block.from_dict(res)
def get_unvoted_blocks(self):
"""Return all the blocks that has not been voted by this node."""
"""Return all the blocks that have not been voted on by this node.
Returns:
:obj:`list` of :obj:`dict`: a list of unvoted blocks
"""
unvoted = self.connection.run(
r.table('bigchain', read_mode=self.read_mode)
@ -739,29 +685,28 @@ class Bigchain(object):
# FIXME: I (@vrde) don't like this solution. Filtering should be done at a
# database level. Solving issue #444 can help untangling the situation
unvoted = filter(lambda block: not util.is_genesis_block(block), unvoted)
unvoted_blocks = filter(lambda block: not util.is_genesis_block(block), unvoted)
return unvoted_blocks
return list(unvoted)
def block_election_status(self, block):
def block_election_status(self, block_id, voters):
"""Tally the votes on a block, and return the status: valid, invalid, or undecided."""
votes = self.connection.run(r.table('votes', read_mode=self.read_mode)
.between([block['id'], r.minval], [block['id'], r.maxval], index='block_and_voter'))
.between([block_id, r.minval], [block_id, r.maxval], index='block_and_voter'))
votes = list(votes)
n_voters = len(block['block']['voters'])
n_voters = len(voters)
voter_counts = collections.Counter([vote['node_pubkey'] for vote in votes])
for node in voter_counts:
if voter_counts[node] > 1:
raise exceptions.MultipleVotesError('Block {block_id} has multiple votes ({n_votes}) from voting node {node_id}'
.format(block_id=block['id'], n_votes=str(voter_counts[node]), node_id=node))
.format(block_id=block_id, n_votes=str(voter_counts[node]), node_id=node))
if len(votes) > n_voters:
raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes cast, but only {n_voters} voters'
.format(block_id=block['id'], n_votes=str(len(votes)), n_voters=str(n_voters)))
.format(block_id=block_id, n_votes=str(len(votes)), n_voters=str(n_voters)))
# vote_cast is the list of votes e.g. [True, True, False]
vote_cast = [vote['vote']['is_block_valid'] for vote in votes]
@ -770,7 +715,7 @@ class Bigchain(object):
prev_block = [vote['vote']['previous_block'] for vote in votes]
# vote_validity checks whether a vote is valid
# or invalid, e.g. [False, True, True]
vote_validity = [self.consensus.verify_vote_signature(block, vote) for vote in votes]
vote_validity = [self.consensus.verify_vote_signature(voters, vote) for vote in votes]
# element-wise product of stated vote and validity of vote
# vote_cast = [True, True, False] and

View File

@ -1,17 +0,0 @@
# Separate all crypto code so that we can easily test several implementations
import sha3
from cryptoconditions import crypto
def hash_data(data):
"""Hash the provided data using SHA3-256"""
return sha3.sha3_256(data.encode()).hexdigest()
def generate_key_pair():
sk, pk = crypto.ed25519_generate_key_pair()
return sk.decode(), pk.decode()
SigningKey = crypto.Ed25519SigningKey
VerifyingKey = crypto.Ed25519VerifyingKey

View File

@ -3,10 +3,10 @@
import time
import logging
from bigchaindb_common import exceptions
import rethinkdb as r
import bigchaindb
from bigchaindb import exceptions
logger = logging.getLogger(__name__)

View File

@ -1,65 +0,0 @@
"""Custom exceptions used in the `bigchaindb` package.
"""
class ConfigurationError(Exception):
"""Raised when there is a problem with server configuration"""
class OperationError(Exception):
"""Raised when an operation cannot go through"""
class TransactionDoesNotExist(Exception):
"""Raised if the transaction is not in the database"""
class TransactionOwnerError(Exception):
"""Raised if a user tries to transfer a transaction they don't own"""
class DoubleSpend(Exception):
"""Raised if a double spend is found"""
class InvalidHash(Exception):
"""Raised if there was an error checking the hash for a particular operation"""
class InvalidSignature(Exception):
"""Raised if there was an error checking the signature for a particular operation"""
class DatabaseAlreadyExists(Exception):
"""Raised when trying to create the database but the db is already there"""
class DatabaseDoesNotExist(Exception):
"""Raised when trying to delete the database but the db is not there"""
class KeypairNotFoundException(Exception):
"""Raised if operation cannot proceed because the keypair was not given"""
class KeypairMismatchException(Exception):
"""Raised if the private key(s) provided for signing don't match any of the curret owner(s)"""
class StartupError(Exception):
"""Raised when there is an error starting up the system"""
class ImproperVoteError(Exception):
"""Raised if a vote is not constructed correctly, or signed incorrectly"""
class MultipleVotesError(Exception):
"""Raised if a voter has voted more than once"""
class GenesisBlockAlreadyExistsError(Exception):
"""Raised when trying to create the already existing genesis block"""
class CyclicBlockchainError(Exception):
"""Raised when there is a cycle in the blockchain"""

208
bigchaindb/models.py Normal file
View File

@ -0,0 +1,208 @@
from bigchaindb_common.crypto import hash_data, VerifyingKey, SigningKey
from bigchaindb_common.exceptions import (InvalidHash, InvalidSignature,
OperationError, DoubleSpend,
TransactionDoesNotExist)
from bigchaindb_common.transaction import Transaction
from bigchaindb_common.util import gen_timestamp, serialize
class Transaction(Transaction):
def validate(self, bigchain):
"""Validate a transaction.
Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
Returns:
The transaction (Transaction) if the transaction is valid else it
raises an exception describing the reason why the transaction is
invalid.
Raises:
OperationError: if the transaction operation is not supported
TransactionDoesNotExist: if the input of the transaction is not
found
TransactionOwnerError: if the new transaction is using an input it
doesn't own
DoubleSpend: if the transaction is a double spend
InvalidHash: if the hash of the transaction is wrong
InvalidSignature: if the signature of the transaction is wrong
"""
if len(self.fulfillments) == 0:
raise ValueError('Transaction contains no fulfillments')
input_conditions = []
inputs_defined = all([ffill.tx_input for ffill in self.fulfillments])
if self.operation in (Transaction.CREATE, Transaction.GENESIS):
if inputs_defined:
raise ValueError('A CREATE operation has no inputs')
elif self.operation == Transaction.TRANSFER:
if not inputs_defined:
raise ValueError('Only `CREATE` transactions can have null '
'inputs')
for ffill in self.fulfillments:
input_txid = ffill.tx_input.txid
input_cid = ffill.tx_input.cid
input_tx = bigchain.get_transaction(input_txid)
if input_tx is None:
raise TransactionDoesNotExist("input `{}` doesn't exist"
.format(input_txid))
spent = bigchain.get_spent(input_txid, ffill.tx_input.cid)
if spent and spent.id != self.id:
raise DoubleSpend('input `{}` was already spent'
.format(input_txid))
input_conditions.append(input_tx.conditions[input_cid])
else:
allowed_operations = ', '.join(Transaction.ALLOWED_OPERATIONS)
raise TypeError('`operation`: `{}` must be either {}.'
.format(self.operation, allowed_operations))
if not self.fulfillments_valid(input_conditions):
raise InvalidSignature()
else:
return self
class Block(object):
def __init__(self, transactions=None, node_pubkey=None, timestamp=None,
voters=None, signature=None):
if transactions is not None and not isinstance(transactions, list):
raise TypeError('`transactions` must be a list instance or None')
else:
self.transactions = transactions or []
if voters is not None and not isinstance(voters, list):
raise TypeError('`voters` must be a list instance or None')
else:
self.voters = voters or []
if timestamp is not None:
self.timestamp = timestamp
else:
self.timestamp = gen_timestamp()
self.node_pubkey = node_pubkey
self.signature = signature
def __eq__(self, other):
try:
other = other.to_dict()
except AttributeError:
return False
return self.to_dict() == other
def validate(self, bigchain):
"""Validate a block.
Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
Returns:
block (Block): The block as a `Block` object if it is valid.
Else it raises an appropriate exception describing
the reason of invalidity.
Raises:
OperationError: if a non-federation node signed the block.
"""
# First, make sure this node hasn't already voted on this block
if bigchain.has_previous_vote(self.id, self.voters):
return self
# Check if the block was created by a federation node
possible_voters = (bigchain.nodes_except_me + [bigchain.me])
if self.node_pubkey not in possible_voters:
raise OperationError('Only federation nodes can create blocks')
if not self.is_signature_valid():
raise InvalidSignature('Block signature invalid')
# Finally: Tentative assumption that every blockchain will want to
# validate all transactions in each block
for tx in self.transactions:
# NOTE: If a transaction is not valid, `is_valid` will throw an
# an exception and block validation will be canceled.
bigchain.validate_transaction(tx)
return self
def sign(self, signing_key):
block_body = self.to_dict()
block_serialized = serialize(block_body['block'])
signing_key = SigningKey(signing_key)
self.signature = signing_key.sign(block_serialized)
return self
def is_signature_valid(self):
block = self.to_dict()['block']
block_serialized = serialize(block)
verifying_key = VerifyingKey(block['node_pubkey'])
try:
# NOTE: CC throws a `ValueError` on some wrong signatures
# https://github.com/bigchaindb/cryptoconditions/issues/27
return verifying_key.verify(block_serialized, self.signature)
except (ValueError, AttributeError):
return False
@classmethod
def from_dict(cls, block_body):
block = block_body['block']
block_serialized = serialize(block)
block_id = hash_data(block_serialized)
verifying_key = VerifyingKey(block['node_pubkey'])
try:
signature = block_body['signature']
except KeyError:
signature = None
if block_id != block_body['id']:
raise InvalidHash()
if signature is not None:
# NOTE: CC throws a `ValueError` on some wrong signatures
# https://github.com/bigchaindb/cryptoconditions/issues/27
try:
signature_valid = verifying_key.verify(block_serialized,
signature)
except ValueError:
signature_valid = False
if signature_valid is False:
raise InvalidSignature('Invalid block signature')
transactions = [Transaction.from_dict(tx) for tx
in block['transactions']]
return cls(transactions, block['node_pubkey'],
block['timestamp'], block['voters'], signature)
@property
def id(self):
return self.to_dict()['id']
def to_dict(self):
if len(self.transactions) == 0:
raise OperationError('Empty block creation is not allowed')
block = {
'timestamp': self.timestamp,
'transactions': [tx.to_dict() for tx in self.transactions],
'node_pubkey': self.node_pubkey,
'voters': self.voters,
}
block_serialized = serialize(block)
block_id = hash_data(block_serialized)
return {
'id': block_id,
'block': block,
'signature': self.signature,
}
def to_str(self):
return serialize(self.to_dict())

View File

@ -30,4 +30,3 @@ class Monitor(statsd.StatsClient):
if 'port' not in kwargs:
kwargs['port'] = bigchaindb.config['statsd']['port']
super().__init__(*args, **kwargs)

View File

@ -1,6 +1,6 @@
"""This module takes care of all the logic related to block creation.
The logic is encapsulated in the ``Block`` class, while the sequence
The logic is encapsulated in the ``BlockPipeline`` class, while the sequence
of actions to do on transactions is specified in the ``create_pipeline``
function.
"""
@ -10,6 +10,7 @@ import logging
import rethinkdb as r
from multipipes import Pipeline, Node
from bigchaindb.models import Transaction
from bigchaindb.pipelines.utils import ChangeFeed
from bigchaindb import Bigchain
@ -17,7 +18,7 @@ from bigchaindb import Bigchain
logger = logging.getLogger(__name__)
class Block:
class BlockPipeline:
"""This class encapsulates the logic to create blocks.
Note:
@ -25,7 +26,7 @@ class Block:
"""
def __init__(self):
"""Initialize the Block creator"""
"""Initialize the BlockPipeline creator"""
self.bigchain = Bigchain()
self.txs = []
@ -36,10 +37,9 @@ class Block:
tx (dict): the transaction to process.
Returns:
The transaction if assigned to the current node,
dict: The transaction if assigned to the current node,
``None`` otherwise.
"""
if tx['assignee'] == self.bigchain.me:
tx.pop('assignee')
tx.pop('assignment_timestamp')
@ -55,19 +55,21 @@ class Block:
tx (dict): the transaction to validate.
Returns:
The transaction if valid, ``None`` otherwise.
:class:`~bigchaindb.models.Transaction`: The transaction if valid,
``None`` otherwise.
"""
if self.bigchain.transaction_exists(tx['id']):
tx = Transaction.from_dict(tx)
if self.bigchain.transaction_exists(tx.id):
# if the transaction already exists, we must check whether
# it's in a valid or undecided block
tx, status = self.bigchain.get_transaction(tx['id'],
tx, status = self.bigchain.get_transaction(tx.id,
include_status=True)
if status == self.bigchain.TX_VALID \
or status == self.bigchain.TX_UNDECIDED:
# if the tx is already in a valid or undecided block,
# then it no longer should be in the backlog, or added
# to a new block. We can delete and drop it.
r.table('backlog').get(tx['id']) \
r.table('backlog').get(tx.id) \
.delete(durability='hard') \
.run(self.bigchain.conn)
return None
@ -78,7 +80,7 @@ class Block:
else:
# if the transaction is not valid, remove it from the
# backlog
r.table('backlog').get(tx['id']) \
r.table('backlog').get(tx.id) \
.delete(durability='hard') \
.run(self.bigchain.conn)
return None
@ -92,13 +94,14 @@ class Block:
- a timeout happened.
Args:
tx (dict): the transaction to validate, might be None if
a timeout happens.
tx (:class:`~bigchaindb.models.Transaction`): the transaction
to validate, might be None if a timeout happens.
timeout (bool): ``True`` if a timeout happened
(Default: ``False``).
Returns:
The block, if a block is ready, or ``None``.
:class:`~bigchaindb.models.Block`: The block,
if a block is ready, or ``None``.
"""
if tx:
self.txs.append(tx)
@ -111,14 +114,14 @@ class Block:
"""Write the block to the Database.
Args:
block (dict): the block of transactions to write to the database.
block (:class:`~bigchaindb.models.Block`): the block of
transactions to write to the database.
Returns:
The block.
:class:`~bigchaindb.models.Block`: The Block.
"""
logger.info('Write new block %s with %s transactions',
block['id'],
len(block['block']['transactions']))
logger.info('Write new block {} with {} transactions'.format(block.id,
len(block.transactions)))
self.bigchain.write_block(block)
return block
@ -126,13 +129,14 @@ class Block:
"""Delete transactions.
Args:
block (dict): the block containg the transactions to delete.
block (:class:`~bigchaindb.models.Block`): the block
containg the transactions to delete.
Returns:
The block.
:class:`~bigchaindb.models.Block`: The block.
"""
r.table('backlog')\
.get_all(*[tx['id'] for tx in block['block']['transactions']])\
.get_all(*[tx.id for tx in block.transactions])\
.delete(durability='hard')\
.run(self.bigchain.conn)
@ -166,24 +170,22 @@ def create_pipeline():
"""Create and return the pipeline of operations to be distributed
on different processes."""
block = Block()
block_pipeline = BlockPipeline()
block_pipeline = Pipeline([
Node(block.filter_tx),
Node(block.validate_tx, fraction_of_cores=1),
Node(block.create, timeout=1),
Node(block.write),
Node(block.delete_tx),
pipeline = Pipeline([
Node(block_pipeline.filter_tx),
Node(block_pipeline.validate_tx, fraction_of_cores=1),
Node(block_pipeline.create, timeout=1),
Node(block_pipeline.write),
Node(block_pipeline.delete_tx),
])
return block_pipeline
return pipeline
def start():
"""Create, start, and return the block pipeline."""
pipeline = create_pipeline()
pipeline.setup(indata=get_changefeed())
pipeline.start()
return pipeline

View File

@ -10,6 +10,7 @@ import rethinkdb as r
from multipipes import Pipeline, Node
from bigchaindb.pipelines.utils import ChangeFeed
from bigchaindb.models import Block
from bigchaindb import Bigchain
@ -17,6 +18,7 @@ logger = logging.getLogger(__name__)
class Election:
"""Election class."""
def __init__(self):
self.bigchain = Bigchain()
@ -24,22 +26,28 @@ class Election:
def check_for_quorum(self, next_vote):
"""
Checks if block has enough invalid votes to make a decision
Args:
next_vote: The next vote.
"""
next_block = self.bigchain.connection.run(
r.table('bigchain')
.get(next_vote['vote']['voting_for_block']))
if self.bigchain.block_election_status(next_block) == self.bigchain.BLOCK_INVALID:
return next_block
block_status = self.bigchain.block_election_status(next_block['id'],
next_block['block']['voters'])
if block_status == self.bigchain.BLOCK_INVALID:
return Block.from_dict(next_block)
def requeue_transactions(self, invalid_block):
"""
Liquidates transactions from invalid blocks so they can be processed again
"""
logger.info('Rewriting %s transactions from invalid block %s',
len(invalid_block['block']['transactions']),
invalid_block['id'])
for tx in invalid_block['block']['transactions']:
len(invalid_block.transactions),
invalid_block.id)
for tx in invalid_block.transactions:
self.bigchain.write_transaction(tx)
return invalid_block

View File

@ -8,22 +8,14 @@ function.
from collections import Counter
from multipipes import Pipeline, Node
from bigchaindb_common import exceptions
from bigchaindb import config_utils, exceptions
from bigchaindb.consensus import BaseConsensusRules
from bigchaindb.models import Transaction, Block
from bigchaindb.pipelines.utils import ChangeFeed
from bigchaindb import Bigchain
def create_invalid_tx():
"""Create and return an invalid transaction.
The transaction is invalid because it's missing the signature."""
b = Bigchain()
tx = b.create_transaction(b.me, b.me, None, 'CREATE')
return tx
class Vote:
"""This class encapsulates the logic to vote on blocks.
@ -37,35 +29,50 @@ class Vote:
# Since cannot share a connection to RethinkDB using multiprocessing,
# we need to create a temporary instance of BigchainDB that we use
# only to query RethinkDB
last_voted = Bigchain().get_last_voted_block()
self.consensus = config_utils.load_consensus_plugin()
self.consensus = BaseConsensusRules
# This is the Bigchain instance that will be "shared" (aka: copied)
# by all the subprocesses
self.bigchain = Bigchain()
self.last_voted_id = last_voted['id']
self.last_voted_id = Bigchain().get_last_voted_block().id
self.counters = Counter()
self.validity = {}
self.invalid_dummy_tx = create_invalid_tx()
self.invalid_dummy_tx = Transaction.create([self.bigchain.me],
[self.bigchain.me])
def validate_block(self, block):
if not self.bigchain.has_previous_vote(block):
if not self.bigchain.has_previous_vote(block['id'], block['block']['voters']):
try:
block = Block.from_dict(block)
except (exceptions.InvalidHash, exceptions.InvalidSignature):
# XXX: if a block is invalid we should skip the `validate_tx`
# step, but since we are in a pipeline we cannot just jump to
# another function. Hackish solution: generate an invalid
# transaction and propagate it to the next steps of the
# pipeline.
return block['id'], [self.invalid_dummy_tx]
try:
self.consensus.validate_block(self.bigchain, block)
valid = True
except (exceptions.InvalidHash,
exceptions.OperationError,
exceptions.InvalidSignature) as e:
valid = False
return block, valid
exceptions.InvalidSignature):
# XXX: if a block is invalid we should skip the `validate_tx`
# step, but since we are in a pipeline we cannot just jump to
# another function. Hackish solution: generate an invalid
# transaction and propagate it to the next steps of the
# pipeline.
return block.id, [self.invalid_dummy_tx]
return block.id, block.transactions
def ungroup(self, block, valid):
def ungroup(self, block_id, transactions):
"""Given a block, ungroup the transactions in it.
Args:
block (dict): the block to process
block_id (str): the id of the block in progress.
transactions (list(Transaction)): transactions of the block in
progress.
Returns:
``None`` if the block has been already voted, an iterator that
@ -73,16 +80,9 @@ class Vote:
transactions contained in the block otherwise.
"""
# XXX: if a block is invalid we should skip the `validate_tx` step,
# but since we are in a pipeline we cannot just jump to another
# function. Hackish solution: generate an invalid transaction
# and propagate it to the next steps of the pipeline
if valid:
num_tx = len(block['block']['transactions'])
for tx in block['block']['transactions']:
yield tx, block['id'], num_tx
else:
yield self.invalid_dummy_tx, block['id'], 1
num_tx = len(transactions)
for tx in transactions:
yield tx, block_id, num_tx
def validate_tx(self, tx, block_id, num_tx):
"""Validate a transaction.

View File

@ -1,6 +1,6 @@
import copy
import time
import contextlib
from copy import deepcopy
import threading
import queue
import multiprocessing as mp
@ -8,12 +8,13 @@ import uuid
import rapidjson
from bigchaindb_common import crypto, exceptions
from bigchaindb_common.util import serialize
import cryptoconditions as cc
from cryptoconditions.exceptions import ParsingError
import bigchaindb
from bigchaindb import exceptions
from bigchaindb import crypto
from bigchaindb.models import Transaction
class ProcessGroup(object):
@ -93,440 +94,7 @@ def pool(builder, size, timeout=None):
return pooled
def serialize(data):
"""Serialize a dict into a JSON formatted string.
This function enforces rules like the separator and order of keys. This ensures that all dicts
are serialized in the same way.
This is specially important for hashing data. We need to make sure that everyone serializes their data
in the same way so that we do not have hash mismatches for the same structure due to serialization
differences.
Args:
data (dict): dict to serialize
Returns:
str: JSON formatted string
"""
return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False, sort_keys=True)
def deserialize(data):
"""Deserialize a JSON formatted string into a dict.
Args:
data (str): JSON formatted string.
Returns:
dict: dict resulting from the serialization of a JSON formatted string.
"""
return rapidjson.loads(data)
def timestamp():
"""The Unix time, rounded to the nearest second.
See https://en.wikipedia.org/wiki/Unix_time
Returns:
str: the Unix time
"""
return str(round(time.time()))
# TODO: Consider remove the operation (if there are no inputs CREATE else TRANSFER)
def create_tx(owners_before, owners_after, inputs, operation, payload=None):
"""Create a new transaction
A transaction in the bigchain is a transfer of a digital asset between two entities represented
by public keys.
Currently the bigchain supports two types of operations:
`CREATE` - Only federation nodes are allowed to use this operation. In a create operation
a federation node creates a digital asset in the bigchain and assigns that asset to a public
key. The owner of the private key can then decided to transfer this digital asset by using the
`transaction id` of the transaction as an input in a `TRANSFER` transaction.
`TRANSFER` - A transfer operation allows for a transfer of the digital assets between entities.
Args:
owners_before (list): base58 encoded public key of the current owners of the asset.
owners_after (list): base58 encoded public key of the new owners of the digital asset.
inputs (list): id of the transaction to use as input.
operation (str): Either `CREATE` or `TRANSFER` operation.
payload (Optional[dict]): dictionary with information about asset.
Returns:
dict: unsigned transaction.
Raises:
TypeError: if the optional ``payload`` argument is not a ``dict``.
Reference:
{
"id": "<sha3 hash>",
"transaction": {
"version": "transaction version number",
"fulfillments": [
{
"owners_before": ["list of <pub-keys>"],
"input": {
"txid": "<sha3 hash>",
"cid": "condition index"
},
"fulfillment": "fulfillement of condition cid",
"fid": "fulfillment index"
}
],
"conditions": [
{
"owners_after": ["list of <pub-keys>"],
"condition": "condition to be met",
"cid": "condition index (1-to-1 mapping with fid)"
}
],
"operation": "<string>",
"timestamp": "<timestamp from client>",
"data": {
"hash": "<SHA3-256 hash hexdigest of payload>",
"payload": {
"title": "The Winds of Plast",
"creator": "Johnathan Plunkett",
"IPFS_key": "QmfQ5QAjvg4GtA3wg3adpnDJug8ktA1BxurVqBD8rtgVjP"
}
}
},
}
"""
# validate arguments (owners and inputs should be lists or None)
# The None case appears on fulfilling a hashlock
if owners_before is None:
owners_before = []
if not isinstance(owners_before, list):
owners_before = [owners_before]
# The None case appears on assigning a hashlock
if owners_after is None:
owners_after = []
if not isinstance(owners_after, list):
owners_after = [owners_after]
if not isinstance(inputs, list):
inputs = [inputs]
# handle payload
if payload is not None and not isinstance(payload, dict):
raise TypeError('`payload` must be an dict instance or None')
data = {
'uuid': str(uuid.uuid4()),
'payload': payload
}
# handle inputs
fulfillments = []
# transfer
if inputs:
for fid, tx_input in enumerate(inputs):
fulfillments.append({
'owners_before': owners_before,
'input': tx_input,
'fulfillment': None,
'fid': fid
})
# create
else:
fulfillments.append({
'owners_before': owners_before,
'input': None,
'fulfillment': None,
'fid': 0
})
# handle outputs
conditions = []
for fulfillment in fulfillments:
# threshold condition
if len(owners_after) > 1:
condition = cc.ThresholdSha256Fulfillment(threshold=len(owners_after))
for owner_after in owners_after:
condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=owner_after))
# simple signature condition
elif len(owners_after) == 1:
condition = cc.Ed25519Fulfillment(public_key=owners_after[0])
# to be added later (hashlock conditions)
else:
condition = None
if condition:
conditions.append({
'owners_after': owners_after,
'condition': {
'details': condition.to_dict(),
'uri': condition.condition_uri
},
'cid': fulfillment['fid']
})
tx = {
'version': 1,
'fulfillments': fulfillments,
'conditions': conditions,
'operation': operation,
'timestamp': timestamp(),
'data': data
}
# serialize and convert to bytes
tx_hash = get_hash_data(tx)
# create the transaction
transaction = {
'id': tx_hash,
'transaction': tx
}
return transaction
def sign_tx(transaction, signing_keys, bigchain=None):
"""Sign a transaction
A transaction signed with the `owner_before` corresponding private key.
Args:
transaction (dict): transaction to sign.
signing_keys (list): list of base58 encoded private keys to create the fulfillments of the transaction.
bigchain (obj): bigchain instance used to get the details of the previous transaction outputs. Useful
if the `Bigchain` instance was instantiated with parameters that override the config file.
Returns:
dict: transaction with the `fulfillment` fields populated.
"""
# validate sk
if not isinstance(signing_keys, list):
signing_keys = [signing_keys]
# create a mapping between sk and vk so that we can match the private key to the owners_before
key_pairs = {}
for sk in signing_keys:
signing_key = crypto.SigningKey(sk)
vk = signing_key.get_verifying_key().to_ascii().decode()
key_pairs[vk] = signing_key
tx = copy.deepcopy(transaction)
bigchain = bigchain if bigchain is not None else bigchaindb.Bigchain()
for fulfillment in tx['transaction']['fulfillments']:
fulfillment_message = get_fulfillment_message(transaction, fulfillment)
# TODO: avoid instantiation, pass as argument!
input_condition = get_input_condition(bigchain, fulfillment)
parsed_fulfillment = cc.Fulfillment.from_dict(input_condition['condition']['details'])
# for the case in which the type of fulfillment is not covered by this method
parsed_fulfillment_signed = parsed_fulfillment
# single current owner
if isinstance(parsed_fulfillment, cc.Ed25519Fulfillment):
parsed_fulfillment_signed = fulfill_simple_signature_fulfillment(fulfillment,
parsed_fulfillment,
fulfillment_message,
key_pairs)
# multiple current owners
elif isinstance(parsed_fulfillment, cc.ThresholdSha256Fulfillment):
parsed_fulfillment_signed = fulfill_threshold_signature_fulfillment(fulfillment,
parsed_fulfillment,
fulfillment_message,
key_pairs)
signed_fulfillment = parsed_fulfillment_signed.serialize_uri()
fulfillment.update({'fulfillment': signed_fulfillment})
return tx
def fulfill_simple_signature_fulfillment(fulfillment, parsed_fulfillment, fulfillment_message, key_pairs):
"""Fulfill a cryptoconditions.Ed25519Fulfillment
Args:
fulfillment (dict): BigchainDB fulfillment to fulfill.
parsed_fulfillment (cryptoconditions.Ed25519Fulfillment): cryptoconditions.Ed25519Fulfillment instance.
fulfillment_message (dict): message to sign.
key_pairs (dict): dictionary of (public_key, private_key) pairs.
Returns:
object: fulfilled cryptoconditions.Ed25519Fulfillment
"""
owner_before = fulfillment['owners_before'][0]
try:
parsed_fulfillment.sign(serialize(fulfillment_message), key_pairs[owner_before])
except KeyError:
raise exceptions.KeypairMismatchException('Public key {} is not a pair to any of the private keys'
.format(owner_before))
return parsed_fulfillment
def fulfill_threshold_signature_fulfillment(fulfillment, parsed_fulfillment, fulfillment_message, key_pairs):
"""Fulfill a cryptoconditions.ThresholdSha256Fulfillment
Args:
fulfillment (dict): BigchainDB fulfillment to fulfill.
parsed_fulfillment (cryptoconditions.ThresholdSha256Fulfillment): cryptoconditions.ThresholdSha256Fulfillment instance.
fulfillment_message (dict): message to sign.
key_pairs (dict): dictionary of (public_key, private_key) pairs.
Returns:
object: fulfilled cryptoconditions.ThresholdSha256Fulfillment
"""
parsed_fulfillment_copy = copy.deepcopy(parsed_fulfillment)
parsed_fulfillment.subconditions = []
for owner_before in fulfillment['owners_before']:
try:
subfulfillment = parsed_fulfillment_copy.get_subcondition_from_vk(owner_before)[0]
except IndexError:
raise exceptions.KeypairMismatchException(
'Public key {} cannot be found in the fulfillment'.format(owner_before))
try:
private_key = key_pairs[owner_before]
except KeyError:
raise exceptions.KeypairMismatchException(
'Public key {} is not a pair to any of the private keys'.format(owner_before))
subfulfillment.sign(serialize(fulfillment_message), private_key)
parsed_fulfillment.add_subfulfillment(subfulfillment)
return parsed_fulfillment
def create_and_sign_tx(private_key, owner_before, owner_after, tx_input, operation='TRANSFER', payload=None):
tx = create_tx(owner_before, owner_after, tx_input, operation, payload)
return sign_tx(tx, private_key)
def check_hash_and_signature(transaction):
# Check hash of the transaction
calculated_hash = get_hash_data(transaction)
if calculated_hash != transaction['id']:
raise exceptions.InvalidHash()
# Check signature
if not validate_fulfillments(transaction):
raise exceptions.InvalidSignature()
def validate_fulfillments(signed_transaction):
"""Verify the signature of a transaction
A valid transaction should have been signed `owner_before` corresponding private key.
Args:
signed_transaction (dict): a transaction with the `signature` included.
Returns:
bool: True if the signature is correct, False otherwise.
"""
for fulfillment in signed_transaction['transaction']['fulfillments']:
fulfillment_message = get_fulfillment_message(signed_transaction, fulfillment)
try:
parsed_fulfillment = cc.Fulfillment.from_uri(fulfillment['fulfillment'])
except (TypeError, ValueError, ParsingError):
return False
# TODO: might already break on a False here
is_valid = parsed_fulfillment.validate(message=serialize(fulfillment_message),
now=timestamp())
# if transaction has an input (i.e. not a `CREATE` transaction)
# TODO: avoid instantiation, pass as argument!
bigchain = bigchaindb.Bigchain()
input_condition = get_input_condition(bigchain, fulfillment)
is_valid = is_valid and parsed_fulfillment.condition_uri == input_condition['condition']['uri']
if not is_valid:
return False
return True
def get_fulfillment_message(transaction, fulfillment, serialized=False):
"""Get the fulfillment message for signing a specific fulfillment in a transaction
Args:
transaction (dict): a transaction
fulfillment (dict): a specific fulfillment (for a condition index) within the transaction
serialized (Optional[bool]): False returns a dict, True returns a serialized string
Returns:
str|dict: fulfillment message
"""
# data to sign contains common transaction data
fulfillment_message = {
'operation': transaction['transaction']['operation'],
'timestamp': transaction['transaction']['timestamp'],
'data': transaction['transaction']['data'],
'version': transaction['transaction']['version'],
'id': transaction['id']
}
# and the condition which needs to be retrieved from the output of a previous transaction
# or created on the fly it this is a `CREATE` transaction
fulfillment_message.update({
'fulfillment': copy.deepcopy(fulfillment),
'condition': transaction['transaction']['conditions'][fulfillment['fid']]
})
# remove any fulfillment, as a fulfillment cannot sign itself
fulfillment_message['fulfillment']['fulfillment'] = None
if serialized:
return serialize(fulfillment_message)
return fulfillment_message
def get_input_condition(bigchain, fulfillment):
"""
Args:
bigchain:
fulfillment:
Returns:
"""
input_tx = fulfillment['input']
# if `TRANSFER` transaction
if input_tx:
# get previous condition
previous_tx = bigchain.get_transaction(input_tx['txid'])
conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid'])
return conditions[input_tx['cid']]
# if `CREATE` transaction
# there is no previous transaction so we need to create one on the fly
else:
owner_before = fulfillment['owners_before'][0]
condition = cc.Ed25519Fulfillment(public_key=owner_before)
return {
'condition': {
'details': condition.to_dict(),
'uri': condition.condition_uri
}
}
# TODO: Rename this function, it's handling fulfillments not conditions
def condition_details_has_owner(condition_details, owner):
"""
@ -558,33 +126,13 @@ def condition_details_has_owner(condition_details, owner):
return False
def get_hash_data(transaction):
""" Get the hashed data that (should) correspond to the `transaction['id']`
Args:
transaction (dict): the transaction to be hashed
Returns:
str: the hash of the transaction
"""
tx = copy.deepcopy(transaction)
if 'transaction' in tx:
tx = tx['transaction']
# remove the fulfillment messages (signatures)
for fulfillment in tx['fulfillments']:
fulfillment['fulfillment'] = None
return crypto.hash_data(serialize(tx))
def verify_vote_signature(block, signed_vote):
def verify_vote_signature(voters, signed_vote):
"""Verify the signature of a vote
A valid vote should have been signed `owner_before` corresponding private key.
Args:
block (dict): block under election
voters (list): voters of the block that is under election
signed_vote (dict): a vote with the `signature` included.
Returns:
@ -595,32 +143,18 @@ def verify_vote_signature(block, signed_vote):
vk_base58 = signed_vote['node_pubkey']
# immediately return False if the voter is not in the block voter list
if vk_base58 not in block['block']['voters']:
if vk_base58 not in voters:
return False
public_key = crypto.VerifyingKey(vk_base58)
return public_key.verify(serialize(signed_vote['vote']), signature)
def transform_create(tx):
"""Change the owner and signature for a ``CREATE`` transaction created by a node"""
# XXX: the next instruction opens a new connection to the DB, consider using a singleton or a global
# if you need a Bigchain instance.
b = bigchaindb.Bigchain()
transaction = tx['transaction']
payload = None
if transaction['data'] and 'payload' in transaction['data']:
payload = transaction['data']['payload']
new_tx = create_tx(b.me, transaction['fulfillments'][0]['owners_before'], None, 'CREATE', payload=payload)
return new_tx
def is_genesis_block(block):
"""Check if the block is the genesis block.
Args:
block (dict): the block to check
block (dict | Block): the block to check
Returns:
bool: True if the block is the genesis block, False otherwise.
@ -628,5 +162,8 @@ def is_genesis_block(block):
# we cannot have empty blocks, there will always be at least one
# element in the list so we can safely refer to it
return block['block']['transactions'][0]['transaction']['operation'] == 'GENESIS'
# TODO: Remove this try-except and only handle `Block` as input
try:
return block.transactions[0].operation == 'GENESIS'
except AttributeError:
return block['block']['transactions'][0]['transaction']['operation'] == 'GENESIS'

View File

@ -91,4 +91,3 @@ def create_server(settings):
app = create_app(settings)
standalone = StandaloneApplication(app, settings)
return standalone

View File

@ -23,4 +23,3 @@ def home():
'keyring': bigchaindb.config['keyring'],
'api_endpoint': bigchaindb.config['api_endpoint']
})

View File

@ -6,8 +6,10 @@ For more information please refer to the documentation on ReadTheDocs:
from flask import current_app, request, Blueprint
from flask_restful import Resource, Api
from bigchaindb_common.exceptions import InvalidHash, InvalidSignature
import bigchaindb
from bigchaindb import util
from bigchaindb.models import Transaction
from bigchaindb.web.views.base import make_error
@ -54,7 +56,7 @@ class TransactionApi(Resource):
if not tx:
return make_error(404)
return tx
return tx.to_dict()
class TransactionStatusApi(Resource):
@ -94,17 +96,19 @@ class TransactionListApi(Resource):
# set to `application/json`
tx = request.get_json(force=True)
try:
tx_obj = Transaction.from_dict(tx)
except (InvalidHash, InvalidSignature):
return make_error(400, 'Invalid transaction')
with pool() as bigchain:
if tx['transaction']['operation'] == 'CREATE':
tx = util.transform_create(tx)
tx = bigchain.consensus.sign_transaction(tx, private_key=bigchain.me_private)
if not bigchain.is_valid_transaction(tx):
if bigchain.is_valid_transaction(tx_obj):
rate = bigchaindb.config['statsd']['rate']
with monitor.timer('write_transaction', rate=rate):
bigchain.write_transaction(tx_obj)
else:
return make_error(400, 'Invalid transaction')
with monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']):
bigchain.write_transaction(tx)
return tx
transaction_api.add_resource(TransactionApi,

View File

@ -1,5 +1,5 @@
"""A Python 3 script to write a file with a specified number
of keypairs, using bigchaindb.crypto.generate_key_pair()
of keypairs, using bigchaindb_common.crypto.generate_key_pair()
The written file is always named keypairs.py and it should be
interpreted as a Python 2 script.
@ -16,7 +16,7 @@ Using the list in other Python scripts:
import argparse
from bigchaindb import crypto
from bigchaindb_common import crypto
# Parse the command-line arguments

16
docs.yml Normal file
View File

@ -0,0 +1,16 @@
version: '2'
services:
bdocs:
build:
context: .
dockerfile: ./Dockerfile-dev
volumes:
- .:/usr/src/app/
command: make -C docs html
vdocs:
image: nginx
ports:
- '33333:80'
volumes:
- ./docs/build/html:/usr/share/nginx/html

View File

@ -1,82 +0,0 @@
# BigchainDB Consensus Plugins
BigchainDB has a pluggable block/transaction validation architecture. The default consensus rules can be extended or replaced entirely.
## Installing a plugin
Plugins can be installed via pip!
```bash
$ pip install bigchaindb-plugin-demo
```
Or using setuptools:
```bash
$ cd bigchaindb-plugin-demo/
$ python setup.py install # (or develop)
```
To activate your plugin, you can either set the `consensus_plugin` field in your config file (usually `~/.bigchaindb`) or by setting the `BIGCHAIN_CONSENSUS_PLUGIN` environement variable to the name of your plugin (see the section on [Packaging a plugin](#packaging-a-plugin) for more about plugin names).
## Plugin API
BigchainDB's [current plugin API](https://github.com/bigchaindb/bigchaindb/blob/master/bigchaindb/consensus.py) exposes five functions in an `AbstractConsensusRules` class:
```python
validate_transaction(bigchain, transaction)
validate_block(bigchain, block)
create_transaction(*args, **kwargs)
sign_transaction(transaction, *args, **kwargs)
validate_fulfillments(transaction)
```
Together, these functions are sufficient for most customizations. For example:
- Replace the crypto-system with one your hardware can accelerate
- Re-implement an existing protocol
- Delegate validation to another application
- etc...
## Extending BigchainDB behavior
A default installation of BigchainDB will use the rules in the `BaseConsensusRules` class. If you only need to modify this behavior slightly, you can inherit from that class and call `super()` in any methods you change, so long as the return values remain the same.
Here's a quick example of a plugin that adds nonsense rules:
```python
from bigchaindb.consensus import BaseConsensusRules
class SillyConsensusRules(BaseConsensusRules):
@staticmethod
def validate_transaction(bigchain, transaction):
transaction = super().validate_transaction(bigchain, transaction)
# I only like transactions whose timestamps are even.
if transaction['transaction']['timestamp'] % 2 != 0:
raise StandardError("Odd... very odd indeed.")
return transaction
@staticmethod
def validate_block(bigchain, block):
block = super().validate_block(bigchain, block)
# I don't trust Alice, I think she's shady.
if block['block']['node_pubkey'] == '<ALICE_PUBKEY>':
raise StandardError("Alice is shady, everybody ignore her blocks!")
return block
```
## Packaging a plugin
BigchainDB uses [setuptools](https://setuptools.readthedocs.io/en/latest/)' entry_points to provide the plugin functionality. Any custom plugin needs to add this section to the `setup()` call in their `setup.py`:
```python
entry_points={
'bigchaindb.consensus': [
'PLUGIN_NAME=package.module:ConsensusRulesClass'
]
},
```

View File

@ -0,0 +1,6 @@
#########
Consensus
#########
.. automodule:: bigchaindb.consensus
:members:

View File

@ -13,10 +13,11 @@ Appendices
json-serialization
cryptography
the-Bigchain-class
aws-setup
consensus
pipelines
aws-setup
firewall-notes
ntp-notes
example-rethinkdb-storage-setups
licenses
install-with-lxd
install-with-lxd

View File

@ -0,0 +1,37 @@
#########
Pipelines
#########
Block Creation
==============
.. automodule:: bigchaindb.pipelines.block
:members:
Block Voting
============
.. automodule:: bigchaindb.pipelines.vote
:members:
Block Status
============
.. automodule:: bigchaindb.pipelines.election
:members:
Stale Transaction Monitoring
============================
.. automodule:: bigchaindb.pipelines.stale
:members:
Utilities
=========
.. automodule:: bigchaindb.pipelines.utils
:members:

View File

@ -20,7 +20,6 @@ For convenience, here's a list of all the relevant environment variables (docume
`BIGCHAINDB_SERVER_WORKERS`<br>
`BIGCHAINDB_SERVER_THREADS`<br>
`BIGCHAINDB_API_ENDPOINT`<br>
`BIGCHAINDB_CONSENSUS_PLUGIN`<br>
`BIGCHAINDB_STATSD_HOST`<br>
`BIGCHAINDB_STATSD_PORT`<br>
`BIGCHAINDB_STATSD_RATE`<br>
@ -163,21 +162,6 @@ export BIGCHAINDB_API_ENDPOINT="http://localhost:9984/api/v1"
```
## consensus_plugin
The [consensus plugin](../appendices/consensus.html) to use.
**Example using an environment variable**
```text
export BIGCHAINDB_CONSENSUS_PLUGIN=default
```
**Example config file snippet: the default**
```js
"consensus_plugin": "default"
```
## statsd.host, statsd.port & statsd.rate
These settings are used to configure where, and how often, [StatsD](https://github.com/etsy/statsd) should send data for [cluster monitoring](../clusters-feds/monitoring.html) purposes. `statsd.host` is the hostname of the monitoring server, where StatsD should send its data. `stats.port` is the port. `statsd.rate` is the fraction of transaction operations that should be sampled. It's a float between 0.0 and 1.0.

View File

@ -88,9 +88,6 @@ setup(
'console_scripts': [
'bigchaindb=bigchaindb.commands.bigchain:main'
],
'bigchaindb.consensus': [
'default=bigchaindb.consensus:BaseConsensusRules'
]
},
install_requires=[
'rethinkdb~=2.3',
@ -106,6 +103,7 @@ setup(
'requests~=2.9',
'gunicorn~=19.0',
'multipipes~=0.1.0',
'bigchaindb-common>=0.0.2',
],
setup_requires=['pytest-runner'],
tests_require=tests_require,

View File

@ -6,6 +6,8 @@ from line_profiler import LineProfiler
import bigchaindb
# BIG TODO: Adjust for new transaction model
def speedtest_validate_transaction():
# create a transaction

View File

@ -68,3 +68,21 @@ def b(request, node_config):
from bigchaindb import Bigchain
return Bigchain()
@pytest.fixture
def create_tx(b, user_vk):
from bigchaindb.models import Transaction
return Transaction.create([b.me], [user_vk])
@pytest.fixture
def signed_create_tx(b, create_tx):
return create_tx.sign([b.me_private])
@pytest.fixture
def signed_transfer_tx(signed_create_tx, user_vk, user_sk):
from bigchaindb.models import Transaction
inputs = signed_create_tx.to_inputs()
tx = Transaction.transfer(inputs, [user_vk])
return tx.sign([user_sk])

View File

@ -49,8 +49,8 @@ def setup_database(request, node_config):
r.db(db_name).table('backlog').index_create('transaction_timestamp', r.row['transaction']['timestamp']).run()
# to query by payload uuid
r.db(db_name).table('bigchain').index_create(
'payload_uuid',
r.row['block']['transactions']['transaction']['data']['uuid'],
'payload_uuid',
r.row['block']['transactions']['transaction']['data']['uuid'],
multi=True,
).run()
# compound index to read transactions from the backlog per assignee
@ -59,7 +59,7 @@ def setup_database(request, node_config):
.run()
# compound index to order votes by block id and node
r.db(db_name).table('votes').index_create('block_and_voter',
[r.row['vote']['voting_for_block'], r.row['node_pubkey']]).run()
[r.row['vote']['voting_for_block'], r.row['node_pubkey']]).run()
# order transactions by id
r.db(db_name).table('bigchain').index_create('transaction_id', r.row['block']['transactions']['id'],
multi=True).run()
@ -99,7 +99,8 @@ def cleanup_tables(request, node_config):
@pytest.fixture
def inputs(user_vk):
from bigchaindb.exceptions import GenesisBlockAlreadyExistsError
from bigchaindb.models import Transaction
from bigchaindb_common.exceptions import GenesisBlockAlreadyExistsError
# 1. create the genesis block
b = Bigchain()
try:
@ -109,11 +110,10 @@ def inputs(user_vk):
# 2. create block with transactions for `USER` to spend
for block in range(4):
transactions = []
for i in range(10):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
transactions.append(tx_signed)
transactions = [
Transaction.create(
[b.me], [user_vk], payload={'i': i}).sign([b.me_private])
for i in range(10)
]
block = b.create_block(transactions)
b.write_block(block, durability='hard')

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import builtins
from bigchaindb_common import exceptions
import pytest
import rethinkdb as r
@ -150,7 +151,7 @@ def test_init_fails_if_db_exists():
# The db is set up by fixtures
assert r.db_list().contains(dbname).run(conn) is True
with pytest.raises(bigchaindb.exceptions.DatabaseAlreadyExists):
with pytest.raises(exceptions.DatabaseAlreadyExists):
utils.init()
@ -200,6 +201,6 @@ def test_drop_non_existent_db_raises_an_error():
assert r.db_list().contains(dbname).run(conn) is True
utils.drop(assume_yes=True)
with pytest.raises(bigchaindb.exceptions.DatabaseDoesNotExist):
with pytest.raises(exceptions.DatabaseDoesNotExist):
utils.drop(assume_yes=True)

View File

@ -2,6 +2,7 @@ import json
from time import sleep
import cryptoconditions as cc
from bigchaindb_common.util import gen_timestamp
from bigchaindb import Bigchain, util, crypto, exceptions
@ -315,7 +316,7 @@ tx_timeout = b.create_transaction(b.me, None, None, 'CREATE')
# Set expiry time (12 secs from now)
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep)
time_expire = str(float(gen_timestamp()) + time_sleep)
# only valid if the server time <= time_expire
condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire)
@ -355,7 +356,7 @@ tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_f
# no need to sign transaction, like with hashlocks
for i in range(time_sleep - 4):
tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
seconds_to_timeout = int(float(time_expire) - float(gen_timestamp()))
print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout))
sleep(1)
@ -370,7 +371,7 @@ tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub],
# Set expiry time (12 secs from now)
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep)
time_expire = str(float(gen_timestamp()) + time_sleep)
# Create escrow and timeout condition
condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate
@ -486,6 +487,6 @@ for i in range(time_sleep - 4):
valid_execute = b.is_valid_transaction(tx_escrow_execute) == tx_escrow_execute
valid_abort = b.is_valid_transaction(tx_escrow_abort) == tx_escrow_abort
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
seconds_to_timeout = int(float(time_expire) - float(gen_timestamp()))
print('tx_execute valid: {} - tx_abort valid {} ({}s to timeout)'.format(valid_execute, valid_abort, seconds_to_timeout))
sleep(1)

View File

@ -16,4 +16,3 @@ def setup_database(request, node_config):
@pytest.fixture(scope='function', autouse=True)
def cleanup_tables(request, node_config):
conftest.cleanup_tables(request, node_config)

View File

@ -1,20 +1,18 @@
import time
import random
from unittest.mock import patch
import rethinkdb as r
from bigchaindb.pipelines import block
from multipipes import Pipe, Pipeline
from multipipes import Pipe
def test_filter_by_assignee(b, user_vk):
block_maker = block.Block()
def test_filter_by_assignee(b, signed_create_tx):
from bigchaindb.pipelines.block import BlockPipeline
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx['assignee'] = b.me
tx['assignment_timestamp'] = 111
block_maker = BlockPipeline()
tx = signed_create_tx.to_dict()
tx.update({'assignee': b.me, 'assignment_timestamp': 111})
# filter_tx has side effects on the `tx` instance by popping 'assignee'
# and 'assignment_timestamp'
@ -23,91 +21,97 @@ def test_filter_by_assignee(b, user_vk):
assert 'assignee' not in filtered_tx
assert 'assignment_timestamp' not in filtered_tx
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx['assignee'] = 'nobody'
tx = signed_create_tx.to_dict()
tx.update({'assignee': 'nobody'})
assert block_maker.filter_tx(tx) is None
def test_validate_transaction(b, user_vk):
block_maker = block.Block()
def test_validate_transaction(b, create_tx):
from bigchaindb.pipelines.block import BlockPipeline
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx['id'] = 'a' * 64
block_maker = BlockPipeline()
assert block_maker.validate_tx(tx) is None
assert block_maker.validate_tx(create_tx.to_dict()) is None
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
assert block_maker.validate_tx(tx) == tx
valid_tx = create_tx.sign([b.me_private])
assert block_maker.validate_tx(valid_tx.to_dict()) == valid_tx
def test_create_block(b, user_vk):
block_maker = block.Block()
from bigchaindb.models import Transaction
from bigchaindb.pipelines.block import BlockPipeline
block_maker = BlockPipeline()
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
block_maker.create(tx)
# force the output triggering a `timeout`
block_doc = block_maker.create(None, timeout=True)
assert len(block_doc['block']['transactions']) == 100
assert len(block_doc.transactions) == 100
def test_write_block(b, user_vk):
block_maker = block.Block()
from bigchaindb.models import Block, Transaction
from bigchaindb.pipelines.block import BlockPipeline
block_maker = BlockPipeline()
txs = []
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
txs.append(tx)
block_doc = b.create_block(txs)
block_maker.write(block_doc)
expected = r.table('bigchain').get(block_doc.id).run(b.conn)
expected = Block.from_dict(expected)
assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc
assert expected == block_doc
def test_duplicate_transaction(b, user_vk):
block_maker = block.Block()
from bigchaindb.models import Transaction
from bigchaindb.pipelines import block
block_maker = block.BlockPipeline()
txs = []
for i in range(10):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
txs.append(tx)
block_doc = b.create_block(txs)
block_maker.write(block_doc)
# block is in bigchain
assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc
assert r.table('bigchain').get(block_doc.id).run(b.conn) == block_doc.to_dict()
b.write_transaction(txs[0])
# verify tx is in the backlog
assert r.table('backlog').get(txs[0]['id']).run(b.conn) is not None
assert r.table('backlog').get(txs[0].id).run(b.conn) is not None
# try to validate a transaction that's already in the chain; should not
# work
assert block_maker.validate_tx(txs[0]) is None
assert block_maker.validate_tx(txs[0].to_dict()) is None
# duplicate tx should be removed from backlog
assert r.table('backlog').get(txs[0]['id']).run(b.conn) is None
assert r.table('backlog').get(txs[0].id).run(b.conn) is None
def test_delete_tx(b, user_vk):
block_maker = block.Block()
from bigchaindb.models import Transaction
from bigchaindb.pipelines.block import BlockPipeline
block_maker = BlockPipeline()
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
block_maker.create(tx)
# make sure the tx appears in the backlog
b.write_transaction(tx)
@ -115,7 +119,7 @@ def test_delete_tx(b, user_vk):
# force the output triggering a `timeout`
block_doc = block_maker.create(None, timeout=True)
for tx in block_doc['block']['transactions']:
for tx in block_doc.to_dict()['block']['transactions']:
returned_tx = r.table('backlog').get(tx['id']).run(b.conn)
returned_tx.pop('assignee')
returned_tx.pop('assignment_timestamp')
@ -125,37 +129,47 @@ def test_delete_tx(b, user_vk):
assert returned_block == block_doc
for tx in block_doc['block']['transactions']:
for tx in block_doc.to_dict()['block']['transactions']:
assert r.table('backlog').get(tx['id']).run(b.conn) is None
def test_prefeed(b, user_vk):
import random
from bigchaindb.models import Transaction
from bigchaindb.pipelines.block import initial
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk], {'msg': random.random()})
tx = tx.sign([b.me_private])
b.write_transaction(tx)
backlog = block.initial()
backlog = initial()
assert len(list(backlog)) == 100
@patch.object(Pipeline, 'start')
def test_start(mock_start):
# TODO: `block.start` is just a wrapper around `block.create_pipeline`,
# that is tested by `test_full_pipeline`.
# If anyone has better ideas on how to test this, please do a PR :)
block.start()
mock_start.assert_called_with()
@patch('bigchaindb.pipelines.block.create_pipeline')
def test_start(create_pipeline):
from bigchaindb.pipelines import block
pipeline = block.start()
assert create_pipeline.called
assert create_pipeline.return_value.setup.called
assert create_pipeline.return_value.start.called
assert pipeline == create_pipeline.return_value
def test_full_pipeline(b, user_vk):
import random
from bigchaindb.models import Block, Transaction
from bigchaindb.pipelines.block import create_pipeline, get_changefeed
outpipe = Pipe()
count_assigned_to_me = 0
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk], {'msg': random.random()})
tx = tx.sign([b.me_private]).to_dict()
assignee = random.choice([b.me, 'aaa', 'bbb', 'ccc'])
tx['assignee'] = assignee
tx['assignment_timestamp'] = time.time()
@ -165,16 +179,17 @@ def test_full_pipeline(b, user_vk):
assert r.table('backlog').count().run(b.conn) == 100
pipeline = block.create_pipeline()
pipeline.setup(indata=block.get_changefeed(), outdata=outpipe)
pipeline = create_pipeline()
pipeline.setup(indata=get_changefeed(), outdata=outpipe)
pipeline.start()
time.sleep(2)
pipeline.terminate()
block_doc = outpipe.get()
chained_block = r.table('bigchain').get(block_doc.id).run(b.conn)
chained_block = Block.from_dict(chained_block)
assert len(block_doc['block']['transactions']) == count_assigned_to_me
assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc
assert len(block_doc.transactions) == count_assigned_to_me
assert chained_block == block_doc
assert r.table('backlog').count().run(b.conn) == 100 - count_assigned_to_me

View File

@ -1,19 +1,21 @@
import time
import random
from bigchaindb import crypto, Bigchain
from unittest.mock import patch
from bigchaindb_common import crypto
import rethinkdb as r
from bigchaindb.pipelines import election
from multipipes import Pipe, Pipeline
from bigchaindb import Bigchain
from bigchaindb.pipelines import election
def test_check_for_quorum_invalid(b, user_vk):
from bigchaindb.models import Transaction
e = election.Election()
# create blocks with transactions
tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx1 = Transaction.create([b.me], [user_vk])
test_block = b.create_block([tx1])
# simulate a federation with four voters
@ -22,12 +24,13 @@ def test_check_for_quorum_invalid(b, user_vk):
for key_pair in key_pairs]
# add voters to block and write
test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
test_block.voters = [key_pair[1] for key_pair in key_pairs]
test_block = test_block.sign(b.me_private)
b.write_block(test_block)
# split_vote (invalid)
votes = [member.vote(test_block['id'], 'abc', True) for member in test_federation[:2]] + \
[member.vote(test_block['id'], 'abc', False) for member in test_federation[2:]]
votes = [member.vote(test_block.id, 'abc', True) for member in test_federation[:2]] + \
[member.vote(test_block.id, 'abc', False) for member in test_federation[2:]]
# cast votes
r.table('votes').insert(votes, durability='hard').run(b.conn)
@ -37,10 +40,11 @@ def test_check_for_quorum_invalid(b, user_vk):
def test_check_for_quorum_invalid_prev_node(b, user_vk):
from bigchaindb.models import Transaction
e = election.Election()
# create blocks with transactions
tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx1 = Transaction.create([b.me], [user_vk])
test_block = b.create_block([tx1])
# simulate a federation with four voters
@ -49,12 +53,13 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk):
for key_pair in key_pairs]
# add voters to block and write
test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
test_block.voters = [key_pair[1] for key_pair in key_pairs]
test_block = test_block.sign(b.me_private)
b.write_block(test_block)
# split vote over prev node
votes = [member.vote(test_block['id'], 'abc', True) for member in test_federation[:2]] + \
[member.vote(test_block['id'], 'def', True) for member in test_federation[2:]]
votes = [member.vote(test_block.id, 'abc', True) for member in test_federation[:2]] + \
[member.vote(test_block.id, 'def', True) for member in test_federation[2:]]
# cast votes
r.table('votes').insert(votes, durability='hard').run(b.conn)
@ -64,10 +69,12 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk):
def test_check_for_quorum_valid(b, user_vk):
from bigchaindb.models import Transaction
e = election.Election()
# create blocks with transactions
tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx1 = Transaction.create([b.me], [user_vk])
test_block = b.create_block([tx1])
# simulate a federation with four voters
@ -76,12 +83,13 @@ def test_check_for_quorum_valid(b, user_vk):
for key_pair in key_pairs]
# add voters to block and write
test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
test_block.voters = [key_pair[1] for key_pair in key_pairs]
test_block = test_block.sign(b.me_private)
b.write_block(test_block)
# votes for block one
votes = [member.vote(test_block['id'], 'abc', True)
for member in test_federation]
votes = [member.vote(test_block.id, 'abc', True)
for member in test_federation]
# cast votes
r.table('votes').insert(votes, durability='hard').run(b.conn)
@ -90,18 +98,19 @@ def test_check_for_quorum_valid(b, user_vk):
def test_check_requeue_transaction(b, user_vk):
from bigchaindb.models import Transaction
e = election.Election()
# create blocks with transactions
tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx1 = Transaction.create([b.me], [user_vk])
test_block = b.create_block([tx1])
e.requeue_transactions(test_block)
tx_backlog = r.table('backlog').get(tx1['id']).run(b.conn)
tx_backlog.pop('assignee')
tx_backlog.pop('assignment_timestamp')
assert tx_backlog == tx1
backlog_tx = r.table('backlog').get(tx1.id).run(b.conn)
backlog_tx.pop('assignee')
backlog_tx.pop('assignment_timestamp')
assert backlog_tx == tx1.to_dict()
@patch.object(Pipeline, 'start')
@ -114,13 +123,16 @@ def test_start(mock_start):
def test_full_pipeline(b, user_vk):
import random
from bigchaindb.models import Transaction
outpipe = Pipe()
# write two blocks
txs = []
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk], {'msg': random.random()})
tx = tx.sign([b.me_private])
txs.append(tx)
valid_block = b.create_block(txs)
@ -128,8 +140,8 @@ def test_full_pipeline(b, user_vk):
txs = []
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk], {'msg': random.random()})
tx = tx.sign([b.me_private])
txs.append(tx)
invalid_block = b.create_block(txs)
@ -140,8 +152,8 @@ def test_full_pipeline(b, user_vk):
pipeline.start()
time.sleep(1)
# vote one block valid, one invalid
vote_valid = b.vote(valid_block['id'], 'abc', True)
vote_invalid = b.vote(invalid_block['id'], 'abc', False)
vote_valid = b.vote(valid_block.id, 'abc', True)
vote_invalid = b.vote(invalid_block.id, 'abc', False)
r.table('votes').insert(vote_valid, durability='hard').run(b.conn)
r.table('votes').insert(vote_invalid, durability='hard').run(b.conn)
@ -152,6 +164,7 @@ def test_full_pipeline(b, user_vk):
# only transactions from the invalid block should be returned to
# the backlog
assert r.table('backlog').count().run(b.conn) == 100
tx_from_block = set([tx['id'] for tx in invalid_block['block']['transactions']])
# NOTE: I'm still, I'm still tx from the block.
tx_from_block = set([tx.id for tx in invalid_block.transactions])
tx_from_backlog = set([tx['id'] for tx in list(r.table('backlog').run(b.conn))])
assert tx_from_block == tx_from_backlog

View File

@ -9,8 +9,9 @@ import os
def test_get_stale(b, user_vk):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
from bigchaindb.models import Transaction
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
b.write_transaction(tx, durability='hard')
stm = stale.StaleTransactionMonitor(timeout=0.001,
@ -20,22 +21,23 @@ def test_get_stale(b, user_vk):
for _tx in tx_stale:
_tx.pop('assignee')
_tx.pop('assignment_timestamp')
assert tx == _tx
assert tx.to_dict() == _tx
def test_reassign_transactions(b, user_vk):
from bigchaindb.models import Transaction
# test with single node
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
b.write_transaction(tx, durability='hard')
stm = stale.StaleTransactionMonitor(timeout=0.001,
backlog_reassign_delay=0.001)
stm.reassign_transactions(tx)
stm.reassign_transactions(tx.to_dict())
# test with federation
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private])
b.write_transaction(tx, durability='hard')
stm = stale.StaleTransactionMonitor(timeout=0.001,
@ -49,8 +51,8 @@ def test_reassign_transactions(b, user_vk):
assert reassigned_tx['assignee'] != tx['assignee']
# test with node not in federation
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [user_vk])
tx = tx.sign([b.me_private]).to_dict()
tx.update({'assignee': 'lol'})
tx.update({'assignment_timestamp': time.time()})
r.table('backlog').insert(tx, durability='hard').run(b.conn)
@ -61,6 +63,7 @@ def test_reassign_transactions(b, user_vk):
def test_full_pipeline(user_vk):
from bigchaindb.models import Transaction
CONFIG = {
'database': {
'name': 'bigchain_test_{}'.format(os.getpid())
@ -77,13 +80,18 @@ def test_full_pipeline(user_vk):
outpipe = Pipe()
original_txs = {}
original_txc = []
for i in range(100):
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx = b.sign_transaction(tx, b.me_private)
# FIXME Notice the payload. This is only to make sure that the
# transactions hashes are unique. See
# https://github.com/bigchaindb/bigchaindb-common/issues/21
tx = Transaction.create([b.me], [user_vk], payload={'i': i})
tx = tx.sign([b.me_private])
original_txc.append(tx.to_dict())
b.write_transaction(tx)
original_txs[tx['id']] = r.table('backlog').get(tx['id']).run(b.conn)
original_txs[tx.id] = r.table('backlog').get(tx.id).run(b.conn)
assert r.table('backlog').count().run(b.conn) == 100

View File

@ -1,15 +1,14 @@
from unittest.mock import patch
import rethinkdb as r
from multipipes import Pipe, Pipeline
from bigchaindb import util
from bigchaindb import crypto
def dummy_tx(b):
tx = b.create_transaction(b.me, b.me, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
return tx_signed
from bigchaindb.models import Transaction
tx = Transaction.create([b.me], [b.me])
tx = tx.sign([b.me_private])
return tx
def dummy_block(b):
@ -18,34 +17,40 @@ def dummy_block(b):
def test_vote_creation_valid(b):
from bigchaindb_common import crypto
from bigchaindb_common.util import serialize
# create valid block
block = dummy_block(b)
# retrieve vote
vote = b.vote(block['id'], 'abc', True)
vote = b.vote(block.id, 'abc', True)
# assert vote is correct
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['voting_for_block'] == block.id
assert vote['vote']['previous_block'] == 'abc'
assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']),
assert crypto.VerifyingKey(b.me).verify(serialize(vote['vote']),
vote['signature']) is True
def test_vote_creation_invalid(b):
from bigchaindb_common import crypto
from bigchaindb_common.util import serialize
# create valid block
block = dummy_block(b)
# retrieve vote
vote = b.vote(block['id'], 'abc', False)
vote = b.vote(block.id, 'abc', False)
# assert vote is correct
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['voting_for_block'] == block.id
assert vote['vote']['previous_block'] == 'abc'
assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']),
assert crypto.VerifyingKey(b.me).verify(serialize(vote['vote']),
vote['signature']) is True
@ -55,7 +60,7 @@ def test_vote_ungroup_returns_a_set_of_results(b):
b.create_genesis_block()
block = dummy_block(b)
vote_obj = vote.Vote()
txs = list(vote_obj.ungroup(block, True))
txs = list(vote_obj.ungroup(block.id, block.transactions))
assert len(txs) == 10
@ -68,19 +73,53 @@ def test_vote_validate_block(b):
block = b.create_block([tx])
vote_obj = vote.Vote()
validation = vote_obj.validate_block(block)
assert validation == (block, True)
validation = vote_obj.validate_block(block.to_dict())
assert validation[0] == block.id
for tx1, tx2 in zip(validation[1], block.transactions):
assert tx1 == tx2
block = b.create_block([tx])
block['block']['id'] = 'this-is-not-a-valid-hash'
# NOTE: Setting a blocks signature to `None` invalidates it.
block.signature = None
vote_obj = vote.Vote()
validation = vote_obj.validate_block(block)
assert validation == (block, False)
validation = vote_obj.validate_block(block.to_dict())
assert validation[0] == block.id
for tx1, tx2 in zip(validation[1], [vote_obj.invalid_dummy_tx]):
assert tx1 == tx2
def test_validate_block_with_invalid_id(b):
from bigchaindb.pipelines import vote
b.create_genesis_block()
tx = dummy_tx(b)
block = b.create_block([tx]).to_dict()
block['id'] = 'an invalid id'
vote_obj = vote.Vote()
block_id, invalid_dummy_tx = vote_obj.validate_block(block)
assert block_id == block['id']
assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx]
def test_validate_block_with_invalid_signature(b):
from bigchaindb.pipelines import vote
b.create_genesis_block()
tx = dummy_tx(b)
block = b.create_block([tx]).to_dict()
block['signature'] = 'an invalid signature'
vote_obj = vote.Vote()
block_id, invalid_dummy_tx = vote_obj.validate_block(block)
assert block_id == block['id']
assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx]
def test_vote_validate_transaction(b):
from bigchaindb.pipelines import vote
from bigchaindb.models import Transaction
b.create_genesis_block()
tx = dummy_tx(b)
@ -88,7 +127,8 @@ def test_vote_validate_transaction(b):
validation = vote_obj.validate_tx(tx, 123, 1)
assert validation == (True, 123, 1)
tx['id'] = 'a' * 64
# NOTE: Submit unsigned transaction to `validate_tx` yields `False`.
tx = Transaction.create([b.me], [b.me])
validation = vote_obj.validate_tx(tx, 456, 10)
assert validation == (False, 456, 10)
@ -102,32 +142,34 @@ def test_vote_accumulates_transactions(b):
for _ in range(10):
tx = dummy_tx(b)
tx = tx
validation = vote_obj.validate_tx(tx, 123, 1)
assert validation == (True, 123, 1)
tx['id'] = 'a' * 64
tx.fulfillments[0].fulfillment.signature = None
validation = vote_obj.validate_tx(tx, 456, 10)
assert validation == (False, 456, 10)
def test_valid_block_voting_sequential(b, monkeypatch):
from bigchaindb_common import crypto, util
from bigchaindb.pipelines import vote
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_obj = vote.Vote()
block = dummy_block(b)
for tx, block_id, num_tx in vote_obj.ungroup(block, True):
for tx, block_id, num_tx in vote_obj.ungroup(block.id, block.transactions):
last_vote = vote_obj.vote(*vote_obj.validate_tx(tx, block_id, num_tx))
vote_obj.write_vote(last_vote)
vote_rs = r.table('votes').get_all([block['id'], b.me],
vote_rs = r.table('votes').get_all([block.id, b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
assert vote_doc['vote'] == {'voting_for_block': block.id,
'previous_block': genesis.id,
'is_block_valid': True,
'invalid_reason': None,
'timestamp': '1'}
@ -138,29 +180,30 @@ def test_valid_block_voting_sequential(b, monkeypatch):
def test_valid_block_voting_multiprocessing(b, monkeypatch):
from bigchaindb_common import crypto, util
from bigchaindb.pipelines import vote
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
block = dummy_block(b)
inpipe.put(block)
inpipe.put(block.to_dict())
vote_pipeline.start()
vote_out = outpipe.get()
vote_pipeline.terminate()
vote_rs = r.table('votes').get_all([block['id'], b.me],
vote_rs = r.table('votes').get_all([block.id, b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
assert vote_doc['vote'] == {'voting_for_block': block.id,
'previous_block': genesis.id,
'is_block_valid': True,
'invalid_reason': None,
'timestamp': '1'}
@ -171,17 +214,19 @@ def test_valid_block_voting_multiprocessing(b, monkeypatch):
def test_valid_block_voting_with_create_transaction(b, monkeypatch):
from bigchaindb_common import crypto, util
from bigchaindb.models import Transaction
from bigchaindb.pipelines import vote
genesis = b.create_genesis_block()
# create a `CREATE` transaction
test_user_priv, test_user_pub = crypto.generate_key_pair()
tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [test_user_pub])
tx = tx.sign([b.me_private])
monkeypatch.setattr(util, 'timestamp', lambda: '1')
block = b.create_block([tx_signed])
monkeypatch.setattr('time.time', lambda: 1)
block = b.create_block([tx])
inpipe = Pipe()
outpipe = Pipe()
@ -189,17 +234,17 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch):
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
inpipe.put(block)
inpipe.put(block.to_dict())
vote_pipeline.start()
vote_out = outpipe.get()
vote_pipeline.terminate()
vote_rs = r.table('votes').get_all([block['id'], b.me],
vote_rs = r.table('votes').get_all([block.id, b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
assert vote_doc['vote'] == {'voting_for_block': block.id,
'previous_block': genesis.id,
'is_block_valid': True,
'invalid_reason': None,
'timestamp': '1'}
@ -210,27 +255,28 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch):
def test_valid_block_voting_with_transfer_transactions(monkeypatch, b):
from bigchaindb_common import crypto, util
from bigchaindb.models import Transaction
from bigchaindb.pipelines import vote
genesis = b.create_genesis_block()
# create a `CREATE` transaction
test_user_priv, test_user_pub = crypto.generate_key_pair()
tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
tx = Transaction.create([b.me], [test_user_pub])
tx = tx.sign([b.me_private])
monkeypatch.setattr(util, 'timestamp', lambda: '1')
block = b.create_block([tx_signed])
monkeypatch.setattr('time.time', lambda: 1)
block = b.create_block([tx])
b.write_block(block, durability='hard')
# create a `TRANSFER` transaction
test_user2_priv, test_user2_pub = crypto.generate_key_pair()
tx2 = b.create_transaction(test_user_pub, test_user2_pub,
{'txid': tx['id'], 'cid': 0}, 'TRANSFER')
tx2_signed = b.sign_transaction(tx2, test_user_priv)
tx2 = Transaction.transfer(tx.to_inputs(), [test_user2_pub])
tx2 = tx2.sign([test_user_priv])
monkeypatch.setattr(util, 'timestamp', lambda: '2')
block2 = b.create_block([tx2_signed])
monkeypatch.setattr('time.time', lambda: 2)
block2 = b.create_block([tx2])
b.write_block(block2, durability='hard')
inpipe = Pipe()
@ -239,19 +285,19 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b):
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
inpipe.put(block)
inpipe.put(block2)
inpipe.put(block.to_dict())
inpipe.put(block2.to_dict())
vote_pipeline.start()
vote_out = outpipe.get()
vote2_out = outpipe.get()
vote_pipeline.terminate()
vote_rs = r.table('votes').get_all([block['id'], b.me],
vote_rs = r.table('votes').get_all([block.id, b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
assert vote_doc['vote'] == {'voting_for_block': block.id,
'previous_block': genesis.id,
'is_block_valid': True,
'invalid_reason': None,
'timestamp': '2'}
@ -260,12 +306,12 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b):
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']),
vote_doc['signature']) is True
vote2_rs = r.table('votes').get_all([block2['id'], b.me],
vote2_rs = r.table('votes').get_all([block2.id, b.me],
index='block_and_voter').run(b.conn)
vote2_doc = vote2_rs.next()
assert vote2_out['vote'] == vote2_doc['vote']
assert vote2_doc['vote'] == {'voting_for_block': block2['id'],
'previous_block': block['id'],
assert vote2_doc['vote'] == {'voting_for_block': block2.id,
'previous_block': block.id,
'is_block_valid': True,
'invalid_reason': None,
'timestamp': '2'}
@ -275,19 +321,61 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b):
vote2_doc['signature']) is True
def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk):
def test_unsigned_tx_in_block_voting(monkeypatch, b, user_vk):
from bigchaindb_common import crypto, util
from bigchaindb.models import Transaction
from bigchaindb.pipelines import vote
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
block = dummy_block(b)
block['block']['transactions'][0]['id'] = 'abc'
# NOTE: `tx` is invalid, because it wasn't signed.
tx = Transaction.create([b.me], [b.me])
block = b.create_block([tx])
inpipe.put(block.to_dict())
vote_pipeline.start()
vote_out = outpipe.get()
vote_pipeline.terminate()
vote_rs = r.table('votes').get_all([block.id, b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block.id,
'previous_block': genesis.id,
'is_block_valid': False,
'invalid_reason': None,
'timestamp': '1'}
assert vote_doc['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']),
vote_doc['signature']) is True
def test_invalid_id_tx_in_block_voting(monkeypatch, b, user_vk):
from bigchaindb_common import crypto, util
from bigchaindb.models import Transaction
from bigchaindb.pipelines import vote
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
# NOTE: `tx` is invalid, because its id is not corresponding to its content
tx = Transaction.create([b.me], [b.me])
tx = tx.sign([b.me_private])
block = b.create_block([tx]).to_dict()
block['block']['transactions'][0]['id'] = 'an invalid tx id'
inpipe.put(block)
vote_pipeline.start()
@ -299,7 +387,46 @@ def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk):
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
'previous_block': genesis.id,
'is_block_valid': False,
'invalid_reason': None,
'timestamp': '1'}
assert vote_doc['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']),
vote_doc['signature']) is True
def test_invalid_content_in_tx_in_block_voting(monkeypatch, b, user_vk):
from bigchaindb_common import crypto, util
from bigchaindb.models import Transaction
from bigchaindb.pipelines import vote
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
# NOTE: `tx` is invalid, because its content is not corresponding to its id
tx = Transaction.create([b.me], [b.me])
tx = tx.sign([b.me_private])
block = b.create_block([tx]).to_dict()
block['block']['transactions'][0]['id'] = 'an invalid tx id'
inpipe.put(block)
vote_pipeline.start()
vote_out = outpipe.get()
vote_pipeline.terminate()
vote_rs = r.table('votes').get_all([block['id'], b.me],
index='block_and_voter').run(b.conn)
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis.id,
'is_block_valid': False,
'invalid_reason': None,
'timestamp': '1'}
@ -310,17 +437,18 @@ def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk):
def test_invalid_block_voting(monkeypatch, b, user_vk):
from bigchaindb_common import crypto, util
from bigchaindb.pipelines import vote
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
genesis = b.create_genesis_block()
vote_pipeline = vote.create_pipeline()
vote_pipeline.setup(indata=inpipe, outdata=outpipe)
block = dummy_block(b)
block = dummy_block(b).to_dict()
block['block']['id'] = 'this-is-not-a-valid-hash'
inpipe.put(block)
@ -333,7 +461,7 @@ def test_invalid_block_voting(monkeypatch, b, user_vk):
vote_doc = vote_rs.next()
assert vote_out['vote'] == vote_doc['vote']
assert vote_doc['vote'] == {'voting_for_block': block['id'],
'previous_block': genesis['id'],
'previous_block': genesis.id,
'is_block_valid': False,
'invalid_reason': None,
'timestamp': '1'}
@ -348,13 +476,15 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b):
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
b.create_genesis_block()
# insert blocks in the database while the voter process is not listening
# (these blocks won't appear in the changefeed)
monkeypatch.setattr('time.time', lambda: 2)
block_1 = dummy_block(b)
b.write_block(block_1, durability='hard')
monkeypatch.setattr('time.time', lambda: 3)
block_2 = dummy_block(b)
b.write_block(block_2, durability='hard')
@ -368,6 +498,7 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b):
outpipe.get()
# create a new block that will appear in the changefeed
monkeypatch.setattr('time.time', lambda: 4)
block_3 = dummy_block(b)
b.write_block(block_3, durability='hard')
@ -376,7 +507,7 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b):
vote_pipeline.terminate()
# retrive blocks from bigchain
# retrieve blocks from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
@ -398,14 +529,14 @@ def test_voter_chains_blocks_with_the_previous_ones(monkeypatch, b):
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
b.create_genesis_block()
monkeypatch.setattr(util, 'timestamp', lambda: '2')
monkeypatch.setattr('time.time', lambda: 2)
block_1 = dummy_block(b)
b.write_block(block_1, durability='hard')
monkeypatch.setattr(util, 'timestamp', lambda: '3')
monkeypatch.setattr('time.time', lambda: 3)
block_2 = dummy_block(b)
b.write_block(block_2, durability='hard')
@ -437,11 +568,12 @@ def test_voter_checks_for_previous_vote(monkeypatch, b):
inpipe = Pipe()
outpipe = Pipe()
monkeypatch.setattr(util, 'timestamp', lambda: '1')
monkeypatch.setattr('time.time', lambda: 1)
b.create_genesis_block()
monkeypatch.setattr('time.time', lambda: 2)
block_1 = dummy_block(b)
inpipe.put(block_1)
inpipe.put(block_1.to_dict())
assert r.table('votes').count().run(b.conn) == 0
@ -453,10 +585,12 @@ def test_voter_checks_for_previous_vote(monkeypatch, b):
outpipe.get()
# queue block for voting AGAIN
inpipe.put(block_1)
monkeypatch.setattr('time.time', lambda: 3)
inpipe.put(block_1.to_dict())
# queue another block
inpipe.put(dummy_block(b))
monkeypatch.setattr('time.time', lambda: 4)
inpipe.put(dummy_block(b).to_dict())
# wait for the result of the new block
outpipe.get()

View File

@ -1,74 +0,0 @@
import pytest
@pytest.fixture
def client():
from bigchaindb.client import temp_client
return temp_client()
@pytest.fixture
def mock_requests_post(monkeypatch):
class MockResponse:
def __init__(self, json):
self._json = json
def json(self):
return self._json
def mockreturn(*args, **kwargs):
return MockResponse(kwargs.get('json'))
monkeypatch.setattr('requests.post', mockreturn)
@pytest.fixture
def mock_bigchaindb_sign(monkeypatch):
def mockreturn(transaction, private_key, bigchain):
return transaction
monkeypatch.setattr('bigchaindb.util.sign_tx', mockreturn)
def test_temp_client_returns_a_temp_client():
from bigchaindb.client import temp_client
client = temp_client()
assert client.public_key
assert client.private_key
@pytest.mark.usefixtures('restore_config')
def test_client_can_create_assets(mock_requests_post, client):
from bigchaindb import util
tx = client.create()
# XXX: `CREATE` operations require the node that receives the transaction to modify the data in
# the transaction itself.
# `owner_before` will be overwritten with the public key of the node in the federation
# that will create the real transaction. `signature` will be overwritten with the new signature.
# Note that this scenario is ignored by this test.
assert tx['transaction']['fulfillments'][0]['owners_before'][0] == client.public_key
assert tx['transaction']['conditions'][0]['owners_after'][0] == client.public_key
assert tx['transaction']['fulfillments'][0]['input'] is None
assert util.validate_fulfillments(tx)
def test_client_can_transfer_assets(mock_requests_post, mock_bigchaindb_sign, client):
tx = client.transfer(client.public_key, 123)
assert tx['transaction']['fulfillments'][0]['owners_before'][0] == client.public_key
assert tx['transaction']['conditions'][0]['owners_after'][0] == client.public_key
assert tx['transaction']['fulfillments'][0]['input'] == 123
@pytest.mark.parametrize('pubkey,privkey', (
(None, None), ('pubkey', None), (None, 'privkey'),
))
def test_init_client_with_incomplete_keypair(pubkey, privkey, monkeypatch):
from bigchaindb import config
from bigchaindb.client import Client
from bigchaindb.exceptions import KeypairNotFoundException
keypair = {'public': pubkey, 'private': privkey}
monkeypatch.setitem(config, 'keypair', keypair)
with pytest.raises(KeypairNotFoundException):
Client()

View File

@ -22,7 +22,7 @@ def mock_write_config(monkeypatch):
@pytest.fixture
def mock_db_init_with_existing_db(monkeypatch):
from bigchaindb import db
from bigchaindb.exceptions import DatabaseAlreadyExists
from bigchaindb_common.exceptions import DatabaseAlreadyExists
def mockreturn():
raise DatabaseAlreadyExists
@ -48,7 +48,7 @@ def mock_rethink_db_drop(monkeypatch):
@pytest.fixture
def mock_generate_key_pair(monkeypatch):
monkeypatch.setattr('bigchaindb.crypto.generate_key_pair', lambda: ('privkey', 'pubkey'))
monkeypatch.setattr('bigchaindb_common.crypto.generate_key_pair', lambda: ('privkey', 'pubkey'))
@pytest.fixture
@ -283,15 +283,15 @@ def test_start_rethinkdb_returns_a_process_when_successful(mock_popen):
@patch('subprocess.Popen')
def test_start_rethinkdb_exits_when_cannot_start(mock_popen):
from bigchaindb import exceptions
from bigchaindb_common import exceptions
from bigchaindb.commands import utils
mock_popen.return_value = Mock(stdout=['Nopety nope'])
with pytest.raises(exceptions.StartupError):
utils.start_rethinkdb()
@patch('bigchaindb.crypto.generate_key_pair', return_value=('private_key',
'public_key'))
@patch('bigchaindb_common.crypto.generate_key_pair',
return_value=('private_key', 'public_key'))
def test_allow_temp_keypair_generates_one_on_the_fly(mock_gen_keypair,
mock_processes_start,
mock_db_init_with_existing_db):
@ -307,8 +307,8 @@ def test_allow_temp_keypair_generates_one_on_the_fly(mock_gen_keypair,
assert bigchaindb.config['keypair']['public'] == 'public_key'
@patch('bigchaindb.crypto.generate_key_pair', return_value=('private_key',
'public_key'))
@patch('bigchaindb_common.crypto.generate_key_pair',
return_value=('private_key', 'public_key'))
def test_allow_temp_keypair_doesnt_override_if_keypair_found(mock_gen_keypair,
mock_processes_start,
mock_db_init_with_existing_db):
@ -415,3 +415,81 @@ def test_set_replicas_raises_exception(mock_log, monkeypatch, b):
run_set_replicas(args)
assert mock_log.called
@patch('argparse.ArgumentParser.parse_args')
@patch('bigchaindb.commands.utils.base_parser')
@patch('bigchaindb.commands.utils.start')
def test_calling_main(start_mock, base_parser_mock, parse_args_mock,
monkeypatch):
from bigchaindb.commands.bigchain import main
argparser_mock = Mock()
parser = Mock()
subparsers = Mock()
subsubparsers = Mock()
subparsers.add_parser.return_value = subsubparsers
parser.add_subparsers.return_value = subparsers
argparser_mock.return_value = parser
monkeypatch.setattr('argparse.ArgumentParser', argparser_mock)
main()
assert argparser_mock.called is True
assert parser.add_argument.called is True
parser.add_argument.assert_any_call('--dev-start-rethinkdb',
dest='start_rethinkdb',
action='store_true',
help='Run RethinkDB on start')
parser.add_subparsers.assert_called_with(title='Commands',
dest='command')
subparsers.add_parser.assert_any_call('configure',
help='Prepare the config file '
'and create the node keypair')
subparsers.add_parser.assert_any_call('show-config',
help='Show the current '
'configuration')
subparsers.add_parserassert_any_call('export-my-pubkey',
help="Export this node's public "
'key')
subparsers.add_parser.assert_any_call('init', help='Init the database')
subparsers.add_parser.assert_any_call('drop', help='Drop the database')
subparsers.add_parser.assert_any_call('start', help='Start BigchainDB')
subparsers.add_parser.assert_any_call('set-shards',
help='Configure number of shards')
subsubparsers.add_argument.assert_any_call('num_shards',
metavar='num_shards',
type=int, default=1,
help='Number of shards')
subparsers.add_parser.assert_any_call('set-replicas',
help='Configure number of replicas')
subsubparsers.add_argument.assert_any_call('num_replicas',
metavar='num_replicas',
type=int, default=1,
help='Number of replicas (i.e. '
'the replication factor)')
subparsers.add_parser.assert_any_call('load',
help='Write transactions to the '
'backlog')
subsubparsers.add_argument.assert_any_call('-m', '--multiprocess',
nargs='?', type=int,
default=False,
help='Spawn multiple processes '
'to run the command, if no '
'value is provided, the number '
'of processes is equal to the '
'number of cores of the host '
'machine')
subsubparsers.add_argument.assert_any_call('-c', '--count',
default=0,
type=int,
help='Number of transactions '
'to push. If the parameter -m '
'is set, the count is '
'distributed equally to all '
'the processes')
assert start_mock.called is True

View File

@ -4,7 +4,6 @@ from unittest.mock import mock_open, patch
import pytest
import bigchaindb
from bigchaindb import exceptions
ORIGINAL_CONFIG = copy.deepcopy(bigchaindb._config)
@ -43,6 +42,7 @@ def test_bigchain_instance_is_initialized_when_conf_provided():
def test_bigchain_instance_raises_when_not_configured(monkeypatch):
from bigchaindb import config_utils
from bigchaindb_common import exceptions
assert 'CONFIGURED' not in bigchaindb.config
# We need to disable ``bigchaindb.config_utils.autoconfigure`` to avoid reading
@ -53,36 +53,6 @@ def test_bigchain_instance_raises_when_not_configured(monkeypatch):
bigchaindb.Bigchain()
def test_load_consensus_plugin_loads_default_rules_without_name():
from bigchaindb import config_utils
from bigchaindb.consensus import BaseConsensusRules
assert config_utils.load_consensus_plugin() == BaseConsensusRules
def test_load_consensus_plugin_raises_with_unknown_name():
from pkg_resources import ResolutionError
from bigchaindb import config_utils
with pytest.raises(ResolutionError):
config_utils.load_consensus_plugin('bogus')
def test_load_consensus_plugin_raises_with_invalid_subclass(monkeypatch):
# Monkeypatch entry_point.load to return something other than a
# ConsensusRules instance
from bigchaindb import config_utils
import time
monkeypatch.setattr(config_utils,
'iter_entry_points',
lambda *args: [type('entry_point', (object), {'load': lambda: object})])
with pytest.raises(TypeError):
# Since the function is decorated with `lru_cache`, we need to
# "miss" the cache using a name that has not been used previously
config_utils.load_consensus_plugin(str(time.time()))
def test_map_leafs_iterator():
from bigchaindb import config_utils
@ -180,7 +150,6 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch):
'rate': 0.01,
},
'api_endpoint': 'http://localhost:9984/api/v1',
'consensus_plugin': 'default',
'backlog_reassign_delay': 5
}
@ -232,8 +201,9 @@ def test_file_config():
def test_invalid_file_config():
from bigchaindb.config_utils import file_config, CONFIG_DEFAULT_PATH
with patch('builtins.open', mock_open(read_data='{_INVALID_JSON_}')) as m:
from bigchaindb.config_utils import file_config
from bigchaindb_common import exceptions
with patch('builtins.open', mock_open(read_data='{_INVALID_JSON_}')):
with pytest.raises(exceptions.ConfigurationError):
file_config()

View File

@ -1,15 +1,2 @@
import pytest
class TestBaseConsensusRules(object):
def test_validate_transaction(self):
from bigchaindb.consensus import BaseConsensusRules
transaction = {
'transaction': {
'operation': None,
'fulfillments': None,
},
}
with pytest.raises(ValueError):
BaseConsensusRules.validate_transaction(None, transaction)
pass

View File

@ -19,7 +19,6 @@ def config(request, monkeypatch):
},
'keyring': [],
'CONFIGURED': True,
'consensus_plugin': 'default',
'backlog_reassign_delay': 30
}
@ -52,7 +51,6 @@ def test_bigchain_class_initialization_with_parameters(config):
'public_key': 'white',
'private_key': 'black',
'keyring': ['key_one', 'key_two'],
'consensus_plugin': 'default'
}
bigchain = Bigchain(**init_kwargs)
assert bigchain.host == init_kwargs['host']
@ -71,9 +69,9 @@ def test_get_blocks_status_containing_tx(monkeypatch):
{'id': 1}, {'id': 2}
]
monkeypatch.setattr(
Bigchain, 'search_block_election_on_index', lambda x, y, z: blocks)
Bigchain, 'search_block_election_on_index', lambda x, y: blocks)
monkeypatch.setattr(
Bigchain, 'block_election_status', lambda x, y: Bigchain.BLOCK_VALID)
Bigchain, 'block_election_status', lambda x, y, z: Bigchain.BLOCK_VALID)
bigchain = Bigchain(public_key='pubkey', private_key='privkey')
with pytest.raises(Exception):
bigchain.get_blocks_status_containing_tx('txid')
@ -82,7 +80,7 @@ def test_get_blocks_status_containing_tx(monkeypatch):
def test_has_previous_vote(monkeypatch):
from bigchaindb.core import Bigchain
monkeypatch.setattr(
'bigchaindb.util.verify_vote_signature', lambda block, vote: False)
'bigchaindb.util.verify_vote_signature', lambda voters, vote: False)
bigchain = Bigchain(public_key='pubkey', private_key='privkey')
block = {'votes': ({'node_pubkey': 'pubkey'},)}
with pytest.raises(Exception):
@ -98,6 +96,7 @@ def test_transaction_exists(monkeypatch, items, exists):
assert bigchain.transaction_exists('txid') is exists
@pytest.mark.skip(reason='until tim decides')
def test_write_transaction_no_sideffects(b):
from rethinkdb.errors import ReqlOpFailedError
transaction = {'id': 'abc'}

178
tests/test_models.py Normal file
View File

@ -0,0 +1,178 @@
from pytest import raises
class TestTransactionModel(object):
def test_validating_an_invalid_transaction(self, b):
from bigchaindb.models import Transaction
tx = Transaction.create([b.me], [b.me])
tx.operation = 'something invalid'
with raises(TypeError):
tx.validate(b)
tx.operation = 'CREATE'
tx.fulfillments = []
with raises(ValueError):
tx.validate(b)
class TestBlockModel(object):
def test_block_initialization(self, monkeypatch):
from bigchaindb.models import Block
monkeypatch.setattr('time.time', lambda: 1)
block = Block()
assert block.transactions == []
assert block.voters == []
assert block.timestamp == '1'
assert block.node_pubkey is None
assert block.signature is None
with raises(TypeError):
Block('not a list or None')
with raises(TypeError):
Block(None, 'valid node_pubkey', 'valid timestamp',
'not a list or None')
def test_block_serialization(self, b):
from bigchaindb_common.crypto import hash_data
from bigchaindb_common.util import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [b.me])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected_block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': b.me,
'voters': voters,
}
expected = {
'id': hash_data(serialize(expected_block)),
'block': expected_block,
'signature': None,
}
block = Block(transactions, b.me, timestamp, voters)
assert block.to_dict() == expected
def test_block_invalid_serializaton(self):
from bigchaindb_common.exceptions import OperationError
from bigchaindb.models import Block
block = Block([])
with raises(OperationError):
block.to_dict()
def test_block_deserialization(self, b):
from bigchaindb_common.crypto import hash_data
from bigchaindb_common.util import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [b.me])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected = Block(transactions, b.me, timestamp, voters)
block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': b.me,
'voters': voters,
}
block_body = {
'id': hash_data(serialize(block)),
'block': block,
'signature': None,
}
assert expected == Block.from_dict(block_body)
def test_block_invalid_id_deserialization(self, b):
from bigchaindb_common.exceptions import InvalidHash
from bigchaindb.models import Block
block = {
'id': 'an invalid id',
'block': {
'node_pubkey': b.me,
}
}
with raises(InvalidHash):
Block.from_dict(block)
def test_block_invalid_signature_deserialization(self, b):
from bigchaindb_common.crypto import hash_data
from bigchaindb_common.exceptions import InvalidSignature
from bigchaindb_common.util import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [b.me])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': b.me,
'voters': voters,
}
block_body = {
'id': hash_data(serialize(block)),
'block': block,
'signature': 'an invalid signature',
}
with raises(InvalidSignature):
Block.from_dict(block_body)
def test_compare_blocks(self, b):
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [b.me])]
assert Block() != 'invalid comparison'
assert Block(transactions) == Block(transactions)
def test_sign_block(self, b):
from bigchaindb_common.crypto import SigningKey, VerifyingKey
from bigchaindb_common.util import gen_timestamp, serialize
from bigchaindb.models import Block, Transaction
transactions = [Transaction.create([b.me], [b.me])]
timestamp = gen_timestamp()
voters = ['Qaaa', 'Qbbb']
expected_block = {
'timestamp': timestamp,
'transactions': [tx.to_dict() for tx in transactions],
'node_pubkey': b.me,
'voters': voters,
}
expected_block_serialized = serialize(expected_block)
expected = SigningKey(b.me_private).sign(expected_block_serialized)
block = Block(transactions, b.me, timestamp, voters)
block = block.sign(b.me_private)
assert block.signature == expected
verifying_key = VerifyingKey(b.me)
assert verifying_key.verify(expected_block_serialized, block.signature)
def test_validate_already_voted_on_block(self, b, monkeypatch):
from unittest.mock import Mock
from bigchaindb.models import Transaction
tx = Transaction.create([b.me], [b.me])
block = b.create_block([tx])
has_previous_vote = Mock()
has_previous_vote.return_value = True
monkeypatch.setattr(b, 'has_previous_vote', has_previous_vote)
assert block == block.validate(b)
assert has_previous_vote.called is True

View File

@ -3,8 +3,6 @@ from unittest.mock import patch, call
import pytest
from cryptoconditions import ThresholdSha256Fulfillment
@pytest.fixture
def mock_queue(monkeypatch):
@ -29,17 +27,6 @@ def mock_queue(monkeypatch):
return mockqueue
def test_transform_create(b, user_sk, user_vk):
from bigchaindb import util
tx = util.create_tx(user_vk, user_vk, None, 'CREATE')
tx = util.transform_create(tx)
tx = util.sign_tx(tx, b.me_private)
assert tx['transaction']['fulfillments'][0]['owners_before'][0] == b.me
assert tx['transaction']['conditions'][0]['owners_after'][0] == user_vk
assert util.validate_fulfillments(tx)
def test_empty_pool_is_populated_with_instances(mock_queue):
from bigchaindb import util
@ -146,72 +133,6 @@ def test_process_group_instantiates_and_start_processes(mock_process):
process.start.assert_called_with()
def test_create_tx_with_empty_inputs():
from bigchaindb.util import create_tx
tx = create_tx(None, None, [], None)
assert 'id' in tx
assert 'transaction' in tx
assert 'version' in tx['transaction']
assert 'fulfillments' in tx['transaction']
assert 'conditions' in tx['transaction']
assert 'operation' in tx['transaction']
assert 'timestamp' in tx['transaction']
assert 'data' in tx['transaction']
assert len(tx['transaction']['fulfillments']) == 1
assert tx['transaction']['fulfillments'][0] == {
'owners_before': [], 'input': None, 'fulfillment': None, 'fid': 0}
def test_fulfill_threshold_signature_fulfillment_pubkey_notfound(monkeypatch):
from bigchaindb.exceptions import KeypairMismatchException
from bigchaindb.util import fulfill_threshold_signature_fulfillment
monkeypatch.setattr(
ThresholdSha256Fulfillment,
'get_subcondition_from_vk',
lambda x, y: []
)
fulfillment = {'owners_before': (None,)}
parsed_fulfillment = ThresholdSha256Fulfillment()
with pytest.raises(KeypairMismatchException):
fulfill_threshold_signature_fulfillment(
fulfillment, parsed_fulfillment, None, None)
def test_fulfill_threshold_signature_fulfillment_wrong_privkeys(monkeypatch):
from bigchaindb.exceptions import KeypairMismatchException
from bigchaindb.util import fulfill_threshold_signature_fulfillment
monkeypatch.setattr(
ThresholdSha256Fulfillment,
'get_subcondition_from_vk',
lambda x, y: (None,)
)
fulfillment = {'owners_before': ('alice-pub-key',)}
parsed_fulfillment = ThresholdSha256Fulfillment()
with pytest.raises(KeypairMismatchException):
fulfill_threshold_signature_fulfillment(
fulfillment, parsed_fulfillment, None, {})
def test_check_hash_and_signature_invalid_hash(monkeypatch):
from bigchaindb.exceptions import InvalidHash
from bigchaindb.util import check_hash_and_signature
transaction = {'id': 'txid'}
monkeypatch.setattr('bigchaindb.util.get_hash_data', lambda tx: 'txhash')
with pytest.raises(InvalidHash):
check_hash_and_signature(transaction)
def test_check_hash_and_signature_invalid_signature(monkeypatch):
from bigchaindb.exceptions import InvalidSignature
from bigchaindb.util import check_hash_and_signature
transaction = {'id': 'txid'}
monkeypatch.setattr('bigchaindb.util.get_hash_data', lambda tx: 'txid')
monkeypatch.setattr(
'bigchaindb.util.validate_fulfillments', lambda tx: False)
with pytest.raises(InvalidSignature):
check_hash_and_signature(transaction)
def test_is_genesis_block_returns_true_if_genesis(b):
from bigchaindb.util import is_genesis_block
genesis_block = b.prepare_genesis_block()

View File

@ -30,6 +30,8 @@ def app(request, node_config):
@pytest.fixture
# NOTE: In order to have a database setup as well as the `input` fixture,
# we have to proxy `db.conftest.input` here.
# TODO: If possible replace this function with something nicer.
def inputs(user_vk):
conftest.inputs(user_vk)

View File

@ -1,8 +1,7 @@
import json
import pytest
from bigchaindb import crypto
from bigchaindb import util
from bigchaindb_common import crypto
TX_ENDPOINT = '/api/v1/transactions/'
@ -11,14 +10,10 @@ TX_ENDPOINT = '/api/v1/transactions/'
@pytest.mark.usefixtures('inputs')
def test_get_transaction_endpoint(b, client, user_vk):
input_tx = b.get_owned_ids(user_vk).pop()
tx = b.get_transaction(input_tx['txid'])
res = client.get(TX_ENDPOINT + input_tx['txid'])
tx = b.get_transaction(input_tx.txid)
res = client.get(TX_ENDPOINT + tx.id)
assert tx.to_dict() == res.json
assert res.status_code == 200
assert tx == res.json
res = client.get(TX_ENDPOINT + input_tx['txid'] + '/')
assert res.status_code == 200
assert tx == res.json
@pytest.mark.usefixtures('inputs')
@ -38,57 +33,82 @@ def test_api_endpoint_shows_basic_info(client):
def test_post_create_transaction_endpoint(b, client):
sk, vk = crypto.generate_key_pair()
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
tx = util.create_and_sign_tx(sk, vk, vk, None, 'CREATE')
tx = Transaction.create([user_pub], [user_pub])
tx = tx.sign([user_priv])
res = client.post(TX_ENDPOINT, data=json.dumps(tx.to_dict()))
assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == user_pub
assert res.json['transaction']['conditions'][0]['owners_after'][0] == user_pub
def test_post_create_transaction_with_invalid_id(b, client):
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
tx = Transaction.create([user_pub], [user_pub])
tx = tx.sign([user_priv]).to_dict()
tx['id'] = 'invalid id'
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == b.me
assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk
assert res.status_code == 400
def test_post_create_transaction_endpoint_without_trailing_slash(b, client):
sk, vk = crypto.generate_key_pair()
def test_post_create_transaction_with_invalid_signature(b, client):
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
tx = util.create_and_sign_tx(sk, vk, vk, None, 'CREATE')
tx = Transaction.create([user_pub], [user_pub])
tx = tx.sign([user_priv]).to_dict()
tx['transaction']['fulfillments'][0]['fulfillment'] = 'invalid signature'
res = client.post(TX_ENDPOINT[:-1], data=json.dumps(tx))
assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == b.me
assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
assert res.status_code == 400
@pytest.mark.usefixtures('inputs')
def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk):
sk, vk = crypto.generate_key_pair()
input_valid = b.get_owned_ids(user_vk).pop()
from bigchaindb.models import Transaction
transfer = util.create_and_sign_tx(user_sk, user_vk, vk, input_valid)
res = client.post(TX_ENDPOINT, data=json.dumps(transfer))
user_priv, user_pub = crypto.generate_key_pair()
input_valid = b.get_owned_ids(user_vk).pop()
create_tx = b.get_transaction(input_valid.txid)
transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub])
transfer_tx = transfer_tx.sign([user_sk])
res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict()))
assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == user_vk
assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk
assert res.json['transaction']['conditions'][0]['owners_after'][0] == user_pub
@pytest.mark.usefixtures('inputs')
def test_post_invalid_transfer_transaction_returns_400(b, client, user_vk, user_sk):
sk, vk = crypto.generate_key_pair()
input_valid = b.get_owned_ids(user_vk).pop()
transfer = b.create_transaction(user_vk, vk, input_valid, 'TRANSFER')
# transfer is not signed
res = client.post(TX_ENDPOINT, data=json.dumps(transfer))
from bigchaindb.models import Transaction
user_priv, user_pub = crypto.generate_key_pair()
input_valid = b.get_owned_ids(user_vk).pop()
create_tx = b.get_transaction(input_valid.txid)
transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub])
res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict()))
assert res.status_code == 400
@pytest.mark.usefixtures('inputs')
def test_get_transaction_status_endpoint(b, client, user_vk):
input_tx = b.get_owned_ids(user_vk).pop()
tx, status = b.get_transaction(input_tx['txid'], include_status=True)
res = client.get(TX_ENDPOINT + input_tx['txid'] + "/status")
tx, status = b.get_transaction(input_tx.txid, include_status=True)
res = client.get(TX_ENDPOINT + input_tx.txid + "/status")
assert status == res.json['status']
assert res.status_code == 200
res = client.get(TX_ENDPOINT + input_tx['txid'] + "/status/")
res = client.get(TX_ENDPOINT + input_tx.txid + "/status/")
assert status == res.json['status']
assert res.status_code == 200
@ -100,4 +120,3 @@ def test_get_transaction_status_returns_404_if_not_found(client):
res = client.get(TX_ENDPOINT + '123' + "/status/")
assert res.status_code == 404

View File

@ -1,6 +1,3 @@
import copy
def test_settings(monkeypatch):
import bigchaindb
from bigchaindb.web import server
@ -10,4 +7,3 @@ def test_settings(monkeypatch):
# for whatever reason the value is wrapped in a list
# needs further investigation
assert s.cfg.bind[0] == bigchaindb.config['server']['bind']