Handcrafting Transactions

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

Note

The contents are presented for BigchainDB 0.8. The transaction schema is constantly evolving at this stage and the current contents may be outdated by a new release.

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 with a number of nested structures.

The first level has three keys:

  • id – a str;
  • version – an int; and
  • transaction – a dict

Because a transaction must be signed before being sent, the id is required to be provided by the client.

When you assemble the payload you’ll have:

whose id can be generated by hashing the above with SHA-3’s SHA256 algorithm.

Important

Implications of Signed Payloads

Because transactions are signed by the client submitting them, various values that could traditionally be generated on the server side need to be generated on the client side.

These values include:

  • transaction id, which is a hash of the entire payload, without the signature(s)
  • asset id
  • metadata id
  • any optional value, such as version which defaults to 1

This makes the assembling of a payload more involved as one needs to provide all values regardless of whether there are defaults or not.

The transaction body

The transaction body is made up of the following keys:

  • assetdict
  • metadatadict
  • operationstr
  • conditionslist of dict
  • fulfillmentslist of dict
asset
asset = {
    'data': {},
    'divisible': False,
    'refillable': False,
    'updatable': False,
    'id': '',
}

Example of an asset payload:

asset = {
    'data': {
        'bicycle': {
            'manufacturer': 'bkfab',
            'serial_number': 'abcd1234',
        },
    },
    'divisible': False,
    'refillable': False,
    'updatable': False,
    'id': '7ab63c48-4c24-41df-a1bd-934bb609a7f7',
}

Note

In many client-server architectures, the values for the keys:

  • 'divisible'
  • 'refillable'
  • 'updatable'
  • 'id'

could all be generated on the server side.

In the case of BigchainDB, because we rely on cryptographic signatures, the payloads need to be fully prepared and signed on the client side. This prevents the server(s) from tempering with the provided data.

metadata
metadata = {
    'data': {},
    'id': '',
}

Example of a metadata payload:

metadata = {
    'data': {
        'planet': 'earth',
    },
    'id': 'ad8c83bd-9192-43b3-b636-af93a3a6b07c',
}

Note

In many client-server architectures, the value of the 'id' could be generated on the server side.

In the case of BigchainDB, because we rely on cryptographic signatures, the payloads need to be fully prepared and signed on the client side. This prevents the server(s) from tempering with the provided data.

operation
operation = '<operation>'

<operation> must be one of 'CREATE', 'TRANSFER', or 'GENESIS'

Important

Case sensitive; all letters must be capitalized.

conditions

The purpose of the condition is to lock the transaction, such that a valid 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.

Example of a condition payload:

{
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': {
            'bitmask': 32,
            'public_key': '8L6ngTZ5ixuFEr1GiunrFNWtGkft4swWWArXjWJu2Uwc',
            'signature': None,
            'type': 'fulfillment',
            'type_id': 4,
        },
        'uri': 'cc:4:20:bOZjTedaOgPsbYjh3QeOEQCj1o1lIvVefR71sS8egnM:96'
    },
    'owners_after': ['8L6ngTZ5ixuFEr1GiunrFNWtGkft4swWWArXjWJu2Uwc'],
}
fulfillments

A fulfillment payload is first prepared without its fulfillment uri (e.g., containing the signature), and included in the transaction payload, which will be hashed to generate the transaction id.

In a second step, after the transaction id has been generated, the fulfillment URI (e.g. containing a signature) can be added.

Moreover, payloads for CREATE operations are a bit different.

Note

We hope to be able to simplify the payload structure and validation, such that this is no longer required.

Todo

Point to issues addressing the topic.

Example of a fulfillment payload before fulfilling it, for a CREATE operation:

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': ['8L6ngTZ5ixuFEr1GiunrFNWtGkft4swWWArXjWJu2Uwc'],
}

Note

Because it is a CREATE operation, the 'input' field is set to None.

Todo

Example of a fulfillment payload after fulfilling it:

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',
   ...:     owners_before=alice.verifying_key,
   ...:     asset=bicycle,
   ...:     metadata=metadata,
   ...: )
   ...: 

and the payload of the prepared transaction looked similar to:

In [7]: prepared_creation_tx
Out[7]: 
{'id': '8880a3689b407b9930b3585a5db226e044627e1766f02ea82efa1363b7d2e3eb',
 'transaction': {'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
     'serial_number': 'abcd1234'}},
   'divisible': False,
   'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1',
   'refillable': False,
   'updatable': False},
  'conditions': [{'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'},
    'owners_after': ['EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb']}],
  'fulfillments': [{'fid': 0,
    'fulfillment': {'bitmask': 32,
     'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
     'signature': None,
     'type': 'fulfillment',
     'type_id': 4},
    'input': None,
    'owners_before': ['EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb']}],
  'metadata': {'data': {'planet': 'earth'},
   'id': '06eaa731-43f9-4c45-9675-8b88ee0960b3'},
  'operation': 'CREATE'},
 'version': 1}

Note alice‘s public key:

In [8]: alice.verifying_key
Out[8]: 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb'

We are now going to craft this payload by hand.

Extract asset id and metadata id:

