blockstack-browser icon indicating copy to clipboard operation
blockstack-browser copied to clipboard

Stale profile.json (data loss)

Open zone117x opened this issue 6 years ago • 3 comments

Related to https://github.com/blockstack/blockstack-browser/issues/1745 @timstackblock and I decided to open a new issue for this since the bug(s) and solution(s) may be a superset of the original issue.


Several actions performed within a blockstack-browser tab cause a cached outdated profile.json to overwrite any changes made from another tab.

Reproducible Example

  1. Keep one or more blockstack-browser tabs open (likely common user behavior).
  2. Sign into a new blockstack app that uses the publish_data scope - look up your profile.json to see the app added. e.g.: https://core.blockstack.org/v1/names/zone117x.id -> https://gaia.blockstack.org/hub/1Nw25PemCRv24UQAcZdaj4uD11nkTCWRTE/profile.json
  3. Go to an open tab and update something like your name, bio, a social proof, etc.
  4. Look up your profile.json again, see the app entry missing.

Potential Fix

Adherence to the single source of truth principle.

  • Never use the cached localStorage profile object except for a few explicit cases (e.g. using the cached name while it is pending registration, or display UI quickly while latest data is being fetched).
  • All writes to the profile should perform a fetch latest -> change only the intended profile fields -> upload the updated profile.
  • These write actions include:
    • Uploading avatar / profile pic.
    • Updating human name.
    • Updating bio / description.
    • Adding an app entry (during auth with publish_data).
    • Updating a social proof or btc/eth/pgp/ssh public key.
  • The fetch latest profile function should always attempt a fresh resolution starting with the source of truth (the identity address) so that name/zonefile updates are respected.
  • All areas performing gaia read & write operations should take into consideration that more than one identity/name can be used, and each identity can have its own gaiahub configuration. Operations on the selected identity should use the gaiahub config specific to that identity. Many areas essentially use gaiahub config for identity[0] and/or whatever the global app state API setting happens to represent.

Demo code example

async function fetchProfile(identityAddress: string) {
/**
 * First tries:
 *   1. Fetch `names` for the `address` with `bitcoinAddressLookupUrl`. 
 *   2. Fetch zonefile with `nameLookupUrl` using the first name returned. 
 *   3. Parse zonefile for the `profile.json` url. 
 * 
 * In some situations one or more parts of this resolution chain will be 
 * unavailable. Examples: 
 *   * Pending name registration (address but no name/zonefile available).
 *   * Legacy zonefile not containing a `profile.json` url. 
 *   * Legacy `profile.json` file not containing a gaiaHubUrl.
 * 
 * In these situations, the locally cached identities will be checked for a 
 * matching address which may contain the profile.json url if a name 
 * is pending registration. 
 * 
 * If still not found then the default gaiahub server is searched for a matching
 * profile.json file at expected & legacy paths. The existing 
 * `fetchProfileLocations` function can be used for this.
 */
}

async function updateLatestProfile(updateCallback: (profile) => void) {
  const profile = await fetchProfile()
  updateCallback(profile)
  await uploadProfile(profile)
}

async function updateProfileBio(desc: string) {
  await updateLatestProfile(profile => {
    profile.account.description = desc
  })
}

async function updateAvatar(picUrl: string) {
  await updateLatestProfile(profile => {
    let imageObj = profile.image.find(img => img.name === 'avatar')
    imageObj = Object.assign({}, DEFAULT_AVATAR, imageObj)
    imageObj.contentUrl = picUrl
  })
}

async function updateAppBucket(appDomain: string, appBucketUrl: string) {
  await updateLatestProfile(profile => {
    profile.apps[appDomain] = appBucketUrl
  })
}

Stale UI in a tab

(This may need to be moved to a new issue)

Typical user behavior could involve having the blockstack-browser webpage open in a perma-tab. Or several tabs, in potentially different web browsers / devices.

The Page Visibility API https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API could be used to prevent a browser tab state from becoming stale.

A simple document.onvisibilitychange hook can be used to fetch possibly updated profile data, without having to perform something like constant polling.

zone117x avatar Apr 19 '19 15:04 zone117x

@timstackblock Would you consider this bug P2 or P3?

markmhendrickson avatar Jul 01 '19 11:07 markmhendrickson

@yknl I see you added this to a DX sprint a few months ago – was progress made?

@hstove are these fixes we can make implicitly as part of the rebuild?

markmhendrickson avatar Jan 21 '20 13:01 markmhendrickson

The latest update to blockstack.js and Gaia that improves concurrency may help with this issue, but some browser changes are required. https://github.com/blockstack/blockstack.js/pull/743

yknl avatar Jan 21 '20 15:01 yknl