threshold fulfillment

tests
This commit is contained in:
diminator 2016-03-18 18:46:22 +01:00
parent 46cd3323e8
commit 821ca9f8e6
7 changed files with 498 additions and 29 deletions

View File

@ -51,6 +51,8 @@ class BitmaskRegistry:
}) })
from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment
from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment
BitmaskRegistry.register_type(ThresholdSha256Fulfillment)
BitmaskRegistry.register_type(Ed25519Sha256Fulfillment) BitmaskRegistry.register_type(Ed25519Sha256Fulfillment)

View File

@ -4,7 +4,7 @@ from abc import ABCMeta
from six import string_types from six import string_types
from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding from bigchaindb.crypto.iostream import base64_add_padding, base64_remove_padding, Writer, Reader
CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$' CONDITION_REGEX = r'^cc:1:[1-9a-f][0-9a-f]{0,2}:[a-zA-Z0-9_-]{43}:[1-9][0-9]{0,50}$'
@ -47,6 +47,27 @@ class Condition(metaclass=ABCMeta):
return condition return condition
@staticmethod
def from_binary(reader):
"""
* Create a Condition object from a binary blob.
*
* This method will parse a stream of binary data and construct a
* corresponding Condition object.
*
Args:
reader (Reader): Binary stream implementing the Reader interface
Returns:
Condition: Resulting object
"""
reader = Reader.from_source(reader)
# Instantiate condition
condition = Condition()
condition.parse_binary(reader)
return condition
@property @property
def bitmask(self): def bitmask(self):
""" """
@ -152,12 +173,53 @@ class Condition(metaclass=ABCMeta):
Turns the condition into a URI containing only URL-safe characters. This Turns the condition into a URI containing only URL-safe characters. This
format is convenient for passing around conditions in URLs, JSON and other text-based formats. format is convenient for passing around conditions in URLs, JSON and other text-based formats.
"cc:" BASE10(VERSION) ":" BASE16(TYPE_BITMASK) ":" BASE64URL(HASH) ":" BASE10(MAX_FULFILLMENT_LENGTH)
Returns: Returns:
string: Condition as a URI string: Condition as a URI
""" """
return 'cc:1:{}:{}:{}'.format(self.bitmask, return 'cc:1:{:x}:{}:{}'.format(self.bitmask,
base64_remove_padding( base64_remove_padding(
base64.urlsafe_b64encode(self.hash) base64.urlsafe_b64encode(self.hash)
).decode('utf-8'), ).decode('utf-8'),
self.max_fulfillment_length) self.max_fulfillment_length)
def serialize_binary(self):
"""
Serialize condition to a buffer.
Encodes the condition as a string of bytes. This is used internally for
encoding subconditions, but can also be used to passing around conditions
in a binary protocol for instance.
CONDITION =
VARUINT TYPE_BITMASK
VARBYTES HASH
VARUINT MAX_FULFILLMENT_LENGTH
Return:
Serialized condition
"""
writer = Writer()
writer.write_var_uint(self.bitmask)
writer.write_var_bytes(self.hash)
writer.write_var_uint(self.max_fulfillment_length)
return b''.join(writer.components)
def parse_binary(self, reader):
"""
* Parse any condition in binary format.
*
* Will populate the condition object with data from the provided binary
* stream.
*
Args:
reader (Reader): Binary stream containing the condition.
"""
self.bitmask = reader.read_var_uint()
# TODO: Ensure bitmask is supported?
self.hash = reader.read_var_bytes()
self.max_fulfillment_length = reader.read_var_uint()

View File