In [9]: asset_id = prepared_creation_tx['transaction']['asset']['id']

In [10]: metadata_id = prepared_creation_tx['transaction']['metadata']['id']

The transaction body

asset
In [11]: asset = {
   ....:     'data': {
   ....:         'bicycle': {
   ....:             'manufacturer': 'bkfab',
   ....:             'serial_number': 'abcd1234',
   ....:         },
   ....:     },
   ....:     'divisible': False,
   ....:     'refillable': False,
   ....:     'updatable': False,
   ....:     'id': asset_id,
   ....: }
   ....: 
metadata
In [12]: metadata = {
   ....:     'data': {
   ....:         'planet': 'earth',
   ....:     },
   ....:     'id': metadata_id,
   ....: }
   ....: 
operation
In [13]: operation = 'CREATE'

Important

Case sensitive; all letters must be capitalized.

conditions

The purpose of the condition is to lock the transaction, such that a valid 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 condition payload of the prepared transaction, to see what we are aiming for:

In [14]: prepared_creation_tx['transaction']['conditions'][0]
Out[14]: 
{'amount': 1,
 'cid': 0,
 'condition': {'details': {'bitmask': 32,
   'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
   'signature': None,
   'type': 'fulfillment',
   'type_id': 4},
  'uri': 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'},
 'owners_after': ['EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb']}

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

In [15]: from cryptoconditions import Ed25519Fulfillment

In [16]: ed25519 = Ed25519Fulfillment(public_key=alice.verifying_key)

generate the condition uri:

In [17]: ed25519.condition_uri
Out[17]: 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'

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

As for the details:

In [18]: ed25519.to_dict()
Out[18]: 
{'bitmask': 32,
 'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
 'signature': None,
 'type': 'fulfillment',
 'type_id': 4}

We can now easily assemble the dict for the condition:

In [19]: condition = {
   ....:     'amount': 1,
   ....:     'cid': 0,
   ....:     'condition': {
   ....:         'details': ed25519.to_dict(),
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'owners_after': (alice.verifying_key,),
   ....: }
   ....: 

Let’s recap and set the conditions key:

In [20]: from cryptoconditions import Ed25519Fulfillment

In [21]: ed25519 = Ed25519Fulfillment(public_key=alice.verifying_key)

In [22]: condition = {
   ....:     'amount': 1,
   ....:     'cid': 0,
   ....:     'condition': {
   ....:         'details': ed25519.to_dict(),
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'owners_after': (alice.verifying_key,),
   ....: }
   ....: 

In [23]: conditions = (condition,)

The key part is the condition URI:

In [24]: ed25519.condition_uri
Out[24]: 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'

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

fulfillments

The fulfillment for a CREATE operation is somewhat special:

In [25]: fulfillment = {
   ....:     'fid': 0,
   ....:     'fulfillment': None,
   ....:     'input': None,
   ....:     'owners_before': (alice.verifying_key,)
   ....: }
   ....: 
  • The input field is empty because it’s a CREATE operation;
  • The 'fulfillemnt' 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 fulfillments value is simply a list or tuple of all fulfillments:

In [26]: fulfillments = (fulfillment,)

Note

You may rightfully observe that the prepared_creation_tx fulfillment generated via the prepare_transaction function differs:

In [27]: prepared_creation_tx['transaction']['fulfillments'][0]
Out[27]: 
{'fid': 0,
 'fulfillment': {'bitmask': 32,
  'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
  'signature': None,
  'type': 'fulfillment',
  'type_id': 4},
 'input': None,
 'owners_before': ['EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb']}

More precisely, the value of 'fulfillment':

In [28]: prepared_creation_tx['transaction']['fulfillments'][0]['fulfillment']
Out[28]: 
{'bitmask': 32,
 'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
 'signature': None,
 'type': 'fulfillment',
 'type_id': 4}

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

Putting it all together:

In [29]: handcrafted_creation_tx = {
   ....:     'transaction': {
   ....:         'asset': asset,
   ....:         'metadata': metadata,
   ....:         'operation': operation,
   ....:         'conditions': conditions,
   ....:         'fulfillments': fulfillments,
   ....:     },
   ....:     'version': 1,
   ....: }
   ....: 

In [30]: handcrafted_creation_tx
Out[30]: 
{'transaction': {'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
     'serial_number': 'abcd1234'}},
   'divisible': False,
   'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1',
   'refillable': False,
   'updatable': False},
  'conditions': ({'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'},
    'owners_after': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'fulfillments': ({'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'metadata': {'data': {'planet': 'earth'},
   'id': '06eaa731-43f9-4c45-9675-8b88ee0960b3'},
  'operation': 'CREATE'},
 'version': 1}

We’re missing the id, and 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 Ed25519Fulfillment
from bigchaindb_driver.crypto import CryptoKeypair

alice = CryptoKeypair(
    verifying_key=alice.verifying_key,
    signing_key=alice.signing_key,
)

operation = 'CREATE'

asset = {
    'data': {
        'bicycle': {
            'manufacturer': 'bkfab',
            'serial_number': 'abcd1234',
        },
    },
    'divisible': False,
    'refillable': False,
    'updatable': False,
    'id': asset_id,
}

metadata = {
    'data': {
        'planet': 'earth',
    },
    'id': metadata_id,
}

ed25519 = Ed25519Fulfillment(public_key=alice.verifying_key)

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': ed25519.to_dict(),
        'uri': ed25519.condition_uri,
    },
    'owners_after': (alice.verifying_key,),
}
conditions = (condition,)

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': (alice.verifying_key,)
}
fulfillments = (fulfillment,)

