Merge remote-tracking branch 'remotes/origin/master' into monitor-yml-fix

This commit is contained in:
Rodolphe Marques 2016-05-03 14:38:32 +02:00
commit 1be81dbdf9
43 changed files with 3327 additions and 527 deletions

1
.gitignore vendored
View File

@ -71,3 +71,4 @@ target/
# Some files created when deploying a cluster on AWS
deploy-cluster-aws/conf/rethinkdb.conf
deploy-cluster-aws/hostlist.py
deploy-cluster-aws/confiles/

View File

@ -14,6 +14,29 @@ For reference, the possible headings are:
* **External Contributors** to list contributors outside of ascribe GmbH.
## [0.3.0] - 2016-05-03
Tag name: v0.3.0
= commit:
committed:
### Added
- Crypto-conditions specs according to the Interledger protocol: [Pull Request #174](https://github.com/bigchaindb/bigchaindb/pull/174)
- Added support for anonymous hashlocked conditions and fulfillments: [Pull Request #211](https://github.com/bigchaindb/bigchaindb/pull/211)
### Changed
- Several improvements to the aws deployment scripts: [Pull Request #227](https://github.com/bigchaindb/bigchaindb/pull/227)
### Fixed
- Bug related to block validation: [Pull Request #233](https://github.com/bigchaindb/bigchaindb/pull/233)
### Notes
This release completely refactored the structure of the transactions and broke compatibility with older versions
of BigchainDB. The refactor of the transactions was made in order to add support for multiple inputs/outputs and
the crypto-conditions specs from the Interledger protocol.
We also updated the rethinkdb python drivers so you need to upgrade to rethinkdb v2.3+
## [0.2.0] - 2016-04-26
Tag name: v0.2.0
= commit: 0c4a2b380aabdcf50fa2d7fb351c290aaedc3db7

View File

@ -1,12 +1,26 @@
FROM python:3.5
FROM rethinkdb:2.3
RUN apt-get update
RUN apt-get -y install python3 python3-pip
RUN pip3 install --upgrade pip
RUN pip3 install --upgrade setuptools
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
RUN pip install --upgrade pip
COPY . /usr/src/app/
RUN pip install --no-cache-dir -e .[dev]
WORKDIR /usr/src/app
RUN pip3 install --no-cache-dir -e .
WORKDIR /data
ENV BIGCHAINDB_CONFIG_PATH /data/.bigchaindb
ENV BIGCHAINDB_SERVER_BIND 0.0.0.0:9984
ENV BIGCHAINDB_API_ENDPOINT http://bigchaindb:9984/api/v1
ENTRYPOINT ["bigchaindb", "--experimental-start-rethinkdb"]
CMD ["start"]
EXPOSE 8080 9984 28015 29015

View File

@ -43,6 +43,16 @@ my_string = 'This is a very long string, so long that it will not fit into just
It seems the preference is for slashes, but using parentheses is okay too. (There are good arguments either way. Arguing about it seems like a waste of time.)
### How to Format Long import Statements
If you need to `import` lots of names from a module or package, and they won't all fit in one line (without making the line too long), then use parentheses to spread the names across multiple lines, like so:
```python
from Tkinter import (Tk, Frame, Button, Entry, Canvas, Text,
LEFT, DISABLED, NORMAL, RIDGE, END)
```
For the rationale, see [PEP 328](https://www.python.org/dev/peps/pep-0328/#rationale-for-parentheses).
### Using the % operator or `format()` to Format Strings
Given the choice:

View File

@ -2,7 +2,7 @@
[![PyPI](https://img.shields.io/pypi/v/bigchaindb.svg)](https://pypi.python.org/pypi/BigchainDB)
[![Travis branch](https://img.shields.io/travis/bigchaindb/bigchaindb/master.svg)](https://travis-ci.org/bigchaindb/bigchaindb)
[![Codecov branch](https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb/master.svg)](https://codecov.io/github/bigchaindb/bigchaindb?branch=master)
[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=stable)](https://bigchaindb.readthedocs.org/en/stable/)
[![Documentation Status](https://readthedocs.org/projects/bigchaindb/badge/?version=latest)](https://bigchaindb.readthedocs.org/en/latest/)
# BigchainDB

View File

@ -9,12 +9,20 @@ import logging
import argparse
import copy
import json
import builtins
import logstats
import bigchaindb
import bigchaindb.config_utils
from bigchaindb.util import ProcessGroup
from bigchaindb.client import temp_client
from bigchaindb import db
from bigchaindb.exceptions import DatabaseAlreadyExists, KeypairNotFoundException
from bigchaindb.commands.utils import base_parser, start
from bigchaindb.exceptions import (StartupError,
DatabaseAlreadyExists,
KeypairNotFoundException)
from bigchaindb.commands import utils
from bigchaindb.processes import Processes
from bigchaindb import crypto
@ -23,6 +31,14 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# We need this because `input` always prints on stdout, while it should print
# to stderr. It's a very old bug, check it out here:
# - https://bugs.python.org/issue1927
def input(prompt):
print(prompt, end='', file=sys.stderr)
return builtins.input()
def run_show_config(args):
"""Show the current configuration"""
# TODO Proposal: remove the "hidden" configuration. Only show config. If
@ -43,7 +59,11 @@ def run_configure(args, skip_if_exists=False):
skip_if_exists (bool): skip the function if a config file already exists
"""
config_path = args.config or bigchaindb.config_utils.CONFIG_DEFAULT_PATH
config_file_exists = os.path.exists(config_path)
config_file_exists = False
# if the config path is `-` then it's stdout
if config_path != '-':
config_file_exists = os.path.exists(config_path)
if config_file_exists and skip_if_exists:
return
@ -54,10 +74,15 @@ def run_configure(args, skip_if_exists=False):
if want != 'y':
return
# Patch the default configuration with the new values
conf = copy.deepcopy(bigchaindb._config)
conf = copy.deepcopy(bigchaindb.config)
print('Generating keypair')
# Patch the default configuration with the new values
conf = bigchaindb.config_utils.update(
conf,
bigchaindb.config_utils.env_config(bigchaindb.config))
print('Generating keypair', file=sys.stderr)
conf['keypair']['private'], conf['keypair']['public'] = \
crypto.generate_key_pair()
@ -80,9 +105,12 @@ def run_configure(args, skip_if_exists=False):
input('Statsd {}? (default `{}`): '.format(key, val)) \
or val
bigchaindb.config_utils.write_config(conf, config_path)
print('Configuration written to {}'.format(config_path))
print('Ready to go!')
if config_path != '-':
bigchaindb.config_utils.write_config(conf, config_path)
else:
print(json.dumps(conf, indent=4, sort_keys=True))
print('Configuration written to {}'.format(config_path), file=sys.stderr)
print('Ready to go!', file=sys.stderr)
def run_export_my_pubkey(args):
@ -110,8 +138,8 @@ def run_init(args):
try:
db.init()
except DatabaseAlreadyExists:
print('The database already exists.')
print('If you wish to re-initialize it, first drop it.')
print('The database already exists.', file=sys.stderr)
print('If you wish to re-initialize it, first drop it.', file=sys.stderr)
def run_drop(args):
@ -122,8 +150,15 @@ def run_drop(args):
def run_start(args):
"""Start the processes to run the node"""
# run_configure(args, skip_if_exists=True)
bigchaindb.config_utils.autoconfigure(filename=args.config, force=True)
if args.start_rethinkdb:
try:
proc = utils.start_rethinkdb()
except StartupError as e:
sys.exit('Error starting RethinkDB, reason is: {}'.format(e))
logger.info('RethinkDB started with PID %s' % proc.pid)
try:
db.init()
except DatabaseAlreadyExists:
@ -137,10 +172,46 @@ def run_start(args):
processes.start()
def _run_load(tx_left, stats):
logstats.thread.start(stats)
client = temp_client()
while True:
tx = client.create()
stats['transactions'] += 1
if tx_left is not None:
tx_left -= 1
if tx_left == 0:
break
def run_load(args):
bigchaindb.config_utils.autoconfigure(filename=args.config, force=True)
logger.info('Starting %s processes', args.multiprocess)
stats = logstats.Logstats()
logstats.thread.start(stats)
tx_left = None
if args.count > 0:
tx_left = int(args.count / args.multiprocess)
workers = ProcessGroup(concurrency=args.multiprocess,
target=_run_load,
args=(tx_left, stats.get_child()))
workers.start()
def main():
parser = argparse.ArgumentParser(
description='Control your BigchainDB node.',
parents=[base_parser])
parents=[utils.base_parser])
parser.add_argument('--experimental-start-rethinkdb',
dest='start_rethinkdb',
action='store_true',
help='Run RethinkDB on start')
# all the commands are contained in the subparsers object,
# the command selected by the user will be stored in `args.command`
@ -172,7 +243,25 @@ def main():
subparsers.add_parser('start',
help='Start BigchainDB')
start(parser, globals())
load_parser = subparsers.add_parser('load',
help='Write transactions to the backlog')
load_parser.add_argument('-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')
load_parser.add_argument('-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')
utils.start(parser, globals())
if __name__ == '__main__':

View File

@ -1,84 +0,0 @@
"""Command line interface for the `bigchaindb-benchmark` command."""
import logging
import argparse
import logstats
import bigchaindb
import bigchaindb.config_utils
from bigchaindb.util import ProcessGroup
from bigchaindb.client import temp_client
from bigchaindb.commands.utils import base_parser, start
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _run_load(tx_left, stats):
logstats.thread.start(stats)
client = temp_client()
# b = bigchaindb.Bigchain()
while True:
tx = client.create()
stats['transactions'] += 1
if tx_left is not None:
tx_left -= 1
if tx_left == 0:
break
def run_load(args):
bigchaindb.config_utils.autoconfigure(filename=args.config, force=True)
logger.info('Starting %s processes', args.multiprocess)
stats = logstats.Logstats()
logstats.thread.start(stats)
tx_left = None
if args.count > 0:
tx_left = int(args.count / args.multiprocess)
workers = ProcessGroup(concurrency=args.multiprocess,
target=_run_load,
args=(tx_left, stats.get_child()))
workers.start()
def main():
parser = argparse.ArgumentParser(description='Benchmark your bigchain federation.',
parents=[base_parser])
# all the commands are contained in the subparsers object,
# the command selected by the user will be stored in `args.command`
# that is used by the `main` function to select which other
# function to call.
subparsers = parser.add_subparsers(title='Commands',
dest='command')
# parser for database level commands
load_parser = subparsers.add_parser('load',
help='Write transactions to the backlog')
load_parser.add_argument('-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')
load_parser.add_argument('-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')
start(parser, globals())
if __name__ == '__main__':
main()

View File

@ -4,10 +4,57 @@ for ``argparse.ArgumentParser``.
import argparse
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.
Raises:
``bigchaindb.exceptions.StartupError`` if RethinkDB cannot
be started.
"""
proc = subprocess.Popen(['rethinkdb', '--bind', 'all'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
dbname = bigchaindb.config['database']['name']
line = ''
for line in proc.stdout:
if line.startswith('Server ready'):
# FIXME: seems like tables are not ready when the server is ready,
# that's why we need to query RethinkDB to know the state
# of the database. This code assumes the tables are ready
# when the database is ready. This seems a valid assumption.
try:
conn = db.get_conn()
# Before checking if the db is ready, we need to query
# the server to check if it contains that db
if r.db_list().contains(dbname).run(conn):
r.db(dbname).wait().run(conn)
except (r.ReqlOpFailedError, r.ReqlDriverError) as exc:
raise StartupError('Error waiting for the database `{}` '
'to be ready'.format(dbname)) from exc
return proc
# We are here when we exhaust the stdout of the process.
# The last `line` contains info about the error.
raise StartupError(line)
def start(parser, scope):
"""Utility function to execute a subcommand.
@ -51,7 +98,8 @@ def start(parser, scope):
base_parser = argparse.ArgumentParser(add_help=False, prog='bigchaindb')
base_parser.add_argument('-c', '--config',
help='Specify the location of the configuration file')
help='Specify the location of the configuration file '
'(use "-" for stdout)')
base_parser.add_argument('-y', '--yes', '--yes-please',
action='store_true',

View File

@ -91,7 +91,8 @@ def file_config(filename=None):
file at CONFIG_DEFAULT_PATH, if filename == None)
"""
logger.debug('On entry into file_config(), filename = {}'.format(filename))
if not filename:
if filename is None:
filename = CONFIG_DEFAULT_PATH
logger.debug('file_config() will try to open `{}`'.format(filename))

View File

@ -1,3 +1,4 @@
import copy
from abc import ABCMeta, abstractmethod
import bigchaindb.exceptions as exceptions
@ -119,45 +120,40 @@ class BaseConsensusRules(AbstractConsensusRules):
# If the operation is CREATE the transaction should have no inputs and
# should be signed by a federation node
if transaction['transaction']['operation'] == 'CREATE':
if transaction['transaction']['input']:
# 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')
if transaction['transaction']['current_owner'] not in (
# TODO: for now lets assume a CREATE transaction only has one current_owner
if transaction['transaction']['fulfillments'][0]['current_owners'][0] not in (
bigchain.federation_nodes + [bigchain.me]):
raise exceptions.OperationError(
'Only federation nodes can use the operation `CREATE`')
else:
# check if the input exists, is owned by the current_owner
if not transaction['transaction']['input']:
raise ValueError(
'Only `CREATE` transactions can have null inputs')
if not transaction['transaction']['fulfillments']:
raise ValueError('Transaction contains no fulfillments')
tx_input = bigchain.get_transaction(
transaction['transaction']['input'])
# 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(
transaction['transaction']['input']))
if (tx_input['transaction']['new_owner'] !=
transaction['transaction']['current_owner']):
raise exceptions.TransactionOwnerError(
'current_owner `{}` does not own the input `{}`'.format(
transaction['transaction']['current_owner'],
transaction['transaction']['input']))
# check if the input was already spent by a transaction other than
# this one.
spent = bigchain.get_spent(tx_input['id'])
if spent and spent['id'] != transaction['id']:
raise exceptions.DoubleSpend(
'input `{}` was already spent'.format(
transaction['transaction']['input']))
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 = crypto.hash_data(util.serialize(
transaction['transaction']))
calculated_hash = util.get_hash_data(transaction)
if calculated_hash != transaction['id']:
raise exceptions.InvalidHash()
@ -167,7 +163,6 @@ class BaseConsensusRules(AbstractConsensusRules):
return transaction
# TODO: Unsure if a bigchain parameter is really necessary here?
@staticmethod
def validate_block(bigchain, block):
"""Validate a block.
@ -189,6 +184,15 @@ class BaseConsensusRules(AbstractConsensusRules):
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.federation_nodes + [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
@staticmethod

View File

@ -174,36 +174,38 @@ class Bigchain(object):
returns an empty list `[]`
"""
cursor = r.table('bigchain')\
.get_all(payload_hash, index='payload_hash')\
cursor = r.table('bigchain') \
.get_all(payload_hash, index='payload_hash') \
.run(self.conn)
transactions = list(cursor)
return transactions
def get_spent(self, txid):
def get_spent(self, tx_input):
"""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:
txid (str): transaction id.
tx_input (dict): Input of a transaction in the form `{'txid': 'transaction id', 'cid': 'condition id'}`
Returns:
The transaction that used the `txid` as an input if it exists else it returns `None`
"""
# checks if an input was already spent
# checks if the bigchain has any transaction with input `transaction_id`
# checks if the bigchain has any transaction with input {'txid': ..., 'cid': ...}
response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\
.filter(lambda transaction: transaction['transaction']['input'] == txid).run(self.conn)
.filter(lambda transaction: transaction['transaction']['fulfillments']
.contains(lambda fulfillment: fulfillment['input'] == tx_input))\
.run(self.conn)
# a transaction_id should have been spent at most one time
transactions = list(response)
if transactions:
if len(transactions) != 1:
raise Exception('`{}` was spent more then once. There is a problem with the chain'.format(
txid))
raise exceptions.DoubleSpend('`{}` was spent more then once. There is a problem with the chain'.format(
tx_input['txid']))
else:
return transactions[0]
else:
@ -219,17 +221,34 @@ class Bigchain(object):
list: list of `txids` currently owned by `owner`
"""
response = r.table('bigchain')\
.concat_map(lambda doc: doc['block']['transactions'])\
.filter({'transaction': {'new_owner': owner}})\
.pluck('id')['id']\
.run(self.conn)
# get all transactions in which owner is in the `new_owners` list
response = r.table('bigchain') \
.concat_map(lambda doc: doc['block']['transactions']) \
.filter(lambda tx: tx['transaction']['conditions']
.contains(lambda c: c['new_owners']
.contains(owner))) \
.run(self.conn)
owned = []
# remove all inputs already spent
for tx_input in list(response):
if not self.get_spent(tx_input):
owned.append(tx_input)
for tx in response:
# 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 simple signature conditions there are no subfulfillments
# check if the owner is in the condition `new_owners`
if len(condition['new_owners']) == 1:
if condition['condition']['details']['public_key'] == owner:
tx_input = {'txid': tx['id'], 'cid': condition['cid']}
else:
# for transactions with multiple `new_owners` there will be several subfulfillments nested
# in the condition. We need to iterate the subfulfillments to make sure there is a
# subfulfillment for `owner`
for subfulfillment in condition['condition']['details']['subfulfillments']:
if subfulfillment['public_key'] == owner:
tx_input = {'txid': tx['id'], 'cid': condition['cid']}
# check if input was already spent
if not self.get_spent(tx_input):
owned.append(tx_input)
return owned
@ -378,7 +397,7 @@ class Bigchain(object):
raise GenesisBlockAlreadyExistsError('Cannot create the Genesis block')
payload = {'message': 'Hello World from the BigchainDB'}
transaction = self.create_transaction(self.me, self.me, None, 'GENESIS', payload=payload)
transaction = self.create_transaction([self.me], [self.me], None, 'GENESIS', payload=payload)
transaction_signed = self.sign_transaction(transaction, self.me_private)
# create the block
@ -429,37 +448,37 @@ class Bigchain(object):
if 'block_number' not in block:
update['block_number'] = block_number
r.table('bigchain')\
.get(vote['vote']['voting_for_block'])\
.update(update)\
.run(self.conn)
r.table('bigchain') \
.get(vote['vote']['voting_for_block']) \
.update(update) \
.run(self.conn)
def get_last_voted_block(self):
"""Returns the last block that this node voted on."""
# query bigchain for all blocks this node is a voter but didn't voted on
last_voted = r.table('bigchain')\
.filter(r.row['block']['voters'].contains(self.me))\
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me))\
.order_by(r.desc('block_number'))\
.limit(1)\
last_voted = r.table('bigchain') \
.filter(r.row['block']['voters'].contains(self.me)) \
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me)) \
.order_by(r.desc('block_number')) \
.limit(1) \
.run(self.conn)
# return last vote if last vote exists else return Genesis block
last_voted = list(last_voted)
if not last_voted:
return list(r.table('bigchain')
.filter(r.row['block_number'] == 0)
.run(self.conn))[0]
.filter(r.row['block_number'] == 0)
.run(self.conn))[0]
return last_voted[0]
def get_unvoted_blocks(self):
"""Return all the blocks that has not been voted by this node."""
unvoted = r.table('bigchain')\
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me).not_())\
.order_by(r.asc((r.row['block']['timestamp'])))\
unvoted = r.table('bigchain') \
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me).not_()) \
.order_by(r.asc((r.row['block']['timestamp']))) \
.run(self.conn)
if unvoted and unvoted[0].get('block_number') == 0:

View File

@ -1,7 +1,7 @@
# Separate all crypto code so that we can easily test several implementations
import sha3
from cryptoconditions import ed25519
from cryptoconditions import crypto
def hash_data(data):
@ -10,8 +10,8 @@ def hash_data(data):
def generate_key_pair():
sk, pk = ed25519.ed25519_generate_key_pair()
sk, pk = crypto.ed25519_generate_key_pair()
return sk.decode(), pk.decode()
SigningKey = ed25519.SigningKey
VerifyingKey = ed25519.VerifyingKey
SigningKey = crypto.Ed25519SigningKey
VerifyingKey = crypto.Ed25519VerifyingKey

View File

@ -28,4 +28,9 @@ class DatabaseDoesNotExist(Exception):
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"""

View File

@ -1,4 +1,4 @@
import copy
import json
import time
import contextlib
@ -7,6 +7,9 @@ import queue
import multiprocessing as mp
from datetime import datetime
import cryptoconditions as cc
from cryptoconditions.exceptions import ParsingError
import bigchaindb
from bigchaindb import exceptions
from bigchaindb import crypto
@ -134,7 +137,8 @@ def timestamp():
return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6)
def create_tx(current_owner, new_owner, tx_input, operation, payload=None):
# TODO: Consider remove the operation (if there are no inputs CREATE else TRANSFER)
def create_tx(current_owners, new_owners, inputs, operation, payload=None):
"""Create a new transaction
A transaction in the bigchain is a transfer of a digital asset between two entities represented
@ -150,9 +154,9 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None):
`TRANSFER` - A transfer operation allows for a transfer of the digital assets between entities.
Args:
current_owner (str): base58 encoded public key of the current owner of the asset.
new_owner (str): base58 encoded public key of the new owner of the digital asset.
tx_input (str): id of the transaction to use as input.
current_owners (list): base58 encoded public key of the current owners of the asset.
new_owners (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.
@ -162,8 +166,61 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None):
Raises:
TypeError: if the optional ``payload`` argument is not a ``dict``.
"""
Reference:
{
"id": "<sha3 hash>",
"version": "transaction version number",
"transaction": {
"fulfillments": [
{
"current_owners": ["list of <pub-keys>"],
"input": {
"txid": "<sha3 hash>",
"cid": "condition index"
},
"fulfillment": "fulfillement of condition cid",
"fid": "fulfillment index"
}
],
"conditions": [
{
"new_owners": ["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 current_owners is None:
current_owners = []
if not isinstance(current_owners, list):
current_owners = [current_owners]
# The None case appears on assigning a hashlock
if new_owners is None:
new_owners = []
if not isinstance(new_owners, list):
new_owners = [new_owners]
if not isinstance(inputs, list):
inputs = [inputs]
# handle payload
data = None
if payload is not None:
if isinstance(payload, dict):
@ -175,52 +232,181 @@ def create_tx(current_owner, new_owner, tx_input, operation, payload=None):
else:
raise TypeError('`payload` must be an dict instance')
hash_payload = crypto.hash_data(serialize(payload))
data = {
'hash': hash_payload,
'payload': payload
}
# handle inputs
fulfillments = []
# transfer
if inputs:
for fid, tx_input in enumerate(inputs):
fulfillments.append({
'current_owners': current_owners,
'input': tx_input,
'fulfillment': None,
'fid': fid
})
# create
else:
fulfillments.append({
'current_owners': current_owners,
'input': None,
'fulfillment': None,
'fid': 0
})
# handle outputs
conditions = []
for fulfillment in fulfillments:
# threshold condition
if len(new_owners) > 1:
condition = cc.ThresholdSha256Fulfillment(threshold=len(new_owners))
for new_owner in new_owners:
condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=new_owner))
# simple signature condition
elif len(new_owners) == 1:
condition = cc.Ed25519Fulfillment(public_key=new_owners[0])
# to be added later (hashlock conditions)
else:
condition = None
if condition:
conditions.append({
'new_owners': new_owners,
'condition': {
'details': json.loads(condition.serialize_json()),
'uri': condition.condition.serialize_uri()
},
'cid': fulfillment['fid']
})
tx = {
'current_owner': current_owner,
'new_owner': new_owner,
'input': tx_input,
'fulfillments': fulfillments,
'conditions': conditions,
'operation': operation,
'timestamp': timestamp(),
'data': data
}
# serialize and convert to bytes
tx_serialized = serialize(tx)
tx_hash = crypto.hash_data(tx_serialized)
tx_hash = get_hash_data(tx)
# create the transaction
transaction = {
'id': tx_hash,
'version': 1,
'transaction': tx
}
return transaction
def sign_tx(transaction, private_key):
def sign_tx(transaction, signing_keys):
"""Sign a transaction
A transaction signed with the `current_owner` corresponding private key.
Args:
transaction (dict): transaction to sign.
private_key (str): base58 encoded private key to create a signature of the transaction.
signing_keys (list): list of base58 encoded private keys to create the fulfillments of the transaction.
Returns:
dict: transaction with the `signature` field included.
dict: transaction with the `fulfillment` fields populated.
"""
private_key = crypto.SigningKey(private_key)
signature = private_key.sign(serialize(transaction))
signed_transaction = transaction.copy()
signed_transaction.update({'signature': signature})
return signed_transaction
# 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 current_owners
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)
for fulfillment in tx['transaction']['fulfillments']:
fulfillment_message = get_fulfillment_message(transaction, fulfillment)
parsed_fulfillment = cc.Fulfillment.from_json(fulfillment_message['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 (object): cryptoconditions.Ed25519Fulfillment instance.
fulfillment_message (dict): message to sign.
key_pairs (dict): dictionary of (public_key, private_key) pairs.
Returns:
object: fulfilled cryptoconditions.Ed25519Fulfillment
"""
current_owner = fulfillment['current_owners'][0]
try:
parsed_fulfillment.sign(serialize(fulfillment_message), key_pairs[current_owner])
except KeyError:
raise exceptions.KeypairMismatchException('Public key {} is not a pair to any of the private keys'
.format(current_owner))
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 (object): 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 current_owner in fulfillment['current_owners']:
try:
subfulfillment = parsed_fulfillment_copy.get_subcondition_from_vk(current_owner)[0]
except IndexError:
exceptions.KeypairMismatchException('Public key {} cannot be found in the fulfillment'
.format(current_owner))
try:
subfulfillment.sign(serialize(fulfillment_message), key_pairs[current_owner])
except KeyError:
raise exceptions.KeypairMismatchException('Public key {} is not a pair to any of the private keys'
.format(current_owner))
parsed_fulfillment.add_subfulfillment(subfulfillment)
return parsed_fulfillment
def create_and_sign_tx(private_key, current_owner, new_owner, tx_input, operation='TRANSFER', payload=None):
@ -230,7 +416,7 @@ def create_and_sign_tx(private_key, current_owner, new_owner, tx_input, operatio
def check_hash_and_signature(transaction):
# Check hash of the transaction
calculated_hash = crypto.hash_data(serialize(transaction['transaction']))
calculated_hash = get_hash_data(transaction)
if calculated_hash != transaction['id']:
raise exceptions.InvalidHash()
@ -251,16 +437,87 @@ def verify_signature(signed_transaction):
bool: True if the signature is correct, False otherwise.
"""
data = signed_transaction.copy()
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
is_valid = parsed_fulfillment.validate(serialize(fulfillment_message))
# if assignee field in the transaction, remove it
if 'assignee' in data:
data.pop('assignee')
# if transaction has an input (i.e. not a `CREATE` transaction)
if fulfillment['input']:
is_valid &= parsed_fulfillment.condition.serialize_uri() == \
fulfillment_message['condition']['condition']['uri']
if not is_valid:
return False
signature = data.pop('signature')
public_key_base58 = signed_transaction['transaction']['current_owner']
public_key = crypto.VerifyingKey(public_key_base58)
return public_key.verify(serialize(data), signature)
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
"""
b = bigchaindb.Bigchain()
# data to sign contains common transaction data
fulfillment_message = {
'operation': transaction['transaction']['operation'],
'timestamp': transaction['transaction']['timestamp'],
'data': transaction['transaction']['data'],
'version': 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({
'input': fulfillment['input'],
'condition': None,
})
# if `TRANSFER` transaction
if fulfillment['input']:
# get previous condition
previous_tx = b.get_transaction(fulfillment['input']['txid'])
conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid'])
fulfillment_message['condition'] = conditions[fulfillment['input']['cid']]
# if `CREATE` transaction
# there is no previous transaction so we need to create one on the fly
else:
current_owner = transaction['transaction']['fulfillments'][0]['current_owners'][0]
condition = json.loads(cc.Ed25519Fulfillment(public_key=current_owner).serialize_json())
fulfillment_message['condition'] = {'condition': {'details': condition}}
if serialized:
return serialize(fulfillment_message)
return fulfillment_message
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 transform_create(tx):
@ -273,6 +530,6 @@ def transform_create(tx):
payload = None
if transaction['data'] and 'payload' in transaction['data']:
payload = transaction['data']['payload']
new_tx = create_tx(b.me, transaction['current_owner'], None, 'CREATE', payload=payload)
new_tx = create_tx(b.me, transaction['fulfillments'][0]['current_owners'], None, 'CREATE', payload=payload)
return new_tx

View File

@ -1,2 +1,2 @@
__version__ = '0.2.0'
__short_version__ = '0.2'
__version__ = '0.3.0'
__short_version__ = '0.3'

View File

@ -26,6 +26,7 @@ coverage:
- "deploy-cluster-aws/*"
- "docs/*"
- "tests/*"
- "bigchaindb/version.py"
comment:
# @stevepeak (from codecov.io) suggested we change 'suggestions' to 'uncovered'

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""Given a directory full of default BigchainDB config files,
transform them into config files for a cluster with proper
keyrings, API endpoint values, etc.
Note: This script assumes that there is a file named hostlist.py
containing public_dns_names = a list of the public DNS names of
all the hosts in the cluster.
Usage:
python clusterize_confiles.py <dir> <number_of_files>
"""
from __future__ import unicode_literals
import os
import json
import argparse
from hostlist import public_dns_names
# Parse the command-line arguments
parser = argparse.ArgumentParser()
parser.add_argument('dir',
help='Directory containing the config files')
parser.add_argument('number_of_files',
help='Number of config files expected in dir',
type=int)
args = parser.parse_args()
conf_dir = args.dir
numfiles_expected = int(args.number_of_files)
# Check if the number of files in conf_dir is what was expected
conf_files = os.listdir(conf_dir)
numfiles = len(conf_files)
if numfiles != numfiles_expected:
raise ValueError('There are {} files in {} but {} were expected'.
format(numfiles, conf_dir, numfiles_expected))
# Make a list containing all the public keys from
# all the config files
pubkeys = []
for filename in conf_files:
file_path = os.path.join(conf_dir, filename)
with open(file_path, 'r') as f:
conf_dict = json.load(f)
pubkey = conf_dict['keypair']['public']
pubkeys.append(pubkey)
# Rewrite each config file, one at a time
for i, filename in enumerate(conf_files):
file_path = os.path.join(conf_dir, filename)
with open(file_path, 'r') as f:
conf_dict = json.load(f)
# The keyring is the list of *all* public keys
# minus the config file's own public key
keyring = list(pubkeys)
keyring.remove(conf_dict['keypair']['public'])
conf_dict['keyring'] = keyring
# Allow incoming server traffic from any IP address
# to port 9984
conf_dict['server']['bind'] = '0.0.0.0:9984'
# Set the api_endpoint
conf_dict['api_endpoint'] = 'http://' + public_dns_names[i] + \
':9984/api/v1'
# Delete the config file
os.remove(file_path)
# Write new config file with the same filename
print('Rewriting {}'.format(file_path))
with open(file_path, 'w') as f2:
json.dump(conf_dict, f2)

View File

@ -5,7 +5,7 @@ BigchainDB, including its storage backend (RethinkDB).
from __future__ import with_statement, unicode_literals
from fabric.api import sudo, env
from fabric.api import sudo, env, hosts
from fabric.api import task, parallel
from fabric.contrib.files import sed
from fabric.operations import run, put
@ -32,24 +32,19 @@ newrelic_license_key = 'you_need_a_real_license_key'
# DON'T PUT @parallel
@task
def set_hosts(hosts):
"""A helper function to change env.hosts from the
command line.
def set_host(host_index):
"""A helper task to change env.hosts from the
command line. It will only "stick" for the duration
of the fab command that called it.
Args:
hosts (str): 'one_node' or 'two_nodes'
host_index (int): 0, 1, 2, 3, etc.
Example:
fab set_hosts:one_node init_bigchaindb
fab set_host:4 fab_task_A fab_task_B
will set env.hosts = [public_dns_names[4]]
but only for doing fab_task_A and fab_task_B
"""
if hosts == 'one_node':
env.hosts = public_dns_names[:1]
elif hosts == 'two_nodes':
env.hosts = public_dns_names[:2]
else:
raise ValueError('Invalid input to set_hosts.'
' Expected one_node or two_nodes.'
' Got {}'.format(hosts))
env.hosts = [public_dns_names[int(host_index)]]
# Install base software
@ -138,13 +133,26 @@ def configure_bigchaindb():
run('bigchaindb -y configure', pty=False)
# Send the specified configuration file to
# the remote host and save it there in
# ~/.bigchaindb
# Use in conjunction with set_host()
# No @parallel
@task
def send_confile(confile):
put('confiles/' + confile, 'tempfile')
run('mv tempfile ~/.bigchaindb')
print('For this node, bigchaindb show-config says:')
run('bigchaindb show-config')
# Initialize BigchainDB
# i.e. create the database, the tables,
# the indexes, and the genesis block.
# (This only needs to be run on one node.)
# Call using:
# fab set_hosts:one_node init_bigchaindb
# (The @hosts decorator is used to make this
# task run on only one node. See http://tinyurl.com/h9qqf3t )
@task
@hosts(public_dns_names[0])
def init_bigchaindb():
run('bigchaindb init', pty=False)

View File

@ -14,6 +14,7 @@
from __future__ import unicode_literals
import sys
import time
import socket
import argparse
import botocore
import boto3
@ -192,8 +193,27 @@ with open('hostlist.py', 'w') as f:
f.write('\n')
f.write('public_dns_names = {}\n'.format(public_dns_names))
# Wait
wait_time = 45
print('Waiting {} seconds to make sure all instances are ready...'.
format(wait_time))
time.sleep(wait_time)
# For each node in the cluster, check port 22 (ssh) until it's reachable
for instance in instances_with_tag:
ip_address = instance.public_ip_address
# Create a socket
# Address Family: AF_INET (means IPv4)
# Type: SOCK_STREAM (means connection-oriented TCP protocol)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('Attempting to connect to {} on port 22 (ssh)...'.
format(ip_address))
unreachable = True
while unreachable:
try:
# Open a connection to the remote node on port 22
s.connect((ip_address, 22))
except socket.error as e:
print(' Socket error: {}'.format(e))
print(' Trying again in 3 seconds')
time.sleep(3.0)
else:
print(' Port 22 is reachable!')
s.shutdown(socket.SHUT_WR)
s.close()
unreachable = False

View File

@ -0,0 +1,40 @@
#! /bin/bash
# The set -e option instructs bash to immediately exit
# if any command has a non-zero exit status
set -e
function printErr()
{
echo "usage: ./make_confiles.sh <dir> <number_of_files>"
echo "No argument $1 supplied"
}
if [ -z "$1" ]; then
printErr "<dir>"
exit 1
fi
if [ -z "$2" ]; then
printErr "<number_of_files>"
exit 1
fi
CONFDIR=$1
NUMFILES=$2
# If $CONFDIR exists, remove it
if [ -d "$CONFDIR" ]; then
rm -rf $CONFDIR
fi
# Create $CONFDIR
mkdir $CONFDIR
# Use the bigchaindb configure command to create
# $NUMFILES BigchainDB config files in $CONFDIR
for (( i=0; i<$NUMFILES; i++ )); do
CONPATH=$CONFDIR"/bcdb_conf"$i
echo "Writing "$CONPATH
bigchaindb -y -c $CONPATH configure
done

View File

@ -21,7 +21,7 @@ if [ -z "$2" ]; then
fi
TAG=$1
NODES=$2
NUM_NODES=$2
# If they don't include a third argument (<pypi_or_branch>)
# then assume BRANCH = "pypi" by default
@ -38,6 +38,13 @@ if [ ! -f "pem/bigchaindb.pem" ]; then
exit 1
fi
# Check for the confiles directory
if [ ! -d "confiles" ]; then
echo "Directory confiles is needed but does not exist"
echo "See make_confiles.sh to find out how to make it"
exit 1
fi
# Change the file permissions on pem/bigchaindb.pem
# so that the owner can read it, but that's all
chmod 0400 pem/bigchaindb.pem
@ -52,7 +59,7 @@ chmod 0400 pem/bigchaindb.pem
# 5. writes the shellscript add2known_hosts.sh
# 6. (over)writes a file named hostlist.py
# containing a list of all public DNS names.
python launch_ec2_nodes.py --tag $TAG --nodes $NODES
python launch_ec2_nodes.py --tag $TAG --nodes $NUM_NODES
# Make add2known_hosts.sh executable then execute it.
# This adds remote keys to ~/.ssh/known_hosts
@ -86,22 +93,38 @@ else
fi
# Configure BigchainDB on all nodes
fab configure_bigchaindb
# TODO: Get public keys from all nodes
# The idea is to send a bunch of locally-created configuration
# files out to each of the instances / nodes.
# Assume a set of $NUM_NODES BigchaindB config files
# already exists in the confiles directory.
# One can create a set using a command like
# ./make_confiles.sh confiles $NUM_NODES
# (We can't do that here now because this virtual environment
# is a Python 2 environment that may not even have
# bigchaindb installed, so bigchaindb configure can't be called)
# TODO: Add list of public keys to keyring of all nodes
# Transform the config files in the confiles directory
# to have proper keyrings, api_endpoint values, etc.
python clusterize_confiles.py confiles $NUM_NODES
# Send one of the config files to each instance
for (( HOST=0 ; HOST<$NUM_NODES ; HOST++ )); do
CONFILE="bcdb_conf"$HOST
echo "Sending "$CONFILE
fab set_host:$HOST send_confile:$CONFILE
done
# Send a "bigchaindb init" command to one node
# to initialize the BigchainDB database
# i.e. create the database, the tables,
# the indexes, and the genesis block.
fab set_hosts:one_node init_bigchaindb
# Initialize BigchainDB (i.e. Create the RethinkDB database,
# the tables, the indexes, and genesis glock). Note that
# this will only be sent to one of the nodes, see the
# definition of init_bigchaindb() in fabfile.py to see why.
fab init_bigchaindb
# Start BigchainDB on all the nodes using "screen"
fab start_bigchaindb
# cleanup
rm add2known_hosts.sh
# rm -rf temp_confs

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,6 +1,6 @@
# The BigchainDB Command Line Interface (CLI)
There are some command-line commands for working with BigchainDB: `bigchaindb` and `bigchaindb-benchmark`. This section provides an overview of those commands.
The command to interact with BigchainDB is `bigchaindb`. This section provides an overview of the command.
## bigchaindb
@ -37,10 +37,9 @@ This command drops (erases) the RethinkDB database. You will be prompted to make
This command starts BigchainDB. It always begins by trying a `bigchaindb init` first. See the note in the documentation for `bigchaindb init`.
## bigchaindb-benchmark
### bigchaindb load
The `bigchaindb-benchmark` command is used to run benchmarking tests. You can learn more about it using:
This command is used to run benchmarking tests. You can learn more about it using:
```text
$ bigchaindb-benchmark -h
$ bigchaindb-benchmark load -h
$ bigchaindb load -h
```

View File

@ -83,16 +83,35 @@ Add some rules for Inbound traffic:
**Note: These rules are extremely lax! They're meant to make testing easy.** You'll want to tighten them up if you intend to have a secure cluster. For example, Source = 0.0.0.0/0 is [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) for "allow this traffic to come from _any_ IP address."
## Deployment
## AWS Deployment
Here's an example of how one could launch a BigchainDB cluster of four nodes tagged `wrigley` on AWS:
### AWS Deployment Step 1
Suppose _N_ is the number of nodes you want in your BigchainDB cluster. If you already have a set of _N_ BigchainDB configuration files in the `deploy-cluster-aws/confiles` directory, then you can jump to step 2. To create such a set, you can do something like:
```text
# in a Python 3 virtual environment where bigchaindb is installed
cd bigchaindb
cd deploy-cluster-aws
./startup.sh wrigley 4 pypi
./make_confiles.sh confiles 3
```
The `pypi` on the end means that it will install the latest (stable) `bigchaindb` package from the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each instance, BigchainDB is installed using `pip install bigchaindb`.
That will create three (3) _default_ BigchainDB configuration files in the `deploy-cluster-aws/confiles` directory (which will be created if it doesn't already exist). The three files will be named `bcdb_conf0`, `bcdb_conf1`, and `bcdb_conf2`.
You can look inside those files if you're curious. In step 2, they'll be modified. For example, the default keyring is an empty list. In step 2, the deployment script automatically changes the keyring of each node to be a list of the public keys of all other nodes. Other changes are also made.
### AWS Deployment Step 2
Step 2 is to launch the nodes ("instances") on AWS, to install all the necessary software on them, configure the software, run the software, and more.
Here's an example of how one could launch a BigchainDB cluster of three (3) nodes tagged `wrigley` on AWS:
```text
# in a Python 2.5-2.7 virtual environment where fabric, boto3, etc. are installed
cd bigchaindb
cd deploy-cluster-aws
./startup.sh wrigley 3 pypi
```
The `pypi` on the end means that it will install the latest (stable) `bigchaindb` package from the [Python Package Index (PyPI)](https://pypi.python.org/pypi). That is, on each node, BigchainDB is installed using `pip install bigchaindb`.
`startup.sh` is a Bash script which calls some Python and Fabric scripts. The usage is:
```text
@ -101,20 +120,7 @@ The `pypi` on the end means that it will install the latest (stable) `bigchaindb
The first two arguments are self-explanatory. The third argument can be `pypi` or the name of a local Git branch (e.g. `master` or `feat/3752/quote-asimov-on-tuesdays`). If you don't include a third argument, then `pypi` will be assumed by default.
Here's what the `startup.sh` script does; it:
0. allocates more elastic IP addresses if necessary,
1. launches the specified number of nodes (instances) on Amazon EC2,
2. tags them with the specified tag,
3. waits until those instances exist and are running,
4. for each instance, it associates an elastic IP address with that instance,
5. adds remote keys to `~/.ssh/known_hosts`,
6. (re)creates the RethinkDB configuration file `conf/rethinkdb.conf`,
7. installs base (prerequisite) software on all instances,
8. installs RethinkDB on all instances,
9. installs BigchainDB on all instances,
10. initializes the BigchainDB database,
11. starts BigchainDB on all instances.
If you're curious what the `startup.sh` script does, the source code has lots of explanatory comments, so it's quite easy to read. Here's a link to the latest version on GitHub: [`startup.sh`](https://github.com/bigchaindb/bigchaindb/blob/master/deploy-cluster-aws/startup.sh)
It should take a few minutes for the deployment to finish. If you run into problems, see the section on Known Deployment Issues below.

View File

@ -111,9 +111,110 @@ If it's the first time you've run `bigchaindb start`, then it creates the databa
**NOT for Production Use**
For those who like using Docker and wish to experiment with BigchainDB in non-production environments, we currently maintain a `dockerfile` that can be used to build an image for `bigchaindb`, along with a `docker-compose.yml` file to manage a "standalone node", consisting mainly of two containers: one for RethinkDB, and another for BigchainDB.
For those who like using Docker and wish to experiment with BigchainDB in
non-production environments, we currently maintain a Docker image and a
`Dockerfile` that can be used to build an image for `bigchaindb`.
Assuming you have `docker` and `docker-compose` installed, you would proceed as follows.
### Pull and Run the Image from Docker Hub
Assuming you have Docker installed, you would proceed as follows.
In a terminal shell, pull the latest version of the BigchainDB Docker image using:
```text
docker pull bigchaindb/bigchaindb:latest
```
then do a one-time configuration step to create the config file; we will use
the `-y` option to accept all the default values. The configuration file will
be stored in a file on your host machine at `~/bigchaindb_docker/.bigchaindb`:
```text
$ docker run --rm -v "$HOME/bigchaindb_docker:/data" -ti \
bigchaindb/bigchaindb:latest -y configure
Generating keypair
Configuration written to /data/.bigchaindb
Ready to go!
```
Let's analyze that command:
* `docker run` tells Docker to run some image
* `--rm` remove the container once we are done
* `-v "$HOME/bigchaindb_docker:/data"` map the host directory
`$HOME/bigchaindb_docker` to the container directory `/data`;
this allows us to have the data persisted on the host machine,
you can read more in the [official Docker
documentation](https://docs.docker.com/engine/userguide/containers/dockervolumes/#mount-a-host-directory-as-a-data-volume)
* `-t` allocate a pseudo-TTY
* `-i` keep STDIN open even if not attached
* `bigchaindb/bigchaindb:latest` the image to use
* `-y configure` execute the `configure` sub-command (of the `bigchaindb` command) inside the container, with the `-y` option to automatically use all the default config values
After configuring the system, you can run BigchainDB with the following command:
```text
$ docker run -v "$HOME/bigchaindb_docker:/data" -d \
--name bigchaindb \
-p "58080:8080" -p "59984:9984" \
bigchaindb/bigchaindb:latest start
```
The command is slightly different from the previous one, the differences are:
* `-d` run the container in the background
* `--name bigchaindb` give a nice name to the container so it's easier to
refer to it later
* `-p "58080:8080"` map the host port `58080` to the container port `8080`
(the RethinkDB admin interface)
* `-p "59984:9984"` map the host port `59984` to the container port `9984`
(the BigchainDB API server)
* `start` start the BigchainDB service
Another way to publish the ports exposed by the container is to use the `-P` (or
`--publish-all`) option. This will publish all exposed ports to random ports. You can
always run `docker ps` to check the random mapping.
You can also access the RethinkDB dashboard at
[http://localhost:58080/](http://localhost:58080/)
If that doesn't work, then replace `localhost` with the IP or hostname of the
machine running the Docker engine. If you are running docker-machine (e.g. on
Mac OS X) this will be the IP of the Docker machine (`docker-machine ip
machine_name`).
#### Load Testing with Docker
Now that we have BigchainDB running in the Docker container named `bigchaindb`, we can
start another BigchainDB container to generate a load test for it.
First, make sure the container named `bigchaindb` is still running. You can check that using:
```text
docker ps
```
You should see a container named `bigchaindb` in the list.
You can load test the BigchainDB running in that container by running the `bigchaindb load` command in a second container:
```text
$ docker run --rm -v "$HOME/bigchaindb_docker:/data" -ti \
--link bigchaindb \
bigchaindb/bigchaindb:latest load
```
Note the `--link` option to link to the first container (named `bigchaindb`).
Aside: The `bigchaindb load` command has several options (e.g. `-m`). You can read more about it in [the documentation about the BigchainDB command line interface](bigchaindb-cli.html).
If you look at the RethinkDB dashboard (in your web browser), you should see the effects of the load test. You can also see some effects in the Docker logs using:
```text
$ docker logs -f bigchaindb
```
### Building Your Own Image
Assuming you have Docker installed, you would proceed as follows.
In a terminal shell:
```text
@ -122,41 +223,7 @@ $ git clone git@github.com:bigchaindb/bigchaindb.git
Build the Docker image:
```text
$ docker-compose build
$ docker build --tag local-bigchaindb .
```
then do a one-time configuration step to create the config file; it will be
stored on your host machine under ` ~/.bigchaindb_docker/config`:
```text
$ docker-compose run --rm bigchaindb bigchaindb configure
Starting bigchaindb_rethinkdb-data_1
Generating keypair
API Server bind? (default `localhost:9984`):
Database host? (default `localhost`): rethinkdb
Database port? (default `28015`):
Database name? (default `bigchain`):
Statsd host? (default `localhost`):
Statsd port? (default `8125`):
Statsd rate? (default `0.01`):
Ready to go!
```
As shown above, make sure that you set the database and statsd hosts to their
corresponding service names (`rethinkdb`, `statsd`), defined in`docker-compose.yml`
and `docker-compose-monitor.yml`.
You can then start it up (in the background, as a daemon) using:
```text
$ docker-compose up -d
```
then you can load test transactions via:
```text
$ docker exec -it docker-bigchaindb bigchaindb-benchmark load -m
```
If you're on Linux, you can probably view the RethinkDB dashboard at:
[http://localhost:58080/](http://localhost:58080/)
If that doesn't work, then replace `localhost` with the IP or hostname of the machine running the Docker engine. If you are running docker-machine (e.g.: on Mac OS X) this will be the IP of the Docker machine (`docker-machine ip machine_name`).
Now you can use your own image to run BigchainDB containers.

View File

@ -7,37 +7,146 @@ Transactions, blocks and votes are represented using JSON documents with the fol
```json
{
"id": "<sha3 hash>",
"version": "<transaction version number>",
"transaction": {
"current_owner": "<pub-key>",
"new_owner": "<pub-key>",
"input": "<sha3 hash>",
"fulfillments": ["<list of <fullfillment>"],
"conditions": ["<list of <condition>"],
"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"
}
"payload": "<generic json document>"
}
},
"signature": "<signature of the transaction>"
}
}
```
A transaction is an operation between the `current_owner` and the `new_owner` over the digital content described by `hash`. For example if could be a transfer of ownership of the digital content `hash`
- `id`: sha3 hash of the transaction. The `id` is also the DB primary key.
- `current_owner`: Public key of the current owner of the digital content with hash `hash`
- `new_owner`: Public key of the new owner of the digital content with hash `hash`
- `input`: id (sha3 hash) of the transaction in which the content was transfered to the user (similar to input in the blockchain). Right now we will assume that there is only one input per transaction to simplify the prototype. This can be changed in the future to allow multiple inputs per transaction.
- `operation`: String representation of the operation being performed (REGISTER, TRANSFER, ...) this will define how
the transactions should be validated
- `timestamp`: Time of creation of the transaction in UTC. It's provided by the client.
- `data`: JSON object describing the asset (digital content). It contains at least the field `hash` which is a
sha3 hash of the digital content.
- `signature`: Signature of the transaction with the `current_owner` private key
- **Transaction header**:
- `id`: sha3 hash of the transaction. The `id` is also the DB primary key.
- `version`: Version of the transaction. For future compability with changes in the transaction model.
- **Transaction body**:
- `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to a unspent digital asset
and a _crypto fulfillment_ that satisfies a spending condition set on the unspent digital asset. A _fulfillment_
is usually a signature proving the ownership of the digital asset.
See [conditions and fulfillments](models.md#conditions-and-fulfillments)
- `conditions`: List of conditions. Each _condition_ a _crypto condition_ that needs to be fulfilled by the
new owner in order to spend the digital asset.
See [conditions and fulfillments](models.md#conditions-and-fulfillments)
- `operation`: String representation of the operation being performed (`CREATE`, `TRANSFER`, ...) this will define how
the transactions should be validated
- `timestamp`: Time of creation of the transaction in UTC. It's provided by the client.
- `data`: JSON object describing the asset (digital content). It contains at least the field `hash` which is a
sha3 hash of the digital content.
## Conditions and Fulfillments
### Conditions
##### Simple Signature
If there is only one _new owner_ the condition will be a single signature condition.
```json
{
"cid": "<condition index>",
"condition": {
"details": {
"bitmask": "<base16 int>",
"public_key": "<explain>",
"signature": null,
"type": "fulfillment",
"type_id": "<base16 int>"
},
"uri": "<string>"
},
"new_owners": ["<list of <base58 string>>"]
}
```
- **Condition header**:
- `cid`: Condition index so that we can reference this output as an input to another transaction. It also matches
the input `fid`, making this the condition to fulfill in order to spend the digital asset used as input with `fid`
- `new_owners`: List of public keys of the new owners.
- **Condition body**:
- `bitmask`: a set of bits representing the features required by the condition type
- `public_key`: the base58 representation of the _new_owner's_ verifying key.
- `type_id`: the fulfillment type ID (see the [ILP spec](https://interledger.org/five-bells-condition/spec.html))
- `uri`: binary representation of the condition using only URL-safe characters
##### Multi Signature
If there are multiple _new owners_ by default we create a condition requiring a signature from each new owner in order
to spend the digital asset.
Example of a condition with two _new owners_:
```json
{
"cid": "<condition index>",
"condition": {
"details": {
"bitmask": 41,
"subfulfillments": [
{
"bitmask": 32,
"public_key": "<new owner 1 public key>",
"signature": null,
"type": "fulfillment",
"type_id": 4,
"weight": 1
},
{
"bitmask": 32,
"public_key": "<new owner 2 public key>",
"signature": null,
"type": "fulfillment",
"type_id": 4,
"weight": 1
}
],
"threshold": 2,
"type": "fulfillment",
"type_id": 2
},
"uri": "cc:2:29:ytNK3X6-bZsbF-nCGDTuopUIMi1HCyCkyPewm6oLI3o:206"},
"new_owners": [
"<new owner 1 public key>",
"<new owner 2 public key>"
]
}
```
- `subfulfillments`: a list of fulfillments
- `weight`: integer weight for each subfulfillment's contribution to the threshold
- `threshold`: threshold to reach for the subfulfillments to reach a valid fulfillment
### Fulfillments
##### Simple Signature
If there is only one _current owner_ the fulfillment will be a single signature fulfillment.
```json
{
"current_owners": ["<Public Key>"],
"fid": 0,
"fulfillment": "cf:4:RxFzIE679tFBk8zwEgizhmTuciAylvTUwy6EL6ehddHFJOhK5F4IjwQ1xLu2oQK9iyRCZJdfWAefZVjTt3DeG5j2exqxpGliOPYseNkRAWEakqJ_UrCwgnj92dnFRAEE",
"input": {
"cid": 0,
"txid": "11b3e7d893cc5fdfcf1a1706809c7def290a3b10b0bef6525d10b024649c42d3"
}
}
```
- `fid`: Fulfillment index. It matches a `cid` in the conditions with a new _crypto condition_ that the new owner(s)
need to fulfill to spend this digital asset
- `current_owners`: Public key of the current owner(s)
- `fulfillment`:
- `input`: Pointer to the digital asset and condition of a previous transaction
- `cid`: Condition index
- `txid`: Transaction id
## The Block Model

View File

@ -22,11 +22,11 @@ then point a browser tab to:
The login and password are `admin` by default. If BigchainDB is running and processing transactions, you should see analytics—if not, [start BigchainDB](installing-server.html#run-bigchaindb) and load some test transactions:
```text
$ bigchaindb-benchmark load
$ bigchaindb load
```
then refresh the page after a few seconds.
If you're not interested in monitoring, don't worry: BigchainDB will function just fine without any monitoring setup.
Feel free to modify the [custom Grafana dashboard](https://github.com/rhsimplex/grafana-bigchaindb-docker/blob/master/bigchaindb_dashboard.js) to your liking!
Feel free to modify the [custom Grafana dashboard](https://github.com/rhsimplex/grafana-bigchaindb-docker/blob/master/bigchaindb_dashboard.js) to your liking!

View File

@ -68,47 +68,143 @@ After a couple of seconds, we can check if the transactions was included in the
```python
# retrieve a transaction from the bigchain
tx_retrieved = b.get_transaction(tx_signed['id'])
{ 'id': '6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd',
'transaction': { 'current_owner': 'pvGtcm5dvwWMzCqagki1N6CDKYs2J1cCwTNw8CqJic3Q',
'data': { 'hash': '872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8',
'payload': {'msg': 'Hello BigchainDB!'}},
'input': None,
'new_owner': 'ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA',
'operation': 'CREATE',
'timestamp': '1455108421.753908'}}
tx_retrieved
```
The new owner of the digital asset is now `ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA`, which is the public key of `testuser1`.
```python
{
"id":"933cd83a419d2735822a2154c84176a2f419cbd449a74b94e592ab807af23861",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:oqXTWvR3afHHX8OaOO84kZxS6nH4GEBXD4Vw8Mc5iBo:96"
},
"new_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs"
]
}
],
"data":{
"hash":"872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8",
"payload":{
"msg":"Hello BigchainDB!"
}
},
"fulfillments":[
{
"current_owners":[
"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9"
],
"fid":0,
"fulfillment":"cf:4:Iq-BcczwraM2UpF-TDPdwK8fQ6IXkD_6uJaxBZd984yxCGX7Csx-S2FBVe8LVyW2sAtmjsOSV0oiw9-s_9qSJB0dDUl_x8YQk5yxNdQyNVWVM1mWSGQL68gMngdmFG8O",
"input":None
}
],
"operation":"CREATE",
"timestamp":"1460981667.449279"
},
"version":1
}
```
The new owner of the digital asset is now `BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs`, which is the public key of `testuser1`.
Note that the current owner with public key `3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9` refers to one of the federation nodes that actually created the asset and assigned it to `testuser1`.
## Transfer the Digital Asset
Now that `testuser1` has a digital asset assigned to him, he can transfer it to another user. Transfer transactions require an input. The input will be the transaction id of a digital asset that was assigned to `testuser1`, which in our case is `6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd`.
Now that `testuser1` has a digital asset assigned to him, he can transfer it to another user. Transfer transactions require an input. The input will be the transaction id of a digital asset that was assigned to `testuser1`, which in our case is `cdb6331f26ecec0ee7e67e4d5dcd63734e7f75bbd1ebe40699fc6d2960ae4cb2`.
BigchainDB makes use of the crypto-conditions library to both cryptographically lock and unlock transactions.
The locking script is refered to as a `condition` and a corresponding `fulfillment` unlocks the condition of the `input_tx`.
Since a transaction can have multiple outputs with each their own (crypto)condition, each transaction input should also refer to the condition index `cid`.
![BigchainDB transactions connecting fulfillments with conditions](./_static/tx_single_condition_single_fulfillment_v1.png)
```python
# create a second testuser
testuser2_priv, testuser2_pub = b.generate_keys()
# Create a second testuser
testuser2_priv, testuser2_pub = crypto.generate_key_pair()
# create a transfer transaction
tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved['id'], 'TRANSFER')
# Retrieve the transaction with condition id
tx_retrieved_id = b.get_owned_ids(testuser1_pub).pop()
tx_retrieved_id
```
# sign the transaction
```python
{
"cid":0,
"txid":"933cd83a419d2735822a2154c84176a2f419cbd449a74b94e592ab807af23861"
}
```
```python
# Create a transfer transaction
tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER')
# Sign the transaction
tx_transfer_signed = b.sign_transaction(tx_transfer, testuser1_priv)
# write the transaction
# Write the transaction
b.write_transaction(tx_transfer_signed)
# check if the transaction is already in the bigchain
# Check if the transaction is already in the bigchain
tx_transfer_retrieved = b.get_transaction(tx_transfer_signed['id'])
tx_transfer_retrieved
```
{ 'id': '1b78c313257540189f27da480152ed8c0b758569cdadd123d9810c057da408c3',
'signature': '3045022056166de447001db8ef024cfa1eecdba4306f92688920ac24325729d5a5068d47022100fbd495077cb1040c48bd7dc050b2515b296ca215cb5ce3369f094928e31955f6',
'transaction': { 'current_owner': 'ssQnnjketNYmbU3hwgFMEQsc4JVYAmZyWHnHCtFS8aeA',
'data': None,
'input': '6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd',
'new_owner': 'zVzophT73m4Wvf3f8gFYokddkYe3b9PbaMzobiUK7fmP',
'operation': 'TRANSFER',
'timestamp': '1455109497.480323'}}
```python
{
"id":"aa11365317cb89bfdae2375bae76d6b8232008f8672507080e3766ca06976dcd",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:DIfyalZvV_9ukoO01mxmK3nxsfAWSKYYF33XDYkbY4E:96"
},
"new_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs"
],
"fid":0,
"fulfillment":"cf:4:oqXTWvR3afHHX8OaOO84kZxS6nH4GEBXD4Vw8Mc5iBqzkVR6cFJhRvMGKa-Lc81sdYWVu0ZSMPGht-P7s6FZLkRXDqrwwInabLhjx14eABY34oHb6IyWcB-dyQnlVNEI",
"input":{
"cid":0,
"txid":"933cd83a419d2735822a2154c84176a2f419cbd449a74b94e592ab807af23861"
}
}
],
"operation":"TRANSFER",
"timestamp":"1460981677.472037"
},
"version":1
}
```
## Double Spends
@ -118,13 +214,674 @@ BigchainDB makes sure that a user can't transfer the same digital asset two or m
If we try to create another transaction with the same input as before, the transaction will be marked invalid and the validation will throw a double spend exception:
```python
# create another transfer transaction with the same input
tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved['id'], 'TRANSFER')
# Create another transfer transaction with the same input
tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER')
# sign the transaction
# Sign the transaction
tx_transfer_signed2 = b.sign_transaction(tx_transfer2, testuser1_priv)
# check if the transaction is valid
# Check if the transaction is valid
b.validate_transaction(tx_transfer_signed2)
Exception: input `6539dded9479c47b3c83385ae569ecaa90bcf387240d1ee2ea3ae0f7986aeddd` was already spent
```
```python
DoubleSpend: input `{'cid': 0, 'txid': '933cd83a419d2735822a2154c84176a2f419cbd449a74b94e592ab807af23861'}` was already spent
```
## Multiple Owners
When creating a transaction to a group of people with shared ownership of the asset, one can simply provide a list of `new_owners`:
```python
# Create a new asset and assign it to multiple owners
tx_multisig = b.create_transaction(b.me, [testuser1_pub, testuser2_pub], None, 'CREATE')
# Have the federation sign the transaction
tx_multisig_signed = b.sign_transaction(tx_multisig, b.me_private)
b.write_transaction(tx_multisig_signed)
# Check if the transaction is already in the bigchain
tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id'])
tx_multisig_retrieved
```
```python
{
"id":"a9a6e5c74ea02b8885c83125f1b74a2ba8ca42236ec5e1c358aa1053ec721ccb",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":41,
"subfulfillments":[
{
"bitmask":32,
"public_key":"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"signature":None,
"type":"fulfillment",
"type_id":4,
"weight":1
},
{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":None,
"type":"fulfillment",
"type_id":4,
"weight":1
}
],
"threshold":2,
"type":"fulfillment",
"type_id":2
},
"uri":"cc:2:29:DpflJzUSlnTUBx8lD8QUolOA-M9nQnrGwvWSk7f3REc:206"
},
"new_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9"
],
"fid":0,
"fulfillment":"cf:4:Iq-BcczwraM2UpF-TDPdwK8fQ6IXkD_6uJaxBZd984z5qdHRz9Jag68dkOyZS5_YoTR_0WpwiUnBGoNgwjwEuIn5JNm7Kksi0nUnHsWssyXISkmqRnHH-30HQhKjznIH",
"input":None
}
],
"operation":"CREATE",
"timestamp":"1460981687.501433"
},
"version":1
}
```
The asset can be transfered as soon as each of the `new_owners` signs the transaction.
To do so, simply provide a list of all private keys to the signing routine:
```python
# Create a third testuser
testuser3_priv, testuser3_pub = crypto.generate_key_pair()
# Retrieve the multisig transaction
tx_multisig_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
# Transfer the asset from the 2 owners to the third testuser
tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER')
# Sign with both private keys
tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv])
# Write to bigchain
b.write_transaction(tx_multisig_transfer_signed)
# Check if the transaction is already in the bigchain
tx_multisig_retrieved = b.get_transaction(tx_multisig_transfer_signed['id'])
tx_multisig_retrieved
```
```python
{
"assignee":"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9",
"id":"e689e23f774e7c562eeb310c7c712b34fb6210bea5deb9175e48b68810029150",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"8YN9fALMj9CkeCcmTiM2kxwurpkMzHg9RkwSLJKMasvG",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:cAq6JQJXtwlxURqrksiyqLThB9zh08ZxSPLTDSaReYE:96"
},
"new_owners":[
"8YN9fALMj9CkeCcmTiM2kxwurpkMzHg9RkwSLJKMasvG"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
],
"fid":0,
"fulfillment":"cf:4:oqXTWvR3afHHX8OaOO84kZxS6nH4GEBXD4Vw8Mc5iBrcuiGDNVgpH9SwiuNeYZ-nugSTbxykH8W1eH5UJiunmnBSlKnJb8_QYOQsMAXl3MyLq2pWAyI45ZSG1rr2CksI",
"input":{
"cid":0,
"txid":"aa11365317cb89bfdae2375bae76d6b8232008f8672507080e3766ca06976dcd"
}
}
],
"operation":"TRANSFER",
"timestamp":"1460981697.526878"
},
"version":1
}
```
## Multiple Inputs and Outputs
With BigchainDB it is possible to send multiple assets to someone in a single transfer.
The transaction will create a `fulfillment` - `condition` pair for each input, which can be refered to by `fid` and `cid` respectively.
![BigchainDB transactions connecting multiple fulfillments with multiple conditions](./_static/tx_multi_condition_multi_fulfillment_v1.png)
```python
# Create some assets for bulk transfer
for i in range(3):
tx_mimo_asset = b.create_transaction(b.me, testuser1_pub, None, 'CREATE')
tx_mimo_asset_signed = b.sign_transaction(tx_mimo_asset, b.me_private)
b.write_transaction(tx_mimo_asset_signed)
# Wait until they appear on the bigchain and get the inputs
owned_mimo_inputs = b.get_owned_ids(testuser1_pub)
# Check the number of assets
print(len(owned_mimo_inputs))
# Create a TRANSFER transaction with all the assets
tx_mimo = b.create_transaction(testuser1_pub, testuser2_pub, owned_mimo_inputs, 'TRANSFER')
tx_mimo_signed = b.sign_transaction(tx_mimo, testuser1_priv)
# Write the transaction
b.write_transaction(tx_mimo_signed)
# Check if the transaction is already in the bigchain
tx_mimo_retrieved = b.get_transaction(tx_mimo_signed['id'])
tx_mimo_retrieved
```
```python
{
"id":"8b63689691a3c2e8faba89c6efe3caa0661f862c14d88d1e63ebd65d49484de2",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:2AXg2JJ7mQ8o2Q9-hafP-XmFh3YR7I2_Sz55AubfxIc:96"
},
"new_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
},
{
"cid":1,
"condition":{
"details":{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:2AXg2JJ7mQ8o2Q9-hafP-XmFh3YR7I2_Sz55AubfxIc:96"
},
"new_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
},
{
"cid":2,
"condition":{
"details":{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:2AXg2JJ7mQ8o2Q9-hafP-XmFh3YR7I2_Sz55AubfxIc:96"
},
"new_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs"
],
"fid":0,
"fulfillment":"cf:4:sTzo4fvm8U8XrlXcgcGkNZgkfS9QHg2grgrJiX-c0LT_a83V0wbNRVbmb0eOy6tLyRw0kW1FtsN29yTcTAILX5-fyBITrPUqPzIzF85l8yIAMSjVfH-h6YNcUQBj0o4B",
"input":{
"cid":0,
"txid":"9a99f3c82aea23fb344acb1505926365e2c6b722761c4be6ab8916702c94c024"
}
},
{
"current_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs"
],
"fid":1,
"fulfillment":"cf:4:sTzo4fvm8U8XrlXcgcGkNZgkfS9QHg2grgrJiX-c0LSJe3B_yjgXd1JHPBJhAdywCzR_ykEezi3bPNucGHl5mgPvpsLpHWrdIvZa3arFD91AepXILaNCF0y8cxIBOyEE",
"input":{
"cid":0,
"txid":"783014b92f35da0c2526e1db6f81452c61853d29eda50d057fd043d507d03ef9"
}
},
{
"current_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs"
],
"fid":2,
"fulfillment":"cf:4:sTzo4fvm8U8XrlXcgcGkNZgkfS9QHg2grgrJiX-c0LReUQd-vDMseuVi03qY5Fxetv81fYpy3z1ncHIGc2bX7R69aS-yH5_deV9qaKjc1ZZFN5xXsB9WFpQkf9VQ-T8B",
"input":{
"cid":0,
"txid":"9ab6151334b06f3f3aab282597ee8a7c12b9d7a0c43f356713f7ef9663375f50"
}
}
],
"operation":"TRANSFER",
"timestamp":"1461049149.568927"
},
"version":1
}
```
## Crypto-Conditions (Advanced)
### Introduction
Crypto-conditions provide a mechanism to describe a signed message such that multiple actors in a distributed system can all verify the same signed message and agree on whether it matches the description.
This provides a useful primitive for event-based systems that are distributed on the Internet since we can describe events in a standard deterministic manner (represented by signed messages) and therefore define generic authenticated event handlers.
Crypto-conditions are part of the Interledger protocol and the full specification can be found [here](https://interledger.org/five-bells-condition/spec.html).
Implementations of the crypto-conditions are available in [Python](https://github.com/bigchaindb/cryptoconditions) and [JavaScript](https://github.com/interledger/five-bells-condition).
### Threshold Conditions
Threshold conditions introduce multi-signatures, m-of-n signatures or even more complex binary Merkle trees to BigchainDB.
Setting up a generic threshold condition is a bit more elaborate than regular transaction signing but allow for flexible signing between multiple parties or groups.
The basic workflow for creating a more complex cryptocondition is the following:
1. Create a transaction template that include the public key of all (nested) parties as `new_owners`
2. Set up the threshold condition using the [cryptocondition library](https://github.com/bigchaindb/cryptoconditions)
3. Update the condition and hash in the transaction template
We'll illustrate this by a threshold condition where 2 out of 3 `new_owners` need to sign the transaction:
```python
import copy
import json
import cryptoconditions as cc
from bigchaindb import util, crypto
# Create some new testusers
thresholduser1_priv, thresholduser1_pub = crypto.generate_key_pair()
thresholduser2_priv, thresholduser2_pub = crypto.generate_key_pair()
thresholduser3_priv, thresholduser3_pub = crypto.generate_key_pair()
# Retrieve the last transaction of testuser2
tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
# Create a base template for a 1-input/2-output transaction
threshold_tx = b.create_transaction(testuser2_pub, [thresholduser1_pub, thresholduser2_pub, thresholduser3_pub], tx_retrieved_id, 'TRANSFER')
# Create a Threshold Cryptocondition
threshold_condition = cc.ThresholdSha256Fulfillment(threshold=2)
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser1_pub))
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser2_pub))
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser3_pub))
# Update the condition in the newly created transaction
threshold_tx['transaction']['conditions'][0]['condition'] = {
'details': json.loads(threshold_condition.serialize_json()),
'uri': threshold_condition.condition.serialize_uri()
}
# Conditions have been updated, so the transaction hash (ID) needs updating
threshold_tx['id'] = util.get_hash_data(threshold_tx)
# Sign the transaction
threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv)
# Write the transaction
b.write_transaction(threshold_tx_signed)
# Check if the transaction is already in the bigchain
tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id'])
tx_threshold_retrieved
```
```python
{
"id":"0057d29ff735d91505decf5e7195ea8da675b01676165abf23ea774bbb469383",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":41,
"subfulfillments":[
{
"bitmask":32,
"public_key":"8NaGq26YMcEvj8Sc5MnqspKzFTQd1eZBAuuPDw4ERHpz",
"signature":None,
"type":"fulfillment",
"type_id":4,
"weight":1
},
{
"bitmask":32,
"public_key":"ALE9Agojob28D1fHWCxFXJwpqrYPkcsUs26YksBVj27z",
"signature":None,
"type":"fulfillment",
"type_id":4,
"weight":1
},
{
"bitmask":32,
"public_key":"Cx4jWSGci7fw6z5QyeApCijbwnMpyuhp4C1kzuFc3XrM",
"signature":None,
"type":"fulfillment",
"type_id":4,
"weight":1
}
],
"threshold":2,
"type":"fulfillment",
"type_id":2
},
"uri":"cc:2:29:FoElId4TE5TU2loonT7sayXhxwcmaJVoCeIduh56Dxw:246"
},
"new_owners":[
"8NaGq26YMcEvj8Sc5MnqspKzFTQd1eZBAuuPDw4ERHpz",
"ALE9Agojob28D1fHWCxFXJwpqrYPkcsUs26YksBVj27z",
"Cx4jWSGci7fw6z5QyeApCijbwnMpyuhp4C1kzuFc3XrM"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
],
"fid":0,
"fulfillment":"cf:4:DIfyalZvV_9ukoO01mxmK3nxsfAWSKYYF33XDYkbY4EbD7-_neXJJEe_tVTDc1_EqldlP_ulysFMprcW3VG4gzLzCMMpxA8kCr_pvywSFIEVYJHnI1csMvPivvBGHvkD",
"input":{
"cid":0,
"txid":"aa11365317cb89bfdae2375bae76d6b8232008f8672507080e3766ca06976dcd"
}
}
],
"operation":"TRANSFER",
"timestamp":"1460981707.559401"
},
"version":1
}
```
The transaction can now be transfered by fulfilling the threshold condition.
The fulfillment involves:
1. Create a transaction template that include the public key of all (nested) parties as `current_owners`
2. Parsing the threshold condition into a fulfillment using the [cryptocondition library](https://github.com/bigchaindb/cryptoconditions)
3. Signing all necessary subfulfillments and updating the fulfillment field in the transaction
```python
# Create a new testuser to receive
thresholduser4_priv, thresholduser4_pub = crypto.generate_key_pair()
# Retrieve the last transaction of thresholduser1_pub
tx_retrieved_id = b.get_owned_ids(thresholduser1_pub).pop()
# Create a base template for a 2-input/1-output transaction
threshold_tx_transfer = b.create_transaction([thresholduser1_pub, thresholduser2_pub, thresholduser3_pub], thresholduser4_pub, tx_retrieved_id, 'TRANSFER')
# Parse the threshold cryptocondition
threshold_fulfillment = cc.Fulfillment.from_json(threshold_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment1 = threshold_fulfillment.get_subcondition_from_vk(thresholduser1_pub)[0]
subfulfillment2 = threshold_fulfillment.get_subcondition_from_vk(thresholduser2_pub)[0]
subfulfillment3 = threshold_fulfillment.get_subcondition_from_vk(thresholduser3_pub)[0]
# Get the fulfillment message to sign
threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer,
threshold_tx_transfer['transaction']['fulfillments'][0],
serialized=True)
# Clear the subconditions of the threshold fulfillment, they will be added again after signing
threshold_fulfillment.subconditions = []
# Sign and add the subconditions until threshold of 2 is reached
subfulfillment1.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser1_priv))
threshold_fulfillment.add_subfulfillment(subfulfillment1)
subfulfillment2.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser2_priv))
threshold_fulfillment.add_subfulfillment(subfulfillment2)
# Add remaining (unfulfilled) fulfillment as a condition
threshold_fulfillment.add_subcondition(subfulfillment3.condition)
# Update the fulfillment
threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri()
# Optional validation checks
assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True
assert b.verify_signature(threshold_tx_transfer) == True
assert b.validate_transaction(threshold_tx_transfer)
b.write_transaction(threshold_tx_transfer)
threshold_tx_transfer
```
```python
{
"assignee":"3LQ5dTiddXymDhNzETB1rEkp4mA7fEV1Qeiu5ghHiJm9",
"id":"a45b2340c59df7422a5788b3c462dee708a18cdf09d1a10bd26be3f31af4b8d7",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"ED2pyPfsbNRTHkdMnaFkAwCSpZWRmbaM1h8fYzgRRMmc",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:xDz3NhRG-3eVzIB9sgnd99LKjOyDF-KlxWuf1TgNT0s:96"
},
"new_owners":[
"ED2pyPfsbNRTHkdMnaFkAwCSpZWRmbaM1h8fYzgRRMmc"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"8NaGq26YMcEvj8Sc5MnqspKzFTQd1eZBAuuPDw4ERHpz",
"ALE9Agojob28D1fHWCxFXJwpqrYPkcsUs26YksBVj27z",
"Cx4jWSGci7fw6z5QyeApCijbwnMpyuhp4C1kzuFc3XrM"
],
"fid":0,
"fulfillment":"cf:2:AQIBAwEBACcABAEgILGLuLLaNHo-KE59tkrpYmlVeucu16Eg9TcSuBqnMVwmAWABAWMABGBtiKCT8NBtSdnxJNdGYkyWqoRy2qOeNZ5UdUvpALcBD4vGRaohuVP9pQYNHpAA5GjTNNQT9CVMB67D8QL_DJsRU8ICSIVIG2P8pRqX6oia-304Xqq67wY-wLh_3IKlUg0AAQFjAARgiqYTeWkT6-jRMriCK4i8ceE2TwPys0JXgIrbw4kbwElVNnc7Aqw5c-Ts8-ymLp3d9_xTIb3-mPaV4JjhBqcobKuq2msJAjrxZOEeuYuAyC0tpduwTajOyp_Kmwzhdm8PAA",
"input":{
"cid":0,
"txid":"0057d29ff735d91505decf5e7195ea8da675b01676165abf23ea774bbb469383"
}
}
],
"operation":"TRANSFER",
"timestamp":"1460981717.579700"
},
"version":1
}
```
### Hash-locked Conditions
By creating a hash of a difficult-to-guess 256-bit random or pseudo-random integer it is possible to create a condition which the creator can trivially fulfill by publishing the random value. However, for anyone else, the condition is cryptographically hard to fulfill, because they would have to find a preimage for the given condition hash.
One possible usecase might be to redeem a digital voucher when given a secret (voucher code).
```python
# Create a hash-locked asset without any new_owners
hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE')
# Define a secret that will be hashed - fulfillments need to guess the secret
secret = b'much secret! wow!'
first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret)
# The conditions list is empty, so we need to append a new condition
hashlock_tx['transaction']['conditions'].append({
'condition': {
'uri': first_tx_condition.condition.serialize_uri()
},
'cid': 0,
'new_owners': None
})
# Conditions have been updated, so hash needs updating
hashlock_tx['id'] = util.get_hash_data(hashlock_tx)
# The asset needs to be signed by the current_owner
hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private)
# Some validations
assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed
assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed
b.write_transaction(hashlock_tx_signed)
hashlock_tx_signed
```
```python
{
"assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2",
"id":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"uri":"cc:0:3:nsW2IiYgk9EUtsg4uBe3pBnOgRoAEX2IIsPgjqZz47U:17"
},
"new_owners":None
}
],
"data":None,
"fulfillments":[
{
"current_owners":[
"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2"
],
"fid":0,
"fulfillment":"cf:4:21-D-LfNhIQhvY5914ArFTUGpgPKc7EVC1ZtJqqOTHGx1p9FuRr9tRfkbdqtX2MZWh7sRVUmMnwp7I1-xZbCnCkeADf69IwDHbZvNS6aTr1CpekREsV9ZG8m_wjlZiUN",
"input":None
}
],
"operation":"CREATE",
"timestamp":"1461250387.910102"
},
"version":1
}
```
In order to redeem the asset, one needs to create a fulfillment the correct secret as a preimage:
```python
hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair()
# create hashlock fulfillment tx
hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_pub, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER')
# provide a wrong secret
hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'')
hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \
hashlock_fulfill_tx_fulfillment.serialize_uri()
assert b.is_valid_transaction(hashlock_fulfill_tx) == False
# provide the right secret
hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret)
hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \
hashlock_fulfill_tx_fulfillment.serialize_uri()
assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
b.write_transaction(hashlock_fulfill_tx)
hashlock_fulfill_tx
```
```python
{
"assignee":"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2",
"id":"fe6871bf3ca62eb61c52c5555cec2e07af51df817723f0cb76e5cf6248f449d2",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":32,
"public_key":"EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb",
"signature":None,
"type":"fulfillment",
"type_id":4
},
"uri":"cc:4:20:y9884Md2YI_wdnGSTJGhwvFaNsKLe8sqwimqk-2JLSI:96"
},
"new_owners":[
"EiqCKxnBCmmNb83qyGch48tULK9RLaEt4xFA43UVCVDb"
]
}
],
"data":None,
"fulfillments":[
{
"current_owners":[],
"fid":0,
"fulfillment":"cf:0:bXVjaCBzZWNyZXQhIHdvdyE",
"input":{
"cid":0,
"txid":"604c520244b7ff63604527baf269e0cbfb887122f503703120fd347d6b99a237"
}
}
],
"operation":"TRANSFER",
"timestamp":"1461250397.944510"
},
"version":1
}
```

View File

@ -18,16 +18,3 @@ $ python setup.py test
(Aside: How does the above command work? The documentation for [pytest-runner](https://pypi.python.org/pypi/pytest-runner) explains. We use [pytest](http://pytest.org/latest/) to write all unit tests.)
### Using docker-compose to Run the Tests
You can also use `docker-compose` to run the unit tests. (You don't have to start RethinkDB first: `docker-compose` does that on its own, when it reads the `docker-compose.yml` file.)
First, build the images (~once), using:
```text
$ docker-compose build
```
then run the unit tests using:
```text
$ docker-compose run --rm bigchaindb py.test -v
```

View File

@ -65,23 +65,21 @@ setup(
entry_points={
'console_scripts': [
'bigchaindb=bigchaindb.commands.bigchain:main',
'bigchaindb-benchmark=bigchaindb.commands.bigchain_benchmark:main'
'bigchaindb=bigchaindb.commands.bigchain:main'
],
'bigchaindb.consensus': [
'default=bigchaindb.consensus:BaseConsensusRules'
]
},
install_requires=[
'rethinkdb==2.2.0.post4',
'rethinkdb==2.3.0',
'pysha3==0.3',
'pytz==2015.7',
'cryptoconditions==0.1.6',
'cryptoconditions==0.2.2',
'statsd==3.2.1',
'python-rapidjson==0.0.6',
'logstats==0.2.1',
'base58==0.2.2',
'bitcoin==1.1.42',
'flask==0.10.1',
'requests==2.9',
'gunicorn~=19.0',

View File

@ -25,8 +25,8 @@ CONFIG = {
}
# Test user. inputs will be created for this user. Cryptography Keys
USER_PRIVATE_KEY = '8eJ8q9ZQpReWyQT5aFCiwtZ5wDZC4eDnCen88p3tQ6ie'
USER_PUBLIC_KEY = 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'
USER_SIGNING_KEY = '8eJ8q9ZQpReWyQT5aFCiwtZ5wDZC4eDnCen88p3tQ6ie'
USER_VERIFYING_KEY = 'JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE'
# We need this function to avoid loading an existing
@ -54,13 +54,13 @@ def node_config():
@pytest.fixture
def user_private_key():
return USER_PRIVATE_KEY
def user_sk():
return USER_SIGNING_KEY
@pytest.fixture
def user_public_key():
return USER_PUBLIC_KEY
def user_vk():
return USER_VERIFYING_KEY
@pytest.fixture

View File

@ -81,7 +81,7 @@ def cleanup_tables(request, node_config):
@pytest.fixture
def inputs(user_public_key, amount=1, b=None):
def inputs(user_vk, amount=1, b=None):
# 1. create the genesis block
b = b or Bigchain()
try:
@ -92,7 +92,7 @@ def inputs(user_public_key, amount=1, b=None):
# 2. create block with transactions for `USER` to spend
transactions = []
for i in range(amount):
tx = b.create_transaction(b.me, user_public_key, None, 'CREATE')
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
transactions.append(tx_signed)
b.write_transaction(tx_signed)

File diff suppressed because it is too large Load Diff

View File

@ -125,7 +125,7 @@ class TestBigchainVoter(object):
# create a `TRANSFER` transaction
test_user2_priv, test_user2_pub = crypto.generate_key_pair()
tx2 = b.create_transaction(test_user_pub, test_user2_pub, tx['id'], 'TRANSFER')
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)
assert b.is_valid_transaction(tx2_signed)
@ -160,13 +160,13 @@ class TestBigchainVoter(object):
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_invalid_block_voting(self, b, user_public_key):
def test_invalid_block_voting(self, b, user_vk):
# create queue and voter
q_new_block = mp.Queue()
voter = Voter(q_new_block)
# create transaction
transaction = b.create_transaction(b.me, user_public_key, None, 'CREATE')
transaction = b.create_transaction(b.me, user_vk, None, 'CREATE')
transaction_signed = b.sign_transaction(transaction, b.me_private)
genesis = b.create_genesis_block()

0
tests/doc/__init__.py Normal file
View File

View File

@ -0,0 +1,292 @@
import json
from time import sleep
import cryptoconditions as cc
from bigchaindb import Bigchain, util, crypto, exceptions
b = Bigchain()
"""
Create a Digital Asset
"""
# create a test user
testuser1_priv, testuser1_pub = crypto.generate_key_pair()
# define a digital asset data payload
digital_asset_payload = {'msg': 'Hello BigchainDB!'}
# a create transaction uses the operation `CREATE` and has no inputs
tx = b.create_transaction(b.me, testuser1_pub, None, 'CREATE', payload=digital_asset_payload)
# all transactions need to be signed by the user creating the transaction
tx_signed = b.sign_transaction(tx, b.me_private)
# write the transaction to the bigchain
# the transaction will be stored in a backlog where it will be validated,
# included in a block, and written to the bigchain
b.write_transaction(tx_signed)
sleep(10)
"""
Read the Creation Transaction from the DB
"""
tx_retrieved = b.get_transaction(tx_signed['id'])
print(json.dumps(tx_retrieved, sort_keys=True, indent=4, separators=(',', ':')))
print(testuser1_pub)
print(b.me)
print(tx_retrieved['id'])
"""
Transfer the Digital Asset
"""
# create a second testuser
testuser2_priv, testuser2_pub = crypto.generate_key_pair()
# retrieve the transaction with condition id
tx_retrieved_id = b.get_owned_ids(testuser1_pub).pop()
print(json.dumps(tx_retrieved_id, sort_keys=True, indent=4, separators=(',', ':')))
# create a transfer transaction
tx_transfer = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER')
# sign the transaction
tx_transfer_signed = b.sign_transaction(tx_transfer, testuser1_priv)
# write the transaction
b.write_transaction(tx_transfer_signed)
sleep(10)
# check if the transaction is already in the bigchain
tx_transfer_retrieved = b.get_transaction(tx_transfer_signed['id'])
print(json.dumps(tx_transfer_retrieved, sort_keys=True, indent=4, separators=(',', ':')))
"""
Double Spends
"""
# create another transfer transaction with the same input
tx_transfer2 = b.create_transaction(testuser1_pub, testuser2_pub, tx_retrieved_id, 'TRANSFER')
# sign the transaction
tx_transfer_signed2 = b.sign_transaction(tx_transfer2, testuser1_priv)
# check if the transaction is valid
try:
b.validate_transaction(tx_transfer_signed2)
except exceptions.DoubleSpend as e:
print(e)
"""
Multiple Owners
"""
# Create a new asset and assign it to multiple owners
tx_multisig = b.create_transaction(b.me, [testuser1_pub, testuser2_pub], None, 'CREATE')
# Have the federation sign the transaction
tx_multisig_signed = b.sign_transaction(tx_multisig, b.me_private)
b.write_transaction(tx_multisig_signed)
# wait a few seconds for the asset to appear on the blockchain
sleep(10)
# retrieve the transaction
tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id'])
print(json.dumps(tx_multisig_retrieved, sort_keys=True, indent=4, separators=(',', ':')))
testuser3_priv, testuser3_pub = crypto.generate_key_pair()
tx_multisig_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER')
tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv])
b.write_transaction(tx_multisig_transfer_signed)
# wait a few seconds for the asset to appear on the blockchain
sleep(10)
# retrieve the transaction
tx_multisig_retrieved = b.get_transaction(tx_multisig_transfer_signed['id'])
print(json.dumps(tx_multisig_transfer_signed, sort_keys=True, indent=4, separators=(',', ':')))
"""
Multiple Inputs and Outputs
"""
for i in range(3):
tx_mimo_asset = b.create_transaction(b.me, testuser1_pub, None, 'CREATE')
tx_mimo_asset_signed = b.sign_transaction(tx_mimo_asset, b.me_private)
b.write_transaction(tx_mimo_asset_signed)
sleep(10)
# get inputs
owned_mimo_inputs = b.get_owned_ids(testuser1_pub)
print(len(owned_mimo_inputs))
# create a transaction
tx_mimo = b.create_transaction(testuser1_pub, testuser2_pub, owned_mimo_inputs, 'TRANSFER')
tx_mimo_signed = b.sign_transaction(tx_mimo, testuser1_priv)
# write the transaction
b.write_transaction(tx_mimo_signed)
print(json.dumps(tx_mimo_signed, sort_keys=True, indent=4, separators=(',', ':')))
"""
Threshold Conditions
"""
# create some new testusers
thresholduser1_priv, thresholduser1_pub = crypto.generate_key_pair()
thresholduser2_priv, thresholduser2_pub = crypto.generate_key_pair()
thresholduser3_priv, thresholduser3_pub = crypto.generate_key_pair()
# retrieve the last transaction of testuser2
tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
# create a base template for a 1-input/3-output transaction
threshold_tx = b.create_transaction(testuser2_pub, [thresholduser1_pub, thresholduser2_pub, thresholduser3_pub],
tx_retrieved_id, 'TRANSFER')
# create a 2-out-of-3 Threshold Cryptocondition
threshold_condition = cc.ThresholdSha256Fulfillment(threshold=2)
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser1_pub))
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser2_pub))
threshold_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=thresholduser3_pub))
# update the condition in the newly created transaction
threshold_tx['transaction']['conditions'][0]['condition'] = {
'details': json.loads(threshold_condition.serialize_json()),
'uri': threshold_condition.condition.serialize_uri()
}
# conditions have been updated, so hash needs updating
threshold_tx['id'] = util.get_hash_data(threshold_tx)
# sign the transaction
threshold_tx_signed = b.sign_transaction(threshold_tx, testuser2_priv)
# write the transaction
b.write_transaction(threshold_tx_signed)
sleep(10)
# check if the transaction is already in the bigchain
tx_threshold_retrieved = b.get_transaction(threshold_tx_signed['id'])
print(json.dumps(tx_threshold_retrieved, sort_keys=True, indent=4, separators=(',', ':')))
thresholduser4_priv, thresholduser4_pub = crypto.generate_key_pair()
# retrieve the last transaction of thresholduser1_pub
tx_retrieved_id = b.get_owned_ids(thresholduser1_pub).pop()
# create a base template for a 2-input/1-output transaction
threshold_tx_transfer = b.create_transaction([thresholduser1_pub, thresholduser2_pub, thresholduser3_pub],
thresholduser4_pub, tx_retrieved_id, 'TRANSFER')
# parse the threshold cryptocondition
threshold_fulfillment = cc.Fulfillment.from_json(threshold_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment1 = threshold_fulfillment.get_subcondition_from_vk(thresholduser1_pub)[0]
subfulfillment2 = threshold_fulfillment.get_subcondition_from_vk(thresholduser2_pub)[0]
subfulfillment3 = threshold_fulfillment.get_subcondition_from_vk(thresholduser3_pub)[0]
# get the fulfillment message to sign
threshold_tx_fulfillment_message = util.get_fulfillment_message(threshold_tx_transfer,
threshold_tx_transfer['transaction']['fulfillments'][0],
serialized=True)
# clear the subconditions of the threshold fulfillment, they will be added again after signing
threshold_fulfillment.subconditions = []
# sign and add the subconditions until threshold of 2 is reached
subfulfillment1.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser1_priv))
threshold_fulfillment.add_subfulfillment(subfulfillment1)
subfulfillment2.sign(threshold_tx_fulfillment_message, crypto.SigningKey(thresholduser2_priv))
threshold_fulfillment.add_subfulfillment(subfulfillment2)
# Add remaining (unfulfilled) fulfillment as a condition
threshold_fulfillment.add_subcondition(subfulfillment3.condition)
assert threshold_fulfillment.validate(threshold_tx_fulfillment_message) == True
threshold_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = threshold_fulfillment.serialize_uri()
assert b.verify_signature(threshold_tx_transfer) == True
assert b.validate_transaction(threshold_tx_transfer) == threshold_tx_transfer
b.write_transaction(threshold_tx_transfer)
print(json.dumps(threshold_tx_transfer, sort_keys=True, indent=4, separators=(',', ':')))
"""
Hashlocked Conditions
"""
# Create a hash-locked asset without any new_owners
hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE')
# Define a secret that will be hashed - fulfillments need to guess the secret
secret = b'much secret! wow!'
first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret)
# The conditions list is empty, so we need to append a new condition
hashlock_tx['transaction']['conditions'].append({
'condition': {
'uri': first_tx_condition.condition.serialize_uri()
},
'cid': 0,
'new_owners': None
})
# Conditions have been updated, so hash needs updating
hashlock_tx['id'] = util.get_hash_data(hashlock_tx)
# The asset needs to be signed by the current_owner
hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private)
# Some validations
assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed
assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed
b.write_transaction(hashlock_tx_signed)
print(json.dumps(hashlock_tx_signed, sort_keys=True, indent=4, separators=(',', ':')))
sleep(10)
hashlockuser_priv, hashlockuser_pub = crypto.generate_key_pair()
# create hashlock fulfillment tx
hashlock_fulfill_tx = b.create_transaction(None, hashlockuser_priv, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER')
# try a wrong secret
hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'')
hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \
hashlock_fulfill_tx_fulfillment.serialize_uri()
assert b.is_valid_transaction(hashlock_fulfill_tx) == False
# provide the right secret
hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret)
hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \
hashlock_fulfill_tx_fulfillment.serialize_uri()
assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
b.write_transaction(hashlock_fulfill_tx)
print(json.dumps(hashlock_fulfill_tx, sort_keys=True, indent=4, separators=(',', ':')))

View File

@ -21,6 +21,13 @@ def mock_requests_post(monkeypatch):
monkeypatch.setattr('requests.post', mockreturn)
@pytest.fixture
def mock_bigchaindb_sign(monkeypatch):
def mockreturn(transaction, private_key):
return transaction
monkeypatch.setattr('bigchaindb.util.sign_tx', mockreturn)
def test_temp_client_returns_a_temp_client():
from bigchaindb.client import temp_client
@ -29,6 +36,7 @@ def test_temp_client_returns_a_temp_client():
assert client.private_key
@pytest.mark.usefixtures('restore_config')
def test_client_can_create_assets(mock_requests_post, client):
from bigchaindb import util
@ -39,21 +47,19 @@ def test_client_can_create_assets(mock_requests_post, client):
# `current_owner` 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']['current_owner'] == client.public_key
assert tx['transaction']['new_owner'] == client.public_key
assert tx['transaction']['input'] == None
assert tx['transaction']['fulfillments'][0]['current_owners'][0] == client.public_key
assert tx['transaction']['conditions'][0]['new_owners'][0] == client.public_key
assert tx['transaction']['fulfillments'][0]['input'] is None
assert util.verify_signature(tx)
def test_client_can_transfer_assets(mock_requests_post, client):
def test_client_can_transfer_assets(mock_requests_post, mock_bigchaindb_sign, client):
from bigchaindb import util
tx = client.transfer('a', 123)
tx = client.transfer(client.public_key, 123)
assert tx['transaction']['current_owner'] == client.public_key
assert tx['transaction']['new_owner'] == 'a'
assert tx['transaction']['input'] == 123
assert util.verify_signature(tx)
assert tx['transaction']['fulfillments'][0]['current_owners'][0] == client.public_key
assert tx['transaction']['conditions'][0]['new_owners'][0] == client.public_key
assert tx['transaction']['fulfillments'][0]['input'] == 123

View File

@ -1,4 +1,5 @@
import json
from unittest.mock import Mock, patch
from argparse import Namespace
from pprint import pprint
import copy
@ -62,10 +63,22 @@ def mock_bigchaindb_backup_config(monkeypatch):
def test_bigchain_run_start(mock_run_configure, mock_processes_start, mock_db_init_with_existing_db):
from bigchaindb.commands.bigchain import run_start
args = Namespace(config=None, yes=True)
args = Namespace(start_rethinkdb=False, config=None, yes=True)
run_start(args)
@patch('bigchaindb.commands.utils.start_rethinkdb')
def test_bigchain_run_start_with_rethinkdb(mock_start_rethinkdb,
mock_run_configure,
mock_processes_start,
mock_db_init_with_existing_db):
from bigchaindb.commands.bigchain import run_start
args = Namespace(start_rethinkdb=True, config=None, yes=True)
run_start(args)
mock_start_rethinkdb.assert_called_with()
@pytest.mark.skipif(reason="BigchainDB doesn't support the automatic creation of a config file anymore")
def test_bigchain_run_start_assume_yes_create_default_config(monkeypatch, mock_processes_start,
mock_generate_key_pair, mock_db_init_with_existing_db):
@ -173,7 +186,7 @@ def test_run_configure_when_config_does_not_exist(monkeypatch,
mock_bigchaindb_backup_config):
from bigchaindb.commands.bigchain import run_configure
monkeypatch.setattr('os.path.exists', lambda path: False)
monkeypatch.setattr('builtins.input', lambda question: '\n')
monkeypatch.setattr('builtins.input', lambda: '\n')
args = Namespace(config='foo', yes=True)
return_value = run_configure(args)
assert return_value is None
@ -189,9 +202,26 @@ def test_run_configure_when_config_does_exist(monkeypatch,
from bigchaindb.commands.bigchain import run_configure
monkeypatch.setattr('os.path.exists', lambda path: True)
monkeypatch.setattr('builtins.input', lambda question: '\n')
monkeypatch.setattr('builtins.input', lambda: '\n')
monkeypatch.setattr('bigchaindb.config_utils.write_config', mock_write_config)
args = Namespace(config='foo', yes=None)
run_configure(args)
assert value == {}
@patch('subprocess.Popen')
def test_start_rethinkdb_returns_a_process_when_successful(mock_popen):
from bigchaindb.commands import utils
mock_popen.return_value = Mock(stdout=['Server ready'])
assert utils.start_rethinkdb() is mock_popen.return_value
@patch('subprocess.Popen')
def test_start_rethinkdb_exits_when_cannot_start(mock_popen):
from bigchaindb import exceptions
from bigchaindb.commands import utils
mock_popen.return_value = Mock(stdout=['Nopety nope'])
with pytest.raises(exceptions.StartupError):
utils.start_rethinkdb()

View File

@ -26,15 +26,14 @@ def mock_queue(monkeypatch):
return mockqueue
def test_transform_create(b, user_private_key, user_public_key):
def test_transform_create(b, user_sk, user_vk):
from bigchaindb import util
tx = util.create_tx(user_public_key, user_public_key, None, 'CREATE')
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']['current_owner'] == b.me
assert tx['transaction']['new_owner'] == user_public_key
assert tx['transaction']['fulfillments'][0]['current_owners'][0] == b.me
assert tx['transaction']['conditions'][0]['new_owners'][0] == user_vk
assert util.verify_signature(tx)

View File

@ -30,6 +30,6 @@ def app(request, node_config):
@pytest.fixture
def inputs(user_public_key):
conftest.inputs(user_public_key)
def inputs(user_vk):
conftest.inputs(user_vk)

View File

@ -9,10 +9,10 @@ TX_ENDPOINT = '/api/v1/transactions/'
@pytest.mark.usefixtures('inputs')
def test_get_transaction_endpoint(b, client, user_public_key):
input_tx = b.get_owned_ids(user_public_key).pop()
tx = b.get_transaction(input_tx)
res = client.get(TX_ENDPOINT + input_tx)
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'])
assert tx == res.json
@ -22,21 +22,18 @@ def test_post_create_transaction_endpoint(b, client):
tx = util.create_and_sign_tx(keypair[0], keypair[1], keypair[1], None, 'CREATE')
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
assert res.json['transaction']['current_owner'] == b.me
assert res.json['transaction']['new_owner'] == keypair[1]
assert res.json['transaction']['fulfillments'][0]['current_owners'][0] == b.me
assert res.json['transaction']['conditions'][0]['new_owners'][0] == keypair[1]
def test_post_transfer_transaction_endpoint(b, client):
from_keypair = crypto.generate_key_pair()
@pytest.mark.usefixtures('inputs')
def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk):
to_keypair = crypto.generate_key_pair()
input_valid = b.get_owned_ids(user_vk).pop()
tx = util.create_and_sign_tx(from_keypair[0], from_keypair[1], from_keypair[1], None, 'CREATE')
res = client.post(TX_ENDPOINT, data=json.dumps(tx))
tx_id = res.json['id']
transfer = util.create_and_sign_tx(from_keypair[0], from_keypair[1], to_keypair[1], tx_id)
transfer = util.create_and_sign_tx(user_sk, user_vk, to_keypair[1], input_valid)
res = client.post(TX_ENDPOINT, data=json.dumps(transfer))
assert res.json['transaction']['current_owner'] == from_keypair[1]
assert res.json['transaction']['new_owner'] == to_keypair[1]
assert res.json['transaction']['fulfillments'][0]['current_owners'][0] == user_vk
assert res.json['transaction']['conditions'][0]['new_owners'][0] == to_keypair[1]