diff --git a/bigchaindb/crypto/bitmark_registry.py b/bigchaindb/crypto/bitmark_registry.py index 5e3b071f..d5d06abe 100644 --- a/bigchaindb/crypto/bitmark_registry.py +++ b/bigchaindb/crypto/bitmark_registry.py @@ -51,6 +51,8 @@ class BitmaskRegistry: }) +from bigchaindb.crypto.fulfillments.threshold_sha256 import ThresholdSha256Fulfillment from bigchaindb.crypto.fulfillments.ed25519_sha256 import Ed25519Sha256Fulfillment +BitmaskRegistry.register_type(ThresholdSha256Fulfillment) BitmaskRegistry.register_type(Ed25519Sha256Fulfillment) diff --git a/bigchaindb/crypto/condition.py b/bigchaindb/crypto/condition.py index 201c2eab..d218926c 100644 --- a/bigchaindb/crypto/condition.py +++ b/bigchaindb/crypto/condition.py @@ -4,7 +4,7 @@ from abc import ABCMeta 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}$' @@ -47,6 +47,27 @@ class Condition(metaclass=ABCMeta): 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 def bitmask(self): """ @@ -152,12 +173,53 @@ class Condition(metaclass=ABCMeta): 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. + "cc:" BASE10(VERSION) ":" BASE16(TYPE_BITMASK) ":" BASE64URL(HASH) ":" BASE10(MAX_FULFILLMENT_LENGTH) + Returns: string: Condition as a URI """ - return 'cc:1:{}:{}:{}'.format(self.bitmask, - base64_remove_padding( - base64.urlsafe_b64encode(self.hash) - ).decode('utf-8'), - self.max_fulfillment_length) + return 'cc:1:{:x}:{}:{}'.format(self.bitmask, + base64_remove_padding( + base64.urlsafe_b64encode(self.hash) + ).decode('utf-8'), + 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() diff --git a/bigchaindb/crypto/fulfillment.py b/bigchaindb/crypto/fulfillment.py index fd32c19c..bed4dc45 100644 --- a/bigchaindb/crypto/fulfillment.py +++ b/bigchaindb/crypto/fulfillment.py @@ -54,6 +54,30 @@ class Fulfillment(metaclass=ABCMeta): 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 def bitmask(self): """ @@ -99,15 +123,37 @@ class Fulfillment(metaclass=ABCMeta): format is convenient for passing around fulfillments in URLs, JSON and other text-based formats. + "cf:" BASE10(VERSION) ":" BASE16(TYPE_BIT) ":" BASE64URL(FULFILLMENT_PAYLOAD) + Return: string: Fulfillment as a URI """ - return 'cf:1:{}:{}'.format(self.bitmask, - base64_remove_padding( - base64.urlsafe_b64encode( - b''.join(self.serialize_payload().components) - ) - ).decode('utf-8')) + return 'cf:1:{:x}:{}'.format(self._bitmask, + base64_remove_padding( + base64.urlsafe_b64encode( + b''.join(self.serialize_payload().components) + ) + ).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): """ diff --git a/bigchaindb/crypto/fulfillments/ed25519_sha256.py b/bigchaindb/crypto/fulfillments/ed25519_sha256.py index 388a27cf..321e41dc 100644 --- a/bigchaindb/crypto/fulfillments/ed25519_sha256.py +++ b/bigchaindb/crypto/fulfillments/ed25519_sha256.py @@ -8,11 +8,13 @@ from bigchaindb.crypto.iostream import Predictor class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): _bitmask = 0x08 - _message_prefix = None - _max_dynamic_message_length = None - _public_key = None - _message = None - _signature = None + + def __init__(self): + self._message_prefix = None + self._max_dynamic_message_length = None + self._public_key = None + self._message = None + self._signature = None @property def message_prefix(self): @@ -155,7 +157,6 @@ class Ed25519Sha256Fulfillment(BaseSha256Fulfillment): self.max_dynamic_message_length = reader.read_var_uint() self.message = reader.read_var_bytes() self.signature = reader.read_var_bytes() - print(self.signature) def write_payload(self, writer): """ diff --git a/bigchaindb/crypto/fulfillments/threshold_sha256.py b/bigchaindb/crypto/fulfillments/threshold_sha256.py new file mode 100644 index 00000000..f0edad89 --- /dev/null +++ b/bigchaindb/crypto/fulfillments/threshold_sha256.py @@ -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 diff --git a/bigchaindb/crypto/iostream.py b/bigchaindb/crypto/iostream.py index 2dd76a68..6ac51705 100644 --- a/bigchaindb/crypto/iostream.py +++ b/bigchaindb/crypto/iostream.py @@ -96,7 +96,9 @@ class Writer: class Hasher(Writer): - hash = None + def __init__(self): + self.hash = None + super().__init__() def __init__(self, algorithm): if algorithm == 'sha256': @@ -149,7 +151,9 @@ class Hasher(Writer): class Predictor: - size = 0 + + def __init__(self): + self.size = 0 def write_var_uint(self, val): """ diff --git a/tests/crypto/test_fulfillment.py b/tests/crypto/test_fulfillment.py index 33ae1150..cfa26b31 100644 --- a/tests/crypto/test_fulfillment.py +++ b/tests/crypto/test_fulfillment.py @@ -1,12 +1,26 @@ import binascii +from math import floor, ceil + +import pytest + from bigchaindb.crypto.condition import Condition from bigchaindb.crypto.ed25519 import ED25519PrivateKey, ED25519PublicKey from bigchaindb.crypto.fulfillment import Fulfillment 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_B64_ILP = b'7Bcrk61eVjv0kyxw4SRQNMNUZ+8u/U1k6/gZaDRn4r8' PUBLIC_B58_ILP = 'Gtbi6WQDB6wUePiZm8aYs5XZ5pUqx9jMMLvRVHPESTjU' @@ -54,7 +68,6 @@ class TestBigchainILPFulfillmentEd25519Sha256: assert fulfillment.validate() def test_deserialize_condition(self): - deserialized_condition = Condition.from_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.validate() - def test_serializer_deserialize_fulfillment(self): + def test_serialize_deserialize_fulfillment(self): sk = ED25519PrivateKey(self.PRIVATE_B58_ILP) vk = ED25519PublicKey(self.PUBLIC_B58_ILP) @@ -105,11 +118,121 @@ class TestBigchainILPFulfillmentEd25519Sha256: 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): - example_condition = self.CONDITION_SHA256_ILP - condition = Condition.from_uri(example_condition) - assert condition.serialize_uri() == self.CONDITION_SHA256_ILP + CONDITION_ED25519_ILP_2 = 'cc:1:8:_WzTrHvFnv4I-H0cAKWZ6Q3g3Y0Du3aW01nIsaAsio8:116' + FULFILLMENT_ED25519_ILP_2 = \ + 'cf:1:8:IOwXK5OtXlY79JMscOEkUDTDVGfvLv1NZOv4GWg0Z-K_D0hlbGxvIHVuaXZlcnNlISAbIENvbmRpdGlvbnMgYXJlIGV2ZXJ5d2hlc' \ + '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()