示例#1
0
    async def open(self) -> 'NodePool':
        """
        Explicit entry. Opens pool as configured, for later closure via close().
        Creates pool if it does not yet exist, using configured genesis transaction file.
        For use when keeping pool open across multiple calls.

        Raise any AbsentPool if node pool ledger configuration is not available.

        :return: current object
        """

        LOGGER.debug('NodePool.open >>>')

        await pool.set_protocol_version(self.protocol.indy())
        LOGGER.info('Pool ledger %s set protocol %s', self.name, self.protocol)

        try:
            self._handle = await pool.open_pool_ledger(self.name, json.dumps(self.config))
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.PoolLedgerNotCreatedError:
                LOGGER.debug('NodePool.open <!< Absent node pool %s ledger configuration', self.name)
                raise AbsentPool('Absent node pool {} ledger configuration'.format(self.name))
            LOGGER.debug(
                'NodePool.open <!< cannot open node pool %s: indy error code %s',
                self.name,
                x_indy.error_code)
            raise

        LOGGER.debug('NodePool.open <<<')
        return self
示例#2
0
    async def _submit(self, req_json: str) -> str:
        """
        Submit (json) request to ledger; return (json) result.

        Raise AbsentPool for no pool, ClosedPool if pool is not yet open, or BadLedgerTxn on failure.

        :param req_json: json of request to sign and submit
        :return: json response
        """

        LOGGER.debug('BaseAnchor._submit >>> req_json: %s', req_json)

        if not self.pool:
            LOGGER.debug('BaseAnchor._submit <!< absent pool')
            raise AbsentPool('Cannot submit request: absent pool')

        if not self.pool.handle:
            LOGGER.debug('BaseAnchor._submit <!< closed pool %s',
                         self.pool.name)
            raise ClosedPool('Cannot submit request to closed pool {}'.format(
                self.pool.name))

        rv_json = await ledger.submit_request(self.pool.handle, req_json)
        await asyncio.sleep(0)

        resp = json.loads(rv_json)
        if resp.get('op', '') in ('REQNACK', 'REJECT'):
            LOGGER.debug('BaseAnchor._submit <!< ledger rejected request: %s',
                         resp['reason'])
            raise BadLedgerTxn(
                'Ledger rejected transaction request: {}'.format(
                    resp['reason']))

        LOGGER.debug('BaseAnchor._submit <<< %s', rv_json)
        return rv_json
示例#3
0
    async def _sign_submit(self, req_json: str) -> str:
        """
        Sign and submit (json) request to ledger; return (json) result.

        Raise:
        * AbsentPool for no pool
        * ClosedPool if pool is not yet open
        * CorruptWallet if existing wallet appears not to pertain to the anchor's pool
        * WalletState if wallet is closed
        * BadLedgerTxn on ledger rejection of transaction.

        :param req_json: json of request to sign and submit
        :return: json response
        """

        LOGGER.debug('BaseAnchor._sign_submit >>> req_json: %s', req_json)

        if not self.pool:
            LOGGER.debug('BaseAnchor._sign_submit <!< absent pool')
            raise AbsentPool('Cannot sign and submit request: absent pool')

        if not self.pool.handle:
            LOGGER.debug('BaseAnchor._submit <!< closed pool %s', self.pool.name)
            raise ClosedPool('Cannot sign and submit request to closed pool {}'.format(self.pool.name))

        if not self.wallet.handle:
            LOGGER.debug('BaseAnchor._sign_submit <!< Wallet %s is closed', self.name)
            raise WalletState('Wallet {} is closed'.format(self.name))

        try:
            rv_json = await ledger.sign_and_submit_request(self.pool.handle, self.wallet.handle, self.did, req_json)
            await asyncio.sleep(0)
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.WalletIncompatiblePoolError:
                LOGGER.debug(
                    'BaseAnchor._sign_submit <!< Corrupt wallet %s is not compatible with pool %s',
                    self.name,
                    self.pool.name)
                raise CorruptWallet('Corrupt wallet {} is not compatible with pool {}'.format(
                    self.name,
                    self.pool.name))
            LOGGER.debug(
                'BaseAnchor._sign_submit <!< cannot sign/submit request for ledger: indy error code %s',
                x_indy.error_code)
            raise BadLedgerTxn('Cannot sign/submit request for ledger: indy error code {}'.format(
                x_indy.error_code))

        resp = json.loads(rv_json)
        if resp.get('op', '') in ('REQNACK', 'REJECT'):
            LOGGER.debug('BaseAnchor._sign_submit: ledger rejected request: %s', resp['reason'])
            raise BadLedgerTxn('Ledger rejected transaction request: {}'.format(resp['reason']))

        LOGGER.debug('BaseAnchor._sign_submit <<< %s', rv_json)
        return rv_json
