From d2606997a35a719d9bde90ad9436d32f22cd13d8 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 23 Jun 2020 10:48:58 -0300 Subject: [PATCH] Add ALM failed validator transactions detection (#357) --- alm/.env.example | 3 + alm/package.json | 2 + alm/src/components/ConfirmationsContainer.tsx | 6 +- alm/src/components/ExecutionConfirmation.tsx | 4 +- alm/src/components/StatusContainer.tsx | 2 +- .../components/ValidatorsConfirmations.tsx | 4 +- alm/src/components/commons/Labels.tsx | 7 + alm/src/config/constants.ts | 12 ++ alm/src/hooks/useMessageConfirmations.ts | 46 +++-- alm/src/hooks/useTransactionStatus.ts | 4 +- alm/src/themes/Dark.tsx | 4 + alm/src/themes/GlobalStyle.tsx | 2 + alm/src/utils/executionWaitingForBlocks.ts | 7 + alm/src/utils/explorer.ts | 164 +++++++++++++++++ alm/src/utils/getConfirmationsForTx.ts | 170 +++++++++++++----- alm/src/utils/getFinalizationEvent.ts | 60 ++++++- alm/src/utils/web3.ts | 13 ++ yarn.lock | 30 ++++ 18 files changed, 471 insertions(+), 69 deletions(-) create mode 100644 alm/src/utils/explorer.ts diff --git a/alm/.env.example b/alm/.env.example index 1fb92664..16ae5492 100644 --- a/alm/.env.example +++ b/alm/.env.example @@ -9,3 +9,6 @@ ALM_FOREIGN_NETWORK_NAME=Kovan Testnet ALM_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s ALM_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s + +ALM_HOME_EXPLORER_API=https://blockscout.com/poa/sokol/api +ALM_FOREIGN_EXPLORER_API=https://kovan.etherscan.io/api diff --git a/alm/package.json b/alm/package.json index bafc9ef2..8d45f626 100644 --- a/alm/package.json +++ b/alm/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^7.1.2", "@types/jest": "^24.0.0", "@types/node": "^12.0.0", + "@types/promise-retry": "^1.1.3", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@types/react-router-dom": "^5.1.5", @@ -16,6 +17,7 @@ "customize-cra": "^1.0.0", "date-fns": "^2.14.0", "fast-memoize": "^2.5.2", + "promise-retry": "^2.0.1", "react": "^16.13.1", "react-app-rewired": "^2.1.6", "react-dom": "^16.13.1", diff --git a/alm/src/components/ConfirmationsContainer.tsx b/alm/src/components/ConfirmationsContainer.tsx index 3fcee5a2..34cdb1f0 100644 --- a/alm/src/components/ConfirmationsContainer.tsx +++ b/alm/src/components/ConfirmationsContainer.tsx @@ -35,9 +35,10 @@ export interface ConfirmationsContainerParams { message: MessageObject receipt: Maybe fromHome: boolean + timestamp: number } -export const ConfirmationsContainer = ({ message, receipt, fromHome }: ConfirmationsContainerParams) => { +export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }: ConfirmationsContainerParams) => { const { home: { name: homeName }, foreign: { name: foreignName } @@ -45,7 +46,8 @@ export const ConfirmationsContainer = ({ message, receipt, fromHome }: Confirmat const { confirmations, status, executionData, signatureCollected } = useMessageConfirmations({ message, receipt, - fromHome + fromHome, + timestamp }) return ( diff --git a/alm/src/components/ExecutionConfirmation.tsx b/alm/src/components/ExecutionConfirmation.tsx index 755e6c99..54b658e6 100644 --- a/alm/src/components/ExecutionConfirmation.tsx +++ b/alm/src/components/ExecutionConfirmation.tsx @@ -5,7 +5,7 @@ import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' import { SimpleLoading } from './commons/Loading' import styled from 'styled-components' import { ExecutionData } from '../hooks/useMessageConfirmations' -import { GreyLabel, SuccessLabel } from './commons/Labels' +import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' import { ExplorerTxLink } from './commons/ExplorerTxLink' const Thead = styled.thead` @@ -32,6 +32,8 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir switch (validatorStatus) { case VALIDATOR_CONFIRMATION_STATUS.SUCCESS: return {validatorStatus} + case VALIDATOR_CONFIRMATION_STATUS.FAILED: + return {validatorStatus} case VALIDATOR_CONFIRMATION_STATUS.WAITING: return {validatorStatus} default: diff --git a/alm/src/components/StatusContainer.tsx b/alm/src/components/StatusContainer.tsx index 6d959119..0f3ebc56 100644 --- a/alm/src/components/StatusContainer.tsx +++ b/alm/src/components/StatusContainer.tsx @@ -75,7 +75,7 @@ export const StatusContainer = () => { )} {displayMessageSelector && } {displayConfirmations && ( - + )} ) diff --git a/alm/src/components/ValidatorsConfirmations.tsx b/alm/src/components/ValidatorsConfirmations.tsx index 4bff387d..1cbd2103 100644 --- a/alm/src/components/ValidatorsConfirmations.tsx +++ b/alm/src/components/ValidatorsConfirmations.tsx @@ -6,7 +6,7 @@ import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' import { SimpleLoading } from './commons/Loading' import styled from 'styled-components' import { ConfirmationParam } from '../hooks/useMessageConfirmations' -import { GreyLabel, SuccessLabel } from './commons/Labels' +import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' const Thead = styled.thead` border-bottom: 2px solid #9e9e9e; @@ -30,6 +30,8 @@ export const ValidatorsConfirmations = ({ confirmations }: ValidatorsConfirmatio switch (validatorStatus) { case VALIDATOR_CONFIRMATION_STATUS.SUCCESS: return {validatorStatus} + case VALIDATOR_CONFIRMATION_STATUS.FAILED: + return {validatorStatus} case VALIDATOR_CONFIRMATION_STATUS.WAITING: case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED: return {validatorStatus} diff --git a/alm/src/components/commons/Labels.tsx b/alm/src/components/commons/Labels.tsx index 28203122..579745b9 100644 --- a/alm/src/components/commons/Labels.tsx +++ b/alm/src/components/commons/Labels.tsx @@ -13,3 +13,10 @@ export const GreyLabel = styled.label` padding: 0.4rem 0.7rem; border-radius: 4px; ` + +export const RedLabel = styled.label` + color: var(--failed-color); + background-color: var(--failed-bg-color); + padding: 0.4rem 0.7rem; + border-radius: 4px; +` diff --git a/alm/src/config/constants.ts b/alm/src/config/constants.ts index fef4e53c..505a9cd5 100644 --- a/alm/src/config/constants.ts +++ b/alm/src/config/constants.ts @@ -10,9 +10,21 @@ export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NE export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || '' export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || '' +export const HOME_EXPLORER_API: string = process.env.REACT_APP_ALM_HOME_EXPLORER_API || '' +export const FOREIGN_EXPLORER_API: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_API || '' + export const HOME_RPC_POLLING_INTERVAL: number = 5000 export const FOREIGN_RPC_POLLING_INTERVAL: number = 15000 export const BLOCK_RANGE: number = 50 +export const ONE_DAY_TIMESTAMP: number = 86400 +export const THREE_DAYS_TIMESTAMP: number = 259200 + +export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f' +export const SUBMIT_SIGNATURE_HASH = '630cea8e' +export const EXECUTE_SIGNATURES_HASH = '3f7658fd' + +export const CACHE_KEY_FAILED = 'failed-confirmation-validator-' +export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-' export const TRANSACTION_STATUS = { SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES', diff --git a/alm/src/hooks/useMessageConfirmations.ts b/alm/src/hooks/useMessageConfirmations.ts index 4615b15b..d93f1567 100644 --- a/alm/src/hooks/useMessageConfirmations.ts +++ b/alm/src/hooks/useMessageConfirmations.ts @@ -17,11 +17,13 @@ import { getCollectedSignaturesEvent } from '../utils/getCollectedSignaturesEven import { checkWaitingBlocksForExecution } from '../utils/executionWaitingForBlocks' import { getConfirmationsForTx } from '../utils/getConfirmationsForTx' import { getFinalizationEvent } from '../utils/getFinalizationEvent' +import { getValidatorFailedTransactionsForMessage, getExecutionFailedTransactionForMessage } from '../utils/explorer' export interface useMessageConfirmationsParams { message: MessageObject receipt: Maybe fromHome: boolean + timestamp: number } export interface ConfirmationParam { @@ -37,7 +39,7 @@ export interface ExecutionData { executionResult: boolean } -export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessageConfirmationsParams) => { +export const useMessageConfirmations = ({ message, receipt, fromHome, timestamp }: useMessageConfirmationsParams) => { const { home, foreign } = useStateProvider() const [confirmations, setConfirmations] = useState>([]) const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED) @@ -54,6 +56,8 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa }) const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false) const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false) + const [failedConfirmations, setFailedConfirmations] = useState(false) + const [failedExecution, setFailedExecution] = useState(false) // Check if the validators are waiting for block confirmations to verify the message useEffect( @@ -182,7 +186,7 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa // To avoid making extra requests, this is only executed when validators finished waiting for blocks confirmations useEffect( () => { - if (!waitingBlocksResolved) return + if (!waitingBlocksResolved || !timestamp) return const subscriptions: Array = [] @@ -204,7 +208,10 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa home.requiredSignatures, setSignatureCollected, waitingBlocksResolved, - subscriptions + subscriptions, + timestamp, + getValidatorFailedTransactionsForMessage, + setFailedConfirmations ) return () => { @@ -218,7 +225,8 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa home.validatorList, home.bridgeContract, home.requiredSignatures, - waitingBlocksResolved + waitingBlocksResolved, + timestamp ] ) @@ -248,9 +256,13 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa providedWeb3, setExecutionData, waitingBlocksResolved, - message.id, + message, interval, - subscriptions + subscriptions, + timestamp, + collectedSignaturesEvent, + getExecutionFailedTransactionForMessage, + setFailedExecution ) return () => { @@ -261,18 +273,20 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa fromHome, foreign.bridgeContract, home.bridgeContract, - message.id, + message, foreign.web3, home.web3, waitingBlocksResolved, - waitingBlocksForExecutionResolved + waitingBlocksForExecutionResolved, + timestamp, + collectedSignaturesEvent ] ) // Sets the message status based in the collected information useEffect( () => { - if (executionData.txHash) { + if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) { const newStatus = executionData.executionResult ? CONFIRMATIONS_STATUS.SUCCESS : CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED @@ -281,6 +295,8 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa if (fromHome) { if (waitingBlocksForExecution) { setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING) + } else if (failedExecution) { + setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED) } else { setStatus(CONFIRMATIONS_STATUS.UNDEFINED) } @@ -289,11 +305,21 @@ export const useMessageConfirmations = ({ message, receipt, fromHome }: useMessa } } else if (waitingBlocks) { setStatus(CONFIRMATIONS_STATUS.WAITING) + } else if (failedConfirmations) { + setStatus(CONFIRMATIONS_STATUS.FAILED) } else { setStatus(CONFIRMATIONS_STATUS.UNDEFINED) } }, - [executionData, fromHome, signatureCollected, waitingBlocks, waitingBlocksForExecution] + [ + executionData, + fromHome, + signatureCollected, + waitingBlocks, + waitingBlocksForExecution, + failedConfirmations, + failedExecution + ] ) return { diff --git a/alm/src/hooks/useTransactionStatus.ts b/alm/src/hooks/useTransactionStatus.ts index 9a981744..c6ad73e2 100644 --- a/alm/src/hooks/useTransactionStatus.ts +++ b/alm/src/hooks/useTransactionStatus.ts @@ -3,7 +3,7 @@ import { TransactionReceipt } from 'web3-eth' import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants' import { getTransactionStatusDescription } from '../utils/networks' import { useStateProvider } from '../state/StateProvider' -import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject } from '../utils/web3' +import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject, getBlock } from '../utils/web3' export const useTransactionStatus = ({ txHash, chainId }: { txHash: string; chainId: number }) => { const { home, foreign } = useStateProvider() @@ -41,7 +41,7 @@ export const useTransactionStatus = ({ txHash, chainId }: { txHash: string; chai subscriptions.push(timeoutId) } else { const blockNumber = txReceipt.blockNumber - const block = await web3.eth.getBlock(blockNumber) + const block = await getBlock(web3, blockNumber) const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp setTimestamp(blockTimestamp) diff --git a/alm/src/themes/Dark.tsx b/alm/src/themes/Dark.tsx index 07a4e0f4..4bb63531 100644 --- a/alm/src/themes/Dark.tsx +++ b/alm/src/themes/Dark.tsx @@ -12,6 +12,10 @@ const theme = { notRequired: { textColor: '#bdbdbd', backgroundColor: '#424242' + }, + failed: { + textColor: '#EF5350', + backgroundColor: '#4E342E' } } export default theme diff --git a/alm/src/themes/GlobalStyle.tsx b/alm/src/themes/GlobalStyle.tsx index d10ea42d..04485df0 100644 --- a/alm/src/themes/GlobalStyle.tsx +++ b/alm/src/themes/GlobalStyle.tsx @@ -25,5 +25,7 @@ export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>` --success-bg-color: ${props => props.theme.success.backgroundColor}; --not-required-color: ${props => props.theme.notRequired.textColor}; --not-required-bg-color: ${props => props.theme.notRequired.backgroundColor}; + --failed-color: ${props => props.theme.failed.textColor}; + --failed-bg-color: ${props => props.theme.failed.backgroundColor}; } ` diff --git a/alm/src/utils/executionWaitingForBlocks.ts b/alm/src/utils/executionWaitingForBlocks.ts index 61228491..95834826 100644 --- a/alm/src/utils/executionWaitingForBlocks.ts +++ b/alm/src/utils/executionWaitingForBlocks.ts @@ -15,6 +15,13 @@ export const checkWaitingBlocksForExecution = async ( const currentBlock = blockProvider.get() if (currentBlock && currentBlock >= targetBlock) { + setExecutionData({ + status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED, + validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay, + txHash: '', + timestamp: 0, + executionResult: false + }) setWaitingBlocksForExecution(false) setWaitingBlocksForExecutionResolved(true) blockProvider.stop() diff --git a/alm/src/utils/explorer.ts b/alm/src/utils/explorer.ts new file mode 100644 index 00000000..a6ba84ac --- /dev/null +++ b/alm/src/utils/explorer.ts @@ -0,0 +1,164 @@ +import { + EXECUTE_AFFIRMATION_HASH, + EXECUTE_SIGNATURES_HASH, + FOREIGN_EXPLORER_API, + HOME_EXPLORER_API, + SUBMIT_SIGNATURE_HASH +} from '../config/constants' + +export interface APITransaction { + timeStamp: string + isError: string + input: string + to: string + hash: string +} + +export interface AccountTransactionsParams { + account: string + to: string + startTimestamp: number + endTimestamp: number + api: string +} + +export interface GetFailedTransactionParams { + account: string + to: string + messageData: string + startTimestamp: number + endTimestamp: number +} + +export const fetchAccountTransactionsFromBlockscout = async ({ + account, + to, + startTimestamp, + endTimestamp, + api +}: AccountTransactionsParams): Promise => { + const url = `${api}?module=account&action=txlist&address=${account}&filterby=from=${account}&to=${to}&starttimestamp=${startTimestamp}&endtimestamp=${endTimestamp}` + + try { + const result = await fetch(url).then(res => res.json()) + if (result.status === '0') { + return [] + } + + return result.result + } catch (e) { + console.log(e) + return [] + } +} + +export const getBlockByTimestampUrl = (api: string, timestamp: number) => + `${api}?module=block&action=getblocknobytime×tamp=${timestamp}&closest=before` + +export const fetchAccountTransactionsFromEtherscan = async ({ + account, + to, + startTimestamp, + endTimestamp, + api +}: AccountTransactionsParams): Promise => { + const startBlockUrl = getBlockByTimestampUrl(api, startTimestamp) + const endBlockUrl = getBlockByTimestampUrl(api, endTimestamp) + let fromBlock = 0 + let toBlock = 9999999999999 + try { + const [fromBlockResult, toBlockResult] = await Promise.all([ + fetch(startBlockUrl).then(res => res.json()), + fetch(endBlockUrl).then(res => res.json()) + ]) + + if (fromBlockResult.status !== '0') { + fromBlock = parseInt(fromBlockResult.result) + } + + if (toBlockResult.status !== '0') { + toBlock = parseInt(toBlockResult.result) + } + } catch (e) { + console.log(e) + return [] + } + + const url = `${api}?module=account&action=txlist&address=${account}&startblock=${fromBlock}&endblock=${toBlock}` + + try { + const result = await fetch(url).then(res => res.json()) + + if (result.status === '0') { + return [] + } + + const toAddressLowerCase = to.toLowerCase() + const transactions: APITransaction[] = result.result + return transactions.filter(t => t.to.toLowerCase() === toAddressLowerCase) + } catch (e) { + console.log(e) + return [] + } +} + +export const fetchAccountTransactions = (api: string) => { + return api.includes('blockscout') ? fetchAccountTransactionsFromBlockscout : fetchAccountTransactionsFromEtherscan +} + +export const getFailedTransactions = async ( + account: string, + to: string, + startTimestamp: number, + endTimestamp: number, + api: string, + fetchAccountTransactions: (args: AccountTransactionsParams) => Promise +): Promise => { + const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api }) + + return transactions.filter(t => t.isError !== '0') +} + +export const getValidatorFailedTransactionsForMessage = async ({ + account, + to, + messageData, + startTimestamp, + endTimestamp +}: GetFailedTransactionParams): Promise => { + const failedTransactions = await getFailedTransactions( + account, + to, + startTimestamp, + endTimestamp, + HOME_EXPLORER_API, + fetchAccountTransactionsFromBlockscout + ) + + const messageDataValue = messageData.replace('0x', '') + return failedTransactions.filter( + t => + (t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) && + t.input.includes(messageDataValue) + ) +} + +export const getExecutionFailedTransactionForMessage = async ({ + account, + to, + messageData, + startTimestamp, + endTimestamp +}: GetFailedTransactionParams): Promise => { + const failedTransactions = await getFailedTransactions( + account, + to, + startTimestamp, + endTimestamp, + FOREIGN_EXPLORER_API, + fetchAccountTransactions(FOREIGN_EXPLORER_API) + ) + + const messageDataValue = messageData.replace('0x', '') + return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue)) +} diff --git a/alm/src/utils/getConfirmationsForTx.ts b/alm/src/utils/getConfirmationsForTx.ts index e5b29097..009ad46e 100644 --- a/alm/src/utils/getConfirmationsForTx.ts +++ b/alm/src/utils/getConfirmationsForTx.ts @@ -1,7 +1,81 @@ import Web3 from 'web3' import { Contract } from 'web3-eth-contract' import validatorsCache from '../services/ValidatorsCache' -import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' +import { + CACHE_KEY_FAILED, + HOME_RPC_POLLING_INTERVAL, + ONE_DAY_TIMESTAMP, + VALIDATOR_CONFIRMATION_STATUS +} from '../config/constants' +import { GetFailedTransactionParams, APITransaction } from './explorer' +import { ConfirmationParam } from '../hooks/useMessageConfirmations' + +export const getValidatorConfirmation = ( + web3: Web3, + hashMsg: string, + bridgeContract: Contract, + confirmationContractMethod: Function +) => async (validator: string): Promise => { + const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg) + + const signatureFromCache = validatorsCache.get(hashSenderMsg) + if (signatureFromCache) { + return { + validator, + status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS + } + } + + const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg) + const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED + + // If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change + if (confirmed) { + validatorsCache.set(hashSenderMsg, confirmed) + } + + return { + validator, + status + } +} + +export const getValidatorFailedTransaction = ( + bridgeContract: Contract, + messageData: string, + timestamp: number, + getFailedTransactions: (args: GetFailedTransactionParams) => Promise +) => async (validatorData: ConfirmationParam): Promise => { + const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}` + const failedFromCache = validatorsCache.get(validatorCacheKey) + + if (failedFromCache) { + return { + validator: validatorData.validator, + status: VALIDATOR_CONFIRMATION_STATUS.FAILED + } + } + + const failedTransactions = await getFailedTransactions({ + account: validatorData.validator, + to: bridgeContract.options.address, + messageData, + startTimestamp: timestamp, + endTimestamp: timestamp + ONE_DAY_TIMESTAMP + }) + const newStatus = + failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED + + // If validator signature failed, we cache the result to avoid doing future requests for a result that won't change + if (failedTransactions.length > 0) { + validatorsCache.set(validatorCacheKey, true) + } + + return { + validator: validatorData.validator, + status: newStatus + } +} export const getConfirmationsForTx = async ( messageData: string, @@ -13,63 +87,69 @@ export const getConfirmationsForTx = async ( requiredSignatures: number, setSignatureCollected: Function, waitingBlocksResolved: boolean, - subscriptions: number[] + subscriptions: number[], + timestamp: number, + getFailedTransactions: (args: GetFailedTransactionParams) => Promise, + setFailedConfirmations: Function ) => { if (!web3 || !validatorList || !bridgeContract || !waitingBlocksResolved) return const hashMsg = web3.utils.soliditySha3Raw(messageData) let validatorConfirmations = await Promise.all( - validatorList.map(async validator => { - const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg) - - const signatureFromCache = validatorsCache.get(hashSenderMsg) - if (signatureFromCache) { - return { - validator, - status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS - } - } - - const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg) - const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED - - // If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change - if (confirmed) { - validatorsCache.set(hashSenderMsg, confirmed) - } - - return { - validator, - status - } - }) + validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, confirmationContractMethod)) ) const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) + const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS) + // If signatures not collected, it needs to retry in the next blocks if (successConfirmations.length !== requiredSignatures) { - const timeoutId = setTimeout( - () => - getConfirmationsForTx( - messageData, - web3, - validatorList, - bridgeContract, - confirmationContractMethod, - setResult, - requiredSignatures, - setSignatureCollected, - waitingBlocksResolved, - subscriptions - ), - HOME_RPC_POLLING_INTERVAL + // Check if confirmation failed + const validatorFailedConfirmationsChecks = await Promise.all( + notSuccessConfirmations.map( + getValidatorFailedTransaction(bridgeContract, messageData, timestamp, getFailedTransactions) + ) ) - subscriptions.push(timeoutId) + const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter( + c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED + ) + validatorFailedConfirmations.forEach(validatorData => { + const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator) + validatorConfirmations[index] = validatorData + }) + const messageConfirmationsFailed = validatorFailedConfirmations.length > validatorList.length - requiredSignatures + if (messageConfirmationsFailed) { + setFailedConfirmations(true) + } + + const missingConfirmations = validatorConfirmations.filter( + c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED + ) + + if (missingConfirmations.length > 0) { + const timeoutId = setTimeout( + () => + getConfirmationsForTx( + messageData, + web3, + validatorList, + bridgeContract, + confirmationContractMethod, + setResult, + requiredSignatures, + setSignatureCollected, + waitingBlocksResolved, + subscriptions, + timestamp, + getFailedTransactions, + setFailedConfirmations + ), + HOME_RPC_POLLING_INTERVAL + ) + subscriptions.push(timeoutId) + } } else { // If signatures collected, it should set other signatures as not required - const notSuccessConfirmations = validatorConfirmations.filter( - c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS - ) const notRequiredConfirmations = notSuccessConfirmations.map(c => ({ validator: c.validator, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED diff --git a/alm/src/utils/getFinalizationEvent.ts b/alm/src/utils/getFinalizationEvent.ts index a391ebb5..b87e15ed 100644 --- a/alm/src/utils/getFinalizationEvent.ts +++ b/alm/src/utils/getFinalizationEvent.ts @@ -1,7 +1,10 @@ import { Contract, EventData } from 'web3-eth-contract' import Web3 from 'web3' -import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' +import { CACHE_KEY_EXECUTION_FAILED, THREE_DAYS_TIMESTAMP, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants' import { ExecutionData } from '../hooks/useMessageConfirmations' +import { APITransaction, GetFailedTransactionParams } from './explorer' +import { getBlock, MessageObject } from './web3' +import validatorsCache from '../services/ValidatorsCache' export const getFinalizationEvent = async ( contract: Maybe, @@ -9,9 +12,13 @@ export const getFinalizationEvent = async ( web3: Maybe, setResult: React.Dispatch>, waitingBlocksResolved: boolean, - messageId: string, + message: MessageObject, interval: number, - subscriptions: number[] + subscriptions: number[], + timestamp: number, + collectedSignaturesEvent: Maybe, + getFailedExecution: (args: GetFailedTransactionParams) => Promise, + setFailedExecution: Function ) => { if (!contract || !web3 || !waitingBlocksResolved) return // Since it filters by the message id, only one event will be fetched @@ -20,14 +27,14 @@ export const getFinalizationEvent = async ( fromBlock: 0, toBlock: 'latest', filter: { - messageId + messageId: message.id } }) if (events.length > 0) { const event = events[0] const [txReceipt, block] = await Promise.all([ web3.eth.getTransactionReceipt(event.transactionHash), - web3.eth.getBlock(event.blockNumber) + getBlock(web3, event.blockNumber) ]) const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp @@ -41,6 +48,41 @@ export const getFinalizationEvent = async ( executionResult: event.returnValues.status }) } else { + // If event is defined, it means it is a message from Home to Foreign + if (collectedSignaturesEvent) { + const validator = collectedSignaturesEvent.returnValues.authorityResponsibleForRelay + + const validatorExecutionCacheKey = `${CACHE_KEY_EXECUTION_FAILED}${validator}` + const failedFromCache = validatorsCache.get(validatorExecutionCacheKey) + + if (!failedFromCache) { + const failedTransactions = await getFailedExecution({ + account: validator, + to: contract.options.address, + messageData: message.data, + startTimestamp: timestamp, + endTimestamp: timestamp + THREE_DAYS_TIMESTAMP + }) + + if (failedTransactions.length > 0) { + const failedTx = failedTransactions[0] + + // If validator execution failed, we cache the result to avoid doing future requests for a result that won't change + validatorsCache.set(validatorExecutionCacheKey, true) + + const timestamp = parseInt(failedTx.timeStamp) + setResult({ + status: VALIDATOR_CONFIRMATION_STATUS.FAILED, + validator: validator, + txHash: failedTx.hash, + timestamp, + executionResult: false + }) + setFailedExecution(true) + } + } + } + const timeoutId = setTimeout( () => getFinalizationEvent( @@ -49,9 +91,13 @@ export const getFinalizationEvent = async ( web3, setResult, waitingBlocksResolved, - messageId, + message, interval, - subscriptions + subscriptions, + timestamp, + collectedSignaturesEvent, + getFailedExecution, + setFailedExecution ), interval ) diff --git a/alm/src/utils/web3.ts b/alm/src/utils/web3.ts index d35dd364..db91d59e 100644 --- a/alm/src/utils/web3.ts +++ b/alm/src/utils/web3.ts @@ -1,7 +1,9 @@ import Web3 from 'web3' +import { BlockTransactionString } from 'web3-eth' import { TransactionReceipt } from 'web3-eth' import { AbiItem } from 'web3-utils' import memoize from 'fast-memoize' +import promiseRetry from 'promise-retry' import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../../../commons' export interface MessageObject { @@ -48,3 +50,14 @@ export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web )[0] return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi) } + +// In some rare cases the block data is not available yet for the block of a new event detected +// so this logic retry to get the block in case it fails +export const getBlock = async (web3: Web3, blockNumber: number): Promise => + promiseRetry(async retry => { + const result = await web3.eth.getBlock(blockNumber) + if (!result) { + return retry('Error getting block data') + } + return result + }) diff --git a/yarn.lock b/yarn.lock index b90bd96b..20ab8039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2980,6 +2980,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" integrity sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ== +"@types/promise-retry@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/promise-retry/-/promise-retry-1.1.3.tgz#baab427419da9088a1d2f21bf56249c21b3dd43c" + integrity sha512-LxIlEpEX6frE3co3vCO2EUJfHIta1IOmhDlcAsR4GMMv9hev1iTI9VwberVGkePJAuLZs5rMucrV8CziCfuJMw== + dependencies: + "@types/retry" "*" + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -3042,6 +3049,11 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/retry@*": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/solidity-parser-antlr@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@types/solidity-parser-antlr/-/solidity-parser-antlr-0.2.3.tgz#bb2d9c6511bf483afe4fc3e2714da8a924e59e3f" @@ -7712,6 +7724,11 @@ err-code@^1.0.0: resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errno@^0.1.3, errno@~0.1.1, errno@~0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" @@ -16100,6 +16117,14 @@ promise-retry@^1.1.1: err-code "^1.0.0" retry "^0.10.0" +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise-to-callback@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7" @@ -17410,6 +17435,11 @@ retry@^0.10.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"