Remove condition details signature, rename subfulfillments to subconditions (#1589)

* implement output.condition.details marshalling and remove signature field

* rename outputs[].condition.details.subfulfillments to subconditions

* simpler threshold depth overlow handling

* pass public_key as kwarg

* change ccv1 condition uri in docs

* import base58 at top in test_transaction
This commit is contained in:
libscott 2017-06-30 09:44:22 +02:00 committed by GitHub
parent b1ad6045b6
commit 5e9b7f4ffe
8 changed files with 195 additions and 94 deletions

View File

@ -106,3 +106,7 @@ class SybilError(ValidationError):
class DuplicateTransaction(ValidationError):
"""Raised if a duplicated transaction is found"""
class ThresholdTooDeep(ValidationError):
"""Raised if threshold condition is too deep"""

View File

@ -150,8 +150,7 @@ definitions:
- uri
properties:
details:
type: object
additionalProperties: true
"$ref": "#/definitions/condition_details"
uri:
type: string
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})?(.+)$"
@ -174,28 +173,14 @@ definitions:
description: |
List of public keys of the previous owners of the asset.
fulfillment:
description: |
Fulfillment of an `Output.condition`_, or, put a different way, a payload
that satisfies the condition of a previous output to prove that the
creator(s) of this transaction have control over the listed asset.
anyOf:
- type: object
additionalProperties: false
properties:
bitmask:
type: integer
public_key:
type: string
type:
type: string
signature:
anyOf:
- type: string
- type: 'null'
type_id:
type: integer
description: |
Fulfillment of an `Output.condition`_, or, put a different way, a payload
that satisfies the condition of a previous output to prove that the
creator(s) of this transaction have control over the listed asset.
- type: string
pattern: "^[a-zA-Z0-9_-]*$"
- "$ref": "#/definitions/condition_details"
fulfills:
anyOf:
- type: 'object'
@ -224,3 +209,37 @@ definitions:
additionalProperties: true
minProperties: 1
- type: 'null'
condition_details:
description: |
Details needed to reconstruct the condition associated with an output.
Currently, BigchainDB only supports ed25519 and threshold condition types.
anyOf:
- type: object
additionalProperties: false
required:
- type
- public_key
properties:
type:
type: string
pattern: "^ed25519-sha-256$"
public_key:
"$ref": "#/definitions/base58"
- type: object
additionalProperties: false
required:
- type
- threshold
- subconditions
properties:
type:
type: "string"
pattern: "^threshold-sha-256$"
threshold:
type: integer
minimum: 1
maximum: 100
subconditions:
type: array
items:
"$ref": "#/definitions/condition_details"

View File

@ -4,12 +4,13 @@ from functools import reduce
import base58
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256
from cryptoconditions.exceptions import (
ParsingError, ASN1DecodeError, ASN1EncodeError)
ParsingError, ASN1DecodeError, ASN1EncodeError, UnsupportedTypeError)
from bigchaindb.common.crypto import PrivateKey, hash_data
from bigchaindb.common.exceptions import (KeypairMismatchException,
InvalidHash, InvalidSignature,
AmountError, AssetIdMismatch)
AmountError, AssetIdMismatch,
ThresholdTooDeep)
from bigchaindb.common.utils import serialize
@ -66,15 +67,7 @@ class Input(object):
try:
fulfillment = self.fulfillment.serialize_uri()
except (TypeError, AttributeError, ASN1EncodeError):
# NOTE: When a non-signed transaction is casted to a dict,
# `self.inputs` value is lost, as in the node's
# transaction model that is saved to the database, does not
# account for its dictionary form but just for its signed uri
# form.
# Hence, when a non-signed fulfillment is to be cast to a
# dict, we just call its internal `to_dict` method here and
# its `from_dict` method in `Fulfillment.from_dict`.
fulfillment = self.fulfillment.to_dict()
fulfillment = _fulfillment_to_details(self.fulfillment)
try:
# NOTE: `self.fulfills` can be `None` and that's fine
@ -125,11 +118,63 @@ class Input(object):
except TypeError:
# NOTE: See comment about this special case in
# `Input.to_dict`
fulfillment = Fulfillment.from_dict(data['fulfillment'])
fulfillment = _fulfillment_from_details(data['fulfillment'])
fulfills = TransactionLink.from_dict(data['fulfills'])
return cls(fulfillment, data['owners_before'], fulfills)
def _fulfillment_to_details(fulfillment):
"""
Encode a fulfillment as a details dictionary
Args:
fulfillment: Crypto-conditions Fulfillment object
"""
if fulfillment.type_name == 'ed25519-sha-256':
return {
'type': 'ed25519-sha-256',
'public_key': base58.b58encode(fulfillment.public_key),
}
if fulfillment.type_name == 'threshold-sha-256':
subconditions = [
_fulfillment_to_details(cond['body'])
for cond in fulfillment.subconditions
]
return {
'type': 'threshold-sha-256',
'threshold': fulfillment.threshold,
'subconditions': subconditions,
}
raise UnsupportedTypeError(fulfillment.type_name)
def _fulfillment_from_details(data):
"""
Load a fulfillment for a signing spec dictionary
Args:
data: tx.output[].condition.details dictionary
"""
if data['type'] == 'ed25519-sha-256':
public_key = base58.b58decode(data['public_key'])
return Ed25519Sha256(public_key=public_key)
if data['type'] == 'threshold-sha-256':
try:
threshold = ThresholdSha256(data['threshold'])
for cond in data['subconditions']:
cond = _fulfillment_from_details(cond)
threshold.add_subfulfillment(cond)
return threshold
except RecursionError:
raise ThresholdTooDeep()
raise UnsupportedTypeError(data.get('type'))
class TransactionLink(object):
"""An object for unidirectional linking to a Transaction's Output.
@ -262,7 +307,7 @@ class Output(object):
# and fulfillment!
condition = {}
try:
condition['details'] = self.fulfillment.to_dict()
condition['details'] = _fulfillment_to_details(self.fulfillment)
except AttributeError:
pass
@ -389,7 +434,7 @@ class Output(object):
:class:`~bigchaindb.common.transaction.Output`
"""
try:
fulfillment = Fulfillment.from_dict(data['condition']['details'])
fulfillment = _fulfillment_from_details(data['condition']['details'])
except KeyError:
# NOTE: Hashlock condition case
fulfillment = data['condition']['uri']

