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:
- Preparing the transaction payload;
- Fulfilling the prepared transaction payload; and
- 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
– astr
;version
– anint
; andtransaction
– adict
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 to1
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:
asset
–dict
metadata
–dict
operation
–str
conditions
–list
ofdict
fulfillments
–list
ofdict
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,
....: }
....:
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 isNone
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)