handcrafted_creation_tx = {
    'transaction': {
        'asset': asset,
        'metadata': metadata,
        'operation': operation,
        'conditions': conditions,
        'fulfillments': fulfillments,
    },
    'version': 1,
}
id
In [31]: import json

In [32]: from sha3 import sha3_256

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

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

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

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

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

You may observe that

In [37]: handcrafted_creation_tx == prepared_creation_tx
Out[37]: False
In [38]: from copy import deepcopy

In [39]: # back up

In [40]: prepared_creation_tx_bk = deepcopy(prepared_creation_tx)

In [41]: # set fulfillment to None

In [42]: prepared_creation_tx['transaction']['fulfillments'][0]['fulfillment'] = None

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

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

In [44]: # serialize to json str

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

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

In [48]: prepared_creation_tx = prepared_creation_tx_bk

The full handcrafted yet-to-be-fulfilled transaction payload:

In [49]: handcrafted_creation_tx
Out[49]: 
{'id': '8880a3689b407b9930b3585a5db226e044627e1766f02ea82efa1363b7d2e3eb',
 'transaction': {'asset': {'data': {'bicycle': {'manufacturer': 'bkfab',
     'serial_number': 'abcd1234'}},
   'divisible': False,
   'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1',
   'refillable': False,
   'updatable': False},
  'conditions': ({'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': 'EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFI:96'},
    'owners_after': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'fulfillments': ({'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'metadata': {'data': {'planet': 'earth'},
   'id': '06eaa731-43f9-4c45-9675-8b88ee0960b3'},
  'operation': 'CREATE'},
 'version': 1}

The Fulfilled Transaction

In [50]: from cryptoconditions.crypto import Ed25519SigningKey

In [51]: from bigchaindb_driver.offchain import fulfill_transaction

In [52]: fulfilled_creation_tx = fulfill_transaction(
   ....:     prepared_creation_tx,
   ....:     private_keys=alice.signing_key,
   ....: )
   ....: 

In [53]: sk = Ed25519SigningKey(alice.signing_key)

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

In [55]: ed25519.sign(message.encode(), sk)

In [56]: fulfillment = ed25519.serialize_uri()

In [57]: handcrafted_creation_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment

Let’s check this:

In [58]: fulfilled_creation_tx['transaction']['fulfillments'][0]['fulfillment'] == fulfillment
Out[58]: True

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

In a nutshell

Handcrafting a 'CREATE' transaction can be done as follows:

import json
from uuid import uuid4

import sha3
import cryptoconditions

from bigchaindb_driver.crypto import generate_keypair


alice = generate_keypair()

operation = 'CREATE'

asset_id = str(uuid4())
asset = {
    'data': {
        'bicycle': {
            'manufacturer': 'bkfab',
            'serial_number': 'abcd1234',
        },
    },
    'divisible': False,
    'refillable': False,
    'updatable': False,
    'id': asset_id,
}

metadata_id = str(uuid4())
metadata = {
    'data': {
        'planet': 'earth',
    },
    'id': metadata_id,
}

ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=alice.verifying_key)

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': ed25519.to_dict(),
        'uri': ed25519.condition_uri,
    },
    'owners_after': (alice.verifying_key,),
}
conditions = (condition,)

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': (alice.verifying_key,)
}
fulfillments = (fulfillment,)

handcrafted_creation_tx = {
    'transaction': {
        'asset': asset,
        'metadata': metadata,
        'operation': operation,
        'conditions': conditions,
        'fulfillments': fulfillments,
    },
    'version': 1,
}

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

sk = cryptoconditions.crypto.Ed25519SigningKey(alice.signing_key)

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

ed25519.sign(message.encode(), sk)

fulfillment = ed25519.serialize_uri()

handcrafted_creation_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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 [60]: creation_tx = fulfilled_creation_tx

In [61]: bob = generate_keypair()

In [62]: cid = 0

In [63]: condition = creation_tx['transaction']['conditions'][cid]

In [64]: transfer_input = {
   ....:     'fulfillment': condition['condition']['details'],
   ....:     'input': {
   ....:          'cid': cid,
   ....:          'txid': creation_tx['id'],
   ....:      },
   ....:      'owners_before': condition['owners_after'],
   ....: }
   ....: 

In [65]: prepared_transfer_tx = prepare_transaction(
   ....:     operation='TRANSFER',
   ....:     asset=creation_tx['transaction']['asset'],
   ....:     inputs=transfer_input,
   ....:     owners_after=bob.verifying_key,
   ....: )
   ....: 

In [66]: fulfilled_transfer_tx = fulfill_transaction(
   ....:     prepared_transfer_tx,
   ....:     private_keys=alice.signing_key,
   ....: )
   ....: 

