Merge remote-tracking branch 'origin/master' into update-changelog

This commit is contained in:
troymc 2016-05-27 11:40:06 +02:00
commit d2d10f7340
11 changed files with 1155 additions and 86 deletions

View File

@ -136,7 +136,6 @@ class Bigchain(object):
response = r.table('backlog').insert(signed_transaction, durability=durability).run(self.conn) response = r.table('backlog').insert(signed_transaction, durability=durability).run(self.conn)
return response return response
# TODO: the same `txid` can be in two different blocks
def get_transaction(self, txid): def get_transaction(self, txid):
"""Retrieve a transaction with `txid` from bigchain. """Retrieve a transaction with `txid` from bigchain.
@ -151,16 +150,77 @@ class Bigchain(object):
If no transaction with that `txid` was found it returns `None` If no transaction with that `txid` was found it returns `None`
""" """
response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\ validity = self.get_blocks_status_containing_tx(txid)
.filter(lambda transaction: transaction['id'] == txid).run(self.conn)
if validity:
# Disregard invalid blocks, and return if there are no valid or undecided blocks
validity = {_id: status for _id, status in validity.items()
if status != Bigchain.BLOCK_INVALID}
if not validity:
return None
# If the transaction is in a valid or any undecided block, return it. Does not check
# if transactions in undecided blocks are consistent, but selects the valid block before
# undecided ones
for _id in validity:
target_block_id = _id
if validity[_id] == Bigchain.BLOCK_VALID:
break
# Query the transaction in the target block and return
response = r.table('bigchain').get(target_block_id).get_field('block')\
.get_field('transactions').filter(lambda tx: tx['id'] == txid).run(self.conn)
return response[0]
else:
return None
def search_block_election_on_index(self, value, index):
"""Retrieve block election information given a secondary index and value
Args:
value: a value to search (e.g. transaction id string, payload hash string)
index (str): name of a secondary index, e.g. 'transaction_id'
Returns:
A list of blocks with with only election information
"""
# First, get information on all blocks which contain this transaction
response = r.table('bigchain').get_all(value, index=index)\
.pluck('votes', 'id', {'block': ['voters']}).run(self.conn)
return list(response)
def get_blocks_status_containing_tx(self, txid):
"""Retrieve block ids and statuses related to a transaction
Transactions may occur in multiple blocks, but no more than one valid block.
Args:
txid (str): transaction id of the transaction to query
Returns:
A dict of blocks containing the transaction,
e.g. {block_id_1: 'valid', block_id_2: 'invalid' ...}, or None
"""
# First, get information on all blocks which contain this transaction
blocks = self.search_block_election_on_index(txid, 'transaction_id')
if blocks:
# Determine the election status of each block
validity = {block['id']: self.block_election_status(block) for block in blocks}
# If there are multiple valid blocks with this transaction, something has gone wrong
if list(validity.values()).count(Bigchain.BLOCK_VALID) > 1:
block_ids = str([block for block in validity
if validity[block] == Bigchain.BLOCK_VALID])
raise Exception('Transaction {tx} is present in multiple valid blocks: {block_ids}'
.format(tx=txid, block_ids=block_ids))
return validity
# transaction ids should be unique
transactions = list(response)
if transactions:
if len(transactions) != 1:
raise Exception('Transaction ids should be unique. There is a problem with the chain')
else:
return transactions[0]
else: else:
return None return None
@ -208,14 +268,25 @@ class Bigchain(object):
.contains(lambda fulfillment: fulfillment['input'] == tx_input))\ .contains(lambda fulfillment: fulfillment['input'] == tx_input))\
.run(self.conn) .run(self.conn)
# a transaction_id should have been spent at most one time
transactions = list(response) transactions = list(response)
# a transaction_id should have been spent at most one time
if transactions: if transactions:
if len(transactions) != 1: # determine if these valid transactions appear in more than one valid block
raise exceptions.DoubleSpend('`{}` was spent more then once. There is a problem with the chain'.format( num_valid_transactions = 0
tx_input['txid'])) for transaction in transactions:
else: # ignore invalid blocks
if self.get_transaction(transaction['id']):
num_valid_transactions += 1
if num_valid_transactions > 1:
raise exceptions.DoubleSpend('`{}` was spent more then once. There is a problem with the chain'.format(
tx_input['txid']))
if num_valid_transactions:
return transactions[0] return transactions[0]
else:
# all queried transactions were invalid
return None
else: else:
return None return None
@ -239,6 +310,12 @@ class Bigchain(object):
owned = [] owned = []
for tx in response: for tx in response:
# disregard transactions from invalid blocks
validity = self.get_blocks_status_containing_tx(tx['id'])
if Bigchain.BLOCK_VALID not in validity.values():
if Bigchain.BLOCK_UNDECIDED not in validity.values():
continue
# a transaction can contain multiple outputs (conditions) so we need to iterate over all of them # a transaction can contain multiple outputs (conditions) so we need to iterate over all of them
# to get a list of outputs available to spend # to get a list of outputs available to spend
for condition in tx['transaction']['conditions']: for condition in tx['transaction']['conditions']:
@ -251,9 +328,8 @@ class Bigchain(object):
# for transactions with multiple `new_owners` there will be several subfulfillments nested # for transactions with multiple `new_owners` there will be several subfulfillments nested
# in the condition. We need to iterate the subfulfillments to make sure there is a # in the condition. We need to iterate the subfulfillments to make sure there is a
# subfulfillment for `owner` # subfulfillment for `owner`
for subfulfillment in condition['condition']['details']['subfulfillments']: if util.condition_details_has_owner(condition['condition']['details'], owner):
if subfulfillment['public_key'] == owner: tx_input = {'txid': tx['id'], 'cid': condition['cid']}
tx_input = {'txid': tx['id'], 'cid': condition['cid']}
# check if input was already spent # check if input was already spent
if not self.get_spent(tx_input): if not self.get_spent(tx_input):
owned.append(tx_input) owned.append(tx_input)

View File

