BigchainDB Python Driver

BigchainDB Python Driver

https://img.shields.io/pypi/v/bigchaindb-driver.svg https://img.shields.io/travis/bigchaindb/bigchaindb-driver/master.svg https://img.shields.io/codecov/c/github/bigchaindb/bigchaindb-driver/master.svg Documentation Status Updates

Features

  • Support for preparing, fulfilling, and sending transactions to a BigchainDB node.
  • Retrieval of transactions by id.
  • Getting status of a transaction by id.

Compatibility Matrix

BigchainDB Server BigchainDB Driver
>= 0.8.2 >= 0.1.3
>= 0.9.1 0.2.x
== 1.0.0rc1 0.3.x
>= 1.0.0 0.4.x

Although we do our best to keep the master branches in sync, there may be occasional delays.

Credits

This package was initially created using Cookiecutter and the audreyr/cookiecutter-pypackage project template. Many BigchainDB developers have contributed since then.

Quickstart / Installation

The BigchainDB Python Driver depends on:

  1. libffi/ffi.h
  2. libssl-dev
  3. Python 3.5+
  4. A recent Python 3 version of pip
  5. A recent Python 3 version of setuptools

If you’re missing one of those, then see below. Otherwise, you can install the BigchainDB Python Driver (bigchaindb_driver) using:

$ pip3 install bigchaindb_driver

Next: determine the BigchainDB Root URL of the BigchainDB node or cluster you want to connect to.

How to Install the Dependencies

Dependency 1: ffi.h

BigchainDB (server and driver) depends on cryptoconditions, which depends on PyNaCl (Networking and Cryptography library), which depends on ffi.h. Hence, depending on your setup, you may need to install the development files for libffi.

On Ubuntu 14.04 and 16.04, this works:

$ sudo apt-get update

$ sudo apt-get install libffi-dev

On Fedora 23 and 24, this works:

$ sudo dnf update

$ sudo dnf install libffi-devel

For other operating systems, just do some web searches for “ffi.h” with the name of your OS.

Dependency 2: libssl-dev

BigchainDB (server and driver) also depends on cryptography, which in turn depends on libssl AND libcrypto. Hence, depending on your setup you need to install the libssl-dev (Ubuntu) OR openssl-devel (RHEL) package, which installs the development libraries and header files for libssl and libcrypto.

On Ubuntu 14.04 and 16.04:

$ sudo apt-get install libssl-dev

On Fedora 23 and 24:

$ sudo dnf install openssl-devel

Dependency 3: Python 3.5+

The BigchainDB Python Driver uses Python 3.5+. You can check your version of Python using:

$ python --version

OR

$ python3 --version

An easy way to install a specific version of Python, and to switch between versions of Python, is to use virtualenv. Another option is conda.

Dependency 4: pip

You also need to get a recent, Python 3 version of pip, the Python package manager.

If you’re using virtualenv or conda, then each virtual environment should include an appropriate version of pip.

You can check your version of pip using:

$ pip --version

OR

$ pip3 --version

pip was at version 9.0.0 as of November 2016. If you need to upgrade your version of pip, then see the pip documentation or our page about that in the BigchainDB Server docs.

Dependency 5: setuptools

Once you have a recent Python 3 version of pip, you should be able to upgrade setuptools using:

$ pip install --upgrade setuptools

OR

$ pip3 install --upgrade setuptools

Installing the Driver

Now you can install the BigchainDB Python Driver (bigchaindb_driver) using:

$ pip install bigchaindb_driver

OR

$ pip3 install bigchaindb_driver

Next: determine the BigchainDB Root URL of the BigchainDB node or cluster you want to connect to.

Advanced Installation Options

See the Advanced Installation Options page.

Determine the BigchainDB Root URL

If you want to use the BigchainDB Python Driver to communicate with a BigchainDB node or cluster, then you will need its BigchainDB Root URL. This page is to help you determine it.

Case 1: BigchainDB on localhost

If a BigchainDB node is running locally (and the BIGCHAINDB_SERVER_BIND setting wasn’t changed from the default localhost:9984), then the BigchainDB Root URL is:

bdb_root_url = 'http://localhost:9984'

Case 2: A Cluster Hosted by Someone Else

If you’re connecting to a BigchainDB cluster hosted by someone else, then they’ll tell you their BigchaindB Root URL. It can take many forms. It can use HTTP or HTTPS. It can use a hostname or an IP address. The port might not be 9984. Here are some examples:

bdb_root_url = 'http://example.com:9984'
bdb_root_url = 'http://api.example.com:9984'
bdb_root_url = 'http://example.com:1234'
bdb_root_url = 'http://example.com'  # http is port 80 by default
bdb_root_url = 'https://example.com'  # https is port 443 by default
bdb_root_url = 'http://12.34.56.123:9984'
bdb_root_url = 'http://12.34.56.123:5000'

Case 3: Docker Container on localhost

If you are running the Docker-based dev setup that comes along with the bigchaindb_driver repository (see Development Environment with Docker for more information), and wish to connect to it from the bdb-driver linked (container) service, use:

bdb_root_url = 'http://bdb-server:9984'

Alternatively, you may connect to the containerized BigchainDB node from “outside”, in which case you need to know the port binding:

$ docker-compose port bdb-server 9984
0.0.0.0:32780
bdb_root_url = 'http://0.0.0.0:32780'

Next, try some of the basic usage examples.

Basic Usage Examples

For the examples on this page, we assume you’re using a Python 3 version of IPython (or similar), you’ve installed the bigchaindb_driver Python package, and you have determined the BigchainDB Root URL of the node or cluster you want to connect to.

Getting Started

We begin by creating an object of class BigchainDB:

In [1]: from bigchaindb_driver import BigchainDB

In [1]: bdb_root_url = 'https://example.com:9984'  # Use YOUR BigchainDB Root URL here

If the BigchainDB node or cluster doesn’t require authentication tokens, you can do:

In [1]: bdb = BigchainDB(bdb_root_url)

If it does require authentication tokens, you can do put them in a dict like so:

In [1]: tokens = {'app_id': 'your_app_id', 'app_key': 'your_app_key'}

In [1]: bdb = BigchainDB(bdb_root_url, headers=tokens)

Digital Asset Definition

As an example, let’s consider the creation and transfer of a digital asset that represents a bicycle:

In [1]: bicycle = {
   ...:     'data': {
   ...:         'bicycle': {
   ...:             'serial_number': 'abcd1234',
   ...:             'manufacturer': 'bkfab',
   ...:         },
   ...:     },
   ...: }
   ...: 

We’ll suppose that the bike belongs to Alice, and that it will be transferred to Bob.

In general, you may use any dictionary for the 'data' property.

Metadata Definition (optional)

You can optionally add metadata to a transaction. Any dictionary is accepted.

For example:

In [1]: metadata = {'planet': 'earth'}

Cryptographic Identities Generation

Alice and Bob are represented by public/private key pairs. The private key is used to sign transactions, meanwhile the public key is used to verify that a signed transaction was indeed signed by the one who claims to be the signee.

In [1]: from bigchaindb_driver.crypto import generate_keypair

In [1]: alice, bob = generate_keypair(), generate_keypair()

Asset Creation

We’re now ready to create the digital asset. First, let’s prepare the transaction:

In [1]: prepared_creation_tx = bdb.transactions.prepare(
   ...:     operation='CREATE',
   ...:     signers=alice.public_key,
   ...:     asset=bicycle,
   ...:     metadata=metadata,
   ...: )
   ...: 

The prepared_creation_tx dictionary should be similar to:

In [1]: prepared_creation_tx

The transaction now needs to be fulfilled by signing it with Alice’s private key:

In [1]: fulfilled_creation_tx = bdb.transactions.fulfill(
   ...:     prepared_creation_tx, private_keys=alice.private_key)
   ...: 
In [1]: fulfilled_creation_tx

And sent over to a BigchainDB node:

>>> sent_creation_tx = bdb.transactions.send(fulfilled_creation_tx)

Note that the response from the node should be the same as that which was sent:

>>> sent_creation_tx == fulfilled_creation_tx
True

Notice the transaction id:

In [1]: txid = fulfilled_creation_tx['id']

In [1]: txid

To check the status of the transaction:

>>> bdb.transactions.status(txid)

Note

It may take a small amount of time before a BigchainDB cluster confirms a transaction as being valid.

Here’s some code that keeps checking the status of the transaction until it is valid:

>>> trials = 0

>>> while trials < 100:
...     try:
...         if bdb.transactions.status(txid).get('status') == 'valid':
...             break
...     except bigchaindb_driver.exceptions.NotFoundError:
...         trials += 1

>>> bdb.transactions.status(txid)
{'status': 'valid'}

Asset Transfer

Imagine some time goes by, during which Alice is happy with her bicycle, and one day, she meets Bob, who is interested in acquiring her bicycle. The timing is good for Alice as she had been wanting to get a new bicycle.

To transfer the bicycle (asset) to Bob, Alice must consume the transaction in which the Bicycle asset was created.

Alice could retrieve the transaction:

>>>  creation_tx = bdb.transactions.retrieve(txid)

or simply use fulfilled_creation_tx:

In [1]: creation_tx = fulfilled_creation_tx

In order to prepare the transfer transaction, we first need to know the id of the asset we’ll be transferring. Here, because Alice is consuming a CREATE transaction, we have a special case in that the asset id is NOT found on the asset itself, but is simply the CREATE transaction’s id:

In [1]: asset_id = creation_tx['id']

In [1]: transfer_asset = {
   ...:     'id': asset_id,
   ...: }
   ...: 

Let’s now prepare the transfer transaction:

In [1]: output_index = 0

In [1]: output = creation_tx['outputs'][output_index]

In [1]: transfer_input = {
   ...:     'fulfillment': output['condition']['details'],
   ...:     'fulfills': {
   ...:          'output_index': output_index,
   ...:          'transaction_id': creation_tx['id'],
   ...:      },
   ...:      'owners_before': output['public_keys'],
   ...: }
   ...: 

In [1]: prepared_transfer_tx = bdb.transactions.prepare(
   ...:     operation='TRANSFER',
   ...:     asset=transfer_asset,
   ...:     inputs=transfer_input,
   ...:     recipients=bob.public_key,
   ...: )
   ...: 

fulfill it:

In [1]: fulfilled_transfer_tx = bdb.transactions.fulfill(
   ...:     prepared_transfer_tx,
   ...:     private_keys=alice.private_key,
   ...: )
   ...: 

and finally send it to the connected BigchainDB node:

>>> sent_transfer_tx = bdb.transactions.send(fulfilled_transfer_tx)

>>> sent_transfer_tx == fulfilled_transfer_tx
True

The fulfilled_transfer_tx dictionary should look something like:

In [1]: fulfilled_transfer_tx

Bob is the new owner:

In [1]: fulfilled_transfer_tx['outputs'][0]['public_keys'][0] == bob.public_key

Alice is the former owner:

In [1]: fulfilled_transfer_tx['inputs'][0]['owners_before'][0] == alice.public_key

Note

Obtaining asset ids:

You might have noticed that we considered Alice’s case of consuming a CREATE transaction as a special case. In order to obtain the asset id of a CREATE transaction, we had to use the CREATE transaction’s id:

transfer_asset_id = create_tx['id']

If you instead wanted to consume TRANSFER transactions (for example, fulfilled_transfer_tx), you could obtain the asset id to transfer from the asset['id'] property:

transfer_asset_id = transfer_tx['asset']['id']

Recap: Asset Creation & Transfer

from bigchaindb_driver import BigchainDB
from bigchaindb_driver.crypto import generate_keypair
from time import sleep
from sys import exit

alice, bob = generate_keypair(), generate_keypair()

bdb_root_url = 'https://example.com:9984'  # Use YOUR BigchainDB Root URL here

bdb = BigchainDB(bdb_root_url)

bicycle_asset = {
    'data': {
        'bicycle': {
            'serial_number': 'abcd1234',
            'manufacturer': 'bkfab'
        },
    },
}

bicycle_asset_metadata = {
    'planet': 'earth'
}

prepared_creation_tx = bdb.transactions.prepare(
    operation='CREATE',
    signers=alice.public_key,
    asset=bicycle_asset,
    metadata=bicycle_asset_metadata
)

fulfilled_creation_tx = bdb.transactions.fulfill(
    prepared_creation_tx,
    private_keys=alice.private_key
)

sent_creation_tx = bdb.transactions.send(fulfilled_creation_tx)

txid = fulfilled_creation_tx['id']

trials = 0
while trials < 60:
    try:
        if bdb.transactions.status(txid).get('status') == 'valid':
            print('Tx valid in:', trials, 'secs')
            break
    except bigchaindb_driver.exceptions.NotFoundError:
        trials += 1
        sleep(1)

if trials == 60:
    print('Tx is still being processed... Bye!')
    exit(0)

asset_id = txid

transfer_asset = {
    'id': asset_id
}

output_index = 0
output = fulfilled_creation_tx['outputs'][output_index]

transfer_input = {
    'fulfillment': output['condition']['details'],
    'fulfills': {
        'output_index': output_index,
        'transaction_id': fulfilled_creation_tx['id']
    },
    'owners_before': output['public_keys']
}

prepared_transfer_tx = bdb.transactions.prepare(
    operation='TRANSFER',
    asset=transfer_asset,
    inputs=transfer_input,
    recipients=bob.public_key,
)

fulfilled_transfer_tx = bdb.transactions.fulfill(
    prepared_transfer_tx,
    private_keys=alice.private_key,
)

sent_transfer_tx = bdb.transactions.send(fulfilled_transfer_tx)

print("Is Bob the owner?",
    sent_transfer_tx['outputs'][0]['public_keys'][0] == bob.public_key)

print("Was Alice the previous owner?",
    fulfilled_transfer_tx['inputs'][0]['owners_before'][0] == alice.public_key)

Transaction Status

Using the id of a transaction, its status can be obtained:

>>> bdb.transactions.status(creation_tx['id'])
{'status': 'valid'}

Handling cases for which the transaction id may not be found:

import logging

from bigchaindb_driver import BigchainDB
from bigchaindb_driver.exceptions import NotFoundError

logger = logging.getLogger(__name__)
logging.basicConfig(format='%(asctime)-15s %(status)-3s %(message)s')

bdb_root_url = 'https://example.com:9984'  # Use YOUR BigchainDB Root URL here
bdb = BigchainDB(bdb_root_url)
txid = '12345'
try:
    status = bdb.transactions.status(txid)
except NotFoundError as e:
    logger.error('Transaction "%s" was not found.',
                 txid,
                 extra={'status': e.status_code})

Running the above code should give something similar to:

2016-09-29 15:06:30,606 404 Transaction "12345" was not found.

Divisible Assets

All assets in BigchainDB become implicitly divisible if a transaction contains more than one of that asset (we’ll see how this happens shortly).

Let’s continue with the bicycle example. Bob is now the proud owner of the bicycle and he decides he wants to rent the bicycle. Bob starts by creating a time sharing token in which one token corresponds to one hour of riding time:

In [1]: bicycle_token = {
   ...:     'data': {
   ...:         'token_for': {
   ...:             'bicycle': {
   ...:                 'serial_number': 'abcd1234',
   ...:                 'manufacturer': 'bkfab'
   ...:             }
   ...:         },
   ...:         'description': 'Time share token. Each token equals one hour of riding.',
   ...:     },
   ...: }
   ...: 