@ -54,6 +54,30 @@ class Fulfillment(metaclass=ABCMeta):
return fulfillment return fulfillment
@staticmethod
def from_binary(reader):
"""
Create a Fulfillment object from a binary blob.
This method will parse a stream of binary data and construct a
corresponding Fulfillment object.
Args:
reader (Reader): Binary stream implementing the Reader interface
Returns:
Fulfillment: Resulting object
"""
reader = Reader.from_source(reader)
from bigchaindb.crypto.bitmark_registry import BitmaskRegistry
cls = BitmaskRegistry.get_class_from_typebit(reader.read_var_uint())
fulfillment = cls()
fulfillment.parse_payload(reader)
return fulfillment
@property @property
def bitmask(self): def bitmask(self):
""" """
@ -99,16 +123,38 @@ class Fulfillment(metaclass=ABCMeta):
format is convenient for passing around fulfillments in URLs, JSON and format is convenient for passing around fulfillments in URLs, JSON and
other text-based formats. other text-based formats.
"cf:" BASE10(VERSION) ":" BASE16(TYPE_BIT) ":" BASE64URL(FULFILLMENT_PAYLOAD)
Return: Return:
string: Fulfillment as a URI string: Fulfillment as a URI
""" """
return 'cf:1:{}:{}'.format(self.bitmask, return 'cf:1:{:x}:{}'.format(self._bitmask,
base64_remove_padding( base64_remove_padding(
base64.urlsafe_b64encode( base64.urlsafe_b64encode(
b''.join(self.serialize_payload().components) b''.join(self.serialize_payload().components)
) )
).decode('utf-8')) ).decode('utf-8'))
def serialize_binary(self):
"""
Serialize fulfillment to a buffer.
Encodes the fulfillment as a string of bytes. This is used internally for
encoding subfulfillments, but can also be used to passing around
fulfillments in a binary protocol for instance.
FULFILLMENT =
VARUINT TYPE_BIT
FULFILLMENT_PAYLOAD
Return:
Serialized fulfillment
"""
writer = Writer()
writer.write_var_uint(self.bitmask)
self.write_payload(writer)
return b''.join(writer.components)
def serialize_payload(self): def serialize_payload(self):
""" """
Return the fulfillment payload as a buffer. Return the fulfillment payload as a buffer.

View File

@ -8,11 +8,13 @@ from bigchaindb.crypto.iostream import Predictor
class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): class Ed25519Sha256Fulfillment(BaseSha256Fulfillment):
_bitmask = 0x08 _bitmask = 0x08
_message_prefix = None
_max_dynamic_message_length = None def __init__(self):
_public_key = None self._message_prefix = None
_message = None self._max_dynamic_message_length = None
_signature = None self._public_key = None
self._message = None
self._signature = None
@property @property
def message_prefix(self): def message_prefix(self):
@ -155,7 +157,6 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment):
self.max_dynamic_message_length = reader.read_var_uint() self.max_dynamic_message_length = reader.read_var_uint()
self.message = reader.read_var_bytes() self.message = reader.read_var_bytes()
self.signature = reader.read_var_bytes() self.signature = reader.read_var_bytes()
print(self.signature)
def write_payload(self, writer): def write_payload(self, writer):
""" """

View File