In [67]: fulfilled_transfer_tx
Out[67]: 
{'id': '51ac719744bb2314021fecfff719d3003c43f565b2bbe34e1fd7ccc44f4fa7a2',
 'transaction': {'asset': {'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1'},
  'conditions': [{'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': '2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:E_3fTnWuj_otkgASTrrkbeuzcx1U91j0ECkqwLrbKMI:96'},
    'owners_after': ['2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj']}],
  'fulfillments': [{'fid': 0,
    'fulfillment': 'cf:4:yQ2u5mFjevhya8_IyAjgcPky-nyzFlSGSGP4Sv6fDFKtqVJk1BDoTEtgXxVg7BUd7LQEgw8B1An7m6ahxjCvC6cTsPHIVZUpY20VgUywZtUPFdJn5znMFpHNk7or3QoM',
    'input': {'cid': 0,
     'txid': '8880a3689b407b9930b3585a5db226e044627e1766f02ea82efa1363b7d2e3eb'},
    'owners_before': ['EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb']}],
  'metadata': None,
  'operation': 'TRANSFER'},
 'version': 1}

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

The transaction body

asset
In [68]: asset = {'id': asset_id}
metadata
In [69]: metadata = None
operation
In [70]: operation = 'TRANSFER'
conditions
In [71]: from cryptoconditions import Ed25519Fulfillment

In [72]: ed25519 = Ed25519Fulfillment(public_key=bob.verifying_key)

In [73]: condition = {
   ....:     'amount': 1,
   ....:     'cid': 0,
   ....:     'condition': {
   ....:         'details': ed25519.to_dict(),
   ....:         'uri': ed25519.condition_uri,
   ....:     },
   ....:     'owners_after': (bob.verifying_key,),
   ....: }
   ....: 

In [74]: conditions = (condition,)
fulfillments
In [75]: fulfillment = {
   ....:     'fid': 0,
   ....:     'fulfillment': None,
   ....:     'input': {
   ....:         'txid': creation_tx['id'],
   ....:         'cid': 0,
   ....:     },
   ....:     'owners_before': (alice.verifying_key,)
   ....: }
   ....: 

In [76]: fulfillments = (fulfillment,)

A few notes:

  • The input field points to the condition 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 [77]: handcrafted_transfer_tx = {
   ....:     'transaction': {
   ....:         'asset': asset,
   ....:         'metadata': metadata,
   ....:         'operation': operation,
   ....:         'conditions': conditions,
   ....:         'fulfillments': fulfillments,
   ....:     },
   ....:     'version': 1,
   ....: }
   ....: 

In [78]: handcrafted_transfer_tx
Out[78]: 
{'transaction': {'asset': {'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1'},
  'conditions': ({'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': '2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:E_3fTnWuj_otkgASTrrkbeuzcx1U91j0ECkqwLrbKMI:96'},
    'owners_after': ('2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj',)},),
  'fulfillments': ({'fid': 0,
    'fulfillment': None,
    'input': {'cid': 0,
     'txid': '8880a3689b407b9930b3585a5db226e044627e1766f02ea82efa1363b7d2e3eb'},
    'owners_before': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'metadata': None,
  'operation': 'TRANSFER'},
 'version': 1}

We’re missing the id, and we’ll generate it, but before, let’s recap how we’ve put all the code together to generate the above payload:

from cryptoconditions import Ed25519Fulfillment
from bigchaindb_driver.crypto import CryptoKeypair

bob = CryptoKeypair(
    verifying_key=bob.verifying_key,
    signing_key=bob.signing_key,
)

operation = 'TRANSFER'
asset = {'id': asset_id}
metadata = None

ed25519 = Ed25519Fulfillment(public_key=bob.verifying_key)

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': ed25519.to_dict(),
        'uri': ed25519.condition_uri,
    },
    'owners_after': (bob.verifying_key,),
}
conditions = (condition,)

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': {
        'txid': creation_tx['id'],
        'cid': 0,
    },
    'owners_before': (alice.verifying_key,)
}
fulfillments = (fulfillment,)

handcrafted_transfer_tx = {
    'transaction': {
        'asset': asset,
        'metadata': metadata,
        'operation': operation,
        'conditions': conditions,
        'fulfillments': fulfillments,
    },
    'version': 1,
}
id
In [79]: import json

In [80]: from sha3 import sha3_256

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

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

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

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

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

You may observe that

In [85]: handcrafted_transfer_tx == prepared_transfer_tx
Out[85]: False
In [86]: from copy import deepcopy

In [87]: # back up

In [88]: prepared_transfer_tx_bk = deepcopy(prepared_transfer_tx)

In [89]: # set fulfillment to None

In [90]: prepared_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = None

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

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

In [92]: # serialize to json str

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

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

In [96]: prepared_transfer_tx = prepared_transfer_tx_bk

The full handcrafted yet-to-be-fulfilled transaction payload:

In [97]: handcrafted_transfer_tx
Out[97]: 
{'id': '51ac719744bb2314021fecfff719d3003c43f565b2bbe34e1fd7ccc44f4fa7a2',
 'transaction': {'asset': {'id': 'a590340d-2c5c-4e32-9aad-a25f360672a1'},
  'conditions': ({'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': '2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:E_3fTnWuj_otkgASTrrkbeuzcx1U91j0ECkqwLrbKMI:96'},
    'owners_after': ('2M3GngCrmCPZgk8UFSqJ3Y8QHpoBU9xYdZUgTShsL1Zj',)},),
  'fulfillments': ({'fid': 0,
    'fulfillment': None,
    'input': {'cid': 0,
     'txid': '8880a3689b407b9930b3585a5db226e044627e1766f02ea82efa1363b7d2e3eb'},
    'owners_before': ('EXq3pyXnJgaWcmnNMAq2MjEXYo3h4cMzWoetuQeAggFb',)},),
  'metadata': None,
  'operation': 'TRANSFER'},
 'version': 1}

The Fulfilled Transaction

In [98]: from cryptoconditions.crypto import Ed25519SigningKey

In [99]: from bigchaindb_driver.offchain import fulfill_transaction

In [100]: fulfilled_transfer_tx = fulfill_transaction(
   .....:     prepared_transfer_tx,
   .....:     private_keys=alice.signing_key,
   .....: )
   .....: 

In [101]: sk = Ed25519SigningKey(alice.signing_key)

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

In [103]: ed25519.sign(message.encode(), sk)

In [104]: fulfillment = ed25519.serialize_uri()

In [105]: handcrafted_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment

Let’s check this:

In [106]: fulfilled_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] == fulfillment
Out[106]: True

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

In a nutshell

import json

import sha3
import cryptoconditions

from bigchaindb_driver.crypto import generate_keypair


bob = generate_keypair()

operation = 'TRANSFER'
asset = {'id': asset_id}
metadata = None

ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=bob.verifying_key)

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': ed25519.to_dict(),
        'uri': ed25519.condition_uri,
    },
    'owners_after': (bob.verifying_key,),
}
conditions = (condition,)

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': {
        'txid': creation_txid,
        'cid': 0,
    },
    'owners_before': (alice.verifying_key,)
}
fulfillments = (fulfillment,)

handcrafted_transfer_tx = {
    'transaction': {
        'asset': asset,
        'metadata': metadata,
        'operation': operation,
        'conditions': conditions,
        'fulfillments': fulfillments,
    },
    'version': 1,
}

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

sk = cryptoconditions.crypto.Ed25519SigningKey(alice.signing_key)

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

ed25519.sign(message.encode(), sk)

fulfillment = ed25519.serialize_uri()

handcrafted_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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:

import json
from uuid import uuid4

import sha3
import cryptoconditions

from bigchaindb_driver.crypto import generate_keypair


bob, carly = generate_keypair(), generate_keypair()

asset_id = str(uuid4())
asset = {
    'divisible': True,
    'data': {
        'token_for': {
            'bicycle': {
                'manufacturer': 'bkfab',
                'serial_number': 'abcd1234',
            },
            'description': 'time share token. each token equals 1 hour of riding.'
        },
    },
    'refillable': False,
    'updatable': False,
    'id': asset_id,
}

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for carly
ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=carly.verifying_key)

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

# CRYPTO-CONDITIONS: get the unsigned fulfillment dictionary (details)
unsigned_fulfillment_dict = ed25519.to_dict()

condition = {
    'amount': 10,
    'cid': 0,
    'condition': {
        'details': unsigned_fulfillment_dict,
        'uri': condition_uri,
    },
    'owners_after': (carly.verifying_key,),
}

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': (bob.verifying_key,)
}

token_creation_tx = {
    'transaction': {
        'asset': asset,
        'metadata': None,
        'operation': 'CREATE',
        'conditions': (condition,),
        'fulfillments': (fulfillment,),
    },
    'version': 1,
}

# 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(),
             cryptoconditions.crypto.Ed25519SigningKey(bob.signing_key))

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

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

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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['transaction']['fulfillments'][0]['owners_before'][0] == bob.verifying_key
True

>>> token_creation_tx['transaction']['conditions'][0]['owners_after'][0] == carly.verifying_key
True

>>> token_creation_tx['transaction']['conditions'][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 = cryptoconditions.Ed25519Fulfillment(public_key=bob.verifying_key)

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for carly
carly_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=carly.verifying_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 = bob_ed25519.to_dict()
carly_unsigned_fulfillment_dict = carly_ed25519.to_dict()

bob_condition = {
    'amount': 2,
    'cid': 0,
    'condition': {
        'details': bob_unsigned_fulfillment_dict,
        'uri': bob_condition_uri,
    },
    'owners_after': (bob.verifying_key,),
}
carly_condition = {
    'amount': 8,
    'cid': 1,
    'condition': {
        'details': carly_unsigned_fulfillment_dict,
        'uri': carly_condition_uri,
    },
    'owners_after': (carly.verifying_key,),
}

fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': {
        'txid': token_creation_tx['id'],
        'cid': 0,
    },
    'owners_before': (carly.verifying_key,)
}

