diff --git a/examples/flash.py b/examples/flash.py new file mode 100644 index 0000000..cea3e0b --- /dev/null +++ b/examples/flash.py @@ -0,0 +1,141 @@ +from iota import Address +from iota.crypto.types import Seed +from iota.flash.api import FlashIota +from iota.flash.types import FlashUser, FlashState + +ONE_SEED = Seed(b'USERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSERONEUSER') +ONE_SETTLEMENT = Address(b'USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9U') +TWO_SEED = Seed(b'USERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSERTWOUSER') +TWO_SETTLEMENT = Address(b'USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9U') + +# General channel configuration +SECURITY = 2 # security level +SIGNERS_COUNT = 2 # number of parties taking signing part in the channel +TREE_DEPTH = 4 # Flash tree depth +CHANNEL_BALANCE = 2000 # total channel Balance +DEPOSITS = [1000, 1000] # users deposits + +############################################ +# (0) Initialize Flash objects +############################################ + +print('(0) Initializing Flash objects') + +# user one +iota_one = FlashIota(adapter='http://localhost:14265', seed=ONE_SEED) +flash_one = FlashState(signers_count=SIGNERS_COUNT, + balance=CHANNEL_BALANCE, + deposit=list(DEPOSITS)) +user_one = FlashUser(user_index=0, + seed=ONE_SEED, + index=0, + security=SECURITY, + depth=TREE_DEPTH, + flash=flash_one) + +# user two +flash_two = FlashState(signers_count=SIGNERS_COUNT, + balance=CHANNEL_BALANCE, + deposit=list(DEPOSITS)) +user_two = FlashUser(user_index=1, + seed=TWO_SEED, + index=0, + security=SECURITY, + depth=TREE_DEPTH, + flash=flash_two) +iota_two = FlashIota(adapter='http://localhost:14265', seed=TWO_SEED) +############################################ +# (1) Generate Digests +############################################ + +print('(1) Generating digests for each user') + +for _ in range(TREE_DEPTH + 1): + digest = iota_one.get_digests(index=user_one.index, security_level=user_one.security)['digests'] + user_one.index += 1 + user_one.partial_digests.append(digest[0]) + +for _ in range(TREE_DEPTH + 1): + digest = iota_two.get_digests(index=user_two.index, security_level=user_two.security)['digests'] + user_two.index += 1 + user_two.partial_digests.append(digest[0]) + +############################################ +# (2) Create Multisignature Addresses +############################################ + +print('(2) Creating multisignature addresses') + +all_digests = list(zip(user_one.partial_digests, user_two.partial_digests)) +one_multisigs = [iota_one.compose_flash_address(digests) for digests in all_digests] +two_multisigs = [iota_two.compose_flash_address(digests) for digests in all_digests] + +############################################ +# (3) Organize Addresses for use +############################################ + +print('(3) Organizing addresses for use') + +# Set remainder address (Same on both users) +flash_one.remainder_address = one_multisigs.pop(0) +flash_two.remainder_address = two_multisigs.pop(0) + +# Nest trees +for i in range(1, len(one_multisigs)): + one_multisigs[i - 1]['children'].append(one_multisigs[i]) +for i in range(1, len(two_multisigs)): + two_multisigs[i - 1]['children'].append(two_multisigs[i]) + +# Set deposit address +flash_one.deposit_address = one_multisigs[0] +flash_two.deposit_address = two_multisigs[0] + +# Set root of tree +flash_one.root = one_multisigs[0] +flash_two.root = two_multisigs[0] + +# Set settlement addresses (Usually sent over when the digests are.) +settlement_addresses = [ONE_SETTLEMENT, TWO_SETTLEMENT] +flash_one.settlement_addresses = settlement_addresses +flash_two.settlement_addresses = settlement_addresses + +# Set digest/key index +user_one.index = len(user_one.partial_digests) +user_two.index = len(user_two.partial_digests) + +############################################ +# (4) Compose Transaction from user one +############################################ +print('(4) Compose transactions. Sending 200 tokens to', TWO_SETTLEMENT) + +transfers = [{ + 'value': 200, + 'address': TWO_SETTLEMENT +}] +bundles = iota_one.create_flash_transaction(user=user_one, transactions=transfers) + +# ToDO + +############################################ +# (5) Sign Bundles +############################################ + +print('(5) Signing bundles') + +# ToDO + +############################################ +# (6) Apply Signed Bundles +############################################ + +print('(6) Applying signed bundles') + +# ToDO + +############################################ +# (7) Close Channel +############################################ + +print('(7) Closing channel') + +# ToDO diff --git a/iota/flash/__init__.py b/iota/flash/__init__.py new file mode 100644 index 0000000..443133a --- /dev/null +++ b/iota/flash/__init__.py @@ -0,0 +1 @@ +MAX_USES = 3 diff --git a/iota/flash/api.py b/iota/flash/api.py new file mode 100644 index 0000000..830c9df --- /dev/null +++ b/iota/flash/api.py @@ -0,0 +1,76 @@ +from typing import Iterable + +from iota.commands import discover_commands +from iota.crypto.types import Digest +from iota.flash.commands.create_transaction import CreateFlashTransactionCommand +from iota.flash.commands.multisig.compose_address_node import ComposeAddressNodeCommand +from iota.flash.types import FlashUser +from iota.multisig import MultisigIota + +__all__ = [ + 'FlashIota', +] + + +class FlashIota(MultisigIota): + """ + Extends the IOTA API so that it can manage Flash channels. + + **CAUTION:** Make sure you understand how Flash channel work before + attempting to use it. If you are not careful, you could easily + compromise the security of your private keys, send IOTAs to + unspendable addresses, etc. + + References: + - https://github.com/iotaledger/iota.flash.js + """ + + commands = discover_commands('iota.flash.commands') + + def compose_flash_address(self, digests): + # type: (Iterable[Digest]) -> dict + """ + Composes a multisig address for Flash channels from a collection of digests. + + :param digests: + Digests to use to create the multisig address. + + IMPORTANT: In order to spend IOTAs from a multisig address, the + signature must be generated from the corresponding private keys + in the exact same order. + + :return: + Dict with the following items:: + + { + 'address': dict, + The generated multisig address object. + } + """ + return ComposeAddressNodeCommand(self.adapter)( + digests = digests, + ) + + def create_flash_transaction(self, user, transactions, close=False): + # type: (FlashUser, Iterable[dict], bool) -> Iterable[dict] + """ + + :param user: Flash object of user storing relevant metadata of the channel + :param transactions: list of transaction, which should be executed + :param close: Flag indicating a closing of the channel + :return: + Dict with the following items:: + { + 'bundles': List[Bundles],, + List of generated bundles. + } + """ + return CreateFlashTransactionCommand(self.adapter)( + user=user, + transactions=transactions, + close=close + ) + + + + diff --git a/iota/flash/commands/__init__.py b/iota/flash/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iota/flash/commands/create_transaction.py b/iota/flash/commands/create_transaction.py new file mode 100644 index 0000000..ddea6b8 --- /dev/null +++ b/iota/flash/commands/create_transaction.py @@ -0,0 +1,62 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from collections import Iterable + +from iota import Address, Bundle +from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.filters import Trytes +from iota.flash.commands.multisig import update_leaf_to_root +from iota.flash.types import FlashUser + + +class CreateFlashTransactionCommand(FilterCommand): + """ + Helper for creating a transaction within a Flash channel + """ + command = 'createFlashTransaction' + + def get_request_filter(self): + return CreateFlashTransactionRequestFilter() + + def get_response_filter(self): + pass + # return CreateFlashTransactionResponseFilter() + + def _execute(self, request): + user = request['user'] # type: FlashUser + transactions = request['transactions'] # type: Iterable[dict] + close = request['close'] # type: bool + + # From the LEAF recurse up the tree to the root + # and find how many new addresses need to be generated if any. + to_use, to_generate = update_leaf_to_root(root=user.flash.root) + if to_generate != 0: + # TODO: handle this case + pass + + return [] + + +class CreateFlashTransactionRequestFilter(RequestFilter): + def __init__(self): + super(CreateFlashTransactionRequestFilter, self).__init__({ + 'user': f.Required | f.Type(FlashUser), + 'transactions': f.Required | + f.Array | + f.FilterRepeater(f.Required | + f.FilterMapper({ + 'value': f.Type(int) | f.Min(0), + 'address': f.Required | Trytes(result_type=Address) + })), + 'close': f.Type(bool) | f.Optional(default=False) + }) + + +class CreateFlashTransactionResponseFilter(ResponseFilter): + def __init__(self): + super(CreateFlashTransactionResponseFilter, self).__init__({ + 'bundles': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Bundle)) + }) diff --git a/iota/flash/commands/multisig/__init__.py b/iota/flash/commands/multisig/__init__.py new file mode 100644 index 0000000..89f848f --- /dev/null +++ b/iota/flash/commands/multisig/__init__.py @@ -0,0 +1,66 @@ +from typing import List + +from iota.flash import MAX_USES + + +def get_last_branch(root): + # type: (dict) -> List[dict] + """ + Searches for last used branch in tree and returns list of node beginning with + root node. + + :param root: root of tree + :return list of nodes in last used branch beginning with root node + """ + multisigs = [] + node = root + while node: + multisigs.append(node) + node = node['children'][-1] if node['children'] else None + return multisigs + + +def get_minimum_branch(root): + # type: (dict) -> List[dict] + """ + Searches for minimum branch in tree, which stores transactions + + :param root: root of tree + :return list of nodes storing transactions + """ + multisigs = [] + node = root + while node: + multisigs.append(node) + if len(node['children']) and len(node['bundles']) == MAX_USES: + node = node['children'][-1] + else: + node = None + return multisigs + + +def update_leaf_to_root(root): + # type: (dict) -> (dict, int) + """ + Searches tree for node in branch that can be used and number of addresses that + needs to be generated + :param root: + :returns: tuple (multisig_address, num_to_ generate) + :rtype: (dict, int) + """ + multisigs = get_last_branch(root) + + # get the first one that does not pass all the way down + for i in range(len(multisigs) - 1): + transactions = multisigs[i]['bundles'] + if not any([t.value > 0 and t.address == multisigs[i + 1]['address'] for t in transactions]): + return multisigs[i], 0 + + # TODO: TEST this section + # get the first from the bottom that is used less than MAX_USES times + num_generate = 0 + for i in reversed(range(len(multisigs))): + if len(multisigs[i]['bundles']) != MAX_USES: + break + num_generate += 1 + return multisigs[i], num_generate diff --git a/iota/flash/commands/multisig/compose_address_node.py b/iota/flash/commands/multisig/compose_address_node.py new file mode 100644 index 0000000..118b4e6 --- /dev/null +++ b/iota/flash/commands/multisig/compose_address_node.py @@ -0,0 +1,59 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Iterable + +import filters as f + +from iota import Address +from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.crypto.types import Digest +from iota.filters import Trytes +from iota.multisig.commands import CreateMultisigAddressCommand + +__all__ = [ + 'ComposeAddressNodeCommand', +] + + +# ToDo: TEST +class ComposeAddressNodeCommand(FilterCommand): + """ + Composes address for Flash channels + """ + + command = 'composeAddressNode' + + def get_request_filter(self): + return ComposeAddressNodeRequestFilter() + + def get_response_filter(self): + return ComposeAddressNodeResponseFilter() + + def _execute(self, request): + digests = request['digests'] # type: Iterable[Digest] + + address = CreateMultisigAddressCommand(self.adapter)(digests=digests)['address'] + + return { + 'address': address, + 'children': [], + 'bundles': [] + } + + +class ComposeAddressNodeRequestFilter(RequestFilter): + def __init__(self): + super(ComposeAddressNodeRequestFilter, self).__init__({ + 'digests': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Digest)) + }) + + +class ComposeAddressNodeResponseFilter(ResponseFilter): + def __init__(self): + super(ComposeAddressNodeResponseFilter, self).__init__({ + 'address': f.Required | Trytes(result_type=Address), + 'children': f.Array, + 'bundles': f.Array + }) diff --git a/iota/flash/commands/transfer/__init__.py b/iota/flash/commands/transfer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iota/flash/multisig.py b/iota/flash/multisig.py new file mode 100644 index 0000000..1882251 --- /dev/null +++ b/iota/flash/multisig.py @@ -0,0 +1,17 @@ +from iota.multisig.commands import GetDigestsCommand + + +def get_digest(seed, index, security): + + GetDigestsCommand(self.adapter)( + seed=self.seed, + index=index, + count=count, + securityLevel=security_level, + ) + + return { + 'digest': IOTACrypto.multisig.getDigest(seed, index, security), + 'security': security, + 'index': index + } diff --git a/iota/flash/types.py b/iota/flash/types.py new file mode 100644 index 0000000..f26e239 --- /dev/null +++ b/iota/flash/types.py @@ -0,0 +1,43 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import List + +from iota import Bundle +from iota.crypto.types import Seed, Digest + + +class FlashUser(object): + """ + Object representing a user withing a Flash channel + """ + + def __init__(self, user_index, seed, index, security, depth, flash): + # type: (FlashUser, int, Seed, int, int, int, FlashState) -> None + self.user_index = user_index # type: int + self.seed = seed # type: Seed + self.index = index # type: int + self.security = security # type: int + self.depth = depth # type: int + self.flash = flash # type: FlashState + self.bundles = [] # type: List[Bundle] + self.partial_digests = [] # type: List[Digest] + + +class FlashState(object): + """ + Object storing information of the current state of the channel + """ + + def __init__(self, signers_count, balance, deposit): + # type: (FlashState, int, int, list) -> None + self.signers_count = signers_count # type: int + self.balane = balance # type: int + self.deposit = deposit # type: List[int] + self.outputs = {} # type: dict # TODO: refine this type + self.transfers = [] # type: list # TODO: refine this type + self.remainder_address = None # type: dict + self.deposit_address = None # type: dict + self.settlement_addresses = None # type: dict + self.root = None # type: dict diff --git a/test/flash/__init__.py b/test/flash/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/flash/comands/__init__.py b/test/flash/comands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/flash/comands/create_transaction_test.py b/test/flash/comands/create_transaction_test.py new file mode 100644 index 0000000..f389ad0 --- /dev/null +++ b/test/flash/comands/create_transaction_test.py @@ -0,0 +1,9 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + + +class CreateFlashTransactionCommandTestCase(TestCase): + pass diff --git a/test/flash/comands/multisig/__init__.py b/test/flash/comands/multisig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/flash/comands/multisig/compose_address_node_test.py b/test/flash/comands/multisig/compose_address_node_test.py new file mode 100644 index 0000000..73dfd66 --- /dev/null +++ b/test/flash/comands/multisig/compose_address_node_test.py @@ -0,0 +1,9 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + + +class ComposeAddressNodeCommandTestCase(TestCase): + pass diff --git a/test/flash/comands/multisig/utils_test.py b/test/flash/comands/multisig/utils_test.py new file mode 100644 index 0000000..56d8acc --- /dev/null +++ b/test/flash/comands/multisig/utils_test.py @@ -0,0 +1,86 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota import Address +from iota.flash import MAX_USES +from iota.flash.commands.multisig import get_last_branch, update_leaf_to_root, get_minimum_branch + + +class FlashUtilsTestCase(TestCase): + + def setUp(self): + super(FlashUtilsTestCase, self).setUp() + + self.address_1 = { + 'address': Address(b'USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9USERONE9ADDRESS9U'), + 'children': [], + 'bundles': [] + } + self.address_2 = { + 'address': Address(b'USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9USERTWO9ADDRESS9U'), + 'children': [], + 'bundles': [] + } + self.address_3 = { + 'address': Address(b'USERTHR9ADDRESS9USERTHR9ADDRESS9USERTHR9ADDRESS9USERTHR9ADDRESS9USERTHR9ADDRESS9U'), + 'children': [], + 'bundles': [] + } + self.address_4 = { + 'address': Address(b'USERFOU9ADDRESS9USERFOU9ADDRESS9USERFOU9ADDRESS9USERFOU9ADDRESS9USERFOU9ADDRESS9U'), + 'children': [], + 'bundles': [] + } + + # wire up test tree + self.root_1 = self.address_1 + self.root_1['children'].append(self.address_2) + self.address_2['children'].append(self.address_3) + self.address_2['children'].append(self.address_4) + + def test_pass_get_last_branch_full_tree(self): + """ + Tests if proper last branch is found in full tree + """ + branch = get_last_branch(root=self.root_1) + self.assertListEqual(branch, [self.address_1, self.address_2, self.address_4]) + + def test_pass_get_last_branch_single_tree(self): + """ + Tests if proper last branch is found in tree with one node + """ + branch = get_last_branch(root=self.address_4) + self.assertListEqual(branch, [self.address_4]) + + def test_fail_get_last_branch_full_tree(self): + """ + Tests if proper last branch is not found in full tree + """ + branch = get_last_branch(root=self.address_2) + self.assertNotEqual(branch, [self.address_1, self.address_2, self.address_4]) + + def test_pass_get_minimum_branch_unused_tree(self): + """ + Tests if proper minimum branch is found in unused tree + """ + branch = get_minimum_branch(root=self.root_1) + self.assertListEqual(branch, [self.address_1]) + + def test_pass_get_minimum_branch_used_tree(self): + """ + Tests if proper minimum branch is found in used tree + """ + self.root_1['bundles'] = list(range(MAX_USES)) # fill with dummy data + branch = get_minimum_branch(root=self.root_1) + self.assertListEqual(branch, [self.address_1, self.address_2]) + + def test_pass_update_leaf_to_root(self): + """ + Tests if proper address is returned in full tree search + """ + address, num_generate = update_leaf_to_root(root=self.root_1) + self.assertEqual(address, self.address_1) + self.assertEqual(num_generate, 0)