@ -0,0 +1,231 @@
from bigchaindb.crypto.condition import Condition
from bigchaindb.crypto.fulfillment import Fulfillment
from bigchaindb.crypto.fulfillments.base_sha256 import BaseSha256Fulfillment
from bigchaindb.crypto.iostream import Predictor, Reader, Writer
class ThresholdSha256Fulfillment(BaseSha256Fulfillment):
_bitmask = 0x04
def __init__(self):
self._threshold = None
self.subconditions = []
self.subfulfillments = []
def add_subcondition(self, subcondition):
"""
Add a subcondition (unfulfilled).
This can be used to generate a new threshold condition from a set of
subconditions or to provide a non-fulfilled subcondition when creating a threshold fulfillment.
Args:
subcondition (Condition): Condition to add
"""
if not isinstance(subcondition, Condition):
raise TypeError('Subconditions must be objects of type Condition')
self.subconditions.append(subcondition)
def add_subfulfillment(self, subfulfillment):
"""
Add a fulfilled subcondition.
When constructing a threshold fulfillment, this method allows you to
provide a fulfillment for one of the subconditions.
Note that you do **not** have to add the subcondition if you're adding the
fulfillment. The condition can be calculated from the fulfillment and will
be added automatically.
Args:
subfulfillment (Fulfillment): Fulfillment to add
"""
if not isinstance(subfulfillment, Fulfillment):
raise TypeError('Subfulfillments must be objects of type Fulfillment')
self.subfulfillments.append(subfulfillment)
def get_all_subconditions(self):
"""
Returns all subconditions including fulfilled ones.
This method returns the subconditions plus all subfulfillments, converted to conditions.
@return {Condition[]} Set of subconditions
"""
return self.subconditions + [f.condition for f in self.subfulfillments]
@property
def threshold(self):
return self._threshold
@threshold.setter
def threshold(self, value):
"""
Set the threshold.
Determines the weighted threshold that is used to consider this condition
fulfilled. If the added weight of all valid subfulfillments is greater or
equal to this number, the threshold condition is considered to be fulfilled.
Args:
value (int): Integer threshold
"""
self._threshold = value
@property
def bitmask(self):
"""
Get full bitmask.
This is a type of condition that can contain subconditions. A complete
bitmask must contain the set of types that must be supported in order to
validate this fulfillment. Therefore, we need to calculate the bitwise OR
of this condition's TYPE_BIT and all subcondition's and subfulfillment's bitmasks.
@return {Number} Complete bitmask for this fulfillment.
"""
bitmask = self._bitmask
for cond in self.subconditions:
bitmask |= cond.bitmask
for f in self.subfulfillments:
bitmask |= f.bitmask
return bitmask
def write_hash_payload(self, hasher):
"""
Produce the contents of the condition hash.
This function is called internally by the `getCondition` method.
Args:
hasher (Hasher): Hash generator
"""
if not (len(self.subconditions) or len(self.subfulfillments)):
raise ValueError('Requires subconditions')
subconditions = [c.serialize_binary() for c in self.get_all_subconditions()]
subconditions.sort(key=len)
hasher.write_var_uint(ThresholdSha256Fulfillment()._bitmask)
hasher.write_var_uint(self.threshold)
hasher.write_var_uint(len(subconditions))
for cond in subconditions:
hasher.write(cond)
return hasher
def calculate_max_fulfillment_length(self):
"""
Calculates the longest possible fulfillment length.
In a threshold condition, the maximum length of the fulfillment depends on
the maximum lengths of the fulfillments of the subconditions. However,
usually not all subconditions must be fulfilled to meet the threshold. This
means we only need to consider the worst case where the largest number of
largest fulfillments are provided and the smaller fulfillments are not.
The algorithm to calculate the worst case fulfillment size is not trivial,
however, it does not need to provide the exact worst-case fulfillment
length, only an upper bound for it.
@return {Number} Maximum length of the fulfillment payload
"""
# TODO: Currently wrong
predictor = Predictor()
# Calculate length of longest fulfillments
max_fulfillments_length = [c.max_fulfillment_length for c in self.get_all_subconditions()]
max_fulfillments_length.sort()
worst_case_fulfillments_length = sum(max_fulfillments_length[-self.threshold:])
predictor.write_var_uint(2)
predictor.skip(worst_case_fulfillments_length)
return predictor.size
def parse_payload(self, reader):
"""
Parse a fulfillment payload.
Read a fulfillment payload from a Reader and populate this object with that fulfillment.
Args:
reader (Reader): Source to read the fulfillment payload from.
"""
if not isinstance(reader, Reader):
raise TypeError('reader must be a Reader instance')
self.threshold = reader.read_var_uint()
fulfillment_count = reader.read_var_uint()
for i in range(fulfillment_count):
# TODO: Read weights
# const weight = 1
reader.skip_var_uint()
self.add_subfulfillment(Fulfillment.from_binary(reader))
condition_count = reader.read_var_uint()
for i in range(condition_count):
# TODO: Read weights
# const weight = 1
reader.skip_var_uint()
self.add_subcondition(Condition.from_binary(reader))
def write_payload(self, writer):
"""
Generate the fulfillment payload.
This writes the fulfillment payload to a Writer.
Args:
writer (Writer): Subject for writing the fulfillment payload.
"""
if not isinstance(writer, Writer):
raise TypeError('writer must be a Writer instance')
conditions = [c.serialize_binary() for c in self.subconditions]
# Get as many fulfillments as possible
fulfillments = [{'fulfillment': f, 'binary': f.serialize_binary()} for f in self.subfulfillments]
# Prefer shorter fulfillments
fulfillments.sort(key=lambda f: len(f['binary']))
if len(fulfillments) < self.threshold:
raise ValueError('Not enough subfulfillments')
while len(fulfillments) > self.threshold:
# TODO: only for valid fulfillments?
conditions.append(fulfillments.pop()['fulfillment'].condition.serialize_binary())
writer.write_var_uint(self.threshold)
writer.write_var_uint(len(fulfillments))
for fulfillment in fulfillments:
# TODO: Support custom weights
writer.write_var_uint(1)
writer.write(fulfillment['binary'])
writer.write_var_uint(len(conditions))
for condition in conditions:
# TODO: Support custom weights
writer.write_var_uint(1)
writer.write(condition)
return writer
def validate(self):
"""
Check whether this fulfillment meets all validation criteria.
This will validate the subfulfillments and verify that there are enough
subfulfillments to meet the threshold.
@return {Boolean} Whether this fulfillment is valid.
"""
validations = [f.validate() for f in self.subfulfillments]
return len([v for v in validations]) >= self.threshold

View File

@ -96,7 +96,9 @@ class Writer:
class Hasher(Writer): class Hasher(Writer):
hash = None def __init__(self):
self.hash = None
super().__init__()
def __init__(self, algorithm): def __init__(self, algorithm):
if algorithm == 'sha256': if algorithm == 'sha256':
@ -149,7 +151,9 @@ class Hasher(Writer):
class Predictor: class Predictor:
size = 0
def __init__(self):
self.size = 0
def write_var_uint(self, val): def write_var_uint(self, val):
""" """

