Commit a11c4c4f authored by Murphy Law's avatar Murphy Law Committed by GitHub

Merge branch 'develop' into feat/publish-tx

parents 36991e68 99021e29
---
'@eth-optimism/data-transport-layer': patch
---
Removes the unused L1DataTransportClient and its dependencies
---
'@eth-optimism/replica-healthcheck': patch
---
Fix bug in replica healthcheck dockerfile
---
'@eth-optimism/contracts': patch
---
Add a fetch batches hardhat task
---
'@eth-optimism/integration-tests': patch
---
Add test coverage for zlib compressed batches
---
'@eth-optimism/common-ts': patch
---
Update log lines for service shutdown
---
'@eth-optimism/batch-submitter': patch
---
Update to allow for zlib compressed batches
---
'@eth-optimism/contracts': patch
---
Remove yargs as a contracts dependency (unused)
---
'@eth-optimism/teleportr': patch
---
Add teleportr API server
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
'@eth-optimism/data-transport-layer': patch '@eth-optimism/data-transport-layer': patch
--- ---
Enable typed batch support dtl: Support basic authentication for RPC endpoints
---
'@eth-optimism/batch-submitter-service': patch
---
Move L2 dial logic out of bss-core to avoid l2geth dependency
---
'@eth-optimism/integration-tests': patch
---
Replaces contract references in integration tests with SDK CrossChainMessenger objects.
---
'@eth-optimism/common-ts': patch
---
Update metric names to include proper snake_case for strings that include "L1" or "L2"
---
'@eth-optimism/common-ts': patch
'@eth-optimism/message-relayer': patch
'@eth-optimism/replica-healthcheck': patch
---
Have BaseServiceV2 add spaces to environment variable names
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
'@eth-optimism/sdk': patch '@eth-optimism/sdk': patch
--- ---
Add a function for waiting for a particular message status Comment out non-functional getMessagesByAddress function
---
'@eth-optimism/teleportr': patch
---
Restructure Deposit and CompletedTeleport to use struct embeddings
---
'@eth-optimism/core-utils': patch
---
Add toJSON methods to the batch primitives
---
'@eth-optimism/teleportr': patch
---
Add LoadInTeleport method to database
---
'@eth-optimism/batch-submitter-service': patch
---
Enable the usage of typed batches and type 0 zlib compressed batches
---
'@eth-optimism/core-utils': patch
---
Update batch serialization with typed batches and zlib compression
---
'@eth-optimism/teleportr': patch
---
Add btree index on deposit.txn_hash and deposit.address
...@@ -65,14 +65,6 @@ jobs: ...@@ -65,14 +65,6 @@ jobs:
image-name: data-transport-layer image-name: data-transport-layer
target: data-transport-layer target: data-transport-layer
dockerfile: ./ops/docker/Dockerfile.packages dockerfile: ./ops/docker/Dockerfile.packages
build-batch-submitter:
docker:
- image: cimg/base:2021.04
steps:
- build-dockerfile:
image-name: batch-submitter
target: batch-submitter
dockerfile: ./ops/docker/Dockerfile.packages
build-go-batch-submitter: build-go-batch-submitter:
docker: docker:
- image: cimg/base:2021.04 - image: cimg/base:2021.04
...@@ -264,11 +256,6 @@ workflows: ...@@ -264,11 +256,6 @@ workflows:
- optimism - optimism
- slack - slack
<<: *slack-nightly-build-fail-post-step <<: *slack-nightly-build-fail-post-step
- build-batch-submitter:
context:
- optimism
- slack
<<: *slack-nightly-build-fail-post-step
- build-deployer: - build-deployer:
context: context:
- optimism - optimism
...@@ -306,7 +293,6 @@ workflows: ...@@ -306,7 +293,6 @@ workflows:
<<: *slack-nightly-build-fail-post-step <<: *slack-nightly-build-fail-post-step
requires: requires:
- build-dtl - build-dtl
- build-batch-submitter
- build-go-batch-submitter - build-go-batch-submitter
- build-deployer - build-deployer
- build-l2geth - build-l2geth
......
...@@ -27,6 +27,7 @@ module.exports = { ...@@ -27,6 +27,7 @@ module.exports = {
parserOptions: { parserOptions: {
project: 'tsconfig.json', project: 'tsconfig.json',
sourceType: 'module', sourceType: 'module',
allowAutomaticSingleRunInference: true,
}, },
rules: { rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/adjacent-overload-signatures': 'error',
...@@ -102,13 +103,9 @@ module.exports = { ...@@ -102,13 +103,9 @@ module.exports = {
'import/no-extraneous-dependencies': ['error'], 'import/no-extraneous-dependencies': ['error'],
'import/no-internal-modules': 'off', 'import/no-internal-modules': 'off',
'import/order': [ 'import/order': [
"error", 'error',
{ {
groups: [ groups: ['builtin', 'external', 'internal'],
'builtin',
'external',
'internal',
],
'newlines-between': 'always', 'newlines-between': 'always',
}, },
], ],
......
# CODEOWNERS can be disruptive because it automatically requests review from individuals across the go/bss-core @cfromknecht @tynes
# board. We still like to use this file to track who's working on what, but all lines are commented go/batch-submitter @cfromknecht @tynes
# out so that GitHub won't trigger review requests. go/gas-oracle @tynes
go/l2geth-exporter @optimisticben @mslipper
go/op-exporter @optimisticben @mslipper
go/proxyd @mslipper @inphi
go/teleportr @mslipper @cfromknecht
# l2geth/ @smartcontracts @tynes @karlfloersch integration-tests/ @tynes @mslipper
# packages/specs/l2geth/ @smartcontracts @tynes @karlfloersch
# packages/contracts/ @smartcontracts @ben-chain @maurelian @elenadimitrova packages/core-utils @smartcontracts @tynes
# packages/specs/protocol/ @smartcontracts @ben-chain @maurelian packages/common-ts/ @smartcontracts
# ops/ @tynes @karlfloersch packages/message-relayer/ @smartcontracts
# packages/core-utils/ @smartcontracts @annieke @ben-chain packages/data-transport-layer/ @tynes @smartcontracts
# packages/common-ts/ @annieke packages/replica-healthcheck @optimisticben @tynes
# packages/core-utils/src/watcher.ts @K-Ho packages/sdk @smartcontracts @mslipper
# packages/message-relayer/ @K-Ho packages/contracts @elenadimitrova @maurelian @smartcontracts
# packages/batch-submitter/ @annieke @karlfloersch
# packages/data-transport-layer/ @annieke l2geth @tynes @cfromknecht @smartcontracts
# packages/replica-healthcheck/ @annieke
# integration-tests/ @tynes ops @tynes @optimisticben @mslipper
...@@ -8,7 +8,7 @@ on: ...@@ -8,7 +8,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
......
...@@ -8,7 +8,7 @@ on: ...@@ -8,7 +8,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
......
...@@ -69,7 +69,7 @@ jobs: ...@@ -69,7 +69,7 @@ jobs:
if: failure() if: failure()
uses: jwalton/gh-docker-logs@v1 uses: jwalton/gh-docker-logs@v1
with: 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' images: 'ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth,ethereumoptimism/integration-tests'
dest: '~/logs' dest: '~/logs'
- name: Tar logs - name: Tar logs
......
...@@ -8,7 +8,7 @@ on: ...@@ -8,7 +8,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
......
...@@ -8,7 +8,7 @@ on: ...@@ -8,7 +8,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
paths: paths:
- 'l2geth/**' - 'l2geth/**'
......
...@@ -10,7 +10,7 @@ on: ...@@ -10,7 +10,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
......
...@@ -6,7 +6,7 @@ on: ...@@ -6,7 +6,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
......
This diff is collapsed.
This diff is collapsed.
...@@ -61,7 +61,7 @@ jobs: ...@@ -61,7 +61,7 @@ jobs:
if: failure() if: failure()
uses: jwalton/gh-docker-logs@v1 uses: jwalton/gh-docker-logs@v1
with: with:
images: 'ethereumoptimism/builder,ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth' images: 'ethereumoptimism/hardhat,ethereumoptimism/deployer,ethereumoptimism/data-transport-layer,ethereumoptimism/l2geth,ethereumoptimism/message-relayer,ethereumoptimism/batch-submitter,ethereumoptimism/l2geth'
dest: './logs' dest: './logs'
- name: Tar logs - name: Tar logs
......
...@@ -8,7 +8,7 @@ on: ...@@ -8,7 +8,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
branches: branches:
- '*' - '*'
......
...@@ -6,7 +6,7 @@ on: ...@@ -6,7 +6,7 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
- '*rc' - '*rc'
- 'regenesis/*' - 'release/*'
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
......
...@@ -24,4 +24,5 @@ packages/data-transport-layer/db ...@@ -24,4 +24,5 @@ packages/data-transport-layer/db
.env .env
.env* .env*
!.env.example
*.log *.log
...@@ -18,7 +18,7 @@ Note that we have a [Code of Conduct](https://github.com/ethereum-optimism/.gith ...@@ -18,7 +18,7 @@ Note that we have a [Code of Conduct](https://github.com/ethereum-optimism/.gith
In general, the smaller the diff the easier it will be for us to review quickly. In general, the smaller the diff the easier it will be for us to review quickly.
In order to contribute, fork the appropriate branch, for non-breaking changes to production that is `develop` and for the next regenesis release that is normally `regenesis...` branch, see [details about our branching model](https://github.com/ethereum-optimism/optimism/blob/develop/README.md#branching-model-and-releases). In order to contribute, fork the appropriate branch, for non-breaking changes to production that is `develop` and for the next release that is normally `release/X.X.X` branch, see [details about our branching model](https://github.com/ethereum-optimism/optimism/blob/develop/README.md#branching-model-and-releases).
Additionally, if you are writing a new feature, please ensure you add appropriate test cases. Additionally, if you are writing a new feature, please ensure you add appropriate test cases.
...@@ -109,7 +109,6 @@ docker-compose build ...@@ -109,7 +109,6 @@ docker-compose build
This will build the following containers: This will build the following containers:
* [`builder`](https://hub.docker.com/r/ethereumoptimism/builder): used to build the TypeScript packages
* [`l1_chain`](https://hub.docker.com/r/ethereumoptimism/hardhat): simulated L1 chain using hardhat-evm as a backend * [`l1_chain`](https://hub.docker.com/r/ethereumoptimism/hardhat): simulated L1 chain using hardhat-evm as a backend
* [`deployer`](https://hub.docker.com/r/ethereumoptimism/deployer): process that deploys L1 smart contracts to the L1 chain * [`deployer`](https://hub.docker.com/r/ethereumoptimism/deployer): process that deploys L1 smart contracts to the L1 chain
* [`dtl`](https://hub.docker.com/r/ethereumoptimism/data-transport-layer): service that indexes transaction data from the L1 chain * [`dtl`](https://hub.docker.com/r/ethereumoptimism/data-transport-layer): service that indexes transaction data from the L1 chain
...@@ -129,16 +128,6 @@ docker-compose build -- l2geth ...@@ -129,16 +128,6 @@ docker-compose build -- l2geth
docker-compose start l2geth docker-compose start l2geth
``` ```
For the typescript services, you'll need to rebuild the `builder` so that the compiled
files are re-generated, and then your service, e.g. for the batch submitter
```bash
cd ops
docker-compose stop -- batch_submitter
docker-compose build -- builder batch_submitter
docker-compose start batch_submitter
```
Source code changes can have an impact on more than one container. Source code changes can have an impact on more than one container.
**If you're unsure about which containers to rebuild, just rebuild them all**: **If you're unsure about which containers to rebuild, just rebuild them all**:
...@@ -149,6 +138,8 @@ docker-compose build ...@@ -149,6 +138,8 @@ docker-compose build
docker-compose up docker-compose up
``` ```
**If a node process exits with exit code: 137** you may need to increase the default memory limit of docker containers
Finally, **if you're running into weird problems and nothing seems to be working**, run: Finally, **if you're running into weird problems and nothing seems to be working**, run:
```bash ```bash
......
...@@ -56,8 +56,8 @@ root ...@@ -56,8 +56,8 @@ root
| Branch | Status | | Branch | Status |
| --------------- | -------------------------------------------------------------------------------- | | --------------- | -------------------------------------------------------------------------------- |
| [master](https://github.com/ethereum-optimism/optimism/tree/master/) | Accepts PRs from `develop` when we intend to deploy to mainnet. | | [master](https://github.com/ethereum-optimism/optimism/tree/master/) | Accepts PRs from `develop` when we intend to deploy to mainnet. |
| [develop](https://github.com/ethereum-optimism/optimism/tree/develop/) | Accepts PRs that are compatible with `master` OR from `regenesis/X.X.X` branches. | | [develop](https://github.com/ethereum-optimism/optimism/tree/develop/) | Accepts PRs that are compatible with `master` OR from `release/X.X.X` branches. |
| regenesis/X.X.X | Accepts PRs for all changes, particularly those not backwards compatible with `develop` and `master`. | | release/X.X.X | Accepts PRs for all changes, particularly those not backwards compatible with `develop` and `master`. |
### Overview ### Overview
...@@ -90,10 +90,10 @@ Be sure to not merge other pull requests into `develop` if partially through the ...@@ -90,10 +90,10 @@ Be sure to not merge other pull requests into `develop` if partially through the
### Release candidate branches ### Release candidate branches
Branches marked `regenesis/X.X.X` are **release candidate branches**. Branches marked `release/X.X.X` are **release candidate branches**.
Changes that are not backwards compatible and all changes to contracts within `packages/contracts/contracts` MUST be directed towards a release candidate branch. Changes that are not backwards compatible and all changes to contracts within `packages/contracts/contracts` MUST be directed towards a release candidate branch.
Release candidates are merged into `develop` and then into `master` once they've been fully deployed. Release candidates are merged into `develop` and then into `master` once they've been fully deployed.
We may sometimes have more than one active `regenesis/X.X.X` branch if we're in the middle of a deployment. We may sometimes have more than one active `release/X.X.X` branch if we're in the middle of a deployment.
See table in the **Active Branches** section above to find the right branch to target. See table in the **Active Branches** section above to find the right branch to target.
### Releasing new versions ### Releasing new versions
......
Please see our security policy document [here](https://github.com/ethereum-optimism/.github/blob/master/SECURITY.md).
# @eth-optimism/batch-submitter-service # @eth-optimism/batch-submitter-service
## 0.1.7
### Patch Changes
- aca0684e: Add 20% buffer to gas estimation on tx-batch submission to prevent OOG reverts
- 75040ca5: Adds MIN_L1_TX_SIZE configuration
## 0.1.6
### Patch Changes
- 6af67df5: Move L2 dial logic out of bss-core to avoid l2geth dependency
- fe680568: Enable the usage of typed batches and type 0 zlib compressed batches
## 0.1.5 ## 0.1.5
### Patch Changes ### Patch Changes
......
...@@ -27,6 +27,10 @@ func Main(gitVersion string) func(ctx *cli.Context) error { ...@@ -27,6 +27,10 @@ func Main(gitVersion string) func(ctx *cli.Context) error {
return err return err
} }
log.Info("Config parsed",
"min_tx_size", cfg.MinL1TxSize,
"max_tx_size", cfg.MaxL1TxSize)
// The call to defer is done here so that any errors logged from // The call to defer is done here so that any errors logged from
// this point on are posted to Sentry before exiting. // this point on are posted to Sentry before exiting.
if cfg.SentryEnable { if cfg.SentryEnable {
...@@ -121,6 +125,7 @@ func Main(gitVersion string) func(ctx *cli.Context) error { ...@@ -121,6 +125,7 @@ func Main(gitVersion string) func(ctx *cli.Context) error {
L1Client: l1Client, L1Client: l1Client,
L2Client: l2Client, L2Client: l2Client,
BlockOffset: cfg.BlockOffset, BlockOffset: cfg.BlockOffset,
MinTxSize: cfg.MinL1TxSize,
MaxTxSize: cfg.MaxL1TxSize, MaxTxSize: cfg.MaxL1TxSize,
CTCAddr: ctcAddress, CTCAddr: ctcAddress,
ChainID: chainID, ChainID: chainID,
......
...@@ -197,6 +197,7 @@ func NewConfig(ctx *cli.Context) (Config, error) { ...@@ -197,6 +197,7 @@ func NewConfig(ctx *cli.Context) (Config, error) {
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name), L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name), CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name),
SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name), SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name),
MinL1TxSize: ctx.GlobalUint64(flags.MinL1TxSizeFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name), MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name),
MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name), MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name), PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/go/bss-core/metrics" "github.com/ethereum-optimism/optimism/go/bss-core/metrics"
"github.com/ethereum-optimism/optimism/go/bss-core/txmgr" "github.com/ethereum-optimism/optimism/go/bss-core/txmgr"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient" l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -32,6 +33,7 @@ type Config struct { ...@@ -32,6 +33,7 @@ type Config struct {
L1Client *ethclient.Client L1Client *ethclient.Client
L2Client *l2ethclient.Client L2Client *l2ethclient.Client
BlockOffset uint64 BlockOffset uint64
MinTxSize uint64
MaxTxSize uint64 MaxTxSize uint64
CTCAddr common.Address CTCAddr common.Address
ChainID *big.Int ChainID *big.Int
...@@ -150,7 +152,8 @@ func (d *Driver) GetBatchBlockRange( ...@@ -150,7 +152,8 @@ func (d *Driver) GetBatchBlockRange(
// CraftBatchTx transforms the L2 blocks between start and end into a batch // CraftBatchTx transforms the L2 blocks between start and end into a batch
// transaction using the given nonce. A dummy gas price is used in the resulting // transaction using the given nonce. A dummy gas price is used in the resulting
// transaction to use for size estimation. // transaction to use for size estimation. A nil transaction is returned if the
// transaction does not meet the minimum size requirements.
// //
// NOTE: This method SHOULD NOT publish the resulting transaction. // NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) CraftBatchTx( func (d *Driver) CraftBatchTx(
...@@ -211,13 +214,18 @@ func (d *Driver) CraftBatchTx( ...@@ -211,13 +214,18 @@ func (d *Driver) CraftBatchTx(
batchCallData := append(appendSequencerBatchID, batchArguments...) batchCallData := append(appendSequencerBatchID, batchArguments...)
// Continue pruning until calldata size is less than configured max. // Continue pruning until calldata size is less than configured max.
if uint64(len(batchCallData)) > d.cfg.MaxTxSize { calldataSize := uint64(len(batchCallData))
if calldataSize > d.cfg.MaxTxSize {
oldLen := len(batchElements) oldLen := len(batchElements)
newBatchElementsLen := (oldLen * 9) / 10 newBatchElementsLen := (oldLen * 9) / 10
batchElements = batchElements[:newBatchElementsLen] batchElements = batchElements[:newBatchElementsLen]
log.Info(name+" pruned batch", "old_num_txs", oldLen, "new_num_txs", newBatchElementsLen) log.Info(name+" pruned batch", "old_num_txs", oldLen, "new_num_txs", newBatchElementsLen)
pruneCount++ pruneCount++
continue continue
} else if calldataSize < d.cfg.MinTxSize {
log.Info(name+" batch tx size below minimum",
"size", calldataSize, "min_tx_size", d.cfg.MinTxSize)
return nil, nil
} }
d.metrics.NumElementsPerBatch().Observe(float64(len(batchElements))) d.metrics.NumElementsPerBatch().Observe(float64(len(batchElements)))
...@@ -267,6 +275,46 @@ func (d *Driver) UpdateGasPrice( ...@@ -267,6 +275,46 @@ func (d *Driver) UpdateGasPrice(
tx *types.Transaction, tx *types.Transaction,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
gasTipCap, err := d.cfg.L1Client.SuggestGasTipCap(ctx)
if err != nil {
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this
// method, so in the event their API is unreachable we can fallback to a
// degraded mode of operation. This also applies to our test
// environments, as hardhat doesn't support the query either.
if !drivers.IsMaxPriorityFeePerGasNotFoundError(err) {
return nil, err
}
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
gasTipCap = drivers.FallbackGasTipCap
}
header, err := d.cfg.L1Client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
gasFeeCap := txmgr.CalcGasFeeCap(header.BaseFee, gasTipCap)
// The estimated gas limits performed by RawTransact fail semi-regularly
// with out of gas exceptions. To remedy this we extract the internal calls
// to perform gas price/gas limit estimation here and add a buffer to
// account for any network variability.
gasLimit, err := d.cfg.L1Client.EstimateGas(ctx, ethereum.CallMsg{
From: d.walletAddr,
To: &d.cfg.CTCAddr,
GasPrice: nil,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Value: nil,
Data: tx.Data(),
})
if err != nil {
return nil, err
}
opts, err := bind.NewKeyedTransactorWithChainID( opts, err := bind.NewKeyedTransactorWithChainID(
d.cfg.PrivKey, d.cfg.ChainID, d.cfg.PrivKey, d.cfg.ChainID,
) )
...@@ -275,28 +323,12 @@ func (d *Driver) UpdateGasPrice( ...@@ -275,28 +323,12 @@ func (d *Driver) UpdateGasPrice(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce()) opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.GasTipCap = gasTipCap
opts.GasFeeCap = gasFeeCap
opts.GasLimit = 6 * gasLimit / 5 // add 20% buffer to gas limit
opts.NoSend = true opts.NoSend = true
finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data()) return d.rawCtcContract.RawTransact(opts, tx.Data())
switch {
case err == nil:
return finalTx, nil
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
// Currently Alchemy is the only backend provider that exposes this method,
// so in the event their API is unreachable we can fallback to a degraded
// mode of operation. This also applies to our test environments, as hardhat
// doesn't support the query either.
case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
"by current backend, using fallback gasTipCap")
opts.GasTipCap = drivers.FallbackGasTipCap
return d.rawCtcContract.RawTransact(opts, tx.Data())
default:
return nil, err
}
} }
// SendTransaction injects a signed transaction into the pending pool for // SendTransaction injects a signed transaction into the pending pool for
......
...@@ -52,6 +52,13 @@ var ( ...@@ -52,6 +52,13 @@ var (
Required: true, Required: true,
EnvVar: "SCC_ADDRESS", EnvVar: "SCC_ADDRESS",
} }
MinL1TxSizeFlag = cli.Uint64Flag{
Name: "min-l1-tx-size",
Usage: "Minimum size in bytes of any L1 transaction that gets " +
"generated by the batch submitter",
Required: true,
EnvVar: prefixEnvVar("MIN_L1_TX_SIZE"),
}
MaxL1TxSizeFlag = cli.Uint64Flag{ MaxL1TxSizeFlag = cli.Uint64Flag{
Name: "max-l1-tx-size", Name: "max-l1-tx-size",
Usage: "Maximum size in bytes of any L1 transaction that gets " + Usage: "Maximum size in bytes of any L1 transaction that gets " +
...@@ -231,6 +238,7 @@ var requiredFlags = []cli.Flag{ ...@@ -231,6 +238,7 @@ var requiredFlags = []cli.Flag{
L2EthRpcFlag, L2EthRpcFlag,
CTCAddressFlag, CTCAddressFlag,
SCCAddressFlag, SCCAddressFlag,
MinL1TxSizeFlag,
MaxL1TxSizeFlag, MaxL1TxSizeFlag,
MaxBatchSubmissionTimeFlag, MaxBatchSubmissionTimeFlag,
PollIntervalFlag, PollIntervalFlag,
......
{ {
"name": "@eth-optimism/batch-submitter-service", "name": "@eth-optimism/batch-submitter-service",
"version": "0.1.5", "version": "0.1.7",
"private": true, "private": true,
"devDependencies": {} "devDependencies": {}
} }
...@@ -46,7 +46,9 @@ type Driver interface { ...@@ -46,7 +46,9 @@ type Driver interface {
// CraftBatchTx transforms the L2 blocks between start and end into a batch // CraftBatchTx transforms the L2 blocks between start and end into a batch
// transaction using the given nonce. A dummy gas price is used in the // transaction using the given nonce. A dummy gas price is used in the
// resulting transaction to use for size estimation. // resulting transaction to use for size estimation. The driver may return a
// nil value for transaction if there is no action that needs to be
// performed.
// //
// NOTE: This method SHOULD NOT publish the resulting transaction. // NOTE: This method SHOULD NOT publish the resulting transaction.
CraftBatchTx( CraftBatchTx(
...@@ -184,6 +186,8 @@ func (s *Service) eventLoop() { ...@@ -184,6 +186,8 @@ func (s *Service) eventLoop() {
log.Error(name+" unable to craft batch tx", log.Error(name+" unable to craft batch tx",
"err", err) "err", err)
continue continue
} else if tx == nil {
continue
} }
batchTxBuildTime := time.Since(batchTxBuildStart) / time.Millisecond batchTxBuildTime := time.Since(batchTxBuildStart) / time.Millisecond
s.metrics.BatchTxBuildTimeMs().Set(float64(batchTxBuildTime)) s.metrics.BatchTxBuildTimeMs().Set(float64(batchTxBuildTime))
......
GITCOMMIT := $(shell git rev-parse HEAD)
GITDATE := $(shell git show -s --format='%ct')
GITVERSION := $(shell cat package.json | jq .version)
LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT)
LDFLAGSSTRING +=-X main.GitDate=$(GITDATE)
LDFLAGSSTRING +=-X main.GitVersion=$(GITVERSION)
LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
L1BRIDGE_ABI_ARTIFACT = ../../packages/contracts/artifacts/contracts/L1/messaging/L1StandardBridge.sol/L1StandardBridge.json
L2BRIDGE_ABI_ARTIFACT = ../../packages/contracts/artifacts/contracts/L2/messaging/L2StandardBridge.sol/L2StandardBridge.json
ERC20_ABI_ARTIFACT = ./contracts/ERC20.sol/ERC20.json
SCC_ABI_ARTIFACT = ../../packages/contracts/artifacts/contracts/L1/rollup/StateCommitmentChain.sol/StateCommitmentChain.json
indexer:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/indexer
clean:
rm indexer
test:
go test -v ./...
lint:
golangci-lint run ./...
bindings: bindings-l1bridge bindings-l2bridge bindings-l1erc20 bindings-l2erc20 bindings-scc bindings-address-manager
bindings-l1bridge:
$(eval temp := $(shell mktemp))
cat $(L1BRIDGE_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(L1BRIDGE_ABI_ARTIFACT) \
| jq .abi \
| abigen --pkg l1bridge \
--abi - \
--out bindings/l1bridge/l1_standard_bridge.go \
--type L1StandardBridge \
--bin $(temp)
rm $(temp)
bindings-l2bridge:
$(eval temp := $(shell mktemp))
cat $(L2BRIDGE_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(L2BRIDGE_ABI_ARTIFACT) \
| jq .abi \
| ../../l2geth/build/bin/abigen --pkg l2bridge \
--abi - \
--out bindings/l2bridge/l2_standard_bridge.go \
--type L2StandardBridge \
--bin $(temp)
rm $(temp)
bindings-l1erc20:
$(eval temp := $(shell mktemp))
cat $(ERC20_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(ERC20_ABI_ARTIFACT) \
| jq .abi \
| abigen --pkg l1erc20 \
--abi - \
--out bindings/l1erc20/l1erc20.go \
--type L1ERC20 \
--bin $(temp)
rm $(temp)
bindings-l2erc20:
$(eval temp := $(shell mktemp))
cat $(ERC20_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(ERC20_ABI_ARTIFACT) \
| jq .abi \
| ../../l2geth/build/bin/abigen --pkg l2erc20 \
--abi - \
--out bindings/l2erc20/l2erc20.go \
--type L2ERC20 \
--bin $(temp)
rm $(temp)
bindings-scc:
$(eval temp := $(shell mktemp))
cat $(SCC_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(SCC_ABI_ARTIFACT) \
| jq .abi \
| abigen --pkg scc \
--abi - \
--out bindings/scc/statecommitmentchain.go \
--type StateCommitmentChain \
--bin $(temp)
rm $(temp)
bindings-address-manager:
$(eval temp := $(shell mktemp))
cat $(ADDRESS_MANAGER_ABI_ARTIFACT) \
| jq -r .bytecode > $(temp)
cat $(ADDRESS_MANAGER_ABI_ARTIFACT) \
| jq .abi \
| abigen --pkg address_manager \
--abi - \
--out ./bindings/address_manager/address_manager.go \
--type AddressManager \
--bin $(temp)
.PHONY: \
indexer \
bindings \
bindings-l1bridge \
bindings-l2bridge \
bindings-l1erc20 \
bindings-l2erc20 \
bindings-scc \
bindings-address-manager
clean \
test \
lint
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/go/indexer"
"github.com/ethereum-optimism/optimism/go/indexer/flags"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
// Set up logger with a default INFO level in case we fail to parse flags.
// Otherwise the final crtiical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp()
app.Flags = flags.Flags
app.Version = fmt.Sprintf("%s-%s", GitVersion, params.VersionWithCommit(GitCommit, GitDate))
app.Name = "indexer"
app.Usage = "Indexer Service"
app.Description = "Service for indexing deposits and withdrawals " +
"by account on L1 and L2"
app.Action = indexer.Main(GitVersion)
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}
package indexer
import (
"errors"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/go/indexer/flags"
)
var (
// ErrSentryDSNNotSet signals that not Data Source Name was provided
// with which to configure Sentry logging.
ErrSentryDSNNotSet = errors.New("sentry-dsn must be set if use-sentry " +
"is true")
)
type Config struct {
/* Required Params */
// BuildEnv identifies the environment this binary is intended for, i.e.
// production, development, etc.
BuildEnv string
// EthNetworkName identifies the intended Ethereum network.
EthNetworkName string
// ChainID identifies the chain being indexed.
ChainID int64
// L1EthRpc is the HTTP provider URL for L1.
L1EthRpc string
// L2EthRpc is the HTTP provider URL for L1.
L2EthRpc string
// L1AddressManagerAddress is the address of the address manager for L1.
L1AddressManagerAddress string
// L2GenesisBlockHash is the l2 genesis block hash.
L2GenesisBlockHash string
// PollInterval is the delay between querying L2 for more transaction
// and creating a new batch.
PollInterval time.Duration
// Hostname of the database connection.
DBHost string
// Port of the database connection.
DBPort uint64
// Username of the database connection.
DBUser string
// Password of the database connection.
DBPassword string
// Database name of the database connection.
DBName string
/* Optional Params */
// LogLevel is the lowest log level that will be output.
LogLevel string
// LogTerminal if true, prints to stdout in terminal format, otherwise
// prints using JSON. If SentryEnable is true this flag is ignored, and logs
// are printed using JSON.
LogTerminal bool
// SentryEnable if true, logs any error messages to sentry. SentryDsn
// must also be set if SentryEnable is true.
SentryEnable bool
// SentryDsn is the sentry Data Source Name.
SentryDsn string
// SentryTraceRate the frequency with which Sentry should flush buffered
// events.
SentryTraceRate time.Duration
// StartBlockNumber is the block number to start indexing from.
StartBlockNumber uint64
// StartBlockHash is the block hash to start indexing from.
StartBlockHash string
// ConfDepth is the number of confirmations after which headers are
// considered confirmed.
ConfDepth uint64
// MaxHeaderBatchSize is the maximum number of headers to request as a
// batch.
MaxHeaderBatchSize uint64
// RESTHostname is the hostname at which the REST server is running.
RESTHostname string
// RESTPort is the port at which the REST server is running.
RESTPort uint64
// MetricsServerEnable if true, will create a metrics client and log to
// Prometheus.
MetricsServerEnable bool
// MetricsHostname is the hostname at which the metrics server is running.
MetricsHostname string
// MetricsPort is the port at which the metrics server is running.
MetricsPort uint64
// DisableIndexer enables/disables the indexer.
DisableIndexer bool
}
// NewConfig parses the Config from the provided flags or environment variables.
// This method fails if ValidateConfig deems the configuration to be malformed.
func NewConfig(ctx *cli.Context) (Config, error) {
cfg := Config{
/* Required Flags */
BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name),
EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name),
ChainID: ctx.GlobalInt64(flags.ChainIDFlag.Name),
L1EthRpc: ctx.GlobalString(flags.L1EthRPCFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRPCFlag.Name),
L1AddressManagerAddress: ctx.GlobalString(flags.L1AddressManagerAddressFlag.Name),
L2GenesisBlockHash: ctx.GlobalString(flags.L2GenesisBlockHashFlag.Name),
DBHost: ctx.GlobalString(flags.DBHostFlag.Name),
DBPort: ctx.GlobalUint64(flags.DBPortFlag.Name),
DBUser: ctx.GlobalString(flags.DBUserFlag.Name),
DBPassword: ctx.GlobalString(flags.DBPasswordFlag.Name),
DBName: ctx.GlobalString(flags.DBNameFlag.Name),
/* Optional Flags */
DisableIndexer: ctx.GlobalBool(flags.DisableIndexer.Name),
LogLevel: ctx.GlobalString(flags.LogLevelFlag.Name),
LogTerminal: ctx.GlobalBool(flags.LogTerminalFlag.Name),
SentryEnable: ctx.GlobalBool(flags.SentryEnableFlag.Name),
SentryDsn: ctx.GlobalString(flags.SentryDsnFlag.Name),
SentryTraceRate: ctx.GlobalDuration(flags.SentryTraceRateFlag.Name),
StartBlockNumber: ctx.GlobalUint64(flags.StartBlockNumberFlag.Name),
StartBlockHash: ctx.GlobalString(flags.StartBlockHashFlag.Name),
ConfDepth: ctx.GlobalUint64(flags.ConfDepthFlag.Name),
MaxHeaderBatchSize: ctx.GlobalUint64(flags.MaxHeaderBatchSizeFlag.Name),
MetricsServerEnable: ctx.GlobalBool(flags.MetricsServerEnableFlag.Name),
RESTHostname: ctx.GlobalString(flags.RESTHostnameFlag.Name),
RESTPort: ctx.GlobalUint64(flags.RESTPortFlag.Name),
MetricsHostname: ctx.GlobalString(flags.MetricsHostnameFlag.Name),
MetricsPort: ctx.GlobalUint64(flags.MetricsPortFlag.Name),
}
err := ValidateConfig(&cfg)
if err != nil {
return Config{}, err
}
return cfg, nil
}
// ValidateConfig ensures additional constraints on the parsed configuration to
// ensure that it is well-formed.
func ValidateConfig(cfg *Config) error {
// Sanity check log level.
if cfg.LogLevel == "" {
cfg.LogLevel = "debug"
}
_, err := log.LvlFromString(cfg.LogLevel)
if err != nil {
return err
}
// Ensure the Sentry Data Source Name is set when using Sentry.
if cfg.SentryEnable && cfg.SentryDsn == "" {
return ErrSentryDSNNotSet
}
return nil
}
package indexer_test
import (
"fmt"
"testing"
indexer "github.com/ethereum-optimism/optimism/go/indexer"
"github.com/stretchr/testify/require"
)
var validateConfigTests = []struct {
name string
cfg indexer.Config
expErr error
}{
{
name: "bad log level",
cfg: indexer.Config{
LogLevel: "unknown",
},
expErr: fmt.Errorf("unknown level: unknown"),
},
}
// TestValidateConfig asserts the behavior of ValidateConfig by testing expected
// error and success configurations.
func TestValidateConfig(t *testing.T) {
for _, test := range validateConfigTests {
t.Run(test.name, func(t *testing.T) {
err := indexer.ValidateConfig(&test.cfg)
require.Equal(t, err, test.expErr)
})
}
}
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/d8a0f286587dfbd9a9d1e4f1e98e7119.json"
}
This diff is collapsed.
package indexer
import (
"fmt"
l2common "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum/go-ethereum/common"
)
// ParseL1Address parses a L1 ETH addres from a hex string. This method will
// fail if the address is not a valid hexidecimal address.
func ParseL1Address(address string) (common.Address, error) {
if common.IsHexAddress(address) {
return common.HexToAddress(address), nil
}
return common.Address{}, fmt.Errorf("invalid address: %v", address)
}
// ParseL2Address parses a L2 ETH addres from a hex string. This method will
// fail if the address is not a valid hexidecimal address.
func ParseL2Address(address string) (l2common.Address, error) {
if l2common.IsHexAddress(address) {
return l2common.HexToAddress(address), nil
}
return l2common.Address{}, fmt.Errorf("invalid address: %v", address)
}
This diff is collapsed.
This diff is collapsed.
package db
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
)
// Deposit contains transaction data for deposits made via the L1 to L2 bridge.
type Deposit struct {
GUID string
TxHash common.Hash
L1Token common.Address
L2Token common.Address
FromAddress common.Address
ToAddress common.Address
Amount *big.Int
Data []byte
LogIndex uint
}
// String returns the tx hash for the deposit.
func (d Deposit) String() string {
return d.TxHash.String()
}
// DepositJSON contains Deposit data suitable for JSON serialization.
type DepositJSON struct {
GUID string `json:"guid"`
FromAddress string `json:"from"`
ToAddress string `json:"to"`
L1Token *Token `json:"l1Token"`
L2Token string `json:"l2Token"`
Amount string `json:"amount"`
Data []byte `json:"data"`
LogIndex uint64 `json:"logIndex"`
BlockNumber uint64 `json:"blockNumber"`
BlockTimestamp string `json:"blockTimestamp"`
TxHash string `json:"transactionHash"`
}
package db
import l2common "github.com/ethereum-optimism/optimism/l2geth/common"
// ETHL1Token is a placeholder token for differentiating ETH transactions from
// ERC20 transactions on L1.
var ETHL1Token = &Token{
Address: "0x0000000000000000000000000000000000000000",
Name: "Ethereum",
Symbol: "ETH",
Decimals: 18,
}
// ETHL2Address is a placeholder address for differentiating ETH transactions
// from ERC20 transactions on L2.
var ETHL2Address = l2common.HexToAddress("0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000")
// ETHL2Token is a placeholder token for differentiating ETH transactions from
// ERC20 transactions on L2.
var ETHL2Token = &Token{
Address: "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000",
Name: "Ethereum",
Symbol: "ETH",
Decimals: 18,
}
package db
import "github.com/google/uuid"
// NewGUID returns a new guid.
func NewGUID() string {
return uuid.New().String()
}
This diff is collapsed.
package db
import (
l2common "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum/go-ethereum/common"
)
// L1BlockLocator contains the block locator for a L1 block.
type L1BlockLocator struct {
Number uint64 `json:"number"`
Hash common.Hash `json:"hash"`
}
// L2BlockLocator contains the block locator for a L2 block.
type L2BlockLocator struct {
Number uint64 `json:"number"`
Hash l2common.Hash `json:"hash"`
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment