From d16800522fffefa43ad738b40808f5298e572886 Mon Sep 17 00:00:00 2001 From: Maurelian Date: Fri, 28 May 2021 07:56:36 -0400 Subject: [PATCH 1/6] chore: reduce hardhat timeout to 20 seconds (#968) --- .changeset/cold-cows-cross.md | 5 +++++ integration-tests/hardhat.config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/cold-cows-cross.md diff --git a/.changeset/cold-cows-cross.md b/.changeset/cold-cows-cross.md new file mode 100644 index 000000000000..cc67cf4d950d --- /dev/null +++ b/.changeset/cold-cows-cross.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/integration-tests': patch +--- + +Reduce test timeout from 100 to 20 seconds 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: { From 245136f14afed2a731a5ca5f9b5b071a3131961b Mon Sep 17 00:00:00 2001 From: smartcontracts Date: Fri, 28 May 2021 13:00:15 -0400 Subject: [PATCH 2/6] fix: force LF line endings for scripts to avoid docker problems on Windows (#974) * fix: use correct line endings for windows * chore: add changeset --- .changeset/thin-waves-bathe.md | 5 +++++ .gitattributes | 1 + packages/contracts/bin/deploy.ts | 2 -- packages/contracts/package.json | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changeset/thin-waves-bathe.md create mode 100644 .gitattributes diff --git a/.changeset/thin-waves-bathe.md b/.changeset/thin-waves-bathe.md new file mode 100644 index 000000000000..605e410ac5df --- /dev/null +++ b/.changeset/thin-waves-bathe.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts': patch +--- + +Minor change to how deploy.ts is invoked 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/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/package.json b/packages/contracts/package.json index 0dd1f1c288c4..e8f750f6d7a3 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -38,13 +38,13 @@ "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", From 4e03f8a97b3c2f17a2e758323706f2a880a834d9 Mon Sep 17 00:00:00 2001 From: Karl Floersch Date: Fri, 28 May 2021 20:05:36 -0400 Subject: [PATCH 3/6] feat: add hardhat deploy instructions to readme (#965) * feat: add deployment instructions to readme * Add changeset * fix style * Update README.md --- .changeset/late-countries-guess.md | 5 ++ packages/contracts/README.md | 76 ++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 .changeset/late-countries-guess.md diff --git a/.changeset/late-countries-guess.md b/.changeset/late-countries-guess.md new file mode 100644 index 000000000000..2edddd99dad3 --- /dev/null +++ b/.changeset/late-countries-guess.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts': patch +--- + +Update contracts README to add deploy instructions. 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. From a64f8161514faee540a9ed90f0c460ff5597cb95 Mon Sep 17 00:00:00 2001 From: Mark Tyneway Date: Fri, 28 May 2021 19:43:41 -0700 Subject: [PATCH 4/6] feat: fees v2 (#976) * l2 geth: new fee logic * l2 geth: migrate to fees package * core-utils: new fee scheme * chore: add changeset * l2geth: delete dead code * integration-tests: fix typo * integration-tests: fixes * fees: use fee scalar * lint: fix * rollup: correct gas payment comparison * fix(integration-tests): do not hardcode gas price * core-utils: update with new scheme * l2geth: refactor rollup oracle * l2geth: clean up DoEstimateGas * l2geth: implement latest scheme * tests: fix up * lint: fix * l2geth: better sycn service test * optimism: rename to TxGasLimit * fee: fix docstring * tests: fix * variables: rename * l2geth: prevent users from sending txs with too high of a fee * integration-tests: fix import * integration-tests: fix type * integration-tests: fix gas limits * lint: fix * l2geth: log error Co-authored-by: Georgios Konstantopoulos --- .changeset/fuzzy-dogs-share.md | 6 + integration-tests/test/erc20.spec.ts | 6 +- integration-tests/test/fee-payment.spec.ts | 7 +- integration-tests/test/native-eth.spec.ts | 4 +- integration-tests/test/rpc.spec.ts | 48 +++++--- l2geth/core/rollup_fee.go | 126 ------------------- l2geth/core/rollup_fee_test.go | 121 ------------------- l2geth/eth/gasprice/rollup_gasprice.go | 53 ++++---- l2geth/internal/ethapi/api.go | 24 ++-- l2geth/rollup/client.go | 2 +- l2geth/rollup/fees/rollup_fee.go | 94 +++++++++++++++ l2geth/rollup/fees/rollup_fee_test.go | 103 ++++++++++++++++ l2geth/rollup/sync_service.go | 50 ++++---- l2geth/rollup/sync_service_test.go | 7 +- packages/core-utils/README.md | 39 +++--- packages/core-utils/src/fees.ts | 79 ++---------- packages/core-utils/test/fees/fees.spec.ts | 133 ++++++++++----------- 17 files changed, 402 insertions(+), 500 deletions(-) create mode 100644 .changeset/fuzzy-dogs-share.md delete mode 100644 l2geth/core/rollup_fee.go delete mode 100644 l2geth/core/rollup_fee_test.go create mode 100644 l2geth/rollup/fees/rollup_fee.go create mode 100644 l2geth/rollup/fees/rollup_fee_test.go diff --git a/.changeset/fuzzy-dogs-share.md b/.changeset/fuzzy-dogs-share.md new file mode 100644 index 000000000000..3b890e4e9e8d --- /dev/null +++ b/.changeset/fuzzy-dogs-share.md @@ -0,0 +1,6 @@ +--- +'@eth-optimism/l2geth': patch +'@eth-optimism/core-utils': patch +--- + +Implement the next fee spec in both geth and in core-utils 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..3583df4493c8 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 } from '@eth-optimism/core-utils' describe('Fee Payment Integration Tests', async () => { let env: OptimismEnv @@ -29,7 +29,7 @@ describe('Fee Payment Integration Tests', async () => { ) const executionGas = await (env.ovmEth .provider as any).send('eth_estimateExecutionGas', [tx]) - const decoded = L2GasLimit.decode(gas) + 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..fc11c82c8be6 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(0x0ef897216d)) }) 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(61400489396)) }) }) diff --git a/integration-tests/test/rpc.spec.ts b/integration-tests/test/rpc.spec.ts index a65dcab5dfdf..4c88cc4952a7 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 = 59300000001 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: 59808999999, // 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: 177008999999, // 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(0x0dce9004c7) }) 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), @@ -359,16 +375,16 @@ describe('Basic RPC tests', () => { tx, ]) - 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/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/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/internal/ethapi/api.go b/l2geth/internal/ethapi/api.go index 2828d83707bd..648ddea679cd 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 } diff --git a/l2geth/rollup/client.go b/l2geth/rollup/client.go index b8b08222c73b..2e6402bfd4e6 100644 --- a/l2geth/rollup/client.go +++ b/l2geth/rollup/client.go @@ -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/fees/rollup_fee.go b/l2geth/rollup/fees/rollup_fee.go new file mode 100644 index 000000000000..2f5455057e7d --- /dev/null +++ b/l2geth/rollup/fees/rollup_fee.go @@ -0,0 +1,94 @@ +package fees + +import ( + "math/big" + + "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 + +// hundredMillion is a constant used in the gas encoding formula +const hundredMillion uint64 = 100_000_000 + +// feeScalar is used to scale the calculations in EncodeL2GasLimit +// to prevent them from being too large +const feeScalar uint64 = 1000 + +// 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) +var bigHundredMillion = new(big.Int).SetUint64(hundredMillion) + +// 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 +// l1Fee = l1GasPrice * l1GasLimit +// l2Fee = l2GasPrice * l2GasLimit +// sum = l1Fee + l2Fee +// scaled = sum / scalar +// rounded = ceilmod(scaled, hundredMillion) +// result = rounded + l2GasLimit +// 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) + l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit) + l2Fee := new(big.Int).Mul(l2GasPrice, l2GasLimit) + sum := new(big.Int).Add(l1Fee, l2Fee) + scaled := new(big.Int).Div(sum, bigFeeScalar) + remainder := new(big.Int).Mod(scaled, bigHundredMillion) + scaledSum := new(big.Int).Add(scaled, bigHundredMillion) + rounded := new(big.Int).Sub(scaledSum, remainder) + result := new(big.Int).Add(rounded, l2GasLimit) + return result +} + +// DecodeL2GasLimit decodes the L2 gas limit from an encoded L2 gas limit +func DecodeL2GasLimit(gasLimit *big.Int) *big.Int { + return new(big.Int).Mod(gasLimit, bigHundredMillion) +} + +// 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..138a0d0bb16a --- /dev/null +++ b/l2geth/rollup/fees/rollup_fee_test.go @@ -0,0 +1,103 @@ +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: 99999999, + 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) + if l2GasLimit.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..98460aa7387a 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 @@ -445,13 +446,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 +723,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 +745,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 +785,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/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/src/fees.ts b/packages/core-utils/src/fees.ts index cc754e5fcb81..3910d19196df 100644 --- a/packages/core-utils/src/fees.ts +++ b/packages/core-utils/src/fees.ts @@ -6,9 +6,11 @@ import { BigNumber } from 'ethers' import { remove0x } from './common' const hundredMillion = BigNumber.from(100_000_000) +const feeScalar = 1000 +export const TxGasPrice = BigNumber.from(feeScalar + feeScalar / 2) const txDataZeroGas = 4 const txDataNonZeroGasEIP2028 = 16 -const overhead = 4200 +const overhead = 4200 + 200 * txDataNonZeroGasEIP2028 export interface EncodableL2GasLimit { data: Buffer | string @@ -29,17 +31,15 @@ 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 l1Fee = l1GasLimit.mul(l1GasPrice) const l2Fee = l2GasLimit.mul(l2GasPrice) - return l1Fee.add(l2Fee) + const sum = l1Fee.add(l2Fee) + const scaled = sum.div(feeScalar) + const remainder = scaled.mod(hundredMillion) + const scaledSum = scaled.add(hundredMillion) + const rounded = scaledSum.sub(remainder) + return rounded.add(l2GasLimit) } function decode(fee: BigNumber | number): BigNumber { @@ -49,39 +49,17 @@ function decode(fee: BigNumber | number): BigNumber { return fee.mod(hundredMillion) } -export const L2GasLimit = { +export const TxGasLimit = { encode, decode, } -export function verifyL2GasPrice(gasPrice: BigNumber | number): boolean { - if (typeof gasPrice === 'number') { - gasPrice = BigNumber.from(gasPrice) - } - // 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 (gasPrice.eq(0)) { - return false - } - return true -} - -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 +77,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/fees/fees.spec.ts b/packages/core-utils/test/fees/fees.spec.ts index bc548c5baa67..c624c0b3128b 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,99 @@ 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: 10 ** 8 - 1, }, { 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) + expect(decoded).to.deep.eq(BigNumber.from(test.l2GasLimit)) }) } }) From 11a9296fabe1a54c28ccd2043bd362dc7c42720a Mon Sep 17 00:00:00 2001 From: Elena Gesheva Date: Mon, 31 May 2021 16:12:21 +0300 Subject: [PATCH 5/6] Add static analysis action (#848) * Add static analysis github action setup python and install slither * Add nvmrc file for setting node to v14.17 * Update slither command run to link missing contract packages from monorepo root * Add steps for installing dependencies * Add yarn build step to github action * Enable colour in github action for static analysis * Disable certain detectors * Ensure slither does not fail build * Add instructions on running static analysis to monorepo readme --- .github/workflows/static-analysis.yml | 61 ++++++++++++++++++++++++++ .gitignore | 3 ++ .nvmrc | 1 + README.md | 14 ++++-- packages/contracts/package.json | 3 ++ packages/contracts/slither.config.json | 12 +++++ 6 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/static-analysis.yml create mode 100644 .nvmrc create mode 100644 packages/contracts/slither.config.json 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/.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/packages/contracts/package.json b/packages/contracts/package.json index e8f750f6d7a3..ec5771a978c3 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -32,6 +32,9 @@ "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\"", 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" +} From 5a7984973622d1d6e610ac98cfc206ab9a3bfe1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 May 2021 19:21:45 +0300 Subject: [PATCH 6/6] build(deps): bump ws from 7.4.4 to 7.4.6 in /ops/docker/hardhat (#987) Bumps [ws](https://github.com/websockets/ws) from 7.4.4 to 7.4.6. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.4.4...7.4.6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- ops/docker/hardhat/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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"