diff --git a/.changeset/chilled-books-grab.md b/.changeset/chilled-books-grab.md deleted file mode 100644 index 10efc8f70022..000000000000 --- a/.changeset/chilled-books-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@eth-optimism/l2geth': patch ---- - -Fixes incorrect type parsing in the RollupClient. The gasLimit became greater than the largest safe JS number so it needs to be represented as a string diff --git a/.changeset/dull-fishes-type.md b/.changeset/dull-fishes-type.md deleted file mode 100644 index 3a2e75a32a95..000000000000 --- a/.changeset/dull-fishes-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@eth-optimism/data-transport-layer': patch ---- - -Represent gaslimit as a string to avoid an overflow diff --git a/.changeset/lazy-toes-teach.md b/.changeset/lazy-toes-teach.md new file mode 100644 index 000000000000..3c482f92504d --- /dev/null +++ b/.changeset/lazy-toes-teach.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/l2geth': patch +--- + +Fixes an off-by-one error that would sometimes break replica syncing when stopping and restarting geth. diff --git a/.changeset/neat-melons-lie.md b/.changeset/neat-melons-lie.md deleted file mode 100644 index 833a9b34d248..000000000000 --- a/.changeset/neat-melons-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@eth-optimism/smock': patch ---- - -Fix a bug where overloaded functions would not be handled correctly diff --git a/.changeset/nervous-bobcats-grow.md b/.changeset/nervous-bobcats-grow.md deleted file mode 100644 index fc963b069701..000000000000 --- a/.changeset/nervous-bobcats-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@eth-optimism/l2geth": patch ---- - -Refactor the SyncService to more closely implement the specification. This includes using query params to select the backend from the DTL, trailing syncing of batches for the sequencer, syncing by batches as the verifier as well as unified code paths for transaction ingestion to prevent double ingestion or missed ingestion diff --git a/.changeset/ten-pumas-perform.md b/.changeset/ten-pumas-perform.md new file mode 100644 index 000000000000..e40a6484c396 --- /dev/null +++ b/.changeset/ten-pumas-perform.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/l2geth': patch +--- + +Correctly log 'end of OVM execution' message. diff --git a/.changeset/unlucky-shoes-bake.md b/.changeset/unlucky-shoes-bake.md deleted file mode 100644 index 7b24244798b9..000000000000 --- a/.changeset/unlucky-shoes-bake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@eth-optimism/integration-tests': patch ---- - -Add verifier sync test and extra docker-compose functions diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..dfdb8b771ce0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index feb1c407fe80..924c601c1677 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,14 +1,18 @@ -l2geth/ @smartcontracts @tynes @karlfloersch -packages/specs/l2geth/ @smartcontracts @tynes @karlfloersch -packages/contracts/ @smartcontracts @ben-chain @maurelian @elenadimitrova -packages/specs/protocol/ @smartcontracts @ben-chain @maurelian -ops/ @tynes @karlfloersch -packages/hardhat-ovm/ @smartcontracts -packages/smock/ @smartcontracts @maurelian -packages/core-utils/ @smartcontracts @annieke @ben-chain -packages/common-ts/ @annieke -packages/core-utils/src/watcher.ts @K-Ho -packages/message-relayer/ @K-Ho -packages/batch-submitter/ @annieke @karlfloersch -packages/data-transport-layer/ @annieke -integration-tests/ @tynes +# CODEOWNERS can be disruptive because it automatically requests review from individuals across the +# board. We still like to use this file to track who's working on what, but all lines are commented +# out so that GitHub won't trigger review requests. + +# l2geth/ @smartcontracts @tynes @karlfloersch +# packages/specs/l2geth/ @smartcontracts @tynes @karlfloersch +# packages/contracts/ @smartcontracts @ben-chain @maurelian @elenadimitrova +# packages/specs/protocol/ @smartcontracts @ben-chain @maurelian +# ops/ @tynes @karlfloersch +# packages/hardhat-ovm/ @smartcontracts +# packages/smock/ @smartcontracts @maurelian +# packages/core-utils/ @smartcontracts @annieke @ben-chain +# packages/common-ts/ @annieke +# packages/core-utils/src/watcher.ts @K-Ho +# packages/message-relayer/ @K-Ho +# packages/batch-submitter/ @annieke @karlfloersch +# packages/data-transport-layer/ @annieke +# integration-tests/ @tynes diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index f9ba9f19c20b..2e4697184a05 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -93,3 +93,21 @@ jobs: yarn compile yarn compile:ovm yarn test:integration:ovm + + - name: Collect docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v1 + with: + images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth,ethereumoptimism/integration-tests' + dest: './logs' + + - name: Tar logs + if: failure() + run: tar cvzf ./logs.tgz ./logs + + - name: Upload logs to GitHub + if: failure() + uses: actions/upload-artifact@master + with: + name: logs.tgz + path: ./logs.tgz \ No newline at end of file diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml index 67e53a6b54c9..d5cb35ddb93f 100644 --- a/.github/workflows/publish-canary.yml +++ b/.github/workflows/publish-canary.yml @@ -2,7 +2,12 @@ name: Publish Packages (canary) on: # enable users to manually trigger with workflow_dispatch - workflow_dispatch: {} + workflow_dispatch: + inputs: + customImageName: + description: 'Custom Docker Image Tag (keep empty for git hash)' + required: false + default: '0.0.0-rc-0' jobs: canary-publish: @@ -66,6 +71,18 @@ jobs: run: | node ops/scripts/ci-versions.js ${{ toJSON(steps.changesets.outputs.publishedPackages) }} + - name: Docker Image Name + id: docker_image_name + run: | + if [ $CUSTOM_IMAGE_NAME == '' ] + then + echo "::set-output name=canary-docker-tag::${GITHUB_SHA::8}" + else + echo "::set-output name=canary-docker-tag::prerelease-$CUSTOM_IMAGE_NAME" + fi + env: + CUSTOM_IMAGE_NAME: ${{ github.event.inputs.customImageName }} + # The below code is duplicated, would be ideal if we could use a matrix with a # key/value being dynamically generated from the `publishedPackages` output @@ -155,7 +172,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.message-relayer push: true - tags: ethereumoptimism/message-relayer:${{ needs.builder.outputs.message-relayer }} + tags: ethereumoptimism/message-relayer:${{ steps.docker_image_name.outputs.canary-docker-tag }} batch-submitter: name: Publish Batch Submitter Version ${{ needs.builder.outputs.batch-submitter }} @@ -181,7 +198,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.batch-submitter push: true - tags: ethereumoptimism/batch-submitter:${{ needs.builder.outputs.batch-submitter }} + tags: ethereumoptimism/batch-submitter:${{ steps.docker_image_name.outputs.canary-docker-tag }} data-transport-layer: name: Publish Data Transport Layer Version ${{ needs.builder.outputs.data-transport-layer }} @@ -207,7 +224,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.data-transport-layer push: true - tags: ethereumoptimism/data-transport-layer:${{ needs.builder.outputs.data-transport-layer }} + tags: ethereumoptimism/data-transport-layer:${{ steps.docker_image_name.outputs.canary-docker-tag }} contracts: name: Publish Deployer Version ${{ needs.builder.outputs.contracts }} @@ -233,7 +250,7 @@ jobs: context: . file: ./ops/docker/Dockerfile.deployer push: true - tags: ethereumoptimism/deployer:${{ needs.builder.outputs.contracts }} + tags: ethereumoptimism/deployer:${{ steps.docker_image_name.outputs.canary-docker-tag }} integration_tests: name: Publish Integration tests ${{ needs.builder.outputs.integration-tests }} @@ -259,4 +276,4 @@ jobs: context: . file: ./ops/docker/Dockerfile.integration-tests push: true - tags: ethereumoptimism/integration-tests:${{ needs.builder.outputs.integration-tests }} + tags: ethereumoptimism/integration-tests:${{ steps.docker_image_name.outputs.canary-docker-tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2bdc59c027b4..65bfc5216d1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,6 +92,14 @@ jobs: push: true tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }} + - name: Publish rpc-proxy + uses: docker/build-push-action@v2 + with: + context: . + file: ./ops/docker/Dockerfile.rpc-proxy + push: true + tags: ethereumoptimism/rpc-proxy:${{ needs.release.outputs.l2geth }} + # pushes the base builder image to dockerhub builder: name: Prepare the base builder image for the services diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 000000000000..9eab9a0b3de9 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,61 @@ +name: Static analysis + +on: + push: + branches: + - master + - develop + pull_request: + workflow_dispatch: + +env: + PYTEST_ADDOPTS: "--color=yes" + +jobs: + slither: + name: Slither run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Fetch history + run: git fetch + + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: '12.x' + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install Dependencies + # only install dependencies if there was a change in the deps + # if: steps.yarn-cache.outputs.cache-hit != 'true' + run: yarn install + + - name: Build + run: yarn build + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install Slither + run: pip3 install slither-analyzer + + - name: Run analysis + working-directory: ./packages/contracts + shell: bash + run: yarn test:slither + continue-on-error: true diff --git a/.github/workflows/sync-tests.yml b/.github/workflows/sync-tests.yml new file mode 100644 index 000000000000..b70e12a9bdab --- /dev/null +++ b/.github/workflows/sync-tests.yml @@ -0,0 +1,44 @@ +name: sync-tests + +on: + push: + branches: + - 'master' + - 'develop' + - 'regenesis/*' + pull_request: + workflow_dispatch: + +jobs: + integration-sync-test: + runs-on: ubuntu-latest + env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + steps: + - uses: actions/checkout@v2 + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Build the services + working-directory: ./ops + run: ./scripts/build-ci.sh + + - name: Bring the stack up + working-directory: ./ops + run: docker-compose up -d && ./scripts/wait-for-sequencer.sh + + - name: Run the sync tests + working-directory: ./integration-tests + run: | + yarn + yarn test:sync diff --git a/.gitignore b/.gitignore index 18b731ead5fc..45c8671678a8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ cache-ovm l2geth/build/bin packages/contracts/deployments/custom packages/contracts/coverage* +packages/contracts/@ens* +packages/contracts/@openzeppelin* +packages/contracts/hardhat* packages/data-transport-layer/db diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..62df50f1eefe --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14.17.0 diff --git a/README.md b/README.md index d7b11ea584f2..8ed492095de5 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ Extensive documentation is available [here](http://community.optimism.io/docs/) * [`message-relayer`](./packages/message-relayer): Service for relaying L2 messages to L1 * [`l2geth`](./l2geth): Fork of [go-ethereum v1.9.10](https://github.com/ethereum/go-ethereum/tree/v1.9.10) implementing the [OVM](https://research.paradigm.xyz/optimism#optimistic-geth). * [`integration-tests`](./integration-tests): Integration tests between a L1 testnet, `l2geth`, -* [`ops`](./ops): Contains Dockerfiles for containerizing each service involved in the protocol, +* [`ops`](./ops): Contains Dockerfiles for containerizing each service involved in the protocol, as well as a docker-compose file for bringing up local testnets easily ## Quickstart ### Installation -Dependency management is done using `yarn`. +Dependency management is done using `yarn`. ```bash git clone git@github.com:ethereum-optimism/optimism.git @@ -67,7 +67,7 @@ you can run `yarn lerna run test --parallel --since master` #### Running the integration tests The integration tests first require bringing up the Optimism stack. This is done via -a Docker Compose network. For better performance, we also recommend enabling Docker +a Docker Compose network. For better performance, we also recommend enabling Docker BuildKit ```bash @@ -110,3 +110,11 @@ can be hard to filter through. In order to view the logs from a specific service ``` docker-compose logs --follow ``` +### Static analysis + +To run `slither` locally in `./packages/contracts` do + +``` +pip3 install slither-analyzer +yarn test:slither +``` diff --git a/examples/truffle/truffle-config-ovm.js b/examples/truffle/truffle-config-ovm.js index b5dc2736725c..b7b849d43ee4 100644 --- a/examples/truffle/truffle-config-ovm.js +++ b/examples/truffle/truffle-config-ovm.js @@ -17,6 +17,7 @@ module.exports = { host: '127.0.0.1', port: 8545, gasPrice: 0, + gas: 54180127, } }, compilers: { @@ -31,4 +32,4 @@ module.exports = { } } } -} \ No newline at end of file +} diff --git a/integration-tests/CHANGELOG.md b/integration-tests/CHANGELOG.md index 1d370a90264e..ea2181a934dd 100644 --- a/integration-tests/CHANGELOG.md +++ b/integration-tests/CHANGELOG.md @@ -1,5 +1,13 @@ # @eth-optimism/integration-tests +## 0.0.7 + +### Patch Changes + +- d1680052: Reduce test timeout from 100 to 20 seconds +- c2b6e14b: Implement the latest fee spec such that the L2 gas limit is scaled and the tx.gasPrice/tx.gasLimit show correctly in metamask +- 77108d37: Add verifier sync test and extra docker-compose functions + ## 0.0.6 ### Patch Changes diff --git a/integration-tests/hardhat.config.ts b/integration-tests/hardhat.config.ts index b68e8646ba7b..0fcc9b23325f 100644 --- a/integration-tests/hardhat.config.ts +++ b/integration-tests/hardhat.config.ts @@ -10,7 +10,7 @@ const enableGasReport = !!process.env.ENABLE_GAS_REPORT const config: HardhatUserConfig = { mocha: { - timeout: 100000, + timeout: 20000, }, networks: { optimism: { diff --git a/integration-tests/package.json b/integration-tests/package.json index 03629b169883..ca67edcc4fcf 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/integration-tests", - "version": "0.0.6", + "version": "0.0.7", "description": "[Optimism] Integration Tests", "private": true, "author": "Optimism PBC", @@ -17,8 +17,8 @@ "clean": "rimraf cache artifacts artifacts-ovm cache-ovm" }, "devDependencies": { - "@eth-optimism/contracts": "^0.3.4", - "@eth-optimism/core-utils": "^0.4.4", + "@eth-optimism/contracts": "^0.3.5", + "@eth-optimism/core-utils": "^0.4.5", "@eth-optimism/hardhat-ovm": "^0.2.2", "@ethersproject/providers": "^5.0.24", "@nomiclabs/hardhat-ethers": "^2.0.2", diff --git a/integration-tests/sync-tests/sync-verifier.spec.ts b/integration-tests/sync-tests/sync-verifier.spec.ts index c1fe1e7e40f1..2c931eb87c4f 100644 --- a/integration-tests/sync-tests/sync-verifier.spec.ts +++ b/integration-tests/sync-tests/sync-verifier.spec.ts @@ -60,7 +60,7 @@ describe('Syncing a verifier', () => { }) describe('Basic transactions', () => { - afterEach(async () => { + after(async () => { await verifier.stop('verifier') await verifier.rm() }) @@ -97,5 +97,13 @@ describe('Syncing a verifier', () => { latestSequencerBlock.stateRoot ) }) + + it('should have matching block data', async () => { + const sequencerTip = await sequencerProvider.getBlock('latest') + const verifierTip = await provider.getBlock('latest') + + expect(sequencerTip.number).to.deep.eq(verifierTip.number) + expect(sequencerTip.hash).to.deep.eq(verifierTip.hash) + }) }) }) diff --git a/integration-tests/test/erc20.spec.ts b/integration-tests/test/erc20.spec.ts index 925ec01e6f21..1c03476592a2 100644 --- a/integration-tests/test/erc20.spec.ts +++ b/integration-tests/test/erc20.spec.ts @@ -1,5 +1,6 @@ import { Contract, ContractFactory, Wallet } from 'ethers' import { ethers } from 'hardhat' +import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils' import chai, { expect } from 'chai' import { GWEI } from './shared/utils' import { OptimismEnv } from './shared/env' @@ -64,7 +65,10 @@ describe('Basic ERC20 interactions', async () => { const receipt = await transfer.wait() // The expected fee paid is the value returned by eth_estimateGas - const expectedFeePaid = await ERC20.estimateGas.transfer(other.address, 100) + const gasLimit = await ERC20.estimateGas.transfer(other.address, 100) + const gasPrice = await wallet.getGasPrice() + expect(gasPrice).to.deep.equal(TxGasPrice) + const expectedFeePaid = gasLimit.mul(gasPrice) // There are two events from the transfer with the first being // the ETH fee paid and the second of the value transfered (100) diff --git a/integration-tests/test/fee-payment.spec.ts b/integration-tests/test/fee-payment.spec.ts index d57718539819..f7de4d3611a2 100644 --- a/integration-tests/test/fee-payment.spec.ts +++ b/integration-tests/test/fee-payment.spec.ts @@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised' chai.use(chaiAsPromised) import { BigNumber, utils } from 'ethers' import { OptimismEnv } from './shared/env' -import { L2GasLimit } from '@eth-optimism/core-utils' +import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils' describe('Fee Payment Integration Tests', async () => { let env: OptimismEnv @@ -13,9 +13,9 @@ describe('Fee Payment Integration Tests', async () => { env = await OptimismEnv.new() }) - it('Should return a gasPrice of 1 wei', async () => { + it(`Should return a gasPrice of ${TxGasPrice.toString()} wei`, async () => { const gasPrice = await env.l2Wallet.getGasPrice() - expect(gasPrice.eq(1)) + expect(gasPrice).to.deep.eq(TxGasPrice) }) it('Should estimateGas with recoverable L2 gasLimit', async () => { @@ -28,8 +28,8 @@ describe('Fee Payment Integration Tests', async () => { utils.parseEther('0.5') ) const executionGas = await (env.ovmEth - .provider as any).send('eth_estimateExecutionGas', [tx]) - const decoded = L2GasLimit.decode(gas) + .provider as any).send('eth_estimateExecutionGas', [tx, true]) + const decoded = TxGasLimit.decode(gas) expect(BigNumber.from(executionGas)).deep.eq(decoded) }) @@ -38,8 +38,7 @@ describe('Fee Payment Integration Tests', async () => { const balanceBefore = await env.l2Wallet.getBalance() expect(balanceBefore.gt(amount)) - const gas = await env.ovmEth.estimateGas.transfer(other, amount) - const tx = await env.ovmEth.transfer(other, amount, { gasPrice: 1 }) + const tx = await env.ovmEth.transfer(other, amount) const receipt = await tx.wait() expect(receipt.status).to.eq(1) diff --git a/integration-tests/test/native-eth.spec.ts b/integration-tests/test/native-eth.spec.ts index 017199d28cf6..68c27f52a639 100644 --- a/integration-tests/test/native-eth.spec.ts +++ b/integration-tests/test/native-eth.spec.ts @@ -45,13 +45,13 @@ describe('Native ETH Integration Tests', async () => { const amount = utils.parseEther('0.5') const addr = '0x' + '1234'.repeat(10) const gas = await env.ovmEth.estimateGas.transfer(addr, amount) - expect(gas).to.be.deep.eq(BigNumber.from(0x23284d28fe6d)) + expect(gas).to.be.deep.eq(BigNumber.from(6430020)) }) it('Should estimate gas for ETH withdraw', async () => { const amount = utils.parseEther('0.5') const gas = await env.ovmEth.estimateGas.withdraw(amount) - expect(gas).to.be.deep.eq(BigNumber.from(0x207ad91a77b4)) + expect(gas).to.be.deep.eq(BigNumber.from(6140049)) }) }) diff --git a/integration-tests/test/rpc.spec.ts b/integration-tests/test/rpc.spec.ts index a65dcab5dfdf..9866ba906f5d 100644 --- a/integration-tests/test/rpc.spec.ts +++ b/integration-tests/test/rpc.spec.ts @@ -1,12 +1,13 @@ import { injectL2Context, - L2GasLimit, - roundL1GasPrice, + TxGasLimit, + TxGasPrice, + toRpcHexString, } from '@eth-optimism/core-utils' import { Wallet, BigNumber, Contract } from 'ethers' import { ethers } from 'hardhat' import chai, { expect } from 'chai' -import { sleep, l2Provider, GWEI } from './shared/utils' +import { sleep, l2Provider, l1Provider } from './shared/utils' import chaiAsPromised from 'chai-as-promised' import { OptimismEnv } from './shared/env' import { @@ -130,11 +131,25 @@ describe('Basic RPC tests', () => { const tx = { ...DEFAULT_TRANSACTION, gasLimit: 1, - gasPrice: 1, + gasPrice: TxGasPrice, } + const fee = tx.gasPrice.mul(tx.gasLimit) + const gasLimit = 5920001 await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith( - 'fee too low: 1, use at least tx.gasLimit = 33600000000001 and tx.gasPrice = 1' + `fee too low: ${fee}, use at least tx.gasLimit = ${gasLimit} and tx.gasPrice = ${TxGasPrice.toString()}` + ) + }) + + it('should reject a transaction with an incorrect gas price', async () => { + const tx = { + ...DEFAULT_TRANSACTION, + gasLimit: 1, + gasPrice: TxGasPrice.sub(1), + } + + await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith( + `tx.gasPrice must be ${TxGasPrice.toString()}` ) }) @@ -198,7 +213,7 @@ describe('Basic RPC tests', () => { it('correctly exposes revert data for contract calls', async () => { const req: TransactionRequest = { ...revertingTx, - gasLimit: 934111908999999, // override gas estimation + gasLimit: 5980899, // override gas estimation } const tx = await wallet.sendTransaction(req) @@ -221,7 +236,7 @@ describe('Basic RPC tests', () => { it('correctly exposes revert data for contract creations', async () => { const req: TransactionRequest = { ...revertingDeployTx, - gasLimit: 1051391908999999, // override gas estimation + gasLimit: 17700899, // override gas estimation } const tx = await wallet.sendTransaction(req) @@ -311,12 +326,14 @@ describe('Basic RPC tests', () => { }) describe('eth_gasPrice', () => { - it('gas price should be 1 gwei', async () => { - expect(await provider.getGasPrice()).to.be.deep.equal(1) + it('gas price should be the fee scalar', async () => { + expect(await provider.getGasPrice()).to.be.deep.equal( + TxGasPrice.toNumber() + ) }) }) - describe('eth_estimateGas (returns the fee)', () => { + describe('eth_estimateGas (returns the scaled fee)', () => { it('gas estimation is deterministic', async () => { let lastEstimate: BigNumber for (let i = 0; i < 10; i++) { @@ -338,7 +355,7 @@ describe('Basic RPC tests', () => { to: DEFAULT_TRANSACTION.to, value: 0, }) - expect(estimate).to.be.eq(33600000119751) + expect(estimate).to.be.eq(5920012) }) it('should return a gas estimate that grows with the size of data', async () => { @@ -349,7 +366,6 @@ describe('Basic RPC tests', () => { for (const data of dataLen) { const tx = { to: '0x' + '1234'.repeat(10), - gasPrice: '0x1', value: '0x0', data: '0x' + '00'.repeat(data), from: '0x' + '1234'.repeat(10), @@ -357,18 +373,19 @@ describe('Basic RPC tests', () => { const estimate = await l2Provider.estimateGas(tx) const l2Gaslimit = await l2Provider.send('eth_estimateExecutionGas', [ tx, + true, ]) - const decoded = L2GasLimit.decode(estimate) + const decoded = TxGasLimit.decode(estimate) expect(decoded).to.deep.eq(BigNumber.from(l2Gaslimit)) expect(estimate.toString().endsWith(l2Gaslimit.toString())) + const l2GasPrice = BigNumber.from(0) // The L2GasPrice should be fetched from the L2GasPrice oracle contract, // but it does not yet exist. Use the default value for now - const l2GasPrice = BigNumber.from(1) - const expected = L2GasLimit.encode({ + const expected = TxGasLimit.encode({ data: tx.data, - l1GasPrice: roundL1GasPrice(l1GasPrice), + l1GasPrice, l2GasLimit: BigNumber.from(l2Gaslimit), l2GasPrice, }) diff --git a/l2geth/CHANGELOG.md b/l2geth/CHANGELOG.md index f50227477351..d0a0f3a73c23 100644 --- a/l2geth/CHANGELOG.md +++ b/l2geth/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.3.8 + +### Patch Changes + +- 989a3027: Optimize main polling loops +- cc6c7f07: Bump golang version to 1.15 + +## 0.3.7 + +### Patch Changes + +- cb4a928b: Make block hashes deterministic by using the same clique signer key +- f1b27318: Fixes incorrect type parsing in the RollupClient. The gasLimit became greater than the largest safe JS number so it needs to be represented as a string +- a64f8161: Implement the next fee spec in both geth and in core-utils +- 5e4eaea1: fix potential underflow when launching the chain when the last verified index is 0 +- 1293825c: Fix gasLimit overflow +- a25acbbd: Refactor the SyncService to more closely implement the specification. This includes using query params to select the backend from the DTL, trailing syncing of batches for the sequencer, syncing by batches as the verifier as well as unified code paths for transaction ingestion to prevent double ingestion or missed ingestion +- c2b6e14b: Implement the latest fee spec such that the L2 gas limit is scaled and the tx.gasPrice/tx.gasLimit show correctly in metamask + ## 0.3.6 ### Patch Changes diff --git a/l2geth/accounts/keystore/keystore.go b/l2geth/accounts/keystore/keystore.go index 5b55175b1f3e..1ddd98a4622f 100644 --- a/l2geth/accounts/keystore/keystore.go +++ b/l2geth/accounts/keystore/keystore.go @@ -21,6 +21,7 @@ package keystore import ( + "bytes" "crypto/ecdsa" crand "crypto/rand" "errors" @@ -36,8 +37,10 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" ) var ( @@ -80,6 +83,21 @@ func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { keydir, _ = filepath.Abs(keydir) ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP, false}} ks.init(keydir) + if vm.UsingOVM { + // Add a deterministic key to the key store so that + // all clique blocks are signed with the same key + input := make([]byte, 65) + rng := bytes.NewReader(input) + key, err := newKey(rng) + log.Info("Adding key to keyring", "address", key.Address.Hex()) + if err != nil { + panic(fmt.Sprintf("cannot create key: %s", err)) + } + _, err = ks.importKey(key, "") + if err != nil { + panic(fmt.Sprintf("cannot import key: %s", err)) + } + } return ks } diff --git a/l2geth/cmd/utils/flags.go b/l2geth/cmd/utils/flags.go index 6c5c4447401b..c28526ae252a 100644 --- a/l2geth/cmd/utils/flags.go +++ b/l2geth/cmd/utils/flags.go @@ -471,12 +471,13 @@ var ( MinerEtherbaseFlag = cli.StringFlag{ Name: "miner.etherbase", Usage: "Public address for block mining rewards (default = first account)", - Value: "0", + + Value: "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf", + EnvVar: "ETHERBASE", } MinerLegacyEtherbaseFlag = cli.StringFlag{ Name: "etherbase", Usage: "Public address for block mining rewards (default = first account, deprecated, use --miner.etherbase)", - Value: "0", } MinerExtraDataFlag = cli.StringFlag{ Name: "miner.extradata", diff --git a/l2geth/consensus/clique/clique.go b/l2geth/consensus/clique/clique.go index 29201e2ba0d1..62bc27c1fe0f 100644 --- a/l2geth/consensus/clique/clique.go +++ b/l2geth/consensus/clique/clique.go @@ -23,7 +23,6 @@ import ( "io" "math/big" "math/rand" - "os" "sync" "time" @@ -34,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -249,7 +249,7 @@ func (c *Clique) verifyHeader(chain consensus.ChainReader, header *types.Header, } number := header.Number.Uint64() - if os.Getenv("USING_OVM") != "true" { + if vm.UsingOVM { // Don't waste time checking blocks from the future if header.Time > uint64(time.Now().Unix()) { return consensus.ErrFutureBlock @@ -631,7 +631,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle)) } - if os.Getenv("USING_OVM") == "true" { + if vm.UsingOVM { delay = 0 } // Sign all the things! diff --git a/l2geth/core/rollup_fee.go b/l2geth/core/rollup_fee.go deleted file mode 100644 index 17662547df22..000000000000 --- a/l2geth/core/rollup_fee.go +++ /dev/null @@ -1,126 +0,0 @@ -package core - -import ( - "errors" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" -) - -// overhead represents the fixed cost of batch submission of a single -// transaction in gas -const overhead uint64 = 4200 - -// hundredMillion is a constant used in the gas encoding formula -const hundredMillion uint64 = 100_000_000 - -var bigHundredMillion = new(big.Int).SetUint64(hundredMillion) - -// errInvalidGasPrice is the error returned when a user submits an incorrect gas -// price. The gas price must satisfy a particular equation depending on if it -// is a L1 gas price or a L2 gas price -var errInvalidGasPrice = errors.New("rollup fee: invalid gas price") - -// CalculateFee calculates the fee that must be paid to the Rollup sequencer, taking into -// account the cost of publishing data to L1. -// l2_gas_price * l2_gas_limit + l1_gas_price * l1_gas_used -// where the l2 gas price must satisfy the equation `x % (10**8) = 1` -// and the l1 gas price must satisfy the equation `x % (10**8) = 0` -func CalculateRollupFee(data []byte, l1GasPrice, l2GasLimit, l2GasPrice *big.Int) (*big.Int, error) { - if err := VerifyL1GasPrice(l1GasPrice); err != nil { - return nil, fmt.Errorf("invalid L1 gas price %d: %w", l1GasPrice, err) - } - if err := VerifyL2GasPrice(l2GasPrice); err != nil { - return nil, fmt.Errorf("invalid L2 gas price %d: %w", l2GasPrice, err) - } - l1GasLimit := calculateL1GasLimit(data, overhead) - l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit) - l2Fee := new(big.Int).Mul(l2GasLimit, l2GasPrice) - fee := new(big.Int).Add(l1Fee, l2Fee) - return fee, nil -} - -// calculateL1GasLimit computes the L1 gasLimit based on the calldata and -// constant sized overhead. The overhead can be decreased as the cost of the -// batch submission goes down via contract optimizations. This will not overflow -// under standard network conditions. -func calculateL1GasLimit(data []byte, overhead uint64) *big.Int { - zeroes, ones := zeroesAndOnes(data) - zeroesCost := zeroes * params.TxDataZeroGas - onesCost := ones * params.TxDataNonZeroGasEIP2028 - gasLimit := zeroesCost + onesCost + overhead - return new(big.Int).SetUint64(gasLimit) -} - -// ceilModOneHundredMillion rounds the input integer up to the nearest modulus -// of one hundred million -func ceilModOneHundredMillion(num *big.Int) *big.Int { - if new(big.Int).Mod(num, bigHundredMillion).Cmp(common.Big0) == 0 { - return num - } - sum := new(big.Int).Add(num, bigHundredMillion) - mod := new(big.Int).Mod(num, bigHundredMillion) - return new(big.Int).Sub(sum, mod) -} - -// VerifyL1GasPrice returns an error if the number is an invalid possible L1 gas -// price -func VerifyL1GasPrice(l1GasPrice *big.Int) error { - if new(big.Int).Mod(l1GasPrice, bigHundredMillion).Cmp(common.Big0) != 0 { - return errInvalidGasPrice - } - return nil -} - -// VerifyL2GasPrice returns an error if the number is an invalid possible L2 gas -// price -func VerifyL2GasPrice(l2GasPrice *big.Int) error { - isNonZero := l2GasPrice.Cmp(common.Big0) != 0 - isNotModHundredMillion := new(big.Int).Mod(l2GasPrice, bigHundredMillion).Cmp(common.Big1) != 0 - if isNonZero && isNotModHundredMillion { - return errInvalidGasPrice - } - if l2GasPrice.Cmp(common.Big0) == 0 { - return errInvalidGasPrice - } - return nil -} - -// RoundL1GasPrice returns a ceilModOneHundredMillion where 0 -// is the identity function -func RoundL1GasPrice(gasPrice *big.Int) *big.Int { - return ceilModOneHundredMillion(gasPrice) -} - -// RoundL2GasPriceBig implements the algorithm: -// if gasPrice is 0; return 1 -// if gasPrice is 1; return 10**8 + 1 -// return ceilModOneHundredMillion(gasPrice-1)+1 -func RoundL2GasPrice(gasPrice *big.Int) *big.Int { - if gasPrice.Cmp(common.Big0) == 0 { - return big.NewInt(1) - } - if gasPrice.Cmp(common.Big1) == 0 { - return new(big.Int).Add(bigHundredMillion, common.Big1) - } - gp := new(big.Int).Sub(gasPrice, common.Big1) - mod := ceilModOneHundredMillion(gp) - return new(big.Int).Add(mod, common.Big1) -} - -func DecodeL2GasLimit(gasLimit *big.Int) *big.Int { - return new(big.Int).Mod(gasLimit, bigHundredMillion) -} - -func zeroesAndOnes(data []byte) (uint64, uint64) { - var zeroes uint64 - for _, byt := range data { - if byt == 0 { - zeroes++ - } - } - ones := uint64(len(data)) - zeroes - return zeroes, ones -} diff --git a/l2geth/core/rollup_fee_test.go b/l2geth/core/rollup_fee_test.go deleted file mode 100644 index dfe68107ad3f..000000000000 --- a/l2geth/core/rollup_fee_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package core - -import ( - "errors" - "math" - "math/big" - "testing" -) - -var roundingL1GasPriceTests = map[string]struct { - input uint64 - expect uint64 -}{ - "simple": {10, pow10(8)}, - "one-over": {pow10(8) + 1, 2 * pow10(8)}, - "exact": {pow10(8), pow10(8)}, - "one-under": {pow10(8) - 1, pow10(8)}, - "small": {3, pow10(8)}, - "two": {2, pow10(8)}, - "one": {1, pow10(8)}, - "zero": {0, 0}, -} - -func TestRoundL1GasPrice(t *testing.T) { - for name, tt := range roundingL1GasPriceTests { - t.Run(name, func(t *testing.T) { - got := RoundL1GasPrice(new(big.Int).SetUint64(tt.input)) - if got.Uint64() != tt.expect { - t.Fatalf("Mismatched rounding to nearest, got %d expected %d", got, tt.expect) - } - }) - } -} - -var roundingL2GasPriceTests = map[string]struct { - input uint64 - expect uint64 -}{ - "simple": {10, pow10(8) + 1}, - "one-over": {pow10(8) + 2, 2*pow10(8) + 1}, - "exact": {pow10(8) + 1, pow10(8) + 1}, - "one-under": {pow10(8), pow10(8) + 1}, - "small": {3, pow10(8) + 1}, - "two": {2, pow10(8) + 1}, - "one": {1, pow10(8) + 1}, - "zero": {0, 1}, -} - -func TestRoundL2GasPrice(t *testing.T) { - for name, tt := range roundingL2GasPriceTests { - t.Run(name, func(t *testing.T) { - got := RoundL2GasPrice(new(big.Int).SetUint64(tt.input)) - if got.Uint64() != tt.expect { - t.Fatalf("Mismatched rounding to nearest, got %d expected %d", got, tt.expect) - } - }) - } -} - -var l1GasLimitTests = map[string]struct { - data []byte - overhead uint64 - expect *big.Int -}{ - "simple": {[]byte{}, 0, big.NewInt(0)}, - "simple-overhead": {[]byte{}, 10, big.NewInt(10)}, - "zeros": {[]byte{0x00, 0x00, 0x00, 0x00}, 10, big.NewInt(26)}, - "ones": {[]byte{0x01, 0x02, 0x03, 0x04}, 200, big.NewInt(16*4 + 200)}, -} - -func TestL1GasLimit(t *testing.T) { - for name, tt := range l1GasLimitTests { - t.Run(name, func(t *testing.T) { - got := calculateL1GasLimit(tt.data, tt.overhead) - if got.Cmp(tt.expect) != 0 { - t.Fatal("Calculated gas limit does not match") - } - }) - } -} - -var feeTests = map[string]struct { - dataLen int - l1GasPrice uint64 - l2GasLimit uint64 - l2GasPrice uint64 - err error -}{ - "simple": {100, 100_000_000, 437118, 100_000_001, nil}, - "zero-l2-gasprice": {10, 100_000_000, 196205, 0, errInvalidGasPrice}, - "one-l2-gasprice": {10, 100_000_000, 196205, 1, nil}, - "zero-l1-gasprice": {10, 0, 196205, 100_000_001, nil}, - "one-l1-gasprice": {10, 1, 23255, 23254, errInvalidGasPrice}, -} - -func TestCalculateRollupFee(t *testing.T) { - for name, tt := range feeTests { - t.Run(name, func(t *testing.T) { - data := make([]byte, 0, tt.dataLen) - l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice) - l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit) - l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice) - - fee, err := CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice) - if !errors.Is(err, tt.err) { - t.Fatalf("Cannot calculate fee: %s", err) - } - - if err == nil { - decodedGasLimit := DecodeL2GasLimit(fee) - if l2GasLimit.Cmp(decodedGasLimit) != 0 { - t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit) - } - } - }) - } -} - -func pow10(x int) uint64 { - return uint64(math.Pow10(x)) -} diff --git a/l2geth/core/tx_pool.go b/l2geth/core/tx_pool.go index c92e3fde1c5c..1244e27835ca 100644 --- a/l2geth/core/tx_pool.go +++ b/l2geth/core/tx_pool.go @@ -92,9 +92,8 @@ var ( ) var ( - evictionInterval = time.Minute // Time interval to check for evictable transactions - statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats - gwei = big.NewInt(params.GWei) // 1 gwei, used as a flag for "rollup" transactions + evictionInterval = time.Minute // Time interval to check for evictable transactions + statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats ) var ( @@ -540,10 +539,14 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { } // Ensure the transaction doesn't exceed the current block limit gas. - // We skip this condition check if the transaction's gasPrice is set to 1gwei, - // which indicates a "rollup" transaction that's paying for its data. - if pool.currentMaxGas < tx.L2Gas() && tx.GasPrice().Cmp(gwei) != 0 { - return ErrGasLimit + if vm.UsingOVM { + if pool.currentMaxGas < tx.L2Gas() { + return ErrGasLimit + } + } else { + if pool.currentMaxGas < tx.Gas() { + return ErrGasLimit + } } // Make sure the transaction is signed properly diff --git a/l2geth/core/types/transaction.go b/l2geth/core/types/transaction.go index a3fc9874acf6..6f30076f9bf5 100644 --- a/l2geth/core/types/transaction.go +++ b/l2geth/core/types/transaction.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rollup/fees" ) //go:generate gencodec -type txdata -field-override txdataMarshaling -out gen_tx_json.go @@ -225,7 +226,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { func (tx *Transaction) Data() []byte { return common.CopyBytes(tx.data.Payload) } func (tx *Transaction) Gas() uint64 { return tx.data.GasLimit } -func (tx *Transaction) L2Gas() uint64 { return tx.data.GasLimit % 100_000_000 } +func (tx *Transaction) L2Gas() uint64 { return fees.DecodeL2GasLimitU64(tx.data.GasLimit) } func (tx *Transaction) GasPrice() *big.Int { return new(big.Int).Set(tx.data.Price) } func (tx *Transaction) Value() *big.Int { return new(big.Int).Set(tx.data.Amount) } func (tx *Transaction) Nonce() uint64 { return tx.data.AccountNonce } diff --git a/l2geth/core/vm/evm.go b/l2geth/core/vm/evm.go index b54b9bd5d777..20fdc2dfcb60 100644 --- a/l2geth/core/vm/evm.go +++ b/l2geth/core/vm/evm.go @@ -464,10 +464,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas ret = []byte{} } } - } - - if evm.Context.EthCallSender == nil { - log.Debug("Reached the end of an OVM execution", "ID", evm.Id, "Return Data", hexutil.Encode(ret), "Error", err) + if evm.Context.EthCallSender == nil { + log.Debug("Reached the end of an OVM execution", "ID", evm.Id, "Return Data", hexutil.Encode(ret), "Error", err) + } } } diff --git a/l2geth/eth/gasprice/rollup_gasprice.go b/l2geth/eth/gasprice/rollup_gasprice.go index 388161febecc..7b0a062e6c41 100644 --- a/l2geth/eth/gasprice/rollup_gasprice.go +++ b/l2geth/eth/gasprice/rollup_gasprice.go @@ -5,62 +5,55 @@ import ( "math/big" "sync" - "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/log" ) // RollupOracle holds the L1 and L2 gas prices for fee calculation type RollupOracle struct { - dataPrice *big.Int - executionPrice *big.Int - dataPriceLock sync.RWMutex - executionPriceLock sync.RWMutex + l1GasPrice *big.Int + l2GasPrice *big.Int + l1GasPriceLock sync.RWMutex + l2GasPriceLock sync.RWMutex } // NewRollupOracle returns an initialized RollupOracle -func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle { +func NewRollupOracle(l1GasPrice *big.Int, l2GasPrice *big.Int) *RollupOracle { return &RollupOracle{ - dataPrice: dataPrice, - executionPrice: executionPrice, + l1GasPrice: l1GasPrice, + l2GasPrice: l2GasPrice, } } // SuggestL1GasPrice returns the gas price which should be charged per byte of published // data by the sequencer. func (gpo *RollupOracle) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) { - gpo.dataPriceLock.RLock() - defer gpo.dataPriceLock.RUnlock() - return gpo.dataPrice, nil + gpo.l1GasPriceLock.RLock() + defer gpo.l1GasPriceLock.RUnlock() + return gpo.l1GasPrice, nil } // SetL1GasPrice returns the current L1 gas price -func (gpo *RollupOracle) SetL1GasPrice(dataPrice *big.Int) error { - gpo.dataPriceLock.Lock() - defer gpo.dataPriceLock.Unlock() - if err := core.VerifyL1GasPrice(dataPrice); err != nil { - return err - } - gpo.dataPrice = dataPrice - log.Info("Set L1 Gas Price", "gasprice", gpo.dataPrice) +func (gpo *RollupOracle) SetL1GasPrice(gasPrice *big.Int) error { + gpo.l1GasPriceLock.Lock() + defer gpo.l1GasPriceLock.Unlock() + gpo.l1GasPrice = gasPrice + log.Info("Set L1 Gas Price", "gasprice", gpo.l1GasPrice) return nil } // SuggestL2GasPrice returns the gas price which should be charged per unit of gas // set manually by the sequencer depending on congestion func (gpo *RollupOracle) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) { - gpo.executionPriceLock.RLock() - defer gpo.executionPriceLock.RUnlock() - return gpo.executionPrice, nil + gpo.l2GasPriceLock.RLock() + defer gpo.l2GasPriceLock.RUnlock() + return gpo.l2GasPrice, nil } // SetL2GasPrice returns the current L2 gas price -func (gpo *RollupOracle) SetL2GasPrice(executionPrice *big.Int) error { - gpo.executionPriceLock.Lock() - defer gpo.executionPriceLock.Unlock() - if err := core.VerifyL2GasPrice(executionPrice); err != nil { - return err - } - gpo.executionPrice = executionPrice - log.Info("Set L2 Gas Price", "gasprice", gpo.executionPrice) +func (gpo *RollupOracle) SetL2GasPrice(gasPrice *big.Int) error { + gpo.l2GasPriceLock.Lock() + defer gpo.l2GasPriceLock.Unlock() + gpo.l2GasPrice = gasPrice + log.Info("Set L2 Gas Price", "gasprice", gpo.l2GasPrice) return nil } diff --git a/l2geth/go.mod b/l2geth/go.mod index 47277f950dba..d8239f1d0c52 100644 --- a/l2geth/go.mod +++ b/l2geth/go.mod @@ -1,6 +1,6 @@ module github.com/ethereum/go-ethereum -go 1.13 +go 1.15 require ( github.com/Azure/azure-pipeline-go v0.2.2 // indirect diff --git a/l2geth/internal/ethapi/api.go b/l2geth/internal/ethapi/api.go index 2828d83707bd..abec7e0b1c06 100644 --- a/l2geth/internal/ethapi/api.go +++ b/l2geth/internal/ethapi/api.go @@ -45,15 +45,17 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rollup/fees" "github.com/ethereum/go-ethereum/rpc" "github.com/tyler-smith/go-bip39" ) const ( - defaultGasPrice = params.Wei + defaultGasPrice = params.Wei * fees.TxGasPrice ) var errOVMUnsupported = errors.New("OVM: Unsupported RPC Method") +var bigDefaultGasPrice = new(big.Int).SetUint64(defaultGasPrice) // PublicEthereumAPI provides an API to access Ethereum related information. // It offers only methods that operate on public data that is freely available to anyone. @@ -68,7 +70,7 @@ func NewPublicEthereumAPI(b Backend) *PublicEthereumAPI { // GasPrice always returns 1 gwei. See `DoEstimateGas` below for context. func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { - return (*hexutil.Big)(big.NewInt(defaultGasPrice)), nil + return (*hexutil.Big)(bigDefaultGasPrice), nil } // ProtocolVersion returns the current Ethereum protocol version this node supports @@ -1037,31 +1039,27 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash if err != nil { return 0, err } - // 2a. fetch the data price, depends on how the sequencer has chosen to update their values based on the // l1 gas prices l1GasPrice, err := b.SuggestL1GasPrice(ctx) if err != nil { return 0, err } - // 2b. fetch the execution gas price, by the typical mempool dynamics l2GasPrice, err := b.SuggestL2GasPrice(ctx) if err != nil { return 0, err } - - var data []byte - if args.Data == nil { - data = []byte{} - } else { + data := []byte{} + if args.Data != nil { data = *args.Data } - // 3. calculate the fee + // 3. calculate the fee using just the calldata. The additional overhead of + // RLP encoding is covered inside of EncodeL2GasLimit l2GasLimit := new(big.Int).SetUint64(uint64(gasUsed)) - fee, err := core.CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice) - if err != nil { - return 0, err + fee := fees.EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice) + if !fee.IsUint64() { + return 0, fmt.Errorf("estimate gas overflow: %s", fee) } return (hexutil.Uint64)(fee.Uint64()), nil } @@ -1150,9 +1148,17 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (h // EstimateExecutionGas returns an estimate of the amount of gas needed to execute the // given transaction against the current pending block. -func (s *PublicBlockChainAPI) EstimateExecutionGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { +func (s *PublicBlockChainAPI) EstimateExecutionGas(ctx context.Context, args CallArgs, round *bool) (hexutil.Uint64, error) { blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - return legacyDoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap()) + estimate, err := legacyDoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap()) + if err != nil { + return estimate, err + } + if round != nil && *round { + rounded := fees.Ceilmod(new(big.Int).SetUint64(uint64(estimate)), fees.BigTenThousand) + estimate = (hexutil.Uint64)(rounded.Uint64()) + } + return estimate, nil } // ExecutionResult groups all structured logs emitted by the EVM diff --git a/l2geth/miner/worker.go b/l2geth/miner/worker.go index 2c9a0a177997..e8da4757e148 100644 --- a/l2geth/miner/worker.go +++ b/l2geth/miner/worker.go @@ -196,7 +196,7 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth), pendingTasks: make(map[common.Hash]*task), txsCh: make(chan core.NewTxsEvent, txChanSize), - rollupCh: make(chan core.NewTxsEvent, txChanSize), + rollupCh: make(chan core.NewTxsEvent, 1), chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize), chainSideCh: make(chan core.ChainSideEvent, chainSideChanSize), newWorkCh: make(chan *newWorkReq), diff --git a/l2geth/package.json b/l2geth/package.json index 494c2e3fef89..56a9a6a93700 100644 --- a/l2geth/package.json +++ b/l2geth/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/l2geth", - "version": "0.3.6", + "version": "0.3.8", "private": true, "devDependencies": {} } diff --git a/l2geth/rollup/client.go b/l2geth/rollup/client.go index b8b08222c73b..4a1a0f5b14e5 100644 --- a/l2geth/rollup/client.go +++ b/l2geth/rollup/client.go @@ -69,7 +69,7 @@ type transaction struct { BlockNumber uint64 `json:"blockNumber"` Timestamp uint64 `json:"timestamp"` Value hexutil.Uint64 `json:"value"` - GasLimit uint64 `json:"gasLimit"` + GasLimit uint64 `json:"gasLimit,string"` Target common.Address `json:"target"` Origin *common.Address `json:"origin"` Data hexutil.Bytes `json:"data"` @@ -84,7 +84,7 @@ type Enqueue struct { Index *uint64 `json:"ctcIndex"` Target *common.Address `json:"target"` Data *hexutil.Bytes `json:"data"` - GasLimit *uint64 `json:"gasLimit"` + GasLimit *uint64 `json:"gasLimit,string"` Origin *common.Address `json:"origin"` BlockNumber *uint64 `json:"blockNumber"` Timestamp *uint64 `json:"timestamp"` @@ -570,7 +570,7 @@ func (c *Client) GetTransactionBatch(index uint64) (*Batch, []*types.Transaction Get("/batch/transaction/index/{index}") if err != nil { - return nil, nil, fmt.Errorf("Cannot get transaction batch %d", index) + return nil, nil, fmt.Errorf("Cannot get transaction batch %d: %w", index, err) } txBatch, ok := response.Result().(*TransactionBatchResponse) if !ok { diff --git a/l2geth/rollup/config.go b/l2geth/rollup/config.go index 22d9b3f35ce5..2ce695201645 100644 --- a/l2geth/rollup/config.go +++ b/l2geth/rollup/config.go @@ -10,8 +10,6 @@ import ( type Config struct { // Maximum calldata size for a Queue Origin Sequencer Tx MaxCallDataSize int - // Number of confs before applying a L1 to L2 tx - Eth1ConfirmationDepth uint64 // Verifier mode IsVerifier bool // Enable the sync service diff --git a/l2geth/rollup/fees/rollup_fee.go b/l2geth/rollup/fees/rollup_fee.go new file mode 100644 index 000000000000..13feafbbdef7 --- /dev/null +++ b/l2geth/rollup/fees/rollup_fee.go @@ -0,0 +1,113 @@ +package fees + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// overhead represents the fixed cost of batch submission of a single +// transaction in gas. +const overhead uint64 = 4200 + 200*params.TxDataNonZeroGasEIP2028 + +// feeScalar is used to scale the calculations in EncodeL2GasLimit +// to prevent them from being too large +const feeScalar uint64 = 10_000_000 + +// TxGasPrice is a constant that determines the result of `eth_gasPrice` +// It is scaled upwards by 50% +// tx.gasPrice is hard coded to 1500 * wei and all transactions must set that +// gas price. +const TxGasPrice uint64 = feeScalar + (feeScalar / 2) + +// BigTxGasPrice is the L2GasPrice as type big.Int +var BigTxGasPrice = new(big.Int).SetUint64(TxGasPrice) +var bigFeeScalar = new(big.Int).SetUint64(feeScalar) + +const tenThousand = 10000 + +var BigTenThousand = new(big.Int).SetUint64(tenThousand) + +// EncodeTxGasLimit computes the `tx.gasLimit` based on the L1/L2 gas prices and +// the L2 gas limit. The L2 gas limit is encoded inside of the lower order bits +// of the number like so: [ | l2GasLimit ] +// [ tx.gaslimit ] +// The lower order bits must be large enough to fit the L2 gas limit, so 10**8 +// is chosen. If higher order bits collide with any bits from the L2 gas limit, +// the L2 gas limit will not be able to be decoded. +// An explicit design goal of this scheme was to make the L2 gas limit be human +// readable. The entire number is interpreted as the gas limit when computing +// the fee, so increasing the L2 Gas limit will increase the fee paid. +// The calculation is: +// l1GasLimit = zero_count(data) * 4 + non_zero_count(data) * 16 + overhead +// roundedL2GasLimit = ceilmod(l2GasLimit, 10_000) +// l1Fee = l1GasPrice * l1GasLimit +// l2Fee = l2GasPrice * roundedL2GasLimit +// sum = l1Fee + l2Fee +// scaled = sum / scalar +// rounded = ceilmod(scaled, tenThousand) +// roundedScaledL2GasLimit = roundedL2GasLimit / tenThousand +// result = rounded + roundedScaledL2GasLimit +// Note that for simplicity purposes, only the calldata is passed into this +// function when in reality the RLP encoded transaction should be. The +// additional cost is added to the overhead constant to prevent the need to RLP +// encode transactions during calls to `eth_estimateGas` +func EncodeTxGasLimit(data []byte, l1GasPrice, l2GasLimit, l2GasPrice *big.Int) *big.Int { + l1GasLimit := calculateL1GasLimit(data, overhead) + roundedL2GasLimit := Ceilmod(l2GasLimit, BigTenThousand) + l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit) + l2Fee := new(big.Int).Mul(l2GasPrice, roundedL2GasLimit) + sum := new(big.Int).Add(l1Fee, l2Fee) + scaled := new(big.Int).Div(sum, bigFeeScalar) + rounded := Ceilmod(scaled, BigTenThousand) + roundedScaledL2GasLimit := new(big.Int).Div(roundedL2GasLimit, BigTenThousand) + result := new(big.Int).Add(rounded, roundedScaledL2GasLimit) + return result +} + +func Ceilmod(a, b *big.Int) *big.Int { + remainder := new(big.Int).Mod(a, b) + if remainder.Cmp(common.Big0) == 0 { + return a + } + sum := new(big.Int).Add(a, b) + rounded := new(big.Int).Sub(sum, remainder) + return rounded +} + +// DecodeL2GasLimit decodes the L2 gas limit from an encoded L2 gas limit +func DecodeL2GasLimit(gasLimit *big.Int) *big.Int { + scaled := new(big.Int).Mod(gasLimit, BigTenThousand) + return new(big.Int).Mul(scaled, BigTenThousand) +} + +func DecodeL2GasLimitU64(gasLimit uint64) uint64 { + scaled := gasLimit % tenThousand + return scaled * tenThousand +} + +// calculateL1GasLimit computes the L1 gasLimit based on the calldata and +// constant sized overhead. The overhead can be decreased as the cost of the +// batch submission goes down via contract optimizations. This will not overflow +// under standard network conditions. +func calculateL1GasLimit(data []byte, overhead uint64) *big.Int { + zeroes, ones := zeroesAndOnes(data) + zeroesCost := zeroes * params.TxDataZeroGas + onesCost := ones * params.TxDataNonZeroGasEIP2028 + gasLimit := zeroesCost + onesCost + overhead + return new(big.Int).SetUint64(gasLimit) +} + +func zeroesAndOnes(data []byte) (uint64, uint64) { + var zeroes uint64 + var ones uint64 + for _, byt := range data { + if byt == 0 { + zeroes++ + } else { + ones++ + } + } + return zeroes, ones +} diff --git a/l2geth/rollup/fees/rollup_fee_test.go b/l2geth/rollup/fees/rollup_fee_test.go new file mode 100644 index 000000000000..254d418ad8fa --- /dev/null +++ b/l2geth/rollup/fees/rollup_fee_test.go @@ -0,0 +1,104 @@ +package fees + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/params" +) + +var l1GasLimitTests = map[string]struct { + data []byte + overhead uint64 + expect *big.Int +}{ + "simple": {[]byte{}, 0, big.NewInt(0)}, + "simple-overhead": {[]byte{}, 10, big.NewInt(10)}, + "zeros": {[]byte{0x00, 0x00, 0x00, 0x00}, 10, big.NewInt(26)}, + "ones": {[]byte{0x01, 0x02, 0x03, 0x04}, 200, big.NewInt(16*4 + 200)}, +} + +func TestL1GasLimit(t *testing.T) { + for name, tt := range l1GasLimitTests { + t.Run(name, func(t *testing.T) { + got := calculateL1GasLimit(tt.data, tt.overhead) + if got.Cmp(tt.expect) != 0 { + t.Fatal("Calculated gas limit does not match") + } + }) + } +} + +var feeTests = map[string]struct { + dataLen int + l1GasPrice uint64 + l2GasLimit uint64 + l2GasPrice uint64 +}{ + "simple": { + dataLen: 10, + l1GasPrice: params.GWei, + l2GasLimit: 437118, + l2GasPrice: params.GWei, + }, + "zero-l2-gasprice": { + dataLen: 10, + l1GasPrice: params.GWei, + l2GasLimit: 196205, + l2GasPrice: 0, + }, + "one-l2-gasprice": { + dataLen: 10, + l1GasPrice: params.GWei, + l2GasLimit: 196205, + l2GasPrice: 1, + }, + "zero-l1-gasprice": { + dataLen: 10, + l1GasPrice: 0, + l2GasLimit: 196205, + l2GasPrice: params.GWei, + }, + "one-l1-gasprice": { + dataLen: 10, + l1GasPrice: 1, + l2GasLimit: 23255, + l2GasPrice: params.GWei, + }, + "zero-gasprices": { + dataLen: 10, + l1GasPrice: 0, + l2GasLimit: 23255, + l2GasPrice: 0, + }, + "max-gaslimit": { + dataLen: 10, + l1GasPrice: params.GWei, + l2GasLimit: 99_970_000, + l2GasPrice: params.GWei, + }, + "larger-divisor": { + dataLen: 10, + l1GasPrice: 0, + l2GasLimit: 10, + l2GasPrice: 0, + }, +} + +func TestCalculateRollupFee(t *testing.T) { + for name, tt := range feeTests { + t.Run(name, func(t *testing.T) { + data := make([]byte, tt.dataLen) + l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice) + l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit) + l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice) + + fee := EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice) + decodedGasLimit := DecodeL2GasLimit(fee) + roundedL2GasLimit := Ceilmod(l2GasLimit, BigTenThousand) + if roundedL2GasLimit.Cmp(decodedGasLimit) != 0 { + t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit) + } + }) + } +} diff --git a/l2geth/rollup/sync_service.go b/l2geth/rollup/sync_service.go index b3f831559a79..0b71d20193ee 100644 --- a/l2geth/rollup/sync_service.go +++ b/l2geth/rollup/sync_service.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/gasprice" + "github.com/ethereum/go-ethereum/rollup/fees" ) // errShortRemoteTip is an error for when the remote tip is shorter than the @@ -52,7 +53,6 @@ type SyncService struct { syncing atomic.Value chainHeadSub event.Subscription OVMContext OVMContext - confirmationDepth uint64 pollInterval time.Duration timestampRefreshThreshold time.Duration chainHeadCh chan core.ChainHeadEvent @@ -102,7 +102,6 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co cancel: cancel, verifier: cfg.IsVerifier, enable: cfg.Eth1SyncServiceEnable, - confirmationDepth: cfg.Eth1ConfirmationDepth, syncing: atomic.Value{}, bc: bc, txpool: txpool, @@ -139,17 +138,18 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co } // Wait until the remote service is done syncing - for { + t := time.NewTicker(10 * time.Second) + for ; true; <-t.C { status, err := service.client.SyncStatus(service.backend) if err != nil { log.Error("Cannot get sync status") continue } if !status.Syncing { + t.Stop() break } log.Info("Still syncing", "index", status.CurrentTransactionIndex, "tip", status.HighestKnownTransactionIndex) - time.Sleep(10 * time.Second) } // Initialize the latest L1 data here to make sure that @@ -244,20 +244,26 @@ func (s *SyncService) initializeLatestL1(ctcDeployHeight *big.Int) error { s.SetLatestL1BlockNumber(context.BlockNumber) } else { log.Info("Found latest index", "index", *index) - block := s.bc.GetBlockByNumber(*index - 1) + block := s.bc.GetBlockByNumber(*index + 1) if block == nil { block = s.bc.CurrentBlock() - idx := block.Number().Uint64() - if idx > *index { + blockNum := block.Number().Uint64() + if blockNum > *index { // This is recoverable with a reorg but should never happen return fmt.Errorf("Current block height greater than index") } - s.SetLatestIndex(&idx) - log.Info("Block not found, resetting index", "new", idx, "old", *index-1) + var idx *uint64 + if blockNum > 0 { + num := blockNum - 1 + idx = &num + } + s.SetLatestIndex(idx) + log.Info("Block not found, resetting index", "new", stringify(idx), "old", *index) } txs := block.Transactions() if len(txs) != 1 { - log.Error("Unexpected number of transactions in block: %d", len(txs)) + log.Error("Unexpected number of transactions in block", "count", len(txs)) + panic("Cannot recover OVM Context") } tx := txs[0] s.SetLatestL1Timestamp(tx.L1Timestamp()) @@ -316,7 +322,8 @@ func (s *SyncService) Stop() error { // VerifierLoop is the main loop for Verifier mode func (s *SyncService) VerifierLoop() { log.Info("Starting Verifier Loop", "poll-interval", s.pollInterval, "timestamp-refresh-threshold", s.timestampRefreshThreshold) - for { + t := time.NewTicker(s.pollInterval) + for ; true; <-t.C { if err := s.updateL1GasPrice(); err != nil { log.Error("Cannot update L1 gas price", "msg", err) } @@ -326,7 +333,6 @@ func (s *SyncService) VerifierLoop() { if err := s.updateL2GasPrice(nil); err != nil { log.Error("Cannot update L2 gas price", "msg", err) } - time.Sleep(s.pollInterval) } } @@ -350,7 +356,8 @@ func (s *SyncService) verify() error { // transactions and then updates the EthContext. func (s *SyncService) SequencerLoop() { log.Info("Starting Sequencer Loop", "poll-interval", s.pollInterval, "timestamp-refresh-threshold", s.timestampRefreshThreshold) - for { + t := time.NewTicker(s.pollInterval) + for ; true; <-t.C { if err := s.updateL1GasPrice(); err != nil { log.Error("Cannot update L1 gas price", "msg", err) } @@ -366,7 +373,6 @@ func (s *SyncService) SequencerLoop() { if err := s.updateContext(); err != nil { log.Error("Could not update execution context", "error", err) } - time.Sleep(s.pollInterval) } } @@ -445,13 +451,7 @@ func (s *SyncService) updateL2GasPrice(hash *common.Hash) error { return err } result := state.GetState(s.gpoAddress, l2GasPriceSlot) - gasPrice := result.Big() - if err := core.VerifyL2GasPrice(gasPrice); err != nil { - gp := core.RoundL2GasPrice(gasPrice) - log.Warn("Invalid gas price detected in state", "state", gasPrice, "using", gp) - gasPrice = gp - } - s.RollupGpo.SetL2GasPrice(gasPrice) + s.RollupGpo.SetL2GasPrice(result.Big()) return nil } @@ -728,11 +728,18 @@ func (s *SyncService) applyBatchedTransaction(tx *types.Transaction) error { // verifyFee will verify that a valid fee is being paid. func (s *SyncService) verifyFee(tx *types.Transaction) error { - // Exit early if fees are enforced and the gasPrice is set to 0 - if s.enforceFees && tx.GasPrice().Cmp(common.Big0) == 0 { - return errors.New("cannot accept 0 gas price transaction") + if tx.GasPrice().Cmp(common.Big0) == 0 { + // Exit early if fees are enforced and the gasPrice is set to 0 + if s.enforceFees { + return errors.New("cannot accept 0 gas price transaction") + } + // If fees are not enforced and the gas price is 0, return early + return nil + } + // When the gas price is non zero, it must be equal to the constant + if tx.GasPrice().Cmp(fees.BigTxGasPrice) != 0 { + return fmt.Errorf("tx.gasPrice must be %d", fees.TxGasPrice) } - l1GasPrice, err := s.RollupGpo.SuggestL1GasPrice(context.Background()) if err != nil { return err @@ -743,22 +750,29 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error { } // Calculate the fee based on decoded L2 gas limit gas := new(big.Int).SetUint64(tx.Gas()) - l2GasLimit := core.DecodeL2GasLimit(gas) - fee, err := core.CalculateRollupFee(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice) + l2GasLimit := fees.DecodeL2GasLimit(gas) + // Only count the calldata here as the overhead of the fully encoded + // RLP transaction is handled inside of EncodeL2GasLimit + fee := fees.EncodeTxGasLimit(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice) if err != nil { return err } - // If fees are not enforced and the gas price is 0, return early - if !s.enforceFees && tx.GasPrice().Cmp(common.Big0) == 0 { - return nil - } // This should only happen if the transaction fee is greater than 18.44 ETH if !fee.IsUint64() { return fmt.Errorf("fee overflow: %s", fee.String()) } - // Make sure that the fee is paid - if tx.Gas() < fee.Uint64() { - return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = 1", tx.Gas(), fee.Uint64()) + // Compute the user's fee + paying := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice()) + // Compute the minimum expected fee + expecting := new(big.Int).Mul(fee, fees.BigTxGasPrice) + if paying.Cmp(expecting) == -1 { + return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d", paying, fee.Uint64(), fees.BigTxGasPrice) + } + // Protect users from overpaying by too much + overpaying := new(big.Int).Sub(paying, expecting) + threshold := new(big.Int).Mul(expecting, common.Big3) + if overpaying.Cmp(threshold) == 1 { + return fmt.Errorf("fee too large: %d", paying) } return nil } @@ -776,7 +790,6 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction if err := s.verifyFee(tx); err != nil { return err } - s.txLock.Lock() defer s.txLock.Unlock() log.Trace("Sequencer transaction validation", "hash", tx.Hash().Hex()) diff --git a/l2geth/rollup/sync_service_test.go b/l2geth/rollup/sync_service_test.go index d23f5ad304d4..705539be7fe4 100644 --- a/l2geth/rollup/sync_service_test.go +++ b/l2geth/rollup/sync_service_test.go @@ -507,7 +507,8 @@ func TestSyncServiceL1GasPrice(t *testing.T) { t.Fatal(err) } - if gasAfter.Cmp(core.RoundL1GasPrice(big.NewInt(1))) != 0 { + expect, _ := service.client.GetL1GasPrice() + if gasAfter.Cmp(expect) != 0 { t.Fatal("expected 100 gas price, got", gasAfter) } } @@ -533,7 +534,7 @@ func TestSyncServiceL2GasPrice(t *testing.T) { if err != nil { t.Fatal("Cannot get state db") } - l2GasPrice := big.NewInt(100000001) + l2GasPrice := big.NewInt(100000000000) state.SetState(service.gpoAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice)) root, _ := state.Commit(false) @@ -824,7 +825,7 @@ func (m *mockClient) SyncStatus(backend Backend) (*SyncStatus, error) { } func (m *mockClient) GetL1GasPrice() (*big.Int, error) { - price := core.RoundL1GasPrice(big.NewInt(2)) + price := big.NewInt(1) return price, nil } diff --git a/ops/README.md b/ops/README.md index 488e3a6f2a62..1dc7858efb0e 100644 --- a/ops/README.md +++ b/ops/README.md @@ -14,6 +14,8 @@ The base `docker-compose.yml` file will start the required components for a full Supplementing the base configuration is an additional metric enabling file, `docker-compose-metrics.yml`. Adding this configuration to the stack will enable metric emission for l2geth and start grafana (for metrics visualisation) and influxdb (for metric collection) instances. +Also available for testing is the `rpc-proxy` service in the `docker-compose-rpc-proxy.yml` file. It can be used to restrict what RPC methods are allowed to the Sequencer. + The base stack can be started and stopped with a command like this (there is no need to specify the default docker-compose.yml) ``` docker-compose \ diff --git a/ops/docker-compose-rpc-proxy.yml b/ops/docker-compose-rpc-proxy.yml new file mode 100644 index 000000000000..dccd31c1cb07 --- /dev/null +++ b/ops/docker-compose-rpc-proxy.yml @@ -0,0 +1,17 @@ +version: "3" +services: + rpc-proxy: + depends_on: + - l1_chain + - deployer + - l2geth + image: rpc-proxy + build: + context: .. + dockerfile: ./ops/docker/Dockerfile.rpc-proxy + environment: + SEQUENCER: l2geth:8545 + ETH_CALLS_ALLOWED: eth_blockNumber,eth_sendRawTransaction + ports: + - 9546:8080 + - 9145:9145 diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml index 2470c5e9dc8a..496f3738705f 100644 --- a/ops/docker-compose.yml +++ b/ops/docker-compose.yml @@ -80,6 +80,7 @@ services: - ./envs/geth.env environment: ETH1_HTTP: http://l1_chain:8545 + ROLLUP_TIMESTAMP_REFRESH: 5s ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json # used for getting the addresses URL: http://deployer:8081/addresses.json @@ -172,3 +173,4 @@ services: URL: http://deployer:8081/addresses.json ENABLE_GAS_REPORT: 1 NO_NETWORK: 1 + diff --git a/ops/docker/Dockerfile.geth b/ops/docker/Dockerfile.geth index 03f4a8b25bff..9b52339f9dd6 100644 --- a/ops/docker/Dockerfile.geth +++ b/ops/docker/Dockerfile.geth @@ -1,5 +1,5 @@ # Build Geth in a stock Go builder container -FROM golang:1.14-alpine as builder +FROM golang:1.15-alpine as builder RUN apk add --no-cache make gcc musl-dev linux-headers git diff --git a/ops/docker/Dockerfile.rpc-proxy b/ops/docker/Dockerfile.rpc-proxy new file mode 100644 index 000000000000..c0e03b93bd14 --- /dev/null +++ b/ops/docker/Dockerfile.rpc-proxy @@ -0,0 +1,21 @@ +FROM openresty/openresty:buster +LABEL maintainer="Optimistic Systems " +ARG GOTEMPLATE_VERSION=v3.9.0 + +RUN DEBIAN_FRONTEND=noninteractive apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + openresty-opm \ + && opm get knyar/nginx-lua-prometheus + +RUN curl -o /usr/local/bin/gomplate \ + -sSL https://github.com/hairyhenderson/gomplate/releases/download/$GOTEMPLATE_VERSION/gomplate_linux-amd64-slim \ + && chmod +x /usr/local/bin/gomplate + +RUN mkdir -p /var/log/nginx/ \ + && ln -sf /dev/stdout /var/log/nginx/access.log \ + && ln -sf /dev/stderr /var/log/nginx/error.log + +COPY ./ops/docker/rpc-proxy/eth-jsonrpc-access.lua /usr/local/openresty/nginx/eth-jsonrpc-access.lua +COPY ./ops/docker/rpc-proxy/nginx.template.conf /docker-entrypoint.d/nginx.template.conf +COPY ./ops/docker/rpc-proxy/docker-entrypoint.sh /docker-entrypoint.sh +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/ops/docker/hardhat/yarn.lock b/ops/docker/hardhat/yarn.lock index bad1ef0d6d31..5d7149c252ea 100644 --- a/ops/docker/hardhat/yarn.lock +++ b/ops/docker/hardhat/yarn.lock @@ -2252,9 +2252,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= ws@^7.2.1: - version "7.4.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0: version "4.0.2" diff --git a/ops/docker/rpc-proxy/docker-entrypoint.sh b/ops/docker/rpc-proxy/docker-entrypoint.sh new file mode 100755 index 000000000000..477f1dd8f934 --- /dev/null +++ b/ops/docker/rpc-proxy/docker-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -eo pipefail + +if [ -z "$SEQUENCER" ];then + echo "SEQUENCER env must be set, exiting" + exit 1 +fi + +if [ -z "$ETH_CALLS_ALLOWED" ];then + echo "ETH_CALLS_ALLOWED env must be set, exiting" + exit 1 +fi + +gomplate -f /docker-entrypoint.d/nginx.template.conf > /usr/local/openresty/nginx/conf/nginx.conf + +cat /usr/local/openresty/nginx/conf/nginx.conf + +exec openresty "$@" diff --git a/ops/docker/rpc-proxy/eth-jsonrpc-access.lua b/ops/docker/rpc-proxy/eth-jsonrpc-access.lua new file mode 100644 index 000000000000..3f2280ec815f --- /dev/null +++ b/ops/docker/rpc-proxy/eth-jsonrpc-access.lua @@ -0,0 +1,91 @@ +-- Source: https://github.com/adetante/ethereum-nginx-proxy +local cjson = require('cjson') + +local function empty(s) + return s == nil or s == '' +end + +local function split(s) + local res = {} + local i = 1 + for v in string.gmatch(s, "([^,]+)") do + res[i] = v + i = i + 1 + end + return res +end + +local function contains(arr, val) + for i, v in ipairs (arr) do + if v == val then + return true + end + end + return false +end + +-- parse conf +local blacklist, whitelist = nil +if not empty(ngx.var.jsonrpc_blacklist) then + blacklist = split(ngx.var.jsonrpc_blacklist) +end +if not empty(ngx.var.jsonrpc_whitelist) then + whitelist = split(ngx.var.jsonrpc_whitelist) +end + +-- check conf +if blacklist ~= nil and whitelist ~= nil then + ngx.log(ngx.ERR, 'invalid conf: jsonrpc_blacklist and jsonrpc_whitelist are both set') + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + return +end + +-- get request content +ngx.req.read_body() + +-- try to parse the body as JSON +local success, body = pcall(cjson.decode, ngx.var.request_body); +if not success then + ngx.log(ngx.ERR, 'invalid JSON request') + ngx.exit(ngx.HTTP_BAD_REQUEST) + return +end + +local method = body['method'] +local version = body['jsonrpc'] + +-- check we have a method and a version +if empty(method) or empty(version) then + ngx.log(ngx.ERR, 'no method and/or jsonrpc attribute') + ngx.exit(ngx.HTTP_BAD_REQUEST) + return +end + +metric_sequencer_requests:inc(1, {method, ngx.var.server_name, ngx.var.status}) + +-- check the version is supported +if version ~= "2.0" then + ngx.log(ngx.ERR, 'jsonrpc version not supported: ' .. version) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) + return +end + +-- if whitelist is configured, check that the method is whitelisted +if whitelist ~= nil then + if not contains(whitelist, method) then + ngx.log(ngx.ERR, 'jsonrpc method is not whitelisted: ' .. method) + ngx.exit(ngx.HTTP_FORBIDDEN) + return + end +end + +-- if blacklist is configured, check that the method is not blacklisted +if blacklist ~= nil then + if contains(blacklist, method) then + ngx.log(ngx.ERR, 'jsonrpc method is blacklisted: ' .. method) + ngx.exit(ngx.HTTP_FORBIDDEN) + return + end +end + +return diff --git a/ops/docker/rpc-proxy/nginx.template.conf b/ops/docker/rpc-proxy/nginx.template.conf new file mode 100644 index 000000000000..4530b5ddb849 --- /dev/null +++ b/ops/docker/rpc-proxy/nginx.template.conf @@ -0,0 +1,80 @@ +worker_processes 5; +daemon off; +error_log /var/log/nginx/error.log; +worker_rlimit_nofile 8192; +pcre_jit on; + +events { + worker_connections 4096; +} + +http { + include mime.types; + index index.html; + + # See Move default writable paths to a dedicated directory (#119) + # https://github.com/openresty/docker-openresty/issues/119 + client_body_temp_path /var/run/openresty/nginx-client-body; + proxy_temp_path /var/run/openresty/nginx-proxy; + fastcgi_temp_path /var/run/openresty/nginx-fastcgi; + uwsgi_temp_path /var/run/openresty/nginx-uwsgi; + scgi_temp_path /var/run/openresty/nginx-scgi; + + keepalive_timeout 0; + + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] $status ' + '"$request" $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + tcp_nopush on; + + lua_shared_dict prometheus_metrics 10M; + init_worker_by_lua_block { + prometheus = require("prometheus").init("prometheus_metrics") + metric_requests = prometheus:counter( + "nginx_http_requests_total", "Number of HTTP requests", {"host", "status"}) + metric_sequencer_requests = prometheus:counter( + "nginx_eth_sequencer_requests", "Number of requests going to the sequencer", {"method", "host", "status"}) + metric_replica_requests = prometheus:counter( + "nginx_eth_replica_requests", "Number of requests going to the replicas", {"host", "status"}) + metric_latency = prometheus:histogram( + "nginx_http_request_duration_seconds", "HTTP request latency", {"host"}) + metric_connections = prometheus:gauge( + "nginx_http_connections", "Number of HTTP connections", {"state"}) + } + log_by_lua_block { + metric_requests:inc(1, {ngx.var.server_name, ngx.var.status}) + metric_latency:observe(tonumber(ngx.var.request_time), {ngx.var.server_name}) + } + + upstream sequencer { + server {{env.Getenv "SEQUENCER"}}; + } + + server { # RPC proxy server + listen 8080; + location = /healthz { + return 200 'healthz'; + } + location / { + set $jsonrpc_whitelist {{env.Getenv "ETH_CALLS_ALLOWED"}}; + access_by_lua_file 'eth-jsonrpc-access.lua'; + proxy_pass http://sequencer; + } + } + + server { # Metrics server + listen 9145; + location /metrics { + content_by_lua_block { + metric_connections:set(ngx.var.connections_reading, {"reading"}) + metric_connections:set(ngx.var.connections_waiting, {"waiting"}) + metric_connections:set(ngx.var.connections_writing, {"writing"}) + prometheus:collect() + } + } + } + +} \ No newline at end of file diff --git a/ops/envs/geth.env b/ops/envs/geth.env index bf73226cf815..358a41b0d366 100644 --- a/ops/envs/geth.env +++ b/ops/envs/geth.env @@ -9,10 +9,12 @@ ROLLUP_POLL_INTERVAL_FLAG=500ms ROLLUP_ENABLE_L2_GAS_POLLING=true # ROLLUP_ENFORCE_FEES= +ETHERBASE=0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf + RPC_ENABLE=true RPC_ADDR=0.0.0.0 RPC_PORT=8545 -RPC_API=eth,net,rollup,web3 +RPC_API=eth,net,rollup,web3,debug RPC_CORS_DOMAIN=* RPC_VHOSTS=* diff --git a/packages/batch-submitter/CHANGELOG.md b/packages/batch-submitter/CHANGELOG.md index be440911b141..6b0e1457b7fb 100644 --- a/packages/batch-submitter/CHANGELOG.md +++ b/packages/batch-submitter/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 0.3.4 + +### Patch Changes + +- baa3b761: Improve Sentry support, initializing as needed and ensuring ERROR logs route to Sentry +- cc742715: Fix typo in USE_HARDHAT config +- 98b7839f: Change monotonicity band-aid code to log warnings not errors +- c520100d: Fix a bug in fixMonotonicity auto healer +- 85362d44: Log additional data in monotonicity violation +- Updated dependencies [baa3b761] + - @eth-optimism/common-ts@0.1.3 + +## 0.3.3 + +### Patch Changes + +- 750a5021: Remove dead imports from core-utils +- Updated dependencies [a64f8161] +- Updated dependencies [4e03f8a9] +- Updated dependencies [8e2bfd07] +- Updated dependencies [750a5021] +- Updated dependencies [c2b6e14b] +- Updated dependencies [245136f1] + - @eth-optimism/core-utils@0.4.5 + - @eth-optimism/contracts@0.3.5 + ## 0.3.2 ### Patch Changes diff --git a/packages/batch-submitter/package.json b/packages/batch-submitter/package.json index a2b2ed31d71c..102b8c8f6773 100644 --- a/packages/batch-submitter/package.json +++ b/packages/batch-submitter/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/batch-submitter", - "version": "0.3.2", + "version": "0.3.4", "private": true, "description": "[Optimism] Batch submission for sequencer & aggregators", "main": "dist/index", @@ -31,12 +31,13 @@ "url": "https://github.com/ethereum-optimism/optimism-monorepo.git" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.2", - "@eth-optimism/contracts": "^0.3.1", - "@eth-optimism/core-utils": "^0.4.3", + "@eth-optimism/common-ts": "^0.1.3", + "@eth-optimism/contracts": "^0.3.5", + "@eth-optimism/core-utils": "^0.4.5", "@eth-optimism/ynatm": "^0.2.2", "@ethersproject/abstract-provider": "^5.0.5", "@ethersproject/providers": "^5.0.14", + "@sentry/node": "^6.2.5", "bcfg": "^0.1.6", "bluebird": "^3.7.2", "dotenv": "^8.2.0", @@ -45,7 +46,7 @@ "prom-client": "^13.1.0" }, "devDependencies": { - "@eth-optimism/smock": "^1.1.4", + "@eth-optimism/smock": "^1.1.5", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@types/bluebird": "^3.5.34", diff --git a/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts b/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts index 2ccb68b8a8b9..319dae882081 100644 --- a/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts +++ b/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts @@ -178,15 +178,18 @@ export class StateBatchSubmitter extends BatchSubmitter { const nonce = await this.signer.getTransactionCount() const contractFunction = async (gasPrice): Promise => { + this.logger.info('Submitting appendStateBatch transaction', { + gasPrice, + nonce, + contractAddr: this.chainContract.address, + }) const contractTx = await this.chainContract.appendStateBatch( batch, offsetStartsAtIndex, { nonce, gasPrice } ) this.logger.info('Submitted appendStateBatch transaction', { - nonce, txHash: contractTx.hash, - contractAddr: this.chainContract.address, from: contractTx.from, }) this.logger.debug('appendStateBatch transaction data', { diff --git a/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts b/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts index b52be781aa52..713c2a560aa8 100644 --- a/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts +++ b/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts @@ -143,14 +143,17 @@ export class TransactionBatchSubmitter extends BatchSubmitter { const contractFunction = async ( gasPrice ): Promise => { + this.logger.info('Submitting appendQueueBatch transaction', { + gasPrice, + nonce, + contractAddr: this.chainContract.address, + }) const tx = await this.chainContract.appendQueueBatch(99999999, { nonce, gasPrice, }) this.logger.info('Submitted appendQueueBatch transaction', { - nonce, txHash: tx.hash, - contractAddr: this.chainContract.address, from: tx.from, }) this.logger.debug('appendQueueBatch transaction data', { @@ -250,14 +253,17 @@ export class TransactionBatchSubmitter extends BatchSubmitter { const nonce = await this.signer.getTransactionCount() const contractFunction = async (gasPrice): Promise => { + this.logger.info('Submitting appendSequencerBatch transaction', { + gasPrice, + nonce, + contractAddr: this.chainContract.address, + }) const tx = await this.chainContract.appendSequencerBatch(batchParams, { nonce, gasPrice, }) this.logger.info('Submitted appendSequencerBatch transaction', { - nonce, txHash: tx.hash, - contractAddr: this.chainContract.address, from: tx.from, }) this.logger.debug('appendSequencerBatch transaction data', { @@ -350,13 +356,17 @@ export class TransactionBatchSubmitter extends BatchSubmitter { // Verify all of the batch elements are monotonic let lastTimestamp: number let lastBlockNumber: number - for (const ele of batch) { + for (const [idx, ele] of batch.entries()) { if (ele.timestamp < lastTimestamp) { - this.logger.error('Timestamp monotonicity violated! Element', { ele }) + this.logger.error('Timestamp monotonicity violated! Element', { + idx, + ele, + }) return false } if (ele.blockNumber < lastBlockNumber) { this.logger.error('Block Number monotonicity violated! Element', { + idx, ele, }) return false @@ -463,7 +473,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ] = await this.chainContract.getQueueElement(nextQueueIndex) if (timestamp < ele.timestamp || blockNumber < ele.blockNumber) { - this.logger.error('Fixing skipped deposit', { + this.logger.warn('Fixing skipped deposit', { badTimestamp: ele.timestamp, skippedQueueTimestamp: timestamp, badBlockNumber: ele.blockNumber, @@ -557,38 +567,31 @@ export class TransactionBatchSubmitter extends BatchSubmitter { ele.timestamp < earliestTimestamp || ele.blockNumber < earliestBlockNumber ) { - this.logger.error('Fixing timestamp/blockNumber too small', { + this.logger.warn('Fixing timestamp/blockNumber too small', { oldTimestamp: ele.timestamp, newTimestamp: earliestTimestamp, oldBlockNumber: ele.blockNumber, newBlockNumber: earliestBlockNumber, }) - fixedBatch.push({ - ...ele, - timestamp: earliestTimestamp, - blockNumber: earliestBlockNumber, - }) - continue + ele.timestamp = earliestTimestamp + ele.blockNumber = earliestBlockNumber } // Fix the element if its timestammp/blockNumber is too large if ( ele.timestamp > latestTimestamp || ele.blockNumber > latestBlockNumber ) { - this.logger.error('Fixing timestamp/blockNumber too large.', { + this.logger.warn('Fixing timestamp/blockNumber too large.', { oldTimestamp: ele.timestamp, newTimestamp: latestTimestamp, oldBlockNumber: ele.blockNumber, newBlockNumber: latestBlockNumber, }) - fixedBatch.push({ - ...ele, - timestamp: latestTimestamp, - blockNumber: latestBlockNumber, - }) - continue + ele.timestamp = latestTimestamp + ele.blockNumber = latestBlockNumber } - // No fixes needed! + earliestTimestamp = ele.timestamp + earliestBlockNumber = ele.blockNumber fixedBatch.push(ele) } return fixedBatch diff --git a/packages/batch-submitter/src/exec/run-batch-submitter.ts b/packages/batch-submitter/src/exec/run-batch-submitter.ts index d1439d64520f..e3f21a3139cd 100644 --- a/packages/batch-submitter/src/exec/run-batch-submitter.ts +++ b/packages/batch-submitter/src/exec/run-batch-submitter.ts @@ -1,5 +1,6 @@ /* External Imports */ import { injectL2Context, Bcfg } from '@eth-optimism/core-utils' +import * as Sentry from '@sentry/node' import { Logger, Metrics, createMetricsServer } from '@eth-optimism/common-ts' import { exit } from 'process' import { Signer, Wallet } from 'ethers' @@ -101,22 +102,24 @@ export const run = async () => { let logger if (config.bool('use-sentry', env.USE_SENTRY === 'true')) { + const sentryOptions = { + release, + dsn: sentryDsn, + tracesSampleRate: sentryTraceRate, + environment: network, + } + Sentry.init(sentryOptions) // Initialize Sentry for Batch Submitter deployed to a network logger = new Logger({ name, - sentryOptions: { - release, - dsn: sentryDsn, - tracesSampleRate: sentryTraceRate, - environment: network, - }, + sentryOptions, }) } else { // Skip initializing Sentry logger = new Logger({ name }) } - const useHardhat = config.bool('use-hardhat', !!env.USE_HARDAT) + const useHardhat = config.bool('use-hardhat', !!env.USE_HARDHAT) const DEBUG_IMPERSONATE_SEQUENCER_ADDRESS = config.str( 'debug-impersonate-sequencer-address', env.DEBUG_IMPERSONATE_SEQUENCER_ADDRESS diff --git a/packages/batch-submitter/test/batch-submitter/batch-submitter.spec.ts b/packages/batch-submitter/test/batch-submitter/batch-submitter.spec.ts index f0b0e4686e8e..dffbdc33867c 100644 --- a/packages/batch-submitter/test/batch-submitter/batch-submitter.spec.ts +++ b/packages/batch-submitter/test/batch-submitter/batch-submitter.spec.ts @@ -33,7 +33,6 @@ import { QueueOrigin, Batch, Signature, - TxType, remove0x, } from '@eth-optimism/core-utils' import { Logger, Metrics } from '@eth-optimism/common-ts' @@ -253,7 +252,7 @@ describe('BatchSubmitter', () => { { rawTransaction: '0x1234', l1BlockNumber: nextQueueElement.blockNumber - 1, - txType: TxType.EIP155, + txType: 0, queueOrigin: QueueOrigin.Sequencer, l1TxOrigin: null, } as any, @@ -302,7 +301,7 @@ describe('BatchSubmitter', () => { { rawTransaction: '0x1234', l1BlockNumber: nextQueueElement.blockNumber - 1, - txType: TxType.EthSign, + txType: 1, queueOrigin: QueueOrigin.Sequencer, l1TxOrigin: null, } as any, @@ -406,7 +405,7 @@ describe('BatchSubmitter', () => { { rawTransaction: '0x1234', l1BlockNumber: nextQueueElement.blockNumber - 1, - txType: TxType.EIP155, + txType: 0, queueOrigin: QueueOrigin.Sequencer, l1TxOrigin: null, } as any, diff --git a/packages/common-ts/CHANGELOG.md b/packages/common-ts/CHANGELOG.md index 85386b02aaad..60781b255f87 100644 --- a/packages/common-ts/CHANGELOG.md +++ b/packages/common-ts/CHANGELOG.md @@ -1,5 +1,11 @@ # @eth-optimism/common-ts +## 0.1.3 + +### Patch Changes + +- baa3b761: Improve Sentry support, initializing as needed and ensuring ERROR logs route to Sentry + ## 0.1.2 ### Patch Changes diff --git a/packages/common-ts/package.json b/packages/common-ts/package.json index 8bf3d9b75052..ac3f92b2d573 100644 --- a/packages/common-ts/package.json +++ b/packages/common-ts/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/common-ts", - "version": "0.1.2", + "version": "0.1.3", "main": "dist/index", "files": [ "dist/*" diff --git a/packages/common-ts/src/base-service.ts b/packages/common-ts/src/base-service.ts index df21cf200601..1f1fee6bfd7b 100644 --- a/packages/common-ts/src/base-service.ts +++ b/packages/common-ts/src/base-service.ts @@ -9,6 +9,11 @@ type OptionSettings = { } } +type BaseServiceOptions = T & { + logger?: Logger + metrics?: Metrics +} + /** * Base for other "Service" objects. Handles your standard initialization process, can dynamically * start and stop. @@ -21,11 +26,18 @@ export class BaseService { protected initialized: boolean = false protected running: boolean = false - constructor(name: string, options: T, optionSettings: OptionSettings) { + constructor( + name: string, + options: BaseServiceOptions, + optionSettings: OptionSettings + ) { validateOptions(options, optionSettings) this.name = name this.options = mergeDefaultOptions(options, optionSettings) - this.logger = new Logger({ name }) + this.logger = options.logger || new Logger({ name }) + if (options.metrics) { + this.metrics = options.metrics + } } /** diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md index ae5579530c4f..857f869507d8 100644 --- a/packages/contracts/CHANGELOG.md +++ b/packages/contracts/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.3.5 + +### Patch Changes + +- 4e03f8a9: Update contracts README to add deploy instructions. +- 8e2bfd07: Introduces the congestion price oracle contract +- 245136f1: Minor change to how deploy.ts is invoked +- Updated dependencies [a64f8161] +- Updated dependencies [750a5021] +- Updated dependencies [c2b6e14b] + - @eth-optimism/core-utils@0.4.5 + ## 0.3.4 ### Patch Changes diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 9c2280db7813..9c7ed7b7b1cc 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -84,5 +84,81 @@ You can also build specific components as follows: yarn build:contracts ``` +### Deploying the Contracts +To deploy the contracts first clone, install, and build the contracts package. + +Next set the following env vars: + +```bash +CONTRACTS_TARGET_NETWORK=... +CONTRACTS_DEPLOYER_KEY=... +CONTRACTS_RPC_URL=... +``` + +Then to perform the actual deployment run: + +```bash +npx hardhat deploy \ + --network ... \ # `network` MUST equal your env var `CONTRACTS_TARGET_NETWORK` + --ovm-address-manager-owner ... \ + --ovm-proposer-address ... \ + --ovm-relayer-address ... \ + --ovm-sequencer-address ... \ + --scc-fraud-proof-window ... \ + --scc-sequencer-publish-window ... +``` + +This will deploy the contracts to the network specified in your env and create +an artifacts directory in `./deployments`. + +To view all deployment options run: + +```bash +npx hardhat deploy --help + +Hardhat version 2.2.1 + +Usage: hardhat [GLOBAL OPTIONS] deploy [--ctc-force-inclusion-period-seconds ] [--ctc-max-transaction-gas-limit ] --deploy-scripts [--em-max-gas-per-queue-per-epoch ] [--em-max-transaction-gas-limit ] [--em-min-transaction-gas-limit ] [--em-ovm-chain-id ] [--em-seconds-per-epoch ] --export --export-all --gasprice [--l1-block-time-seconds ] [--no-compile] [--no-impersonation] --ovm-address-manager-owner --ovm-proposer-address --ovm-relayer-address --ovm-sequencer-address [--reset] [--scc-fraud-proof-window ] [--scc-sequencer-publish-window ] [--silent] --tags [--watch] --write + +OPTIONS: + + --ctc-force-inclusion-period-seconds Number of seconds that the sequencer has to include transactions before the L1 queue. (default: 2592000) + --ctc-max-transaction-gas-limit Max gas limit for L1 queue transactions. (default: 9000000) + --deploy-scripts override deploy script folder path + --em-max-gas-per-queue-per-epoch Maximum gas allowed in a given queue for each epoch. (default: 250000000) + --em-max-transaction-gas-limit Maximum allowed transaction gas limit. (default: 9000000) + --em-min-transaction-gas-limit Minimum allowed transaction gas limit. (default: 50000) + --em-ovm-chain-id Chain ID for the L2 network. (default: 420) + --em-seconds-per-epoch Number of seconds in each epoch. (default: 0) + --export export current network deployments + --export-all export all deployments into one file + --gasprice gas price to use for transactions + --l1-block-time-seconds Number of seconds on average between every L1 block. (default: 15) + --no-compile disable pre compilation + --no-impersonation do not impersonate unknown accounts + --ovm-address-manager-owner Address that will own the Lib_AddressManager. Must be provided or this deployment will fail. + --ovm-proposer-address Address of the account that will propose state roots. Must be provided or this deployment will fail. + --ovm-relayer-address Address of the message relayer. Must be provided or this deployment will fail. + --ovm-sequencer-address Address of the sequencer. Must be provided or this deployment will fail. + --reset whether to delete deployments files first + --scc-fraud-proof-window Number of seconds until a transaction is considered finalized. (default: 604800) + --scc-sequencer-publish-window Number of seconds that the sequencer is exclusively allowed to post state roots. (default: 1800) + --silent whether to remove log + --tags specify which deploy script to execute via tags, separated by commas + --watch redeploy on every change of contract or deploy script + --write whether to write deployments to file + +deploy: Deploy contracts + +For global options help run: hardhat help +``` + +### Verifying Deployments on Etherscan +If you are using a network which Etherscan supports you can verify your contracts with: + +```bash +npx hardhat etherscan-verify --api-key ... --network ... +``` + ## Security Please refer to our [Security Policy](https://github.com/ethereum-optimism/.github/security/policy) for information about how to disclose security issues with this code. diff --git a/packages/contracts/bin/deploy.ts b/packages/contracts/bin/deploy.ts index 719ac691c4fa..7ac14d6615e1 100755 --- a/packages/contracts/bin/deploy.ts +++ b/packages/contracts/bin/deploy.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env ts-node-script - import { Wallet } from 'ethers' import path from 'path' import dirtree from 'directory-tree' diff --git a/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_GasPriceOracle.sol b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_GasPriceOracle.sol new file mode 100644 index 000000000000..03cc96a860b0 --- /dev/null +++ b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_GasPriceOracle.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; + +/* External Imports */ +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title OVM_GasPriceOracle + * @dev This contract exposes the current l2 gas price, a measure of how congested the network + * currently is. This measure is used by the Sequencer to determine what fee to charge for + * transactions. When the system is more congested, the l2 gas price will increase and fees + * will also increase as a result. + * + * Compiler used: optimistic-solc + * Runtime target: OVM + */ +contract OVM_GasPriceOracle is Ownable { + + /************* + * Variables * + *************/ + + // Current l2 gas price + uint256 public gasPrice; + + /*************** + * Constructor * + ***************/ + + /** + * @param _owner Address that will initially own this contract. + */ + constructor( + address _owner, + uint256 _initialGasPrice + ) + Ownable() + { + setGasPrice(_initialGasPrice); + transferOwnership(_owner); + } + + + /******************** + * Public Functions * + ********************/ + + /** + * Allows the owner to modify the l2 gas price. + * @param _gasPrice New l2 gas price. + */ + function setGasPrice( + uint256 _gasPrice + ) + public + onlyOwner + { + gasPrice = _gasPrice; + } +} diff --git a/packages/contracts/deploy-l2/000-OVM_GasPriceOracle.deploy.ts b/packages/contracts/deploy-l2/000-OVM_GasPriceOracle.deploy.ts new file mode 100644 index 000000000000..96eb8fd1b175 --- /dev/null +++ b/packages/contracts/deploy-l2/000-OVM_GasPriceOracle.deploy.ts @@ -0,0 +1,31 @@ +/* Imports: External */ +import { DeployFunction } from 'hardhat-deploy/dist/types' + +/* Imports: Internal */ +import { getContractDefinition } from '../src' + +const deployFn: DeployFunction = async (hre: any) => { + const { deployments, getNamedAccounts } = hre + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + const gasPriceOracle = getContractDefinition('OVM_GasPriceOracle', true) + + const gasOracleOwner = (hre as any).deployConfig.ovmSequencerAddress + const initialGasPrice = (hre as any).deployConfig.initialGasPriceOracleGasPrice + + if (!gasOracleOwner || !initialGasPrice) { + throw new Error('initialGasPrice & ovmSequencerAddress required to deploy gas price oracle') + } + + await deploy('OVM_GasPriceOracle', { + contract: gasPriceOracle, + from: deployer, + args: [gasOracleOwner, initialGasPrice], + log: true, + }); +} + +deployFn.tags = ['OVM_GasPriceOracle'] + +export default deployFn diff --git a/packages/contracts/deployments/README.md b/packages/contracts/deployments/README.md index 133f6ba27b34..a6d5b7156e9b 100644 --- a/packages/contracts/deployments/README.md +++ b/packages/contracts/deployments/README.md @@ -1,6 +1,15 @@ # Optimism Regenesis Deployments ## LAYER 2 +## OPTIMISTIC-KOVAN + +Network : __optimistic-kovan (chain id: 69)__ + +|Contract|Address| +|--|--| +|OVM_GasPriceOracle|[0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76](https://kovan-optimistic.etherscan.io/address/0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76)| +--- + ### Chain IDs: - Mainnet: 10 - Kovan: 69 diff --git a/packages/contracts/deployments/optimistic-kovan/.chainId b/packages/contracts/deployments/optimistic-kovan/.chainId new file mode 100644 index 000000000000..8c0474e3239f --- /dev/null +++ b/packages/contracts/deployments/optimistic-kovan/.chainId @@ -0,0 +1 @@ +69 \ No newline at end of file diff --git a/packages/contracts/deployments/optimistic-kovan/OVM_GasPriceOracle.json b/packages/contracts/deployments/optimistic-kovan/OVM_GasPriceOracle.json new file mode 100644 index 000000000000..3ad5351c0abc --- /dev/null +++ b/packages/contracts/deployments/optimistic-kovan/OVM_GasPriceOracle.json @@ -0,0 +1,164 @@ +{ + "address": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_initialGasPrice", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "gasPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_gasPrice", + "type": "uint256" + } + ], + "name": "setGasPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "transactionHash": "0xed5fd0757566bc0bc1f3f7d701e31199835d6fe7b1e74353ad502983f2f5e744", + "receipt": { + "to": null, + "from": "0x18394B52d3Cb931dfA76F63251919D051953413d", + "contractAddress": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76", + "transactionIndex": 0, + "gasUsed": "1732518", + "logsBloom": "0x00000000000000000000000000000000000000000000000000840000000000000000000000000000000000100000000000000000000000140000000000000000000000000100000000000008000000000001000010000000000000000000000400000000020000000000000000008800000000000000000000400010000000400000000000000000000000000000000000000000002000000000000000000000000000000000000000010000000000000000000000000000000000000000000000008002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x310208064b53df696581d48cee7439d3e94acac8c31a519e5d07e7cc542c920c", + "transactionHash": "0xed5fd0757566bc0bc1f3f7d701e31199835d6fe7b1e74353ad502983f2f5e744", + "logs": [ + { + "transactionIndex": 0, + "blockNumber": 336546, + "transactionHash": "0xed5fd0757566bc0bc1f3f7d701e31199835d6fe7b1e74353ad502983f2f5e744", + "address": "0x4200000000000000000000000000000000000006", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x00000000000000000000000018394b52d3cb931dfa76f63251919d051953413d", + "0x0000000000000000000000004200000000000000000000000000000000000005" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000000", + "logIndex": 0, + "blockHash": "0x310208064b53df696581d48cee7439d3e94acac8c31a519e5d07e7cc542c920c" + }, + { + "transactionIndex": 0, + "blockNumber": 336546, + "transactionHash": "0xed5fd0757566bc0bc1f3f7d701e31199835d6fe7b1e74353ad502983f2f5e744", + "address": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000018394b52d3cb931dfa76f63251919d051953413d" + ], + "data": "0x", + "logIndex": 1, + "blockHash": "0x310208064b53df696581d48cee7439d3e94acac8c31a519e5d07e7cc542c920c" + }, + { + "transactionIndex": 0, + "blockNumber": 336546, + "transactionHash": "0xed5fd0757566bc0bc1f3f7d701e31199835d6fe7b1e74353ad502983f2f5e744", + "address": "0x038a8825A3C3B0c08d52Cc76E5E361953Cf6Dc76", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x00000000000000000000000018394b52d3cb931dfa76f63251919d051953413d", + "0x00000000000000000000000018394b52d3cb931dfa76f63251919d051953413d" + ], + "data": "0x", + "logIndex": 2, + "blockHash": "0x310208064b53df696581d48cee7439d3e94acac8c31a519e5d07e7cc542c920c" + } + ], + "blockNumber": 336546, + "cumulativeGasUsed": "1732518", + "status": 1, + "byzantium": true + }, + "args": [ + "0x18394B52d3Cb931dfA76F63251919D051953413d", + 1000000 + ], + "bytecode": "0x60806040523480156200001c5760008062000019620002ea565b50505b5060405162000a7338038062000a73833981810160405260408110156200004d576000806200004a620002ea565b50505b81019080805192919060200180519250600091506200006d9050620000ec565b90508060006001816200007f62000357565b816001600160a01b0302191690836001600160a01b0316021790620000a3620003b9565b5050506001600160a01b038116600060008051602062000a5383398151915260405160405180910390a350620000d981620000fe565b620000e48262000184565b50506200044f565b60005a620000f962000408565b905090565b62000108620000ec565b6001600160a01b03166200011b620002c7565b6001600160a01b031614620001705760405162461bcd60e51b8152602060048201819052602482015260008051602062000a338339815191526044820152606401604051809103906200016d620002ea565b50505b808060016200017e620003b9565b50505050565b6200018e620000ec565b6001600160a01b0316620001a1620002c7565b6001600160a01b031614620001f65760405162461bcd60e51b8152602060048201819052602482015260008051602062000a33833981519152604482015260640160405180910390620001f3620002ea565b50505b6001600160a01b038116620002485760405162461bcd60e51b815260040180806020018281038252602681526020018062000a0d602691396040019150506040518091039062000245620002ea565b50505b806001600160a01b03166000806200025f62000357565b906101000a90046001600160a01b03166001600160a01b031660008051602062000a5383398151915260405160405180910390a3806000600181620002a362000357565b816001600160a01b0302191690836001600160a01b03160217906200017e620003b9565b60008080620002d562000357565b906101000a90046001600160a01b0316905090565b632a2a7adb598160e01b8152600481016020815285602082015260005b868110156200032457808601518282016040015260200162000307565b506020828760640184336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b505050565b6303daa959598160e01b8152836004820152602081602483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b8051935060005b6040811015620003b4576000828201526020016200039b565b505050565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b6000815260206200039b565b6373509064598160e01b8152602081600483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b805160008252935060206200039b565b6105ae806200045f6000396000f3fe60806040523480156100195760008061001661042d565b50505b50600436106100605760003560e01c8063715018a61461006e5780638da5cb5b14610078578063bf1fe4201461009c578063f2fde38b146100c2578063fe173b97146100f1575b60008061006b61042d565b50505b61007661010b565b005b61008061020d565b6040516001600160a01b03909116815260200160405180910390f35b610076600480360360208110156100bb576000806100b861042d565b50505b503561022e565b610076600480360360208110156100e1576000806100de61042d565b50505b50356001600160a01b03166102bc565b6100f9610410565b60405190815260200160405180910390f35b61011361041d565b6001600160a01b031661012461020d565b6001600160a01b0316146101875760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016040518091039061018461042d565b50505b60008080610193610498565b906101000a90046001600160a01b03166001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a36000806001816101e6610498565b816001600160a01b0302191690836001600160a01b03160217906102086104f3565b505050565b60008080610219610498565b906101000a90046001600160a01b0316905090565b61023661041d565b6001600160a01b031661024761020d565b6001600160a01b0316146102aa5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401604051809103906102a761042d565b50505b808060016102b66104f3565b50505050565b6102c461041d565b6001600160a01b03166102d561020d565b6001600160a01b0316146103385760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016040518091039061033561042d565b50505b6001600160a01b0381166103865760405162461bcd60e51b8152600401808060200182810382526026815260200180610588602691396040019150506040518091039061038361042d565b50505b806001600160a01b031660008061039b610498565b906101000a90046001600160a01b03166001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a38060006001816103ee610498565b816001600160a01b0302191690836001600160a01b03160217906102b66104f3565b600161041a610498565b81565b60005a610428610541565b905090565b632a2a7adb598160e01b8152600481016020815285602082015260005b8681101561046557808601518282016040015260200161044a565b506020828760640184336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b505050565b6303daa959598160e01b8152836004820152602081602483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b8051935060005b6040811015610208576000828201526020016104dc565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b6000815260206104dc565b6373509064598160e01b8152602081600483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b805160008252935060206104dc56fe4f776e61626c653a206e6577206f776e657220697320746865207a65726f20616464726573734f776e61626c653a206e6577206f776e657220697320746865207a65726f20616464726573734f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65728be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "deployedBytecode": "0x60806040523480156100195760008061001661042d565b50505b50600436106100605760003560e01c8063715018a61461006e5780638da5cb5b14610078578063bf1fe4201461009c578063f2fde38b146100c2578063fe173b97146100f1575b60008061006b61042d565b50505b61007661010b565b005b61008061020d565b6040516001600160a01b03909116815260200160405180910390f35b610076600480360360208110156100bb576000806100b861042d565b50505b503561022e565b610076600480360360208110156100e1576000806100de61042d565b50505b50356001600160a01b03166102bc565b6100f9610410565b60405190815260200160405180910390f35b61011361041d565b6001600160a01b031661012461020d565b6001600160a01b0316146101875760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016040518091039061018461042d565b50505b60008080610193610498565b906101000a90046001600160a01b03166001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a36000806001816101e6610498565b816001600160a01b0302191690836001600160a01b03160217906102086104f3565b505050565b60008080610219610498565b906101000a90046001600160a01b0316905090565b61023661041d565b6001600160a01b031661024761020d565b6001600160a01b0316146102aa5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401604051809103906102a761042d565b50505b808060016102b66104f3565b50505050565b6102c461041d565b6001600160a01b03166102d561020d565b6001600160a01b0316146103385760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e657260448201526064016040518091039061033561042d565b50505b6001600160a01b0381166103865760405162461bcd60e51b8152600401808060200182810382526026815260200180610588602691396040019150506040518091039061038361042d565b50505b806001600160a01b031660008061039b610498565b906101000a90046001600160a01b03166001600160a01b03167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a38060006001816103ee610498565b816001600160a01b0302191690836001600160a01b03160217906102b66104f3565b600161041a610498565b81565b60005a610428610541565b905090565b632a2a7adb598160e01b8152600481016020815285602082015260005b8681101561046557808601518282016040015260200161044a565b506020828760640184336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b505050565b6303daa959598160e01b8152836004820152602081602483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b8051935060005b6040811015610208576000828201526020016104dc565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b6000815260206104dc565b6373509064598160e01b8152602081600483336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b805160008252935060206104dc56fe4f776e61626c653a206e6577206f776e657220697320746865207a65726f2061646472657373" +} \ No newline at end of file diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 0dd1f1c288c4..eb4e94dddf50 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/contracts", - "version": "0.3.4", + "version": "0.3.5", "main": "dist/index", "files": [ "dist/**/*.js", @@ -32,22 +32,25 @@ "test:contracts": "hardhat test --show-stack-traces", "test:gas": "hardhat test \"test/contracts/OVM/execution/OVM_StateManager.gas-spec.ts\" --no-compile --show-stack-traces", "test:coverage": "NODE_OPTIONS=--max_old_space_size=8192 hardhat coverage", + "test:slither": "slither .", + "pretest:slither": "rm -f @openzeppelin && rm -f @ens && rm -f hardhat && ln -s ../../node_modules/@openzeppelin @openzeppelin && ln -s ../../node_modules/@ens @ens && ln -s ../../node_modules/hardhat hardhat", + "posttest:slither": "rm -f @openzeppelin && rm -f @ens && rm -f hardhat", "lint": "yarn lint:fix && yarn lint:check", "lint:fix": "yarn run lint:fix:typescript", "lint:fix:typescript": "prettier --config .prettierrc.json --write \"hardhat.config.ts\" \"{src,test}/**/*.ts\"", "lint:check": "yarn run lint:typescript", "lint:typescript": "tslint --format stylish --project .", "clean": "rm -rf ./dist ./artifacts ./artifacts-ovm ./cache ./cache-ovm ./tsconfig.build.tsbuildinfo", - "deploy": "./bin/deploy.ts && yarn generate-markdown", + "deploy": "ts-node \"./bin/deploy.ts\" && yarn generate-markdown", "serve": "./bin/serve_dump.sh", "prepublishOnly": "yarn copyfiles -u 2 \"contracts/optimistic-ethereum/**/*\" ./", "postpublish": "rimraf OVM iOVM libraries mockOVM", "prepack": "yarn prepublishOnly", "postpack": "yarn postpublish", - "generate-markdown": "node scripts/generate-markdown.js" + "generate-markdown": "node \"./scripts/generate-markdown.js\"" }, "dependencies": { - "@eth-optimism/core-utils": "^0.4.4", + "@eth-optimism/core-utils": "^0.4.5", "@ethersproject/abstract-provider": "^5.0.8", "@ethersproject/abstract-signer": "^5.1.0", "@ethersproject/contracts": "^5.0.5", @@ -56,7 +59,7 @@ "devDependencies": { "@codechecks/client": "0.1.10-beta", "@eth-optimism/hardhat-ovm": "^0.2.2", - "@eth-optimism/smock": "^1.1.3", + "@eth-optimism/smock": "^1.1.5", "@nomiclabs/hardhat-ethers": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^3.3.0", diff --git a/packages/contracts/slither.config.json b/packages/contracts/slither.config.json new file mode 100644 index 000000000000..8827f71e57ec --- /dev/null +++ b/packages/contracts/slither.config.json @@ -0,0 +1,12 @@ +{ + "detectors_to_exclude": "conformance-to-solidity-naming-conventions,assembly-usage,low-level-calls,block-timestamp", + "exclude_informational": false, + "exclude_low": false, + "exclude_medium": false, + "exclude_high": false, + "solc_disable_warnings": false, + "hardhat_ignore_compile": true, + "disable_color": false, + "exclude_dependencies": true, + "filter_paths": "@openzeppelin|hardhat|contracts/test-helpers|contracts/test-libraries" +} diff --git a/packages/contracts/tasks/deploy.ts b/packages/contracts/tasks/deploy.ts index 35d2e041b692..8c3a793b062b 100644 --- a/packages/contracts/tasks/deploy.ts +++ b/packages/contracts/tasks/deploy.ts @@ -99,6 +99,12 @@ task('deploy') undefined, types.string ) + .addOptionalParam( + 'initialGasPriceOracleGasPrice', + 'The initial execution price for the gas price oracle.', + undefined, + types.int + ) .setAction(async (args, hre: any, runSuper) => { // Necessary because hardhat doesn't let us attach non-optional parameters to existing tasks. const validateAddressArg = (argName: string) => { diff --git a/packages/contracts/test/contracts/OVM/precompiles/OVM_GasPriceOracle.spec.ts b/packages/contracts/test/contracts/OVM/precompiles/OVM_GasPriceOracle.spec.ts new file mode 100644 index 000000000000..ead0baebf899 --- /dev/null +++ b/packages/contracts/test/contracts/OVM/precompiles/OVM_GasPriceOracle.spec.ts @@ -0,0 +1,80 @@ +import { expect } from '../../../setup' + +/* External Imports */ +import { ethers } from 'hardhat' +import { ContractFactory, Contract, Signer } from 'ethers' + +describe('OVM_GasPriceOracle', () => { + const initialGasPrice = 0 + let signer1: Signer + let signer2: Signer + before(async () => { + ;[signer1, signer2] = await ethers.getSigners() + }) + + let Factory__OVM_GasPriceOracle: ContractFactory + before(async () => { + Factory__OVM_GasPriceOracle = await ethers.getContractFactory( + 'OVM_GasPriceOracle' + ) + }) + + let OVM_GasPriceOracle: Contract + beforeEach(async () => { + OVM_GasPriceOracle = await Factory__OVM_GasPriceOracle.deploy( + await signer1.getAddress(), + initialGasPrice + ) + }) + + describe('owner', () => { + it('should have an owner', async () => { + expect(await OVM_GasPriceOracle.owner()).to.equal( + await signer1.getAddress() + ) + }) + }) + + describe('setGasPrice', () => { + it('should revert if called by someone other than the owner', async () => { + await expect(OVM_GasPriceOracle.connect(signer2).setGasPrice(1234)).to.be + .reverted + }) + + it('should succeed if called by the owner and is equal to `0`', async () => { + await expect(OVM_GasPriceOracle.connect(signer1).setGasPrice(0)).to.not.be + .reverted + }) + }) + + describe('get gasPrice', () => { + it('should return zero at first', async () => { + expect(await OVM_GasPriceOracle.gasPrice()).to.equal(initialGasPrice) + }) + + it('should change when setGasPrice is called', async () => { + const gasPrice = 1234 + + await OVM_GasPriceOracle.connect(signer1).setGasPrice(gasPrice) + + expect(await OVM_GasPriceOracle.gasPrice()).to.equal(gasPrice) + }) + + it('is the 1st storage slot', async () => { + const gasPrice = 1234 + const slot = 1 + + // set the price + await OVM_GasPriceOracle.connect(signer1).setGasPrice(gasPrice) + + // get the storage slot value + const priceAtSlot = await signer1.provider.getStorageAt( + OVM_GasPriceOracle.address, + slot + ) + expect(await OVM_GasPriceOracle.gasPrice()).to.equal( + ethers.BigNumber.from(priceAtSlot) + ) + }) + }) +}) diff --git a/packages/contracts/test/helpers/test-runner/test.types.ts b/packages/contracts/test/helpers/test-runner/test.types.ts index fa799dd6b4e1..b31b03d17318 100644 --- a/packages/contracts/test/helpers/test-runner/test.types.ts +++ b/packages/contracts/test/helpers/test-runner/test.types.ts @@ -213,7 +213,6 @@ export const isTestStep_Context = ( 'ovmCALLER', 'ovmNUMBER', 'ovmADDRESS', - 'ovmNUMBER', 'ovmL1TXORIGIN', 'ovmTIMESTAMP', 'ovmGASLIMIT', diff --git a/packages/core-utils/CHANGELOG.md b/packages/core-utils/CHANGELOG.md index a5d6db922774..69d1bbdd6ec8 100644 --- a/packages/core-utils/CHANGELOG.md +++ b/packages/core-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @eth-optimism/core-utils +## 0.4.5 + +### Patch Changes + +- a64f8161: Implement the next fee spec in both geth and in core-utils +- 750a5021: Delete dead transaction coders. These are no longer used now that RLP encoded transactions are used +- c2b6e14b: Implement the latest fee spec such that the L2 gas limit is scaled and the tx.gasPrice/tx.gasLimit show correctly in metamask + ## 0.4.4 ### Patch Changes diff --git a/packages/core-utils/README.md b/packages/core-utils/README.md index 20a6539f5293..fc01bdfdfe78 100644 --- a/packages/core-utils/README.md +++ b/packages/core-utils/README.md @@ -25,36 +25,29 @@ $ yarn lint ### L2 Fees -The Layer 2 fee is encoded in `tx.gasLimit`. The Layer 2 `gasLimit` is encoded -in the lower order bits of the `tx.gasLimit`. For this scheme to work, both the -L1 gas price and the L2 gas price must satisfy specific equations. There are -functions that help ensure that the correct gas prices are used. - -- `roundL1GasPrice` -- `roundL2GasPrice` - -The Layer 2 fee is based on both the cost of submitting the data to L1 as well -as the cost of execution on L2. To make libraries like `ethers` just work, the -return value of `eth_estimateGas` has been modified to return the fee. A new RPC -endpoint `eth_estimateExecutionGas` has been added that returns the L2 gas used. - -To locally encode the `tx.gasLimit`, the `L2GasLimit` methods `encode` and -`decode` should be used. +`TxGasLimit` can be used to `encode` and `decode` the L2 Gas Limit +locally. ```typescript -import { L2GasLimit, roundL1GasPrice, roundL2GasPrice } from '@eth-optimism/core-utils' +import { TxGasLimit } from '@eth-optimism/core-utils' import { JsonRpcProvider } from 'ethers' -const provider = new JsonRpcProvider('https://mainnet.optimism.io') -const gasLimit = await provider.send('eth_estimateExecutionGas', [tx]) +const L2Provider = new JsonRpcProvider('https://mainnet.optimism.io') +const L1Provider = new JsonRpcProvider('http://127.0.0.1:8545') -const encoded = L2GasLimit.encode({ +const l2GasLimit = await L2Provider.send('eth_estimateExecutionGas', [tx]) +const l1GasPrice = await L1Provider.getGasPrice() + +const encoded = TxGasLimit.encode({ data: '0x', - l1GasPrice: roundL1GasPrice(1), - l2GasLimit: gasLimit, - l2GasPrice: roundL2GasPrice(1), + l1GasPrice, + l2GasLimit, + l2GasPrice: 10000000, }) -const decoded = L2GasLimit.decode(encoded) +const decoded = TxGasLimit.decode(encoded) assert(decoded.eq(gasLimit)) + +const estimate = await L2Provider.estimateGas() +assert(estimate.eq(encoded)) ``` diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json index dea8684a4ccd..74166fb352bd 100644 --- a/packages/core-utils/package.json +++ b/packages/core-utils/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/core-utils", - "version": "0.4.4", + "version": "0.4.5", "main": "dist/index", "files": [ "dist/*" diff --git a/packages/core-utils/src/coders/ecdsa-coder.ts b/packages/core-utils/src/coders/ecdsa-coder.ts deleted file mode 100644 index be381a107723..000000000000 --- a/packages/core-utils/src/coders/ecdsa-coder.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* Internal Imports */ -import { add0x, remove0x, toVerifiedBytes, encodeHex, getLen } from '../common' -import { Coder, Signature, Uint16, Uint8, Uint24, Address } from './types' - -/*********************** - * TxTypes and TxData * - **********************/ - -export enum TxType { - EIP155 = 0, - EthSign = 1, - EthSign2 = 2, -} - -export const txTypePlainText = { - 0: TxType.EIP155, - 1: TxType.EthSign, - 2: TxType.EthSign2, - EIP155: TxType.EIP155, - EthSign: TxType.EthSign, -} - -export interface DefaultEcdsaTxData { - sig: Signature - gasLimit: Uint16 - gasPrice: Uint8 - nonce: Uint24 - target: Address - data: string - type: TxType -} - -export interface EIP155TxData extends DefaultEcdsaTxData {} -export interface EthSignTxData extends DefaultEcdsaTxData {} - -/*********************** - * Encoding Positions * - **********************/ - -/* - * The positions in the tx data for the different transaction types - */ - -export const TX_TYPE_POSITION = { start: 0, end: 1 } - -/* - * The positions in the tx data for the EIP155TxData and EthSignTxData - */ - -export const SIGNATURE_FIELD_POSITIONS = { - r: { start: 1, end: 33 }, // 32 bytes - s: { start: 33, end: 65 }, // 32 bytes - v: { start: 65, end: 66 }, // 1 byte -} - -export const DEFAULT_ECDSA_TX_FIELD_POSITIONS = { - txType: TX_TYPE_POSITION, // 1 byte - sig: SIGNATURE_FIELD_POSITIONS, // 65 bytes - gasLimit: { start: 66, end: 69 }, // 3 bytes - gasPrice: { start: 69, end: 72 }, // 3 byte - nonce: { start: 72, end: 75 }, // 3 bytes - target: { start: 75, end: 95 }, // 20 bytes - data: { start: 95 }, // byte 95 onward -} - -export const EIP155_TX_FIELD_POSITIONS = DEFAULT_ECDSA_TX_FIELD_POSITIONS -export const ETH_SIGN_TX_FIELD_POSITIONS = DEFAULT_ECDSA_TX_FIELD_POSITIONS -export const CTC_TX_GAS_PRICE_MULT_FACTOR = 1_000_000 - -/*************** - * EcdsaCoders * - **************/ - -class DefaultEcdsaTxCoder implements Coder { - constructor(readonly txType: TxType) {} - - public encode(txData: DefaultEcdsaTxData): string { - const txType = encodeHex( - this.txType, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.txType) - ) - - const r = toVerifiedBytes( - txData.sig.r, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.sig.r) - ) - const s = toVerifiedBytes( - txData.sig.s, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.sig.s) - ) - const v = encodeHex( - txData.sig.v, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.sig.v) - ) - - const gasLimit = encodeHex( - txData.gasLimit, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.gasLimit) - ) - if (txData.gasPrice % CTC_TX_GAS_PRICE_MULT_FACTOR !== 0) { - throw new Error(`Gas Price ${txData.gasPrice} cannot be encoded`) - } - const gasPrice = encodeHex( - txData.gasPrice / CTC_TX_GAS_PRICE_MULT_FACTOR, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.gasPrice) - ) - const nonce = encodeHex( - txData.nonce, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.nonce) - ) - const target = toVerifiedBytes( - txData.target, - getLen(DEFAULT_ECDSA_TX_FIELD_POSITIONS.target) - ) - // Make sure that the data is even - if (txData.data.length % 2 !== 0) { - throw new Error('Non-even hex string for tx data!') - } - const encoding = - '0x' + - txType + - r + - s + - v + - gasLimit + - gasPrice + - nonce + - target + - remove0x(txData.data) - return encoding - } - - public decode(txData: string): DefaultEcdsaTxData { - txData = remove0x(txData) - const sliceBytes = (position: { start; end? }): string => - txData.slice(position.start * 2, position.end * 2) - - const pos = DEFAULT_ECDSA_TX_FIELD_POSITIONS - if (parseInt(sliceBytes(pos.txType), 16) !== this.txType) { - throw new Error('Invalid tx type') - } - - return { - sig: { - r: add0x(sliceBytes(pos.sig.r)), - s: add0x(sliceBytes(pos.sig.s)), - v: parseInt(sliceBytes(pos.sig.v), 16), - }, - gasLimit: parseInt(sliceBytes(pos.gasLimit), 16), - gasPrice: - parseInt(sliceBytes(pos.gasPrice), 16) * CTC_TX_GAS_PRICE_MULT_FACTOR, - nonce: parseInt(sliceBytes(pos.nonce), 16), - target: add0x(sliceBytes(pos.target)), - data: add0x(txData.slice(pos.data.start * 2)), - type: this.txType, - } - } -} - -class EthSignTxCoder extends DefaultEcdsaTxCoder { - constructor() { - super(TxType.EthSign) - } - - public encode(txData: EthSignTxData): string { - return super.encode(txData) - } - - public decode(txData: string): EthSignTxData { - return super.decode(txData) - } -} - -class EthSign2TxCoder extends DefaultEcdsaTxCoder { - constructor() { - super(TxType.EthSign2) - } - - public encode(txData: EthSignTxData): string { - return super.encode(txData) - } - - public decode(txData: string): EthSignTxData { - return super.decode(txData) - } -} - -class Eip155TxCoder extends DefaultEcdsaTxCoder { - constructor() { - super(TxType.EIP155) - } - - public encode(txData: EIP155TxData): string { - return super.encode(txData) - } - - public decode(txData: string): EIP155TxData { - return super.decode(txData) - } -} - -/************* - * ctcCoder * - ************/ - -function encode(data: EIP155TxData): string { - if (data.type === TxType.EIP155) { - return new Eip155TxCoder().encode(data) - } - if (data.type === TxType.EthSign) { - return new EthSignTxCoder().encode(data) - } - return null -} - -function decode(data: string | Buffer): EIP155TxData { - if (Buffer.isBuffer(data)) { - data = data.toString() - } - data = remove0x(data) - const type = parseInt(data.slice(0, 2), 16) - if (type === TxType.EIP155) { - return new Eip155TxCoder().decode(data) - } - if (type === TxType.EthSign) { - return new EthSignTxCoder().decode(data) - } - if (type === TxType.EthSign2) { - return new EthSign2TxCoder().decode(data) - } - return null -} - -/* - * Encoding and decoding functions for all txData types. - */ -export const ctcCoder = { - eip155TxData: new Eip155TxCoder(), - ethSignTxData: new EthSignTxCoder(), - ethSign2TxData: new EthSign2TxCoder(), - encode, - decode, -} diff --git a/packages/core-utils/src/coders/index.ts b/packages/core-utils/src/coders/index.ts index 4d9613d932e7..dfa4404fbe81 100644 --- a/packages/core-utils/src/coders/index.ts +++ b/packages/core-utils/src/coders/index.ts @@ -1,3 +1,2 @@ -export * from './ecdsa-coder' export * from './types' export * from './sequencer-batch' diff --git a/packages/core-utils/src/coders/types.ts b/packages/core-utils/src/coders/types.ts index 928c2cc58266..1280fe3d7099 100644 --- a/packages/core-utils/src/coders/types.ts +++ b/packages/core-utils/src/coders/types.ts @@ -8,8 +8,3 @@ export type Uint16 = number export type Uint8 = number export type Uint24 = number export type Address = string - -export interface Coder { - encode: Function - decode: Function -} diff --git a/packages/core-utils/src/fees.ts b/packages/core-utils/src/fees.ts index cc754e5fcb81..5a1008b1aa5a 100644 --- a/packages/core-utils/src/fees.ts +++ b/packages/core-utils/src/fees.ts @@ -6,9 +6,12 @@ import { BigNumber } from 'ethers' import { remove0x } from './common' const hundredMillion = BigNumber.from(100_000_000) +const feeScalar = 10_000_000 +export const TxGasPrice = BigNumber.from(feeScalar + feeScalar / 2) const txDataZeroGas = 4 const txDataNonZeroGasEIP2028 = 16 -const overhead = 4200 +const overhead = 4200 + 200 * txDataNonZeroGasEIP2028 +const tenThousand = BigNumber.from(10_000) export interface EncodableL2GasLimit { data: Buffer | string @@ -29,59 +32,52 @@ function encode(input: EncodableL2GasLimit): BigNumber { if (typeof l2GasPrice === 'number') { l2GasPrice = BigNumber.from(l2GasPrice) } - - if (!verifyL2GasPrice(l2GasPrice)) { - throw new Error(`Invalid L2 Gas Price: ${l2GasPrice.toString()}`) - } - if (!verifyL1GasPrice(l1GasPrice)) { - throw new Error(`Invalid L1 Gas Price: ${l1GasPrice.toString()}`) - } const l1GasLimit = calculateL1GasLimit(data) - const l1Fee = l1GasPrice.mul(l1GasLimit) - const l2Fee = l2GasLimit.mul(l2GasPrice) - return l1Fee.add(l2Fee) + const roundedL2GasLimit = ceilmod(l2GasLimit, tenThousand) + const l1Fee = l1GasLimit.mul(l1GasPrice) + const l2Fee = roundedL2GasLimit.mul(l2GasPrice) + const sum = l1Fee.add(l2Fee) + const scaled = sum.div(feeScalar) + const rounded = ceilmod(scaled, tenThousand) + const roundedScaledL2GasLimit = roundedL2GasLimit.div(tenThousand) + return rounded.add(roundedScaledL2GasLimit) } function decode(fee: BigNumber | number): BigNumber { if (typeof fee === 'number') { fee = BigNumber.from(fee) } - return fee.mod(hundredMillion) + const scaled = fee.mod(tenThousand) + return scaled.mul(tenThousand) } -export const L2GasLimit = { +export const TxGasLimit = { encode, decode, } -export function verifyL2GasPrice(gasPrice: BigNumber | number): boolean { - if (typeof gasPrice === 'number') { - gasPrice = BigNumber.from(gasPrice) +export function ceilmod(a: BigNumber | number, b: BigNumber | number) { + if (typeof a === 'number') { + a = BigNumber.from(a) } - // If the gas price is not equal to 0 and the gas price mod - // one hundred million is not one - if (!gasPrice.eq(0) && !gasPrice.mod(hundredMillion).eq(1)) { - return false + if (typeof b === 'number') { + b = BigNumber.from(b) } - if (gasPrice.eq(0)) { - return false + const remainder = a.mod(b) + if (remainder.eq(0)) { + return a } - return true + const sum = a.add(b) + const rounded = sum.sub(remainder) + return rounded } -export function verifyL1GasPrice(gasPrice: BigNumber | number): boolean { - if (typeof gasPrice === 'number') { - gasPrice = BigNumber.from(gasPrice) - } - return gasPrice.mod(hundredMillion).eq(0) -} - -export function calculateL1GasLimit(data: string | Buffer): number { +export function calculateL1GasLimit(data: string | Buffer): BigNumber { const [zeroes, ones] = zeroesAndOnes(data) const zeroesCost = zeroes * txDataZeroGas const onesCost = ones * txDataNonZeroGasEIP2028 const gasLimit = zeroesCost + onesCost + overhead - return gasLimit + return BigNumber.from(gasLimit) } export function zeroesAndOnes(data: Buffer | string): Array { @@ -99,34 +95,3 @@ export function zeroesAndOnes(data: Buffer | string): Array { } return [zeros, ones] } - -export function roundL1GasPrice(gasPrice: BigNumber | number): BigNumber { - if (typeof gasPrice === 'number') { - gasPrice = BigNumber.from(gasPrice) - } - return ceilModOneHundredMillion(gasPrice) -} - -function ceilModOneHundredMillion(num: BigNumber): BigNumber { - if (num.mod(hundredMillion).eq(0)) { - return num - } - const sum = num.add(hundredMillion) - const mod = num.mod(hundredMillion) - return sum.sub(mod) -} - -export function roundL2GasPrice(gasPrice: BigNumber | number): BigNumber { - if (typeof gasPrice === 'number') { - gasPrice = BigNumber.from(gasPrice) - } - if (gasPrice.eq(0)) { - return BigNumber.from(1) - } - if (gasPrice.eq(1)) { - return hundredMillion.add(1) - } - const gp = gasPrice.sub(1) - const mod = ceilModOneHundredMillion(gp) - return mod.add(1) -} diff --git a/packages/core-utils/test/coders/batch-encoder.spec.ts b/packages/core-utils/test/coders/batch-encoder.spec.ts index 2ad7c2eeeeb9..ce277a72d051 100644 --- a/packages/core-utils/test/coders/batch-encoder.spec.ts +++ b/packages/core-utils/test/coders/batch-encoder.spec.ts @@ -2,63 +2,13 @@ import '../setup' /* Internal Imports */ import { - ctcCoder, encodeAppendSequencerBatch, decodeAppendSequencerBatch, - TxType, sequencerBatch, } from '../../src' import { expect } from 'chai' describe('BatchEncoder', () => { - describe('eip155TxData', () => { - it('should encode & then decode to the correct value', () => { - const eip155TxData = { - sig: { - v: 1, - r: '0x' + '11'.repeat(32), - s: '0x' + '22'.repeat(32), - }, - gasLimit: 500, - gasPrice: 1000000, - nonce: 100, - target: '0x' + '12'.repeat(20), - data: '0x' + '99'.repeat(10), - type: TxType.EIP155, - } - const encoded = ctcCoder.eip155TxData.encode(eip155TxData) - const decoded = ctcCoder.eip155TxData.decode(encoded) - expect(eip155TxData).to.deep.equal(decoded) - }) - - it('should fail encoding a bad gas price', () => { - const badGasPrice = 1000001 - const eip155TxData = { - sig: { - v: 1, - r: '0x' + '11'.repeat(32), - s: '0x' + '22'.repeat(32), - }, - gasLimit: 500, - gasPrice: badGasPrice, - nonce: 100, - target: '0x' + '12'.repeat(20), - data: '0x' + '99'.repeat(10), - type: TxType.EIP155, - } - - let error - try { - ctcCoder.eip155TxData.encode(eip155TxData) - } catch (e) { - error = e - } - expect(error.message).to.equal( - `Gas Price ${badGasPrice} cannot be encoded` - ) - }) - }) - describe('appendSequencerBatch', () => { it('should work with the simple case', () => { const batch = { @@ -100,43 +50,4 @@ describe('BatchEncoder', () => { } }) }) - - describe('generic ctcCoder', () => { - it('should decode EIP155 txs to the correct value', () => { - const eip155TxData = { - sig: { - v: 1, - r: '0x' + '11'.repeat(32), - s: '0x' + '22'.repeat(32), - }, - gasLimit: 500, - gasPrice: 1000000, - nonce: 100, - target: '0x' + '12'.repeat(20), - data: '0x' + '99'.repeat(10), - type: TxType.EIP155, - } - const encoded = ctcCoder.encode(eip155TxData) - const decoded = ctcCoder.decode(encoded) - expect(eip155TxData).to.deep.equal(decoded) - }) - - it('should return null when encoding an unknown type', () => { - const weirdTypeTxData = { - sig: { - v: 1, - r: '0x' + '11'.repeat(32), - s: '0x' + '22'.repeat(32), - }, - gasLimit: 500, - gasPrice: 100, - nonce: 100, - target: '0x' + '12'.repeat(20), - data: '0x' + '99'.repeat(10), - type: 420, - } - const encoded = ctcCoder.encode(weirdTypeTxData) - expect(encoded).to.be.null - }) - }) }) diff --git a/packages/core-utils/test/fees/fees.spec.ts b/packages/core-utils/test/fees/fees.spec.ts index bc548c5baa67..3870092dfc57 100644 --- a/packages/core-utils/test/fees/fees.spec.ts +++ b/packages/core-utils/test/fees/fees.spec.ts @@ -1,6 +1,9 @@ import { expect } from '../setup' import * as fees from '../../src/fees' -import { BigNumber } from 'ethers' +import { BigNumber, utils } from 'ethers' + +const hundredBillion = 10 ** 11 +const million = 10 ** 6 describe('Fees', () => { it('should count zeros and ones', () => { @@ -18,115 +21,100 @@ describe('Fees', () => { } }) - describe('Round L1 Gas Price', () => { - const roundL1GasPriceTests = [ - { input: 10, expect: 10 ** 8, name: 'simple' }, - { input: 10 ** 8 + 1, expect: 2 * 10 ** 8, name: 'one-over' }, - { input: 10 ** 8, expect: 10 ** 8, name: 'exact' }, - { input: 10 ** 8 - 1, expect: 10 ** 8, name: 'one-under' }, - { input: 3, expect: 10 ** 8, name: 'small' }, - { input: 2, expect: 10 ** 8, name: 'two' }, - { input: 1, expect: 10 ** 8, name: 'one' }, - { input: 0, expect: 0, name: 'zero' }, - ] - - for (const test of roundL1GasPriceTests) { - it(`should pass for ${test.name} case`, () => { - const got = fees.roundL1GasPrice(test.input) - const expected = BigNumber.from(test.expect) - expect(got).to.deep.equal(expected) - }) - } - }) - - describe('Round L2 Gas Price', () => { - const roundL2GasPriceTests = [ - { input: 10, expect: 10 ** 8 + 1, name: 'simple' }, - { input: 10 ** 8 + 2, expect: 2 * 10 ** 8 + 1, name: 'one-over' }, - { input: 10 ** 8 + 1, expect: 10 ** 8 + 1, name: 'exact' }, - { input: 10 ** 8, expect: 10 ** 8 + 1, name: 'one-under' }, - { input: 3, expect: 10 ** 8 + 1, name: 'small' }, - { input: 2, expect: 10 ** 8 + 1, name: 'two' }, - { input: 1, expect: 10 ** 8 + 1, name: 'one' }, - { input: 0, expect: 1, name: 'zero' }, - ] - - for (const test of roundL2GasPriceTests) { - it(`should pass for ${test.name} case`, () => { - const got = fees.roundL2GasPrice(test.input) - const expected = BigNumber.from(test.expect) - expect(got).to.deep.equal(expected) - }) - } - }) - describe('Rollup Fees', () => { const rollupFeesTests = [ { name: 'simple', dataLen: 10, - l1GasPrice: 100_000_000, - l2GasPrice: 100_000_001, + l1GasPrice: utils.parseUnits('1', 'gwei'), + l2GasPrice: utils.parseUnits('1', 'gwei'), l2GasLimit: 437118, - error: false, + }, + { + name: 'small-gasprices-max-gaslimit', + dataLen: 10, + l1GasPrice: utils.parseUnits('1', 'wei'), + l2GasPrice: utils.parseUnits('1', 'wei'), + l2GasLimit: 0x4ffffff, + }, + { + name: 'large-gasprices-max-gaslimit', + dataLen: 10, + l1GasPrice: utils.parseUnits('1', 'ether'), + l2GasPrice: utils.parseUnits('1', 'ether'), + l2GasLimit: 0x4ffffff, + }, + { + name: 'small-gasprices-max-gaslimit', + dataLen: 10, + l1GasPrice: utils.parseUnits('1', 'ether'), + l2GasPrice: utils.parseUnits('1', 'ether'), + l2GasLimit: 1, + }, + { + name: 'max-gas-limit', + dataLen: 10, + l1GasPrice: utils.parseUnits('5', 'ether'), + l2GasPrice: utils.parseUnits('5', 'ether'), + l2GasLimit: 99_970_000, }, { name: 'zero-l2-gasprice', dataLen: 10, - l1GasPrice: 100_000_000, + l1GasPrice: hundredBillion, l2GasPrice: 0, l2GasLimit: 196205, - error: true, }, { name: 'one-l2-gasprice', dataLen: 10, - l1GasPrice: 100_000_000, + l1GasPrice: hundredBillion, l2GasPrice: 1, l2GasLimit: 196205, - error: false, }, { name: 'zero-l1-gasprice', dataLen: 10, l1GasPrice: 0, - l2GasPrice: 100_000_001, + l2GasPrice: hundredBillion, l2GasLimit: 196205, - error: false, }, { name: 'one-l1-gasprice', dataLen: 10, l1GasPrice: 1, - l2GasPrice: 23254, + l2GasPrice: hundredBillion, + l2GasLimit: 23255, + }, + { + name: 'zero-gasprices', + dataLen: 10, + l1GasPrice: 0, + l2GasPrice: 0, l2GasLimit: 23255, - error: true, + }, + { + name: 'larger-divisor', + dataLen: 10, + l1GasPrice: 0, + l2GasLimit: 10, + l2GasPrice: 0, }, ] for (const test of rollupFeesTests) { it(`should pass for ${test.name} case`, () => { const data = Buffer.alloc(test.dataLen) + const got = fees.TxGasLimit.encode({ + data, + l1GasPrice: test.l1GasPrice, + l2GasPrice: test.l2GasPrice, + l2GasLimit: test.l2GasLimit, + }) - let got - let err = false - try { - got = fees.L2GasLimit.encode({ - data, - l1GasPrice: test.l1GasPrice, - l2GasPrice: test.l2GasPrice, - l2GasLimit: test.l2GasLimit, - }) - } catch (e) { - err = true - } - - expect(err).to.equal(test.error) - - if (!err) { - const decoded = fees.L2GasLimit.decode(got) - expect(decoded).to.deep.eq(BigNumber.from(test.l2GasLimit)) - } + const decoded = fees.TxGasLimit.decode(got) + const roundedL2GasLimit = fees.ceilmod(test.l2GasLimit, 10_000) + expect(decoded).to.deep.eq(roundedL2GasLimit) }) } }) diff --git a/packages/data-transport-layer/CHANGELOG.md b/packages/data-transport-layer/CHANGELOG.md index 61c4944d5ac0..cd696595dba6 100644 --- a/packages/data-transport-layer/CHANGELOG.md +++ b/packages/data-transport-layer/CHANGELOG.md @@ -1,5 +1,38 @@ # data transport layer +## 0.3.6 + +### Patch Changes + +- baa3b761: Improve Sentry support, initializing as needed and ensuring ERROR logs route to Sentry +- Updated dependencies [baa3b761] + - @eth-optimism/common-ts@0.1.3 + +## 0.3.5 + +### Patch Changes + +- 1b692415: incorrect parsing of eth_getBlockRange result + +## 0.3.4 + +### Patch Changes + +- f1b27318: Represent gaslimit as a string to avoid an overflow +- 750a5021: Remove dead imports from core-utils +- 1293825c: Fix gasLimit overflow +- a75f05b7: Fixes a bug that prevented verifiers from syncing properly with the DTL +- e52ccd98: Logs the error stacktrace for a failed HTTP request +- 8ac4c74c: improve slow blocking JSON parsing that occurs during l2 sync +- Updated dependencies [a64f8161] +- Updated dependencies [4e03f8a9] +- Updated dependencies [8e2bfd07] +- Updated dependencies [750a5021] +- Updated dependencies [c2b6e14b] +- Updated dependencies [245136f1] + - @eth-optimism/core-utils@0.4.5 + - @eth-optimism/contracts@0.3.5 + ## 0.3.3 ### Patch Changes diff --git a/packages/data-transport-layer/package.json b/packages/data-transport-layer/package.json index c959e434e2c1..940bb46393f8 100644 --- a/packages/data-transport-layer/package.json +++ b/packages/data-transport-layer/package.json @@ -1,6 +1,6 @@ { "name": "@eth-optimism/data-transport-layer", - "version": "0.3.3", + "version": "0.3.6", "private": true, "main": "dist/index", "files": [ @@ -21,15 +21,17 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.2", - "@eth-optimism/contracts": "^0.3.3", - "@eth-optimism/core-utils": "^0.4.3", + "@eth-optimism/common-ts": "^0.1.3", + "@eth-optimism/contracts": "^0.3.5", + "@eth-optimism/core-utils": "^0.4.5", "@ethersproject/providers": "^5.0.21", "@ethersproject/transactions": "^5.0.21", "@sentry/node": "^6.3.1", "@sentry/tracing": "^6.3.1", "@types/express": "^4.17.11", + "axios": "^0.21.1", "bcfg": "^0.1.6", + "bfj": "^7.0.2", "browser-or-node": "^1.3.0", "cors": "^2.8.5", "dotenv": "^8.2.0", @@ -49,6 +51,7 @@ "@types/levelup": "^4.3.0", "@types/mocha": "^8.2.2", "@types/node-fetch": "^2.5.8", + "@types/workerpool": "^6.0.0", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "hardhat": "^2.2.1", diff --git a/packages/data-transport-layer/src/db/transport-db.ts b/packages/data-transport-layer/src/db/transport-db.ts index be80e6314b55..777ba4423dde 100644 --- a/packages/data-transport-layer/src/db/transport-db.ts +++ b/packages/data-transport-layer/src/db/transport-db.ts @@ -379,8 +379,9 @@ export class TransportDB { if (index === null) { return null } - - return this.db.get(`${key}:index`, index) + let entry = await this.db.get(`${key}:index`, index) + entry = stringify(entry) + return entry } private async _getEntries( @@ -388,6 +389,28 @@ export class TransportDB { startIndex: number, endIndex: number ): Promise { - return this.db.range(`${key}:index`, startIndex, endIndex) + const entries = await this.db.range( + `${key}:index`, + startIndex, + endIndex + ) + const results = [] + for (const entry of entries) { + results.push(stringify(entry)) + } + return results + } +} + +function stringify(entry) { + if (entry === null || entry === undefined) { + return entry + } + if (entry.gasLimit) { + entry.gasLimit = BigNumber.from(entry.gasLimit).toString() + } + if (entry.decoded) { + entry.decoded.gasLimit = BigNumber.from(entry.decoded.gasLimit).toString() } + return entry } diff --git a/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts b/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts index 71b483f4dcde..114e04f3c5c1 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts @@ -2,11 +2,9 @@ import { BigNumber, ethers, constants } from 'ethers' import { getContractFactory } from '@eth-optimism/contracts' import { - ctcCoder, fromHexString, toHexString, toRpcHexString, - TxType, EventArgsSequencerBatchAppended, } from '@eth-optimism/core-utils' @@ -22,6 +20,7 @@ import { import { SEQUENCER_ENTRYPOINT_ADDRESS, SEQUENCER_GAS_LIMIT, + parseSignatureVParam, } from '../../../utils' export const handleEventsSequencerBatchAppended: EventHandlerSet< @@ -69,7 +68,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< submitter: l1Transaction.from, l1TransactionHash: l1Transaction.hash, l1TransactionData: l1Transaction.data, - gasLimit: SEQUENCER_GAS_LIMIT, + gasLimit: `${SEQUENCER_GAS_LIMIT}`, prevTotalElements: batchSubmissionEvent.args._prevTotalElements, batchIndex: batchSubmissionEvent.args._batchIndex, @@ -78,7 +77,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< batchExtraData: batchSubmissionEvent.args._extraData, } }, - parseEvent: (event, extraData) => { + parseEvent: (event, extraData, l2ChainId) => { const transactionEntries: TransactionEntry[] = [] // It's easier to deal with this data if it's a Buffer. @@ -105,7 +104,8 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< ) const decoded = maybeDecodeSequencerBatchTransaction( - sequencerTransaction + sequencerTransaction, + l2ChainId ) transactionEntries.push({ @@ -115,7 +115,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< batchIndex: extraData.batchIndex.toNumber(), blockNumber: BigNumber.from(context.blockNumber).toNumber(), timestamp: BigNumber.from(context.timestamp).toNumber(), - gasLimit: BigNumber.from(extraData.gasLimit).toNumber(), + gasLimit: BigNumber.from(extraData.gasLimit).toString(), target: SEQUENCER_ENTRYPOINT_ADDRESS, origin: null, data: toHexString(sequencerTransaction), @@ -147,7 +147,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< batchIndex: extraData.batchIndex.toNumber(), blockNumber: BigNumber.from(0).toNumber(), timestamp: BigNumber.from(0).toNumber(), - gasLimit: BigNumber.from(0).toNumber(), + gasLimit: BigNumber.from(0).toString(), target: constants.AddressZero, origin: constants.AddressZero, data: '0x', @@ -236,7 +236,8 @@ const parseSequencerBatchTransaction = ( } const maybeDecodeSequencerBatchTransaction = ( - transaction: Buffer + transaction: Buffer, + l2ChainId: number ): DecodedSequencerBatchTransaction | null => { try { const decodedTx = ethers.utils.parseTransaction(transaction) @@ -249,7 +250,7 @@ const maybeDecodeSequencerBatchTransaction = ( target: toHexString(decodedTx.to), // Maybe null this out for creations? data: toHexString(decodedTx.data), sig: { - v: BigNumber.from(decodedTx.v).toNumber(), + v: parseSignatureVParam(decodedTx.v, l2ChainId), r: toHexString(decodedTx.r), s: toHexString(decodedTx.s), }, diff --git a/packages/data-transport-layer/src/services/l1-ingestion/handlers/transaction-enqueued.ts b/packages/data-transport-layer/src/services/l1-ingestion/handlers/transaction-enqueued.ts index e343a7e78fbf..7e6bcb9b2b66 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/handlers/transaction-enqueued.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/handlers/transaction-enqueued.ts @@ -17,7 +17,7 @@ export const handleEventsTransactionEnqueued: EventHandlerSet< index: event.args._queueIndex.toNumber(), target: event.args._target, data: event.args._data, - gasLimit: event.args._gasLimit.toNumber(), + gasLimit: event.args._gasLimit.toString(), origin: event.args._l1TxOrigin, blockNumber: BigNumber.from(event.blockNumber).toNumber(), timestamp: event.args._timestamp.toNumber(), diff --git a/packages/data-transport-layer/src/services/l1-ingestion/service.ts b/packages/data-transport-layer/src/services/l1-ingestion/service.ts index 3bc862cf4feb..d682fa5436b8 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/service.ts @@ -3,6 +3,7 @@ import { fromHexString, EventArgsAddressSet } from '@eth-optimism/core-utils' import { BaseService } from '@eth-optimism/common-ts' import { JsonRpcProvider } from '@ethersproject/providers' import { LevelUp } from 'levelup' +import { ethers, constants } from 'ethers' /* Imports: Internal */ import { TransportDB } from '../../db/transport-db' @@ -18,7 +19,6 @@ import { handleEventsTransactionEnqueued } from './handlers/transaction-enqueued import { handleEventsSequencerBatchAppended } from './handlers/sequencer-batch-appended' import { handleEventsStateBatchAppended } from './handlers/state-batch-appended' import { L1DataTransportServiceOptions } from '../main/service' -import { constants } from 'ethers' export interface L1IngestionServiceOptions extends L1DataTransportServiceOptions { @@ -65,6 +65,7 @@ export class L1IngestionService extends BaseService { contracts: OptimismContracts l1RpcProvider: JsonRpcProvider startingL1BlockNumber: number + l2ChainId: number } = {} as any protected async _init(): Promise { @@ -114,6 +115,10 @@ export class L1IngestionService extends BaseService { this.options.addressManager ) + this.state.l2ChainId = ethers.BigNumber.from( + await this.state.contracts.OVM_ExecutionManager.ovmCHAINID() + ).toNumber() + const startingL1BlockNumber = await this.state.db.getStartingL1Block() if (startingL1BlockNumber) { this.state.startingL1BlockNumber = startingL1BlockNumber @@ -295,7 +300,11 @@ export class L1IngestionService extends BaseService { event, this.state.l1RpcProvider ) - const parsedEvent = await handlers.parseEvent(event, extraData) + const parsedEvent = await handlers.parseEvent( + event, + extraData, + this.state.l2ChainId + ) await handlers.storeEvent(parsedEvent, this.state.db) } diff --git a/packages/data-transport-layer/src/services/l2-ingestion/handlers/transaction.ts b/packages/data-transport-layer/src/services/l2-ingestion/handlers/transaction.ts index 0022afc285d7..0c2bb410b1cd 100644 --- a/packages/data-transport-layer/src/services/l2-ingestion/handlers/transaction.ts +++ b/packages/data-transport-layer/src/services/l2-ingestion/handlers/transaction.ts @@ -13,6 +13,7 @@ import { padHexString, SEQUENCER_ENTRYPOINT_ADDRESS, SEQUENCER_GAS_LIMIT, + parseSignatureVParam, } from '../../../utils' export const handleSequencerBlock = { @@ -43,7 +44,7 @@ export const handleSequencerBlock = { if (transaction.queueOrigin === 'sequencer') { const decodedTransaction: DecodedSequencerBatchTransaction = { sig: { - v: BigNumber.from(transaction.v).toNumber() - 2 * chainId - 35, + v: parseSignatureVParam(transaction.v, chainId), r: padHexString(transaction.r, 32), s: padHexString(transaction.s, 32), }, @@ -57,7 +58,7 @@ export const handleSequencerBlock = { transactionEntry = { ...transactionEntry, - gasLimit: SEQUENCER_GAS_LIMIT, // ? + gasLimit: `${SEQUENCER_GAS_LIMIT}`, // ? target: SEQUENCER_ENTRYPOINT_ADDRESS, origin: null, data: serialize( @@ -82,7 +83,7 @@ export const handleSequencerBlock = { } else { transactionEntry = { ...transactionEntry, - gasLimit: BigNumber.from(transaction.gas).toNumber(), + gasLimit: BigNumber.from(transaction.gas).toString(), target: ethers.utils.getAddress(transaction.to), origin: ethers.utils.getAddress(transaction.l1TxOrigin), data: transaction.input, diff --git a/packages/data-transport-layer/src/services/l2-ingestion/service.ts b/packages/data-transport-layer/src/services/l2-ingestion/service.ts index 120bb35ad734..a9f59f7cb261 100644 --- a/packages/data-transport-layer/src/services/l2-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l2-ingestion/service.ts @@ -3,6 +3,8 @@ import { BaseService } from '@eth-optimism/common-ts' import { JsonRpcProvider } from '@ethersproject/providers' import { BigNumber } from 'ethers' import { LevelUp } from 'levelup' +import axios from 'axios' +import bfj from 'bfj' /* Imports: Internal */ import { TransportDB } from '../../db/transport-db' @@ -168,11 +170,31 @@ export class L2IngestionService extends BaseService { ) }) } else { - blocks = await this.state.l2RpcProvider.send('eth_getBlockRange', [ - toRpcHexString(startBlockNumber), - toRpcHexString(endBlockNumber), - true, - ]) + // This request returns a large response. Parsing it into JSON inside the ethers library is + // quite slow, and can block the event loop for upwards of multiple seconds. When this happens, + // incoming http requests will likely timeout and fail. + // Instead, we will parse the incoming http stream directly with the bfj package, which yields + // the event loop periodically so that we don't fail to serve requests. + const req = { + jsonrpc: '2.0', + method: 'eth_getBlockRange', + params: [ + toRpcHexString(startBlockNumber), + toRpcHexString(endBlockNumber), + true, + ], + id: '1', + } + + const resp = await axios.post( + this.state.l2RpcProvider.connection.url, + req, + { responseType: 'stream' } + ) + const respJson = await bfj.parse(resp.data, { + yieldRate: 4096, // this yields abit more often than the default of 16384 + }) + blocks = respJson.result } for (const block of blocks) { diff --git a/packages/data-transport-layer/src/services/main/service.ts b/packages/data-transport-layer/src/services/main/service.ts index d4a16923e2f6..b4d0839a360e 100644 --- a/packages/data-transport-layer/src/services/main/service.ts +++ b/packages/data-transport-layer/src/services/main/service.ts @@ -1,5 +1,5 @@ /* Imports: External */ -import { BaseService } from '@eth-optimism/common-ts' +import { BaseService, Logger } from '@eth-optimism/common-ts' import { LevelUp } from 'levelup' import level from 'level' diff --git a/packages/data-transport-layer/src/services/server/service.ts b/packages/data-transport-layer/src/services/server/service.ts index 8774181ade3e..7accc0bd8dec 100644 --- a/packages/data-transport-layer/src/services/server/service.ts +++ b/packages/data-transport-layer/src/services/server/service.ts @@ -1,5 +1,5 @@ /* Imports: External */ -import { BaseService, Metrics } from '@eth-optimism/common-ts' +import { BaseService, Logger, Metrics } from '@eth-optimism/common-ts' import express, { Request, Response } from 'express' import promBundle from 'express-prom-bundle' import cors from 'cors' @@ -125,10 +125,17 @@ export class L1TransportServer extends BaseService { * Initialize Sentry and related middleware */ private _initSentry() { - Sentry.init({ + const sentryOptions = { dsn: this.options.sentryDsn, release: this.options.release, environment: this.options.ethNetworkName, + } + this.logger = new Logger({ + name: this.name, + sentryOptions, + }) + Sentry.init({ + ...sentryOptions, integrations: [ new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ @@ -198,6 +205,7 @@ export class L1TransportServer extends BaseService { url: req.url, elapsed, msg: e.toString(), + stack: e.stack, }) return res.status(400).json({ error: e.toString(), diff --git a/packages/data-transport-layer/src/types/database-types.ts b/packages/data-transport-layer/src/types/database-types.ts index 32e52288ee4f..470b402c3590 100644 --- a/packages/data-transport-layer/src/types/database-types.ts +++ b/packages/data-transport-layer/src/types/database-types.ts @@ -16,7 +16,7 @@ export interface EnqueueEntry { index: number target: string data: string - gasLimit: number + gasLimit: string origin: string blockNumber: number timestamp: number @@ -28,7 +28,7 @@ export interface TransactionEntry { data: string blockNumber: number timestamp: number - gasLimit: number + gasLimit: string target: string origin: string value: string diff --git a/packages/data-transport-layer/src/types/event-handler-types.ts b/packages/data-transport-layer/src/types/event-handler-types.ts index 8b2e190b6efd..1839796b527e 100644 --- a/packages/data-transport-layer/src/types/event-handler-types.ts +++ b/packages/data-transport-layer/src/types/event-handler-types.ts @@ -20,7 +20,8 @@ export type GetExtraDataHandler = ( export type ParseEventHandler = ( event: TypedEthersEvent, - extraData: TExtraData + extraData: TExtraData, + l2ChainId: number ) => TParsedEvent export type StoreEventHandler = ( @@ -40,7 +41,7 @@ export interface SequencerBatchAppendedExtraData { submitter: string l1TransactionData: string l1TransactionHash: string - gasLimit: number + gasLimit: string // Stuff from TransactionBatchAppended. prevTotalElements: BigNumber diff --git a/packages/data-transport-layer/src/utils/eth-tx.ts b/packages/data-transport-layer/src/utils/eth-tx.ts new file mode 100644 index 000000000000..cce210d26909 --- /dev/null +++ b/packages/data-transport-layer/src/utils/eth-tx.ts @@ -0,0 +1,9 @@ +/* Imports: External */ +import { ethers } from 'ethers' + +export const parseSignatureVParam = ( + v: number | ethers.BigNumber, + chainId: number +): number => { + return ethers.BigNumber.from(v).toNumber() - 2 * chainId - 35 +} diff --git a/packages/data-transport-layer/src/utils/index.ts b/packages/data-transport-layer/src/utils/index.ts index 482c5161307c..411d8cdb6d80 100644 --- a/packages/data-transport-layer/src/utils/index.ts +++ b/packages/data-transport-layer/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './common' export * from './constants' export * from './contracts' export * from './validation' +export * from './eth-tx' diff --git a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/sequencer-batch-appended.spec.ts b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/sequencer-batch-appended.spec.ts index 0a6690bf832e..f4ad79c89730 100644 --- a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/sequencer-batch-appended.spec.ts +++ b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/sequencer-batch-appended.spec.ts @@ -1,9 +1,9 @@ import { BigNumber, ethers } from 'ethers' + +/* Imports: Internal */ import { expect } from '../../../../setup' import { handleEventsSequencerBatchAppended } from '../../../../../src/services/l1-ingestion/handlers/sequencer-batch-appended' import { SequencerBatchAppendedExtraData } from '../../../../../src/types' -import { l1TransactionData } from '../../../examples/l1-data' -import { blocksOnL2 } from '../../../examples/l2-data' describe('Event Handlers: OVM_CanonicalTransactionChain.SequencerBatchAppended', () => { describe('handleEventsSequencerBatchAppended.parseEvent', () => { @@ -18,7 +18,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.SequencerBatchAppended', submitter: '0xfd7d4de366850c08ee2cba32d851385a3071ec8d', l1TransactionHash: '0x6effe006836b841205ace4d99d7ae1b74ee96aac499a3f358b97fccd32ee9af2', - gasLimit: 548976, + gasLimit: '548976', prevTotalElements: BigNumber.from(73677), batchIndex: BigNumber.from(743), batchSize: BigNumber.from(101), @@ -28,7 +28,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.SequencerBatchAppended', } it('should error on malformed transaction data', async () => { - const input1: [any, SequencerBatchAppendedExtraData] = [ + const input1: [any, SequencerBatchAppendedExtraData, number] = [ { args: { _startingQueueIndex: ethers.constants.Zero, @@ -40,6 +40,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.SequencerBatchAppended', l1TransactionData: '0x00000', ...exampleExtraData, }, + 0, ] expect(() => { diff --git a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/state-batch-appended.spec.ts b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/state-batch-appended.spec.ts index fb16de430cbe..8090a05cb4f4 100644 --- a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/state-batch-appended.spec.ts +++ b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/state-batch-appended.spec.ts @@ -1,7 +1,10 @@ +import { expect } from '../../../../setup' + +/* Imports: External */ import { BigNumber } from 'ethers' import { Block } from '@ethersproject/abstract-provider' -import { expect } from '../../../../setup' +/* Imports: Internal */ import { handleEventsStateBatchAppended } from '../../../../../src/services/l1-ingestion/handlers/state-batch-appended' import { StateBatchAppendedExtraData } from '../../../../../src/types' import { l1StateBatchData } from '../../../examples/l1-data' @@ -73,7 +76,11 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.StateBatchAppended', () l1TransactionHash: '0x4ca72484e93cdb50fe1089984db152258c2bbffc2534dcafbfe032b596bd5b49', } - const input1: [any, StateBatchAppendedExtraData] = [event, extraData] + const input1: [any, StateBatchAppendedExtraData, number] = [ + event, + extraData, + 0, + ] const output1 = handleEventsStateBatchAppended.parseEvent(...input1) diff --git a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/transaction-enqueued.spec.ts b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/transaction-enqueued.spec.ts index 52e5da27739e..0e3ef5e25541 100644 --- a/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/transaction-enqueued.spec.ts +++ b/packages/data-transport-layer/test/unit-tests/services/l1-ingestion/handlers/transaction-enqueued.spec.ts @@ -1,6 +1,9 @@ +import { expect } from '../../../../setup' + +/* Imports: External */ import { ethers, BigNumber } from 'ethers' -import { expect } from '../../../../setup' +/* Imports: Internal */ import { handleEventsTransactionEnqueued } from '../../../../../src/services/l1-ingestion/handlers/transaction-enqueued' const MAX_ITERATIONS = 128 @@ -22,7 +25,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () // but it's probably better to get wider test coverage first. it('should have a ctcIndex equal to null', () => { - const input1: [any, any] = [ + const input1: [any, any, number] = [ { blockNumber: 0, args: { @@ -32,6 +35,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () }, }, null, + 0, ] const output1 = handleEventsTransactionEnqueued.parseEvent(...input1) @@ -47,7 +51,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () i < Number.MAX_SAFE_INTEGER; i += Math.floor(Number.MAX_SAFE_INTEGER / MAX_ITERATIONS) ) { - const input1: [any, any] = [ + const input1: [any, any, number] = [ { blockNumber: i, args: { @@ -57,6 +61,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () }, }, null, + 0, ] const output1 = handleEventsTransactionEnqueued.parseEvent(...input1) @@ -73,7 +78,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () i < Number.MAX_SAFE_INTEGER; i += Math.floor(Number.MAX_SAFE_INTEGER / MAX_ITERATIONS) ) { - const input1: [any, any] = [ + const input1: [any, any, number] = [ { blockNumber: 0, args: { @@ -83,6 +88,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () }, }, null, + 0, ] const output1 = handleEventsTransactionEnqueued.parseEvent(...input1) @@ -93,13 +99,13 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () } }) - it('should have a gasLimit equal to the integer value of the _gasLimit argument', () => { + it('should have a gasLimit equal to the string value of the _gasLimit argument', () => { for ( let i = 0; i < Number.MAX_SAFE_INTEGER; i += Math.floor(Number.MAX_SAFE_INTEGER / MAX_ITERATIONS) ) { - const input1: [any, any] = [ + const input1: [any, any, number] = [ { blockNumber: 0, args: { @@ -109,11 +115,12 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () }, }, null, + 0, ] const output1 = handleEventsTransactionEnqueued.parseEvent(...input1) - const expected1 = BigNumber.from(i).toNumber() + const expected1 = BigNumber.from(i).toString() expect(output1).to.have.property('gasLimit', expected1) } @@ -125,7 +132,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () i < Number.MAX_SAFE_INTEGER; i += Math.floor(Number.MAX_SAFE_INTEGER / MAX_ITERATIONS) ) { - const input1: [any, any] = [ + const input1: [any, any, number] = [ { blockNumber: 0, args: { @@ -135,6 +142,7 @@ describe('Event Handlers: OVM_CanonicalTransactionChain.TransactionEnqueued', () }, }, null, + 0, ] const output1 = handleEventsTransactionEnqueued.parseEvent(...input1) diff --git a/packages/data-transport-layer/test/unit-tests/services/l2-ingestion/handlers/transaction.spec.ts b/packages/data-transport-layer/test/unit-tests/services/l2-ingestion/handlers/transaction.spec.ts index dac1dcc3aa45..0f7a71323623 100644 --- a/packages/data-transport-layer/test/unit-tests/services/l2-ingestion/handlers/transaction.spec.ts +++ b/packages/data-transport-layer/test/unit-tests/services/l2-ingestion/handlers/transaction.spec.ts @@ -1,4 +1,6 @@ import { expect } from '../../../../setup' + +/* Imports: Internal */ import { l2Block } from '../../../examples/l2-data' import { handleSequencerBlock } from '../../../../../src/services/l2-ingestion/handlers/transaction' diff --git a/packages/message-relayer/CHANGELOG.md b/packages/message-relayer/CHANGELOG.md index 01e27330bcfe..0a8a41e3a2bb 100644 --- a/packages/message-relayer/CHANGELOG.md +++ b/packages/message-relayer/CHANGELOG.md @@ -1,5 +1,29 @@ # @eth-optimism/message-relayer +## 0.1.5 + +### Patch Changes + +- baa3b761: Improve Sentry support, initializing as needed and ensuring ERROR logs route to Sentry +- Updated dependencies [baa3b761] + - @eth-optimism/common-ts@0.1.3 + +## 0.1.4 + +### Patch Changes + +- 9d39121b: Adds a README and cleans up the interface for generating messages and proofs +- 86708bb5: Adds a new set of tools for generating messages to be relayed and their proofs +- 064c03af: Removes spreadsheet mode from the message relayer +- Updated dependencies [a64f8161] +- Updated dependencies [4e03f8a9] +- Updated dependencies [8e2bfd07] +- Updated dependencies [750a5021] +- Updated dependencies [c2b6e14b] +- Updated dependencies [245136f1] + - @eth-optimism/core-utils@0.4.5 + - @eth-optimism/contracts@0.3.5 + ## 0.1.3 ### Patch Changes diff --git a/packages/message-relayer/README.md b/packages/message-relayer/README.md new file mode 100644 index 000000000000..964f1a67e5e9 --- /dev/null +++ b/packages/message-relayer/README.md @@ -0,0 +1,69 @@ +# @eth-optimism/message-relayer + +This package contains: + +1. A service for relaying messages from L2 to L1. +2. Utilities for finding these messages and relaying them. + +## Installation + +``` +yarn add @eth-optimism/message-relayer +``` + +## Relay Utilities + +### getMessagesAndProofsForL2Transaction + +Finds all L2 => L1 messages sent in a given L2 transaction and generates proof for each. + +#### Usage + +```typescript +import { getMessagesAndProofsForL2Transaction } from '@eth-optimism/message-relayer' + +const main = async () => { + const l1RpcProviderUrl = 'https://layer1.endpoint' + const l2RpcProviderUrl = 'https://layer2.endpoint' + const l1StateCommitmentChainAddress = 'address of OVM_StateCommitmentChain from deployments page' + const l2CrossDomainMessengerAddress = 'address of OVM_L2CrossDomainMessenger from deployments page' + const l2TransactionHash = 'hash of the transaction with messages to relay' + + const messagePairs = await getMessagesAndProofsForL2Transaction( + l1RpcProviderUrl, + l2RpcProviderUrl, + l1StateCommitmentChainAddress, + l2CrossDomainMessengerAddress, + l2TransactionHash + ) + + console.log(messagePairs) + // Will log something along the lines of: + // [ + // { + // message: { + // target: '0x...', + // sender: '0x...', + // message: '0x...', + // messageNonce: 1234... + // }, + // proof: { + // // complicated + // } + // } + // ] + + // You can then do something along the lines of: + // for (const { message, proof } of messagePairs) { + // await l1CrossDomainMessenger.relayMessage( + // message.target, + // message.sender, + // message.message, + // message.messageNonce, + // proof + // ) + // } +} + +main() +``` diff --git a/packages/message-relayer/hardhat.config.ts b/packages/message-relayer/hardhat.config.ts new file mode 100644 index 000000000000..fac7c21376f0 --- /dev/null +++ b/packages/message-relayer/hardhat.config.ts @@ -0,0 +1,15 @@ +import { HardhatUserConfig } from 'hardhat/config' + +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' + +const config: HardhatUserConfig = { + paths: { + sources: './test/test-contracts', + }, + solidity: { + version: '0.7.6', + }, +} + +export default config diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json index 20ce2406e96b..2847bc3af931 100644 --- a/packages/message-relayer/package.json +++ b/packages/message-relayer/package.json @@ -1,7 +1,6 @@ { "name": "@eth-optimism/message-relayer", - "version": "0.1.3", - "private": true, + "version": "0.1.5", "description": "[Optimism] Cross Domain Message Relayer service", "main": "dist/index", "types": "dist/index", @@ -14,7 +13,8 @@ "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo", "lint": "yarn lint:fix && yarn lint:check", "lint:fix": "prettier --config .prettierrc.json --write \"{src,exec,test}/**/*.ts\"", - "lint:check": "tslint --format stylish --project ." + "lint:check": "tslint --format stylish --project .", + "test": "hardhat test --show-stack-traces" }, "keywords": [ "optimism", @@ -29,17 +29,29 @@ "url": "https://github.com/ethereum-optimism/optimism.git" }, "dependencies": { - "@eth-optimism/common-ts": "^0.1.2", + "@eth-optimism/common-ts": "^0.1.3", + "@eth-optimism/contracts": "^0.3.5", + "@eth-optimism/core-utils": "^0.4.5", + "@sentry/node": "6.2.5", "bcfg": "^0.1.6", - "@eth-optimism/contracts": "^0.3.3", - "@eth-optimism/core-utils": "^0.4.3", "dotenv": "^8.2.0", "ethers": "^5.1.0", - "google-spreadsheet": "^3.1.15", "merkletreejs": "^0.2.18", "rlp": "^2.2.6" }, "devDependencies": { + "@eth-optimism/smock": "^1.1.5", + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "@types/chai": "^4.2.18", + "@types/chai-as-promised": "^7.1.4", + "@types/mocha": "^8.2.2", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "ethereum-waffle": "^3.3.0", + "hardhat": "^2.3.0", + "lodash": "^4.17.21", + "mocha": "^8.4.0", "prettier": "^2.2.1", "tslint": "^6.1.3", "tslint-config-prettier": "^1.18.0", diff --git a/packages/message-relayer/src/exec/run.ts b/packages/message-relayer/src/exec/run.ts index c12c67d09215..1cca64005d0a 100644 --- a/packages/message-relayer/src/exec/run.ts +++ b/packages/message-relayer/src/exec/run.ts @@ -1,7 +1,8 @@ import { Wallet, providers } from 'ethers' import { MessageRelayerService } from '../service' import { Bcfg } from '@eth-optimism/core-utils' -import SpreadSheet from '../spreadsheet' +import { Logger, LoggerOptions } from '@eth-optimism/common-ts' +import * as Sentry from '@sentry/node' import * as dotenv from 'dotenv' import Config from 'bcfg' @@ -15,6 +16,27 @@ const main = async () => { }) const env = process.env + + const SENTRY_DSN = config.str('sentry-dsn', env.SENTRY_DSN) + const USE_SENTRY = config.bool('use-sentry', env.USE_SENTRY === 'true') + const ETH_NETWORK_NAME = config.str('eth-network-name', env.ETH_NETWORK_NAME) + + const loggerOptions: LoggerOptions = { + name: 'Message_Relayer', + } + + if (USE_SENTRY) { + const sentryOptions = { + release: `message-relayer@${process.env.npm_package_version}`, + dsn: SENTRY_DSN, + environment: ETH_NETWORK_NAME, + } + loggerOptions.sentryOptions = sentryOptions + Sentry.init(sentryOptions) + } + + const logger = new Logger(loggerOptions) + const L2_NODE_WEB3_URL = config.str('l2-node-web3-url', env.L2_NODE_WEB3_URL) const L1_NODE_WEB3_URL = config.str('l1-node-web3-url', env.L1_NODE_WEB3_URL) const ADDRESS_MANAGER_ADDRESS = config.str( @@ -49,18 +71,6 @@ const main = async () => { parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0 ) - // Spreadsheet configuration - const SPREADSHEET_MODE = config.bool( - 'spreadsheet-mode', - !!env.SPREADSHEET_MODE || false - ) - const SHEET_ID = config.str('sheet-id', env.SHEET_ID) - const CLIENT_EMAIL = config.str('client-email', env.CLIENT_EMAIL) - const CLIENT_PRIVATE_KEY = config.str( - 'client-private-key', - env.CLIENT_PRIVATE_KEY - ) - if (!ADDRESS_MANAGER_ADDRESS) { throw new Error('Must pass ADDRESS_MANAGER_ADDRESS') } @@ -84,22 +94,6 @@ const main = async () => { throw new Error('Must pass one of L1_WALLET_KEY or MNEMONIC') } - let spreadsheet = null - if (SPREADSHEET_MODE) { - if (!SHEET_ID) { - throw new Error('Must pass SHEET_ID') - } - if (!CLIENT_EMAIL) { - throw new Error('Must pass CLIENT_EMAIL') - } - if (!CLIENT_PRIVATE_KEY) { - throw new Error('Must pass CLIENT_PRIVATE_KEY') - } - const privateKey = CLIENT_PRIVATE_KEY.replace(/\\n/g, '\n') - spreadsheet = new SpreadSheet(SHEET_ID) - await spreadsheet.init(CLIENT_EMAIL, privateKey) - } - const service = new MessageRelayerService({ l1RpcProvider: l1Provider, l2RpcProvider: l2Provider, @@ -111,8 +105,7 @@ const main = async () => { l2BlockOffset: L2_BLOCK_OFFSET, l1StartOffset: L1_START_OFFSET, getLogsInterval: GET_LOGS_INTERVAL, - spreadsheetMode: !!SPREADSHEET_MODE, - spreadsheet, + logger, }) await service.start() diff --git a/packages/message-relayer/src/index.ts b/packages/message-relayer/src/index.ts new file mode 100644 index 000000000000..419b5715f12d --- /dev/null +++ b/packages/message-relayer/src/index.ts @@ -0,0 +1 @@ +export * from './relay-tx' diff --git a/packages/message-relayer/src/relay-tx.ts b/packages/message-relayer/src/relay-tx.ts new file mode 100644 index 000000000000..92c83e189add --- /dev/null +++ b/packages/message-relayer/src/relay-tx.ts @@ -0,0 +1,425 @@ +/* Imports: External */ +import { ethers } from 'ethers' +import { + fromHexString, + remove0x, + toHexString, + toRpcHexString, +} from '@eth-optimism/core-utils' +import { getContractInterface, predeploys } from '@eth-optimism/contracts' +import * as rlp from 'rlp' +import { MerkleTree } from 'merkletreejs' + +// Number of blocks added to the L2 chain before the first L2 transaction. Genesis are added to the +// chain to initialize the system. However, they create a discrepancy between the L2 block number +// the index of the transaction that corresponds to that block number. For example, if there's 1 +// genesis block, then the transaction with an index of 0 corresponds to the block with index 1. +const NUM_L2_GENESIS_BLOCKS = 1 + +interface StateRootBatchHeader { + batchIndex: ethers.BigNumber + batchRoot: string + batchSize: ethers.BigNumber + prevTotalElements: ethers.BigNumber + extraData: string +} + +interface StateRootBatch { + header: StateRootBatchHeader + stateRoots: string[] +} + +interface CrossDomainMessage { + target: string + sender: string + message: string + messageNonce: number +} + +interface CrossDomainMessageProof { + stateRoot: string + stateRootBatchHeader: StateRootBatchHeader + stateRootProof: { + index: number + siblings: string[] + } + stateTrieWitness: string + storageTrieWitness: string +} + +interface CrossDomainMessagePair { + message: CrossDomainMessage + proof: CrossDomainMessageProof +} + +interface StateTrieProof { + accountProof: string + storageProof: string +} + +/** + * Finds all L2 => L1 messages triggered by a given L2 transaction, if the message exists. + * @param l2RpcProvider L2 RPC provider. + * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. + * @param l2TransactionHash Hash of the L2 transaction to find a message for. + * @returns Messages associated with the transaction. + */ +export const getMessagesByTransactionHash = async ( + l2RpcProvider: ethers.providers.JsonRpcProvider, + l2CrossDomainMessengerAddress: string, + l2TransactionHash: string +): Promise => { + // Complain if we can't find the given transaction. + const transaction = await l2RpcProvider.getTransaction(l2TransactionHash) + if (transaction === null) { + throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) + } + + const l2CrossDomainMessenger = new ethers.Contract( + l2CrossDomainMessengerAddress, + getContractInterface('OVM_L2CrossDomainMessenger'), + l2RpcProvider + ) + + // Find all SentMessage events created in the same block as the given transaction. This is + // reliable because we should only have one transaction per block. + const sentMessageEvents = await l2CrossDomainMessenger.queryFilter( + l2CrossDomainMessenger.filters.SentMessage(), + transaction.blockNumber, + transaction.blockNumber + ) + + // Decode the messages and turn them into a nicer struct. + const sentMessages = sentMessageEvents.map((sentMessageEvent) => { + const encodedMessage = sentMessageEvent.args.message + const decodedMessage = l2CrossDomainMessenger.interface.decodeFunctionData( + 'relayMessage', + encodedMessage + ) + + return { + target: decodedMessage._target, + sender: decodedMessage._sender, + message: decodedMessage._message, + messageNonce: decodedMessage._messageNonce.toNumber(), + } + }) + + return sentMessages +} + +/** + * Encodes a cross domain message. + * @param message Message to encode. + * @returns Encoded message. + */ +const encodeCrossDomainMessage = (message: CrossDomainMessage): string => { + return getContractInterface( + 'OVM_L2CrossDomainMessenger' + ).encodeFunctionData('relayMessage', [ + message.target, + message.sender, + message.message, + message.messageNonce, + ]) +} + +/** + * Finds the StateBatchAppended event associated with a given L2 transaction. + * @param l1RpcProvider L1 RPC provider. + * @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain. + * @param l2TransactionIndex Index of the L2 transaction to find a StateBatchAppended event for. + * @returns StateBatchAppended event for the given transaction or null if no such event exists. + */ +export const getStateBatchAppendedEventByTransactionIndex = async ( + l1RpcProvider: ethers.providers.JsonRpcProvider, + l1StateCommitmentChainAddress: string, + l2TransactionIndex: number +): Promise => { + const l1StateCommitmentChain = new ethers.Contract( + l1StateCommitmentChainAddress, + getContractInterface('OVM_StateCommitmentChain'), + l1RpcProvider + ) + + const getStateBatchAppendedEventByBatchIndex = async ( + index: number + ): Promise => { + const eventQueryResult = await l1StateCommitmentChain.queryFilter( + l1StateCommitmentChain.filters.StateBatchAppended(index) + ) + if (eventQueryResult.length === 0) { + return null + } else { + return eventQueryResult[0] + } + } + + const isEventHi = (event: ethers.Event, index: number) => { + const prevTotalElements = event.args._prevTotalElements.toNumber() + return index < prevTotalElements + } + + const isEventLo = (event: ethers.Event, index: number) => { + const prevTotalElements = event.args._prevTotalElements.toNumber() + const batchSize = event.args._batchSize.toNumber() + return index >= prevTotalElements + batchSize + } + + const totalBatches: ethers.BigNumber = await l1StateCommitmentChain.getTotalBatches() + if (totalBatches.eq(0)) { + return null + } + + let lowerBound = 0 + let upperBound = totalBatches.toNumber() - 1 + let batchEvent: ethers.Event | null = await getStateBatchAppendedEventByBatchIndex( + upperBound + ) + + if (isEventLo(batchEvent, l2TransactionIndex)) { + // Upper bound is too low, means this transaction doesn't have a corresponding state batch yet. + return null + } else if (!isEventHi(batchEvent, l2TransactionIndex)) { + // Upper bound is not too low and also not too high. This means the upper bound event is the + // one we're looking for! Return it. + return batchEvent + } + + // Binary search to find the right event. The above checks will guarantee that the event does + // exist and that we'll find it during this search. + while (lowerBound < upperBound) { + const middleOfBounds = Math.floor((lowerBound + upperBound) / 2) + batchEvent = await getStateBatchAppendedEventByBatchIndex(middleOfBounds) + + if (isEventHi(batchEvent, l2TransactionIndex)) { + upperBound = middleOfBounds + } else if (isEventLo(batchEvent, l2TransactionIndex)) { + lowerBound = middleOfBounds + } else { + break + } + } + + return batchEvent +} + +/** + * Finds the full state root batch associated with a given transaction index. + * @param l1RpcProvider L1 RPC provider. + * @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain. + * @param l2TransactionIndex Index of the L2 transaction to find a state root batch for. + * @returns State root batch associated with the given transaction index or null if no state root + * batch exists. + */ +export const getStateRootBatchByTransactionIndex = async ( + l1RpcProvider: ethers.providers.JsonRpcProvider, + l1StateCommitmentChainAddress: string, + l2TransactionIndex: number +): Promise => { + const l1StateCommitmentChain = new ethers.Contract( + l1StateCommitmentChainAddress, + getContractInterface('OVM_StateCommitmentChain'), + l1RpcProvider + ) + + const stateBatchAppendedEvent = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + l1StateCommitmentChainAddress, + l2TransactionIndex + ) + if (stateBatchAppendedEvent === null) { + return null + } + + const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction() + const [stateRoots] = l1StateCommitmentChain.interface.decodeFunctionData( + 'appendStateBatch', + stateBatchTransaction.data + ) + + return { + header: { + batchIndex: stateBatchAppendedEvent.args._batchIndex, + batchRoot: stateBatchAppendedEvent.args._batchRoot, + batchSize: stateBatchAppendedEvent.args._batchSize, + prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements, + extraData: stateBatchAppendedEvent.args._extraData, + }, + stateRoots, + } +} + +/** + * Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree). + * @param leaves Leaves of the merkle tree. + * @param index Index to generate a proof for. + * @returns Merkle proof sibling leaves, as hex strings. + */ +const getMerkleTreeProof = (leaves: string[], index: number): string[] => { + // Our specific Merkle tree implementation requires that the number of leaves is a power of 2. + // If the number of given leaves is less than a power of 2, we need to round up to the next + // available power of 2. We fill the remaining space with the hash of bytes32(0). + const correctedTreeSize = Math.pow(2, Math.ceil(Math.log2(leaves.length))) + const parsedLeaves = [] + for (let i = 0; i < correctedTreeSize; i++) { + if (i < leaves.length) { + parsedLeaves.push(leaves[i]) + } else { + parsedLeaves.push(ethers.utils.keccak256('0x' + '00'.repeat(32))) + } + } + + // merkletreejs prefers things to be Buffers. + const bufLeaves = parsedLeaves.map(fromHexString) + const tree = new MerkleTree( + bufLeaves, + (el: Buffer | string): Buffer => { + return fromHexString(ethers.utils.keccak256(el)) + } + ) + + const proof = tree.getProof(bufLeaves[index], index).map((element: any) => { + return toHexString(element.data) + }) + + return proof +} + +/** + * Generates a Merkle-Patricia trie proof for a given account and storage slot. + * @param l2RpcProvider L2 RPC provider. + * @param blockNumber Block number to generate the proof at. + * @param address Address to generate the proof for. + * @param slot Storage slot to generate the proof for. + * @returns Account proof and storage proof. + */ +const getStateTrieProof = async ( + l2RpcProvider: ethers.providers.JsonRpcProvider, + blockNumber: number, + address: string, + slot: string +): Promise => { + const proof = await l2RpcProvider.send('eth_getProof', [ + address, + [slot], + toRpcHexString(blockNumber), + ]) + + return { + accountProof: toHexString(rlp.encode(proof.accountProof)), + storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)), + } +} + +/** + * Finds all L2 => L1 messages sent in a given L2 transaction and generates proofs for each of + * those messages. + * @param l1RpcProvider L1 RPC provider. + * @param l2RpcProvider L2 RPC provider. + * @param l1StateCommitmentChainAddress Address of the StateCommitmentChain. + * @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger. + * @param l2TransactionHash L2 transaction hash to generate a relay transaction for. + * @returns An array of messages sent in the transaction and a proof of inclusion for each. + */ +export const getMessagesAndProofsForL2Transaction = async ( + l1RpcProvider: ethers.providers.JsonRpcProvider | string, + l2RpcProvider: ethers.providers.JsonRpcProvider | string, + l1StateCommitmentChainAddress: string, + l2CrossDomainMessengerAddress: string, + l2TransactionHash: string +): Promise => { + if (typeof l1RpcProvider === 'string') { + l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProvider) + } + if (typeof l2RpcProvider === 'string') { + l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProvider) + } + + const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash) + if (l2Transaction === null) { + throw new Error(`unable to find tx with hash: ${l2TransactionHash}`) + } + + // Need to find the state batch for the given transaction. If no state batch has been published + // yet then we will not be able to generate a proof. + const batch = await getStateRootBatchByTransactionIndex( + l1RpcProvider, + l1StateCommitmentChainAddress, + l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS + ) + if (batch === null) { + throw new Error( + `unable to find state root batch for tx with hash: ${l2TransactionHash}` + ) + } + + // Adjust the transaction index based on the number of L2 genesis block we have. "Index" here + // refers to the position of the transaction within the *Canonical Transaction Chain*. + const l2TransactionIndex = l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS + + // Here the index refers to the position of the state root that corresponds to this transaction + // within the batch of state roots in which that state root was published. + const txIndexInBatch = + l2TransactionIndex - batch.header.prevTotalElements.toNumber() + + // Find every message that was sent during this transaction. We'll then attach a proof for each. + const messages = await getMessagesByTransactionHash( + l2RpcProvider, + l2CrossDomainMessengerAddress, + l2TransactionHash + ) + + const messagePairs: CrossDomainMessagePair[] = [] + for (const message of messages) { + // We need to calculate the specific storage slot that demonstrates that this message was + // actually included in the L2 chain. The following calculation is based on the fact that + // messages are stored in the following mapping on L2: + // https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_L2ToL1MessagePasser.sol#L23 + // You can read more about how Solidity storage slots are computed for mappings here: + // https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays + const messageSlot = ethers.utils.keccak256( + ethers.utils.keccak256( + encodeCrossDomainMessage(message) + + remove0x(l2CrossDomainMessengerAddress) + ) + '00'.repeat(32) + ) + + // We need a Merkle trie proof for the given storage slot. This allows us to prove to L1 that + // the message was actually sent on L2. + const stateTrieProof = await getStateTrieProof( + l2RpcProvider, + l2Transaction.blockNumber, + predeploys.OVM_L2ToL1MessagePasser, + messageSlot + ) + + // State roots are published in batches to L1 and correspond 1:1 to transactions. We compute a + // Merkle root for these state roots so that we only need to store the minimum amount of + // information on-chain. So we need to create a Merkle proof for the specific state root that + // corresponds to this transaction. + const stateRootMerkleProof = getMerkleTreeProof( + batch.stateRoots, + txIndexInBatch + ) + + // We now have enough information to create the message proof. + const proof: CrossDomainMessageProof = { + stateRoot: batch.stateRoots[txIndexInBatch], + stateRootBatchHeader: batch.header, + stateRootProof: { + index: txIndexInBatch, + siblings: stateRootMerkleProof, + }, + stateTrieWitness: stateTrieProof.accountProof, + storageTrieWitness: stateTrieProof.storageProof, + } + + messagePairs.push({ + message, + proof, + }) + } + + return messagePairs +} diff --git a/packages/message-relayer/src/service.ts b/packages/message-relayer/src/service.ts index 9568f3e1ea23..c69305fd262c 100644 --- a/packages/message-relayer/src/service.ts +++ b/packages/message-relayer/src/service.ts @@ -5,8 +5,7 @@ import { MerkleTree } from 'merkletreejs' /* Imports: Internal */ import { fromHexString, sleep } from '@eth-optimism/core-utils' -import { BaseService } from '@eth-optimism/common-ts' -import SpreadSheet from './spreadsheet' +import { Logger, BaseService, Metrics } from '@eth-optimism/common-ts' import { loadContract, loadContractFromManager } from '@eth-optimism/contracts' import { StateRootBatchHeader, SentMessage, SentMessageProof } from './types' @@ -42,9 +41,11 @@ interface MessageRelayerOptions { // Number of blocks within each getLogs query - max is 2000 getLogsInterval?: number - // Append txs to a spreadsheet instead of submitting transactions - spreadsheetMode?: boolean - spreadsheet?: SpreadSheet + // A custom logger to transport logs via; default STDOUT + logger?: Logger + + // A custom metrics tracker to manage metrics; default undefined + metrics?: Metrics } const optionSettings = { @@ -54,7 +55,6 @@ const optionSettings = { l2BlockOffset: { default: 1 }, l1StartOffset: { default: 0 }, getLogsInterval: { default: 2000 }, - spreadsheetMode: { default: false }, } export class MessageRelayerService extends BaseService { @@ -62,9 +62,6 @@ export class MessageRelayerService extends BaseService { super('Message_Relayer', options, optionSettings) } - protected spreadsheetMode: boolean - protected spreadsheet: SpreadSheet - private state: { lastFinalizedTxHeight: number nextUnfinalizedTxHeight: number @@ -84,7 +81,6 @@ export class MessageRelayerService extends BaseService { pollingInterval: this.options.pollingInterval, l2BlockOffset: this.options.l2BlockOffset, getLogsInterval: this.options.getLogsInterval, - spreadSheetMode: this.options.spreadsheetMode, }) // Need to improve this, sorry. this.state = {} as any @@ -141,10 +137,6 @@ export class MessageRelayerService extends BaseService { this.logger.info('Connected to all contracts.') - if (this.options.spreadsheetMode) { - this.logger.info('Running in spreadsheet mode') - } - this.state.lastQueriedL1Block = this.options.l1StartOffset this.state.eventCache = [] @@ -494,67 +486,12 @@ export class MessageRelayerService extends BaseService { message: SentMessage, proof: SentMessageProof ): Promise { - if (this.options.spreadsheetMode) { - try { - await this.options.spreadsheet.addRow({ - target: message.target, - sender: message.sender, - message: message.message, - messageNonce: message.messageNonce.toString(), - encodedMessage: message.encodedMessage, - encodedMessageHash: message.encodedMessageHash, - parentTransactionIndex: message.parentTransactionIndex, - parentTransactionHash: message.parentTransactionIndex, - stateRoot: proof.stateRoot, - batchIndex: proof.stateRootBatchHeader.batchIndex.toString(), - batchRoot: proof.stateRootBatchHeader.batchRoot, - batchSize: proof.stateRootBatchHeader.batchSize.toString(), - prevTotalElements: proof.stateRootBatchHeader.prevTotalElements.toString(), - extraData: proof.stateRootBatchHeader.extraData, - index: proof.stateRootProof.index, - siblings: proof.stateRootProof.siblings.join(','), - stateTrieWitness: proof.stateTrieWitness.toString('hex'), - storageTrieWitness: proof.storageTrieWitness.toString('hex'), - }) - this.logger.info('Submitted relay message to spreadsheet') - } catch (e) { - this.logger.error('Cannot submit message to spreadsheet') - this.logger.error(e.message) - } - } else { - try { - this.logger.info( - 'Dry-run, checking to make sure proof would succeed...' - ) - - await this.state.OVM_L1CrossDomainMessenger.connect( - this.options.l1Wallet - ).callStatic.relayMessage( - message.target, - message.sender, - message.message, - message.messageNonce, - proof, - { - gasLimit: this.options.relayGasLimit, - } - ) + try { + this.logger.info('Dry-run, checking to make sure proof would succeed...') - this.logger.info( - 'Proof should succeed. Submitting for real this time...' - ) - } catch (err) { - this.logger.error('Proof would fail, skipping', { - message: err.toString(), - stack: err.stack, - code: err.code, - }) - return - } - - const result = await this.state.OVM_L1CrossDomainMessenger.connect( + await this.state.OVM_L1CrossDomainMessenger.connect( this.options.l1Wallet - ).relayMessage( + ).callStatic.relayMessage( message.target, message.sender, message.message, @@ -565,29 +502,51 @@ export class MessageRelayerService extends BaseService { } ) - this.logger.info('Relay message transaction sent', { - transactionHash: result, + this.logger.info('Proof should succeed. Submitting for real this time...') + } catch (err) { + this.logger.error('Proof would fail, skipping', { + message: err.toString(), + stack: err.stack, + code: err.code, }) + return + } - try { - const receipt = await result.wait() - - this.logger.info('Relay message included in block', { - transactionHash: receipt.transactionHash, - blockNumber: receipt.blockNumber, - gasUsed: receipt.gasUsed.toString(), - confirmations: receipt.confirmations, - status: receipt.status, - }) - } catch (err) { - this.logger.error('Real relay attempt failed, skipping.', { - message: err.toString(), - stack: err.stack, - code: err.code, - }) - return + const result = await this.state.OVM_L1CrossDomainMessenger.connect( + this.options.l1Wallet + ).relayMessage( + message.target, + message.sender, + message.message, + message.messageNonce, + proof, + { + gasLimit: this.options.relayGasLimit, } - this.logger.info('Message successfully relayed to Layer 1!') + ) + + this.logger.info('Relay message transaction sent', { + transactionHash: result, + }) + + try { + const receipt = await result.wait() + + this.logger.info('Relay message included in block', { + transactionHash: receipt.transactionHash, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed.toString(), + confirmations: receipt.confirmations, + status: receipt.status, + }) + } catch (err) { + this.logger.error('Real relay attempt failed, skipping.', { + message: err.toString(), + stack: err.stack, + code: err.code, + }) + return } + this.logger.info('Message successfully relayed to Layer 1!') } } diff --git a/packages/message-relayer/src/spreadsheet.ts b/packages/message-relayer/src/spreadsheet.ts deleted file mode 100644 index bf1260c88746..000000000000 --- a/packages/message-relayer/src/spreadsheet.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GoogleSpreadsheet } from 'google-spreadsheet' - -export default class SpreadSheet { - public doc - public sheet - - constructor(id) { - this.doc = new GoogleSpreadsheet(id) - this.sheet = null - } - - async init(email, privateKey) { - await this.doc.useServiceAccountAuth({ - client_email: email, - private_key: privateKey, - }) - - await this.doc.loadInfo() - this.sheet = this.doc.sheetsByIndex[0] - } - - async addRow(row) { - return this.sheet.addRow(row) - } -} diff --git a/packages/message-relayer/test/setup.ts b/packages/message-relayer/test/setup.ts new file mode 100644 index 000000000000..f5952afdbe6b --- /dev/null +++ b/packages/message-relayer/test/setup.ts @@ -0,0 +1,12 @@ +/* External Imports */ +import chai = require('chai') +import Mocha from 'mocha' +import { solidity } from 'ethereum-waffle' +import chaiAsPromised from 'chai-as-promised' + +chai.use(solidity) +chai.use(chaiAsPromised) +const should = chai.should() +const expect = chai.expect + +export { should, expect, Mocha } diff --git a/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol b/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol new file mode 100644 index 000000000000..7545691229b8 --- /dev/null +++ b/packages/message-relayer/test/test-contracts/MockL2CrossDomainMessenger.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.0 <0.9.0; +pragma experimental ABIEncoderV2; + +contract MockL2CrossDomainMessenger { + struct MessageData { + address target; + address sender; + bytes message; + uint256 messageNonce; + } + + event SentMessage(bytes message); + + function emitSentMessageEvent( + MessageData memory _message + ) + public + { + emit SentMessage( + abi.encodeWithSignature( + "relayMessage(address,address,bytes,uint256)", + _message.target, + _message.sender, + _message.message, + _message.messageNonce + ) + ); + } + + function emitMultipleSentMessageEvents( + MessageData[] memory _messages + ) + public + { + for (uint256 i = 0; i < _messages.length; i++) { + emitSentMessageEvent( + _messages[i] + ); + } + } + + function doNothing() public {} +} diff --git a/packages/message-relayer/test/unit-tests/relay-tx.spec.ts b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts new file mode 100644 index 000000000000..1745a620f5f2 --- /dev/null +++ b/packages/message-relayer/test/unit-tests/relay-tx.spec.ts @@ -0,0 +1,377 @@ +import { expect } from '../setup' + +/* Imports: External */ +import hre from 'hardhat' +import { Contract, Signer } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { smockit } from '@eth-optimism/smock' +import { toPlainObject } from 'lodash' + +/* Imports: Internal */ +import { + getMessagesAndProofsForL2Transaction, + getStateRootBatchByTransactionIndex, + getStateBatchAppendedEventByTransactionIndex, + getMessagesByTransactionHash, +} from '../../src/relay-tx' + +describe('relay transaction generation functions', () => { + const ethers = (hre as any).ethers + const l1RpcProvider = ethers.provider + const l2RpcProvider = ethers.provider + + let signer1: Signer + before(async () => { + ;[signer1] = await ethers.getSigners() + }) + + let MockL2CrossDomainMessenger: Contract + beforeEach(async () => { + const factory = await ethers.getContractFactory( + 'MockL2CrossDomainMessenger' + ) + MockL2CrossDomainMessenger = await factory.deploy() + }) + + let StateCommitmentChain: Contract + beforeEach(async () => { + const factory1 = getContractFactory('Lib_AddressManager') + const factory2 = getContractFactory('OVM_ChainStorageContainer') + const factory3 = getContractFactory('OVM_StateCommitmentChain') + + const mockBondManager = await smockit(getContractFactory('OVM_BondManager')) + const mockCanonicalTransactionChain = await smockit( + getContractFactory('OVM_CanonicalTransactionChain') + ) + + mockBondManager.smocked.isCollateralized.will.return.with(true) + mockCanonicalTransactionChain.smocked.getTotalElements.will.return.with( + 999999 + ) + + const AddressManager = await factory1.connect(signer1).deploy() + const ChainStorageContainer = await factory2 + .connect(signer1) + .deploy(AddressManager.address, 'OVM_StateCommitmentChain') + StateCommitmentChain = await factory3 + .connect(signer1) + .deploy(AddressManager.address, 0, 0) + + await AddressManager.setAddress( + 'OVM_ChainStorageContainer:SCC:batches', + ChainStorageContainer.address + ) + + await AddressManager.setAddress( + 'OVM_StateCommitmentChain', + StateCommitmentChain.address + ) + + await AddressManager.setAddress('OVM_BondManager', mockBondManager.address) + + await AddressManager.setAddress( + 'OVM_CanonicalTransactionChain', + mockCanonicalTransactionChain.address + ) + }) + + describe('getMessageByTransactionHash', () => { + it('should throw an error if a transaction with the given hash does not exist', async () => { + await expect( + getMessagesByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + ethers.constants.HashZero + ) + ).to.be.rejected + }) + + it('should return null if the transaction did not emit a SentMessage event', async () => { + const tx = await MockL2CrossDomainMessenger.doNothing() + + expect( + await getMessagesByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.deep.equal([]) + }) + + it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => { + const message = { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + } + const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent(message) + + expect( + await getMessagesByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.deep.equal([message]) + }) + + it('should return the parsed events if the transaction emitted more than one SentMessage event', async () => { + const messages = [ + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + }, + { + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 1, + }, + ] + + const tx = await MockL2CrossDomainMessenger.emitMultipleSentMessageEvents( + messages + ) + + expect( + await getMessagesByTransactionHash( + l2RpcProvider, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.deep.equal(messages) + }) + }) + + describe('getStateBatchAppendedEventByTransactionIndex', () => { + it('should return null when there are no batches yet', async () => { + expect( + await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 0 + ) + ).to.equal(null) + }) + + it('should return null if a batch for the index does not exist', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) + + expect( + await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 // Index 0 is ok but 1 should return null + ) + ).to.equal(null) + }) + + it('should return the batch if the index is part of the first batch', async () => { + // 5 elements + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 0 + ) + + // Add another 5 so we have two batches and can isolate tests against the first. + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 5 + ) + + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(0), + _batchSize: ethers.BigNumber.from(5), + _prevTotalElements: ethers.BigNumber.from(0), + }) + }) + + it('should return the batch if the index is part of the last batch', async () => { + // 5 elements + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 0 + ) + + // Add another 5 so we have two batches and can isolate tests against the second. + await StateCommitmentChain.appendStateBatch( + [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + 5 + ) + + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 7 + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(1), + _batchSize: ethers.BigNumber.from(5), + _prevTotalElements: ethers.BigNumber.from(5), + }) + }) + + for (const numBatches of [1, 2, 8]) { + const elementsPerBatch = 8 + describe(`when there are ${numBatches} batch(es) of ${elementsPerBatch} elements each`, () => { + const totalElements = numBatches * elementsPerBatch + beforeEach(async () => { + for (let i = 0; i < numBatches; i++) { + await StateCommitmentChain.appendStateBatch( + new Array(elementsPerBatch).fill(ethers.constants.HashZero), + i * elementsPerBatch + ) + } + }) + + for (let i = 0; i < totalElements; i += elementsPerBatch) { + it(`should be able to get the correct event for the ${i}th/st/rd/whatever element`, async () => { + const event = await getStateBatchAppendedEventByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + i + ) + + expect(toPlainObject(event.args)).to.deep.include({ + _batchIndex: ethers.BigNumber.from(i / elementsPerBatch), + _batchSize: ethers.BigNumber.from(elementsPerBatch), + _prevTotalElements: ethers.BigNumber.from(i), + }) + }) + } + }) + } + }) + + describe('getStateRootBatchByTransactionIndex', () => { + it('should return null if a batch for the index does not exist', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) + + expect( + await getStateRootBatchByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 1 // Index 0 is ok but 1 should return null + ) + ).to.equal(null) + }) + + it('should return the full batch for a given index when it exists', async () => { + // Should have a total of 1 element now. + await StateCommitmentChain.appendStateBatch( + [ethers.constants.HashZero], + 0 + ) + + const batch = await getStateRootBatchByTransactionIndex( + l1RpcProvider, + StateCommitmentChain.address, + 0 // Index 0 is ok but 1 should return null + ) + + expect(batch.header).to.deep.include({ + batchIndex: ethers.BigNumber.from(0), + batchSize: ethers.BigNumber.from(1), + prevTotalElements: ethers.BigNumber.from(0), + }) + + expect(batch.stateRoots).to.deep.equal([ethers.constants.HashZero]) + }) + }) + + describe('makeRelayTransactionData', () => { + it('should throw an error if the transaction does not exist', async () => { + await expect( + getMessagesAndProofsForL2Transaction( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + ethers.constants.HashZero + ) + ).to.be.rejected + }) + + it('should throw an error if the transaction did not send a message', async () => { + const tx = await MockL2CrossDomainMessenger.doNothing() + + await expect( + getMessagesAndProofsForL2Transaction( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.be.rejected + }) + + it('should throw an error if the corresponding state batch has not been submitted', async () => { + const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent({ + target: ethers.constants.AddressZero, + sender: ethers.constants.AddressZero, + message: '0x', + messageNonce: 0, + }) + + await expect( + getMessagesAndProofsForL2Transaction( + l1RpcProvider, + l2RpcProvider, + StateCommitmentChain.address, + MockL2CrossDomainMessenger.address, + tx.hash + ) + ).to.be.rejected + }) + + // Unfortunately this is hard to test here because hardhat doesn't support eth_getProof. + // Because this function is embedded into the message relayer, we should be able to use + // integration tests to sufficiently test this. + it.skip('should otherwise return the encoded transaction data', () => { + // TODO? + }) + }) +}) diff --git a/packages/smock/CHANGELOG.md b/packages/smock/CHANGELOG.md index 9d108ba56d19..1140eb913484 100644 --- a/packages/smock/CHANGELOG.md +++ b/packages/smock/CHANGELOG.md @@ -1,5 +1,16 @@ # @eth-optimism/smock +## 1.1.5 + +### Patch Changes + +- 5e3c5d1c: Fixes a bug that would break call assertions for overloaded smocked functions +- e6e87ae1: Fix a bug where overloaded functions would not be handled correctly +- Updated dependencies [a64f8161] +- Updated dependencies [750a5021] +- Updated dependencies [c2b6e14b] + - @eth-optimism/core-utils@0.4.5 + ## 1.1.4 ### Patch Changes diff --git a/packages/smock/package.json b/packages/smock/package.json index b21d1551f8ed..4a41c1d58444 100644 --- a/packages/smock/package.json +++ b/packages/smock/package.json @@ -3,7 +3,7 @@ "files": [ "dist/src/*" ], - "version": "1.1.4", + "version": "1.1.5", "main": "dist/src/index", "types": "dist/src/index", "author": "Optimism PBC", @@ -26,7 +26,7 @@ "hardhat": "^2.2.1" }, "dependencies": { - "@eth-optimism/core-utils": "^0.4.1", + "@eth-optimism/core-utils": "^0.4.5", "bn.js": "^5.2.0" }, "devDependencies": { diff --git a/packages/smock/src/smockit/smockit.ts b/packages/smock/src/smockit/smockit.ts index 9ca98ba8c087..1673064518aa 100644 --- a/packages/smock/src/smockit/smockit.ts +++ b/packages/smock/src/smockit/smockit.ts @@ -79,18 +79,25 @@ const smockifyFunction = ( let data: any = toHexString(calldataBuf) try { - data = contract.interface.decodeFunctionData(fragment.name, data) + data = contract.interface.decodeFunctionData( + fragment.format(), + data + ) } catch (e) { console.error(e) } return { functionName: fragment.name, + functionSignature: fragment.format(), data, } }) .filter((functionResult: any) => { - return functionResult.functionName === functionName + return ( + functionResult.functionName === functionName || + functionResult.functionSignature === functionName + ) }) .map((functionResult: any) => { return functionResult.data diff --git a/packages/smock/test/contracts/TestHelpers_MockCaller.sol b/packages/smock/test/contracts/TestHelpers_MockCaller.sol new file mode 100644 index 000000000000..818f3d77087a --- /dev/null +++ b/packages/smock/test/contracts/TestHelpers_MockCaller.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +contract TestHelpers_MockCaller { + function callMock(address _target, bytes memory _data) public { + _target.call(_data); + } +} diff --git a/packages/smock/test/smockit/call-assertions.spec.ts b/packages/smock/test/smockit/call-assertions.spec.ts new file mode 100644 index 000000000000..f4c920410669 --- /dev/null +++ b/packages/smock/test/smockit/call-assertions.spec.ts @@ -0,0 +1,72 @@ +/* Imports: External */ +import hre from 'hardhat' +import { expect } from 'chai' +import { Contract } from 'ethers' + +/* Imports: Internal */ +import { MockContract, smockit } from '../../src' + +describe('[smock]: call assertion tests', () => { + const ethers = (hre as any).ethers + + let mock: MockContract + beforeEach(async () => { + mock = await smockit('TestHelpers_BasicReturnContract') + }) + + let mockCaller: Contract + before(async () => { + const mockCallerFactory = await ethers.getContractFactory( + 'TestHelpers_MockCaller' + ) + mockCaller = await mockCallerFactory.deploy() + }) + + describe('call assertions for functions', () => { + it('should be able to make assertions about a non-overloaded function', async () => { + mock.smocked.getInputtedUint256.will.return.with(0) + + const expected1 = ethers.BigNumber.from(1234) + await mockCaller.callMock( + mock.address, + mock.interface.encodeFunctionData('getInputtedUint256(uint256)', [ + expected1, + ]) + ) + + expect(mock.smocked.getInputtedUint256.calls[0]).to.deep.equal([ + expected1, + ]) + }) + + it('should be able to make assertions about both versions of an overloaded function', async () => { + mock.smocked['overloadedFunction(uint256)'].will.return.with(0) + mock.smocked['overloadedFunction(uint256,uint256)'].will.return.with(0) + + const expected1 = ethers.BigNumber.from(1234) + await mockCaller.callMock( + mock.address, + mock.interface.encodeFunctionData('overloadedFunction(uint256)', [ + expected1, + ]) + ) + + expect( + mock.smocked['overloadedFunction(uint256)'].calls[0] + ).to.deep.equal([expected1]) + + const expected2 = ethers.BigNumber.from(5678) + await mockCaller.callMock( + mock.address, + mock.interface.encodeFunctionData( + 'overloadedFunction(uint256,uint256)', + [expected2, expected2] + ) + ) + + expect( + mock.smocked['overloadedFunction(uint256,uint256)'].calls[0] + ).to.deep.equal([expected2, expected2]) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index eaf60064a1bc..cc075769cbea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1875,15 +1875,15 @@ "@sentry/utils" "6.2.5" tslib "^1.9.3" -"@sentry/core@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.3.5.tgz#6b73de736eb9d0040be94cdbb06a744cd6b9172e" - integrity sha512-VR2ibDy33mryD0mT6d9fGhKjdNzS2FSwwZPe9GvmNOjkyjly/oV91BKVoYJneCqOeq8fyj2lvkJGKuupdJNDqg== - dependencies: - "@sentry/hub" "6.3.5" - "@sentry/minimal" "6.3.5" - "@sentry/types" "6.3.5" - "@sentry/utils" "6.3.5" +"@sentry/core@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.6.0.tgz#51661d2dd5023d6cd07467422de1854282ced7e5" + integrity sha512-EjdeT6paAdxAZgfsVCB8wneahQF3nAUt9GxOJxaOBUv8BSc3HQ/svcTU3RU7k8YsP26PseEOIsedaxsEVZ+7og== + dependencies: + "@sentry/hub" "6.6.0" + "@sentry/minimal" "6.6.0" + "@sentry/types" "6.6.0" + "@sentry/utils" "6.6.0" tslib "^1.9.3" "@sentry/hub@5.30.0": @@ -1913,13 +1913,13 @@ "@sentry/utils" "6.3.1" tslib "^1.9.3" -"@sentry/hub@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.3.5.tgz#c5bc6760f7e4e53e87149703b106804299060389" - integrity sha512-ZYFo7VYKwdPVjuV9BDFiYn+MpANn6eZMz5QDBfZ2dugIvIVbuOyOOLx8PSa3ZXJoVTZZ7s2wD2fi/ZxKjNjZOQ== +"@sentry/hub@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.6.0.tgz#1b9fa22ee104b7d6afd2dc4c40a1459fda259366" + integrity sha512-1Yw0kbxcvO7njZUDGvCKB6DxU5jQio7Be3Kx5qxwcx8ojpT9lo9p+IYZajgl6zQqkjjbVm/4SoYqU24ozu5vxw== dependencies: - "@sentry/types" "6.3.5" - "@sentry/utils" "6.3.5" + "@sentry/types" "6.6.0" + "@sentry/utils" "6.6.0" tslib "^1.9.3" "@sentry/minimal@5.30.0": @@ -1949,13 +1949,28 @@ "@sentry/types" "6.3.1" tslib "^1.9.3" -"@sentry/minimal@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.3.5.tgz#ef4894771243d01d81e91819400d2ecdcb34b411" - integrity sha512-4RqIGAU0+8iI/1sw0GYPTr4SUA88/i2+JPjFJ+qloh5ANVaNwhFPRChw+Ys9xpre8LV9JZrEsEf8AvQr4fkNbA== +"@sentry/minimal@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.6.0.tgz#48684734e3c380e5e63a9357d05f0c18bae84419" + integrity sha512-xVBlZIDxSvHvNdvD5KmjTf8Xgi78vLpT4xqJaDUkW7B+DqWMVJZe5aUdQmcp7X/zWxctBwyMKsdHO7oiHkpS+Q== dependencies: - "@sentry/hub" "6.3.5" - "@sentry/types" "6.3.5" + "@sentry/hub" "6.6.0" + "@sentry/types" "6.6.0" + tslib "^1.9.3" + +"@sentry/node@6.2.5", "@sentry/node@^6.2.5": + version "6.2.5" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.2.5.tgz#6e6694c0c3ce6ca231710f40da0cac7fd5c645ef" + integrity sha512-/iM3khzGnUH713VFhZBAEYJhb/saEQSVz7Udogml+O7mFQ4rutnwJhgoGcB9YYrwMv2m7qOSszkdZbemDV6k2g== + dependencies: + "@sentry/core" "6.2.5" + "@sentry/hub" "6.2.5" + "@sentry/tracing" "6.2.5" + "@sentry/types" "6.2.5" + "@sentry/utils" "6.2.5" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" tslib "^1.9.3" "@sentry/node@^5.18.1": @@ -1973,31 +1988,16 @@ lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/node@^6.2.5": - version "6.2.5" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.2.5.tgz#6e6694c0c3ce6ca231710f40da0cac7fd5c645ef" - integrity sha512-/iM3khzGnUH713VFhZBAEYJhb/saEQSVz7Udogml+O7mFQ4rutnwJhgoGcB9YYrwMv2m7qOSszkdZbemDV6k2g== - dependencies: - "@sentry/core" "6.2.5" - "@sentry/hub" "6.2.5" - "@sentry/tracing" "6.2.5" - "@sentry/types" "6.2.5" - "@sentry/utils" "6.2.5" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - "@sentry/node@^6.3.1": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.3.5.tgz#d5cbf941d0a4caf7b8e644d71cc6b463eeda214e" - integrity sha512-scPB+DoAEPaqkYuyb8d/gVWbFmX5PhaYSNHybeHncaP/P4itLdq/AoAWGNxl0Hj4EQokfT4OZWxaaJi7SCYnaw== - dependencies: - "@sentry/core" "6.3.5" - "@sentry/hub" "6.3.5" - "@sentry/tracing" "6.3.5" - "@sentry/types" "6.3.5" - "@sentry/utils" "6.3.5" + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.6.0.tgz#e535e1e679cf894752810529ffdee93cbfd078f0" + integrity sha512-heKie/AOanYq3mCsKR1igPn1sUIxBmGibBp79Xc0iSAgliPKnnLkqUjvAIKu6mcevL9UOUhpMDLzhilkaG+bAA== + dependencies: + "@sentry/core" "6.6.0" + "@sentry/hub" "6.6.0" + "@sentry/tracing" "6.6.0" + "@sentry/types" "6.6.0" + "@sentry/utils" "6.6.0" cookie "^0.4.1" https-proxy-agent "^5.0.0" lru_map "^0.3.3" @@ -2025,15 +2025,15 @@ "@sentry/utils" "6.2.5" tslib "^1.9.3" -"@sentry/tracing@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.3.5.tgz#f76c362159141f860081ec7df80aa9f85b545860" - integrity sha512-TNKAST1ge2g24BlTfVxNp4gP5t3drbi0OVCh8h8ah+J7UjHSfdiqhd9W2h5qv1GO61gGlpWeN/TyioyQmOxu0Q== +"@sentry/tracing@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.6.0.tgz#ce62fcb951faa6447cf47889f91efe3617b9eed2" + integrity sha512-tjXrmAOFfVBfx+ZmgE5bkpDPs/euNj0xrUg8MowCWGfCRn01W679tTb+dyNeP6faxQTo2RcaD68xD8oLroJwwA== dependencies: - "@sentry/hub" "6.3.5" - "@sentry/minimal" "6.3.5" - "@sentry/types" "6.3.5" - "@sentry/utils" "6.3.5" + "@sentry/hub" "6.6.0" + "@sentry/minimal" "6.6.0" + "@sentry/types" "6.6.0" + "@sentry/utils" "6.6.0" tslib "^1.9.3" "@sentry/tracing@^6.3.1": @@ -2062,10 +2062,10 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.1.tgz#af3b54728b29f633f38fbe51b8c10e3834fbc158" integrity sha512-BEBn8JX1yaooCAuonbaMci9z0RjwwMbQ3Eny/eyDdd+rjXprZCZaStZnCvSThbNBqAJ8YaUqY2YBMnEwJxarAw== -"@sentry/types@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.5.tgz#d5eca7e76c250882ab78c01a8df894a9a9ca537d" - integrity sha512-tY/3pkAmGYJ3F0BtwInsdt/uclNvF8aNG7XHsTPQNzk7BkNVWjCXx0sjxi6CILirl5nwNxYxVeTr2ZYAEZ/dSQ== +"@sentry/types@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.6.0.tgz#55cbca23859bad87411f0f32135a968e6e40a639" + integrity sha512-lZ1uFN0lSNftAohi0lciEoSL58Gk/Ib1lLKaj0FSOvB1PAUmvo5dPtLdd0qjtNdtoaM8zqhrAbwCTQ8XZCDRsg== "@sentry/utils@5.30.0": version "5.30.0" @@ -2091,12 +2091,12 @@ "@sentry/types" "6.3.1" tslib "^1.9.3" -"@sentry/utils@6.3.5": - version "6.3.5" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.3.5.tgz#a4805448cb0314d3d119688162aa695598a10bbb" - integrity sha512-kHUcZ37QYlNzz7c9LVdApITXHaNmQK7+sw/If3M/qpff1fd5XoecA8laLfcYuz+Cw5mRhVmdhPcCRM3Xi1IGXg== +"@sentry/utils@6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.6.0.tgz#b34d342d05eefc25b7ddd3f27f41c050f1e7e1ef" + integrity sha512-FK9yqz2x+ef50B54tueeJ6mfb7Pf3lN75omx/YQBDL5cicyOV4j4kJDqn8/VKYhcSuX+ZaCZ/8bvOf0lxe0aHg== dependencies: - "@sentry/types" "6.3.5" + "@sentry/types" "6.6.0" tslib "^1.9.3" "@sindresorhus/is@^0.14.0": @@ -2248,6 +2248,13 @@ dependencies: "@types/chai" "*" +"@types/chai-as-promised@^7.1.4": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601" + integrity sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA== + dependencies: + "@types/chai" "*" + "@types/chai@*", "@types/chai@^4.1.7": version "4.2.16" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.16.tgz#f09cc36e18d28274f942e7201147cce34d97e8c8" @@ -2594,6 +2601,13 @@ "@types/bn.js" "*" "@types/underscore" "*" +"@types/workerpool@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/workerpool/-/workerpool-6.0.0.tgz#068c31191f7df9b3d49ebe348b1eeb601e75e2d3" + integrity sha512-BjbKVHFBWblQ3vZ5yFq29kbM2TsaUaTOwYgVxqnNjMrT6CktVF8AvMxOJZgHGgNbAzP4z8DK+EshyZcYpdvAhQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -2953,7 +2967,7 @@ arrify@^1.0.0, arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -arrify@^2.0.0, arrify@^2.0.1: +arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== @@ -3601,7 +3615,7 @@ base-x@^3.0.2, base-x@^3.0.8: dependencies: safe-buffer "^5.0.1" -base64-js@^1.3.0, base64-js@^1.3.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3650,6 +3664,16 @@ better-path-resolve@1.0.0: dependencies: is-windows "^1.0.0" +bfj@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" + integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== + dependencies: + bluebird "^3.5.5" + check-types "^11.1.1" + hoopy "^0.1.4" + tryer "^1.0.1" + bignumber.js@^9.0.0, bignumber.js@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" @@ -3704,7 +3728,7 @@ blakejs@^1.1.0: resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.0.tgz#69df92ef953aa88ca51a32df6ab1c54a155fc7a5" integrity sha1-ad+S75U6qIylGjLfarHFShVfx6U= -bluebird@^3.5.0, bluebird@^3.5.2, bluebird@^3.5.3, bluebird@^3.7.2: +bluebird@^3.5.0, bluebird@^3.5.2, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -3894,11 +3918,6 @@ bsert@~0.0.10: resolved "https://registry.yarnpkg.com/bsert/-/bsert-0.0.10.tgz#231ac82873a1418c6ade301ab5cd9ae385895597" integrity sha512-NHNwlac+WPy4t2LoNh8pXk8uaIGH3NSaIUbTTRXGpE2WEbq0te/tDykYHkFK57YKLPjv/aGHmbqvnGeVWDz57Q== -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-from@^1.0.0, buffer-from@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -4188,6 +4207,11 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +check-types@^11.1.1: + version "11.1.2" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" + integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== + checkpoint-store@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/checkpoint-store/-/checkpoint-store-1.1.0.tgz#04e4cb516b91433893581e6d4601a78e9552ea06" @@ -5234,13 +5258,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -6206,7 +6223,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.2, extend@~3.0.2: +extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -6298,11 +6315,6 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== -fast-text-encoding@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" - integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== - fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -6700,25 +6712,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaxios@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.2.0.tgz#33bdc4fc241fc33b8915a4b8c07cfb368b932e46" - integrity sha512-Ms7fNifGv0XVU+6eIyL9LB7RVESeML9+cMvkwGS70xyD6w2Z80wl6RiqiJ9k1KFlJCUTQqFFc8tXmPQfSKUe8g== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gcp-metadata@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" - integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== - dependencies: - gaxios "^4.0.0" - json-bigint "^1.0.0" - get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -6867,20 +6860,13 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob-parent@^5.1.0, glob-parent@^5.1.1: +glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - glob@7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -6971,37 +6957,6 @@ globby@^11.0.0, globby@^11.0.2: merge2 "^1.3.0" slash "^3.0.0" -google-auth-library@^6.1.3: - version "6.1.6" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-6.1.6.tgz#deacdcdb883d9ed6bac78bb5d79a078877fdf572" - integrity sha512-Q+ZjUEvLQj/lrVHF/IQwRo6p3s8Nc44Zk/DALsN+ac3T4HY/g/3rrufkgtl+nZ1TW7DNAw5cTChdVp4apUXVgQ== - dependencies: - arrify "^2.0.0" - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^4.0.0" - gcp-metadata "^4.2.0" - gtoken "^5.0.4" - jws "^4.0.0" - lru-cache "^6.0.0" - -google-p12-pem@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" - integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== - dependencies: - node-forge "^0.10.0" - -google-spreadsheet@^3.1.15: - version "3.1.15" - resolved "https://registry.yarnpkg.com/google-spreadsheet/-/google-spreadsheet-3.1.15.tgz#e7a86f750d8166faaa3e16929561baceb807bf5a" - integrity sha512-S5477f3Gf3Mz6AXgCw7dbaYnzu5aHou1AX4sDqrGboQWnAytkxqJGKQiXN+zzRTTcYzSTJCe0g7KqCPZO9xiOw== - dependencies: - axios "^0.21.1" - google-auth-library "^6.1.3" - lodash "^4.17.20" - got@9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -7054,15 +7009,6 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -gtoken@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.2.1.tgz#4dae1fea17270f457954b4a45234bba5fc796d16" - integrity sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw== - dependencies: - gaxios "^4.0.0" - google-p12-pem "^3.0.3" - jws "^4.0.0" - handlebars@^4.0.1, handlebars@^4.7.6: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -7178,6 +7124,57 @@ hardhat@^2.2.1: uuid "^3.3.2" ws "^7.2.1" +hardhat@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.3.0.tgz#5c29f8b4d08155c3dc8c908af9713fd5079522d5" + integrity sha512-nc4ro2bM4wPaA6/0Y22o5F5QrifQk2KCyPUUKLPUeFFZoGNGYB8vmeW/k9gV9DdMukdWTzfYlKc2Jn4bfb6tDQ== + 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" @@ -7322,6 +7319,11 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -8056,13 +8058,6 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -8196,23 +8191,6 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== -jwa@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" - integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" - integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== - dependencies: - jwa "^2.0.0" - safe-buffer "^5.0.1" - keccak@3.0.1, keccak@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff" @@ -9687,7 +9665,7 @@ node-fetch@2.1.2: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= -node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -9700,11 +9678,6 @@ node-fetch@~1.7.1: encoding "^0.1.11" is-stream "^1.0.1" -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - node-gyp-build@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" @@ -12790,6 +12763,11 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-2.2.1.tgz#c5bf04a5bbec3fd118be4084461b3a27c4d796bf" integrity sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q== +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + ts-essentials@^1.0.0, ts-essentials@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a"