diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index c598775..8c2b2a7 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -16,6 +16,7 @@ __all__ = [ 'KeyGenerator', 'KeyIterator', + 'normalize', 'SignatureFragmentGenerator', 'validate_signature_fragments', ] diff --git a/iota/transaction/__init__.py b/iota/transaction/__init__.py index 8a0ddf4..2414a61 100644 --- a/iota/transaction/__init__.py +++ b/iota/transaction/__init__.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals # Import symbols to package namespace, for backwards-compatibility with # PyOTA 1.1.x. diff --git a/iota/transaction/base.py b/iota/transaction/base.py index da7dd1c..776dba4 100644 --- a/iota/transaction/base.py +++ b/iota/transaction/base.py @@ -1,550 +1,599 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from operator import attrgetter from typing import Iterable, Iterator, List, MutableSequence, \ - Optional, Sequence, Text + Optional, Sequence, Text from iota.codecs import TrytesDecodeError from iota.crypto import Curl, HASH_LENGTH from iota.json import JsonSerializable from iota.transaction.types import BundleHash, Fragment, Nonce, \ - TransactionHash, TransactionTrytes + TransactionHash, TransactionTrytes from iota.trits import int_from_trits, trits_from_int from iota.types import Address, Tag, TryteString, TrytesCompatible __all__ = [ - 'Bundle', - 'Transaction', + 'Bundle', + 'Transaction', ] class Transaction(JsonSerializable): - """ - A transaction that has been attached to the Tangle. - """ - @classmethod - def from_tryte_string(cls, trytes, hash_=None): - # type: (TrytesCompatible, Optional[TransactionHash]) -> Transaction """ - Creates a Transaction object from a sequence of trytes. + A transaction that has been attached to the Tangle. + """ + + @classmethod + def from_tryte_string(cls, trytes, hash_=None): + # type: (TrytesCompatible, Optional[TransactionHash]) -> Transaction + """ + Creates a Transaction object from a sequence of trytes. + + :param trytes: + Raw trytes. Should be exactly 2673 trytes long. + + :param hash_: + The transaction hash, if available. + + If not provided, it will be computed from the transaction + trytes. + """ + tryte_string = TransactionTrytes(trytes) + + if not hash_: + hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(tryte_string.as_trits()) + sponge.squeeze(hash_trits) + + hash_ = TransactionHash.from_trits(hash_trits) + + return cls( + hash_=hash_, + signature_message_fragment=Fragment(tryte_string[0:2187]), + address=Address(tryte_string[2187:2268]), + value=int_from_trits(tryte_string[2268:2295].as_trits()), + legacy_tag=Tag(tryte_string[2295:2322]), + timestamp=int_from_trits(tryte_string[2322:2331].as_trits()), + current_index=int_from_trits(tryte_string[2331:2340].as_trits()), + last_index=int_from_trits(tryte_string[2340:2349].as_trits()), + bundle_hash=BundleHash(tryte_string[2349:2430]), + trunk_transaction_hash=TransactionHash(tryte_string[2430:2511]), + branch_transaction_hash=TransactionHash(tryte_string[2511:2592]), + tag=Tag(tryte_string[2592:2619]), + + attachment_timestamp=int_from_trits( + tryte_string[2619:2628].as_trits()), + + attachment_timestamp_lower_bound=int_from_trits( + tryte_string[2628:2637].as_trits()), + + attachment_timestamp_upper_bound=int_from_trits( + tryte_string[2637:2646].as_trits()), + + nonce=Nonce(tryte_string[2646:2673]), + ) + + def __init__( + self, + hash_, # type: Optional[TransactionHash] + signature_message_fragment, # type: Optional[Fragment] + address, # type: Address + value, # type: int + timestamp, # type: int + current_index, # type: Optional[int] + last_index, # type: Optional[int] + bundle_hash, # type: Optional[BundleHash] + trunk_transaction_hash, # type: Optional[TransactionHash] + branch_transaction_hash, # type: Optional[TransactionHash] + tag, # type: Optional[Tag] + attachment_timestamp, # type: Optional[int] + attachment_timestamp_lower_bound, # type: Optional[int] + attachment_timestamp_upper_bound, # type: Optional[int] + nonce, # type: Optional[Nonce] + legacy_tag=None # type: Optional[Tag] + ): + self.hash = hash_ + """ + Transaction ID, generated by taking a hash of the transaction + trits. + """ + + self.bundle_hash = bundle_hash + """ + Bundle hash, generated by taking a hash of metadata from all the + transactions in the bundle. + """ + + self.address = address + """ + The address associated with this transaction. + + If ``value`` is != 0, the associated address' balance is + adjusted as a result of this transaction. + """ + + self.value = value + """ + Amount to adjust the balance of ``address``. + Can be negative (i.e., for spending inputs). + """ + + self._legacy_tag = legacy_tag + """ + Optional classification legacy_tag applied to this transaction. + """ + + self.nonce = nonce + """ + Unique value used to increase security of the transaction hash. + """ + + self.timestamp = timestamp + """ + Timestamp used to increase the security of the transaction hash. + + .. important:: + This value is easy to forge! + Do not rely on it when resolving conflicts! + """ + + self.current_index = current_index + """ + The position of the transaction inside the bundle. + + For value transfers, the "spend" transaction is generally in the + 0th position, followed by inputs, and the "change" transaction + is last. + """ + + self.last_index = last_index + """ + The position of the final transaction inside the bundle. + """ + + self.trunk_transaction_hash = trunk_transaction_hash + """ + In order to add a transaction to the Tangle, the client must + perform PoW to "approve" two existing transactions, called the + "trunk" and "branch" transactions. + + The trunk transaction is generally used to link transactions + within a bundle. + """ + + self.branch_transaction_hash = branch_transaction_hash + """ + In order to add a transaction to the Tangle, the client must + perform PoW to "approve" two existing transactions, called the + "trunk" and "branch" transactions. + + The branch transaction may be selected strategically to maximize + the bundle's chances of getting confirmed; otherwise it usually + has no significance. + """ + + self.tag = tag + """ + Optional classification tag applied to this transaction. + """ + + self.attachment_timestamp = attachment_timestamp + + self.attachment_timestamp_lower_bound = attachment_timestamp_lower_bound + + self.attachment_timestamp_upper_bound = attachment_timestamp_upper_bound + + self.signature_message_fragment = signature_message_fragment + """ + "Signature/Message Fragment" (note the slash): + + - For inputs, this contains a fragment of the cryptographic + signature, used to verify the transaction (depending on the + security level of the corresponding address, the entire + signature is usually too large to fit into a single + transaction, so it is split across multiple transactions + instead). + + - For other transactions, this contains a fragment of the + message attached to the transaction (if any). This can be + pretty much any value. Like signatures, the message may be + split across multiple transactions if it is too large to fit + inside a single transaction. + """ + + self.is_confirmed = None # type: Optional[bool] + """ + Whether this transaction has been confirmed by neighbor nodes. + Must be set manually via the ``getInclusionStates`` API command. + + References: + + - :py:meth:`iota.api.StrictIota.get_inclusion_states` + - :py:meth:`iota.api.Iota.get_transfers` + """ + + @property + def is_tail(self): + # type: () -> bool + """ + Returns whether this transaction is a tail (first one in the + bundle). + + Because of the way the Tangle is organized, the tail transaction + is generally the last one in the bundle that gets attached, even + though it occupies the first logical position inside the bundle. + """ + return self.current_index == 0 + + @property + def value_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`value`. + """ + # Note that we are padding to 81 *trits*. + return TryteString.from_trits(trits_from_int(self.value, pad=81)) + + @property + def timestamp_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`timestamp`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) + + @property + def current_index_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`current_index`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.current_index, pad=27), + ) + + @property + def last_index_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`last_index`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + + @property + def attachment_timestamp_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp, pad=27), + ) + + @property + def attachment_timestamp_lower_bound_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp_lower_bound`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp_lower_bound, pad=27), + ) + + @property + def attachment_timestamp_upper_bound_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp_upper_bound`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp_upper_bound, pad=27), + ) + + def as_json_compatible(self): + # type: () -> dict + """ + Returns a JSON-compatible representation of the object. + + References: + + - :py:class:`iota.json.JsonEncoder`. + """ + return { + 'hash_': self.hash, + 'signature_message_fragment': self.signature_message_fragment, + 'address': self.address, + 'value': self.value, + 'legacy_tag': self.legacy_tag, + 'timestamp': self.timestamp, + 'current_index': self.current_index, + 'last_index': self.last_index, + 'bundle_hash': self.bundle_hash, + 'trunk_transaction_hash': self.trunk_transaction_hash, + 'branch_transaction_hash': self.branch_transaction_hash, + 'tag': self.tag, + 'attachment_timestamp': self.attachment_timestamp, + + 'attachment_timestamp_lower_bound': + self.attachment_timestamp_lower_bound, + + 'attachment_timestamp_upper_bound': + self.attachment_timestamp_upper_bound, + + 'nonce': self.nonce, + } + + def as_tryte_string(self): + # type: () -> TransactionTrytes + """ + Returns a TryteString representation of the transaction. + """ + return TransactionTrytes( + self.signature_message_fragment + + self.address.address + + self.value_as_trytes + + self.legacy_tag + + self.timestamp_as_trytes + + self.current_index_as_trytes + + self.last_index_as_trytes + + self.bundle_hash + + self.trunk_transaction_hash + + self.branch_transaction_hash + + self.tag + + self.attachment_timestamp_as_trytes + + self.attachment_timestamp_lower_bound_as_trytes + + self.attachment_timestamp_upper_bound_as_trytes + + self.nonce + ) + + def get_signature_validation_trytes(self): + # type: () -> TryteString + """ + Returns the values needed to validate the transaction's + ``signature_message_fragment`` value. + """ + return ( + self.address.address + + self.value_as_trytes + + self.legacy_tag + + self.timestamp_as_trytes + + self.current_index_as_trytes + + self.last_index_as_trytes + ) + + @property + def legacy_tag(self): + # type: () -> Tag + """ + Return the legacy tag of the transaction. + If no legacy tag was set, returns the tag instead. + """ + return self._legacy_tag or self.tag - :param trytes: - Raw trytes. Should be exactly 2673 trytes long. - :param hash_: - The transaction hash, if available. - If not provided, it will be computed from the transaction trytes. - """ - tryte_string = TransactionTrytes(trytes) - - if not hash_: - hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] - - sponge = Curl() - sponge.absorb(tryte_string.as_trits()) - sponge.squeeze(hash_trits) - - hash_ = TransactionHash.from_trits(hash_trits) - - return cls( - hash_ = hash_, - signature_message_fragment = Fragment(tryte_string[0:2187]), - address = Address(tryte_string[2187:2268]), - value = int_from_trits(tryte_string[2268:2295].as_trits()), - legacy_tag = Tag(tryte_string[2295:2322]), - timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), - current_index = int_from_trits(tryte_string[2331:2340].as_trits()), - last_index = int_from_trits(tryte_string[2340:2349].as_trits()), - bundle_hash = BundleHash(tryte_string[2349:2430]), - trunk_transaction_hash = TransactionHash(tryte_string[2430:2511]), - branch_transaction_hash = TransactionHash(tryte_string[2511:2592]), - tag = Tag(tryte_string[2592:2619]), - attachment_timestamp = int_from_trits(tryte_string[2619:2628].as_trits()), - attachment_timestamp_lower_bound = int_from_trits(tryte_string[2628:2637].as_trits()), - attachment_timestamp_upper_bound = int_from_trits(tryte_string[2637:2646].as_trits()), - nonce = Nonce(tryte_string[2646:2673]), - ) - - def __init__( - self, - hash_, # type: Optional[TransactionHash] - signature_message_fragment, # type: Optional[Fragment] - address, # type: Address - value, # type: int - timestamp, # type: int - current_index, # type: Optional[int] - last_index, # type: Optional[int] - bundle_hash, # type: Optional[BundleHash] - trunk_transaction_hash, # type: Optional[TransactionHash] - branch_transaction_hash, # type: Optional[TransactionHash] - tag, # type: Optional[Tag] - attachment_timestamp, # type: Optional[int] - attachment_timestamp_lower_bound, # type: Optional[int] - attachment_timestamp_upper_bound, # type: Optional[int] - nonce, # type: Optional[Nonce] - legacy_tag = None # type: Optional[Tag] - ): - self.hash = hash_ - """ - Transaction ID, generated by taking a hash of the transaction - trits. - """ - - self.bundle_hash = bundle_hash - """ - Bundle hash, generated by taking a hash of metadata from all the - transactions in the bundle. - """ - - self.address = address - """ - The address associated with this transaction. - If ``value`` is != 0, the associated address' balance is adjusted - as a result of this transaction. - """ - - self.value = value - """ - Amount to adjust the balance of ``address``. - Can be negative (i.e., for spending inputs). - """ - - self._legacy_tag = legacy_tag - """ - Optional classification legacy_tag applied to this transaction. - """ - - self.nonce = nonce - """ - Unique value used to increase security of the transaction hash. - """ - - self.timestamp = timestamp - """ - Timestamp used to increase the security of the transaction hash. - - IMPORTANT: This value is easy to forge! - Do not rely on it when resolving conflicts! - """ - - self.current_index = current_index - """ - The position of the transaction inside the bundle. - - For value transfers, the "spend" transaction is generally in the - 0th position, followed by inputs, and the "change" transaction is - last. - """ - - self.last_index = last_index - """ - The position of the final transaction inside the bundle. - """ - - self.trunk_transaction_hash = trunk_transaction_hash - """ - In order to add a transaction to the Tangle, you must perform PoW - to "approve" two existing transactions, called the "trunk" and - "branch" transactions. - - The trunk transaction is generally used to link transactions within - a bundle. - """ - - self.branch_transaction_hash = branch_transaction_hash - """ - In order to add a transaction to the Tangle, you must perform PoW - to "approve" two existing transactions, called the "trunk" and - "branch" transactions. - - The branch transaction generally has no significance. - """ - - self.tag = tag - """ - Optional classification tag applied to this transaction. - """ - - self.attachment_timestamp = attachment_timestamp - - self.attachment_timestamp_lower_bound = attachment_timestamp_lower_bound - - self.attachment_timestamp_upper_bound = attachment_timestamp_upper_bound - - self.signature_message_fragment = signature_message_fragment - """ - "Signature/Message Fragment" (note the slash): - - - For inputs, this contains a fragment of the cryptographic - signature, used to verify the transaction (the entire signature - is too large to fit into a single transaction, so it is split - across multiple transactions instead). - - - For other transactions, this contains a fragment of the message - attached to the transaction (if any). This can be pretty much - any value. Like signatures, the message may be split across - multiple transactions if it is too large to fit inside a single - transaction. - """ - - self.is_confirmed = None # type: Optional[bool] - """ - Whether this transaction has been confirmed by neighbor nodes. - Must be set manually via the ``getInclusionStates`` API command. - - References: - - :py:meth:`iota.api.StrictIota.get_inclusion_states` - - :py:meth:`iota.api.Iota.get_transfers` - """ - - @property - def is_tail(self): - # type: () -> bool - """ - Returns whether this transaction is a tail. - """ - return self.current_index == 0 - - @property - def value_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's value. - """ - # Note that we are padding to 81 _trits_. - return TryteString.from_trits(trits_from_int(self.value, pad=81)) - - @property - def timestamp_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - timestamp. - """ - # Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) - - @property - def current_index_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - ``current_index`` value. - """ - # Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.current_index, pad=27)) - - @property - def last_index_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - ``last_index`` value. - """ - # Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.last_index, pad=27)) - - @property - def attachment_timestamp_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp. - """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp, pad=27)) - - @property - def attachment_timestamp_lower_bound_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp lower bound. +class Bundle(JsonSerializable, Sequence[Transaction]): """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp_lower_bound, pad=27)) + A collection of transactions, treated as an atomic unit when + attached to the Tangle. - @property - def attachment_timestamp_upper_bound_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp upper bound. - """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp_upper_bound, pad=27)) + Note: unlike a block in a blockchain, bundles are not first-class + citizens in IOTA; only transactions get stored in the Tangle. - def as_json_compatible(self): - # type: () -> dict - """ - Returns a JSON-compatible representation of the object. + Instead, Bundles must be inferred by following linked transactions + with the same bundle hash. References: - - :py:class:`iota.json.JsonEncoder`. - """ - return { - 'hash_': self.hash, - 'signature_message_fragment': self.signature_message_fragment, - 'address': self.address, - 'value': self.value, - 'legacy_tag': self.legacy_tag, - 'timestamp': self.timestamp, - 'current_index': self.current_index, - 'last_index': self.last_index, - 'bundle_hash': self.bundle_hash, - 'trunk_transaction_hash': self.trunk_transaction_hash, - 'branch_transaction_hash': self.branch_transaction_hash, - 'tag': self.tag, - 'attachment_timestamp': self.attachment_timestamp, - 'attachment_timestamp_lower_bound': self.attachment_timestamp_lower_bound, - 'attachment_timestamp_upper_bound': self.attachment_timestamp_upper_bound, - 'nonce': self.nonce, - } - - def as_tryte_string(self): - # type: () -> TransactionTrytes - """ - Returns a TryteString representation of the transaction. - """ - return TransactionTrytes( - self.signature_message_fragment - + self.address.address - + self.value_as_trytes - + self.legacy_tag - + self.timestamp_as_trytes - + self.current_index_as_trytes - + self.last_index_as_trytes - + self.bundle_hash - + self.trunk_transaction_hash - + self.branch_transaction_hash - + self.tag - + self.attachment_timestamp_as_trytes - + self.attachment_timestamp_lower_bound_as_trytes - + self.attachment_timestamp_upper_bound_as_trytes - + self.nonce - ) - - def get_signature_validation_trytes(self): - # type: () -> TryteString - """ - Returns the values needed to validate the transaction's - ``signature_message_fragment`` value. - """ - return ( - self.address.address - + self.value_as_trytes - + self.legacy_tag - + self.timestamp_as_trytes - + self.current_index_as_trytes - + self.last_index_as_trytes - ) - - @property - def legacy_tag(self): - # type: () -> Tag - """ - Return the legacy tag of the transaction. - If no legacy tag was set, returns the tag instead. - """ - return self._legacy_tag or self.tag - - -class Bundle(JsonSerializable, Sequence[Transaction]): - """ - A collection of transactions, treated as an atomic unit when - attached to the Tangle. - - Note: unlike a block in a blockchain, bundles are not first-class - citizens in IOTA; only transactions get stored in the Tangle. - - Instead, Bundles must be inferred by following linked transactions - with the same bundle hash. - References: - :py:class:`iota.commands.extended.get_bundles.GetBundlesCommand` - """ - @classmethod - def from_tryte_strings(cls, trytes): - # type: (Iterable[TryteString]) -> Bundle """ - Creates a Bundle object from a list of tryte values. - """ - return cls(map(Transaction.from_tryte_string, trytes)) - def __init__(self, transactions=None): - # type: (Optional[Iterable[Transaction]]) -> None - super(Bundle, self).__init__() + @classmethod + def from_tryte_strings(cls, trytes): + # type: (Iterable[TryteString]) -> Bundle + """ + Creates a Bundle object from a list of tryte values. + """ + return cls(map(Transaction.from_tryte_string, trytes)) - self.transactions = [] # type: List[Transaction] - if transactions: - self.transactions.extend( - sorted(transactions, key=attrgetter('current_index')) - ) + def __init__(self, transactions=None): + # type: (Optional[Iterable[Transaction]]) -> None + super(Bundle, self).__init__() - self._is_confirmed = None # type: Optional[bool] - """ - Whether this bundle has been confirmed by neighbor nodes. - Must be set manually. + self.transactions = [] # type: List[Transaction] + if transactions: + self.transactions.extend( + sorted(transactions, key=attrgetter('current_index')), + ) - References: - - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` - """ + self._is_confirmed = None # type: Optional[bool] + """ + Whether this bundle has been confirmed by neighbor nodes. + Must be set manually. - def __contains__(self, transaction): - # type: (Transaction) -> bool - return transaction in self.transactions + References: - def __getitem__(self, index): - # type: (int) -> Transaction - return self.transactions[index] + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ - def __iter__(self): - # type: () -> Iterator[Transaction] - return iter(self.transactions) - - def __len__(self): - # type: () -> int - return len(self.transactions) - - @property - def is_confirmed(self): - # type: () -> Optional[bool] - """ - Returns whether this bundle has been confirmed by neighbor nodes. + def __contains__(self, transaction): + # type: (Transaction) -> bool + return transaction in self.transactions - This attribute must be set manually. + def __getitem__(self, index): + # type: (int) -> Transaction + return self.transactions[index] - References: - - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` - """ - return self._is_confirmed + def __iter__(self): + # type: () -> Iterator[Transaction] + return iter(self.transactions) - @is_confirmed.setter - def is_confirmed(self, new_is_confirmed): - # type: (bool) -> None - """ - Sets the ``is_confirmed`` for the bundle. - """ - self._is_confirmed = new_is_confirmed + def __len__(self): + # type: () -> int + return len(self.transactions) - for txn in self: - txn.is_confirmed = new_is_confirmed + @property + def is_confirmed(self): + # type: () -> Optional[bool] + """ + Returns whether this bundle has been confirmed by neighbor + nodes. - @property - def hash(self): - # type: () -> Optional[BundleHash] - """ - Returns the hash of the bundle. + This attribute must be set manually. - This value is determined by inspecting the bundle's tail - transaction, so in a few edge cases, it may be incorrect. + References: - If the bundle has no transactions, this method returns `None`. - """ - try: - return self.tail_transaction.bundle_hash - except IndexError: - return None - - @property - def tail_transaction(self): - # type: () -> Transaction - """ - Returns the tail transaction of the bundle. - """ - return self[0] + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ + return self._is_confirmed - def get_messages(self, errors='drop'): - # type: (Text) -> List[Text] - """ - Attempts to decipher encoded messages from the transactions in the - bundle. - - :param errors: - How to handle trytes that can't be converted, or bytes that can't - be decoded using UTF-8: - - 'drop': drop the trytes from the result. - - 'strict': raise an exception. - - 'replace': replace with a placeholder character. - - 'ignore': omit the invalid tryte/byte sequence. - """ - decode_errors = 'strict' if errors == 'drop' else errors + @is_confirmed.setter + def is_confirmed(self, new_is_confirmed): + # type: (bool) -> None + """ + Sets the ``is_confirmed`` for the bundle. + """ + self._is_confirmed = new_is_confirmed - messages = [] + for txn in self: + txn.is_confirmed = new_is_confirmed - for group in self.group_transactions(): - # Ignore inputs. - if group[0].value < 0: - continue + @property + def hash(self): + # type: () -> Optional[BundleHash] + """ + Returns the hash of the bundle. - message_trytes = TryteString(b'') - for txn in group: - message_trytes += txn.signature_message_fragment + This value is determined by inspecting the bundle's tail + transaction, so in a few edge cases, it may be incorrect. - if message_trytes: + If the bundle has no transactions, this method returns ``None``. + """ try: - messages.append(message_trytes.decode(decode_errors)) - except (TrytesDecodeError, UnicodeDecodeError): - if errors != 'drop': - raise - - return messages - - def as_tryte_strings(self, head_to_tail=False): - # type: (bool) -> List[TransactionTrytes] - """ - Returns TryteString representations of the transactions in this - bundle. - - :param head_to_tail: - Determines the order of the transactions: - - - ``True``: head txn first, tail txn last. - - ``False`` (default): tail txn first, head txn last. - - Note that the order is reversed by default, as this is the way - bundles are typically broadcast to the Tangle. - """ - transactions = self if head_to_tail else reversed(self) - return [t.as_tryte_string() for t in transactions] - - def as_json_compatible(self): - # type: () -> List[dict] - """ - Returns a JSON-compatible representation of the object. - - References: - - :py:class:`iota.json.JsonEncoder`. - """ - return [txn.as_json_compatible() for txn in self] - - def group_transactions(self): - # type: () -> List[List[Transaction]] - """ - Groups transactions in the bundle by address. - """ - groups = [] - - if self: - last_txn = self.tail_transaction - current_group = [last_txn] - for current_txn in self.transactions[1:]: - # Transactions are grouped by address, so as long as the - # address stays consistent from one transaction to another, we - # are still in the same group. - if current_txn.address == last_txn.address: - current_group.append(current_txn) - else: - groups.append(current_group) - current_group = [current_txn] - - last_txn = current_txn - - if current_group: - groups.append(current_group) - - return groups - - + return self.tail_transaction.bundle_hash + except IndexError: + return None + + @property + def tail_transaction(self): + # type: () -> Transaction + """ + Returns the tail transaction of the bundle. + """ + return self[0] + + def get_messages(self, errors='drop'): + # type: (Text) -> List[Text] + """ + Attempts to decipher encoded messages from the transactions in + the bundle. + + :param errors: + How to handle trytes that can't be converted, or bytes that + can't be decoded using UTF-8: + + 'drop' + Drop the trytes from the result. + + 'strict' + Raise an exception. + + 'replace' + Replace with a placeholder character. + + 'ignore' + Omit the invalid tryte/byte sequence. + """ + decode_errors = 'strict' if errors == 'drop' else errors + + messages = [] + + for group in self.group_transactions(): + # Ignore inputs. + if group[0].value < 0: + continue + + message_trytes = TryteString(b'') + for txn in group: + message_trytes += txn.signature_message_fragment + + if message_trytes: + try: + messages.append(message_trytes.decode(decode_errors)) + except (TrytesDecodeError, UnicodeDecodeError): + if errors != 'drop': + raise + + return messages + + def as_tryte_strings(self, head_to_tail=False): + # type: (bool) -> List[TransactionTrytes] + """ + Returns TryteString representations of the transactions in this + bundle. + + :param head_to_tail: + Determines the order of the transactions: + + - ``True``: head txn first, tail txn last. + - ``False`` (default): tail txn first, head txn last. + + Note that the order is reversed by default, as this is the + way bundles are typically broadcast to the Tangle. + """ + transactions = self if head_to_tail else reversed(self) + return [t.as_tryte_string() for t in transactions] + + def as_json_compatible(self): + # type: () -> List[dict] + """ + Returns a JSON-compatible representation of the object. + + References: + + - :py:class:`iota.json.JsonEncoder`. + """ + return [txn.as_json_compatible() for txn in self] + + def group_transactions(self): + # type: () -> List[List[Transaction]] + """ + Groups transactions in the bundle by address. + """ + groups = [] + + if self: + last_txn = self.tail_transaction + current_group = [last_txn] + for current_txn in self.transactions[1:]: + # Transactions are grouped by address, so as long as the + # address stays consistent from one transaction to + # another, we are still in the same group. + if current_txn.address == last_txn.address: + current_group.append(current_txn) + else: + groups.append(current_group) + current_group = [current_txn] + + last_txn = current_txn + + if current_group: + groups.append(current_group) + + return groups diff --git a/iota/transaction/creation.py b/iota/transaction/creation.py index 9dd1d16..e4c89fd 100644 --- a/iota/transaction/creation.py +++ b/iota/transaction/creation.py @@ -2,8 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, Iterator, List, MutableSequence, Optional, \ - Sequence, Tuple +from typing import Iterable, Iterator, List, Optional, Sequence from six import PY2 @@ -33,8 +32,14 @@ class ProposedTransaction(Transaction): tangle and publish/store. """ - def __init__(self, address, value, tag=None, message=None, timestamp=None): - # type: (Address, int, Optional[Tag], Optional[TryteString], Optional[int]) -> None + def __init__( + self, + address, # type: Address + value, # type: int + tag=None, # type: Optional[Tag] + message=None, # type: Optional[TryteString] + timestamp=None, # type: Optional[int] + ): if not timestamp: timestamp = get_current_timestamp() @@ -44,7 +49,8 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): timestamp=timestamp, value=value, - # These values will be populated when the bundle is finalized. + # These values will be populated when the bundle is + # finalized. bundle_hash=None, current_index=None, hash_=None, @@ -54,8 +60,8 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): attachment_timestamp_lower_bound=0, attachment_timestamp_upper_bound=0, - # These values start out empty; they will be populated when the - # node does PoW. + # These values start out empty; they will be populated when + # the node does PoW. branch_transaction_hash=TransactionHash(b''), nonce=Nonce(b''), trunk_transaction_hash=TransactionHash(b''), @@ -91,10 +97,12 @@ def increment_legacy_tag(self): bundle hashes when finalizing a bundle. References: - - https://github.com/iotaledger/iota.lib.py/issues/84 + + - https://github.com/iotaledger/iota.lib.py/issues/84 """ - self._legacy_tag =\ + self._legacy_tag = ( Tag.from_trits(add_trits(self.legacy_tag.as_trits(), [1])) + ) Transfer = ProposedTransaction @@ -110,8 +118,12 @@ class ProposedBundle(Bundle, Sequence[ProposedTransaction]): unit when attached to the Tangle. """ - def __init__(self, transactions=None, inputs=None, change_address=None): - # type: (Optional[Iterable[ProposedTransaction]], Optional[Iterable[Address]], Optional[Address]) -> None + def __init__( + self, + transactions=None, # type: Optional[Iterable[ProposedTransaction]] + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + ): super(ProposedBundle, self).__init__() self._transactions = [] # type: List[ProposedTransaction] @@ -168,12 +180,13 @@ def balance(self): Returns the bundle balance. In order for a bundle to be valid, its balance must be 0: - - A positive balance means that there aren't enough inputs to - cover the spent amount. - Add more inputs using :py:meth:`add_inputs`. - - A negative balance means that there are unspent inputs. - Use :py:meth:`send_unspent_inputs_to` to send the unspent - inputs to a "change" address. + - A positive balance means that there aren't enough inputs to + cover the spent amount; add more inputs using + :py:meth:`add_inputs`. + + - A negative balance means that there are unspent inputs; use + :py:meth:`send_unspent_inputs_to` to send the unspent inputs + to a "change" address. """ return sum(t.value for t in self._transactions) @@ -185,7 +198,6 @@ def tag(self): """ for txn in reversed(self): # type: ProposedTransaction if txn.tag: - # noinspection PyTypeChecker return txn.tag return Tag(b'') @@ -196,7 +208,8 @@ def as_json_compatible(self): Returns a JSON-compatible representation of the object. References: - - :py:class:`iota.json.JsonEncoder`. + + - :py:class:`iota.json.JsonEncoder`. """ return [txn.as_json_compatible() for txn in self] @@ -242,14 +255,15 @@ def add_inputs(self, inputs): """ Adds inputs to spend in the bundle. - Note that each input may require multiple transactions, in order to - hold the entire signature. + Note that each input may require multiple transactions, in order + to hold the entire signature. :param inputs: - Addresses to use as the inputs for this bundle. + Addresses to use as the inputs for this bundle. - IMPORTANT: Must have ``balance`` and ``key_index`` attributes! - Use :py:meth:`iota.api.get_inputs` to prepare inputs. + .. important:: + Must have ``balance`` and ``key_index`` attributes! + Use :py:meth:`iota.api.get_inputs` to prepare inputs. """ if self.hash: raise RuntimeError('Bundle is already finalized.') @@ -340,13 +354,13 @@ def finalize(self): sponge = Kerl() last_index = len(self) - 1 - for (i, txn) in enumerate(self): # type: Tuple[int, ProposedTransaction] + for i, txn in enumerate(self): txn.current_index = i txn.last_index = last_index sponge.absorb(txn.get_signature_validation_trytes().as_trits()) - bundle_hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] + bundle_hash_trits = [0] * HASH_LENGTH sponge.squeeze(bundle_hash_trits) bundle_hash = BundleHash.from_trits(bundle_hash_trits) @@ -355,7 +369,9 @@ def finalize(self): # https://github.com/iotaledger/iota.lib.py/issues/84 if any(13 in part for part in normalize(bundle_hash)): # Increment the legacy tag and try again. - tail_transaction = self.tail_transaction # type: ProposedTransaction + tail_transaction = ( + self.tail_transaction + ) # type: ProposedTransaction tail_transaction.increment_legacy_tag() else: break @@ -381,12 +397,13 @@ def sign_inputs(self, key_generator): txn = self[i] if txn.value < 0: - # In order to sign the input, we need to know the index of - # the private key used to generate it. + # In order to sign the input, we need to know the index + # of the private key used to generate it. if txn.address.key_index is None: raise with_context( exc=ValueError( - 'Unable to sign input {input}; ``key_index`` is None ' + 'Unable to sign input {input}; ' + '``key_index`` is None ' '(``exc.context`` has more info).'.format( input=txn.address, ), @@ -400,7 +417,8 @@ def sign_inputs(self, key_generator): if txn.address.security_level is None: raise with_context( exc=ValueError( - 'Unable to sign input {input}; ``security_level`` is None ' + 'Unable to sign input {input}; ' + '``security_level`` is None ' '(``exc.context`` has more info).'.format( input=txn.address, ), @@ -415,8 +433,8 @@ def sign_inputs(self, key_generator): i += txn.address.security_level else: - # No signature needed (nor even possible, in some cases); skip - # this transaction. + # No signature needed (nor even possible, in some + # cases); skip this transaction. i += 1 def sign_input_at(self, start_index, private_key): @@ -425,18 +443,20 @@ def sign_input_at(self, start_index, private_key): Signs the input at the specified index. :param start_index: - The index of the first input transaction. + The index of the first input transaction. - If necessary, the resulting signature will be split across - multiple transactions automatically (i.e., if an input has - ``security_level=2``, you still only need to call - :py:meth:`sign_input_at` once). + If necessary, the resulting signature will be split across + multiple transactions automatically (i.e., if an input has + ``security_level=2``, you still only need to call + :py:meth:`sign_input_at` once). :param private_key: - The private key that will be used to generate the signature. + The private key that will be used to generate the signature. - Important: be sure that the private key was generated using the - correct seed, or the resulting signature will be invalid! + .. important:: + Be sure that the private key was generated using the + correct seed, or the resulting signature will be + invalid! """ if not self.hash: raise RuntimeError('Cannot sign inputs until bundle is finalized.') @@ -452,8 +472,8 @@ def _create_input_transactions(self, addy): address=addy, tag=self.tag, - # Spend the entire address balance; if necessary, we will add a - # change transaction to the bundle. + # Spend the entire address balance; if necessary, we will + # add a change transaction to the bundle. value=-addy.balance, )) diff --git a/iota/transaction/types.py b/iota/transaction/types.py index 3a32c57..b92da78 100644 --- a/iota/transaction/types.py +++ b/iota/transaction/types.py @@ -1,97 +1,98 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from iota.crypto import FRAGMENT_LENGTH from iota.exceptions import with_context from iota.types import Hash, TryteString, TrytesCompatible __all__ = [ - 'BundleHash', - 'Fragment', - 'TransactionHash', - 'TransactionTrytes', - 'Nonce' + 'BundleHash', + 'Fragment', + 'TransactionHash', + 'TransactionTrytes', + 'Nonce' ] class BundleHash(Hash): - """ - A TryteString that acts as a bundle hash. - """ - pass + """ + A TryteString that acts as a bundle hash. + """ + pass class TransactionHash(Hash): - """ - A TryteString that acts as a transaction hash. - """ - pass + """ + A TryteString that acts as a transaction hash. + """ + pass class Fragment(TryteString): - """ - A signature/message fragment in a transaction. - """ - LEN = FRAGMENT_LENGTH + """ + A signature/message fragment in a transaction. + """ + LEN = FRAGMENT_LENGTH - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Fragment, self).__init__(trytes, pad=self.LEN) + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Fragment, self).__init__(trytes, pad=self.LEN) - if len(self._trytes) > self.LEN: - raise with_context( - exc = ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )), + if len(self._trytes) > self.LEN: + raise with_context( + exc=ValueError('{cls} values must be {len} trytes long.'.format( + cls=type(self).__name__, + len=self.LEN + )), - context = { - 'trytes': trytes, - }, - ) + context={ + 'trytes': trytes, + }, + ) class TransactionTrytes(TryteString): - """ - A TryteString representation of a Transaction. - """ - LEN = 2673 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(TransactionTrytes, self).__init__(trytes, pad=self.LEN) - - if len(self._trytes) > self.LEN: - raise with_context( - exc = ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )), - - context = { - 'trytes': trytes, - }, - ) + """ + A TryteString representation of a Transaction. + """ + LEN = 2673 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(TransactionTrytes, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise with_context( + exc=ValueError('{cls} values must be {len} trytes long.'.format( + cls=type(self).__name__, + len=self.LEN + )), + + context={ + 'trytes': trytes, + }, + ) + class Nonce(TryteString): - """ - A TryteString that acts as a transaction nonce. - """ - LEN = 27 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Nonce, self).__init__(trytes, pad=self.LEN) - - if len(self._trytes) > self.LEN: - raise with_context( - exc = ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )), - - context = { - 'trytes': trytes, - }, - ) \ No newline at end of file + """ + A TryteString that acts as a transaction nonce. + """ + LEN = 27 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Nonce, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise with_context( + exc=ValueError('{cls} values must be {len} trytes long.'.format( + cls=type(self).__name__, + len=self.LEN + )), + + context={ + 'trytes': trytes, + }, + ) diff --git a/iota/transaction/utils.py b/iota/transaction/utils.py index e7a6297..b9505f8 100644 --- a/iota/transaction/utils.py +++ b/iota/transaction/utils.py @@ -1,73 +1,77 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from calendar import timegm as unix_timestamp from datetime import datetime - from typing import Text from iota import STANDARD_UNITS from iota.exceptions import with_context __all__ = [ - 'convert_value_to_standard_unit', - 'get_current_timestamp', + 'convert_value_to_standard_unit', + 'get_current_timestamp', ] def convert_value_to_standard_unit(value, symbol='i'): - # type: (Text, Text) -> float - """ + # type: (Text, Text) -> float + """ Converts between any two standard units of iota. :param value: - Value (affixed) to convert. For example: '1.618 Mi'. + Value (affixed) to convert. For example: '1.618 Mi'. :param symbol: - Unit symbol of iota to convert to. For example: 'Gi'. + Unit symbol of iota to convert to. For example: 'Gi'. :return: - Float as units of given symbol to convert to. - """ - try: - # Get input value - value_tuple = value.split() - amount = float(value_tuple[0]) - except (ValueError, IndexError, AttributeError): - raise with_context(ValueError('Value to convert is not valid.'), - context = { - 'value': value, - }, - ) + Float as units of given symbol to convert to. + """ + try: + # Get input value + value_tuple = value.split() + amount = float(value_tuple[0]) + except (ValueError, IndexError, AttributeError): + raise with_context( + ValueError('Value to convert is not valid.'), + + context={ + 'value': value, + }, + ) + + try: + # Set unit symbols and find factor/multiplier. + unit_symbol_from = value_tuple[1] + unit_factor_from = float(STANDARD_UNITS[unit_symbol_from]) + unit_factor_to = float(STANDARD_UNITS[symbol]) + except (KeyError, IndexError): + # Invalid symbol or no factor + raise with_context( + ValueError('Invalid IOTA unit.'), + + context={ + 'value': value, + 'symbol': symbol, + }, + ) - try: - # Set unit symbols and find factor/multiplier. - unit_symbol_from = value_tuple[1] - unit_factor_from = float(STANDARD_UNITS[unit_symbol_from]) - unit_factor_to = float(STANDARD_UNITS[symbol]) - except (KeyError, IndexError): - # Invalid symbol or no factor - raise with_context(ValueError('Invalid IOTA unit.'), - context = { - 'value': value, - 'symbol': symbol, - }, - ) + return amount * (unit_factor_from / unit_factor_to) - return amount * (unit_factor_from / unit_factor_to) def get_current_timestamp(): - # type: () -> int - """ - Returns the current timestamp, used to set ``timestamp`` for new - :py:class:`ProposedTransaction` objects. + # type: () -> int + """ + Returns the current timestamp, used to set ``timestamp`` for new + :py:class:`ProposedTransaction` objects. - Split out into a separate function so that it can be mocked during - unit tests. - """ - # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, but - # for compatibility with Python 2, we have to do it the old-fashioned - # way. - # http://stackoverflow.com/q/2775864/ - return unix_timestamp(datetime.utcnow().timetuple()) + Split out into a separate function so that it can be mocked during + unit tests. + """ + # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, but + # for compatibility with Python 2, we have to do it the + # old-fashioned way. + # http://stackoverflow.com/q/2775864/ + return unix_timestamp(datetime.utcnow().timetuple()) diff --git a/iota/transaction/validator.py b/iota/transaction/validator.py index d1088eb..e9308cd 100644 --- a/iota/transaction/validator.py +++ b/iota/transaction/validator.py @@ -1,19 +1,17 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals -from typing import Generator, List, Optional, Text, Tuple +from typing import Generator, List, Optional, Text from iota.crypto.kerl import Kerl from iota.crypto.signing import validate_signature_fragments from iota.transaction.base import Bundle, Transaction __all__ = [ - 'BundleValidator', + 'BundleValidator', ] - -# # In very rare cases, the IOTA protocol may switch hash algorithms. # When this happens, the IOTA Foundation will create a snapshot, so # that all new objects on the Tangle use the new hash algorithm. @@ -21,235 +19,250 @@ # However, the snapshot will still contain references to addresses # created using the legacy hash algorithm, so the bundle validator has # to be able to use that as a fallback when validation fails. -# -SUPPORTED_SPONGE = Kerl -LEGACY_SPONGE = None # Curl +SUPPORTED_SPONGE = Kerl +LEGACY_SPONGE = None # Curl class BundleValidator(object): - """ - Checks a bundle and its transactions for problems. - """ - def __init__(self, bundle): - # type: (Bundle) -> None - super(BundleValidator, self).__init__() - - self.bundle = bundle - - self._errors = [] # type: Optional[List[Text]] - self._validator = self._create_validator() - - @property - def errors(self): - # type: () -> List[Text] """ - Returns all errors found with the bundle. + Checks a bundle and its transactions for problems. """ - try: - self._errors.extend(self._validator) # type: List[Text] - except StopIteration: - pass - - return self._errors - def is_valid(self): - # type: () -> bool - """ - Returns whether the bundle is valid. - """ - if not self._errors: - try: - # We only have to check for a single error to determine if the - # bundle is valid or not. - self._errors.append(next(self._validator)) - except StopIteration: - pass - - return not self._errors - - def _create_validator(self): - # type: () -> Generator[Text, None, None] - """ - Creates a generator that does all the work. - """ - # Group transactions by address to make it easier to iterate over - # inputs. - grouped_transactions = self.bundle.group_transactions() - - # Define a few expected values. - bundle_hash = self.bundle.hash - last_index = len(self.bundle) - 1 - - # Track a few others as we go along. - balance = 0 - - # Check indices and balance first. - # Note that we use a counter to keep track of the current index, - # since at this point we can't trust that the transactions have - # correct ``current_index`` values. - counter = 0 - for group in grouped_transactions: - for txn in group: - balance += txn.value - - if txn.bundle_hash != bundle_hash: - yield 'Transaction {i} has invalid bundle hash.'.format( - i = counter, - ) - - if txn.current_index != counter: - yield ( - 'Transaction {i} has invalid current index value ' - '(expected {i}, actual {actual}).'.format( - actual = txn.current_index, - i = counter, - ) - ) - - if txn.last_index != last_index: - yield ( - 'Transaction {i} has invalid last index value ' - '(expected {expected}, actual {actual}).'.format( - actual = txn.last_index, - expected = last_index, - i = counter, + def __init__(self, bundle): + # type: (Bundle) -> None + super(BundleValidator, self).__init__() + + self.bundle = bundle + + self._errors = [] # type: Optional[List[Text]] + self._validator = self._create_validator() + + @property + def errors(self): + # type: () -> List[Text] + """ + Returns all errors found with the bundle. + """ + try: + self._errors.extend(self._validator) # type: List[Text] + except StopIteration: + pass + + return self._errors + + def is_valid(self): + # type: () -> bool + """ + Returns whether the bundle is valid. + """ + if not self._errors: + try: + # We only have to check for a single error to determine + # if the bundle is valid or not. + self._errors.append(next(self._validator)) + except StopIteration: + pass + + return not self._errors + + def _create_validator(self): + # type: () -> Generator[Text, None, None] + """ + Creates a generator that does all the work. + """ + # Group transactions by address to make it easier to iterate + # over inputs. + grouped_transactions = self.bundle.group_transactions() + + # Define a few expected values. + bundle_hash = self.bundle.hash + last_index = len(self.bundle) - 1 + + # Track a few others as we go along. + balance = 0 + + # Check indices and balance first. + # Note that we use a counter to keep track of the current index, + # since at this point we can't trust that the transactions have + # correct ``current_index`` values. + counter = 0 + for group in grouped_transactions: + for txn in group: + balance += txn.value + + if txn.bundle_hash != bundle_hash: + yield 'Transaction {i} has invalid bundle hash.'.format( + i=counter, + ) + + if txn.current_index != counter: + yield ( + 'Transaction {i} has invalid current index value ' + '(expected {i}, actual {actual}).'.format( + actual=txn.current_index, + i=counter, + ) + ) + + if txn.last_index != last_index: + yield ( + 'Transaction {i} has invalid last index value ' + '(expected {expected}, actual {actual}).'.format( + actual=txn.last_index, + expected=last_index, + i=counter, + ) + ) + + counter += 1 + + # Bundle must be balanced (spends must match inputs). + if balance != 0: + yield ( + 'Bundle has invalid balance ' + '(expected 0, actual {actual}).'.format( + actual=balance, + ) ) - ) - counter += 1 - - # Bundle must be balanced (spends must match inputs). - if balance != 0: - yield ( - 'Bundle has invalid balance (expected 0, actual {actual}).'.format( - actual = balance, + # Signature validation is only meaningful if the transactions + # are otherwise valid. + if not self._errors: + signature_validation_queue = [] # type: List[List[Transaction]] + + for group in grouped_transactions: + # Signature validation only applies to inputs. + if group[0].value >= 0: + continue + + validate_group_signature = True + for j, txn in enumerate(group): + if (j > 0) and (txn.value != 0): + # Input is malformed; signature fragments after + # the first should have zero value. + yield ( + 'Transaction {i} has invalid value ' + '(expected 0, actual {actual}).'.format( + actual=txn.value, + + # If we get to this point, we know that + # the ``current_index`` value for each + # transaction can be trusted. + i=txn.current_index, + ) + ) + + # We won't be able to validate the signature, + # but continue anyway, so that we can check that + # the other transactions in the group have the + # correct ``value``. + validate_group_signature = False + continue + + # After collecting the signature fragment from each + # transaction in the group, queue them up to run through + # the validator. + # + # We have to perform signature validation separately so + # that we can try different algorithms (for + # backwards-compatibility). + # + # References: + # + # - https://github.com/iotaledger/kerl#kerl-integration-in-iota + if validate_group_signature: + signature_validation_queue.append(group) + + # Once we've finished checking the attributes from each + # transaction in the bundle, go back and validate + # signatures. + if signature_validation_queue: + # ``yield from`` is an option here, but for + # compatibility with Python 2 clients, we will do it the + # old-fashioned way. + for error in self._get_bundle_signature_errors( + signature_validation_queue + ): + yield error + + def _get_bundle_signature_errors(self, groups): + # type: (List[List[Transaction]]) -> List[Text] + """ + Validates the signature fragments in the bundle. + + :return: + List of error messages. + If empty, signature fragments are valid. + """ + # Start with the currently-supported hash algo. + current_pos = None + current_errors = [] + for current_pos, group in enumerate(groups): + error = self._get_group_signature_error(group, SUPPORTED_SPONGE) + if error: + current_errors.append(error) + + # Pause and retry with the legacy algo. + break + + # If validation failed, then go back and try with the legacy + # algo (only applies if we are currently transitioning to a new + # algo). + if current_errors and LEGACY_SPONGE: + for group in groups: + # noinspection PyTypeChecker + if self._get_group_signature_error(group, LEGACY_SPONGE): + # Legacy algo doesn't work, either; no point in + # continuing. + break + else: + # If we get here, then we were able to validate the + # signature fragments successfully using the legacy + # algorithm. + return [] + + # If we get here, then validation also failed when using the + # legacy algorithm. + + # At this point, we know that the bundle is invalid, but we will + # continue validating with the supported algorithm anyway, so + # that we can return an error message for every invalid input. + current_errors.extend(filter(None, ( + self._get_group_signature_error(group, SUPPORTED_SPONGE) + for group in groups[current_pos + 1:] + ))) + + return current_errors + + @staticmethod + def _get_group_signature_error(group, sponge_type): + # type: (List[Transaction], type) -> Optional[Text] + """ + Validates the signature fragments for a group of transactions + using the specified sponge type. + + Note: this method assumes that the transactions in the group + have already passed basic validation (see + :py:meth:`_create_validator`). + + :return: + - ``None``: Indicates that the signature fragments are valid. + - ``Text``: Error message indicating the fragments are invalid. + """ + validate_group_signature = validate_signature_fragments( + fragments=[txn.signature_message_fragment for txn in group], + hash_=group[0].bundle_hash, + public_key=group[0].address, + sponge_type=sponge_type, ) - ) - - # Signature validation is only meaningful if the transactions are - # otherwise valid. - if not self._errors: - signature_validation_queue = [] # type: List[List[Transaction]] - - for group in grouped_transactions: - # Signature validation only applies to inputs. - if group[0].value >= 0: - continue - - validate_group_signature = True - for j, txn in enumerate(group): # type: Tuple[int, Transaction] - if (j > 0) and (txn.value != 0): - # Input is malformed; signature fragments after the first - # should have zero value. - yield ( - 'Transaction {i} has invalid value ' - '(expected 0, actual {actual}).'.format( - actual = txn.value, - - # If we get to this point, we know that the - # ``current_index`` value for each transaction can be - # trusted. - i = txn.current_index, - ) - ) - # We won't be able to validate the signature, but continue - # anyway, so that we can check that the other transactions - # in the group have the correct ``value``. - validate_group_signature = False - continue - - # After collecting the signature fragment from each transaction - # in the group, queue them up to run through the validator. - # - # We have to perform signature validation separately so that we - # can try different algorithms (for backwards-compatibility). - # - # References: - # - https://github.com/iotaledger/kerl#kerl-integration-in-iota if validate_group_signature: - signature_validation_queue.append(group) - - # Once we've finished checking the attributes from each - # transaction in the bundle, go back and validate signatures. - if signature_validation_queue: - for error in self._get_bundle_signature_errors(signature_validation_queue): - yield error + return None - def _get_bundle_signature_errors(self, groups): - # type: (List[List[Transaction]]) -> List[Text] - """ - Validates the signature fragments in the bundle. - - :return: - List of error messages. If empty, signature fragments are valid. - """ - # Start with the currently-supported hash algo. - current_pos = None - current_errors = [] - for current_pos, group in enumerate(groups): # type: Tuple[int, List[Transaction]] - error = self._get_group_signature_error(group, SUPPORTED_SPONGE) - if error: - current_errors.append(error) - - # Pause and retry with the legacy algo. - break - - # If validation failed, then go back and try with the legacy algo - # (only applies if we are currently transitioning to a new algo). - if current_errors and LEGACY_SPONGE: - for group in groups: - # noinspection PyTypeChecker - if self._get_group_signature_error(group, LEGACY_SPONGE): - # Legacy algo doesn't work, either; no point in continuing. - break - else: - # If we get here, then we were able to validate the signature - # fragments successfully using the legacy algorithm. - return [] - - # If we get here, then validation also failed when using the legacy - # algorithm. - - # At this point, we know that the bundle is invalid, but we will - # continue validating with the supported algorithm anyway, so that - # we can return an error message for every invalid input. - current_errors.extend(filter(None, ( - self._get_group_signature_error(group, SUPPORTED_SPONGE) - for group in groups[current_pos+1:] - ))) - - return current_errors - - @staticmethod - def _get_group_signature_error(group, sponge_type): - # type: (List[Transaction], type) -> Optional[Text] - """ - Validates the signature fragments for a group of transactions using - the specified sponge type. - - Note: this method assumes that the transactions in the group have - already passed basic validation (see :py:meth:`_create_validator`). - - :return: - - ``None``: Indicates that the signature fragments are valid. - - ``Text``: Error message indicating the fragments are invalid. - """ - validate_group_signature =\ - validate_signature_fragments( - fragments = [txn.signature_message_fragment for txn in group], - hash_ = group[0].bundle_hash, - public_key = group[0].address, - sponge_type = sponge_type, - ) - - if validate_group_signature: - return None - - return ( - 'Transaction {i} has invalid signature ' - '(using {fragments} fragments).'.format( - fragments = len(group), - i = group[0].current_index, - ) - ) + return ( + 'Transaction {i} has invalid signature ' + '(using {fragments} fragments).'.format( + fragments=len(group), + i=group[0].current_index, + ) + )