示例#4
0
    async def add_config(self, name: str, genesis: str = None) -> None:
        """
        Given pool name and genesis transaction path or data, add node pool
        configuration to indy home directory.

        Raise ExtantPool if node pool configuration on input name already exists.
        Raise AbsentPool if unable to create the pool ledger configuration.

        :param name: pool name
        :param genesis: genesis transaction path or raw data
        """

        LOGGER.debug('NodePoolManager.add_config >>> name: %s, genesis: %s',
                     name, genesis)

        if name in await self.list():
            LOGGER.debug(
                'NodePoolManager.add_config: <!< Node pool %s configuration already present',
                name)
            raise ExtantPool(
                'Node pool {} configuration already present'.format(name))

        genesis_tmp = None
        path_gen = realpath(expanduser(expandvars(genesis)))
        try:
            if not isfile(path_gen):
                genesis_tmp = NamedTemporaryFile(mode='w+b',
                                                 buffering=0,
                                                 delete=False)
                with genesis_tmp:
                    genesis_tmp.write(genesis.encode())
            await pool.create_pool_ledger_config(
                name,
                json.dumps({
                    'genesis_txn':
                    path_gen if isfile(path_gen) else genesis_tmp.name
                }))
        except IndyError as x_indy:
            LOGGER.debug(
                'NodePoolManager.add_config <!< could not create pool %s ledger configuration: indy error %s',
                name, x_indy.error_code)
            raise AbsentPool(
                'Could not create pool {} ledger configuration: indy error {}'.
                format(name, x_indy.error_code))

        finally:
            if genesis_tmp:
                remove(genesis_tmp.name)

        LOGGER.debug('NodePoolManager.add_config <<<')
示例#5
0
    async def create_cred_offer(self, schema_seq_no: int) -> str:
        """
        Create credential offer as Issuer for given schema.

        Raise CorruptWallet if the wallet has no private key for the corresponding credential definition.
        Raise WalletState for closed wallet.

        :param schema_seq_no: schema sequence number
        :return: credential offer json for use in storing credentials at HolderProver.
        """

        LOGGER.debug('Issuer.create_cred_offer >>> schema_seq_no: %s',
                     schema_seq_no)

        if not self.wallet.handle:
            LOGGER.debug('Issuer.create_cred_offer <!< Wallet %s is closed',
                         self.name)
            raise WalletState('Wallet {} is closed'.format(self.name))

        if not self.pool:
            LOGGER.debug('Issuer.create_cred_offer <!< issuer %s has no pool',
                         self.name)
            raise AbsentPool(
                'Issuer {} has no pool: cannot create cred offer'.format(
                    self.name))

        rv = None
        cd_id = cred_def_id(self.did, schema_seq_no, self.pool.protocol)
        try:
            rv = await anoncreds.issuer_create_credential_offer(
                self.wallet.handle, cd_id)
        except IndyError as x_indy:
            if x_indy.error_code == ErrorCode.WalletItemNotFound:
                LOGGER.debug(
                    'Issuer.create_cred_offer <!< did not issue cred definition from wallet %s',
                    self.name)
                raise CorruptWallet(
                    'Cannot create cred offer: did not issue cred definition from wallet {}'
                    .format(self.name))
            LOGGER.debug(
                'Issuer.create_cred_offer <!< cannot create cred offer, indy error code %s',
                x_indy.error_code)
            raise

        LOGGER.debug('Issuer.create_cred_offer <<< %s', rv)
        return rv
示例#6
0
async def setnym(ini_path: str) -> int:
    """
    Set configuration. Open pool, trustee anchor, and wallet of anchor whose nym to send.
    Register exit hooks to close pool and trustee anchor.

    Engage trustee anchor to send nym for VON anchor, if it differs on the ledger from configuration.

    :param ini_path: path to configuration file
    :return: 0 for OK, 1 for failure
    """

    config = inis2dict(ini_path)
    if config['Trustee Anchor']['name'] == config['VON Anchor']['name']:
        raise ExtantWallet(
            'Wallet names must differ between VON Anchor and Trustee Anchor')

    cfg_van_role = config['VON Anchor'].get(
        'role', None) or None  # nudge empty value from '' to None
    if not ok_role(cfg_van_role):
        raise BadRole('Configured role {} is not valid'.format(cfg_van_role))

    pool_data = NodePoolData(
        config['Node Pool']['name'],
        config['Node Pool'].get('genesis.txn.path', None) or None)

    an_data = {
        'tan':
        AnchorData(
            Role.TRUSTEE, config['Trustee Anchor']['name'],
            config['Trustee Anchor'].get('seed', None) or None,
            config['Trustee Anchor'].get('did', None) or None,
            config['Trustee Anchor'].get('wallet.create',
                                         '0').lower() in ['1', 'true', 'yes'],
            config['Trustee Anchor'].get('wallet.type', None) or None,
            config['Trustee Anchor'].get('wallet.access', None) or None),
        'van':
        AnchorData(
            Role.get(cfg_van_role), config['VON Anchor']['name'],
            config['VON Anchor'].get('seed', None) or None,
            config['VON Anchor'].get('did', None) or None,
            config['VON Anchor'].get('wallet.create',
                                     '0').lower() in ['1', 'true', 'yes'],
            config['VON Anchor'].get('wallet.type', None) or None,
            config['VON Anchor'].get('wallet.access', None) or None)
    }

    an_wallet = await _set_wallets(an_data)

    p_mgr = NodePoolManager()
    if pool_data.name not in await p_mgr.list():
        if pool_data.genesis_txn_path:
            await p_mgr.add_config(pool_data.name, pool_data.genesis_txn_path)
        else:
            raise AbsentPool(
                'Node pool {} has no ledger configuration, but {} specifies no genesis txn path'
                .format(pool_data.name, ini_path))

    async with an_wallet['tan'] as w_tan, (an_wallet['van']) as w_van, (
            p_mgr.get(pool_data.name)) as pool, (TrusteeAnchor(
                w_tan, pool)) as tan, (NominalAnchor(w_van, pool)) as van:

        send_verkey = van.verkey
        try:
            nym_role = await tan.get_nym_role(van.did)
            if an_data['van'].role == nym_role:
                return 0  # ledger is as per configuration
            send_verkey = None  # only owner can touch verkey
            if nym_role != Role.USER:  # only remove role when it is not already None on the ledger
                await tan.send_nym(van.did, send_verkey, van.wallet.name,
                                   Role.ROLE_REMOVE)
        except AbsentNym:
            pass  # cryptonym not there yet, fall through

        await tan.send_nym(van.did, send_verkey, van.wallet.name,
                           an_data['van'].role)

    return 0
示例#7
0
def boot() -> None:
    """
    Boot the service: instantiate tails server anchor. Raise AbsentPool if node pool ledger configuration
    neither present nor sufficiently specified; raise AbsentNym if tails server anchor nym is not on the ledger.
    """

    config = do_wait(MEM_CACHE.get('config'))

    # setup pool and wallet
    pool_data = NodePoolData(config['Node Pool']['name'],
                             config['Node Pool'].get('genesis.txn.path', None)
                             or None)  # nudge empty value from '' to None
    p_mgr = NodePoolManager()
    if pool_data.name not in do_wait(p_mgr.list()):
        if pool_data.genesis_txn_path:
            do_wait(
                p_mgr.add_config(pool_data.name, pool_data.genesis_txn_path))
        else:
            LOGGER.debug(
                'Node pool %s has no ledger configuration but %s specifies no genesis txn path',
                pool_data.name, do_wait(MEM_CACHE.get('config.ini')))
            raise AbsentPool(
                'Node pool {} has no ledger configuration but {} specifies no genesis txn path'
                .format(pool_data.name, do_wait(MEM_CACHE.get('config.ini'))))

    pool = p_mgr.get(pool_data.name)
    do_wait(pool.open())
    do_wait(MEM_CACHE.set('pool', pool))

    # instantiate tails server anchor
    tsan_data = AnchorData(
        Role.USER, config['VON Anchor']['name'],
        config['VON Anchor'].get('seed', None) or None, None,
        config['VON Anchor'].get('wallet.create', '0').lower()
        in ['1', 'true', 'yes'], config['VON Anchor'].get('wallet.type', None)
        or None, config['VON Anchor'].get('wallet.access', None) or None)

    w_mgr = WalletManager()
    wallet = None

    wallet_config = {'id': tsan_data.name}
    if tsan_data.wallet_type:
        wallet_config['storage_type'] = tsan_data.wallet_type
    if tsan_data.wallet_create:
        if tsan_data.seed:
            wallet_config['seed'] = tsan_data.seed
        try:
            wallet = do_wait(
                w_mgr.create(wallet_config, access=tsan_data.wallet_access))
            LOGGER.info('Created wallet %s', tsan_data.name)
        except ExtantWallet:
            wallet = w_mgr.get(wallet_config, access=tsan_data.wallet_access)
            LOGGER.warning(
                'Wallet %s already exists: remove seed and wallet.create from config file',
                tsan_data.name)
    else:
        wallet = w_mgr.get(wallet_config, access=tsan_data.wallet_access)

    do_wait(wallet.open())
    tsan = NominalAnchor(wallet, pool)
    do_wait(tsan.open())
    if not json.loads(do_wait(tsan.get_nym())):
        LOGGER.debug('Anchor %s has no cryptonym on ledger %s',
                     tsan_data.wallet_name, pool_data.name)
        raise AbsentNym('Anchor {} has no cryptonym on ledger {}'.format(
            tsan_data.wallet_name, pool_data.name))

    do_wait(MEM_CACHE.set('tsan', tsan))
