Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wallet] Fix web3 syncing sagas #809

Merged
merged 2 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/mobile/src/networkInfo/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { call, cancelled, put, spawn, take } from 'redux-saga/effects'
import { waitForGethConnectivity } from 'src/geth/saga'
import { setNetworkConnectivity } from 'src/networkInfo/actions'
import Logger from 'src/utils/Logger'
import { checkWeb3Sync } from 'src/web3/saga'
import { waitForWeb3Sync } from 'src/web3/saga'

const TAG = 'networkInfo/saga'

Expand All @@ -17,7 +17,7 @@ export function* waitForRehydrate() {

export function* waitWeb3LastBlock() {
yield waitForGethConnectivity()
yield checkWeb3Sync()
yield waitForWeb3Sync()
Copy link
Contributor Author

@jmrossy jmrossy Sep 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmcewen IIRC you recommended calling this check but wait is what it's really doing. Mind if we rename?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Volpe and I had a discussion about this - the reason I don't like using wait is that it actually makes a call to get the latest block to the geth node. Since I think we treat the waitFor generators as free in terms of cost, I would be wary of someone just adding this elsewhere and we end up doing a bunch of duplicate polling. I think waitFor should imply solely waiting on state changes.

}

function createNetworkStatusChannel() {
Expand Down
15 changes: 0 additions & 15 deletions packages/mobile/src/web3/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ export interface SetLatestBlockNumberAction {
latestBlockNumber: number
}

export interface SetProgressAction {
type: Actions.SET_PROGRESS
payload: {
syncProgress: number
}
}

export interface UpdateWeb3SyncProgressAction {
type: Actions.UPDATE_WEB3_SYNC_PROGRESS
payload: {
Expand All @@ -51,7 +44,6 @@ export interface UpdateWeb3SyncProgressAction {
export type ActionTypes =
| SetAccountAction
| SetCommentKeyAction
| SetProgressAction
| SetLatestBlockNumberAction
| UpdateWeb3SyncProgressAction

Expand All @@ -75,13 +67,6 @@ export const setLatestBlockNumber = (latestBlockNumber: number): SetLatestBlockN
latestBlockNumber,
})

export const setSyncProgress = (syncProgress: number) => ({
type: Actions.SET_PROGRESS,
payload: {
syncProgress,
},
})

// TODO: Remove duplicaiton with SetProgress action (this is currently unused)
export const updateWeb3SyncProgress = (payload: {
startingBlock: number
Expand Down
33 changes: 8 additions & 25 deletions packages/mobile/src/web3/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper'
import { Actions, ActionTypes, UpdateWeb3SyncProgressAction } from 'src/web3/actions'
import { Actions, ActionTypes } from 'src/web3/actions'

export interface State {
syncProgress: number
syncProgressData: {
syncProgress: {
startingBlock: number
martinvol marked this conversation as resolved.
Show resolved Hide resolved
currentBlock: number
startBlock: number
highestBlock: number
}
latestBlockNumber: number
Expand All @@ -14,26 +13,16 @@ export interface State {
}

const initialState: State = {
syncProgress: 0,
syncProgressData: {
syncProgress: {
startingBlock: 0,
currentBlock: 0,
highestBlock: 0,
startBlock: 0,
},
latestBlockNumber: 0,
account: null,
commentKey: null,
}

function calculateSyncProgress(action: UpdateWeb3SyncProgressAction) {
if (action.payload.currentBlock === 0) {
return 0
}
const numerator = action.payload.currentBlock - action.payload.startingBlock
const denominator = action.payload.highestBlock - action.payload.startingBlock
return (100 * numerator) / denominator
}

export const reducer = (
state: State | undefined = initialState,
action: ActionTypes | RehydrateAction
Expand All @@ -44,11 +33,10 @@ export const reducer = (
return {
...state,
...getRehydratePayload(action, 'web3'),
syncProgress: 0,
syncProgressData: {
syncProgress: {
startingBlock: 0,
currentBlock: 0,
highestBlock: 0,
startBlock: 0,
},
latestBlockNumber: 0,
}
Expand All @@ -63,11 +51,6 @@ export const reducer = (
...state,
commentKey: action.commentKey,
}
case Actions.SET_PROGRESS:
return {
...state,
syncProgress: action.payload.syncProgress,
}
case Actions.SET_BLOCK_NUMBER:
return {
...state,
Expand All @@ -76,7 +59,7 @@ export const reducer = (
case Actions.UPDATE_WEB3_SYNC_PROGRESS:
return {
...state,
syncProgress: calculateSyncProgress(action),
syncProgress: action.payload,
}

default:
Expand Down
40 changes: 16 additions & 24 deletions packages/mobile/src/web3/saga.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { expectSaga } from 'redux-saga-test-plan'
import { call, delay, select } from 'redux-saga/effects'
import { pincodeSelector } from 'src/account/reducer'
import { waitForGethConnectivity } from 'src/geth/saga'
import { navigateToError } from 'src/navigator/NavigationService'
import { setLatestBlockNumber, updateWeb3SyncProgress } from 'src/web3/actions'
import {
getLatestBlock,
setLatestBlockNumber,
setSyncProgress,
updateWeb3SyncProgress,
} from 'src/web3/actions'
import {
_CHECK_SYNC_PROGRESS_TIMEOUT,
_checkWeb3SyncProgressClaim,
checkWeb3Sync,
checkWeb3SyncProgress,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinvol I do like the convention of _ before functions only exported to test, but until this VSCode bug gets fixed, it just causes a headache :(
prettier/prettier-vscode#716
We'd need to either disable VS code import organizing entirely (would not recommend) or fix this file manually before lint will pass.
Suggest we table this convention for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, no problem. Disabling VS code import organizing might not be a bad a idea since the pre-commit hook also sorts imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but the way it de-dupes imports for you and removes unused ones is really nice

createNewAccount,
SYNC_TIMEOUT,
waitForWeb3Sync,
} from 'src/web3/saga'
import { currentAccountSelector } from 'src/web3/selectors'
import { createMockStore, sleep } from 'test/utils'
Expand All @@ -37,6 +31,7 @@ jest.mock('src/web3/contracts', () => ({
.fn()
.mockReturnValueOnce({ startingBlock: 0, currentBlock: 10, highestBlock: 100 })
.mockReturnValueOnce(false),
getBlock: jest.fn(() => ({ number: LAST_BLOCK_NUMBER })),
},
},
}))
Expand Down Expand Up @@ -66,40 +61,37 @@ describe(createNewAccount, () => {
})
})

describe(checkWeb3Sync, () => {
describe(waitForWeb3Sync, () => {
it('reports connection successfully', async () => {
await expectSaga(checkWeb3Sync)
await expectSaga(waitForWeb3Sync)
.withState(state)
.provide([
[call(waitForGethConnectivity), true],
// needs this async function to win the race with a delay
[call(_checkWeb3SyncProgressClaim), call(async () => true)],
[call(getLatestBlock), { number: LAST_BLOCK_NUMBER }],
[call(checkWeb3SyncProgress), call(async () => true)],
])
.put(setLatestBlockNumber(LAST_BLOCK_NUMBER))
.returns(true)
.run()
})

it('times out', async () => {
await expectSaga(checkWeb3Sync)
await expectSaga(waitForWeb3Sync)
.withState(state)
.provide([
[call(waitForGethConnectivity), true],
[call(_checkWeb3SyncProgressClaim), call(sleep, 100)], // sleep so timeout always wins the race
[delay(_CHECK_SYNC_PROGRESS_TIMEOUT), true],
[call(getLatestBlock), { number: LAST_BLOCK_NUMBER }],
[call(checkWeb3SyncProgress), call(sleep, 100)], // sleep so timeout always wins the race
[delay(SYNC_TIMEOUT), true],
])
.returns(false)
.run()
expect(navigateToError).toHaveBeenCalled()
})
})

describe(_checkWeb3SyncProgressClaim, () => {
describe(checkWeb3SyncProgress, () => {
it('reports web3 status correctly', async () => {
await expectSaga(_checkWeb3SyncProgressClaim)
await expectSaga(checkWeb3SyncProgress)
.withState(state)
.put(updateWeb3SyncProgress({ startingBlock: 0, currentBlock: 10, highestBlock: 100 })) // is syncing the first time
.put(setSyncProgress(100)) // finished syncing the second time
.put(setLatestBlockNumber(LAST_BLOCK_NUMBER)) // finished syncing the second time
.returns(true)
.run()
})
Expand Down
85 changes: 39 additions & 46 deletions packages/mobile/src/web3/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { currentLanguageSelector } from 'src/app/reducers'
import { getWordlist } from 'src/backup/utils'
import { UNLOCK_DURATION } from 'src/geth/consts'
import { deleteChainData } from 'src/geth/geth'
import { waitForGethConnectivity } from 'src/geth/saga'
import { navigateToError } from 'src/navigator/NavigationService'
import { waitWeb3LastBlock } from 'src/networkInfo/saga'
import Logger from 'src/utils/Logger'
Expand All @@ -23,7 +22,6 @@ import {
setAccount,
setLatestBlockNumber,
setPrivateCommentKey,
setSyncProgress,
unlockAccount,
updateWeb3SyncProgress,
} from 'src/web3/actions'
Expand All @@ -36,68 +34,67 @@ const MNEMONIC_BIT_LENGTH = (ETH_PRIVATE_KEY_LENGTH * 8) / 2

const TAG = 'web3/saga'
// The timeout for web3 to complete syncing and the latestBlock to be > 0
const CHECK_SYNC_PROGRESS_TIMEOUT = 60000
export const SYNC_TIMEOUT = 60000
martinvol marked this conversation as resolved.
Show resolved Hide resolved
const BLOCK_CHAIN_CORRUPTION_ERROR = "Error: CONNECTION ERROR: Couldn't connect to node on IPC."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove this to a json as nice to have?


// checks if web3 claims it is currently syncing or not
function* checkWeb3SyncProgressClaim() {
// checks if web3 claims it is currently syncing and attempts to wait for it to complete
export function* checkWeb3SyncProgress() {
while (true) {
try {
const syncProgress = yield web3.eth.isSyncing() // returns true when it's still syncing and thus not ready
if (typeof syncProgress === 'boolean' && !syncProgress) {
Logger.debug(TAG, 'checkWeb3SyncProgressClaim', 'sync complete')
Logger.debug(TAG, 'checkWeb3SyncProgress', 'Checking sync progress')

// isSyncing returns a syncProgress object when it's still syncing, false otherwise
const syncProgress = yield web3.eth.isSyncing()

yield put(setSyncProgress(100))
return true
if (typeof syncProgress === 'boolean' && !syncProgress) {
Logger.debug(TAG, 'checkWeb3SyncProgress', 'Sync maybe complete, checking')

const latestBlock: Block = yield call(getLatestBlock)
martinvol marked this conversation as resolved.
Show resolved Hide resolved
if (latestBlock && latestBlock.number > 0) {
yield put(setLatestBlockNumber(latestBlock.number))
Logger.debug(TAG, 'checkWeb3SyncProgress', 'Sync is complete')
return true
} else {
Logger.debug(TAG, 'checkWeb3SyncProgress', 'Sync not actually complete, still waiting')
}
} else {
yield put(updateWeb3SyncProgress(syncProgress))
}
Logger.debug(TAG, 'checkWeb3SyncProgressClaim', 'sync in progress')
yield put(updateWeb3SyncProgress(syncProgress))
// not ready yet, keep looping

yield delay(100) // wait 100ms while web3 syncs
} catch (error) {
if (error.toString().toLowerCase() === BLOCK_CHAIN_CORRUPTION_ERROR.toLowerCase()) {
CeloAnalytics.track(CustomEventNames.blockChainCorruption, {}, true)
const deleted = yield call(deleteChainData)
if (deleted) {
navigateToError('corruptedChainDeleted')
}
throw new Error('Corrupted chain data encountered')
} else {
Logger.error(TAG, `checking web3 sync progress: ${error}`)
Logger.error(TAG, 'Unexpected sync error', error)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinvol it's better to provide the error as the 3rd param to Logger.error, which has special logic to extract and format the error message nicely

}
return false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinvol previously, you were not throwing or returning false in this case, which allowed the calling function to finish as though syncing were done.

}
}
}

// The worker listening to sync progress requests
export function* checkWeb3Sync() {
export function* waitForWeb3Sync() {
try {
yield call(waitForGethConnectivity)
try {
const { timeout } = yield race({
checkProgress: call(checkWeb3SyncProgressClaim),
timeout: delay(CHECK_SYNC_PROGRESS_TIMEOUT),
})

if (timeout) {
Logger.error(TAG, 'Could not complete sync progress check')
navigateToError('web3FailedToSync')
}

const latestBlock: Block = yield call(getLatestBlock)
if (latestBlock && latestBlock.number > 0) {
yield put(setLatestBlockNumber(latestBlock.number))
} else {
Logger.error(
TAG,
`web3 indicated sync complete, yet the latest block is ${JSON.stringify(latestBlock)}`
)
}
} catch (error) {
Logger.error(TAG, 'checkWeb3Sync', error)
navigateToError('errorDuringSync')
const { syncComplete, timeout } = yield race({
syncComplete: call(checkWeb3SyncProgress),
timeout: delay(SYNC_TIMEOUT),
})

if (timeout || !syncComplete) {
Logger.error(TAG, 'Could not complete sync')
navigateToError('web3FailedToSync')
return false
}

return true
} catch (error) {
Logger.error(TAG, 'checkWeb3Sync saga error', error)
Logger.error(TAG, 'checkWeb3Sync', error)
navigateToError('errorDuringSync')
return false
}
}

Expand Down Expand Up @@ -218,7 +215,3 @@ export function* getConnectedUnlockedAccount() {
throw new Error(ErrorMessages.INCORRECT_PIN)
}
}

// exported for testing
export const _checkWeb3SyncProgressClaim = checkWeb3SyncProgressClaim
export const _CHECK_SYNC_PROGRESS_TIMEOUT = CHECK_SYNC_PROGRESS_TIMEOUT
8 changes: 8 additions & 0 deletions packages/mobile/test/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ const v0Schema = {
recipients: {
recipientCache: {},
},
web3: {
...vNeg1Schema.web3,
syncProgress: {
startingBlock: 0,
currentBlock: 0,
highestBlock: 0,
},
},
}

export function getLatestSchema(): Partial<RootState> {
Expand Down