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:
parent
e74b4ee528
commit
50b0b3cef2
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ config = {
|
|||
'rate': 0.01,
|
||||
},
|
||||
'api_endpoint': 'http://localhost:9984/api/v1',
|
||||
'consensus_plugin': 'default',
|
||||
'backlog_reassign_delay': 30
|
||||
}
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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__)
|
||||
|
|
|
@ -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"""
|
|
@ -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())
|
|
@ -30,4 +30,3 @@ class Monitor(statsd.StatsClient):
|
|||
if 'port' not in kwargs:
|
||||
kwargs['port'] = bigchaindb.config['statsd']['port']
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -91,4 +91,3 @@ def create_server(settings):
|
|||
app = create_app(settings)
|
||||
standalone = StandaloneApplication(app, settings)
|
||||
return standalone
|
||||
|
||||
|
|
|
@ -23,4 +23,3 @@ def home():
|
|||
'keyring': bigchaindb.config['keyring'],
|
||||
'api_endpoint': bigchaindb.config['api_endpoint']
|
||||
})
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
]
|
||||
},
|
||||
```
|
|
@ -0,0 +1,6 @@
|
|||
#########
|
||||
Consensus
|
||||
#########
|
||||
|
||||
.. automodule:: bigchaindb.consensus
|
||||
:members:
|
|
@ -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
|
||||
|
|
|
@ -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:
|
|
@ -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.
|
||||
|
|
4
setup.py
4
setup.py
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
Loading…
Reference in New Issue