示例#8
0
async def setnym(ini_path: str) -> int:
    """
    Set configuration. Open pool, trustee anchor, and wallet of anchor whose nym to send.
    Register exit hooks to close pool and trustee anchor.

    Engage trustee anchor to send nym for VON anchor, if it differs on the ledger from configuration.

    :param ini_path: path to configuration file
    :return: 0 for OK, 1 for failure
    """

    config = inis2dict(ini_path)
    cfg_van_role = config['VON Anchor'].get(
        'role', None) or None  # nudge empty value from '' to None
    if not ok_role(cfg_van_role):
        raise BadRole('Configured role {} is not valid'.format(cfg_van_role))

    if config['Trustee Anchor']['wallet.name'] == config['VON Anchor'][
            'wallet.name']:
        raise ExtantWallet(
            'Wallet names must differ between VON Anchor and Trustee Anchor')

    pool_data = NodePoolData(
        config['Node Pool']['name'],
        config['Node Pool'].get('genesis.txn.path', None) or None)
    an_data = {
        'tan':
        AnchorData(Role.TRUSTEE, config['Trustee Anchor'].get('seed', None)
                   or None, config['Trustee Anchor']['wallet.name'],
                   config['Trustee Anchor'].get('wallet.type', None) or None,
                   config['Trustee Anchor'].get('wallet.key', None) or None),
        'van':
        AnchorData(Role.get(cfg_van_role),
                   config['VON Anchor'].get('seed', None) or None,
                   config['VON Anchor']['wallet.name'],
                   config['VON Anchor'].get('wallet.type', None) or None,
                   config['VON Anchor'].get('wallet.key', None) or None)
    }
    an_wallet = {
        an: Wallet(an_data[an].wallet_name, an_data[an].wallet_type, None,
                   {'key': an_data[an].wallet_key}
                   if an_data[an].wallet_key else None)
        for an in an_data
    }

    for anchor in an_data:  # create wallet if seed configured, silently continue if extant
        if an_data[anchor].seed:
            try:
                await an_wallet[anchor].create(an_data[anchor].seed)
            except ExtantWallet:
                pass

    manager = NodePoolManager()
    if pool_data.name not in await manager.list():
        if pool_data.genesis_txn_path:
            await manager.add_config(pool_data.name,
                                     pool_data.genesis_txn_path)
        else:
            raise AbsentPool(
                'Node pool {} has no ledger configuration, but {} specifies no genesis txn path'
                .format(pool_data.name, ini_path))

    async with an_wallet['tan'] as w_tan, (an_wallet['van']) as w_van, (
            manager.get(pool_data.name)) as pool, (TrusteeAnchor(
                w_tan, pool)) as tan, (NominalAnchor(w_van, pool)) as van:

        send_verkey = van.verkey
        try:
            nym_role = await tan.get_nym_role(van.did)
            if an_data['van'].role == nym_role:
                return 0  # ledger is as per configuration
            send_verkey = None  # only owner can touch verkey
            await tan.send_nym(van.did, send_verkey, van.wallet.name,
                               Role.ROLE_REMOVE)
        except AbsentNym:
            pass  # cryptonym not there yet, fall through

        await tan.send_nym(van.did, send_verkey, van.wallet.name,
                           an_data['van'].role)

    return 0
