Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit 5d16af1

Browse files
committed
[#10] Minor reorg.
- Moved signature functionality into new `PrivateKey.sign_bundle_at` method. - Split out `BundleValidator` into own module to avoid circular import. - Renamed `iota.transaction.{read,write}` to `base` and `creation`, respectively. - Removed some unit tests that would conflict with multisig (might add back later tho).
1 parent 52a3927 commit 5d16af1

File tree

9 files changed

+269
-340
lines changed

9 files changed

+269
-340
lines changed

iota/commands/extended/get_bundles.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from iota.commands.core.get_trytes import GetTrytesCommand
1212
from iota.exceptions import with_context
1313
from iota.filters import Trytes
14-
from iota.transaction import BundleValidator
14+
from iota.transaction.validator import BundleValidator
1515

1616
__all__ = [
1717
'GetBundlesCommand',

iota/crypto/types.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from typing import MutableSequence, Optional, Tuple
66

7-
from iota import Hash, TryteString, TrytesCompatible
87
from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH
98
from iota.exceptions import with_context
9+
from iota.transaction.base import Bundle
10+
from iota.types import Hash, TryteString, TrytesCompatible
1011

1112
__all__ = [
1213
'Digest',
@@ -179,3 +180,88 @@ def get_digest(self):
179180
digest[fragment_start:fragment_end] = hash_trits
180181

181182
return Digest(TryteString.from_trits(digest), self.key_index)
183+
184+
def sign_bundle_at(self, bundle, start_index):
185+
# type: (Bundle, int) -> None
186+
"""
187+
Signs the input at the specified index.
188+
189+
:param start_index:
190+
The index of the first input transaction.
191+
192+
If necessary, the resulting signature will be split across
193+
subsequent transactions automatically.
194+
"""
195+
196+
if not bundle.hash:
197+
raise with_context(
198+
exc = ValueError('Cannot sign inputs without a bundle hash!'),
199+
200+
context = {
201+
'bundle': bundle,
202+
'key_index': self.key_index,
203+
'start_index': start_index,
204+
},
205+
)
206+
207+
from iota.crypto.signing import SignatureFragmentGenerator
208+
signature_fragment_generator = SignatureFragmentGenerator(self, bundle.hash)
209+
210+
# We can only fit one signature fragment into each transaction,
211+
# so we have to split the entire signature.
212+
for j in range(self.security_level):
213+
# Do lots of validation before we attempt to sign the
214+
# transaction, and attach lots of context info to any exception.
215+
# This method is likely to be invoked at a very low level in the
216+
# application, so if anything goes wrong, we want to make sure
217+
# it's as easy to troubleshoot as possible!
218+
try:
219+
txn = bundle[start_index+j]
220+
except IndexError as e:
221+
raise with_context(
222+
exc = e,
223+
224+
context = {
225+
'bundle': bundle,
226+
'key_index': self.key_index,
227+
'current_index': start_index + j,
228+
},
229+
)
230+
231+
# Only inputs can be signed.
232+
if txn.value > 0:
233+
raise with_context(
234+
exc =
235+
ValueError(
236+
'Attempting to sign non-input transaction #{i} '
237+
'(value={value}).'.format(
238+
i = txn.current_index,
239+
value = txn.value,
240+
),
241+
),
242+
243+
context = {
244+
'bundle': bundle,
245+
'key_index': self.key_index,
246+
'start_index': start_index,
247+
},
248+
)
249+
250+
if txn.signature_message_fragment:
251+
raise with_context(
252+
exc =
253+
ValueError(
254+
'Attempting to sign input transaction #{i}, '
255+
'but it has a non-empty fragment (is it already signed?).'.format(
256+
i = txn.current_index,
257+
),
258+
),
259+
260+
context = {
261+
'bundle': bundle,
262+
'key_index': self.key_index,
263+
'start_index': start_index,
264+
},
265+
)
266+
267+
txn.signature_message_fragment = next(signature_fragment_generator)

iota/transaction/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from __future__ import absolute_import, division, print_function, \
33
unicode_literals
44

5-
# Import symbols for backwards-compatibility with PyOTA 1.1.x.
6-
from .read import *
5+
# Import symbols to package namespace, for backwards-compatibility with
6+
# PyOTA 1.1.x.
7+
from .base import *
8+
from .creation import *
79
from .types import *
810
from .utils import *
9-
from .write import *
11+
from .validator import *

iota/transaction/read.py renamed to iota/transaction/base.py

Lines changed: 2 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
unicode_literals
44

55
from operator import attrgetter
6-
from typing import Generator, Iterable, Iterator, List, MutableSequence, \
7-
Optional, Sequence, Text, Tuple
6+
from typing import Iterable, Iterator, List, MutableSequence, \
7+
Optional, Sequence, Text
88

99
from iota.codecs import TrytesDecodeError
1010
from iota.crypto import Curl, HASH_LENGTH
11-
from iota.crypto.signing import validate_signature_fragments
1211
from iota.json import JsonSerializable
1312
from iota.transaction.types import BundleHash, Fragment, TransactionHash, \
1413
TransactionTrytes
@@ -17,7 +16,6 @@
1716

1817
__all__ = [
1918
'Bundle',
20-
'BundleValidator',
2119
'Transaction',
2220
]
2321

@@ -482,158 +480,3 @@ def group_transactions(self):
482480
return groups
483481

484482

485-
class BundleValidator(object):
486-
"""
487-
Checks a bundle and its transactions for problems.
488-
"""
489-
def __init__(self, bundle):
490-
# type: (Bundle) -> None
491-
super(BundleValidator, self).__init__()
492-
493-
self.bundle = bundle
494-
495-
self._errors = [] # type: Optional[List[Text]]
496-
self._validator = self._create_validator()
497-
498-
@property
499-
def errors(self):
500-
# type: () -> List[Text]
501-
"""
502-
Returns all errors found with the bundle.
503-
"""
504-
try:
505-
self._errors.extend(self._validator) # type: List[Text]
506-
except StopIteration:
507-
pass
508-
509-
return self._errors
510-
511-
def is_valid(self):
512-
# type: () -> bool
513-
"""
514-
Returns whether the bundle is valid.
515-
"""
516-
if not self._errors:
517-
try:
518-
# We only have to check for a single error to determine if the
519-
# bundle is valid or not.
520-
self._errors.append(next(self._validator))
521-
except StopIteration:
522-
pass
523-
524-
return not self._errors
525-
526-
def _create_validator(self):
527-
# type: () -> Generator[Text]
528-
"""
529-
Creates a generator that does all the work.
530-
"""
531-
# Group transactions by address to make it easier to iterate over
532-
# inputs.
533-
grouped_transactions = self.bundle.group_transactions()
534-
535-
# Define a few expected values.
536-
bundle_hash = self.bundle.hash
537-
last_index = len(self.bundle) - 1
538-
539-
# Track a few others as we go along.
540-
balance = 0
541-
542-
# Check indices and balance first.
543-
# Note that we use a counter to keep track of the current index,
544-
# since at this point we can't trust that the transactions have
545-
# correct ``current_index`` values.
546-
counter = 0
547-
for group in grouped_transactions:
548-
for txn in group:
549-
balance += txn.value
550-
551-
if txn.bundle_hash != bundle_hash:
552-
yield 'Transaction {i} has invalid bundle hash.'.format(
553-
i = counter,
554-
)
555-
556-
if txn.current_index != counter:
557-
yield (
558-
'Transaction {i} has invalid current index value '
559-
'(expected {i}, actual {actual}).'.format(
560-
actual = txn.current_index,
561-
i = counter,
562-
)
563-
)
564-
565-
if txn.last_index != last_index:
566-
yield (
567-
'Transaction {i} has invalid last index value '
568-
'(expected {expected}, actual {actual}).'.format(
569-
actual = txn.last_index,
570-
expected = last_index,
571-
i = counter,
572-
)
573-
)
574-
575-
counter += 1
576-
577-
# Bundle must be balanced (spends must match inputs).
578-
if balance != 0:
579-
yield (
580-
'Bundle has invalid balance (expected 0, actual {actual}).'.format(
581-
actual = balance,
582-
)
583-
)
584-
585-
# Signature validation is only meaningful if the transactions are
586-
# otherwise valid.
587-
if not self._errors:
588-
for group in grouped_transactions:
589-
# Signature validation only applies to inputs.
590-
if group[0].value >= 0:
591-
continue
592-
593-
signature_valid = True
594-
signature_fragments = []
595-
for j, txn in enumerate(group): # type: Tuple[int, Transaction]
596-
if (j > 0) and (txn.value != 0):
597-
# Input is malformed; signature fragments after the first
598-
# should have zero value.
599-
yield (
600-
'Transaction {i} has invalid amount '
601-
'(expected 0, actual {actual}).'.format(
602-
actual = txn.value,
603-
604-
# If we get to this point, we know that the
605-
# ``current_index`` value for each transaction can be
606-
# trusted.
607-
i = txn.current_index,
608-
)
609-
)
610-
611-
# We won't be able to validate the signature, but continue
612-
# anyway, so that we can check that the other transactions
613-
# in the group have the correct ``value``.
614-
signature_valid = False
615-
continue
616-
617-
signature_fragments.append(txn.signature_message_fragment)
618-
619-
# After collecting the signature fragment from each transaction
620-
# in the group, run it through the validator.
621-
if signature_valid:
622-
signature_valid = validate_signature_fragments(
623-
fragments = signature_fragments,
624-
hash_ = txn.bundle_hash,
625-
public_key = txn.address,
626-
)
627-
628-
if not signature_valid:
629-
yield (
630-
'Transaction {i} has invalid signature '
631-
'(using {fragments} fragments).'.format(
632-
fragments = len(signature_fragments),
633-
634-
# If we get to this point, we know that the
635-
# ``current_index`` value for each transaction can be
636-
# trusted.
637-
i = group[0].current_index,
638-
)
639-
)

0 commit comments

Comments
 (0)