mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Merge remote-tracking branch 'origin/master' into update-changelog
This commit is contained in:
commit
d2d10f7340
@ -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)
|
||||||
@ -496,7 +572,7 @@ class Bigchain(object):
|
|||||||
|
|
||||||
def block_election_status(self, block):
|
def block_election_status(self, block):
|
||||||
"""Tally the votes on a block, and return the status: valid, invalid, or undecided."""
|
"""Tally the votes on a block, and return the status: valid, invalid, or undecided."""
|
||||||
|
|
||||||
n_voters = len(block['block']['voters'])
|
n_voters = len(block['block']['voters'])
|
||||||
vote_cast = [vote['vote']['is_block_valid'] for vote in block['votes']]
|
vote_cast = [vote['vote']['is_block_valid'] for vote in block['votes']]
|
||||||
vote_validity = [self.consensus.verify_vote_signature(block, vote) for vote in block['votes']]
|
vote_validity = [self.consensus.verify_vote_signature(block, vote) for vote in block['votes']]
|
||||||
|
@ -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']`
|
||||||
|
|
||||||
|
BIN
docs/source/_static/cc_escrow_execute_abort.png
Normal file
BIN
docs/source/_static/cc_escrow_execute_abort.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
docs/source/_static/tx_escrow_execute_abort.png
Normal file
BIN
docs/source/_static/tx_escrow_execute_abort.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
@ -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.
|
||||||
|
@ -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`.
|
||||||
|
|
||||||

|
<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.
|
||||||
|
|
||||||

|
<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
|
||||||
@ -885,4 +887,416 @@ 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 (⚪) 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.
|
||||||
|
2
setup.py
2
setup.py
@ -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',
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user