示例#9
0
    async def send_cred_def(self,
                            s_id: str,
                            revo: bool = True,
                            rr_size: int = None) -> str:
        """
        Create a credential definition as Issuer, store it in its wallet, and send it to the ledger.

        Raise CorruptWallet for wallet not pertaining to current ledger, BadLedgerTxn on failure
        to send credential definition to ledger if need be, WalletState for closed wallet,
        or IndyError for any other failure to create and store credential definition in wallet.

        :param s_id: schema identifier
        :param revo: whether to support revocation for cred def
        :param rr_size: size of initial revocation registry (default as per RevRegBuilder.create_rev_reg()),
            if revocation supported
        :return: json credential definition as it appears on ledger
        """

        LOGGER.debug(
            'Issuer.send_cred_def >>> s_id: %s, revo: %s, rr_size: %s', s_id,
            revo, rr_size)

        if not ok_schema_id(s_id):
            LOGGER.debug('Issuer.send_cred_def <!< Bad schema id %s', s_id)
            raise BadIdentifier('Bad schema id {}'.format(s_id))

        if not self.wallet.handle:
            LOGGER.debug('Issuer.send_cred_def <!< Wallet %s is closed',
                         self.name)
            raise WalletState('Wallet {} is closed'.format(self.name))

        if not self.pool:
            LOGGER.debug('Issuer.send_cred_def <!< issuer %s has no pool',
                         self.name)
            raise AbsentPool(
                'Issuer {} has no pool: cannot send cred def'.format(
                    self.name))

        rv_json = json.dumps({})
        schema_json = await self.get_schema(schema_key(s_id))
        schema = json.loads(schema_json)

        cd_id = cred_def_id(self.did, schema['seqNo'], self.pool.protocol)
        private_key_ok = True
        with CRED_DEF_CACHE.lock:
            try:
                rv_json = await self.get_cred_def(cd_id)
                LOGGER.info(
                    'Cred def on schema %s version %s already exists on ledger; Issuer %s not sending another',
                    schema['name'], schema['version'], self.name)
            except AbsentCredDef:
                pass  # OK - about to create, store, and send it

            (cred_def_json, private_key_ok) = await self._create_cred_def(
                schema, json.loads(rv_json), revo)

            if not json.loads(
                    rv_json
            ):  # checking the ledger returned no cred def: send it
                req_json = await ledger.build_cred_def_request(
                    self.did, cred_def_json)
                await self._sign_submit(req_json)

                for _ in range(16):  # reasonable timeout
                    try:
                        rv_json = await self.get_cred_def(cd_id
                                                          )  # adds to cache
                        break
                    except AbsentCredDef:
                        await asyncio.sleep(1)
                        LOGGER.info(
                            'Sent cred def %s to ledger, waiting 1s for its appearance',
                            cd_id)

                if not rv_json:
                    LOGGER.debug(
                        'Issuer.send_cred_def <!< timed out waiting on sent cred_def %s',
                        cd_id)
                    raise BadLedgerTxn(
                        'Timed out waiting on sent cred_def {}'.format(cd_id))

                if revo:  # create new rev reg for tag '0'
                    if self.rrbx:
                        (_, rr_size_suggested) = Tails.next_tag(
                            self.dir_tails, cd_id)
                        self.rrb.mark_in_progress(rev_reg_id(cd_id, '0'),
                                                  rr_size or rr_size_suggested)

                    await self._sync_revoc_for_issue(rev_reg_id(
                        cd_id, '0'), rr_size)  # sync rev reg on tag '0'

        if revo and private_key_ok:
            for tag in [
                    str(t) for t in range(
                        1, int(Tails.next_tag(self.dir_tails, cd_id)[0]))
            ]:  # '1' to next-1
                await self._sync_revoc_for_issue(
                    rev_reg_id(cd_id, tag), rr_size if tag == '0' else None)

        makedirs(
            join(self.dir_tails, cd_id),
            exist_ok=True)  # dir required for box id collection, revo or not

        LOGGER.debug('Issuer.send_cred_def <<< %s', rv_json)
        return rv_json