@ -447,7 +447,8 @@ def validate_fulfillments(signed_transaction):
return False return False
# TODO: might already break on a False here # TODO: might already break on a False here
is_valid = parsed_fulfillment.validate(serialize(fulfillment_message)) is_valid = parsed_fulfillment.validate(message=serialize(fulfillment_message),
now=timestamp())
# if transaction has an input (i.e. not a `CREATE` transaction) # if transaction has an input (i.e. not a `CREATE` transaction)
# TODO: avoid instantiation, pass as argument! # TODO: avoid instantiation, pass as argument!
@ -525,6 +526,37 @@ def get_input_condition(bigchain, fulfillment):
} }
def condition_details_has_owner(condition_details, owner):
"""
Check if the public_key of owner is in the condition details
as an Ed25519Fulfillment.public_key
Args:
condition_details (dict): dict with condition details
owner (str): base58 public key of owner
Returns:
bool: True if the public key is found in the condition details, False otherwise
"""
if 'subfulfillments' in condition_details:
result = condition_details_has_owner(condition_details['subfulfillments'], owner)
if result:
return True
elif isinstance(condition_details, list):
for subcondition in condition_details:
result = condition_details_has_owner(subcondition, owner)
if result:
return True
else:
if 'public_key' in condition_details \
and owner == condition_details['public_key']:
return True
return False
def get_hash_data(transaction): def get_hash_data(transaction):
""" Get the hashed data that (should) correspond to the `transaction['id']` """ Get the hashed data that (should) correspond to the `transaction['id']`

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -6,47 +6,53 @@ Transactions, blocks and votes are represented using JSON documents with the fol
```json ```json
{ {
"id": "<sha3 hash>", "id": "<SHA3-256 hash hexdigest of transaction (below)>",
"version": "<transaction version number>", "version": "<version number of the transaction model>",
"transaction": { "transaction": {
"fulfillments": ["<list of <fullfillment>"], "fulfillments": ["<list of fulfillments>"],
"conditions": ["<list of <condition>"], "conditions": ["<list of conditions>"],
"operation": "<string>", "operation": "<string>",
"timestamp": "<timestamp from client>", "timestamp": "<timestamp from client>",
"data": { "data": {
"hash": "<SHA3-256 hash hexdigest of payload>", "hash": "<SHA3-256 hash hexdigest of payload>",
"payload": "<generic json document>" "payload": "<any JSON document>"
} }
} }
} }
``` ```
A transaction is an operation between the `current_owner` and the `new_owner` over the digital content described by `hash`. For example if could be a transfer of ownership of the digital content `hash` Transactions are the basic records stored by BigchainDB. There are two kinds:
- **Transaction header**: 1. A "CREATE" transaction creates a new asset. It has `"operation": "CREATE"`. The `payload` or a "CREATE" transaction describes, encodes, or links to the asset in some way.
- `id`: sha3 hash of the transaction. The `id` is also the DB primary key. 2. A "TRANSFER" transaction transfers one or more assets. It has `"operation": "TRANSFER"`. The `payload` of a "TRANSFER" transaction can be empty, but it can also be used for use-case-specific information (e.g. different kinds of transfers).
- `version`: Version of the transaction. For future compability with changes in the transaction model.
- **Transaction body**: Here's some explanation of the contents of a transaction:
- `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to an unspent digital asset
and a _crypto fulfillment_ that satisfies a spending condition set on the unspent digital asset. A _fulfillment_ - `id`: The SHA3-256 hash hexdigest of everything inside the serialized `transaction` body (i.e. `fulfillments`, `conditions`, `operation`, `timestamp` and `data`; see below). The `id` is also the database primary key.
is usually a signature proving the ownership of the digital asset. - `version`: Version number of the transaction model, so that software can support different transaction models.
See [conditions and fulfillments](models.md#conditions-and-fulfillments) - `transaction`:
- `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to an unspent asset
and a _crypto fulfillment_ that satisfies a spending condition set on the unspent asset. A _fulfillment_
is usually a signature proving the ownership of the asset.
See [Conditions and Fulfillments](#conditions-and-fulfillments) below.
- `conditions`: List of conditions. Each _condition_ is a _crypto condition_ that needs to be fulfilled by the - `conditions`: List of conditions. Each _condition_ is a _crypto condition_ that needs to be fulfilled by the
new owner in order to spend the digital asset. new owner in order to spend the asset.
See [conditions and fulfillments](models.md#conditions-and-fulfillments) See [Conditions and Fulfillments](#conditions-and-fulfillments) below.
- `operation`: String representation of the operation being performed (`CREATE`, `TRANSFER`, ...) this will define how - `operation`: String representation of the operation being performed (currently either "CREATE" or "TRANSFER"). It determines how
the transactions should be validated the transaction should be validated.
- `timestamp`: Time of creation of the transaction in UTC. It's provided by the client. - `timestamp`: Time of creation of the transaction in UTC. It's provided by the client.
- `data`: JSON object describing the asset (digital content). It contains at least the field `hash` which is a - `data`:
sha3 hash of the digital content. - `hash`: The SHA3-256 hash hexdigest of the serialized `payload`.
- `payload`: Can be any JSON document. Its meaning depends on the whether the transaction
is a "CREATE" or "TRANSFER" transaction; see above.
## Conditions and Fulfillments ## Conditions and Fulfillments
### Conditions ### Conditions
##### Simple Signature #### One New Owner
If there is only one _new owner_ the condition will be a single signature condition. If there is only one _new owner_, the condition will be a single-signature condition.
```json ```json
{ {
@ -54,33 +60,32 @@ If there is only one _new owner_ the condition will be a single signature condit
"condition": { "condition": {
"details": { "details": {
"bitmask": "<base16 int>", "bitmask": "<base16 int>",
"public_key": "<explain>", "public_key": "<new owner public key>",
"signature": null, "signature": null,
"type": "fulfillment", "type": "fulfillment",
"type_id": "<base16 int>" "type_id": "<base16 int>"
}, },
"uri": "<string>" "uri": "<string>"
}, },
"new_owners": ["<list of <base58 string>>"] "new_owners": ["<new owner public key>"]
} }
``` ```
- **Condition header**: - **Condition header**:
- `cid`: Condition index so that we can reference this output as an input to another transaction. It also matches - `cid`: Condition index so that we can reference this output as an input to another transaction. It also matches
the input `fid`, making this the condition to fulfill in order to spend the digital asset used as input with `fid` the input `fid`, making this the condition to fulfill in order to spend the asset used as input with `fid`.
- `new_owners`: List of public keys of the new owners. - `new_owners`: A list containing one item: the public key of the new owner.
- **Condition body**: - **Condition body**:
- `bitmask`: a set of bits representing the features required by the condition type - `bitmask`: A set of bits representing the features required by the condition type.
- `public_key`: the base58 representation of the _new_owner's_ verifying key. - `public_key`: The _new_owner's_ public key.
- `type_id`: the fulfillment type ID (see the [ILP spec](https://interledger.org/five-bells-condition/spec.html)) - `type_id`: The fulfillment type ID; see the [ILP spec](https://interledger.org/five-bells-condition/spec.html).
- `uri`: binary representation of the condition using only URL-safe characters - `uri`: Binary representation of the condition using only URL-safe characters.
##### Multi Signature #### Multiple New Owners
If there are multiple _new owners_ by default we create a condition requiring a signature from each new owner in order If there are multiple _new owners_, we can create a ThresholdCondition requiring a signature from each new owner in order
to spend the digital asset. to spend the asset. For example:
Example of a condition with two _new owners_:
```json ```json
{ {
"cid": "<condition index>", "cid": "<condition index>",
@ -121,16 +126,17 @@ Example of a condition with two _new owners_:
- `weight`: integer weight for each subfulfillment's contribution to the threshold - `weight`: integer weight for each subfulfillment's contribution to the threshold
- `threshold`: threshold to reach for the subfulfillments to reach a valid fulfillment - `threshold`: threshold to reach for the subfulfillments to reach a valid fulfillment
The `weight`s and `threshold` could be adjusted. For example, if the `threshold` was changed to 1 above, then only one of the new owners would have to provide a signature to spend the asset.
### Fulfillments ### Fulfillments
##### Simple Signature #### One Current Owner
If there is only one _current owner_ the fulfillment will be a single signature fulfillment. If there is only one _current owner_, the fulfillment will be a single-signature fulfillment.
```json ```json
{ {
"current_owners": ["<Public Key>"], "current_owners": ["<public key of current owner>"],
"fid": 0, "fid": 0,
"fulfillment": "cf:4:RxFzIE679tFBk8zwEgizhmTuciAylvTUwy6EL6ehddHFJOhK5F4IjwQ1xLu2oQK9iyRCZJdfWAefZVjTt3DeG5j2exqxpGliOPYseNkRAWEakqJ_UrCwgnj92dnFRAEE", "fulfillment": "cf:4:RxFzIE679tFBk8zwEgizhmTuciAylvTUwy6EL6ehddHFJOhK5F4IjwQ1xLu2oQK9iyRCZJdfWAefZVjTt3DeG5j2exqxpGliOPYseNkRAWEakqJ_UrCwgnj92dnFRAEE",
"input": { "input": {
@ -140,11 +146,11 @@ If there is only one _current owner_ the fulfillment will be a single signature
} }
``` ```
- `fid`: Fulfillment index. It matches a `cid` in the conditions with a new _crypto condition_ that the new owner(s) - `fid`: Fulfillment index. It matches a `cid` in the conditions with a new _crypto condition_ that the new owner
need to fulfill to spend this digital asset needs to fulfill to spend this asset.
- `current_owners`: Public key of the current owner(s) - `current_owners`: A list of public keys of the current owners; in this case it has just one public key.
- `fulfillment`: - `fulfillment`: A cryptoconditions URI that encodes the cryptographic fulfillments like signatures and others, see crypto-conditions.
- `input`: Pointer to the digital asset and condition of a previous transaction - `input`: Pointer to the asset and condition of a previous transaction
- `cid`: Condition index - `cid`: Condition index
- `txid`: Transaction id - `txid`: Transaction id
@ -152,7 +158,7 @@ need to fulfill to spend this digital asset
```json ```json
{ {
"id": "<sha3 hash of the serialized block contents>", "id": "<SHA3-256 hash hexdigest of the serialized block contents>",
"block": { "block": {
"timestamp": "<block-creation timestamp>", "timestamp": "<block-creation timestamp>",
"transactions": ["<list of transactions>"], "transactions": ["<list of transactions>"],
@ -164,21 +170,16 @@ need to fulfill to spend this digital asset
} }
``` ```
Still to be defined when new blocks are created (after x number of transactions, or after x amount of seconds, - `id`: SHA3-256 hash hexdigest of the contents of `block` (i.e. the timestamp, list of transactions, node_pubkey, and voters). This is also a database primary key; that's how we ensure that all blocks are unique.
or both). - `block`:
- `timestamp`: Timestamp when the block was created. It's provided by the node that created the block.
A block contains a group of transactions and includes the hash of the hash of the previous block to build the chain. - `transactions`: A list of the transactions included in the block.
- `node_pubkey`: The public key of the node that create the block.
- `id`: sha3 hash of the contents of `block` (i.e. the timestamp, list of transactions, node_pubkey, and voters). This is also a RethinkDB primary key; that's how we ensure that all blocks are unique. - `voters`: A list of public keys of federation nodes. Since the size of the
- `block`: The actual block federation may change over time, this will tell us how many nodes existed
- `timestamp`: timestamp when the block was created. It's provided by the node that created the block. in the federation when the block was created, so that at a later point in
- `transactions`: the list of transactions included in the block
- `node_pubkey`: the public key of the node that create the block
- `voters`: list of public keys of the federation nodes. Since the size of the
federation may change over time this will tell us how many nodes existed
in the federation when the block was created so that at a later point in
time we can check that the block received the correct number of votes. time we can check that the block received the correct number of votes.
- `signature`: Signature of the block by the node that created the block (i.e. To create it, the node serialized the block contents and signed that with its private key) - `signature`: Signature of the block by the node that created the block. (To create the signature, the node serializes the block contents and signs that with its private key.)
- `votes`: Initially an empty list. New votes are appended as they come in from the nodes. - `votes`: Initially an empty list. New votes are appended as they come in from the nodes.
## The Vote Model ## The Vote Model
@ -187,7 +188,7 @@ Each node must generate a vote for each block, to be appended to that block's `v
```json ```json
{ {
"node_pubkey": "<the pubkey of the voting node>", "node_pubkey": "<the public key of the voting node>",
"vote": { "vote": {
"voting_for_block": "<id of the block the node is voting for>", "voting_for_block": "<id of the block the node is voting for>",
"previous_block": "<id of the block previous to this one>", "previous_block": "<id of the block previous to this one>",
@ -199,4 +200,4 @@ Each node must generate a vote for each block, to be appended to that block's `v
} }
``` ```
Note: The `invalid_reason` was not being used as of v0.1.3. Note: The `invalid_reason` was not being used as of v0.1.3 and may be dropped in a future version of BigchainDB.

View File

@ -129,7 +129,9 @@ The locking script is refered to as a `condition` and a corresponding `fulfillme
Since a transaction can have multiple outputs with each its own (crypto)condition, each transaction input should also refer to the condition index `cid`. Since a transaction can have multiple outputs with each its own (crypto)condition, each transaction input should also refer to the condition index `cid`.
![BigchainDB transactions connecting fulfillments with conditions](./_static/tx_single_condition_single_fulfillment_v1.png) <p align="center">
<img width="70%" height="70%" src ="./_static/tx_single_condition_single_fulfillment_v1.png" />
</p>
```python ```python
@ -379,7 +381,9 @@ With BigchainDB it is possible to send multiple assets to someone in a single tr
The transaction will create a `fulfillment` - `condition` pair for each input, which can be refered to by `fid` and `cid` respectively. The transaction will create a `fulfillment` - `condition` pair for each input, which can be refered to by `fid` and `cid` respectively.
![BigchainDB transactions connecting multiple fulfillments with multiple conditions](./_static/tx_multi_condition_multi_fulfillment_v1.png) <p align="center">
<img width="70%" height="70%" src ="./_static/tx_multi_condition_multi_fulfillment_v1.png" />
</p>
```python ```python
# Create some assets for bulk transfer # Create some assets for bulk transfer
@ -782,7 +786,6 @@ hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private)
# Some validations # Some validations
assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed
assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed
b.write_transaction(hashlock_tx_signed) b.write_transaction(hashlock_tx_signed)
hashlock_tx_signed hashlock_tx_signed
@ -840,7 +843,6 @@ hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \
hashlock_fulfill_tx_fulfillment.serialize_uri() hashlock_fulfill_tx_fulfillment.serialize_uri()
assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
b.write_transaction(hashlock_fulfill_tx) b.write_transaction(hashlock_fulfill_tx)
hashlock_fulfill_tx hashlock_fulfill_tx
@ -886,3 +888,415 @@ hashlock_fulfill_tx
"version":1 "version":1
} }
``` ```
### Timeout Conditions
Timeout conditions allow assets to expire after a certain time.
The primary use case of timeout conditions is to enable [Escrow](#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.
__Caveat__: The times between nodes in a BigchainDB federation may (and will) differ slightly. In this case, the majority of the nodes will decide.
```python
# Create a timeout asset without any new_owners
tx_timeout = b.create_transaction(b.me, None, None, 'CREATE')
# Set expiry time - the asset needs to be transfered before expiration
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep) # 12 secs from now
condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire)
# The conditions list is empty, so we need to append a new condition
tx_timeout['transaction']['conditions'].append({
'condition': {
'details': json.loads(condition_timeout.serialize_json()),
'uri': condition_timeout.condition.serialize_uri()
},
'cid': 0,
'new_owners': None
})
# Conditions have been updated, so the hash needs updating
tx_timeout['id'] = util.get_hash_data(tx_timeout)
# The asset needs to be signed by the current_owner
tx_timeout_signed = b.sign_transaction(tx_timeout, b.me_private)
# Some validations
assert b.validate_transaction(tx_timeout_signed) == tx_timeout_signed
b.write_transaction(tx_timeout_signed)
tx_timeout_signed
```
```python
{
"id":"78145396cd368f7168fb01c97aaf1df6f85244d7b544073dfcb42397dae38f90",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":9,
"expire_time":"1464167910.643431",
"type":"fulfillment",
"type_id":99
},
"uri":"cc:63:9:sceU_NZc3cAjAvaR1TVmgj7am5y8hJEBoqLm-tbqGbQ:17"
},
"new_owners":null
}
],
"data":null,
"fulfillments":[
{
"current_owners":[
"FmLm6MxCABc8TsiZKdeYaZKo5yZWMM6Vty7Q1B6EgcP2"
],
"fid":0,
"fulfillment":null,
"input":null
}
],
"operation":"CREATE",
"timestamp":"1464167898.643353"
},
"version":1
}
```
The following demonstrates that the transaction invalidates once the timeout occurs:
```python
from time import sleep
# Create a timeout fulfillment tx
tx_timeout_transfer = b.create_transaction(None, testuser1_pub, {'txid': tx_timeout['id'], 'cid': 0}, 'TRANSFER')
# Parse the timeout condition and create the corresponding fulfillment
timeout_fulfillment = cc.Fulfillment.from_json(
tx_timeout['transaction']['conditions'][0]['condition']['details'])
tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_fulfillment.serialize_uri()
# No need to sign transaction, like with hashlocks
# Small test to see the state change
for i in range(time_sleep - 4):
tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout))
sleep(1)
```
If you were fast enough, you should see the following output:
```python
tx_timeout valid: True (3s to timeout)
tx_timeout valid: True (2s to timeout)
tx_timeout valid: True (1s to timeout)
tx_timeout valid: True (0s to timeout)
tx_timeout valid: False (0s to timeout)
tx_timeout valid: False (-1s to timeout)
tx_timeout valid: False (-2s to timeout)
tx_timeout valid: False (-3s to timeout)
```
## Escrow
Escrow is a mechanism for conditional release of assets.
This means that a 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.
<p align="center">
<img width="70%" height="70%" src ="./_static/tx_escrow_execute_abort.png" />
</p>
The logic for switching between `execute` and `abort` conditions is conceptually simple:
```python
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:
<p align="center">
<img width="100%" height="100%" src ="./_static/cc_escrow_execute_abort.png" />
</p>
The small circle (&#9898;) at an input of a threshold condition denotes an inversion of the fulfillment:
```python
inverted_fulfillment.validate(msg) == not fulfillment.validate(msg)
```
An inverted input to a threshold condition is simply obtained by negative weights.
__Note__: negative weights are BigchainDB-specific and not (yet) supported by the ILP standard.
The following code snippet shows how to create an escrow condition:
```python
# Retrieve the last transaction of testuser2_pub (or create a new asset)
tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
# Create a base template with the execute and abort address
tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub], tx_retrieved_id, 'TRANSFER')
# Set expiry time - the execute address needs to fulfill before expiration
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep) # 12 secs from now
# Create the escrow and timeout condition
condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate
condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) # only valid if now() <= time_expire
# Create the execute branch
condition_execute = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate
condition_execute.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser1_pub)) # execute address
condition_execute.add_subfulfillment(condition_timeout) # federation checks on expiry
condition_escrow.add_subfulfillment(condition_execute)
# Create the abort branch
condition_abort = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate
condition_abort.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser2_pub)) # abort address
condition_abort.add_subfulfillment(condition_timeout, weight=-1) # the negative weight inverts the condition
condition_escrow.add_subfulfillment(condition_abort)
# Update the condition in the newly created transaction
tx_escrow['transaction']['conditions'][0]['condition'] = {
'details': json.loads(condition_escrow.serialize_json()),
'uri': condition_escrow.condition.serialize_uri()
}
# Conditions have been updated, so the hash needs updating
tx_escrow['id'] = util.get_hash_data(tx_escrow)
# The asset needs to be signed by the current_owner
tx_escrow_signed = b.sign_transaction(tx_escrow, testuser2_priv)
# Some validations
assert b.validate_transaction(tx_escrow_signed) == tx_escrow_signed
b.write_transaction(tx_escrow_signed)
tx_escrow_signed
```
```python
{
"id":"1a281da2b9bc3d2beba92479058d440de3353427fd64045a61737bad0d0c809c",
"transaction":{
"conditions":[
{
"cid":0,
"condition":{
"details":{
"bitmask":41,
"subfulfillments":[
{
"bitmask":41,
"subfulfillments":[
{
"bitmask":32,
"public_key":"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor",
"signature":null,
"type":"fulfillment",
"type_id":4,
"weight":1
},
{
"bitmask":9,
"expire_time":"1464242352.227917",
"type":"fulfillment",
"type_id":99,
"weight":1
}
],
"threshold":2,
"type":"fulfillment",
"type_id":2,
"weight":1
},
{
"bitmask":41,
"subfulfillments":[
{
"bitmask":32,
"public_key":"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"signature":null,
"type":"fulfillment",
"type_id":4,
"weight":1
},
{
"bitmask":9,
"expire_time":"1464242352.227917",
"type":"fulfillment",
"type_id":99,
"weight":-1
}
],
"threshold":2,
"type":"fulfillment",
"type_id":2,
"weight":1
}
],
"threshold":1,
"type":"fulfillment",
"type_id":2
},
"uri":"cc:2:29:sg08ERtppQrGxot7mu7XMdNkZTc29xCbWE1r8DgxuL8:181"
},
"new_owners":[
"BwuhqQX8FPsmqYiRV2CSZYWWsSWgSSQQFHjqxKEuqkPs",
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
]
}
],
"data":null,
"fulfillments":[
{
"current_owners":[
"qv8DvdNG5nZHWCP5aPSqgqxAvaPJpQj19abRvFCntor"
],
"fid":0,
"fulfillment":"cf:4:B6VAa7KAMD1v-pyvDx9RuBLb6l2Qs3vhucgXqzU_RbuRucOp6tNY8AoNMoC-HAOZBJSnHXZsdJ7pLCZ6aDTwUHXf0zxyLaCgy1NpES3h8qcuxbfv4Nchw3BtUcVSY3AM",
"input":{
"cid":1,
"txid":"d3f5e78f6d4346466178745f1c01cbcaf1c1dce1932a16cd653051b16ee29bac"
}
}
],
"operation":"TRANSFER",
"timestamp":"1464242340.227787"
},
"version":1
}
```
At any given moment `testuser1` and `testuser2` can try to fulfill the `execute` and `abort` branch respectively.
Whether the fulfillment will validate depends on the timeout condition.
We'll illustrate this by example.
In the case of `testuser1`, we create the `execute` fulfillment:
```python
# Create a base template for execute fulfillment
tx_escrow_execute = b.create_transaction([testuser2_pub, testuser1_pub], testuser1_pub, {'txid': tx_escrow_signed['id'], 'cid': 0}, 'TRANSFER')
# Parse the Escrow cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
tx_escrow['transaction']['conditions'][0]['condition']['details'])
subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0]
subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0]
subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body']
# Get the fulfillment message to sign
tx_escrow_execute_fulfillment_message = \
util.get_fulfillment_message(tx_escrow_execute,
tx_escrow_execute['transaction']['fulfillments'][0],
serialized=True)
# Clear the subconditions of the escrow fulfillment
escrow_fulfillment.subconditions = []
# Fulfill the execute branch
fulfillment_execute = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_testuser1.sign(tx_escrow_execute_fulfillment_message, crypto.SigningKey(testuser1_priv))
fulfillment_execute.add_subfulfillment(subfulfillment_testuser1)
fulfillment_execute.add_subfulfillment(subfulfillment_timeout)
escrow_fulfillment.add_subfulfillment(fulfillment_execute)
# Do not fulfill the abort branch
condition_abort = cc.ThresholdSha256Fulfillment(threshold=2)
condition_abort.add_subfulfillment(subfulfillment_testuser2)
condition_abort.add_subfulfillment(subfulfillment_timeout, weight=-1)
escrow_fulfillment.add_subcondition(condition_abort.condition) # Adding only the condition here
# Update the execute transaction with the fulfillment
tx_escrow_execute['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
```
In the case of `testuser2`, we create the `abort` fulfillment:
```python
# Create a base template for execute fulfillment
tx_escrow_abort = b.create_transaction([testuser2_pub, testuser1_pub], testuser2_pub, {'txid': tx_escrow_signed['id'], 'cid': 0}, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
tx_escrow['transaction']['conditions'][0]['condition']['details'])
subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0]
subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0]
subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body']
# Get the fulfillment message to sign
tx_escrow_abort_fulfillment_message = \
util.get_fulfillment_message(tx_escrow_abort,
tx_escrow_abort['transaction']['fulfillments'][0],
serialized=True)
# Clear the subconditions of the escrow fulfillment
escrow_fulfillment.subconditions = []
# Do not fulfill the execute branch
condition_execute = cc.ThresholdSha256Fulfillment(threshold=2)
condition_execute.add_subfulfillment(subfulfillment_testuser1)
condition_execute.add_subfulfillment(subfulfillment_timeout)
escrow_fulfillment.add_subcondition(condition_execute.condition) # Adding only the condition here
# Fulfill the abort branch
fulfillment_abort = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_testuser2.sign(tx_escrow_abort_fulfillment_message, crypto.SigningKey(testuser2_priv))
fulfillment_abort.add_subfulfillment(subfulfillment_testuser2)
fulfillment_abort.add_subfulfillment(subfulfillment_timeout, weight=-1)
escrow_fulfillment.add_subfulfillment(fulfillment_abort)
# Update the abort transaction with the fulfillment
tx_escrow_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
```
The following demonstrates that the transaction validation switches once the timeout occurs:
```python
for i in range(time_sleep - 4):
valid_execute = b.is_valid_transaction(tx_escrow_execute) == tx_escrow_execute
valid_abort = b.is_valid_transaction(tx_escrow_abort) == tx_escrow_abort
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
print('tx_execute valid: {} - tx_abort valid {} ({}s to timeout)'.format(valid_execute, valid_abort, seconds_to_timeout))
sleep(1)
```
If you execute in a timely fashion, you should see the following:
```python
tx_execute valid: True - tx_abort valid False (3s to timeout)
tx_execute valid: True - tx_abort valid False (2s to timeout)
tx_execute valid: True - tx_abort valid False (1s to timeout)
tx_execute valid: True - tx_abort valid False (0s to timeout)
tx_execute valid: False - tx_abort valid True (0s to timeout)
tx_execute valid: False - tx_abort valid True (-1s to timeout)
tx_execute valid: False - tx_abort valid True (-2s to timeout)
tx_execute valid: False - tx_abort valid True (-3s to timeout)
```
Of course, when the `execute` transaction was accepted in-time by bigchaindb, then writing the `abort` transaction after expiry will yield a `Doublespend` error.

View File

@ -96,7 +96,7 @@ setup(
'rethinkdb==2.3.0', 'rethinkdb==2.3.0',
'pysha3==0.3', 'pysha3==0.3',
'pytz==2015.7', 'pytz==2015.7',
'cryptoconditions==0.2.2', 'cryptoconditions==0.3.0',
'statsd==3.2.1', 'statsd==3.2.1',
'python-rapidjson==0.0.6', 'python-rapidjson==0.0.6',
'logstats==0.2.1', 'logstats==0.2.1',

View File

@ -50,6 +50,11 @@ def setup_database(request, node_config):
r.db(db_name).table('backlog')\ r.db(db_name).table('backlog')\
.index_create('assignee__transaction_timestamp', [r.row['assignee'], r.row['transaction']['timestamp']])\ .index_create('assignee__transaction_timestamp', [r.row['assignee'], r.row['transaction']['timestamp']])\
.run() .run()
# order transactions by id
r.db(db_name).table('bigchain').index_create('transaction_id', r.row['block']['transactions']['id'],
multi=True).run()
r.db(db_name).table('bigchain').index_wait('transaction_id').run()
def fin(): def fin():
print('Deleting `{}` database'.format(db_name)) print('Deleting `{}` database'.format(db_name))

View File

@ -107,6 +107,25 @@ class TestBigchainApi(object):
response = b.get_transaction(tx_signed["id"]) response = b.get_transaction(tx_signed["id"])
assert util.serialize(tx_signed) == util.serialize(response) assert util.serialize(tx_signed) == util.serialize(response)
@pytest.mark.usefixtures('inputs')
def test_read_transaction_invalid_block(self, b, user_vk, user_sk):
input_tx = b.get_owned_ids(user_vk).pop()
tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER')
tx_signed = b.sign_transaction(tx, user_sk)
b.write_transaction(tx_signed)
# create block
block = b.create_block([tx_signed])
b.write_block(block, durability='hard')
# vote the block invalid
vote = b.vote(block, b.get_last_voted_block()['id'], False)
b.write_vote(block, vote, 3)
response = b.get_transaction(tx_signed["id"])
# should be None, because invalid blocks are ignored
assert response is None
@pytest.mark.usefixtures('inputs') @pytest.mark.usefixtures('inputs')
def test_assign_transaction_one_node(self, b, user_vk, user_sk): def test_assign_transaction_one_node(self, b, user_vk, user_sk):
input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_owned_ids(user_vk).pop()
@ -1033,6 +1052,43 @@ class TestMultipleInputs(object):
assert owned_inputs_user1 == [] assert owned_inputs_user1 == []
assert owned_inputs_user2 == [{'cid': 0, 'txid': tx['id']}] assert owned_inputs_user2 == [{'cid': 0, 'txid': tx['id']}]
def test_get_owned_ids_single_tx_single_output_invalid_block(self, b, user_sk, user_vk):
# create a new users
user2_sk, user2_vk = crypto.generate_key_pair()
# create input to spend
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
block = b.create_block([tx_signed])
b.write_block(block, durability='hard')
# vote the block VALID
vote = b.vote(block, b.get_unvoted_blocks()[0]['id'], True)
b.write_vote(block, vote, 2)
# get input
owned_inputs_user1 = b.get_owned_ids(user_vk)
owned_inputs_user2 = b.get_owned_ids(user2_vk)
assert owned_inputs_user1 == [{'cid': 0, 'txid': tx['id']}]
assert owned_inputs_user2 == []
# create a transaction and block
tx_invalid = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER')
tx_invalid_signed = b.sign_transaction(tx_invalid, user_sk)
block = b.create_block([tx_invalid_signed])
b.write_block(block, durability='hard')
# vote the block invalid
vote = b.vote(block, b.get_last_voted_block()['id'], False)
b.write_vote(block, vote, 3)
owned_inputs_user1 = b.get_owned_ids(user_vk)
owned_inputs_user2 = b.get_owned_ids(user2_vk)
# should be the same as before (note tx, not tx_invalid)
assert owned_inputs_user1 == [{'cid': 0, 'txid': tx['id']}]
assert owned_inputs_user2 == []
def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_vk): def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_vk):
# create a new users # create a new users
user2_sk, user2_vk = crypto.generate_key_pair() user2_sk, user2_vk = crypto.generate_key_pair()
@ -1124,6 +1180,42 @@ class TestMultipleInputs(object):
spent_inputs_user1 = b.get_spent(owned_inputs_user1[0]) spent_inputs_user1 = b.get_spent(owned_inputs_user1[0])
assert spent_inputs_user1 == tx_signed assert spent_inputs_user1 == tx_signed
def test_get_spent_single_tx_single_output_invalid_block(self, b, user_sk, user_vk):
# create a new users
user2_sk, user2_vk = crypto.generate_key_pair()
# create input to spend
tx = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
block = b.create_block([tx_signed])
b.write_block(block, durability='hard')
# vote the block VALID
vote = b.vote(block, b.get_unvoted_blocks()[0]['id'], True)
b.write_vote(block, vote, 2)
# get input
owned_inputs_user1 = b.get_owned_ids(user_vk)
# check spents
spent_inputs_user1 = b.get_spent(owned_inputs_user1[0])
assert spent_inputs_user1 is None
# create a transaction and block
tx = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER')
tx_signed = b.sign_transaction(tx, user_sk)
block = b.create_block([tx_signed])
b.write_block(block, durability='hard')
# vote the block invalid
vote = b.vote(block, b.get_last_voted_block()['id'], False)
b.write_vote(block, vote, 2)
response = b.get_transaction(tx_signed["id"])
spent_inputs_user1 = b.get_spent(owned_inputs_user1[0])
# Now there should be no spents (the block is invalid)
assert spent_inputs_user1 is None
def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_vk): def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_vk):
# create a new users # create a new users
user2_sk, user2_vk = crypto.generate_key_pair() user2_sk, user2_vk = crypto.generate_key_pair()
@ -1878,3 +1970,265 @@ class TestCryptoconditions(object):
for new_owner in new_owners: for new_owner in new_owners:
subcondition = condition.get_subcondition_from_vk(new_owner)[0] subcondition = condition.get_subcondition_from_vk(new_owner)[0]
assert subcondition.public_key.to_ascii().decode() == new_owner assert subcondition.public_key.to_ascii().decode() == new_owner
@pytest.mark.usefixtures('inputs')
def test_transfer_asset_with_escrow_condition(self, b, user_vk, user_sk):
first_input_tx = b.get_owned_ids(user_vk).pop()
user2_sk, user2_vk = crypto.generate_key_pair()
# ESCROW
escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER')
time_sleep = 3
condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1)
fulfillment_timeout = cc.TimeoutFulfillment(expire_time=str(float(util.timestamp()) + time_sleep))
condition_user = cc.Ed25519Fulfillment(public_key=user_vk)
condition_user2 = cc.Ed25519Fulfillment(public_key=user2_vk)
# execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_execute.add_subfulfillment(condition_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
# do not fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_abort.add_subfulfillment(condition_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
condition_escrow.add_subfulfillment(fulfillment_and_execute)
condition_escrow.add_subfulfillment(fulfillment_and_abort)
# Update the condition in the newly created transaction
escrow_tx['transaction']['conditions'][0]['condition'] = {
'details': json.loads(condition_escrow.serialize_json()),
'uri': condition_escrow.condition.serialize_uri()
}
# conditions have been updated, so hash needs updating
escrow_tx['id'] = util.get_hash_data(escrow_tx)
escrow_tx_signed = b.sign_transaction(escrow_tx, user_sk)
assert b.validate_transaction(escrow_tx_signed) == escrow_tx_signed
assert b.is_valid_transaction(escrow_tx_signed) == escrow_tx_signed
b.write_transaction(escrow_tx_signed)
# create and write block to bigchain
block = b.create_block([escrow_tx_signed])
b.write_block(block, durability='hard')
# Retrieve the last transaction of thresholduser1_pub
tx_retrieved_id = b.get_owned_ids(user2_vk).pop()
# EXECUTE
# Create a base template for output transaction
escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
escrow_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0]
subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0]
# Get the fulfillment message to sign
escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_transfer,
escrow_tx_transfer['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# fulfill execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_user2.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user2_sk))
fulfillment_and_execute.add_subfulfillment(subfulfillment_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
escrow_fulfillment.add_subfulfillment(fulfillment_and_execute)
# do not fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_abort.add_subfulfillment(subfulfillment_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
escrow_fulfillment.add_subcondition(fulfillment_and_abort.condition)
escrow_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
# in-time validation (execute)
assert b.is_valid_transaction(escrow_tx_transfer) == escrow_tx_transfer
assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer
time.sleep(time_sleep)
assert b.is_valid_transaction(escrow_tx_transfer) is False
with pytest.raises(exceptions.InvalidSignature):
assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer
# ABORT
# Create a base template for output transaction
escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
escrow_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0]
subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0]
# Get the fulfillment message to sign
escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_abort,
escrow_tx_abort['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# fulfill execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_execute.add_subfulfillment(subfulfillment_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
escrow_fulfillment.add_subcondition(fulfillment_and_execute.condition)
# do not fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_user.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user_sk))
fulfillment_and_abort.add_subfulfillment(subfulfillment_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
escrow_fulfillment.add_subfulfillment(fulfillment_and_abort)
escrow_tx_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
# out-of-time validation (abort)
assert b.validate_transaction(escrow_tx_abort) == escrow_tx_abort
assert b.is_valid_transaction(escrow_tx_abort) == escrow_tx_abort
@pytest.mark.usefixtures('inputs')
def test_transfer_asset_with_escrow_condition_doublespend(self, b, user_vk, user_sk):
first_input_tx = b.get_owned_ids(user_vk).pop()
user2_sk, user2_vk = crypto.generate_key_pair()
# ESCROW
escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER')
time_sleep = 3
condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1)
fulfillment_timeout = cc.TimeoutFulfillment(expire_time=str(float(util.timestamp()) + time_sleep))
condition_user = cc.Ed25519Fulfillment(public_key=user_vk)
condition_user2 = cc.Ed25519Fulfillment(public_key=user2_vk)
# execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_execute.add_subfulfillment(condition_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
# do not fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_abort.add_subfulfillment(condition_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
condition_escrow.add_subfulfillment(fulfillment_and_execute)
condition_escrow.add_subfulfillment(fulfillment_and_abort)
# Update the condition in the newly created transaction
escrow_tx['transaction']['conditions'][0]['condition'] = {
'details': json.loads(condition_escrow.serialize_json()),
'uri': condition_escrow.condition.serialize_uri()
}
# conditions have been updated, so hash needs updating
escrow_tx['id'] = util.get_hash_data(escrow_tx)
escrow_tx_signed = b.sign_transaction(escrow_tx, user_sk)
assert b.validate_transaction(escrow_tx_signed) == escrow_tx_signed
assert b.is_valid_transaction(escrow_tx_signed) == escrow_tx_signed
b.write_transaction(escrow_tx_signed)
# create and write block to bigchain
block = b.create_block([escrow_tx_signed])
b.write_block(block, durability='hard')
# Retrieve the last transaction of thresholduser1_pub
tx_retrieved_id = b.get_owned_ids(user2_vk).pop()
# EXECUTE
# Create a base template for output transaction
escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
escrow_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0]
subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0]
# Get the fulfillment message to sign
escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_transfer,
escrow_tx_transfer['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# fulfill execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_user2.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user2_sk))
fulfillment_and_execute.add_subfulfillment(subfulfillment_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
escrow_fulfillment.add_subfulfillment(fulfillment_and_execute)
# do not fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_abort.add_subfulfillment(subfulfillment_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
escrow_fulfillment.add_subcondition(fulfillment_and_abort.condition)
escrow_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
# in-time validation (execute)
assert b.is_valid_transaction(escrow_tx_transfer) == escrow_tx_transfer
assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer
b.write_transaction(escrow_tx_transfer)
# create and write block to bigchain
block = b.create_block([escrow_tx_transfer])
b.write_block(block, durability='hard')
time.sleep(time_sleep)
assert b.is_valid_transaction(escrow_tx_transfer) is False
with pytest.raises(exceptions.InvalidSignature):
assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer
# ABORT
# Create a base template for output transaction
escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
escrow_tx['transaction']['conditions'][0]['condition']['details'])
subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0]
subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0]
# Get the fulfillment message to sign
escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_abort,
escrow_tx_abort['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# do not fulfill execute branch
fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2)
fulfillment_and_execute.add_subfulfillment(subfulfillment_user2)
fulfillment_and_execute.add_subfulfillment(fulfillment_timeout)
escrow_fulfillment.add_subcondition(fulfillment_and_execute.condition)
# fulfill abort branch
fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_user.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user_sk))
fulfillment_and_abort.add_subfulfillment(subfulfillment_user)
fulfillment_and_abort.add_subfulfillment(fulfillment_timeout, weight=-1)
escrow_fulfillment.add_subfulfillment(fulfillment_and_abort)
escrow_tx_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
# out-of-time validation (abort)
with pytest.raises(exceptions.DoubleSpend):
b.validate_transaction(escrow_tx_abort)
assert b.is_valid_transaction(escrow_tx_abort) is False

