From 8c268d6f0697ad35215ae4e32de0a5a5f2e89c2a Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Mon, 6 Jul 2020 15:33:23 -0300 Subject: [PATCH] Add ALM snapshots (#382) --- alm/.gitignore | 2 + alm/package.json | 8 +- alm/scripts/createSnapshots.js | 118 ++++++++++++++++++ alm/src/components/ExecutionConfirmation.tsx | 11 +- .../components/ValidatorsConfirmations.tsx | 11 +- alm/src/components/commons/Table.tsx | 13 ++ alm/src/hooks/useBlockConfirmations.ts | 9 +- alm/src/hooks/useNetwork.ts | 13 +- alm/src/hooks/useValidatorContract.ts | 22 ++-- alm/src/services/SnapshotProvider.ts | 69 ++++++++++ alm/src/snapshots/.gitkeep | 0 alm/src/state/StateProvider.tsx | 5 +- alm/src/utils/contract.ts | 65 +++++++--- alm/src/utils/web3.ts | 9 ++ yarn.lock | 5 + 15 files changed, 309 insertions(+), 51 deletions(-) create mode 100644 alm/scripts/createSnapshots.js create mode 100644 alm/src/components/commons/Table.tsx create mode 100644 alm/src/services/SnapshotProvider.ts create mode 100644 alm/src/snapshots/.gitkeep diff --git a/alm/.gitignore b/alm/.gitignore index 4d29575d..d77603ef 100644 --- a/alm/.gitignore +++ b/alm/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +src/snapshots/*.json + # dependencies /node_modules /.pnp diff --git a/alm/package.json b/alm/package.json index 16b6d329..dfee20a9 100644 --- a/alm/package.json +++ b/alm/package.json @@ -17,6 +17,7 @@ "@use-it/interval": "^0.1.3", "customize-cra": "^1.0.0", "date-fns": "^2.14.0", + "dotenv": "^8.2.0", "fast-memoize": "^2.5.2", "promise-retry": "^2.0.1", "react": "^16.13.1", @@ -30,11 +31,12 @@ "web3-eth-contract": "1.2.7" }, "scripts": { - "start": "./load-env.sh react-app-rewired start", - "build": "./load-env.sh react-app-rewired build", + "start": "yarn createSnapshots && ./load-env.sh react-app-rewired start", + "build": "yarn createSnapshots && ./load-env.sh react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject", - "lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore" + "lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore", + "createSnapshots": "node scripts/createSnapshots.js" }, "eslintConfig": { "extends": "react-app" diff --git a/alm/scripts/createSnapshots.js b/alm/scripts/createSnapshots.js new file mode 100644 index 00000000..604c72d4 --- /dev/null +++ b/alm/scripts/createSnapshots.js @@ -0,0 +1,118 @@ +const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons') + +const path = require('path') +require('dotenv').config() +const Web3 = require('web3') + +const fs = require('fs') + +const { + COMMON_HOME_RPC_URL, + COMMON_HOME_BRIDGE_ADDRESS, + COMMON_FOREIGN_RPC_URL, + COMMON_FOREIGN_BRIDGE_ADDRESS +} = process.env + +const generateSnapshot = async (side, url, bridgeAddress) => { + const snapshotPath = `../src/snapshots/${side}.json` + const snapshotFullPath = path.join(__dirname, snapshotPath) + const snapshot = {} + + const web3 = new Web3(new Web3.providers.HttpProvider(url)) + + const currentBlockNumber = await web3.eth.getBlockNumber() + snapshot.snapshotBlockNumber = currentBlockNumber + + // Save chainId + snapshot.chainId = await web3.eth.getChainId() + + const bridgeContract = new web3.eth.Contract(HOME_AMB_ABI, bridgeAddress) + + // Save RequiredBlockConfirmationChanged events + let requiredBlockConfirmationChangedEvents = await bridgeContract.getPastEvents('RequiredBlockConfirmationChanged', { + fromBlock: 0, + toBlock: currentBlockNumber + }) + + // In case RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB + // manually generate an event for this. Example Sokol - Kovan bridge + if (requiredBlockConfirmationChangedEvents.length === 0) { + const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call() + const blockConfirmations = await bridgeContract.methods.requiredBlockConfirmations().call() + + requiredBlockConfirmationChangedEvents.push({ + blockNumber: parseInt(deployedAtBlock), + returnValues: { + requiredBlockConfirmations: blockConfirmations + } + }) + } + + snapshot.RequiredBlockConfirmationChanged = requiredBlockConfirmationChangedEvents.map(e => ({ + blockNumber: e.blockNumber, + returnValues: { + requiredBlockConfirmations: e.returnValues.requiredBlockConfirmations + } + })) + + const validatorAddress = await bridgeContract.methods.validatorContract().call() + const validatorContract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorAddress) + + // Save RequiredSignaturesChanged events + const RequiredSignaturesChangedEvents = await validatorContract.getPastEvents('RequiredSignaturesChanged', { + fromBlock: 0, + toBlock: currentBlockNumber + }) + snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({ + blockNumber: e.blockNumber, + returnValues: { + requiredSignatures: e.returnValues.requiredSignatures + } + })) + + // Save ValidatorAdded events + const validatorAddedEvents = await validatorContract.getPastEvents('ValidatorAdded', { + fromBlock: 0, + toBlock: currentBlockNumber + }) + + snapshot.ValidatorAdded = validatorAddedEvents.map(e => ({ + blockNumber: e.blockNumber, + returnValues: { + validator: e.returnValues.validator + }, + event: 'ValidatorAdded' + })) + + // Save ValidatorRemoved events + const validatorRemovedEvents = await validatorContract.getPastEvents('ValidatorRemoved', { + fromBlock: 0, + toBlock: currentBlockNumber + }) + + snapshot.ValidatorRemoved = validatorRemovedEvents.map(e => ({ + blockNumber: e.blockNumber, + returnValues: { + validator: e.returnValues.validator + }, + event: 'ValidatorRemoved' + })) + + // Write snapshot + fs.writeFileSync(snapshotFullPath, JSON.stringify(snapshot, null, 2)) +} + +const main = async () => { + await Promise.all([ + generateSnapshot('home', COMMON_HOME_RPC_URL, COMMON_HOME_BRIDGE_ADDRESS), + generateSnapshot('foreign', COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS) + ]) +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.log('Error while creating snapshots') + console.error(error) + process.exit(0) + }) diff --git a/alm/src/components/ExecutionConfirmation.tsx b/alm/src/components/ExecutionConfirmation.tsx index d2f5fee5..17d78148 100644 --- a/alm/src/components/ExecutionConfirmation.tsx +++ b/alm/src/components/ExecutionConfirmation.tsx @@ -7,10 +7,7 @@ import styled from 'styled-components' import { ExecutionData } from '../hooks/useMessageConfirmations' import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' import { ExplorerTxLink } from './commons/ExplorerTxLink' - -const Thead = styled.thead` - border-bottom: 2px solid #9e9e9e; -` +import { Thead, AgeTd, StatusTd } from './commons/Table' const StyledExecutionConfirmation = styled.div` margin-top: 30px; @@ -55,12 +52,12 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir {formattedValidator ? formattedValidator : } - {getExecutionStatusElement(executionData.status)} - + {getExecutionStatusElement(executionData.status)} + {executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''} - + diff --git a/alm/src/components/ValidatorsConfirmations.tsx b/alm/src/components/ValidatorsConfirmations.tsx index df8d94de..bcd08394 100644 --- a/alm/src/components/ValidatorsConfirmations.tsx +++ b/alm/src/components/ValidatorsConfirmations.tsx @@ -7,10 +7,7 @@ import styled from 'styled-components' import { ConfirmationParam } from '../hooks/useMessageConfirmations' import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' import { ExplorerTxLink } from './commons/ExplorerTxLink' - -const Thead = styled.thead` - border-bottom: 2px solid #9e9e9e; -` +import { Thead, AgeTd, StatusTd } from './commons/Table' const RequiredConfirmations = styled.label` font-size: 14px; @@ -70,14 +67,14 @@ export const ValidatorsConfirmations = ({ return ( {windowWidth < 850 ? formatTxHash(validator) : validator} - {getValidatorStatusElement(displayedStatus)} - + {getValidatorStatusElement(displayedStatus)} + {confirmation && confirmation.timestamp > 0 ? formatTimestamp(confirmation.timestamp) : elementIfNoTimestamp} - + ) })} diff --git a/alm/src/components/commons/Table.tsx b/alm/src/components/commons/Table.tsx new file mode 100644 index 00000000..698b2d54 --- /dev/null +++ b/alm/src/components/commons/Table.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components' + +export const Thead = styled.thead` + border-bottom: 2px solid #9e9e9e; +` + +export const StatusTd = styled.td` + width: 150px; +` + +export const AgeTd = styled.td` + width: 180px; +` diff --git a/alm/src/hooks/useBlockConfirmations.ts b/alm/src/hooks/useBlockConfirmations.ts index 438e9e5a..470cfc23 100644 --- a/alm/src/hooks/useBlockConfirmations.ts +++ b/alm/src/hooks/useBlockConfirmations.ts @@ -3,6 +3,7 @@ import { TransactionReceipt } from 'web3-eth' import { useStateProvider } from '../state/StateProvider' import { Contract } from 'web3-eth-contract' import { getRequiredBlockConfirmations } from '../utils/contract' +import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider' export interface UseBlockConfirmationsParams { fromHome: boolean @@ -17,17 +18,19 @@ export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmatio const callRequireBlockConfirmations = async ( contract: Contract, receipt: TransactionReceipt, - setResult: Function + setResult: Function, + snapshotProvider: SnapshotProvider ) => { - const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber) + const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider) setResult(result) } useEffect( () => { const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract + const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider if (!bridgeContract || !receipt) return - callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations) + callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider) }, [home.bridgeContract, foreign.bridgeContract, receipt, fromHome] ) diff --git a/alm/src/hooks/useNetwork.ts b/alm/src/hooks/useNetwork.ts index 8c017b39..21a48886 100644 --- a/alm/src/hooks/useNetwork.ts +++ b/alm/src/hooks/useNetwork.ts @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react' -import { getWeb3 } from '../utils/web3' +import { getChainId, getWeb3 } from '../utils/web3' +import { SnapshotProvider } from '../services/SnapshotProvider' -export const useNetwork = (url: string) => { +export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => { const [loading, setLoading] = useState(true) const [chainId, setChainId] = useState(0) const web3 = getWeb3(url) @@ -9,14 +10,14 @@ export const useNetwork = (url: string) => { useEffect( () => { setLoading(true) - const getChainId = async () => { - const id = await web3.eth.getChainId() + const getWeb3ChainId = async () => { + const id = await getChainId(web3, snapshotProvider) setChainId(id) setLoading(false) } - getChainId() + getWeb3ChainId() }, - [web3.eth] + [web3, snapshotProvider] ) return { diff --git a/alm/src/hooks/useValidatorContract.ts b/alm/src/hooks/useValidatorContract.ts index 07ea7cfb..8557883f 100644 --- a/alm/src/hooks/useValidatorContract.ts +++ b/alm/src/hooks/useValidatorContract.ts @@ -5,6 +5,7 @@ import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '.. import { BRIDGE_VALIDATORS_ABI } from '../abis' import { useStateProvider } from '../state/StateProvider' import { TransactionReceipt } from 'web3-eth' +import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider' export interface useValidatorContractParams { fromHome: boolean @@ -28,16 +29,22 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract const callRequiredSignatures = async ( contract: Maybe, receipt: TransactionReceipt, - setResult: Function + setResult: Function, + snapshotProvider: SnapshotProvider ) => { if (!contract) return - const result = await getRequiredSignatures(contract, receipt.blockNumber) + const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider) setResult(result) } - const callValidatorList = async (contract: Maybe, receipt: TransactionReceipt, setResult: Function) => { + const callValidatorList = async ( + contract: Maybe, + receipt: TransactionReceipt, + setResult: Function, + snapshotProvider: SnapshotProvider + ) => { if (!contract) return - const result = await getValidatorList(contract, receipt.blockNumber) + const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider) setResult(result) } @@ -55,10 +62,11 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract useEffect( () => { if (!receipt) return - callRequiredSignatures(validatorContract, receipt, setRequiredSignatures) - callValidatorList(validatorContract, receipt, setValidatorList) + const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider + callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider) + callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider) }, - [validatorContract, receipt] + [validatorContract, receipt, fromHome] ) return { diff --git a/alm/src/services/SnapshotProvider.ts b/alm/src/services/SnapshotProvider.ts new file mode 100644 index 00000000..fec8a1ef --- /dev/null +++ b/alm/src/services/SnapshotProvider.ts @@ -0,0 +1,69 @@ +const initialValue = { + chainId: 0, + RequiredBlockConfirmationChanged: [], + RequiredSignaturesChanged: [], + ValidatorAdded: [], + ValidatorRemoved: [], + snapshotBlockNumber: 0 +} + +export interface SnapshotEvent { + blockNumber: number + returnValues: any +} + +export interface SnapshotValidatorEvent { + blockNumber: number + returnValues: any + event: string +} + +export interface Snapshot { + chainId: number + RequiredBlockConfirmationChanged: SnapshotEvent[] + RequiredSignaturesChanged: SnapshotEvent[] + ValidatorAdded: SnapshotValidatorEvent[] + ValidatorRemoved: SnapshotValidatorEvent[] + snapshotBlockNumber: number +} + +export class SnapshotProvider { + private data: Snapshot + + constructor(side: string) { + let data = initialValue + try { + data = require(`../snapshots/${side}.json`) + } catch (e) { + console.log('Snapshot not found') + } + this.data = data + } + + chainId() { + return this.data.chainId + } + + snapshotBlockNumber() { + return this.data.snapshotBlockNumber + } + + requiredBlockConfirmationEvents(toBlock: number) { + return this.data.RequiredBlockConfirmationChanged.filter(e => e.blockNumber <= toBlock) + } + + requiredSignaturesEvents(toBlock: number) { + return this.data.RequiredSignaturesChanged.filter(e => e.blockNumber <= toBlock) + } + + validatorAddedEvents(fromBlock: number) { + return this.data.ValidatorAdded.filter(e => e.blockNumber >= fromBlock) + } + + validatorRemovedEvents(fromBlock: number) { + return this.data.ValidatorRemoved.filter(e => e.blockNumber >= fromBlock) + } +} + +export const homeSnapshotProvider = new SnapshotProvider('home') +export const foreignSnapshotProvider = new SnapshotProvider('foreign') diff --git a/alm/src/snapshots/.gitkeep b/alm/src/snapshots/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/alm/src/state/StateProvider.tsx b/alm/src/state/StateProvider.tsx index 16762011..c495ef65 100644 --- a/alm/src/state/StateProvider.tsx +++ b/alm/src/state/StateProvider.tsx @@ -11,6 +11,7 @@ import { import Web3 from 'web3' import { useBridgeContracts } from '../hooks/useBridgeContracts' import { Contract } from 'web3-eth-contract' +import { foreignSnapshotProvider, homeSnapshotProvider } from '../services/SnapshotProvider' export interface BaseNetworkParams { chainId: number @@ -47,8 +48,8 @@ const initialState = { const StateContext = createContext(initialState) export const StateProvider = ({ children }: { children: ReactNode }) => { - const homeNetwork = useNetwork(HOME_RPC_URL) - const foreignNetwork = useNetwork(FOREIGN_RPC_URL) + const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider) + const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider) const { homeBridge, foreignBridge } = useBridgeContracts({ homeWeb3: homeNetwork.web3, foreignWeb3: foreignNetwork.web3 diff --git a/alm/src/utils/contract.ts b/alm/src/utils/contract.ts index 2b81c290..f5578c01 100644 --- a/alm/src/utils/contract.ts +++ b/alm/src/utils/contract.ts @@ -1,10 +1,24 @@ import { Contract } from 'web3-eth-contract' +import { EventData } from 'web3-eth-contract' +import { SnapshotProvider } from '../services/SnapshotProvider' -export const getRequiredBlockConfirmations = async (contract: Contract, blockNumber: number) => { - const events = await contract.getPastEvents('RequiredBlockConfirmationChanged', { - fromBlock: 0, - toBlock: blockNumber - }) +export const getRequiredBlockConfirmations = async ( + contract: Contract, + blockNumber: number, + snapshotProvider: SnapshotProvider +) => { + const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber) + const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber() + + let contractEvents: EventData[] = [] + if (blockNumber > snapshotBlockNumber) { + contractEvents = await contract.getPastEvents('RequiredBlockConfirmationChanged', { + fromBlock: snapshotBlockNumber + 1, + toBlock: blockNumber + }) + } + + const events = [...eventsFromSnapshot, ...contractEvents] let blockConfirmations if (events.length > 0) { @@ -21,11 +35,23 @@ export const getRequiredBlockConfirmations = async (contract: Contract, blockNum export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call() -export const getRequiredSignatures = async (contract: Contract, blockNumber: number) => { - const events = await contract.getPastEvents('RequiredSignaturesChanged', { - fromBlock: 0, - toBlock: blockNumber - }) +export const getRequiredSignatures = async ( + contract: Contract, + blockNumber: number, + snapshotProvider: SnapshotProvider +) => { + const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber) + const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber() + + let contractEvents: EventData[] = [] + if (blockNumber > snapshotBlockNumber) { + contractEvents = await contract.getPastEvents('RequiredSignaturesChanged', { + fromBlock: snapshotBlockNumber + 1, + toBlock: blockNumber + }) + } + + const events = [...eventsFromSnapshot, ...contractEvents] // Use the value form last event before the transaction const event = events[events.length - 1] @@ -33,19 +59,26 @@ export const getRequiredSignatures = async (contract: Contract, blockNumber: num return parseInt(requiredSignatures) } -export const getValidatorList = async (contract: Contract, blockNumber: number) => { - let currentList: string[] = await contract.methods.validatorList().call() - const [added, removed] = await Promise.all([ +export const getValidatorList = async (contract: Contract, blockNumber: number, snapshotProvider: SnapshotProvider) => { + const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber) + const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber) + const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber() + + const fromBlock = snapshotBlockNumber > blockNumber ? snapshotBlockNumber + 1 : blockNumber + const [currentList, added, removed] = await Promise.all([ + contract.methods.validatorList().call(), contract.getPastEvents('ValidatorAdded', { - fromBlock: blockNumber + fromBlock }), contract.getPastEvents('ValidatorRemoved', { - fromBlock: blockNumber + fromBlock }) ]) // Ordered desc - const orderedEvents = [...added, ...removed].sort(({ blockNumber: prev }, { blockNumber: next }) => next - prev) + const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort( + ({ blockNumber: prev }, { blockNumber: next }) => next - prev + ) // Stored as a Set to avoid duplicates const validatorList = new Set(currentList) diff --git a/alm/src/utils/web3.ts b/alm/src/utils/web3.ts index 0ed56a0a..09d129b7 100644 --- a/alm/src/utils/web3.ts +++ b/alm/src/utils/web3.ts @@ -5,6 +5,7 @@ import { AbiItem } from 'web3-utils' import memoize from 'fast-memoize' import promiseRetry from 'promise-retry' import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis' +import { SnapshotProvider } from '../services/SnapshotProvider' export interface MessageObject { id: string @@ -61,3 +62,11 @@ export const getBlock = async (web3: Web3, blockNumber: number): Promise { + let id = snapshotProvider.chainId() + if (id === 0) { + id = await web3.eth.getChainId() + } + return id +} diff --git a/yarn.lock b/yarn.lock index 4edd5fa5..e462ecb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7550,6 +7550,11 @@ dotenv@^7.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905"