Bob has now decided to issue 10 tokens and assigns them to Carly. Notice how we denote Carly as receiving 10 tokens by using a tuple: ([carly.public_key], 10).

In [1]: bob, carly = generate_keypair(), generate_keypair()

In [1]: prepared_token_tx = bdb.transactions.prepare(
   ...:     operation='CREATE',
   ...:     signers=bob.public_key,
   ...:     recipients=[([carly.public_key], 10)],
   ...:     asset=bicycle_token,
   ...: )
   ...: 

In [1]: fulfilled_token_tx = bdb.transactions.fulfill(
   ...:     prepared_token_tx, private_keys=bob.private_key)
   ...: 

Sending the transaction:

>>> sent_token_tx = bdb.transactions.send(fulfilled_token_tx)

>>> sent_token_tx == fulfilled_token_tx
True

Note

Defining recipients:

To create divisible assets, we need to specify an amount >1 together with the public keys. The way we do this is by passing a list of tuples in recipients where each tuple corresponds to an output.

For instance, instead of creating a transaction with one output containing amount=10 we could have created a transaction with two outputs each holding amount=5:

recipients=[([carly.public_key], 5), ([carly.public_key], 5)]

The reason why the addresses are contained in lists is because each output can have multiple recipients. For instance, we can create an output with amount=10 in which both Carly and Alice are recipients (of the same asset):

recipients=[([carly.public_key, alice.public_key], 10)]

The fulfilled_token_tx dictionary should look something like:

In [1]: fulfilled_token_tx

Bob is the issuer:

In [1]: fulfilled_token_tx['inputs'][0]['owners_before'][0] == bob.public_key

Carly is the owner of 10 tokens:

In [1]: fulfilled_token_tx['outputs'][0]['public_keys'][0] == carly.public_key

In [1]: fulfilled_token_tx['outputs'][0]['amount'] == '10'

Now in possession of the tokens, Carly wants to ride the bicycle for two hours. To do so, she needs to send two tokens to Bob:

In [1]: output_index = 0

In [1]: output = prepared_token_tx['outputs'][output_index]

In [1]: transfer_input = {
   ...:     'fulfillment': output['condition']['details'],
   ...:     'fulfills': {
   ...:         'output_index': output_index,
   ...:         'transaction_id': prepared_token_tx['id'],
   ...:     },
   ...:     'owners_before': output['public_keys'],
   ...: }
   ...: 

In [1]: transfer_asset = {
   ...:     'id': prepared_token_tx['id'],
   ...: }
   ...: 

In [1]: prepared_transfer_tx = bdb.transactions.prepare(
   ...:     operation='TRANSFER',
   ...:     asset=transfer_asset,
   ...:     inputs=transfer_input,
   ...:     recipients=[([bob.public_key], 2), ([carly.public_key], 8)]
   ...: )
   ...: 

In [1]: fulfilled_transfer_tx = bdb.transactions.fulfill(
   ...:     prepared_transfer_tx, private_keys=carly.private_key)
   ...: 
>>> sent_transfer_tx = bdb.transactions.send(fulfilled_transfer_tx)

>>> sent_transfer_tx == fulfilled_transfer_tx
True

Notice how Carly needs to reassign the remaining eight tokens to herself if she wants to only transfer two tokens (out of the available 10) to Bob. BigchainDB ensures that the amount being consumed in each transaction (with divisible assets) is the same as the amount being output. This ensures that no amounts are lost.

Also note how, because we were consuming a TRANSFER transaction, we were able to directly use the TRANSFER transaction’s asset as the new transaction’s asset because it already contained the asset’s id.

The fulfilled_transfer_tx dictionary should have two outputs, one with amount='2' and the other with amount='8':

In [1]: fulfilled_transfer_tx

Querying for Assets

BigchainDB allows you to query for assets using simple text search. This search is applied to all the strings inside the asset payload and returns all the assets that match a given text search string.

Let’s assume that we created 3 assets that look like this:

In [1]: assets = [
   ...:    {'data': {'msg': 'Hello BigchainDB 1!'}},
   ...:    {'data': {'msg': 'Hello BigchainDB 2!'}},
   ...:    {'data': {'msg': 'Hello BigchainDB 3!'}}
   ...: ]
   ...: 

Let’s perform a text search for all assets that contain the word bigchaindb:

>> bdb.assets.get(search='bigchaindb')
[
    {
        'data': {'msg': 'Hello BigchainDB 1!'},
        'id': '7582d7a81652d0230fefb47dafc360ff09b2c2566b68f05c3a004d57e7fe7610'
    },
    {
        'data': {'msg': 'Hello BigchainDB 2!'},
        'id': 'e40f4b6ac70b9c1b3b237ec13f4174384fd4d54d36dfde25520171577c49caa4'
    },
    {
        'data': {'msg': 'Hello BigchainDB 3!'},
        'id': '748f6c30daaf771c9020d84db9ad8ac4d1f7c8de7013db55e16f10ba090f7013'
    }
]

This call returns all the assets that match the string bigchaindb, sorted by text score, as well as the asset id. This is the same id of the transaction that created the asset.

It’s also possible to limit the amount of returned results using the limit argument:

>> bdb.assets.get(search='bigchaindb', limit=2)
[
    {
        'data': {'msg': 'Hello BigchainDB 1!'},
        'id': '7582d7a81652d0230fefb47dafc360ff09b2c2566b68f05c3a004d57e7fe7610'
    },
    {
        'data': {'msg': 'Hello BigchainDB 2!'},
        'id': 'e40f4b6ac70b9c1b3b237ec13f4174384fd4d54d36dfde25520171577c49caa4'
    }
]

Advanced Usage Examples

This page has examples of using the Python Driver for more advanced use cases such as escrow.

Todo

This is work in progress. More examples will gradually appear as issues like

are taken care of.

For the examples on this page, we assume you’re using a Python 3 version of IPython (or similar), you’ve installed the bigchaindb_driver Python package, and you have determined the BigchainDB Root URL of the node or cluster you want to connect to.

Getting Started

We begin by creating an object of class BigchainDB:

In [1]: from bigchaindb_driver import BigchainDB

In [2]: bdb_root_url = 'https://example.com:9984'  # Use YOUR BigchainDB Root URL here

In [3]: bdb = BigchainDB(bdb_root_url)

That last command instantiates an object bdb of class BigchainDB. When instantiating a BigchainDB object without arguments, it uses the default BigchainDB Root URL http://localhost:9984.

Create a Digital Asset

At a high level, a “digital asset” is something which can be represented digitally and can be assigned to a user. In BigchainDB, users are identified by their public key, and the data payload in a digital asset is represented using a generic Python dict.

In BigchainDB, digital assets can be created by doing a special kind of transaction: a CREATE transaction.

In [4]: from bigchaindb_driver.crypto import generate_keypair

Create a test user: alice

In [5]: alice = generate_keypair()

Define a digital asset data payload

In [6]: digital_asset_payload = {'data': {'msg': 'Hello BigchainDB!'}}

In [7]: tx = bdb.transactions.prepare(operation='CREATE',
   ...:                               signers=alice.public_key,
   ...:                               asset=digital_asset_payload)
   ...: 

All transactions need to be signed by the user creating the transaction.

In [8]: signed_tx = bdb.transactions.fulfill(tx, private_keys=alice.private_key)

In [9]: signed_tx
Out[9]: 
{'asset': {'data': {'msg': 'Hello BigchainDB!'}},
 'id': '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b',
 'inputs': [{'fulfillment': 'pGSAIA8EdrlpQuGB-xYLwnDOchlGAjSbtd7w22TqPwJDdud_gUDGXGeKXCL6_TUstmZy_vX-VbjDcbXwepcuwiiQnG7v4zdqOg485GVxWDCrqcibNIH3AAKECTjecaYi6CvHNtAK',
   'fulfills': None,
   'owners_before': ['21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi']}],
 'metadata': None,
 'operation': 'CREATE',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': '21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;xfVu6qB3f2_O4lEwzeWkmAbeJf1Qsrk4feiYZ_tA0fY?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi']}],
 'version': '1.0'}

Write the transaction to BigchainDB. The transaction will be stored in a backlog where it will be validated before being included in a block.

>>> sent_tx = bdb.transactions.send(signed_tx)

Note that the transaction payload returned by the BigchainDB node is equivalent to the signed transaction payload.

>>> sent_tx == signed_tx
True

Read the Creation Transaction from the DB

After a couple of seconds, we can check if the transaction was validated in a block:

# Retrieve a validated transaction
>>> tx_retrieved = bdb.transactions.retrieve(tx['id'])

The new owner of the digital asset is now Alice (or more correctly, her public key):

In [10]: alice.public_key
Out[10]: '21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi'

Transfer the Digital Asset

Now that alice has a digital asset assigned to her, she can transfer it to another person. Transfer transactions require an input. The input will be the transaction id of a digital asset that was assigned to alice, which in our case is

In [11]: signed_tx['id']
Out[11]: '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b'

BigchainDB makes use of the crypto-conditions library to cryptographically lock and unlock transactions. The locking script is referred to as a condition (put inside an “output”) and a corresponding fulfillment (put inside an “input”) unlocks the output condition of an input_tx.

Since a transaction can have multiple outputs each with their own (crypto)condition, each transaction input is required to refer to the output condition that they fulfill via fulfills['output_index'].

_images/tx_single_condition_single_fulfillment_v1.png

In order to prepare a transfer transaction, Alice needs to provide at least three things:

  1. inputs – one or more fulfillments that fulfill a prior transaction’s output conditions.
  2. asset['id'] – the id of the asset being transferred.
  3. Recipient public_keys – one or more public keys representing the new recipients(s).

To construct the input:

In [12]: output_index = 0

In [13]: output = tx['outputs'][output_index]

In [14]: input_ = {
   ....:     'fulfillment': output['condition']['details'],
   ....:     'fulfills': {
   ....:         'output_index': output_index,
   ....:         'transaction_id': tx['id'],
   ....:     },
   ....:     'owners_before': output['public_keys'],
   ....: }
   ....: 

The asset in a TRANSFER transaction must be a dictionary with an id key denoting the asset to transfer. This asset id is either the id of the CREATE transaction of the asset (as it is in this case), or is the asset['id'] property in a TRANSFER transaction (note that this value simply points to the id of the asset’s CREATE transaction):

In [15]: transfer_asset_id = tx['id']

In [16]: transfer_asset = {
   ....:     'id': transfer_asset_id,
   ....: }
   ....: 

Create a second test user, bob:

In [17]: bob = generate_keypair()

In [18]: bob.public_key
Out[18]: 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez'

And prepare the transfer transaction:

In [19]: tx_transfer = bdb.transactions.prepare(
   ....:     operation='TRANSFER',
   ....:     inputs=input_,
   ....:     asset=transfer_asset,
   ....:     recipients=bob.public_key,
   ....: )
   ....: 

The tx_transfer dictionary should look something like:

In [20]: tx_transfer
Out[20]: 
{'asset': {'id': '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b'},
 'id': '97e4cf8fac9291f04f906718a9a10b1a0f75bb122067dcee9227c12ee7acebb2',
 'inputs': [{'fulfillment': {'public_key': '21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi',
    'type': 'ed25519-sha-256'},
   'fulfills': {'output_index': 0,
    'transaction_id': '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b'},
   'owners_before': ['21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi']}],
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;4-lC0t6UQmPyDiQ7OSxTqNksS4duitNu4Cq0A3gWchc?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez']}],
 'version': '1.0'}

Notice, bob‘s public key, appearing in the above dict.

In [21]: tx_transfer['outputs'][0]['public_keys'][0]
Out[21]: 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez'

In [22]: bob.public_key
Out[22]: 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez'

The transaction now needs to be fulfilled by alice:

In [23]: signed_tx_transfer = bdb.transactions.fulfill(
   ....:     tx_transfer,
   ....:     private_keys=alice.private_key,
   ....: )
   ....: 

If you look at the content of signed_tx_transfer you should see the added fulfilment uri, holding the signature:

In [24]: signed_tx_transfer
Out[24]: 
{'asset': {'id': '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b'},
 'id': '97e4cf8fac9291f04f906718a9a10b1a0f75bb122067dcee9227c12ee7acebb2',
 'inputs': [{'fulfillment': 'pGSAIA8EdrlpQuGB-xYLwnDOchlGAjSbtd7w22TqPwJDdud_gUCSluPo2NoS2R5siaEUz-nQIMGHS1ExxFZgvGvYj4YocWhuyJ3KtkCyUqt3-jg_jCcu3PWmY23alB6UCcSnDL4O',
   'fulfills': {'output_index': 0,
    'transaction_id': '5fc62d1a2d9bf86abcf3da084c54e53696f01d3299092e018e8655f94fc3bb1b'},
   'owners_before': ['21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi']}],
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;4-lC0t6UQmPyDiQ7OSxTqNksS4duitNu4Cq0A3gWchc?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez']}],
 'version': '1.0'}

More precisely:

In [25]: signed_tx_transfer['inputs'][0]['fulfillment']
Out[25]: 'pGSAIA8EdrlpQuGB-xYLwnDOchlGAjSbtd7w22TqPwJDdud_gUCSluPo2NoS2R5siaEUz-nQIMGHS1ExxFZgvGvYj4YocWhuyJ3KtkCyUqt3-jg_jCcu3PWmY23alB6UCcSnDL4O'

We have yet to send the transaction over to a BigchainDB node, as both preparing and fulfilling a transaction are done “offchain,” that is, without the need to have a connection to a BigchainDB federation.

sent_tx_transfer = bdb.transactions.send(signed_tx_transfer)

Again, as with the 'CREATE' transaction, notice how the payload returned by the server is equal to the signed one.

>>> sent_tx_transfer == signed_tx_transfer
True

Double Spends

BigchainDB makes sure that a user can’t transfer the same digital asset two or more times (i.e. it prevents double spends).

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.

Let’s suppose that Alice tries to re-send the asset back to her “secret” account.

In [26]: alice_secret_stash = generate_keypair()

Create another transfer transaction with the same input

In [27]: tx_transfer_2 = bdb.transactions.prepare(
   ....:     operation='TRANSFER',
   ....:     inputs=input_,
   ....:     asset=transfer_asset,
   ....:     recipients=alice_secret_stash.public_key,
   ....: )
   ....: 

Fulfill the transaction

In [28]: fulfilled_tx_transfer_2 = bdb.transactions.fulfill(
   ....:     tx_transfer_2,
   ....:     private_keys=alice.private_key,
   ....: )
   ....: 

Send the transaction over to the node

>>> from bigchaindb_driver.exceptions import BigchaindbException
>>> try:
...     bdb.transactions.send(fulfilled_tx_transfer_2)
... except BigchaindbException as e:
...     print(e.info)

{'message': 'Invalid transaction', 'status': 400}

Todo

Update the above output once https://github.com/bigchaindb/bigchaindb/issues/664 is taken care of.

Multiple Owners

Say alice and bob own a car together:

In [29]: car_asset = {
   ....:     'data': {
   ....:         'car': {
   ....:             'vin': '5YJRE11B781000196'
   ....:         }
   ....:     }
   ....: }
   ....: 

and they agree that alice will be the one issuing the asset. To create a new digital asset with multiple owners, one can simply provide a list or tuple of recipients:

In [30]: car_creation_tx = bdb.transactions.prepare(
   ....:     operation='CREATE',
   ....:     signers=alice.public_key,
   ....:     recipients=(alice.public_key, bob.public_key),
   ....:     asset=car_asset,
   ....: )
   ....: 