示例#10
0
    async def send_cred_def(self, s_id: str, revocation: bool = True, rr_size: int = None) -> str:
        """
        Create a credential definition as Issuer, store it in its wallet, and send it to the ledger.

        Raise CorruptWallet for wallet not pertaining to current ledger, BadLedgerTxn on failure
        to send credential definition to ledger if need be, WalletState for closed wallet,
        or IndyError for any other failure to create and store credential definition in wallet.

        :param s_id: schema identifier
        :param revocation: whether to support revocation for cred def
        :param rr_size: size of initial revocation registry (default as per RevRegBuilder._create_rev_reg()),
            if revocation supported
        :return: json credential definition as it appears on ledger
        """

        LOGGER.debug('Issuer.send_cred_def >>> s_id: %s, revocation: %s, rr_size: %s', s_id, revocation, rr_size)

        if not ok_schema_id(s_id):
            LOGGER.debug('Issuer.send_cred_def <!< Bad schema id %s', s_id)
            raise BadIdentifier('Bad schema id {}'.format(s_id))

        if not self.wallet.handle:
            LOGGER.debug('Issuer.send_cred_def <!< Wallet %s is closed', self.wallet.name)
            raise WalletState('Wallet {} is closed'.format(self.wallet.name))

        if not self.pool:
            LOGGER.debug('Issuer.send_cred_def <!< issuer %s has no pool', self.wallet.name)
            raise AbsentPool('Issuer {} has no pool: cannot send cred def'.format(self.wallet.name))

        rv_json = json.dumps({})
        schema_json = await self.get_schema(schema_key(s_id))
        schema = json.loads(schema_json)

        cd_id = cred_def_id(self.did, schema['seqNo'], self.pool.protocol)
        private_key_ok = True
        with CRED_DEF_CACHE.lock:
            try:
                rv_json = await self.get_cred_def(cd_id)
                LOGGER.info(
                    'Cred def on schema %s version %s already exists on ledger; Issuer %s not sending another',
                    schema['name'],
                    schema['version'],
                    self.wallet.name)
            except AbsentCredDef:
                pass  # OK - about to create, store, and send it

            try:
                (_, cred_def_json) = await anoncreds.issuer_create_and_store_credential_def(
                    self.wallet.handle,
                    self.did,  # issuer DID
                    schema_json,
                    self.pool.protocol.cd_id_tag(False),  # expect only one cred def per schema and issuer
                    'CL',
                    json.dumps({'support_revocation': revocation}))
                if json.loads(rv_json):
                    private_key_ok = False
                    LOGGER.warning(
                        'New cred def on %s in wallet shadows existing one on ledger: private key not usable', cd_id)
                        # carry on though, this anchor may have other capacities so public key may be good enough
            except IndyError as x_indy:
                if x_indy.error_code == ErrorCode.AnoncredsCredDefAlreadyExistsError:
                    if json.loads(rv_json):
                        LOGGER.info(
                            'Issuer wallet %s reusing existing cred def on schema %s version %s',
                            self.wallet.name,
                            schema['name'],
                            schema['version'])
                    else:
                        LOGGER.debug('Issuer.send_cred_def <!< corrupt wallet %s', self.wallet.name)
                        raise CorruptWallet('Corrupt Issuer wallet {} has cred def on schema {} not on ledger'.format(
                            self.wallet.name,
                            s_id))
                else:
                    LOGGER.debug(
                        'Issuer.send_cred_def <!< cannot store cred def in wallet %s: indy error code %s',
                        self.wallet.name,
                        x_indy.error_code)
                    raise

            if not json.loads(rv_json):  # checking the ledger returned no cred def: send it
                req_json = await ledger.build_cred_def_request(self.did, cred_def_json)
                await self._sign_submit(req_json)

                for _ in range(16):  # reasonable timeout
                    try:
                        rv_json = await self.get_cred_def(cd_id)  # adds to cache
                        break
                    except AbsentCredDef:
                        await asyncio.sleep(1)
                        LOGGER.info('Sent cred def %s to ledger, waiting 1s for its appearance', cd_id)

                if not rv_json:
                    LOGGER.debug('Issuer.send_cred_def <!< timed out waiting on sent cred_def %s', cd_id)
                    raise BadLedgerTxn('Timed out waiting on sent cred_def {}'.format(cd_id))

                if revocation:  # create new rev reg for tag '0'
                    if self._rrbx:
                        (_, rr_size_suggested) = Tails.next_tag(self._dir_tails, cd_id)
                        rr_id = rev_reg_id(cd_id, '0')
                        try:
                            makedirs(join(self._dir_tails_sentinel, rr_id), exist_ok=False)
                        except FileExistsError:
                            LOGGER.warning('Rev reg %s construction already in progress', rr_id)
                        else:
                            open(
                                join(self._dir_tails_sentinel, rr_id, '.{}'.format(rr_size or rr_size_suggested)),
                                'w').close()

                    await self._sync_revoc_for_issue(rev_reg_id(cd_id, '0'), rr_size)  # sync rev reg on tag '0'

        if revocation and private_key_ok:
            for tag in [str(t) for t in range(1, int(Tails.next_tag(self._dir_tails, cd_id)[0]))]:  # '1' to str(next-1)
                await self._sync_revoc_for_issue(rev_reg_id(cd_id, tag), rr_size if tag == '0' else None)

        makedirs(join(self._dir_tails, cd_id), exist_ok=True)  # ensure dir exists for box id collection, revo or not

        LOGGER.debug('Issuer.send_cred_def <<< %s', rv_json)
        return rv_json