View File

@ -96,8 +96,8 @@ def condition_details_has_owner(condition_details, owner):
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 'subconditions' in condition_details:
result = condition_details_has_owner(condition_details['subconditions'], owner)
if result:
return True

View File

@ -81,30 +81,20 @@ to spend the asset. For example:
{
"condition": {
"details": {
"bitmask": 41,
"subfulfillments": [
"type": "threshold-sha-256",
"threshold": 2,
"subconditions": [
{
"bitmask": 32,
"public_key": "<new owner 1 public key>",
"signature": null,
"type": "fulfillment",
"type_id": 4,
"weight": 1
"type": "ed25519-sha-256",
},
{
"bitmask": 32,
"public_key": "<new owner 2 public key>",
"signature": null,
"type": "fulfillment",
"type_id": 4,
"weight": 1
"type": "ed25519-sha-256",
}
],
"threshold": 2,
"type": "fulfillment",
"type_id": 2
},
"uri": "cc:2:29:ytNK3X6-bZsbF-nCGDTuopUIMi1HCyCkyPewm6oLI3o:206"},
"uri": "ni:///sha-256;PNYwdxaRaNw60N6LDFzOWO97b8tJeragczakL8PrAPc?fpt=ed25519-sha-256&cost=131072"},
"public_keys": [
"<owner 1 public key>",
"<owner 2 public key>"
@ -112,11 +102,10 @@ to spend the asset. For example:
}
- ``subfulfillments``: a list of fulfillments
- ``weight``: integer weight for each subfulfillment's contribution to the threshold
- ``threshold``: threshold to reach for the subfulfillments to reach a valid fulfillment
- ``subconditions``: a list of condition specs
- ``threshold``: threshold to reach for the subconditions 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.
The ``threshold`` can 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. If it is desired to give a different weight to a subcondition, it should be specified multiple times.
Inputs
------

View File

@ -52,8 +52,8 @@ def test_single_in_single_own_single_out_multiple_own_create(b, user_pk):
assert tx_signed.outputs[0].amount == 100
output = tx_signed.outputs[0].to_dict()
assert 'subfulfillments' in output['condition']['details']
assert len(output['condition']['details']['subfulfillments']) == 2
assert 'subconditions' in output['condition']['details']
assert len(output['condition']['details']['subconditions']) == 2
assert len(tx_signed.inputs) == 1
@ -76,8 +76,8 @@ def test_single_in_single_own_multiple_out_mix_own_create(b, user_pk):
assert tx_signed.outputs[1].amount == 50
output_cid1 = tx_signed.outputs[1].to_dict()
assert 'subfulfillments' in output_cid1['condition']['details']
assert len(output_cid1['condition']['details']['subfulfillments']) == 2
assert 'subconditions' in output_cid1['condition']['details']
assert len(output_cid1['condition']['details']['subconditions']) == 2
assert len(tx_signed.inputs) == 1
@ -89,6 +89,7 @@ def test_single_in_single_own_multiple_out_mix_own_create(b, user_pk):
def test_single_in_multiple_own_single_out_single_own_create(b, user_pk,
user_sk):
from bigchaindb.models import Transaction
from bigchaindb.common.transaction import _fulfillment_to_details
tx = Transaction.create([b.me, user_pk], [([user_pk], 100)])
tx_signed = tx.sign([b.me_private, user_sk])
@ -97,9 +98,9 @@ def test_single_in_multiple_own_single_out_single_own_create(b, user_pk,
assert tx_signed.outputs[0].amount == 100
assert len(tx_signed.inputs) == 1
ffill = tx_signed.inputs[0].fulfillment.to_dict()
assert 'subfulfillments' in ffill
assert len(ffill['subfulfillments']) == 2
ffill = _fulfillment_to_details(tx_signed.inputs[0].fulfillment)
assert 'subconditions' in ffill
assert len(ffill['subconditions']) == 2
# TRANSFER divisible asset
@ -207,8 +208,8 @@ def test_single_in_single_own_single_out_multiple_own_transfer(b, user_pk,
assert tx_transfer_signed.outputs[0].amount == 100
condition = tx_transfer_signed.outputs[0].to_dict()
assert 'subfulfillments' in condition['condition']['details']
assert len(condition['condition']['details']['subfulfillments']) == 2
assert 'subconditions' in condition['condition']['details']
assert len(condition['condition']['details']['subconditions']) == 2
assert len(tx_transfer_signed.inputs) == 1
@ -248,8 +249,8 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_pk,
assert tx_transfer_signed.outputs[1].amount == 50
output_cid1 = tx_transfer_signed.outputs[1].to_dict()
assert 'subfulfillments' in output_cid1['condition']['details']
assert len(output_cid1['condition']['details']['subfulfillments']) == 2
assert 'subconditions' in output_cid1['condition']['details']
assert len(output_cid1['condition']['details']['subconditions']) == 2
assert len(tx_transfer_signed.inputs) == 1
@ -264,6 +265,7 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_pk,
def test_single_in_multiple_own_single_out_single_own_transfer(b, user_pk,
user_sk):
from bigchaindb.models import Transaction
from bigchaindb.common.transaction import _fulfillment_to_details
# CREATE divisible asset
tx_create = Transaction.create([b.me], [([b.me, user_pk], 100)])
@ -286,9 +288,9 @@ def test_single_in_multiple_own_single_out_single_own_transfer(b, user_pk,
assert tx_transfer_signed.outputs[0].amount == 100
assert len(tx_transfer_signed.inputs) == 1
ffill = tx_transfer_signed.inputs[0].fulfillment.to_dict()
assert 'subfulfillments' in ffill
assert len(ffill['subfulfillments']) == 2
ffill = _fulfillment_to_details(tx_transfer_signed.inputs[0].fulfillment)
assert 'subconditions' in ffill
assert len(ffill['subconditions']) == 2
# TRANSFER divisible asset
@ -334,6 +336,7 @@ def test_multiple_in_single_own_single_out_single_own_transfer(b, user_pk,
def test_multiple_in_multiple_own_single_out_single_own_transfer(b, user_pk,
user_sk):
from bigchaindb.models import Transaction
from bigchaindb.common.transaction import _fulfillment_to_details
# CREATE divisible asset
tx_create = Transaction.create([b.me], [([user_pk, b.me], 50), ([user_pk, b.me], 50)])
@ -356,12 +359,12 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(b, user_pk,
assert tx_transfer_signed.outputs[0].amount == 100
assert len(tx_transfer_signed.inputs) == 2
ffill_fid0 = tx_transfer_signed.inputs[0].fulfillment.to_dict()
ffill_fid1 = tx_transfer_signed.inputs[1].fulfillment.to_dict()
assert 'subfulfillments' in ffill_fid0
assert 'subfulfillments' in ffill_fid1
assert len(ffill_fid0['subfulfillments']) == 2
assert len(ffill_fid1['subfulfillments']) == 2
ffill_fid0 = _fulfillment_to_details(tx_transfer_signed.inputs[0].fulfillment)
ffill_fid1 = _fulfillment_to_details(tx_transfer_signed.inputs[1].fulfillment)
assert 'subconditions' in ffill_fid0
assert 'subconditions' in ffill_fid1
assert len(ffill_fid0['subconditions']) == 2
assert len(ffill_fid1['subconditions']) == 2
# TRANSFER divisible asset
@ -375,6 +378,7 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(b, user_pk,
def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(b, user_pk,
user_sk):
from bigchaindb.models import Transaction
from bigchaindb.common.transaction import _fulfillment_to_details
# CREATE divisible asset
tx_create = Transaction.create([b.me], [([user_pk], 50), ([user_pk, b.me], 50)])
@ -397,11 +401,11 @@ def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(b, user_pk,
assert tx_transfer_signed.outputs[0].amount == 100
assert len(tx_transfer_signed.inputs) == 2
ffill_fid0 = tx_transfer_signed.inputs[0].fulfillment.to_dict()
ffill_fid1 = tx_transfer_signed.inputs[1].fulfillment.to_dict()
assert 'subfulfillments' not in ffill_fid0
assert 'subfulfillments' in ffill_fid1
assert len(ffill_fid1['subfulfillments']) == 2
ffill_fid0 = _fulfillment_to_details(tx_transfer_signed.inputs[0].fulfillment)
ffill_fid1 = _fulfillment_to_details(tx_transfer_signed.inputs[1].fulfillment)
assert 'subconditions' not in ffill_fid0
assert 'subconditions' in ffill_fid1
assert len(ffill_fid1['subconditions']) == 2
# TRANSFER divisible asset
@ -416,6 +420,7 @@ def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(b, user_pk,
def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(b, user_pk,
user_sk):
from bigchaindb.models import Transaction
from bigchaindb.common.transaction import _fulfillment_to_details
# CREATE divisible asset
tx_create = Transaction.create([b.me], [([user_pk], 50), ([user_pk, b.me], 50)])
@ -442,15 +447,15 @@ def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(b, user_pk,
cond_cid0 = tx_transfer_signed.outputs[0].to_dict()
cond_cid1 = tx_transfer_signed.outputs[1].to_dict()
assert 'subfulfillments' not in cond_cid0['condition']['details']
assert 'subfulfillments' in cond_cid1['condition']['details']
assert len(cond_cid1['condition']['details']['subfulfillments']) == 2
assert 'subconditions' not in cond_cid0['condition']['details']
assert 'subconditions' in cond_cid1['condition']['details']
assert len(cond_cid1['condition']['details']['subconditions']) == 2
ffill_fid0 = tx_transfer_signed.inputs[0].fulfillment.to_dict()
ffill_fid1 = tx_transfer_signed.inputs[1].fulfillment.to_dict()
assert 'subfulfillments' not in ffill_fid0
assert 'subfulfillments' in ffill_fid1
assert len(ffill_fid1['subfulfillments']) == 2
ffill_fid0 = _fulfillment_to_details(tx_transfer_signed.inputs[0].fulfillment)
ffill_fid1 = _fulfillment_to_details(tx_transfer_signed.inputs[1].fulfillment)
assert 'subconditions' not in ffill_fid0
assert 'subconditions' in ffill_fid1
assert len(ffill_fid1['subconditions']) == 2
# TRANSFER divisible asset

View File

@ -4,7 +4,7 @@ Tests for transaction validation are separate.
"""
from copy import deepcopy
from base58 import b58decode
from base58 import b58encode, b58decode
from pytest import raises
@ -82,7 +82,10 @@ def test_output_serialization(user_Ed25519, user_pub):
expected = {
'condition': {
'uri': user_Ed25519.condition_uri,
'details': user_Ed25519.to_dict(),
'details': {
'type': 'ed25519-sha-256',
'public_key': b58encode(user_Ed25519.public_key),
},
},
'public_keys': [user_pub],
'amount': '1',
@ -100,7 +103,10 @@ def test_output_deserialization(user_Ed25519, user_pub):
cond = {
'condition': {
'uri': user_Ed25519.condition_uri,
'details': user_Ed25519.to_dict()
'details': {
'type': 'ed25519-sha-256',
'public_key': b58encode(user_Ed25519.public_key),
},
},
'public_keys': [user_pub],
'amount': '1',

View File

@ -5,9 +5,11 @@ structural / schematic issues are caught when reading a transaction
"""
import pytest
from unittest.mock import MagicMock
from bigchaindb.common.exceptions import (AmountError, InvalidHash,
SchemaValidationError)
SchemaValidationError,
ThresholdTooDeep)
from bigchaindb.models import Transaction
@ -161,6 +163,37 @@ def test_high_amounts(create_tx):
validate(create_tx)
################################################################################
# Conditions
def test_handle_threshold_overflow():
from bigchaindb.common import transaction
cond = {
'type': 'ed25519-sha-256',
'public_key': 'a' * 43,
}
for i in range(1000):
cond = {
'type': 'threshold-sha-256',
'threshold': 1,
'subconditions': [cond],
}
with pytest.raises(ThresholdTooDeep):
transaction._fulfillment_from_details(cond)
def test_unsupported_condition_type():
from bigchaindb.common import transaction
from cryptoconditions.exceptions import UnsupportedTypeError
with pytest.raises(UnsupportedTypeError):
transaction._fulfillment_from_details({'type': 'a'})
with pytest.raises(UnsupportedTypeError):
transaction._fulfillment_to_details(MagicMock(type_name='a'))
################################################################################
# Version