token_transfer_tx = {
    'transaction': {
        'asset': {'id': asset_id},
        'metadata': None,
        'operation': 'TRANSFER',
        'conditions': (bob_condition, carly_condition),
        'fulfillments': (fulfillment,),
    },
    'version': 1,
}

# 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(),
                 cryptoconditions.crypto.Ed25519SigningKey(carly.signing_key))

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

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

Sending it over to a BigchainDB node:

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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['transaction']['fulfillments'][0]['owners_before'][0] == carly.verifying_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, to compare our work.

Say alice and bob own a car together:

In [108]: from bigchaindb_driver.crypto import generate_keypair

In [109]: from bigchaindb_driver import offchain

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

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

In [112]: car_creation_tx = offchain.prepare_transaction(
   .....:     operation='CREATE',
   .....:     owners_before=alice.verifying_key,
   .....:     owners_after=(alice.verifying_key, bob.verifying_key),
   .....:     asset=car_asset,
   .....: )
   .....: 

In [113]: signed_car_creation_tx = offchain.fulfill_transaction(
   .....:     car_creation_tx,
   .....:     private_keys=alice.signing_key,
   .....: )
   .....: 

In [114]: signed_car_creation_tx
Out[114]: 
{'id': 'c3e13ecd6bbc8821568729fedeba4c6c746a1d4f895667adaf042f696e0276a7',
 'transaction': {'asset': {'data': {'car': {'vin': '5YJRE11B781000196'}},
   'divisible': False,
   'id': '3b93aa34-f618-43b0-995b-e557865291bd',
   'refillable': False,
   'updatable': False},
  'conditions': [{'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 41,
      'subfulfillments': [{'bitmask': 32,
        'public_key': '6RWNntJW5adrdY1e3PZW5kFos2MmrEEqhvcCku1L8Nqa',
        'signature': None,
        'type': 'fulfillment',
        'type_id': 4,
        'weight': 1},
       {'bitmask': 32,
        'public_key': '8xAKT1gKjfsjPf4ETJg2ZeLdZp4eK9fQroXcRM9PzL3T',
        'signature': None,
        'type': 'fulfillment',
        'type_id': 4,
        'weight': 1}],
      'threshold': 2,
      'type': 'fulfillment',
      'type_id': 2},
     'uri': 'cc:2:29:8QTyD__AJT6kZYLj7ESKYYg73TfIQ1DcUBw3gMFxGrI:206'},
    'owners_after': ['6RWNntJW5adrdY1e3PZW5kFos2MmrEEqhvcCku1L8Nqa',
     '8xAKT1gKjfsjPf4ETJg2ZeLdZp4eK9fQroXcRM9PzL3T']}],
  'fulfillments': [{'fid': 0,
    'fulfillment': 'cf:4:UJGW2lyJ4RSmrYeR_UnTe7iJolhkIpx53g63WWCD4723YWFpgV2TxvsRUyoLCoXBNb5ttTSbV9X_3gzlGMqsT_fuBJ3ng-e7aGmUHEB4T_xYwhxQKR2QKhxMVMyzlLcO',
    'input': None,
    'owners_before': ['6RWNntJW5adrdY1e3PZW5kFos2MmrEEqhvcCku1L8Nqa']}],
  'metadata': None,
  'operation': 'CREATE'},
 'version': 1}
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 [115]: carol = generate_keypair()

In [116]: cid = 0

In [117]: condition = signed_car_creation_tx['transaction']['conditions'][cid]

In [118]: input_ = {
   .....:     'fulfillment': condition['condition']['details'],
   .....:     'input': {
   .....:         'cid': cid,
   .....:         'txid': signed_car_creation_tx['id'],
   .....:     },
   .....:     'owners_before': condition['owners_after'],
   .....: }
   .....: 

In [119]: asset = signed_car_creation_tx['transaction']['asset']

In [120]: car_transfer_tx = offchain.prepare_transaction(
   .....:     operation='TRANSFER',
   .....:     owners_after=carol.verifying_key,
   .....:     asset=asset,
   .....:     inputs=input_,
   .....: )
   .....: 

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

