Commit 8a4fccf6 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into refcell/fix/log_warns

parents d37d91cc ca71678a
---
'@eth-optimism/chain-mon': minor
---
Introduces the balance-mon service to chain-mon.
...@@ -18,6 +18,7 @@ jobs: ...@@ -18,6 +18,7 @@ jobs:
l2geth: ${{ steps.packages.outputs.l2geth }} l2geth: ${{ steps.packages.outputs.l2geth }}
message-relayer: ${{ steps.packages.outputs.message-relayer }} message-relayer: ${{ steps.packages.outputs.message-relayer }}
fault-detector: ${{ steps.packages.outputs.fault-detector }} fault-detector: ${{ steps.packages.outputs.fault-detector }}
balance-mon: ${{ steps.packages.outputs.balance-mon }}
drippie-mon: ${{ steps.packages.outputs.drippie-mon }} drippie-mon: ${{ steps.packages.outputs.drippie-mon }}
wd-mon: ${{ steps.packages.outputs.wd-mon }} wd-mon: ${{ steps.packages.outputs.wd-mon }}
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
...@@ -230,6 +231,33 @@ jobs: ...@@ -230,6 +231,33 @@ jobs:
push: true push: true
tags: ethereumoptimism/fault-detector:${{ needs.canary-publish.outputs.canary-docker-tag }} tags: ethereumoptimism/fault-detector:${{ needs.canary-publish.outputs.canary-docker-tag }}
balance-mon:
name: Publish Balance Monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish
if: needs.canary-publish.outputs.balance-mon != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.packages
target: balance-mon
push: true
tags: ethereumoptimism/balance-mon:${{ needs.canary-publish.outputs.canary-docker-tag }}
drippie-mon: drippie-mon:
name: Publish Drippie Monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }} name: Publish Drippie Monitor Version ${{ needs.canary-publish.outputs.canary-docker-tag }}
needs: canary-publish needs: canary-publish
......
...@@ -14,6 +14,7 @@ jobs: ...@@ -14,6 +14,7 @@ jobs:
l2geth: ${{ steps.packages.outputs.l2geth }} l2geth: ${{ steps.packages.outputs.l2geth }}
message-relayer: ${{ steps.packages.outputs.message-relayer }} message-relayer: ${{ steps.packages.outputs.message-relayer }}
fault-detector: ${{ steps.packages.outputs.fault-detector }} fault-detector: ${{ steps.packages.outputs.fault-detector }}
balance-mon: ${{ steps.packages.outputs.drippie-mon }}
drippie-mon: ${{ steps.packages.outputs.drippie-mon }} drippie-mon: ${{ steps.packages.outputs.drippie-mon }}
wd-mon: ${{ steps.packages.outputs.wd-mon }} wd-mon: ${{ steps.packages.outputs.wd-mon }}
data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }} data-transport-layer: ${{ steps.packages.outputs.data-transport-layer }}
...@@ -364,6 +365,33 @@ jobs: ...@@ -364,6 +365,33 @@ jobs:
push: true push: true
tags: ethereumoptimism/wd-mon:${{ needs.release.outputs.wd-mon }},ethereumoptimism/wd-mon:latest tags: ethereumoptimism/wd-mon:${{ needs.release.outputs.wd-mon }},ethereumoptimism/wd-mon:latest
drippie-mon:
name: Publish Balance Monitor Version ${{ needs.release.outputs.balance-mon }}
needs: release
if: needs.release.outputs.balance-mon != ''
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_USERNAME }}
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN_SECRET }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./ops/docker/Dockerfile.packages
target: balance-mon
push: true
tags: ethereumoptimism/balance-mon:${{ needs.release.outputs.balance-mon }},ethereumoptimism/balance-mon:latest
drippie-mon: drippie-mon:
name: Publish Drippie Monitor Version ${{ needs.release.outputs.drippie-mon }} name: Publish Drippie Monitor Version ${{ needs.release.outputs.drippie-mon }}
needs: release needs: release
......
...@@ -201,7 +201,7 @@ Once you’ve built both repositories, you’ll need head back to the Optimism M ...@@ -201,7 +201,7 @@ Once you’ve built both repositories, you’ll need head back to the Optimism M
- Replace `"BATCHER"` with the address of the Batcher account you generated earlier. - Replace `"BATCHER"` with the address of the Batcher account you generated earlier.
- Replace `"SEQUENCER"` with the address of the Sequencer account you generated earlier. - Replace `"SEQUENCER"` with the address of the Sequencer account you generated earlier.
- Replace `"BLOCKHASH"` with the blockhash you got from the `cast` command. - Replace `"BLOCKHASH"` with the blockhash you got from the `cast` command.
- Replace `"TIMESTAMP"` with the timestamp you got from the `cast` command. Note that although all the other fields are strings, this field is a number! Don’t include the quotation marks. - Replace `TIMESTAMP` with the timestamp you got from the `cast` command. Note that although all the other fields are strings, this field is a number! Don’t include the quotation marks.
## Deploy the L1 contracts ## Deploy the L1 contracts
...@@ -390,9 +390,7 @@ Head over to the `op-node` package and start the `op-node` using the following c ...@@ -390,9 +390,7 @@ Head over to the `op-node` package and start the `op-node` using the following c
--rollup.config=./rollup.json \ --rollup.config=./rollup.json \
--rpc.addr=0.0.0.0 \ --rpc.addr=0.0.0.0 \
--rpc.port=8547 \ --rpc.port=8547 \
--p2p.listen.ip=0.0.0.0 \ --p2p.disable \
--p2p.listen.tcp=9003 \
--p2p.listen.udp=9003 \
--rpc.enable-admin \ --rpc.enable-admin \
--p2p.sequencer.key=<SEQUENCERKEY> \ --p2p.sequencer.key=<SEQUENCERKEY> \
--l1=<RPC> \ --l1=<RPC> \
...@@ -402,6 +400,26 @@ Head over to the `op-node` package and start the `op-node` using the following c ...@@ -402,6 +400,26 @@ Head over to the `op-node` package and start the `op-node` using the following c
Once you run this command, you should start seeing the `op-node` begin to process all of the L1 information after the starting block number that you picked earlier. Once the `op-node` has enough information, it’ll begin sending Engine API payloads to `op-geth`. At that point, you’ll start to see blocks being created inside of `op-geth`. We’re live! Once you run this command, you should start seeing the `op-node` begin to process all of the L1 information after the starting block number that you picked earlier. Once the `op-node` has enough information, it’ll begin sending Engine API payloads to `op-geth`. At that point, you’ll start to see blocks being created inside of `op-geth`. We’re live!
::: tip Peer to peer synchronization
If you use a chain ID that is also used by others, for example the default (42069), your `op-node` will try to use peer to peer to speed up synchronization.
These attempts will fail, because they will be signed with the wrong key, but they will waste time and network resources.
To avoid this , we start with peer to peer synchronization disabled (`--p2p.disable`).
Once you have multiple nodes, it makes sense to use these command line parameters to synchronize between them without getting confused by other blockchains.
```
--p2p.static=<nodes> \
--p2p.listen.ip=0.0.0.0 \
--p2p.listen.tcp=9003 \
--p2p.listen.udp=9003 \
```
:::
## Run op-batcher ## Run op-batcher
The final component necessary to put all the pieces together is the `op-batcher`. The `op-batcher` takes transactions from the Sequencer and publishes those transactions to L1. Once transactions are on L1, they’re officially part of the Rollup. Without the `op-batcher`, transactions sent to the Sequencer would never make it to L1 and wouldn’t become part of the canonical chain. The `op-batcher` is critical! The final component necessary to put all the pieces together is the `op-batcher`. The `op-batcher` takes transactions from the Sequencer and publishes those transactions to L1. Once transactions are on L1, they’re officially part of the Rollup. Without the `op-batcher`, transactions sent to the Sequencer would never make it to L1 and wouldn’t become part of the canonical chain. The `op-batcher` is critical!
...@@ -516,15 +534,47 @@ To use any other development stack, see the getting started tutorial, just repla ...@@ -516,15 +534,47 @@ To use any other development stack, see the getting started tutorial, just repla
### Stopping your Rollup ### Stopping your Rollup
To stop `op-geth` you should use Ctrl-C. An orderly shutdown is done in the reverse order to the order in which components were started:
1. Stop `op-batcher`.
1. Stop `op-node`.
1. Stop `op-geth`.
### Starting your Rollup
To restart the blockchain, use the same order of components you did when you initialized it.
1. `op-geth`
1. `op-node`
1. `op-batcher`
::: tip Synchronization takes time
`op-batcher` might have warning messages similar to:
```
WARN [03-21|14:13:55.248] Error calculating L2 block range err="failed to get sync status: Post \"http://localhost:8547\": context deadline exceeded"
WARN [03-21|14:13:57.328] Error calculating L2 block range err="failed to get sync status: Post \"http://localhost:8547\": context deadline exceeded"
```
This means that `op-node` is not yet synchronized up to the present time.
Just wait until it is.
:::
If `op-geth` aborts (for example, because the computer it is running on crashes), you will get these errors on `op-node`:
### Errors
#### Corrupt data directory
If `op-geth` aborts (for example, because the computer it is running on crashes), you might get these errors on `op-node`:
``` ```
WARN [02-16|21:22:02.868] Derivation process temporary error attempts=14 err="stage 0 failed resetting: temp: failed to find the L2 Heads to start from: failed to fetch L2 block by hash 0x0000000000000000000000000000000000000000000000000000000000000000: failed to determine block-hash of hash 0x0000000000000000000000000000000000000000000000000000000000000000, could not get payload: not found" WARN [02-16|21:22:02.868] Derivation process temporary error attempts=14 err="stage 0 failed resetting: temp: failed to find the L2 Heads to start from: failed to fetch L2 block by hash 0x0000000000000000000000000000000000000000000000000000000000000000: failed to determine block-hash of hash 0x0000000000000000000000000000000000000000000000000000000000000000, could not get payload: not found"
``` ```
In that case, you need to remove `datadir`, reinitialize it: This means that the data directory is corrupt and you need to reinitialize it:
```bash ```bash
cd ~/op-geth cd ~/op-geth
...@@ -536,17 +586,23 @@ echo "<SEQUENCER KEY HERE>" > datadir/block-signer-key ...@@ -536,17 +586,23 @@ echo "<SEQUENCER KEY HERE>" > datadir/block-signer-key
./build/bin/geth init --datadir=./datadir ./genesis.json ./build/bin/geth init --datadir=./datadir ./genesis.json
``` ```
### Starting your Rollup
To restart the blockchain, use the same order of components you did when you initialized it. #### Batcher out of ETH
1. `op-geth` If `op-batcher` runs out of ETH, it cannot submit write new transaction batches to L1.
2. `op-node` You will get error messages similar to this one:
3. `op-batcher`
```
INFO [03-21|14:22:32.754] publishing transaction service=batcher txHash=2ace6d..7eb248 nonce=2516 gasTipCap=2,340,741 gasFeeCap=172,028,434,515
ERROR[03-21|14:22:32.844] unable to publish transaction service=batcher txHash=2ace6d..7eb248 nonce=2516 gasTipCap=2,340,741 gasFeeCap=172,028,434,515 err="insufficient funds for gas * price + value"
```
Just send more ETH and to the batcher, and the problem will be resolved.
## Adding nodes ## Adding nodes
To add nodes to the rollup, you need to initialize `op-node` and `op-geth`, similar to what you did for the first node: To add nodes to the rollup, you need to initialize `op-node` and `op-geth`, similar to what you did for the first node.
You should *not* add an `op-bathcer`, there should be only one.
1. Configure the OS and prerequisites as you did for the first node. 1. Configure the OS and prerequisites as you did for the first node.
1. Build the Optimism monorepo and `op-geth` as you did for the first node. 1. Build the Optimism monorepo and `op-geth` as you did for the first node.
...@@ -574,8 +630,8 @@ To add nodes to the rollup, you need to initialize `op-node` and `op-geth`, simi ...@@ -574,8 +630,8 @@ To add nodes to the rollup, you need to initialize `op-node` and `op-geth`, simi
1. Start `op-geth` (using the same command line you used on the initial node) 1. Start `op-geth` (using the same command line you used on the initial node)
1. Start `op-node` (using the same command line you used on the initial node) 1. Start `op-node` (using the same command line you used on the initial node)
1. Wait while the node synchronizes
## What’s next? ## What’s next?
You can use this rollup the same way you’d use any other test blockchain. Once the superchain is available, this blockchain should be able to join the test version. Alternatively, you could [modify the blockchain in various ways](./hacks.md). **Please note that OP Stack Hacks are unofficial and are not explicitly supported by the OP Stack.** You will not be able to receive significant developer support for any modifications you make to the OP Stack. You can use this rollup the same way you’d use any other test blockchain. Once the superchain is available, this blockchain should be able to join the test version. Alternatively, you could [modify the blockchain in various ways](./hacks.md). **Please note that OP Stack Hacks are unofficial and are not explicitly supported by the OP Stack.** You will not be able to receive significant developer support for any modifications you make to the OP Stack.
\ No newline at end of file
...@@ -34,6 +34,7 @@ require ( ...@@ -34,6 +34,7 @@ require (
golang.org/x/crypto v0.6.0 golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/term v0.5.0 golang.org/x/term v0.5.0
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
) )
require ( require (
...@@ -178,7 +179,6 @@ require ( ...@@ -178,7 +179,6 @@ require (
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.6.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
......
...@@ -110,6 +110,14 @@ type CLIConfig struct { ...@@ -110,6 +110,14 @@ type CLIConfig struct {
/* Optional Params */ /* Optional Params */
// TxManagerTimeout is the max amount of time to wait for the [txmgr].
// This will default to: 10 * time.Minute.
TxManagerTimeout time.Duration
// OfflineGasEstimation specifies whether the batcher should calculate
// gas estimations offline using the [core.IntrinsicGas] function.
OfflineGasEstimation bool
// MaxL1TxSize is the maximum size of a batch tx submitted to L1. // MaxL1TxSize is the maximum size of a batch tx submitted to L1.
MaxL1TxSize uint64 MaxL1TxSize uint64
...@@ -168,19 +176,21 @@ func NewConfig(ctx *cli.Context) CLIConfig { ...@@ -168,19 +176,21 @@ func NewConfig(ctx *cli.Context) CLIConfig {
ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name), ResubmissionTimeout: ctx.GlobalDuration(flags.ResubmissionTimeoutFlag.Name),
/* Optional Flags */ /* Optional Flags */
MaxChannelDuration: ctx.GlobalUint64(flags.MaxChannelDurationFlag.Name), OfflineGasEstimation: ctx.GlobalBool(flags.OfflineGasEstimationFlag.Name),
MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeBytesFlag.Name), TxManagerTimeout: ctx.GlobalDuration(flags.TxManagerTimeoutFlag.Name),
TargetL1TxSize: ctx.GlobalUint64(flags.TargetL1TxSizeBytesFlag.Name), MaxChannelDuration: ctx.GlobalUint64(flags.MaxChannelDurationFlag.Name),
TargetNumFrames: ctx.GlobalInt(flags.TargetNumFramesFlag.Name), MaxL1TxSize: ctx.GlobalUint64(flags.MaxL1TxSizeBytesFlag.Name),
ApproxComprRatio: ctx.GlobalFloat64(flags.ApproxComprRatioFlag.Name), TargetL1TxSize: ctx.GlobalUint64(flags.TargetL1TxSizeBytesFlag.Name),
Stopped: ctx.GlobalBool(flags.StoppedFlag.Name), TargetNumFrames: ctx.GlobalInt(flags.TargetNumFramesFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name), ApproxComprRatio: ctx.GlobalFloat64(flags.ApproxComprRatioFlag.Name),
SequencerHDPath: ctx.GlobalString(flags.SequencerHDPathFlag.Name), Stopped: ctx.GlobalBool(flags.StoppedFlag.Name),
PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name), Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
RPCConfig: rpc.ReadCLIConfig(ctx), SequencerHDPath: ctx.GlobalString(flags.SequencerHDPathFlag.Name),
LogConfig: oplog.ReadCLIConfig(ctx), PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name),
MetricsConfig: opmetrics.ReadCLIConfig(ctx), RPCConfig: rpc.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx), LogConfig: oplog.ReadCLIConfig(ctx),
SignerConfig: opsigner.ReadCLIConfig(ctx), MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
SignerConfig: opsigner.ReadCLIConfig(ctx),
} }
} }
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"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"
) )
...@@ -24,7 +25,7 @@ import ( ...@@ -24,7 +25,7 @@ import (
type BatchSubmitter struct { type BatchSubmitter struct {
Config // directly embed the config + sources Config // directly embed the config + sources
txMgr *TransactionManager txMgr txmgr.TxManager
wg sync.WaitGroup wg sync.WaitGroup
done chan struct{} done chan struct{}
...@@ -79,6 +80,7 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger, m metrics.Metri ...@@ -79,6 +80,7 @@ func NewBatchSubmitterFromCLIConfig(cfg CLIConfig, l log.Logger, m metrics.Metri
NumConfirmations: cfg.NumConfirmations, NumConfirmations: cfg.NumConfirmations,
SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount, SafeAbortNonceTooLowCount: cfg.SafeAbortNonceTooLowCount,
From: fromAddress, From: fromAddress,
ChainID: rcfg.L1ChainID,
Signer: signer(rcfg.L1ChainID), Signer: signer(rcfg.L1ChainID),
} }
...@@ -125,10 +127,8 @@ func NewBatchSubmitter(ctx context.Context, cfg Config, l log.Logger, m metrics. ...@@ -125,10 +127,8 @@ func NewBatchSubmitter(ctx context.Context, cfg Config, l log.Logger, m metrics.
return &BatchSubmitter{ return &BatchSubmitter{
Config: cfg, Config: cfg,
txMgr: NewTransactionManager(l, txMgr: txmgr.NewSimpleTxManager("batcher", l, cfg.TxManagerConfig, cfg.L1Client),
cfg.TxManagerConfig, cfg.Rollup.BatchInboxAddress, cfg.Rollup.L1ChainID, state: NewChannelManager(l, m, cfg.Channel),
cfg.From, cfg.L1Client),
state: NewChannelManager(l, m, cfg.Channel),
}, nil }, nil
} }
...@@ -226,7 +226,7 @@ func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context) { ...@@ -226,7 +226,7 @@ func (l *BatchSubmitter) loadBlocksIntoState(ctx context.Context) {
// loadBlockIntoState fetches & stores a single block into `state`. It returns the block it loaded. // loadBlockIntoState fetches & stores a single block into `state`. It returns the block it loaded.
func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uint64) (*types.Block, error) { func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uint64) (*types.Block, error) {
ctx, cancel := context.WithTimeout(ctx, networkTimeout) ctx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel() defer cancel()
block, err := l.L2Client.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber)) block, err := l.L2Client.BlockByNumber(ctx, new(big.Int).SetUint64(blockNumber))
if err != nil { if err != nil {
...@@ -244,7 +244,7 @@ func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uin ...@@ -244,7 +244,7 @@ func (l *BatchSubmitter) loadBlockIntoState(ctx context.Context, blockNumber uin
// calculateL2BlockRangeToStore determines the range (start,end] that should be loaded into the local state. // calculateL2BlockRangeToStore determines the range (start,end] that should be loaded into the local state.
// It also takes care of initializing some local state (i.e. will modify l.lastStoredBlock in certain conditions) // It also takes care of initializing some local state (i.e. will modify l.lastStoredBlock in certain conditions)
func (l *BatchSubmitter) calculateL2BlockRangeToStore(ctx context.Context) (eth.BlockID, eth.BlockID, error) { func (l *BatchSubmitter) calculateL2BlockRangeToStore(ctx context.Context) (eth.BlockID, eth.BlockID, error) {
childCtx, cancel := context.WithTimeout(ctx, networkTimeout) childCtx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel() defer cancel()
syncStatus, err := l.RollupNode.SyncStatus(childCtx) syncStatus, err := l.RollupNode.SyncStatus(childCtx)
// Ensure that we have the sync status // Ensure that we have the sync status
...@@ -312,8 +312,9 @@ func (l *BatchSubmitter) loop() { ...@@ -312,8 +312,9 @@ func (l *BatchSubmitter) loop() {
l.log.Error("unable to get tx data", "err", err) l.log.Error("unable to get tx data", "err", err)
break break
} }
// Record TX Status // Record TX Status
if receipt, err := l.txMgr.SendTransaction(l.ctx, txdata.Bytes()); err != nil { if receipt, err := l.sendTransaction(l.ctx, txdata.Bytes()); err != nil {
l.recordFailedTx(txdata.ID(), err) l.recordFailedTx(txdata.ID(), err)
} else { } else {
l.recordConfirmedTx(txdata.ID(), receipt) l.recordConfirmedTx(txdata.ID(), receipt)
...@@ -335,6 +336,40 @@ func (l *BatchSubmitter) loop() { ...@@ -335,6 +336,40 @@ func (l *BatchSubmitter) loop() {
} }
} }
const networkTimeout = 2 * time.Second // How long a single network request can take. TODO: put in a config somewhere
// fix(refcell):
// combined with above, these config variables should also be replicated in the op-proposer
// along with op-proposer changes to include the updated tx manager
const txManagerTimeout = 2 * time.Minute // How long the tx manager can take to send a transaction.
// sendTransaction creates & submits a transaction to the batch inbox address with the given `data`.
// It currently uses the underlying `txmgr` to handle transaction sending & price management.
// This is a blocking method. It should not be called concurrently.
func (l *BatchSubmitter) sendTransaction(ctx context.Context, data []byte) (*types.Receipt, error) {
// Do the gas estimation offline. A value of 0 will cause the [txmgr] to estimate the gas limit.
intrinsicGas, err := core.IntrinsicGas(data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to calculate intrinsic gas: %w", err)
}
// Send the transaction through the txmgr
ctx, cancel := context.WithTimeout(ctx, txManagerTimeout)
defer cancel()
if receipt, err := l.txMgr.Send(ctx, txmgr.TxCandidate{
To: l.Rollup.BatchInboxAddress,
TxData: data,
From: l.From,
GasLimit: intrinsicGas,
}); err != nil {
l.log.Warn("unable to publish tx", "err", err, "data_size", len(data))
return nil, err
} else {
l.log.Info("tx successfully published", "tx_hash", receipt.TxHash, "data_size", len(data))
return receipt, nil
}
}
func (l *BatchSubmitter) recordL1Tip(l1tip eth.L1BlockRef) { func (l *BatchSubmitter) recordL1Tip(l1tip eth.L1BlockRef) {
if l.lastL1Tip == l1tip { if l.lastL1Tip == l1tip {
return return
......
package batcher
import (
"context"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/mocks"
)
// TestBatchSubmitter_SendTransaction tests the driver's
// [SendTransaction] external facing function.
func TestBatchSubmitter_SendTransaction(t *testing.T) {
log := testlog.Logger(t, log.LvlCrit)
txMgr := mocks.TxManager{}
batcherInboxAddress := common.HexToAddress("0x42000000000000000000000000000000000000ff")
chainID := big.NewInt(1)
sender := common.HexToAddress("0xdeadbeef")
bs := BatchSubmitter{
Config: Config{
log: log,
From: sender,
Rollup: &rollup.Config{
L1ChainID: chainID,
BatchInboxAddress: batcherInboxAddress,
},
},
txMgr: &txMgr,
}
txData := []byte{0x00, 0x01, 0x02}
gasTipCap := big.NewInt(136)
gasFeeCap := big.NewInt(137)
gas := uint64(1337)
// Candidate gas should be calculated with [core.IntrinsicGas]
intrinsicGas, err := core.IntrinsicGas(txData, nil, false, true, true, false)
require.NoError(t, err)
candidate := txmgr.TxCandidate{
To: batcherInboxAddress,
TxData: txData,
From: sender,
GasLimit: intrinsicGas,
}
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainID,
Nonce: 0,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: gas,
To: &batcherInboxAddress,
Data: txData,
})
txHash := tx.Hash()
expectedReceipt := types.Receipt{
Type: 1,
PostState: []byte{},
Status: uint64(1),
CumulativeGasUsed: gas,
TxHash: txHash,
GasUsed: gas,
}
txMgr.On("Send", mock.Anything, candidate).Return(&expectedReceipt, nil)
receipt, err := bs.sendTransaction(context.Background(), tx.Data())
require.NoError(t, err)
require.Equal(t, receipt, &expectedReceipt)
}
package batcher
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
)
const networkTimeout = 2 * time.Second // How long a single network request can take. TODO: put in a config somewhere
// TransactionManager wraps the simple txmgr package to make it easy to send & wait for transactions
type TransactionManager struct {
// Config
batchInboxAddress common.Address
senderAddress common.Address
chainID *big.Int
// Outside world
txMgr txmgr.TxManager
l1Client *ethclient.Client
signerFn opcrypto.SignerFn
log log.Logger
}
func NewTransactionManager(log log.Logger, txMgrConfg txmgr.Config, batchInboxAddress common.Address, chainID *big.Int, senderAddress common.Address, l1Client *ethclient.Client) *TransactionManager {
t := &TransactionManager{
batchInboxAddress: batchInboxAddress,
senderAddress: senderAddress,
chainID: chainID,
txMgr: txmgr.NewSimpleTxManager("batcher", log, txMgrConfg, l1Client),
l1Client: l1Client,
signerFn: txMgrConfg.Signer,
log: log,
}
return t
}
// SendTransaction creates & submits a transaction to the batch inbox address with the given `data`.
// It currently uses the underlying `txmgr` to handle transaction sending & price management.
// This is a blocking method. It should not be called concurrently.
// TODO: where to put concurrent transaction handling logic.
func (t *TransactionManager) SendTransaction(ctx context.Context, data []byte) (*types.Receipt, error) {
tx, err := t.CraftTx(ctx, data)
if err != nil {
return nil, fmt.Errorf("failed to create tx: %w", err)
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) // TODO: Select a timeout that makes sense here.
defer cancel()
if receipt, err := t.txMgr.Send(ctx, tx); err != nil {
t.log.Warn("unable to publish tx", "err", err, "data_size", len(data))
return nil, err
} else {
t.log.Info("tx successfully published", "tx_hash", receipt.TxHash, "data_size", len(data))
return receipt, nil
}
}
// calcGasTipAndFeeCap queries L1 to determine what a suitable miner tip & basefee limit would be for timely inclusion
func (t *TransactionManager) calcGasTipAndFeeCap(ctx context.Context) (gasTipCap *big.Int, gasFeeCap *big.Int, err error) {
childCtx, cancel := context.WithTimeout(ctx, networkTimeout)
gasTipCap, err = t.l1Client.SuggestGasTipCap(childCtx)
cancel()
if err != nil {
return nil, nil, fmt.Errorf("failed to get suggested gas tip cap: %w", err)
}
if gasTipCap == nil {
t.log.Warn("unexpected unset gasTipCap, using default 2 gwei")
gasTipCap = new(big.Int).SetUint64(params.GWei * 2)
}
childCtx, cancel = context.WithTimeout(ctx, networkTimeout)
head, err := t.l1Client.HeaderByNumber(childCtx, nil)
cancel()
if err != nil || head == nil {
return nil, nil, fmt.Errorf("failed to get L1 head block for fee cap: %w", err)
}
if head.BaseFee == nil {
return nil, nil, fmt.Errorf("failed to get L1 basefee in block %d for fee cap", head.Number)
}
gasFeeCap = txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
return gasTipCap, gasFeeCap, nil
}
// CraftTx creates the signed transaction to the batchInboxAddress.
// It queries L1 for the current fee market conditions as well as for the nonce.
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (t *TransactionManager) CraftTx(ctx context.Context, data []byte) (*types.Transaction, error) {
gasTipCap, gasFeeCap, err := t.calcGasTipAndFeeCap(ctx)
if err != nil {
return nil, err
}
childCtx, cancel := context.WithTimeout(ctx, networkTimeout)
nonce, err := t.l1Client.NonceAt(childCtx, t.senderAddress, nil)
cancel()
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
rawTx := &types.DynamicFeeTx{
ChainID: t.chainID,
Nonce: nonce,
To: &t.batchInboxAddress,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: data,
}
t.log.Info("creating tx", "to", rawTx.To, "from", t.senderAddress)
gas, err := core.IntrinsicGas(rawTx.Data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to calculate intrinsic gas: %w", err)
}
rawTx.Gas = gas
ctx, cancel = context.WithTimeout(ctx, networkTimeout)
defer cancel()
tx := types.NewTx(rawTx)
return t.signerFn(ctx, t.senderAddress, tx)
}
package flags package flags
import ( import (
"time"
"github.com/urfave/cli" "github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-batcher/rpc" "github.com/ethereum-optimism/optimism/op-batcher/rpc"
...@@ -74,7 +76,17 @@ var ( ...@@ -74,7 +76,17 @@ var (
} }
/* Optional flags */ /* Optional flags */
OfflineGasEstimationFlag = cli.BoolFlag{
Name: "offline-gas-estimation",
Usage: "Whether to use offline gas estimation",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "OFFLINE_GAS_ESTIMATION"),
}
TxManagerTimeoutFlag = cli.DurationFlag{
Name: "tx-manager-timeout",
Usage: "Maximum duration to wait for L1 transactions, including resubmissions",
Value: 10 * time.Minute,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "TX_MANAGER_TIMEOUT"),
}
MaxChannelDurationFlag = cli.Uint64Flag{ MaxChannelDurationFlag = cli.Uint64Flag{
Name: "max-channel-duration", Name: "max-channel-duration",
Usage: "The maximum duration of L1-blocks to keep a channel open. 0 to disable.", Usage: "The maximum duration of L1-blocks to keep a channel open. 0 to disable.",
...@@ -141,6 +153,8 @@ var requiredFlags = []cli.Flag{ ...@@ -141,6 +153,8 @@ var requiredFlags = []cli.Flag{
} }
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
OfflineGasEstimationFlag,
TxManagerTimeoutFlag,
MaxChannelDurationFlag, MaxChannelDurationFlag,
MaxL1TxSizeBytesFlag, MaxL1TxSizeBytesFlag,
TargetL1TxSizeBytesFlag, TargetL1TxSizeBytesFlag,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
...@@ -22,19 +23,45 @@ import ( ...@@ -22,19 +23,45 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/state" "github.com/ethereum-optimism/optimism/op-chain-ops/state"
) )
var proxies = []string{ var (
"SystemConfigProxy", // proxies represents the set of proxies in front of contracts.
"L2OutputOracleProxy", proxies = []string{
"L1CrossDomainMessengerProxy", "SystemConfigProxy",
"L1StandardBridgeProxy", "L2OutputOracleProxy",
"OptimismPortalProxy", "L1CrossDomainMessengerProxy",
"OptimismMintableERC20FactoryProxy", "L1StandardBridgeProxy",
} "OptimismPortalProxy",
"OptimismMintableERC20FactoryProxy",
var portalMeteringSlot = common.Hash{31: 0x01} }
// portalMeteringSlot is the storage slot containing the metering params.
portalMeteringSlot = common.Hash{31: 0x01}
// zeroHash represents the zero value for a hash.
zeroHash = common.Hash{}
// uint128Max is type(uint128).max and is set in the init function.
uint128Max = new(big.Int)
// The default values for the ResourceConfig, used as part of
// an EIP-1559 curve for deposit gas.
defaultResourceConfig = bindings.ResourceMeteringResourceConfig{
MaxResourceLimit: 20_000_000,
ElasticityMultiplier: 10,
BaseFeeMaxChangeDenominator: 8,
MinimumBaseFee: params.GWei,
SystemTxMaxGas: 1_000_000,
}
)
var zeroHash common.Hash func init() {
var ok bool
uint128Max, ok = new(big.Int).SetString("ffffffffffffffffffffffffffffffff", 16)
if !ok {
panic("bad uint128Max")
}
// Set the maximum base fee on the default config.
defaultResourceConfig.MaximumBaseFee = uint128Max
}
// BuildL1DeveloperGenesis will create a L1 genesis block after creating
// all of the state required for an Optimism network to function.
func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if config.L2OutputOracleStartingTimestamp != -1 { if config.L2OutputOracleStartingTimestamp != -1 {
return nil, errors.New("l2oo starting timestamp must be -1") return nil, errors.New("l2oo starting timestamp must be -1")
...@@ -67,6 +94,26 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -67,6 +94,26 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
portalABI, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
// Initialize the OptimismPortal without being paused
data, err := portalABI.Pack("initialize", false)
if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for OptimismPortal: %w", err)
}
if _, err := upgradeProxy(
backend,
opts,
depsByName["OptimismPortalProxy"].Address,
depsByName["OptimismPortal"].Address,
data,
); err != nil {
return nil, fmt.Errorf("cannot upgrade OptimismPortalProxy: %w", err)
}
sysCfgABI, err := bindings.SystemConfigMetaData.GetAbi() sysCfgABI, err := bindings.SystemConfigMetaData.GetAbi()
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -75,7 +122,8 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -75,7 +122,8 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
if gasLimit == 0 { if gasLimit == 0 {
gasLimit = defaultL2GasLimit gasLimit = defaultL2GasLimit
} }
data, err := sysCfgABI.Pack(
data, err = sysCfgABI.Pack(
"initialize", "initialize",
config.FinalSystemOwner, config.FinalSystemOwner,
uint642Big(config.GasPriceOracleOverhead), uint642Big(config.GasPriceOracleOverhead),
...@@ -83,6 +131,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -83,6 +131,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
config.BatchSenderAddress.Hash(), config.BatchSenderAddress.Hash(),
gasLimit, gasLimit,
config.P2PSequencerAddress, config.P2PSequencerAddress,
defaultResourceConfig,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for SystemConfig: %w", err) return nil, fmt.Errorf("cannot abi encode initialize for SystemConfig: %w", err)
...@@ -94,7 +143,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -94,7 +143,7 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
depsByName["SystemConfig"].Address, depsByName["SystemConfig"].Address,
data, data,
); err != nil { ); err != nil {
return nil, err return nil, fmt.Errorf("cannot upgrade SystemConfigProxy: %w", err)
} }
l2ooABI, err := bindings.L2OutputOracleMetaData.GetAbi() l2ooABI, err := bindings.L2OutputOracleMetaData.GetAbi()
...@@ -119,24 +168,6 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) { ...@@ -119,24 +168,6 @@ func BuildL1DeveloperGenesis(config *DeployConfig) (*core.Genesis, error) {
return nil, err return nil, err
} }
portalABI, err := bindings.OptimismPortalMetaData.GetAbi()
if err != nil {
return nil, err
}
// Initialize the OptimismPortal without being paused
data, err = portalABI.Pack("initialize", false)
if err != nil {
return nil, fmt.Errorf("cannot abi encode initialize for OptimismPortal: %w", err)
}
if _, err := upgradeProxy(
backend,
opts,
depsByName["OptimismPortalProxy"].Address,
depsByName["OptimismPortal"].Address,
data,
); err != nil {
return nil, err
}
l1XDMABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi() l1XDMABI, err := bindings.L1CrossDomainMessengerMetaData.GetAbi()
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -264,6 +295,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend) ...@@ -264,6 +295,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
if gasLimit == 0 { if gasLimit == 0 {
gasLimit = defaultL2GasLimit gasLimit = defaultL2GasLimit
} }
constructors = append(constructors, []deployer.Constructor{ constructors = append(constructors, []deployer.Constructor{
{ {
Name: "SystemConfig", Name: "SystemConfig",
...@@ -274,6 +306,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend) ...@@ -274,6 +306,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
config.BatchSenderAddress.Hash(), // left-padded 32 bytes value, version is zero anyway config.BatchSenderAddress.Hash(), // left-padded 32 bytes value, version is zero anyway
gasLimit, gasLimit,
config.P2PSequencerAddress, config.P2PSequencerAddress,
defaultResourceConfig,
}, },
}, },
{ {
...@@ -297,6 +330,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend) ...@@ -297,6 +330,7 @@ func deployL1Contracts(config *DeployConfig, backend *backends.SimulatedBackend)
predeploys.DevL2OutputOracleAddr, predeploys.DevL2OutputOracleAddr,
config.PortalGuardian, config.PortalGuardian,
true, // _paused true, // _paused
predeploys.DevSystemConfigAddr,
}, },
}, },
{ {
...@@ -342,6 +376,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep ...@@ -342,6 +376,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
deployment.Args[3].(common.Hash), deployment.Args[3].(common.Hash),
deployment.Args[4].(uint64), deployment.Args[4].(uint64),
deployment.Args[5].(common.Address), deployment.Args[5].(common.Address),
deployment.Args[6].(bindings.ResourceMeteringResourceConfig),
) )
case "L2OutputOracle": case "L2OutputOracle":
_, tx, _, err = bindings.DeployL2OutputOracle( _, tx, _, err = bindings.DeployL2OutputOracle(
...@@ -362,6 +397,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep ...@@ -362,6 +397,7 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
deployment.Args[0].(common.Address), deployment.Args[0].(common.Address),
deployment.Args[1].(common.Address), deployment.Args[1].(common.Address),
deployment.Args[2].(bool), deployment.Args[2].(bool),
deployment.Args[3].(common.Address),
) )
case "L1CrossDomainMessenger": case "L1CrossDomainMessenger":
_, tx, _, err = bindings.DeployL1CrossDomainMessenger( _, tx, _, err = bindings.DeployL1CrossDomainMessenger(
...@@ -421,6 +457,15 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep ...@@ -421,6 +457,15 @@ func l1Deployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, dep
func upgradeProxy(backend *backends.SimulatedBackend, opts *bind.TransactOpts, proxyAddr common.Address, implAddr common.Address, callData []byte) (*types.Transaction, error) { func upgradeProxy(backend *backends.SimulatedBackend, opts *bind.TransactOpts, proxyAddr common.Address, implAddr common.Address, callData []byte) (*types.Transaction, error) {
var tx *types.Transaction var tx *types.Transaction
code, err := backend.CodeAt(context.Background(), implAddr, nil)
if err != nil {
return nil, err
}
if len(code) == 0 {
return nil, fmt.Errorf("no code at %s", implAddr)
}
proxy, err := bindings.NewProxy(proxyAddr, backend) proxy, err := bindings.NewProxy(proxyAddr, backend)
if err != nil { if err != nil {
return nil, err return nil, err
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
...@@ -100,6 +101,30 @@ func TestBuildL1DeveloperGenesis(t *testing.T) { ...@@ -100,6 +101,30 @@ func TestBuildL1DeveloperGenesis(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "Wrapped Ether", name) require.Equal(t, "Wrapped Ether", name)
sysCfg, err := bindings.NewSystemConfig(predeploys.DevSystemConfigAddr, sim)
require.NoError(t, err)
cfg, err := sysCfg.ResourceConfig(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, cfg, defaultResourceConfig)
owner, err = sysCfg.Owner(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, owner, config.FinalSystemOwner)
overhead, err := sysCfg.Overhead(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, overhead.Uint64(), config.GasPriceOracleOverhead)
scalar, err := sysCfg.Scalar(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, scalar.Uint64(), config.GasPriceOracleScalar)
batcherHash, err := sysCfg.BatcherHash(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, common.Hash(batcherHash), config.BatchSenderAddress.Hash())
gasLimit, err := sysCfg.GasLimit(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, gasLimit, uint64(config.L2GenesisBlockGasLimit))
unsafeBlockSigner, err := sysCfg.UnsafeBlockSigner(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, unsafeBlockSigner, config.P2PSequencerAddress)
// test that we can do deposits, etc. // test that we can do deposits, etc.
priv, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") priv, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
require.NoError(t, err) require.NoError(t, err)
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"l1BlockTime": 15, "l1BlockTime": 15,
"l1GenesisBlockNonce": "0x0", "l1GenesisBlockNonce": "0x0",
"cliqueSignerAddress": "0x0000000000000000000000000000000000000000", "cliqueSignerAddress": "0x0000000000000000000000000000000000000000",
"l1GenesisBlockGasLimit": "0xe4e1c0", "l1GenesisBlockGasLimit": "0x1c9c380",
"l1GenesisBlockDifficulty": "0x1", "l1GenesisBlockDifficulty": "0x1",
"finalSystemOwner": "0x0000000000000000000000000000000000000111", "finalSystemOwner": "0x0000000000000000000000000000000000000111",
"portalGuardian": "0x0000000000000000000000000000000000000112", "portalGuardian": "0x0000000000000000000000000000000000000112",
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
"l1GenesisBlockTimestamp": "0x0", "l1GenesisBlockTimestamp": "0x0",
"l1GenesisBlockBaseFeePerGas": "0x3b9aca00", "l1GenesisBlockBaseFeePerGas": "0x3b9aca00",
"l2GenesisBlockNonce": "0x0", "l2GenesisBlockNonce": "0x0",
"l2GenesisBlockGasLimit": "0xe4e1c0", "l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockDifficulty": "0x1", "l2GenesisBlockDifficulty": "0x1",
"l2GenesisBlockMixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "l2GenesisBlockMixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"l2GenesisBlockNumber": "0x0", "l2GenesisBlockNumber": "0x0",
......
...@@ -6,11 +6,13 @@ import ( ...@@ -6,11 +6,13 @@ import (
"math/big" "math/big"
"time" "time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
...@@ -27,11 +29,14 @@ type ProposerCfg struct { ...@@ -27,11 +29,14 @@ type ProposerCfg struct {
} }
type L2Proposer struct { type L2Proposer struct {
log log.Logger log log.Logger
l1 *ethclient.Client l1 *ethclient.Client
driver *proposer.L2OutputSubmitter driver *proposer.L2OutputSubmitter
address common.Address address common.Address
lastTx common.Hash privKey *ecdsa.PrivateKey
signer opcrypto.SignerFn
contractAddr common.Address
lastTx common.Hash
} }
func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Client, rollupCl *sources.RollupClient) *L2Proposer { func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Client, rollupCl *sources.RollupClient) *L2Proposer {
...@@ -52,6 +57,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl ...@@ -52,6 +57,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
NumConfirmations: 1, NumConfirmations: 1,
SafeAbortNonceTooLowCount: 4, SafeAbortNonceTooLowCount: 4,
From: from, From: from,
ChainID: big.NewInt(420),
// Signer is loaded in `proposer.NewL2OutputSubmitter` // Signer is loaded in `proposer.NewL2OutputSubmitter`
}, },
L1Client: l1, L1Client: l1,
...@@ -65,13 +71,56 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl ...@@ -65,13 +71,56 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
require.NoError(t, err) require.NoError(t, err)
return &L2Proposer{ return &L2Proposer{
log: log, log: log,
l1: l1, l1: l1,
driver: dr, driver: dr,
address: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey), address: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey),
privKey: cfg.ProposerKey,
signer: proposerCfg.TxManagerConfig.Signer,
contractAddr: cfg.OutputOracleAddr,
} }
} }
// sendTx reimplements creating & sending transactions because we need to do the final send as async in
// the action tests while we do it synchronously in the real system.
func (p *L2Proposer) sendTx(t Testing, data []byte) {
gasTipCap := big.NewInt(2 * params.GWei)
pendingHeader, err := p.l1.HeaderByNumber(t.Ctx(), big.NewInt(-1))
require.NoError(t, err, "need l1 pending header for gas price estimation")
gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(pendingHeader.BaseFee, big.NewInt(2)))
chainID, err := p.l1.ChainID(t.Ctx())
require.NoError(t, err)
nonce, err := p.l1.NonceAt(t.Ctx(), p.address, nil)
require.NoError(t, err)
gasLimit, err := p.l1.EstimateGas(t.Ctx(), ethereum.CallMsg{
From: p.address,
To: &p.contractAddr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Data: data,
})
require.NoError(t, err)
rawTx := &types.DynamicFeeTx{
Nonce: nonce,
To: &p.contractAddr,
Data: data,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Gas: gasLimit,
ChainID: chainID,
}
tx, err := types.SignNewTx(p.privKey, types.LatestSignerForChainID(chainID), rawTx)
require.NoError(t, err, "need to sign tx")
err = p.l1.SendTransaction(t.Ctx(), tx)
require.NoError(t, err, "need to send tx")
p.lastTx = tx.Hash()
}
func (p *L2Proposer) CanPropose(t Testing) bool { func (p *L2Proposer) CanPropose(t Testing) bool {
_, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx()) _, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx())
require.NoError(t, err) require.NoError(t, err)
...@@ -85,15 +134,12 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) { ...@@ -85,15 +134,12 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) {
} }
require.NoError(t, err) require.NoError(t, err)
tx, err := p.driver.CreateProposalTx(t.Ctx(), output) txData, err := p.driver.ProposeL2OutputTxData(output)
require.NoError(t, err) require.NoError(t, err)
// Note: Use L1 instead of the output submitter's transaction manager because // Note: Use L1 instead of the output submitter's transaction manager because
// this is non-blocking while the txmgr is blocking & deadlocks the tests // this is non-blocking while the txmgr is blocking & deadlocks the tests
err = p.l1.SendTransaction(t.Ctx(), tx) p.sendTx(t, txData)
require.NoError(t, err)
p.lastTx = tx.Hash()
} }
func (p *L2Proposer) LastProposalTx() common.Hash { func (p *L2Proposer) LastProposalTx() common.Hash {
......
...@@ -79,7 +79,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams { ...@@ -79,7 +79,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
L1GenesisBlockNonce: 0, L1GenesisBlockNonce: 0,
CliqueSignerAddress: common.Address{}, // proof of stake, no clique CliqueSignerAddress: common.Address{}, // proof of stake, no clique
L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()), L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()),
L1GenesisBlockGasLimit: 15_000_000, L1GenesisBlockGasLimit: 30_000_000,
L1GenesisBlockDifficulty: uint64ToBig(1), L1GenesisBlockDifficulty: uint64ToBig(1),
L1GenesisBlockMixHash: common.Hash{}, L1GenesisBlockMixHash: common.Hash{},
L1GenesisBlockCoinbase: common.Address{}, L1GenesisBlockCoinbase: common.Address{},
...@@ -90,7 +90,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams { ...@@ -90,7 +90,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
FinalizationPeriodSeconds: 12, FinalizationPeriodSeconds: 12,
L2GenesisBlockNonce: 0, L2GenesisBlockNonce: 0,
L2GenesisBlockGasLimit: 15_000_000, L2GenesisBlockGasLimit: 30_000_000,
L2GenesisBlockDifficulty: uint64ToBig(0), L2GenesisBlockDifficulty: uint64ToBig(0),
L2GenesisBlockMixHash: common.Hash{}, L2GenesisBlockMixHash: common.Hash{},
L2GenesisBlockNumber: 0, L2GenesisBlockNumber: 0,
......
...@@ -270,9 +270,12 @@ func TestMigration(t *testing.T) { ...@@ -270,9 +270,12 @@ func TestMigration(t *testing.T) {
snapLog.SetHandler(log.DiscardHandler()) snapLog.SetHandler(log.DiscardHandler())
rollupNodeConfig := &node.Config{ rollupNodeConfig := &node.Config{
L1: &node.L1EndpointConfig{ L1: &node.L1EndpointConfig{
L1NodeAddr: forkedL1URL, L1NodeAddr: forkedL1URL,
L1TrustRPC: false, L1TrustRPC: false,
L1RPCKind: sources.RPCKindBasic, L1RPCKind: sources.RPCKindBasic,
RateLimit: 0,
BatchSize: 20,
HttpPollInterval: 12 * time.Second,
}, },
L2: &node.L2EndpointConfig{ L2: &node.L2EndpointConfig{
L2EngineAddr: gethNode.HTTPAuthEndpoint(), L2EngineAddr: gethNode.HTTPAuthEndpoint(),
...@@ -329,6 +332,8 @@ func TestMigration(t *testing.T) { ...@@ -329,6 +332,8 @@ func TestMigration(t *testing.T) {
L1EthRpc: forkedL1URL, L1EthRpc: forkedL1URL,
L2EthRpc: gethNode.WSEndpoint(), L2EthRpc: gethNode.WSEndpoint(),
RollupRpc: rollupNode.HTTPEndpoint(), RollupRpc: rollupNode.HTTPEndpoint(),
TxManagerTimeout: 10 * time.Minute,
OfflineGasEstimation: true,
MaxChannelDuration: 1, MaxChannelDuration: 1,
MaxL1TxSize: 120_000, MaxL1TxSize: 120_000,
TargetL1TxSize: 100_000, TargetL1TxSize: 100_000,
......
...@@ -75,7 +75,7 @@ func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, e ...@@ -75,7 +75,7 @@ func NewOpGeth(t *testing.T, ctx context.Context, cfg *SystemConfig) (*OpGeth, e
require.Nil(t, node.Start()) require.Nil(t, node.Start())
auth := rpc.WithHTTPAuth(gn.NewJWTAuth(cfg.JWTSecret)) auth := rpc.WithHTTPAuth(gn.NewJWTAuth(cfg.JWTSecret))
l2Node, err := client.NewRPC(ctx, logger, node.WSAuthEndpoint(), auth) l2Node, err := client.NewRPC(ctx, logger, node.WSAuthEndpoint(), client.WithGethRPCOptions(auth))
require.Nil(t, err) require.Nil(t, err)
// Finally create the engine client // Finally create the engine client
......
...@@ -75,7 +75,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -75,7 +75,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
L1GenesisBlockNonce: 4660, L1GenesisBlockNonce: 4660,
CliqueSignerAddress: addresses.CliqueSigner, CliqueSignerAddress: addresses.CliqueSigner,
L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()), L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()),
L1GenesisBlockGasLimit: 8_000_000, L1GenesisBlockGasLimit: 30_000_000,
L1GenesisBlockDifficulty: uint642big(1), L1GenesisBlockDifficulty: uint642big(1),
L1GenesisBlockMixHash: common.Hash{}, L1GenesisBlockMixHash: common.Hash{},
L1GenesisBlockCoinbase: common.Address{}, L1GenesisBlockCoinbase: common.Address{},
...@@ -85,7 +85,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -85,7 +85,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
L1GenesisBlockBaseFeePerGas: uint642big(7), L1GenesisBlockBaseFeePerGas: uint642big(7),
L2GenesisBlockNonce: 0, L2GenesisBlockNonce: 0,
L2GenesisBlockGasLimit: 8_000_000, L2GenesisBlockGasLimit: 30_000_000,
L2GenesisBlockDifficulty: uint642big(1), L2GenesisBlockDifficulty: uint642big(1),
L2GenesisBlockMixHash: common.Hash{}, L2GenesisBlockMixHash: common.Hash{},
L2GenesisBlockNumber: 0, L2GenesisBlockNumber: 0,
...@@ -413,9 +413,12 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -413,9 +413,12 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
l2EndpointConfig = sys.Nodes[name].HTTPAuthEndpoint() l2EndpointConfig = sys.Nodes[name].HTTPAuthEndpoint()
} }
rollupCfg.L1 = &rollupNode.L1EndpointConfig{ rollupCfg.L1 = &rollupNode.L1EndpointConfig{
L1NodeAddr: l1EndpointConfig, L1NodeAddr: l1EndpointConfig,
L1TrustRPC: false, L1TrustRPC: false,
L1RPCKind: sources.RPCKindBasic, L1RPCKind: sources.RPCKindBasic,
RateLimit: 0,
BatchSize: 20,
HttpPollInterval: time.Duration(cfg.DeployConfig.L1BlockTime) * time.Second / 10,
} }
rollupCfg.L2 = &rollupNode.L2EndpointConfig{ rollupCfg.L2 = &rollupNode.L2EndpointConfig{
L2EngineAddr: l2EndpointConfig, L2EngineAddr: l2EndpointConfig,
...@@ -590,10 +593,13 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) { ...@@ -590,10 +593,13 @@ func (cfg SystemConfig) Start(_opts ...SystemConfigOption) (*System, error) {
} }
// Batch Submitter // Batch Submitter
txManagerTimeout := 10 * time.Minute
sys.BatchSubmitter, err = bss.NewBatchSubmitterFromCLIConfig(bss.CLIConfig{ sys.BatchSubmitter, err = bss.NewBatchSubmitterFromCLIConfig(bss.CLIConfig{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(), L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(), L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(), RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
TxManagerTimeout: txManagerTimeout,
OfflineGasEstimation: true,
MaxChannelDuration: 1, MaxChannelDuration: 1,
MaxL1TxSize: 120_000, MaxL1TxSize: 120_000,
TargetL1TxSize: 100_000, TargetL1TxSize: 100_000,
......
...@@ -304,7 +304,7 @@ func TestPendingGasLimit(t *testing.T) { ...@@ -304,7 +304,7 @@ func TestPendingGasLimit(t *testing.T) {
cfg := DefaultSystemConfig(t) cfg := DefaultSystemConfig(t)
// configure the L2 gas limit to be high, and the pending gas limits to be lower for resource saving. // configure the L2 gas limit to be high, and the pending gas limits to be lower for resource saving.
cfg.DeployConfig.L2GenesisBlockGasLimit = 20_000_000 cfg.DeployConfig.L2GenesisBlockGasLimit = 30_000_000
cfg.GethOptions["sequencer"] = []GethOption{ cfg.GethOptions["sequencer"] = []GethOption{
func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error { func(ethCfg *ethconfig.Config, nodeCfg *node.Config) error {
ethCfg.Miner.GasCeil = 10_000_000 ethCfg.Miner.GasCeil = 10_000_000
...@@ -342,8 +342,8 @@ func TestPendingGasLimit(t *testing.T) { ...@@ -342,8 +342,8 @@ func TestPendingGasLimit(t *testing.T) {
for { for {
checkGasLimit(l2Seq, big.NewInt(-1), 10_000_000) checkGasLimit(l2Seq, big.NewInt(-1), 10_000_000)
checkGasLimit(l2Verif, big.NewInt(-1), 9_000_000) checkGasLimit(l2Verif, big.NewInt(-1), 9_000_000)
checkGasLimit(l2Seq, nil, 20_000_000) checkGasLimit(l2Seq, nil, 30_000_000)
latestVerifHeader := checkGasLimit(l2Verif, nil, 20_000_000) latestVerifHeader := checkGasLimit(l2Verif, nil, 30_000_000)
// Stop once the verifier passes genesis: // Stop once the verifier passes genesis:
// this implies we checked a new block from the sequencer, on both sequencer and verifier nodes. // this implies we checked a new block from the sequencer, on both sequencer and verifier nodes.
......
...@@ -57,7 +57,7 @@ func NewPollingClient(ctx context.Context, lgr log.Logger, c RPC, opts ...Wrappe ...@@ -57,7 +57,7 @@ func NewPollingClient(ctx context.Context, lgr log.Logger, c RPC, opts ...Wrappe
res := &PollingClient{ res := &PollingClient{
c: c, c: c,
lgr: lgr, lgr: lgr,
pollRate: 250 * time.Millisecond, pollRate: 12 * time.Second,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
pollReqCh: make(chan struct{}, 1), pollReqCh: make(chan struct{}, 1),
......
package client
import (
"context"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/rpc"
"golang.org/x/time/rate"
)
// RateLimitingClient is a wrapper around a pure RPC that implements a global rate-limit on requests.
type RateLimitingClient struct {
c RPC
rl *rate.Limiter
}
// NewRateLimitingClient implements a global rate-limit for all RPC requests.
// A limit of N will ensure that over a long enough time-frame the given number of tokens per second is targeted.
// Burst limits how far off we can be from the target, by specifying how many requests are allowed at once.
func NewRateLimitingClient(c RPC, limit rate.Limit, burst int) *RateLimitingClient {
return &RateLimitingClient{c: c, rl: rate.NewLimiter(limit, burst)}
}
func (b *RateLimitingClient) Close() {
b.c.Close()
}
func (b *RateLimitingClient) CallContext(ctx context.Context, result any, method string, args ...any) error {
if err := b.rl.Wait(ctx); err != nil {
return err
}
cCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return b.c.CallContext(cCtx, result, method, args...)
}
func (b *RateLimitingClient) BatchCallContext(ctx context.Context, batch []rpc.BatchElem) error {
if err := b.rl.WaitN(ctx, len(batch)); err != nil {
return err
}
cCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
return b.c.BatchCallContext(cCtx, batch)
}
func (b *RateLimitingClient) EthSubscribe(ctx context.Context, channel any, args ...any) (ethereum.Subscription, error) {
if err := b.rl.Wait(ctx); err != nil {
return nil, err
}
return b.c.EthSubscribe(ctx, channel, args...)
}
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"golang.org/x/time/rate"
"github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
...@@ -24,27 +25,85 @@ type RPC interface { ...@@ -24,27 +25,85 @@ type RPC interface {
EthSubscribe(ctx context.Context, channel any, args ...any) (ethereum.Subscription, error) EthSubscribe(ctx context.Context, channel any, args ...any) (ethereum.Subscription, error)
} }
type rpcConfig struct {
gethRPCOptions []rpc.ClientOption
httpPollInterval time.Duration
backoffAttempts int
limit float64
burst int
}
type RPCOption func(cfg *rpcConfig) error
// WithDialBackoff configures the number of attempts for the initial dial to the RPC,
// attempts are executed with an exponential backoff strategy.
func WithDialBackoff(attempts int) RPCOption {
return func(cfg *rpcConfig) error {
cfg.backoffAttempts = attempts
return nil
}
}
// WithHttpPollInterval configures the RPC to poll at the given rate, in case RPC subscriptions are not available.
func WithHttpPollInterval(duration time.Duration) RPCOption {
return func(cfg *rpcConfig) error {
cfg.httpPollInterval = duration
return nil
}
}
// WithGethRPCOptions passes the list of go-ethereum RPC options to the internal RPC instance.
func WithGethRPCOptions(gethRPCOptions ...rpc.ClientOption) RPCOption {
return func(cfg *rpcConfig) error {
cfg.gethRPCOptions = append(cfg.gethRPCOptions, gethRPCOptions...)
return nil
}
}
// WithRateLimit configures the RPC to target the given rate limit (in requests / second).
// See NewRateLimitingClient for more details.
func WithRateLimit(rateLimit float64, burst int) RPCOption {
return func(cfg *rpcConfig) error {
cfg.limit = rateLimit
cfg.burst = burst
return nil
}
}
// NewRPC returns the correct client.RPC instance for a given RPC url. // NewRPC returns the correct client.RPC instance for a given RPC url.
func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...rpc.ClientOption) (RPC, error) { func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...RPCOption) (RPC, error) {
underlying, err := DialRPCClientWithBackoff(ctx, lgr, addr, opts...) var cfg rpcConfig
for i, opt := range opts {
if err := opt(&cfg); err != nil {
return nil, fmt.Errorf("rpc option %d failed to apply to RPC config: %w", i, err)
}
}
if cfg.backoffAttempts < 1 { // default to at least 1 attempt, or it always fails to dial.
cfg.backoffAttempts = 1
}
underlying, err := dialRPCClientWithBackoff(ctx, lgr, addr, cfg.backoffAttempts, cfg.gethRPCOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
wrapped := &BaseRPCClient{ var wrapped RPC = &BaseRPCClient{c: underlying}
c: underlying,
if cfg.limit != 0 {
wrapped = NewRateLimitingClient(wrapped, rate.Limit(cfg.limit), cfg.burst)
} }
if httpRegex.MatchString(addr) { if httpRegex.MatchString(addr) {
return NewPollingClient(ctx, lgr, wrapped), nil wrapped = NewPollingClient(ctx, lgr, wrapped, WithPollRate(cfg.httpPollInterval))
} }
return wrapped, nil return wrapped, nil
} }
// Dials a JSON-RPC endpoint repeatedly, with a backoff, until a client connection is established. Auth is optional. // Dials a JSON-RPC endpoint repeatedly, with a backoff, until a client connection is established. Auth is optional.
func DialRPCClientWithBackoff(ctx context.Context, log log.Logger, addr string, opts ...rpc.ClientOption) (*rpc.Client, error) { func dialRPCClientWithBackoff(ctx context.Context, log log.Logger, addr string, attempts int, opts ...rpc.ClientOption) (*rpc.Client, error) {
bOff := backoff.Exponential() bOff := backoff.Exponential()
var ret *rpc.Client var ret *rpc.Client
err := backoff.DoCtx(ctx, 10, bOff, func() error { err := backoff.DoCtx(ctx, attempts, bOff, func() error {
client, err := rpc.DialOptions(ctx, addr, opts...) client, err := rpc.DialOptions(ctx, addr, opts...)
if err != nil { if err != nil {
if client == nil { if client == nil {
......
...@@ -52,6 +52,12 @@ jq "select(.valid_data == false)|.tx.hash" $TX_DIR ...@@ -52,6 +52,12 @@ jq "select(.valid_data == false)|.tx.hash" $TX_DIR
# Select all channels that are not ready and then get the id and inclusion block & tx hash of the first frame. # Select all channels that are not ready and then get the id and inclusion block & tx hash of the first frame.
jq "select(.is_ready == false)|[.id, .frames[0].inclusion_block, .frames[0].transaction_hash]" $CHANNEL_DIR jq "select(.is_ready == false)|[.id, .frames[0].inclusion_block, .frames[0].transaction_hash]" $CHANNEL_DIR
# Show all of the frames in a channel without seeing the batches or frame data
jq 'del(.batches)|del(.frames[]|.frame.data)' $CHANNEL_FILE
# Show all batches (without timestamps) in a channel
jq '.batches|del(.[]|.Transactions)' $CHANNEL_FILE
``` ```
......
...@@ -14,10 +14,10 @@ import ( ...@@ -14,10 +14,10 @@ import (
// Flags // Flags
const envVarPrefix = "OP_NODE_" const envVarPrefix = "OP_NODE"
func prefixEnvVar(name string) string { func prefixEnvVar(name string) string {
return envVarPrefix + name return envVarPrefix + "_" + name
} }
var ( var (
...@@ -75,6 +75,24 @@ var ( ...@@ -75,6 +75,24 @@ var (
return &out return &out
}(), }(),
} }
L1RPCRateLimit = cli.Float64Flag{
Name: "l1.rpc-rate-limit",
Usage: "Optional self-imposed global rate-limit on L1 RPC requests, specified in requests / second. Disabled if set to 0.",
EnvVar: prefixEnvVar("L1_RPC_RATE_LIMIT"),
Value: 0,
}
L1RPCMaxBatchSize = cli.IntFlag{
Name: "l1.rpc-max-batch-size",
Usage: "Maximum number of RPC requests to bundle, e.g. during L1 blocks receipt fetching. The L1 RPC rate limit counts this as N items, but allows it to burst at once.",
EnvVar: prefixEnvVar("L1_RPC_MAX_BATCH_SIZE"),
Value: 20,
}
L1HTTPPollInterval = cli.DurationFlag{
Name: "l1.http-poll-interval",
Usage: "Polling interval for latest-block subscription when using an HTTP RPC provider. Ignored for other types of RPC endpoints.",
EnvVar: prefixEnvVar("L1_HTTP_POLL_INTERVAL"),
Value: time.Second * 12,
}
L2EngineJWTSecret = cli.StringFlag{ L2EngineJWTSecret = cli.StringFlag{
Name: "l2.jwt-secret", Name: "l2.jwt-secret",
Usage: "Path to JWT secret key. Keys are 32 bytes, hex encoded in a file. A new key will be generated if left empty.", Usage: "Path to JWT secret key. Keys are 32 bytes, hex encoded in a file. A new key will be generated if left empty.",
...@@ -196,6 +214,9 @@ var optionalFlags = []cli.Flag{ ...@@ -196,6 +214,9 @@ var optionalFlags = []cli.Flag{
Network, Network,
L1TrustRPC, L1TrustRPC,
L1RPCProviderKind, L1RPCProviderKind,
L1RPCRateLimit,
L1RPCMaxBatchSize,
L1HTTPPollInterval,
L2EngineJWTSecret, L2EngineJWTSecret,
VerifierL1Confs, VerifierL1Confs,
SequencerEnabledFlag, SequencerEnabledFlag,
......
...@@ -4,8 +4,10 @@ import ( ...@@ -4,8 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -15,14 +17,14 @@ import ( ...@@ -15,14 +17,14 @@ import (
type L2EndpointSetup interface { type L2EndpointSetup interface {
// Setup a RPC client to a L2 execution engine to process rollup blocks with. // Setup a RPC client to a L2 execution engine to process rollup blocks with.
Setup(ctx context.Context, log log.Logger) (cl client.RPC, err error) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (cl client.RPC, rpcCfg *sources.EngineClientConfig, err error)
Check() error Check() error
} }
type L2SyncEndpointSetup interface { type L2SyncEndpointSetup interface {
// Setup a RPC client to another L2 node to sync L2 blocks from. // Setup a RPC client to another L2 node to sync L2 blocks from.
// It may return a nil client with nil error if RPC based sync is not enabled. // It may return a nil client with nil error if RPC based sync is not enabled.
Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (cl client.RPC, rpcCfg *sources.SyncClientConfig, err error)
Check() error Check() error
} }
...@@ -30,7 +32,8 @@ type L1EndpointSetup interface { ...@@ -30,7 +32,8 @@ type L1EndpointSetup interface {
// Setup a RPC client to a L1 node to pull rollup input-data from. // Setup a RPC client to a L1 node to pull rollup input-data from.
// The results of the RPC client may be trusted for faster processing, or strictly validated. // The results of the RPC client may be trusted for faster processing, or strictly validated.
// The kind of the RPC may be non-basic, to optimize RPC usage. // The kind of the RPC may be non-basic, to optimize RPC usage.
Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, kind sources.RPCProviderKind, err error) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (cl client.RPC, rpcCfg *sources.L1ClientConfig, err error)
Check() error
} }
type L2EndpointConfig struct { type L2EndpointConfig struct {
...@@ -51,17 +54,17 @@ func (cfg *L2EndpointConfig) Check() error { ...@@ -51,17 +54,17 @@ func (cfg *L2EndpointConfig) Check() error {
return nil return nil
} }
func (cfg *L2EndpointConfig) Setup(ctx context.Context, log log.Logger) (client.RPC, error) { func (cfg *L2EndpointConfig) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.EngineClientConfig, error) {
if err := cfg.Check(); err != nil { if err := cfg.Check(); err != nil {
return nil, err return nil, nil, err
} }
auth := rpc.WithHTTPAuth(gn.NewJWTAuth(cfg.L2EngineJWTSecret)) auth := rpc.WithHTTPAuth(gn.NewJWTAuth(cfg.L2EngineJWTSecret))
l2Node, err := client.NewRPC(ctx, log, cfg.L2EngineAddr, auth) l2Node, err := client.NewRPC(ctx, log, cfg.L2EngineAddr, client.WithGethRPCOptions(auth))
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return l2Node, nil return l2Node, sources.EngineClientDefaultConfig(rollupCfg), nil
} }
// PreparedL2Endpoints enables testing with in-process pre-setup RPC connections to L2 engines // PreparedL2Endpoints enables testing with in-process pre-setup RPC connections to L2 engines
...@@ -78,8 +81,8 @@ func (p *PreparedL2Endpoints) Check() error { ...@@ -78,8 +81,8 @@ func (p *PreparedL2Endpoints) Check() error {
var _ L2EndpointSetup = (*PreparedL2Endpoints)(nil) var _ L2EndpointSetup = (*PreparedL2Endpoints)(nil)
func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger) (client.RPC, error) { func (p *PreparedL2Endpoints) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.EngineClientConfig, error) {
return p.Client, nil return p.Client, sources.EngineClientDefaultConfig(rollupCfg), nil
} }
// L2SyncEndpointConfig contains configuration for the fallback sync endpoint // L2SyncEndpointConfig contains configuration for the fallback sync endpoint
...@@ -93,16 +96,16 @@ var _ L2SyncEndpointSetup = (*L2SyncEndpointConfig)(nil) ...@@ -93,16 +96,16 @@ var _ L2SyncEndpointSetup = (*L2SyncEndpointConfig)(nil)
// Setup creates an RPC client to sync from. // Setup creates an RPC client to sync from.
// It will return nil without error if no sync method is configured. // It will return nil without error if no sync method is configured.
func (cfg *L2SyncEndpointConfig) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error) { func (cfg *L2SyncEndpointConfig) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.SyncClientConfig, error) {
if cfg.L2NodeAddr == "" { if cfg.L2NodeAddr == "" {
return nil, false, nil return nil, nil, nil
} }
l2Node, err := client.NewRPC(ctx, log, cfg.L2NodeAddr) l2Node, err := client.NewRPC(ctx, log, cfg.L2NodeAddr)
if err != nil { if err != nil {
return nil, false, err return nil, nil, err
} }
return l2Node, cfg.TrustRPC, nil return l2Node, sources.SyncClientDefaultConfig(rollupCfg, cfg.TrustRPC), nil
} }
func (cfg *L2SyncEndpointConfig) Check() error { func (cfg *L2SyncEndpointConfig) Check() error {
...@@ -118,8 +121,8 @@ type PreparedL2SyncEndpoint struct { ...@@ -118,8 +121,8 @@ type PreparedL2SyncEndpoint struct {
var _ L2SyncEndpointSetup = (*PreparedL2SyncEndpoint)(nil) var _ L2SyncEndpointSetup = (*PreparedL2SyncEndpoint)(nil)
func (cfg *PreparedL2SyncEndpoint) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, err error) { func (cfg *PreparedL2SyncEndpoint) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.SyncClientConfig, error) {
return cfg.Client, cfg.TrustRPC, nil return cfg.Client, sources.SyncClientDefaultConfig(rollupCfg, cfg.TrustRPC), nil
} }
func (cfg *PreparedL2SyncEndpoint) Check() error { func (cfg *PreparedL2SyncEndpoint) Check() error {
...@@ -137,16 +140,48 @@ type L1EndpointConfig struct { ...@@ -137,16 +140,48 @@ type L1EndpointConfig struct {
// L1RPCKind identifies the RPC provider kind that serves the RPC, // L1RPCKind identifies the RPC provider kind that serves the RPC,
// to inform the optimal usage of the RPC for transaction receipts fetching. // to inform the optimal usage of the RPC for transaction receipts fetching.
L1RPCKind sources.RPCProviderKind L1RPCKind sources.RPCProviderKind
// RateLimit specifies a self-imposed rate-limit on L1 requests. 0 is no rate-limit.
RateLimit float64
// BatchSize specifies the maximum batch-size, which also applies as L1 rate-limit burst amount (if set).
BatchSize int
// HttpPollInterval specifies the interval between polling for the latest L1 block,
// when the RPC is detected to be an HTTP type.
// It is recommended to use websockets or IPC for efficient following of the changing block.
// Setting this to 0 disables polling.
HttpPollInterval time.Duration
} }
var _ L1EndpointSetup = (*L1EndpointConfig)(nil) var _ L1EndpointSetup = (*L1EndpointConfig)(nil)
func (cfg *L1EndpointConfig) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, kind sources.RPCProviderKind, err error) { func (cfg *L1EndpointConfig) Check() error {
l1Node, err := client.NewRPC(ctx, log, cfg.L1NodeAddr) if cfg.BatchSize < 1 || cfg.BatchSize > 500 {
return fmt.Errorf("batch size is invalid or unreasonable: %d", cfg.BatchSize)
}
if cfg.RateLimit < 0 {
return fmt.Errorf("rate limit cannot be negative")
}
return nil
}
func (cfg *L1EndpointConfig) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.L1ClientConfig, error) {
opts := []client.RPCOption{
client.WithHttpPollInterval(cfg.HttpPollInterval),
client.WithDialBackoff(10),
}
if cfg.RateLimit != 0 {
opts = append(opts, client.WithRateLimit(cfg.RateLimit, cfg.BatchSize))
}
l1Node, err := client.NewRPC(ctx, log, cfg.L1NodeAddr, opts...)
if err != nil { if err != nil {
return nil, false, sources.RPCKindBasic, fmt.Errorf("failed to dial L1 address (%s): %w", cfg.L1NodeAddr, err) return nil, nil, fmt.Errorf("failed to dial L1 address (%s): %w", cfg.L1NodeAddr, err)
} }
return l1Node, cfg.L1TrustRPC, cfg.L1RPCKind, nil rpcCfg := sources.L1ClientDefaultConfig(rollupCfg, cfg.L1TrustRPC, cfg.L1RPCKind)
rpcCfg.MaxRequestsPerBatch = cfg.BatchSize
return l1Node, rpcCfg, nil
} }
// PreparedL1Endpoint enables testing with an in-process pre-setup RPC connection to L1 // PreparedL1Endpoint enables testing with an in-process pre-setup RPC connection to L1
...@@ -158,6 +193,14 @@ type PreparedL1Endpoint struct { ...@@ -158,6 +193,14 @@ type PreparedL1Endpoint struct {
var _ L1EndpointSetup = (*PreparedL1Endpoint)(nil) var _ L1EndpointSetup = (*PreparedL1Endpoint)(nil)
func (p *PreparedL1Endpoint) Setup(ctx context.Context, log log.Logger) (cl client.RPC, trust bool, kind sources.RPCProviderKind, err error) { func (p *PreparedL1Endpoint) Setup(ctx context.Context, log log.Logger, rollupCfg *rollup.Config) (client.RPC, *sources.L1ClientConfig, error) {
return p.Client, p.TrustRPC, p.RPCProviderKind, nil return p.Client, sources.L1ClientDefaultConfig(rollupCfg, p.TrustRPC, p.RPCProviderKind), nil
}
func (cfg *PreparedL1Endpoint) Check() error {
if cfg.Client == nil {
return errors.New("rpc client cannot be nil")
}
return nil
} }
...@@ -116,14 +116,13 @@ func (n *OpNode) initTracer(ctx context.Context, cfg *Config) error { ...@@ -116,14 +116,13 @@ func (n *OpNode) initTracer(ctx context.Context, cfg *Config) error {
} }
func (n *OpNode) initL1(ctx context.Context, cfg *Config) error { func (n *OpNode) initL1(ctx context.Context, cfg *Config) error {
l1Node, trustRPC, rpcProvKind, err := cfg.L1.Setup(ctx, n.log) l1Node, rpcCfg, err := cfg.L1.Setup(ctx, n.log, &cfg.Rollup)
if err != nil { if err != nil {
return fmt.Errorf("failed to get L1 RPC client: %w", err) return fmt.Errorf("failed to get L1 RPC client: %w", err)
} }
n.l1Source, err = sources.NewL1Client( n.l1Source, err = sources.NewL1Client(
client.NewInstrumentedRPC(l1Node, n.metrics), n.log, n.metrics.L1SourceCache, client.NewInstrumentedRPC(l1Node, n.metrics), n.log, n.metrics.L1SourceCache, rpcCfg)
sources.L1ClientDefaultConfig(&cfg.Rollup, trustRPC, rpcProvKind))
if err != nil { if err != nil {
return fmt.Errorf("failed to create L1 source: %w", err) return fmt.Errorf("failed to create L1 source: %w", err)
} }
...@@ -184,14 +183,13 @@ func (n *OpNode) initRuntimeConfig(ctx context.Context, cfg *Config) error { ...@@ -184,14 +183,13 @@ func (n *OpNode) initRuntimeConfig(ctx context.Context, cfg *Config) error {
} }
func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger) error { func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger) error {
rpcClient, err := cfg.L2.Setup(ctx, n.log) rpcClient, rpcCfg, err := cfg.L2.Setup(ctx, n.log, &cfg.Rollup)
if err != nil { if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client: %w", err) return fmt.Errorf("failed to setup L2 execution-engine RPC client: %w", err)
} }
n.l2Source, err = sources.NewEngineClient( n.l2Source, err = sources.NewEngineClient(
client.NewInstrumentedRPC(rpcClient, n.metrics), n.log, n.metrics.L2SourceCache, client.NewInstrumentedRPC(rpcClient, n.metrics), n.log, n.metrics.L2SourceCache, rpcCfg,
sources.EngineClientDefaultConfig(&cfg.Rollup),
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create Engine client: %w", err) return fmt.Errorf("failed to create Engine client: %w", err)
...@@ -207,17 +205,14 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger ...@@ -207,17 +205,14 @@ func (n *OpNode) initL2(ctx context.Context, cfg *Config, snapshotLog log.Logger
} }
func (n *OpNode) initRPCSync(ctx context.Context, cfg *Config) error { func (n *OpNode) initRPCSync(ctx context.Context, cfg *Config) error {
rpcSyncClient, trustRPC, err := cfg.L2Sync.Setup(ctx, n.log) rpcSyncClient, rpcCfg, err := cfg.L2Sync.Setup(ctx, n.log, &cfg.Rollup)
if err != nil { if err != nil {
return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err) return fmt.Errorf("failed to setup L2 execution-engine RPC client for backup sync: %w", err)
} }
if rpcSyncClient == nil { // if no RPC client is configured to sync from, then don't add the RPC sync client if rpcSyncClient == nil { // if no RPC client is configured to sync from, then don't add the RPC sync client
return nil return nil
} }
syncClient, err := sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, rpcCfg)
config := sources.SyncClientDefaultConfig(&cfg.Rollup, trustRPC)
syncClient, err := sources.NewSyncClient(n.OnUnsafeL2Payload, rpcSyncClient, n.log, n.metrics.L2SourceCache, config)
if err != nil { if err != nil {
return fmt.Errorf("failed to create sync client: %w", err) return fmt.Errorf("failed to create sync client: %w", err)
} }
......
...@@ -115,7 +115,7 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -115,7 +115,7 @@ func TestOutputAtBlock(t *testing.T) {
require.NoError(t, server.Start()) require.NoError(t, server.Start())
defer server.Stop() defer server.Stop()
client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String()) client, err := rpcclient.NewRPC(context.Background(), log, "http://"+server.Addr().String(), rpcclient.WithDialBackoff(3))
require.NoError(t, err) require.NoError(t, err)
var out *eth.OutputResponse var out *eth.OutputResponse
...@@ -147,7 +147,7 @@ func TestVersion(t *testing.T) { ...@@ -147,7 +147,7 @@ func TestVersion(t *testing.T) {
assert.NoError(t, server.Start()) assert.NoError(t, server.Start())
defer server.Stop() defer server.Stop()
client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String()) client, err := rpcclient.NewRPC(context.Background(), log, "http://"+server.Addr().String(), rpcclient.WithDialBackoff(3))
assert.NoError(t, err) assert.NoError(t, err)
var out string var out string
...@@ -189,7 +189,7 @@ func TestSyncStatus(t *testing.T) { ...@@ -189,7 +189,7 @@ func TestSyncStatus(t *testing.T) {
assert.NoError(t, server.Start()) assert.NoError(t, server.Start())
defer server.Stop() defer server.Stop()
client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String()) client, err := rpcclient.NewRPC(context.Background(), log, "http://"+server.Addr().String(), rpcclient.WithDialBackoff(3))
assert.NoError(t, err) assert.NoError(t, err)
var out *eth.SyncStatus var out *eth.SyncStatus
......
...@@ -117,6 +117,11 @@ func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer ...@@ -117,6 +117,11 @@ func NewScorer(peerGater PeerGater, peerStore Peerstore, metricer GossipMetricer
func (s *scorer) SnapshotHook() pubsub.ExtendedPeerScoreInspectFn { func (s *scorer) SnapshotHook() pubsub.ExtendedPeerScoreInspectFn {
return func(m map[peer.ID]*pubsub.PeerScoreSnapshot) { return func(m map[peer.ID]*pubsub.PeerScoreSnapshot) {
scoreMap := make(map[string]float64) scoreMap := make(map[string]float64)
// Zero out all bands.
for _, b := range s.bandScoreThresholds.bands {
scoreMap[b.band] = 0
}
// Now set the new scores.
for id, snap := range m { for id, snap := range m {
band := s.bandScoreThresholds.Bucket(snap.Score) band := s.bandScoreThresholds.Bucket(snap.Score)
scoreMap[band] += 1 scoreMap[band] += 1
......
...@@ -29,7 +29,7 @@ func (testSuite *PeerScorerTestSuite) SetupTest() { ...@@ -29,7 +29,7 @@ func (testSuite *PeerScorerTestSuite) SetupTest() {
testSuite.mockGater = &p2pMocks.PeerGater{} testSuite.mockGater = &p2pMocks.PeerGater{}
testSuite.mockStore = &p2pMocks.Peerstore{} testSuite.mockStore = &p2pMocks.Peerstore{}
testSuite.mockMetricer = &p2pMocks.GossipMetricer{} testSuite.mockMetricer = &p2pMocks.GossipMetricer{}
bandScorer, err := p2p.NewBandScorer("0:graylist;") bandScorer, err := p2p.NewBandScorer("-40:graylist;0:friend;")
testSuite.NoError(err) testSuite.NoError(err)
testSuite.bandScorer = bandScorer testSuite.bandScorer = bandScorer
testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError) testSuite.logger = testlog.Logger(testSuite.T(), log.LvlError)
...@@ -76,12 +76,13 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHook() { ...@@ -76,12 +76,13 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHook() {
inspectFn := scorer.SnapshotHook() inspectFn := scorer.SnapshotHook()
// Mock the peer gater call // Mock the peer gater call
testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-100)).Return(nil) testSuite.mockGater.On("Update", peer.ID("peer1"), float64(-100)).Return(nil).Once()
// The metricer should then be called with the peer score band map // The metricer should then be called with the peer score band map
testSuite.mockMetricer.On("SetPeerScores", map[string]float64{ testSuite.mockMetricer.On("SetPeerScores", map[string]float64{
"friend": 0,
"graylist": 1, "graylist": 1,
}).Return(nil) }).Return(nil).Once()
// Apply the snapshot // Apply the snapshot
snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{ snapshotMap := map[peer.ID]*pubsub.PeerScoreSnapshot{
...@@ -90,6 +91,23 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHook() { ...@@ -90,6 +91,23 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHook() {
}, },
} }
inspectFn(snapshotMap) inspectFn(snapshotMap)
// Change the peer score now to a different band
testSuite.mockGater.On("Update", peer.ID("peer1"), float64(0)).Return(nil).Once()
// The metricer should then be called with the peer score band map
testSuite.mockMetricer.On("SetPeerScores", map[string]float64{
"friend": 1,
"graylist": 0,
}).Return(nil).Once()
// Apply the snapshot
snapshotMap = map[peer.ID]*pubsub.PeerScoreSnapshot{
peer.ID("peer1"): {
Score: 0,
},
}
inspectFn(snapshotMap)
} }
// TestScorer_SnapshotHookBlocksPeer tests running the snapshot hook on the peer scorer with a peer score below the threshold. // TestScorer_SnapshotHookBlocksPeer tests running the snapshot hook on the peer scorer with a peer score below the threshold.
...@@ -109,6 +127,7 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHookBlocksPeer() { ...@@ -109,6 +127,7 @@ func (testSuite *PeerScorerTestSuite) TestScorer_SnapshotHookBlocksPeer() {
// The metricer should then be called with the peer score band map // The metricer should then be called with the peer score band map
testSuite.mockMetricer.On("SetPeerScores", map[string]float64{ testSuite.mockMetricer.On("SetPeerScores", map[string]float64{
"friend": 0,
"graylist": 1, "graylist": 1,
}).Return(nil) }).Return(nil)
......
...@@ -109,6 +109,9 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -109,6 +109,9 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
return nil, fmt.Errorf("failed to fetch current L2 forkchoice state: %w", err) return nil, fmt.Errorf("failed to fetch current L2 forkchoice state: %w", err)
} }
lgr.Info("Loaded current L2 heads", "unsafe", result.Unsafe, "safe", result.Safe, "finalized", result.Finalized,
"unsafe_origin", result.Unsafe.L1Origin, "unsafe_origin", result.Safe.L1Origin)
// Remember original unsafe block to determine reorg depth // Remember original unsafe block to determine reorg depth
prevUnsafe := result.Unsafe prevUnsafe := result.Unsafe
...@@ -134,6 +137,7 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -134,6 +137,7 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
// Exit, find-sync start should start over, to move to an available L1 chain with block-by-number / not-found case. // Exit, find-sync start should start over, to move to an available L1 chain with block-by-number / not-found case.
return nil, fmt.Errorf("failed to retrieve L1 block: %w", err) return nil, fmt.Errorf("failed to retrieve L1 block: %w", err)
} }
lgr.Info("Walking back L1Block by hash", "curr", l1Block, "next", b, "l2block", n)
l1Block = b l1Block = b
ahead = false ahead = false
} else if l1Block == (eth.L1BlockRef{}) || n.L1Origin.Hash != l1Block.Hash { } else if l1Block == (eth.L1BlockRef{}) || n.L1Origin.Hash != l1Block.Hash {
...@@ -145,9 +149,10 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -145,9 +149,10 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
} }
l1Block = b l1Block = b
ahead = notFound ahead = notFound
lgr.Info("Walking back L1Block by number", "curr", l1Block, "next", b, "l2block", n)
} }
lgr.Trace("walking sync start", "number", n.Number) lgr.Trace("walking sync start", "l2block", n)
// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin, // Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin,
// the L2 chain is building on the wrong L1 branch. // the L2 chain is building on the wrong L1 branch.
...@@ -201,6 +206,8 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain ...@@ -201,6 +206,8 @@ func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain
// Don't traverse further than the finalized head to find a safe head // Don't traverse further than the finalized head to find a safe head
if n.Number == result.Finalized.Number { if n.Number == result.Finalized.Number {
lgr.Info("Hit finalized L2 head, returning immediately", "unsafe", result.Unsafe, "safe", result.Safe,
"finalized", result.Finalized, "unsafe_origin", result.Unsafe.L1Origin, "unsafe_origin", result.Safe.L1Origin)
result.Safe = n result.Safe = n
return result, nil return result, nil
} }
......
...@@ -95,9 +95,12 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { ...@@ -95,9 +95,12 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) {
func NewL1EndpointConfig(ctx *cli.Context) *node.L1EndpointConfig { func NewL1EndpointConfig(ctx *cli.Context) *node.L1EndpointConfig {
return &node.L1EndpointConfig{ return &node.L1EndpointConfig{
L1NodeAddr: ctx.GlobalString(flags.L1NodeAddr.Name), L1NodeAddr: ctx.GlobalString(flags.L1NodeAddr.Name),
L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name), L1TrustRPC: ctx.GlobalBool(flags.L1TrustRPC.Name),
L1RPCKind: sources.RPCProviderKind(strings.ToLower(ctx.GlobalString(flags.L1RPCProviderKind.Name))), L1RPCKind: sources.RPCProviderKind(strings.ToLower(ctx.GlobalString(flags.L1RPCProviderKind.Name))),
RateLimit: ctx.GlobalFloat64(flags.L1RPCRateLimit.Name),
BatchSize: ctx.GlobalInt(flags.L1RPCMaxBatchSize.Name),
HttpPollInterval: ctx.Duration(flags.L1HTTPPollInterval.Name),
} }
} }
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/big" "math/big"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -56,6 +57,11 @@ type EthClientConfig struct { ...@@ -56,6 +57,11 @@ type EthClientConfig struct {
// RPCProviderKind is a hint at what type of RPC provider we are dealing with // RPCProviderKind is a hint at what type of RPC provider we are dealing with
RPCProviderKind RPCProviderKind RPCProviderKind RPCProviderKind
// Method reset duration defines how long we stick to available RPC methods,
// till we re-attempt the user-preferred methods.
// If this is 0 then the client does not fall back to less optimal but available methods.
MethodResetDuration time.Duration
} }
func (c *EthClientConfig) Check() error { func (c *EthClientConfig) Check() error {
...@@ -118,9 +124,25 @@ type EthClient struct { ...@@ -118,9 +124,25 @@ type EthClient struct {
// This may be modified concurrently, but we don't lock since it's a single // This may be modified concurrently, but we don't lock since it's a single
// uint64 that's not critical (fine to miss or mix up a modification) // uint64 that's not critical (fine to miss or mix up a modification)
availableReceiptMethods ReceiptsFetchingMethod availableReceiptMethods ReceiptsFetchingMethod
// lastMethodsReset tracks when availableReceiptMethods was last reset.
// When receipt-fetching fails it falls back to available methods,
// but periodically it will try to reset to the preferred optimal methods.
lastMethodsReset time.Time
// methodResetDuration defines how long we take till we reset lastMethodsReset
methodResetDuration time.Duration
} }
func (s *EthClient) PickReceiptsMethod(txCount uint64) ReceiptsFetchingMethod { func (s *EthClient) PickReceiptsMethod(txCount uint64) ReceiptsFetchingMethod {
if now := time.Now(); now.Sub(s.lastMethodsReset) > s.methodResetDuration {
m := AvailableReceiptsFetchingMethods(s.provKind)
if s.availableReceiptMethods != m {
s.log.Warn("resetting back RPC preferences, please review RPC provider kind setting", "kind", s.provKind.String())
}
s.availableReceiptMethods = m
s.lastMethodsReset = now
}
return PickBestReceiptsFetchingMethod(s.provKind, s.availableReceiptMethods, txCount) return PickBestReceiptsFetchingMethod(s.provKind, s.availableReceiptMethods, txCount)
} }
...@@ -128,7 +150,7 @@ func (s *EthClient) OnReceiptsMethodErr(m ReceiptsFetchingMethod, err error) { ...@@ -128,7 +150,7 @@ func (s *EthClient) OnReceiptsMethodErr(m ReceiptsFetchingMethod, err error) {
if unusableMethod(err) { if unusableMethod(err) {
// clear the bit of the method that errored // clear the bit of the method that errored
s.availableReceiptMethods &^= m s.availableReceiptMethods &^= m
s.log.Warn("failed to use selected RPC method for receipt fetching, falling back to alternatives", s.log.Warn("failed to use selected RPC method for receipt fetching, temporarily falling back to alternatives",
"provider_kind", s.provKind, "failed_method", m, "fallback", s.availableReceiptMethods, "err", err) "provider_kind", s.provKind, "failed_method", m, "fallback", s.availableReceiptMethods, "err", err)
} else { } else {
s.log.Debug("failed to use selected RPC method for receipt fetching, but method does appear to be available, so we continue to use it", s.log.Debug("failed to use selected RPC method for receipt fetching, but method does appear to be available, so we continue to use it",
...@@ -155,6 +177,8 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co ...@@ -155,6 +177,8 @@ func NewEthClient(client client.RPC, log log.Logger, metrics caching.Metrics, co
headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize), headersCache: caching.NewLRUCache(metrics, "headers", config.HeadersCacheSize),
payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize), payloadsCache: caching.NewLRUCache(metrics, "payloads", config.PayloadsCacheSize),
availableReceiptMethods: AvailableReceiptsFetchingMethods(config.RPCProviderKind), availableReceiptMethods: AvailableReceiptsFetchingMethods(config.RPCProviderKind),
lastMethodsReset: time.Now(),
methodResetDuration: config.MethodResetDuration,
}, nil }, nil
} }
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -40,6 +41,7 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool, kind RPCProvide ...@@ -40,6 +41,7 @@ func L1ClientDefaultConfig(config *rollup.Config, trustRPC bool, kind RPCProvide
TrustRPC: trustRPC, TrustRPC: trustRPC,
MustBePostMerge: false, MustBePostMerge: false,
RPCProviderKind: kind, RPCProviderKind: kind,
MethodResetDuration: time.Minute,
}, },
// Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors. // Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors.
L1BlockRefsCacheSize: fullSpan, L1BlockRefsCacheSize: fullSpan,
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -50,6 +51,7 @@ func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig ...@@ -50,6 +51,7 @@ func L2ClientDefaultConfig(config *rollup.Config, trustRPC bool) *L2ClientConfig
TrustRPC: trustRPC, TrustRPC: trustRPC,
MustBePostMerge: true, MustBePostMerge: true,
RPCProviderKind: RPCKindBasic, RPCProviderKind: RPCKindBasic,
MethodResetDuration: time.Minute,
}, },
// Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors. // Not bounded by span, to cover find-sync-start range fully for speedy recovery after errors.
L2BlockRefsCacheSize: fullSpan, L2BlockRefsCacheSize: fullSpan,
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-node/client" "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
...@@ -85,6 +86,7 @@ func (e *methodNotFoundError) Error() string { ...@@ -85,6 +86,7 @@ func (e *methodNotFoundError) Error() string {
type ReceiptsTestCase struct { type ReceiptsTestCase struct {
name string name string
providerKind RPCProviderKind providerKind RPCProviderKind
staticMethod bool
setup func(t *testing.T) (*rpcBlock, []ReceiptsRequest) setup func(t *testing.T) (*rpcBlock, []ReceiptsRequest)
} }
...@@ -142,6 +144,10 @@ func (tc *ReceiptsTestCase) Run(t *testing.T) { ...@@ -142,6 +144,10 @@ func (tc *ReceiptsTestCase) Run(t *testing.T) {
TrustRPC: false, TrustRPC: false,
MustBePostMerge: false, MustBePostMerge: false,
RPCProviderKind: tc.providerKind, RPCProviderKind: tc.providerKind,
MethodResetDuration: time.Minute,
}
if tc.staticMethod { // if static, instantly reset, for fast clock-independent testing
testCfg.MethodResetDuration = 0
} }
logger := testlog.Logger(t, log.LvlError) logger := testlog.Logger(t, log.LvlError)
ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg) ethCl, err := NewEthClient(client.NewBaseRPCClient(cl), logger, nil, testCfg)
...@@ -226,6 +232,12 @@ func TestEthClient_FetchReceipts(t *testing.T) { ...@@ -226,6 +232,12 @@ func TestEthClient_FetchReceipts(t *testing.T) {
providerKind: RPCKindAlchemy, providerKind: RPCKindAlchemy,
setup: fallbackCase(30, AlchemyGetTransactionReceipts), setup: fallbackCase(30, AlchemyGetTransactionReceipts),
}, },
{
name: "alchemy sticky",
providerKind: RPCKindAlchemy,
staticMethod: true,
setup: fallbackCase(30, AlchemyGetTransactionReceipts, AlchemyGetTransactionReceipts),
},
{ {
name: "alchemy fallback 1", name: "alchemy fallback 1",
providerKind: RPCKindAlchemy, providerKind: RPCKindAlchemy,
......
...@@ -245,3 +245,23 @@ func RandomBlockPrependTxs(rng *rand.Rand, txCount int, ptxs ...*types.Transacti ...@@ -245,3 +245,23 @@ func RandomBlockPrependTxs(rng *rand.Rand, txCount int, ptxs ...*types.Transacti
} }
return block, receipts return block, receipts
} }
func RandomOutputResponse(rng *rand.Rand) *eth.OutputResponse {
return &eth.OutputResponse{
Version: eth.Bytes32(RandomHash(rng)),
OutputRoot: eth.Bytes32(RandomHash(rng)),
BlockRef: RandomL2BlockRef(rng),
WithdrawalStorageRoot: RandomHash(rng),
StateRoot: RandomHash(rng),
Status: &eth.SyncStatus{
CurrentL1: RandomBlockRef(rng),
CurrentL1Finalized: RandomBlockRef(rng),
HeadL1: RandomBlockRef(rng),
SafeL1: RandomBlockRef(rng),
FinalizedL1: RandomBlockRef(rng),
UnsafeL2: RandomL2BlockRef(rng),
SafeL2: RandomL2BlockRef(rng),
FinalizedL2: RandomL2BlockRef(rng),
},
}
}
package proposer
import (
"math/big"
"math/rand"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
// setupL2OutputOracle deploys the L2 Output Oracle contract to a simulated backend
func setupL2OutputOracle() (common.Address, *bind.TransactOpts, *backends.SimulatedBackend, *bindings.L2OutputOracle, error) {
privateKey, err := crypto.GenerateKey()
from := crypto.PubkeyToAddress(privateKey.PublicKey)
if err != nil {
return common.Address{}, nil, nil, nil, err
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
if err != nil {
return common.Address{}, nil, nil, nil, err
}
backend := backends.NewSimulatedBackend(core.GenesisAlloc{from: {Balance: big.NewInt(params.Ether)}}, 50_000_000)
_, _, contract, err := bindings.DeployL2OutputOracle(
opts,
backend,
big.NewInt(10),
big.NewInt(2),
big.NewInt(0),
big.NewInt(0),
from,
common.Address{0xdd},
big.NewInt(100))
if err != nil {
return common.Address{}, nil, nil, nil, err
}
return from, opts, backend, contract, nil
}
// TestManualABIPacking ensure that the manual ABI packing is the same as going through the bound contract.
// We don't use the contract to transact because it does not fit our transaction management scheme, but
// we want to make sure that we don't incorrectly create the transaction data.
func TestManualABIPacking(t *testing.T) {
_, opts, _, contract, err := setupL2OutputOracle()
require.NoError(t, err)
rng := rand.New(rand.NewSource(1234))
abi, err := bindings.L2OutputOracleMetaData.GetAbi()
require.NoError(t, err)
output := testutils.RandomOutputResponse(rng)
txData, err := proposeL2OutputTxData(abi, output)
require.NoError(t, err)
// set a gas limit to disable gas estimation. The invariantes that the L2OO tries to uphold
// are not maintained in this test.
opts.GasLimit = 100_000
tx, err := contract.ProposeL2Output(
opts,
output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number),
output.Status.CurrentL1.Hash,
new(big.Int).SetUint64(output.Status.CurrentL1.Number))
require.NoError(t, err)
require.Equal(t, txData, tx.Data())
}
This diff is collapsed.
...@@ -29,6 +29,9 @@ func Do(maxAttempts int, strategy Strategy, op Operation) error { ...@@ -29,6 +29,9 @@ func Do(maxAttempts int, strategy Strategy, op Operation) error {
} }
func DoCtx(ctx context.Context, maxAttempts int, strategy Strategy, op Operation) error { func DoCtx(ctx context.Context, maxAttempts int, strategy Strategy, op Operation) error {
if maxAttempts < 1 {
return fmt.Errorf("need at least 1 attempt to run op, but have %d max attempts", maxAttempts)
}
var attempt int var attempt int
reattemptCh := make(chan struct{}, 1) reattemptCh := make(chan struct{}, 1)
......
// Code generated by mockery v2.23.1. DO NOT EDIT.
package mocks
import (
context "context"
txmgr "github.com/ethereum-optimism/optimism/op-service/txmgr"
mock "github.com/stretchr/testify/mock"
types "github.com/ethereum/go-ethereum/core/types"
)
// TxManager is an autogenerated mock type for the TxManager type
type TxManager struct {
mock.Mock
}
// Send provides a mock function with given fields: ctx, candidate
func (_m *TxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (*types.Receipt, error) {
ret := _m.Called(ctx, candidate)
var r0 *types.Receipt
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, txmgr.TxCandidate) (*types.Receipt, error)); ok {
return rf(ctx, candidate)
}
if rf, ok := ret.Get(0).(func(context.Context, txmgr.TxCandidate) *types.Receipt); ok {
r0 = rf(ctx, candidate)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.Receipt)
}
}
if rf, ok := ret.Get(1).(func(context.Context, txmgr.TxCandidate) error); ok {
r1 = rf(ctx, candidate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewTxManager interface {
mock.TestingT
Cleanup(func())
}
// NewTxManager creates a new instance of TxManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewTxManager(t mockConstructorTestingTNewTxManager) *TxManager {
mock := &TxManager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
package txmgr
import (
"fmt"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
type priceBumpTest struct {
prevGasTip int64
prevBasefee int64
newGasTip int64
newBasefee int64
expectedTip int64
expectedFC int64
}
func (tc *priceBumpTest) run(t *testing.T) {
prevFC := CalcGasFeeCap(big.NewInt(tc.prevBasefee), big.NewInt(tc.prevGasTip))
lgr := testlog.Logger(t, log.LvlCrit)
tip, fc := updateFees(big.NewInt(tc.prevGasTip), prevFC, big.NewInt(tc.newGasTip), big.NewInt(tc.newBasefee), lgr)
require.Equal(t, tc.expectedTip, tip.Int64(), "tip must be as expected")
require.Equal(t, tc.expectedFC, fc.Int64(), "fee cap must be as expected")
}
func TestUpdateFees(t *testing.T) {
tests := []priceBumpTest{
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 900,
expectedTip: 100, expectedFC: 2100,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 1000,
expectedTip: 115, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 100, newBasefee: 1001,
expectedTip: 115, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 900,
expectedTip: 115, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 90, newBasefee: 1010,
expectedTip: 115, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 101, newBasefee: 2000,
expectedTip: 115, expectedFC: 4115,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 900,
expectedTip: 120, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1100,
expectedTip: 120, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1140,
expectedTip: 120, expectedFC: 2415,
},
{
prevGasTip: 100, prevBasefee: 1000,
newGasTip: 120, newBasefee: 1200,
expectedTip: 120, expectedFC: 2520,
},
}
for i, test := range tests {
i := i
test := test
t.Run(fmt.Sprint(i), test.run)
}
}
This diff is collapsed.
This diff is collapsed.
...@@ -123,6 +123,8 @@ services: ...@@ -123,6 +123,8 @@ services:
OP_BATCHER_L1_ETH_RPC: http://l1:8545 OP_BATCHER_L1_ETH_RPC: http://l1:8545
OP_BATCHER_L2_ETH_RPC: http://l2:8545 OP_BATCHER_L2_ETH_RPC: http://l2:8545
OP_BATCHER_ROLLUP_RPC: http://op-node:8545 OP_BATCHER_ROLLUP_RPC: http://op-node:8545
TX_MANAGER_TIMEOUT: 10m
OFFLINE_GAS_ESTIMATION: false
OP_BATCHER_MAX_CHANNEL_DURATION: 1 OP_BATCHER_MAX_CHANNEL_DURATION: 1
OP_BATCHER_MAX_L1_TX_SIZE_BYTES: 120000 OP_BATCHER_MAX_L1_TX_SIZE_BYTES: 120000
OP_BATCHER_TARGET_L1_TX_SIZE_BYTES: 100000 OP_BATCHER_TARGET_L1_TX_SIZE_BYTES: 100000
......
...@@ -117,6 +117,10 @@ FROM base as replica-healthcheck ...@@ -117,6 +117,10 @@ FROM base as replica-healthcheck
WORKDIR /opt/optimism/packages/replica-healthcheck WORKDIR /opt/optimism/packages/replica-healthcheck
ENTRYPOINT ["npm", "run", "start"] ENTRYPOINT ["npm", "run", "start"]
FROM base as balance-mon
WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["npm", "run", "start:balance-mon"]
FROM base as drippie-mon FROM base as drippie-mon
WORKDIR /opt/optimism/packages/chain-mon WORKDIR /opt/optimism/packages/chain-mon
ENTRYPOINT ["npm", "run", "start:drippie-mon"] ENTRYPOINT ["npm", "run", "start:drippie-mon"]
......
###############################################################################
# ↓ balance-mon ↓ #
###############################################################################
# RPC pointing to network to monitor balances on
BALANCE_MON__RPC=
# JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ]
BALANCE_MON__ACCOUNTS=
############################################################################### ###############################################################################
# ↓ drippie-mon ↓ # # ↓ drippie-mon ↓ #
############################################################################### ###############################################################################
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"dist/*" "dist/*"
], ],
"scripts": { "scripts": {
"start:balance-mon": "ts-node ./src/balance-mon/service.ts",
"start:drippie-mon": "ts-node ./src/drippie-mon/service.ts", "start:drippie-mon": "ts-node ./src/drippie-mon/service.ts",
"start:wd-mon": "ts-node ./src/wd-mon/service.ts", "start:wd-mon": "ts-node ./src/wd-mon/service.ts",
"test:coverage": "echo 'No tests defined.'", "test:coverage": "echo 'No tests defined.'",
......
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
} from '@eth-optimism/common-ts'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import { version } from '../../package.json'
type BalanceMonOptions = {
rpc: Provider
accounts: string
}
type BalanceMonMetrics = {
balances: Gauge
unexpectedRpcErrors: Counter
}
type BalanceMonState = {
accounts: Array<{ address: string; nickname: string }>
}
export class BalanceMonService extends BaseServiceV2<
BalanceMonOptions,
BalanceMonMetrics,
BalanceMonState
> {
constructor(options?: Partial<BalanceMonOptions & StandardOptions>) {
super({
version,
name: 'balance-mon',
loop: true,
options: {
loopIntervalMs: 60_000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
accounts: {
validator: validators.str,
desc: 'JSON array of [{ address, nickname }] to monitor balances of',
public: true,
},
},
metricsSpec: {
balances: {
type: Gauge,
desc: 'Balances of addresses',
labels: ['address', 'nickname'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
this.state.accounts = JSON.parse(this.options.accounts)
}
protected async main(): Promise<void> {
for (const account of this.state.accounts) {
let balance: ethers.BigNumber
try {
balance = await this.options.rpc.getBalance(account.address)
} catch (err) {
this.logger.info(`got unexpected RPC error`, {
section: 'balances',
name: 'getBalance',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'balances',
name: 'getBalance',
})
continue
}
this.logger.info(`got balance`, {
address: account.address,
nickname: account.nickname,
balance: balance.toString(),
})
// Parse the balance as an integer instead of via toNumber() to avoid ethers throwing an
// an error. We might get rounding errors but we don't need perfect precision here, just a
// generally accurate sense for what the current balance is.
this.metrics.balances.set(
{ address: account.address, nickname: account.nickname },
parseInt(balance.toString(), 10)
)
}
}
}
if (require.main === module) {
const service = new BalanceMonService()
service.run()
}
export * from './balance-mon/service'
export * from './drippie-mon/service' export * from './drippie-mon/service'
export * from './wd-mon/service' export * from './wd-mon/service'
This diff is collapsed.
...@@ -69,17 +69,18 @@ ...@@ -69,17 +69,18 @@
➡ contracts/L1/SystemConfig.sol:SystemConfig ➡ contracts/L1/SystemConfig.sol:SystemConfig
======================= =======================
| Name | Type | Slot | Offset | Bytes | Contract | | Name | Type | Slot | Offset | Bytes | Contract |
|---------------|-------------|------|--------|-------|--------------------------------------------| |-----------------|----------------------------------------|------|--------|-------|--------------------------------------------|
| _initialized | uint8 | 0 | 0 | 1 | contracts/L1/SystemConfig.sol:SystemConfig | | _initialized | uint8 | 0 | 0 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| _initializing | bool | 0 | 1 | 1 | contracts/L1/SystemConfig.sol:SystemConfig | | _initializing | bool | 0 | 1 | 1 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[50] | 1 | 0 | 1600 | contracts/L1/SystemConfig.sol:SystemConfig | | __gap | uint256[50] | 1 | 0 | 1600 | contracts/L1/SystemConfig.sol:SystemConfig |
| _owner | address | 51 | 0 | 20 | contracts/L1/SystemConfig.sol:SystemConfig | | _owner | address | 51 | 0 | 20 | contracts/L1/SystemConfig.sol:SystemConfig |
| __gap | uint256[49] | 52 | 0 | 1568 | contracts/L1/SystemConfig.sol:SystemConfig | | __gap | uint256[49] | 52 | 0 | 1568 | contracts/L1/SystemConfig.sol:SystemConfig |
| overhead | uint256 | 101 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig | | overhead | uint256 | 101 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| scalar | uint256 | 102 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig | | scalar | uint256 | 102 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| batcherHash | bytes32 | 103 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig | | batcherHash | bytes32 | 103 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
| gasLimit | uint64 | 104 | 0 | 8 | contracts/L1/SystemConfig.sol:SystemConfig | | gasLimit | uint64 | 104 | 0 | 8 | contracts/L1/SystemConfig.sol:SystemConfig |
| _resourceConfig | struct ResourceMetering.ResourceConfig | 105 | 0 | 32 | contracts/L1/SystemConfig.sol:SystemConfig |
======================= =======================
➡ contracts/legacy/DeployerWhitelist.sol:DeployerWhitelist ➡ contracts/legacy/DeployerWhitelist.sol:DeployerWhitelist
......
...@@ -57,7 +57,7 @@ We work on this repository with a combination of [Hardhat](https://hardhat.org) ...@@ -57,7 +57,7 @@ We work on this repository with a combination of [Hardhat](https://hardhat.org)
1. Install Foundry by following [the instructions located here](https://getfoundry.sh/). 1. Install Foundry by following [the instructions located here](https://getfoundry.sh/).
A specific version must be used. A specific version must be used.
```shell ```shell
foundryup -C 2ff99025abade470a795724c10648c800a41025e foundryup -C da2392e58bb8a7fefeba46b40c4df1afad8ccd22
``` ```
2. Install node modules with yarn (v1) and Node.js (16+): 2. Install node modules with yarn (v1) and Node.js (16+):
......
...@@ -4,6 +4,7 @@ pragma solidity 0.8.15; ...@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { SafeCall } from "../libraries/SafeCall.sol"; import { SafeCall } from "../libraries/SafeCall.sol";
import { L2OutputOracle } from "./L2OutputOracle.sol"; import { L2OutputOracle } from "./L2OutputOracle.sol";
import { SystemConfig } from "./SystemConfig.sol";
import { Constants } from "../libraries/Constants.sol"; import { Constants } from "../libraries/Constants.sol";
import { Types } from "../libraries/Types.sol"; import { Types } from "../libraries/Types.sol";
import { Hashing } from "../libraries/Hashing.sol"; import { Hashing } from "../libraries/Hashing.sol";
...@@ -44,10 +45,15 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { ...@@ -44,10 +45,15 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
/** /**
* @notice Address of the L2OutputOracle. * @notice Address of the L2OutputOracle contract.
*/ */
L2OutputOracle public immutable L2_ORACLE; L2OutputOracle public immutable L2_ORACLE;
/**
* @notice Address of the SystemConfig contract.
*/
SystemConfig public immutable SYSTEM_CONFIG;
/** /**
* @notice Address that has the ability to pause and unpause withdrawals. * @notice Address that has the ability to pause and unpause withdrawals.
*/ */
...@@ -135,19 +141,22 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { ...@@ -135,19 +141,22 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
} }
/** /**
* @custom:semver 1.2.0 * @custom:semver 1.3.0
* *
* @param _l2Oracle Address of the L2OutputOracle contract. * @param _l2Oracle Address of the L2OutputOracle contract.
* @param _guardian Address that can pause deposits and withdrawals. * @param _guardian Address that can pause deposits and withdrawals.
* @param _paused Sets the contract's pausability state. * @param _paused Sets the contract's pausability state.
* @param _config Address of the SystemConfig contract.
*/ */
constructor( constructor(
L2OutputOracle _l2Oracle, L2OutputOracle _l2Oracle,
address _guardian, address _guardian,
bool _paused bool _paused,
) Semver(1, 2, 0) { SystemConfig _config
) Semver(1, 3, 0) {
L2_ORACLE = _l2Oracle; L2_ORACLE = _l2Oracle;
GUARDIAN = _guardian; GUARDIAN = _guardian;
SYSTEM_CONFIG = _config;
initialize(_paused); initialize(_paused);
} }
...@@ -197,6 +206,21 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver { ...@@ -197,6 +206,21 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
// Intentionally empty. // Intentionally empty.
} }
/**
* @notice Getter for the resource config. Used internally by the ResourceMetering
* contract. The SystemConfig is the source of truth for the resource config.
*
* @return ResourceMetering.ResourceConfig
*/
function _resourceConfig()
internal
view
override
returns (ResourceMetering.ResourceConfig memory)
{
return SYSTEM_CONFIG.resourceConfig();
}
/** /**
* @notice Proves a withdrawal transaction. * @notice Proves a withdrawal transaction.
* *
......
...@@ -28,44 +28,34 @@ abstract contract ResourceMetering is Initializable { ...@@ -28,44 +28,34 @@ abstract contract ResourceMetering is Initializable {
} }
/** /**
* @notice Maximum amount of the resource that can be used within this block. * @notice Represents the configuration for the EIP-1559 based curve for the deposit gas
* This value cannot be larger than the L2 block gas limit. * market. These values should be set with care as it is possible to set them in
*/ * a way that breaks the deposit gas market. The target resource limit is defined as
int256 public constant MAX_RESOURCE_LIMIT = 20_000_000; * maxResourceLimit / elasticityMultiplier. This struct was designed to fit within a
* single word. There is additional space for additions in the future.
/** *
* @notice Along with the resource limit, determines the target resource limit. * @custom:field maxResourceLimit Represents the maximum amount of deposit gas that
*/ * can be purchased per block.
int256 public constant ELASTICITY_MULTIPLIER = 10; * @custom:field elasticityMultiplier Determines the target resource limit along with
* the resource limit.
/** * @custom:field baseFeeMaxChangeDenominator Determines max change on fee per block.
* @notice Target amount of the resource that should be used within this block. * @custom:field minimumBaseFee The min deposit base fee, it is clamped to this
*/ * value.
int256 public constant TARGET_RESOURCE_LIMIT = MAX_RESOURCE_LIMIT / ELASTICITY_MULTIPLIER; * @custom:field systemTxMaxGas The amount of gas supplied to the system
* transaction. This should be set to the same number
/** * that the op-node sets as the gas limit for the
* @notice Denominator that determines max change on fee per block. * system transaction.
*/ * @custom:field maximumBaseFee The max deposit base fee, it is clamped to this
int256 public constant BASE_FEE_MAX_CHANGE_DENOMINATOR = 8; * value.
/**
* @notice Minimum base fee value, cannot go lower than this.
*/
int256 public constant MINIMUM_BASE_FEE = 1 gwei;
/**
* @notice Maximum base fee value, cannot go higher than this.
* It is possible for the MAXIMUM_BASE_FEE to raise to a value
* that is so large it will consume the entire gas limit of
* an L1 block.
*/
int256 public constant MAXIMUM_BASE_FEE = int256(uint256(type(uint128).max));
/**
* @notice Initial base fee value. This value must be smaller than the
* MAXIMUM_BASE_FEE.
*/ */
uint128 public constant INITIAL_BASE_FEE = 1 gwei; struct ResourceConfig {
uint32 maxResourceLimit;
uint8 elasticityMultiplier;
uint8 baseFeeMaxChangeDenominator;
uint32 minimumBaseFee;
uint32 systemTxMaxGas;
uint128 maximumBaseFee;
}
/** /**
* @notice EIP-1559 style gas parameters. * @notice EIP-1559 style gas parameters.
...@@ -102,20 +92,25 @@ abstract contract ResourceMetering is Initializable { ...@@ -102,20 +92,25 @@ abstract contract ResourceMetering is Initializable {
function _metered(uint64 _amount, uint256 _initialGas) internal { function _metered(uint64 _amount, uint256 _initialGas) internal {
// Update block number and base fee if necessary. // Update block number and base fee if necessary.
uint256 blockDiff = block.number - params.prevBlockNum; uint256 blockDiff = block.number - params.prevBlockNum;
ResourceConfig memory config = _resourceConfig();
int256 targetResourceLimit = int256(uint256(config.maxResourceLimit)) /
int256(uint256(config.elasticityMultiplier));
if (blockDiff > 0) { if (blockDiff > 0) {
// Handle updating EIP-1559 style gas parameters. We use EIP-1559 to restrict the rate // Handle updating EIP-1559 style gas parameters. We use EIP-1559 to restrict the rate
// at which deposits can be created and therefore limit the potential for deposits to // at which deposits can be created and therefore limit the potential for deposits to
// spam the L2 system. Fee scheme is very similar to EIP-1559 with minor changes. // spam the L2 system. Fee scheme is very similar to EIP-1559 with minor changes.
int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - TARGET_RESOURCE_LIMIT; int256 gasUsedDelta = int256(uint256(params.prevBoughtGas)) - targetResourceLimit;
int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) / int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) /
(TARGET_RESOURCE_LIMIT * BASE_FEE_MAX_CHANGE_DENOMINATOR); (targetResourceLimit * int256(uint256(config.baseFeeMaxChangeDenominator)));
// Update base fee by adding the base fee delta and clamp the resulting value between // Update base fee by adding the base fee delta and clamp the resulting value between
// min and max. // min and max.
int256 newBaseFee = Arithmetic.clamp({ int256 newBaseFee = Arithmetic.clamp({
_value: int256(uint256(params.prevBaseFee)) + baseFeeDelta, _value: int256(uint256(params.prevBaseFee)) + baseFeeDelta,
_min: MINIMUM_BASE_FEE, _min: int256(uint256(config.minimumBaseFee)),
_max: MAXIMUM_BASE_FEE _max: int256(uint256(config.maximumBaseFee))
}); });
// If we skipped more than one block, we also need to account for every empty block. // If we skipped more than one block, we also need to account for every empty block.
...@@ -128,11 +123,11 @@ abstract contract ResourceMetering is Initializable { ...@@ -128,11 +123,11 @@ abstract contract ResourceMetering is Initializable {
newBaseFee = Arithmetic.clamp({ newBaseFee = Arithmetic.clamp({
_value: Arithmetic.cdexp({ _value: Arithmetic.cdexp({
_coefficient: newBaseFee, _coefficient: newBaseFee,
_denominator: BASE_FEE_MAX_CHANGE_DENOMINATOR, _denominator: int256(uint256(config.baseFeeMaxChangeDenominator)),
_exponent: int256(blockDiff - 1) _exponent: int256(blockDiff - 1)
}), }),
_min: MINIMUM_BASE_FEE, _min: int256(uint256(config.minimumBaseFee)),
_max: MAXIMUM_BASE_FEE _max: int256(uint256(config.maximumBaseFee))
}); });
} }
...@@ -145,7 +140,7 @@ abstract contract ResourceMetering is Initializable { ...@@ -145,7 +140,7 @@ abstract contract ResourceMetering is Initializable {
// Make sure we can actually buy the resource amount requested by the user. // Make sure we can actually buy the resource amount requested by the user.
params.prevBoughtGas += _amount; params.prevBoughtGas += _amount;
require( require(
int256(uint256(params.prevBoughtGas)) <= MAX_RESOURCE_LIMIT, int256(uint256(params.prevBoughtGas)) <= int256(uint256(config.maxResourceLimit)),
"ResourceMetering: cannot buy more gas than available gas limit" "ResourceMetering: cannot buy more gas than available gas limit"
); );
...@@ -168,6 +163,14 @@ abstract contract ResourceMetering is Initializable { ...@@ -168,6 +163,14 @@ abstract contract ResourceMetering is Initializable {
} }
} }
/**
* @notice Virtual function that returns the resource config. Contracts that inherit this
* contract must implement this function.
*
* @return ResourceConfig
*/
function _resourceConfig() internal virtual returns (ResourceConfig memory);
/** /**
* @notice Sets initial resource parameter values. This function must either be called by the * @notice Sets initial resource parameter values. This function must either be called by the
* initializer function of an upgradeable child contract. * initializer function of an upgradeable child contract.
...@@ -175,7 +178,7 @@ abstract contract ResourceMetering is Initializable { ...@@ -175,7 +178,7 @@ abstract contract ResourceMetering is Initializable {
// solhint-disable-next-line func-name-mixedcase // solhint-disable-next-line func-name-mixedcase
function __ResourceMetering_init() internal onlyInitializing { function __ResourceMetering_init() internal onlyInitializing {
params = ResourceParams({ params = ResourceParams({
prevBaseFee: INITIAL_BASE_FEE, prevBaseFee: 1 gwei,
prevBoughtGas: 0, prevBoughtGas: 0,
prevBlockNum: uint64(block.number) prevBlockNum: uint64(block.number)
}); });
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
OwnableUpgradeable OwnableUpgradeable
} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { Semver } from "../universal/Semver.sol"; import { Semver } from "../universal/Semver.sol";
import { ResourceMetering } from "./ResourceMetering.sol";
/** /**
* @title SystemConfig * @title SystemConfig
...@@ -49,12 +50,12 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -49,12 +50,12 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint64 public constant MINIMUM_GAS_LIMIT = 8_000_000; uint64 public constant MINIMUM_GAS_LIMIT = 8_000_000;
/** /**
* @notice Fixed L2 gas overhead. * @notice Fixed L2 gas overhead. Used as part of the L2 fee calculation.
*/ */
uint256 public overhead; uint256 public overhead;
/** /**
* @notice Dynamic L2 gas overhead. * @notice Dynamic L2 gas overhead. Used as part of the L2 fee calculation.
*/ */
uint256 public scalar; uint256 public scalar;
...@@ -65,10 +66,17 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -65,10 +66,17 @@ contract SystemConfig is OwnableUpgradeable, Semver {
bytes32 public batcherHash; bytes32 public batcherHash;
/** /**
* @notice L2 gas limit. * @notice L2 block gas limit.
*/ */
uint64 public gasLimit; uint64 public gasLimit;
/**
* @notice The configuration for the deposit fee market. Used by the OptimismPortal
* to meter the cost of buying L2 gas on L1. Set as internal and wrapped with a getter
* so that the struct is returned instead of a tuple.
*/
ResourceMetering.ResourceConfig internal _resourceConfig;
/** /**
* @notice Emitted when configuration is updated * @notice Emitted when configuration is updated
* *
...@@ -79,7 +87,7 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -79,7 +87,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data);
/** /**
* @custom:semver 1.0.1 * @custom:semver 1.1.0
* *
* @param _owner Initial owner of the contract. * @param _owner Initial owner of the contract.
* @param _overhead Initial overhead value. * @param _overhead Initial overhead value.
...@@ -87,6 +95,7 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -87,6 +95,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _batcherHash Initial batcher hash. * @param _batcherHash Initial batcher hash.
* @param _gasLimit Initial gas limit. * @param _gasLimit Initial gas limit.
* @param _unsafeBlockSigner Initial unsafe block signer address. * @param _unsafeBlockSigner Initial unsafe block signer address.
* @param _config Initial resource config.
*/ */
constructor( constructor(
address _owner, address _owner,
...@@ -94,13 +103,23 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -94,13 +103,23 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint256 _scalar, uint256 _scalar,
bytes32 _batcherHash, bytes32 _batcherHash,
uint64 _gasLimit, uint64 _gasLimit,
address _unsafeBlockSigner address _unsafeBlockSigner,
) Semver(1, 0, 1) { ResourceMetering.ResourceConfig memory _config
initialize(_owner, _overhead, _scalar, _batcherHash, _gasLimit, _unsafeBlockSigner); ) Semver(1, 1, 0) {
initialize({
_owner: _owner,
_overhead: _overhead,
_scalar: _scalar,
_batcherHash: _batcherHash,
_gasLimit: _gasLimit,
_unsafeBlockSigner: _unsafeBlockSigner,
_config: _config
});
} }
/** /**
* @notice Initializer. * @notice Initializer. The resource config must be set before the
* require check.
* *
* @param _owner Initial owner of the contract. * @param _owner Initial owner of the contract.
* @param _overhead Initial overhead value. * @param _overhead Initial overhead value.
...@@ -108,6 +127,7 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -108,6 +127,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _batcherHash Initial batcher hash. * @param _batcherHash Initial batcher hash.
* @param _gasLimit Initial gas limit. * @param _gasLimit Initial gas limit.
* @param _unsafeBlockSigner Initial unsafe block signer address. * @param _unsafeBlockSigner Initial unsafe block signer address.
* @param _config Initial ResourceConfig.
*/ */
function initialize( function initialize(
address _owner, address _owner,
...@@ -115,9 +135,9 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -115,9 +135,9 @@ contract SystemConfig is OwnableUpgradeable, Semver {
uint256 _scalar, uint256 _scalar,
bytes32 _batcherHash, bytes32 _batcherHash,
uint64 _gasLimit, uint64 _gasLimit,
address _unsafeBlockSigner address _unsafeBlockSigner,
ResourceMetering.ResourceConfig memory _config
) public initializer { ) public initializer {
require(_gasLimit >= MINIMUM_GAS_LIMIT, "SystemConfig: gas limit too low");
__Ownable_init(); __Ownable_init();
transferOwnership(_owner); transferOwnership(_owner);
overhead = _overhead; overhead = _overhead;
...@@ -125,6 +145,21 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -125,6 +145,21 @@ contract SystemConfig is OwnableUpgradeable, Semver {
batcherHash = _batcherHash; batcherHash = _batcherHash;
gasLimit = _gasLimit; gasLimit = _gasLimit;
_setUnsafeBlockSigner(_unsafeBlockSigner); _setUnsafeBlockSigner(_unsafeBlockSigner);
_setResourceConfig(_config);
require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low");
}
/**
* @notice Returns the minimum L2 gas limit that can be safely set for the system to
* operate. The L2 gas limit must be larger than or equal to the amount of
* gas that is allocated for deposits per block plus the amount of gas that
* is allocated for the system transaction.
* This function is used to determine if changes to parameters are safe.
*
* @return uint64
*/
function minimumGasLimit() public view returns (uint64) {
return uint64(_resourceConfig.maxResourceLimit) + uint64(_resourceConfig.systemTxMaxGas);
} }
/** /**
...@@ -188,7 +223,7 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -188,7 +223,7 @@ contract SystemConfig is OwnableUpgradeable, Semver {
* @param _gasLimit New gas limit. * @param _gasLimit New gas limit.
*/ */
function setGasLimit(uint64 _gasLimit) external onlyOwner { function setGasLimit(uint64 _gasLimit) external onlyOwner {
require(_gasLimit >= MINIMUM_GAS_LIMIT, "SystemConfig: gas limit too low"); require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low");
gasLimit = _gasLimit; gasLimit = _gasLimit;
bytes memory data = abi.encode(_gasLimit); bytes memory data = abi.encode(_gasLimit);
...@@ -207,4 +242,60 @@ contract SystemConfig is OwnableUpgradeable, Semver { ...@@ -207,4 +242,60 @@ contract SystemConfig is OwnableUpgradeable, Semver {
sstore(slot, _unsafeBlockSigner) sstore(slot, _unsafeBlockSigner)
} }
} }
/**
* @notice A getter for the resource config. Ensures that the struct is
* returned instead of a tuple.
*
* @return ResourceConfig
*/
function resourceConfig() external view returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig;
}
/**
* @notice An external setter for the resource config. In the future, this
* method may emit an event that the `op-node` picks up for when the
* resource config is changed.
*
* @param _config The new resource config values.
*/
function setResourceConfig(ResourceMetering.ResourceConfig memory _config) external onlyOwner {
_setResourceConfig(_config);
}
/**
* @notice An internal setter for the resource config. Ensures that the
* config is sane before storing it by checking for invariants.
*
* @param _config The new resource config.
*/
function _setResourceConfig(ResourceMetering.ResourceConfig memory _config) internal {
// Min base fee must be less than or equal to max base fee.
require(
_config.minimumBaseFee <= _config.maximumBaseFee,
"SystemConfig: min base fee must be less than max base"
);
// Base fee change denominator must be greater than 0.
require(_config.baseFeeMaxChangeDenominator > 0, "SystemConfig: denominator cannot be 0");
// Max resource limit plus system tx gas must be less than or equal to the L2 gas limit.
// The gas limit must be increased before these values can be increased.
require(
_config.maxResourceLimit + _config.systemTxMaxGas <= gasLimit,
"SystemConfig: gas limit too low"
);
// Elasticity multiplier must be greater than 0.
require(
_config.elasticityMultiplier > 0,
"SystemConfig: elasticity multiplier cannot be 0"
);
// No precision loss when computing target resource limit.
require(
((_config.maxResourceLimit / _config.elasticityMultiplier) *
_config.elasticityMultiplier) == _config.maxResourceLimit,
"SystemConfig: precision loss with target resource limit"
);
_resourceConfig = _config;
}
} }
...@@ -16,6 +16,8 @@ import { ProxyAdmin } from "../universal/ProxyAdmin.sol"; ...@@ -16,6 +16,8 @@ import { ProxyAdmin } from "../universal/ProxyAdmin.sol";
import { OptimismMintableERC20Factory } from "../universal/OptimismMintableERC20Factory.sol"; import { OptimismMintableERC20Factory } from "../universal/OptimismMintableERC20Factory.sol";
import { PortalSender } from "./PortalSender.sol"; import { PortalSender } from "./PortalSender.sol";
import { SystemConfig } from "../L1/SystemConfig.sol"; import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
/** /**
* @title SystemDictator * @title SystemDictator
...@@ -79,6 +81,7 @@ contract SystemDictator is OwnableUpgradeable { ...@@ -79,6 +81,7 @@ contract SystemDictator is OwnableUpgradeable {
bytes32 batcherHash; bytes32 batcherHash;
uint64 gasLimit; uint64 gasLimit;
address unsafeBlockSigner; address unsafeBlockSigner;
ResourceMetering.ResourceConfig resourceConfig;
} }
/** /**
...@@ -160,6 +163,8 @@ contract SystemDictator is OwnableUpgradeable { ...@@ -160,6 +163,8 @@ contract SystemDictator is OwnableUpgradeable {
* initialized upon deployment. * initialized upon deployment.
*/ */
constructor() { constructor() {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
// Using this shorter variable as an alias for address(0) just prevents us from having to // Using this shorter variable as an alias for address(0) just prevents us from having to
// to use a new line for every single parameter. // to use a new line for every single parameter.
address zero = address(0); address zero = address(0);
...@@ -177,7 +182,7 @@ contract SystemDictator is OwnableUpgradeable { ...@@ -177,7 +182,7 @@ contract SystemDictator is OwnableUpgradeable {
PortalSender(zero), PortalSender(zero),
SystemConfig(zero) SystemConfig(zero)
), ),
SystemConfigConfig(zero, 0, 0, bytes32(0), 0, zero) SystemConfigConfig(zero, 0, 0, bytes32(0), 0, zero, rcfg)
) )
); );
} }
...@@ -244,7 +249,8 @@ contract SystemDictator is OwnableUpgradeable { ...@@ -244,7 +249,8 @@ contract SystemDictator is OwnableUpgradeable {
config.systemConfigConfig.scalar, config.systemConfigConfig.scalar,
config.systemConfigConfig.batcherHash, config.systemConfigConfig.batcherHash,
config.systemConfigConfig.gasLimit, config.systemConfigConfig.gasLimit,
config.systemConfigConfig.unsafeBlockSigner config.systemConfigConfig.unsafeBlockSigner,
config.systemConfigConfig.resourceConfig
) )
) )
); );
......
...@@ -3,16 +3,32 @@ pragma solidity 0.8.15; ...@@ -3,16 +3,32 @@ pragma solidity 0.8.15;
import { OptimismPortal } from "../L1/OptimismPortal.sol"; import { OptimismPortal } from "../L1/OptimismPortal.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol"; import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzOptimismPortal { contract EchidnaFuzzOptimismPortal {
OptimismPortal internal portal; OptimismPortal internal portal;
bool internal failedToComplete; bool internal failedToComplete;
constructor() { constructor() {
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
SystemConfig systemConfig = new SystemConfig({
_owner: address(1),
_overhead: 0,
_scalar: 10000,
_batcherHash: bytes32(0),
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(0),
_config: rcfg
});
portal = new OptimismPortal({ portal = new OptimismPortal({
_l2Oracle: L2OutputOracle(address(0)), _l2Oracle: L2OutputOracle(address(0)),
_guardian: address(0), _guardian: address(0),
_paused: false _paused: false,
_config: systemConfig
}); });
} }
......
...@@ -3,6 +3,7 @@ pragma solidity 0.8.15; ...@@ -3,6 +3,7 @@ pragma solidity 0.8.15;
import { ResourceMetering } from "../L1/ResourceMetering.sol"; import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Arithmetic } from "../libraries/Arithmetic.sol"; import { Arithmetic } from "../libraries/Arithmetic.sol";
import { StdUtils } from "forge-std/Test.sol"; import { StdUtils } from "forge-std/Test.sol";
import { Constants } from "../libraries/Constants.sol";
contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
bool internal failedMaxGasPerBlock; bool internal failedMaxGasPerBlock;
...@@ -24,6 +25,20 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -24,6 +25,20 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
__ResourceMetering_init(); __ResourceMetering_init();
} }
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
ResourceMetering.ResourceConfig memory rcfg = Constants.DEFAULT_RESOURCE_CONFIG();
return rcfg;
}
/** /**
* @notice Takes the necessary parameters to allow us to burn arbitrary amounts of gas to test * @notice Takes the necessary parameters to allow us to burn arbitrary amounts of gas to test
* the underlying resource metering/gas market logic * the underlying resource metering/gas market logic
...@@ -34,12 +49,16 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -34,12 +49,16 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
uint256 cachedPrevBoughtGas = uint256(params.prevBoughtGas); uint256 cachedPrevBoughtGas = uint256(params.prevBoughtGas);
uint256 cachedPrevBlockNum = uint256(params.prevBlockNum); uint256 cachedPrevBlockNum = uint256(params.prevBlockNum);
ResourceMetering.ResourceConfig memory rcfg = resourceConfig();
uint256 targetResourceLimit = uint256(rcfg.maxResourceLimit) /
uint256(rcfg.elasticityMultiplier);
// check that the last block's base fee hasn't dropped below the minimum // check that the last block's base fee hasn't dropped below the minimum
if (cachedPrevBaseFee < uint256(MINIMUM_BASE_FEE)) { if (cachedPrevBaseFee < uint256(rcfg.minimumBaseFee)) {
failedNeverBelowMinBaseFee = true; failedNeverBelowMinBaseFee = true;
} }
// check that the last block didn't consume more than the max amount of gas // check that the last block didn't consume more than the max amount of gas
if (cachedPrevBoughtGas > uint256(MAX_RESOURCE_LIMIT)) { if (cachedPrevBoughtGas > uint256(rcfg.maxResourceLimit)) {
failedMaxGasPerBlock = true; failedMaxGasPerBlock = true;
} }
...@@ -51,11 +70,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -51,11 +70,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
if (_raiseBaseFee) { if (_raiseBaseFee) {
gasToBurn = bound( gasToBurn = bound(
_gasToBurn, _gasToBurn,
uint256(TARGET_RESOURCE_LIMIT), uint256(targetResourceLimit),
uint256(MAX_RESOURCE_LIMIT) uint256(rcfg.maxResourceLimit)
); );
} else { } else {
gasToBurn = bound(_gasToBurn, 0, uint256(TARGET_RESOURCE_LIMIT)); gasToBurn = bound(_gasToBurn, 0, targetResourceLimit);
} }
_burnInternal(uint64(gasToBurn)); _burnInternal(uint64(gasToBurn));
...@@ -63,13 +82,13 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -63,13 +82,13 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
// Part 3: we run checks and modify our invariant flags based on the updated params values // Part 3: we run checks and modify our invariant flags based on the updated params values
// Calculate the maximum allowed baseFee change (per block) // Calculate the maximum allowed baseFee change (per block)
uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(BASE_FEE_MAX_CHANGE_DENOMINATOR); uint256 maxBaseFeeChange = cachedPrevBaseFee / uint256(rcfg.baseFeeMaxChangeDenominator);
// If the last block used more than the target amount of gas (and there were no // If the last block used more than the target amount of gas (and there were no
// empty blocks in between), ensure this block's baseFee increased, but not by // empty blocks in between), ensure this block's baseFee increased, but not by
// more than the max amount per block // more than the max amount per block
if ( if (
(cachedPrevBoughtGas > uint256(TARGET_RESOURCE_LIMIT)) && (cachedPrevBoughtGas > uint256(targetResourceLimit)) &&
(uint256(params.prevBlockNum) - cachedPrevBlockNum == 1) (uint256(params.prevBlockNum) - cachedPrevBlockNum == 1)
) { ) {
failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee); failedRaiseBaseFee = failedRaiseBaseFee || (params.prevBaseFee <= cachedPrevBaseFee);
...@@ -81,7 +100,7 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -81,7 +100,7 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
// If the last block used less than the target amount of gas, (or was empty), // If the last block used less than the target amount of gas, (or was empty),
// ensure that: this block's baseFee was decreased, but not by more than the max amount // ensure that: this block's baseFee was decreased, but not by more than the max amount
if ( if (
(cachedPrevBoughtGas < uint256(TARGET_RESOURCE_LIMIT)) || (cachedPrevBoughtGas < uint256(targetResourceLimit)) ||
(uint256(params.prevBlockNum) - cachedPrevBlockNum > 1) (uint256(params.prevBlockNum) - cachedPrevBlockNum > 1)
) { ) {
// Invariant: baseFee should decrease // Invariant: baseFee should decrease
...@@ -104,11 +123,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils { ...@@ -104,11 +123,11 @@ contract EchidnaFuzzResourceMetering is ResourceMetering, StdUtils {
Arithmetic.clamp( Arithmetic.clamp(
Arithmetic.cdexp( Arithmetic.cdexp(
int256(cachedPrevBaseFee), int256(cachedPrevBaseFee),
BASE_FEE_MAX_CHANGE_DENOMINATOR, int256(uint256(rcfg.baseFeeMaxChangeDenominator)),
int256(uint256(params.prevBlockNum) - cachedPrevBlockNum) int256(uint256(params.prevBlockNum) - cachedPrevBlockNum)
), ),
MINIMUM_BASE_FEE, int256(uint256(rcfg.minimumBaseFee)),
MAXIMUM_BASE_FEE int256(uint256(rcfg.maximumBaseFee))
) )
); );
} }
......
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import { ResourceMetering } from "../L1/ResourceMetering.sol";
/** /**
* @title Constants * @title Constants
* @notice Constants is a library for storing constants. Simple! Don't put everything in here, just * @notice Constants is a library for storing constants. Simple! Don't put everything in here, just
...@@ -24,4 +26,24 @@ library Constants { ...@@ -24,4 +26,24 @@ library Constants {
* non-zero to reduce the gas cost of message passing transactions. * non-zero to reduce the gas cost of message passing transactions.
*/ */
address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD; address internal constant DEFAULT_L2_SENDER = 0x000000000000000000000000000000000000dEaD;
/**
* @notice Returns the default values for the ResourceConfig. These are the recommended values
* for a production network.
*/
function DEFAULT_RESOURCE_CONFIG()
internal
pure
returns (ResourceMetering.ResourceConfig memory)
{
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
return config;
}
} }
...@@ -32,8 +32,6 @@ contract SetPrevBaseFee_Test is Portal_Initializer { ...@@ -32,8 +32,6 @@ contract SetPrevBaseFee_Test is Portal_Initializer {
// In order to achieve this we make no assertions, and handle everything else in the setUp() // In order to achieve this we make no assertions, and handle everything else in the setUp()
// function. // function.
contract GasBenchMark_OptimismPortal is Portal_Initializer { contract GasBenchMark_OptimismPortal is Portal_Initializer {
uint128 internal INITIAL_BASE_FEE;
// Reusable default values for a test withdrawal // Reusable default values for a test withdrawal
Types.WithdrawalTransaction _defaultTx; Types.WithdrawalTransaction _defaultTx;
...@@ -86,8 +84,6 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer { ...@@ -86,8 +84,6 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
1 1
); );
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
// Fund the portal so that we can withdraw ETH. // Fund the portal so that we can withdraw ETH.
vm.deal(address(op), 0xFFFFFFFF); vm.deal(address(op), 0xFFFFFFFF);
} }
...@@ -103,7 +99,7 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer { ...@@ -103,7 +99,7 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
} }
function test_depositTransaction_benchmark_1() external { function test_depositTransaction_benchmark_1() external {
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE); setPrevBaseFee(vm, address(op), 1 gwei);
op.depositTransaction{ value: NON_ZERO_VALUE }( op.depositTransaction{ value: NON_ZERO_VALUE }(
NON_ZERO_ADDRESS, NON_ZERO_ADDRESS,
ZERO_VALUE, ZERO_VALUE,
...@@ -124,16 +120,9 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer { ...@@ -124,16 +120,9 @@ contract GasBenchMark_OptimismPortal is Portal_Initializer {
} }
contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer { contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer {
uint128 internal INITIAL_BASE_FEE;
function setUp() public virtual override {
super.setUp();
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
}
function test_sendMessage_benchmark_0() external { function test_sendMessage_benchmark_0() external {
vm.pauseGasMetering(); vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE); setPrevBaseFee(vm, address(op), 1 gwei);
// The amount of data typically sent during a bridge deposit. // The amount of data typically sent during a bridge deposit.
bytes bytes
memory data = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; memory data = hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
...@@ -153,11 +142,8 @@ contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer { ...@@ -153,11 +142,8 @@ contract GasBenchMark_L1CrossDomainMessenger is Messenger_Initializer {
} }
contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer { contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
uint128 internal INITIAL_BASE_FEE;
function setUp() public virtual override { function setUp() public virtual override {
super.setUp(); super.setUp();
INITIAL_BASE_FEE = op.INITIAL_BASE_FEE();
deal(address(L1Token), alice, 100000, true); deal(address(L1Token), alice, 100000, true);
vm.startPrank(alice, alice); vm.startPrank(alice, alice);
L1Token.approve(address(L1Bridge), type(uint256).max); L1Token.approve(address(L1Bridge), type(uint256).max);
...@@ -165,7 +151,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer { ...@@ -165,7 +151,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
function test_depositETH_benchmark_0() external { function test_depositETH_benchmark_0() external {
vm.pauseGasMetering(); vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE); setPrevBaseFee(vm, address(op), 1 gwei);
vm.resumeGasMetering(); vm.resumeGasMetering();
L1Bridge.depositETH{ value: 500 }(50000, hex""); L1Bridge.depositETH{ value: 500 }(50000, hex"");
} }
...@@ -179,7 +165,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer { ...@@ -179,7 +165,7 @@ contract GasBenchMark_L1StandardBridge_Deposit is Bridge_Initializer {
function test_depositERC20_benchmark_0() external { function test_depositERC20_benchmark_0() external {
vm.pauseGasMetering(); vm.pauseGasMetering();
setPrevBaseFee(vm, address(op), INITIAL_BASE_FEE); setPrevBaseFee(vm, address(op), 1 gwei);
vm.resumeGasMetering(); vm.resumeGasMetering();
L1Bridge.bridgeERC20({ L1Bridge.bridgeERC20({
_localToken: address(L1Token), _localToken: address(L1Token),
......
...@@ -28,6 +28,9 @@ import { L1ChugSplashProxy } from "../legacy/L1ChugSplashProxy.sol"; ...@@ -28,6 +28,9 @@ import { L1ChugSplashProxy } from "../legacy/L1ChugSplashProxy.sol";
import { IL1ChugSplashDeployer } from "../legacy/L1ChugSplashProxy.sol"; import { IL1ChugSplashDeployer } from "../legacy/L1ChugSplashProxy.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { LegacyMintableERC20 } from "../legacy/LegacyMintableERC20.sol"; import { LegacyMintableERC20 } from "../legacy/LegacyMintableERC20.sol";
import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract CommonTest is Test { contract CommonTest is Test {
address alice = address(128); address alice = address(128);
...@@ -158,6 +161,7 @@ contract Portal_Initializer is L2OutputOracle_Initializer { ...@@ -158,6 +161,7 @@ contract Portal_Initializer is L2OutputOracle_Initializer {
// Test target // Test target
OptimismPortal internal opImpl; OptimismPortal internal opImpl;
OptimismPortal internal op; OptimismPortal internal op;
SystemConfig systemConfig;
event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success);
event WithdrawalProven( event WithdrawalProven(
...@@ -169,7 +173,25 @@ contract Portal_Initializer is L2OutputOracle_Initializer { ...@@ -169,7 +173,25 @@ contract Portal_Initializer is L2OutputOracle_Initializer {
function setUp() public virtual override { function setUp() public virtual override {
super.setUp(); super.setUp();
opImpl = new OptimismPortal({ _l2Oracle: oracle, _guardian: guardian, _paused: true }); ResourceMetering.ResourceConfig memory config = Constants.DEFAULT_RESOURCE_CONFIG();
systemConfig = new SystemConfig({
_owner: address(1),
_overhead: 0,
_scalar: 10000,
_batcherHash: bytes32(0),
_gasLimit: 30_000_000,
_unsafeBlockSigner: address(0),
_config: config
});
opImpl = new OptimismPortal({
_l2Oracle: oracle,
_guardian: guardian,
_paused: true,
_config: systemConfig
});
Proxy proxy = new Proxy(multisig); Proxy proxy = new Proxy(multisig);
vm.prank(multisig); vm.prank(multisig);
proxy.upgradeToAndCall( proxy.upgradeToAndCall(
......
...@@ -9,6 +9,7 @@ import { OptimismPortal } from "../L1/OptimismPortal.sol"; ...@@ -9,6 +9,7 @@ import { OptimismPortal } from "../L1/OptimismPortal.sol";
import { Types } from "../libraries/Types.sol"; import { Types } from "../libraries/Types.sol";
import { Hashing } from "../libraries/Hashing.sol"; import { Hashing } from "../libraries/Hashing.sol";
import { Proxy } from "../universal/Proxy.sol"; import { Proxy } from "../universal/Proxy.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
contract OptimismPortal_Test is Portal_Initializer { contract OptimismPortal_Test is Portal_Initializer {
event Paused(address); event Paused(address);
...@@ -1045,10 +1046,12 @@ contract OptimismPortalUpgradeable_Test is Portal_Initializer { ...@@ -1045,10 +1046,12 @@ contract OptimismPortalUpgradeable_Test is Portal_Initializer {
} }
function test_params_initValuesOnProxy_succeeds() external { function test_params_initValuesOnProxy_succeeds() external {
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = OptimismPortal( OptimismPortal p = OptimismPortal(payable(address(proxy)));
payable(address(proxy))
).params(); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = p.params();
assertEq(prevBaseFee, opImpl.INITIAL_BASE_FEE());
ResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig();
assertEq(prevBaseFee, rcfg.minimumBaseFee);
assertEq(prevBoughtGas, 0); assertEq(prevBoughtGas, 0);
assertEq(prevBlockNum, initialBlockNum); assertEq(prevBlockNum, initialBlockNum);
} }
......
...@@ -4,6 +4,7 @@ pragma solidity 0.8.15; ...@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol"; import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Proxy } from "../universal/Proxy.sol"; import { Proxy } from "../universal/Proxy.sol";
import { Constants } from "../libraries/Constants.sol";
contract MeterUser is ResourceMetering { contract MeterUser is ResourceMetering {
constructor() { constructor() {
...@@ -14,6 +15,19 @@ contract MeterUser is ResourceMetering { ...@@ -14,6 +15,19 @@ contract MeterUser is ResourceMetering {
__ResourceMetering_init(); __ResourceMetering_init();
} }
function resourceConfig() public pure returns (ResourceMetering.ResourceConfig memory) {
return _resourceConfig();
}
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
return Constants.DEFAULT_RESOURCE_CONFIG();
}
function use(uint64 _amount) public metered(_amount) {} function use(uint64 _amount) public metered(_amount) {}
function set( function set(
...@@ -29,6 +43,11 @@ contract MeterUser is ResourceMetering { ...@@ -29,6 +43,11 @@ contract MeterUser is ResourceMetering {
} }
} }
/**
* @title ResourceConfig
* @notice The tests are based on the default config values. It is expected that
* the config values used in these tests are ran in production.
*/
contract ResourceMetering_Test is Test { contract ResourceMetering_Test is Test {
MeterUser internal meter; MeterUser internal meter;
uint64 initialBlockNum; uint64 initialBlockNum;
...@@ -38,42 +57,15 @@ contract ResourceMetering_Test is Test { ...@@ -38,42 +57,15 @@ contract ResourceMetering_Test is Test {
initialBlockNum = uint64(block.number); initialBlockNum = uint64(block.number);
} }
/**
* @notice The INITIAL_BASE_FEE must be less than the MAXIMUM_BASE_FEE
* and greater than the MINIMUM_BASE_FEE.
*/
function test_meter_initialBaseFee_succeeds() external {
uint256 max = uint256(meter.MAXIMUM_BASE_FEE());
uint256 min = uint256(meter.MINIMUM_BASE_FEE());
uint256 initial = uint256(meter.INITIAL_BASE_FEE());
assertTrue(max >= initial);
assertTrue(min <= initial);
}
/**
* @notice The MINIMUM_BASE_FEE must be less than the MAXIMUM_BASE_FEE.
*/
function test_meter_minBaseFeeLessThanMaxBaseFee_succeeds() external {
uint256 max = uint256(meter.MAXIMUM_BASE_FEE());
uint256 min = uint256(meter.MINIMUM_BASE_FEE());
assertTrue(max > min);
}
function test_meter_initialResourceParams_succeeds() external { function test_meter_initialResourceParams_succeeds() external {
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params(); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
assertEq(prevBaseFee, meter.INITIAL_BASE_FEE()); assertEq(prevBaseFee, rcfg.minimumBaseFee);
assertEq(prevBoughtGas, 0); assertEq(prevBoughtGas, 0);
assertEq(prevBlockNum, initialBlockNum); assertEq(prevBlockNum, initialBlockNum);
} }
function test_meter_maxValue_succeeds() external {
uint256 max = uint256(meter.MAX_RESOURCE_LIMIT());
uint256 target = uint256(meter.TARGET_RESOURCE_LIMIT());
uint256 elasticity = uint256(meter.ELASTICITY_MULTIPLIER());
assertEq(max / elasticity, target);
}
function test_meter_updateParamsNoChange_succeeds() external { function test_meter_updateParamsNoChange_succeeds() external {
meter.use(0); // equivalent to just updating the base fee and block number meter.use(0); // equivalent to just updating the base fee and block number
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params(); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
...@@ -116,8 +108,9 @@ contract ResourceMetering_Test is Test { ...@@ -116,8 +108,9 @@ contract ResourceMetering_Test is Test {
} }
function test_meter_updateNoGasDelta_succeeds() external { function test_meter_updateNoGasDelta_succeeds() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT())); ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
meter.use(target); uint256 target = uint256(rcfg.maxResourceLimit) / uint256(rcfg.elasticityMultiplier);
meter.use(uint64(target));
(uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params(); (uint128 prevBaseFee, uint64 prevBoughtGas, uint64 prevBlockNum) = meter.params();
assertEq(prevBaseFee, 1000000000); assertEq(prevBaseFee, 1000000000);
...@@ -126,12 +119,14 @@ contract ResourceMetering_Test is Test { ...@@ -126,12 +119,14 @@ contract ResourceMetering_Test is Test {
} }
function test_meter_useMax_succeeds() external { function test_meter_useMax_succeeds() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT())); ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER())); uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
meter.use(target * elasticity); uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
meter.use(target * elasticityMultiplier);
(, uint64 prevBoughtGas, ) = meter.params(); (, uint64 prevBoughtGas, ) = meter.params();
assertEq(prevBoughtGas, target * elasticity); assertEq(prevBoughtGas, target * elasticityMultiplier);
vm.roll(initialBlockNum + 1); vm.roll(initialBlockNum + 1);
meter.use(0); meter.use(0);
...@@ -140,10 +135,12 @@ contract ResourceMetering_Test is Test { ...@@ -140,10 +135,12 @@ contract ResourceMetering_Test is Test {
} }
function test_meter_useMoreThanMax_reverts() external { function test_meter_useMoreThanMax_reverts() external {
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT())); ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER())); uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
vm.expectRevert("ResourceMetering: cannot buy more gas than available gas limit"); vm.expectRevert("ResourceMetering: cannot buy more gas than available gas limit");
meter.use(target * elasticity + 1); meter.use(target * elasticityMultiplier + 1);
} }
// Demonstrates that the resource metering arithmetic can tolerate very large gaps between // Demonstrates that the resource metering arithmetic can tolerate very large gaps between
...@@ -153,9 +150,11 @@ contract ResourceMetering_Test is Test { ...@@ -153,9 +150,11 @@ contract ResourceMetering_Test is Test {
// At 12 seconds per block, this number is effectively unreachable. // At 12 seconds per block, this number is effectively unreachable.
vm.assume(_blockDiff < 433576281058164217753225238677900874458691); vm.assume(_blockDiff < 433576281058164217753225238677900874458691);
uint64 target = uint64(uint256(meter.TARGET_RESOURCE_LIMIT())); ResourceMetering.ResourceConfig memory rcfg = meter.resourceConfig();
uint64 elasticity = uint64(uint256(meter.ELASTICITY_MULTIPLIER())); uint64 target = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
vm.assume(_amount < target * elasticity); uint64 elasticityMultiplier = uint64(rcfg.elasticityMultiplier);
vm.assume(_amount < target * elasticityMultiplier);
vm.roll(initialBlockNum + _blockDiff); vm.roll(initialBlockNum + _blockDiff);
meter.use(_amount); meter.use(_amount);
} }
...@@ -182,6 +181,15 @@ contract CustomMeterUser is ResourceMetering { ...@@ -182,6 +181,15 @@ contract CustomMeterUser is ResourceMetering {
}); });
} }
function _resourceConfig()
internal
pure
override
returns (ResourceMetering.ResourceConfig memory)
{
return Constants.DEFAULT_RESOURCE_CONFIG();
}
function use(uint64 _amount) public returns (uint256) { function use(uint64 _amount) public returns (uint256) {
uint256 initialGas = gasleft(); uint256 initialGas = gasleft();
_metered(_amount, initialGas); _metered(_amount, initialGas);
...@@ -224,10 +232,11 @@ contract ArtifactResourceMetering_Test is Test { ...@@ -224,10 +232,11 @@ contract ArtifactResourceMetering_Test is Test {
vm.roll(1_000_000); vm.roll(1_000_000);
MeterUser base = new MeterUser(); MeterUser base = new MeterUser();
minimumBaseFee = uint128(uint256(base.MINIMUM_BASE_FEE())); ResourceMetering.ResourceConfig memory rcfg = base.resourceConfig();
maximumBaseFee = uint128(uint256(base.MAXIMUM_BASE_FEE())); minimumBaseFee = uint128(rcfg.minimumBaseFee);
maxResourceLimit = uint64(uint256(base.MAX_RESOURCE_LIMIT())); maximumBaseFee = rcfg.maximumBaseFee;
targetResourceLimit = uint64(uint256(base.TARGET_RESOURCE_LIMIT())); maxResourceLimit = uint64(rcfg.maxResourceLimit);
targetResourceLimit = uint64(rcfg.maxResourceLimit) / uint64(rcfg.elasticityMultiplier);
outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv"); outfile = string.concat(vm.projectRoot(), "/.resource-metering.csv");
try vm.removeFile(outfile) {} catch {} try vm.removeFile(outfile) {} catch {}
......
...@@ -3,36 +3,58 @@ pragma solidity 0.8.15; ...@@ -3,36 +3,58 @@ pragma solidity 0.8.15;
import { CommonTest } from "./CommonTest.t.sol"; import { CommonTest } from "./CommonTest.t.sol";
import { SystemConfig } from "../L1/SystemConfig.sol"; import { SystemConfig } from "../L1/SystemConfig.sol";
import { ResourceMetering } from "../L1/ResourceMetering.sol";
import { Constants } from "../libraries/Constants.sol";
contract SystemConfig_Init is CommonTest { contract SystemConfig_Init is CommonTest {
SystemConfig sysConf; SystemConfig sysConf;
function setUp() public virtual override { function setUp() public virtual override {
super.setUp(); super.setUp();
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
sysConf = new SystemConfig({ sysConf = new SystemConfig({
_owner: alice, _owner: alice,
_overhead: 2100, _overhead: 2100,
_scalar: 1000000, _scalar: 1000000,
_batcherHash: bytes32(hex"abcd"), _batcherHash: bytes32(hex"abcd"),
_gasLimit: 9_000_000, _gasLimit: 30_000_000,
_unsafeBlockSigner: address(1) _unsafeBlockSigner: address(1),
_config: config
}); });
} }
} }
contract SystemConfig_Initialize_TestFail is CommonTest { contract SystemConfig_Initialize_TestFail is SystemConfig_Init {
function test_initialize_lowGasLimit_reverts() external { function test_initialize_lowGasLimit_reverts() external {
vm.expectRevert("SystemConfig: gas limit too low"); uint64 minimumGasLimit = sysConf.minimumGasLimit();
ResourceMetering.ResourceConfig memory cfg = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: 1 gwei,
systemTxMaxGas: 1_000_000,
maximumBaseFee: type(uint128).max
});
// The minimum gas limit defined in SystemConfig: vm.expectRevert("SystemConfig: gas limit too low");
uint64 MINIMUM_GAS_LIMIT = 8_000_000;
new SystemConfig({ new SystemConfig({
_owner: alice, _owner: alice,
_overhead: 0, _overhead: 0,
_scalar: 0, _scalar: 0,
_batcherHash: bytes32(hex""), _batcherHash: bytes32(hex""),
_gasLimit: MINIMUM_GAS_LIMIT - 1, _gasLimit: minimumGasLimit - 1,
_unsafeBlockSigner: address(1) _unsafeBlockSigner: address(1),
_config: cfg
}); });
} }
} }
...@@ -57,6 +79,70 @@ contract SystemConfig_Setters_TestFail is SystemConfig_Init { ...@@ -57,6 +79,70 @@ contract SystemConfig_Setters_TestFail is SystemConfig_Init {
vm.expectRevert("Ownable: caller is not the owner"); vm.expectRevert("Ownable: caller is not the owner");
sysConf.setUnsafeBlockSigner(address(0x20)); sysConf.setUnsafeBlockSigner(address(0x20));
} }
function test_setResourceConfig_notOwner_reverts() external {
ResourceMetering.ResourceConfig memory config = Constants.DEFAULT_RESOURCE_CONFIG();
vm.expectRevert("Ownable: caller is not the owner");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_badMinMax_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 2 gwei,
maximumBaseFee: 1 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: min base fee must be less than max base");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_zeroDenominator_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 0,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: denominator cannot be 0");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_lowGasLimit_reverts() external {
uint64 gasLimit = sysConf.gasLimit();
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: uint32(gasLimit),
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: uint32(gasLimit),
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: gas limit too low");
sysConf.setResourceConfig(config);
}
function test_setResourceConfig_badPrecision_reverts() external {
ResourceMetering.ResourceConfig memory config = ResourceMetering.ResourceConfig({
maxResourceLimit: 20_000_000,
elasticityMultiplier: 11,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: 1 gwei,
maximumBaseFee: 2 gwei
});
vm.prank(sysConf.owner());
vm.expectRevert("SystemConfig: precision loss with target resource limit");
sysConf.setResourceConfig(config);
}
} }
contract SystemConfig_Setters_Test is SystemConfig_Init { contract SystemConfig_Setters_Test is SystemConfig_Init {
......
...@@ -2,18 +2,23 @@ pragma solidity 0.8.15; ...@@ -2,18 +2,23 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { SystemConfig } from "../../L1/SystemConfig.sol"; import { SystemConfig } from "../../L1/SystemConfig.sol";
import { ResourceMetering } from "../../L1/ResourceMetering.sol";
import { Constants } from "../../libraries/Constants.sol";
contract SystemConfig_GasLimitLowerBound_Invariant is Test { contract SystemConfig_GasLimitLowerBound_Invariant is Test {
SystemConfig public config; SystemConfig public config;
function setUp() public { function setUp() public {
ResourceMetering.ResourceConfig memory cfg = Constants.DEFAULT_RESOURCE_CONFIG();
config = new SystemConfig({ config = new SystemConfig({
_owner: address(0xbeef), _owner: address(0xbeef),
_overhead: 2100, _overhead: 2100,
_scalar: 1000000, _scalar: 1000000,
_batcherHash: bytes32(hex"abcd"), _batcherHash: bytes32(hex"abcd"),
_gasLimit: 8_000_000, _gasLimit: 30_000_000,
_unsafeBlockSigner: address(1) _unsafeBlockSigner: address(1),
_config: cfg
}); });
// Set the target contract to the `config` // Set the target contract to the `config`
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
"l2OutputOracleSubmissionInterval": 120, "l2OutputOracleSubmissionInterval": 120,
"l2OutputOracleStartingBlockNumber": 0, "l2OutputOracleStartingBlockNumber": 0,
"l2OutputOracleStartingTimestamp": "TIMESTAMP", "l2OutputOracleStartingTimestamp": TIMESTAMP,
"l2OutputOracleProposer": "PROPOSER", "l2OutputOracleProposer": "PROPOSER",
"l2OutputOracleChallenger": "ADMIN", "l2OutputOracleChallenger": "ADMIN",
......
...@@ -17,6 +17,10 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -17,6 +17,10 @@ const deployFn: DeployFunction = async (hre) => {
'L2OutputOracleProxy' 'L2OutputOracleProxy'
) )
const Artifact__SystemConfigProxy = await hre.deployments.get(
'SystemConfigProxy'
)
const portalGuardian = hre.deployConfig.portalGuardian const portalGuardian = hre.deployConfig.portalGuardian
const portalGuardianCode = await hre.ethers.provider.getCode(portalGuardian) const portalGuardianCode = await hre.ethers.provider.getCode(portalGuardian)
if (portalGuardianCode === '0x') { if (portalGuardianCode === '0x') {
...@@ -41,6 +45,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -41,6 +45,7 @@ const deployFn: DeployFunction = async (hre) => {
L2OutputOracleProxy.address, L2OutputOracleProxy.address,
portalGuardian, portalGuardian,
true, // paused true, // paused
Artifact__SystemConfigProxy.address,
], ],
postDeployAction: async (contract) => { postDeployAction: async (contract) => {
await assertContractVariable( await assertContractVariable(
...@@ -53,6 +58,11 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -53,6 +58,11 @@ const deployFn: DeployFunction = async (hre) => {
'GUARDIAN', 'GUARDIAN',
hre.deployConfig.portalGuardian hre.deployConfig.portalGuardian
) )
await assertContractVariable(
contract,
'SYSTEM_CONFIG',
Artifact__SystemConfigProxy.address
)
}, },
}) })
} }
......
import assert from 'assert'
import { DeployFunction } from 'hardhat-deploy/dist/types' import { DeployFunction } from 'hardhat-deploy/dist/types'
import '@eth-optimism/hardhat-deploy-config' import '@eth-optimism/hardhat-deploy-config'
import { ethers } from 'ethers'
import { assertContractVariable, deploy } from '../src/deploy-utils' import { assertContractVariable, deploy } from '../src/deploy-utils'
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
const deployFn: DeployFunction = async (hre) => { const deployFn: DeployFunction = async (hre) => {
const batcherHash = hre.ethers.utils const batcherHash = hre.ethers.utils
.hexZeroPad(hre.deployConfig.batchSenderAddress, 32) .hexZeroPad(hre.deployConfig.batchSenderAddress, 32)
...@@ -18,6 +23,14 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -18,6 +23,14 @@ const deployFn: DeployFunction = async (hre) => {
batcherHash, batcherHash,
hre.deployConfig.l2GenesisBlockGasLimit, hre.deployConfig.l2GenesisBlockGasLimit,
hre.deployConfig.p2pSequencerAddress, hre.deployConfig.p2pSequencerAddress,
{
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
systemTxMaxGas: 1_000_000,
minimumBaseFee: ethers.utils.parseUnits('1', 'gwei'),
maximumBaseFee: uint128Max,
},
], ],
postDeployAction: async (contract) => { postDeployAction: async (contract) => {
await assertContractVariable( await assertContractVariable(
...@@ -41,6 +54,14 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -41,6 +54,14 @@ const deployFn: DeployFunction = async (hre) => {
'unsafeBlockSigner', 'unsafeBlockSigner',
hre.deployConfig.p2pSequencerAddress hre.deployConfig.p2pSequencerAddress
) )
const config = await contract.resourceConfig()
assert(config.maxResourceLimit === 20_000_000)
assert(config.elasticityMultiplier === 10)
assert(config.baseFeeMaxChangeDenominator === 8)
assert(config.systemTxMaxGas === 1_000_000)
assert(ethers.utils.parseUnits('1', 'gwei').eq(config.minimumBaseFee))
assert(config.maximumBaseFee.eq(uint128Max))
}, },
}) })
} }
......
import assert from 'assert' import assert from 'assert'
import { ethers } from 'ethers' import { ethers, BigNumber } from 'ethers'
import { DeployFunction } from 'hardhat-deploy/dist/types' import { DeployFunction } from 'hardhat-deploy/dist/types'
import { awaitCondition } from '@eth-optimism/core-utils' import { awaitCondition } from '@eth-optimism/core-utils'
import '@eth-optimism/hardhat-deploy-config' import '@eth-optimism/hardhat-deploy-config'
...@@ -100,6 +100,18 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -100,6 +100,18 @@ const deployFn: DeployFunction = async (hre) => {
), ),
gasLimit: hre.deployConfig.l2GenesisBlockGasLimit, gasLimit: hre.deployConfig.l2GenesisBlockGasLimit,
unsafeBlockSigner: hre.deployConfig.p2pSequencerAddress, unsafeBlockSigner: hre.deployConfig.p2pSequencerAddress,
// The resource config is not exposed to the end user
// to simplify deploy config. It may be introduced in the future.
resourceConfig: {
maxResourceLimit: 20_000_000,
elasticityMultiplier: 10,
baseFeeMaxChangeDenominator: 8,
minimumBaseFee: ethers.utils.parseUnits('1', 'gwei'),
systemTxMaxGas: 1_000_000,
maximumBaseFee: BigNumber.from(
'0xffffffffffffffffffffffffffffffff'
).toString(),
},
}, },
} }
......
...@@ -17,6 +17,8 @@ import { ...@@ -17,6 +17,8 @@ import {
getCastCommand, getCastCommand,
} from '../src/deploy-utils' } from '../src/deploy-utils'
const uint128Max = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffff')
const deployFn: DeployFunction = async (hre) => { const deployFn: DeployFunction = async (hre) => {
const { deployer } = await hre.getNamedAccounts() const { deployer } = await hre.getNamedAccounts()
...@@ -254,6 +256,14 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -254,6 +256,14 @@ const deployFn: DeployFunction = async (hre) => {
'gasLimit', 'gasLimit',
hre.deployConfig.l2GenesisBlockGasLimit hre.deployConfig.l2GenesisBlockGasLimit
) )
const config = await SystemConfigProxy.resourceConfig()
assert(config.maxResourceLimit === 20_000_000)
assert(config.elasticityMultiplier === 10)
assert(config.baseFeeMaxChangeDenominator === 8)
assert(config.systemTxMaxGas === 1_000_000)
assert(ethers.utils.parseUnits('1', 'gwei').eq(config.minimumBaseFee))
assert(config.maximumBaseFee.eq(uint128Max))
}, },
}) })
......
...@@ -234,7 +234,7 @@ const deployFn: DeployFunction = async (hre) => { ...@@ -234,7 +234,7 @@ const deployFn: DeployFunction = async (hre) => {
) )
const resourceParams = await OptimismPortal.params() const resourceParams = await OptimismPortal.params()
assert( assert(
resourceParams.prevBaseFee.eq(await OptimismPortal.INITIAL_BASE_FEE()), resourceParams.prevBaseFee.eq(ethers.utils.parseUnits('1', 'gwei')),
`OptimismPortal was not initialized with the correct initial base fee` `OptimismPortal was not initialized with the correct initial base fee`
) )
assert( assert(
......
...@@ -19,6 +19,6 @@ This invariant asserts that there is no chain of calls that can be made that wil ...@@ -19,6 +19,6 @@ This invariant asserts that there is no chain of calls that can be made that wil
## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`. ## Deposits of any value should always succeed unless `_to` = `address(0)` or `_isCreation` = `true`.
**Test:** [`FuzzOptimismPortal.sol#L41`](../contracts/echidna/FuzzOptimismPortal.sol#L41) **Test:** [`FuzzOptimismPortal.sol#L57`](../contracts/echidna/FuzzOptimismPortal.sol#L57)
All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed. All deposits, barring creation transactions and transactions sent to `address(0)`, should always succeed.
# `ResourceMetering` Invariants # `ResourceMetering` Invariants
## The base fee should increase if the last block used more than the target amount of gas ## The base fee should increase if the last block used more than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L139`](../contracts/echidna/FuzzResourceMetering.sol#L139) **Test:** [`FuzzResourceMetering.sol#L158`](../contracts/echidna/FuzzResourceMetering.sol#L158)
If the last block used more than the target amount of gas (and there were no empty blocks in between), ensure this block's baseFee increased, but not by more than the max amount per block. If the last block used more than the target amount of gas (and there were no empty blocks in between), ensure this block's baseFee increased, but not by more than the max amount per block.
## The base fee should decrease if the last block used less than the target amount of gas ## The base fee should decrease if the last block used less than the target amount of gas
**Test:** [`FuzzResourceMetering.sol#L150`](../contracts/echidna/FuzzResourceMetering.sol#L150) **Test:** [`FuzzResourceMetering.sol#L169`](../contracts/echidna/FuzzResourceMetering.sol#L169)
If the previous block used less than the target amount of gas, the base fee should decrease, but not more than the max amount. If the previous block used less than the target amount of gas, the base fee should decrease, but not more than the max amount.
## A block's base fee should never be below `MINIMUM_BASE_FEE` ## A block's base fee should never be below `MINIMUM_BASE_FEE`
**Test:** [`FuzzResourceMetering.sol#L160`](../contracts/echidna/FuzzResourceMetering.sol#L160) **Test:** [`FuzzResourceMetering.sol#L179`](../contracts/echidna/FuzzResourceMetering.sol#L179)
This test asserts that a block's base fee can never drop below the `MINIMUM_BASE_FEE` threshold. This test asserts that a block's base fee can never drop below the `MINIMUM_BASE_FEE` threshold.
## A block can never consume more than `MAX_RESOURCE_LIMIT` gas. ## A block can never consume more than `MAX_RESOURCE_LIMIT` gas.
**Test:** [`FuzzResourceMetering.sol#L170`](../contracts/echidna/FuzzResourceMetering.sol#L170) **Test:** [`FuzzResourceMetering.sol#L189`](../contracts/echidna/FuzzResourceMetering.sol#L189)
This test asserts that a block can never consume more than the `MAX_RESOURCE_LIMIT` gas threshold. This test asserts that a block can never consume more than the `MAX_RESOURCE_LIMIT` gas threshold.
## The base fee can never be raised more than the max base fee change. ## The base fee can never be raised more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L181`](../contracts/echidna/FuzzResourceMetering.sol#L181) **Test:** [`FuzzResourceMetering.sol#L200`](../contracts/echidna/FuzzResourceMetering.sol#L200)
After a block consumes more gas than the target gas, the base fee cannot be raised more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR` After a block consumes more gas than the target gas, the base fee cannot be raised more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
## The base fee can never be lowered more than the max base fee change. ## The base fee can never be lowered more than the max base fee change.
**Test:** [`FuzzResourceMetering.sol#L192`](../contracts/echidna/FuzzResourceMetering.sol#L192) **Test:** [`FuzzResourceMetering.sol#L211`](../contracts/echidna/FuzzResourceMetering.sol#L211)
After a block consumes less than the target gas, the base fee cannot be lowered more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR` After a block consumes less than the target gas, the base fee cannot be lowered more than the maximum amount allowed. The max base fee change (per-block) is derived as follows: `prevBaseFee / BASE_FEE_MAX_CHANGE_DENOMINATOR`
## The `maxBaseFeeChange` calculation over multiple blocks can never underflow. ## The `maxBaseFeeChange` calculation over multiple blocks can never underflow.
**Test:** [`FuzzResourceMetering.sol#L203`](../contracts/echidna/FuzzResourceMetering.sol#L203) **Test:** [`FuzzResourceMetering.sol#L222`](../contracts/echidna/FuzzResourceMetering.sol#L222)
When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation should never be allowed to underflow. When calculating the `maxBaseFeeChange` after multiple empty blocks, the calculation should never be allowed to underflow.
# `SystemConfig` Invariants # `SystemConfig` Invariants
## The gas limit of the `SystemConfig` contract can never be lower than the hard-coded lower bound. ## The gas limit of the `SystemConfig` contract can never be lower than the hard-coded lower bound.
**Test:** [`SystemConfig.t.sol#L39`](../contracts/test/invariants/SystemConfig.t.sol#L39) **Test:** [`SystemConfig.t.sol#L44`](../contracts/test/invariants/SystemConfig.t.sol#L44)
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
**Table of Contents** **Table of Contents**
- [Gas Stipend](#gas-stipend) - [Gas Stipend](#gas-stipend)
- [Default Values](#default-values)
- [Limiting Guaranteed Gas](#limiting-guaranteed-gas) - [Limiting Guaranteed Gas](#limiting-guaranteed-gas)
- [Rationale for burning L1 Gas](#rationale-for-burning-l1-gas) - [Rationale for burning L1 Gas](#rationale-for-burning-l1-gas)
- [On Preventing Griefing Attacks](#on-preventing-griefing-attacks)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
...@@ -36,6 +38,17 @@ the L2 gas, where `gas spent` is the amount of L1 gas spent processing the depos ...@@ -36,6 +38,17 @@ the L2 gas, where `gas spent` is the amount of L1 gas spent processing the depos
of this credit is greater than the ETH value of the requested guaranteed gas of this credit is greater than the ETH value of the requested guaranteed gas
(`requested guaranteed gas * L2 gas price`), no L1 gas is burnt. (`requested guaranteed gas * L2 gas price`), no L1 gas is burnt.
## Default Values
| Variable | Value |
| ------------------------------- | ----------------- |
| Max Resource Limit | 20,000,000 |
| Elasticity Multiplier | 10 |
| Base Fee Max Change Denominator | 8 |
| Minimum Base Fee | 1 gwei |
| Maximum Base Fee | type(uint128).max |
| System Tx Max Gas | 1,000,000 |
## Limiting Guaranteed Gas ## Limiting Guaranteed Gas
The total amount of guaranteed gas that can be bought in a single L1 block must be limited to The total amount of guaranteed gas that can be bought in a single L1 block must be limited to
...@@ -123,3 +136,20 @@ The payable version (Option 2) will likely have discount applied to it (or conve ...@@ -123,3 +136,20 @@ The payable version (Option 2) will likely have discount applied to it (or conve
premium applied to it). premium applied to it).
For the initial release of bedrock, only #1 is supported. For the initial release of bedrock, only #1 is supported.
## On Preventing Griefing Attacks
The cost of purchasing all of the deposit gas in every block must be expensive
enough to prevent attackers from griefing all deposits to the network.
An attacker would observe a deposit in the mempool and frontrun it with a deposit
that purchases enough gas such that the other deposit reverts.
The smaller the max resource limit is, the easier this attack is to pull off.
This attack is mitigated by having a large resource limit as well as a large
elastcity multiplier. This means that the target resource usage is kept small,
giving a lot of room for the deposit base fee to rise when the max resource limit
is being purchased.
This attack should be too expensive to pull off in practice, but if an extremely
wealthy adversary does decide to grief network deposits for an extended period
of time, efforts will be placed to ensure that deposits are able to be processed
on the network.
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