Advanced Usage Examples¶
This section has examples of using the Python Driver for more advanced use cases such as escrow.
Todo
Work in progress. Will gradually appear as
- https://github.com/bigchaindb/bigchaindb/issues/664
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
Getting Started¶
First, make sure you have RethinkDB and BigchainDB installed and running, i.e. you installed them and you ran:
$ rethinkdb
$ bigchaindb configure
$ bigchaindb start
Don’t shut them down! In a new terminal, open a Python shell:
$ python
Now we can import the BigchainDB
class and create
an instance:
In [1]: from bigchaindb_driver import BigchainDB
In [2]: bdb = BigchainDB()
This instantiates an object bdb
of class
BigchainDB
. When instantiating a
BigchainDB
object without arguments (as above), it
sets the server node URL to http://localhost:9984/api/v1
.
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 [3]: from bigchaindb_driver.crypto import generate_keypair
Create a test user: alice
In [4]: alice = generate_keypair()
Define a digital asset data payload
In [5]: digital_asset_payload = {'data': {'msg': 'Hello BigchainDB!'}}
In [6]: tx = bdb.transactions.prepare(operation='CREATE',
...: owners_before=alice.verifying_key,
...: asset=digital_asset_payload)
...:
All transactions need to be signed by the user creating the transaction.
In [7]: signed_tx = bdb.transactions.fulfill(tx, private_keys=alice.signing_key)
Write the transaction to the bigchain. The transaction will be stored in a backlog where it will be validated, included in a block, and written to the bigchain.
>>> 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
In [8]: signed_tx
Out[8]:
{'id': 'da8b099d83e5f1e4ba6ed5719cfe40a20029c2e9c1689029e981735fa49378e8',
'transaction': {'asset': {'data': {'msg': 'Hello BigchainDB!'},
'divisible': False,
'id': '47f4e1f7-ef85-4e44-9de5-6877b9e1da64',
'refillable': False,
'updatable': False},
'conditions': [{'amount': 1,
'cid': 0,
'condition': {'details': {'bitmask': 32,
'public_key': '5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj',
'signature': None,
'type': 'fulfillment',
'type_id': 4},
'uri': 'cc:4:20:QJavLVcbFP5cpKXTgDo9awtelABrrLEXeWl-68Tw53A:96'},
'owners_after': ['5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj']}],
'fulfillments': [{'fid': 0,
'fulfillment': 'cf:4:QJavLVcbFP5cpKXTgDo9awtelABrrLEXeWl-68Tw53BmQRdGhFoMdDr6ewlDMRan1yCpNeZC2sGZwPyq8iPej0g9-wmRdA68euVriGf2SFldF8TShKKKl9IKZM02guAG',
'input': None,
'owners_before': ['5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj']}],
'metadata': None,
'operation': 'CREATE'},
'version': 1}
Read the Creation Transaction from the DB¶
After a couple of seconds, we can check if the transactions was included in the bigchain:
# Retrieve a transaction from the bigchain
>>> tx_retrieved = bdb.transactions.retrieve(tx['id'])
The new owner of the digital asset is now alice which is the public key, aka
verifying key of alice
.
In [9]: alice.verifying_key
Out[9]: '5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj'
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 [10]: signed_tx['id']
Out[10]: 'da8b099d83e5f1e4ba6ed5719cfe40a20029c2e9c1689029e981735fa49378e8'
BigchainDB makes use of the crypto-conditions library to both cryptographically
lock and unlock transactions. The locking script is referred to as a
condition
and a corresponding fulfillment
unlocks the condition of the
input_tx
.
Since a transaction can have multiple outputs with each its own
(crypto)condition, each transaction input should also refer to the condition
index cid
.
In order to prepare a transfer transaction, alice needs to provide at least three things:
inputs
– one or more conditions that will be fulfilled.asset
– the asset being transferred.owners_after
– one or more public keys representing the new owner(s).
To construct the input:
In [11]: cid = 0
In [12]: condition = tx['transaction']['conditions'][cid]
In [13]: input_ = {
....: 'fulfillment': condition['condition']['details'],
....: 'input': {
....: 'cid': cid,
....: 'txid': tx['id'],
....: },
....: 'owners_before': condition['owners_after'],
....: }
....:
The asset, can be directly retrieved from the input tx
:
In [14]: asset = tx['transaction']['asset']
Create a second test user, bob
:
In [15]: bob = generate_keypair()
In [16]: bob.verifying_key
Out[16]: 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E'
And prepare the transfer transaction:
In [17]: tx_transfer = bdb.transactions.prepare(
....: operation='TRANSFER',
....: inputs=input_,
....: asset=asset,
....: owners_after=bob.verifying_key,
....: )
....:
The tx_transfer
dictionary should look something like:
In [18]: tx_transfer
Out[18]:
{'id': '23134ba6d2f5aedb5f7271403fb90108488bca923e5a5b061fadde537c1f4184',
'transaction': {'asset': {'id': '47f4e1f7-ef85-4e44-9de5-6877b9e1da64'},
'conditions': [{'amount': 1,
'cid': 0,
'condition': {'details': {'bitmask': 32,
'public_key': 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E',
'signature': None,
'type': 'fulfillment',
'type_id': 4},
'uri': 'cc:4:20:86g2SNs5zFwPzhw6bHmeNeyVvpbzd5I3AfO0BdXXb6M:96'},
'owners_after': ['HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E']}],
'fulfillments': [{'fid': 0,
'fulfillment': {'bitmask': 32,
'public_key': '5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj',
'signature': None,
'type': 'fulfillment',
'type_id': 4},
'input': {'cid': 0,
'txid': 'da8b099d83e5f1e4ba6ed5719cfe40a20029c2e9c1689029e981735fa49378e8'},
'owners_before': ['5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj']}],
'metadata': None,
'operation': 'TRANSFER'},
'version': 1}
Notice, bob
‘s verifying key (public key), appearing in the above dict
.
In [19]: bob.verifying_key
Out[19]: 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E'
The transaction now needs to be fulfilled by alice
:
In [20]: signed_tx_transfer = bdb.transactions.fulfill(
....: tx_transfer,
....: private_keys=alice.signing_key,
....: )
....:
If you look at the content of signed_tx_transfer
you should see the added
fulfilment uri, holding the signature:
In [21]: signed_tx_transfer
Out[21]:
{'id': '23134ba6d2f5aedb5f7271403fb90108488bca923e5a5b061fadde537c1f4184',
'transaction': {'asset': {'id': '47f4e1f7-ef85-4e44-9de5-6877b9e1da64'},
'conditions': [{'amount': 1,
'cid': 0,
'condition': {'details': {'bitmask': 32,
'public_key': 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E',
'signature': None,
'type': 'fulfillment',
'type_id': 4},
'uri': 'cc:4:20:86g2SNs5zFwPzhw6bHmeNeyVvpbzd5I3AfO0BdXXb6M:96'},
'owners_after': ['HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E']}],
'fulfillments': [{'fid': 0,
'fulfillment': 'cf:4:QJavLVcbFP5cpKXTgDo9awtelABrrLEXeWl-68Tw53Bnh0UGNc1iIpGr6dTIrKXvTtMXbuUDswS_PKa2mYzGCXd5DfDrDKJhzGq2J7fpzRJvdSavRZggsKDEdW4yhj4C',
'input': {'cid': 0,
'txid': 'da8b099d83e5f1e4ba6ed5719cfe40a20029c2e9c1689029e981735fa49378e8'},
'owners_before': ['5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj']}],
'metadata': None,
'operation': 'TRANSFER'},
'version': 1}
More precisely:
In [22]: signed_tx_transfer['transaction']['fulfillments'][0]['fulfillment']
Out[22]: 'cf:4:QJavLVcbFP5cpKXTgDo9awtelABrrLEXeWl-68Tw53Bnh0UGNc1iIpGr6dTIrKXvTtMXbuUDswS_PKa2mYzGCXd5DfDrDKJhzGq2J7fpzRJvdSavRZggsKDEdW4yhj4C'
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 conenction 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 [23]: alice_secret_stash = generate_keypair()
Create another transfer transaction with the same input
In [24]: tx_transfer_2 = bdb.transactions.prepare(
....: operation='TRANSFER',
....: inputs=input_,
....: asset=asset,
....: owners_after=alice_secret_stash.verifying_key,
....: )
....:
Fulfill the transaction
In [25]: fulfilled_tx_transfer_2 = bdb.transactions.fulfill(
....: tx_transfer_2,
....: private_keys=alice.signing_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 [26]: 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 of owners_after
:
In [27]: car_creation_tx = bdb.transactions.prepare(
....: operation='CREATE',
....: owners_before=alice.verifying_key,
....: owners_after=(alice.verifying_key, bob.verifying_key),
....: asset=car_asset,
....: )
....:
In [28]: signed_car_creation_tx = bdb.transactions.fulfill(
....: car_creation_tx,
....: private_keys=alice.signing_key,
....: )
....:
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 [29]: carol = generate_keypair()
In order to prepare the transfer transaction, alice
and bob
need the
input:
In [30]: cid = 0
In [31]: condition = signed_car_creation_tx['transaction']['conditions'][cid]
In [32]: input_ = {
....: 'fulfillment': condition['condition']['details'],
....: 'input': {
....: 'cid': cid,
....: 'txid': signed_car_creation_tx['id'],
....: },
....: 'owners_before': condition['owners_after'],
....: }
....:
Let’s take a moment to contemplate what this input_
is:
In [33]: input_
Out[33]:
{'fulfillment': {'bitmask': 41,
'subfulfillments': [{'bitmask': 32,
'public_key': '5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj',
'signature': None,
'type': 'fulfillment',
'type_id': 4,
'weight': 1},
{'bitmask': 32,
'public_key': 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E',
'signature': None,
'type': 'fulfillment',
'type_id': 4,
'weight': 1}],
'threshold': 2,
'type': 'fulfillment',
'type_id': 2},
'input': {'cid': 0,
'txid': '34ee6ab27996b213bb7e51f17c3dc117690edf47f1ba341089417adfed7c150a'},
'owners_before': ['5M8MoLhoeJ9TtoMti2vvtg4T5pAKCqKD8MJjjakfZ3Cj',
'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E']}
and the asset:
In [34]: asset = signed_car_creation_tx['transaction']['asset']
then alice
can prepare the transfer:
In [35]: car_transfer_tx = bdb.transactions.prepare(
....: operation='TRANSFER',
....: owners_after=carol.verifying_key,
....: asset=asset,
....: inputs=input_,
....: )
....:
The asset can be transfered as soon as each of the owners_after
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 [36]: signed_car_transfer_tx = bdb.transactions.fulfill(
....: car_transfer_tx, private_keys=[alice.signing_key, bob.signing_key]
....: )
....:
Note, that if one the signing keys is missing, the fulfillment will fail. If we
omit bob
:
In [37]: from bigchaindb_driver.exceptions import MissingSigningKeyError
In [38]: try:
....: signed_car_transfer_tx = bdb.transactions.fulfill(
....: car_transfer_tx,
....: private_keys=alice.signing_key,
....: )
....: except MissingSigningKeyError as e:
....: print(e, e.__cause__, sep='\n')
....:
A signing key is missing!
Public key HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E is not a pair to any of the private keys
Notice bob
‘s public key in the above message:
In [39]: bob.verifying_key
Out[39]: 'HQ8qcX9qaXzYBV2Q5yBFZhn1mtKw1r8T6Q9bYnAvLH4E'
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 and JavaScript.
Threshold Conditions¶
Threshold conditions introduce multi-signatures, m-of-n signatures or even more complex binary Merkle trees to BigchainDB.
Setting up a generic threshold condition is a bit more elaborate than regular transaction signing but allow for flexible signing between multiple parties or groups.
The basic workflow for creating a more complex cryptocondition is the following:
- Create a transaction template that includes the public key of all (nested)
parties as
owners_after
- Set up the threshold condition using the cryptocondition library
- Update the condition and hash in the transaction template
We’ll illustrate this by a threshold condition where 2 out of 3
owners_after
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:
- Create a transaction template that includes the public key of all (nested)
parties as
owners_before
- Parsing the threshold condition into a fulfillment using the cryptocondition library
- Signing all necessary subfulfillments and updating the fulfillment field in 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 (like 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.
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:
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:
Todo
Stay tuned. Will soon be documented once
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
Todo
Stay tuned. Will soon be documented once
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
In the case of bob
, we create the abort
fulfillment:
Todo
Stay tuned. Will soon be documented once
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
The following demonstrates that the transaction validation switches once the timeout occurs:
Todo
Stay tuned. Will soon be documented once
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
If you execute in a timely fashion, you should see the following:
Todo
Stay tuned. Will soon be documented once
- https://github.com/bigchaindb/bigchaindb-driver/issues/108
- https://github.com/bigchaindb/bigchaindb-driver/issues/110
are taken care of.
Of course, when the execute
transaction was accepted in-time by bigchaindb,
then writing the abort
transaction after expiry will yield a
Doublespend
error.