diff --git a/src/sakia/core/account.py b/src/sakia/core/account.py index 4b68f8eb336df42c05908c55af156990a1c38a96..287377e672427afae74b86721fb72125060ce59e 100644 --- a/src/sakia/core/account.py +++ b/src/sakia/core/account.py @@ -338,261 +338,6 @@ class Account(QObject): value += val return value - async def check_registered(self, community): - """ - Checks for the pubkey and the uid of an account in a community - :param sakia.core.Community community: The community we check for registration - :return: (True if found, local value, network value) - """ - def _parse_uid_certifiers(data): - return self.name == data['uid'], self.name, data['uid'] - - def _parse_uid_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - for result in data['results']: - if result["pubkey"] == self.pubkey: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] - found_uid = uid_data["uid"] - return self.name == found_uid, self.name, found_uid - - def _parse_pubkey_certifiers(data): - return self.pubkey == data['pubkey'], self.pubkey, data['pubkey'] - - def _parse_pubkey_lookup(data): - timestamp = BlockUID.empty() - found_uid = "" - found_result = ["", ""] - for result in data['results']: - uids = result['uids'] - for uid_data in uids: - if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) - found_uid = uid_data["uid"] - if found_uid == self.name: - found_result = result['pubkey'], found_uid - if found_result[1] == self.name: - return self.pubkey == found_result[0], self.pubkey, found_result[0] - else: - return False, self.pubkey, None - - async def execute_requests(parsers, search): - tries = 0 - request = bma.wot.CertifiersOf - nonlocal registered - #TODO: The algorithm is quite dirty - #Multiplying the tries without any reason... - while tries < 3 and not registered[0] and not registered[2]: - try: - data = await community.bma_access.simple_request(request, - req_args={'search': search}) - if data: - registered = parsers[request](data) - tries += 1 - except errors.DuniterError as e: - if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, - e.ucode == errors.NO_MATCHING_IDENTITY): - if request == bma.wot.CertifiersOf: - request = bma.wot.Lookup - tries = 0 - else: - tries += 1 - else: - tries += 1 - except asyncio.TimeoutError: - tries += 1 - except ClientError: - tries += 1 - - registered = (False, self.name, None) - # We execute search based on pubkey - # And look for account UID - uid_parsers = { - bma.wot.CertifiersOf: _parse_uid_certifiers, - bma.wot.Lookup: _parse_uid_lookup - } - await execute_requests(uid_parsers, self.pubkey) - - # If the uid wasn't found when looking for the pubkey - # We look for the uid and check for the pubkey - if not registered[0] and not registered[2]: - pubkey_parsers = { - bma.wot.CertifiersOf: _parse_pubkey_certifiers, - bma.wot.Lookup: _parse_pubkey_lookup - } - await execute_requests(pubkey_parsers, self.name) - - return registered - - async def send_selfcert(self, password, community): - """ - Send our self certification to a target community - - :param str password: The account SigningKey password - :param community: The community target of the self certification - """ - try: - block_data = await community.bma_access.simple_request(bma.blockchain.Current) - signed_raw = "{0}{1}\n".format(block_data['raw'], block_data['signature']) - block_uid = Block.from_signed_raw(signed_raw).blockUID - except errors.DuniterError as e: - if e.ucode == errors.NO_CURRENT_BLOCK: - block_uid = BlockUID.empty() - else: - raise - selfcert = SelfCertification(PROTOCOL_VERSION, - community.currency, - self.pubkey, - self.name, - block_uid, - None) - key = SigningKey(self.salt, password) - selfcert.sign([key]) - logging.debug("Key publish : {0}".format(selfcert.signed_raw())) - - responses = await community.bma_access.broadcast(bma.wot.Add, {}, {'identity': selfcert.signed_raw()}) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - if result[0]: - (await self.identity(community)).sigdate = block_uid - return result - - async def send_membership(self, password, community, mstype): - """ - Send a membership document to a target community. - Signal "document_broadcasted" is emitted at the end. - - :param str password: The account SigningKey password - :param community: The community target of the membership document - :param str mstype: The type of membership demand. "IN" to join, "OUT" to leave - """ - logging.debug("Send membership") - - blockUID = community.network.current_blockUID - self_identity = await self._identities_registry.future_find(self.pubkey, community) - selfcert = await self_identity.selfcert(community) - - membership = Membership(PROTOCOL_VERSION, community.currency, - selfcert.pubkey, blockUID, mstype, selfcert.uid, - selfcert.timestamp, None) - key = SigningKey(self.salt, password) - membership.sign([key]) - logging.debug("Membership : {0}".format(membership.signed_raw())) - responses = await community.bma_access.broadcast(bma.blockchain.Membership, {}, - {'membership': membership.signed_raw()}) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - - async def certify(self, password, community, pubkey): - """ - Certify an other identity - - :param str password: The account SigningKey password - :param sakia.core.community.Community community: The community target of the certification - :param str pubkey: The certified identity pubkey - """ - logging.debug("Certdata") - blockUID = community.network.current_blockUID - try: - identity = await self._identities_registry.future_find(pubkey, community) - selfcert = await identity.selfcert(community) - except LookupFailureError as e: - return False, str(e) - - if selfcert: - certification = Certification(PROTOCOL_VERSION, community.currency, - self.pubkey, pubkey, blockUID, None) - - key = SigningKey(self.salt, password) - certification.sign(selfcert, [key]) - signed_cert = certification.signed_raw(selfcert) - logging.debug("Certification : {0}".format(signed_cert)) - - data = {'cert': certification.signed_raw(selfcert)} - logging.debug("Posted data : {0}".format(data)) - responses = await community.bma_access.broadcast(bma.wot.Certify, {}, data) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - # signal certification to all listeners - self.certification_accepted.emit() - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - else: - return False, self.tr("Could not find user self certification.") - - async def revoke(self, password, community): - """ - Revoke self-identity on server, not in blockchain - - :param str password: The account SigningKey password - :param sakia.core.community.Community community: The community target of the revokation - """ - revoked = await self._identities_registry.future_find(self.pubkey, community) - - revokation = Revocation(PROTOCOL_VERSION, community.currency, None) - selfcert = await revoked.selfcert(community) - - key = SigningKey(self.salt, password) - revokation.sign(selfcert, [key]) - - logging.debug("Self-Revokation Document : \n{0}".format(revokation.raw(selfcert))) - logging.debug("Signature : \n{0}".format(revokation.signatures[0])) - - data = { - 'pubkey': revoked.pubkey, - 'self_': selfcert.signed_raw(), - 'sig': revokation.signatures[0] - } - logging.debug("Posted data : {0}".format(data)) - responses = await community.bma_access.broadcast(bma.wot.Revoke, {}, data) - result = (False, "") - for r in responses: - if r.status == 200: - result = (True, (await r.json())) - elif not result[0]: - result = (False, (await r.text())) - else: - await r.release() - return result - - async def generate_revokation(self, community, password): - """ - Generate account revokation document for given community - :param sakia.core.Community community: the community - :param str password: the password - :return: the revokation document - :rtype: duniterpy.documents.certification.Revocation - """ - document = Revocation(PROTOCOL_VERSION, community.currency, self.pubkey, "") - identity = await self.identity(community) - selfcert = await identity.selfcert(community) - - key = SigningKey(self.salt, password) - - document.sign(selfcert, [key]) - return document.signed_raw(selfcert) - def start_coroutines(self): for c in self.communities: c.start_coroutines() diff --git a/src/sakia/data/connectors/bma.py b/src/sakia/data/connectors/bma.py index dabb95a5f6f3fc596d58be6ccb1310884a746310..b938903937e416ab23b37d5e67c395b697965fa4 100644 --- a/src/sakia/data/connectors/bma.py +++ b/src/sakia/data/connectors/bma.py @@ -40,6 +40,7 @@ class BmaConnector: """ Start a request to the network but don't cache its result. + :param str currency: the currency requested :param class request: A bma request class calling for data :param dict req_args: Arguments to pass to the request constructor :param dict get_args: Arguments to pass to the request __get__ method @@ -48,7 +49,6 @@ class BmaConnector: nodes = self.filter_nodes(request, self._nodes_processor.synced_nodes(currency)) if len(nodes) > 0: tries = 0 - json_data = None while tries < 3: node = random.choice(nodes) nodes.pop(node) @@ -60,15 +60,15 @@ class BmaConnector: asyncio.TimeoutError, ValueError, jsonschema.ValidationError) as e: logging.debug(str(e)) tries += 1 - if len(nodes) == 0 or not json_data: + if len(nodes) == 0: raise NoPeerAvailable("", len(nodes)) - return json_data async def broadcast(self, currency, request, req_args={}, post_args={}): """ Broadcast data to a network. Sends the data to all knew nodes. + :param str currency: the currency target :param request: A duniterpy bma request class :param req_args: Arguments to pass to the request constructor :param post_args: Arguments to pass to the request __post__ method diff --git a/src/sakia/data/entities/identity.py b/src/sakia/data/entities/identity.py index 1b498694447df29fdd7bd52c2ab2804421ebfb5d..e22e337dc5099571abcac8411bd7c8177e8f1a14 100644 --- a/src/sakia/data/entities/identity.py +++ b/src/sakia/data/entities/identity.py @@ -1,5 +1,6 @@ import attr -from duniterpy.documents import block_uid, BlockUID +from duniterpy.documents import block_uid, BlockUID, SelfCertification +from duniterpy import PROTOCOL_VERSION @attr.s() @@ -17,3 +18,13 @@ class Identity: membership_timestamp = attr.ib(convert=int, default=0, cmp=False, hash=False) membership_type = attr.ib(convert=str, default='', validator=lambda s, a, t: t in ('', 'IN', 'OUT'), cmp=False, hash=False) membership_written_on = attr.ib(convert=block_uid, default=BlockUID.empty(), cmp=False, hash=False) + + def self_certification(self): + """ + Creates a self cert document for a given identity + :param sakia.data.entities.Identity identity: + :return: the document + :rtype: duniterpy.documents.SelfCertification + """ + return SelfCertification(PROTOCOL_VERSION, self.currency, self.pubkey, + self.uid, self.blockstamp, self.signature) diff --git a/src/sakia/data/processors/identities.py b/src/sakia/data/processors/identities.py index 2a70ff5891d19e6502a1b9f6706dd3c690882d38..e66a848abe84e117a5a98554bf623ff57ba9626f 100644 --- a/src/sakia/data/processors/identities.py +++ b/src/sakia/data/processors/identities.py @@ -4,6 +4,8 @@ from duniterpy.api import bma, errors import asyncio from aiohttp.errors import ClientError from sakia.errors import NoPeerAvailable +from duniterpy.documents import SelfCertification +from duniterpy import PROTOCOL_VERSION @attr.s @@ -55,7 +57,6 @@ class IdentitiesProcessor: def update_identity(self, identity): """ Saves an identity state in the db - :param identity: - :return: + :param sakia.data.entities.Identity identity: the identity updated """ self._identities_repo.update(identity) diff --git a/src/sakia/gui/dialogs/community_cfg/controller.py b/src/sakia/gui/dialogs/community_cfg/controller.py index d009f236c0c7ab65b83d60e74f0e399278893f4c..31f732cab8e7ea5f0dd8d3b1d8006bbfe6917114 100644 --- a/src/sakia/gui/dialogs/community_cfg/controller.py +++ b/src/sakia/gui/dialogs/community_cfg/controller.py @@ -3,7 +3,7 @@ import logging from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QDialog, QApplication, QMenu from aiohttp.errors import DisconnectedError, ClientError, TimeoutError -from sakia.tools.exceptions import NoPeerAvailable +from sakia.errors import NoPeerAvailable from duniterpy.documents import MalformedDocumentError from sakia.decorators import asyncify diff --git a/src/sakia/gui/password_asker.py b/src/sakia/gui/password_asker.py index be1b97c3a64a2e4e09ff76e638f8797f94aeb707..adfd850b9444204da817ac840cfcad7379d71e88 100644 --- a/src/sakia/gui/password_asker.py +++ b/src/sakia/gui/password_asker.py @@ -10,7 +10,7 @@ import asyncio from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QDialog, QMessageBox -from ..gen_resources.password_asker_uic import Ui_PasswordAskerDialog +from .password_asker_uic import Ui_PasswordAskerDialog class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): diff --git a/src/sakia/services/__init__.py b/src/sakia/services/__init__.py index fd456d1a052b30a21646849b6ffc0ac653ac798e..2c5869f54cae9a872309cb779f6e553fbdfd8999 100644 --- a/src/sakia/services/__init__.py +++ b/src/sakia/services/__init__.py @@ -1,3 +1,4 @@ from .network import NetworkService from .identities import IdentitiesService from .blockchain import BlockchainService +from .documents import DocumentsService diff --git a/src/sakia/services/documents.py b/src/sakia/services/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..1fb43d394ab690fe6d7bfdbd8da442dd292c8002 --- /dev/null +++ b/src/sakia/services/documents.py @@ -0,0 +1,273 @@ +import asyncio +import attr +import logging +import jsonschema +from collections import Counter + +from duniterpy.key import SigningKey +from duniterpy import PROTOCOL_VERSION +from duniterpy.documents import BlockUID, Block, SelfCertification, Certification, Membership, Revocation +from duniterpy.api import bma, errors +from sakia.data.entities import Node +from aiohttp.errors import ClientError, DisconnectedError + + +@attr.s() +class DocumentsService: + """ + A service to forge and broadcast documents + to the network + """ + _bma_connector = attr.ib() # :type: sakia.data.connectors.BmaConnector + _blockchain_processor = attr.ib() # :type: sakia.data.processors.BlockchainProcessor + _identities_processor = attr.ib() # :type: sakia.data.processors.IdentitiesProcessor + _logger = attr.ib(default=lambda: logging.getLogger('sakia')) + + async def check_registered(self, currency): + """ + Checks for the pubkey and the uid of an account in a community + :param str currency: The currency we check for registration + :return: (True if found, local value, network value) + """ + def _parse_uid_certifiers(data): + return self.name == data['uid'], self.name, data['uid'] + + def _parse_uid_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + for result in data['results']: + if result["pubkey"] == self.pubkey: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = uid_data["meta"]["timestamp"] + found_uid = uid_data["uid"] + return self.name == found_uid, self.name, found_uid + + def _parse_pubkey_certifiers(data): + return self.pubkey == data['pubkey'], self.pubkey, data['pubkey'] + + def _parse_pubkey_lookup(data): + timestamp = BlockUID.empty() + found_uid = "" + found_result = ["", ""] + for result in data['results']: + uids = result['uids'] + for uid_data in uids: + if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: + timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) + found_uid = uid_data["uid"] + if found_uid == self.name: + found_result = result['pubkey'], found_uid + if found_result[1] == self.name: + return self.pubkey == found_result[0], self.pubkey, found_result[0] + else: + return False, self.pubkey, None + + async def execute_requests(parsers, search): + tries = 0 + request = bma.wot.CertifiersOf + nonlocal registered + #TODO: The algorithm is quite dirty + #Multiplying the tries without any reason... + while tries < 3 and not registered[0] and not registered[2]: + try: + data = await self._bma_connector.get(currency, request, req_args={'search': search}) + if data: + registered = parsers[request](data) + tries += 1 + except errors.DuniterError as e: + if e.ucode in (errors.NO_MEMBER_MATCHING_PUB_OR_UID, + e.ucode == errors.NO_MATCHING_IDENTITY): + if request == bma.wot.CertifiersOf: + request = bma.wot.Lookup + tries = 0 + else: + tries += 1 + else: + tries += 1 + except asyncio.TimeoutError: + tries += 1 + except (ClientError, TimeoutError, ConnectionRefusedError, DisconnectedError, ValueError) as e: + self._logger.debug("{0} : {1}".format(str(e), self.node.pubkey[:5])) + self.node.state = Node.OFFLINE + except jsonschema.ValidationError as e: + self._logger.debug(str(e)) + self._logger.debug("Validation error : {0}".format(self.node.pubkey[:5])) + self.node.state = Node.CORRUPTED + + registered = (False, self.name, None) + # We execute search based on pubkey + # And look for account UID + uid_parsers = { + bma.wot.CertifiersOf: _parse_uid_certifiers, + bma.wot.Lookup: _parse_uid_lookup + } + await execute_requests(uid_parsers, self.pubkey) + + # If the uid wasn't found when looking for the pubkey + # We look for the uid and check for the pubkey + if not registered[0] and not registered[2]: + pubkey_parsers = { + bma.wot.CertifiersOf: _parse_pubkey_certifiers, + bma.wot.Lookup: _parse_pubkey_lookup + } + await execute_requests(pubkey_parsers, self.name) + + return registered + + async def send_selfcert(self, currency, salt, password): + """ + Send our self certification to a target community + + :param str currency: The currency of the identity + :param sakia.data.entities.Identity identity: The certified identity + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + """ + try: + block_data = await self._bma_connector.get(currency, bma.blockchain.Current) + signed_raw = "{0}{1}\n".format(block_data['raw'], block_data['signature']) + block_uid = Block.from_signed_raw(signed_raw).blockUID + except errors.DuniterError as e: + if e.ucode == errors.NO_CURRENT_BLOCK: + block_uid = BlockUID.empty() + else: + raise + selfcert = SelfCertification(PROTOCOL_VERSION, + currency, + self.pubkey, + self.name, + block_uid, + None) + key = SigningKey(self.salt, password) + selfcert.sign([key]) + self._logger.debug("Key publish : {0}".format(selfcert.signed_raw())) + + responses = await self._bma_connector.broadcast(currency, bma.wot.Add, {}, {'identity': selfcert.signed_raw()}) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + async def send_membership(self, currency, identity, password, mstype): + """ + Send a membership document to a target community. + Signal "document_broadcasted" is emitted at the end. + + :param str currency: the currency target + :param sakia.data.entities.Identity identity: the identitiy data + :param str password: The account SigningKey password + :param str mstype: The type of membership demand. "IN" to join, "OUT" to leave + """ + self._logger.debug("Send membership") + + blockUID = self._blockchain_processor.current_buid(currency) + membership = Membership(PROTOCOL_VERSION, currency, + identity.pubkey, blockUID, mstype, identity.uid, + identity.timestamp, None) + key = SigningKey(self.salt, password) + membership.sign([key]) + self._logger.debug("Membership : {0}".format(membership.signed_raw())) + responses = await self._bma_connector.broadcast(currency, bma.blockchain.Membership, {}, + {'membership': membership.signed_raw()}) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + async def certify(self, currency, identity, salt, password): + """ + Certify an other identity + + :param str currency: The currency of the identity + :param sakia.data.entities.Identity identity: The certified identity + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + """ + self._logger.debug("Certdata") + blockUID = self._blockchain_processor.current_buid(currency) + + certification = Certification(PROTOCOL_VERSION, currency, + self.pubkey, identity.pubkey, blockUID, None) + + key = SigningKey(salt, password) + certification.sign(identity.self_certification(), [key]) + signed_cert = certification.signed_raw(identity.self_certification()) + self._logger.debug("Certification : {0}".format(signed_cert)) + + responses = await self._bma_connector.bma_access.broadcast(currency, bma.wot.Certify, {}, + {'cert': signed_cert}) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + # signal certification to all listeners + self.certification_accepted.emit() + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + async def revoke(self, currency, identity, salt, password): + """ + Revoke self-identity on server, not in blockchain + + :param str currency: The currency of the identity + :param sakia.data.entities.Identity identity: The certified identity + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + """ + revocation = Revocation(PROTOCOL_VERSION, currency, None) + self_cert = identity.self_certification() + + key = SigningKey(salt, password) + revocation.sign(self_cert, [key]) + + self._logger.debug("Self-Revokation Document : \n{0}".format(revocation.raw(self_cert))) + self._logger.debug("Signature : \n{0}".format(revocation.signatures[0])) + + data = { + 'pubkey': identity.pubkey, + 'self_': self_cert.signed_raw(), + 'sig': revocation.signatures[0] + } + self._logger.debug("Posted data : {0}".format(data)) + responses = await self._bma_connector.broadcast(currency, bma.wot.Revoke, {}, data) + result = (False, "") + for r in responses: + if r.status == 200: + result = (True, (await r.json())) + elif not result[0]: + result = (False, (await r.text())) + else: + await r.release() + return result + + async def generate_revokation(self, currency, identity, salt, password): + """ + Generate account revokation document for given community + + :param str currency: The currency of the identity + :param sakia.data.entities.Identity identity: The certified identity + :param str salt: The account SigningKey salt + :param str password: The account SigningKey password + """ + document = Revocation(PROTOCOL_VERSION, currency, identity.pubkey, "") + self_cert = identity.self_certification() + + key = SigningKey(salt, password) + + document.sign(self_cert, [key]) + return document.signed_raw(self_cert) diff --git a/src/sakia/tests/technical/test_documents_service.py b/src/sakia/tests/technical/test_documents_service.py new file mode 100644 index 0000000000000000000000000000000000000000..83bec4763205e1606ee1702c230b70a3377735b0 --- /dev/null +++ b/src/sakia/tests/technical/test_documents_service.py @@ -0,0 +1,39 @@ +import asyncio +import unittest +import sqlite3 +import aiohttp +from duniterpy.documents import BlockUID, Peer +from sakia.tests import QuamashTest +from sakia.services import DocumentsService +from sakia.data.connectors import NodeConnector, BmaConnector +from sakia.data.repositories import NodesRepo, MetaDatabase, BlockchainsRepo, IdentitiesRepo +from sakia.data.processors import NodesProcessor, BlockchainProcessor, IdentitiesProcessor + + +class TestDocumentsService(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + sqlite3.register_adapter(BlockUID, str) + sqlite3.register_adapter(bool, int) + sqlite3.register_adapter(list, lambda ls: '\n'.join([str(v) for v in ls])) + sqlite3.register_adapter(tuple, lambda ls: '\n'.join([str(v) for v in ls])) + sqlite3.register_converter("BOOLEAN", lambda v: bool(int(v))) + self.con = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES) + + def tearDown(self): + self.tearDownQuamash() + + def test_certify(self): + meta_repo = MetaDatabase(self.con) + meta_repo.prepare() + meta_repo.upgrade_database() + nodes_repo = NodesRepo(self.con) + nodes_processor = NodesProcessor(nodes_repo) + bma_connector = BmaConnector(nodes_processor) + blockchain_repo = BlockchainsRepo(self.con) + identities_repo = IdentitiesRepo(self.con) + blockchain_processor = BlockchainProcessor(blockchain_repo, bma_connector) + identities_processor = IdentitiesProcessor(identities_repo, bma_connector) + documents_service = DocumentsService(bma_connector, blockchain_processor, identities_processor) + #TODO: Build a framework to test documents broadcasting +