mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Merge pull request #174 from bigchaindb/feat/128/multiple-input-output
WIP Feat/128/multiple input output
This commit is contained in:
commit
9d63b69a9e
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -28,5 +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"""
|
||||
|
||||
|
@ -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,52 @@ 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)
|
||||
if not isinstance(current_owners, list):
|
||||
current_owners = [current_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 +223,171 @@ 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:
|
||||
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))
|
||||
elif len(new_owners) == 1:
|
||||
condition = cc.Ed25519Fulfillment(public_key=new_owners[0])
|
||||
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 +397,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 +418,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 +511,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
|
||||
|
||||
|
BIN
docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png
Normal file
BIN
docs/source/_static/tx_multi_condition_multi_fulfillment_v1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
docs/source/_static/tx_schematics.odg
Normal file
BIN
docs/source/_static/tx_schematics.odg
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -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
|
||||
|
||||
|
@ -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`.
|
||||
|
||||

|
||||
|
||||
|
||||
```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,535 @@ 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.
|
||||
|
||||

|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
3
setup.py
3
setup.py
@ -75,12 +75,11 @@ setup(
|
||||
'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',
|
||||
|
@ -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
|
||||
|
@ -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
@ -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
0
tests/doc/__init__.py
Normal file
236
tests/doc/run_doc_python_server_api_examples.py
Normal file
236
tests/doc/run_doc_python_server_api_examples.py
Normal file
@ -0,0 +1,236 @@
|
||||
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=(',', ':')))
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user