View File

@ -106,6 +106,7 @@ sleep(8)
# retrieve the transaction # retrieve the transaction
tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id']) tx_multisig_retrieved = b.get_transaction(tx_multisig_signed['id'])
assert tx_multisig_retrieved is not None
print(json.dumps(tx_multisig_retrieved, sort_keys=True, indent=4, separators=(',', ':'))) print(json.dumps(tx_multisig_retrieved, sort_keys=True, indent=4, separators=(',', ':')))
@ -115,7 +116,11 @@ tx_multisig_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER') tx_multisig_transfer = b.create_transaction([testuser1_pub, testuser2_pub], testuser3_pub, tx_multisig_retrieved_id, 'TRANSFER')
tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv]) tx_multisig_transfer_signed = b.sign_transaction(tx_multisig_transfer, [testuser1_priv, testuser2_priv])
b.validate_transaction(tx_multisig_transfer_signed) try:
b.validate_transaction(tx_multisig_transfer_signed)
except exceptions.InvalidSignature:
import ipdb; ipdb.set_trace()
b.validate_transaction(tx_multisig_transfer_signed)
b.write_transaction(tx_multisig_transfer_signed) b.write_transaction(tx_multisig_transfer_signed)
# wait a few seconds for the asset to appear on the blockchain # wait a few seconds for the asset to appear on the blockchain
@ -151,6 +156,8 @@ b.write_transaction(tx_mimo_signed)
print(json.dumps(tx_mimo_signed, sort_keys=True, indent=4, separators=(',', ':'))) print(json.dumps(tx_mimo_signed, sort_keys=True, indent=4, separators=(',', ':')))
sleep(8)
""" """
Threshold Conditions Threshold Conditions
""" """
@ -298,3 +305,183 @@ assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx
b.write_transaction(hashlock_fulfill_tx) b.write_transaction(hashlock_fulfill_tx)
print(json.dumps(hashlock_fulfill_tx, sort_keys=True, indent=4, separators=(',', ':'))) print(json.dumps(hashlock_fulfill_tx, sort_keys=True, indent=4, separators=(',', ':')))
"""
Timeout Conditions
"""
# Create transaction template
tx_timeout = b.create_transaction(b.me, None, None, 'CREATE')
# Set expiry time (12 secs from now)
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep)
# only valid if the server time <= time_expire
condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire)
# The conditions list is empty, so we need to append a new condition
tx_timeout['transaction']['conditions'].append({
'condition': {
'details': json.loads(condition_timeout.serialize_json()),
'uri': condition_timeout.condition.serialize_uri()
},
'cid': 0,
'new_owners': None
})
# conditions have been updated, so hash needs updating
tx_timeout['id'] = util.get_hash_data(tx_timeout)
# sign transaction
tx_timeout_signed = b.sign_transaction(tx_timeout, b.me_private)
b.write_transaction(tx_timeout_signed)
print(json.dumps(tx_timeout, sort_keys=True, indent=4, separators=(',', ':')))
sleep(8)
# Retrieve the transaction id of tx_timeout
tx_timeout_id = {'txid': tx_timeout['id'], 'cid': 0}
# Create a template to transfer the tx_timeout
tx_timeout_transfer = b.create_transaction(None, testuser1_pub, tx_timeout_id, 'TRANSFER')
# Parse the threshold cryptocondition
timeout_fulfillment = cc.Fulfillment.from_json(
tx_timeout['transaction']['conditions'][0]['condition']['details'])
tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_fulfillment.serialize_uri()
# no need to sign transaction, like with hashlocks
for i in range(time_sleep - 4):
tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout))
sleep(1)
"""
Escrow Conditions
"""
# retrieve the last transaction of testuser2
tx_retrieved_id = b.get_owned_ids(testuser2_pub).pop()
# Create escrow template with the execute and abort address
tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub], tx_retrieved_id, 'TRANSFER')
# Set expiry time (12 secs from now)
time_sleep = 12
time_expire = str(float(util.timestamp()) + time_sleep)
# Create escrow and timeout condition
condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate
condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) # only valid if now() <= time_expire
# Create execute branch
condition_execute = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate
condition_execute.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser1_pub)) # execute address
condition_execute.add_subfulfillment(condition_timeout) # federation checks on expiry
condition_escrow.add_subfulfillment(condition_execute)
# Create abort branch
condition_abort = cc.ThresholdSha256Fulfillment(threshold=2) # AND gate
condition_abort.add_subfulfillment(cc.Ed25519Fulfillment(public_key=testuser2_pub)) # abort address
condition_abort.add_subfulfillment(condition_timeout, weight=-1) # the negative weight inverts the condition
condition_escrow.add_subfulfillment(condition_abort)
# Update the condition in the newly created transaction
tx_escrow['transaction']['conditions'][0]['condition'] = {
'details': json.loads(condition_escrow.serialize_json()),
'uri': condition_escrow.condition.serialize_uri()
}
# conditions have been updated, so hash needs updating
tx_escrow['id'] = util.get_hash_data(tx_escrow)
# sign transaction
tx_escrow_signed = b.sign_transaction(tx_escrow, testuser2_priv)
# some checks
assert b.validate_transaction(tx_escrow_signed) == tx_escrow_signed
assert b.is_valid_transaction(tx_escrow_signed) == tx_escrow_signed
print(json.dumps(tx_escrow_signed, sort_keys=True, indent=4, separators=(',', ':')))
b.write_transaction(tx_escrow_signed)
sleep(8)
# Retrieve the last transaction of thresholduser1_pub
tx_escrow_id = {'txid': tx_escrow_signed['id'], 'cid': 0}
# Create a base template for output transaction
tx_escrow_execute = b.create_transaction([testuser2_pub, testuser1_pub], testuser1_pub, tx_escrow_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
tx_escrow['transaction']['conditions'][0]['condition']['details'])
subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0]
subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0]
subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body']
# Get the fulfillment message to sign
tx_escrow_execute_fulfillment_message = \
util.get_fulfillment_message(tx_escrow_execute,
tx_escrow_execute['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# fulfill execute branch
fulfillment_execute = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_testuser1.sign(tx_escrow_execute_fulfillment_message, crypto.SigningKey(testuser1_priv))
fulfillment_execute.add_subfulfillment(subfulfillment_testuser1)
fulfillment_execute.add_subfulfillment(subfulfillment_timeout)
escrow_fulfillment.add_subfulfillment(fulfillment_execute)
# do not fulfill abort branch
condition_abort = cc.ThresholdSha256Fulfillment(threshold=2)
condition_abort.add_subfulfillment(subfulfillment_testuser2)
condition_abort.add_subfulfillment(subfulfillment_timeout, weight=-1)
escrow_fulfillment.add_subcondition(condition_abort.condition)
# create fulfillment and append to transaction
tx_escrow_execute['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
# Time has expired, hence the abort branch can redeem
tx_escrow_abort = b.create_transaction([testuser2_pub, testuser1_pub], testuser2_pub, tx_escrow_id, 'TRANSFER')
# Parse the threshold cryptocondition
escrow_fulfillment = cc.Fulfillment.from_json(
tx_escrow['transaction']['conditions'][0]['condition']['details'])
subfulfillment_testuser1 = escrow_fulfillment.get_subcondition_from_vk(testuser1_pub)[0]
subfulfillment_testuser2 = escrow_fulfillment.get_subcondition_from_vk(testuser2_pub)[0]
subfulfillment_timeout = escrow_fulfillment.subconditions[0]['body'].subconditions[1]['body']
tx_escrow_abort_fulfillment_message = \
util.get_fulfillment_message(tx_escrow_abort,
tx_escrow_abort['transaction']['fulfillments'][0],
serialized=True)
escrow_fulfillment.subconditions = []
# Do not fulfill execute branch
condition_execute = cc.ThresholdSha256Fulfillment(threshold=2)
condition_execute.add_subfulfillment(subfulfillment_testuser1)
condition_execute.add_subfulfillment(subfulfillment_timeout)
escrow_fulfillment.add_subcondition(condition_execute.condition)
# Fulfill abort branch
fulfillment_abort = cc.ThresholdSha256Fulfillment(threshold=2)
subfulfillment_testuser2.sign(tx_escrow_abort_fulfillment_message, crypto.SigningKey(testuser2_priv))
fulfillment_abort.add_subfulfillment(subfulfillment_testuser2)
fulfillment_abort.add_subfulfillment(subfulfillment_timeout, weight=-1)
escrow_fulfillment.add_subfulfillment(fulfillment_abort)
tx_escrow_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri()
for i in range(time_sleep - 4):
valid_execute = b.is_valid_transaction(tx_escrow_execute) == tx_escrow_execute
valid_abort = b.is_valid_transaction(tx_escrow_abort) == tx_escrow_abort
seconds_to_timeout = int(float(time_expire) - float(util.timestamp()))
print('tx_execute valid: {} - tx_abort valid {} ({}s to timeout)'.format(valid_execute, valid_abort, seconds_to_timeout))
sleep(1)