def delete_mutable(name, data_id, proxy=None, wallet_keys=None): """ delete_mutable Remove a piece of mutable data from the user's profile. Delete it from the storage providers as well. Returns a dict with {'status': True} on success Returns a dict with {'error': ...} on failure """ if proxy is None: proxy = get_default_proxy() fq_data_id = storage.make_fq_data_id( name, data_id ) legacy = False user_profile, user_zonefile = get_name_profile( name, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True ) if user_profile is None: return user_zonefile # will be an error message name_record = user_zonefile['name_record'] del user_zonefile['name_record'] if blockstack_profiles.is_profile_in_legacy_format( user_zonefile ) or not user_db.is_user_zonefile( user_zonefile ): # zonefile is a legacy profile. There is no immutable data log.info("Profile is in legacy format. No immutable data.") return {'status': True} # already deleted? if not user_db.has_mutable_data( user_profile, data_id ): return {'status': True} # unlink user_db.remove_mutable_data_zonefile( user_profile, data_id ) # put new profile data_privkey = get_data_or_owner_privkey( user_zonefile, name_record['address'], wallet_keys=wallet_keys, config_path=proxy.conf['path'] ) if 'error' in data_privkey: return {'error': data_privkey['error']} else: data_privkey = data_privkey['privatekey'] assert data_privkey is not None rc = storage.put_mutable_data( name, user_profile, data_privkey ) if not rc: return {'error': 'Failed to unlink mutable data from profile'} # remove the data itself rc = storage.delete_mutable_data( fq_data_id, data_privkey ) if not rc: return {'error': 'Failed to delete mutable data from storage providers'} return {'status': True}
def app_delete_resource(blockchain_id, app_domain, res_name, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Remove data from a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, "No data private key or wallet given" data_privkey = wallet_keys.get('data_privkey', None) assert data_privkey, "Wallet does not contain a data private key" data_pubkey = get_pubkey_hex(data_privkey) proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] tombstone = storage.make_data_tombstone(res_data_id) signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey) res = data.delete_mutable(res_data_id, [signed_tombstone], proxy=proxy, storage_drivers=driver_hints, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.error("Failed to delete resource {}: {}".format( res_data_id, res['error'])) return {'error': 'Failed to delete resource'} return {'status': True}
def app_put_resource( blockchain_id, app_domain, res_name, res_data, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ): """ Store data to a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ assert isinstance(res_data, (str, unicode)), "Resource must be a string" try: json.dumps(res_data) except: raise AssertionError("Resource must be a JSON-serializable string") if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) data_pubkey = get_pubkey_hex(data_privkey) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] res_blob = data.make_mutable_data_info(res_data_id, res_data, is_fq_data_id=True) res_blob_str = data.data_blob_serialize(res_blob) res_sig = data.data_blob_sign(res_blob_str, data_privkey) res = data.put_mutable(res_data_id, res_blob_str, data_pubkey, res_sig, res_blob['version'], blockchain_id=blockchain_id, config_path=config_path, storage_drivers=driver_hints) if 'error' in res: log.error("Failed to store resource {}: {}".format(res_data_id, res['error'])) return {'error': 'Failed to store resource'} return {'status': True, 'version': res_blob['version']}
def app_delete_resource( blockchain_id, app_domain, res_name, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ): """ Remove data from a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, "No data private key or wallet given" data_privkey = wallet_keys.get('data_privkey', None) assert data_privkey, "Wallet does not contain a data private key" data_pubkey = get_pubkey_hex(data_privkey) proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] tombstone = storage.make_data_tombstone(res_data_id) signed_tombstone = storage.sign_data_tombstone(res_data_id, data_privkey) res = data.delete_mutable(res_data_id, [signed_tombstone], proxy=proxy, storage_drivers=driver_hints, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.error("Failed to delete resource {}: {}".format(res_data_id, res['error'])) return {'error': 'Failed to delete resource'} return {'status': True}
def put_mutable(name, data_id, data_json, proxy=None, create_only=False, update_only=False, txid=None, version=None, make_version=None, wallet_keys=None): """ put_mutable Given a name, an ID for the data, and the data itself, sign and upload the data to the configured storage providers. Add an entry for it into the user's profile as well. ** Consistency ** @version, if given, is the version to include in the data. @make_version, if given, is a callback that takes the data_id, data_json, and current version as arguments, and generates the version to be included in the data record uploaded. If ver is not given, but make_ver is, then make_ver will be used to generate the version. If neither ver nor make_ver are given, the mutable data (if it already exists) is fetched, and the version is calculated as the larget known version + 1. ** Durability ** Replication is best-effort. If one storage provider driver succeeds, the put_mutable succeeds. If they all fail, then put_mutable fails. More complex behavior can be had by creating a "meta-driver" that calls existing drivers' methods in the desired manner. Returns a dict with {'status': True, 'version': version, ...} on success Returns a dict with 'error' set on failure """ if type(data_json) not in [dict]: raise ValueError("Mutable data must be a dict") if proxy is None: proxy = get_default_proxy() fq_data_id = storage.make_fq_data_id( name, data_id ) name_record = None user_profile, user_zonefile, created_new_zonefile = get_and_migrate_profile( name, create_if_absent=True, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True ) if 'error' in user_profile: return user_profile if created_new_zonefile: log.debug("User profile is legacy") return {'error': "User profile is in legacy format, which does not support this operation. You must first migrate it with the 'migrate' command."} name_record = user_zonefile['name_record'] del user_zonefile['name_record'] log.debug("Profile for %s is currently:\n%s" % (name, json.dumps(user_profile, indent=4, sort_keys=True))) exists = user_db.has_mutable_data( user_profile, data_id ) if not exists and update_only: return {'error': 'Mutable datum does not exist'} if exists and create_only: return {'error': 'Mutable datum already exists'} # get the version to use if version is None: version = put_mutable_get_version( user_profile, data_id, data_json, make_version=make_version ) # generate the mutable zonefile data_privkey = get_data_or_owner_privkey( user_zonefile, name_record['address'], wallet_keys=wallet_keys, config_path=proxy.conf['path'] ) if 'error' in data_privkey: # error text return {'error': data_privkey['error']} else: data_privkey = data_privkey['privatekey'] assert data_privkey is not None urls = storage.make_mutable_data_urls( fq_data_id ) mutable_zonefile = user_db.make_mutable_data_zonefile( data_id, version, urls ) # add the mutable zonefile to the profile rc = user_db.put_mutable_data_zonefile( user_profile, data_id, version, mutable_zonefile ) assert rc, "Failed to put mutable data zonefile" # for legacy migration... txid = None zonefile_hash = None result = {} # update the profile with the new zonefile rc = storage.put_mutable_data( name, user_profile, data_privkey ) if not rc: result['error'] = 'Failed to store mutable data zonefile to profile' return result # put the mutable data record itself rc = storage.put_mutable_data( fq_data_id, data_json, data_privkey ) if not rc: result['error'] = "Failed to store mutable data" return result # remember which version this was rc = store_mutable_data_version(proxy.conf, fq_data_id, version) if not rc: result['error'] = "Failed to store mutable data version" return result result['status'] = True result['version'] = version log.debug("Put '%s' to %s mutable data (version %s)\nProfile is now:\n%s" % (data_id, name, version, json.dumps(user_profile, indent=4, sort_keys=True))) return result
def get_mutable(name, data_id, proxy=None, ver_min=None, ver_max=None, ver_check=None, conf=None, wallet_keys=None): """ get_mutable Fetch a piece of mutable data. Use @data_id to look it up in the user's profile, and then fetch and erify the data itself from the configured storage providers. If @ver_min is given, ensure the data's version is greater or equal to it. If @ver_max is given, ensure the data's version is less than it. If @ver_check is given, it must be a callable that takes the name, data and version and returns True/False Return {'data': the data, 'version': the version} on success Return {'error': ...} on error """ if proxy is None: proxy = get_default_proxy() if conf is None: conf = proxy.conf fq_data_id = storage.make_fq_data_id( name, data_id ) user_profile, user_zonefile = get_name_profile( name, proxy=proxy, wallet_keys=wallet_keys, include_name_record=True ) if user_profile is None: return user_zonefile # will be an error message # recover name record name_record = user_zonefile['name_record'] del user_zonefile['name_record'] if blockstack_profiles.is_profile_in_legacy_format( user_zonefile ) or not user_db.is_user_zonefile( user_zonefile ): # profile has not been converted to the new zonefile format yet. return {'error': 'Profile is in a legacy format that does not support mutable data.'} # get the mutable data zonefile if not user_db.has_mutable_data( user_profile, data_id ): return {'error': "No such mutable datum"} mutable_data_zonefile = user_db.get_mutable_data_zonefile( user_profile, data_id ) assert mutable_data_zonefile is not None, "BUG: could not look up mutable datum '%s'.'%s'" % (name, data_id) # get user's data public key and owner address data_pubkey = user_db.user_zonefile_data_pubkey( user_zonefile ) data_address = name_record['address'] if data_pubkey is None: log.warn("Falling back to owner address for authentication") # get the mutable data itself urls = user_db.mutable_data_zonefile_urls( mutable_data_zonefile ) mutable_data = storage.get_mutable_data(fq_data_id, data_pubkey, urls=urls, data_address=data_address ) if mutable_data is None: return {'error': "Failed to look up mutable datum"} expected_version = load_mutable_data_version( conf, name, data_id ) if expected_version is None: expected_version = 0 # check consistency version = user_db.mutable_data_version( user_profile, data_id ) if ver_min is not None and ver_min > version: return {'error': 'Mutable data is stale'} if ver_max is not None and ver_max <= version: return {'error': 'Mutable data is in the future'} if ver_check is not None: rc = ver_check( name, mutable_data, version ) if not rc: return {'error': 'Mutable data consistency check failed'} elif expected_version > version: return {'error': 'Mutable data is stale; a later version was previously fetched'} rc = store_mutable_data_version( conf, fq_data_id, version ) if not rc: return {'error': 'Failed to store consistency information'} return {'data': mutable_data, 'version': version}
def app_unpublish( blockchain_id, app_domain, force=False, data_privkey=None, app_config=None, wallet_keys=None, proxy=None, config_path=CONFIG_PATH ): """ Unpublish an application Deletes its config and index. Does NOT delete its resources. Does NOT delete user data. if force is True, then we will try to delete the app state even if we can't load the app config WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app. Use with caution! Return {'status': True, 'app_config': ..., 'retry': ...} on success. If retry is True, then retry this method with the given app_config Return {'error': ...} on error """ proxy = get_default_proxy() if proxy is None else proxy # find out where to delete from data_pubkey = None if data_privkey is not None: data_pubkey = get_pubkey_hex(str(data_privkey)) if app_config is None: app_config = app_get_config(blockchain_id, app_domain, data_pubkey=data_pubkey, proxy=proxy, config_path=CONFIG_PATH ) if 'error' in app_config: if not force: log.error("Failed to load app config for {}'s {}".format(blockchain_id, app_domain)) return {'error': 'Failed to load app config'} else: # keep going app_config = None log.warning("Failed to load app config, but proceeding at caller request") config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') index_data_id = storage.make_fq_data_id(app_domain, 'index.html') storage_drivers = None if app_config is not None: # only use the ones we have to urls = user_db.urls_from_uris(app_config['index_uris']) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] storage_drivers = list(set(driver_names)) ret = {} # delete the index index_tombstone = storage.make_data_tombstone(index_data_id) signed_index_tombstone = storage.sign_data_tombstone(index_data_id, data_privkey) res = data.delete_mutable(index_data_id, [signed_index_tombstone], proxy=proxy, storage_drivers=storage_drivers, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete index file {}".format(index_data_id)) ret['app_config'] = app_config ret['retry'] = True # delete the config config_tombstone = storage.make_data_tombstone(config_data_id) signed_config_tombstone = storage.sign_data_tombstone(config_data_id, data_privkey) res = data.delete_mutable(config_data_id, [signed_config_tombstone], proxy=proxy, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete config file {}".format(config_data_id)) if not ret.has_key('app_config'): ret['app_config'] = app_config ret['retry'] = True ret['status'] = True return ret
def app_publish( dev_blockchain_id, app_domain, app_method_list, app_index_uris, app_index_file, app_driver_hints=[], data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH ): """ Instantiate an application. * replicate the (opaque) app index file to "index.html" to each URL in app_uris * replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers. This succeeds even if the app already exists (in which case, it will be overwritten). This method is idempotent, so it can be retried on failure. data_privkey should be the publisher's private key (i.e. their data key) name should be the blockchain ID that points to data_pubkey Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy # replicate configuration data (method list and app URIs) app_cfg = { 'blockchain_id': dev_blockchain_id, 'app_domain': app_domain, 'index_uris': app_index_uris, 'api_methods': app_method_list, 'driver_hints': app_driver_hints, } jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA) data_pubkey = get_pubkey_hex(data_privkey) config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') app_cfg_blob = data.make_mutable_data_info(config_data_id, app_cfg, is_fq_data_id=True) app_cfg_str = data.data_blob_serialize(app_cfg_blob) app_cfg_sig = data.data_blob_sign( app_cfg_str, data_privkey ) res = data.put_mutable(config_data_id, app_cfg_str, data_pubkey, app_cfg_sig, app_cfg_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path) if 'error' in res: log.error('Failed to replicate application configuration {}: {}'.format(config_data_id, res['error'])) return {'error': 'Failed to replicate application config'} # what drivers to use for the index file? urls = user_db.urls_from_uris(app_index_uris) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] driver_names = list(set(driver_names)) index_data_id = storage.make_fq_data_id(app_domain, 'index.html') # replicate app index file (at least one must succeed) # NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored. app_index_blob = data.make_mutable_data_info(index_data_id, app_index_file, is_fq_data_id=True) app_index_blob_str = data.data_blob_serialize(app_index_blob) app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey) res = data.put_mutable( index_data_id, app_index_blob_str, data_pubkey, app_index_sig, app_index_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path, storage_drivers=app_driver_hints ) if 'error' in res: log.error("Failed to replicate application index file to {}: {}".format(",".join(urls), res['error'])) return {'error': 'Failed to replicate index file'} return {'status': True, 'config_fq_data_id': config_data_id, 'index_fq_data_id': index_data_id}
def app_unpublish(blockchain_id, app_domain, force=False, data_privkey=None, app_config=None, wallet_keys=None, proxy=None, config_path=CONFIG_PATH): """ Unpublish an application Deletes its config and index. Does NOT delete its resources. Does NOT delete user data. if force is True, then we will try to delete the app state even if we can't load the app config WARNING: force can be dangerous, since it can delete data via drivers that were never meant for this app. Use with caution! Return {'status': True, 'app_config': ..., 'retry': ...} on success. If retry is True, then retry this method with the given app_config Return {'error': ...} on error """ proxy = get_default_proxy() if proxy is None else proxy # find out where to delete from data_pubkey = None if data_privkey is not None: data_pubkey = get_pubkey_hex(str(data_privkey)) if app_config is None: app_config = app_get_config(blockchain_id, app_domain, data_pubkey=data_pubkey, proxy=proxy, config_path=CONFIG_PATH) if 'error' in app_config: if not force: log.error("Failed to load app config for {}'s {}".format( blockchain_id, app_domain)) return {'error': 'Failed to load app config'} else: # keep going app_config = None log.warning( "Failed to load app config, but proceeding at caller request" ) config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') index_data_id = storage.make_fq_data_id(app_domain, 'index.html') storage_drivers = None if app_config is not None: # only use the ones we have to urls = user_db.urls_from_uris(app_config['index_uris']) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] storage_drivers = list(set(driver_names)) ret = {} # delete the index index_tombstone = storage.make_data_tombstone(index_data_id) signed_index_tombstone = storage.sign_data_tombstone( index_data_id, data_privkey) res = data.delete_mutable(index_data_id, [signed_index_tombstone], proxy=proxy, storage_drivers=storage_drivers, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete index file {}".format(index_data_id)) ret['app_config'] = app_config ret['retry'] = True # delete the config config_tombstone = storage.make_data_tombstone(config_data_id) signed_config_tombstone = storage.sign_data_tombstone( config_data_id, data_privkey) res = data.delete_mutable(config_data_id, [signed_config_tombstone], proxy=proxy, blockchain_id=blockchain_id, is_fq_data_id=True, config_path=config_path) if 'error' in res: log.warning("Failed to delete config file {}".format(config_data_id)) if not ret.has_key('app_config'): ret['app_config'] = app_config ret['retry'] = True ret['status'] = True return ret
def app_put_resource(blockchain_id, app_domain, res_name, res_data, app_config=None, data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Store data to a named application resource in mutable storage. data_privkey should be the publisher's private key name should be a blockchain ID that points to the public key if app_config is not None, then the driver hints will be honored. Return {'status': True, 'version': ...} on success Return {'error': ...} on error """ assert isinstance(res_data, (str, unicode)), "Resource must be a string" try: json.dumps(res_data) except: raise AssertionError("Resource must be a JSON-serializable string") if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy res_data_id = storage.make_fq_data_id(app_domain, res_name) data_pubkey = get_pubkey_hex(data_privkey) driver_hints = None if app_config is not None: # use driver hints driver_hints = app_config['driver_hints'] res_blob = data.make_mutable_data_info(res_data_id, res_data, is_fq_data_id=True) res_blob_str = data.data_blob_serialize(res_blob) res_sig = data.data_blob_sign(res_blob_str, data_privkey) res = data.put_mutable(res_data_id, res_blob_str, data_pubkey, res_sig, res_blob['version'], blockchain_id=blockchain_id, config_path=config_path, storage_drivers=driver_hints) if 'error' in res: log.error("Failed to store resource {}: {}".format( res_data_id, res['error'])) return {'error': 'Failed to store resource'} return {'status': True, 'version': res_blob['version']}
def app_publish(dev_blockchain_id, app_domain, app_method_list, app_index_uris, app_index_file, app_driver_hints=[], data_privkey=None, proxy=None, wallet_keys=None, config_path=CONFIG_PATH): """ Instantiate an application. * replicate the (opaque) app index file to "index.html" to each URL in app_uris * replicate the list of URIs and the list of methods to ".blockstack" via each of the client's storage drivers. This succeeds even if the app already exists (in which case, it will be overwritten). This method is idempotent, so it can be retried on failure. data_privkey should be the publisher's private key (i.e. their data key) name should be the blockchain ID that points to data_pubkey Return {'status': True, 'config_fq_data_id': config's fully-qualified data ID, 'index_fq_data_id': index file's fully-qualified data ID} on success Return {'error': ...} on error """ if data_privkey is None: assert wallet_keys, 'Missing both data private key and wallet keys' data_privkey = wallet_keys.get('data_privkey') assert data_privkey, "Wallet does not have a data private key" proxy = get_default_proxy() if proxy is None else proxy # replicate configuration data (method list and app URIs) app_cfg = { 'blockchain_id': dev_blockchain_id, 'app_domain': app_domain, 'index_uris': app_index_uris, 'api_methods': app_method_list, 'driver_hints': app_driver_hints, } jsonschema.validate(app_cfg, APP_CONFIG_SCHEMA) data_pubkey = get_pubkey_hex(data_privkey) config_data_id = storage.make_fq_data_id(app_domain, '.blockstack') app_cfg_blob = data.make_mutable_data_info(config_data_id, app_cfg, is_fq_data_id=True) app_cfg_str = data.data_blob_serialize(app_cfg_blob) app_cfg_sig = data.data_blob_sign(app_cfg_str, data_privkey) res = data.put_mutable(config_data_id, app_cfg_str, data_pubkey, app_cfg_sig, app_cfg_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path) if 'error' in res: log.error( 'Failed to replicate application configuration {}: {}'.format( config_data_id, res['error'])) return {'error': 'Failed to replicate application config'} # what drivers to use for the index file? urls = user_db.urls_from_uris(app_index_uris) driver_names = [] for url in urls: drivers = storage.get_drivers_for_url(url) driver_names += [d.__name__ for d in drivers] driver_names = list(set(driver_names)) index_data_id = storage.make_fq_data_id(app_domain, 'index.html') # replicate app index file (at least one must succeed) # NOTE: the publisher is free to use alternative URIs that are not supported; they'll just be ignored. app_index_blob = data.make_mutable_data_info(index_data_id, app_index_file, is_fq_data_id=True) app_index_blob_str = data.data_blob_serialize(app_index_blob) app_index_sig = data.data_blob_sign(app_index_blob_str, data_privkey) res = data.put_mutable(index_data_id, app_index_blob_str, data_pubkey, app_index_sig, app_index_blob['version'], blockchain_id=dev_blockchain_id, config_path=config_path, storage_drivers=app_driver_hints) if 'error' in res: log.error( "Failed to replicate application index file to {}: {}".format( ",".join(urls), res['error'])) return {'error': 'Failed to replicate index file'} return { 'status': True, 'config_fq_data_id': config_data_id, 'index_fq_data_id': index_data_id }