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: ...@@ -30,5 +30,8 @@ M-core-utils:
M-dtl: M-dtl:
- any: ['packages/data-transport-layer/**/*'] - any: ['packages/data-transport-layer/**/*']
M-sdk:
- any: ['packages/sdk/**/*']
M-ops: M-ops:
- any: ['ops/**/*'] - any: ['ops/**/*']
...@@ -11,7 +11,7 @@ on: ...@@ -11,7 +11,7 @@ on:
- 'regenesis/*' - 'regenesis/*'
pull_request: pull_request:
paths: paths:
- 'go/batch-submitter/*' - 'go/batch-submitter/**'
workflow_dispatch: workflow_dispatch:
defaults: defaults:
......
...@@ -11,7 +11,7 @@ on: ...@@ -11,7 +11,7 @@ on:
- 'regenesis/*' - 'regenesis/*'
pull_request: pull_request:
paths: paths:
- 'go/bss-core/*' - 'go/bss-core/**'
workflow_dispatch: workflow_dispatch:
defaults: 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 ...@@ -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/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/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="./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="./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="./integration-tests">integration-tests</a>: Various integration tests for the Optimism network
└── <a href="./ops">ops</a>: Tools for running Optimism nodes and networks └── <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 ...@@ -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. 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. 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 ### Release candidate branches
Branches marked `regenesis/X.X.X` are **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 ...@@ -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. 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. 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 ## 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). 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 { ...@@ -108,9 +108,10 @@ func Main(gitVersion string) func(ctx *cli.Context) error {
} }
txManagerConfig := txmgr.Config{ txManagerConfig := txmgr.Config{
ResubmissionTimeout: cfg.ResubmissionTimeout, ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second, ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
} }
var services []*bsscore.Service var services []*bsscore.Service
......
...@@ -89,6 +89,11 @@ type Config struct { ...@@ -89,6 +89,11 @@ type Config struct {
// appending new batches. // appending new batches.
NumConfirmations uint64 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 // ResubmissionTimeout is time we will wait before resubmitting a
// transaction. // transaction.
ResubmissionTimeout time.Duration ResubmissionTimeout time.Duration
...@@ -178,22 +183,23 @@ type Config struct { ...@@ -178,22 +183,23 @@ type Config struct {
func NewConfig(ctx *cli.Context) (Config, error) { func NewConfig(ctx *cli.Context) (Config, error) {
cfg := Config{ cfg := Config{
/* Required Flags */ /* Required Flags */
BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name), BuildEnv: ctx.GlobalString(flags.BuildEnvFlag.Name),
EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name), EthNetworkName: ctx.GlobalString(flags.EthNetworkNameFlag.Name),
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name), L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name), L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name), CTCAddress: ctx.GlobalString(flags.CTCAddressFlag.Name),
SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name), SCCAddress: ctx.GlobalString(flags.SCCAddressFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name), MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeFlag.Name),
MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name), MaxBatchSubmissionTime: ctx.GlobalDuration(flags.MaxBatchSubmissionTimeFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name), PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
NumConfirmations: ctx.GlobalUint64(flags.NumConfirmationsFlag.Name), NumConfirmations: ctx.GlobalUint64(flags.NumConfirmationsFlag.Name),
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name), SafeAbortNonceTooLowCount: ctx.GlobalUint64(flags.SafeAbortNonceTooLowCountFlag.Name),
FinalityConfirmations: ctx.GlobalUint64(flags.FinalityConfirmationsFlag.Name), ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
RunTxBatchSubmitter: ctx.GlobalBool(flags.RunTxBatchSubmitterFlag.Name), FinalityConfirmations: ctx.GlobalUint64(flags.FinalityConfirmationsFlag.Name),
RunStateBatchSubmitter: ctx.GlobalBool(flags.RunStateBatchSubmitterFlag.Name), RunTxBatchSubmitter: ctx.GlobalBool(flags.RunTxBatchSubmitterFlag.Name),
SafeMinimumEtherBalance: ctx.GlobalUint64(flags.SafeMinimumEtherBalanceFlag.Name), RunStateBatchSubmitter: ctx.GlobalBool(flags.RunStateBatchSubmitterFlag.Name),
ClearPendingTxs: ctx.GlobalBool(flags.ClearPendingTxsFlag.Name), SafeMinimumEtherBalance: ctx.GlobalUint64(flags.SafeMinimumEtherBalanceFlag.Name),
ClearPendingTxs: ctx.GlobalBool(flags.ClearPendingTxsFlag.Name),
/* Optional Flags */ /* Optional Flags */
LogLevel: ctx.GlobalString(flags.LogLevelFlag.Name), LogLevel: ctx.GlobalString(flags.LogLevelFlag.Name),
LogTerminal: ctx.GlobalBool(flags.LogTerminalFlag.Name), LogTerminal: ctx.GlobalBool(flags.LogTerminalFlag.Name),
......
...@@ -227,10 +227,11 @@ func (d *Driver) CraftBatchTx( ...@@ -227,10 +227,11 @@ func (d *Driver) CraftBatchTx(
} }
} }
// SubmitBatchTx using the passed transaction as a template, signs and // UpdateGasPrice signs an otherwise identical txn to the one provided but with
// publishes the transaction unmodified apart from sampling the current gas // updated gas prices sampled from the existing network conditions.
// price. The final transaction is returned to the caller. //
func (d *Driver) SubmitBatchTx( // NOTE: Thie method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
...@@ -243,6 +244,7 @@ func (d *Driver) SubmitBatchTx( ...@@ -243,6 +244,7 @@ func (d *Driver) SubmitBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce()) opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.NoSend = true
finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data()) finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data())
switch { switch {
...@@ -265,3 +267,12 @@ func (d *Driver) SubmitBatchTx( ...@@ -265,3 +267,12 @@ func (d *Driver) SubmitBatchTx(
return nil, err 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( ...@@ -257,10 +257,11 @@ func (d *Driver) CraftBatchTx(
} }
} }
// SubmitBatchTx using the passed transaction as a template, signs and publishes // UpdateGasPrice signs an otherwise identical txn to the one provided but with
// the transaction unmodified apart from sampling the current gas price. The // updated gas prices sampled from the existing network conditions.
// final transaction is returned to the caller. //
func (d *Driver) SubmitBatchTx( // NOTE: Thie method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
...@@ -273,6 +274,7 @@ func (d *Driver) SubmitBatchTx( ...@@ -273,6 +274,7 @@ func (d *Driver) SubmitBatchTx(
} }
opts.Context = ctx opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce()) opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
opts.NoSend = true
finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data()) finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data())
switch { switch {
...@@ -295,3 +297,12 @@ func (d *Driver) SubmitBatchTx( ...@@ -295,3 +297,12 @@ func (d *Driver) SubmitBatchTx(
return nil, err 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 ( ...@@ -80,6 +80,14 @@ var (
Required: true, Required: true,
EnvVar: prefixEnvVar("NUM_CONFIRMATIONS"), 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{ ResubmissionTimeoutFlag = cli.DurationFlag{
Name: "resubmission-timeout", Name: "resubmission-timeout",
Usage: "Duration we will wait before resubmitting a " + Usage: "Duration we will wait before resubmitting a " +
...@@ -221,6 +229,7 @@ var requiredFlags = []cli.Flag{ ...@@ -221,6 +229,7 @@ var requiredFlags = []cli.Flag{
MaxBatchSubmissionTimeFlag, MaxBatchSubmissionTimeFlag,
PollIntervalFlag, PollIntervalFlag,
NumConfirmationsFlag, NumConfirmationsFlag,
SafeAbortNonceTooLowCountFlag,
ResubmissionTimeoutFlag, ResubmissionTimeoutFlag,
FinalityConfirmationsFlag, FinalityConfirmationsFlag,
RunTxBatchSubmitterFlag, RunTxBatchSubmitterFlag,
......
...@@ -48,7 +48,7 @@ func ClearPendingTx( ...@@ -48,7 +48,7 @@ func ClearPendingTx(
// Construct the clearing transaction submission clousure that will attempt // Construct the clearing transaction submission clousure that will attempt
// to send the a clearing transaction transaction at the given nonce and gas // to send the a clearing transaction transaction at the given nonce and gas
// price. // price.
sendTx := func( updateGasPrice := func(
ctx context.Context, ctx context.Context,
) (*types.Transaction, error) { ) (*types.Transaction, error) {
log.Info(name+" clearing pending tx", "nonce", nonce) log.Info(name+" clearing pending tx", "nonce", nonce)
...@@ -61,11 +61,16 @@ func ClearPendingTx( ...@@ -61,11 +61,16 @@ func ClearPendingTx(
"err", err) "err", err)
return nil, 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 { switch {
// Clearing transaction successfully confirmed. // Clearing transaction successfully confirmed.
...@@ -74,7 +79,7 @@ func ClearPendingTx( ...@@ -74,7 +79,7 @@ func ClearPendingTx(
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash) "txHash", txHash)
return signedTx, nil return nil
// Getting a nonce too low error implies that a previous transaction in // Getting a nonce too low error implies that a previous transaction in
// the mempool has confirmed and we should abort trying to publish at // the mempool has confirmed and we should abort trying to publish at
...@@ -83,7 +88,7 @@ func ClearPendingTx( ...@@ -83,7 +88,7 @@ func ClearPendingTx(
log.Info(name + " transaction from previous restart confirmed, " + log.Info(name + " transaction from previous restart confirmed, " +
"aborting mempool clearing") "aborting mempool clearing")
cancel() cancel()
return nil, context.Canceled return context.Canceled
// An unexpected error occurred. This also handles the case where the // An unexpected error occurred. This also handles the case where the
// clearing transaction has not yet bested the gas price a prior // clearing transaction has not yet bested the gas price a prior
...@@ -94,11 +99,11 @@ func ClearPendingTx( ...@@ -94,11 +99,11 @@ func ClearPendingTx(
log.Error(name+" unable to submit clearing tx", log.Error(name+" unable to submit clearing tx",
"nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
"txHash", txHash, "err", err) "txHash", txHash, "err", err)
return nil, err return err
} }
} }
receipt, err := txMgr.Send(ctx, sendTx) receipt, err := txMgr.Send(ctx, updateGasPrice, sendTx)
switch { switch {
// If the current context is canceled, a prior transaction in the mempool // If the current context is canceled, a prior transaction in the mempool
......
...@@ -204,9 +204,10 @@ func newClearPendingTxHarnessWithNumConfs( ...@@ -204,9 +204,10 @@ func newClearPendingTxHarnessWithNumConfs(
l1Client := mock.NewL1Client(l1ClientConfig) l1Client := mock.NewL1Client(l1ClientConfig)
txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{ txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{
ResubmissionTimeout: time.Second, ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations, NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3,
}, l1Client) }, l1Client)
return &clearPendingTxHarness{ return &clearPendingTxHarness{
......
...@@ -54,13 +54,18 @@ type Driver interface { ...@@ -54,13 +54,18 @@ type Driver interface {
start, end, nonce *big.Int, start, end, nonce *big.Int,
) (*types.Transaction, error) ) (*types.Transaction, error)
// SubmitBatchTx using the passed transaction as a template, signs and // UpdateGasPrice signs an otherwise identical txn to the one provided but
// publishes the transaction unmodified apart from sampling the current gas // with updated gas prices sampled from the existing network conditions.
// price. The final transaction is returned to the caller. //
SubmitBatchTx( // NOTE: Thie method SHOULD NOT publish the resulting transaction.
UpdateGasPrice(
ctx context.Context, ctx context.Context,
tx *types.Transaction, tx *types.Transaction,
) (*types.Transaction, error) ) (*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 { type ServiceConfig struct {
...@@ -193,30 +198,19 @@ func (s *Service) eventLoop() { ...@@ -193,30 +198,19 @@ func (s *Service) eventLoop() {
// Construct the transaction submission clousure that will attempt // Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price. // to send the next transaction at the given nonce and gas price.
sendTx := func(ctx context.Context) (*types.Transaction, error) { updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" attempting batch tx", "start", start, log.Info(name+" updating batch tx gas price", "start", start,
"end", end, "nonce", nonce) "end", end, "nonce", nonce)
tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx) return s.cfg.Driver.UpdateGasPrice(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
} }
// Wait until one of our submitted transactions confirms. If no // Wait until one of our submitted transactions confirms. If no
// receipt is received it's likely our gas price was too low. // receipt is received it's likely our gas price was too low.
batchConfirmationStart := time.Now() 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 { if err != nil {
log.Error(name+" unable to publish batch tx", log.Error(name+" unable to publish batch tx",
"err", err) "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 ( ...@@ -8,15 +8,16 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// SendTxFunc defines a function signature for publishing a desired tx with a // UpdateGasPriceSendTxFunc defines a function signature for publishing a
// specific gas price. Implementations of this signature should also return // desired tx with a specific gas price. Implementations of this signature
// promptly when the context is canceled. // should also return promptly when the context is canceled.
type SendTxFunc = func(ctx context.Context) (*types.Transaction, error) 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. // Config houses parameters for altering the behavior of a SimpleTxManager.
type Config struct { type Config struct {
...@@ -37,6 +38,11 @@ type Config struct { ...@@ -37,6 +38,11 @@ type Config struct {
// NumConfirmations specifies how many blocks are need to consider a // NumConfirmations specifies how many blocks are need to consider a
// transaction confirmed. // transaction confirmed.
NumConfirmations uint64 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, // TxManager is an interface that allows callers to reliably publish txs,
...@@ -48,7 +54,11 @@ type TxManager interface { ...@@ -48,7 +54,11 @@ type TxManager interface {
// prices). The method may be canceled using the passed context. // prices). The method may be canceled using the passed context.
// //
// NOTE: Send should be called by AT MOST one caller at a time. // 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 // ReceiptSource is a minimal function signature used to detect the confirmation
...@@ -96,7 +106,10 @@ func NewSimpleTxManager( ...@@ -96,7 +106,10 @@ func NewSimpleTxManager(
// //
// NOTE: Send should be called by AT MOST one caller at a time. // NOTE: Send should be called by AT MOST one caller at a time.
func (m *SimpleTxManager) Send( 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 name := m.name
...@@ -112,6 +125,8 @@ func (m *SimpleTxManager) Send( ...@@ -112,6 +125,8 @@ func (m *SimpleTxManager) Send(
ctxc, cancel := context.WithCancel(ctx) ctxc, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
sendState := NewSendState(m.cfg.SafeAbortNonceTooLowCount)
// Create a closure that will block on passed sendTx function in the // Create a closure that will block on passed sendTx function in the
// background, returning the first successfully mined receipt back to // background, returning the first successfully mined receipt back to
// the main event loop via receiptChan. // the main event loop via receiptChan.
...@@ -119,36 +134,52 @@ func (m *SimpleTxManager) Send( ...@@ -119,36 +134,52 @@ func (m *SimpleTxManager) Send(
sendTxAsync := func() { sendTxAsync := func() {
defer wg.Done() 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. // Sign and publish transaction with current gas price.
tx, err := sendTx(ctxc) err = sendTx(ctxc, tx)
sendState.ProcessSendError(err)
if err != nil { if err != nil {
if err == context.Canceled || if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") { strings.Contains(err.Error(), "context canceled") {
return return
} }
log.Error(name+" unable to publish transaction", "err", err) log.Error(name+" unable to publish transaction", "err", err)
if shouldAbortImmediately(err) { if sendState.ShouldAbortImmediately() {
cancel() cancel()
} }
// TODO(conner): add retry? // TODO(conner): add retry?
return return
} }
txHash := tx.Hash()
gasTipCap := tx.GasTipCap()
gasFeeCap := tx.GasFeeCap()
log.Info(name+" transaction published successfully", "hash", txHash, 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 // Wait for the transaction to be mined, reporting the receipt
// back to the main event loop if found. // back to the main event loop if found.
receipt, err := WaitMined( receipt, err := waitMined(
ctxc, m.backend, tx, m.cfg.ReceiptQueryInterval, ctxc, m.backend, tx, m.cfg.ReceiptQueryInterval,
m.cfg.NumConfirmations, m.cfg.NumConfirmations, sendState,
) )
if err != nil { if err != nil {
log.Debug(name+" send tx failed", "hash", txHash, 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 { if receipt != nil {
// Use non-blocking select to ensure function can exit // Use non-blocking select to ensure function can exit
...@@ -156,7 +187,8 @@ func (m *SimpleTxManager) Send( ...@@ -156,7 +187,8 @@ func (m *SimpleTxManager) Send(
select { select {
case receiptChan <- receipt: case receiptChan <- receipt:
log.Trace(name+" send tx succeeded", "hash", txHash, log.Trace(name+" send tx succeeded", "hash", txHash,
"gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap) "nonce", nonce, "gasTipCap", gasTipCap,
"gasFeeCap", gasFeeCap)
default: default:
} }
} }
...@@ -174,6 +206,14 @@ func (m *SimpleTxManager) Send( ...@@ -174,6 +206,14 @@ func (m *SimpleTxManager) Send(
// Whenever a resubmission timeout has elapsed, bump the gas // Whenever a resubmission timeout has elapsed, bump the gas
// price and publish a new transaction. // price and publish a new transaction.
case <-time.After(m.cfg.ResubmissionTimeout): 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. // Submit and wait for the bumped traction to confirm.
wg.Add(1) wg.Add(1)
go sendTxAsync() go sendTxAsync()
...@@ -190,13 +230,6 @@ func (m *SimpleTxManager) Send( ...@@ -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 // WaitMined blocks until the backend indicates confirmation of tx and returns
// the tx receipt. Queries are made every queryInterval, regardless of whether // the tx receipt. Queries are made every queryInterval, regardless of whether
// the backend returns an error. This method can be canceled using the passed // the backend returns an error. This method can be canceled using the passed
...@@ -208,6 +241,19 @@ func WaitMined( ...@@ -208,6 +241,19 @@ func WaitMined(
queryInterval time.Duration, queryInterval time.Duration,
numConfirmations uint64, numConfirmations uint64,
) (*types.Receipt, error) { ) (*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) queryTicker := time.NewTicker(queryInterval)
defer queryTicker.Stop() defer queryTicker.Stop()
...@@ -218,6 +264,10 @@ func WaitMined( ...@@ -218,6 +264,10 @@ func WaitMined(
receipt, err := backend.TransactionReceipt(ctx, txHash) receipt, err := backend.TransactionReceipt(ctx, txHash)
switch { switch {
case receipt != nil: case receipt != nil:
if sendState != nil {
sendState.TxMined(txHash)
}
txHeight := receipt.BlockNumber.Uint64() txHeight := receipt.BlockNumber.Uint64()
tipHeight, err := backend.BlockNumber(ctx) tipHeight, err := backend.BlockNumber(ctx)
if err != nil { if err != nil {
...@@ -252,6 +302,9 @@ func WaitMined( ...@@ -252,6 +302,9 @@ func WaitMined(
"err", err) "err", err)
default: default:
if sendState != nil {
sendState.TxNotMined(txHash)
}
log.Trace("Transaction not yet mined", "hash", txHash) log.Trace("Transaction not yet mined", "hash", txHash)
} }
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/go/bss-core/txmgr" "github.com/ethereum-optimism/optimism/go/bss-core/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -44,9 +45,10 @@ func newTestHarness() *testHarness { ...@@ -44,9 +45,10 @@ func newTestHarness() *testHarness {
func configWithNumConfs(numConfirmations uint64) txmgr.Config { func configWithNumConfs(numConfirmations uint64) txmgr.Config {
return txmgr.Config{ return txmgr.Config{
ResubmissionTimeout: time.Second, ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond, ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations, NumConfirmations: numConfirmations,
SafeAbortNonceTooLowCount: 3,
} }
} }
...@@ -71,6 +73,10 @@ func (g *gasPricer) expGasFeeCap() *big.Int { ...@@ -71,6 +73,10 @@ func (g *gasPricer) expGasFeeCap() *big.Int {
return gasFeeCap 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) { func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch)) epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch))
epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, 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) { ...@@ -79,15 +85,14 @@ func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
return epochGasTipCap, epochGasFeeCap return epochGasTipCap, epochGasFeeCap
} }
func (g *gasPricer) sample() (*big.Int, *big.Int, bool) { func (g *gasPricer) sample() (*big.Int, *big.Int) {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
g.epoch++ g.epoch++
epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch) epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch)
shouldMine := g.epoch == g.mineAtEpoch
return epochGasTipCap, epochGasFeeCap, shouldMine return epochGasTipCap, epochGasFeeCap
} }
type minedTxInfo struct { type minedTxInfo struct {
...@@ -171,23 +176,29 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) { ...@@ -171,23 +176,29 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
h := newTestHarness() h := newTestHarness()
gasFeeCap := big.NewInt(5) gasPricer := newGasPricer(1)
sendTxFunc := func(
ctx context.Context, updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
) (*types.Transaction, error) { gasTipCap, gasFeeCap := gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{ return types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap, GasFeeCap: gasFeeCap,
}) }), nil
txHash := tx.Hash() }
h.backend.mine(&txHash, gasFeeCap)
return tx, 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() ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) 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 // TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no
...@@ -198,19 +209,23 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) { ...@@ -198,19 +209,23 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context, gasTipCap, gasFeeCap := h.gasPricer.sample()
) (*types.Transaction, error) {
// Don't publish tx to backend, simulating never being mined.
return types.NewTx(&types.DynamicFeeTx{ return types.NewTx(&types.DynamicFeeTx{
GasFeeCap: big.NewInt(5), GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil }), 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) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Equal(t, err, context.DeadlineExceeded) require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt) require.Nil(t, receipt)
} }
...@@ -222,23 +237,24 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) { ...@@ -222,23 +237,24 @@ func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context, gasTipCap, gasFeeCap := h.gasPricer.sample()
) (*types.Transaction, error) { return types.NewTx(&types.DynamicFeeTx{
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap, GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap, GasFeeCap: gasFeeCap,
}) }), nil
if shouldMine { }
sendTx := func(ctx context.Context, tx *types.Transaction) error {
if h.gasPricer.shouldMine(tx.GasFeeCap()) {
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap) h.backend.mine(&txHash, tx.GasFeeCap())
} }
return tx, nil return nil
} }
ctx := context.Background() ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
...@@ -255,16 +271,22 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) { ...@@ -255,16 +271,22 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context, gasTipCap, gasFeeCap := h.gasPricer.sample()
) (*types.Transaction, error) { return types.NewTx(&types.DynamicFeeTx{
return nil, errRpcFailure 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) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Equal(t, err, context.DeadlineExceeded) require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt) require.Nil(t, receipt)
} }
...@@ -277,27 +299,27 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) { ...@@ -277,27 +299,27 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context, gasTipCap, gasFeeCap := h.gasPricer.sample()
) (*types.Transaction, error) { return types.NewTx(&types.DynamicFeeTx{
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample() GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
}), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Fail all but the final attempt. // Fail all but the final attempt.
if !shouldMine { if !h.gasPricer.shouldMine(tx.GasFeeCap()) {
return nil, errRpcFailure return errRpcFailure
} }
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
})
txHash := tx.Hash() txHash := tx.Hash()
h.backend.mine(&txHash, gasFeeCap) h.backend.mine(&txHash, tx.GasFeeCap())
return tx, nil return nil
} }
ctx := context.Background() ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
...@@ -312,26 +334,72 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) { ...@@ -312,26 +334,72 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
h := newTestHarness() h := newTestHarness()
sendTxFunc := func( updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
ctx context.Context, gasTipCap, gasFeeCap := h.gasPricer.sample()
) (*types.Transaction, error) { return types.NewTx(&types.DynamicFeeTx{
gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
tx := types.NewTx(&types.DynamicFeeTx{
GasTipCap: gasTipCap, GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap, GasFeeCap: gasFeeCap,
}) }), nil
}
sendTx := func(ctx context.Context, tx *types.Transaction) error {
// Delay mining the tx with the min gas price. // Delay mining the tx with the min gas price.
if shouldMine { if h.gasPricer.shouldMine(tx.GasFeeCap()) {
time.AfterFunc(5*time.Second, func() { time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash() 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() ctx := context.Background()
receipt, err := h.mgr.Send(ctx, sendTxFunc) receipt, err := h.mgr.Send(ctx, updateGasPrice, sendTx)
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, receipt) require.NotNil(t, receipt)
require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed) 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
)
This diff is collapsed.
...@@ -30,7 +30,6 @@ ...@@ -30,7 +30,6 @@
"devDependencies": { "devDependencies": {
"@eth-optimism/contracts": "0.5.14", "@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7", "@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/message-relayer": "0.2.18",
"@eth-optimism/sdk": "0.2.2", "@eth-optimism/sdk": "0.2.2",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3", "@ethersproject/providers": "^5.5.3",
......
/* Imports: External */ /* Imports: External */
import { Contract, utils, Wallet, providers } from 'ethers' import { Contract, utils, Wallet, providers, Transaction } from 'ethers'
import { TransactionResponse } from '@ethersproject/providers' import {
TransactionResponse,
TransactionReceipt,
} from '@ethersproject/providers'
import { getContractFactory, predeploys } from '@eth-optimism/contracts' import { getContractFactory, predeploys } from '@eth-optimism/contracts'
import { sleep } from '@eth-optimism/core-utils' import { sleep } from '@eth-optimism/core-utils'
import { getMessagesAndProofsForL2Transaction } from '@eth-optimism/message-relayer' import {
import { CrossChainMessenger } from '@eth-optimism/sdk' CrossChainMessenger,
MessageStatus,
MessageDirection,
} from '@eth-optimism/sdk'
/* Imports: Internal */ /* Imports: Internal */
import { import {
...@@ -21,12 +27,14 @@ import { ...@@ -21,12 +27,14 @@ import {
getL1Bridge, getL1Bridge,
getL2Bridge, getL2Bridge,
envConfig, envConfig,
DEFAULT_TEST_GAS_L1,
} from './utils' } from './utils'
import {
CrossDomainMessagePair, export interface CrossDomainMessagePair {
waitForXDomainTransaction, tx: Transaction
} from './watcher-utils' receipt: TransactionReceipt
remoteTx: Transaction
remoteReceipt: TransactionReceipt
}
/// Helper class for instantiating a test environment with a funded account /// Helper class for instantiating a test environment with a funded account
export class OptimismEnv { export class OptimismEnv {
...@@ -170,7 +178,32 @@ export class OptimismEnv { ...@@ -170,7 +178,32 @@ export class OptimismEnv {
async waitForXDomainTransaction( async waitForXDomainTransaction(
tx: Promise<TransactionResponse> | TransactionResponse tx: Promise<TransactionResponse> | TransactionResponse
): Promise<CrossDomainMessagePair> { ): 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 { ...@@ -184,67 +217,48 @@ export class OptimismEnv {
tx = await tx tx = await tx
await tx.wait() await tx.wait()
let messagePairs = [] const messages = await this.messenger.getMessagesByTransaction(tx)
while (true) { if (messages.length === 0) {
try { return
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
}
}
} }
for (const { message, proof } of messagePairs) { for (const message of messages) {
while (true) { 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 { try {
const result = await this.l1Messenger await this.messenger.finalizeMessage(message)
.connect(this.l1Wallet) relayed = true
.relayMessage(
message.target,
message.sender,
message.message,
message.messageNonce,
proof,
{
gasLimit: DEFAULT_TEST_GAS_L1 * 10,
}
)
await result.wait()
break
} catch (err) { } catch (err) {
if (err.message.includes('execution failed due to an exception')) { if (
await sleep(5000) err.message.includes('Nonce too low') ||
} else if (err.message.includes('Nonce too low')) { err.message.includes('transaction was replaced') ||
await sleep(5000)
} else if (err.message.includes('transaction was replaced')) {
// this happens when we run tests in parallel
await sleep(5000)
} else if (
err.message.includes( err.message.includes(
'another transaction with same nonce in the queue' '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) await sleep(5000)
} else if ( } else if (
err.message.includes('message has already been received') err.message.includes('message has already been received')
) { ) {
break // Message already relayed, this is fine.
relayed = true
} else { } else {
throw err 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 { ...@@ -642,7 +642,7 @@ func hashish(x string) bool {
func fetchGenesis(url string) ([]byte, error) { func fetchGenesis(url string) ([]byte, error) {
client := &http.Client{ client := &http.Client{
Timeout: 10 * time.Second, Timeout: 60 * time.Second,
} }
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
......
...@@ -34,7 +34,7 @@ docker-compose \ ...@@ -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. 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. To start the stack with monitoring enabled, just add the metric composition file.
``` ```
...@@ -46,8 +46,9 @@ docker-compose \ ...@@ -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! 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.yml \
-f docker-compose.ts-batch-submitter.yml \ -f docker-compose.ts-batch-submitter.yml \
up --scale \ up --scale \
...@@ -55,7 +56,6 @@ docker-compose ...@@ -55,7 +56,6 @@ docker-compose
--build --detach --build --detach
``` ```
A Makefile has been provided for convience. The following targets are available. A Makefile has been provided for convience. The following targets are available.
- make up - make up
- make down - make down
......
...@@ -8,6 +8,7 @@ BATCH_SUBMITTER_MAX_L1_TX_SIZE=90000 ...@@ -8,6 +8,7 @@ BATCH_SUBMITTER_MAX_L1_TX_SIZE=90000
BATCH_SUBMITTER_MAX_BATCH_SUBMISSION_TIME=0 BATCH_SUBMITTER_MAX_BATCH_SUBMISSION_TIME=0
BATCH_SUBMITTER_POLL_INTERVAL=500ms BATCH_SUBMITTER_POLL_INTERVAL=500ms
BATCH_SUBMITTER_NUM_CONFIRMATIONS=1 BATCH_SUBMITTER_NUM_CONFIRMATIONS=1
BATCH_SUBMITTER_SAFE_ABORT_NONCE_TOO_LOW_COUNT=3
BATCH_SUBMITTER_RESUBMISSION_TIMEOUT=1s BATCH_SUBMITTER_RESUBMISSION_TIMEOUT=1s
BATCH_SUBMITTER_FINALITY_CONFIRMATIONS=0 BATCH_SUBMITTER_FINALITY_CONFIRMATIONS=0
BATCH_SUBMITTER_RUN_TX_BATCH_SUBMITTER=true BATCH_SUBMITTER_RUN_TX_BATCH_SUBMITTER=true
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.14", "@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7", "@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"@eth-optimism/ynatm": "^0.2.2", "@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.5.1", "@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.5.3", "@ethersproject/providers": "^5.5.3",
......
/* External Imports */ /* External Imports */
import { exit } from 'process' 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 * as Sentry from '@sentry/node'
import { Logger, Metrics, createMetricsServer } from '@eth-optimism/common-ts' import { Logger, Metrics, createMetricsServer } from '@eth-optimism/common-ts'
import { Signer, Wallet } from 'ethers' import { Signer, Wallet } from 'ethers'
...@@ -346,7 +347,7 @@ export const run = async () => { ...@@ -346,7 +347,7 @@ export const run = async () => {
const clearPendingTxs = requiredEnvVars.CLEAR_PENDING_TXS const clearPendingTxs = requiredEnvVars.CLEAR_PENDING_TXS
const l2Provider = injectL2Context( const l2Provider = asL2Provider(
new StaticJsonRpcProvider({ new StaticJsonRpcProvider({
url: requiredEnvVars.L2_NODE_WEB3_URL, url: requiredEnvVars.L2_NODE_WEB3_URL,
headers: { 'User-Agent': 'batch-submitter' }, 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 @@ ...@@ -67,6 +67,7 @@
"@codechecks/client": "^0.1.11", "@codechecks/client": "^0.1.11",
"@defi-wonderland/smock": "^2.0.2", "@defi-wonderland/smock": "^2.0.2",
"@eth-optimism/smock": "1.1.10", "@eth-optimism/smock": "1.1.10",
"@nomiclabs/ethereumjs-vm": "^4.2.2",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-etherscan": "^2.1.6", "@nomiclabs/hardhat-etherscan": "^2.1.6",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@nomiclabs/hardhat-waffle": "^2.0.1",
......
...@@ -53,9 +53,18 @@ export const deployAndVerifyAndThen = async ({ ...@@ -53,9 +53,18 @@ export const deployAndVerifyAndThen = async ({
address: result.address, address: result.address,
constructorArguments: args, constructorArguments: args,
}) })
console.log('Successfully verified') console.log('Successfully verified on Etherscan')
} catch (error) { } 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) 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 @@ ...@@ -37,12 +37,10 @@
"@ethersproject/providers": "^5.5.3", "@ethersproject/providers": "^5.5.3",
"@ethersproject/web": "^5.5.1", "@ethersproject/web": "^5.5.1",
"chai": "^4.3.4", "chai": "^4.3.4",
"ethers": "^5.5.4", "ethers": "^5.5.4"
"lodash": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/lodash": "^4.14.168",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
"@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0", "@typescript-eslint/parser": "^4.26.0",
......
...@@ -5,6 +5,4 @@ ...@@ -5,6 +5,4 @@
export * from './alias' export * from './alias'
export * from './batch-encoding' export * from './batch-encoding'
export * from './fees' export * from './fees'
export * from './l2context'
export * from './rollup-types' 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 */ /* 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 { BaseService, Metrics } from '@eth-optimism/common-ts'
import { TypedEvent } from '@eth-optimism/contracts/dist/types/common' import { TypedEvent } from '@eth-optimism/contracts/dist/types/common'
import { BaseProvider } from '@ethersproject/providers' import { BaseProvider } from '@ethersproject/providers'
...@@ -15,7 +19,6 @@ import { MissingElementError } from './handlers/errors' ...@@ -15,7 +19,6 @@ import { MissingElementError } from './handlers/errors'
import { TransportDB } from '../../db/transport-db' import { TransportDB } from '../../db/transport-db'
import { import {
OptimismContracts, OptimismContracts,
sleep,
loadOptimismContracts, loadOptimismContracts,
loadContract, loadContract,
validators, validators,
......
/* Imports: External */ /* Imports: External */
import { BigNumber, ethers } from 'ethers' import { BigNumber, ethers } from 'ethers'
import { serialize } from '@ethersproject/transactions' import { serialize } from '@ethersproject/transactions'
import { padHexString } from '@eth-optimism/core-utils'
/* Imports: Internal */ /* Imports: Internal */
import { TransportDB } from '../../../db/transport-db' import { TransportDB } from '../../../db/transport-db'
...@@ -9,7 +10,7 @@ import { ...@@ -9,7 +10,7 @@ import {
StateRootEntry, StateRootEntry,
TransactionEntry, TransactionEntry,
} from '../../../types' } from '../../../types'
import { padHexString, parseSignatureVParam } from '../../../utils' import { parseSignatureVParam } from '../../../utils'
export const handleSequencerBlock = { export const handleSequencerBlock = {
parseBlock: async ( parseBlock: async (
......
/* Imports: External */ /* Imports: External */
import { BaseService, Metrics } from '@eth-optimism/common-ts' import { BaseService, Metrics } from '@eth-optimism/common-ts'
import { StaticJsonRpcProvider } from '@ethersproject/providers' import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { BigNumber } from 'ethers' import { BigNumber } from 'ethers'
import { LevelUp } from 'levelup' import { LevelUp } from 'levelup'
import axios from 'axios' import axios from 'axios'
...@@ -10,7 +11,7 @@ import { Gauge } from 'prom-client' ...@@ -10,7 +11,7 @@ import { Gauge } from 'prom-client'
/* Imports: Internal */ /* Imports: Internal */
import { handleSequencerBlock } from './handlers/transaction' import { handleSequencerBlock } from './handlers/transaction'
import { TransportDB } from '../../db/transport-db' import { TransportDB } from '../../db/transport-db'
import { sleep, toRpcHexString, validators } from '../../utils' import { validators } from '../../utils'
import { L1DataTransportServiceOptions } from '../main/service' import { L1DataTransportServiceOptions } from '../main/service'
interface L2IngestionMetrics { interface L2IngestionMetrics {
...@@ -118,6 +119,13 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> { ...@@ -118,6 +119,13 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
highestSyncedL2BlockNumber === targetL2Block || highestSyncedL2BlockNumber === targetL2Block ||
currentL2Block === 0 currentL2Block === 0
) { ) {
this.logger.info(
'All Layer 2 (Optimism) transactions are synchronized',
{
currentL2Block,
targetL2Block,
}
)
await sleep(this.options.pollingInterval) await sleep(this.options.pollingInterval)
continue continue
} }
...@@ -218,15 +226,38 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> { ...@@ -218,15 +226,38 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
id: '1', id: '1',
} }
const resp = await axios.post( // Retry the `eth_getBlockRange` query in case the endBlockNumber
this.state.l2RpcProvider.connection.url, // is greater than the tip and `null` is returned. This gives time
req, // for the sync to catch up
{ responseType: 'stream' } let result = null
) let retry = 0
const respJson = await bfj.parse(resp.data, { while (result === null) {
yieldRate: 4096, // this yields abit more often than the default of 16384 if (retry === 6) {
}) throw new Error(
blocks = respJson.result `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) { for (const block of blocks) {
......
...@@ -68,6 +68,12 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp ...@@ -68,6 +68,12 @@ export class L1DataTransportService extends BaseService<L1DataTransportServiceOp
protected async _init(): Promise<void> { protected async _init(): Promise<void> {
this.logger.info('Initializing L1 Data Transport Service...') 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) this.state.db = level(this.options.dbPath)
await this.state.db.open() 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 './contracts'
export * from './validation' export * from './validation'
export * from './eth-tx' export * from './eth-tx'
...@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node' ...@@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
import Config from 'bcfg' import Config from 'bcfg'
import { MessageRelayerService } from '../service' import { MessageRelayerService } from '../src'
dotenv.config() dotenv.config()
...@@ -59,14 +59,6 @@ const main = async () => { ...@@ -59,14 +59,6 @@ const main = async () => {
'get-logs-interval', 'get-logs-interval',
parseInt(env.GET_LOGS_INTERVAL, 10) || 2000 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( const FROM_L2_TRANSACTION_INDEX = config.uint(
'from-l2-transaction-index', 'from-l2-transaction-index',
parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0 parseInt(env.FROM_L2_TRANSACTION_INDEX, 10) || 0
...@@ -102,15 +94,11 @@ const main = async () => { ...@@ -102,15 +94,11 @@ const main = async () => {
} }
const service = new MessageRelayerService({ const service = new MessageRelayerService({
l1RpcProvider: l1Provider,
l2RpcProvider: l2Provider, l2RpcProvider: l2Provider,
addressManagerAddress: ADDRESS_MANAGER_ADDRESS,
l1Wallet: wallet, l1Wallet: wallet,
relayGasLimit: RELAY_GAS_LIMIT, relayGasLimit: RELAY_GAS_LIMIT,
fromL2TransactionIndex: FROM_L2_TRANSACTION_INDEX, fromL2TransactionIndex: FROM_L2_TRANSACTION_INDEX,
pollingInterval: POLLING_INTERVAL, pollingInterval: POLLING_INTERVAL,
l2BlockOffset: L2_BLOCK_OFFSET,
l1StartOffset: L1_START_OFFSET,
getLogsInterval: GET_LOGS_INTERVAL, getLogsInterval: GET_LOGS_INTERVAL,
logger, logger,
}) })
......
...@@ -7,19 +7,14 @@ ...@@ -7,19 +7,14 @@
"files": [ "files": [
"dist/*" "dist/*"
], ],
"bin": {
"withdraw": "./src/exec/withdraw.ts"
},
"scripts": { "scripts": {
"start": "ts-node ./src/exec/run.ts", "start": "ts-node ./bin/run.ts",
"build": "tsc -p ./tsconfig.build.json", "build": "tsc -p ./tsconfig.build.json",
"clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo", "clean": "rimraf dist/ ./tsconfig.build.tsbuildinfo",
"lint": "yarn lint:fix && yarn lint:check", "lint": "yarn lint:fix && yarn lint:check",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"lint:fix": "yarn lint:check --fix", "lint:fix": "yarn lint:check --fix",
"lint:check": "eslint . --max-warnings=0", "lint:check": "eslint . --max-warnings=0"
"test": "hardhat test --show-stack-traces",
"test:coverage": "nyc hardhat test && nyc merge .nyc_output coverage.json"
}, },
"keywords": [ "keywords": [
"optimism", "optimism",
...@@ -35,28 +30,19 @@ ...@@ -35,28 +30,19 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/contracts": "0.5.14",
"@eth-optimism/core-utils": "0.7.7", "@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"@sentry/node": "^6.3.1", "@sentry/node": "^6.3.1",
"bcfg": "^0.1.6", "bcfg": "^0.1.6",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"ethers": "^5.5.4", "ethers": "^5.5.4"
"merkletreejs": "^0.2.18",
"rlp": "^2.2.6"
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/smock": "1.1.10",
"@nomiclabs/ethereumjs-vm": "^4",
"@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-ethers": "^2.0.2",
"@nomiclabs/hardhat-waffle": "^2.0.1", "@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/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0", "@typescript-eslint/parser": "^4.26.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"eslint": "^7.27.0", "eslint": "^7.27.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-import": "^2.23.4",
...@@ -68,8 +54,6 @@ ...@@ -68,8 +54,6 @@
"ethereum-waffle": "^3.3.0", "ethereum-waffle": "^3.3.0",
"hardhat": "^2.3.0", "hardhat": "^2.3.0",
"lint-staged": "11.0.0", "lint-staged": "11.0.0",
"lodash": "^4.17.21",
"mocha": "^8.4.0",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "^4.3.5" "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'
This diff is collapsed.
This diff is collapsed.
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 {}
}
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "0.2.1", "@eth-optimism/common-ts": "0.2.1",
"@eth-optimism/core-utils": "0.7.7", "@eth-optimism/core-utils": "0.7.7",
"@eth-optimism/sdk": "^0.2.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"ethers": "^5.5.4", "ethers": "^5.5.4",
"express": "^4.17.1", "express": "^4.17.1",
......
...@@ -6,7 +6,8 @@ import { Gauge, Histogram } from 'prom-client' ...@@ -6,7 +6,8 @@ import { Gauge, Histogram } from 'prom-client'
import cron from 'node-cron' import cron from 'node-cron'
import { providers, Wallet } from 'ethers' import { providers, Wallet } from 'ethers'
import { Metrics, Logger } from '@eth-optimism/common-ts' 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' import { binarySearchForMismatch } from './helpers'
...@@ -49,7 +50,7 @@ export class HealthcheckServer { ...@@ -49,7 +50,7 @@ export class HealthcheckServer {
init = () => { init = () => {
this.metrics = this.initMetrics() this.metrics = this.initMetrics()
this.server = this.initServer() this.server = this.initServer()
this.replicaProvider = injectL2Context( this.replicaProvider = asL2Provider(
new providers.StaticJsonRpcProvider({ new providers.StaticJsonRpcProvider({
url: this.options.replicaRpcProvider, url: this.options.replicaRpcProvider,
headers: { 'User-Agent': 'replica-healthcheck' }, headers: { 'User-Agent': 'replica-healthcheck' },
...@@ -180,7 +181,7 @@ export class HealthcheckServer { ...@@ -180,7 +181,7 @@ export class HealthcheckServer {
} }
runSyncCheck = async () => { runSyncCheck = async () => {
const sequencerProvider = injectL2Context( const sequencerProvider = asL2Provider(
new providers.StaticJsonRpcProvider({ new providers.StaticJsonRpcProvider({
url: this.options.sequencerRpcProvider, url: this.options.sequencerRpcProvider,
headers: { 'User-Agent': 'replica-healthcheck' }, headers: { 'User-Agent': 'replica-healthcheck' },
......
/* eslint-disable @typescript-eslint/no-unused-vars */ /* 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 { 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 { hexStringEquals } from '@eth-optimism/core-utils'
import { import {
...@@ -17,6 +17,14 @@ import { StandardBridgeAdapter } from './standard-bridge' ...@@ -17,6 +17,14 @@ import { StandardBridgeAdapter } from './standard-bridge'
* Bridge adapter for the ETH bridge. * Bridge adapter for the ETH bridge.
*/ */
export class ETHBridgeAdapter extends StandardBridgeAdapter { 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( public async getDepositsByAddress(
address: AddressLike, address: AddressLike,
opts?: { opts?: {
...@@ -104,6 +112,17 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter { ...@@ -104,6 +112,17 @@ export class ETHBridgeAdapter extends StandardBridgeAdapter {
} }
populateTransaction = { 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 ( deposit: async (
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
......
...@@ -185,6 +185,38 @@ export class StandardBridgeAdapter implements IBridgeAdapter { ...@@ -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( public async deposit(
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
...@@ -217,6 +249,31 @@ export class StandardBridgeAdapter implements IBridgeAdapter { ...@@ -217,6 +249,31 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
} }
populateTransaction = { 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 ( deposit: async (
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
...@@ -288,6 +345,19 @@ export class StandardBridgeAdapter implements IBridgeAdapter { ...@@ -288,6 +345,19 @@ export class StandardBridgeAdapter implements IBridgeAdapter {
} }
estimateGas = { 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 ( deposit: async (
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
......
...@@ -109,7 +109,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -109,7 +109,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if (Provider.isProvider(this.l1SignerOrProvider)) { if (Provider.isProvider(this.l1SignerOrProvider)) {
return this.l1SignerOrProvider return this.l1SignerOrProvider
} else { } else {
return this.l1SignerOrProvider.provider return this.l1SignerOrProvider.provider as any
} }
} }
...@@ -117,7 +117,7 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -117,7 +117,7 @@ export class CrossChainMessenger implements ICrossChainMessenger {
if (Provider.isProvider(this.l2SignerOrProvider)) { if (Provider.isProvider(this.l2SignerOrProvider)) {
return this.l2SignerOrProvider return this.l2SignerOrProvider
} else { } else {
return this.l2SignerOrProvider.provider return this.l2SignerOrProvider.provider as any
} }
} }
...@@ -143,6 +143,13 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -143,6 +143,13 @@ export class CrossChainMessenger implements ICrossChainMessenger {
direction?: MessageDirection direction?: MessageDirection
} = {} } = {}
): Promise<CrossChainMessage[]> { ): 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) const txHash = toTransactionHash(transaction)
let receipt: TransactionReceipt let receipt: TransactionReceipt
...@@ -654,6 +661,11 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -654,6 +661,11 @@ export class CrossChainMessenger implements ICrossChainMessenger {
let batchEvent: ethers.Event | null = let batchEvent: ethers.Event | null =
await this.getStateBatchAppendedEventByBatchIndex(upperBound) await this.getStateBatchAppendedEventByBatchIndex(upperBound)
// Only happens when no batches have been submitted yet.
if (batchEvent === null) {
return null
}
if (isEventLo(batchEvent, transactionIndex)) { if (isEventLo(batchEvent, transactionIndex)) {
// Upper bound is too low, means this transaction doesn't have a corresponding state batch yet. // Upper bound is too low, means this transaction doesn't have a corresponding state batch yet.
return null return null
...@@ -832,6 +844,36 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -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( public async depositERC20(
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
...@@ -974,6 +1016,18 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -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 ( depositERC20: async (
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
...@@ -1070,6 +1124,24 @@ export class CrossChainMessenger implements ICrossChainMessenger { ...@@ -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 ( depositERC20: async (
l1Token: AddressLike, l1Token: AddressLike,
l2Token: AddressLike, l2Token: AddressLike,
......
...@@ -2311,7 +2311,7 @@ ...@@ -2311,7 +2311,7 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@nomiclabs/ethereumjs-vm@^4": "@nomiclabs/ethereumjs-vm@^4.2.2":
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc" resolved "https://registry.yarnpkg.com/@nomiclabs/ethereumjs-vm/-/ethereumjs-vm-4.2.2.tgz#2f8817113ca0fb6c44c1b870d0a809f0e026a6cc"
integrity sha512-8WmX94mMcJaZ7/m7yBbyuS6B+wuOul+eF+RY9fBpGhNaUpyMR/vFIcDojqcWQ4Yafe1tMKY5LDu2yfT4NZgV4Q== 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