View File

@ -1,12 +1,26 @@
import binascii import binascii
from math import floor, ceil
import pytest
from bigchaindb.crypto.condition import Condition from bigchaindb.crypto.condition import Condition
from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey
from bigchaindb.crypto.fulfillment import Fulfillment from bigchaindb.crypto.fulfillment import Fulfillment
from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment
from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment
class TestBigchainILPFulfillmentEd25519Sha256: class TestBigchainILPSha256Condition:
CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1'
def test_deserialize_condition(self):
example_condition = self.CONDITION_SHA256_ILP
condition = Condition.from_uri(example_condition)
assert condition.serialize_uri() == self.CONDITION_SHA256_ILP
class TestBigchainILPEd25519Sha256Fulfillment:
PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf' PUBLIC_HEX_ILP = b'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf'
PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' PUBLIC_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8'
PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU'
@ -54,7 +68,6 @@ class TestBigchainILPFulfillmentEd25519Sha256:
assert fulfillment.validate() assert fulfillment.validate()
def test_deserialize_condition(self): def test_deserialize_condition(self):
deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP) deserialized_condition = Condition.from_uri(self.CONDITION_ED25519_ILP)
assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP assert deserialized_condition.serialize_uri() == self.CONDITION_ED25519_ILP
@ -85,7 +98,7 @@ class TestBigchainILPFulfillmentEd25519Sha256:
assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP assert fulfillment.public_key.public_key.to_ascii(encoding='hex') == self.PUBLIC_HEX_ILP
assert fulfillment.validate() assert fulfillment.validate()
def test_serializer_deserialize_fulfillment(self): def test_serialize_deserialize_fulfillment(self):
sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) sk = ED25519PrivateKey(self.PRIVATE_B58_ILP)
vk = ED25519PublicKey(self.PUBLIC_B58_ILP) vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
@ -105,11 +118,121 @@ class TestBigchainILPFulfillmentEd25519Sha256:
assert deserialized_fulfillment.validate() assert deserialized_fulfillment.validate()
class TestBigchainILPConditionSha256: class TestBigchainILPThresholdSha256Fulfillment:
PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU'
PRIVATE_B58_ILP = '9qLvREC54mhKYivr88VpckyVWdAFmifJpGjbvV5AiTRs'
CONDITION_SHA256_ILP = 'cc:1:1:47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU:1' CONDITION_ED25519_ILP = 'cc:1:8:qQINW2um59C4DB9JSVXH1igqAmaYGGqryllHUgCpfPU:113'
FULFILLMENT_ED25519_ILP = \
'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_DEhlbGxvIHdvcmxkISAVIENvbmRpdGlvbnMgYXJlIGhlcmUhQENbql531' \
'PbCJlRUvKjP56k0XKJMOrIGo2F66ueuTtRnYrJB2t2ZttdfXM4gzD_87eH1nZTpu4rTkAx81hSdpwI'
def test_deserialize_condition(self): CONDITION_ED25519_ILP_2 = 'cc:1:8:_WzTrHvFnv4I-H0cAKWZ6Q3g3Y0Du3aW01nIsaAsio8:116'
example_condition = self.CONDITION_SHA256_ILP FULFILLMENT_ED25519_ILP_2 = \
condition = Condition.from_uri(example_condition) 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_D0hlbGxvIHVuaXZlcnNlISAbIENvbmRpdGlvbnMgYXJlIGV2ZXJ5d2hlc' \
assert condition.serialize_uri() == self.CONDITION_SHA256_ILP 'mUhQNmD2Cvk7e3EFOo-arA2TKYTP-474Z4okhbYmKij6XxObIbRsDScjXILAJ6mV5hP7Xyqkg5fcSsZbfRYypzlsAM'
HASH_ED25519_HEX_ILP = b'a9020d5b6ba6e7d0b80c1f494955c7d6282a026698186aabca59475200a97cf5'
CONDITION_THRESHOLD_ED25519_ILP_2 = 'cc:1:c:IZgoTeE1Weg6tfGMLWGe2JmS-waBN-CUrlbhtI9GBcQ:230'
FULFILLMENT_THRESHOLD_ED25519_ILP_2 = \
'cf:1:4:AgIBCCDsFyuTrV5WO_STLHDhJFA0w1Rn7y79TWTr-BloNGfivwxIZWxsbyB3b3JsZCEgFSBDb25kaXRpb25zIGFyZSBoZXJlIUBDW' \
'6ped9T2wiZUVLyoz-epNFyiTDqyBqNheurnrk7UZ2KyQdrdmbbXX1zOIMw__O3h9Z2U6buK05AMfNYUnacCAQgg7Bcrk61eVjv0kyxw4SRQN' \
'MNUZ-8u_U1k6_gZaDRn4r8MSGVsbG8gd29ybGQhIBUgQ29uZGl0aW9ucyBhcmUgaGVyZSFAQ1uqXnfU9sImVFS8qM_nqTRcokw6sgajYXrq5' \
'65O1GdiskHa3Zm2119cziDMP_zt4fWdlOm7itOQDHzWFJ2nAgEBCCD9bNOse8We_gj4fRwApZnpDeDdjQO7dpbTWcixoCyKj3Q'
def create_fulfillment_ed25519sha256(self):
sk = ED25519PrivateKey(self.PRIVATE_B58_ILP)
vk = ED25519PublicKey(self.PUBLIC_B58_ILP)
fulfillment = Ed25519Sha256Fulfillment()
fulfillment.public_key = vk
fulfillment.message_prefix = 'Hello world!'
fulfillment.max_dynamic_message_length = 32 # defaults to 0
fulfillment.message = ' Conditions are here!'
fulfillment.sign(sk)
return fulfillment
def test_serialize_condition_and_validate_fulfillment(self):
ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP)
ilp_fulfillment_2 = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP_2)
assert ilp_fulfillment.validate() == True
assert ilp_fulfillment_2.validate() == True
THRESHOLD = 2
# Create a threshold condition
fulfillment = ThresholdSha256Fulfillment()
fulfillment.add_subfulfillment(ilp_fulfillment_2)
fulfillment.add_subfulfillment(ilp_fulfillment)
fulfillment.add_subfulfillment(ilp_fulfillment)
fulfillment.threshold = THRESHOLD # defaults to subconditions.length
assert fulfillment.condition.serialize_uri() == self.CONDITION_THRESHOLD_ED25519_ILP_2
# Note: If there are more than enough fulfilled subconditions, shorter
# fulfillments will be chosen over longer ones.
# thresholdFulfillmentUri.length === 65
assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2
assert fulfillment.validate()
def test_deserialize_fulfillment(self):
NUM_FULFILLMENTS = 3
THRESHOLD = 2
fulfillment = Fulfillment.from_uri(self.FULFILLMENT_THRESHOLD_ED25519_ILP_2)
assert fulfillment.threshold == THRESHOLD
assert len(fulfillment.subfulfillments) == THRESHOLD
assert len(fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS
assert fulfillment.serialize_uri() == self.FULFILLMENT_THRESHOLD_ED25519_ILP_2
assert fulfillment.validate()
def test_serialize_deserialize_fulfillment(self):
ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP)
NUM_FULFILLMENTS = 100
THRESHOLD = ceil(NUM_FULFILLMENTS * 2 / 3)
# Create a threshold condition
fulfillment = ThresholdSha256Fulfillment()
for i in range(NUM_FULFILLMENTS):
fulfillment.add_subfulfillment(ilp_fulfillment)
fulfillment.threshold = THRESHOLD
fulfillment_uri = fulfillment.serialize_uri()
assert fulfillment.validate()
deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri)
assert deserialized_fulfillment.threshold == THRESHOLD
assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD
assert len(deserialized_fulfillment.get_all_subconditions()) == NUM_FULFILLMENTS
assert deserialized_fulfillment.serialize_uri() == fulfillment_uri
assert deserialized_fulfillment.validate()
def test_fulfillment_didnt_reach_threshold(self):
ilp_fulfillment = Fulfillment.from_uri(self.FULFILLMENT_ED25519_ILP)
THRESHOLD = 10
# Create a threshold condition
fulfillment = ThresholdSha256Fulfillment()
fulfillment.threshold = THRESHOLD
for i in range(THRESHOLD - 1):
fulfillment.add_subfulfillment(ilp_fulfillment)
with pytest.raises(ValueError):
fulfillment.serialize_uri()
assert fulfillment.validate() is False
fulfillment.add_subfulfillment(ilp_fulfillment)
fulfillment_uri = fulfillment.serialize_uri()
assert fulfillment.validate()
deserialized_fulfillment = Fulfillment.from_uri(fulfillment_uri)
assert deserialized_fulfillment.threshold == THRESHOLD
assert len(deserialized_fulfillment.subfulfillments) == THRESHOLD
assert len(deserialized_fulfillment.get_all_subconditions()) == THRESHOLD
assert deserialized_fulfillment.serialize_uri() == fulfillment_uri
assert deserialized_fulfillment.validate()