| """! |
| Transaction related models to parse and construct transaction |
| payloads. |
| |
| Attributes: |
| UnspentOutput (namedtuple): Object holding the information |
| representing an unspent output. |
| |
| """ |
| from collections import namedtuple |
| from copy import deepcopy |
| from functools import reduce |
| |
| import base58 |
| from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256 |
| from cryptoconditions.exceptions import ( |
| ParsingError, |
| ASN1DecodeError, |
| ASN1EncodeError, |
| UnsupportedTypeError, |
| ) |
| from sha3 import sha3_256 |
| |
| from .crypto import PrivateKey, hash_data |
| from .exceptions import ( |
| KeypairMismatchException, |
| InvalidHash, |
| InvalidSignature, |
| AmountError, |
| AssetIdMismatch, |
| ThresholdTooDeep, |
| DoubleSpend, |
| InputDoesNotExist, |
| ) |
| from .utils import serialize |
| |
| |
| UnspentOutput = namedtuple( |
| "UnspentOutput", |
| ( |
| # TODO 'utxo_hash': sha3_256(f'{txid}{output_index}'.encode()) |
| # 'utxo_hash', # noqa |
| "transaction_id", |
| "output_index", |
| "amount", |
| "asset_id", |
| "condition_uri", |
| ), |
| ) |
| |
| |
| class Input(object): |
| """! A Input is used to spend assets locked by an Output. |
| Wraps around a Crypto-condition Fulfillment. |
| |
| fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment |
| to be signed with a private key. |
| owners_before (:obj:`list` of :obj:`str`): A list of owners after a |
| Transaction was confirmed. |
| fulfills (:class:`~resdb.transaction. TransactionLink`, |
| optional): A link representing the input of a `TRANSFER` |
| Transaction. |
| """ |
| |
| def __init__(self, fulfillment, owners_before, fulfills=None): |
| """! Create an instance of an :class:`~.Input`. |
| @param fulfillment (:class:`cryptoconditions.Fulfillment`): A |
| Fulfillment to be signed with a private key. |
| @param owners_before (:obj:`list` of :obj:`str`): A list of owners |
| after a Transaction was confirmed. |
| @param fulfills (:class:`~resdb.transaction. |
| TransactionLink`, optional): A link representing the input |
| of a `TRANSFER` Transaction. |
| """ |
| if fulfills is not None and not isinstance(fulfills, TransactionLink): |
| raise TypeError("`fulfills` must be a TransactionLink instance") |
| if not isinstance(owners_before, list): |
| raise TypeError("`owners_after` must be a list instance") |
| |
| self.fulfillment = fulfillment |
| self.fulfills = fulfills |
| self.owners_before = owners_before |
| |
| def __eq__(self, other): |
| # TODO: If `other !== Fulfillment` return `False` |
| return self.to_dict() == other.to_dict() |
| |
| def to_dict(self): |
| """! Transforms the object to a Python dictionary. |
| If an Input hasn't been signed yet, this method returns a |
| dictionary representation. |
| |
| @return dict: The Input as an alternative serialization format. |
| """ |
| try: |
| fulfillment = self.fulfillment.serialize_uri() |
| except (TypeError, AttributeError, ASN1EncodeError, ASN1DecodeError): |
| fulfillment = _fulfillment_to_details(self.fulfillment) |
| |
| try: |
| # NOTE: `self.fulfills` can be `None` and that's fine |
| fulfills = self.fulfills.to_dict() |
| except AttributeError: |
| fulfills = None |
| |
| input_ = { |
| "owners_before": self.owners_before, |
| "fulfills": fulfills, |
| "fulfillment": fulfillment, |
| } |
| return input_ |
| |
| @classmethod |
| def generate(cls, public_keys): |
| # TODO: write docstring |
| # The amount here does not really matter. It is only use on the |
| # output data model but here we only care about the fulfillment |
| output = Output.generate(public_keys, 1) |
| return cls(output.fulfillment, public_keys) |
| |
| @classmethod |
| def from_dict(cls, data): |
| """! Transforms a Python dictionary to an Input object. |
| Note: |
| Optionally, this method can also serialize a Cryptoconditions- |
| Fulfillment that is not yet signed. |
| |
| @param data (dict): The Input to be transformed. |
| @return :class:`~resdb.transaction.Input` |
| @exception InvalidSignature: If an Input's URI couldn't be parsed. |
| """ |
| fulfillment = data["fulfillment"] |
| if not isinstance(fulfillment, (Fulfillment, type(None))): |
| try: |
| fulfillment = Fulfillment.from_uri(data["fulfillment"]) |
| except ASN1DecodeError: |
| # TODO Remove as it is legacy code, and simply fall back on |
| # ASN1DecodeError |
| raise InvalidSignature("Fulfillment URI couldn't been parsed") |
| except TypeError: |
| # NOTE: See comment about this special case in |
| # `Input.to_dict` |
| 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: |
| @param fulfillment (:class:`cryptoconditions.Fulfillment`): Crypto-conditions Fulfillment object |
| """ |
| |
| if fulfillment.type_name == "ed25519-sha-256": |
| return { |
| "type": "ed25519-sha-256", |
| "public_key": base58.b58encode(fulfillment.public_key).decode(), |
| } |
| |
| 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, _depth=0): |
| """! Load a fulfillment for a signing spec dictionary |
| @param data tx.output[].condition.details dictionary |
| """ |
| if _depth == 100: |
| raise ThresholdTooDeep() |
| |
| 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": |
| threshold = ThresholdSha256(data["threshold"]) |
| for cond in data["subconditions"]: |
| cond = _fulfillment_from_details(cond, _depth + 1) |
| threshold.add_subfulfillment(cond) |
| return threshold |
| |
| raise UnsupportedTypeError(data.get("type")) |
| |
| |
| class TransactionLink(object): |
| """! An object for unidirectional linking to a Transaction's Output. |
| Attributes: |
| txid (str, optional): A Transaction to link to. |
| output (int, optional): An output's index in a Transaction with id |
| `txid`. |
| """ |
| |
| def __init__(self, txid=None, output=None): |
| """! Create an instance of a :class:`~.TransactionLink`. |
| Note: |
| In an IPLD implementation, this class is not necessary anymore, |
| as an IPLD link can simply point to an object, as well as an |
| objects properties. So instead of having a (de)serializable |
| class, we can have a simple IPLD link of the form: |
| `/<tx_id>/transaction/outputs/<output>/`. |
| |
| @param txid (str): A Transaction to link to. |
| @param output An (int): Outputs's index in a Transaction |
| with id `txid`. |
| """ |
| self.txid = txid |
| self.output = output |
| |
| def __bool__(self): |
| return self.txid is not None and self.output is not None |
| |
| def __eq__(self, other): |
| # TODO: If `other !== TransactionLink` return `False` |
| return self.to_dict() == other.to_dict() |
| |
| def __hash__(self): |
| return hash((self.txid, self.output)) |
| |
| @classmethod |
| def from_dict(cls, link): |
| """! Transforms a Python dictionary to a TransactionLink object. |
| |
| @param link (dict): The link to be transformed. |
| |
| @return :class:`~resdb.transaction.TransactionLink` |
| """ |
| try: |
| return cls(link["transaction_id"], link["output_index"]) |
| except TypeError: |
| return cls() |
| |
| def to_dict(self): |
| """! Transforms the object to a Python dictionary. |
| @return The link as an alternative serialization format. |
| """ |
| if self.txid is None and self.output is None: |
| return None |
| else: |
| return { |
| "transaction_id": self.txid, |
| "output_index": self.output, |
| } |
| |
| def to_uri(self, path=""): |
| if self.txid is None and self.output is None: |
| return None |
| return "{}/transactions/{}/outputs/{}".format(path, self.txid, self.output) |
| |
| |
| class Output(object): |
| """! An Output is used to lock an asset. |
| Wraps around a Crypto-condition Condition. |
| Attributes: |
| fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment |
| to extract a Condition from. |
| public_keys (:obj:`list` of :obj:`str`, optional): A list of |
| owners before a Transaction was confirmed. |
| """ |
| |
| MAX_AMOUNT = 9 * 10**18 |
| |
| def __init__(self, fulfillment, public_keys=None, amount=1): |
| """! Create an instance of a :class:`~.Output`. |
| Args: |
| @param fulfillment (:class:`cryptoconditions.Fulfillment`): A |
| Fulfillment to extract a Condition from. |
| @param public_keys (:obj:`list` of :obj:`str`, optional): A list of |
| owners before a Transaction was confirmed. |
| @param amount (int): The amount of Assets to be locked with this |
| Output. |
| |
| @exception TypeError: if `public_keys` is not instance of `list`. |
| """ |
| if not isinstance(public_keys, list) and public_keys is not None: |
| raise TypeError("`public_keys` must be a list instance or None") |
| if not isinstance(amount, int): |
| raise TypeError("`amount` must be an int") |
| if amount < 1: |
| raise AmountError("`amount` must be greater than 0") |
| if amount > self.MAX_AMOUNT: |
| raise AmountError("`amount` must be <= %s" % self.MAX_AMOUNT) |
| |
| self.fulfillment = fulfillment |
| self.amount = amount |
| self.public_keys = public_keys |
| |
| def __eq__(self, other): |
| # TODO: If `other !== Condition` return `False` |
| return self.to_dict() == other.to_dict() |
| |
| def to_dict(self): |
| """! Transforms the object to a Python dictionary. |
| Note: |
| A dictionary serialization of the Input the Output was |
| derived from is always provided. |
| |
| @return The Output as an alternative serialization format. |
| """ |
| # TODO FOR CC: It must be able to recognize a hashlock condition |
| # and fulfillment! |
| condition = {} |
| try: |
| condition["details"] = _fulfillment_to_details(self.fulfillment) |
| except AttributeError: |
| pass |
| |
| try: |
| condition["uri"] = self.fulfillment.condition_uri |
| except AttributeError: |
| condition["uri"] = self.fulfillment |
| |
| output = { |
| "public_keys": self.public_keys, |
| "condition": condition, |
| "amount": str(self.amount), |
| } |
| return output |
| |
| @classmethod |
| def generate(cls, public_keys, amount): |
| """! Generates a Output from a specifically formed tuple or list. |
| Note: |
| If a ThresholdCondition has to be generated where the threshold |
| is always the number of subconditions it is split between, a |
| list of the following structure is sufficient: |
| [(address|condition)*, [(address|condition)*, ...], ...] |
| |
| @param public_keys (:obj:`list` of :obj:`str`): The public key of |
| the users that should be able to fulfill the Condition |
| that is being created. |
| @param amount (:obj:`int`): The amount locked by the Output. |
| @return An Output that can be used in a Transaction. |
| |
| @exception TypeError: If `public_keys` is not an instance of `list`. |
| @exception ValueError: If `public_keys` is an empty list. |
| """ |
| threshold = len(public_keys) |
| if not isinstance(amount, int): |
| raise TypeError("`amount` must be a int") |
| if amount < 1: |
| raise AmountError("`amount` needs to be greater than zero") |
| if not isinstance(public_keys, list): |
| raise TypeError("`public_keys` must be an instance of list") |
| if len(public_keys) == 0: |
| raise ValueError( |
| "`public_keys` needs to contain at least one" "owner") |
| elif len(public_keys) == 1 and not isinstance(public_keys[0], list): |
| if isinstance(public_keys[0], Fulfillment): |
| ffill = public_keys[0] |
| else: |
| ffill = Ed25519Sha256( |
| public_key=base58.b58decode(public_keys[0])) |
| return cls(ffill, public_keys, amount=amount) |
| else: |
| # Threshold conditions not supported by resdb yet |
| initial_cond = ThresholdSha256(threshold=threshold) |
| threshold_cond = reduce( |
| cls._gen_condition, public_keys, initial_cond) |
| return cls(threshold_cond, public_keys, amount=amount) |
| |
| @classmethod |
| def _gen_condition(cls, initial, new_public_keys): |
| """! Generates ThresholdSha256 conditions from a list of new owners. |
| Note: |
| This method is intended only to be used with a reduce function. |
| For a description on how to use this method, see |
| :meth:`~.Output.generate`. |
| Args: |
| @param initial (:class:`cryptoconditions.ThresholdSha256`): A Condition representing the overall root. |
| @param new_public_keys (:obj:`list` of :obj:`str`|str): A list of new |
| owners or a single new owner. |
| @return :class:`cryptoconditions.ThresholdSha256`: |
| """ |
| try: |
| threshold = len(new_public_keys) |
| except TypeError: |
| threshold = None |
| |
| if isinstance(new_public_keys, list) and len(new_public_keys) > 1: |
| ffill = ThresholdSha256(threshold=threshold) |
| reduce(cls._gen_condition, new_public_keys, ffill) |
| elif isinstance(new_public_keys, list) and len(new_public_keys) <= 1: |
| raise ValueError("Sublist cannot contain single owner") |
| else: |
| try: |
| new_public_keys = new_public_keys.pop() |
| except AttributeError: |
| pass |
| # NOTE: Instead of submitting base58 encoded addresses, a user |
| # of this class can also submit fully instantiated |
| # Cryptoconditions. In the case of casting |
| # `new_public_keys` to a Ed25519Fulfillment with the |
| # result of a `TypeError`, we're assuming that |
| # `new_public_keys` is a Cryptocondition then. |
| if isinstance(new_public_keys, Fulfillment): |
| ffill = new_public_keys |
| else: |
| ffill = Ed25519Sha256( |
| public_key=base58.b58decode(new_public_keys)) |
| initial.add_subfulfillment(ffill) |
| return initial |
| |
| @classmethod |
| def from_dict(cls, data): |
| """! Transforms a Python dictionary to an Output object. |
| Note: |
| To pass a serialization cycle multiple times, a |
| Cryptoconditions Fulfillment needs to be present in the |
| passed-in dictionary, as Condition URIs are not serializable |
| anymore. |
| |
| @param data (dict): The dict to be transformed. |
| @return :class:`~resdb.transaction.Output` |
| """ |
| try: |
| fulfillment = _fulfillment_from_details( |
| data["condition"]["details"]) |
| except KeyError: |
| # NOTE: Hashlock condition case |
| fulfillment = data["condition"]["uri"] |
| try: |
| amount = int(data["amount"]) |
| except ValueError: |
| raise AmountError("Invalid amount: %s" % data["amount"]) |
| return cls(fulfillment, data["public_keys"], amount) |
| |
| |
| class Transaction(object): |
| """! A Transaction is used to create and transfer assets. |
| Note: |
| For adding Inputs and Outputs, this class provides methods |
| to do so. |
| Attributes: |
| operation (str): Defines the operation of the Transaction. |
| inputs (:obj:`list` of :class:`~resdb. |
| transaction.Input`, optional): Define the assets to |
| spend. |
| outputs (:obj:`list` of :class:`~resdb. |
| transaction.Output`, optional): Define the assets to lock. |
| asset (dict): Asset payload for this Transaction. ``CREATE`` |
| Transactions require a dict with a ``data`` |
| property while ``TRANSFER`` Transactions require a dict with a |
| ``id`` property. |
| metadata (dict): |
| Metadata to be stored along with the Transaction. |
| version (string): Defines the version number of a Transaction. |
| """ |
| |
| CREATE = "CREATE" |
| TRANSFER = "TRANSFER" |
| ALLOWED_OPERATIONS = (CREATE, TRANSFER) |
| VERSION = "2.0" |
| |
| def __init__( |
| self, |
| operation, |
| asset, |
| inputs=None, |
| outputs=None, |
| metadata=None, |
| version=None, |
| hash_id=None, |
| ): |
| """! The constructor allows to create a customizable Transaction. |
| Note: |
| When no `version` is provided, one is being |
| generated by this method. |
| |
| @param operation (str): Defines the operation of the Transaction. |
| @param asset (dict): Asset payload for this Transaction. |
| @param inputs (:obj:`list` of :class:`~resdb.transaction.Input`, optional):Define the assets to |
| @param outputs (:obj:`list` of :class:`~resdb.transaction.Output`, optional):Define the assets to lock. |
| @param metadata (dict): Metadata to be stored along with the Transaction. |
| @param version (string): Defines the version number of a Transaction. |
| @param hash_id (string): Hash id of the transaction. |
| """ |
| if operation not in Transaction.ALLOWED_OPERATIONS: |
| allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS) |
| raise ValueError( |
| "`operation` must be one of {}".format(allowed_ops)) |
| |
| # Asset payloads for 'CREATE' operations must be None or |
| # dicts holding a `data` property. Asset payloads for 'TRANSFER' |
| # operations must be dicts holding an `id` property. |
| if ( |
| operation == Transaction.CREATE |
| and asset is not None |
| and not (isinstance(asset, dict) and "data" in asset) |
| ): |
| raise TypeError( |
| ( |
| "`asset` must be None or a dict holding a `data` " |
| " property instance for '{}' " |
| "Transactions".format(operation) |
| ) |
| ) |
| elif operation == Transaction.TRANSFER and not ( |
| isinstance(asset, dict) and "id" in asset |
| ): |
| raise TypeError( |
| ( |
| "`asset` must be a dict holding an `id` property " |
| "for 'TRANSFER' Transactions".format(operation) |
| ) |
| ) |
| |
| if outputs and not isinstance(outputs, list): |
| raise TypeError("`outputs` must be a list instance or None") |
| |
| if inputs and not isinstance(inputs, list): |
| raise TypeError("`inputs` must be a list instance or None") |
| |
| if metadata is not None and not isinstance(metadata, dict): |
| raise TypeError("`metadata` must be a dict or None") |
| |
| self.version = version if version is not None else self.VERSION |
| self.operation = operation |
| self.asset = asset |
| self.inputs = inputs or [] |
| self.outputs = outputs or [] |
| self.metadata = metadata |
| self._id = hash_id |
| |
| @property |
| def unspent_outputs(self): |
| """! UnspentOutput: The outputs of this transaction, in a data |
| structure containing relevant information for storing them in |
| a UTXO set, and performing validation. |
| """ |
| if self.operation == Transaction.CREATE: |
| self._asset_id = self._id |
| elif self.operation == Transaction.TRANSFER: |
| self._asset_id = self.asset["id"] |
| return ( |
| UnspentOutput( |
| transaction_id=self._id, |
| output_index=output_index, |
| amount=output.amount, |
| asset_id=self._asset_id, |
| condition_uri=output.fulfillment.condition_uri, |
| ) |
| for output_index, output in enumerate(self.outputs) |
| ) |
| |
| @property |
| def spent_outputs(self): |
| """! Tuple of :obj:`dict`: Inputs of this transaction. Each input |
| is represented as a dictionary containing a transaction id and |
| output index. |
| """ |
| return (input_.fulfills.to_dict() for input_ in self.inputs if input_.fulfills) |
| |
| @property |
| def serialized(self): |
| return Transaction._to_str(self.to_dict()) |
| |
| def _hash(self): |
| self._id = hash_data(self.serialized) |
| |
| @classmethod |
| def create(cls, tx_signers, recipients, metadata=None, asset=None): |
| """! A simple way to generate a `CREATE` transaction. |
| Note: |
| This method currently supports the following Cryptoconditions |
| use cases: |
| - Ed25519 |
| - ThresholdSha256 |
| Additionally, it provides support for the following Resdb |
| use cases: |
| - Multiple inputs and outputs. |
| |
| @param tx_signers (:obj:`list` of :obj:`str`): A list of keys that |
| represent the signers of the CREATE Transaction. |
| @param recipients (:obj:`list` of :obj:`tuple`): A list of |
| ([keys],amount) that represent the recipients of this |
| Transaction. |
| @param metadata (dict): The metadata to be stored along with the |
| Transaction. |
| @param asset (dict): The metadata associated with the asset that will |
| be created in this Transaction. |
| |
| @return :class:`~resdb.transaction.Transaction` |
| """ |
| if not isinstance(tx_signers, list): |
| raise TypeError("`tx_signers` must be a list instance") |
| if not isinstance(recipients, list): |
| raise TypeError("`recipients` must be a list instance") |
| if len(tx_signers) == 0: |
| raise ValueError("`tx_signers` list cannot be empty") |
| if len(recipients) == 0: |
| raise ValueError("`recipients` list cannot be empty") |
| if not (asset is None or isinstance(asset, dict)): |
| raise TypeError("`asset` must be a dict or None") |
| |
| inputs = [] |
| outputs = [] |
| |
| # generate_outputs |
| for recipient in recipients: |
| if not isinstance(recipient, tuple) or len(recipient) != 2: |
| raise ValueError( |
| ( |
| "Each `recipient` in the list must be a" |
| " tuple of `([<list of public keys>]," |
| " <amount>)`" |
| ) |
| ) |
| pub_keys, amount = recipient |
| outputs.append(Output.generate(pub_keys, amount)) |
| |
| # generate inputs |
| inputs.append(Input.generate(tx_signers)) |
| |
| return cls(cls.CREATE, {"data": asset}, inputs, outputs, metadata) |
| |
| @classmethod |
| def transfer(cls, inputs, recipients, asset_id, metadata=None): |
| """! A simple way to generate a `TRANSFER` transaction. |
| Note: |
| Different cases for threshold conditions: |
| Combining multiple `inputs` with an arbitrary number of |
| `recipients` can yield interesting cases for the creation of |
| threshold conditions we'd like to support. The following |
| notation is proposed: |
| 1. The index of a `recipient` corresponds to the index of |
| an input: |
| e.g. `transfer([input1], [a])`, means `input1` would now be |
| owned by user `a`. |
| 2. `recipients` can (almost) get arbitrary deeply nested, |
| creating various complex threshold conditions: |
| e.g. `transfer([inp1, inp2], [[a, [b, c]], d])`, means |
| `a`'s signature would have a 50% weight on `inp1` |
| compared to `b` and `c` that share 25% of the leftover |
| weight respectively. `inp2` is owned completely by `d`. |
| |
| @param inputs (:obj:`list` of :class:`~resdb.transaction.Input`): Converted `Output`s, intended to |
| be used as inputs in the transfer to generate. |
| @param recipients (:obj:`list` of :obj:`tuple`): A list of |
| ([keys],amount) that represent the recipients of this |
| Transaction. |
| @param asset_id (str): The asset ID of the asset to be transferred in |
| this Transaction. |
| @param metadata (dict): Python dictionary to be stored along with the |
| Transaction. |
| |
| @return :class:`~resdb.transaction.Transaction` |
| """ |
| if not isinstance(inputs, list): |
| raise TypeError("`inputs` must be a list instance") |
| if len(inputs) == 0: |
| raise ValueError("`inputs` must contain at least one item") |
| if not isinstance(recipients, list): |
| raise TypeError("`recipients` must be a list instance") |
| if len(recipients) == 0: |
| raise ValueError("`recipients` list cannot be empty") |
| |
| outputs = [] |
| for recipient in recipients: |
| if not isinstance(recipient, tuple) or len(recipient) != 2: |
| raise ValueError( |
| ( |
| "Each `recipient` in the list must be a" |
| " tuple of `([<list of public keys>]," |
| " <amount>)`" |
| ) |
| ) |
| pub_keys, amount = recipient |
| outputs.append(Output.generate(pub_keys, amount)) |
| |
| if not isinstance(asset_id, str): |
| raise TypeError("`asset_id` must be a string") |
| |
| inputs = deepcopy(inputs) |
| return cls(cls.TRANSFER, {"id": asset_id}, inputs, outputs, metadata) |
| |
| def __eq__(self, other): |
| try: |
| other = other.to_dict() |
| except AttributeError: |
| return False |
| return self.to_dict() == other |
| |
| def to_inputs(self, indices=None): |
| """! Converts a Transaction's outputs to spendable inputs. |
| Note: |
| Takes the Transaction's outputs and derives inputs |
| from that can then be passed into `Transaction.transfer` as |
| `inputs`. |
| A list of integers can be passed to `indices` that |
| defines which outputs should be returned as inputs. |
| If no `indices` are passed (empty list or None) all |
| outputs of the Transaction are returned. |
| |
| @param indices (:obj:`list` of int): Defines which |
| outputs should be returned as inputs. |
| @return :obj:`list` of :class:`~resdb.transaction. |
| Input` |
| """ |
| # NOTE: If no indices are passed, we just assume to take all outputs |
| # as inputs. |
| indices = indices or range(len(self.outputs)) |
| return [ |
| Input( |
| self.outputs[idx].fulfillment, |
| self.outputs[idx].public_keys, |
| TransactionLink(self.id, idx), |
| ) |
| for idx in indices |
| ] |
| |
| def add_input(self, input_): |
| """! Adds an input to a Transaction's list of inputs. |
| @param input_ (:class:`~resdb.transaction. |
| Input`): An Input to be added to the Transaction. |
| """ |
| if not isinstance(input_, Input): |
| raise TypeError("`input_` must be a Input instance") |
| self.inputs.append(input_) |
| |
| def add_output(self, output): |
| """! Adds an output to a Transaction's list of outputs. |
| @param output (:class:`~resdb.transaction. |
| Output`): An Output to be added to the |
| Transaction. |
| """ |
| if not isinstance(output, Output): |
| raise TypeError("`output` must be an Output instance or None") |
| self.outputs.append(output) |
| |
| def sign(self, private_keys): |
| """! Fulfills a previous Transaction's Output by signing Inputs. |
| Note: |
| This method works only for the following Cryptoconditions |
| currently: |
| - Ed25519Fulfillment |
| - ThresholdSha256 |
| Furthermore, note that all keys required to fully sign the |
| Transaction have to be passed to this method. A subset of all |
| will cause this method to fail. |
| |
| @param private_keys (:obj:`list` of :obj:`str`): A complete list of |
| all private keys needed to sign all Fulfillments of this |
| Transaction. |
| @return :class:`~resdb.transaction.Transaction` |
| """ |
| # TODO: Singing should be possible with at least one of all private |
| # keys supplied to this method. |
| if private_keys is None or not isinstance(private_keys, list): |
| raise TypeError("`private_keys` must be a list instance") |
| |
| # NOTE: Generate public keys from private keys and match them in a |
| # dictionary: |
| # key: public_key |
| # value: private_key |
| def gen_public_key(private_key): |
| # TODO FOR CC: Adjust interface so that this function becomes |
| # unnecessary |
| |
| # cc now provides a single method `encode` to return the key |
| # in several different encodings. |
| public_key = private_key.get_verifying_key().encode() |
| # Returned values from cc are always bytestrings so here we need |
| # to decode to convert the bytestring into a python str |
| return public_key.decode() |
| |
| key_pairs = { |
| gen_public_key(PrivateKey(private_key)): PrivateKey(private_key) |
| for private_key in private_keys |
| } |
| |
| tx_dict = self.to_dict() |
| tx_dict = Transaction._remove_signatures(tx_dict) |
| tx_serialized = Transaction._to_str(tx_dict) |
| for i, input_ in enumerate(self.inputs): |
| self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs) |
| |
| self._hash() |
| |
| return self |
| |
| @classmethod |
| def _sign_input(cls, input_, message, key_pairs): |
| """! Signs a single Input. |
| Note: |
| This method works only for the following Cryptoconditions |
| currently: |
| - Ed25519Fulfillment |
| - ThresholdSha256. |
| |
| @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed. |
| @param message (str): The message to be signed |
| @param key_pairs (dict): The keys to sign the Transaction with. |
| """ |
| if isinstance(input_.fulfillment, Ed25519Sha256): |
| return cls._sign_simple_signature_fulfillment(input_, message, key_pairs) |
| elif isinstance(input_.fulfillment, ThresholdSha256): |
| return cls._sign_threshold_signature_fulfillment(input_, message, key_pairs) |
| else: |
| raise ValueError( |
| "Fulfillment couldn't be matched to " |
| "Cryptocondition fulfillment type." |
| ) |
| |
| @classmethod |
| def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs): |
| """! Signs a Ed25519Fulfillment. |
| |
| @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed. |
| @param message (str): The message to be signed |
| @param key_pairs (dict): The keys to sign the Transaction with. |
| """ |
| # NOTE: To eliminate the dangers of accidentally signing a condition by |
| # reference, we remove the reference of input_ here |
| # intentionally. If the user of this class knows how to use it, |
| # this should never happen, but then again, never say never. |
| input_ = deepcopy(input_) |
| public_key = input_.owners_before[0] |
| message = sha3_256(message.encode()) |
| if input_.fulfills: |
| message.update( |
| "{}{}".format(input_.fulfills.txid, |
| input_.fulfills.output).encode() |
| ) |
| |
| try: |
| # cryptoconditions makes no assumptions of the encoding of the |
| # message to sign or verify. It only accepts bytestrings |
| input_.fulfillment.sign( |
| message.digest(), base58.b58decode( |
| key_pairs[public_key].encode()) |
| ) |
| except KeyError: |
| raise KeypairMismatchException( |
| "Public key {} is not a pair to " |
| "any of the private keys".format(public_key) |
| ) |
| return input_ |
| |
| @classmethod |
| def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs): |
| """! Signs a ThresholdSha256. |
| |
| @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed. |
| @param message (str): The message to be signed |
| @param key_pairs (dict): The keys to sign the Transaction with. |
| """ |
| input_ = deepcopy(input_) |
| message = sha3_256(message.encode()) |
| if input_.fulfills: |
| message.update( |
| "{}{}".format(input_.fulfills.txid, |
| input_.fulfills.output).encode() |
| ) |
| |
| for owner_before in set(input_.owners_before): |
| # TODO: CC should throw a KeypairMismatchException, instead of |
| # our manual mapping here |
| |
| # TODO FOR CC: Naming wise this is not so smart, |
| # `get_subcondition` in fact doesn't return a |
| # condition but a fulfillment |
| |
| # TODO FOR CC: `get_subcondition` is singular. One would not |
| # expect to get a list back. |
| ccffill = input_.fulfillment |
| subffills = ccffill.get_subcondition_from_vk( |
| base58.b58decode(owner_before)) |
| if not subffills: |
| raise KeypairMismatchException( |
| "Public key {} cannot be found " |
| "in the fulfillment".format(owner_before) |
| ) |
| try: |
| private_key = key_pairs[owner_before] |
| except KeyError: |
| raise KeypairMismatchException( |
| "Public key {} is not a pair " |
| "to any of the private keys".format(owner_before) |
| ) |
| |
| # cryptoconditions makes no assumptions of the encoding of the |
| # message to sign or verify. It only accepts bytestrings |
| for subffill in subffills: |
| subffill.sign(message.digest(), |
| base58.b58decode(private_key.encode())) |
| return input_ |
| |
| def inputs_valid(self, outputs=None): |
| """! Validates the Inputs in the Transaction against given |
| Outputs. |
| Note: |
| Given a `CREATE` Transaction is passed, |
| dummy values for Outputs are submitted for validation that |
| evaluate parts of the validation-checks to `True`. |
| |
| @param outputs (:obj:`list` of :class:`~resdb. |
| transaction.Output`): A list of Outputs to check the |
| Inputs against. |
| @return If all Inputs are valid. |
| """ |
| if self.operation == Transaction.CREATE: |
| # NOTE: Since in the case of a `CREATE`-transaction we do not have |
| # to check for outputs, we're just submitting dummy |
| # values to the actual method. This simplifies it's logic |
| # greatly, as we do not have to check against `None` values. |
| return self._inputs_valid(["dummyvalue" for _ in self.inputs]) |
| elif self.operation == Transaction.TRANSFER: |
| return self._inputs_valid( |
| [output.fulfillment.condition_uri for output in outputs] |
| ) |
| else: |
| allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS) |
| raise TypeError( |
| "`operation` must be one of {}".format(allowed_ops)) |
| |
| def _inputs_valid(self, output_condition_uris): |
| """!Validates an Input against a given set of Outputs. |
| Note: |
| The number of `output_condition_uris` must be equal to the |
| number of Inputs a Transaction has. |
| |
| @param output_condition_uris (:obj:`list` of :obj:`str`): A list of |
| Outputs to check the Inputs against. |
| @return If all Outputs are valid. |
| """ |
| |
| if len(self.inputs) != len(output_condition_uris): |
| raise ValueError( |
| "Inputs and " "output_condition_uris must have the same count" |
| ) |
| |
| tx_dict = self.to_dict() |
| tx_dict = Transaction._remove_signatures(tx_dict) |
| tx_dict["id"] = None |
| tx_serialized = Transaction._to_str(tx_dict) |
| |
| def validate(i, output_condition_uri=None): |
| """Validate input against output condition URI""" |
| return self._input_valid( |
| self.inputs[i], self.operation, tx_serialized, output_condition_uri |
| ) |
| |
| return all(validate(i, cond) for i, cond in enumerate(output_condition_uris)) |
| |
| @staticmethod |
| def _input_valid(input_, operation, message, output_condition_uri=None): |
| """! Validates a single Input against a single Output. |
| Note: |
| In case of a `CREATE` Transaction, this method |
| does not validate against `output_condition_uri`. |
| |
| @param input_ (:class:`~resdb.transaction.Input`) The Input to be signed. |
| @param operation (str): The type of Transaction. |
| @param message (str): The fulfillment message. |
| @param output_condition_uri (str, optional): An Output to check the |
| Input against. |
| @return If the Input is valid. |
| """ |
| ccffill = input_.fulfillment |
| try: |
| parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri()) |
| except (TypeError, ValueError, ParsingError, ASN1DecodeError, ASN1EncodeError): |
| return False |
| |
| if operation == Transaction.CREATE: |
| # NOTE: In the case of a `CREATE` transaction, the |
| # output is always valid. |
| output_valid = True |
| else: |
| output_valid = output_condition_uri == ccffill.condition_uri |
| |
| message = sha3_256(message.encode()) |
| if input_.fulfills: |
| message.update( |
| "{}{}".format(input_.fulfills.txid, |
| input_.fulfills.output).encode() |
| ) |
| |
| # NOTE: We pass a timestamp to `.validate`, as in case of a timeout |
| # condition we'll have to validate against it |
| |
| # cryptoconditions makes no assumptions of the encoding of the |
| # message to sign or verify. It only accepts bytestrings |
| ffill_valid = parsed_ffill.validate(message=message.digest()) |
| return output_valid and ffill_valid |
| |
| def to_dict(self): |
| """! Transforms the object to a Python dictionary. |
| @return The Transaction as an alternative serialization format. |
| """ |
| return { |
| "inputs": [input_.to_dict() for input_ in self.inputs], |
| "outputs": [output.to_dict() for output in self.outputs], |
| "operation": str(self.operation), |
| "metadata": self.metadata, |
| "asset": self.asset, |
| "version": self.version, |
| "id": self._id, |
| } |
| |
| @staticmethod |
| # TODO: Remove `_dict` prefix of variable. |
| def _remove_signatures(tx_dict): |
| """! Takes a Transaction dictionary and removes all signatures. |
| @param (dict): tx_dict The Transaction to remove all signatures from. |
| @return dict |
| """ |
| # NOTE: We remove the reference since we need `tx_dict` only for the |
| # transaction's hash |
| tx_dict = deepcopy(tx_dict) |
| for input_ in tx_dict["inputs"]: |
| # NOTE: Not all Cryptoconditions return a `signature` key (e.g. |
| # ThresholdSha256), so setting it to `None` in any |
| # case could yield incorrect signatures. This is why we only |
| # set it to `None` if it's set in the dict. |
| input_["fulfillment"] = None |
| return tx_dict |
| |
| @staticmethod |
| def _to_hash(value): |
| return hash_data(value) |
| |
| @property |
| def id(self): |
| return self._id |
| |
| def to_hash(self): |
| return self.to_dict()["id"] |
| |
| @staticmethod |
| def _to_str(value): |
| return serialize(value) |
| |
| # TODO: This method shouldn't call `_remove_signatures` |
| def __str__(self): |
| tx = Transaction._remove_signatures(self.to_dict()) |
| return Transaction._to_str(tx) |
| |
| @staticmethod |
| def get_asset_id(transactions): |
| """! Get the asset id from a list of :class:`~.Transactions`. |
| This is useful when we want to check if the multiple inputs of a |
| transaction are related to the same asset id. |
| Args: |
| @param transactions (:obj:`list` of :class:`~resdb.transaction.Transaction`): |
| A list of Transactions. |
| Usually input Transactions that should have a matching |
| asset ID. |
| @return ID of the asset. |
| @exception If the inputs are related to different assets. |
| """ |
| |
| if not isinstance(transactions, list): |
| transactions = [transactions] |
| |
| # create a set of the transactions' asset ids |
| asset_ids = { |
| tx.id if tx.operation == Transaction.CREATE else tx.asset["id"] |
| for tx in transactions |
| } |
| |
| # check that all the transasctions have the same asset id |
| if len(asset_ids) > 1: |
| raise AssetIdMismatch( |
| ( |
| "All inputs of all transactions passed" |
| " need to have the same asset id" |
| ) |
| ) |
| return asset_ids.pop() |
| |
| @staticmethod |
| def validate_id(tx_body): |
| """! Validate the transaction ID of a transaction |
| @param tx_body (dict): The Transaction to be transformed. |
| """ |
| # NOTE: Remove reference to avoid side effects |
| tx_body = deepcopy(tx_body) |
| try: |
| proposed_tx_id = tx_body["id"] |
| except KeyError: |
| raise InvalidHash("No transaction id found!") |
| |
| tx_body["id"] = None |
| |
| tx_body_serialized = Transaction._to_str(tx_body) |
| valid_tx_id = Transaction._to_hash(tx_body_serialized) |
| |
| if proposed_tx_id != valid_tx_id: |
| err_msg = ( |
| "The transaction's id '{}' isn't equal to " |
| "the hash of its body, i.e. it's not valid." |
| ) |
| raise InvalidHash(err_msg.format(proposed_tx_id)) |
| |
| @classmethod |
| def from_dict(cls, tx, skip_schema_validation=True): |
| """! Transforms a Python dictionary to a Transaction object. |
| @param tx_body (dict): The Transaction to be transformed. |
| @return :class:`~resdb.transaction.Transaction` |
| """ |
| inputs = [Input.from_dict(input_) for input_ in tx["inputs"]] |
| outputs = [Output.from_dict(output) for output in tx["outputs"]] |
| |
| if not skip_schema_validation: |
| cls.validate_id(tx) |
| cls.validate_schema(tx) |
| return cls( |
| tx["operation"], |
| tx["asset"], |
| inputs, |
| outputs, |
| tx["metadata"], |
| tx["version"], |
| hash_id=tx["id"], |
| ) |
| |
| @classmethod |
| def from_db(cls, resdb, tx_dict_list): |
| """! Helper method that reconstructs a transaction dict that was returned |
| from the database. It checks what asset_id to retrieve, retrieves the |
| asset from the asset table and reconstructs the transaction. |
| |
| |
| @param resdb An instance of ResDB used to perform database queries. |
| @param tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or |
| list of transaction dict as returned from the database. |
| |
| @return :class:`~Transaction` |
| |
| """ |
| return_list = True |
| if isinstance(tx_dict_list, dict): |
| tx_dict_list = [tx_dict_list] |
| return_list = False |
| |
| tx_map = {} |
| tx_ids = [] |
| for tx in tx_dict_list: |
| tx.update({"metadata": None}) |
| tx_map[tx["id"]] = tx |
| tx_ids.append(tx["id"]) |
| |
| assets = list(resdb.get_assets(tx_ids)) |
| for asset in assets: |
| if asset is not None: |
| tx = tx_map[asset["id"]] |
| del asset["id"] |
| tx["asset"] = asset |
| |
| tx_ids = list(tx_map.keys()) |
| metadata_list = list(resdb.get_metadata(tx_ids)) |
| for metadata in metadata_list: |
| tx = tx_map[metadata["id"]] |
| tx.update({"metadata": metadata.get("metadata")}) |
| |
| if return_list: |
| tx_list = [] |
| for tx_id, tx in tx_map.items(): |
| tx_list.append(cls.from_dict(tx)) |
| return tx_list |
| else: |
| tx = list(tx_map.values())[0] |
| return cls.from_dict(tx) |
| |
| type_registry = {} |
| |
| @staticmethod |
| def register_type(tx_type, tx_class): |
| Transaction.type_registry[tx_type] = tx_class |
| |
| def resolve_class(operation): |
| """! For the given `tx` based on the `operation` key return its implementation class |
| """ |
| |
| create_txn_class = Transaction.type_registry.get(Transaction.CREATE) |
| return Transaction.type_registry.get(operation, create_txn_class) |
| |
| @classmethod |
| def validate_schema(cls, tx): |
| # TODO |
| pass |
| |
| def validate_transfer_inputs(self, resdb, current_transactions=[]): |
| # store the inputs so that we can check if the asset ids match |
| input_txs = [] |
| input_conditions = [] |
| for input_ in self.inputs: |
| input_txid = input_.fulfills.txid |
| |
| input_tx = resdb.get_transaction(input_txid) |
| |
| if input_tx is None: |
| for ctxn in current_transactions: |
| if ctxn.id == input_txid: |
| input_tx = ctxn |
| |
| if input_tx is None: |
| raise InputDoesNotExist( |
| "input `{}` doesn't exist".format(input_txid)) |
| |
| spent = resdb.get_spent( |
| input_txid, input_.fulfills.output, current_transactions |
| ) |
| if spent: |
| raise DoubleSpend( |
| "input `{}` was already spent".format(input_txid)) |
| |
| output = input_tx.outputs[input_.fulfills.output] |
| input_conditions.append(output) |
| input_txs.append(input_tx) |
| |
| # Validate that all inputs are distinct |
| links = [i.fulfills.to_uri() for i in self.inputs] |
| if len(links) != len(set(links)): |
| raise DoubleSpend('tx "{}" spends inputs twice'.format(self.id)) |
| |
| # validate asset id |
| asset_id = self.get_asset_id(input_txs) |
| if asset_id != self.asset["id"]: |
| raise AssetIdMismatch( |
| ( |
| "The asset id of the input does not" |
| " match the asset id of the" |
| " transaction" |
| ) |
| ) |
| |
| input_amount = sum( |
| [input_condition.amount for input_condition in input_conditions] |
| ) |
| output_amount = sum( |
| [output_condition.amount for output_condition in self.outputs] |
| ) |
| |
| if output_amount != input_amount: |
| raise AmountError( |
| ( |
| "The amount used in the inputs `{}`" |
| " needs to be same as the amount used" |
| " in the outputs `{}`" |
| ).format(input_amount, output_amount) |
| ) |
| |
| if not self.inputs_valid(input_conditions): |
| raise InvalidSignature("Transaction signature is invalid.") |
| |
| return True |