Commit f41a6fda authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2225 from ethereum-optimism/develop

Develop -> Master
parents 68913c1a 6b43eaa0
---
'@eth-optimism/data-transport-layer': patch
---
Add logging when BSS HF1 is active
---
'@eth-optimism/sdk': patch
---
Have SDK properly handle case when no batches are submitted yet
---
'@eth-optimism/l2geth': patch
---
Bump the timeout to download the genesis file on l2geth
---
'@eth-optimism/data-transport-layer': patch
---
Deletes common.ts in data-transport-layer. Uses core-utils.
---
'@eth-optimism/sdk': patch
---
Have SDK wait for transactions in getMessagesByTransaction
---
'@eth-optimism/batch-submitter-service': patch
---
Fixes a bug that causes the txmgr to not wait for the configured numConfirmations
---
'@eth-optimism/core-utils': minor
---
Deletes the Watcher and injectL2Context functions. Use the SDK instead.
---
'@eth-optimism/integration-tests': patch
---
Deletes watcher-utils.ts. Moves it's utilities into env.ts.
---
'@eth-optimism/batch-submitter': patch
'@eth-optimism/replica-healthcheck': patch
---
Use asL2Provider instead of injectL2Context in bss and healthcheck service.
---
'@eth-optimism/message-relayer': minor
'@eth-optimism/integration-tests': patch
---
Removes message relaying utilities from the Message Relayer, to be replaced by the SDK
---
'@eth-optimism/contracts': patch
---
Contracts are additionally verified on sourcify during deploy. This should reduce manual labor during future regeneses.
---
'@eth-optimism/data-transport-layer': patch
---
Handle null response for `eth_getBlockRange` query
---
'@eth-optimism/message-relayer': patch
---
Update message relayer to log sent tx hashes
---
'@eth-optimism/sdk': patch
---
Add approval functions to the SDK
---
'@eth-optimism/data-transport-layer': patch
---
Add logs displaying current sync from l2
......@@ -30,5 +30,8 @@ M-core-utils:
M-dtl:
- any: ['packages/data-transport-layer/**/*']
M-sdk:
- any: ['packages/sdk/**/*']
M-ops:
- any: ['ops/**/*']
......@@ -11,7 +11,7 @@ on:
- 'regenesis/*'
pull_request:
paths:
- 'go/batch-submitter/*'
- 'go/batch-submitter/**'
workflow_dispatch:
defaults:
......
......@@ -11,7 +11,7 @@ on:
- 'regenesis/*'
pull_request:
paths:
- 'go/bss-core/*'
- 'go/bss-core/**'
workflow_dispatch:
defaults:
......
name: teleportr unit tests
on:
push:
paths:
- 'go/teleportr/**'
branches:
- 'master'
- 'develop'
- '*rc'
- 'regenesis/*'
pull_request:
paths:
- 'go/teleportr/**'
workflow_dispatch:
defaults:
run:
working-directory: './go/teleportr'
jobs:
tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
ports:
- 5432:5432
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.16.x
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test -v ./...
......@@ -38,6 +38,11 @@ root
│ ├── <a href="./packages/batch-submitter">batch-submitter</a>: Service for submitting batches of transactions and results to L1
│ ├── <a href="./packages/message-relayer">message-relayer</a>: Tool for automatically relaying L1<>L2 messages in development
│ └── <a href="./packages/replica-healthcheck">replica-healthcheck</a>: Service for monitoring the health of a replica node
├── <a href="./go">go</a>
│ ├── <a href="./go/batch-submitter">batch-submitter</a>: Service for submitting batches of transactions and results to L1
│ ├── <a href="./go/bss-core">bss-core</a>: Core batch-submitter logic and utilities
│ ├── <a href="./go/gas-oracle">gas-oracle</a>: Service for updating L1 gas prices on L2
│ └── <a href="./go/proxyd">proxyd</a>: Configurable RPC request router and proxy
├── <a href="./l2geth">l2geth</a>: Optimism client software, a fork of <a href="https://github.com/ethereum/go-ethereum/tree/v1.9.10">geth v1.9.10</a>
├── <a href="./integration-tests">integration-tests</a>: Various integration tests for the Optimism network
└── <a href="./ops">ops</a>: Tools for running Optimism nodes and networks
......@@ -78,10 +83,6 @@ Some exceptions to this rule exist for cases in which we absolutely must deploy
If you're changing or adding a contract and you're unsure about which branch to make a PR into, default to using the latest release candidate branch.
See below for info about release candidate branches.
### Release new versions
Developers can release new versions of the software by adding changesets to their pull requests using `yarn changeset`. Changesets will persist over time on the `develop` branch without triggering new version bumps to be proposed by the Changesets bot. Once changesets are merged into `master`, the bot will create a new pull request called "Version Packages" which bumps the versions of packages. The correct flow for triggering releases is to re-base these pull requests onto `develop` and merge them, and then create a new pull request to merge `develop` onto `master`. Then, the `release` workflow will trigger the actual publishing to `npm` and Docker hub.
### Release candidate branches
Branches marked `regenesis/X.X.X` are **release candidate branches**.
......@@ -90,6 +91,10 @@ Release candidates are merged into `develop` and then into `master` once they've
We may sometimes have more than one active `regenesis/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.
### Releasing new versions
Developers can release new versions of the software by adding changesets to their pull requests using `yarn changeset`. Changesets will persist over time on the `develop` branch without triggering new version bumps to be proposed by the Changesets bot. Once changesets are merged into `master`, the bot will create a new pull request called "Version Packages" which bumps the versions of packages. The correct flow for triggering releases is to re-base these pull requests onto `develop` and merge them, and then create a new pull request to merge `develop` onto `master`. Then, the `release` workflow will trigger the actual publishing to `npm` and Docker hub.
## License
Code forked from [`go-ethereum`](https://github.com/ethereum/go-ethereum) under the name [`l2geth`](https://github.com/ethereum-optimism/optimism/tree/master/l2geth) is licensed under the [GNU GPLv3](https://gist.github.com/kn9ts/cbe95340d29fc1aaeaa5dd5c059d2e60) in accordance with the [original license](https://github.com/ethereum/go-ethereum/blob/master/COPYING).
......
......@@ -108,9 +108,10 @@ func Main(gitVersion string) func(ctx *cli.Context) error {
}
txManagerConfig := txmgr.Config{
ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
}
var services []*bsscore.Service
......
......@@ -89,6 +89,11 @@ type Config struct {
// appending new batches.
NumConfirmations uint64
// SafeAbortNonceTooLowCount is the number of ErrNonceTooLowObservations
// required to give up on a tx at a particular nonce without receiving
// confirmation.
SafeAbortNonceTooLowCount uint64
// ResubmissionTimeout is time we will wait before resubmitting a
// transaction.
ResubmissionTimeout time.Duration
......@@ -178,22 +183,23 @@ type Config struct {
func NewConfig(ctx *cli.Context) (Config, error) {
cfg := Config{
/* Required Flags */
BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name),
EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name),
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name),
SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name),
MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
NumConfirmations: ctx.GlobalUint64(flags.NumConfirmationsFlag.Name),
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
FinalityConfirmations: ctx.GlobalUint64(flags.FinalityConfirmationsFlag.Name),
RunTxBatchSubmitter: ctx.GlobalBool(flags.RunTxBatchSubmitterFlag.Name),
RunStateBatchSubmitter: ctx.GlobalBool(flags.RunStateBatchSubmitterFlag.Name),
SafeMinimumEtherBalance: ctx.GlobalUint64(flags.SafeMinimumEtherBalanceFlag.Name),
ClearPendingTxs: ctx.GlobalBool(flags.ClearPendingTxsFlag.Name),
BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name),
EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name),
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name),
SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name),
MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
NumConfirmations: ctx.GlobalUint64(flags.NumConfirmationsFlag.Name),
SafeAbortNonceTooLowCount: ctx.GlobalUint64(flags.SafeAbortNonceTooLowCountFlag.Name),
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
FinalityConfirmations: ctx.GlobalUint64(flags.FinalityConfirmationsFlag.Name),
RunTxBatchSubmitter: ctx.GlobalBool(flags.RunTxBatchSubmitterFlag.Name),
RunStateBatchSubmitter: ctx.GlobalBool(flags.RunStateBatchSubmitterFlag.Name),
SafeMinimumEtherBalance: ctx.GlobalUint64(flags.SafeMinimumEtherBalanceFlag.Name),
ClearPendingTxs: ctx.GlobalBool(flags.ClearPendingTxsFlag.Name),
/* Optional Flags */
LogLevel: ctx.GlobalString(flags.LogLevelFlag.Name),
LogTerminal: ctx.GlobalBool(flags.LogTerminalFlag.Name),
......
......@@ -227,10 +227,11 @@ func (d *Driver) CraftBatchTx(
}
}
// SubmitBatchTx using the passed transaction as a template, signs and
// publishes the transaction unmodified apart from sampling the current gas
// price. The final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx(
// UpdateGasPrice signs an otherwise identical txn to the one provided but with
// updated gas prices sampled from the existing network conditions.
//
// NOTE: Thie method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error) {
......@@ -243,6 +244,7 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.NoSend = true
finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data())
switch {
......@@ -265,3 +267,12 @@ func (d *Driver) SubmitBatchTx(
return nil, err
}
}
// SendTransaction injects a signed transaction into the pending pool for
// execution.
func (d *Driver) SendTransaction(
ctx context.Context,
tx *types.Transaction,
) error {
return d.cfg.L1Client.SendTransaction(ctx, tx)
}
......@@ -257,10 +257,11 @@ func (d *Driver) CraftBatchTx(
}
}
// SubmitBatchTx using the passed transaction as a template, signs and publishes
// the transaction unmodified apart from sampling the current gas price. The
// final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx(
// UpdateGasPrice signs an otherwise identical txn to the one provided but with
// updated gas prices sampled from the existing network conditions.
//
// NOTE: Thie method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error) {
......@@ -273,6 +274,7 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.NoSend = true
finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data())
switch {
......@@ -295,3 +297,12 @@ func (d *Driver) SubmitBatchTx(
return nil, err
}
}
// SendTransaction injects a signed transaction into the pending pool for
// execution.
func (d *Driver) SendTransaction(
ctx context.Context,
tx *types.Transaction,
) error {
return d.cfg.L1Client.SendTransaction(ctx, tx)
}
......@@ -80,6 +80,14 @@ var (
Required: true,
EnvVar: prefixEnvVar("NUM_CONFIRMATIONS"),
}
SafeAbortNonceTooLowCountFlag = cli.Uint64Flag{
Name: "safe-abort-nonce-too-low-count",
Usage: "Number of ErrNonceTooLow observations required to " +
"give up on a tx at a particular nonce without receiving " +
"confirmation",
Required: true,
EnvVar: prefixEnvVar("SAFE_ABORT_NONCE_TOO_LOW_COUNT"),
}
ResubmissionTimeoutFlag = cli.DurationFlag{
Name: "resubmission-timeout",
Usage: "Duration we will wait before resubmitting a " +
......@@ -221,6 +229,7 @@ var requiredFlags = []cli.Flag{
MaxBatchSubmissionTimeFlag,
PollIntervalFlag,
NumConfirmationsFlag,
SafeAbortNonceTooLowCountFlag,
ResubmissionTimeoutFlag,
FinalityConfirmationsFlag,
RunTxBatchSubmitterFlag,
......
......@@ -48,7 +48,7 @@ func ClearPendingTx(
// Construct the clearing transaction submission clousure that will attempt
// to send the a clearing transaction transaction at the given nonce and gas
// price.
sendTx := func(
updateGasPrice := func(
ctx context.Context,
) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce)
......@@ -61,11 +61,16 @@ func ClearPendingTx(
"err", err)
return nil, err
}
txHash := signedTx.Hash()
gasTipCap := signedTx.GasTipCap()
gasFeeCap := signedTx.GasFeeCap()
err = l1Client.SendTransaction(ctx, signedTx)
return signedTx, nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
err := l1Client.SendTransaction(ctx, tx)
switch {
// Clearing transaction successfully confirmed.
......@@ -74,7 +79,7 @@ func ClearPendingTx(
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash)
return signedTx, nil
return nil
// Getting a nonce too low error implies that a previous transaction in
// the mempool has confirmed and we should abort trying to publish at
......@@ -83,7 +88,7 @@ func ClearPendingTx(
log.Info(name + " transaction from previous restart confirmed, " +
"aborting mempool clearing")
cancel()
return nil, context.Canceled
return context.Canceled
// An unexpected error occurred. This also handles the case where the
// clearing transaction has not yet bested the gas price a prior
......@@ -94,11 +99,11 @@ func ClearPendingTx(
log.Error(name+" unable to submit clearing tx",
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash, "err", err)
return nil, err
return err
}
}
receipt, err := txMgr.Send(ctx, sendTx)
receipt, err := txMgr.Send(ctx, updateGasPrice, sendTx)
switch {
// If the current context is canceled, a prior transaction in the mempool
......
......@@ -204,9 +204,10 @@ func newClearPendingTxHarnessWithNumConfs(
l1Client := mock.NewL1Client(l1ClientConfig)
txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3,
}, l1Client)
return &clearPendingTxHarness{
......
......@@ -54,13 +54,18 @@ type Driver interface {
start, end, nonce *big.Int,
) (*types.Transaction, error)
// SubmitBatchTx using the passed transaction as a template, signs and
// publishes the transaction unmodified apart from sampling the current gas
// price. The final transaction is returned to the caller.
SubmitBatchTx(
// UpdateGasPrice signs an otherwise identical txn to the one provided but
// with updated gas prices sampled from the existing network conditions.
//
// NOTE: Thie method SHOULD NOT publish the resulting transaction.
UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
SendTransaction(ctx context.Context, tx *types.Transaction) error
}
type ServiceConfig struct {
......@@ -193,30 +198,19 @@ func (s *Service) eventLoop() {
// Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price.
sendTx := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" attempting batch tx", "start", start,
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" updating batch tx gas price", "start", start,
"end", end, "nonce", nonce)
tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx)
if err != nil {
return nil, err
}
log.Info(
name+" submitted batch tx",
"start", start,
"end", end,
"nonce", nonce,
"tx_hash", tx.Hash(),
)
return tx, nil
return s.cfg.Driver.UpdateGasPrice(ctx, tx)
}
// Wait until one of our submitted transactions confirms. If no
// receipt is received it's likely our gas price was too low.
batchConfirmationStart := time.Now()
receipt, err := s.txMgr.Send(s.ctx, sendTx)
receipt, err := s.txMgr.Send(
s.ctx, updateGasPrice, s.cfg.Driver.SendTransaction,
)
if err != nil {
log.Error(name+" unable to publish batch tx",
"err", err)
......
package txmgr
import (
"strings"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
)
// SendState tracks information about the publication state of a given txn. In
// this context, a txn may correspond to multiple different txn hashes due to
// varying gas prices, though we treat them all as the same logical txn. This
// struct is primarly used to determine whether or not the txmgr should abort a
// given txn and retry with a higher nonce.
type SendState struct {
minedTxs map[common.Hash]struct{}
nonceTooLowCount uint64
mu sync.RWMutex
safeAbortNonceTooLowCount uint64
}
// NewSendState parameterizes a new SendState from the passed
// safeAbortNonceTooLowCount.
func NewSendState(safeAbortNonceTooLowCount uint64) *SendState {
if safeAbortNonceTooLowCount == 0 {
panic("txmgr: safeAbortNonceTooLowCount cannot be zero")
}
return &SendState{
minedTxs: make(map[common.Hash]struct{}),
nonceTooLowCount: 0,
safeAbortNonceTooLowCount: safeAbortNonceTooLowCount,
}
}
// ProcessSendError should be invoked with the error returned for each
// publication. It is safe to call this method with nil or arbitrary errors.
// Currently it only acts on errors containing the ErrNonceTooLow message.
func (s *SendState) ProcessSendError(err error) {
// Nothing to do.
if err == nil {
return
}
// Only concerned with ErrNonceTooLow.
if !strings.Contains(err.Error(), core.ErrNonceTooLow.Error()) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
// Record this nonce too low observation.
s.nonceTooLowCount++
}
// TxMined records that the txn with txnHash has been mined and is await
// confirmation. It is safe to call this function multiple times.
func (s *SendState) TxMined(txHash common.Hash) {
s.mu.Lock()
defer s.mu.Unlock()
s.minedTxs[txHash] = struct{}{}
}
// TxMined records that the txn with txnHash has not been mined or has been
// reorg'd out. It is safe to call this function multiple times.
func (s *SendState) TxNotMined(txHash common.Hash) {
s.mu.Lock()
defer s.mu.Unlock()
_, wasMined := s.minedTxs[txHash]
delete(s.minedTxs, txHash)
// If the txn got reorged and left us with no mined txns, reset the nonce
// too low count, otherwise we might abort too soon when processing the next
// error. If the nonce too low errors persist, we want to ensure we wait out
// the full safe abort count to enesure we have a sufficient number of
// observations.
if len(s.minedTxs) == 0 && wasMined {
s.nonceTooLowCount = 0
}
}
// ShouldAbortImmediately returns true if the txmgr should give up on trying a
// given txn with the target nonce. For now, this only happens if we see an
// extended period of getting ErrNonceTooLow without having a txn mined.
func (s *SendState) ShouldAbortImmediately() bool {
s.mu.RLock()
defer s.mu.RUnlock()
// Never abort if our latest sample reports having at least one mined txn.
if len(s.minedTxs) > 0 {
return false
}
// Only abort if we've observed enough ErrNonceTooLow to meet our safe abort
// threshold.
return s.nonceTooLowCount >= s.safeAbortNonceTooLowCount
}
// IsWaitingForConfirmation returns true if we have at least one confirmation on
// one of our txs.
func (s *SendState) IsWaitingForConfirmation() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.minedTxs) > 0
}
package txmgr_test
import (
"errors"
"testing"
"github.com/ethereum-optimism/optimism/go/bss-core/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/stretchr/testify/require"
)
const testSafeAbortNonceTooLowCount = 3
var (
testHash = common.HexToHash("0x01")
)
func newSendState() *txmgr.SendState {
return txmgr.NewSendState(testSafeAbortNonceTooLowCount)
}
func processNSendErrors(sendState *txmgr.SendState, err error, n int) {
for i := 0; i < n; i++ {
sendState.ProcessSendError(err)
}
}
// TestSendStateNoAbortAfterInit asserts that the default SendState won't
// trigger an abort even after the safe abort interval has elapsed.
func TestSendStateNoAbortAfterInit(t *testing.T) {
sendState := newSendState()
require.False(t, sendState.ShouldAbortImmediately())
require.False(t, sendState.IsWaitingForConfirmation())
}
// TestSendStateNoAbortAfterProcessNilError asserts that nil errors are not
// considered for abort status.
func TestSendStateNoAbortAfterProcessNilError(t *testing.T) {
sendState := newSendState()
processNSendErrors(sendState, nil, testSafeAbortNonceTooLowCount)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateNoAbortAfterProcessOtherError asserts that non-nil errors other
// than ErrNonceTooLow are not considered for abort status.
func TestSendStateNoAbortAfterProcessOtherError(t *testing.T) {
sendState := newSendState()
otherError := errors.New("other error")
processNSendErrors(sendState, otherError, testSafeAbortNonceTooLowCount)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined asserts that we will
// abort after the safe abort interval has elapsed if we haven't mined a tx.
func TestSendStateAbortSafelyAfterNonceTooLowButNoTxMined(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateMiningTxCancelsAbort asserts that a tx getting mined after
// processing ErrNonceTooLow takes precedence and doesn't cause an abort.
func TestSendStateMiningTxCancelsAbort(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.TxMined(testHash)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateReorgingTxResetsAbort asserts that unmining a tx does not
// consider ErrNonceTooLow's prior to being mined when determing whether to
// abort.
func TestSendStateReorgingTxResetsAbort(t *testing.T) {
sendState := newSendState()
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined asserts that we will not
// abort if we continue to get ErrNonceTooLow after a tx has been mined.
//
// NOTE: This is the most crucial role of the SendState, as we _expect_ to get
// ErrNonceTooLow failures after one of our txs has been mined, but that
// shouldn't cause us to not continue waiting for confirmations.
func TestSendStateNoAbortEvenIfNonceTooLowAfterTxMined(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
processNSendErrors(
sendState, core.ErrNonceTooLow, testSafeAbortNonceTooLowCount,
)
require.False(t, sendState.ShouldAbortImmediately())
}
// TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine asserts that we will
// correctly abort if we continue to get ErrNonceTooLow after a tx is unmined
// but not remined.
func TestSendStateSafeAbortIfNonceTooLowPersistsAfterUnmine(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
sendState.ProcessSendError(core.ErrNonceTooLow)
sendState.ProcessSendError(core.ErrNonceTooLow)
require.False(t, sendState.ShouldAbortImmediately())
sendState.ProcessSendError(core.ErrNonceTooLow)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx asserts that we will
// correctly abort if we continue to call TxNotMined on txns that haven't been
// mined.
func TestSendStateSafeAbortWhileCallingNotMinedOnUnminedTx(t *testing.T) {
sendState := newSendState()
processNSendErrors(
sendState, core.ErrNonceTooLow, testSafeAbortNonceTooLowCount,
)
sendState.TxNotMined(testHash)
require.True(t, sendState.ShouldAbortImmediately())
}
// TestSendStateIsWaitingForConfirmationAfterTxMined asserts that we are waiting
// for confirmation after a tx is mined.
func TestSendStateIsWaitingForConfirmationAfterTxMined(t *testing.T) {
sendState := newSendState()
testHash2 := common.HexToHash("0x02")
sendState.TxMined(testHash)
require.True(t, sendState.IsWaitingForConfirmation())
sendState.TxMined(testHash2)
require.True(t, sendState.IsWaitingForConfirmation())
}
// TestSendStateIsNotWaitingForConfirmationAfterTxUnmined asserts that we are
// not waiting for confirmation after a tx is mined then unmined.
func TestSendStateIsNotWaitingForConfirmationAfterTxUnmined(t *testing.T) {
sendState := newSendState()
sendState.TxMined(testHash)
sendState.TxNotMined(testHash)
require.False(t, sendState.IsWaitingForConfirmation())
}
......@@ -8,15 +8,16 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// SendTxFunc defines a function signature for publishing a desired tx with a
// specific gas price. Implementations of this signature should also return
// promptly when the context is canceled.
type SendTxFunc = func(ctx context.Context) (*types.Transaction, error)
// UpdateGasPriceSendTxFunc defines a function signature for publishing a
// desired tx with a specific gas price. Implementations of this signature
// should also return promptly when the context is canceled.
type UpdateGasPriceFunc = func(ctx context.Context) (*types.Transaction, error)
type SendTransactionFunc = func(ctx context.Context, tx *types.Transaction) error
// Config houses parameters for altering the behavior of a SimpleTxManager.
type Config struct {
......@@ -37,6 +38,11 @@ type Config struct {
// NumConfirmations specifies how many blocks are need to consider a
// transaction confirmed.
NumConfirmations uint64
// SafeAbortNonceTooLowCount specifies how many ErrNonceTooLow observations
// are required to give up on a tx at a particular nonce without receiving
// confirmation.
SafeAbortNonceTooLowCount uint64
}
// TxManager is an interface that allows callers to reliably publish txs,
......@@ -48,7 +54,11 @@ type TxManager interface {
// prices). The method may be canceled using the passed context.
//
// NOTE: Send should be called by AT MOST one caller at a time.
Send(ctx context.Context, sendTx SendTxFunc) (*types.Receipt, error)
Send(
ctx context.Context,
updateGasPrice UpdateGasPriceFunc,
sendTxn SendTransactionFunc,
) (*types.Receipt, error)
}
// ReceiptSource is a minimal function signature used to detect the confirmation
......@@ -96,7 +106,10 @@ func NewSimpleTxManager(
//
// NOTE: Send should be called by AT MOST one caller at a time.
func (m *SimpleTxManager) Send(
ctx context.Context, sendTx SendTxFunc) (*types.Receipt, error) {
ctx context.Context,
updateGasPrice UpdateGasPriceFunc,
sendTx SendTransactionFunc,
) (*types.Receipt, error) {
name := m.name
......@@ -112,6 +125,8 @@ func (m *SimpleTxManager) Send(
ctxc, cancel := context.WithCancel(ctx)
defer cancel()
sendState := NewSendState(m.cfg.SafeAbortNonceTooLowCount)
// Create a closure that will block on passed sendTx function in the
// background, returning the first successfully mined receipt back to
// the main event loop via receiptChan.
......@@ -119,36 +134,52 @@ func (m *SimpleTxManager) Send(
sendTxAsync := func() {
defer wg.Done()
tx, err := updateGasPrice(ctxc)
if err != nil {
if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") {
return
}
log.Error(name+" unable to update txn gas price", "err", err)
return
}
txHash := tx.Hash()
nonce := tx.Nonce()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
log.Info(name+" publishing transaction", "txHash", txHash,
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
// Sign and publish transaction with current gas price.
tx, err := sendTx(ctxc)
err = sendTx(ctxc, tx)
sendState.ProcessSendError(err)
if err != nil {
if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") {
return
}
log.Error(name+" unable to publish transaction", "err", err)
if shouldAbortImmediately(err) {
if sendState.ShouldAbortImmediately() {
cancel()
}
// TODO(conner): add retry?
return
}
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
log.Info(name+" transaction published successfully", "hash", txHash,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
// Wait for the transaction to be mined, reporting the receipt
// back to the main event loop if found.
receipt, err := WaitMined(
receipt, err := waitMined(
ctxc, m.backend, tx, m.cfg.ReceiptQueryInterval,
m.cfg.NumConfirmations,
m.cfg.NumConfirmations, sendState,
)
if err != nil {
log.Debug(name+" send tx failed", "hash", txHash,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "err", err)
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"err", err)
}
if receipt != nil {
// Use non-blocking select to ensure function can exit
......@@ -156,7 +187,8 @@ func (m *SimpleTxManager) Send(
select {
case receiptChan <- receipt:
log.Trace(name+" send tx succeeded", "hash", txHash,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
"nonce", nonce, "gasTipCap", gasTipCap,
"gasFeeCap", gasFeeCap)
default:
}
}
......@@ -174,6 +206,14 @@ func (m *SimpleTxManager) Send(
// Whenever a resubmission timeout has elapsed, bump the gas
// price and publish a new transaction.
case <-time.After(m.cfg.ResubmissionTimeout):
// Avoid republishing if we are waiting for confirmation on an
// existing tx. This is primarily an optimization to reduce the
// number of API calls we make, but also reduces the chances of
// getting a false postive reading for ShouldAbortImmediately.
if sendState.IsWaitingForConfirmation() {
continue
}
// Submit and wait for the bumped traction to confirm.
wg.Add(1)
go sendTxAsync()
......@@ -190,13 +230,6 @@ func (m *SimpleTxManager) Send(
}
}
// shouldAbortImmediately returns true if the txmgr should cancel all
// publication attempts and retry. For now, this only includes nonce errors, as
// that error indicates that none of the transactions will ever confirm.
func shouldAbortImmediately(err error) bool {
return strings.Contains(err.Error(), core.ErrNonceTooLow.Error())
}
// WaitMined blocks until the backend indicates confirmation of tx and returns
// the tx receipt. Queries are made every queryInterval, regardless of whether
// the backend returns an error. This method can be canceled using the passed
......@@ -208,6 +241,19 @@ func WaitMined(
queryInterval time.Duration,
numConfirmations uint64,
) (*types.Receipt, error) {
return waitMined(ctx, backend, tx, queryInterval, numConfirmations, nil)
}
// waitMined implements the core functionality of WaitMined, with the option to
// pass in a SendState to record whether or not the transaction is mined.
func waitMined(
ctx context.Context,
backend ReceiptSource,
tx *types.Transaction,
queryInterval time.Duration,
numConfirmations uint64,
sendState *SendState,
) (*types.Receipt, error) {
queryTicker := time.NewTicker(queryInterval)
defer queryTicker.Stop()
......@@ -218,6 +264,10 @@ func WaitMined(
receipt, err := backend.TransactionReceipt(ctx, txHash)
switch {
case receipt != nil:
if sendState != nil {
sendState.TxMined(txHash)
}
txHeight := receipt.BlockNumber.Uint64()
tipHeight, err := backend.BlockNumber(ctx)
if err != nil {
......@@ -252,6 +302,9 @@ func WaitMined(
"err", err)
default:
if sendState != nil {
sendState.TxNotMined(txHash)
}
log.Trace("Transaction not yet mined", "hash", txHash)
}
......
......@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/go/bss-core/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)
......@@ -44,9 +45,10 @@ func newTestHarness() *testHarness {
func configWithNumConfs(numConfirmations uint64) txmgr.Config {
return txmgr.Config{
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3,
}
}
......@@ -71,6 +73,10 @@ func (g *gasPricer) expGasFeeCap() *big.Int {
return gasFeeCap
}
func (g *gasPricer) shouldMine(gasFeeCap *big.Int) bool {
return g.expGasFeeCap().Cmp(gasFeeCap) == 0
}
func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch))
epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, big.NewInt(epoch))
......@@ -79,15 +85,14 @@ func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
return epochGasTipCap, epochGasFeeCap
}
func (g *gasPricer) sample() (*big.Int, *big.Int, bool) {
func (g *gasPricer) sample() (*big.Int, *big.Int) {
g.mu.Lock()
defer g.mu.Unlock()
g.epoch++
epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch)
shouldMine := g.epoch == g.mineAtEpoch
return epochGasTipCap, epochGasFeeCap, shouldMine
return epochGasTipCap, epochGasFeeCap
}
type minedTxInfo struct {
......@@ -171,23 +176,29 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
h := newTestHarness()
gasFeeCap := big.NewInt(5)
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
tx := types.NewTx(&types.DynamicFeeTx{
gasPricer := newGasPricer(1)
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap)
return tx, nil
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
}
return nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, gasFeeCap.Uint64(), receipt.GasUsed)
require.Equal(t, gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no
......@@ -198,19 +209,23 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
// Don't publish tx to backend, simulating never being mined.
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasFeeCap: big.NewInt(5),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Don't publish tx to backend, simulating never being mined.
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
......@@ -222,23 +237,24 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
if shouldMine {
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap)
h.backend.mine(&txHash, tx.GasFeeCap())
}
return tx, nil
return nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......@@ -255,16 +271,22 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
return nil, errRpcFailure
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
return errRpcFailure
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
......@@ -277,27 +299,27 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Fail all but the final attempt.
if !shouldMine {
return nil, errRpcFailure
if !h.gasPricer.shouldMine(tx.GasFeeCap()) {
return errRpcFailure
}
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap)
return tx, nil
h.backend.mine(&txHash, tx.GasFeeCap())
return nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err)
require.NotNil(t, receipt)
......@@ -312,26 +334,72 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
) (*types.Transaction, error) {
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Delay mining the tx with the min gas price.
if shouldMine {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap)
h.backend.mine(&txHash, tx.GasFeeCap())
})
}
return nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestTxMgrDoesntAbortNonceTooLowAfterMiningTx
func TestTxMgrDoesntAbortNonceTooLowAfterMiningTx(t *testing.T) {
t.Parallel()
h := newTestHarnessWithConfig(configWithNumConfs(2))
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
gasTipCap, gasFeeCap := h.gasPricer.sample()
return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
switch {
// If the txn's gas fee cap is less than the one we expect to mine,
// accept the txn to the mempool.
case tx.GasFeeCap().Cmp(h.gasPricer.expGasFeeCap()) < 0:
return nil
// Accept and mine the actual txn we expect to confirm.
case h.gasPricer.shouldMine(tx.GasFeeCap()):
txHash := tx.Hash()
h.backend.mine(&txHash, tx.GasFeeCap())
time.AfterFunc(5*time.Second, func() {
h.backend.mine(nil, nil)
})
return nil
// For gas prices greater than our expected, return ErrNonceTooLow since
// the prior txn confirmed and will invalidate subsequent publications.
default:
return core.ErrNonceTooLow
}
return tx, nil
}
ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc)
receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err)
require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
......
package db
import (
"database/sql"
"errors"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/common"
_ "github.com/lib/pq"
)
var (
// ErrZeroTimestamp signals that the caller attempted to insert deposits
// with a timestamp of zero.
ErrZeroTimestamp = errors.New("timestamp is zero")
// ErrUnknownDeposit signals that the target deposit could not be found.
ErrUnknownDeposit = errors.New("unknown deposit")
)
// Deposit represents an event emitted from the TeleportrDeposit contract on L1,
// along with additional info about the tx that generated the event.
type Deposit struct {
ID int64
TxnHash common.Hash
BlockNumber int64
BlockTimestamp time.Time
Address common.Address
Amount *big.Int
}
// ConfirmationInfo holds metadata about a tx on either the L1 or L2 chain.
type ConfirmationInfo struct {
TxnHash common.Hash
BlockNumber int64
BlockTimestamp time.Time
}
// CompletedTeleport represents an L1 deposit that has been disbursed on L2. The
// struct also hold info about the L1 and L2 txns involved.
type CompletedTeleport struct {
ID int64
Address common.Address
Amount *big.Int
Deposit ConfirmationInfo
Disbursement ConfirmationInfo
}
const createDepositsTable = `
CREATE TABLE IF NOT EXISTS deposits (
id INT8 NOT NULL PRIMARY KEY,
txn_hash VARCHAR NOT NULL,
block_number INT8 NOT NULL,
block_timestamp TIMESTAMPTZ NOT NULL,
address VARCHAR NOT NULL,
amount VARCHAR NOT NULL
);
`
const createDisbursementsTable = `
CREATE TABLE IF NOT EXISTS disbursements (
id INT8 NOT NULL PRIMARY KEY REFERENCES deposits(id),
txn_hash VARCHAR NOT NULL,
block_number INT8 NOT NULL,
block_timestamp TIMESTAMPTZ NOT NULL
);
`
var migrations = []string{
createDepositsTable,
createDisbursementsTable,
}
// Config houses the data required to connect to a Postgres backend.
type Config struct {
// Host is the database hostname.
Host string
// Port is the database port.
Port uint16
// User is the database user to log in as.
User string
// Password is the user's password to authenticate.
Password string
// DBName is the name of the database to connect to.
DBName string
// EnableSSL enables SLL on the connection if set to true.
EnableSSL bool
}
// WithDB returns the connection string with a specific database to connect to.
func (c Config) WithDB() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.Host, c.Port, c.User, c.Password, c.DBName, c.sslMode(),
)
}
// WithoutDB returns the connection string without connecting to a specific
// database.
func (c Config) WithoutDB() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s sslmode=%s",
c.Host, c.Port, c.User, c.Password, c.sslMode(),
)
}
// sslMode retuns "enabled" if EnableSSL is true, otherwise returns "disabled".
func (c Config) sslMode() string {
if c.EnableSSL {
return "enable"
}
return "disable"
}
// Database provides a Go API for accessing Teleportr read/write operations.
type Database struct {
conn *sql.DB
}
// Open creates a new database connection to the configured Postgres backend and
// applies any migrations.
func Open(cfg Config) (*Database, error) {
conn, err := sql.Open("postgres", cfg.WithDB())
if err != nil {
return nil, err
}
return &Database{
conn: conn,
}, nil
}
// Migrate applies all existing migrations to the open database.
func (d *Database) Migrate() error {
for _, migration := range migrations {
_, err := d.conn.Exec(migration)
if err != nil {
return err
}
}
return nil
}
// Close closes the connection to the database.
func (d *Database) Close() error {
return d.conn.Close()
}
const upsertDepositStatement = `
INSERT INTO deposits (id, txn_hash, block_number, block_timestamp, address, amount)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id) DO UPDATE
SET (txn_hash, block_number, block_timestamp, address, amount) = ($2, $3, $4, $5, $6)
`
// UpsertDeposits inserts a list of deposits into the database, or updats an
// existing deposit in place if the same ID is found.
func (d *Database) UpsertDeposits(deposits []Deposit) error {
if len(deposits) == 0 {
return nil
}
// Sanity check deposits.
for _, deposit := range deposits {
if deposit.BlockTimestamp.IsZero() {
return ErrZeroTimestamp
}
}
tx, err := d.conn.Begin()
if err != nil {
return err
}
defer tx.Rollback()
for _, deposit := range deposits {
_, err = tx.Exec(
upsertDepositStatement,
deposit.ID,
deposit.TxnHash.String(),
deposit.BlockNumber,
deposit.BlockTimestamp,
deposit.Address.String(),
deposit.Amount.String(),
)
if err != nil {
return err
}
}
return tx.Commit()
}
const latestDepositQuery = `
SELECT block_number FROM deposits
ORDER BY block_number DESC
LIMIT 1
`
// LatestDeposit returns the block number of the latest deposit known to the
// database.
func (d *Database) LatestDeposit() (*int64, error) {
row := d.conn.QueryRow(latestDepositQuery)
var latestTransfer int64
err := row.Scan(&latestTransfer)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return &latestTransfer, nil
}
const confirmedDepositsQuery = `
SELECT dep.*
FROM deposits AS dep
LEFT JOIN disbursements AS dis ON dep.id = dis.id
WHERE dis.id IS NULL AND dep.block_number + $1 <= $2 + 1
ORDER BY dep.id ASC
`
// ConfirmedDeposits returns the set of all deposits that have sufficient
// confirmation, but do not have a recorded disbursement.
func (d *Database) ConfirmedDeposits(blockNumber, confirmations int64) ([]Deposit, error) {
rows, err := d.conn.Query(confirmedDepositsQuery, confirmations, blockNumber)
if err != nil {
return nil, err
}
defer rows.Close()
var deposits []Deposit
for rows.Next() {
var deposit Deposit
var txnHashStr string
var addressStr string
var amountStr string
err = rows.Scan(
&deposit.ID,
&txnHashStr,
&deposit.BlockNumber,
&deposit.BlockTimestamp,
&addressStr,
&amountStr,
)
if err != nil {
return nil, err
}
amount, ok := new(big.Int).SetString(amountStr, 10)
if !ok {
return nil, fmt.Errorf("unable to parse amount %v", amount)
}
deposit.TxnHash = common.HexToHash(txnHashStr)
deposit.BlockTimestamp = deposit.BlockTimestamp.Local()
deposit.Amount = amount
deposit.Address = common.HexToAddress(addressStr)
deposits = append(deposits, deposit)
}
if err := rows.Err(); err != nil {
return nil, err
}
return deposits, nil
}
const markDisbursedStatement = `
INSERT INTO disbursements (id, txn_hash, block_number, block_timestamp)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE
SET (txn_hash, block_number, block_timestamp) = ($2, $3, $4)
`
// UpsertDisbursement inserts a disbursement, or updates an existing record
// in-place if the ID already exists.
func (d *Database) UpsertDisbursement(
id int64,
txnHash common.Hash,
blockNumber int64,
blockTimestamp time.Time,
) error {
if blockTimestamp.IsZero() {
return ErrZeroTimestamp
}
result, err := d.conn.Exec(
markDisbursedStatement,
id,
txnHash.String(),
blockNumber,
blockTimestamp,
)
if err != nil {
if strings.Contains(err.Error(), "violates foreign key constraint") {
return ErrUnknownDeposit
}
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected != 1 {
return ErrUnknownDeposit
}
return nil
}
const completedTeleportsQuery = `
SELECT
dep.id, dep.address, dep.amount,
dep.txn_hash, dep.block_number, dep.block_timestamp,
dis.txn_hash, dis.block_number, dis.block_timestamp
FROM deposits AS dep, disbursements AS dis
WHERE dep.id = dis.id
ORDER BY id DESC
`
// CompletedTeleports returns the set of all deposits that have also been
// disbursed.
func (d *Database) CompletedTeleports() ([]CompletedTeleport, error) {
rows, err := d.conn.Query(completedTeleportsQuery)
if err != nil {
return nil, err
}
defer rows.Close()
var teleports []CompletedTeleport
for rows.Next() {
var teleport CompletedTeleport
var addressStr string
var amountStr string
var depTxnHashStr string
var disTxnHashStr string
err = rows.Scan(
&teleport.ID,
&addressStr,
&amountStr,
&depTxnHashStr,
&teleport.Deposit.BlockNumber,
&teleport.Deposit.BlockTimestamp,
&disTxnHashStr,
&teleport.Disbursement.BlockNumber,
&teleport.Disbursement.BlockTimestamp,
)
if err != nil {
return nil, err
}
amount, ok := new(big.Int).SetString(amountStr, 10)
if !ok {
return nil, fmt.Errorf("unable to parse amount %v", amount)
}
teleport.Address = common.HexToAddress(addressStr)
teleport.Amount = amount
teleport.Deposit.TxnHash = common.HexToHash(depTxnHashStr)
teleport.Deposit.BlockTimestamp = teleport.Deposit.BlockTimestamp.Local()
teleport.Disbursement.TxnHash = common.HexToHash(disTxnHashStr)
teleport.Disbursement.BlockTimestamp = teleport.Disbursement.BlockTimestamp.Local()
teleports = append(teleports, teleport)
}
if err := rows.Err(); err != nil {
return nil, err
}
return teleports, nil
}
package db_test
import (
"database/sql"
"fmt"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/go/teleportr/db"
"github.com/ethereum/go-ethereum/common"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
var (
testTimestamp = time.Unix(time.Now().Unix(), 0)
)
func newDatabase(t *testing.T) *db.Database {
dbName := uuid.NewString()
cfg := db.Config{
Host: "0.0.0.0",
Port: 5432,
User: "postgres",
Password: "password",
DBName: dbName,
}
conn, err := sql.Open("postgres", cfg.WithoutDB())
require.Nil(t, err)
_, err = conn.Exec(fmt.Sprintf("CREATE DATABASE \"%s\";", dbName))
require.Nil(t, err)
err = conn.Close()
require.Nil(t, err)
db, err := db.Open(cfg)
require.Nil(t, err)
err = db.Migrate()
require.Nil(t, err)
return db
}
// TestOpenClose asserts that we are able to open and close the database
// connection.
func TestOpenClose(t *testing.T) {
t.Parallel()
d := newDatabase(t)
err := d.Close()
require.Nil(t, err)
}
// TestUpsert empty deposits asserts that it is safe to call UpsertDeposits with
// an empty list.
func TestUpsertEmptyDeposits(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
err := d.UpsertDeposits(nil)
require.Nil(t, err)
err = d.UpsertDeposits([]db.Deposit{})
require.Nil(t, err)
}
// TestUpsertDepositWithZeroTimestampFails asserts that trying to insert a
// deposit with a zero-timestamp fails.
func TestUpsertDepositWithZeroTimestampFails(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
err := d.UpsertDeposits([]db.Deposit{{}})
require.Equal(t, db.ErrZeroTimestamp, err)
}
// TestLatestDeposit asserts that the LatestDeposit method properly returns the
// highest block number in the databse, or nil if no items are present.
func TestLatestDeposit(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
// Query should return nil on empty databse.
latestDeposit, err := d.LatestDeposit()
require.Nil(t, err)
require.Equal(t, (*int64)(nil), latestDeposit)
// Update table to have a single element.
expLatestDeposit := int64(1)
err = d.UpsertDeposits([]db.Deposit{{
ID: 1,
TxnHash: common.HexToHash("0xf1"),
BlockNumber: expLatestDeposit,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xa1"),
Amount: big.NewInt(1),
}})
require.Nil(t, err)
// Query should return block number of only deposit.
latestDeposit, err = d.LatestDeposit()
require.Nil(t, err)
require.Equal(t, &expLatestDeposit, latestDeposit)
// Update table to have two distinct block numbers.
expLatestDeposit = 2
err = d.UpsertDeposits([]db.Deposit{{
ID: 2,
TxnHash: common.HexToHash("0xf2"),
BlockNumber: expLatestDeposit,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xa2"),
Amount: big.NewInt(2),
}})
require.Nil(t, err)
// Query should return the highest of the two block numbers.
latestDeposit, err = d.LatestDeposit()
require.Nil(t, err)
require.Equal(t, &expLatestDeposit, latestDeposit)
}
// TestUpsertDeposits asserts that UpsertDeposits properly overwrites an
// existing entry with the same ID.
func TestUpsertDeposits(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
deposit1 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
}
err := d.UpsertDeposits([]db.Deposit{deposit1})
require.Nil(t, err)
deposits, err := d.ConfirmedDeposits(1, 1)
require.Nil(t, err)
require.Equal(t, deposits, []db.Deposit{deposit1})
deposit2 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff02"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa02"),
Amount: big.NewInt(2),
}
err = d.UpsertDeposits([]db.Deposit{deposit2})
require.Nil(t, err)
deposits, err = d.ConfirmedDeposits(2, 1)
require.Nil(t, err)
require.Equal(t, deposits, []db.Deposit{deposit2})
}
// TestConfirmedDeposits asserts that ConfirmedDeposits properly returns the set
// of deposits that have sufficient confirmation, but do not have a recorded
// disbursement.
func TestConfirmedDeposits(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
deposits, err := d.ConfirmedDeposits(1e9, 1)
require.Nil(t, err)
require.Equal(t, int(0), len(deposits))
deposit1 := db.Deposit{
ID: 1,
TxnHash: common.HexToHash("0xff01"),
BlockNumber: 1,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa01"),
Amount: big.NewInt(1),
}
deposit2 := db.Deposit{
ID: 2,
TxnHash: common.HexToHash("0xff21"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa21"),
Amount: big.NewInt(2),
}
deposit3 := db.Deposit{
ID: 3,
TxnHash: common.HexToHash("0xff22"),
BlockNumber: 2,
BlockTimestamp: testTimestamp,
Address: common.HexToAddress("0xaa22"),
Amount: big.NewInt(2),
}
err = d.UpsertDeposits([]db.Deposit{
deposit1, deposit2, deposit3,
})
require.Nil(t, err)
// First deposit only has 1 conf, should not be found using 2 confs at block
// 1.
deposits, err = d.ConfirmedDeposits(1, 2)
require.Nil(t, err)
require.Equal(t, int(0), len(deposits))
// First deposit should be returned when querying for 1 conf at block 1.
deposits, err = d.ConfirmedDeposits(1, 1)
require.Nil(t, err)
require.Equal(t, []db.Deposit{deposit1}, deposits)
// All deposits should be returned when querying for 1 conf at block 2.
deposits, err = d.ConfirmedDeposits(2, 1)
require.Nil(t, err)
require.Equal(t, []db.Deposit{deposit1, deposit2, deposit3}, deposits)
err = d.UpsertDisbursement(deposit1.ID, common.HexToHash("0xdd01"), 1, testTimestamp)
require.Nil(t, err)
deposits, err = d.ConfirmedDeposits(2, 1)
require.Nil(t, err)
require.Equal(t, []db.Deposit{deposit2, deposit3}, deposits)
}
// TestUpsertDisbursement asserts that UpsertDisbursement properly inserts new
// disbursements or overwrites existing ones.
func TestUpsertDisbursement(t *testing.T) {
t.Parallel()
d := newDatabase(t)
defer d.Close()
address := common.HexToAddress("0xaa01")
amount := big.NewInt(1)
depTxnHash := common.HexToHash("0xdd01")
depBlockNumber := int64(1)
disTxnHash := common.HexToHash("0xee02")
disBlockNumber := int64(2)
// Calling UpsertDisbursement with the zero timestamp should fail.
err := d.UpsertDisbursement(0, common.HexToHash("0xdd00"), 0, time.Time{})
require.Equal(t, db.ErrZeroTimestamp, err)
// Calling UpsertDisbursement with an unknown id should fail.
err = d.UpsertDisbursement(0, common.HexToHash("0xdd00"), 0, testTimestamp)
require.Equal(t, db.ErrUnknownDeposit, err)
// Now, insert a real deposit that we will disburse.
err = d.UpsertDeposits([]db.Deposit{
{
ID: 1,
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
Address: address,
Amount: amount,
},
})
require.Nil(t, err)
// Mark the deposit as disbursed with some temporary info.
err = d.UpsertDisbursement(1, common.HexToHash("0xee00"), 1, testTimestamp)
require.Nil(t, err)
// Overwrite the disbursement info with the final values.
err = d.UpsertDisbursement(1, disTxnHash, disBlockNumber, testTimestamp)
require.Nil(t, err)
expTeleports := []db.CompletedTeleport{
{
ID: 1,
Address: address,
Amount: amount,
Deposit: db.ConfirmationInfo{
TxnHash: depTxnHash,
BlockNumber: depBlockNumber,
BlockTimestamp: testTimestamp,
},
Disbursement: db.ConfirmationInfo{
TxnHash: disTxnHash,
BlockNumber: disBlockNumber,
BlockTimestamp: testTimestamp,
},
},
}
// Assert that the deposit now shows up in the CompletedTeleports method
// with both the L1 and L2 confirmation info.
teleports, err := d.CompletedTeleports()
require.Nil(t, err)
require.Equal(t, expTeleports, teleports)
}
module github.com/ethereum-optimism/optimism/go/teleportr
go 1.17
require (
github.com/ethereum/go-ethereum v1.10.15
github.com/google/uuid v1.3.0
github.com/lib/pq v1.10.4
github.com/stretchr/testify v1.7.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo=
github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y=
github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4=
github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0=
github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM=
github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ=
github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ethereum/go-ethereum v1.10.15 h1:E9o0kMbD8HXhp7g6UwIwntY05WTDheCGziMhegcBsQw=
github.com/ethereum/go-ethereum v1.10.15/go.mod h1:W3yfrFyL9C1pHcwY5hmRHVDaorTiQxhYBkKyu5mEDHw=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM=
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY=
github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI=
github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk=
github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8=
github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE=
github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0=
github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
github.com/karalabe/usb v0.0.0-20211005121534-4c5740d64559/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
......@@ -30,7 +30,6 @@
"devDependencies": {
"@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/message-relayer": "0.2.18",
"@eth-optimism/sdk": "0.2.2",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3",
......
/* Imports: External */
import { Contract, utils, Wallet, providers } from 'ethers'
import { TransactionResponse } from '@ethersproject/providers'
import { Contract, utils, Wallet, providers, Transaction } from 'ethers'
import {
TransactionResponse,
TransactionReceipt,
} from '@ethersproject/providers'
import { getContractFactory, predeploys } from '@eth-optimism/contracts'
import { sleep } from '@eth-optimism/core-utils'
import { getMessagesAndProofsForL2Transaction } from '@eth-optimism/message-relayer'
import { CrossChainMessenger } from '@eth-optimism/sdk'
import {
CrossChainMessenger,
MessageStatus,
MessageDirection,
} from '@eth-optimism/sdk'
/* Imports: Internal */
import {
......@@ -21,12 +27,14 @@ import {
getL1Bridge,
getL2Bridge,
envConfig,
DEFAULT_TEST_GAS_L1,
} from './utils'
import {
CrossDomainMessagePair,
waitForXDomainTransaction,
} from './watcher-utils'
export interface CrossDomainMessagePair {
tx: Transaction
receipt: TransactionReceipt
remoteTx: Transaction
remoteReceipt: TransactionReceipt
}
/// Helper class for instantiating a test environment with a funded account
export class OptimismEnv {
......@@ -170,7 +178,32 @@ export class OptimismEnv {
async waitForXDomainTransaction(
tx: Promise<TransactionResponse> | TransactionResponse
): Promise<CrossDomainMessagePair> {
return waitForXDomainTransaction(this.messenger, tx)
// await it if needed
tx = await tx
const receipt = await tx.wait()
const resolved = await this.messenger.toCrossChainMessage(tx)
const messageReceipt = await this.messenger.waitForMessageReceipt(tx)
let fullTx: any
let remoteTx: any
if (resolved.direction === MessageDirection.L1_TO_L2) {
fullTx = await this.messenger.l1Provider.getTransaction(tx.hash)
remoteTx = await this.messenger.l2Provider.getTransaction(
messageReceipt.transactionReceipt.transactionHash
)
} else {
fullTx = await this.messenger.l2Provider.getTransaction(tx.hash)
remoteTx = await this.messenger.l1Provider.getTransaction(
messageReceipt.transactionReceipt.transactionHash
)
}
return {
tx: fullTx,
receipt,
remoteTx,
remoteReceipt: messageReceipt.transactionReceipt,
}
}
/**
......@@ -184,67 +217,48 @@ export class OptimismEnv {
tx = await tx
await tx.wait()
let messagePairs = []
while (true) {
try {
messagePairs = await getMessagesAndProofsForL2Transaction(
l1Provider,
l2Provider,
this.scc.address,
predeploys.L2CrossDomainMessenger,
tx.hash
)
break
} catch (err) {
if (err.message.includes('unable to find state root batch for tx')) {
await sleep(5000)
} else {
throw err
}
}
const messages = await this.messenger.getMessagesByTransaction(tx)
if (messages.length === 0) {
return
}
for (const { message, proof } of messagePairs) {
while (true) {
for (const message of messages) {
let status: MessageStatus
while (
status !== MessageStatus.READY_FOR_RELAY &&
status !== MessageStatus.RELAYED
) {
status = await this.messenger.getMessageStatus(message)
await sleep(1000)
}
let relayed = false
while (!relayed) {
try {
const result = await this.l1Messenger
.connect(this.l1Wallet)
.relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof,
{
gasLimit: DEFAULT_TEST_GAS_L1 * 10,
}
)
await result.wait()
break
await this.messenger.finalizeMessage(message)
relayed = true
} catch (err) {
if (err.message.includes('execution failed due to an exception')) {
await sleep(5000)
} else if (err.message.includes('Nonce too low')) {
await sleep(5000)
} else if (err.message.includes('transaction was replaced')) {
// this happens when we run tests in parallel
await sleep(5000)
} else if (
if (
err.message.includes('Nonce too low') ||
err.message.includes('transaction was replaced') ||
err.message.includes(
'another transaction with same nonce in the queue'
)
) {
// this happens when we run tests in parallel
// Sometimes happens when we run tests in parallel.
await sleep(5000)
} else if (
err.message.includes('message has already been received')
) {
break
// Message already relayed, this is fine.
relayed = true
} else {
throw err
}
}
}
await this.messenger.waitForMessageReceipt(message)
}
}
}
import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/providers'
import { Transaction } from 'ethers'
import { CrossChainMessenger, MessageDirection } from '@eth-optimism/sdk'
export interface CrossDomainMessagePair {
tx: Transaction
receipt: TransactionReceipt
remoteTx: Transaction
remoteReceipt: TransactionReceipt
}
export enum Direction {
L1ToL2,
L2ToL1,
}
export const waitForXDomainTransaction = async (
messenger: CrossChainMessenger,
tx: Promise<TransactionResponse> | TransactionResponse
): Promise<CrossDomainMessagePair> => {
// await it if needed
tx = await tx
const receipt = await tx.wait()
const resolved = await messenger.toCrossChainMessage(tx)
const messageReceipt = await messenger.waitForMessageReceipt(tx)
let fullTx: any
let remoteTx: any
if (resolved.direction === MessageDirection.L1_TO_L2) {
fullTx = await messenger.l1Provider.getTransaction(tx.hash)
remoteTx = await messenger.l2Provider.getTransaction(
messageReceipt.transactionReceipt.transactionHash
)
} else {
fullTx = await messenger.l2Provider.getTransaction(tx.hash)
remoteTx = await messenger.l1Provider.getTransaction(
messageReceipt.transactionReceipt.transactionHash
)
}
return {
tx: fullTx,
receipt,
remoteTx,
remoteReceipt: messageReceipt.transactionReceipt,
}
}
......@@ -642,7 +642,7 @@ func hashish(x string) bool {
func fetchGenesis(url string) ([]byte, error) {
client := &http.Client{
Timeout: 10 * time.Second,
Timeout: 60 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
......
......@@ -34,7 +34,7 @@ docker-compose \
Also note that Docker Desktop only allocates 2GB of memory by default, which isn't enough to run the docker-compose services reliably.
To allocate more memory, go to Settings > Resources in the Docker UI and use the slider to change the value (_4GB recommended_). Make sure to click Apply & Restart for the changes to take effect.
To allocate more memory, go to Settings > Resources in the Docker UI and use the slider to change the value (_8GB recommended_). Make sure to click Apply & Restart for the changes to take effect.
To start the stack with monitoring enabled, just add the metric composition file.
```
......@@ -46,8 +46,9 @@ docker-compose \
```
Optionally, run a verifier along the rest of the stack. Run a replica with the same command by switching the service name!
```
docker-compose
docker-compose
-f docker-compose.yml \
-f docker-compose.ts-batch-submitter.yml \
up --scale \
......@@ -55,7 +56,6 @@ docker-compose
--build --detach
```
A Makefile has been provided for convience. The following targets are available.
- make up
- make down
......
......@@ -8,6 +8,7 @@ BATCH_SUBMITTER_MAX_L1_TX_SIZE=90000
BATCH_SUBMITTER_MAX_BATCH_SUBMISSION_TIME=0
BATCH_SUBMITTER_POLL_INTERVAL=500ms
BATCH_SUBMITTER_NUM_CONFIRMATIONS=1
BATCH_SUBMITTER_SAFE_ABORT_NONCE_TOO_LOW_COUNT=3
BATCH_SUBMITTER_RESUBMISSION_TIMEOUT=1s
BATCH_SUBMITTER_FINALITY_CONFIRMATIONS=0
BATCH_SUBMITTER_RUN_TX_BATCH_SUBMITTER=true
......
......@@ -36,6 +36,7 @@
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3",
......
/* External Imports */
import { exit } from 'process'
import { injectL2Context, Bcfg } from '@eth-optimism/core-utils'
import { Bcfg } from '@eth-optimism/core-utils'
import { asL2Provider } from '@eth-optimism/sdk'
import * as Sentry from '@sentry/node'
import { Logger, Metrics, createMetricsServer } from '@eth-optimism/common-ts'
import { Signer, Wallet } from 'ethers'
......@@ -346,7 +347,7 @@ export const run = async () => {
const clearPendingTxs = requiredEnvVars.CLEAR_PENDING_TXS
const l2Provider = injectL2Context(
const l2Provider = asL2Provider(
new StaticJsonRpcProvider({
url: requiredEnvVars.L2_NODE_WEB3_URL,
headers: { 'User-Agent': 'batch-submitter' },
......
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TeleportrDeposit
*
* Shout out to 0xclem for providing the inspiration for this contract:
* https://github.com/0xclem/teleportr/blob/main/contracts/BridgeDeposit.sol
*/
contract TeleportrDeposit is Ownable {
/// The minimum amount that be deposited in a receive.
uint256 public minDepositAmount;
/// The maximum amount that be deposited in a receive.
uint256 public maxDepositAmount;
/// The maximum balance the contract can hold after a receive.
uint256 public maxBalance;
/// The total number of successful deposits received.
uint256 public totalDeposits;
/**
* @notice Emitted any time the minimum deposit amount is set.
* @param previousAmount The previous minimum deposit amount.
* @param newAmount The new minimum deposit amount.
*/
event MinDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the maximum deposit amount is set.
* @param previousAmount The previous maximum deposit amount.
* @param newAmount The new maximum deposit amount.
*/
event MaxDepositAmountSet(uint256 previousAmount, uint256 newAmount);
/**
* @notice Emitted any time the contract maximum balance is set.
* @param previousBalance The previous maximum contract balance.
* @param newBalance The new maximum contract balance.
*/
event MaxBalanceSet(uint256 previousBalance, uint256 newBalance);
/**
* @notice Emitted any time the balance is withdrawn by the owner.
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a successful deposit is received.
* @param depositId A unique sequencer number identifying the deposit.
* @param emitter The sending address of the payer.
* @param amount The amount deposited by the payer.
*/
event EtherReceived(uint256 indexed depositId, address indexed emitter, uint256 indexed amount);
/**
* @notice Initializes a new TeleportrDeposit contract.
* @param _minDepositAmount The initial minimum deposit amount.
* @param _maxDepositAmount The initial maximum deposit amount.
* @param _maxBalance The initial maximum contract balance.
*/
constructor(
uint256 _minDepositAmount,
uint256 _maxDepositAmount,
uint256 _maxBalance
) {
minDepositAmount = _minDepositAmount;
maxDepositAmount = _maxDepositAmount;
maxBalance = _maxBalance;
totalDeposits = 0;
emit MinDepositAmountSet(0, _minDepositAmount);
emit MaxDepositAmountSet(0, _maxDepositAmount);
emit MaxBalanceSet(0, _maxBalance);
}
/**
* @notice Accepts deposits that will be disbursed to the sender's address on L2.
* The method reverts if the amount is less than the current
* minDepositAmount, the amount is greater than the current
* maxDepositAmount, or the amount causes the contract to exceed its maximum
* allowed balance.
*/
receive() external payable {
require(msg.value >= minDepositAmount, "Deposit amount is too small");
require(msg.value <= maxDepositAmount, "Deposit amount is too big");
require(address(this).balance <= maxBalance, "Contract max balance exceeded");
emit EtherReceived(totalDeposits, msg.sender, msg.value);
unchecked {
totalDeposits += 1;
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 _balance = address(this).balance;
emit BalanceWithdrawn(_owner, _balance);
payable(_owner).transfer(_balance);
}
/**
* @notice Sets the minimum amount that can be deposited in a receive.
* @param _minDepositAmount The new minimum deposit amount.
*/
function setMinAmount(uint256 _minDepositAmount) external onlyOwner {
emit MinDepositAmountSet(minDepositAmount, _minDepositAmount);
minDepositAmount = _minDepositAmount;
}
/**
* @notice Sets the maximum amount that can be deposited in a receive.
* @param _maxDepositAmount The new maximum deposit amount.
*/
function setMaxAmount(uint256 _maxDepositAmount) external onlyOwner {
emit MaxDepositAmountSet(maxDepositAmount, _maxDepositAmount);
maxDepositAmount = _maxDepositAmount;
}
/**
* @notice Sets the maximum balance the contract can hold after a receive.
* @param _maxBalance The new maximum contract balance.
*/
function setMaxBalance(uint256 _maxBalance) external onlyOwner {
emit MaxBalanceSet(maxBalance, _maxBalance);
maxBalance = _maxBalance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title TeleportrDisburser
*/
contract TeleportrDisburser is Ownable {
/**
* @notice A struct holding the address and amount to disbursement.
*/
struct Disbursement {
uint256 amount;
address addr;
}
/// The total number of disbursements processed.
uint256 public totalDisbursements;
/**
* @notice Emitted any time the balance is withdrawn by the owner.
* @param owner The current owner and recipient of the funds.
* @param balance The current contract balance paid to the owner.
*/
event BalanceWithdrawn(address indexed owner, uint256 balance);
/**
* @notice Emitted any time a disbursement is successfuly sent.
* @param depositId The unique sequence number identifying the deposit.
* @param to The recipient of the disbursement.
* @param amount The amount sent to the recipient.
*/
event DisbursementSuccess(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @notice Emitted any time a disbursement fails to send.
* @param depositId The unique sequence number identifying the deposit.
* @param to The intended recipient of the disbursement.
* @param amount The amount intended to be sent to the recipient.
*/
event DisbursementFailed(uint256 indexed depositId, address indexed to, uint256 amount);
/**
* @notice Initializes a new TeleportrDisburser contract.
*/
constructor() {
totalDisbursements = 0;
}
/**
* @notice Accepts a list of Disbursements and forwards the amount paid to
* the contract to each recipient. The method reverts if there are zero
* disbursements, the total amount to forward differs from the amount sent
* in the transaction, or the _nextDepositId is unexpected. Failed
* disbursements will not cause the method to revert, but will instead be
* held by the contract and availabe for the owner to withdraw.
* @param _nextDepositId The depositId of the first Dispursement.
* @param _disbursements A list of Disbursements to process.
*/
function disburse(uint256 _nextDepositId, Disbursement[] calldata _disbursements)
external
payable
onlyOwner
{
// Ensure there are disbursements to process.
uint256 _numDisbursements = _disbursements.length;
require(_numDisbursements > 0, "No disbursements");
// Ensure the _nextDepositId matches our expected value.
uint256 _depositId = totalDisbursements;
require(_depositId == _nextDepositId, "Unexpected next deposit id");
unchecked {
totalDisbursements += _numDisbursements;
}
// Ensure the amount sent in the transaction is equal to the sum of the
// disbursements.
uint256 _totalDisbursed = 0;
for (uint256 i = 0; i < _numDisbursements; i++) {
_totalDisbursed += _disbursements[i].amount;
}
require(_totalDisbursed == msg.value, "Disbursement total != amount sent");
// Process disbursements.
for (uint256 i = 0; i < _numDisbursements; i++) {
uint256 _amount = _disbursements[i].amount;
address _addr = _disbursements[i].addr;
// Deliver the dispursement amount to the receiver. If the
// disbursement fails, the amount will be kept by the contract
// rather than reverting to prevent blocking progress on other
// disbursements.
// slither-disable-next-line calls-loop,reentrancy-events
(bool success, ) = _addr.call{ value: _amount, gas: 2300 }("");
if (success) emit DisbursementSuccess(_depositId, _addr, _amount);
else emit DisbursementFailed(_depositId, _addr, _amount);
unchecked {
_depositId += 1;
}
}
}
/**
* @notice Sends the contract's current balance to the owner.
*/
function withdrawBalance() external onlyOwner {
address _owner = owner();
uint256 balance = address(this).balance;
emit BalanceWithdrawn(_owner, balance);
payable(_owner).transfer(balance);
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
/**
* @title FailingReceiver
*/
contract FailingReceiver {
/**
* @notice Receiver that always reverts upon receiving ether.
*/
receive() external payable {
require(false, "FailingReceiver");
}
}
# FailingReceiver
> FailingReceiver
# TeleportrDeposit
> TeleportrDeposit Shout out to 0xclem for providing the inspiration for this contract: https://github.com/0xclem/teleportr/blob/main/contracts/BridgeDeposit.sol
## Methods
### maxBalance
```solidity
function maxBalance() external view returns (uint256)
```
The maximum balance the contract can hold after a receive.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### maxDepositAmount
```solidity
function maxDepositAmount() external view returns (uint256)
```
The maximum amount that be deposited in a receive.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### minDepositAmount
```solidity
function minDepositAmount() external view returns (uint256)
```
The minimum amount that be deposited in a receive.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### owner
```solidity
function owner() external view returns (address)
```
*Returns the address of the current owner.*
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | address | undefined
### renounceOwnership
```solidity
function renounceOwnership() external nonpayable
```
*Leaves the contract without owner. It will not be possible to call `onlyOwner` functions anymore. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.*
### setMaxAmount
```solidity
function setMaxAmount(uint256 _maxDepositAmount) external nonpayable
```
Sets the maximum amount that can be deposited in a receive.
#### Parameters
| Name | Type | Description |
|---|---|---|
| _maxDepositAmount | uint256 | The new maximum deposit amount.
### setMaxBalance
```solidity
function setMaxBalance(uint256 _maxBalance) external nonpayable
```
Sets the maximum balance the contract can hold after a receive.
#### Parameters
| Name | Type | Description |
|---|---|---|
| _maxBalance | uint256 | The new maximum contract balance.
### setMinAmount
```solidity
function setMinAmount(uint256 _minDepositAmount) external nonpayable
```
Sets the minimum amount that can be deposited in a receive.
#### Parameters
| Name | Type | Description |
|---|---|---|
| _minDepositAmount | uint256 | The new minimum deposit amount.
### totalDeposits
```solidity
function totalDeposits() external view returns (uint256)
```
The total number of successful deposits received.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### transferOwnership
```solidity
function transferOwnership(address newOwner) external nonpayable
```
*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.*
#### Parameters
| Name | Type | Description |
|---|---|---|
| newOwner | address | undefined
### withdrawBalance
```solidity
function withdrawBalance() external nonpayable
```
Sends the contract&#39;s current balance to the owner.
## Events
### BalanceWithdrawn
```solidity
event BalanceWithdrawn(address indexed owner, uint256 balance)
```
Emitted any time the balance is withdrawn by the owner.
#### Parameters
| Name | Type | Description |
|---|---|---|
| owner `indexed` | address | The current owner and recipient of the funds. |
| balance | uint256 | The current contract balance paid to the owner. |
### EtherReceived
```solidity
event EtherReceived(uint256 indexed depositId, address indexed emitter, uint256 indexed amount)
```
Emitted any time a successful deposit is received.
#### Parameters
| Name | Type | Description |
|---|---|---|
| depositId `indexed` | uint256 | A unique sequencer number identifying the deposit. |
| emitter `indexed` | address | The sending address of the payer. |
| amount `indexed` | uint256 | The amount deposited by the payer. |
### MaxBalanceSet
```solidity
event MaxBalanceSet(uint256 previousBalance, uint256 newBalance)
```
Emitted any time the contract maximum balance is set.
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousBalance | uint256 | The previous maximum contract balance. |
| newBalance | uint256 | The new maximum contract balance. |
### MaxDepositAmountSet
```solidity
event MaxDepositAmountSet(uint256 previousAmount, uint256 newAmount)
```
Emitted any time the maximum deposit amount is set.
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousAmount | uint256 | The previous maximum deposit amount. |
| newAmount | uint256 | The new maximum deposit amount. |
### MinDepositAmountSet
```solidity
event MinDepositAmountSet(uint256 previousAmount, uint256 newAmount)
```
Emitted any time the minimum deposit amount is set.
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousAmount | uint256 | The previous minimum deposit amount. |
| newAmount | uint256 | The new minimum deposit amount. |
### OwnershipTransferred
```solidity
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
```
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousOwner `indexed` | address | undefined |
| newOwner `indexed` | address | undefined |
# TeleportrDisburser
> TeleportrDisburser
## Methods
### disburse
```solidity
function disburse(uint256 _nextDepositId, TeleportrDisburser.Disbursement[] _disbursements) external payable
```
Accepts a list of Disbursements and forwards the amount paid to the contract to each recipient. The method reverts if there are zero disbursements, the total amount to forward differs from the amount sent in the transaction, or the _nextDepositId is unexpected. Failed disbursements will not cause the method to revert, but will instead be held by the contract and availabe for the owner to withdraw.
#### Parameters
| Name | Type | Description |
|---|---|---|
| _nextDepositId | uint256 | The depositId of the first Dispursement.
| _disbursements | TeleportrDisburser.Disbursement[] | A list of Disbursements to process.
### owner
```solidity
function owner() external view returns (address)
```
*Returns the address of the current owner.*
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | address | undefined
### renounceOwnership
```solidity
function renounceOwnership() external nonpayable
```
*Leaves the contract without owner. It will not be possible to call `onlyOwner` functions anymore. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.*
### totalDisbursements
```solidity
function totalDisbursements() external view returns (uint256)
```
The total number of disbursements processed.
#### Returns
| Name | Type | Description |
|---|---|---|
| _0 | uint256 | undefined
### transferOwnership
```solidity
function transferOwnership(address newOwner) external nonpayable
```
*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.*
#### Parameters
| Name | Type | Description |
|---|---|---|
| newOwner | address | undefined
### withdrawBalance
```solidity
function withdrawBalance() external nonpayable
```
Sends the contract&#39;s current balance to the owner.
## Events
### BalanceWithdrawn
```solidity
event BalanceWithdrawn(address indexed owner, uint256 balance)
```
Emitted any time the balance is withdrawn by the owner.
#### Parameters
| Name | Type | Description |
|---|---|---|
| owner `indexed` | address | The current owner and recipient of the funds. |
| balance | uint256 | The current contract balance paid to the owner. |
### DisbursementFailed
```solidity
event DisbursementFailed(uint256 indexed depositId, address indexed to, uint256 amount)
```
Emitted any time a disbursement fails to send.
#### Parameters
| Name | Type | Description |
|---|---|---|
| depositId `indexed` | uint256 | The unique sequence number identifying the deposit. |
| to `indexed` | address | The intended recipient of the disbursement. |
| amount | uint256 | The amount intended to be sent to the recipient. |
### DisbursementSuccess
```solidity
event DisbursementSuccess(uint256 indexed depositId, address indexed to, uint256 amount)
```
Emitted any time a disbursement is successfuly sent.
#### Parameters
| Name | Type | Description |
|---|---|---|
| depositId `indexed` | uint256 | The unique sequence number identifying the deposit. |
| to `indexed` | address | The recipient of the disbursement. |
| amount | uint256 | The amount sent to the recipient. |
### OwnershipTransferred
```solidity
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
```
#### Parameters
| Name | Type | Description |
|---|---|---|
| previousOwner `indexed` | address | undefined |
| newOwner `indexed` | address | undefined |
......@@ -67,6 +67,7 @@
"@codechecks/client": "^0.1.11",
"@defi-wonderland/smock": "^2.0.2",
"@eth-optimism/smock": "1.1.10",
"@nomiclabs/ethereumjs-vm": "^4.2.2",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^2.1.6",
"@nomiclabs/hardhat-waffle": "^2.0.1",
......
......@@ -53,9 +53,18 @@ export const deployAndVerifyAndThen = async ({
address: result.address,
constructorArguments: args,
})
console.log('Successfully verified')
console.log('Successfully verified on Etherscan')
} catch (error) {
console.log('Error when verifying bytecode on etherscan:')
console.log('Error when verifying bytecode on Etherscan:')
console.log(error)
}
try {
console.log('Verifying on Sourcify...')
await hre.run('sourcify')
console.log('Successfully verified on Sourcify')
} catch (error) {
console.log('Error when verifying bytecode on Sourcify:')
console.log(error)
}
}
......
/* External Imports */
import { ethers } from 'hardhat'
import { Signer, Contract, BigNumber } from 'ethers'
/* Internal Imports */
import { expect } from '../../../setup'
const initialMinDepositAmount = ethers.utils.parseEther('0.01')
const initialMaxDepositAmount = ethers.utils.parseEther('1')
const initialMaxBalance = ethers.utils.parseEther('2')
describe('TeleportrDeposit', async () => {
let teleportrDeposit: Contract
let signer: Signer
let signer2: Signer
let contractAddress: string
let signerAddress: string
let signer2Address: string
before(async () => {
;[signer, signer2] = await ethers.getSigners()
teleportrDeposit = await (
await ethers.getContractFactory('TeleportrDeposit')
).deploy(
initialMinDepositAmount,
initialMaxDepositAmount,
initialMaxBalance
)
contractAddress = teleportrDeposit.address
signerAddress = await signer.getAddress()
signer2Address = await signer2.getAddress()
})
describe('receive', async () => {
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
it('should revert if deposit amount is less than min amount', async () => {
await expect(
signer.sendTransaction({
to: contractAddress,
value: ethers.utils.parseEther('0.001'),
})
).to.be.revertedWith('Deposit amount is too small')
})
it('should revert if deposit amount is greater than max amount', async () => {
await expect(
signer.sendTransaction({
to: contractAddress,
value: ethers.utils.parseEther('1.1'),
})
).to.be.revertedWith('Deposit amount is too big')
})
it('should emit EtherReceived if called by non-owner', async () => {
await expect(
signer2.sendTransaction({
to: contractAddress,
value: oneETH,
})
)
.to.emit(teleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('0'), signer2Address, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
await expect(await ethers.provider.getBalance(contractAddress)).to.equal(
oneETH
)
})
it('should emit EtherReceived if called by owner', async () => {
await expect(
signer.sendTransaction({
to: contractAddress,
value: oneETH,
})
)
.to.emit(teleportrDeposit, 'EtherReceived')
.withArgs(BigNumber.from('1'), signerAddress, oneETH)
})
it('should increase the contract balance by deposit amount', async () => {
await expect(await ethers.provider.getBalance(contractAddress)).to.equal(
twoETH
)
})
it('should revert if deposit will exceed max balance', async () => {
await expect(
signer.sendTransaction({
to: contractAddress,
value: initialMinDepositAmount,
})
).to.be.revertedWith('Contract max balance exceeded')
})
})
describe('withdrawBalance', async () => {
let initialContractBalance: BigNumber
let initialSignerBalance: BigNumber
before(async () => {
initialContractBalance = await ethers.provider.getBalance(contractAddress)
initialSignerBalance = await ethers.provider.getBalance(signerAddress)
})
it('should revert if called by non-owner', async () => {
await expect(
teleportrDeposit.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
await expect(teleportrDeposit.withdrawBalance())
.to.emit(teleportrDeposit, 'BalanceWithdrawn')
.withArgs(signerAddress, initialContractBalance)
})
it('should leave the contract with zero balance', async () => {
await expect(await ethers.provider.getBalance(contractAddress)).to.equal(
ethers.utils.parseEther('0')
)
})
it('should credit owner with contract balance - fees', async () => {
const expSignerBalance = initialSignerBalance.add(initialContractBalance)
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(expSignerBalance, 10 ** 15)
})
})
describe('setMinAmount', async () => {
const newMinDepositAmount = ethers.utils.parseEther('0.02')
it('should revert if called by non-owner', async () => {
await expect(
teleportrDeposit.connect(signer2).setMinAmount(newMinDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MinDepositAmountSet if called by owner', async () => {
await expect(teleportrDeposit.setMinAmount(newMinDepositAmount))
.to.emit(teleportrDeposit, 'MinDepositAmountSet')
.withArgs(initialMinDepositAmount, newMinDepositAmount)
})
it('should have updated minDepositAmount after success', async () => {
await expect(await teleportrDeposit.minDepositAmount()).to.be.eq(
newMinDepositAmount
)
})
})
describe('setMaxAmount', async () => {
const newMaxDepositAmount = ethers.utils.parseEther('2')
it('should revert if called non-owner', async () => {
await expect(
teleportrDeposit.connect(signer2).setMaxAmount(newMaxDepositAmount)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxDepositAmountSet if called by owner', async () => {
await expect(teleportrDeposit.setMaxAmount(newMaxDepositAmount))
.to.emit(teleportrDeposit, 'MaxDepositAmountSet')
.withArgs(initialMaxDepositAmount, newMaxDepositAmount)
})
it('should have an updated maxDepositAmount after success', async () => {
await expect(await teleportrDeposit.maxDepositAmount()).to.be.eq(
newMaxDepositAmount
)
})
})
describe('setMaxBalance', async () => {
const newMaxBalance = ethers.utils.parseEther('2000')
it('should revert if called by non-owner', async () => {
await expect(
teleportrDeposit.connect(signer2).setMaxBalance(newMaxBalance)
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit MaxBalanceSet if called by owner', async () => {
await expect(teleportrDeposit.setMaxBalance(newMaxBalance))
.to.emit(teleportrDeposit, 'MaxBalanceSet')
.withArgs(initialMaxBalance, newMaxBalance)
})
it('should have an updated maxBalance after success', async () => {
await expect(await teleportrDeposit.maxBalance()).to.be.eq(newMaxBalance)
})
})
})
/* External Imports */
import { ethers } from 'hardhat'
import { Signer, Contract, BigNumber } from 'ethers'
/* Internal Imports */
import { expect } from '../../../setup'
describe('TeleportrDisburser', async () => {
const zeroETH = ethers.utils.parseEther('0.0')
const oneETH = ethers.utils.parseEther('1.0')
const twoETH = ethers.utils.parseEther('2.0')
let teleportrDisburser: Contract
let failingReceiver: Contract
let signer: Signer
let signer2: Signer
let contractAddress: string
let failingReceiverAddress: string
let signerAddress: string
let signer2Address: string
before(async () => {
;[signer, signer2] = await ethers.getSigners()
teleportrDisburser = await (
await ethers.getContractFactory('TeleportrDisburser')
).deploy()
failingReceiver = await (
await ethers.getContractFactory('FailingReceiver')
).deploy()
contractAddress = teleportrDisburser.address
failingReceiverAddress = failingReceiver.address
signerAddress = await signer.getAddress()
signer2Address = await signer2.getAddress()
})
describe('disburse checks', async () => {
it('should revert if called by non-owner', async () => {
await expect(
teleportrDisburser.connect(signer2).disburse(0, [], { value: oneETH })
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should revert if no disbursements is zero length', async () => {
await expect(
teleportrDisburser.disburse(0, [], { value: oneETH })
).to.be.revertedWith('No disbursements')
})
it('should revert if nextDepositId does not match expected value', async () => {
await expect(
teleportrDisburser.disburse(1, [[oneETH, signer2Address]], {
value: oneETH,
})
).to.be.revertedWith('Unexpected next deposit id')
})
it('should revert if msg.value does not match total to disburse', async () => {
await expect(
teleportrDisburser.disburse(0, [[oneETH, signer2Address]], {
value: zeroETH,
})
).to.be.revertedWith('Disbursement total != amount sent')
})
})
describe('disburse single success', async () => {
let signerInitialBalance: BigNumber
let signer2InitialBalance: BigNumber
it('should emit DisbursementSuccess for successful disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
signer2InitialBalance = await ethers.provider.getBalance(signer2Address)
await expect(
teleportrDisburser.disburse(0, [[oneETH, signer2Address]], {
value: oneETH,
})
)
.to.emit(teleportrDisburser, 'DisbursementSuccess')
.withArgs(BigNumber.from(0), signer2Address, oneETH)
})
it('should show one total disbursement', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(1)
)
})
it('should leave contract balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(zeroETH)
})
it('should increase recipients balance by disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(signer2Address)
).to.be.equal(signer2InitialBalance.add(oneETH))
})
it('should decrease owners balance by disbursement amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(oneETH), 10 ** 15)
})
})
describe('disburse single failure', async () => {
let signerInitialBalance: BigNumber
it('should emit DisbursementFailed for failed disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
await expect(
teleportrDisburser.disburse(1, [[oneETH, failingReceiverAddress]], {
value: oneETH,
})
)
.to.emit(teleportrDisburser, 'DisbursementFailed')
.withArgs(BigNumber.from(1), failingReceiverAddress, oneETH)
})
it('should show two total disbursements', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(2)
)
})
it('should leave contract with disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(oneETH)
})
it('should leave recipients balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(failingReceiverAddress)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(oneETH), 10 ** 15)
})
})
describe('withdrawBalance', async () => {
let initialContractBalance: BigNumber
let initialSignerBalance: BigNumber
it('should revert if called by non-owner', async () => {
await expect(
teleportrDisburser.connect(signer2).withdrawBalance()
).to.be.revertedWith('Ownable: caller is not the owner')
})
it('should emit BalanceWithdrawn if called by owner', async () => {
initialContractBalance = await ethers.provider.getBalance(contractAddress)
initialSignerBalance = await ethers.provider.getBalance(signerAddress)
await expect(teleportrDisburser.withdrawBalance())
.to.emit(teleportrDisburser, 'BalanceWithdrawn')
.withArgs(signerAddress, oneETH)
})
it('should leave contract with zero balance', async () => {
await expect(await ethers.provider.getBalance(contractAddress)).to.equal(
zeroETH
)
})
it('should credit owner with contract balance - fees', async () => {
const expSignerBalance = initialSignerBalance.add(initialContractBalance)
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(expSignerBalance, 10 ** 15)
})
})
describe('disburse multiple', async () => {
let signerInitialBalance: BigNumber
let signer2InitialBalance: BigNumber
it('should emit DisbursementSuccess for successful disbursement', async () => {
signerInitialBalance = await ethers.provider.getBalance(signerAddress)
signer2InitialBalance = await ethers.provider.getBalance(signer2Address)
await expect(
teleportrDisburser.disburse(
2,
[
[oneETH, signer2Address],
[oneETH, failingReceiverAddress],
],
{ value: twoETH }
)
).to.not.be.reverted
})
it('should show four total disbursements', async () => {
await expect(await teleportrDisburser.totalDisbursements()).to.be.equal(
BigNumber.from(4)
)
})
it('should leave contract balance with failed disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(contractAddress)
).to.be.equal(oneETH)
})
it('should increase success recipients balance by disbursement amount', async () => {
await expect(
await ethers.provider.getBalance(signer2Address)
).to.be.equal(signer2InitialBalance.add(oneETH))
})
it('should leave failed recipients balance at zero ETH', async () => {
await expect(
await ethers.provider.getBalance(failingReceiverAddress)
).to.be.equal(zeroETH)
})
it('should decrease owners balance by disbursement 2*amount - fees', async () => {
await expect(
await ethers.provider.getBalance(signerAddress)
).to.be.closeTo(signerInitialBalance.sub(twoETH), 10 ** 15)
})
})
})
......@@ -37,12 +37,10 @@
"@ethersproject/providers": "^5.5.3",
"@ethersproject/web": "^5.5.1",
"chai": "^4.3.4",
"ethers": "^5.5.4",
"lodash": "^4.17.21"
"ethers": "^5.5.4"
},
"devDependencies": {
"@types/chai": "^4.2.18",
"@types/lodash": "^4.14.168",
"@types/mocha": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
......
......@@ -5,6 +5,4 @@
export * from './alias'
export * from './batch-encoding'
export * from './fees'
export * from './l2context'
export * from './rollup-types'
export * from './watcher'
import cloneDeep from 'lodash/cloneDeep'
import { providers, BigNumber } from 'ethers'
const parseNumber = (n: string | number): number => {
if (typeof n === 'string' && n.startsWith('0x')) {
return parseInt(n, 16)
}
if (typeof n === 'number') {
return n
}
return parseInt(n, 10)
}
/**
* Helper for adding additional L2 context to transactions
*/
export const injectL2Context = (l1Provider: providers.JsonRpcProvider) => {
const provider = cloneDeep(l1Provider)
// Pass through the state root
const blockFormat = provider.formatter.block.bind(provider.formatter)
provider.formatter.block = (block) => {
const b = blockFormat(block)
b.stateRoot = block.stateRoot
return b
}
// Pass through the state root and additional tx data
const blockWithTransactions = provider.formatter.blockWithTransactions.bind(
provider.formatter
)
provider.formatter.blockWithTransactions = (block) => {
const b = blockWithTransactions(block)
b.stateRoot = block.stateRoot
for (let i = 0; i < b.transactions.length; i++) {
b.transactions[i].l1BlockNumber = block.transactions[i].l1BlockNumber
if (b.transactions[i].l1BlockNumber != null) {
b.transactions[i].l1BlockNumber = parseNumber(
b.transactions[i].l1BlockNumber
)
}
b.transactions[i].l1Timestamp = block.transactions[i].l1Timestamp
if (b.transactions[i].l1Timestamp != null) {
b.transactions[i].l1Timestamp = parseNumber(
b.transactions[i].l1Timestamp
)
}
b.transactions[i].l1TxOrigin = block.transactions[i].l1TxOrigin
b.transactions[i].queueOrigin = block.transactions[i].queueOrigin
b.transactions[i].rawTransaction = block.transactions[i].rawTransaction
}
return b
}
// Handle additional tx data
const formatTxResponse = provider.formatter.transactionResponse.bind(
provider.formatter
)
provider.formatter.transactionResponse = (transaction) => {
const tx = formatTxResponse(transaction) as any
tx.txType = transaction.txType
tx.queueOrigin = transaction.queueOrigin
tx.rawTransaction = transaction.rawTransaction
tx.l1BlockNumber = transaction.l1BlockNumber
if (tx.l1BlockNumber != null) {
tx.l1BlockNumber = parseInt(tx.l1BlockNumber, 16)
}
tx.l1TxOrigin = transaction.l1TxOrigin
return tx
}
const formatReceiptResponse = provider.formatter.receipt.bind(
provider.formatter
)
provider.formatter.receipt = (receipt) => {
const r = formatReceiptResponse(receipt)
r.l1GasPrice = BigNumber.from(receipt.l1GasPrice)
r.l1GasUsed = BigNumber.from(receipt.l1GasUsed)
r.l1Fee = BigNumber.from(receipt.l1Fee)
r.l1FeeScalar = parseFloat(receipt.l1FeeScalar)
return r
}
return provider
}
/* External Imports */
import { ethers } from 'ethers'
import { Provider, TransactionReceipt } from '@ethersproject/abstract-provider'
const RELAYED_MESSAGE = ethers.utils.id(`RelayedMessage(bytes32)`)
const FAILED_RELAYED_MESSAGE = ethers.utils.id(`FailedRelayedMessage(bytes32)`)
export interface Layer {
provider: Provider
messengerAddress: string
blocksToFetch?: number
}
export interface WatcherOptions {
l1: Layer
l2: Layer
pollInterval?: number
blocksToFetch?: number
pollForPending?: boolean
}
export class Watcher {
public l1: Layer
public l2: Layer
public pollInterval = 3000
public blocksToFetch = 1500
public pollForPending = true
constructor(opts: WatcherOptions) {
this.l1 = opts.l1
this.l2 = opts.l2
if (typeof opts.pollInterval === 'number') {
this.pollInterval = opts.pollInterval
}
if (typeof opts.blocksToFetch === 'number') {
this.blocksToFetch = opts.blocksToFetch
}
if (typeof opts.pollForPending === 'boolean') {
this.pollForPending = opts.pollForPending
}
}
public async getMessageHashesFromL1Tx(l1TxHash: string): Promise<string[]> {
return this.getMessageHashesFromTx(this.l1, l1TxHash)
}
public async getMessageHashesFromL2Tx(l2TxHash: string): Promise<string[]> {
return this.getMessageHashesFromTx(this.l2, l2TxHash)
}
public async getL1TransactionReceipt(
l2ToL1MsgHash: string,
pollForPending?
): Promise<TransactionReceipt> {
return this.getTransactionReceipt(this.l1, l2ToL1MsgHash, pollForPending)
}
public async getL2TransactionReceipt(
l1ToL2MsgHash: string,
pollForPending?
): Promise<TransactionReceipt> {
return this.getTransactionReceipt(this.l2, l1ToL2MsgHash, pollForPending)
}
public async getMessageHashesFromTx(
layer: Layer,
txHash: string
): Promise<string[]> {
const receipt = await layer.provider.getTransactionReceipt(txHash)
if (!receipt) {
return []
}
const msgHashes = []
const sentMessageEventId = ethers.utils.id(
'SentMessage(address,address,bytes,uint256,uint256)'
)
const l2CrossDomainMessengerRelayAbi = [
'function relayMessage(address _target,address _sender,bytes memory _message,uint256 _messageNonce)',
]
const l2CrossDomainMessengerRelayinterface = new ethers.utils.Interface(
l2CrossDomainMessengerRelayAbi
)
for (const log of receipt.logs) {
if (
log.address === layer.messengerAddress &&
log.topics[0] === sentMessageEventId
) {
const [sender, message, messageNonce] =
ethers.utils.defaultAbiCoder.decode(
['address', 'bytes', 'uint256'],
log.data
)
const [target] = ethers.utils.defaultAbiCoder.decode(
['address'],
log.topics[1]
)
const encodedMessage =
l2CrossDomainMessengerRelayinterface.encodeFunctionData(
'relayMessage',
[target, sender, message, messageNonce]
)
msgHashes.push(
ethers.utils.solidityKeccak256(['bytes'], [encodedMessage])
)
}
}
return msgHashes
}
public async getTransactionReceipt(
layer: Layer,
msgHash: string,
pollForPending?
): Promise<TransactionReceipt> {
if (typeof pollForPending !== 'boolean') {
pollForPending = this.pollForPending
}
let matches: ethers.providers.Log[] = []
let blocksToFetch = layer.blocksToFetch
if (typeof blocksToFetch !== 'number') {
blocksToFetch = this.blocksToFetch
}
// scan for transaction with specified message
while (matches.length === 0) {
const blockNumber = await layer.provider.getBlockNumber()
const startingBlock = Math.max(blockNumber - blocksToFetch, 0)
const successFilter: ethers.providers.Filter = {
address: layer.messengerAddress,
topics: [RELAYED_MESSAGE],
fromBlock: startingBlock,
}
const failureFilter: ethers.providers.Filter = {
address: layer.messengerAddress,
topics: [FAILED_RELAYED_MESSAGE],
fromBlock: startingBlock,
}
const successLogs = await layer.provider.getLogs(successFilter)
const failureLogs = await layer.provider.getLogs(failureFilter)
const logs = successLogs.concat(failureLogs)
matches = logs.filter(
(log: ethers.providers.Log) => log.topics[1] === msgHash
)
// exit loop after first iteration if not polling
if (!pollForPending) {
break
}
// pause awhile before trying again
await new Promise((r) => setTimeout(r, this.pollInterval))
}
// Message was relayed in the past
if (matches.length > 0) {
if (matches.length > 1) {
throw Error(
'Found multiple transactions relaying the same message hash.'
)
}
return layer.provider.getTransactionReceipt(matches[0].transactionHash)
} else {
return Promise.resolve(undefined)
}
}
}
/* Imports: External */
import { fromHexString, FallbackProvider } from '@eth-optimism/core-utils'
import {
fromHexString,
FallbackProvider,
sleep,
} from '@eth-optimism/core-utils'
import { BaseService, Metrics } from '@eth-optimism/common-ts'
import { TypedEvent } from '@eth-optimism/contracts/dist/types/common'
import { BaseProvider } from '@ethersproject/providers'
......@@ -15,7 +19,6 @@ import { MissingElementError } from './handlers/errors'
import { TransportDB } from '../../db/transport-db'
import {
OptimismContracts,
sleep,
loadOptimismContracts,
loadContract,
validators,
......
/* Imports: External */
import { BigNumber, ethers } from 'ethers'
import { serialize } from '@ethersproject/transactions'
import { padHexString } from '@eth-optimism/core-utils'
/* Imports: Internal */
import { TransportDB } from '../../../db/transport-db'
......@@ -9,7 +10,7 @@ import {
StateRootEntry,
TransactionEntry,
} from '../../../types'
import { padHexString, parseSignatureVParam } from '../../../utils'
import { parseSignatureVParam } from '../../../utils'
export const handleSequencerBlock = {
parseBlock: async (
......
/* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts'
import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { BigNumber } from 'ethers'
import { LevelUp } from 'levelup'
import axios from 'axios'
......@@ -10,7 +11,7 @@ import { Gauge } from 'prom-client'
/* Imports: Internal */
import { handleSequencerBlock } from './handlers/transaction'
import { TransportDB } from '../../db/transport-db'
import { sleep, toRpcHexString, validators } from '../../utils'
import { validators } from '../../utils'
import { L1DataTransportServiceOptions } from '../main/service'
interface L2IngestionMetrics {
......@@ -118,6 +119,13 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
highestSyncedL2BlockNumber === targetL2Block ||
currentL2Block === 0
) {
this.logger.info(
'All Layer 2 (Optimism) transactions are synchronized',
{
currentL2Block,
targetL2Block,
}
)
await sleep(this.options.pollingInterval)
continue
}
......@@ -218,15 +226,38 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
id: '1',
}
const resp = await axios.post(
this.state.l2RpcProvider.connection.url,
req,
{ responseType: 'stream' }
)
const respJson = await bfj.parse(resp.data, {
yieldRate: 4096, // this yields abit more often than the default of 16384
})
blocks = respJson.result
// Retry the `eth_getBlockRange` query in case the endBlockNumber
// is greater than the tip and `null` is returned. This gives time
// for the sync to catch up
let result = null
let retry = 0
while (result === null) {
if (retry === 6) {
throw new Error(
`unable to fetch block range [${startBlockNumber},${endBlockNumber})`
)
}
const resp = await axios.post(
this.state.l2RpcProvider.connection.url,
req,
{ responseType: 'stream' }
)
const respJson = await bfj.parse(resp.data, {
yieldRate: 4096, // this yields abit more often than the default of 16384
})
result = respJson.result
if (result === null) {
retry++
this.logger.info(
`request for block range [${startBlockNumber},${endBlockNumber}) returned null, retry ${retry}`
)
await sleep(1000 * retry)
}
}
blocks = result
}
for (const block of blocks) {
......
......@@ -68,6 +68,12 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
protected async _init(): Promise<void> {
this.logger.info('Initializing L1 Data Transport Service...')
if (this.options.bssHardfork1Index !== null && this.options.bssHardfork1Index !== undefined) {
this.logger.info(`BSS HF1 is active at block: ${this.options.bssHardfork1Index}`)
} else {
this.logger.info(`BSS HF1 is not active`)
}
this.state.db = level(this.options.dbPath)
await this.state.db.open()
......
import { toHexString } from '@eth-optimism/core-utils'
/**
* Basic timeout-based async sleep function.
*
* @param ms Number of milliseconds to sleep.
*/
export const sleep = async (ms: number): Promise<void> => {
return new Promise<void>((resolve) => {
setTimeout(resolve, ms)
})
}
export const assert = (condition: () => boolean, reason?: string) => {
try {
if (condition() === false) {
throw new Error(`Assertion failed: ${reason}`)
}
} catch (err) {
throw new Error(`Assertion failed: ${reason}\n${err}`)
}
}
export const toRpcHexString = (n: number): string => {
if (n === 0) {
return '0x0'
} else {
// prettier-ignore
return '0x' + toHexString(n).slice(2).replace(/^0+/, '')
}
}
export const padHexString = (str: string, length: number): string => {
if (str.length === 2 + length * 2) {
return str
} else {
return '0x' + str.slice(2).padStart(length * 2, '0')
}
}
export * from './common'
export * from './contracts'
export * from './validation'
export * from './eth-tx'
......@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node'
import * as dotenv from 'dotenv'
import Config from 'bcfg'
import { MessageRelayerService } from '../service'
import { MessageRelayerService } from '../src'
dotenv.config()
......@@ -59,14 +59,6 @@ const main = async () => {
'get-logs-interval',
parseInt(env.GET_LOGS_INTERVAL, 10) || 2000
)
const L2_BLOCK_OFFSET = config.uint(
'l2-start-offset',
parseInt(env.L2_BLOCK_OFFSET, 10) || 1
)
const L1_START_OFFSET = config.uint(
'l1-start-offset',
parseInt(env.L1_BLOCK_OFFSET, 10) || 1
)
const FROM_L2_TRANSACTION_INDEX = config.uint(
'from-l2-transaction-index',
parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0
......@@ -102,15 +94,11 @@ const main = async () => {
}
const service = new MessageRelayerService({
l1RpcProvider: l1Provider,
l2RpcProvider: l2Provider,
addressManagerAddress: ADDRESS_MANAGER_ADDRESS,
l1Wallet: wallet,
relayGasLimit: RELAY_GAS_LIMIT,
fromL2TransactionIndex: FROM_L2_TRANSACTION_INDEX,
pollingInterval: POLLING_INTERVAL,
l2BlockOffset: L2_BLOCK_OFFSET,
l1StartOffset: L1_START_OFFSET,
getLogsInterval: GET_LOGS_INTERVAL,
logger,
})
......
......@@ -7,19 +7,14 @@
"files": [
"dist/*"
],
"bin": {
"withdraw": "./src/exec/withdraw.ts"
},
"scripts": {
"start": "ts-node ./src/exec/run.ts",
"start": "ts-node ./bin/run.ts",
"build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0",
"test": "hardhat test --show-stack-traces",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json"
"lint:check": "eslint . --max-warnings=0"
},
"keywords": [
"optimism",
......@@ -35,28 +30,19 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"dotenv": "^10.0.0",
"ethers": "^5.5.4",
"merkletreejs": "^0.2.18",
"rlp": "^2.2.6"
"ethers": "^5.5.4"
},
"devDependencies": {
"@eth-optimism/smock": "1.1.10",
"@nomiclabs/ethereumjs-vm": "^4",
"@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/chai": "^4.2.18",
"@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
......@@ -68,8 +54,6 @@
"ethereum-waffle": "^3.3.0",
"hardhat": "^2.3.0",
"lint-staged": "11.0.0",
"lodash": "^4.17.21",
"mocha": "^8.4.0",
"prettier": "^2.3.1",
"ts-node": "^10.0.0",
"typescript": "^4.3.5"
......
#!/usr/bin/env ts-node
/**
* Utility that will relay all L2 => L1 messages created within a given L2 transaction.
*/
/* Imports: External */
import { ethers } from 'ethers'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import { sleep } from '@eth-optimism/core-utils'
import dotenv from 'dotenv'
/* Imports: Internal */
import { getMessagesAndProofsForL2Transaction } from '../relay-tx'
dotenv.config()
const l1RpcProviderUrl = process.env.WITHDRAW__L1_RPC_URL
const l2RpcProviderUrl = process.env.WITHDRAW__L2_RPC_URL
const l1PrivateKey = process.env.WITHDRAW__L1_PRIVATE_KEY
const l1StateCommitmentChainAddress =
process.env.WITHDRAW__STATE_COMMITMENT_CHAIN_ADDRESS
const l1CrossDomainMessengerAddress =
process.env.WITHDRAW__L1_CROSS_DOMAIN_MESSENGER_ADDRESS
const main = async () => {
const l2TransactionHash = process.argv[2]
if (l2TransactionHash === undefined) {
throw new Error(`must provide l2 transaction hash`)
}
const l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProviderUrl)
const l1Wallet = new ethers.Wallet(l1PrivateKey, l1RpcProvider)
const l1WalletBalance = await l1Wallet.getBalance()
console.log(`relayer address: ${l1Wallet.address}`)
console.log(`relayer balance: ${ethers.utils.formatEther(l1WalletBalance)}`)
const l1CrossDomainMessenger = new ethers.Contract(
l1CrossDomainMessengerAddress,
getContractInterface('L1CrossDomainMessenger'),
l1Wallet
)
console.log(`searching for messages in transaction: ${l2TransactionHash}`)
let messagePairs = []
while (true) {
try {
messagePairs = await getMessagesAndProofsForL2Transaction(
l1RpcProviderUrl,
l2RpcProviderUrl,
l1StateCommitmentChainAddress,
predeploys.L2CrossDomainMessenger,
l2TransactionHash
)
break
} catch (err) {
if (err.message.includes('unable to find state root batch for tx')) {
console.log(`no state root batch for tx yet, trying again in 5s...`)
await sleep(5000)
} else {
throw err
}
}
}
console.log(`found ${messagePairs.length} messages`)
for (let i = 0; i < messagePairs.length; i++) {
console.log(`relaying message ${i + 1}/${messagePairs.length}`)
const { message, proof } = messagePairs[i]
while (true) {
try {
const result = await l1CrossDomainMessenger.relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof
)
await result.wait()
console.log(
`relayed message ${i + 1}/${messagePairs.length}! L1 tx hash: ${
result.hash
}`
)
break
} catch (err) {
if (err.message.includes('execution failed due to an exception')) {
console.log(`fraud proof may not be elapsed, trying again in 5s...`)
await sleep(5000)
} else if (err.message.includes('message has already been received')) {
console.log(
`message ${i + 1}/${
messagePairs.length
} was relayed by someone else`
)
break
} else {
throw err
}
}
}
}
}
main()
export * from './relay-tx'
export * from './service'
/* Imports: External */
import { ethers, Transaction } from 'ethers'
import {
fromHexString,
remove0x,
toHexString,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { getContractInterface, predeploys } from '@eth-optimism/contracts'
import * as rlp from 'rlp'
import { MerkleTree } from 'merkletreejs'
// Number of blocks added to the L2 chain before the first L2 transaction. Genesis are added to the
// chain to initialize the system. However, they create a discrepancy between the L2 block number
// the index of the transaction that corresponds to that block number. For example, if there's 1
// genesis block, then the transaction with an index of 0 corresponds to the block with index 1.
const NUM_L2_GENESIS_BLOCKS = 1
interface StateRootBatchHeader {
batchIndex: ethers.BigNumber
batchRoot: string
batchSize: ethers.BigNumber
prevTotalElements: ethers.BigNumber
extraData: string
}
interface StateRootBatch {
header: StateRootBatchHeader
stateRoots: string[]
}
interface CrossDomainMessage {
target: string
sender: string
message: string
messageNonce: number
}
interface CrossDomainMessageProof {
stateRoot: string
stateRootBatchHeader: StateRootBatchHeader
stateRootProof: {
index: number
siblings: string[]
}
stateTrieWitness: string
storageTrieWitness: string
}
interface CrossDomainMessagePair {
message: CrossDomainMessage
proof: CrossDomainMessageProof
}
interface StateTrieProof {
accountProof: string
storageProof: string
}
/**
* Finds all L2 => L1 messages triggered by a given L2 transaction, if the message exists.
*
* @param l2RpcProvider L2 RPC provider.
* @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger.
* @param l2TransactionHash Hash of the L2 transaction to find a message for.
* @returns Messages associated with the transaction.
*/
export const getMessagesByTransactionHash = async (
l2RpcProvider: ethers.providers.JsonRpcProvider,
l2CrossDomainMessengerAddress: string,
l2TransactionHash: string
): Promise<CrossDomainMessage[]> => {
// Complain if we can't find the given transaction.
const transaction = await l2RpcProvider.getTransaction(l2TransactionHash)
if (transaction === null) {
throw new Error(`unable to find tx with hash: ${l2TransactionHash}`)
}
const l2CrossDomainMessenger = new ethers.Contract(
l2CrossDomainMessengerAddress,
getContractInterface('L2CrossDomainMessenger'),
l2RpcProvider
)
// Find all SentMessage events created in the same block as the given transaction. This is
// reliable because we should only have one transaction per block.
const sentMessageEvents = await l2CrossDomainMessenger.queryFilter(
l2CrossDomainMessenger.filters.SentMessage(),
transaction.blockNumber,
transaction.blockNumber
)
// Decode the messages and turn them into a nicer struct.
const sentMessages = sentMessageEvents.map((sentMessageEvent) => {
return {
target: sentMessageEvent.args.target,
sender: sentMessageEvent.args.sender,
message: sentMessageEvent.args.message, // decoded message
messageNonce: sentMessageEvent.args.messageNonce.toNumber(),
}
})
return sentMessages
}
/**
* Encodes a cross domain message.
*
* @param message Message to encode.
* @returns Encoded message.
*/
const encodeCrossDomainMessage = (message: CrossDomainMessage): string => {
return getContractInterface('L2CrossDomainMessenger').encodeFunctionData(
'relayMessage',
[message.target, message.sender, message.message, message.messageNonce]
)
}
/**
* Finds the StateBatchAppended event associated with a given L2 transaction.
*
* @param l1RpcProvider L1 RPC provider.
* @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain.
* @param l2TransactionIndex Index of the L2 transaction to find a StateBatchAppended event for.
* @returns StateBatchAppended event for the given transaction or null if no such event exists.
*/
export const getStateBatchAppendedEventByTransactionIndex = async (
l1RpcProvider: ethers.providers.JsonRpcProvider,
l1StateCommitmentChainAddress: string,
l2TransactionIndex: number
): Promise<ethers.Event | null> => {
const l1StateCommitmentChain = new ethers.Contract(
l1StateCommitmentChainAddress,
getContractInterface('StateCommitmentChain'),
l1RpcProvider
)
const getStateBatchAppendedEventByBatchIndex = async (
index: number
): Promise<ethers.Event | null> => {
const eventQueryResult = await l1StateCommitmentChain.queryFilter(
l1StateCommitmentChain.filters.StateBatchAppended(index)
)
if (eventQueryResult.length === 0) {
return null
} else {
return eventQueryResult[0]
}
}
const isEventHi = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
return index < prevTotalElements
}
const isEventLo = (event: ethers.Event, index: number) => {
const prevTotalElements = event.args._prevTotalElements.toNumber()
const batchSize = event.args._batchSize.toNumber()
return index >= prevTotalElements + batchSize
}
const totalBatches: ethers.BigNumber =
await l1StateCommitmentChain.getTotalBatches()
if (totalBatches.eq(0)) {
return null
}
let lowerBound = 0
let upperBound = totalBatches.toNumber() - 1
let batchEvent: ethers.Event | null =
await getStateBatchAppendedEventByBatchIndex(upperBound)
if (isEventLo(batchEvent, l2TransactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
} else if (!isEventHi(batchEvent, l2TransactionIndex)) {
// Upper bound is not too low and also not too high. This means the upper bound event is the
// one we're looking for! Return it.
return batchEvent
}
// Binary search to find the right event. The above checks will guarantee that the event does
// exist and that we'll find it during this search.
while (lowerBound < upperBound) {
const middleOfBounds = Math.floor((lowerBound + upperBound) / 2)
batchEvent = await getStateBatchAppendedEventByBatchIndex(middleOfBounds)
if (isEventHi(batchEvent, l2TransactionIndex)) {
upperBound = middleOfBounds
} else if (isEventLo(batchEvent, l2TransactionIndex)) {
lowerBound = middleOfBounds
} else {
break
}
}
return batchEvent
}
/**
* Finds the full state root batch associated with a given transaction index.
*
* @param l1RpcProvider L1 RPC provider.
* @param l1StateCommitmentChainAddress Address of the L1StateCommitmentChain.
* @param l2TransactionIndex Index of the L2 transaction to find a state root batch for.
* @returns State root batch associated with the given transaction index or null if no state root
* batch exists.
*/
export const getStateRootBatchByTransactionIndex = async (
l1RpcProvider: ethers.providers.JsonRpcProvider,
l1StateCommitmentChainAddress: string,
l2TransactionIndex: number
): Promise<StateRootBatch | null> => {
const l1StateCommitmentChain = new ethers.Contract(
l1StateCommitmentChainAddress,
getContractInterface('StateCommitmentChain'),
l1RpcProvider
)
const stateBatchAppendedEvent =
await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
l1StateCommitmentChainAddress,
l2TransactionIndex
)
if (stateBatchAppendedEvent === null) {
return null
}
const stateBatchTransaction = await stateBatchAppendedEvent.getTransaction()
const [stateRoots] = l1StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
stateBatchTransaction.data
)
return {
header: {
batchIndex: stateBatchAppendedEvent.args._batchIndex,
batchRoot: stateBatchAppendedEvent.args._batchRoot,
batchSize: stateBatchAppendedEvent.args._batchSize,
prevTotalElements: stateBatchAppendedEvent.args._prevTotalElements,
extraData: stateBatchAppendedEvent.args._extraData,
},
stateRoots,
}
}
/**
* Generates a Merkle proof (using the particular scheme we use within Lib_MerkleTree).
*
* @param leaves Leaves of the merkle tree.
* @param index Index to generate a proof for.
* @returns Merkle proof sibling leaves, as hex strings.
*/
export const getMerkleTreeProof = (
leaves: string[],
index: number
): string[] => {
// Our specific Merkle tree implementation requires that the number of leaves is a power of 2.
// If the number of given leaves is less than a power of 2, we need to round up to the next
// available power of 2. We fill the remaining space with the hash of bytes32(0).
const correctedTreeSize = Math.pow(2, Math.ceil(Math.log2(leaves.length)))
const parsedLeaves = []
for (let i = 0; i < correctedTreeSize; i++) {
if (i < leaves.length) {
parsedLeaves.push(leaves[i])
} else {
parsedLeaves.push(ethers.utils.keccak256('0x' + '00'.repeat(32)))
}
}
// merkletreejs prefers things to be Buffers.
const bufLeaves = parsedLeaves.map(fromHexString)
const tree = new MerkleTree(bufLeaves, (el: Buffer | string): Buffer => {
return fromHexString(ethers.utils.keccak256(el))
})
const proof = tree.getProof(bufLeaves[index], index).map((element: any) => {
return toHexString(element.data)
})
return proof
}
/**
* Generates a Merkle-Patricia trie proof for a given account and storage slot.
*
* @param l2RpcProvider L2 RPC provider.
* @param blockNumber Block number to generate the proof at.
* @param address Address to generate the proof for.
* @param slot Storage slot to generate the proof for.
* @returns Account proof and storage proof.
*/
const getStateTrieProof = async (
l2RpcProvider: ethers.providers.JsonRpcProvider,
blockNumber: number,
address: string,
slot: string
): Promise<StateTrieProof> => {
const proof = await l2RpcProvider.send('eth_getProof', [
address,
[slot],
toRpcHexString(blockNumber),
])
return {
accountProof: toHexString(rlp.encode(proof.accountProof)),
storageProof: toHexString(rlp.encode(proof.storageProof[0].proof)),
}
}
/**
* Finds all L2 => L1 messages sent in a given L2 transaction and generates proofs for each of
* those messages.
*
* @param l1RpcProvider L1 RPC provider.
* @param l2RpcProvider L2 RPC provider.
* @param l1StateCommitmentChainAddress Address of the StateCommitmentChain.
* @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger.
* @param l2TransactionHash L2 transaction hash to generate a relay transaction for.
* @returns An array of messages sent in the transaction and a proof of inclusion for each.
*/
export const getMessagesAndProofsForL2Transaction = async (
l1RpcProvider: ethers.providers.JsonRpcProvider | string,
l2RpcProvider: ethers.providers.JsonRpcProvider | string,
l1StateCommitmentChainAddress: string,
l2CrossDomainMessengerAddress: string,
l2TransactionHash: string
): Promise<CrossDomainMessagePair[]> => {
if (typeof l1RpcProvider === 'string') {
l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProvider)
}
if (typeof l2RpcProvider === 'string') {
l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProvider)
}
const l2Transaction = await l2RpcProvider.getTransaction(l2TransactionHash)
if (l2Transaction === null) {
throw new Error(`unable to find tx with hash: ${l2TransactionHash}`)
}
// Need to find the state batch for the given transaction. If no state batch has been published
// yet then we will not be able to generate a proof.
const batch = await getStateRootBatchByTransactionIndex(
l1RpcProvider,
l1StateCommitmentChainAddress,
l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS
)
if (batch === null) {
throw new Error(
`unable to find state root batch for tx with hash: ${l2TransactionHash}`
)
}
// Adjust the transaction index based on the number of L2 genesis block we have. "Index" here
// refers to the position of the transaction within the *Canonical Transaction Chain*.
const l2TransactionIndex = l2Transaction.blockNumber - NUM_L2_GENESIS_BLOCKS
// Here the index refers to the position of the state root that corresponds to this transaction
// within the batch of state roots in which that state root was published.
const txIndexInBatch =
l2TransactionIndex - batch.header.prevTotalElements.toNumber()
// Find every message that was sent during this transaction. We'll then attach a proof for each.
const messages = await getMessagesByTransactionHash(
l2RpcProvider,
l2CrossDomainMessengerAddress,
l2TransactionHash
)
const messagePairs: CrossDomainMessagePair[] = []
for (const message of messages) {
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossDomainMessage(message) +
remove0x(l2CrossDomainMessengerAddress)
) + '00'.repeat(32)
)
// We need a Merkle trie proof for the given storage slot. This allows us to prove to L1 that
// the message was actually sent on L2.
const stateTrieProof = await getStateTrieProof(
l2RpcProvider,
l2Transaction.blockNumber,
predeploys.OVM_L2ToL1MessagePasser,
messageSlot
)
// State roots are published in batches to L1 and correspond 1:1 to transactions. We compute a
// Merkle root for these state roots so that we only need to store the minimum amount of
// information on-chain. So we need to create a Merkle proof for the specific state root that
// corresponds to this transaction.
const stateRootMerkleProof = getMerkleTreeProof(
batch.stateRoots,
txIndexInBatch
)
// We now have enough information to create the message proof.
const proof: CrossDomainMessageProof = {
stateRoot: batch.stateRoots[txIndexInBatch],
stateRootBatchHeader: batch.header,
stateRootProof: {
index: txIndexInBatch,
siblings: stateRootMerkleProof,
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
}
messagePairs.push({
message,
proof,
})
}
return messagePairs
}
/**
* Allows for proof generation of pre-regenesis L2->L1 messages, by retrieving proofs from
* The genesis state (block 0) of the post-regenesis chain. This is required because the
* history is wiped during regnesis, so old inclusion proofs would no longer work.
*
* @param l1RpcProvider L1 RPC provider.
* @param l2RpcProvider L2 RPC provider of the POST-REGENESIS chain.
* @param legacyL2Transaction A PRE-REGENESIS L2 transaction which sent some L2->L1 messages.
* @param legacyMessages The L2->L1 messages which were sent by the legacy L2 transaction.
* @param l1StateCommitmentChainAddress Address of the POST-REGENESIS StateCommitmentChain.
* @param l2CrossDomainMessengerAddress Address of the L2CrossDomainMessenger.
* @returns An array of messages sent in the transaction and a proof of inclusion for each.
*/
export const getLegacyProofsForL2Transaction = async (
l1RpcProvider: ethers.providers.JsonRpcProvider | string,
l2RpcProvider: ethers.providers.JsonRpcProvider | string,
legacyL2Transaction: Transaction,
legacyMessages: CrossDomainMessage[],
l1StateCommitmentChainAddress: string,
l2CrossDomainMessengerAddress: string
): Promise<CrossDomainMessagePair[]> => {
if (typeof l1RpcProvider === 'string') {
l1RpcProvider = new ethers.providers.JsonRpcProvider(l1RpcProvider)
}
if (typeof l2RpcProvider === 'string') {
l2RpcProvider = new ethers.providers.JsonRpcProvider(l2RpcProvider)
}
// We will use the first ever batch submitted on the new chain
// Because the genesis state already contains all of those state roots, and
// That's the earliest we'll be able to relay the withdrawal.
// This is 1 and not 0 because we don't commit the genesis state.
const postRegenesisBlockToRelayFrom = 1
const batch = await getStateRootBatchByTransactionIndex(
l1RpcProvider,
l1StateCommitmentChainAddress,
postRegenesisBlockToRelayFrom - NUM_L2_GENESIS_BLOCKS
)
if (batch === null) {
throw new Error(
`unable to find first state root batch for legacy withdrawal: ${
legacyL2Transaction?.hash || legacyL2Transaction
}`
)
}
// Here the index refers to the position of the state root that corresponds to this transaction
// within the batch of state roots in which that state root was published.
// Since this is a legacy TX, we get it from 0 always.
// (see comment on `postRegenesisBlockToRelayFrom` above)
const txIndexInBatch = 0
const messagePairs: CrossDomainMessagePair[] = []
for (const message of legacyMessages) {
// We need to calculate the specific storage slot that demonstrates that this message was
// actually included in the L2 chain. The following calculation is based on the fact that
// messages are stored in the following mapping on L2:
// https://github.com/ethereum-optimism/optimism/blob/c84d3450225306abbb39b4e7d6d82424341df2be/packages/contracts/contracts/L2/predeploys/OVM_L2ToL1MessagePasser.sol#L23
// You can read more about how Solidity storage slots are computed for mappings here:
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
encodeCrossDomainMessage(message) +
remove0x(l2CrossDomainMessengerAddress)
) + '00'.repeat(32)
)
// We need a Merkle trie proof for the given storage slot. This allows us to prove to L1 that
// the message was actually sent on L2.
// Because this is a legacy message, we just get it from index 0.
const stateTrieProof = await getStateTrieProof(
l2RpcProvider,
postRegenesisBlockToRelayFrom,
predeploys.OVM_L2ToL1MessagePasser,
messageSlot
)
// State roots are published in batches to L1 and correspond 1:1 to transactions. We compute a
// Merkle root for these state roots so that we only need to store the minimum amount of
// information on-chain. So we need to create a Merkle proof for the specific state root that
// corresponds to this transaction.
const stateRootMerkleProof = getMerkleTreeProof(
batch.stateRoots,
txIndexInBatch
)
// We now have enough information to create the message proof.
const proof: CrossDomainMessageProof = {
stateRoot: batch.stateRoots[txIndexInBatch],
stateRootBatchHeader: batch.header,
stateRootProof: {
index: txIndexInBatch,
siblings: stateRootMerkleProof,
},
stateTrieWitness: stateTrieProof.accountProof,
storageTrieWitness: stateTrieProof.storageProof,
}
messagePairs.push({
message,
proof,
})
}
return messagePairs
}
/* Imports: External */
import { Contract, ethers, Wallet, BigNumber, providers } from 'ethers'
import * as rlp from 'rlp'
import { MerkleTree } from 'merkletreejs'
import { fromHexString, sleep } from '@eth-optimism/core-utils'
import { Wallet } from 'ethers'
import { sleep } from '@eth-optimism/core-utils'
import { Logger, BaseService, Metrics } from '@eth-optimism/common-ts'
import {
loadContract,
loadContractFromManager,
predeploys,
} from '@eth-optimism/contracts'
/* Imports: Internal */
import { StateRootBatchHeader, SentMessage, SentMessageProof } from './types'
CrossChainMessenger,
MessageStatus,
ProviderLike,
} from '@eth-optimism/sdk'
interface MessageRelayerOptions {
// Providers for interacting with L1 and L2.
l1RpcProvider: providers.StaticJsonRpcProvider
l2RpcProvider: providers.StaticJsonRpcProvider
// Address of the AddressManager contract, used to resolve the various addresses we'll need
// within this service.
addressManagerAddress: string
/**
* Provider for interacting with L2.
*/
l2RpcProvider: ProviderLike
// Wallet instance, used to sign and send the L1 relay transactions.
/**
* Wallet used to interact with L1.
*/
l1Wallet: Wallet
// Max gas to relay messages with.
relayGasLimit: number
/**
* Gas to relay transactions with. If not provided, will use the estimated gas for the relay
* transaction.
*/
relayGasLimit?: number
// Height of the L2 transaction to start searching for L2->L1 messages.
/**
* Index of the first L2 transaction to start processing from.
*/
fromL2TransactionIndex?: number
// Interval in seconds to wait between loops.
/**
* Waiting interval between loops when the service is at the tip.
*/
pollingInterval?: number
// Number of blocks that L2 is "ahead" of transaction indices. Can happen if blocks are created
// on L2 after the genesis but before the first state commitment is published.
l2BlockOffset?: number
// L1 block to start querying events from. Recommended to set to the StateCommitmentChain deploy height
l1StartOffset?: number
// Number of blocks within each getLogs query - max is 2000
/**
* Size of the block range to query when looking for new SentMessage events.
*/
getLogsInterval?: number
// A custom logger to transport logs via; default STDOUT
/**
* Logger to transport logs. Defaults to STDOUT.
*/
logger?: Logger
// A custom metrics tracker to manage metrics; default undefined
/**
* Metrics object to use. Defaults to no metrics.
*/
metrics?: Metrics
}
const optionSettings = {
relayGasLimit: { default: 4_000_000 },
fromL2TransactionIndex: { default: 0 },
pollingInterval: { default: 5000 },
l2BlockOffset: { default: 1 },
l1StartOffset: { default: 0 },
getLogsInterval: { default: 2000 },
}
export class MessageRelayerService extends BaseService<MessageRelayerOptions> {
constructor(options: MessageRelayerOptions) {
super('Message_Relayer', options, optionSettings)
super('Message_Relayer', options, {
relayGasLimit: {
default: 4_000_000,
},
fromL2TransactionIndex: {
default: 0,
},
pollingInterval: {
default: 5000,
},
getLogsInterval: {
default: 2000,
},
})
}
private state: {
lastFinalizedTxHeight: number
nextUnfinalizedTxHeight: number
lastQueriedL1Block: number
eventCache: ethers.Event[]
Lib_AddressManager: Contract
StateCommitmentChain: Contract
L1CrossDomainMessenger: Contract
L2CrossDomainMessenger: Contract
OVM_L2ToL1MessagePasser: Contract
}
messenger: CrossChainMessenger
highestCheckedL2Tx: number
} = {} as any
protected async _init(): Promise<void> {
this.logger.info('Initializing message relayer', {
relayGasLimit: this.options.relayGasLimit,
fromL2TransactionIndex: this.options.fromL2TransactionIndex,
pollingInterval: this.options.pollingInterval,
l2BlockOffset: this.options.l2BlockOffset,
getLogsInterval: this.options.getLogsInterval,
})
// Need to improve this, sorry.
this.state = {} as any
const address = await this.options.l1Wallet.getAddress()
this.logger.info('Using L1 EOA', { address })
this.state.Lib_AddressManager = loadContract(
'Lib_AddressManager',
this.options.addressManagerAddress,
this.options.l1RpcProvider
)
this.logger.info('Connecting to StateCommitmentChain...')
this.state.StateCommitmentChain = await loadContractFromManager({
name: 'StateCommitmentChain',
Lib_AddressManager: this.state.Lib_AddressManager,
provider: this.options.l1RpcProvider,
})
this.logger.info('Connected to StateCommitmentChain', {
address: this.state.StateCommitmentChain.address,
})
this.logger.info('Connecting to L1CrossDomainMessenger...')
this.state.L1CrossDomainMessenger = await loadContractFromManager({
name: 'L1CrossDomainMessenger',
proxy: 'Proxy__OVM_L1CrossDomainMessenger',
Lib_AddressManager: this.state.Lib_AddressManager,
provider: this.options.l1RpcProvider,
})
this.logger.info('Connected to L1CrossDomainMessenger', {
address: this.state.L1CrossDomainMessenger.address,
})
this.logger.info('Connecting to L2CrossDomainMessenger...')
this.state.L2CrossDomainMessenger = await loadContractFromManager({
name: 'L2CrossDomainMessenger',
Lib_AddressManager: this.state.Lib_AddressManager,
provider: this.options.l2RpcProvider,
const l1Network = await this.options.l1Wallet.provider.getNetwork()
const l1ChainId = l1Network.chainId
this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.options.l1Wallet,
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId,
})
this.logger.info('Connected to L2CrossDomainMessenger', {
address: this.state.L2CrossDomainMessenger.address,
})
this.logger.info('Connecting to OVM_L2ToL1MessagePasser...')
this.state.OVM_L2ToL1MessagePasser = loadContract(
'OVM_L2ToL1MessagePasser',
predeploys.OVM_L2ToL1MessagePasser,
this.options.l2RpcProvider
)
this.logger.info('Connected to OVM_L2ToL1MessagePasser', {
address: this.state.OVM_L2ToL1MessagePasser.address,
})
this.logger.info('Connected to all contracts.')
this.state.lastQueriedL1Block = this.options.l1StartOffset
this.state.eventCache = []
this.state.lastFinalizedTxHeight = this.options.fromL2TransactionIndex || 0
this.state.nextUnfinalizedTxHeight =
this.options.fromL2TransactionIndex || 0
this.state.highestCheckedL2Tx = this.options.fromL2TransactionIndex || 1
}
protected async _start(): Promise<void> {
......@@ -153,102 +98,85 @@ export class MessageRelayerService extends BaseService<MessageRelayerOptions> {
await sleep(this.options.pollingInterval)
try {
this.logger.info('Checking for newly finalized transactions...')
if (
!(await this._isTransactionFinalized(
this.state.nextUnfinalizedTxHeight
))
) {
this.logger.info('Did not find any newly finalized transactions', {
retryAgainInS: Math.floor(this.options.pollingInterval / 1000),
})
continue
}
this.state.lastFinalizedTxHeight = this.state.nextUnfinalizedTxHeight
while (
await this._isTransactionFinalized(this.state.nextUnfinalizedTxHeight)
) {
const size = (
await this._getStateBatchHeader(this.state.nextUnfinalizedTxHeight)
).batch.batchSize.toNumber()
this.logger.info(
'Found a batch of finalized transaction(s), checking for more...',
{ batchSize: size }
)
this.state.nextUnfinalizedTxHeight += size
// Only deal with ~1000 transactions at a time so we can limit the amount of stuff we
// need to keep in memory. We operate on full batches at a time so the actual amount
// depends on the size of the batches we're processing.
const numTransactionsToProcess =
this.state.nextUnfinalizedTxHeight -
this.state.lastFinalizedTxHeight
if (numTransactionsToProcess > 1000) {
break
// Loop strategy is as follows:
// 1. Get the current L2 tip
// 2. While we're not at the tip:
// 2.1. Get the transaction for the next L2 block to parse.
// 2.2. Find any messages sent in the L2 block.
// 2.3. Make sure all messages are ready to be relayed.
// 3.4. Relay the messages.
const l2BlockNumber =
await this.state.messenger.l2Provider.getBlockNumber()
while (this.state.highestCheckedL2Tx <= l2BlockNumber) {
this.logger.info(`checking L2 block ${this.state.highestCheckedL2Tx}`)
const block =
await this.state.messenger.l2Provider.getBlockWithTransactions(
this.state.highestCheckedL2Tx
)
// Should never happen.
if (block.transactions.length !== 1) {
throw new Error(
`got an unexpected number of transactions in block: ${block.number}`
)
}
}
this.logger.info('Found finalized transactions', {
totalNumber:
this.state.nextUnfinalizedTxHeight -
this.state.lastFinalizedTxHeight,
})
const messages = await this._getSentMessages(
this.state.lastFinalizedTxHeight,
this.state.nextUnfinalizedTxHeight
)
const messages = await this.state.messenger.getMessagesByTransaction(
block.transactions[0].hash
)
for (const message of messages) {
this.logger.info('Found a message sent during transaction', {
index: message.parentTransactionIndex,
})
if (await this._wasMessageRelayed(message)) {
this.logger.info('Message has already been relayed, skipping.')
// No messages in this transaction so we can move on to the next one.
if (messages.length === 0) {
this.state.highestCheckedL2Tx++
continue
}
this.logger.info(
'Message not yet relayed. Attempting to generate a proof...'
)
const proof = await this._getMessageProof(message)
this.logger.info(
'Successfully generated a proof. Attempting to relay to Layer 1...'
)
await this._relayMessageToL1(message, proof)
}
// Make sure that all messages sent within the transaction are finalized. If any messages
// are not finalized, then we're going to break the loop which will trigger the sleep and
// wait for a few seconds before we check again to see if this transaction is finalized.
let isFinalized = true
for (const message of messages) {
const status = await this.state.messenger.getMessageStatus(message)
if (
status === MessageStatus.IN_CHALLENGE_PERIOD ||
status === MessageStatus.STATE_ROOT_NOT_PUBLISHED
) {
isFinalized = false
}
}
if (messages.length === 0) {
this.logger.info('Did not find any L2->L1 messages', {
retryAgainInS: Math.floor(this.options.pollingInterval / 1000),
})
} else {
// Clear the event cache to avoid keeping every single event in memory and eventually
// getting OOM killed. Messages are already sorted in ascending order so the last message
// will have the highest batch index.
const lastMessage = messages[messages.length - 1]
if (!isFinalized) {
this.logger.info(
`tx not yet finalized, waiting: ${this.state.highestCheckedL2Tx}`
)
break
} else {
this.logger.info(
`tx is finalized, relaying: ${this.state.highestCheckedL2Tx}`
)
}
// Find the batch corresponding to the last processed message.
const lastProcessedBatch = await this._getStateBatchHeader(
lastMessage.parentTransactionIndex
)
// If we got here then all messages in the transaction are finalized. Now we can relay
// each message to L1.
for (const message of messages) {
try {
const tx = await this.state.messenger.finalizeMessage(message)
this.logger.info(`relayer sent tx: ${tx.hash}`)
} catch (err) {
if (err.message.includes('message has already been received')) {
// It's fine, the message was relayed by someone else
} else {
throw err
}
}
await this.state.messenger.waitForMessageReceipt(message)
}
// Remove any events from the cache for batches that should've been processed by now.
this.state.eventCache = this.state.eventCache.filter((event) => {
return event.args._batchIndex > lastProcessedBatch.batch.batchIndex
})
// All messages have been relayed so we can move on to the next block.
this.state.highestCheckedL2Tx++
}
this.logger.info(
'Finished searching through newly finalized transactions',
{
retryAgainInS: Math.floor(this.options.pollingInterval / 1000),
}
)
} catch (err) {
this.logger.error('Caught an unhandled error', {
message: err.toString(),
......@@ -258,288 +186,4 @@ export class MessageRelayerService extends BaseService<MessageRelayerOptions> {
}
}
}
private async _getStateBatchHeader(height: number): Promise<
| {
batch: StateRootBatchHeader
stateRoots: string[]
}
| undefined
> {
const getStateBatchAppendedEventForIndex = (
txIndex: number
): ethers.Event => {
return this.state.eventCache.find((cachedEvent) => {
const prevTotalElements = cachedEvent.args._prevTotalElements.toNumber()
const batchSize = cachedEvent.args._batchSize.toNumber()
// Height should be within the bounds of the batch.
return (
txIndex >= prevTotalElements &&
txIndex < prevTotalElements + batchSize
)
})
}
let startingBlock = this.state.lastQueriedL1Block
while (
startingBlock < (await this.options.l1RpcProvider.getBlockNumber())
) {
this.state.lastQueriedL1Block = startingBlock
this.logger.info('Querying events', {
startingBlock,
endBlock: startingBlock + this.options.getLogsInterval,
})
const events: ethers.Event[] =
await this.state.StateCommitmentChain.queryFilter(
this.state.StateCommitmentChain.filters.StateBatchAppended(),
startingBlock,
startingBlock + this.options.getLogsInterval
)
this.state.eventCache = this.state.eventCache.concat(events)
startingBlock += this.options.getLogsInterval
// We need to stop syncing early once we find the event we're looking for to avoid putting
// *all* events into memory at the same time. Otherwise we'll get OOM killed.
if (getStateBatchAppendedEventForIndex(height) !== undefined) {
break
}
}
const event = getStateBatchAppendedEventForIndex(height)
if (event === undefined) {
return undefined
}
const transaction = await this.options.l1RpcProvider.getTransaction(
event.transactionHash
)
const [stateRoots] =
this.state.StateCommitmentChain.interface.decodeFunctionData(
'appendStateBatch',
transaction.data
)
return {
batch: {
batchIndex: event.args._batchIndex,
batchRoot: event.args._batchRoot,
batchSize: event.args._batchSize,
prevTotalElements: event.args._prevTotalElements,
extraData: event.args._extraData,
},
stateRoots,
}
}
private async _isTransactionFinalized(height: number): Promise<boolean> {
this.logger.info('Checking if tx is finalized', { height })
const header = await this._getStateBatchHeader(height)
if (header === undefined) {
this.logger.info('No state batch header found.')
return false
} else {
this.logger.info('Got state batch header', { header })
}
return !(await this.state.StateCommitmentChain.insideFraudProofWindow(
header.batch
))
}
/**
* Returns all sent message events between some start height (inclusive) and an end height
* (exclusive).
*
* @param startHeight Start height to start finding messages from.
* @param endHeight End height to finish finding messages at.
* @returns All sent messages between start and end height, sorted by transaction index in
* ascending order.
*/
private async _getSentMessages(
startHeight: number,
endHeight: number
): Promise<SentMessage[]> {
const filter = this.state.L2CrossDomainMessenger.filters.SentMessage()
const events = await this.state.L2CrossDomainMessenger.queryFilter(
filter,
startHeight + this.options.l2BlockOffset,
endHeight + this.options.l2BlockOffset - 1
)
const messages = events.map((event) => {
const encodedMessage =
this.state.L2CrossDomainMessenger.interface.encodeFunctionData(
'relayMessage',
[
event.args.target,
event.args.sender,
event.args.message,
event.args.messageNonce,
]
)
return {
target: event.args.target,
sender: event.args.sender,
message: event.args.message,
messageNonce: event.args.messageNonce,
encodedMessage,
encodedMessageHash: ethers.utils.keccak256(encodedMessage),
parentTransactionIndex: event.blockNumber - this.options.l2BlockOffset,
parentTransactionHash: event.transactionHash,
}
})
// Sort in ascending order based on tx index and return.
return messages.sort((a, b) => {
return a.parentTransactionIndex - b.parentTransactionIndex
})
}
private async _wasMessageRelayed(message: SentMessage): Promise<boolean> {
return this.state.L1CrossDomainMessenger.successfulMessages(
message.encodedMessageHash
)
}
private async _getMessageProof(
message: SentMessage
): Promise<SentMessageProof> {
const messageSlot = ethers.utils.keccak256(
ethers.utils.keccak256(
message.encodedMessage +
this.state.L2CrossDomainMessenger.address.slice(2)
) + '00'.repeat(32)
)
// TODO: Complain if the proof doesn't exist.
const proof = await this.options.l2RpcProvider.send('eth_getProof', [
this.state.OVM_L2ToL1MessagePasser.address,
[messageSlot],
'0x' +
BigNumber.from(
message.parentTransactionIndex + this.options.l2BlockOffset
)
.toHexString()
.slice(2)
.replace(/^0+/, ''),
])
// TODO: Complain if the batch doesn't exist.
const header = await this._getStateBatchHeader(
message.parentTransactionIndex
)
const elements = []
for (
let i = 0;
i < Math.pow(2, Math.ceil(Math.log2(header.stateRoots.length)));
i++
) {
if (i < header.stateRoots.length) {
elements.push(header.stateRoots[i])
} else {
elements.push(ethers.utils.keccak256('0x' + '00'.repeat(32)))
}
}
const hash = (el: Buffer | string): Buffer => {
return Buffer.from(ethers.utils.keccak256(el).slice(2), 'hex')
}
const leaves = elements.map((element) => {
return fromHexString(element)
})
const tree = new MerkleTree(leaves, hash)
const index =
message.parentTransactionIndex - header.batch.prevTotalElements.toNumber()
const treeProof = tree.getProof(leaves[index], index).map((element) => {
return element.data
})
return {
stateRoot: header.stateRoots[index],
stateRootBatchHeader: header.batch,
stateRootProof: {
index,
siblings: treeProof,
},
stateTrieWitness: rlp.encode(proof.accountProof),
storageTrieWitness: rlp.encode(proof.storageProof[0].proof),
}
}
private async _relayMessageToL1(
message: SentMessage,
proof: SentMessageProof
): Promise<void> {
try {
this.logger.info('Dry-run, checking to make sure proof would succeed...')
await this.state.L1CrossDomainMessenger.connect(
this.options.l1Wallet
).callStatic.relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof,
{
gasLimit: this.options.relayGasLimit,
}
)
this.logger.info('Proof should succeed. Submitting for real this time...')
} catch (err) {
this.logger.error('Proof would fail, skipping', {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
const result = await this.state.L1CrossDomainMessenger.connect(
this.options.l1Wallet
).relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof,
{
gasLimit: this.options.relayGasLimit,
}
)
this.logger.info('Relay message transaction sent', {
transactionHash: result,
})
try {
const receipt = await result.wait()
this.logger.info('Relay message included in block', {
transactionHash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString(),
confirmations: receipt.confirmations,
status: receipt.status,
})
} catch (err) {
this.logger.error('Real relay attempt failed, skipping.', {
message: err.toString(),
stack: err.stack,
code: err.code,
})
return
}
this.logger.info('Message successfully relayed to Layer 1!')
}
}
import { BigNumber } from 'ethers'
export interface StateRootBatchHeader {
batchIndex: BigNumber
batchRoot: string
batchSize: BigNumber
prevTotalElements: BigNumber
extraData: string
}
export interface SentMessage {
target: string
sender: string
message: string
messageNonce: number
encodedMessage: string
encodedMessageHash: string
parentTransactionIndex: number
parentTransactionHash: string
}
export interface SentMessageProof {
stateRoot: string
stateRootBatchHeader: StateRootBatchHeader
stateRootProof: StateRootProof
stateTrieWitness: string | Buffer
storageTrieWitness: string | Buffer
}
export interface StateRootProof {
index: number
siblings: string[]
}
/* External Imports */
import chai = require('chai')
import Mocha from 'mocha'
import { solidity } from 'ethereum-waffle'
import chaiAsPromised from 'chai-as-promised'
chai.use(solidity)
chai.use(chaiAsPromised)
const should = chai.should()
const expect = chai.expect
export { should, expect, Mocha }
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract MockL2CrossDomainMessenger {
struct MessageData {
address target;
address sender;
bytes message;
uint256 messageNonce;
}
event SentMessage(
address indexed target,
address sender,
bytes message,
uint256 messageNonce,
uint256 gasLimit);
function emitSentMessageEvent(
MessageData memory _message
)
public
{
emit SentMessage(
_message.target,
_message.sender,
_message.message,
_message.messageNonce,
0
);
}
function emitMultipleSentMessageEvents(
MessageData[] memory _messages
)
public
{
for (uint256 i = 0; i < _messages.length; i++) {
emitSentMessageEvent(
_messages[i]
);
}
}
function doNothing() public {}
}
/* Imports: External */
import hre from 'hardhat'
import { Contract, Signer } from 'ethers'
import { getContractFactory } from '@eth-optimism/contracts'
import { smockit } from '@eth-optimism/smock'
import { toPlainObject } from 'lodash'
/* Imports: Internal */
import { expect } from '../setup'
import {
getMerkleTreeProof,
getMessagesAndProofsForL2Transaction,
getStateRootBatchByTransactionIndex,
getStateBatchAppendedEventByTransactionIndex,
getMessagesByTransactionHash,
} from '../../src/relay-tx'
describe('relay transaction generation functions', () => {
const ethers = (hre as any).ethers
const l1RpcProvider = ethers.provider
const l2RpcProvider = ethers.provider
let signer1: Signer
before(async () => {
;[signer1] = await ethers.getSigners()
})
let MockL2CrossDomainMessenger: Contract
beforeEach(async () => {
const factory = await ethers.getContractFactory(
'MockL2CrossDomainMessenger'
)
MockL2CrossDomainMessenger = await factory.deploy()
})
let StateCommitmentChain: Contract
beforeEach(async () => {
const factory1 = getContractFactory('Lib_AddressManager')
const factory2 = getContractFactory('ChainStorageContainer')
const factory3 = getContractFactory('StateCommitmentChain')
const mockBondManager = await smockit(getContractFactory('BondManager'))
const mockCanonicalTransactionChain = await smockit(
getContractFactory('CanonicalTransactionChain')
)
mockBondManager.smocked.isCollateralized.will.return.with(true)
mockCanonicalTransactionChain.smocked.getTotalElements.will.return.with(
999999
)
const AddressManager = await factory1.connect(signer1).deploy()
const ChainStorageContainer = await factory2
.connect(signer1)
.deploy(AddressManager.address, 'StateCommitmentChain')
StateCommitmentChain = await factory3
.connect(signer1)
.deploy(AddressManager.address, 0, 0)
await AddressManager.setAddress(
'ChainStorageContainer-SCC-batches',
ChainStorageContainer.address
)
await AddressManager.setAddress(
'StateCommitmentChain',
StateCommitmentChain.address
)
await AddressManager.setAddress('BondManager', mockBondManager.address)
await AddressManager.setAddress(
'CanonicalTransactionChain',
mockCanonicalTransactionChain.address
)
})
describe('getMessageByTransactionHash', () => {
it('should throw an error if a transaction with the given hash does not exist', async () => {
await expect(
getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
ethers.constants.HashZero
)
).to.be.rejected
})
it('should return null if the transaction did not emit a SentMessage event', async () => {
const tx = await MockL2CrossDomainMessenger.doNothing()
expect(
await getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.deep.equal([])
})
it('should return the parsed event if the transaction emitted exactly one SentMessage event', async () => {
const message = {
target: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
message: '0x',
messageNonce: 0,
}
const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent(message)
expect(
await getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.deep.equal([message])
})
it('should return the parsed events if the transaction emitted more than one SentMessage event', async () => {
const messages = [
{
target: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
message: '0x',
messageNonce: 0,
},
{
target: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
message: '0x',
messageNonce: 1,
},
]
const tx = await MockL2CrossDomainMessenger.emitMultipleSentMessageEvents(
messages
)
expect(
await getMessagesByTransactionHash(
l2RpcProvider,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.deep.equal(messages)
})
})
describe('getStateBatchAppendedEventByTransactionIndex', () => {
it('should return null when there are no batches yet', async () => {
expect(
await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
0
)
).to.equal(null)
})
it('should return null if a batch for the index does not exist', async () => {
// Should have a total of 1 element now.
await StateCommitmentChain.appendStateBatch(
[ethers.constants.HashZero],
0
)
expect(
await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
1 // Index 0 is ok but 1 should return null
)
).to.equal(null)
})
it('should return the batch if the index is part of the first batch', async () => {
// 5 elements
await StateCommitmentChain.appendStateBatch(
[
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
],
0
)
// Add another 5 so we have two batches and can isolate tests against the first.
await StateCommitmentChain.appendStateBatch(
[
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
],
5
)
const event = await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
1
)
expect(toPlainObject(event.args)).to.deep.include({
_batchIndex: ethers.BigNumber.from(0),
_batchSize: ethers.BigNumber.from(5),
_prevTotalElements: ethers.BigNumber.from(0),
})
})
it('should return the batch if the index is part of the last batch', async () => {
// 5 elements
await StateCommitmentChain.appendStateBatch(
[
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
],
0
)
// Add another 5 so we have two batches and can isolate tests against the second.
await StateCommitmentChain.appendStateBatch(
[
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
ethers.constants.HashZero,
],
5
)
const event = await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
7
)
expect(toPlainObject(event.args)).to.deep.include({
_batchIndex: ethers.BigNumber.from(1),
_batchSize: ethers.BigNumber.from(5),
_prevTotalElements: ethers.BigNumber.from(5),
})
})
for (const numBatches of [1, 2, 8]) {
const elementsPerBatch = 8
describe(`when there are ${numBatches} batch(es) of ${elementsPerBatch} elements each`, () => {
const totalElements = numBatches * elementsPerBatch
beforeEach(async () => {
for (let i = 0; i < numBatches; i++) {
await StateCommitmentChain.appendStateBatch(
new Array(elementsPerBatch).fill(ethers.constants.HashZero),
i * elementsPerBatch
)
}
})
for (let i = 0; i < totalElements; i += elementsPerBatch) {
it(`should be able to get the correct event for the ${i}th/st/rd/whatever element`, async () => {
const event = await getStateBatchAppendedEventByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
i
)
expect(toPlainObject(event.args)).to.deep.include({
_batchIndex: ethers.BigNumber.from(i / elementsPerBatch),
_batchSize: ethers.BigNumber.from(elementsPerBatch),
_prevTotalElements: ethers.BigNumber.from(i),
})
})
}
})
}
})
describe('getStateRootBatchByTransactionIndex', () => {
it('should return null if a batch for the index does not exist', async () => {
// Should have a total of 1 element now.
await StateCommitmentChain.appendStateBatch(
[ethers.constants.HashZero],
0
)
expect(
await getStateRootBatchByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
1 // Index 0 is ok but 1 should return null
)
).to.equal(null)
})
it('should return the full batch for a given index when it exists', async () => {
// Should have a total of 1 element now.
await StateCommitmentChain.appendStateBatch(
[ethers.constants.HashZero],
0
)
const batch = await getStateRootBatchByTransactionIndex(
l1RpcProvider,
StateCommitmentChain.address,
0 // Index 0 is ok but 1 should return null
)
expect(batch.header).to.deep.include({
batchIndex: ethers.BigNumber.from(0),
batchSize: ethers.BigNumber.from(1),
prevTotalElements: ethers.BigNumber.from(0),
})
expect(batch.stateRoots).to.deep.equal([ethers.constants.HashZero])
})
})
describe('makeRelayTransactionData', () => {
it('should throw an error if the transaction does not exist', async () => {
await expect(
getMessagesAndProofsForL2Transaction(
l1RpcProvider,
l2RpcProvider,
StateCommitmentChain.address,
MockL2CrossDomainMessenger.address,
ethers.constants.HashZero
)
).to.be.rejected
})
it('should throw an error if the transaction did not send a message', async () => {
const tx = await MockL2CrossDomainMessenger.doNothing()
await expect(
getMessagesAndProofsForL2Transaction(
l1RpcProvider,
l2RpcProvider,
StateCommitmentChain.address,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.be.rejected
})
it('should throw an error if the corresponding state batch has not been submitted', async () => {
const tx = await MockL2CrossDomainMessenger.emitSentMessageEvent({
target: ethers.constants.AddressZero,
sender: ethers.constants.AddressZero,
message: '0x',
messageNonce: 0,
})
await expect(
getMessagesAndProofsForL2Transaction(
l1RpcProvider,
l2RpcProvider,
StateCommitmentChain.address,
MockL2CrossDomainMessenger.address,
tx.hash
)
).to.be.rejected
})
// Unfortunately this is hard to test here because hardhat doesn't support eth_getProof.
// Because this function is embedded into the message relayer, we should be able to use
// integration tests to sufficiently test this.
it.skip('should otherwise return the encoded transaction data', () => {
// TODO?
})
})
})
describe('getMerkleTreeProof', () => {
let leaves: string[] = [
'the',
'quick',
'brown',
'fox',
'jumps',
'over',
'the',
'lazy',
'dog',
]
const index: number = 4
it('should generate a merkle tree proof from an odd number of leaves at the correct index', () => {
const expectedProof = [
'0x6f766572',
'0x123268ec1a3f9aac2bc68e899fe4329eefef783c76265722508b8abbfbf11440',
'0x12aaa1b2e09f26e14d86aa3b157b94cfeabe815e44b6742d00c47441a576b12d',
'0x297d90df3f77f93eefdeab4e9f6e9a074b41a3508f9d265e92e9b5449c7b11c8',
]
expect(getMerkleTreeProof(leaves, index)).to.deep.equal(expectedProof)
})
it('should generate a merkle tree proof from an even number of leaves at the correct index', () => {
const expectedProof = [
'0x6f766572',
'0x09e430fa7b513203dd9c74afd734267a73f64299d9dac61ef09e96c3b3b3fe96',
'0x12aaa1b2e09f26e14d86aa3b157b94cfeabe815e44b6742d00c47441a576b12d',
]
leaves = leaves.slice(0, leaves.length - 2)
expect(getMerkleTreeProof(leaves, index)).to.deep.equal(expectedProof)
})
})
......@@ -34,6 +34,7 @@
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"dotenv": "^10.0.0",
"ethers": "^5.5.4",
"express": "^4.17.1",
......
......@@ -6,7 +6,8 @@ import { Gauge, Histogram } from 'prom-client'
import cron from 'node-cron'
import { providers, Wallet } from 'ethers'
import { Metrics, Logger } from '@eth-optimism/common-ts'
import { injectL2Context, sleep } from '@eth-optimism/core-utils'
import { sleep } from '@eth-optimism/core-utils'
import { asL2Provider } from '@eth-optimism/sdk'
import { binarySearchForMismatch } from './helpers'
......@@ -49,7 +50,7 @@ export class HealthcheckServer {
init = () => {
this.metrics = this.initMetrics()
this.server = this.initServer()
this.replicaProvider = injectL2Context(
this.replicaProvider = asL2Provider(
new providers.StaticJsonRpcProvider({
url: this.options.replicaRpcProvider,
headers: { 'User-Agent': 'replica-healthcheck' },
......@@ -180,7 +181,7 @@ export class HealthcheckServer {
}
runSyncCheck = async () => {
const sequencerProvider = injectL2Context(
const sequencerProvider = asL2Provider(
new providers.StaticJsonRpcProvider({
url: this.options.sequencerRpcProvider,
headers: { 'User-Agent': 'replica-healthcheck' },
......
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ethers, Overrides } from 'ethers'
import { ethers, Contract, Overrides, BigNumber } from 'ethers'
import { TransactionRequest, BlockTag } from '@ethersproject/abstract-provider'
import { predeploys } from '@eth-optimism/contracts'
import { predeploys, getContractInterface } from '@eth-optimism/contracts'
import { hexStringEquals } from '@eth-optimism/core-utils'
import {
......@@ -17,6 +17,14 @@ import { StandardBridgeAdapter } from './standard-bridge'
* Bridge adapter for the ETH bridge.
*/
export class ETHBridgeAdapter extends StandardBridgeAdapter {
public async approval(
l1Token: AddressLike,
l2Token: AddressLike,
signer: ethers.Signer
): Promise<BigNumber> {
throw new Error(`approval not necessary for ETH bridge`)
}
public async getDepositsByAddress(
address: AddressLike,
opts?: {
......@@ -104,6 +112,17 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
}
populateTransaction = {
approve: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
throw new Error(`approvals not necessary for ETH bridge`)
},
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
......
......@@ -185,6 +185,38 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
}
}
public async approval(
l1Token: AddressLike,
l2Token: AddressLike,
signer: ethers.Signer
): Promise<BigNumber> {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
const token = new Contract(
toAddress(l1Token),
getContractInterface('L2StandardERC20'), // Any ERC20 will do
this.messenger.l1Provider
)
return token.allowance(await signer.getAddress(), this.l1Bridge.address)
}
public async approve(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse> {
return signer.sendTransaction(
await this.populateTransaction.approve(l1Token, l2Token, amount, opts)
)
}
public async deposit(
l1Token: AddressLike,
l2Token: AddressLike,
......@@ -217,6 +249,31 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
}
populateTransaction = {
approve: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
if (!(await this.supportsTokenPair(l1Token, l2Token))) {
throw new Error(`token pair not supported by bridge`)
}
const token = new Contract(
toAddress(l1Token),
getContractInterface('L2StandardERC20'), // Any ERC20 will do
this.messenger.l1Provider
)
return token.populateTransaction.approve(
this.l1Bridge.address,
amount,
opts?.overrides || {}
)
},
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
......@@ -288,6 +345,19 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
}
estimateGas = {
approve: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.messenger.l1Provider.estimateGas(
await this.populateTransaction.approve(l1Token, l2Token, amount, opts)
)
},
deposit: async (
l1Token: AddressLike,
l2Token: AddressLike,
......
......@@ -109,7 +109,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if (Provider.isProvider(this.l1SignerOrProvider)) {
return this.l1SignerOrProvider
} else {
return this.l1SignerOrProvider.provider
return this.l1SignerOrProvider.provider as any
}
}
......@@ -117,7 +117,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if (Provider.isProvider(this.l2SignerOrProvider)) {
return this.l2SignerOrProvider
} else {
return this.l2SignerOrProvider.provider
return this.l2SignerOrProvider.provider as any
}
}
......@@ -143,6 +143,13 @@ export class CrossChainMessenger implements ICrossChainMessenger {
direction?: MessageDirection
} = {}
): Promise<CrossChainMessage[]> {
// Wait for the transaction receipt if the input is waitable.
// TODO: Maybe worth doing this with more explicit typing but whatever for now.
if (typeof (transaction as any).wait === 'function') {
await (transaction as any).wait()
}
// Convert the input to a transaction hash.
const txHash = toTransactionHash(transaction)
let receipt: TransactionReceipt
......@@ -654,6 +661,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound)
// Only happens when no batches have been submitted yet.
if (batchEvent === null) {
return null
}
if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null
......@@ -832,6 +844,36 @@ export class CrossChainMessenger implements ICrossChainMessenger {
)
}
public async approval(
l1Token: AddressLike,
l2Token: AddressLike,
opts?: {
signer?: Signer
}
): Promise<BigNumber> {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.approval(l1Token, l2Token, opts?.signer || this.l1Signer)
}
public async approveERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse> {
return (opts?.signer || this.l1Signer).sendTransaction(
await this.populateTransaction.approveERC20(
l1Token,
l2Token,
amount,
opts
)
)
}
public async depositERC20(
l1Token: AddressLike,
l2Token: AddressLike,
......@@ -974,6 +1016,18 @@ export class CrossChainMessenger implements ICrossChainMessenger {
)
},
approveERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest> => {
const bridge = await this.getBridgeForTokenPair(l1Token, l2Token)
return bridge.populateTransaction.approve(l1Token, l2Token, amount, opts)
},
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
......@@ -1070,6 +1124,24 @@ export class CrossChainMessenger implements ICrossChainMessenger {
)
},
approveERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.approveERC20(
l1Token,
l2Token,
amount,
opts
)
)
},
depositERC20: async (
l1Token: AddressLike,
l2Token: AddressLike,
......
......@@ -78,6 +78,41 @@ export interface IBridgeAdapter {
l2Token: AddressLike
): Promise<boolean>
/**
* Queries the account's approval amount for a given L1 token.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param signer Signer to query the approval for.
* @returns Amount of tokens approved for deposits from the account.
*/
approval(
l1Token: AddressLike,
l2Token: AddressLike,
signer: Signer
): Promise<BigNumber>
/**
* Approves a deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param signer Signer used to sign and send the transaction.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approve(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
signer: Signer,
opts?: {
overrides?: Overrides
}
): Promise<TransactionResponse>
/**
* Deposits some tokens into the L2 chain.
*
......@@ -131,6 +166,25 @@ export interface IBridgeAdapter {
* Follows the pattern used by ethers.js.
*/
populateTransaction: {
/**
* Generates a transaction for approving some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
approve(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest>
/**
* Generates a transaction for depositing some tokens into the L2 chain.
*
......@@ -181,6 +235,25 @@ export interface IBridgeAdapter {
* Follows the pattern used by ethers.js.
*/
estimateGas: {
/**
* Estimates gas required to approve some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Gas estimate for the transaction.
*/
approve(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber>
/**
* Estimates gas required to deposit some tokens into the L2 chain.
*
......
......@@ -411,6 +411,44 @@ export interface ICrossChainMessenger {
}
): Promise<TransactionResponse>
/**
* Queries the account's approval amount for a given L1 token.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param opts Additional options.
* @param opts.signer Optional signer to get the approval for.
* @returns Amount of tokens approved for deposits from the account.
*/
approval(
l1Token: AddressLike,
l2Token: AddressLike,
opts?: {
signer?: Signer
}
): Promise<BigNumber>
/**
* Approves a deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.signer Optional signer to use to send the transaction.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approveERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
signer?: Signer
overrides?: Overrides
}
): Promise<TransactionResponse>
/**
* Deposits some ERC20 tokens into the L2 chain.
*
......@@ -517,6 +555,25 @@ export interface ICrossChainMessenger {
}
): Promise<TransactionRequest>
/**
* Generates a transaction for approving some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approveERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<TransactionRequest>
/**
* Generates a transaction for depositing some ETH into the L2 chain.
*
......@@ -652,6 +709,25 @@ export interface ICrossChainMessenger {
}
): Promise<BigNumber>
/**
* Estimates gas required to approve some tokens to deposit into the L2 chain.
*
* @param l1Token The L1 token address.
* @param l2Token The L2 token address.
* @param amount Amount of the token to approve.
* @param opts Additional options.
* @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the approval transaction.
*/
approveERC20(
l1Token: AddressLike,
l2Token: AddressLike,
amount: NumberLike,
opts?: {
overrides?: Overrides
}
): Promise<BigNumber>
/**
* Estimates gas required to deposit some ETH into the L2 chain.
*
......
......@@ -2311,7 +2311,7 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@nomiclabs/ethereumjs-vm@^4":
"@nomiclabs/ethereumjs-vm@^4.2.2":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc"
integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q==
......
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