In [122]: signed_car_transfer_tx
Out[122]: 
{'id': '806b566d4898801edfe2bcf06aae109c098d6d82a44958cfd0f2b175c4a0cbbd',
 'transaction': {'asset': {'id': '3b93aa34-f618-43b0-995b-e557865291bd'},
  'conditions': [{'amount': 1,
    'cid': 0,
    'condition': {'details': {'bitmask': 32,
      'public_key': 'GGrhc9UbLCeMgd4wKoDwJifwNo64oefGCUNXKT7Rm3Zu',
      'signature': None,
      'type': 'fulfillment',
      'type_id': 4},
     'uri': 'cc:4:20:4u82TUsyLolgpUaZuS2r43nJ1BweS7ysNzdE0r0nT1w:96'},
    'owners_after': ['GGrhc9UbLCeMgd4wKoDwJifwNo64oefGCUNXKT7Rm3Zu']}],
  'fulfillments': [{'fid': 0,
    'fulfillment': 'cf:2:AQIBAgEBYwAEYFCRltpcieEUpq2Hkf1J03u4iaJYZCKced4Ot1lgg-O9Q2Vkp3OGaDwIcKv-vrOhs9a8j6S_Ddl-WlahI67n6yt-LXXivRtQ-4evtZVq4E1NkLx9wPjgAjBhlG-e6Y1eBAABAWMABGB2I0n5vvB2_56Xxie91CTSydMeoa_G1csfQYFm0ATRgt2SOixiBzqf1_oTDmBabfrf6kK8f7MVuqHvho5ll1o2CF--jZyopjP6arvfXfgz2wZa_Vo_Voq7AG_9JqTCvAcA',
    'input': {'cid': 0,
     'txid': 'c3e13ecd6bbc8821568729fedeba4c6c746a1d4f895667adaf042f696e0276a7'},
    'owners_before': ['6RWNntJW5adrdY1e3PZW5kFos2MmrEEqhvcCku1L8Nqa',
     '8xAKT1gKjfsjPf4ETJg2ZeLdZp4eK9fQroXcRM9PzL3T']}],
  'metadata': None,
  'operation': 'TRANSFER'},
 'version': 1}

Sending the transaction to a BigchainDB node:

sent_car_transfer_tx = bdb.transactions.send(signed_car_transfer_tx)

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

In [123]: import json

In [124]: from sha3 import sha3_256

In [125]: from cryptoconditions import Ed25519Fulfillment, ThresholdSha256Fulfillment

In [126]: from cryptoconditions.crypto import Ed25519SigningKey

Create the asset, setting all values:

In [127]: car_asset_id = signed_car_creation_tx['transaction']['asset']['id']

In [128]: car_asset = {
   .....:     'data': {'car': {'vin': '5YJRE11B781000196'}},
   .....:     'divisible': False,
   .....:     'refillable': False,
   .....:     'updatable': False,
   .....:     'id': car_asset_id,
   .....: }
   .....: 

Generate the condition:

In [129]: alice_ed25519 = Ed25519Fulfillment(public_key=alice.verifying_key)

In [130]: bob_ed25519 = Ed25519Fulfillment(public_key=bob.verifying_key)

In [131]: threshold_sha256 = ThresholdSha256Fulfillment(threshold=2)

In [132]: threshold_sha256.add_subfulfillment(alice_ed25519)

In [133]: threshold_sha256.add_subfulfillment(bob_ed25519)

In [134]: unsigned_subfulfillments_dict = threshold_sha256.to_dict()

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

In [136]: condition = {
   .....:     'amount': 1,
   .....:     'cid': 0,
   .....:     'condition': {
   .....:         'details': unsigned_subfulfillments_dict,
   .....:         'uri': condition_uri,
   .....:     },
   .....:     'owners_after': (alice.verifying_key, bob.verifying_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 [137]: alt_threshold_sha256 = ThresholdSha256Fulfillment(threshold=2)

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

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

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

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

The yet to be fulfilled fulfillment:

In [141]: fulfillment = {
   .....:     'fid': 0,
   .....:     'fulfillment': None,
   .....:     'input': None,
   .....:     'owners_before': (alice.verifying_key,),
   .....: }
   .....: 

Craft the payload:

In [142]: handcrafted_car_creation_tx = {
   .....:     'transaction': {
   .....:         'asset': car_asset,
   .....:         'metadata': None,
   .....:         'operation': 'CREATE',
   .....:         'conditions': (condition,),
   .....:         'fulfillments': (fulfillment,),
   .....:     },
   .....:     'version': 1,
   .....: }
   .....: 

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

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

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

In [145]: 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 [146]: handcrafted_car_creation_tx['id'] == car_creation_tx['id']
Out[146]: True

Sign the transaction:

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

In [148]: alice_ed25519.sign(message.encode(), Ed25519SigningKey(alice.signing_key))

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

In [150]: handcrafted_car_creation_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment_uri

Compare our signed CREATE transaction with the driver’s:

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

The transfer:

In [152]: alice_ed25519 = Ed25519Fulfillment(public_key=alice.verifying_key)

In [153]: bob_ed25519 = Ed25519Fulfillment(public_key=bob.verifying_key)

In [154]: carol_ed25519 = Ed25519Fulfillment(public_key=carol.verifying_key)

In [155]: unsigned_fulfillments_dict = carol_ed25519.to_dict()

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

In [157]: condition = {
   .....:     'amount': 1,
   .....:     'cid': 0,
   .....:     'condition': {
   .....:         'details': unsigned_fulfillments_dict,
   .....:         'uri': condition_uri,
   .....:     },
   .....:     'owners_after': (carol.verifying_key,),
   .....: }
   .....: 

The yet to be fulfilled fulfillments:

In [158]: fulfillment = {
   .....:     'fid': 0,
   .....:     'fulfillment': None,
   .....:     'input': {
   .....:         'txid': handcrafted_car_creation_tx['id'],
   .....:         'cid': 0,
   .....:     },
   .....:     'owners_before': (alice.verifying_key, bob.verifying_key),
   .....: }
   .....: 

Craft the payload:

In [159]: handcrafted_car_transfer_tx = {
   .....:     'transaction': {
   .....:         'asset': {'id': car_asset_id},
   .....:         'metadata': None,
   .....:         'operation': 'TRANSFER',
   .....:         'conditions': (condition,),
   .....:         'fulfillments': (fulfillment,),
   .....:     },
   .....:     'version': 1,
   .....: }
   .....: 

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

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

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

In [162]: 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 [163]: handcrafted_car_transfer_tx['id'] == car_transfer_tx['id']
Out[163]: True

Sign the transaction:

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

In [165]: alice_sk = Ed25519SigningKey(alice.signing_key)

In [166]: bob_sk = Ed25519SigningKey(bob.signing_key)

In [167]: threshold_sha256 = ThresholdSha256Fulfillment(threshold=2)

In [168]: threshold_sha256.add_subfulfillment(alice_ed25519)

In [169]: threshold_sha256.add_subfulfillment(bob_ed25519)

In [170]: alice_condition = threshold_sha256.get_subcondition_from_vk(alice.verifying_key)[0]

In [171]: bob_condition = threshold_sha256.get_subcondition_from_vk(bob.verifying_key)[0]

In [172]: alice_condition.sign(message.encode(), private_key=alice_sk)

In [173]: bob_condition.sign(message.encode(), private_key=bob_sk)

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

In [175]: handcrafted_car_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment_uri

Compare our signed TRANSFER transaction with the driver’s:

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

In a nutshell

Handcrafting the 'CREATE' transaction

import json

import sha3
import cryptoconditions

from bigchaindb_driver.crypto import generate_keypair


car_asset = {
    'data': {
        'car': {
            'vin': '5YJRE11B781000196',
        },
    },
    'divisible': False,
     'refillable': False,
     'updatable': False,
     'id': '5YJRE11B781000196',
}

alice, bob = generate_keypair(), generate_keypair()

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for alice
alice_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=alice.verifying_key)

# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for bob
bob_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=bob.verifying_key)