In [31]: signed_car_creation_tx = bdb.transactions.fulfill(
   ....:     car_creation_tx,
   ....:     private_keys=alice.private_key,
   ....: )
   ....: 
>>> sent_car_tx = bdb.transactions.send(signed_car_creation_tx)

>>> sent_car_tx == signed_car_creation_tx
True

One day, alice and bob, having figured out how to teleport themselves, and realizing they no longer need their car, wish to transfer the ownership of their car over to carol:

In [32]: carol = generate_keypair()

In order to prepare the transfer transaction, alice and bob need the input:

In [33]: output_index = 0

In [34]: output = signed_car_creation_tx['outputs'][output_index]

In [35]: input_ = {
   ....:     'fulfillment': output['condition']['details'],
   ....:     'fulfills': {
   ....:         'output_index': output_index,
   ....:         'transaction_id': signed_car_creation_tx['id'],
   ....:     },
   ....:     'owners_before': output['public_keys'],
   ....: }
   ....: 

Let’s take a moment to contemplate what this input_ is:

In [36]: input_
Out[36]: 
{'fulfillment': {'subconditions': [{'public_key': '21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi',
    'type': 'ed25519-sha-256'},
   {'public_key': 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez',
    'type': 'ed25519-sha-256'}],
  'threshold': 2,
  'type': 'threshold-sha-256'},
 'fulfills': {'output_index': 0,
  'transaction_id': '8df0baf73dfdb23de5aa2f13e0a4e58455ea767513a22691d619e88e83176841'},
 'owners_before': ['21d4fkiZKfEERaejKt7kCueXnM4Tc8fA3VvHH3oq4wYi',
  'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez']}

and the asset (because it’s a CREATE transaction):

In [37]: transfer_asset = {
   ....:     'id': signed_car_creation_tx['id'],
   ....: }
   ....: 

then alice can prepare the transfer:

In [38]: car_transfer_tx = bdb.transactions.prepare(
   ....:     operation='TRANSFER',
   ....:     recipients=carol.public_key,
   ....:     asset=transfer_asset,
   ....:     inputs=input_,
   ....: )
   ....: 

The asset can be transfered as soon as each of the original transaction’s signers fulfills the transaction, that is alice and bob.

To do so, simply provide a list of all private keys to the fulfill method.

Danger

We are currently working to support partial fulfillments, such that not all keys of all parties involved need to be supplied at once. The issue bigchaindb/bigchaindb/issues/729 addresses the current limitation. Your feedback is welcome!

In [39]: signed_car_transfer_tx = bdb.transactions.fulfill(
   ....:     car_transfer_tx, private_keys=[alice.private_key, bob.private_key]
   ....: )
   ....: 

Note, that if one of the private keys is missing, the fulfillment will fail. If we omit bob:

In [40]: from bigchaindb_driver.exceptions import MissingPrivateKeyError

In [41]: try:
   ....:     signed_car_transfer_tx = bdb.transactions.fulfill(
   ....:         car_transfer_tx,
   ....:         private_keys=alice.private_key,
   ....:     )
   ....: except MissingPrivateKeyError as e:
   ....:     print(e, e.__cause__, sep='\n')
   ....: 
A private key is missing!
Public key BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez is not a pair to any of the private keys

Notice bob‘s public key in the above message:

In [42]:  bob.public_key
Out[42]: 'BFUNhaXmUHPZwub7MKCK68t7HckeBtDmEaBXwsXQwcez'

And the same goes for alice. Try it!

Sending the transaction over to a BigchainDB node:

sent_car_transfer_tx = bdb.transactions.send(signed_car_transfer_tx)

if alice and bob wish to check the status of the transfer they may use the status() endpoint:

>>> bdb.transactions.status(sent_car_transfer_tx['id'])
{'status': 'valid'}

Done!

Happy, alice and bob have successfully transferred the ownership of their car to carol, and can go on exploring the countless galaxies of the universe using their new teleportation skills.

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.

Implementations of the crypto-conditions are available in Python, JavaScript, and Java.

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 allows 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 includes the public key of all (nested) parties (signers) in the output‘s public_keys
  2. Set up the threshold condition using the cryptocondition library
  3. Update the output’s condition and hash in the transaction template

We’ll illustrate this with a threshold condition where 2 out of 3 of the signers need to sign the transaction:

Todo

Stay tuned. Will soon be documented once

is taken care of.

The transaction can now be transfered by fulfilling the threshold condition.

The fulfillment involves:

  1. Create a transaction template that includes the public key of all (nested) parties (signers) in the inputs‘s owners_before
  2. Parsing the threshold condition into a fulfillment using the cryptocondition library
  3. Signing all necessary subfulfillments and updating the inputs of the transaction

Todo

Stay tuned. Will soon be documented once

are taken care of.

Hash-locked Conditions

A hash-lock condition on an asset is like a password condition: anyone with the secret preimage (i.e. a password) can fulfill the hash-lock condition and transfer the asset to themselves.

Under the hood, fulfilling a hash-lock condition amounts to finding a string (a “preimage”) which, when hashed, results in a given value. It’s easy to verify that a given preimage hashes to the given value, but it’s computationally difficult to find a string which hashes to the given value. The only practical way to get a valid preimage is to get it from the original creator (possibly via intermediaries).

One possible use case is to distribute preimages as “digital vouchers.” The first person to redeem a voucher will get the associated asset.

A federation node can create an asset with a hash-lock condition and no owners_after. Anyone who can fullfill the hash-lock condition can transfer the asset to themselves.

Todo

Stay tuned. Will soon be documented once

are taken care of.

In order to redeem the asset, one needs to create a fulfillment with the correct secret:

Todo

Stay tuned. Will soon be documented once

are taken care of.

Timeout Conditions

Timeout conditions allow assets to expire after a certain time. The primary use case of timeout conditions is to enable Escrow.

The condition can only be fulfilled before the expiry time. Once expired, the asset is lost and cannot be fulfilled by anyone.

Note

The timeout conditions are BigchainDB-specific and not (yet) supported by the ILP standard.

Important

Caveat: The times between nodes in a BigchainDB federation may (and will) differ slightly. In this case, the majority of the nodes will decide.

Todo

Stay tuned. Will soon be documented once

are taken care of.

The following demonstrates that the transaction invalidates once the timeout occurs:

Todo

Stay tuned. Will soon be documented once

are taken care of.

If you were fast enough, you should see the following output:

Todo

Stay tuned. Will soon be documented once

are taken care of.

Escrow

Escrow is a mechanism for conditional release of assets.

This means that the assets are locked up by a trusted party until an execute condition is presented. In order not to tie up the assets forever, the escrow foresees an abort condition, which is typically an expiry time.

BigchainDB and cryptoconditions provides escrow out-of-the-box, without the need of a trusted party.

A threshold condition is used to represent the escrow, since BigchainDB transactions cannot have a pending state.

_images/tx_escrow_execute_abort.png

The logic for switching between execute and abort conditions is conceptually simple:

if timeout_condition.validate(utcnow()):
    execute_fulfillment.validate(msg) == True
    abort_fulfillment.validate(msg) == False
else:
    execute_fulfillment.validate(msg) == False
    abort_fulfillment.validate(msg) == True

The above switch can be implemented as follows using threshold cryptoconditions:

_images/cc_escrow_execute_abort.png

The inverted timeout is denoted by a -1 threshold, which negates the output of the fulfillment.

inverted_fulfillment.validate(msg) == not fulfillment.validate(msg)

Note

inverted thresholds are BigchainDB-specific and not supported by the ILP standard. The main reason is that it’s difficult to tell whether the fulfillment was negated, or just omitted.

The following code snippet shows how to create an escrow condition:

In the case of bob, we create the abort fulfillment:

The following demonstrates that the transaction validation switches once the timeout occurs:

If you execute in a timely fashion, you should see the following:

Of course, when the execute transaction was accepted in-time by bigchaindb, then writing the abort transaction after expiry will yield a Doublespend error.

Handcrafting Transactions

For those who wish to assemble transaction payloads “by hand”, with examples in Python.

Overview

Submitting a transaction to a BigchainDB node consists of three main steps:

  1. Preparing the transaction payload;
  2. Fulfilling the prepared transaction payload; and
  3. Sending the transaction payload via HTTPS.

Step 1 and 2 can be performed offline on the client. That is, they do not require any connection to any BigchainDB node.

For convenience’s sake, some utilites are provided to prepare and fulfill a transaction via the BigchainDB class, and via the offchain module. For an introduction on using these utilities, see the Basic Usage Examples or Advanced Usage Examples sections.

The rest of this document will guide you through completing steps 1 and 2 manually by revisiting some of the examples provided in the usage sections. We will:

  • provide all values, including the default ones;
  • generate the transaction id;
  • learn to use crypto-conditions to generate a condition that locks the transaction, hence protecting it from being consumed by an unauthorized user;
  • learn to use crypto-conditions to generate a fulfillment that unlocks the transaction asset, and consequently enact an ownership transfer.

In order to perform all of the above, we’ll use the following Python libraries:

  • json: to serialize the transaction dictionary into a JSON formatted string;
  • sha3: to hash the serialized transaction; and
  • cryptoconditions: to create conditions and fulfillments

High-level view of a transaction in Python

For detailled documentation on the transaction schema, please consult The Transaction Model and The Transaction Schema.

From the point of view of Python, a transaction is simply a dictionary:

{
    'operation': 'CREATE',
    'asset': {
        'data': {
            'bicycle': {
                'manufacturer': 'bkfab',
                'serial_number': 'abcd1234'
            }
        }
    },
    'version': '1.0',
    'outputs': [
        {
            'condition': {
                'details': {
                    'public_key': '2GoYB8cMZQrUBZzx9BH9Bq92eGWXBy3oanDXbRK3YRpW',
                    'type': 'ed25519-sha-256'
                },
                'uri': 'ni:///sha-256;1hBHivh6Nxhgi2b1ndUbP55ZlyUFdLC9BipPUBWth7U?fpt=ed25519-sha-256&cost=131072'
            },
            'public_keys': [
                '2GoYB8cMZQrUBZzx9BH9Bq92eGWXBy3oanDXbRK3YRpW'
            ],
            'amount': '1'
        }
    ],
    'inputs': [
        {
            'fulfills': None,
            'owners_before': [
                '2GoYB8cMZQrUBZzx9BH9Bq92eGWXBy3oanDXbRK3YRpW'
            ],
            'fulfillment': {
                'public_key': '2GoYB8cMZQrUBZzx9BH9Bq92eGWXBy3oanDXbRK3YRpW',
                'type': 'ed25519-sha-256'
            }
        }
    ],
    'id': '52f8ec4d56b4187be256231ef11017038f316d16ef0a0d250e235d39e901f4d1',
    'metadata': {
        'planet': 'earth'
    }
}

Because a transaction must be signed before being sent, the id and fulfillment must be provided by the client.

Important

Implications of Signed Payloads

Because BigchainDB relies on cryptographic signatures, the payloads need to be fully prepared and signed on the client side. This prevents the server(s) from tampering with the provided data.

This enhanced security puts more work on the clients, as various values that could traditionally be generated on the server side need to be generated on the client side.

Bicycle Asset Creation Revisited

The Prepared Transaction

Recall that in order to prepare a transaction, we had to do something similar to:

In [1]: from bigchaindb_driver.crypto import generate_keypair

In [2]: from bigchaindb_driver.offchain import prepare_transaction

In [3]: alice = generate_keypair()

In [4]: bicycle = {
   ...:     'data': {
   ...:         'bicycle': {
   ...:             'serial_number': 'abcd1234',
   ...:             'manufacturer': 'bkfab',
   ...:         },
   ...:     },
   ...: }
   ...: 

In [5]: metadata = {'planet': 'earth'}

In [6]: prepared_creation_tx = prepare_transaction(
   ...:     operation='CREATE',
   ...:     signers=alice.public_key,
   ...:     asset=bicycle,
   ...:     metadata=metadata,
   ...: )
   ...: 

and the payload of the prepared transaction looked similar to:

In [7]: prepared_creation_tx
Out[7]: 
{'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
    'serial_number': 'abcd1234'}}},
 'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b',
 'inputs': [{'fulfillment': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
    'type': 'ed25519-sha-256'},
   'fulfills': None,
   'owners_before': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}],
 'metadata': {'planet': 'earth'},
 'operation': 'CREATE',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}],
 'version': '1.0'}

Note alice‘s public key is listed in the public keys of outputs:

In [8]: alice.public_key
Out[8]: 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u'

In [9]: prepared_creation_tx['outputs'][0]['public_keys'][0] == alice.public_key
Out[9]: True

We are now going to craft this payload by hand.

version

As of BigchainDB 1.0, the transaction version is set to 1.0.

In [10]: version = '1.0'
asset

Because this is a CREATE transaction, we provide the data payload for the asset to the transaction (see the transfer example below for how to construct assets in TRANSFER transactions):

In [11]: asset = {
   ....:     'data': {
   ....:         'bicycle': {
   ....:             'manufacturer': 'bkfab',
   ....:             'serial_number': 'abcd1234',
   ....:         },
   ....:     },
   ....: }
   ....: 
metadata
In [12]: metadata = {'planet': 'earth'}
operation
In [13]: operation = 'CREATE'

Important

Case sensitive; all letters must be capitalized.

outputs

The purpose of the output condition is to lock the transaction, such that a valid input fulfillment is required to unlock it. In the case of signature-based schemes, the lock is basically a public key, such that in order to unlock the transaction one needs to have the private key.

Let’s review the output payload of the prepared transaction, to see what we are aiming for:

In [14]: prepared_creation_tx['outputs'][0]
Out[14]: 
{'amount': '1',
 'condition': {'details': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
   'type': 'ed25519-sha-256'},
  'uri': 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'},
 'public_keys': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}

The difficult parts are the condition details and URI. We’‘ll now see how to generate them using the cryptoconditions library:

Note

In BigchainDB keys are encoded in base58 but the cryptoconditions library expects an unencoded byte string so we will have to decode the base58 key before we can use it with cryptoconditions.

In [15]: import base58

A base58 encoded key:

In [16]: alice.public_key
Out[16]: 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u'

Becomes:

In [17]: base58.b58decode(alice.public_key)
Out[17]: b'\x0b\xaf\xfa\xe7\xb3\xf0K\x17\xe85\x0bH\x1d\xad\x92\xdf@\x8c\xbaj\xcb\xc5\xd9\x16&d\x16\xd7W5\xa9\xf6'
In [18]: from cryptoconditions import Ed25519Sha256

In [19]: ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

generate the condition URI:

In [20]: ed25519.condition_uri
Out[20]: 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'

So now you have a condition URI for Alice’s public key.

As for the details:

In [21]: condition_details = {
   ....:     'type': ed25519.TYPE_NAME,
   ....:     'public_key': base58.b58encode(ed25519.public_key),
   ....: }
   ....: 

We can now easily assemble the dict for the output:

