diff --git a/.gitignore b/.gitignore index 9beef2d083742..a6d86485f7c61 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ packages/data-transport-layer/db *.swp .env +.DS_Store +*.spec.ts +*.d.ts diff --git a/packages/fraud-prover/.env.example b/packages/fraud-prover/.env.example new file mode 100644 index 0000000000000..5f8536100fd0e --- /dev/null +++ b/packages/fraud-prover/.env.example @@ -0,0 +1,12 @@ +# Universal Options +L1_NODE_WEB3_URL=http://localhost:9545 +L2_NODE_WEB3_URL=http://localhost:8045 +L1_WALLET_KEY=0x00000000000000000000000000000000000000000000000 +RELAY_GAS_LIMIT=4000000 +POLLING_INTERVAL=5000 +L2_BLOCK_OFFSET=1 +L1_START_OFFSET=0 +L1_BLOCK_FINALITY=0 +FROM_L2_TRANSACTION_INDEX=0 +RUN_GAS_LIMIT=9500000 +ADDRESS_MANAGER_ADDRESS= diff --git a/packages/fraud-prover/CHANGELOG.md b/packages/fraud-prover/CHANGELOG.md new file mode 100644 index 0000000000000..2be7aed1191cc --- /dev/null +++ b/packages/fraud-prover/CHANGELOG.md @@ -0,0 +1 @@ +# @eth-optimism/fraud-prover diff --git a/packages/fraud-prover/README.md b/packages/fraud-prover/README.md new file mode 100644 index 0000000000000..e27ff18abf685 --- /dev/null +++ b/packages/fraud-prover/README.md @@ -0,0 +1,239 @@ +# Fraud Prover + +Contains an executable fraud prover. + +## Configuration + +All configuration is done via environment variables. See below for more information. + +## Building & Running + +1. Make sure dependencies are installed just run `yarn` in the base directory +2. Build `yarn build` +3. Run `yarn start` + +## Testing & linting + +- See lint errors with `yarn lint`; auto-fix with `yarn lint --fix` + +## Envs (Need to reconcile with the below - ToDo) + +| Environment Variable | Required? | Default Value | Description | +| ----------- | --------- | ------------- | ----------- | +| `L1_WALLET_KEY` | Yes | N/A | Private key for an account on Layer 1 (Ethereum) to be used to submit fraud proof transactions. | +| `L2_NODE_WEB3_URL` | No | http://localhost:9545 | HTTP endpoint for a Layer 2 (Optimism) Verifier node. | +| `L1_NODE_WEB3_URL` | No | http://localhost:8545 | HTTP endpoint for a Layer 1 (Ethereum) node. | +| `RELAY_GAS_LIMIT` | No | 9,000,000 | Maximum amount of gas to provide to fraud proof transactions (except for the "transaction execution" step). | +| `RUN_GAS_LIMIT` | No | 9,000,000 | Maximum amount of gas to provide to the "transaction execution" step. | +| `POLLING_INTERVAL` | No | 5,000 | Time (in milliseconds) to wait while polling for new transactions. | +| `L2_BLOCK_OFFSET` | No | 1 | Offset between the `CanonicalTransactionChain` contract on Layer 1 and the blocks on Layer 2. Currently defaults to 1, but will likely be removed as soon as possible. | +| `L1_BLOCK_FINALITY` | No | 0 | Number of Layer 1 blocks to wait before considering a given event. | +| `L1_START_OFFSET` | No | 0 | Layer 1 block number to start scanning for transactions from. | +| `FROM_L2_TRANSACTION_INDEX` | No | 0 | Layer 2 block number to start scanning for transactions from. | + +## Local testing + +The fraud prover will first connect to the relevant chains and then look for mismatched state roots. Note that the *Fraud Prover* does not connect to the *Sequencer*, rather, it connects to the *Verifier*, and the Verifier in turn is looking at the L1. Assuming _your sequencer is not fraudulant_, the standard Fraud Prover output looks like this: + +``` + +{"level":30,"time":1619122304289,"msg":"Looking for mismatched state roots..."} +{"level":30,"time":1619122304295,"nextAttemptInS":5,"msg":"Did not find any mismatched state roots"} +{"level":30,"time":1619122309301,"msg":"Looking for mismatched state roots..."} +{"level":30,"time":1619122309306,"nextAttemptInS":5,"msg":"Did not find any mismatched state roots"} +{"level":30,"time":1619122314311,"msg":"Looking for mismatched state roots..."} + +``` + +When you spin up your local test system some small changes to the generic `local.env.yaml` and `local.yaml` are needed. Also, you will have to provide two extra files, `wait-for-l1-and-l2.sh`. For your testing conveniance, also adding a `Dockerfile.fraud_prover`. + +### local.yaml Settings + +Add to your `local.yaml` +``` +all the usual things here (L2, Batch submitter, Message Relay, Hardhat, Deployer), but then... + + verifier: + image: ethereumoptimism/go-ethereum + volumes: + - verifier:/root/.ethereum:rw + ports: + - 8045:8045 + - 8046:8046 + + fraud_prover: + image: ethereumoptimism/fraud-prover:latest + +volumes: + + geth: + + verifier: +``` + +### local.env.yaml Settings + +Add to your `local.env.yaml` +``` + +x-var: &L1_NODE_WEB3_URL + L1_NODE_WEB3_URL=http://l1_chain:9545 + +x-var: &DEPLOYER_HTTP + DEPLOYER_HTTP=http://deployer:8080 + +x-var: &ADDRESS_MANAGER_ADDRESS + ADDRESS_MANAGER_ADDRESS=0xYOUR_ADDRESS_MANAGER_HERE + +services: + +all the usual things here (L2, Batch submitter, Message Relay, Hardhat, Deployer), but then... + + verifier: + environment: + - *DEPLOYER_HTTP + - *L1_NODE_WEB3_URL + - ROLLUP_VERIFIER_ENABLE=true + - ETH1_SYNC_SERVICE_ENABLE=true + - ETH1_CTC_DEPLOYMENT_HEIGHT=8 + - ETH1_CONFIRMATION_DEPTH=0 + - ROLLUP_CLIENT_HTTP=http://data_transport_layer:7878 + - ROLLUP_POLL_INTERVAL_FLAG=3s + - USING_OVM=true + - CHAIN_ID=420 + - NETWORK_ID=420 + - DEV=true + - DATADIR=/root/.ethereum + - RPC_ENABLE=true + - RPC_ADDR=verifier + - RPC_CORS_DOMAIN=* + - RPC_VHOSTS=* + - RPC_PORT=8045 + - WS=true + - WS_ADDR=0.0.0.0 + - IPC_DISABLE=true + - TARGET_GAS_LIMIT=9000000 + - RPC_API=eth,net,rollup,web3 + - WS_API=eth,net,rollup,web3 + - WS_ORIGINS=* + - GASPRICE=0 + - NO_USB=true + - GCMODE=archive + - NO_DISCOVER=true + - ROLLUP_STATE_DUMP_PATH=http://deployer:8080/dumps/state-dump.latest.json + - RETRIES=60 + + fraud_prover: + environment: + - NO_TIMEOUT=true + - *L1_NODE_WEB3_URL + - *ADDRESS_MANAGER_ADDRESS + - L2_NODE_WEB3_URL=http://verifier:8045 + - L1_WALLET_KEY=0xYOUR_FP_WALLET_KEY_HERE + - POLLING_INTERVAL=5000 + - RUN_GAS_LIMIT=8999999 + - RELAY_GAS_LIMIT=8999999 + - FROM_L2_TRANSACTION_INDEX=0 + - L2_BLOCK_OFFSET=1 + - L1_START_OFFSET=8 + - RETRIES=60 +``` + +### Fraud Prover spinup wait-for-l1-and-l2.sh + +```bash + +#!/bin/bash + +# Copyright Optimism PBC 2020 +# MIT License +# github.com/ethereum-optimism + +cmd="$@" +JSON='{"jsonrpc":"2.0","id":0,"method":"net_version","params":[]}' + +RETRIES=${RETRIES:-50} +until $(curl --silent --fail \ + --output /dev/null \ + -H "Content-Type: application/json" \ + --data "$JSON" "$L1_NODE_WEB3_URL"); do + sleep 1 + echo "Will wait $((RETRIES--)) more times for $L1_NODE_WEB3_URL to be up..." + + if [ "$RETRIES" -lt 0 ]; then + echo "Timeout waiting for layer one node at $L1_NODE_WEB3_URL" + exit 1 + fi +done +echo "Connected to L1 Node at $L1_NODE_WEB3_URL" + +RETRIES=${RETRIES:-50} +until $(curl --silent --fail \ + --output /dev/null \ + -H "Content-Type: application/json" \ + --data "$JSON" "$L2_NODE_WEB3_URL"); do + sleep 1 + echo "Will wait $((RETRIES--)) more times for $L2_NODE_WEB3_URL to be up..." + + if [ "$RETRIES" -lt 0 ]; then + echo "Timeout waiting for layer two node at $L2_NODE_WEB3_URL" + exit 1 + fi +done +echo "Connected to L2 Verifier Node at $L2_NODE_WEB3_URL" + +if [ ! -z "$DEPLOYER_HTTP" ]; then + RETRIES=${RETRIES:-50} + until $(curl --silent --fail \ + --output /dev/null \ + "$DEPLOYER_HTTP/addresses.json"); do + sleep 1 + echo "Will wait $((RETRIES--)) more times for $DEPLOYER_HTTP to be up..." + + if [ "$RETRIES" -lt 0 ]; then + echo "Timeout waiting for contract deployment" + exit 1 + fi + done + echo "Contracts are deployed" + ADDRESS_MANAGER_ADDRESS=$(curl --silent $DEPLOYER_HTTP/addresses.json | jq -r .AddressManager) + exec env \ + ADDRESS_MANAGER_ADDRESS=$ADDRESS_MANAGER_ADDRESS \ + L1_BLOCK_OFFSET=$L1_BLOCK_OFFSET \ + $cmd +else + exec $cmd +fi + +``` + +### Fraud Prover Dockerfile.fraud_prover + +``` + +FROM node:14-buster as base + +RUN apt-get update && apt-get install -y bash curl jq + +FROM base as build + +RUN apt-get update && apt-get install -y bash git python build-essential + +ADD . /opt/fraud-prover + +RUN cd /opt/fraud-prover yarn install yarn build + +FROM base + +RUN apt-get update && apt-get install -y bash curl jq + +COPY --from=build /opt/fraud-prover /opt/fraud-prover + +COPY wait-for-l1-and-l2.sh /opt/ +RUN chmod +x /opt/wait-for-l1-and-l2.sh +RUN chmod +x /opt/fraud-prover/exec/run.js +RUN ln -s /opt/fraud-prover/exec/run.js /usr/local/bin/ + +ENTRYPOINT ["/opt/wait-for-l1-and-l2.sh", "run.js"] + +``` diff --git a/packages/fraud-prover/exec/run.js b/packages/fraud-prover/exec/run.js new file mode 100644 index 0000000000000..734b4a1499b12 --- /dev/null +++ b/packages/fraud-prover/exec/run.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +const main = require("../dist/exec/run").default + +;(async () => { + await main() +})().catch((err) => { + console.log(err) + process.exit(1) +}) diff --git a/packages/fraud-prover/package.json b/packages/fraud-prover/package.json new file mode 100644 index 0000000000000..bb23fed78c671 --- /dev/null +++ b/packages/fraud-prover/package.json @@ -0,0 +1,41 @@ +{ + "name": "@eth-optimism/fraud-prover", + "version": "0.0.1", + "private": true, + "description": "[Optimism] Fraud Prover Service", + "main": "dist/index", + "types": "dist/index", + "files": [ + "dist/index" + ], + "scripts": { + "start": "node ./exec/run.js", + "build": "tsc -p ./tsconfig.build.json", + "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo", + "lint": "yarn lint:fix && yarn lint:check", + "lint:check": "tslint --format stylish --project .", + "lint:fix": "prettier --config prettier-config.json --write \"{src,exec,test}/**/*.ts\"" + }, + "keywords": [ + "optimism", + "ethereum", + "fraud-prover" + ], + "homepage": "https://github.com/ethereum-optimism/optimism-monorepo/tree/master/packages/fraud-prover#readme", + "license": "MIT", + "author": "Optimism", + "repository": { + "type": "git", + "url": "https://github.com/ethereum-optimism/optimism.git" + }, + "dependencies": { + "@eth-optimism/core-utils": "^0.2.0", + "@eth-optimism/contracts": "^0.2.2", + "ethereumjs-util": "^7.0.7", + "dotenv": "^8.2.0", + "ethers": "^5.1.0", + "merkle-patricia-tree": "^4.0.0", + "merkletreejs": "^0.2.18", + "rlp": "^2.2.6" + } +} diff --git a/packages/fraud-prover/prettier-config.json b/packages/fraud-prover/prettier-config.json new file mode 120000 index 0000000000000..e9de01cf0ce01 --- /dev/null +++ b/packages/fraud-prover/prettier-config.json @@ -0,0 +1 @@ +../../prettier-config.json \ No newline at end of file diff --git a/packages/fraud-prover/src/exec/run.ts b/packages/fraud-prover/src/exec/run.ts new file mode 100644 index 0000000000000..8adba45c37a2d --- /dev/null +++ b/packages/fraud-prover/src/exec/run.ts @@ -0,0 +1,63 @@ +import { Wallet, providers } from 'ethers' +import { FraudProverService } from '../service' +import { config } from 'dotenv' +config() + +const env = process.env +const L2_NODE_WEB3_URL = env.L2_NODE_WEB3_URL +const L1_NODE_WEB3_URL = env.L1_NODE_WEB3_URL +const ADDRESS_MANAGER_ADDRESS = env.ADDRESS_MANAGER_ADDRESS +const L1_WALLET_KEY = env.L1_WALLET_KEY +const MNEMONIC = env.MNEMONIC +const HD_PATH = env.HD_PATH +const RELAY_GAS_LIMIT = env.RELAY_GAS_LIMIT || '4000000' +const RUN_GAS_LIMIT = env.RUN_GAS_LIMIT || '95000000' +const POLLING_INTERVAL = env.POLLING_INTERVAL || '5000' +//const GET_LOGS_INTERVAL = env.GET_LOGS_INTERVAL || '2000' +const L2_BLOCK_OFFSET = env.L2_BLOCK_OFFSET || '1' +const L1_START_OFFSET = env.L1_BLOCK_OFFSET || '1' +const L1_BLOCK_FINALITY = env.L1_BLOCK_FINALITY || '0' +const FROM_L2_TRANSACTION_INDEX = env.FROM_L2_TRANSACTION_INDEX || '0' + +const main = async () => { + if (!ADDRESS_MANAGER_ADDRESS) { + throw new Error('Must pass ADDRESS_MANAGER_ADDRESS') + } + if (!L1_NODE_WEB3_URL) { + throw new Error('Must pass L1_NODE_WEB3_URL') + } + if (!L2_NODE_WEB3_URL) { + throw new Error('Must pass L2_NODE_WEB3_URL') + } + + const l2Provider = new providers.JsonRpcProvider(L2_NODE_WEB3_URL) + const l1Provider = new providers.JsonRpcProvider(L1_NODE_WEB3_URL) + + let wallet: Wallet + if (L1_WALLET_KEY) { + wallet = new Wallet(L1_WALLET_KEY, l1Provider) + } else if (MNEMONIC) { + wallet = Wallet.fromMnemonic(MNEMONIC, HD_PATH) + wallet = wallet.connect(l1Provider) + } else { + throw new Error('Must pass one of L1_WALLET_KEY or MNEMONIC') + } + + const service = new FraudProverService({ + l1RpcProvider: l1Provider, + l2RpcProvider: l2Provider, + addressManagerAddress: ADDRESS_MANAGER_ADDRESS, + l1Wallet: wallet, + deployGasLimit: parseInt(RELAY_GAS_LIMIT, 10), //should reconcile naming + runGasLimit: parseInt(RUN_GAS_LIMIT, 10), //should reconcile naming + fromL2TransactionIndex: parseInt(FROM_L2_TRANSACTION_INDEX, 10), + pollingInterval: parseInt(POLLING_INTERVAL, 10), + l2BlockOffset: parseInt(L2_BLOCK_OFFSET, 10), + l1StartOffset: parseInt(L1_START_OFFSET, 10), + l1BlockFinality: parseInt(L1_BLOCK_FINALITY, 10), + //getLogsInterval: parseInt(GET_LOGS_INTERVAL, 10), + }) + + await service.start() +} +export default main diff --git a/packages/fraud-prover/src/service.ts b/packages/fraud-prover/src/service.ts new file mode 100644 index 0000000000000..56ddd0f768003 --- /dev/null +++ b/packages/fraud-prover/src/service.ts @@ -0,0 +1,1162 @@ +/* Imports: External */ +import { Contract, Signer, ethers, Wallet, BigNumber, providers } from 'ethers' +import * as rlp from 'rlp' +import { MerkleTree } from 'merkletreejs' +import { BaseTrie } from 'merkle-patricia-tree' + +import { + BaseService, + sleep, + toHexString, + fromHexString, +} from '@eth-optimism/core-utils' + +import { + loadContract, + loadContractFromManager +} from '@eth-optimism/contracts' + +/* Imports: Internal */ +import { + ZERO_ADDRESS, + L1ProviderWrapper, + L2ProviderWrapper, + encodeAccountState, + hashOvmTransaction, + makeTrieFromProofs, + shuffle, + toStrippedHexString, + toBytes32, +} from './utils' + +import { + StateDiffProof, + StateTransitionPhase, + FraudProofData, + OvmTransaction, + StateRootBatchProof, + TransactionBatchProof, + AccountStateProof, + StorageStateProof, +} from './types' + +interface FraudProverOptions { + // Providers for interacting with L1 and L2. + l1RpcProvider: providers.JsonRpcProvider + l2RpcProvider: providers.JsonRpcProvider + + // Address of the AddressManager contract, used to resolve the various addresses we'll need + // within this service. + addressManagerAddress: string + + // Wallet instance, used to sign the L1 transactions. + l1Wallet: Wallet // l1Wallet: Signer + + // Max gas. + deployGasLimit: number + runGasLimit: number + + // Height of the L2 transaction to start searching for L2->L1 messages. + fromL2TransactionIndex?: number + + // Interval in seconds to wait between loops. + pollingInterval?: number + + // Number of blocks that L2 is "ahead" of transaction indices. Can happen if blocks are created + // on L2 after the genesis but before the first state commitment is published. + l2BlockOffset?: number + + // L1 block to start querying events from. Recommended to set to the StateCommitmentChain deploy height + l1StartOffset?: number + + // When L1 blocks are considered final + l1BlockFinality: number + + // Number of blocks within each getLogs query - max is 2000 + //getLogsInterval?: number +} + +const optionSettings = { + pollingInterval: { default: 5000 }, + deployGasLimit: { default: 4_000_000 }, + runGasLimit: { default: 9_500_000 }, + fromL2TransactionIndex: { default: 0 }, + l2BlockOffset: { default: 1 }, + l1StartOffset: { default: 0 }, + l1BlockFinality: { default: 0 }, + //getLogsInterval: { default: 2000 }, +} + +export class FraudProverService extends BaseService { + constructor(options: FraudProverOptions) { + super('Fraud_Prover', options, optionSettings) + } + + private state: { + nextUnverifiedStateRoot: number + lastFinalizedTxHeight: number + nextUnfinalizedTxHeight: number + lastQueriedL1Block: number + eventCache: ethers.Event[] + Lib_AddressManager: Contract + OVM_StateCommitmentChain: Contract + OVM_L1CrossDomainMessenger: Contract + OVM_L2CrossDomainMessenger: Contract + OVM_L2ToL1MessagePasser: Contract + l1Provider: L1ProviderWrapper + l2Provider: L2ProviderWrapper + OVM_CanonicalTransactionChain: Contract + OVM_FraudVerifier: Contract + OVM_ExecutionManager: Contract + } + + protected async _init(): Promise { + this.logger.info('Initializing fraud prover', { options: this.options }) + // Need to improve this, sorry. + this.state = {} as any + + const address = await this.options.l1Wallet.getAddress() + this.logger.info('Using L1 EOA', { address }) + + this.logger.info('Trying to connect to the L1 network...') + for (let i = 0; i < 10; i++) { + try { + await this.options.l1RpcProvider.detectNetwork() + this.logger.info('Successfully connected to the L1 network.') + break + } catch (err) { + if (i < 9) { + this.logger.info('Unable to connect to L1 network', { + retryAttemptsRemaining: 10 - i, + }) + await sleep(1000) + } else { + throw new Error( + `Unable to connect to the L1 network, check that your L1 endpoint is correct.` + ) + } + } + } + + this.logger.info('Trying to connect to the L2 network...') + for (let i = 0; i < 10; i++) { + try { + await this.options.l2RpcProvider.detectNetwork() + this.logger.info('Successfully connected to the L2 network.') + break + } catch (err) { + if (i < 9) { + this.logger.info('Unable to connect to L1 network', { + retryAttemptsRemaining: 10 - i, + }) + await sleep(1000) + } else { + throw new Error( + `Unable to connect to the L2 network, check that your L2 endpoint is correct.` + ) + } + } + } + + this.logger.info('Connecting to Lib_AddressManager...') + this.state.Lib_AddressManager = loadContract( + 'Lib_AddressManager', + this.options.addressManagerAddress, + this.options.l1RpcProvider + ) + this.logger.info('Connected to Lib_AddressManager', { + address: this.state.Lib_AddressManager.address, + }) + + this.logger.info('Connecting to OVM_StateCommitmentChain...') + this.state.OVM_StateCommitmentChain = await loadContractFromManager({ + name: 'OVM_StateCommitmentChain', + Lib_AddressManager: this.state.Lib_AddressManager, + provider: this.options.l1RpcProvider, + }) + this.logger.info('Connected to OVM_StateCommitmentChain', { + address: this.state.OVM_StateCommitmentChain.address, + }) + + this.logger.info('Connecting to OVM_CanonicalTransactionChain...') + this.state.OVM_CanonicalTransactionChain = await loadContractFromManager({ + name: 'OVM_CanonicalTransactionChain', + Lib_AddressManager: this.state.Lib_AddressManager, + provider: this.options.l1RpcProvider, + }) + this.logger.info('Connected to OVM_CanonicalTransactionChain', { + address: this.state.OVM_CanonicalTransactionChain.address, + }) + + this.logger.info('Connecting to OVM_FraudVerifier...') + this.state.OVM_FraudVerifier = await loadContractFromManager({ + name: 'OVM_FraudVerifier', + Lib_AddressManager: this.state.Lib_AddressManager, + provider: this.options.l1RpcProvider, + }) + this.logger.info('Connected to OVM_FraudVerifier', { + address: this.state.OVM_FraudVerifier.address, + }) + + this.logger.info('Connecting to OVM_ExecutionManager...') + this.state.OVM_ExecutionManager = await loadContractFromManager({ + name: 'OVM_ExecutionManager', + Lib_AddressManager: this.state.Lib_AddressManager, + provider: this.options.l1RpcProvider, + }) + this.logger.info('Connected to OVM_ExecutionManager', { + address: this.state.OVM_ExecutionManager.address, + }) + + this.logger.info('Connected to all contracts.') + + this.state.l1Provider = new L1ProviderWrapper( + this.options.l1RpcProvider, + this.state.OVM_StateCommitmentChain, + this.state.OVM_CanonicalTransactionChain, + this.state.OVM_ExecutionManager, + this.options.l1StartOffset, + this.options.l1BlockFinality + ) + + this.logger.info( + 'Caching events for relevant contracts, this might take a while...' + ) + this.logger.info('Caching events for OVM_StateCommitmentChain...') + await this.state.l1Provider.findAllEvents( + this.state.OVM_StateCommitmentChain, + this.state.OVM_StateCommitmentChain.filters.StateBatchAppended() + ) + + this.logger.info('Caching events for OVM_CanonicalTransactionChain...') + await this.state.l1Provider.findAllEvents( + this.state.OVM_CanonicalTransactionChain, + this.state.OVM_CanonicalTransactionChain.filters.TransactionBatchAppended() + ) + await this.state.l1Provider.findAllEvents( + this.state.OVM_CanonicalTransactionChain, + this.state.OVM_CanonicalTransactionChain.filters.SequencerBatchAppended() + ) + + this.state.lastQueriedL1Block = this.options.l1StartOffset + this.state.eventCache = [] + + this.state.lastFinalizedTxHeight = this.options.fromL2TransactionIndex || 0 + this.state.nextUnfinalizedTxHeight = + this.options.fromL2TransactionIndex || 0 + } + + protected async _start(): Promise { + while (this.running) { + await sleep(this.options.pollingInterval) + + try { + this.logger.info('Looking for mismatched state roots...') + const fraudulentStateRootIndex = await this._findNextFraudulentStateRoot() + + if (fraudulentStateRootIndex === undefined) { + this.logger.info('Did not find any mismatched state roots', { + nextAttemptInS: this.options.pollingInterval / 1000, + }) + continue + } + + this.logger.info('Found a mismatched state root', { + index: fraudulentStateRootIndex, + }) + + this.logger.info('Pulling fraud proof data...') + const proof = await this._getFraudProofData(fraudulentStateRootIndex) + + this.logger.info('Initializing the fraud verification process...') + try { + await this._initializeFraudVerification( + proof.preStateRootProof, + proof.transactionProof + ) + } catch (err) { + if (err.toString().includes('Reverted 0x')) { + this.logger.info( + 'Fraud proof was initialized by someone else, moving on...' + ) + } else { + throw err + } + } + + this.logger.info('Loading fraud proof contracts...') + const { + OVM_StateTransitioner, + OVM_StateManager, + } = await this._getFraudProofContracts( + await this.state.l1Provider.getStateRoot( + fraudulentStateRootIndex - 1 + ), + proof.transactionProof.transaction + ) + + // PRE_EXECUTION phase. + if ( + (await OVM_StateTransitioner.phase()) === + StateTransitionPhase.PRE_EXECUTION + ) { + try { + this.logger.info('Fraud proof is now in the PRE_EXECUTION phase.') + + this.logger.info('Proving account states...') + await this._proveAccountStates( + OVM_StateTransitioner, + OVM_StateManager, + proof.stateDiffProof.accountStateProofs, + fraudulentStateRootIndex + ) + + this.logger.info('Proving storage slot states...') + await this._proveContractStorageStates( + OVM_StateTransitioner, + OVM_StateManager, + proof.stateDiffProof.accountStateProofs + ) + + this.logger.info('Executing transaction...') + try { + await ( + await OVM_StateTransitioner.applyTransaction( + proof.transactionProof.transaction, + { + gasLimit: this.options.runGasLimit, + } + ) + ).wait() + } catch (err) { + await OVM_StateTransitioner.callStatic.applyTransaction( + proof.transactionProof.transaction, + { + gasLimit: this.options.runGasLimit, + } + ) + } + + this.logger.info('Transaction successfully executed.') + } catch (err) { + if ( + err + .toString() + .includes( + 'Function must be called during the correct phase.' + ) || + err + .toString() + .includes( + '46756e6374696f6e206d7573742062652063616c6c656420647572696e672074686520636f72726563742070686173652e' + ) + ) { + this.logger.info( + 'Phase was completed by someone else, moving on.' + ) + } else { + throw err + } + } + } + + // POST_EXECUTION phase. + if ( + (await OVM_StateTransitioner.phase()) === + StateTransitionPhase.POST_EXECUTION + ) { + try { + this.logger.info('Fraud proof is now in the POST_EXECUTION phase.') + + this.logger.info('Committing storage slot state updates...') + await this._updateContractStorageStates( + OVM_StateTransitioner, + OVM_StateManager, + proof.stateDiffProof.accountStateProofs, + proof.storageTries + ) + + this.logger.info('Committing account state updates...') + await this._updateAccountStates( + OVM_StateTransitioner, + OVM_StateManager, + proof.stateDiffProof.accountStateProofs, + proof.stateTrie + ) + + this.logger.info('Completing the state transition...') + try { + await (await OVM_StateTransitioner.completeTransition()).wait() + } catch (err) { + try { + await OVM_StateTransitioner.callStatic.completeTransition() + } catch (err) { + if (err.toString().includes('Reverted 0x')) { + this.logger.info( + 'State transition was completed by someone else, moving on.' + ) + } else { + throw err + } + } + } + + this.logger.info('State transition completed.') + } catch (err) { + if ( + err + .toString() + .includes( + 'Function must be called during the correct phase.' + ) || + err + .toString() + .includes( + '46756e6374696f6e206d7573742062652063616c6c656420647572696e672074686520636f72726563742070686173652e' + ) + ) { + this.logger.info( + 'Phase was completed by someone else, moving on.' + ) + } else { + throw err + } + } + } + + // COMPLETE phase. + if ( + (await OVM_StateTransitioner.phase()) === + StateTransitionPhase.COMPLETE + ) { + this.logger.info('Fraud proof is now in the COMPLETE phase.') + + this.logger.info('Attempting to finalize the fraud proof...') + try { + await this._finalizeFraudVerification( + proof.preStateRootProof, + proof.postStateRootProof, + proof.transactionProof.transaction + ) + + this.logger.info('Fraud proof finalized! Congrats.') + } catch (err) { + if ( + err.toString().includes('Invalid batch header.') || + err.toString().includes('Index out of bounds.') || + err.toString().includes('Reverted 0x') + ) { + this.logger.info('Fraud proof was finalized by someone else.') + } else { + throw err + } + } + } + + this.state.nextUnverifiedStateRoot = proof.preStateRootProof.stateRootBatchHeader.prevTotalElements.toNumber() + } catch (err) { + this.logger.error('Caught an unhandled error', { + err, + }) + } + } + } + + /** + * Finds the index of the next fraudulent state root. + * @return Index of the next fraudulent state root, if any. + */ + private async _findNextFraudulentStateRoot(): Promise { + let nextBatchHeader = await this.state.l1Provider.getStateRootBatchHeader( + this.state.nextUnverifiedStateRoot + ) + + while (nextBatchHeader !== undefined) { + const nextBatchStateRoots = await this.state.l1Provider.getBatchStateRoots( + this.state.nextUnverifiedStateRoot + ) + + for (let i = 0; i < nextBatchHeader.batchSize.toNumber(); i++) { + const index = i + nextBatchHeader.prevTotalElements.toNumber() + this.logger.info('Checking state root for mismatch', { index }) + + const l1StateRoot = nextBatchStateRoots[i] + const l2StateRoot = await this.state.l2Provider.getStateRoot( + index + this.options.l2BlockOffset + ) + + if (l1StateRoot !== l2StateRoot) { + this.logger.info('State roots do not match') + this.logger.info('L1 State Root', { l1StateRoot }) + this.logger.info('L2 State Root', { l2StateRoot }) + return index + } else { + this.logger.info('State root was not mismatched ✓') + } + } + + this.state.nextUnverifiedStateRoot = + nextBatchHeader.prevTotalElements.toNumber() + + nextBatchHeader.batchSize.toNumber() + nextBatchHeader = await this.state.l1Provider.getStateRootBatchHeader( + this.state.nextUnverifiedStateRoot + ) + } + } + + /** + * Generates all transaction proof data for a given transaction index. + * @param transactionIndex Transaction index to get proof data for. + * @return Transaction proof data. + */ + private async _getFraudProofData( + transactionIndex: number + ): Promise { + this.logger.info('Getting pre-state root inclusion proof...') + const preStateRootProof = await this.state.l1Provider.getStateRootBatchProof( + transactionIndex - 1 + ) + + this.logger.info('Getting post-state root inclusion proof...') + const postStateRootProof = await this.state.l1Provider.getStateRootBatchProof( + transactionIndex + ) + + this.logger.info('Getting transaction inclusion proof...') + const transactionProof = await this.state.l1Provider.getTransactionBatchProof( + transactionIndex + ) + + this.logger.info('Getting state diff proof...') + const stateDiffProof: StateDiffProof = await this.state.l2Provider.getStateDiffProof( + transactionIndex + this.options.l2BlockOffset - 1 + ) + + const stateTrie = await this._makeStateTrie(stateDiffProof) + const storageTries = await this._makeAccountTries(stateDiffProof) + + return { + stateDiffProof, + transactionProof, + preStateRootProof, + postStateRootProof, + stateTrie, + storageTries, + } + } + + /** + * Pulls the fraud proof contracts. + * @param preStateRoot Pre-state root to pull contracts for. + * @param transaction Transaction to pull contracts for. + * @return Fraud proof contracts. + */ + private async _getFraudProofContracts( + preStateRoot: string, + transaction: OvmTransaction + ): Promise<{ + OVM_StateTransitioner: Contract + OVM_StateManager: Contract + }> { + this.logger.info('Loading the state transitioner...') + + const stateTransitionerAddress = await this._getStateTransitioner( + preStateRoot, + transaction + ) + + const OVM_StateTransitioner = loadContract( + 'OVM_StateTransitioner', + stateTransitionerAddress, + this.options.l1RpcProvider + ).connect(this.options.l1Wallet) + + this.logger.info('State transitioner', { stateTransitionerAddress }) + + this.logger.info('Loading the corresponding state manager...') + + const stateManagerAddress = await OVM_StateTransitioner.ovmStateManager() + const OVM_StateManager = loadContract( + 'OVM_StateManager', + stateManagerAddress, + this.options.l1RpcProvider + ).connect(this.options.l1Wallet) + + this.logger.info('State manager', { stateManagerAddress }) + + return { + OVM_StateTransitioner, + OVM_StateManager, + } + } + + /** + * Generates a view of the state trie from a state diff proof. + * @param proof State diff proof to generate a trie from. + * @return View of the state trie. + */ + private async _makeStateTrie(proof: StateDiffProof): Promise { + return makeTrieFromProofs( + proof.accountStateProofs.map((accountStateProof) => { + return accountStateProof.accountProof + }) + ) + } + + /** + * Generates a view of a set of account tries from a state diff proof. + * @param proof State diff proof to generate tries from. + * @return View of a set of all account tries. + */ + private async _makeAccountTries( + proof: StateDiffProof + ): Promise<{ + [address: string]: BaseTrie + }> { + const accountTries: { [address: string]: BaseTrie } = {} + + for (const accountStateProof of proof.accountStateProofs) { + accountTries[accountStateProof.address] = await makeTrieFromProofs( + accountStateProof.storageProof.map((storageProof) => { + return storageProof.proof + }) + ) + } + + return accountTries + } + + /** + * Retrieves the state transitioner corresponding to a given pre-state root and transaction. + * @param preStateRoot Pre-state root to retreive a state transitioner for. + * @param transaction Transaction to retreive a state transitioner for. + * @return Address of the corresponding state transitioner. + */ + private async _getStateTransitioner( + preStateRoot: string, + transaction: OvmTransaction + ): Promise { + return this.state.OVM_FraudVerifier.getStateTransitioner( + preStateRoot, + hashOvmTransaction(transaction) + ) + } + + /** + * Simple mechanism for deploying an exact bytecode to a given address. Resulting contract will + * have code exactly matching the given `code` variable, and none of the code will be executed + * during creation. + * @param code Code to store at a given address. + * @return Address of the newly created contract. + */ + private async _deployContractCode(code: string): Promise { + // "Magic" prefix to be prepended to the contract code. Contains a series of opcodes that will + // copy the given code into memory and return it, thereby storing at the contract address. + const prefix = '0x600D380380600D6000396000f3' + const deployCode = prefix + toHexString(code).slice(2) + + const response = await this.options.l1Wallet.sendTransaction({ + to: null, + data: deployCode, + gasLimit: this.options.deployGasLimit, + }) + + const result = await response.wait() + return result.contractAddress + } + + /** + * Proves the state of all given accounts. + * @param OVM_StateTransitioner Ethers contract instance pointed at the state transitioner. + * @param OVM_StateManager Ethers contract instance pointed at the state manager. + * @param accountStateProofs All account state proofs. + * @param fraudulentStateRootIndex Index of the fraudulent state root. + */ + private async _proveAccountStates( + OVM_StateTransitioner: Contract, + OVM_StateManager: Contract, + accountStateProofs: AccountStateProof[], + fraudulentStateRootIndex: number + ): Promise { + for (const accountStateProof of shuffle(accountStateProofs)) { + this.logger.info('Attempting to prove account state', { + address: accountStateProof.address, + }) + + if (await OVM_StateManager.hasAccount(accountStateProof.address)) { + this.logger.info( + 'Someone else already proved this account, skipping...' + ) + continue + } + + const accountCode = await this.options.l2RpcProvider.getCode( + accountStateProof.address, + fraudulentStateRootIndex + this.options.l2BlockOffset + ) + + let ethContractAddress = '0x0000c0De0000C0DE0000c0de0000C0DE0000c0De' + if (accountCode !== '0x') { + this.logger.info('Need to deploy a copy of the account first...') + ethContractAddress = await this._deployContractCode(accountCode) + this.logger.info('Deployed a copy of the account, attempting proof...') + } + + try { + await ( + await OVM_StateTransitioner.proveContractState( + accountStateProof.address, + ethContractAddress, + rlp.encode(accountStateProof.accountProof) + ) + ).wait() + + this.logger.info('Account state proven.') + } catch (err) { + try { + await OVM_StateTransitioner.callStatic.proveContractState( + accountStateProof.address, + ethContractAddress, + rlp.encode(accountStateProof.accountProof) + ) + } catch (err) { + if ( + err.toString().includes('Account state has already been proven') || + err.toString().includes('Reverted 0x') + ) { + this.logger.info( + 'Someone else has already proven this account, skipping.' + ) + } else { + throw err + } + } + } + } + } + + /** + * Proves all contract storage slot states. + * @param OVM_StateTransitioner Ethers contract instance pointed at the state transitioner. + * @param OVM_StateManager Ethers contract instance pointed at the state manager. + * @param accountStateProofs All account state proofs. + */ + private async _proveContractStorageStates( + OVM_StateTransitioner: Contract, + OVM_StateManager: Contract, + accountStateProofs: AccountStateProof[] + ): Promise { + for (const accountStateProof of shuffle(accountStateProofs)) { + for (const slot of shuffle(accountStateProof.storageProof)) { + this.logger.info('Attempting to prove slot.', { + address: accountStateProof.address, + key: slot.key, + value: slot.value, + }) + if ( + await OVM_StateManager.hasContractStorage( + accountStateProof.address, + toBytes32(slot.key) + ) + ) { + this.logger.info( + 'Someone else has already proven this slot, skipping...' + ) + continue + } + + try { + await ( + await OVM_StateTransitioner.proveStorageSlot( + accountStateProof.address, + toBytes32(slot.key), + rlp.encode(slot.proof) + ) + ).wait() + + this.logger.info('Slot value proven.') + } catch (err) { + try { + await OVM_StateTransitioner.callStatic.proveStorageSlot( + accountStateProof.address, + toBytes32(slot.key), + rlp.encode(slot.proof) + ) + } catch (err) { + if ( + err + .toString() + .includes('Storage slot has already been proven.') || + err.toString().includes('Reverted 0x') + ) { + this.logger.info( + 'Someone else has already proven this slot, skipping.' + ) + } else { + throw err + } + } + } + } + } + } + + /** + * Commits all account state changes. + * @param OVM_StateTransitioner Ethers contract instance pointed at the state transitioner. + * @param OVM_StateManager Ethers contract instance pointed at the state manager. + * @param accountStateProofs All account state proofs. + * @param stateTrie State trie view generated from proof data. + */ + private async _updateAccountStates( + OVM_StateTransitioner: Contract, + OVM_StateManager: Contract, + accountStateProofs: AccountStateProof[], + stateTrie: BaseTrie + ): Promise { + while ((await OVM_StateManager.getTotalUncommittedAccounts()) > 0) { + const accountCommittedEvents = await this.state.l1Provider.findAllEvents( + OVM_StateTransitioner, + OVM_StateTransitioner.filters.AccountCommitted() + ) + + // Use events to figure out which accounts we've already committed. + const committedAccounts = accountStateProofs.filter((account) => { + return accountCommittedEvents.some((event) => { + return ( + event.args._address.toLowerCase() === account.address.toLowerCase() + ) + }) + }) + + // Update our trie with the values of any accounts that have already been committed. Order + // here doesn't matter because the trie will still end up with the same root. We can also + // repeatedly update a key with the same value since it won't have an impact on the trie. + for (const account of committedAccounts) { + const accountState = await OVM_StateManager.getAccount(account.address) + + await stateTrie.put( + fromHexString(ethers.utils.keccak256(account.address)), + encodeAccountState({ + ...accountState, + ...{ + nonce: accountState.nonce.toNumber(), + }, + }) + ) + } + + // Find an uncommitted account to attempt to commit. + let nextUncommittedAccount: AccountStateProof + for (const account of shuffle(accountStateProofs)) { + if ( + !(await OVM_StateManager.wasAccountCommitted(account.address)) && + (await OVM_StateManager.wasAccountChanged(account.address)) + ) { + nextUncommittedAccount = account + break + } + } + + if (nextUncommittedAccount === undefined) { + if ((await OVM_StateManager.getTotalUncommittedAccounts()) > 0) { + throw new Error( + `We still have accounts to commit, but we don't have any more proof data. Something went very wrong.` + ) + } else { + return + } + } + + // Generate an inclusion proof for the account, will be used to update the value on-chain. + const accountInclusionProof = toHexString( + rlp.encode( + await BaseTrie.createProof( + stateTrie, + fromHexString( + ethers.utils.keccak256(nextUncommittedAccount.address) + ) + ) + ) + ) + + const updatedAccountState = await OVM_StateManager.getAccount( + nextUncommittedAccount.address + ) + + this.logger.info('Attempting to commit account.', { + address: nextUncommittedAccount.address, + balance: updatedAccountState.balance, + nonce: updatedAccountState.nonce, + storageRoot: updatedAccountState.storageRoot, + codeHash: updatedAccountState.codeHash, + }) + + try { + await ( + await OVM_StateTransitioner.commitContractState( + nextUncommittedAccount.address, + accountInclusionProof, + { + gasLimit: this.options.deployGasLimit, + } + ) + ).wait() + + this.logger.info('Account committed.') + } catch (err) { + try { + await OVM_StateTransitioner.callStatic.commitContractState( + nextUncommittedAccount.address, + accountInclusionProof, + { + gasLimit: this.options.deployGasLimit, + } + ) + } catch (err) { + if ( + err.toString().includes('invalid opcode') || + err.toString().includes('Invalid root hash') || + err + .toString() + .includes( + `Account state wasn't changed or has already been committed.` + ) || + err.toString().includes('Reverted 0x') + ) { + this.logger.info( + 'Could not commit account because another commitment invalidated our proof, skipping for now...' + ) + } else { + throw err + } + } + } + } + } + + /** + * Commits all contract storage slot changes. + * @param OVM_StateTransitioner Ethers contract instance pointed at the state transitioner. + * @param OVM_StateManager Ethers contract instance pointed at the state manager. + * @param accountStateProofs All account state proofs. + * @param stateTrie State trie view generated from proof data. + * @param storageTries Storage trie views generated from proof data. + */ + private async _updateContractStorageStates( + OVM_StateTransitioner: Contract, + OVM_StateManager: Contract, + accountStateProofs: AccountStateProof[], + storageTries: { + [address: string]: BaseTrie + } + ) { + while ((await OVM_StateManager.getTotalUncommittedContractStorage()) > 0) { + const storageCommittedEvents = await this.state.l1Provider.findAllEvents( + OVM_StateTransitioner, + OVM_StateTransitioner.filters.ContractStorageCommitted() + ) + + for (const accountStateProof of accountStateProofs) { + const committedStorageSlots = accountStateProof.storageProof.filter( + (storageProof) => { + return storageCommittedEvents.some((event) => { + return ( + event.args._address.toLowerCase() === + accountStateProof.address.toLowerCase() && + event.args._key.toLowerCase() === storageProof.key.toLowerCase() + ) + }) + } + ) + + for (const storageProof of committedStorageSlots) { + const updatedSlotValue = await OVM_StateManager.getContractStorage( + accountStateProof.address, + storageProof.key + ) + + await storageTries[accountStateProof.address].put( + fromHexString(ethers.utils.keccak256(storageProof.key)), + fromHexString(rlp.encode(toStrippedHexString(updatedSlotValue))) + ) + } + } + + for (const accountStateProof of accountStateProofs) { + let nextUncommittedStorageProof: StorageStateProof + for (const storageProof of accountStateProof.storageProof) { + if ( + !(await OVM_StateManager.wasContractStorageCommitted( + accountStateProof.address, + storageProof.key + )) && + (await OVM_StateManager.wasContractStorageChanged( + accountStateProof.address, + storageProof.key + )) + ) { + nextUncommittedStorageProof = storageProof + break + } + } + + if (nextUncommittedStorageProof === undefined) { + continue + } + + const slotInclusionProof = toHexString( + rlp.encode( + await BaseTrie.createProof( + storageTries[accountStateProof.address], + fromHexString( + ethers.utils.keccak256(nextUncommittedStorageProof.key) + ) + ) + ) + ) + + const updatedSlotValue = await OVM_StateManager.getContractStorage( + accountStateProof.address, + nextUncommittedStorageProof.key + ) + + this.logger.info('Attempting to commit storage slot.', { + address: accountStateProof.address, + key: nextUncommittedStorageProof.key, + value: updatedSlotValue, + }) + + try { + await ( + await OVM_StateTransitioner.commitStorageSlot( + accountStateProof.address, + nextUncommittedStorageProof.key, + slotInclusionProof, + { + gasLimit: this.options.deployGasLimit, + } + ) + ).wait() + + this.logger.info('Storage slot committed.') + } catch (err) { + try { + await OVM_StateTransitioner.callStatic.commitStorageSlot( + accountStateProof.address, + nextUncommittedStorageProof.key, + slotInclusionProof, + { + gasLimit: this.options.deployGasLimit, + } + ) + } catch (err) { + if ( + err.toString().includes('invalid opcode') || + err.toString().includes('Invalid root hash') || + err + .toString() + .includes( + `Storage slot value wasn't changed or has already been committed.` + ) || + err.toString().includes('Reverted 0x') + ) { + this.logger.info( + 'Could not commit slot because another commitment invalidated our proof, skipping for now...' + ) + } else { + throw err + } + } + } + } + } + } + + /** + * Initializes the fraud verification process. + * @param preStateRootProof Proof data for the pre-state root. + * @param transactionProof Proof data for the transaction being verified. + */ + private async _initializeFraudVerification( + preStateRootProof: StateRootBatchProof, + transactionProof: TransactionBatchProof + ): Promise { + const stateTransitionerAddress = await this._getStateTransitioner( + preStateRootProof.stateRoot, + transactionProof.transaction + ) + + if (stateTransitionerAddress !== ZERO_ADDRESS) { + return + } + + try { + await ( + await this.state.OVM_FraudVerifier.connect( + this.options.l1Wallet + ).initializeFraudVerification( + preStateRootProof.stateRoot, + preStateRootProof.stateRootBatchHeader, + preStateRootProof.stateRootProof, + transactionProof.transaction, + transactionProof.transactionChainElement, + transactionProof.transactionBatchHeader, + transactionProof.transactionProof + ) + ).wait() + } catch (err) { + await this.state.OVM_FraudVerifier.connect( + this.options.l1Wallet + ).callStatic.initializeFraudVerification( + preStateRootProof.stateRoot, + preStateRootProof.stateRootBatchHeader, + preStateRootProof.stateRootProof, + transactionProof.transaction, + transactionProof.transactionChainElement, + transactionProof.transactionBatchHeader, + transactionProof.transactionProof + ) + } + } + + /** + * Finalizes the fraud verification process. + * @param preStateRootProof Proof data for the pre-state root. + * @param postStateRootProof Proof data for the post-state root. + * @param transaction Transaction being verified. + */ + private async _finalizeFraudVerification( + preStateRootProof: StateRootBatchProof, + postStateRootProof: StateRootBatchProof, + transaction: OvmTransaction + ): Promise { + try { + await ( + await this.state.OVM_FraudVerifier.connect( + this.options.l1Wallet + ).finalizeFraudVerification( + preStateRootProof.stateRoot, + preStateRootProof.stateRootBatchHeader, + preStateRootProof.stateRootProof, + hashOvmTransaction(transaction), + postStateRootProof.stateRoot, + postStateRootProof.stateRootBatchHeader, + postStateRootProof.stateRootProof + ) + ).wait() + } catch (err) { + await this.state.OVM_FraudVerifier.connect( + this.options.l1Wallet + ).callStatic.finalizeFraudVerification( + preStateRootProof.stateRoot, + preStateRootProof.stateRootBatchHeader, + preStateRootProof.stateRootProof, + hashOvmTransaction(transaction), + postStateRootProof.stateRoot, + postStateRootProof.stateRootBatchHeader, + postStateRootProof.stateRootProof + ) + } + } +} diff --git a/packages/fraud-prover/src/types.ts b/packages/fraud-prover/src/types.ts new file mode 100644 index 0000000000000..ff5a3143c6fe1 --- /dev/null +++ b/packages/fraud-prover/src/types.ts @@ -0,0 +1,123 @@ +import { BigNumber } from 'ethers' +import { BaseTrie } from 'merkle-patricia-tree' + +export interface StateRootBatchHeader { + batchIndex: BigNumber + batchRoot: string + batchSize: BigNumber + prevTotalElements: BigNumber + extraData: string +} + +export interface SentMessage { + target: string + sender: string + message: string + messageNonce: number + encodedMessage: string + encodedMessageHash: string + parentTransactionIndex: number + parentTransactionHash: string +} + +export interface SentMessageProof { + stateRoot: string + stateRootBatchHeader: StateRootBatchHeader + stateRootProof: StateRootProof + stateTrieWitness: string | Buffer + storageTrieWitness: string | Buffer +} + +export interface StateRootProof { + index: number + siblings: string[] +} + +export interface TransactionBatchHeader { + batchIndex: BigNumber + batchRoot: string + batchSize: BigNumber + prevTotalElements: BigNumber + extraData: string +} + +export interface TransactionProof { + index: number + siblings: string[] +} + +export interface StateRootBatchProof { + stateRoot: string + stateRootBatchHeader: StateRootBatchHeader + stateRootProof: StateRootProof +} + +export interface TransactionBatchProof { + transaction: OvmTransaction + transactionChainElement: TransactionChainElement + transactionBatchHeader: TransactionBatchHeader + transactionProof: TransactionProof +} + +export enum StateTransitionPhase { + PRE_EXECUTION, + POST_EXECUTION, + COMPLETE, +} + +export interface AccountStateProof { + address: string + balance: string + nonce: string + codeHash: string + storageHash: string + accountProof: string[] + storageProof: StorageStateProof[] +} + +export interface StorageStateProof { + key: string + value: string + proof: string[] +} + +export interface StateDiffProof { + header: { + number: number + hash: string + stateRoot: string + timestamp: number + } + + accountStateProofs: AccountStateProof[] +} + +export interface OvmTransaction { + blockNumber: number + timestamp: number + entrypoint: string + gasLimit: number + l1TxOrigin: string + l1QueueOrigin: number + data: string +} + +export interface FraudProofData { + stateDiffProof: StateDiffProof + transactionProof: TransactionBatchProof + preStateRootProof: StateRootBatchProof + postStateRootProof: StateRootBatchProof + + stateTrie: BaseTrie + storageTries: { + [address: string]: BaseTrie + } +} + +export interface TransactionChainElement { + isSequenced: boolean + queueIndex?: number + timestamp?: number + blockNumber?: number + txData?: string +} diff --git a/packages/fraud-prover/src/utils/common.ts b/packages/fraud-prover/src/utils/common.ts new file mode 100644 index 0000000000000..4de905f85845a --- /dev/null +++ b/packages/fraud-prover/src/utils/common.ts @@ -0,0 +1,8 @@ +export const shuffle = (array: any[]): any[] => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[array[i], array[j]] = [array[j], array[i]] + } + + return array +} diff --git a/packages/fraud-prover/src/utils/constants.ts b/packages/fraud-prover/src/utils/constants.ts new file mode 100644 index 0000000000000..aab266df5fd2c --- /dev/null +++ b/packages/fraud-prover/src/utils/constants.ts @@ -0,0 +1 @@ +export const ZERO_ADDRESS = '0x' + '00'.repeat(20) diff --git a/packages/fraud-prover/src/utils/eth-utils.ts b/packages/fraud-prover/src/utils/eth-utils.ts new file mode 100644 index 0000000000000..b11f5992b49ad --- /dev/null +++ b/packages/fraud-prover/src/utils/eth-utils.ts @@ -0,0 +1,23 @@ +import * as rlp from 'rlp' +import { BigNumber } from 'ethers' +import { Account, BN } from 'ethereumjs-util' +import { toHexString, fromHexString } from '@eth-optimism/core-utils' + +export const encodeAccountState = (state: Partial): Buffer => { + return new Account( + new BN(state.nonce), + new BN(state.balance.toNumber()), + fromHexString(state.storageRoot), + fromHexString(state.codeHash) + ).serialize() +} + +export const decodeAccountState = (state: Buffer): any => { + const account = Account.fromRlpSerializedAccount(state) + return { + nonce: account.nonce.toNumber(), + balance: BigNumber.from(account.nonce.toNumber()), + storageRoot: toHexString(account.stateRoot), + codeHash: toHexString(account.codeHash), + } +} diff --git a/packages/fraud-prover/src/utils/hex-utils.ts b/packages/fraud-prover/src/utils/hex-utils.ts new file mode 100644 index 0000000000000..ce2ae7b7c2bbf --- /dev/null +++ b/packages/fraud-prover/src/utils/hex-utils.ts @@ -0,0 +1,81 @@ +import { BigNumber } from 'ethers' +import { toHexString, fromHexString } from '@eth-optimism/core-utils' + +export const toUnpaddedHexString = (buf: Buffer | string | number): string => { + // prettier-ignore + const hex = + '0x' + + toHexString(buf) + .slice(2) + .replace(/^0+/, '') + + if (hex === '0x') { + return '0x0' + } else { + return hex + } +} + +export const toStrippedHexString = (buf: Buffer | string | number): string => { + const hex = toUnpaddedHexString(buf).slice(2) + + if (hex === '0') { + return '0x' + } else if (hex.length % 2 === 1) { + return '0x' + '0' + hex + } else { + return '0x' + hex + } +} + +export const toBytes32 = (buf: Buffer | string | number): string => { + return toBytesN(buf, 32) +} + +export const toBytesN = (buf: Buffer | string | number, n: number): string => { + return ( + '0x' + + toHexString(buf) + .slice(2) + .padStart(n * 2, '0') + ) +} + +export const toUint256 = (num: number): string => { + return toUintN(num, 32) +} + +export const toUint8 = (num: number): string => { + return toUintN(num, 1) +} + +export const toUintN = (num: number, n: number): string => { + return ( + '0x' + + BigNumber.from(num) + .toHexString() + .slice(2) + .padStart(n * 2, '0') + ) +} + +/* + +already in @eth-optimism/core-utils + +export const fromHexString = (buf: Buffer | string): Buffer => { + if (typeof buf === 'string' && buf.startsWith('0x')) { + return Buffer.from(buf.slice(2), 'hex') + } + + return Buffer.from(buf) +} + +export const toHexString = (buf: Buffer | string | number | null): string => { + if (typeof buf === 'number') { + return BigNumber.from(buf).toHexString() + } else { + return '0x' + fromHexString(buf).toString('hex') + } +} +*/ diff --git a/packages/fraud-prover/src/utils/index.ts b/packages/fraud-prover/src/utils/index.ts new file mode 100644 index 0000000000000..e9d88892ada60 --- /dev/null +++ b/packages/fraud-prover/src/utils/index.ts @@ -0,0 +1,8 @@ +export * from './providers' +export * from './common' +export * from './constants' +export * from './eth-utils' +export * from './hex-utils' +export * from './ovm-contracts' +export * from './ovm-utils' +export * from './trie-utils' diff --git a/packages/fraud-prover/src/utils/ovm-contracts.ts b/packages/fraud-prover/src/utils/ovm-contracts.ts new file mode 100644 index 0000000000000..4850a8794adcc --- /dev/null +++ b/packages/fraud-prover/src/utils/ovm-contracts.ts @@ -0,0 +1,45 @@ +import { Contract, providers } from 'ethers' +import { getContractInterface } from '@eth-optimism/contracts' + +import { ZERO_ADDRESS } from './constants' + +export const loadContract = ( + name: string, + address: string, + provider: providers.JsonRpcProvider +): Contract => { + return new Contract(address, getContractInterface(name) as any, provider) +} + +export const loadContractFromManager = async ( + name: string, + Lib_AddressManager: Contract, + provider: providers.JsonRpcProvider +): Promise => { + const address = await Lib_AddressManager.getAddress(name) + + if (address === ZERO_ADDRESS) { + throw new Error( + `Lib_AddressManager does not have a record for a contract named: ${name}` + ) + } + + return loadContract(name, address, provider) +} + +export const loadProxyFromManager = async ( + name: string, + proxy: string, + Lib_AddressManager: Contract, + provider: providers.JsonRpcProvider +): Promise => { + const address = await Lib_AddressManager.getAddress(proxy) + + if (address === ZERO_ADDRESS) { + throw new Error( + `Lib_AddressManager does not have a record for a contract named: ${proxy}` + ) + } + + return loadContract(name, address, provider) +} diff --git a/packages/fraud-prover/src/utils/ovm-utils.ts b/packages/fraud-prover/src/utils/ovm-utils.ts new file mode 100644 index 0000000000000..e2043473b5802 --- /dev/null +++ b/packages/fraud-prover/src/utils/ovm-utils.ts @@ -0,0 +1,23 @@ +import { ethers } from 'ethers' + +import { OvmTransaction } from '../types' +import { toUint256, toUint8 } from './hex-utils' +import { toHexString, fromHexString } from '@eth-optimism/core-utils' + +export const encodeOvmTransaction = (transaction: OvmTransaction): string => { + return toHexString( + Buffer.concat([ + fromHexString(toUint256(transaction.timestamp)), + fromHexString(toUint256(transaction.blockNumber)), + fromHexString(toUint8(transaction.l1QueueOrigin)), + fromHexString(transaction.l1TxOrigin), + fromHexString(transaction.entrypoint), + fromHexString(toUint256(transaction.gasLimit)), + fromHexString(transaction.data), + ]) + ) +} + +export const hashOvmTransaction = (transaction: OvmTransaction): string => { + return ethers.utils.keccak256(encodeOvmTransaction(transaction)) +} diff --git a/packages/fraud-prover/src/utils/providers/index.ts b/packages/fraud-prover/src/utils/providers/index.ts new file mode 100644 index 0000000000000..ce1226ee0b424 --- /dev/null +++ b/packages/fraud-prover/src/utils/providers/index.ts @@ -0,0 +1,2 @@ +export * from './l1-provider-wrapper' +export * from './l2-provider-wrapper' diff --git a/packages/fraud-prover/src/utils/providers/l1-provider-wrapper.ts b/packages/fraud-prover/src/utils/providers/l1-provider-wrapper.ts new file mode 100644 index 0000000000000..fd3d615eed6f7 --- /dev/null +++ b/packages/fraud-prover/src/utils/providers/l1-provider-wrapper.ts @@ -0,0 +1,432 @@ +import { ethers, Event, Contract, BigNumber, providers } from 'ethers' +import { MerkleTree } from 'merkletreejs' + +import { + StateRootBatchHeader, + StateRootBatchProof, + TransactionBatchHeader, + TransactionBatchProof, + TransactionChainElement, + OvmTransaction, +} from '../../types' + +import { fromHexString, toHexString } from '@eth-optimism/core-utils' + +export class L1ProviderWrapper { + private eventCache: { + [topic: string]: { + startingBlockNumber: number + events: ethers.Event[] + } + } = {} + + constructor( + public provider: providers.JsonRpcProvider, + public OVM_StateCommitmentChain: Contract, + public OVM_CanonicalTransactionChain: Contract, + public OVM_ExecutionManager: Contract, + public l1StartOffset: number, + public l1BlockFinality: number + ) {} + + public async findAllEvents( + contract: Contract, + filter: ethers.EventFilter, + fromBlock?: number + ): Promise { + const cache = this.eventCache[filter.topics[0] as string] || { + startingBlockNumber: fromBlock || this.l1StartOffset, + events: [], + } + + let events: ethers.Event[] = [] + let startingBlockNumber = cache.startingBlockNumber + let latestL1BlockNumber = await this.provider.getBlockNumber() + while (startingBlockNumber < latestL1BlockNumber) { + events = events.concat( + await contract.queryFilter( + filter, + startingBlockNumber, + Math.min( + startingBlockNumber + 2000, + latestL1BlockNumber - this.l1BlockFinality + ) + ) + ) + + if (startingBlockNumber + 2000 > latestL1BlockNumber) { + cache.startingBlockNumber = latestL1BlockNumber + cache.events = cache.events.concat(events) + break + } + + startingBlockNumber += 2000 + latestL1BlockNumber = await this.provider.getBlockNumber() + } + + this.eventCache[filter.topics[0] as string] = cache + + return cache.events + } + + public async getStateRootBatchHeader( + index: number + ): Promise { + const event = await this._getStateRootBatchEvent(index) + + if (!event) { + return + } + + return { + batchIndex: event.args._batchIndex, + batchRoot: event.args._batchRoot, + batchSize: event.args._batchSize, + prevTotalElements: event.args._prevTotalElements, + extraData: event.args._extraData, + } + } + + public async getStateRoot(index: number): Promise { + const stateRootBatchHeader = await this.getStateRootBatchHeader(index) + if (stateRootBatchHeader === undefined) { + return + } + + const batchStateRoots = await this.getBatchStateRoots(index) + + return batchStateRoots[ + index - stateRootBatchHeader.prevTotalElements.toNumber() + ] + } + + public async getBatchStateRoots(index: number): Promise { + const event = await this._getStateRootBatchEvent(index) + + if (!event) { + return + } + + const transaction = await this.provider.getTransaction( + event.transactionHash + ) + + const [ + stateRoots, + ] = this.OVM_StateCommitmentChain.interface.decodeFunctionData( + 'appendStateBatch', + transaction.data + ) + + return stateRoots + } + + public async getStateRootBatchProof( + index: number + ): Promise { + const batchHeader = await this.getStateRootBatchHeader(index) + const stateRoots = await this.getBatchStateRoots(index) + + const elements = [] + for ( + let i = 0; + i < Math.pow(2, Math.ceil(Math.log2(stateRoots.length))); + i++ + ) { + if (i < stateRoots.length) { + elements.push(stateRoots[i]) + } else { + elements.push(ethers.utils.keccak256('0x' + '00'.repeat(32))) + } + } + + const hash = (el: Buffer | string): Buffer => { + return Buffer.from(ethers.utils.keccak256(el).slice(2), 'hex') + } + + // State roots aren't hashed since they're already bytes32 + const leaves = elements.map((element) => { + return fromHexString(element) + }) + + const tree = new MerkleTree(leaves, hash) + const batchIndex = index - batchHeader.prevTotalElements.toNumber() + const treeProof = tree + .getProof(leaves[batchIndex], batchIndex) + .map((element) => { + return element.data + }) + + return { + stateRoot: stateRoots[batchIndex], + stateRootBatchHeader: batchHeader, + stateRootProof: { + index: batchIndex, + siblings: treeProof, + }, + } + } + + public async getTransactionBatchHeader( + index: number + ): Promise { + const event = await this._getTransactionBatchEvent(index) + + if (!event) { + return + } + + return { + batchIndex: event.args._batchIndex, + batchRoot: event.args._batchRoot, + batchSize: event.args._batchSize, + prevTotalElements: event.args._prevTotalElements, + extraData: event.args._extraData, + } + } + + public async getBatchTransactions( + index: number + ): Promise< + { + transaction: OvmTransaction + transactionChainElement: TransactionChainElement + }[] + > { + const event = await this._getTransactionBatchEvent(index) + + if (!event) { + return + } + + const emGasLimit = await this.OVM_ExecutionManager.getMaxTransactionGasLimit() + + const transaction = await this.provider.getTransaction( + event.transactionHash + ) + + if ((event as any).isSequencerBatch) { + const transactions = [] + const txdata = fromHexString(transaction.data) + const shouldStartAtBatch = BigNumber.from(txdata.slice(4, 9)) + const totalElementsToAppend = BigNumber.from(txdata.slice(9, 12)) + const numContexts = BigNumber.from(txdata.slice(12, 15)) + + let nextTxPointer = 15 + 16 * numContexts.toNumber() + for (let i = 0; i < numContexts.toNumber(); i++) { + const contextPointer = 15 + 16 * i + const context = { + numSequencedTransactions: BigNumber.from( + txdata.slice(contextPointer, contextPointer + 3) + ), + numSubsequentQueueTransactions: BigNumber.from( + txdata.slice(contextPointer + 3, contextPointer + 6) + ), + ctxTimestamp: BigNumber.from( + txdata.slice(contextPointer + 6, contextPointer + 11) + ), + ctxBlockNumber: BigNumber.from( + txdata.slice(contextPointer + 11, contextPointer + 16) + ), + } + + for (let j = 0; j < context.numSequencedTransactions.toNumber(); j++) { + const txDataLength = BigNumber.from( + txdata.slice(nextTxPointer, nextTxPointer + 3) + ) + const txData = txdata.slice( + nextTxPointer + 3, + nextTxPointer + 3 + txDataLength.toNumber() + ) + + transactions.push({ + transaction: { + blockNumber: context.ctxBlockNumber.toNumber(), + timestamp: context.ctxTimestamp.toNumber(), + gasLimit: emGasLimit, + entrypoint: '0x4200000000000000000000000000000000000005', + l1TxOrigin: '0x' + '00'.repeat(20), + l1QueueOrigin: 0, + data: toHexString(txData), + }, + transactionChainElement: { + isSequenced: true, + queueIndex: 0, + timestamp: context.ctxTimestamp.toNumber(), + blockNumber: context.ctxBlockNumber.toNumber(), + txData: toHexString(txData), + }, + }) + + nextTxPointer += 3 + txDataLength.toNumber() + } + } + + return transactions + } else { + return [] + } + } + + public async getTransactionBatchProof( + index: number + ): Promise { + const batchHeader = await this.getTransactionBatchHeader(index) + const transactions = await this.getBatchTransactions(index) + + const elements = [] + for ( + let i = 0; + i < Math.pow(2, Math.ceil(Math.log2(transactions.length))); + i++ + ) { + if (i < transactions.length) { + // TODO: FIX + const tx = transactions[i] + elements.push( + `0x01${BigNumber.from(tx.transaction.timestamp) + .toHexString() + .slice(2) + .padStart(64, '0')}${BigNumber.from(tx.transaction.blockNumber) + .toHexString() + .slice(2) + .padStart(64, '0')}${tx.transaction.data.slice(2)}` + ) + } else { + elements.push('0x' + '00'.repeat(32)) + } + } + + const hash = (el: Buffer | string): Buffer => { + return Buffer.from(ethers.utils.keccak256(el).slice(2), 'hex') + } + + const leaves = elements.map((element) => { + return hash(element) + }) + + const tree = new MerkleTree(leaves, hash) + const batchIndex = index - batchHeader.prevTotalElements.toNumber() + const treeProof = tree + .getProof(leaves[batchIndex], batchIndex) + .map((element) => { + return element.data + }) + + return { + transaction: transactions[batchIndex].transaction, + transactionChainElement: transactions[batchIndex].transactionChainElement, + transactionBatchHeader: batchHeader, + transactionProof: { + index: batchIndex, + siblings: treeProof, + }, + } + } + + private async _getStateRootBatchEvent(index: number): Promise { + const events = await this.findAllEvents( + this.OVM_StateCommitmentChain, + this.OVM_StateCommitmentChain.filters.StateBatchAppended() + ) + + if (events.length === 0) { + return + } + + const matching = events.filter((event) => { + return ( + event.args._prevTotalElements.toNumber() <= index && + event.args._prevTotalElements.toNumber() + + event.args._batchSize.toNumber() > + index + ) + }) + + const deletions = await this.findAllEvents( + this.OVM_StateCommitmentChain, + this.OVM_StateCommitmentChain.filters.StateBatchDeleted() + ) + + const results: ethers.Event[] = [] + for (const event of matching) { + const wasDeleted = deletions.some((deletion) => { + return ( + deletion.blockNumber > event.blockNumber && + deletion.args._batchIndex.toNumber() === + event.args._batchIndex.toNumber() + ) + }) + + if (!wasDeleted) { + results.push(event) + } + } + + if (results.length === 0) { + return + } + + if (results.length > 2) { + throw new Error( + `Found more than one batch header for the same state root, this shouldn't happen.` + ) + } + + return results[results.length - 1] + } + + private async _getTransactionBatchEvent( + index: number + ): Promise { + const events = await this.findAllEvents( + this.OVM_CanonicalTransactionChain, + this.OVM_CanonicalTransactionChain.filters.TransactionBatchAppended() + ) + + if (events.length === 0) { + return + } + + // tslint:disable-next-line + const event = events.find((event) => { + return ( + event.args._prevTotalElements.toNumber() <= index && + event.args._prevTotalElements.toNumber() + + event.args._batchSize.toNumber() > + index + ) + }) + + if (!event) { + return + } + + const batchSubmissionEvents = await this.findAllEvents( + this.OVM_CanonicalTransactionChain, + this.OVM_CanonicalTransactionChain.filters.SequencerBatchAppended() + ) + + if (batchSubmissionEvents.length === 0) { + ;(event as any).isSequencerBatch = false + } else { + // tslint:disable-next-line + const batchSubmissionEvent = batchSubmissionEvents.find((event) => { + return ( + event.args._startingQueueIndex.toNumber() <= index && + event.args._startingQueueIndex.toNumber() + + event.args._totalElements.toNumber() > + index + ) + }) + + if (batchSubmissionEvent) { + ;(event as any).isSequencerBatch = true + } else { + ;(event as any).isSequencerBatch = false + } + } + + return event as any + } +} diff --git a/packages/fraud-prover/src/utils/providers/l2-provider-wrapper.ts b/packages/fraud-prover/src/utils/providers/l2-provider-wrapper.ts new file mode 100644 index 0000000000000..97d07e5ac4bb7 --- /dev/null +++ b/packages/fraud-prover/src/utils/providers/l2-provider-wrapper.ts @@ -0,0 +1,58 @@ +//import { JsonRpcProvider } from '@ethersproject/providers' + +import { providers } from 'ethers' +import { StateDiffProof } from '../../types' +import { toUnpaddedHexString } from '../hex-utils' + +export class L2ProviderWrapper { + constructor(public provider: providers.JsonRpcProvider) {} + + public async getStateRoot(index: number): Promise { + const block = await this.provider.send('eth_getBlockByNumber', [ + toUnpaddedHexString(index), + false, + ]) + return block.stateRoot + } + + public async getTransaction(index: number): Promise { + const transaction = await this.provider.send( + 'eth_getTransactionByBlockNumberAndIndex', + [toUnpaddedHexString(index), '0x0'] + ) + + return transaction.input + } + + public async getProof( + index: number, + address: string, + slots: string[] = [] + ): Promise { + return this.provider.send('eth_getProof', [ + address, + slots, + toUnpaddedHexString(index), + ]) + } + + public async getStateDiffProof(index: number): Promise { + const proof = await this.provider.send('eth_getStateDiffProof', [ + toUnpaddedHexString(index), + ]) + + return { + header: proof.header, + accountStateProofs: proof.accounts, + } + } + + public async getRollupInfo(): Promise { + return this.provider.send('rollup_getInfo', []) + } + + public async getAddressManagerAddress(): Promise { + const rollupInfo = await this.getRollupInfo() + return rollupInfo.addresses.addressResolver + } +} diff --git a/packages/fraud-prover/src/utils/trie-utils.ts b/packages/fraud-prover/src/utils/trie-utils.ts new file mode 100644 index 0000000000000..8e954799e70e8 --- /dev/null +++ b/packages/fraud-prover/src/utils/trie-utils.ts @@ -0,0 +1,31 @@ +import { BaseTrie } from 'merkle-patricia-tree' +import { fromHexString } from '@eth-optimism/core-utils' + +export const makeTrieFromProofs = (proofs: string[][]): Promise => { + if ( + proofs.length === 0 || + proofs.every((proof) => { + return proof.length === 0 + }) + ) { + return BaseTrie.fromProof([]) + } + + const nodes = proofs.reduce( + // tslint:disable-next-line + (nodes, proof) => { + if (proof.length > 1) { + return nodes.concat(proof.slice(1)) + } else { + return nodes + } + }, + [proofs[0][0]] + ) + + return BaseTrie.fromProof( + nodes.map((node) => { + return fromHexString(node) + }) + ) +} diff --git a/packages/fraud-prover/tsconfig.build.json b/packages/fraud-prover/tsconfig.build.json new file mode 100644 index 0000000000000..7ade983395d0b --- /dev/null +++ b/packages/fraud-prover/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + + "include": [ + "src/**/*" + ] +} diff --git a/packages/fraud-prover/tsconfig.json b/packages/fraud-prover/tsconfig.json new file mode 100644 index 0000000000000..17100a25bcda4 --- /dev/null +++ b/packages/fraud-prover/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "resolveJsonModule": true + } +} diff --git a/packages/fraud-prover/tslint.json b/packages/fraud-prover/tslint.json new file mode 100644 index 0000000000000..6587c444f045f --- /dev/null +++ b/packages/fraud-prover/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tslint.base.json" +} diff --git a/yarn.lock b/yarn.lock index a8f182b80cbd4..07d5a890722e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -305,6 +305,76 @@ patch-package "^6.2.2" postinstall-postinstall "^2.1.0" +"@ethereumjs/block@^3.2.0", "@ethereumjs/block@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-3.2.1.tgz#c24c345e6dd6299efa4bed40979280b7dda96d3a" + integrity sha512-FCxo5KwwULne2A2Yuae4iaGGqSsRjwzXOlDhGalOFiBbLfP3hE04RHaHGw4c8vh1PfOrLauwi0dQNUBkOG3zIA== + dependencies: + "@ethereumjs/common" "^2.2.0" + "@ethereumjs/tx" "^3.1.3" + ethereumjs-util "^7.0.10" + merkle-patricia-tree "^4.1.0" + +"@ethereumjs/blockchain@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-5.2.1.tgz#83ed83647667265f1666f111caf065ef9d1e82b5" + integrity sha512-+hshP2qSOOFsiYvZCbaDQFG7jYTWafE8sfBi+pAsdhAHfP7BN7VLyob7qoQISgwS1s7NTR4c4+2t/woU9ahItw== + dependencies: + "@ethereumjs/block" "^3.2.0" + "@ethereumjs/common" "^2.2.0" + "@ethereumjs/ethash" "^1.0.0" + debug "^2.2.0" + ethereumjs-util "^7.0.9" + level-mem "^5.0.1" + lru-cache "^5.1.1" + rlp "^2.2.4" + semaphore-async-await "^1.5.1" + +"@ethereumjs/common@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-2.2.0.tgz#850a3e3e594ee707ad8d44a11e8152fb62450535" + integrity sha512-PyQiTG00MJtBRkJmv46ChZL8u2XWxNBeAthznAUIUiefxPAXjbkuiCZOuncgJS34/XkMbNc9zMt/PlgKRBElig== + dependencies: + crc-32 "^1.2.0" + ethereumjs-util "^7.0.9" + +"@ethereumjs/ethash@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/ethash/-/ethash-1.0.0.tgz#4e77f85b37be1ade5393e8719bdabac3e796ddaa" + integrity sha512-iIqnGG6NMKesyOxv2YctB2guOVX18qMAWlj3QlZyrc+GqfzLqoihti+cVNQnyNxr7eYuPdqwLQOFuPe6g/uKjw== + dependencies: + "@types/levelup" "^4.3.0" + buffer-xor "^2.0.1" + ethereumjs-util "^7.0.7" + miller-rabin "^4.0.0" + +"@ethereumjs/tx@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.1.3.tgz#0e4b0ccec2f12b1f0bbbb0e7542dd79d9ec25d87" + integrity sha512-DJBu6cbwYtiPTFeCUR8DF5p+PF0jxs+0rALJZiEcTz2tiRPIEkM72GEbrkGuqzENLCzBrJHT43O0DxSYTqeo+g== + dependencies: + "@ethereumjs/common" "^2.2.0" + ethereumjs-util "^7.0.10" + +"@ethereumjs/vm@^5.3.2": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-5.3.2.tgz#b4d83a3d50a7ad22d6d412cc21bbde221b3e2871" + integrity sha512-QmCUQrW6xbhgEbQh9njue4kAJdM056C+ytBFUTF/kDYa3kNDm4Qxp9HUyTlt1OCSXvDhws0qqlh8+q+pmXpN7g== + dependencies: + "@ethereumjs/block" "^3.2.1" + "@ethereumjs/blockchain" "^5.2.1" + "@ethereumjs/common" "^2.2.0" + "@ethereumjs/tx" "^3.1.3" + async-eventemitter "^0.2.4" + core-js-pure "^3.0.1" + debug "^2.2.0" + ethereumjs-util "^7.0.10" + functional-red-black-tree "^1.0.1" + mcl-wasm "^0.7.1" + merkle-patricia-tree "^4.1.0" + rustbn.js "~0.2.0" + util.promisify "^1.0.1" + "@ethersproject/abi@5.0.0-beta.153": version "5.0.0-beta.153" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.0-beta.153.tgz#43a37172b33794e4562999f6e2d555b7599a8eee" @@ -1479,7 +1549,7 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@nomiclabs/ethereumjs-vm@4.2.2": +"@nomiclabs/ethereumjs-vm@4.2.2", "@nomiclabs/ethereumjs-vm@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q== @@ -2720,7 +2790,7 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -async-eventemitter@^0.2.2: +async-eventemitter@^0.2.2, async-eventemitter@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/async-eventemitter/-/async-eventemitter-0.2.4.tgz#f5e7c8ca7d3e46aab9ec40a292baf686a0bafaca" integrity sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw== @@ -4382,6 +4452,14 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +crc-32@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" + integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -5485,7 +5563,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.8: +ethereumjs-util@^7.0.10, ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.7, ethereumjs-util@^7.0.8, ethereumjs-util@^7.0.9: version "7.0.10" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz#5fb7b69fa1fda0acc59634cf39d6b0291180fc1f" integrity sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw== @@ -5727,6 +5805,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -6736,6 +6819,57 @@ hardhat@^2.0.8, hardhat@^2.0.9, hardhat@^2.1.1, hardhat@^2.1.2: uuid "^3.3.2" ws "^7.2.1" +hardhat@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.2.1.tgz#bef0031b994e3f60a88d428f2097195c58cf9ed2" + integrity sha512-8s7MtGXdh0NDwQKdlA8m8QdloVIN1+hv5aFpn0G5Ljj9vfNY9kUoc0a9pMboeGbd9WrS+XrZs5YlsPgQjaW/Tg== + dependencies: + "@ethereumjs/block" "^3.2.1" + "@ethereumjs/blockchain" "^5.2.1" + "@ethereumjs/common" "^2.2.0" + "@ethereumjs/tx" "^3.1.3" + "@ethereumjs/vm" "^5.3.2" + "@sentry/node" "^5.18.1" + "@solidity-parser/parser" "^0.11.0" + "@types/bn.js" "^5.1.0" + "@types/lru-cache" "^5.1.0" + abort-controller "^3.0.0" + adm-zip "^0.4.16" + ansi-escapes "^4.3.0" + chalk "^2.4.2" + chokidar "^3.4.0" + ci-info "^2.0.0" + debug "^4.1.1" + enquirer "^2.3.0" + env-paths "^2.2.0" + eth-sig-util "^2.5.2" + ethereum-cryptography "^0.1.2" + ethereumjs-abi "^0.6.8" + ethereumjs-util "^7.0.10" + find-up "^2.1.0" + fp-ts "1.19.3" + fs-extra "^7.0.1" + glob "^7.1.3" + immutable "^4.0.0-rc.12" + io-ts "1.10.4" + lodash "^4.17.11" + merkle-patricia-tree "^4.1.0" + mnemonist "^0.38.0" + mocha "^7.1.2" + node-fetch "^2.6.0" + qs "^6.7.0" + raw-body "^2.4.1" + resolve "1.17.0" + semver "^6.3.0" + slash "^3.0.0" + solc "0.7.3" + source-map-support "^0.5.13" + stacktrace-parser "^0.1.10" + "true-case-path" "^2.2.1" + tsort "0.0.1" + uuid "^3.3.2" + ws "^7.2.1" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -8398,6 +8532,11 @@ match-all@^1.2.6: resolved "https://registry.yarnpkg.com/match-all/-/match-all-1.2.6.tgz#66d276ad6b49655551e63d3a6ee53e8be0566f8d" integrity sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ== +mcl-wasm@^0.7.1: + version "0.7.6" + resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.6.tgz#c1789ebda5565d49b77d2ee195ff3e4d282f1554" + integrity sha512-cbRl3sUOkBeRY2hsM4t1EIln2TIdQBkSiTOqNTv/4Hu5KOECnMWCgjIf+a9Ebunyn22VKqkMF3zj6ejRzz7YBw== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -8554,7 +8693,7 @@ merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: rlp "^2.0.0" semaphore ">=1.0.1" -merkle-patricia-tree@^4.0.0: +merkle-patricia-tree@^4.0.0, merkle-patricia-tree@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/merkle-patricia-tree/-/merkle-patricia-tree-4.1.0.tgz#010636c4cfd68682df33a2e3186b7d0be7b98b9d" integrity sha512-vmP1J7FwIpprFMVjjSMM1JAwFce85Q+tp0TYIedYv8qaMh2oLUZ3ETXn9wbgi9S6elySzKzGa+Ai6VNKGEwSlg== @@ -10177,15 +10316,20 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier@2.2.1, prettier@^2.1.2, prettier@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" + integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== + prettier@^1.14.2, prettier@^1.16.4, prettier@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -prettier@^2.1.2, prettier@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +printj@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== private@^0.1.6, private@^0.1.8: version "0.1.8" @@ -12605,7 +12749,7 @@ util-promisify@^2.1.0: dependencies: object.getownpropertydescriptors "^2.0.3" -util.promisify@^1.0.0: +util.promisify@^1.0.0, util.promisify@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.1.1.tgz#77832f57ced2c9478174149cae9b96e9918cd54b" integrity sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==