diff --git a/.travis.yml b/.travis.yml index c28a2109..012f3ce0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: trusty language: python python: - '2.7' diff --git a/iota/api.py b/iota/api.py index 049d31fc..251bfa4f 100644 --- a/iota/api.py +++ b/iota/api.py @@ -133,7 +133,7 @@ def default_min_weight_magnitude(self): Returns the default ``min_weight_magnitude`` value to use for API requests. """ - return 9 if self.testnet else 15 + return 9 if self.testnet else 14 def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 3c910f52..c75f0201 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -4,17 +4,17 @@ from abc import ABCMeta, abstractmethod as abstract_method from importlib import import_module -from inspect import isabstract as is_abstract, isclass as is_class, \ - getmembers as get_members +from inspect import getmembers as get_members, isabstract as is_abstract, \ + isclass as is_class from pkgutil import walk_packages from types import ModuleType -from typing import Dict, Mapping, Optional, Text, Union +from typing import Any, Dict, Mapping, Optional, Text, Union import filters as f -from iota.exceptions import with_context -from six import with_metaclass, string_types +from six import string_types, with_metaclass from iota.adapter import BaseAdapter +from iota.exceptions import with_context __all__ = [ 'BaseCommand', @@ -49,10 +49,14 @@ def discover_commands(package, recursively=True): commands = {} - for _, name, is_package in walk_packages(package.__path__): + for _, name, is_package in walk_packages(package.__path__, package.__name__ + '.'): # Loading the module is good enough; the CommandMeta metaclass will # ensure that any commands in the module get registered. - sub_package = import_module(package.__name__ + '.' + name) + + # Prefix in name module move to function "walk_packages" for fix + # conflict with names importing packages + # Bug https://github.com/iotaledger/iota.lib.py/issues/63 + sub_package = import_module(name) # Index any command classes that we find. for (_, obj) in get_members(sub_package): @@ -99,7 +103,7 @@ def __init__(self, adapter): self.response = None # type: dict def __call__(self, **kwargs): - # type: (dict) -> dict + # type: (**Any) -> dict """ Sends the command to the node. """ diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 89355c3c..b14c9a8a 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -2,11 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional, List +from typing import List, Optional import filters as f -from iota import Address +from iota import Address from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.crypto.addresses import AddressGenerator diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index d76492ab..2c1a5703 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -5,7 +5,7 @@ from typing import Generator, Iterable, List, MutableSequence from iota import Address, TRITS_PER_TRYTE, TrytesCompatible -from iota.crypto import Curl +from iota.crypto.kerl import Kerl from iota.crypto.signing import KeyGenerator, KeyIterator from iota.crypto.types import Digest, PrivateKey, Seed from iota.exceptions import with_context @@ -157,7 +157,7 @@ def address_from_digest(digest): """ address_trits = [0] * (Address.LEN * TRITS_PER_TRYTE) # type: MutableSequence[int] - sponge = Curl() + sponge = Kerl() sponge.absorb(digest.as_trits()) sponge.squeeze(address_trits) diff --git a/iota/crypto/kerl/__init__.py b/iota/crypto/kerl/__init__.py new file mode 100644 index 00000000..489aceb5 --- /dev/null +++ b/iota/crypto/kerl/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from .pykerl import * diff --git a/iota/crypto/kerl/conv.py b/iota/crypto/kerl/conv.py new file mode 100644 index 00000000..8d54888e --- /dev/null +++ b/iota/crypto/kerl/conv.py @@ -0,0 +1,153 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +BYTE_HASH_LENGTH = 48 +TRIT_HASH_LENGTH = 243 + +tryte_table = { + '9': [ 0, 0, 0], # 0 + 'A': [ 1, 0, 0], # 1 + 'B': [-1, 1, 0], # 2 + 'C': [ 0, 1, 0], # 3 + 'D': [ 1, 1, 0], # 4 + 'E': [-1, -1, 1], # 5 + 'F': [ 0, -1, 1], # 6 + 'G': [ 1, -1, 1], # 7 + 'H': [-1, 0, 1], # 8 + 'I': [ 0, 0, 1], # 9 + 'J': [ 1, 0, 1], # 10 + 'K': [-1, 1, 1], # 11 + 'L': [ 0, 1, 1], # 12 + 'M': [ 1, 1, 1], # 13 + 'N': [-1, -1, -1], # -13 + 'O': [ 0, -1, -1], # -12 + 'P': [ 1, -1, -1], # -11 + 'Q': [-1, 0, -1], # -10 + 'R': [ 0, 0, -1], # -9 + 'S': [ 1, 0, -1], # -8 + 'T': [-1, 1, -1], # -7 + 'U': [ 0, 1, -1], # -6 + 'V': [ 1, 1, -1], # -5 + 'W': [-1, -1, 0], # -4 + 'X': [ 0, -1, 0], # -3 + 'Y': [ 1, -1, 0], # -2 + 'Z': [-1, 0, 0], # -1 + } + +# Invert for trit -> tryte lookup +trit_table = {tuple(v): k for k, v in tryte_table.items()} + +def trytes_to_trits(trytes): + trits = [] + for tryte in trytes: + trits.extend(tryte_table[tryte]) + + return trits + +def trits_to_trytes(trits): + trytes = [] + trits_chunks = [trits[i:i + 3] for i in range(0, len(trits), 3)] + + for trit in trits_chunks: + trytes.extend(trit_table[tuple(trit)]) + + return ''.join(trytes) + +def convertToTrits(bytes_k): + bigInt = convertBytesToBigInt(bytes_k) + trits = convertBigintToBase(bigInt, 3, TRIT_HASH_LENGTH) + return trits + +def convertToBytes(trits): + bigInt = convertBaseToBigint(trits, 3) + bytes_k = convertBigintToBytes(bigInt) + return bytes_k + +def convertBytesToBigInt(ba): + # copy of array + bytesArray = list(map(lambda x: x, ba)) + + # number sign in MSB + signum = (1 if bytesArray[0] >= 0 else -1) + + if signum == -1: + # sub1 + for pos in reversed(range(len(bytesArray))): + sub = (bytesArray[pos] & 0xFF) - 1 + bytesArray[pos] = (sub if sub <= 0x7F else sub - 0x100) + if bytesArray[pos] != -1: + break + + # 1-compliment + bytesArray = list(map(lambda x: ~x, bytesArray)) + + # sum magnitudes and set sign + return sum((x & 0xFF) << pos * 8 for (pos, x) in + enumerate(reversed(bytesArray))) * signum + + +def convertBigintToBytes(big): + bytesArrayTemp = [(abs(big) >> pos * 8) % (1 << 8) for pos in + range(48)] + + # big endian and balanced + bytesArray = list(map(lambda x: (x if x <= 0x7F else x - 0x100), + reversed(bytesArrayTemp))) + + if big < 0: + # 1-compliment + bytesArray = list(map(lambda x: ~x, bytesArray)) + + # add1 + for pos in reversed(range(len(bytesArray))): + add = (bytesArray[pos] & 0xFF) + 1 + bytesArray[pos] = (add if add <= 0x7F else add - 0x100) + if bytesArray[pos] != 0: + break + + return bytesArray + +def convertBaseToBigint(array, base): + bigint = 0 + + for i in range(len(array)): + bigint += array[i] * (base ** i) + + return bigint + +def convertBigintToBase(bigInt, base, length): + result = [] + + is_negative = bigInt < 0 + quotient = abs(bigInt) + + MAX = (base-1) // 2 + if is_negative: + MAX = base // 2 + + for i in range(length): + quotient, remainder = divmod(quotient, base) + + if remainder > MAX: + # Lend 1 to the next place so we can make this digit negative. + quotient += 1 + remainder -= base + + if is_negative: + remainder = remainder * -1 + + result.append(remainder) + + return result + +def convert_sign(byte): + """ + Convert between signed and unsigned bytes + """ + if byte < 0: + return 256 + byte + elif byte > 127: + return -256 + byte + return byte diff --git a/iota/crypto/kerl/pykerl.py b/iota/crypto/kerl/pykerl.py new file mode 100644 index 00000000..ea4187ec --- /dev/null +++ b/iota/crypto/kerl/pykerl.py @@ -0,0 +1,140 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from sha3 import keccak_384 +from six import PY2 +from typing import MutableSequence, Optional + +from iota.crypto.kerl import conv +from iota.exceptions import with_context + +__all__ = [ + 'Kerl', +] + +BYTE_HASH_LENGTH = 48 +TRIT_HASH_LENGTH = 243 + +class Kerl(object): + k = None # type: keccak_384 + + def __init__(self): + self.reset() + + def absorb(self, trits, offset=0, length=None): + # type: (MutableSequence[int], int, Optional[int]) -> None + """ + Absorb trits into the sponge from a buffer. + + :param trits: + Buffer that contains the trits to absorb. + + :param offset: + Starting offset in ``trits``. + + :param length: + Number of trits to absorb. Defaults to ``len(trits)``. + """ + # Pad input if necessary, so that it can be divided evenly into + # hashes. + # Note that this operation creates a COPY of ``trits``; the + # incoming buffer is not modified! + pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) + trits += [0] * (TRIT_HASH_LENGTH - pad) + + if length is None: + length = len(trits) + + if length < 1: + raise with_context( + exc = ValueError('Invalid length passed to ``absorb``.'), + + context = { + 'trits': trits, + 'offset': offset, + 'length': length, + }, + ) + + while offset < length: + stop = min(offset + TRIT_HASH_LENGTH, length) + + # If we're copying over a full chunk, zero last trit + if stop - offset == TRIT_HASH_LENGTH: + trits[stop - 1] = 0 + + signed_nums = conv.convertToBytes(trits[offset:stop]) + + # Convert signed bytes into their equivalent unsigned representation + # In order to use Python's built-in bytes type + unsigned_bytes = bytearray(conv.convert_sign(b) for b in signed_nums) + + self.k.update(unsigned_bytes) + + offset += TRIT_HASH_LENGTH + + def squeeze(self, trits, offset=0, length=None): + # type: (MutableSequence[int], int, Optional[int]) -> None + """ + Squeeze trits from the sponge into a buffer. + + :param trits: + Buffer that will hold the squeezed trits. + + IMPORTANT: If ``trits`` is too small, it will be extended! + + :param offset: + Starting offset in ``trits``. + + :param length: + Number of trits to squeeze from the sponge. + + If not specified, defaults to :py:data:`TRIT_HASH_LENGTH` (i.e., + by default, we will try to squeeze exactly 1 hash). + """ + # Pad input if necessary, so that it can be divided evenly into + # hashes. + pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) + trits += [0] * (TRIT_HASH_LENGTH - pad) + + if length is None: + # By default, we will try to squeeze one hash. + # Note that this is different than ``absorb``. + length = len(trits) or TRIT_HASH_LENGTH + + if length < 1: + raise with_context( + exc = ValueError('Invalid length passed to ``squeeze``.'), + + context = { + 'trits': trits, + 'offset': offset, + 'length': length, + }, + ) + + while offset < length: + unsigned_hash = self.k.digest() + + if PY2: + unsigned_hash = map(ord, unsigned_hash) # type: ignore + + signed_hash = [conv.convert_sign(b) for b in unsigned_hash] + + trits_from_hash = conv.convertToTrits(signed_hash) + trits_from_hash[TRIT_HASH_LENGTH - 1] = 0 + + stop = min(TRIT_HASH_LENGTH, length-offset) + trits[offset:offset+stop] = trits_from_hash[0:stop] + + flipped_bytes = bytearray(conv.convert_sign(~b) for b in unsigned_hash) + + # Reset internal state before feeding back in + self.reset() + self.k.update(flipped_bytes) + + offset += TRIT_HASH_LENGTH + + def reset(self): + self.k = keccak_384() diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index be82d9de..ce2337e6 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -23,7 +23,7 @@ Number of trits that a Curl sponge stores internally. """ -NUMBER_OF_ROUNDS = 27 +NUMBER_OF_ROUNDS = 81 """ Number of iterations to perform per transform operation. diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 9ce42fe1..e741c1ec 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -6,8 +6,9 @@ from six import PY2 -from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash -from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH +from iota import Hash, TRITS_PER_TRYTE, TryteString, TrytesCompatible +from iota.crypto import FRAGMENT_LENGTH, HASH_LENGTH +from iota.crypto.kerl import Kerl from iota.crypto.types import PrivateKey, Seed from iota.exceptions import with_context @@ -237,8 +238,12 @@ def __init__(self, seed, start, step, security_level): }, ) + # In order to work correctly, the seed must be padded so that it is + # a multiple of 81 trytes. + seed += b'9' * (Hash.LEN - ((len(seed) % Hash.LEN) or Hash.LEN)) + self.security_level = security_level - self.seed = seed + self.seed_as_trits = seed.as_trits() self.start = start self.step = step @@ -257,7 +262,7 @@ def __next__(self): sponge = self._create_sponge(self.current) key = [0] * (self.fragment_length * self.security_level) - buffer = [0] * HASH_LENGTH # type: MutableSequence[int] + buffer = [0] * len(self.seed_as_trits) for fragment_seq in range(self.security_level): # Squeeze trits from the buffer and append them to the key, one @@ -270,7 +275,10 @@ def __next__(self): key_stop = key_start + HASH_LENGTH - key[key_start:key_stop] = buffer + # Ensure we only capture one hash from the buffer, in case + # it is longer than that (i.e., if the seed is longer than 81 + # trytes). + key[key_start:key_stop] = buffer[0:HASH_LENGTH] private_key =\ PrivateKey.from_trits( @@ -293,11 +301,11 @@ def advance(self): self.current += self.step def _create_sponge(self, index): - # type: (int) -> Curl + # type: (int) -> Kerl """ - Prepares the Curl sponge for the generator. + Prepares the hash sponge for the generator. """ - seed = self.seed.as_trits() # type: MutableSequence[int] + seed = self.seed_as_trits[:] for i in range(index): # Treat ``seed`` like a really big number and add ``index``. @@ -311,12 +319,12 @@ def _create_sponge(self, index): else: break - sponge = Curl() + sponge = Kerl() sponge.absorb(seed) # Squeeze all of the trits out of the sponge and re-absorb them. - # Note that Curl transforms several times per operation, so this - # sequence is not as redundant as it looks at first glance. + # Note that the sponge transforms several times per operation, so + # this sequence is not as redundant as it looks at first glance. sponge.squeeze(seed) sponge.reset() sponge.absorb(seed) @@ -338,7 +346,7 @@ def __init__(self, private_key, hash_): self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) self._iteration = -1 self._normalized_hash = normalize(hash_) - self._sponge = Curl() + self._sponge = Kerl() def __iter__(self): # type: () -> SignatureFragmentGenerator @@ -388,8 +396,13 @@ def __next__(self): next = __next__ -def validate_signature_fragments(fragments, hash_, public_key): - # type: (Sequence[TryteString], Hash, TryteString) -> bool +def validate_signature_fragments( + fragments, + hash_, + public_key, + sponge_type = Kerl, +): + # type: (Sequence[TryteString], Hash, TryteString, type) -> bool """ Returns whether a sequence of signature fragments is valid. @@ -404,12 +417,15 @@ def validate_signature_fragments(fragments, hash_, public_key): :param public_key: The public key value used to verify the signature digest (usually a :py:class:`iota.types.Address` instance). + + :param sponge_type: + The class used to create the cryptographic sponge (i.e., Curl or Kerl). """ checksum = [0] * (HASH_LENGTH * len(fragments)) normalized_hash = normalize(hash_) for (i, fragment) in enumerate(fragments): # type: Tuple[int, TryteString] - outer_sponge = Curl() + outer_sponge = sponge_type() # If there are more than 3 iterations, loop back around to the # start. @@ -418,7 +434,7 @@ def validate_signature_fragments(fragments, hash_, public_key): buffer = [] for (j, hash_trytes) in enumerate(fragment.iter_chunks(Hash.LEN)): # type: Tuple[int, TryteString] buffer = hash_trytes.as_trits() # type: MutableSequence[int] - inner_sponge = Curl() + inner_sponge = sponge_type() # Note the sign flip compared to ``SignatureFragmentGenerator``. for _ in range(13 + normalized_chunk[j]): @@ -432,7 +448,7 @@ def validate_signature_fragments(fragments, hash_, public_key): checksum[i*HASH_LENGTH:(i+1)*HASH_LENGTH] = buffer actual_public_key = [0] * HASH_LENGTH # type: MutableSequence[int] - addy_sponge = Curl() + addy_sponge = sponge_type() addy_sponge.absorb(checksum) addy_sponge.squeeze(actual_public_key) diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 7cfd9a74..e22f8f0c 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -4,7 +4,8 @@ from typing import MutableSequence, Optional, Tuple -from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH +from iota.crypto import FRAGMENT_LENGTH, HASH_LENGTH +from iota.crypto.kerl import Kerl from iota.exceptions import with_context from iota.transaction.base import Bundle from iota.types import Hash, TryteString, TrytesCompatible @@ -157,7 +158,7 @@ def get_digest(self): hash_trits = fragment_trits[hash_start:hash_end] # type: MutableSequence[int] for k in range(26): - sponge = Curl() + sponge = Kerl() sponge.absorb(hash_trits) sponge.squeeze(hash_trits) @@ -170,7 +171,7 @@ def get_digest(self): # Note that we will do this once per fragment in the key, so the # longer the key is, the longer the digest will be. # - sponge = Curl() + sponge = Kerl() sponge.absorb(key_fragment) sponge.squeeze(hash_trits) diff --git a/iota/multisig/crypto/addresses.py b/iota/multisig/crypto/addresses.py index c39ba6e5..d19a7e27 100644 --- a/iota/multisig/crypto/addresses.py +++ b/iota/multisig/crypto/addresses.py @@ -4,7 +4,8 @@ from typing import List, Optional -from iota.crypto import Curl, HASH_LENGTH +from iota.crypto import HASH_LENGTH +from iota.crypto.kerl import Kerl from iota.crypto.types import Digest from iota.multisig.types import MultisigAddress @@ -39,7 +40,7 @@ def __init__(self): only generate a single address. """ - self._sponge = Curl() + self._sponge = Kerl() def add_digest(self, digest): # type: (Digest) -> None diff --git a/iota/transaction/creation.py b/iota/transaction/creation.py index 49d2d791..c0875fe2 100644 --- a/iota/transaction/creation.py +++ b/iota/transaction/creation.py @@ -7,7 +7,8 @@ from six import PY2 -from iota.crypto import Curl, HASH_LENGTH +from iota.crypto import HASH_LENGTH +from iota.crypto.kerl import Kerl from iota.crypto.signing import KeyGenerator from iota.crypto.types import PrivateKey from iota.exceptions import with_context @@ -310,7 +311,7 @@ def finalize(self): ) # Generate bundle hash. - sponge = Curl() + sponge = Kerl() last_index = len(self) - 1 for (i, txn) in enumerate(self): # type: Tuple[int, ProposedTransaction] diff --git a/iota/transaction/validator.py b/iota/transaction/validator.py index 7227121a..0666caba 100644 --- a/iota/transaction/validator.py +++ b/iota/transaction/validator.py @@ -4,6 +4,7 @@ from typing import Generator, List, Optional, Text, Tuple +from iota.crypto.kerl import Kerl from iota.crypto.signing import validate_signature_fragments from iota.transaction.base import Bundle, Transaction @@ -11,6 +12,20 @@ '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. +# +# 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 + + class BundleValidator(object): """ Checks a bundle and its transactions for problems. @@ -114,19 +129,20 @@ def _create_validator(self): # 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 - signature_valid = True - signature_fragments = [] + 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 amount ' + 'Transaction {i} has invalid value ' '(expected 0, actual {actual}).'.format( actual = txn.value, @@ -140,29 +156,100 @@ def _create_validator(self): # 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``. - signature_valid = False + validate_group_signature = False continue - signature_fragments.append(txn.signature_message_fragment) - # After collecting the signature fragment from each transaction - # in the group, run it through the validator. - if signature_valid: - signature_valid = validate_signature_fragments( - fragments = signature_fragments, - hash_ = txn.bundle_hash, - public_key = txn.address, - ) + # 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 + + def _get_bundle_signature_errors(self, groups): + # type: (List[List[Transaction]]) -> List[Text] + """ + Validates the signature fragments in the bundle. - if not signature_valid: - yield ( - 'Transaction {i} has invalid signature ' - '(using {fragments} fragments).'.format( - fragments = len(signature_fragments), + :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. - # If we get to this point, we know that the - # ``current_index`` value for each transaction can be - # trusted. - i = group[0].current_index, - ) - ) + 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, + ) + ) diff --git a/iota/types.py b/iota/types.py index a7ba36dc..d5a9a4cb 100644 --- a/iota/types.py +++ b/iota/types.py @@ -13,7 +13,8 @@ text_type from iota import TRITS_PER_TRYTE, TrytesCodec -from iota.crypto import Curl, HASH_LENGTH +from iota.crypto import HASH_LENGTH +from iota.crypto.kerl import Kerl from iota.exceptions import with_context from iota.json import JsonSerializable @@ -790,13 +791,13 @@ def _generate_checksum(self): """ checksum_trits = [] # type: MutableSequence[int] - sponge = Curl() + sponge = Kerl() sponge.absorb(self.address.as_trits()) sponge.squeeze(checksum_trits) checksum_length = AddressChecksum.LEN * TRITS_PER_TRYTE - return TryteString.from_trits(checksum_trits[:checksum_length]) + return TryteString.from_trits(checksum_trits[-checksum_length:]) class AddressChecksum(TryteString): diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2a9acf13 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py index 250379d5..373719d8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function from codecs import StreamReader, open -from sys import version_info from setuptools import find_packages, setup @@ -15,41 +14,14 @@ long_description = f.read() -## -# For compatibility with versions of pip < 9, we will determine -# dependencies at runtime. -# Maybe once Travis upgrades their containers to use a newer version, -# we'll switch to the newer syntax (: -install_dependencies = [ - 'filters', - 'six', - - # ``security`` extra wasn't introduced until 2.4.1 - # http://docs.python-requests.org/en/latest/community/updates/#id35 - 'requests[security] >= 2.4.1', -] - -unit_test_dependencies = [ - 'nose', - ] - -if version_info[0] < 3: - install_dependencies.extend([ - 'typing', - ]) - - unit_test_dependencies.extend([ - 'mock', # 'mock; python_version < "3.0"', - ]) - - ## # Off we go! +# noinspection SpellCheckingInspection setup( name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = '1.2.0b1', + version = '2.0.0a1', long_description = long_description, @@ -68,7 +40,17 @@ ], }, - install_requires = install_dependencies, + install_requires = [ + 'filters', + 'pysha3', + + # ``security`` extra wasn't introduced until 2.4.1 + # http://docs.python-requests.org/en/latest/community/updates/#id35 + 'requests[security] >= 2.4.1', + + 'six', + 'typing; python_version < "3.0"', + ], extras_require = { 'ccurl': ['pyota-ccurl'], @@ -76,7 +58,10 @@ test_suite = 'test', test_loader = 'nose.loader:TestLoader', - tests_require = unit_test_dependencies, + tests_require = [ + 'mock; python_version < "3.0"', + 'nose', + ], license = 'MIT', diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index d706e462..7ca24c14 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -154,32 +154,32 @@ def test_single_transaction(self): # This value is computed automatically, so it has to be real. hash_ = TransactionHash( - b'TAOICZV9ZSXIZINMNRLOLCWNLL9IDKGVWTJITNGU' - b'HAIKLHZLBZWOQ9HJSODUDISTYGIYPWTYDCFMVRBQN' + b'UGQBSMKGNXXWDCS9XZCFTPUXFADCT9I9KCNQGUXK' + b'NDJDUXLWODOVJQWJHCLWTODAELDXGL9SMQYQZFWHE', ), address = Address( b'TESTVALUE9DONTUSEINPRODUCTION99999OCSGVF' - b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC' + b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC', ), bundle_hash = BundleHash( - b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD' - b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U' + b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD' + b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U', ), branch_transaction_hash = TransactionHash( b'TESTVALUE9DONTUSEINPRODUCTION99999BBCEDI' - b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV' + b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV', ), trunk_transaction_hash = TransactionHash( b'TESTVALUE9DONTUSEINPRODUCTION999999ARAYA' - b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS' + b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS', ), ) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index 5eb287ef..55b73f0b 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -323,14 +323,14 @@ def setUp(self): self.addy_1 =\ Address( - b'ESCYAARULFBXJRETFWOFWGAURYZHTLYBLVJTNSTK' - b'EPHCNLMLMHBPVBDRLYTMQOWPKCFMCQUFRCOVYRTQZ', + b'NYMWLBUJEISSACZZBRENC9HEHYQXHCGQHSNHVCEA' + b'ZDCTEVNGSDUEKTSYBSQGMVJRIEDHWDYSEYCFAZAH9', ) self.addy_2 =\ Address( - b'VWTLNSGVCDEGJKEFZOHTHZLUYEZGDDWZZWBJTAPE' - b'WNOUOAKTEQHCVIMJGTOCDFCEQJTZ9LFIKEYFH9WFA', + b'NTPSEVZHQITARYWHIRTSIFSERINLRYVXLGIQKKHY' + b'IWYTLQUUHDWSOVXLIKVJTYZBFKLABWRBFYVSMD9NB', ) def test_wireup(self): @@ -382,13 +382,13 @@ def test_security_level(self): 'addresses': [ Address( - b'BKWTRXXUNKVSDWDYXP9TXTFFQNJHOONPDBJVJRUF' - b'FKGTTKZOTDOOEEFRVRBJCIXKYTYCRJO9VVRUETVHD', + b'ERBTZTPT9SKDQEGETKMZLYNRQMZYZIDENGWCSGRF' + b'9TLURIEFVKUBSWOIMLMWTWMWTTHSUREPISXDPLCQC', ), Address( - b'GBUHYIWFMYNTVIIODGRUQSGRAUTRJWFWXECUKTDH' - b'K9GKDXCZALJILASXFCEWSDFRUVXYOHGVNVOLOJ9DU', + b'QVHEMGYHVMCFAISJKTWPFSKDAFRZHXQZK9E9KOUQ' + b'LOLVBN9BFAZDDY9O9EYYMHMDWZAKXI9OPBPEYM9FC', ), ], }, @@ -405,7 +405,7 @@ def test_get_addresses_online(self): 'duration': 18, 'hashes': [ - 'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + 'TESTVALUE9DONTUSEINPRODUCTION99999ITQLQN' 'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', ], }) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 48a7fdea..ea243596 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -6,7 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type +from six import binary_type, iterkeys from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ TryteString @@ -398,6 +398,103 @@ def test_fail_inputs_contents_invalid(self): # noinspection SpellCheckingInspection class PrepareTransferCommandTestCase(TestCase): + """ + Generating validation data using the JS lib: + + .. code-block:: javascript + + var Bundle = require('./lib/crypto/bundle/bundle'), + Converter = require('./lib/crypto/converter/converter'), + IOTA = require('./lib/iota'), + Signing = require('./lib/crypto/signing/signing'), + Utils = require('./lib/utils/utils'); + + // Set the seed that will be used to generate signing keys. + // Skip this step if there are no inputs. + var seed = 'SEED9GOES9HERE'; + + // Specify constant timestamp value to use for transactions. + var timestamp = 1482938294; + + // Specify (optional) tag to attach to transactions. + // IMPORTANT: This must be exactly 27 trytes long! + var tag = 'TAG'; + tag += '9'.repeat(Math.max(27-tag.length, 0)); + + // Define parameters we will use to generate inputs/signatures. + var inputs = [ + {balance: 100, keyIndex: 0, securityLevel: 3}, + ... + ]; + + // Assemble the bundle. + var bundle = new Bundle(); + var iota = new IOTA(); + + // Add spend transaction(s) to the bundle. + // See ``lib/crypto/bundle/bundle.js:Bundle.prototype.addEntry`` + bundle.addEntry(1, 'RECIPIENT9ADDY', 42, tag, timestamp); + ... + + // Count the number of spend transactions. + // We'll need this later. + // Skip this step if there are no inputs. + var currentIndex = bundle.bundle.length; + + // Add input transaction(s) to the bundle. + // Skip this step if there are no inputs. + for(var i=0; i