mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Resolved merge conflicts with tendermint branch
This commit is contained in:
commit
d6a7500abc
@ -22,6 +22,15 @@ def store_transaction(conn, signed_transaction):
|
||||
pass
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def store_transactions(conn, signed_transactions):
|
||||
try:
|
||||
return conn.run(conn.collection('transactions')
|
||||
.insert_many(signed_transactions))
|
||||
except DuplicateKeyError:
|
||||
pass
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def get_transaction(conn, transaction_id):
|
||||
try:
|
||||
@ -33,7 +42,18 @@ def get_transaction(conn, transaction_id):
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def store_metadata(conn, metadata):
|
||||
def get_transactions(conn, transaction_ids):
|
||||
try:
|
||||
return conn.run(
|
||||
conn.collection('transactions')
|
||||
.find({'id': {'$in': transaction_ids}},
|
||||
projection={'_id': False}))
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def store_metadatas(conn, metadata):
|
||||
try:
|
||||
return conn.run(
|
||||
conn.collection('metadata')
|
||||
@ -60,6 +80,16 @@ def store_asset(conn, asset):
|
||||
pass
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def store_assets(conn, assets):
|
||||
try:
|
||||
return conn.run(
|
||||
conn.collection('assets')
|
||||
.insert_many(assets, ordered=False))
|
||||
except DuplicateKeyError:
|
||||
pass
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def get_asset(conn, asset_id):
|
||||
try:
|
||||
@ -153,3 +183,19 @@ def get_spending_transactions(conn, inputs):
|
||||
{'$project': {'_id': False}}
|
||||
]))
|
||||
return cursor
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def get_block(conn, block_id):
|
||||
return conn.run(
|
||||
conn.collection('blocks')
|
||||
.find_one({'height': block_id},
|
||||
projection={'_id': False}))
|
||||
|
||||
|
||||
@register_query(LocalMongoDBConnection)
|
||||
def get_block_with_transaction(conn, txid):
|
||||
return conn.run(
|
||||
conn.collection('blocks')
|
||||
.find({'transactions': txid},
|
||||
projection={'_id': False, 'height': True}))
|
||||
|
@ -34,11 +34,25 @@ def store_asset(connection, asset):
|
||||
|
||||
|
||||
@singledispatch
|
||||
def store_metadata(connection, metadata):
|
||||
"""Write metadata to the metadata table.
|
||||
def store_assets(connection, assets):
|
||||
"""Write a list of assets to the assets table.
|
||||
|
||||
Args:
|
||||
metadata (dict): transaction metadata.
|
||||
assets (list): a list of assets to write.
|
||||
|
||||
Returns:
|
||||
The database response.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def store_metadatas(connection, metadata):
|
||||
"""Write a list of metadata to metadata table.
|
||||
|
||||
Args:
|
||||
metadata (list): list of metadata.
|
||||
|
||||
Returns:
|
||||
The result of the operation.
|
||||
@ -54,6 +68,13 @@ def store_transaction(connection, signed_transaction):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def store_transactions(connection, signed_transactions):
|
||||
"""Store list of transactions."""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def get_transaction(connection, transaction_id):
|
||||
"""Get a transaction from the transactions table.
|
||||
@ -68,6 +89,20 @@ def get_transaction(connection, transaction_id):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def get_transactions(connection, transaction_ids):
|
||||
"""Get transactions from the transactions table.
|
||||
|
||||
Args:
|
||||
transaction_ids (list): list of transaction ids to fetch
|
||||
|
||||
Returns:
|
||||
The result of the operation.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def get_asset(connection, asset_id):
|
||||
"""Get a transaction from the transactions table.
|
||||
@ -304,6 +339,20 @@ def get_block(connection, block_id):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def get_block_with_transaction(connection, txid):
|
||||
"""Get a block containing transaction id `txid`
|
||||
|
||||
Args:
|
||||
txid (str): id of transaction to be searched.
|
||||
|
||||
Returns:
|
||||
block_id (int): the block id or `None`
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@singledispatch
|
||||
def write_assets(connection, assets):
|
||||
"""Write a list of assets to the assets table.
|
||||
|
@ -9,6 +9,7 @@ import base58
|
||||
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256
|
||||
from cryptoconditions.exceptions import (
|
||||
ParsingError, ASN1DecodeError, ASN1EncodeError, UnsupportedTypeError)
|
||||
from sha3 import sha3_256
|
||||
|
||||
from bigchaindb.common.crypto import PrivateKey, hash_data
|
||||
from bigchaindb.common.exceptions import (KeypairMismatchException,
|
||||
@ -812,13 +813,16 @@ class Transaction(object):
|
||||
# this should never happen, but then again, never say never.
|
||||
input_ = deepcopy(input_)
|
||||
public_key = input_.owners_before[0]
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update('{}{}'.format(
|
||||
input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
try:
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
input_.fulfillment.sign(
|
||||
message.encode(),
|
||||
base58.b58decode(key_pairs[public_key].encode()),
|
||||
)
|
||||
message.digest(), base58.b58decode(key_pairs[public_key].encode()))
|
||||
except KeyError:
|
||||
raise KeypairMismatchException('Public key {} is not a pair to '
|
||||
'any of the private keys'
|
||||
@ -836,6 +840,11 @@ class Transaction(object):
|
||||
key_pairs (dict): The keys to sign the Transaction with.
|
||||
"""
|
||||
input_ = deepcopy(input_)
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update('{}{}'.format(
|
||||
input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
for owner_before in set(input_.owners_before):
|
||||
# TODO: CC should throw a KeypairMismatchException, instead of
|
||||
# our manual mapping here
|
||||
@ -863,7 +872,8 @@ class Transaction(object):
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
for subffill in subffills:
|
||||
subffill.sign(message.encode(), base58.b58decode(private_key.encode()))
|
||||
subffill.sign(
|
||||
message.digest(), base58.b58decode(private_key.encode()))
|
||||
return input_
|
||||
|
||||
def inputs_valid(self, outputs=None):
|
||||
@ -931,7 +941,7 @@ class Transaction(object):
|
||||
for i, cond in enumerate(output_condition_uris))
|
||||
|
||||
@staticmethod
|
||||
def _input_valid(input_, operation, tx_serialized, output_condition_uri=None):
|
||||
def _input_valid(input_, operation, message, output_condition_uri=None):
|
||||
"""Validates a single Input against a single Output.
|
||||
|
||||
Note:
|
||||
@ -942,8 +952,7 @@ class Transaction(object):
|
||||
input_ (:class:`~bigchaindb.common.transaction.
|
||||
Input`) The Input to be signed.
|
||||
operation (str): The type of Transaction.
|
||||
tx_serialized (str): The Transaction used as a message when
|
||||
initially signing it.
|
||||
message (str): The fulfillment message.
|
||||
output_condition_uri (str, optional): An Output to check the
|
||||
Input against.
|
||||
|
||||
@ -964,12 +973,17 @@ class Transaction(object):
|
||||
else:
|
||||
output_valid = output_condition_uri == ccffill.condition_uri
|
||||
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update('{}{}'.format(
|
||||
input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
# NOTE: We pass a timestamp to `.validate`, as in case of a timeout
|
||||
# condition we'll have to validate against it
|
||||
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
ffill_valid = parsed_ffill.validate(message=tx_serialized.encode())
|
||||
ffill_valid = parsed_ffill.validate(message=message.digest())
|
||||
return output_valid and ffill_valid
|
||||
|
||||
def to_dict(self):
|
||||
|
@ -14,7 +14,7 @@ from bigchaindb.backend.schema import validate_language_key
|
||||
|
||||
|
||||
class Transaction(Transaction):
|
||||
def validate(self, bigchain):
|
||||
def validate(self, bigchain, current_transactions=[]):
|
||||
"""Validate transaction spend
|
||||
|
||||
Args:
|
||||
@ -31,7 +31,8 @@ class Transaction(Transaction):
|
||||
input_conditions = []
|
||||
|
||||
if self.operation == Transaction.CREATE:
|
||||
if bigchain.get_transaction(self.to_dict()['id']):
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == self.id)
|
||||
if bigchain.get_transaction(self.to_dict()['id']) or duplicates:
|
||||
raise DuplicateTransaction('transaction `{}` already exists'
|
||||
.format(self.id))
|
||||
elif self.operation == Transaction.TRANSFER:
|
||||
@ -42,6 +43,14 @@ class Transaction(Transaction):
|
||||
input_tx, status = bigchain.\
|
||||
get_transaction(input_txid, include_status=True)
|
||||
|
||||
if input_tx is None:
|
||||
for ctxn in current_transactions:
|
||||
# assume that the status as valid for previously validated
|
||||
# transactions in current round
|
||||
if ctxn.id == input_txid:
|
||||
input_tx = ctxn
|
||||
status = bigchain.TX_VALID
|
||||
|
||||
if input_tx is None:
|
||||
raise InputDoesNotExist("input `{}` doesn't exist"
|
||||
.format(input_txid))
|
||||
@ -51,7 +60,8 @@ class Transaction(Transaction):
|
||||
'input `{}` does not exist in a valid block'.format(
|
||||
input_txid))
|
||||
|
||||
spent = bigchain.get_spent(input_txid, input_.fulfills.output)
|
||||
spent = bigchain.get_spent(input_txid, input_.fulfills.output,
|
||||
current_transactions)
|
||||
if spent and spent.id != self.id:
|
||||
raise DoubleSpend('input `{}` was already spent'
|
||||
.format(input_txid))
|
||||
@ -96,7 +106,7 @@ class Transaction(Transaction):
|
||||
return super().from_dict(tx_body)
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, bigchain, tx_dict):
|
||||
def from_db(cls, bigchain, tx_dict_list):
|
||||
"""Helper method that reconstructs a transaction dict that was returned
|
||||
from the database. It checks what asset_id to retrieve, retrieves the
|
||||
asset from the asset table and reconstructs the transaction.
|
||||
@ -104,29 +114,46 @@ class Transaction(Transaction):
|
||||
Args:
|
||||
bigchain (:class:`~bigchaindb.Bigchain`): An instance of Bigchain
|
||||
used to perform database queries.
|
||||
tx_dict (:obj:`dict`): The transaction dict as returned from the
|
||||
database.
|
||||
tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or
|
||||
list of transaction dict as returned from the database.
|
||||
|
||||
Returns:
|
||||
:class:`~Transaction`
|
||||
|
||||
"""
|
||||
if tx_dict['operation'] in [Transaction.CREATE, Transaction.GENESIS]:
|
||||
# TODO: Maybe replace this call to a call to get_asset_by_id
|
||||
asset = list(bigchain.get_assets([tx_dict['id']]))[0]
|
||||
return_list = True
|
||||
if isinstance(tx_dict_list, dict):
|
||||
tx_dict_list = [tx_dict_list]
|
||||
return_list = False
|
||||
|
||||
tx_map = {}
|
||||
tx_ids = []
|
||||
for tx in tx_dict_list:
|
||||
tx.update({'metadata': None})
|
||||
tx_map[tx['id']] = tx
|
||||
if tx['operation'] in [Transaction.CREATE, Transaction.GENESIS]:
|
||||
tx_ids.append(tx['id'])
|
||||
|
||||
assets = list(bigchain.get_assets(tx_ids))
|
||||
for asset in assets:
|
||||
tx = tx_map[asset['id']]
|
||||
del asset['id']
|
||||
tx_dict.update({'asset': asset})
|
||||
tx.update({'asset': asset})
|
||||
|
||||
# get metadata of the transaction
|
||||
metadata = list(bigchain.get_metadata([tx_dict['id']]))
|
||||
if 'metadata' not in tx_dict:
|
||||
metadata = metadata[0] if metadata else None
|
||||
if metadata:
|
||||
metadata = metadata.get('metadata')
|
||||
tx_ids = list(tx_map.keys())
|
||||
metadata_list = list(bigchain.get_metadata(tx_ids))
|
||||
for metadata in metadata_list:
|
||||
tx = tx_map[metadata['id']]
|
||||
tx.update({'metadata': metadata.get('metadata')})
|
||||
|
||||
tx_dict.update({'metadata': metadata})
|
||||
|
||||
return cls.from_dict(tx_dict)
|
||||
if return_list:
|
||||
tx_list = []
|
||||
for tx_id, tx in tx_map.items():
|
||||
tx_list.append(cls.from_dict(tx))
|
||||
return tx_list
|
||||
else:
|
||||
tx = list(tx_map.values())[0]
|
||||
return cls.from_dict(tx)
|
||||
|
||||
|
||||
class Block(object):
|
||||
|
@ -25,13 +25,14 @@ class App(BaseApplication):
|
||||
self.bigchaindb = bigchaindb
|
||||
self.block_txn_ids = []
|
||||
self.block_txn_hash = ''
|
||||
self.block_transactions = []
|
||||
self.validators = None
|
||||
self.new_height = None
|
||||
|
||||
def init_chain(self, validators):
|
||||
"""Initialize chain with block of height 0"""
|
||||
|
||||
block = Block(app_hash='', height=0)
|
||||
block = Block(app_hash='', height=0, transactions=[])
|
||||
self.bigchaindb.store_block(block._asdict())
|
||||
|
||||
def info(self):
|
||||
@ -70,6 +71,7 @@ class App(BaseApplication):
|
||||
"""
|
||||
|
||||
self.block_txn_ids = []
|
||||
self.block_transactions = []
|
||||
|
||||
def deliver_tx(self, raw_transaction):
|
||||
"""Validate the transaction before mutating the state.
|
||||
@ -78,15 +80,16 @@ class App(BaseApplication):
|
||||
raw_tx: a raw string (in bytes) transaction."""
|
||||
logger.debug('deliver_tx: %s', raw_transaction)
|
||||
transaction = self.bigchaindb.validate_transaction(
|
||||
decode_transaction(raw_transaction))
|
||||
decode_transaction(raw_transaction), self.block_transactions)
|
||||
|
||||
if not transaction:
|
||||
logger.debug('deliver_tx: INVALID')
|
||||
return Result.error(log='Invalid transaction')
|
||||
else:
|
||||
logger.debug('storing tx')
|
||||
self.bigchaindb.store_transaction(transaction)
|
||||
# self.bigchaindb.store_transaction(transaction)
|
||||
self.block_txn_ids.append(transaction.id)
|
||||
self.block_transactions.append(transaction)
|
||||
return Result.ok()
|
||||
|
||||
def end_block(self, height):
|
||||
@ -112,7 +115,10 @@ class App(BaseApplication):
|
||||
|
||||
# register a new block only when new transactions are received
|
||||
if self.block_txn_ids:
|
||||
block = Block(app_hash=self.block_txn_hash, height=self.new_height)
|
||||
self.bigchaindb.store_bulk_transactions(self.block_transactions)
|
||||
block = Block(app_hash=self.block_txn_hash,
|
||||
height=self.new_height,
|
||||
transactions=self.block_txn_ids)
|
||||
self.bigchaindb.store_block(block._asdict())
|
||||
|
||||
data = self.block_txn_hash.encode('utf-8')
|
||||
|
@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from os import getenv
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
from bigchaindb.common.utils import gen_timestamp
|
||||
@ -10,8 +11,8 @@ from bigchaindb.events import EventTypes, Event
|
||||
from bigchaindb.tendermint.utils import decode_transaction_base64
|
||||
|
||||
|
||||
HOST = 'localhost'
|
||||
PORT = 46657
|
||||
HOST = getenv('TENDERMINT_HOST', 'localhost')
|
||||
PORT = int(getenv('TENDERMINT_PORT', 46657))
|
||||
URL = f'ws://{HOST}:{PORT}/websocket'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -12,6 +12,7 @@ from bigchaindb.models import Transaction
|
||||
from bigchaindb.common.exceptions import SchemaValidationError, ValidationError
|
||||
from bigchaindb.tendermint.utils import encode_transaction
|
||||
from bigchaindb.tendermint import fastquery
|
||||
from bigchaindb import exceptions as core_exceptions
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -19,15 +20,21 @@ logger = logging.getLogger(__name__)
|
||||
TENDERMINT_HOST = getenv('TENDERMINT_HOST', 'localhost')
|
||||
TENDERMINT_PORT = getenv('TENDERMINT_PORT', '46657')
|
||||
ENDPOINT = 'http://{}:{}/'.format(TENDERMINT_HOST, TENDERMINT_PORT)
|
||||
MODE_LIST = ('broadcast_tx_async',
|
||||
'broadcast_tx_sync',
|
||||
'broadcast_tx_commit')
|
||||
|
||||
|
||||
class BigchainDB(Bigchain):
|
||||
|
||||
def post_transaction(self, transaction):
|
||||
def post_transaction(self, transaction, mode):
|
||||
"""Submit a valid transaction to the mempool."""
|
||||
if not mode or mode not in MODE_LIST:
|
||||
raise ValidationError(('Mode must be one of the following {}.')
|
||||
.format(', '.join(MODE_LIST)))
|
||||
|
||||
payload = {
|
||||
'method': 'broadcast_tx_async',
|
||||
'method': mode,
|
||||
'jsonrpc': '2.0',
|
||||
'params': [encode_transaction(transaction.to_dict())],
|
||||
'id': str(uuid4())
|
||||
@ -35,11 +42,10 @@ class BigchainDB(Bigchain):
|
||||
# TODO: handle connection errors!
|
||||
requests.post(ENDPOINT, json=payload)
|
||||
|
||||
def write_transaction(self, transaction):
|
||||
def write_transaction(self, transaction, mode):
|
||||
# This method offers backward compatibility with the Web API.
|
||||
"""Submit a valid transaction to the mempool."""
|
||||
|
||||
self.post_transaction(transaction)
|
||||
self.post_transaction(transaction, mode)
|
||||
|
||||
def store_transaction(self, transaction):
|
||||
"""Store a valid transaction to the transactions collection."""
|
||||
@ -55,10 +61,32 @@ class BigchainDB(Bigchain):
|
||||
transaction_metadata = {'id': transaction['id'],
|
||||
'metadata': metadata}
|
||||
|
||||
backend.query.store_metadata(self.connection, [transaction_metadata])
|
||||
backend.query.store_metadatas(self.connection, [transaction_metadata])
|
||||
|
||||
return backend.query.store_transaction(self.connection, transaction)
|
||||
|
||||
def store_bulk_transactions(self, transactions):
|
||||
txns = []
|
||||
assets = []
|
||||
txn_metadatas = []
|
||||
for transaction in transactions:
|
||||
transaction = transaction.to_dict()
|
||||
if transaction['operation'] == 'CREATE':
|
||||
asset = transaction.pop('asset')
|
||||
asset['id'] = transaction['id']
|
||||
if asset['data'] is not None:
|
||||
assets.append(asset)
|
||||
|
||||
metadata = transaction.pop('metadata')
|
||||
txn_metadatas.append({'id': transaction['id'],
|
||||
'metadata': metadata})
|
||||
txns.append(transaction)
|
||||
|
||||
backend.query.store_metadatas(self.connection, txn_metadatas)
|
||||
if assets:
|
||||
backend.query.store_assets(self.connection, assets)
|
||||
return backend.query.store_transactions(self.connection, txns)
|
||||
|
||||
def get_transaction(self, transaction_id, include_status=False):
|
||||
transaction = backend.query.get_transaction(self.connection, transaction_id)
|
||||
asset = backend.query.get_asset(self.connection, transaction_id)
|
||||
@ -84,9 +112,25 @@ class BigchainDB(Bigchain):
|
||||
else:
|
||||
return transaction
|
||||
|
||||
def get_spent(self, txid, output):
|
||||
transaction = backend.query.get_spent(self.connection, txid,
|
||||
output)
|
||||
def get_spent(self, txid, output, current_transactions=[]):
|
||||
transactions = backend.query.get_spent(self.connection, txid,
|
||||
output)
|
||||
transactions = list(transactions) if transactions else []
|
||||
|
||||
for ctxn in current_transactions:
|
||||
for ctxn_input in ctxn.inputs:
|
||||
if ctxn_input.fulfills.txid == txid and\
|
||||
ctxn_input.fulfills.output == output:
|
||||
transactions.append(ctxn.to_dict())
|
||||
|
||||
transaction = None
|
||||
if len(transactions) > 1:
|
||||
raise core_exceptions.CriticalDoubleSpend(
|
||||
'`{}` was spent more than once. There is a problem'
|
||||
' with the chain'.format(txid))
|
||||
elif transactions:
|
||||
transaction = transactions[0]
|
||||
|
||||
if transaction and transaction['operation'] == 'CREATE':
|
||||
asset = backend.query.get_asset(self.connection, transaction['id'])
|
||||
|
||||
@ -111,7 +155,59 @@ class BigchainDB(Bigchain):
|
||||
|
||||
return backend.query.get_latest_block(self.connection)
|
||||
|
||||
def validate_transaction(self, tx):
|
||||
def get_block(self, block_id, include_status=False):
|
||||
"""Get the block with the specified `block_id` (and optionally its status)
|
||||
|
||||
Returns the block corresponding to `block_id` or None if no match is
|
||||
found.
|
||||
|
||||
Args:
|
||||
block_id (str): block id of the block to get
|
||||
include_status (bool): also return the status of the block
|
||||
the return value is then a tuple: (block, status)
|
||||
"""
|
||||
# get block from database
|
||||
if isinstance(block_id, str):
|
||||
block_id = int(block_id)
|
||||
|
||||
block = backend.query.get_block(self.connection, block_id)
|
||||
if block:
|
||||
transactions = backend.query.get_transactions(self.connection, block['transactions'])
|
||||
transactions = Transaction.from_db(self, transactions)
|
||||
|
||||
block = {'height': block['height'],
|
||||
'transactions': []}
|
||||
block_txns = block['transactions']
|
||||
for txn in transactions:
|
||||
block_txns.append(txn.to_dict())
|
||||
|
||||
status = None
|
||||
if include_status:
|
||||
# NOTE: (In Tendermint) a block is an abstract entity which
|
||||
# exists only after it has been validated
|
||||
if block:
|
||||
status = self.BLOCK_VALID
|
||||
return block, status
|
||||
else:
|
||||
return block
|
||||
|
||||
def get_block_containing_tx(self, txid):
|
||||
"""Retrieve the list of blocks (block ids) containing a
|
||||
transaction with transaction id `txid`
|
||||
|
||||
Args:
|
||||
txid (str): transaction id of the transaction to query
|
||||
|
||||
Returns:
|
||||
Block id list (list(int))
|
||||
"""
|
||||
blocks = list(backend.query.get_block_with_transaction(self.connection, txid))
|
||||
if len(blocks) > 1:
|
||||
logger.critical('Transaction id %s exists in multiple blocks', txid)
|
||||
|
||||
return [block['height'] for block in blocks]
|
||||
|
||||
def validate_transaction(self, tx, current_transactions=[]):
|
||||
"""Validate a transaction against the current status of the database."""
|
||||
|
||||
transaction = tx
|
||||
@ -126,7 +222,7 @@ class BigchainDB(Bigchain):
|
||||
logger.warning('Invalid transaction (%s): %s', type(e).__name__, e)
|
||||
return False
|
||||
try:
|
||||
return transaction.validate(self)
|
||||
return transaction.validate(self, current_transactions)
|
||||
except ValidationError as e:
|
||||
logger.warning('Invalid transaction (%s): %s', type(e).__name__, e)
|
||||
return False
|
||||
@ -137,4 +233,4 @@ class BigchainDB(Bigchain):
|
||||
return fastquery.FastQuery(self.connection, self.me)
|
||||
|
||||
|
||||
Block = namedtuple('Block', ('app_hash', 'height'))
|
||||
Block = namedtuple('Block', ('app_hash', 'height', 'transactions'))
|
||||
|
@ -5,7 +5,6 @@ For more information please refer to the documentation: http://bigchaindb.com/ht
|
||||
from flask import current_app
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
from bigchaindb import Bigchain
|
||||
from bigchaindb.web.views.base import make_error
|
||||
|
||||
|
||||
@ -42,18 +41,13 @@ class BlockListApi(Resource):
|
||||
"""
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('transaction_id', type=str, required=True)
|
||||
parser.add_argument('status', type=str, case_sensitive=False,
|
||||
choices=[Bigchain.BLOCK_VALID, Bigchain.BLOCK_INVALID, Bigchain.BLOCK_UNDECIDED])
|
||||
|
||||
args = parser.parse_args(strict=True)
|
||||
tx_id = args['transaction_id']
|
||||
status = args['status']
|
||||
|
||||
pool = current_app.config['bigchain_pool']
|
||||
|
||||
with pool() as bigchain:
|
||||
block_statuses = bigchain.get_blocks_status_containing_tx(tx_id)
|
||||
blocks = [block_id for block_id, block_status in block_statuses.items()
|
||||
if not status or block_status == status]
|
||||
blocks = bigchain.get_block_containing_tx(tx_id)
|
||||
|
||||
return blocks
|
||||
|
@ -29,4 +29,14 @@ def valid_operation(op):
|
||||
return 'CREATE'
|
||||
if op == 'TRANSFER':
|
||||
return 'TRANSFER'
|
||||
raise ValueError('Operation must be "CREATE" or "TRANSFER')
|
||||
raise ValueError('Operation must be "CREATE" or "TRANSFER"')
|
||||
|
||||
|
||||
def valid_mode(mode):
|
||||
if mode == 'async':
|
||||
return 'broadcast_tx_async'
|
||||
if mode == 'sync':
|
||||
return 'broadcast_tx_sync'
|
||||
if mode == 'commit':
|
||||
return 'broadcast_tx_commit'
|
||||
raise ValueError('Mode must be "async", "sync" or "commit"')
|
||||
|
@ -55,6 +55,12 @@ class TransactionListApi(Resource):
|
||||
Return:
|
||||
A ``dict`` containing the data about the transaction.
|
||||
"""
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('mode', type=parameters.valid_mode,
|
||||
default='broadcast_tx_async')
|
||||
args = parser.parse_args()
|
||||
mode = str(args['mode'])
|
||||
|
||||
pool = current_app.config['bigchain_pool']
|
||||
|
||||
# `force` will try to format the body of the POST request even if the
|
||||
@ -85,7 +91,7 @@ class TransactionListApi(Resource):
|
||||
'Invalid transaction ({}): {}'.format(type(e).__name__, e)
|
||||
)
|
||||
else:
|
||||
bigchain.write_transaction(tx_obj)
|
||||
bigchain.write_transaction(tx_obj, mode)
|
||||
|
||||
response = jsonify(tx)
|
||||
response.status_code = 202
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
- "9984"
|
||||
command: bigchaindb -l DEBUG start
|
||||
tendermint:
|
||||
image: tendermint/tendermint
|
||||
image: tendermint/tendermint:0.13
|
||||
volumes:
|
||||
- ./tmdata:/tendermint
|
||||
entrypoint: ''
|
||||
|
@ -71,5 +71,5 @@ Role-Based Access Control (RBAC)
|
||||
================================
|
||||
|
||||
In September 2017, we published a `blog post about how one can define an RBAC sub-system on top of BigchainDB <https://blog.bigchaindb.com/role-based-access-control-for-bigchaindb-assets-b7cada491997>`_.
|
||||
At the time of writing (October 2017), doing so required the use of a plugin, so it's not possible using standard BigchainDB (which is what's available on `IPDB <https://ipdb.io/>`_). That may change in the future.
|
||||
At the time of writing (January 2018), doing so required the use of a plugin, so it's not possible using standard BigchainDB (which is what's available on the `BigchainDB Testnet <https://testnet.bigchaindb.com/>`_). That may change in the future.
|
||||
If you're interested, `contact BigchainDB <https://www.bigchaindb.com/contact/>`_.
|
||||
|
@ -7,6 +7,7 @@ import os.path
|
||||
from bigchaindb.common.transaction import Transaction, Input, TransactionLink
|
||||
from bigchaindb.core import Bigchain
|
||||
from bigchaindb.models import Block
|
||||
from bigchaindb.tendermint import lib
|
||||
from bigchaindb.web import server
|
||||
|
||||
|
||||
@ -58,7 +59,7 @@ Content-Type: application/json
|
||||
"""
|
||||
|
||||
TPLS['post-tx-request'] = """\
|
||||
POST /api/v1/transactions/ HTTP/1.1
|
||||
POST /api/v1/transactions?mode=async HTTP/1.1
|
||||
Host: example.com
|
||||
Content-Type: application/json
|
||||
|
||||
@ -242,27 +243,33 @@ def main():
|
||||
node_private = "5G2kE1zJAgTajkVSbPAQWo4c2izvtwqaNHYsaNpbbvxX"
|
||||
node_public = "DngBurxfeNVKZWCEcDnLj1eMPAS7focUZTE5FndFGuHT"
|
||||
signature = "53wxrEQDYk1dXzmvNSytbCfmNVnPqPkDQaTnAe8Jf43s6ssejPxezkCvUnGTnduNUmaLjhaan1iRLi3peu6s5DzA"
|
||||
block = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
|
||||
ctx['block'] = pretty_json(block.to_dict())
|
||||
ctx['blockid'] = block.id
|
||||
|
||||
block_transfer = Block(transactions=[tx_transfer], node_pubkey=node_public,
|
||||
voters=[node_public], signature=signature)
|
||||
ctx['block_transfer'] = pretty_json(block.to_dict())
|
||||
|
||||
# vote
|
||||
DUMMY_SHA3 = '0123456789abcdef' * 4
|
||||
b = Bigchain(public_key=node_public, private_key=node_private)
|
||||
vote = b.vote(block.id, DUMMY_SHA3, True)
|
||||
ctx['vote'] = pretty_json(vote)
|
||||
app_hash = 'f6e0c49c6d94d6924351f25bb334cf2a99af4206339bf784e741d1a5ab599056'
|
||||
block = lib.Block(height=1, transactions=[tx.to_dict()], app_hash=app_hash)
|
||||
block_dict = block._asdict()
|
||||
block_dict.pop('app_hash')
|
||||
ctx['block'] = pretty_json(block_dict)
|
||||
ctx['blockid'] = block.height
|
||||
|
||||
# block status
|
||||
block_list = [
|
||||
block_transfer.id,
|
||||
block.id
|
||||
block.height
|
||||
]
|
||||
ctx['block_list'] = pretty_json(block_list)
|
||||
|
||||
|
||||
# block = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
|
||||
block_transfer = Block(transactions=[tx_transfer], node_pubkey=node_public,
|
||||
voters=[node_public], signature=signature)
|
||||
ctx['block_transfer'] = pretty_json(block_transfer.to_dict())
|
||||
|
||||
# vote
|
||||
vblock = Block(transactions=[tx], node_pubkey=node_public, voters=[node_public], signature=signature)
|
||||
DUMMY_SHA3 = '0123456789abcdef' * 4
|
||||
b = Bigchain(public_key=node_public, private_key=node_private)
|
||||
vote = b.vote(vblock.id, DUMMY_SHA3, True)
|
||||
ctx['vote'] = pretty_json(vote)
|
||||
|
||||
base_path = os.path.join(os.path.dirname(__file__),
|
||||
'source/http-samples')
|
||||
if not os.path.exists(base_path):
|
||||
|
@ -11,82 +11,23 @@ A block must contain the following JSON keys
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"id": "<ID of the block>",
|
||||
"block": {
|
||||
"timestamp": "<Block-creation timestamp>",
|
||||
"transactions": ["<List of transactions>"],
|
||||
"node_pubkey": "<Public key of the node which created the block>",
|
||||
"voters": ["<List of public keys of all nodes in the cluster>"]
|
||||
},
|
||||
"signature": "<Signature of inner block object>"
|
||||
"height": "<Height of the block>",
|
||||
"transactions": ["<List of transactions>"]
|
||||
}
|
||||
|
||||
|
||||
The JSON Keys in a Block
|
||||
------------------------
|
||||
|
||||
**id**
|
||||
**height**
|
||||
|
||||
The transaction ID and also the SHA3-256 hash
|
||||
of the inner ``block`` object, loosely speaking.
|
||||
It's a string.
|
||||
To compute it, 1) construct an :term:`associative array` ``d`` containing
|
||||
``block.timestamp``, ``block.transactions``, ``block.node_pubkey``,
|
||||
``block.voters``, and their values. 2) compute ``id = hash_of_aa(d)``.
|
||||
There's pseudocode for the ``hash_of_aa()`` function
|
||||
in the `IPDB Transaction Spec page about cryptographic hashes
|
||||
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-hashes.html#computing-the-hash-of-an-associative-array>`_.
|
||||
The result (``id``) is a string: the block ID.
|
||||
An example is ``"b60adf655932bf47ef58c0bfb2dd276d4795b94346b36cbb477e10d7eb02cea8"``
|
||||
The block ``"height"`` (``integer``) denotes the height of the blockchain when the given block was committed.
|
||||
Since the blockchain height increases monotonically the height of block can be regarded as its id.
|
||||
|
||||
**NOTE**: The genesis block has height ``0``
|
||||
|
||||
|
||||
**block.timestamp**
|
||||
|
||||
The `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_
|
||||
when the block was created, according to the node which created it.
|
||||
It's a string representation of an integer.
|
||||
An example is ``"1507294217"``.
|
||||
|
||||
|
||||
**block.transactions**
|
||||
**transactions**
|
||||
|
||||
A list of the :ref:`transactions <the-transaction-model>` included in the block.
|
||||
(Each transaction is a JSON object.)
|
||||
|
||||
|
||||
**block.node_pubkey**
|
||||
|
||||
The public key of the node that created the block.
|
||||
It's a string.
|
||||
See the `IPDB Transaction Spec page about cryptographic keys & signatures
|
||||
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-keys-and-sigs.html>`_.
|
||||
|
||||
|
||||
**block.voters**
|
||||
|
||||
A list of the public keys of all cluster nodes at the time the block was created.
|
||||
It's a list of strings.
|
||||
This list can change from block to block, as nodes join and leave the cluster.
|
||||
|
||||
|
||||
**signature**
|
||||
|
||||
The cryptographic signature of the inner ``block``
|
||||
by the node that created the block
|
||||
(i.e. the node with public key ``node_pubkey``).
|
||||
To compute that:
|
||||
|
||||
#. Construct an :term:`associative array` ``d`` containing the contents
|
||||
of the inner ``block``
|
||||
(i.e. ``block.timestamp``, ``block.transactions``, ``block.node_pubkey``,
|
||||
``block.voters``, and their values).
|
||||
#. Compute ``signature = sig_of_aa(d, private_key)``,
|
||||
where ``private_key`` is the node's private key
|
||||
(i.e. ``node_pubkey`` and ``private_key`` are a key pair). There's pseudocode
|
||||
for the ``sig_of_aa()`` function
|
||||
on `the IPDB Transaction Spec page about cryptographic keys and signatures
|
||||
<https://the-ipdb-transaction-spec.readthedocs.io/en/latest/common-operations/crypto-keys-and-sigs.html#computing-the-signature-of-an-associative-array>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``d_bytes`` computed when computing the block ID will be the *same* as the ``d_bytes`` computed when computing the block signature. This can be used to avoid redundant calculations.
|
||||
|
@ -25,4 +25,4 @@ Community-Driven Libraries and Tools
|
||||
* `Go driver <https://github.com/zbo14/envoke/blob/master/bigchain/bigchain.go>`_
|
||||
* `Java driver <https://github.com/authenteq/java-bigchaindb-driver>`_
|
||||
* `Ruby driver <https://github.com/LicenseRocks/bigchaindb_ruby>`_
|
||||
* `Ruby library for preparing/signing transactions and submitting them or querying a BigchainDB/IPDB node (MIT licensed) <https://rubygems.org/gems/bigchaindb>`_
|
||||
* `Ruby library for preparing/signing transactions and submitting them or querying a BigchainDB node (MIT licensed) <https://rubygems.org/gems/bigchaindb>`_
|
||||
|
@ -128,11 +128,20 @@ Transactions
|
||||
:statuscode 400: The request wasn't understood by the server, e.g. the ``asset_id`` querystring was not included in the request.
|
||||
|
||||
|
||||
.. http:post:: /api/v1/transactions
|
||||
.. http:post:: /api/v1/transactions?mode={mode}
|
||||
|
||||
Push a new transaction.
|
||||
Tendermint offers a `broadcast API
|
||||
<http://tendermint.readthedocs.io/projects/tools/en/master/using-tendermint.html#broadcast-api>`_ with three different modes to post transactions.
|
||||
By setting the mode, a new transaction can be pushed with a different mode than the default. The default mode is ``async``, which
|
||||
will return immediately and not wait to see if the transaction is valid. The ``sync`` mode will return after the transaction is validated, while ``commit``
|
||||
returns after the transaction is committed to a block.
|
||||
|
||||
.. note::
|
||||
|
||||
This option is only available when using BigchainDB with Tendermint.
|
||||
|
||||
.. note::
|
||||
|
||||
The posted `transaction
|
||||
<https://docs.bigchaindb.com/projects/server/en/latest/data-models/transaction-model.html>`_
|
||||
should be structurally valid and not spending an already spent output.
|
||||
@ -141,6 +150,8 @@ Transactions
|
||||
<https://docs.bigchaindb.com/projects/py-driver/en/latest/index.html>`_
|
||||
to build a valid transaction.
|
||||
|
||||
:query string mode: (Optional) One of the three supported modes to send a transaction: ``async``, ``sync``, ``commit``.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. literalinclude:: http-samples/post-tx-request.http
|
||||
@ -166,6 +177,11 @@ Transactions
|
||||
:statuscode 400: The transaction was malformed and not accepted in the ``BACKLOG``.
|
||||
|
||||
|
||||
.. http:post:: /api/v1/transactions
|
||||
|
||||
This endpoint (without any parameters) will push a new transaction. If BigchainDB is used with Tendermint, the default mode ``async`` is used.
|
||||
|
||||
|
||||
Transaction Outputs
|
||||
-------------------
|
||||
|
||||
@ -585,14 +601,12 @@ The `votes endpoint <#votes>`_ contains all the voting information for a specifi
|
||||
Blocks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. http:get:: /api/v1/blocks/{block_id}
|
||||
.. http:get:: /api/v1/blocks/{block_height}
|
||||
|
||||
Get the block with the ID ``block_id``. Any blocks, be they ``VALID``, ``UNDECIDED`` or ``INVALID`` will be
|
||||
returned. To check a block's status independently, use the `Statuses endpoint <#status>`_.
|
||||
To check the votes on a block, have a look at the `votes endpoint <#votes>`_.
|
||||
Get the block with the height ``block_height``.
|
||||
|
||||
:param block_id: block ID
|
||||
:type block_id: hex string
|
||||
:param block_height: block ID
|
||||
:type block_height: integer
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -608,7 +622,7 @@ Blocks
|
||||
:resheader Content-Type: ``application/json``
|
||||
|
||||
:statuscode 200: A block with that ID was found.
|
||||
:statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks`` without the ``block_id``.
|
||||
:statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks`` without the ``block_height``.
|
||||
:statuscode 404: A block with that ID was not found.
|
||||
|
||||
|
||||
@ -635,19 +649,22 @@ Blocks
|
||||
|
||||
:statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks`` without the ``block_id``.
|
||||
|
||||
.. http:get:: /api/v1/blocks?transaction_id={transaction_id}&status={UNDECIDED|VALID|INVALID}
|
||||
.. http:get:: /api/v1/blocks?transaction_id={transaction_id}
|
||||
|
||||
Retrieve a list of ``block_id`` with their corresponding status that contain a transaction with the ID ``transaction_id``.
|
||||
Retrieve a list of block IDs (block heights), such that the blocks with those IDs contain a transaction with the ID ``transaction_id``. A correct response may consist of an empty list or a list with one block ID.
|
||||
|
||||
Any blocks, be they ``UNDECIDED``, ``VALID`` or ``INVALID`` will be
|
||||
returned if no status filter is provided.
|
||||
.. note::
|
||||
The query parameter ``status`` has been deprecated. It allowed
|
||||
users to filter blocks based on their status i.e. only blocks with the specified
|
||||
status were included in the response. Since then this behavior has changed
|
||||
and now block are created only after the transactions are accepted by the
|
||||
network i.e. blocks have only one status ``VALID``
|
||||
|
||||
.. note::
|
||||
In case no block was found, an empty list and an HTTP status code
|
||||
``200 OK`` is returned, as the request was still successful.
|
||||
|
||||
:query string transaction_id: transaction ID *(required)*
|
||||
:query string status: Filter blocks by their status. One of ``VALID``, ``UNDECIDED`` or ``INVALID``.
|
||||
|
||||
**Example request**:
|
||||
|
||||
@ -661,7 +678,7 @@ Blocks
|
||||
|
||||
:resheader Content-Type: ``application/json``
|
||||
|
||||
:statuscode 200: A list of blocks containing a transaction with ID ``transaction_id`` was found and returned.
|
||||
:statuscode 200: The request was properly formed and zero or more blocks were found containing the specified ``transaction_id``.
|
||||
:statuscode 400: The request wasn't understood by the server, e.g. just requesting ``/blocks``, without defining ``transaction_id``.
|
||||
|
||||
|
||||
|
@ -50,10 +50,10 @@ Create a BigchainDB transaction and post it to a BigchainDB network in 20 second
|
||||
|
||||
## Develop an App
|
||||
|
||||
To develop an app that talks to a BigchainDB network, you'll want a test network to test it against. IPDB is the Interplanetary Database. The IPDB Test Network is a free-to-use, publicly-available BigchainDB network that you can test against.
|
||||
To develop an app that talks to a BigchainDB network, you'll want a test network to test it against. The BigchainDB Testnet is a free-to-use, publicly-available BigchainDB network that you can test against.
|
||||
|
||||
<div class="buttondiv">
|
||||
<a class="button" href="https://ipdb.io/#getstarted">Get started with IPDB</a>
|
||||
<a class="button" href="https://testnet.bigchaindb.com/">Get started with the BigchainDB Testnet</a>
|
||||
</div>
|
||||
|
||||
Regardless of which BigchainDB network you use, you'll probably use one of the [BigchainDB drivers or tools](https://www.bigchaindb.com/getstarted/#drivers).
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mongo:3.4.4
|
||||
FROM mongo:3.4.10
|
||||
LABEL maintainer "dev@bigchaindb.com"
|
||||
WORKDIR /
|
||||
RUN apt-get update \
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build -t bigchaindb/mongodb:3.2 .
|
||||
docker build -t bigchaindb/mongodb:3.3 .
|
||||
|
||||
docker push bigchaindb/mongodb:3.2
|
||||
docker push bigchaindb/mongodb:3.3
|
||||
|
@ -65,9 +65,11 @@ if [[ -z "${REPLICA_SET_NAME:?REPLICA_SET_NAME not specified. Exiting!}" || \
|
||||
-z "${MONGODB_IP:?MONGODB_IP not specified. Exiting!}" || \
|
||||
-z "${MONGODB_KEY_FILE_PATH:?MONGODB_KEY_FILE_PATH not specified. Exiting!}" || \
|
||||
-z "${MONGODB_CA_FILE_PATH:?MONGODB_CA_FILE_PATH not specified. Exiting!}" || \
|
||||
-z "${MONGODB_CRL_FILE_PATH:?MONGODB_CRL_FILE_PATH not specified. Exiting!}" || \
|
||||
-z "${STORAGE_ENGINE_CACHE_SIZE:=''}" ]] ; then
|
||||
#-z "${MONGODB_KEY_FILE_PASSWORD:?MongoDB Key File Password not specified. Exiting!}" || \
|
||||
-z "${MONGODB_CRL_FILE_PATH:?MONGODB_CRL_FILE_PATH not specified. Exiting!}" ]] ; then
|
||||
# Not handling the STORAGE_ENGINE_CACHE_SIZE because
|
||||
# it is optional. If not specified the default cache
|
||||
# size is: max((50% RAM - 1GB), 256MB)
|
||||
echo "Missing required enviroment variable(s)."
|
||||
exit 1
|
||||
else
|
||||
echo REPLICA_SET_NAME="$REPLICA_SET_NAME"
|
||||
|
@ -1,3 +1,4 @@
|
||||
[pytest]
|
||||
testpaths = tests/
|
||||
norecursedirs = .* *.egg *.egg-info env* devenv* docs
|
||||
addopts = -m tendermint
|
||||
|
2
setup.py
2
setup.py
@ -81,7 +81,7 @@ install_requires = [
|
||||
'multipipes~=0.1.0',
|
||||
'jsonschema~=2.5.1',
|
||||
'pyyaml~=3.12',
|
||||
'aiohttp~=2.0',
|
||||
'aiohttp~=2.3',
|
||||
'python-rapidjson-schema==0.1.1',
|
||||
'statsd==3.2.1',
|
||||
'abci~=0.3.0',
|
||||
|
@ -88,7 +88,7 @@ def test_write_metadata():
|
||||
]
|
||||
|
||||
# write the assets
|
||||
query.store_metadata(conn, deepcopy(metadata))
|
||||
query.store_metadatas(conn, deepcopy(metadata))
|
||||
|
||||
# check that 3 assets were written to the database
|
||||
cursor = conn.db.metadata.find({}, projection={'_id': False})\
|
||||
@ -152,3 +152,31 @@ def test_get_spending_transactions(user_pk, user_sk):
|
||||
|
||||
# tx3 not a member because input 1 not asked for
|
||||
assert txns == [tx2.to_dict(), tx4.to_dict()]
|
||||
|
||||
|
||||
def test_store_block():
|
||||
from bigchaindb.backend import connect, query
|
||||
from bigchaindb.tendermint.lib import Block
|
||||
conn = connect()
|
||||
|
||||
block = Block(app_hash='random_utxo',
|
||||
height=3,
|
||||
transactions=[])
|
||||
query.store_block(conn, block._asdict())
|
||||
cursor = conn.db.blocks.find({}, projection={'_id': False})
|
||||
assert cursor.count() == 1
|
||||
|
||||
|
||||
def test_get_block():
|
||||
from bigchaindb.backend import connect, query
|
||||
from bigchaindb.tendermint.lib import Block
|
||||
conn = connect()
|
||||
|
||||
block = Block(app_hash='random_utxo',
|
||||
height=3,
|
||||
transactions=[])
|
||||
|
||||
conn.db.blocks.insert_one(block._asdict())
|
||||
|
||||
block = dict(query.get_block(conn, 3))
|
||||
assert block['height'] == 3
|
||||
|
@ -479,7 +479,11 @@ def test_validate_tx_simple_create_signature(user_input, user_output, user_priv,
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output])
|
||||
expected = deepcopy(user_output)
|
||||
message = str(tx).encode()
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict['inputs'][0]['fulfillment'] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True,
|
||||
separators=(',', ':'), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
expected.fulfillment.sign(message, b58decode(user_priv))
|
||||
tx.sign([user_priv])
|
||||
|
||||
@ -539,7 +543,11 @@ def test_validate_tx_threshold_create_signature(user_user2_threshold_input,
|
||||
tx = Transaction(Transaction.CREATE, asset_definition,
|
||||
[user_user2_threshold_input],
|
||||
[user_user2_threshold_output])
|
||||
message = str(tx).encode()
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict['inputs'][0]['fulfillment'] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True,
|
||||
separators=(',', ':'), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
expected = deepcopy(user_user2_threshold_output)
|
||||
expected.fulfillment.subconditions[0]['body'].sign(
|
||||
message, b58decode(user_priv))
|
||||
@ -570,11 +578,18 @@ def test_validate_tx_threshold_duplicated_pk(user_pub, user_priv,
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition,
|
||||
[threshold_input], [threshold_output])
|
||||
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict['inputs'][0]['fulfillment'] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True,
|
||||
separators=(',', ':'), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
|
||||
expected = deepcopy(threshold_input)
|
||||
expected.fulfillment.subconditions[0]['body'].sign(
|
||||
str(tx).encode(), b58decode(user_priv))
|
||||
message, b58decode(user_priv))
|
||||
expected.fulfillment.subconditions[1]['body'].sign(
|
||||
str(tx).encode(), b58decode(user_priv))
|
||||
message, b58decode(user_priv))
|
||||
|
||||
tx.sign([user_priv, user_priv])
|
||||
|
||||
@ -807,7 +822,6 @@ def test_outputs_to_inputs(tx):
|
||||
def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub,
|
||||
user2_output, user_priv):
|
||||
from bigchaindb.common.transaction import Transaction
|
||||
from bigchaindb.common.utils import serialize
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
expected = {
|
||||
@ -839,8 +853,14 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub,
|
||||
transfer_tx = transfer_tx.to_dict()
|
||||
|
||||
expected_input = deepcopy(inputs[0])
|
||||
expected_input.fulfillment.sign(
|
||||
serialize(expected).encode(), b58decode(user_priv))
|
||||
json_serialized_tx = json.dumps(expected, sort_keys=True,
|
||||
separators=(',', ':'), ensure_ascii=True)
|
||||
message = sha3_256(json_serialized_tx.encode())
|
||||
message.update('{}{}'.format(
|
||||
expected['inputs'][0]['fulfills']['transaction_id'],
|
||||
expected['inputs'][0]['fulfills']['output_index'],
|
||||
).encode())
|
||||
expected_input.fulfillment.sign(message.digest(), b58decode(user_priv))
|
||||
expected_ffill = expected_input.fulfillment.serialize_uri()
|
||||
transfer_ffill = transfer_tx['inputs'][0]['fulfillment']
|
||||
|
||||
|
@ -486,6 +486,19 @@ def db_conn():
|
||||
return connect()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_context(db_config, db_host, db_port, db_name, db_conn):
|
||||
DBContext = namedtuple(
|
||||
'DBContext', ('config', 'host', 'port', 'name', 'conn'))
|
||||
return DBContext(
|
||||
config=db_config,
|
||||
host=db_host,
|
||||
port=db_port,
|
||||
name=db_name,
|
||||
conn=db_conn,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_setup_pub_logger(mocker):
|
||||
return mocker.patch(
|
||||
|
@ -3,7 +3,7 @@ import json
|
||||
import pytest
|
||||
|
||||
|
||||
pytestmark = pytest.mark.tendermint
|
||||
pytestmark = [pytest.mark.tendermint, pytest.mark.bdb]
|
||||
|
||||
|
||||
def encode_tx_to_bytes(transaction):
|
||||
@ -43,6 +43,7 @@ def test_check_tx__unsigned_create_is_error(b):
|
||||
assert result.is_error()
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
def test_deliver_tx__valid_create_updates_db(b):
|
||||
from bigchaindb.tendermint import App
|
||||
from bigchaindb.models import Transaction
|
||||
@ -56,8 +57,14 @@ def test_deliver_tx__valid_create_updates_db(b):
|
||||
.sign([alice.private_key])
|
||||
|
||||
app = App(b)
|
||||
app.init_chain(["ignore"])
|
||||
app.begin_block("ignore")
|
||||
|
||||
result = app.deliver_tx(encode_tx_to_bytes(tx))
|
||||
assert result.is_ok()
|
||||
|
||||
app.end_block(99)
|
||||
app.commit()
|
||||
assert b.get_transaction(tx.id).id == tx.id
|
||||
|
||||
|
||||
@ -74,8 +81,15 @@ def test_deliver_tx__double_spend_fails(b):
|
||||
.sign([alice.private_key])
|
||||
|
||||
app = App(b)
|
||||
app.init_chain(["ignore"])
|
||||
app.begin_block("ignore")
|
||||
|
||||
result = app.deliver_tx(encode_tx_to_bytes(tx))
|
||||
assert result.is_ok()
|
||||
|
||||
app.end_block(99)
|
||||
app.commit()
|
||||
|
||||
assert b.get_transaction(tx.id).id == tx.id
|
||||
result = app.deliver_tx(encode_tx_to_bytes(tx))
|
||||
assert result.is_error()
|
||||
@ -87,6 +101,9 @@ def test_deliver_transfer_tx__double_spend_fails(b):
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
|
||||
app = App(b)
|
||||
app.init_chain(["ignore"])
|
||||
app.begin_block("ignore")
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
carly = generate_key_pair()
|
||||
|
@ -12,12 +12,13 @@ pytestmark = pytest.mark.tendermint
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
def test_app(b):
|
||||
def test_app(tb):
|
||||
from bigchaindb.tendermint import App
|
||||
from bigchaindb.tendermint.utils import calculate_hash
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
from bigchaindb.models import Transaction
|
||||
|
||||
b = tb
|
||||
app = App(b)
|
||||
p = ProtocolHandler(app)
|
||||
|
||||
@ -56,7 +57,6 @@ def test_app(b):
|
||||
res, err = read_message(BytesIO(data), types.Response)
|
||||
assert res
|
||||
assert res.deliver_tx.code == 0
|
||||
assert b.get_transaction(tx.id).id == tx.id
|
||||
|
||||
new_block_txn_hash = calculate_hash([tx.id])
|
||||
|
||||
@ -74,6 +74,7 @@ def test_app(b):
|
||||
assert res
|
||||
assert res.commit.code == 0
|
||||
assert res.commit.data == new_block_hash.encode('utf-8')
|
||||
assert b.get_transaction(tx.id).id == tx.id
|
||||
|
||||
block0 = b.get_latest_block()
|
||||
assert block0
|
||||
|
@ -1,9 +1,9 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from bigchaindb import backend
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
pytestmark = pytest.mark.tendermint
|
||||
@ -37,12 +37,16 @@ def test_asset_is_separated_from_transaciton(b):
|
||||
assert b.get_transaction(tx.id) == tx
|
||||
|
||||
|
||||
def test_get_latest_block(b):
|
||||
@pytest.mark.bdb
|
||||
def test_get_latest_block(tb):
|
||||
from bigchaindb.tendermint.lib import Block
|
||||
|
||||
b = tb
|
||||
for i in range(10):
|
||||
app_hash = os.urandom(16).hex()
|
||||
block = Block(app_hash=app_hash, height=i)._asdict()
|
||||
txn_id = os.urandom(16).hex()
|
||||
block = Block(app_hash=app_hash, height=i,
|
||||
transactions=[txn_id])._asdict()
|
||||
b.store_block(block)
|
||||
|
||||
block = b.get_latest_block()
|
||||
@ -76,10 +80,45 @@ def test_write_and_post_transaction(mock_post, b):
|
||||
.sign([alice.private_key]).to_dict()
|
||||
|
||||
tx = b.validate_transaction(tx)
|
||||
b.write_transaction(tx)
|
||||
b.write_transaction(tx, 'broadcast_tx_async')
|
||||
|
||||
assert mock_post.called
|
||||
args, kwargs = mock_post.call_args
|
||||
assert 'broadcast_tx_async' == kwargs['json']['method']
|
||||
encoded_tx = [encode_transaction(tx.to_dict())]
|
||||
assert encoded_tx == kwargs['json']['params']
|
||||
|
||||
|
||||
@patch('requests.post')
|
||||
@pytest.mark.parametrize('mode', [
|
||||
'broadcast_tx_async',
|
||||
'broadcast_tx_sync',
|
||||
'broadcast_tx_commit'
|
||||
])
|
||||
def test_post_transaction_valid_modes(mock_post, b, mode):
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
alice = generate_key_pair()
|
||||
tx = Transaction.create([alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=None) \
|
||||
.sign([alice.private_key]).to_dict()
|
||||
tx = b.validate_transaction(tx)
|
||||
b.write_transaction(tx, mode)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
assert mode == kwargs['json']['method']
|
||||
|
||||
|
||||
def test_post_transaction_invalid_mode(b):
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
from bigchaindb.common.exceptions import ValidationError
|
||||
alice = generate_key_pair()
|
||||
tx = Transaction.create([alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=None) \
|
||||
.sign([alice.private_key]).to_dict()
|
||||
tx = b.validate_transaction(tx)
|
||||
with pytest.raises(ValidationError):
|
||||
b.write_transaction(tx, 'nope')
|
||||
|
@ -4,6 +4,7 @@ import rethinkdb as r
|
||||
|
||||
from bigchaindb.backend.mongodb.connection import MongoDBConnection
|
||||
from bigchaindb.backend.rethinkdb.connection import RethinkDBConnection
|
||||
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
|
||||
|
||||
|
||||
@singledispatch
|
||||
@ -47,6 +48,15 @@ def flush_mongo_db(connection, dbname):
|
||||
connection.conn[dbname].metadata.delete_many({})
|
||||
|
||||
|
||||
@flush_db.register(LocalMongoDBConnection)
|
||||
def flush_localmongo_db(connection, dbname):
|
||||
connection.conn[dbname].bigchain.delete_many({})
|
||||
connection.conn[dbname].blocks.delete_many({})
|
||||
connection.conn[dbname].transactions.delete_many({})
|
||||
connection.conn[dbname].assets.delete_many({})
|
||||
connection.conn[dbname].metadata.delete_many({})
|
||||
|
||||
|
||||
@singledispatch
|
||||
def update_table_config(connection, table, **kwrgas):
|
||||
raise NotImplementedError
|
||||
|
92
tests/web/test_block_tendermint.py
Normal file
92
tests/web/test_block_tendermint.py
Normal file
@ -0,0 +1,92 @@
|
||||
import pytest
|
||||
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.tendermint.lib import Block
|
||||
|
||||
BLOCKS_ENDPOINT = '/api/v1/blocks/'
|
||||
|
||||
pytestmark = pytest.mark.tendermint
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
@pytest.mark.usefixtures('inputs')
|
||||
def test_get_block_endpoint(tb, client):
|
||||
b = tb
|
||||
tx = Transaction.create([b.me], [([b.me], 1)], asset={'cycle': 'hero'})
|
||||
tx = tx.sign([b.me_private])
|
||||
b.store_transaction(tx)
|
||||
|
||||
block = Block(app_hash='random_utxo',
|
||||
height=31,
|
||||
transactions=[tx.id])
|
||||
b.store_block(block._asdict())
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + str(block.height))
|
||||
expected_response = {'height': block.height, 'transactions': [tx.to_dict()]}
|
||||
assert res.json == expected_response
|
||||
assert res.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
@pytest.mark.usefixtures('inputs')
|
||||
def test_get_block_returns_404_if_not_found(client):
|
||||
res = client.get(BLOCKS_ENDPOINT + '123')
|
||||
assert res.status_code == 404
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + '123/')
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
def test_get_block_containing_transaction(tb, client):
|
||||
b = tb
|
||||
tx = Transaction.create([b.me], [([b.me], 1)], asset={'cycle': 'hero'})
|
||||
tx = tx.sign([b.me_private])
|
||||
b.store_transaction(tx)
|
||||
|
||||
block = Block(app_hash='random_utxo',
|
||||
height=13,
|
||||
transactions=[tx.id])
|
||||
b.store_block(block._asdict())
|
||||
|
||||
res = client.get('{}?transaction_id={}'.format(BLOCKS_ENDPOINT, tx.id))
|
||||
expected_response = [block.height]
|
||||
assert res.json == expected_response
|
||||
assert res.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
def test_get_blocks_by_txid_endpoint_returns_empty_list_not_found(client):
|
||||
res = client.get(BLOCKS_ENDPOINT + '?transaction_id=')
|
||||
assert res.status_code == 200
|
||||
assert len(res.json) == 0
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + '?transaction_id=123')
|
||||
assert res.status_code == 200
|
||||
assert len(res.json) == 0
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
def test_get_blocks_by_txid_endpoint_returns_400_bad_query_params(client):
|
||||
res = client.get(BLOCKS_ENDPOINT)
|
||||
assert res.status_code == 400
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + '?ts_id=123')
|
||||
assert res.status_code == 400
|
||||
assert res.json == {
|
||||
'message': {
|
||||
'transaction_id': 'Missing required parameter in the JSON body or the post body or the query string'
|
||||
}
|
||||
}
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + '?transaction_id=123&foo=123')
|
||||
assert res.status_code == 400
|
||||
assert res.json == {
|
||||
'message': 'Unknown arguments: foo'
|
||||
}
|
||||
|
||||
res = client.get(BLOCKS_ENDPOINT + '?transaction_id=123&status=123')
|
||||
assert res.status_code == 400
|
||||
assert res.json == {
|
||||
'message': 'Unknown arguments: status'
|
||||
}
|
@ -410,3 +410,41 @@ def test_return_only_valid_transaction(client):
|
||||
get_transaction_patched(Bigchain.TX_IN_BACKLOG)):
|
||||
url = '{}{}'.format(TX_ENDPOINT, '123')
|
||||
assert client.get(url).status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.tendermint
|
||||
@patch('requests.post')
|
||||
@pytest.mark.parametrize('mode', [
|
||||
('', 'broadcast_tx_async'),
|
||||
('?mode=async', 'broadcast_tx_async'),
|
||||
('?mode=sync', 'broadcast_tx_sync'),
|
||||
('?mode=commit', 'broadcast_tx_commit'),
|
||||
])
|
||||
def test_post_transaction_valid_modes(mock_post, client, mode):
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
alice = generate_key_pair()
|
||||
tx = Transaction.create([alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=None) \
|
||||
.sign([alice.private_key])
|
||||
mode_endpoint = TX_ENDPOINT + mode[0]
|
||||
client.post(mode_endpoint, data=json.dumps(tx.to_dict()))
|
||||
args, kwargs = mock_post.call_args
|
||||
assert mode[1] == kwargs['json']['method']
|
||||
|
||||
|
||||
@pytest.mark.tendermint
|
||||
def test_post_transaction_invalid_mode(client):
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.common.crypto import generate_key_pair
|
||||
alice = generate_key_pair()
|
||||
tx = Transaction.create([alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=None) \
|
||||
.sign([alice.private_key])
|
||||
mode_endpoint = TX_ENDPOINT + '?mode=nope'
|
||||
response = client.post(mode_endpoint, data=json.dumps(tx.to_dict()))
|
||||
assert '400 BAD REQUEST' in response.status
|
||||
assert 'Mode must be "async", "sync" or "commit"' ==\
|
||||
json.loads(response.data.decode('utf8'))['message']['mode']
|
||||
|
Loading…
x
Reference in New Issue
Block a user