In [22]: output = {
   ....:     'amount': '1',
   ....:     'condition': {
   ....:         'details': condition_details,
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'public_keys': (alice.public_key,),
   ....: }
   ....: 

Let’s recap and set the outputs key with our self-constructed condition:

In [23]: from cryptoconditions import Ed25519Sha256

In [24]: ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

In [25]: output = {
   ....:     'amount': '1',
   ....:     'condition': {
   ....:         'details': {
   ....:             'type': ed25519.TYPE_NAME,
   ....:             'public_key': base58.b58encode(ed25519.public_key),
   ....:         },
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'public_keys': (alice.public_key,),
   ....: }
   ....: 

In [26]: outputs = (output,)

The key part is the condition URI:

In [27]: ed25519.condition_uri
Out[27]: 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'

To know more about its meaning, you may read the cryptoconditions internet draft.

inputs

The input fulfillment for a CREATE operation is somewhat special, and simplified:

In [28]: input_ = {
   ....:     'fulfillment': None,
   ....:     'fulfills': None,
   ....:     'owners_before': (alice.public_key,)
   ....: }
   ....: 
  • The fulfills field is empty because it’s a CREATE operation;
  • The 'fulfillment' value is None as it will be set during the fulfillment step; and
  • The 'owners_before' field identifies the issuer(s) of the asset that is being created.

The inputs value is simply a list or tuple of all inputs:

In [29]: inputs = (input_,)

Note

You may rightfully observe that the input generated in prepared_creation_tx via prepare_transaction() differs:

In [30]: prepared_creation_tx['inputs'][0]
Out[30]: 
{'fulfillment': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
  'type': 'ed25519-sha-256'},
 'fulfills': None,
 'owners_before': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}

More precisely, the value of 'fulfillment' is not None:

In [31]: prepared_creation_tx['inputs'][0]['fulfillment']
Out[31]: 
{'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
 'type': 'ed25519-sha-256'}

The quick answer is that it simply is not needed, and can be set to None.

Up to now

Putting it all together:

In [32]: handcrafted_creation_tx = {
   ....:     'asset': asset,
   ....:     'metadata': metadata,
   ....:     'operation': operation,
   ....:     'outputs': outputs,
   ....:     'inputs': inputs,
   ....:     'version': version,
   ....: }
   ....: 

In [33]: handcrafted_creation_tx
Out[33]: 
{'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
    'serial_number': 'abcd1234'}}},
 'inputs': ({'fulfillment': None,
   'fulfills': None,
   'owners_before': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'metadata': {'planet': 'earth'},
 'operation': 'CREATE',
 'outputs': ({'amount': '1',
   'condition': {'details': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'version': '1.0'}

The only thing we’re missing now is the id. We’ll generate it soon, but before that, let’s recap how we’ve put all the code together to generate the above payload:

from cryptoconditions import Ed25519Sha256
from bigchaindb_driver.crypto import CryptoKeypair

alice = CryptoKeypair(
    public_key=alice.public_key,
    private_key=alice.private_key,
)

operation = 'CREATE'

version = '1.0'

asset = {
    'data': {
        'bicycle': {
            'manufacturer': 'bkfab',
            'serial_number': 'abcd1234',
        },
    },
}

metadata = {'planet': 'earth'}

ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

output = {
    'amount': '1',
    'condition': {
        'details': {
            'type': ed25519.TYPE_NAME,
            'public_key': base58.b58encode(ed25519.public_key),
        },
        'uri': ed25519.condition_uri,
    },
    'public_keys': (alice.public_key,),
}
outputs = (output,)

input_ = {
    'fulfillment': None,
    'fulfills': None,
    'owners_before': (alice.public_key,)
}
inputs = (input_,)

handcrafted_creation_tx = {
    'asset': asset,
    'metadata': metadata,
    'operation': operation,
    'outputs': outputs,
    'inputs': inputs,
    'version': version,
}
id

The transaction’s id is essentially a SHA3-256 hash of the entire transaction (up to now), with a few additional tweaks:

In [34]: import json

In [35]: from sha3 import sha3_256

In [36]: json_str_tx = json.dumps(
   ....:     handcrafted_creation_tx,
   ....:     sort_keys=True,
   ....:     separators=(',', ':'),
   ....:     ensure_ascii=False,
   ....: )
   ....: 

In [37]: txid = sha3_256(json_str_tx.encode()).hexdigest()

In [38]: handcrafted_creation_tx['id'] = txid

Compare this to the txid of the transaction generated via prepare_transaction():

In [39]: txid == prepared_creation_tx['id']
Out[39]: True

You may observe that

In [40]: handcrafted_creation_tx == prepared_creation_tx
Out[40]: False
In [41]: from copy import deepcopy

In [42]: # back up

In [43]: prepared_creation_tx_bk = deepcopy(prepared_creation_tx)

In [44]: # set input fulfillment to None

In [45]: prepared_creation_tx['inputs'][0]['fulfillment'] = None

In [46]: handcrafted_creation_tx == prepared_creation_tx
Out[46]: False

Are still not equal because we used tuples instead of lists.

In [47]: # serialize to json str

In [48]: json_str_handcrafted_tx = json.dumps(handcrafted_creation_tx, sort_keys=True)

In [49]: json_str_prepared_tx = json.dumps(prepared_creation_tx, sort_keys=True)
In [50]: json_str_handcrafted_tx == json_str_prepared_tx
Out[50]: True

In [51]: prepared_creation_tx = prepared_creation_tx_bk

The fully handcrafted, yet-to-be-fulfilled CREATE transaction payload:

In [52]: handcrafted_creation_tx
Out[52]: 
{'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
    'serial_number': 'abcd1234'}}},
 'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b',
 'inputs': ({'fulfillment': None,
   'fulfills': None,
   'owners_before': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'metadata': {'planet': 'earth'},
 'operation': 'CREATE',
 'outputs': ({'amount': '1',
   'condition': {'details': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'version': '1.0'}

The Fulfilled Transaction

In [53]: from cryptoconditions.crypto import Ed25519SigningKey

In [54]: # fulfill prepared transaction

In [55]: from bigchaindb_driver.offchain import fulfill_transaction

In [56]: fulfilled_creation_tx = fulfill_transaction(
   ....:     prepared_creation_tx,
   ....:     private_keys=alice.private_key,
   ....: )
   ....: 

In [57]: # fulfill handcrafted transaction (with our previously built ED25519 fulfillment)

In [58]: ed25519.to_dict()
Out[58]: 
{'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
 'signature': None,
 'type': 'ed25519-sha-256'}

In [59]: message = json.dumps(
   ....:     handcrafted_creation_tx,
   ....:     sort_keys=True,
   ....:     separators=(',', ':'),
   ....:     ensure_ascii=False,
   ....: )
   ....: 

In [60]: ed25519.sign(message.encode(), base58.b58decode(alice.private_key))
Out[60]: b'\x7fi\x19ed\x9dA\xa6\xf0\x11S\xe8\xb9Z\xb2qa{\xcbEE2\xda\xd8\x97\xb4\xe4\xeaG;=\x81\xa8\x9f\x01\x1dM\xed\xfc=\xde\x9e\xde$\x1fO\xc0\x13>3\xf3\x9e\x82\xb0\x9e\x1f\xa7\x89\xc8\x1er\x93\x81\x06'

In [61]: fulfillment_uri = ed25519.serialize_uri()

In [62]: handcrafted_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Let’s check this:

In [63]: fulfilled_creation_tx['inputs'][0]['fulfillment'] == fulfillment_uri
Out[63]: True

In [64]: json.dumps(fulfilled_creation_tx, sort_keys=True) == json.dumps(handcrafted_creation_tx, sort_keys=True)
Out[64]: True

The fulfilled transaction, ready to be sent over to a BigchainDB node:

In [65]: fulfilled_creation_tx
Out[65]: 
{'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
    'serial_number': 'abcd1234'}}},
 'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b',
 'inputs': [{'fulfillment': 'pGSAIAuv-uez8EsX6DULSB2tkt9AjLpqy8XZFiZkFtdXNan2gUB_aRllZJ1BpvARU-i5WrJxYXvLRUUy2tiXtOTqRzs9gaifAR1N7fw93p7eJB9PwBM-M_OegrCeH6eJyB5yk4EG',
   'fulfills': None,
   'owners_before': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}],
 'metadata': {'planet': 'earth'},
 'operation': 'CREATE',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;YdVi1F2QwHus94FdNHJD375Ibcm6xU-PM0OK90UNKFg?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}],
 'version': '1.0'}

In a nutshell

Handcrafting a CREATE transaction can be done as follows:

import json

import base58
import sha3
from cryptoconditions import Ed25519Sha256

from bigchaindb_driver.crypto import generate_keypair


alice = generate_keypair()

operation = 'CREATE'

version = '1.0'

asset = {
    'data': {
        'bicycle': {
            'manufacturer': 'bkfab',
            'serial_number': 'abcd1234',
        },
    },
}

metadata = {'planet': 'earth'}

ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

output = {
    'amount': '1',
    'condition': {
        'details': {
            'type': ed25519.TYPE_NAME,
            'public_key': base58.b58encode(ed25519.public_key),
        },
        'uri': ed25519.condition_uri,
    },
    'public_keys': (alice.public_key,),
}
outputs = (output,)

input_ = {
    'fulfillment': None,
    'fulfills': None,
    'owners_before': (alice.public_key,)
}
inputs = (input_,)

handcrafted_creation_tx = {
    'asset': asset,
    'metadata': metadata,
    'operation': operation,
    'outputs': outputs,
    'inputs': inputs,
    'version': version,
}

json_str_tx = json.dumps(
    handcrafted_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

creation_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

handcrafted_creation_tx['id'] = creation_txid

message = json.dumps(
    handcrafted_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

ed25519.sign(message.encode(), base58.b58decode(alice.private_key))

fulfillment_uri = ed25519.serialize_uri()

handcrafted_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984')
returned_creation_tx = bdb.transactions.send(handcrafted_creation_tx)

A few checks:

>>> json.dumps(returned_creation_tx, sort_keys=True) == json.dumps(handcrafted_creation_tx, sort_keys=True)
True
>>> bdb.transactions.status(creation_txid)
{'status': 'valid'}

Tip

When checking for the status of a transaction, one should keep in mind tiny delays before a transaction reaches a valid status.

Bicycle Asset Transfer Revisited

In the bicycle transfer example , we showed that the transfer transaction was prepared and fulfilled as follows:

In [66]: creation_tx = fulfilled_creation_tx

In [67]: bob = generate_keypair()

In [68]: output_index = 0

In [69]: output = creation_tx['outputs'][output_index]

In [70]: transfer_input = {
   ....:     'fulfillment': output['condition']['details'],
   ....:     'fulfills': {
   ....:          'output_index': output_index,
   ....:          'transaction_id': creation_tx['id'],
   ....:      },
   ....:      'owners_before': output['public_keys'],
   ....: }
   ....: 

In [71]: transfer_asset = {
   ....:     'id': creation_tx['id'],
   ....: }
   ....: 

In [72]: prepared_transfer_tx = prepare_transaction(
   ....:     operation='TRANSFER',
   ....:     asset=transfer_asset,
   ....:     inputs=transfer_input,
   ....:     recipients=bob.public_key,
   ....: )
   ....: 

In [73]: fulfilled_transfer_tx = fulfill_transaction(
   ....:     prepared_transfer_tx,
   ....:     private_keys=alice.private_key,
   ....: )
   ....: 

In [74]: fulfilled_transfer_tx
Out[74]: 
{'asset': {'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
 'id': '6e9c0d161caa794c2ae8d5ead205b19cbb5a4ddd46399428bcc1fe957ea3755a',
 'inputs': [{'fulfillment': 'pGSAIAuv-uez8EsX6DULSB2tkt9AjLpqy8XZFiZkFtdXNan2gUA-Ygw2JwZPgmsfG1hd3FtJl86U5_lsD7Fp60IJvLRVri2hXkkdylB7kIbe8EuRaFFHxXRn1RVBP0Be2eR0Yd8H',
   'fulfills': {'output_index': 0,
    'transaction_id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
   'owners_before': ['nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u']}],
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;lKK-PDVNEWfWL549MgY60KsSuZKLAEgotO-lE2dRlfQ?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA']}],
 'version': '1.0'}

Our goal is now to handcraft a payload equal to fulfilled_transfer_tx with the help of

  • json: to serialize the transaction dictionary into a JSON formatted string.
  • sha3: to hash the serialized transaction
  • cryptoconditions: to create conditions and fulfillments

The Prepared Transaction

version
In [75]: version = '1.0'
asset

The asset payload for TRANSFER transaction is a dict with only the asset id (i.e. the id of the CREATE transaction for the asset):

In [76]: asset = {'id': creation_tx['id']}
metadata
In [77]: metadata = None
operation
In [78]: operation = 'TRANSFER'
outputs
In [79]: from cryptoconditions import Ed25519Sha256

In [80]: ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

In [81]: output = {
   ....:     'amount': '1',
   ....:     'condition': {
   ....:         'details': {
   ....:             'type': ed25519.TYPE_NAME,
   ....:             'public_key': base58.b58encode(ed25519.public_key),
   ....:         },
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'public_keys': (bob.public_key,),
   ....: }
   ....: 

In [82]: outputs = (output,)
fulfillments
In [83]: input_ = {
   ....:     'fulfillment': None,
   ....:     'fulfills': {
   ....:         'transaction_id': creation_tx['id'],
   ....:         'output_index': 0,
   ....:     },
   ....:     'owners_before': (alice.public_key,)
   ....: }
   ....: 

In [84]: inputs = (input_,)

A few notes:

  • The fulfills field points to the condition (in a transaction) that needs to be fulfilled;
  • The 'fulfillment' value is None as it will be set during the fulfillment step; and
  • The 'owners_before' field identifies the fulfiller(s).

Putting it all together:

In [85]: handcrafted_transfer_tx = {
   ....:     'asset': asset,
   ....:     'metadata': metadata,
   ....:     'operation': operation,
   ....:     'outputs': outputs,
   ....:     'inputs': inputs,
   ....:     'version': version,
   ....: }
   ....: 

In [86]: handcrafted_transfer_tx
Out[86]: 
{'asset': {'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
 'inputs': ({'fulfillment': None,
   'fulfills': {'output_index': 0,
    'transaction_id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
   'owners_before': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': ({'amount': '1',
   'condition': {'details': {'public_key': 'FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;lKK-PDVNEWfWL549MgY60KsSuZKLAEgotO-lE2dRlfQ?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ('FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',)},),
 'version': '1.0'}
Up to now

Before we generate the id, let’s recap how we got here:

from cryptoconditions import Ed25519Sha256
from bigchaindb_driver.crypto import CryptoKeypair

bob = CryptoKeypair(
    public_key=bob.public_key,
    private_key=bob.private_key,
)

operation = 'TRANSFER'
version = '1.0'
asset = {'id': creation_tx['id']}
metadata = None

ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

output = {
    'amount': '1',
    'condition': {
        'details': {
            'type': ed25519.TYPE_NAME,
            'public_key': base58.b58encode(ed25519.public_key),
        },
        'uri': ed25519.condition_uri,
    },
    'public_keys': (bob.public_key,),
}
outputs = (output,)

input_ = {
    'fulfillment': None,
    'fulfills': {
        'transaction_id': creation_tx['id'],
        'output_index': 0,
    },
    'owners_before': (alice.public_key,)
}
inputs = (input_,)

handcrafted_transfer_tx = {
    'asset': asset,
    'metadata': metadata,
    'operation': operation,
    'outputs': outputs,
    'inputs': inputs,
    'version': version,
}
id
In [87]: import json

In [88]: from sha3 import sha3_256

In [89]: json_str_tx = json.dumps(
   ....:     handcrafted_transfer_tx,
   ....:     sort_keys=True,
   ....:     separators=(',', ':'),
   ....:     ensure_ascii=False,
   ....: )
   ....: 

In [90]: txid = sha3_256(json_str_tx.encode()).hexdigest()

In [91]: handcrafted_transfer_tx['id'] = txid

Compare this to the txid of the transaction generated via prepare_transaction()

In [92]: txid == prepared_transfer_tx['id']
Out[92]: True

You may observe that

In [93]: handcrafted_transfer_tx == prepared_transfer_tx
Out[93]: False
In [94]: from copy import deepcopy

In [95]: # back up

In [96]: prepared_transfer_tx_bk = deepcopy(prepared_transfer_tx)

In [97]: # set fulfillment to None

In [98]: prepared_transfer_tx['inputs'][0]['fulfillment'] = None

In [99]: handcrafted_transfer_tx == prepared_transfer_tx
Out[99]: False

Are still not equal because we used tuples instead of lists.

In [100]: # serialize to json str

In [101]: json_str_handcrafted_tx = json.dumps(handcrafted_transfer_tx, sort_keys=True)

In [102]: json_str_prepared_tx = json.dumps(prepared_transfer_tx, sort_keys=True)
In [103]: json_str_handcrafted_tx == json_str_prepared_tx
Out[103]: True

In [104]: prepared_transfer_tx = prepared_transfer_tx_bk

The fully handcrafted, yet-to-be-fulfilled TRANSFER transaction payload:

In [105]: handcrafted_transfer_tx
Out[105]: 
{'asset': {'id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
 'id': '6e9c0d161caa794c2ae8d5ead205b19cbb5a4ddd46399428bcc1fe957ea3755a',
 'inputs': ({'fulfillment': None,
   'fulfills': {'output_index': 0,
    'transaction_id': '9982ea1200b42719734e504ec49cced45fbbdf19bca4a290805b6145a7d6c31b'},
   'owners_before': ('nd86e7A7BEqmqxiGMVwNwQkhFZheS7U8vXWSpbXSU4u',)},),
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': ({'amount': '1',
   'condition': {'details': {'public_key': 'FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;lKK-PDVNEWfWL549MgY60KsSuZKLAEgotO-lE2dRlfQ?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ('FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',)},),
 'version': '1.0'}

The Fulfilled Transaction

In [106]: from bigchaindb_driver.offchain import fulfill_transaction

In [107]: # fulfill prepared transaction

In [108]: fulfilled_transfer_tx = fulfill_transaction(
   .....:     prepared_transfer_tx,
   .....:     private_keys=alice.private_key,
   .....: )
   .....: 

In [109]: # fulfill handcrafted transaction (with our previously built ED25519 fulfillment)

In [110]: ed25519.to_dict()
Out[110]: 
{'public_key': 'FPoGii4N4EUTe2R6gwfznsTL91W52aKBv3FqECW6X9nA',
 'signature': None,
 'type': 'ed25519-sha-256'}

In [111]: message = json.dumps(
   .....:     handcrafted_transfer_tx,
   .....:     sort_keys=True,
   .....:     separators=(',', ':'),
   .....:     ensure_ascii=False,
   .....: )
   .....: 

In [112]: ed25519.sign(message.encode(), base58.b58decode(alice.private_key))
Out[112]: b">b\x0c6'\x06O\x82k\x1f\x1bX]\xdc[I\x97\xce\x94\xe7\xf9l\x0f\xb1i\xebB\t\xbc\xb4U\xae-\xa1^I\x1d\xcaP{\x90\x86\xde\xf0K\x91hQG\xc5tg\xd5\x15A?@^\xd9\xe4ta\xdf\x07"

In [113]: fulfillment_uri = ed25519.serialize_uri()

In [114]: handcrafted_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Let’s check this:

In [115]: fulfilled_transfer_tx['inputs'][0]['fulfillment'] == fulfillment_uri
Out[115]: True

In [116]: json.dumps(fulfilled_transfer_tx, sort_keys=True) == json.dumps(handcrafted_transfer_tx, sort_keys=True)
Out[116]: True

In a nutshell

import json

import base58
import sha3
from cryptoconditions import Ed25519Sha256

from bigchaindb_driver.crypto import generate_keypair


bob = generate_keypair()

operation = 'TRANSFER'
version = '1.0'
asset = {'id': handcrafted_creation_tx['id']}
metadata = None

ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

output = {
    'amount': '1',
    'condition': {
        'details': {
            'type': ed25519.TYPE_NAME,
            'public_key': base58.b58encode(ed25519.public_key),
        },
        'uri': ed25519.condition_uri,
    },
    'public_keys': (bob.public_key,),
}
outputs = (output,)

input_ = {
    'fulfillment': None,
    'fulfills': {
        'transaction_id': creation_txid,
        'output_index': 0,
    },
    'owners_before': (alice.public_key,)
}
inputs = (input_,)

handcrafted_transfer_tx = {
    'asset': asset,
    'metadata': metadata,
    'operation': operation,
    'outputs': outputs,
    'inputs': inputs,
    'version': version,
}

json_str_tx = json.dumps(
    handcrafted_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

transfer_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

handcrafted_transfer_tx['id'] = transfer_txid

message = json.dumps(
    handcrafted_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

ed25519.sign(message.encode(), base58.b58decode(alice.private_key))

fulfillment_uri = ed25519.serialize_uri()

handcrafted_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984')
returned_transfer_tx = bdb.transactions.send(handcrafted_transfer_tx)

A few checks:

>>> json.dumps(returned_transfer_tx, sort_keys=True) == json.dumps(handcrafted_transfer_tx, sort_keys=True)
True
>>> bdb.transactions.status(transfer_txid)
{'status': 'valid'}

Tip

When checking for the status of a transaction, one should keep in mind tiny delays before a transaction reaches a valid status.

Bicycle Sharing Revisited

Handcrafting the CREATE transaction for our bicycle sharing example:

import json

import sha3
from cryptoconditions import Ed25519Sha256

from bigchaindb_driver.crypto import generate_keypair


bob, carly = generate_keypair(), generate_keypair()
version = '1.0'

asset = {
    'data': {
        'token_for': {
            'bicycle': {
                'manufacturer': 'bkfab',
                'serial_number': 'abcd1234',
            },
            'description': 'time share token. each token equals 1 hour of riding.'
        },
    },
}

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for carly
ed25519 = Ed25519Sha256(public_key=base58.b58decode(carly.public_key))

# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = ed25519.condition.serialize_uri()

# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict = {
    'type': ed25519.TYPE_NAME,
    'public_key': base58.b58encode(ed25519.public_key),
}

output = {
    'amount': '10',
    'condition': {
        'details': unsigned_fulfillment_dict,
        'uri': condition_uri,
    },
    'public_keys': (carly.public_key,),
}

input_ = {
    'fulfillment': None,
    'fulfills': None,
    'owners_before': (bob.public_key,)
}

token_creation_tx = {
    'operation': 'CREATE',
    'asset': asset,
    'metadata': None,
    'outputs': (output,),
    'inputs': (input_,),
    'version': version,
}

# JSON: serialize the id-less transaction to a json formatted string
json_str_tx = json.dumps(
    token_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# SHA3: hash the serialized id-less transaction to generate the id
creation_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

# add the id
token_creation_tx['id'] = creation_txid

# JSON: serialize the transaction-with-id to a json formatted string
message = json.dumps(
    token_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# CRYPTO-CONDITIONS: sign the serialized transaction-with-id
ed25519.sign(message.encode(), base58.b58decode(bob.private_key))

# CRYPTO-CONDITIONS: generate the fulfillment uri
fulfillment_uri = ed25519.serialize_uri()

# add the fulfillment uri (signature)
token_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984')
returned_creation_tx = bdb.transactions.send(token_creation_tx)

A few checks:

>>> json.dumps(returned_creation_tx, sort_keys=True) == json.dumps(token_creation_tx, sort_keys=True)
True

>>> token_creation_tx['inputs'][0]['owners_before'][0] == bob.public_key
True

>>> token_creation_tx['outputs'][0]['public_keys'][0] == carly.public_key
True

>>> token_creation_tx['outputs'][0]['amount'] == '10'
True
>>> bdb.transactions.status(creation_txid)
{'status': 'valid'}

Tip

When checking for the status of a transaction, one should keep in mind tiny delays before a transaction reaches a valid status.

Now Carly wants to ride the bicycle for 2 hours so she needs to send 2 tokens to Bob:

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for carly
bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for carly
carly_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carly.public_key))

# CRYPTO-CONDITIONS: generate the condition uris
bob_condition_uri = bob_ed25519.condition.serialize_uri()
carly_condition_uri = carly_ed25519.condition.serialize_uri()

# CRYPTO-CONDITIONS: get the unsigned fulfillment dictionary (details)
bob_unsigned_fulfillment_dict = {
    'type': bob_ed25519.TYPE_NAME,
    'public_key': base58.b58encode(bob_ed25519.public_key),
}
carly_unsigned_fulfillment_dict = {
    'type': carly_ed25519.TYPE_NAME,
    'public_key': base58.b58encode(carly_ed25519.public_key),
}

bob_output = {
    'amount': '2',
    'condition': {
        'details': bob_unsigned_fulfillment_dict,
        'uri': bob_condition_uri,
    },
    'public_keys': (bob.public_key,),
}
carly_output = {
    'amount': '8',
    'condition': {
        'details': carly_unsigned_fulfillment_dict,
        'uri': carly_condition_uri,
    },
    'public_keys': (carly.public_key,),
}

input_ = {
    'fulfillment': None,
    'fulfills': {
        'transaction_id': token_creation_tx['id'],
        'output_index': 0,
    },
    'owners_before': (carly.public_key,)
}

token_transfer_tx = {
    'operation': 'TRANSFER',
    'asset': {'id': token_creation_tx['id']},
    'metadata': None,
    'outputs': (bob_output, carly_output),
    'inputs': (input_,),
    'version': version,
}

# JSON: serialize the id-less transaction to a json formatted string
json_str_tx = json.dumps(
    token_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# SHA3: hash the serialized id-less transaction to generate the id
transfer_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

# add the id
token_transfer_tx['id'] = transfer_txid

# JSON: serialize the transaction-with-id to a json formatted string
message = json.dumps(
    token_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# CRYPTO-CONDITIONS: sign the serialized transaction-with-id for bob
carly_ed25519.sign(message.encode(), base58.b58decode(carly.private_key))

# CRYPTO-CONDITIONS: generate bob's fulfillment uri
fulfillment_uri = carly_ed25519.serialize_uri()

# add bob's fulfillment uri (signature)
token_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

bdb = BigchainDB('http://bdb-server:9984')
returned_transfer_tx = bdb.transactions.send(token_transfer_tx)

A few checks:

>>> json.dumps(returned_transfer_tx, sort_keys=True) == json.dumps(token_transfer_tx, sort_keys=True)
True

>>> token_transfer_tx['inputs'][0]['owners_before'][0] == carly.public_key
True
>>> bdb.transactions.status(creation_txid)
{'status': 'valid'}

Tip

When checking for the status of a transaction, one should keep in mind tiny delays before a transaction reaches a valid status.

Multiple Owners Revisited

Walkthrough

We’ll re-use the example of Alice and Bob owning a car together to handcraft transactions with multiple owners.

Say alice and bob own a car together:

In [117]: from bigchaindb_driver.crypto import generate_keypair

In [118]: from bigchaindb_driver import offchain

In [119]: alice, bob = generate_keypair(), generate_keypair()

In [120]: car_asset = {'data': {'car': {'vin': '5YJRE11B781000196'}}}

In [121]: car_creation_tx = offchain.prepare_transaction(
   .....:     operation='CREATE',
   .....:     signers=alice.public_key,
   .....:     recipients=(alice.public_key, bob.public_key),
   .....:     asset=car_asset,
   .....: )
   .....: 

In [122]: signed_car_creation_tx = offchain.fulfill_transaction(
   .....:     car_creation_tx,
   .....:     private_keys=alice.private_key,
   .....: )
   .....: 

In [123]: signed_car_creation_tx
Out[123]: 
{'asset': {'data': {'car': {'vin': '5YJRE11B781000196'}}},
 'id': 'cec2e07329339b9221a4231f98233b1d3fd139eea8265a18b74e6fe7abb458aa',
 'inputs': [{'fulfillment': 'pGSAIPkSJGbmhV_aUvrM8kBuwxra5KSkIwG2FSUgQc12ofGGgUCt2WDbKJLwklqLT4Pe-Kc9DB1XdMsVC3COlAdX5LttA2AORYKu3XKZ1H4xgS8mP39y4ep2cxuRtEn7JahduxQP',
   'fulfills': None,
   'owners_before': ['HmGZasLnKJbeTzX7YZQYTpLTbp4wnqy8Hpea4QjnW1Mf']}],
 'metadata': None,
 'operation': 'CREATE',
 'outputs': [{'amount': '1',
   'condition': {'details': {'subconditions': [{'public_key': 'HmGZasLnKJbeTzX7YZQYTpLTbp4wnqy8Hpea4QjnW1Mf',
       'type': 'ed25519-sha-256'},
      {'public_key': 'ECf8nhyZXApZ1ZxRqDiaMLYygFDhPY7gwiubqoaKeLkD',
       'type': 'ed25519-sha-256'}],
     'threshold': 2,
     'type': 'threshold-sha-256'},
    'uri': 'ni:///sha-256;u4bAILnVjmueTTcZ3REwMQy_N3YL2tr8m0EZSA2Q6p0?fpt=threshold-sha-256&cost=264192&subtypes=ed25519-sha-256'},
   'public_keys': ['HmGZasLnKJbeTzX7YZQYTpLTbp4wnqy8Hpea4QjnW1Mf',
    'ECf8nhyZXApZ1ZxRqDiaMLYygFDhPY7gwiubqoaKeLkD']}],
 'version': '1.0'}
sent_car_tx = bdb.transactions.send(signed_car_creation_tx)

One day, alice and bob, having figured out how to teleport themselves, and realizing they no longer need their car, wish to transfer the ownership of their car over to carol:

In [124]: carol = generate_keypair()

In [125]: output_index = 0

In [126]: output = signed_car_creation_tx['outputs'][output_index]

In [127]: input_ = {
   .....:     'fulfillment': output['condition']['details'],
   .....:     'fulfills': {
   .....:         'output_index': output_index,
   .....:         'transaction_id': signed_car_creation_tx['id'],
   .....:     },
   .....:     'owners_before': output['public_keys'],
   .....: }
   .....: 

In [128]: asset = signed_car_creation_tx['id']

In [129]: car_transfer_tx = offchain.prepare_transaction(
   .....:     operation='TRANSFER',
   .....:     recipients=carol.public_key,
   .....:     asset={'id': car_creation_tx['id']},
   .....:     inputs=input_,
   .....: )
   .....: 

In [130]: signed_car_transfer_tx = offchain.fulfill_transaction(
   .....:     car_transfer_tx, private_keys=[alice.private_key, bob.private_key]
   .....: )
   .....: 

In [131]: signed_car_transfer_tx
Out[131]: 
{'asset': {'id': 'cec2e07329339b9221a4231f98233b1d3fd139eea8265a18b74e6fe7abb458aa'},
 'id': '3d08f65b06eb5651f31eddf6b6d527a391b08a9c347589e169f2630ba2e456c2',
 'inputs': [{'fulfillment': 'ooHRoIHMpGSAIMQkcABrq09wYYPXuJc2L8uCxdrXh3F2yNbkByhVa-V-gUBb-yvt6EpLw9UNB2DW54dcZs8Vt6zTClgxoFSpWJmL-kb2BUyNK7k4jRELMbxHA_PPo3KIWot0WSDe2Kpx_6YPpGSAIPkSJGbmhV_aUvrM8kBuwxra5KSkIwG2FSUgQc12ofGGgUDWqW0bajhCXVIhncBQedWYQJfutKUG4KtSmS47eXitOWIfSHpf3cfKOCoAUehzgLgkw-7Lo5AkJA6j_ny7UYUPoQA',
   'fulfills': {'output_index': 0,
    'transaction_id': 'cec2e07329339b9221a4231f98233b1d3fd139eea8265a18b74e6fe7abb458aa'},
   'owners_before': ['HmGZasLnKJbeTzX7YZQYTpLTbp4wnqy8Hpea4QjnW1Mf',
    'ECf8nhyZXApZ1ZxRqDiaMLYygFDhPY7gwiubqoaKeLkD']}],
 'metadata': None,
 'operation': 'TRANSFER',
 'outputs': [{'amount': '1',
   'condition': {'details': {'public_key': 'D1sXu7eXFBJF2hhbkepNQfjYy2YGKH77PA5TmQwBJmNd',
     'type': 'ed25519-sha-256'},
    'uri': 'ni:///sha-256;yL5HVlkfk5KVaig-1uoyXY819UTF4-fUbgUznA78CvQ?fpt=ed25519-sha-256&cost=131072'},
   'public_keys': ['D1sXu7eXFBJF2hhbkepNQfjYy2YGKH77PA5TmQwBJmNd']}],
 'version': '1.0'}

Sending the transaction to a BigchainDB node:

sent_car_transfer_tx = bdb.transactions.send(signed_car_transfer_tx)
Doing this manually

In order to do this manually, let’s first import the necessary tools (json, sha3, and cryptoconditions):

In [132]: import json

In [133]: from sha3 import sha3_256

In [134]: from cryptoconditions import Ed25519Sha256, ThresholdSha256

Create the asset, setting all values:

In [135]: car_asset = {
   .....:     'data': {
   .....:         'car': {
   .....:             'vin': '5YJRE11B781000196',
   .....:         },
   .....:     },
   .....: }
   .....: 

Generate the output condition:

In [136]: alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

In [137]: bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

In [138]: threshold_sha256 = ThresholdSha256(threshold=2)

In [139]: threshold_sha256.add_subfulfillment(alice_ed25519)

In [140]: threshold_sha256.add_subfulfillment(bob_ed25519)

In [141]: condition_uri = threshold_sha256.condition.serialize_uri()

In [142]: condition_details = {
   .....:     'subconditions': [
   .....:         {'type': s['body'].TYPE_NAME,
   .....:          'public_key': base58.b58encode(s['body'].public_key)}
   .....:         for s in threshold_sha256.subconditions
   .....:         if (s['type'] == 'fulfillment' and
   .....:             s['body'].TYPE_NAME == 'ed25519-sha-256')
   .....:      ],
   .....:     'threshold': threshold_sha256.threshold,
   .....:     'type': threshold_sha256.TYPE_NAME,
   .....: }
   .....: 

In [143]: output = {
   .....:     'amount': '1',
   .....:     'condition': {
   .....:         'details': condition_details,
   .....:         'uri': condition_uri,
   .....:     },
   .....:     'public_keys': (alice.public_key, bob.public_key),
   .....: }
   .....: 

Tip

The condition uri could have been generated in a slightly different way, which may be more intuitive to you. You can think of the threshold condition containing sub conditions:

In [144]: alt_threshold_sha256 = ThresholdSha256(threshold=2)

In [145]: alt_threshold_sha256.add_subcondition(alice_ed25519.condition)

In [146]: alt_threshold_sha256.add_subcondition(bob_ed25519.condition)

In [147]: alt_threshold_sha256.condition.serialize_uri() == condition_uri
Out[147]: True

The details on the other hand hold the associated fulfillments not yet fulfilled.

The yet to be fulfilled input:

In [148]: input_ = {
   .....:     'fulfillment': None,
   .....:     'fulfills': None,
   .....:     'owners_before': (alice.public_key,),
   .....: }
   .....: 

Craft the payload:

In [149]: version = '1.0'

In [150]: handcrafted_car_creation_tx = {
   .....:     'operation': 'CREATE',
   .....:     'asset': car_asset,
   .....:     'metadata': None,
   .....:     'outputs': (output,),
   .....:     'inputs': (input_,),
   .....:     'version': version,
   .....: }
   .....: 

Generate the id, by hashing the encoded json formatted string representation of the transaction:

In [151]: json_str_tx = json.dumps(
   .....:     handcrafted_car_creation_tx,
   .....:     sort_keys=True,
   .....:     separators=(',', ':'),
   .....:     ensure_ascii=False,
   .....: )
   .....: 

In [152]: car_creation_txid = sha3_256(json_str_tx.encode()).hexdigest()

In [153]: handcrafted_car_creation_tx['id'] = car_creation_txid

Let’s make sure our txid is the same as the one provided by the driver:

In [154]: handcrafted_car_creation_tx['id'] == car_creation_tx['id']
Out[154]: True

Sign the transaction:

In [155]: message = json.dumps(
   .....:     handcrafted_car_creation_tx,
   .....:     sort_keys=True,
   .....:     separators=(',', ':'),
   .....:     ensure_ascii=False,
   .....: )
   .....: 

In [156]: alice_ed25519.sign(message.encode(), base58.b58decode(alice.private_key))
Out[156]: b'\xad\xd9`\xdb(\x92\xf0\x92Z\x8bO\x83\xde\xf8\xa7=\x0c\x1dWt\xcb\x15\x0bp\x8e\x94\x07W\xe4\xbbm\x03`\x0eE\x82\xae\xddr\x99\xd4~1\x81/&?\x7fr\xe1\xeavs\x1b\x91\xb4I\xfb%\xa8]\xbb\x14\x0f'

In [157]: fulfillment_uri = alice_ed25519.serialize_uri()

In [158]: handcrafted_car_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Compare our signed CREATE transaction with the driver’s:

In [159]: (json.dumps(handcrafted_car_creation_tx, sort_keys=True) ==
   .....:  json.dumps(signed_car_creation_tx, sort_keys=True))
   .....: 
Out[159]: True

The transfer to Carol:

In [160]: alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

In [161]: bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

In [162]: carol_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))

In [163]: unsigned_fulfillments_dict = {
   .....:     'type': carol_ed25519.TYPE_NAME,
   .....:     'public_key': base58.b58encode(carol_ed25519.public_key),
   .....: }
   .....: 

In [164]: condition_uri = carol_ed25519.condition.serialize_uri()

In [165]: output = {
   .....:     'amount': '1',
   .....:     'condition': {
   .....:         'details': unsigned_fulfillments_dict,
   .....:         'uri': condition_uri,
   .....:     },
   .....:     'public_keys': (carol.public_key,),
   .....: }
   .....: 

The yet to be fulfilled input:

In [166]: input_ = {
   .....:     'fulfillment': None,
   .....:     'fulfills': {
   .....:         'transaction_id': handcrafted_car_creation_tx['id'],
   .....:         'output_index': 0,
   .....:     },
   .....:     'owners_before': (alice.public_key, bob.public_key),
   .....: }
   .....: 

Craft the payload:

In [167]: handcrafted_car_transfer_tx = {
   .....:     'operation': 'TRANSFER',
   .....:     'asset': {'id': handcrafted_car_creation_tx['id']},
   .....:     'metadata': None,
   .....:     'outputs': (output,),
   .....:     'inputs': (input_,),
   .....:     'version': version,
   .....: }
   .....: 

Generate the id, by hashing the encoded json formatted string representation of the transaction:

In [168]: json_str_tx = json.dumps(
   .....:     handcrafted_car_transfer_tx,
   .....:     sort_keys=True,
   .....:     separators=(',', ':'),
   .....:     ensure_ascii=False,
   .....: )
   .....: 

In [169]: car_transfer_txid = sha3_256(json_str_tx.encode()).hexdigest()

In [170]: handcrafted_car_transfer_tx['id'] = car_transfer_txid

Let’s make sure our txid is the same as the one provided by the driver:

In [171]: handcrafted_car_transfer_tx['id'] == car_transfer_tx['id']
Out[171]: True

Sign the transaction:

In [172]: message = json.dumps(
   .....:     handcrafted_car_transfer_tx,
   .....:     sort_keys=True,
   .....:     separators=(',', ':'),
   .....:     ensure_ascii=False,
   .....: )
   .....: 

In [173]: threshold_sha256 = ThresholdSha256(threshold=2)

In [174]: alice_ed25519.sign(message=message.encode(),

In [174]: bob_ed25519.sign(message=message.encode(),

In [174]: threshold_sha256.add_subfulfillment(alice_ed25519)

In [174]: threshold_sha256.add_subfulfillment(bob_ed25519)

In [174]: fulfillment_uri = threshold_sha256.serialize_uri()

In [174]: handcrafted_car_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Compare our signed TRANSFER transaction with the driver’s:

In [174]: (json.dumps(handcrafted_car_transfer_tx, sort_keys=True) ==
   .....:  json.dumps(signed_car_transfer_tx, sort_keys=True))
   .....: 

In a nutshell

Handcrafting the 'CREATE' transaction
import json

import base58
import sha3
from cryptoconditions import Ed25519Sha256, ThresholdSha256

from bigchaindb_driver.crypto import generate_keypair


version = '1.0'

car_asset = {
    'data': {
        'car': {
            'vin': '5YJRE11B781000196',
        },
    },
}

alice, bob = generate_keypair(), generate_keypair()

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for alice
alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for bob
bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

# CRYPTO-CONDITIONS: instantiate a threshold SHA 256 crypto-condition
threshold_sha256 = ThresholdSha256(threshold=2)

# CRYPTO-CONDITIONS: add alice ed25519 to the threshold SHA 256 condition
threshold_sha256.add_subfulfillment(alice_ed25519)

# CRYPTO-CONDITIONS: add bob ed25519 to the threshold SHA 256 condition
threshold_sha256.add_subfulfillment(bob_ed25519)

# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = threshold_sha256.condition.serialize_uri()

# CRYPTO-CONDITIONS: get the unsigned fulfillment dictionary (details)
condition_details = {
    'subconditions': [
        {'type': s['body'].TYPE_NAME,
         'public_key': base58.b58encode(s['body'].public_key)}
        for s in threshold_sha256.subconditions
        if (s['type'] == 'fulfillment' and
            s['body'].TYPE_NAME == 'ed25519-sha-256')
    ],
    'threshold': threshold_sha256.threshold,
    'type': threshold_sha256.TYPE_NAME,
}

output = {
    'amount': '1',
    'condition': {
        'details': condition_details,
        'uri': threshold_sha256.condition_uri,
    },
    'public_keys': (alice.public_key, bob.public_key),
}

# The yet to be fulfilled input:
input_ = {
    'fulfillment': None,
    'fulfills': None,
    'owners_before': (alice.public_key,),
}

# Craft the payload:
handcrafted_car_creation_tx = {
    'operation': 'CREATE',
    'asset': car_asset,
    'metadata': None,
    'outputs': (output,),
    'inputs': (input_,),
    'version': version,
}

# JSON: serialize the id-less transaction to a json formatted string
# Generate the id, by hashing the encoded json formatted string representation of
# the transaction:
json_str_tx = json.dumps(
    handcrafted_car_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# SHA3: hash the serialized id-less transaction to generate the id
car_creation_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

# add the id
handcrafted_car_creation_tx['id'] = car_creation_txid

# JSON: serialize the transaction-with-id to a json formatted string
message = json.dumps(
    handcrafted_car_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# CRYPTO-CONDITIONS: sign the serialized transaction-with-id
alice_ed25519.sign(message.encode(), base58.b58decode(alice.private_key))

# CRYPTO-CONDITIONS: generate the fulfillment uri
fulfillment_uri = alice_ed25519.serialize_uri()

# add the fulfillment uri (signature)
handcrafted_car_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984')
returned_car_creation_tx = bdb.transactions.send(handcrafted_car_creation_tx)

Wait for some nano seconds, and check the status:

>>> bdb.transactions.status(returned_car_creation_tx['id'])
{'status': 'valid'}
Handcrafting the 'TRANSFER' transaction
version = '1.0'

carol = generate_keypair()

alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

carol_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))

unsigned_fulfillments_dict = {
    'type': carol_ed25519.TYPE_NAME,
    'public_key': base58.b58encode(carol_ed25519.public_key),
}

condition_uri = carol_ed25519.condition.serialize_uri()

output = {
    'amount': '1',
    'condition': {
        'details': unsigned_fulfillments_dict,
        'uri': condition_uri,
    },
    'public_keys': (carol.public_key,),
}

# The yet to be fulfilled input:
input_ = {
    'fulfillment': None,
    'fulfills': {
        'transaction_id': handcrafted_car_creation_tx['id'],
        'output_index': 0,
    },
    'owners_before': (alice.public_key, bob.public_key),
}

# Craft the payload:
handcrafted_car_transfer_tx = {
    'operation': 'TRANSFER',
    'asset': {'id': handcrafted_car_creation_tx['id']},
    'metadata': None,
    'outputs': (output,),
    'inputs': (input_,),
    'version': version,
}

# Generate the id, by hashing the encoded json formatted string
# representation of the transaction:
json_str_tx = json.dumps(
    handcrafted_car_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

car_transfer_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

handcrafted_car_transfer_tx['id'] = car_transfer_txid

# Sign the transaction:
message = json.dumps(
    handcrafted_car_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

threshold_sha256 = ThresholdSha256(threshold=2)

alice_ed25519.sign(message=message.encode(),
                   private_key=base58.b58decode(alice.private_key))
bob_ed25519.sign(message=message.encode(),
                 private_key=base58.b58decode(bob.private_key))

threshold_sha256.add_subfulfillment(alice_ed25519)

threshold_sha256.add_subfulfillment(bob_ed25519)

fulfillment_uri = threshold_sha256.serialize_uri()

handcrafted_car_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

bdb = BigchainDB('http://bdb-server:9984')
returned_car_transfer_tx = bdb.transactions.send(handcrafted_car_transfer_tx)

Wait for some nano seconds, and check the status:

>>> bdb.transactions.status(returned_car_transfer_tx['id'])
{'status': 'valid'}

Multiple Owners with m-of-n Signatures

In this example, alice and bob co-own a car asset such that only one of them is required to sign the transfer transaction. The example is very similar to the one where both owners are required to sign, but with minor differences that are very important, in order to make the fulfillment URI valid.

We only show the “nutshell” version for now. The example is self-contained.

In a nutshell

Handcrafting the 'CREATE' transaction
import json

import base58
import sha3
from cryptoconditions import Ed25519Sha256, ThresholdSha256

from bigchaindb_driver.crypto import generate_keypair


version = '1.0'

car_asset = {
    'data': {
        'car': {
            'vin': '5YJRE11B781000196',
        },
    },
}

alice, bob = generate_keypair(), generate_keypair()

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for alice
alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for bob
bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

# CRYPTO-CONDITIONS: instantiate a threshold SHA 256 crypto-condition
# NOTICE that the threshold is set to 1, not 2
threshold_sha256 = ThresholdSha256(threshold=1)

# CRYPTO-CONDITIONS: add alice ed25519 to the threshold SHA 256 condition
threshold_sha256.add_subfulfillment(alice_ed25519)

# CRYPTO-CONDITIONS: add bob ed25519 to the threshold SHA 256 condition
threshold_sha256.add_subfulfillment(bob_ed25519)

# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = threshold_sha256.condition.serialize_uri()

# CRYPTO-CONDITIONS: get the unsigned fulfillment dictionary (details)
condition_details = {
    'subconditions': [
        {'type': s['body'].TYPE_NAME,
         'public_key': base58.b58encode(s['body'].public_key)}
        for s in threshold_sha256.subconditions
        if (s['type'] == 'fulfillment' and
            s['body'].TYPE_NAME == 'ed25519-sha-256')
    ],
    'threshold': threshold_sha256.threshold,
    'type': threshold_sha256.TYPE_NAME,
}

output = {
    'amount': '1',
    'condition': {
        'details': condition_details,
        'uri': threshold_sha256.condition_uri,
    },
    'public_keys': (alice.public_key, bob.public_key),
}

# The yet to be fulfilled input:
input_ = {
    'fulfillment': None,
    'fulfills': None,
    'owners_before': (alice.public_key,),
}

# Craft the payload:
handcrafted_car_creation_tx = {
    'operation': 'CREATE',
    'asset': car_asset,
    'metadata': None,
    'outputs': (output,),
    'inputs': (input_,),
    'version': version,
}

# JSON: serialize the id-less transaction to a json formatted string
# Generate the id, by hashing the encoded json formatted string representation of
# the transaction:
json_str_tx = json.dumps(
    handcrafted_car_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# SHA3: hash the serialized id-less transaction to generate the id
car_creation_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

# add the id
handcrafted_car_creation_tx['id'] = car_creation_txid

# JSON: serialize the transaction-with-id to a json formatted string
message = json.dumps(
    handcrafted_car_creation_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

# CRYPTO-CONDITIONS: sign the serialized transaction-with-id
alice_ed25519.sign(message.encode(), base58.b58decode(alice.private_key))

# CRYPTO-CONDITIONS: generate the fulfillment uri
fulfillment_uri = alice_ed25519.serialize_uri()

# add the fulfillment uri (signature)
handcrafted_car_creation_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984')
returned_car_creation_tx = bdb.transactions.send(handcrafted_car_creation_tx)

Wait for some nano seconds, and check the status:

>>> bdb.transactions.status(returned_car_creation_tx['id'])
 {'status': 'valid'}
Handcrafting the 'TRANSFER' transaction
version = '1.0'

carol = generate_keypair()

alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))

bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))

carol_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))

condition_uri = carol_ed25519.condition.serialize_uri()

output = {
    'amount': '1',
    'condition': {
        'details': {
            'type': carol_ed25519.TYPE_NAME,
            'public_key': base58.b58encode(carol_ed25519.public_key),
        },
        'uri': condition_uri,
    },
    'public_keys': (carol.public_key,),
}

# The yet to be fulfilled input:
input_ = {
    'fulfillment': None,
    'fulfills': {
        'transaction_id': handcrafted_car_creation_tx['id'],
        'output_index': 0,
    },
    'owners_before': (alice.public_key, bob.public_key),
}

# Craft the payload:
handcrafted_car_transfer_tx = {
    'operation': 'TRANSFER',
    'asset': {'id': handcrafted_car_creation_tx['id']},
    'metadata': None,
    'outputs': (output,),
    'inputs': (input_,),
    'version': version,
}

# Generate the id, by hashing the encoded json formatted string
# representation of the transaction:
json_str_tx = json.dumps(
    handcrafted_car_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

car_transfer_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()

handcrafted_car_transfer_tx['id'] = car_transfer_txid

# Sign the transaction:
message = json.dumps(
    handcrafted_car_transfer_tx,
    sort_keys=True,
    separators=(',', ':'),
    ensure_ascii=False,
)

threshold_sha256 = ThresholdSha256(threshold=1)

alice_ed25519.sign(message.encode(),
                   private_key=base58.b58decode(alice.private_key))

threshold_sha256.add_subfulfillment(alice_ed25519)

threshold_sha256.add_subcondition(bob_ed25519.condition)

fulfillment_uri = threshold_sha256.serialize_uri()

handcrafted_car_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

bdb = BigchainDB('http://bdb-server:9984')
returned_car_transfer_tx = bdb.transactions.send(handcrafted_car_transfer_tx)

Wait for some nano seconds, and check the status:

>>> bdb.transactions.status(returned_car_transfer_tx['id'])
 {'status': 'valid'}

Advanced Installation Options

Installing from the Source Code

The source code for the BigchainDB Python Driver can be downloaded from the Github repo. You can either clone the public repository:

git clone git://github.com/bigchaindb/bigchaindb-driver

Or download the tarball:

curl  -OL https://github.com/bigchaindb/bigchaindb-driver/tarball/master

Once you have a copy of the source code, you can install it by going to the directory containing setup.py and doing:

python setup.py install

Installing latest master with pip

To work with the latest BigchainDB (server) master branch:

$ pip install --process-dependency-links git+https://github.com/bigchaindb/bigchaindb-driver.git

Then connect to some BigchainDB node which is running BigchainDB server master.

Upgrading

Upgrading Using pip

If you installed the BigchainDB Python Driver using pip install bigchaindb_driver, then you can upgrade it to the latest version using:

pip install --upgrade bigchaindb_driver

Library Reference

driver

class bigchaindb_driver.BigchainDB(*nodes, transport_class=<class 'bigchaindb_driver.transport.Transport'>, headers=None)[source]

Bases: object

BigchainDB driver class.

A BigchainDB driver is able to create, sign, and submit transactions to one or more nodes in a Federation.

If initialized with >1 nodes, the driver will send successive requests to different nodes in a round-robin fashion (this will be customizable in the future).

__init__(*nodes, transport_class=<class 'bigchaindb_driver.transport.Transport'>, headers=None)[source]

Initialize a BigchainDB driver instance.

Parameters:
  • *nodes (str) – BigchainDB nodes to connect to. Currently, the full URL must be given. In the absence of any node, the default ('http://localhost:9984') will be used.
  • transport_class – Optional transport class to use. Defaults to Transport.
  • headers (dict) – Optional headers that will be passed with each request. To pass headers only on a per-request basis, you can pass the headers to the method of choice (e.g. BigchainDB().transactions.send()).
api_info(headers=None)[source]

Retrieves information provided by the API root endpoint '/api/v1'.

Parameters:headers (dict) – Optional headers to pass to the request.
Returns:Details of the HTTP API provided by the BigchainDB server.
Return type:dict
assets

AssetsEndpoint – Exposes functionalities of the '/assets' endpoint.

blocks

BlocksEndpoint – Exposes functionalities of the '/blocks' endpoint.

info(headers=None)[source]

Retrieves information of the node being connected to via the root endpoint '/'.

Parameters:headers (dict) – Optional headers to pass to the request.
Returns:Details of the node that this instance is connected to. Some information that may be interesting:
  • the server version,
  • the public key of the node, and
  • its key ring (list of public keys of the nodes this node is connected to).
Return type:dict

Note

Currently limited to one node, and will be expanded to return information for each node that this instance is connected to.

nodes

tuple of str – URLs of connected nodes.

outputs

OutputsEndpoint – Exposes functionalities of the '/outputs' endpoint.

transactions

TransactionsEndpoint – Exposes functionalities of the '/transactions' endpoint.

transport

Transport – Object responsible for forwarding requests to a Connection instance (node).

class bigchaindb_driver.driver.TransactionsEndpoint(driver)[source]

Bases: bigchaindb_driver.driver.NamespacedDriver

Exposes functionality of the '/transactions/' endpoint.

path

str – The path of the endpoint.

static fulfill(transaction, private_keys)[source]

Fulfills the given transaction.

Parameters:
  • transaction (dict) – The transaction to be fulfilled.
  • private_keys (str | list | tuple) – One or more private keys to be used for fulfilling the transaction.
Returns:

The fulfilled transaction payload, ready to be sent to a BigchainDB federation.

Return type:

dict

Raises:

MissingPrivateKeyError – If a private key is missing.

get(*, asset_id, operation=None, headers=None)[source]

Given an asset id, get its list of transactions (and optionally filter for only 'CREATE' or 'TRANSFER' transactions).

Parameters:
  • asset_id (str) – Id of the asset.
  • operation (str) – The type of operation the transaction should be. Either 'CREATE' or 'TRANSFER'. Defaults to None.
  • headers (dict) – Optional headers to pass to the request.

Note

Please note that the id of an asset in BigchainDB is actually the id of the transaction which created the asset. In other words, when querying for an asset id with the operation set to 'CREATE', only one transaction should be expected. This transaction will be the transaction in which the asset was created, and the transaction id will be equal to the given asset id. Hence, the following calls to retrieve() and get() should return the same transaction.

>>> bdb = BigchainDB()
>>> bdb.transactions.retrieve('foo')
>>> bdb.transactions.get(asset_id='foo', operation='CREATE')

Since get() returns a list of transactions, it may be more efficient to use retrieve() instead, if one is only interested in the 'CREATE' operation.

Returns:List of transactions.
Return type:list
static prepare(*, operation='CREATE', signers=None, recipients=None, asset=None, metadata=None, inputs=None)[source]

Prepares a transaction payload, ready to be fulfilled.

Parameters:
  • operation (str) – The operation to perform. Must be 'CREATE' or 'TRANSFER'. Case insensitive. Defaults to 'CREATE'.
  • signers (list | tuple | str, optional) – One or more public keys representing the issuer(s) of the asset being created. Only applies for 'CREATE' operations. Defaults to None.
  • recipients (list | tuple | str, optional) – One or more public keys representing the new recipients(s) of the asset being created or transferred. Defaults to None.
  • asset (dict, optional) – The asset to be created or transferred. MUST be supplied for 'TRANSFER' operations. Defaults to None.
  • metadata (dict, optional) – Metadata associated with the transaction. Defaults to None.
  • inputs (dict | list | tuple, optional) – One or more inputs holding the condition(s) that this transaction intends to fulfill. Each input is expected to be a dict. Only applies to, and MUST be supplied for, 'TRANSFER' operations.
Returns:

The prepared transaction.

Return type:

dict

Raises:

BigchaindbException – If operation is not 'CREATE' or 'TRANSFER'.

Important

CREATE operations

  • signers MUST be set.

  • recipients, asset, and metadata MAY be set.

  • If asset is set, it MUST be in the form of:

    {
        'data': {
            ...
        }
    }
    
  • The argument inputs is ignored.

  • If recipients is not given, or evaluates to False, it will be set equal to signers:

    if not recipients:
        recipients = signers
    

TRANSFER operations

  • recipients, asset, and inputs MUST be set.

  • asset MUST be in the form of:

    {
        'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
    }
    
  • metadata MAY be set.

  • The argument signers is ignored.

retrieve(txid, headers=None)[source]

Retrieves the transaction with the given id.

Parameters:
  • txid (str) – Id of the transaction to retrieve.
  • headers (dict) – Optional headers to pass to the request.
Returns:

The transaction with the given id.

Return type:

dict

send(transaction, headers=None)[source]

Submit a transaction to the Federation.

Parameters:
  • transaction (dict) – the transaction to be sent to the Federation node(s).
  • headers (dict) – Optional headers to pass to the request.
Returns:

The transaction sent to the Federation node(s).

Return type:

dict

status(txid, headers=None)[source]

Retrieves the status of the transaction with the given id.

Parameters:
  • txid (str) – Id of the transaction to retrieve the status for.
  • headers (dict) – Optional headers to pass to the request.
Returns:

A dict containing a ‘status’ item for the transaction.

Return type:

dict

class bigchaindb_driver.driver.OutputsEndpoint(driver)[source]

Bases: bigchaindb_driver.driver.NamespacedDriver

Exposes functionality of the '/outputs' endpoint.

path

str – The path of the endpoint.

get(public_key, spent=None, headers=None)[source]
Parameters:
  • public_key (str) – Public key for which unfulfilled conditions are sought.
  • spent (bool) – Indicate if the result set should include only spent or only unspent outputs. If not specified (None) the result includes all the outputs (both spent and unspent) associated with the public key.
  • headers (dict) – Optional headers to pass to the request.
Returns:

List of unfulfilled conditions.

Return type:

list of str

Example

Given a transaction with id da1b64a907ba54 having an ed25519 condition (at index 0) with alice’s public key:

>>> bdb = BigchainDB()
>>> bdb.outputs.get(alice_pubkey)
... ['../transactions/da1b64a907ba54/conditions/0']
class bigchaindb_driver.driver.AssetsEndpoint(driver)[source]

Bases: bigchaindb_driver.driver.NamespacedDriver

Exposes functionality of the '/assets' endpoint.

path

str – The path of the endpoint.

get(*, search, limit=0, headers=None)[source]

Retrieves the assets that match a given text search string.

Parameters:
  • search (str) – Text search string.
  • limit (int) – Limit the number of returned documents. Defaults to zero meaning that it returns all the matching assets.
  • headers (dict) – Optional headers to pass to the request.
Returns:

List of assets that match the query.

Return type:

list of dict

class bigchaindb_driver.driver.NamespacedDriver(driver)[source]

Bases: object

Base class for creating endpoints (namespaced objects) that can be added under the BigchainDB driver.

__init__(driver)[source]

Initializes an instance of NamespacedDriver with the given driver instance.

Parameters:driver (BigchainDB) – Instance of BigchainDB.

offchain

Module for operations that can be performed “offchain”, meaning without a connection to one or more BigchainDB federation nodes.

bigchaindb_driver.offchain.prepare_transaction(*, operation='CREATE', signers=None, recipients=None, asset=None, metadata=None, inputs=None)[source]

Prepares a transaction payload, ready to be fulfilled. Depending on the value of operation, simply dispatches to either prepare_create_transaction() or prepare_transfer_transaction().

Parameters:
  • operation (str) – The operation to perform. Must be 'CREATE' or 'TRANSFER'. Case insensitive. Defaults to 'CREATE'.
  • signers (list | tuple | str, optional) – One or more public keys representing the issuer(s) of the asset being created. Only applies for 'CREATE' operations. Defaults to None.
  • recipients (list | tuple | str, optional) – One or more public keys representing the new recipients(s) of the asset being created or transferred. Defaults to None.
  • asset (dict, optional) – The asset to be created or transferred. MUST be supplied for 'TRANSFER' operations. Defaults to None.
  • metadata (dict, optional) – Metadata associated with the transaction. Defaults to None.
  • inputs (dict | list | tuple, optional) – One or more inputs holding the condition(s) that this transaction intends to fulfill. Each input is expected to be a dict. Only applies to, and MUST be supplied for, 'TRANSFER' operations.
Returns:

The prepared transaction.

Return type:

dict

Raises:

BigchaindbException – If operation is not 'CREATE' or 'TRANSFER'.

Important

CREATE operations

  • signers MUST be set.

  • recipients, asset, and metadata MAY be set.

  • If asset is set, it MUST be in the form of:

    {
        'data': {
            ...
        }
    }
    
  • The argument inputs is ignored.

  • If recipients is not given, or evaluates to False, it will be set equal to signers:

    if not recipients:
        recipients = signers
    

TRANSFER operations

  • recipients, asset, and inputs MUST be set.

  • asset MUST be in the form of:

    {
        'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
    }
    
  • metadata MAY be set.

  • The argument signers is ignored.

bigchaindb_driver.offchain.prepare_create_transaction(*, signers, recipients=None, asset=None, metadata=None)[source]

Prepares a "CREATE" transaction payload, ready to be fulfilled.

Parameters:
  • signers (list | tuple | str) – One or more public keys representing the issuer(s) of the asset being created.
  • recipients (list | tuple | str, optional) – One or more public keys representing the new recipients(s) of the asset being created. Defaults to None.
  • asset (dict, optional) – The asset to be created. Defaults to None.
  • metadata (dict, optional) – Metadata associated with the transaction. Defaults to None.
Returns:

The prepared "CREATE" transaction.

Return type:

dict

Important

  • If asset is set, it MUST be in the form of:

    {
        'data': {
            ...
        }
    }
    
  • If recipients is not given, or evaluates to False, it will be set equal to signers:

    if not recipients:
        recipients = signers
    
bigchaindb_driver.offchain.prepare_transfer_transaction(*, inputs, recipients, asset, metadata=None)[source]

Prepares a "TRANSFER" transaction payload, ready to be fulfilled.

Parameters:
  • inputs (dict | list | tuple) – One or more inputs holding the condition(s) that this transaction intends to fulfill. Each input is expected to be a dict.
  • recipients (str | list | tuple) – One or more public keys representing the new recipients(s) of the asset being transferred.
  • asset (dict) – A single-key dictionary holding the id of the asset being transferred with this transaction.
  • metadata (dict) – Metadata associated with the transaction. Defaults to None.
Returns:

The prepared "TRANSFER" transaction.

Return type:

dict

Important

  • asset MUST be in the form of:

    {
        'id': '<Asset ID (i.e. TX ID of its CREATE transaction)>'
    }
    

Example

Todo

Replace this section with docs.

In case it may not be clear what an input should look like, say Alice (public key: '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf') wishes to transfer an asset over to Bob (public key: 'EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA'). Let the asset creation transaction payload be denoted by tx:

# noqa E501
>>> tx
    {'asset': {'data': {'msg': 'Hello BigchainDB!'}},
     'id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87',
     'inputs': [{'fulfillment': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
                                 'type': 'ed25519-sha-256'},
                 'fulfills': None,
                 'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
     'metadata': None,
     'operation': 'CREATE',
     'outputs': [{'amount': '1',
                  'condition': {'details': {'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
                                            'type': 'ed25519-sha-256'},
                                'uri': 'ni:///sha-256;7ApQLsLLQgj5WOUipJg1txojmge68pctwFxvc3iOl54?fpt=ed25519-sha-256&cost=131072'},
                  'public_keys': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}],
     'version': '1.0'}

Then, the input may be constructed in this way:

output_index
output = tx['transaction']['outputs'][output_index]
input_ = {
    'fulfillment': output['condition']['details'],
    'input': {
        'output_index': output_index,
        'transaction_id': tx['id'],
    },
    'owners_before': output['owners_after'],
}

Displaying the input on the prompt would look like:

>>> input_
{'fulfillment': {
  'public_key': '3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf',
  'type': 'ed25519-sha-256'},
 'input': {'output_index': 0,
  'transaction_id': '9650055df2539223586d33d273cb8fd05bd6d485b1fef1caf7c8901a49464c87'},
 'owners_before': ['3Cxh1eKZk3Wp9KGBWFS7iVde465UvqUKnEqTg2MW4wNf']}

To prepare the transfer:

>>> prepare_transfer_transaction(
...     inputs=input_,
...     recipients='EcRawy3Y22eAUSS94vLF8BVJi62wbqbD9iSUSUNU9wAA',
...     asset=tx['transaction']['asset'],
... )
bigchaindb_driver.offchain.fulfill_transaction(transaction, *, private_keys)[source]

Fulfills the given transaction.

Parameters:
  • transaction (dict) – The transaction to be fulfilled.
  • private_keys (str | list | tuple) – One or more private keys to be used for fulfilling the transaction.
Returns:

The fulfilled transaction payload, ready to be sent to a BigchainDB federation.

Return type:

dict

Raises:

MissingPrivateKeyError – If a private key is missing.

transport

class bigchaindb_driver.transport.Transport(*nodes, headers=None)[source]

Bases: object

Transport class.

__init__(*nodes, headers=None)[source]

Initializes an instance of Transport.

Parameters:
  • nodes – nodes
  • headers (dict) – Optional headers to pass to the Connection instances, which will add it to the headers to be sent with each request.
forward_request(method, path=None, json=None, params=None, headers=None)[source]

Forwards an http request to a connection.

Parameters:
  • method (str) – HTTP method name (e.g.: 'GET').
  • path (str) – Path to be appended to the base url of a node. E.g.: '/transactions').
  • json (dict) – Payload to be sent with the HTTP request.
  • params (dict)) – Dictionary of URL (query) parameters.
  • headers (dict) – Optional headers to pass to the request.
Returns:

Result of requests.models.Response.json()

Return type:

dict

get_connection()[source]

Gets a connection from the pool.

Returns:A Connection instance.
init_pool(nodes, headers=None)[source]

Initializes the pool of connections.

pool

class bigchaindb_driver.pool.Pool(connections, picker_class=<class 'bigchaindb_driver.pool.RoundRobinPicker'>)[source]

Bases: object

Pool of connections.

__init__(connections, picker_class=<class 'bigchaindb_driver.pool.RoundRobinPicker'>)[source]

Initializes a Pool instance.

Parameters:connections (list) – List of Connection instances.
get_connection()[source]

Gets a Connection instance from the pool.

Returns:A Connection instance.
class bigchaindb_driver.pool.RoundRobinPicker[source]

Bases: bigchaindb_driver.pool.AbstractPicker

Object to pick a Connection instance from a list of connections.

picked

str – List index of Connection instance that has been picked.

__init__()[source]

Initializes a RoundRobinPicker instance. Sets picked to -1.

pick(connections)[source]

Picks a Connection instance from the given list of Connection instances.

Parameters:connections (List) – List of Connection instances.
class bigchaindb_driver.pool.AbstractPicker[source]

Bases: object

Abstract class for picker classes that pick connections from a pool.

pick(connections)[source]

Picks a Connection instance from the given list of Connection instances.

Parameters:connections (List) – List of Connection instances.

connection

class bigchaindb_driver.connection.Connection(*, node_url, headers=None)[source]

Bases: object

A Connection object to make HTTP requests.

__init__(*, node_url, headers=None)[source]

Initializes a Connection instance.

Parameters:
  • node_url (str) – Url of the node to connect to.
  • headers (dict) – Optional headers to send with each request.
request(method, *, path=None, json=None, params=None, headers=None, **kwargs)[source]

Performs an HTTP requests for the specified arguments.

Parameters:
  • method (str) – HTTP method (e.g.: 'GET').
  • path (str) – API endpoint path (e.g.: '/transactions').
  • json (dict) – JSON data to send along with the request.
  • params (dict)) – Dictionary of URL (query) parameters.
  • headers (dict) – Optional headers to pass to the request.
  • kwargs – Optional keyword arguments.

crypto

class bigchaindb_driver.crypto.CryptoKeypair(private_key, public_key)

Bases: tuple

private_key

Alias for field number 0

public_key

Alias for field number 1

bigchaindb_driver.crypto.generate_keypair()[source]

Generates a cryptographic key pair.

Returns:A collections.namedtuple with named fields private_key and public_key.
Return type:CryptoKeypair

exceptions

Exceptions used by bigchaindb_driver.

exception bigchaindb_driver.exceptions.BigchaindbException[source]

Bases: Exception

Base exception for all Bigchaindb exceptions.

exception bigchaindb_driver.exceptions.TransportError[source]

Bases: bigchaindb_driver.exceptions.BigchaindbException

Base exception for transport related errors.

This is mainly for cases where the status code denotes an HTTP error, and for cases in which there was a connection error.

exception bigchaindb_driver.exceptions.ConnectionError[source]

Bases: bigchaindb_driver.exceptions.TransportError

Exception for errors occurring when connecting, and/or making a request to Bigchaindb.

exception bigchaindb_driver.exceptions.NotFoundError[source]

Bases: bigchaindb_driver.exceptions.TransportError

Exception for HTTP 404 errors.

exception bigchaindb_driver.exceptions.KeypairNotFoundException[source]

Bases: bigchaindb_driver.exceptions.BigchaindbException

Raised if an operation cannot proceed because the keypair was not given.

exception bigchaindb_driver.exceptions.InvalidPrivateKey[source]

Bases: bigchaindb_driver.exceptions.BigchaindbException

Raised if a private key is invalid. E.g.: None.

exception bigchaindb_driver.exceptions.InvalidPublicKey[source]

Bases: bigchaindb_driver.exceptions.BigchaindbException

Raised if a public key is invalid. E.g.: None.

exception bigchaindb_driver.exceptions.MissingPrivateKeyError[source]

Bases: bigchaindb_driver.exceptions.BigchaindbException

Raised if a private key is missing.

utils

Set of utilities to support various functionalities of the driver.

bigchaindb_driver.utils.ops_map

dict – Mapping between operation strings and classes. E.g.: The string 'CREATE' is mapped to CreateOperation.

class bigchaindb_driver.utils.CreateOperation[source]

Bases: object

Class representing the 'CREATE' transaction operation.

class bigchaindb_driver.utils.TransferOperation[source]

Bases: object

Class representing the 'TRANSFER' transaction operation.

bigchaindb_driver.utils._normalize_operation(operation)[source]

Normalizes the given operation string. For now, this simply means converting the given string to uppercase, looking it up in ops_map, and returning the corresponding class if present.

Parameters:operation (str) – The operation string to convert.
Returns:The class corresponding to the given string, CreateOperation or TransferOperation.

Important

If the str.upper() step, or the ops_map lookup fails, the given operation argument is returned.

About this Documentation

This section contains instructions to build and view the documentation locally, using the docker-compose docs.yml file of the bigchaindb-driver repository: https://github.com/bigchaindb/bigchaindb-driver.

If you do not have a clone of the repo, you need to get one.

Building the documentation

To build the docs, simply run

$ docker-compose --file docs.yml up -d bdocs

Or if you prefer, start a bash session,

$ docker-compose --file docs.yml run --rm bdocs bash

and build the docs:

root@a651959a1f2d:/usr/src/app# make -C docs html

Viewing the documentation

You can start a little web server to view the docs at http://localhost:55555/

$ docker-compose --file docs.yml up -d vdocs

Note

If you are using docker-machine you need to replace localhost with the ip of the machine (e.g.: docker-machine ip tm if your machine is named tm).

Making changes

The necessary source code is mounted, which allows you to make modifications, and view the changes by simply re-building the docs, and refreshing the browser.

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions

Report Bugs

Report bugs at https://github.com/bigchaindb/bigchaindb-driver/issues.

If you are reporting a bug, please include:

  • Your operating system name and version.
  • Any details about your local setup that might be helpful in troubleshooting.
  • Detailed steps to reproduce the bug.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.

Implement Features

Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.

Write Documentation

bigchaindb-driver could always use more documentation, whether as part of the official bigchaindb-driver docs, in docstrings, or even on the web in blog posts, articles, and such.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/bigchaindb/bigchaindb-driver/issues.

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Get Started!

Ready to contribute? Here’s how to set up bigchaindb-driver for local development.

  1. Fork the bigchaindb-driver repo on GitHub.

  2. Clone your fork locally and enter into the project:

    $ git clone git@github.com:your_name_here/bigchaindb-driver.git
    $ cd bigchaindb-driver/
    
  3. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  4. When you’re done making changes, check that your changes pass flake8 and the tests. For the tests, you’ll need to start the MongoDB and BigchainDB servers:

    $ docker-compose up -d db
    $ docker-compose up -d bdb-server
    
  5. flake8 check:

    $ docker-compose run --rm bdb flake8 bigchaindb_driver tests
    
  6. To run the tests:

    $ docker-compose run --rm bdb pytest -v
    
  7. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin name-of-your-bugfix-or-feature
    
  8. Submit a pull request through the GitHub website.

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.
  2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
  3. The pull request should work for Python 3.5, and pass the flake8 check. Check https://travis-ci.org/bigchaindb/bigchaindb-driver/pull_requests and make sure that the tests pass for all supported Python versions.

Tips

Development Environment with Docker

Depending on what you are doing, you may need to run at least one BigchainDB node. You can use the docker-compose.yml file to run a node, and perform other tasks that depend on the running node. To run a BigchainDB node, (for development), you start a MongoDB node, followed by the linked BigchainDB node:

$ docker-compose up -d db
$ docker-compose up -d bdb-server

You can monitor the logs:

$ docker-compose logs -f

Tests

To run a subset of tests:

$ docker-compose run --rm bdb pytest -v tests/test_driver.py

Important

When running tests, unless you are targeting a test that does not require a connection with the BigchainDB server, you need to run the BigchainDB and MongoDB servers:

$ docker-compose up -d db
$ docker-compose up -d bdb-server

Using RethinkDB as the backend

The default docker-compose file runs MongoDB as a backend. In order to work with RethinkDB, one has to use the docker-compose.rdb.yml file, which implies working with multiple compose files. The workflow is the same as with MongoDB.

First start RethinkDB:

$ docker-compose -f docker-compose.rdb.yml up -d db

then one BigchainDB server node:

$ docker-compose -f docker-compose.rdb.yml up -d bdb-server

and run the tests:

$ docker-compose -f docker-compose.rdb.yml run --rm bdb pytest -v

Dependency on Bigchaindb

By default, the development requirements, BigchainDB server Dockerfile, and .travis.yml are set to depend from BigchainDB’s master branch to more easily track changes against BigchainDB’s API.

Credits

Development Lead

Contributors

None yet. Why not be the first?

Changelog

0.4.1 (2017-08-02)

Fixed

0.4.0 (2017-07-05)

Added

  • Support for BigchainDB server (HTTP API) 1.0.0.

0.3.0 (2017-06-23)

Added

  • Support for BigchainDB server (HTTP API) 1.0.0rc1.
  • Support for crypto-conditions RFC draft version 02.
  • Added support for text search endpoint /assets?search=

0.2.0 (2017-02-06)

Added

  • Support for BigchainDB server 0.9.
  • Methods for GET / and GET /api/v1

Changed

  • Node URLs, passed to BigchainDB() MUST not include the api prefix '/api/v1', e.g.:

    • BEFORE: http://localhost:9984/api/v1
    • NOW: http://localhost:9984

0.1.0 (2016-11-29)

Added

  • Support for BigchainDB server 0.8.0.
  • Support for divisible assets.

Removed

  • create() and transfer() under TransactionEndpoint, and available via BigchainDB.transactions. Replaced by the three “canonical” transaction operations: prepare(), fulfill(), and send().
  • Support for client side timestamps.

0.0.3 (2016-11-25)

Added

  • Support for “canonical” transaction operations:

    • prepare
    • fulfill
    • send

Deprecated

  • create() and transfer() under TransactionEndpoint, and available via BigchainDB.transactions. Replaced by the above three “canonical” transaction operations: prepare(), fulfill(), and send().

Fixed

  • BigchainDB() default node setting on its transport class. See commit 0a80206

0.0.2 (2016-10-28)

Added

  • Support for BigchainDB server 0.7.0

0.0.1dev1 (2016-08-25)

  • Development (pre-alpha) release on PyPI.

Added

  • Minimal support for POST (via create() and transfer()), and GET operations on the /transactions endpoint.

0.0.1a1 (2016-08-12)

  • Planning release on PyPI.

Indices and tables