# CRYPTO-CONDITIONS: instantiate a threshold SHA 256 crypto-condition
threshold_sha256 = cryptoconditions.ThresholdSha256Fulfillment(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: get the unsigned fulfillment dictionary (details)
unsigned_subfulfillments_dict = threshold_sha256.to_dict()

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

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': unsigned_subfulfillments_dict,
        'uri': threshold_sha256.condition_uri,
    },
    'owners_after': (alice.verifying_key, bob.verifying_key),
}

# The yet to be fulfilled fulfillment:
fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': None,
    'owners_before': (alice.verifying_key,),
}

# Craft the payload:
handcrafted_car_creation_tx = {
    'transaction': {
        'asset': car_asset,
        'metadata': None,
        'operation': 'CREATE',
        'conditions': (condition,),
        'fulfillments': (fulfillment,),
    },
    'version': 1,
}

# 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(),
                   cryptoconditions.crypto.Ed25519SigningKey(alice.signing_key))

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

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

Sending it over to a BigchainDB node:

from bigchaindb_driver import BigchainDB

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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

carol = generate_keypair()

alice_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=alice.verifying_key)

bob_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=bob.verifying_key)

carol_ed25519 = cryptoconditions.Ed25519Fulfillment(public_key=carol.verifying_key)

unsigned_fulfillments_dict = carol_ed25519.to_dict()

condition_uri = carol_ed25519.condition.serialize_uri()

condition = {
    'amount': 1,
    'cid': 0,
    'condition': {
        'details': unsigned_fulfillments_dict,
        'uri': condition_uri,
    },
    'owners_after': (carol.verifying_key,),
}

# The yet to be fulfilled fulfillments:
fulfillment = {
    'fid': 0,
    'fulfillment': None,
    'input': {
        'txid': handcrafted_car_creation_tx['id'],
        'cid': 0,
    },
    'owners_before': (alice.verifying_key, bob.verifying_key),
}

# Craft the payload:
handcrafted_car_transfer_tx = {
    'transaction': {
        'asset': {'id': car_asset['id']},
        'metadata': None,
        'operation': 'TRANSFER',
        'conditions': (condition,),
        'fulfillments': (fulfillment,),
    },
    'version': 1,
}

# 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,
)

alice_sk = cryptoconditions.crypto.Ed25519SigningKey(alice.signing_key)

bob_sk = cryptoconditions.crypto.Ed25519SigningKey(bob.signing_key)

threshold_sha256 = cryptoconditions.ThresholdSha256Fulfillment(threshold=2)

threshold_sha256.add_subfulfillment(alice_ed25519)

threshold_sha256.add_subfulfillment(bob_ed25519)

alice_condition = threshold_sha256.get_subcondition_from_vk(alice.verifying_key)[0]

bob_condition = threshold_sha256.get_subcondition_from_vk(bob.verifying_key)[0]

alice_condition.sign(message.encode(), private_key=alice_sk)

bob_condition.sign(message.encode(), private_key=bob_sk)

fulfillment_uri = threshold_sha256.serialize_uri()

handcrafted_car_transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment_uri

Sending it over to a BigchainDB node:

bdb = BigchainDB('http://bdb-server:9984/api/v1')
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'}