diff --git a/.changeset/early-baboons-look.md b/.changeset/early-baboons-look.md
new file mode 100644
index 000000000000..41f29ac2f4be
--- /dev/null
+++ b/.changeset/early-baboons-look.md
@@ -0,0 +1,5 @@
+---
+'@eth-optimism/integration-tests': patch
+---
+
+Add in berlin hardfork tests
diff --git a/.changeset/many-cougars-scream.md b/.changeset/many-cougars-scream.md
new file mode 100644
index 000000000000..2f042ca8cdf8
--- /dev/null
+++ b/.changeset/many-cougars-scream.md
@@ -0,0 +1,5 @@
+---
+'@eth-optimism/l2geth': patch
+---
+
+Implement berlin hardfork
diff --git a/.changeset/serious-pets-fly.md b/.changeset/serious-pets-fly.md
new file mode 100644
index 000000000000..30e6597daa78
--- /dev/null
+++ b/.changeset/serious-pets-fly.md
@@ -0,0 +1,5 @@
+---
+'@eth-optimism/batch-submitter-service': patch
+---
+
+use EIP-1559 txns for tx/state batches
diff --git a/.changeset/smooth-points-appear.md b/.changeset/smooth-points-appear.md
new file mode 100644
index 000000000000..93f50e490579
--- /dev/null
+++ b/.changeset/smooth-points-appear.md
@@ -0,0 +1,5 @@
+---
+'@eth-optimism/contracts': patch
+---
+
+Add berlin hardfork config to genesis creation
diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml
index 55a6d3582119..e914c43931fa 100644
--- a/.github/workflows/publish-canary.yml
+++ b/.github/workflows/publish-canary.yml
@@ -65,7 +65,7 @@ jobs:
run: yarn changeset version --snapshot
- name: Publish To NPM
- uses: changesets/action@master
+ uses: changesets/action@v1
id: changesets
with:
publish: yarn changeset publish --tag canary
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6d62a35608bf..bf6a7fb0312e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -55,7 +55,7 @@ jobs:
run: yarn
- name: Publish To NPM or Create Release Pull Request
- uses: changesets/action@master
+ uses: changesets/action@v1
id: changesets
with:
publish: yarn release
@@ -101,14 +101,6 @@ jobs:
push: true
tags: ethereumoptimism/l2geth:${{ needs.release.outputs.l2geth }},ethereumoptimism/l2geth:latest
- - name: Publish rpc-proxy
- uses: docker/build-push-action@v2
- with:
- context: .
- file: ./ops/docker/Dockerfile.rpc-proxy
- push: true
- tags: ethereumoptimism/rpc-proxy:${{ needs.release.outputs.l2geth }},ethereumoptimism/rpc-proxy:latest
-
gas-oracle:
name: Publish Gas Oracle Version ${{ needs.release.outputs.gas-oracle }}
needs: release
diff --git a/go/batch-submitter/batch_submitter.go b/go/batch-submitter/batch_submitter.go
index 862c4cc27430..74b3bb921d8f 100644
--- a/go/batch-submitter/batch_submitter.go
+++ b/go/batch-submitter/batch_submitter.go
@@ -15,7 +15,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/drivers/proposer"
"github.com/ethereum-optimism/optimism/go/batch-submitter/drivers/sequencer"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
- "github.com/ethereum-optimism/optimism/go/batch-submitter/utils"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
l2rpc "github.com/ethereum-optimism/optimism/l2geth/rpc"
"github.com/ethereum/go-ethereum/common"
@@ -163,9 +162,6 @@ func NewBatchSubmitter(cfg Config, gitVersion string) (*BatchSubmitter, error) {
}
txManagerConfig := txmgr.Config{
- MinGasPrice: utils.GasPriceFromGwei(1),
- MaxGasPrice: utils.GasPriceFromGwei(cfg.MaxGasPriceInGwei),
- GasRetryIncrement: utils.GasPriceFromGwei(cfg.GasRetryIncrement),
ResubmissionTimeout: cfg.ResubmissionTimeout,
ReceiptQueryInterval: time.Second,
NumConfirmations: cfg.NumConfirmations,
diff --git a/go/batch-submitter/config.go b/go/batch-submitter/config.go
index 1931a387d38e..8ee8381faae3 100644
--- a/go/batch-submitter/config.go
+++ b/go/batch-submitter/config.go
@@ -133,14 +133,6 @@ type Config struct {
// blocks.
BlockOffset uint64
- // MaxGasPriceInGwei is the maximum gas price in gwei we will allow in order
- // to confirm a transaction.
- MaxGasPriceInGwei uint64
-
- // GasRetryIncrement is the step size (in gwei) by which we will ratchet the
- // gas price in order to get a transaction confirmed.
- GasRetryIncrement uint64
-
// SequencerPrivateKey the private key of the wallet used to submit
// transactions to the CTC contract.
SequencerPrivateKey string
@@ -202,8 +194,6 @@ func NewConfig(ctx *cli.Context) (Config, error) {
SentryDsn: ctx.GlobalString(flags.SentryDsnFlag.Name),
SentryTraceRate: ctx.GlobalDuration(flags.SentryTraceRateFlag.Name),
BlockOffset: ctx.GlobalUint64(flags.BlockOffsetFlag.Name),
- MaxGasPriceInGwei: ctx.GlobalUint64(flags.MaxGasPriceInGweiFlag.Name),
- GasRetryIncrement: ctx.GlobalUint64(flags.GasRetryIncrementFlag.Name),
SequencerPrivateKey: ctx.GlobalString(flags.SequencerPrivateKeyFlag.Name),
ProposerPrivateKey: ctx.GlobalString(flags.ProposerPrivateKeyFlag.Name),
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
diff --git a/go/batch-submitter/drivers/clear_pending_tx.go b/go/batch-submitter/drivers/clear_pending_tx.go
index b6cf18d0290e..790805b731c4 100644
--- a/go/batch-submitter/drivers/clear_pending_tx.go
+++ b/go/batch-submitter/drivers/clear_pending_tx.go
@@ -8,7 +8,6 @@ import (
"strings"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
- "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
@@ -50,20 +49,20 @@ func ClearPendingTx(
// price.
sendTx := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
- log.Info(name+" clearing pending tx", "nonce", nonce,
- "gasPrice", gasPrice)
+ log.Info(name+" clearing pending tx", "nonce", nonce)
signedTx, err := SignClearingTx(
- ctx, walletAddr, nonce, gasPrice, l1Client, privKey, chainID,
+ name, ctx, walletAddr, nonce, l1Client, privKey, chainID,
)
if err != nil {
log.Error(name+" unable to sign clearing tx", "nonce", nonce,
- "gasPrice", gasPrice, "err", err)
+ "err", err)
return nil, err
}
txHash := signedTx.Hash()
+ gasTipCap := signedTx.GasTipCap()
+ gasFeeCap := signedTx.GasFeeCap()
err = l1Client.SendTransaction(ctx, signedTx)
switch {
@@ -71,7 +70,8 @@ func ClearPendingTx(
// Clearing transaction successfully confirmed.
case err == nil:
log.Info(name+" submitted clearing tx", "nonce", nonce,
- "gasPrice", gasPrice, "txHash", txHash)
+ "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
+ "txHash", txHash)
return signedTx, nil
@@ -91,8 +91,8 @@ func ClearPendingTx(
// transaction, or abort if the old one confirms.
default:
log.Error(name+" unable to submit clearing tx",
- "nonce", nonce, "gasPrice", gasPrice, "txHash", txHash,
- "err", err)
+ "nonce", nonce, "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap,
+ "txHash", txHash, "err", err)
return nil, err
}
}
@@ -127,26 +127,39 @@ func ClearPendingTx(
// SignClearingTx creates a signed clearing tranaction which sends 0 ETH back to
// the sender's address. EstimateGas is used to set an appropriate gas limit.
func SignClearingTx(
+ name string,
ctx context.Context,
walletAddr common.Address,
nonce uint64,
- gasPrice *big.Int,
l1Client L1Client,
privKey *ecdsa.PrivateKey,
chainID *big.Int,
) (*types.Transaction, error) {
- gasLimit, err := l1Client.EstimateGas(ctx, ethereum.CallMsg{
- To: &walletAddr,
- GasPrice: gasPrice,
- Value: nil,
- Data: nil,
- })
+ gasTipCap, err := l1Client.SuggestGasTipCap(ctx)
+ if err != nil {
+ if !IsMaxPriorityFeePerGasNotFoundError(err) {
+ return nil, err
+ }
+
+ // If the transaction failed because the backend does not support
+ // eth_maxPriorityFeePerGas, fallback to using the default constant.
+ // Currently Alchemy is the only backend provider that exposes this
+ // method, so in the event their API is unreachable we can fallback to a
+ // degraded mode of operation. This also applies to our test
+ // environments, as hardhat doesn't support the query either.
+ log.Warn(name + " eth_maxPriorityFeePerGas is unsupported " +
+ "by current backend, using fallback gasTipCap")
+ gasTipCap = FallbackGasTipCap
+ }
+
+ head, err := l1Client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}
- tx := CraftClearingTx(walletAddr, nonce, gasPrice, gasLimit)
+ gasFeeCap := txmgr.CalcGasFeeCap(head.BaseFee, gasTipCap)
+ tx := CraftClearingTx(walletAddr, nonce, gasFeeCap, gasTipCap)
return types.SignTx(
tx, types.LatestSignerForChainID(chainID), privKey,
@@ -158,16 +171,16 @@ func SignClearingTx(
func CraftClearingTx(
walletAddr common.Address,
nonce uint64,
- gasPrice *big.Int,
- gasLimit uint64,
+ gasFeeCap *big.Int,
+ gasTipCap *big.Int,
) *types.Transaction {
- return types.NewTx(&types.LegacyTx{
- To: &walletAddr,
- Nonce: nonce,
- GasPrice: gasPrice,
- Gas: gasLimit,
- Value: nil,
- Data: nil,
+ return types.NewTx(&types.DynamicFeeTx{
+ To: &walletAddr,
+ Nonce: nonce,
+ GasFeeCap: gasFeeCap,
+ GasTipCap: gasTipCap,
+ Value: nil,
+ Data: nil,
})
}
diff --git a/go/batch-submitter/drivers/clear_pending_tx_test.go b/go/batch-submitter/drivers/clear_pending_tx_test.go
index b0daf30ed90a..15f67c967401 100644
--- a/go/batch-submitter/drivers/clear_pending_tx_test.go
+++ b/go/batch-submitter/drivers/clear_pending_tx_test.go
@@ -11,8 +11,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/drivers"
"github.com/ethereum-optimism/optimism/go/batch-submitter/mock"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
- "github.com/ethereum-optimism/optimism/go/batch-submitter/utils"
- "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
@@ -27,8 +25,6 @@ func init() {
}
testPrivKey = privKey
testWalletAddr = crypto.PubkeyToAddress(privKey.PublicKey)
- testChainID = new(big.Int).SetUint64(1)
- testGasPrice = new(big.Int).SetUint64(3)
}
var (
@@ -36,21 +32,22 @@ var (
testWalletAddr common.Address
testChainID = big.NewInt(1)
testNonce = uint64(2)
- testGasPrice = big.NewInt(3)
- testGasLimit = uint64(4)
+ testGasFeeCap = big.NewInt(3)
+ testGasTipCap = big.NewInt(4)
testBlockNumber = uint64(5)
+ testBaseFee = big.NewInt(6)
)
// TestCraftClearingTx asserts that CraftClearingTx produces the expected
// unsigned clearing transaction.
func TestCraftClearingTx(t *testing.T) {
tx := drivers.CraftClearingTx(
- testWalletAddr, testNonce, testGasPrice, testGasLimit,
+ testWalletAddr, testNonce, testGasFeeCap, testGasTipCap,
)
require.Equal(t, &testWalletAddr, tx.To())
require.Equal(t, testNonce, tx.Nonce())
- require.Equal(t, testGasPrice, tx.GasPrice())
- require.Equal(t, testGasLimit, tx.Gas())
+ require.Equal(t, testGasFeeCap, tx.GasFeeCap())
+ require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data())
}
@@ -59,21 +56,31 @@ func TestCraftClearingTx(t *testing.T) {
// clearing transaction when the call to EstimateGas succeeds.
func TestSignClearingTxEstimateGasSuccess(t *testing.T) {
l1Client := mock.NewL1Client(mock.L1ClientConfig{
- EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) {
- return testGasLimit, nil
+ HeaderByNumber: func(_ context.Context, _ *big.Int) (*types.Header, error) {
+ return &types.Header{
+ BaseFee: testBaseFee,
+ }, nil
+ },
+ SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
+ return testGasTipCap, nil
},
})
+ expGasFeeCap := new(big.Int).Add(
+ testGasTipCap,
+ new(big.Int).Mul(testBaseFee, big.NewInt(2)),
+ )
+
tx, err := drivers.SignClearingTx(
- context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client,
+ "TEST", context.Background(), testWalletAddr, testNonce, l1Client,
testPrivKey, testChainID,
)
require.Nil(t, err)
require.NotNil(t, tx)
require.Equal(t, &testWalletAddr, tx.To())
require.Equal(t, testNonce, tx.Nonce())
- require.Equal(t, testGasPrice, tx.GasPrice())
- require.Equal(t, testGasLimit, tx.Gas())
+ require.Equal(t, expGasFeeCap, tx.GasFeeCap())
+ require.Equal(t, testGasTipCap, tx.GasTipCap())
require.Equal(t, new(big.Int), tx.Value())
require.Nil(t, tx.Data())
@@ -83,22 +90,44 @@ func TestSignClearingTxEstimateGasSuccess(t *testing.T) {
require.Equal(t, testWalletAddr, sender)
}
-// TestSignClearingTxEstimateGasFail asserts that signing a clearing transaction
-// will fail if the underlying call to EstimateGas fails.
-func TestSignClearingTxEstimateGasFail(t *testing.T) {
- errEstimateGas := errors.New("estimate gas")
+// TestSignClearingTxSuggestGasTipCapFail asserts that signing a clearing
+// transaction will fail if the underlying call to SuggestGasTipCap fails.
+func TestSignClearingTxSuggestGasTipCapFail(t *testing.T) {
+ errSuggestGasTipCap := errors.New("suggest gas tip cap")
l1Client := mock.NewL1Client(mock.L1ClientConfig{
- EstimateGas: func(_ context.Context, _ ethereum.CallMsg) (uint64, error) {
- return 0, errEstimateGas
+ SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
+ return nil, errSuggestGasTipCap
},
})
tx, err := drivers.SignClearingTx(
- context.Background(), testWalletAddr, testNonce, testGasPrice, l1Client,
+ "TEST", context.Background(), testWalletAddr, testNonce, l1Client,
testPrivKey, testChainID,
)
- require.Equal(t, errEstimateGas, err)
+ require.Equal(t, errSuggestGasTipCap, err)
+ require.Nil(t, tx)
+}
+
+// TestSignClearingTxHeaderByNumberFail asserts that signing a clearing
+// transaction will fail if the underlying call to HeaderByNumber fails.
+func TestSignClearingTxHeaderByNumberFail(t *testing.T) {
+ errHeaderByNumber := errors.New("header by number")
+
+ l1Client := mock.NewL1Client(mock.L1ClientConfig{
+ HeaderByNumber: func(_ context.Context, _ *big.Int) (*types.Header, error) {
+ return nil, errHeaderByNumber
+ },
+ SuggestGasTipCap: func(_ context.Context) (*big.Int, error) {
+ return testGasTipCap, nil
+ },
+ })
+
+ tx, err := drivers.SignClearingTx(
+ "TEST", context.Background(), testWalletAddr, testNonce, l1Client,
+ testPrivKey, testChainID,
+ )
+ require.Equal(t, errHeaderByNumber, err)
require.Nil(t, tx)
}
@@ -117,22 +146,26 @@ func newClearPendingTxHarnessWithNumConfs(
return testBlockNumber, nil
}
}
+ if l1ClientConfig.HeaderByNumber == nil {
+ l1ClientConfig.HeaderByNumber = func(_ context.Context, _ *big.Int) (*types.Header, error) {
+ return &types.Header{
+ BaseFee: testBaseFee,
+ }, nil
+ }
+ }
if l1ClientConfig.NonceAt == nil {
l1ClientConfig.NonceAt = func(_ context.Context, _ common.Address, _ *big.Int) (uint64, error) {
return testNonce, nil
}
}
- if l1ClientConfig.EstimateGas == nil {
- l1ClientConfig.EstimateGas = func(_ context.Context, _ ethereum.CallMsg) (uint64, error) {
- return testGasLimit, nil
+ if l1ClientConfig.SuggestGasTipCap == nil {
+ l1ClientConfig.SuggestGasTipCap = func(_ context.Context) (*big.Int, error) {
+ return testGasTipCap, nil
}
}
l1Client := mock.NewL1Client(l1ClientConfig)
txMgr := txmgr.NewSimpleTxManager("test", txmgr.Config{
- MinGasPrice: utils.GasPriceFromGwei(1),
- MaxGasPrice: utils.GasPriceFromGwei(100),
- GasRetryIncrement: utils.GasPriceFromGwei(5),
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
@@ -200,11 +233,14 @@ func TestClearPendingTxTimeout(t *testing.T) {
},
})
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
err := drivers.ClearPendingTx(
- "test", context.Background(), h.txMgr, h.l1Client, testWalletAddr,
- testPrivKey, testChainID,
+ "test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
+ testChainID,
)
- require.Equal(t, txmgr.ErrPublishTimeout, err)
+ require.Equal(t, context.DeadlineExceeded, err)
}
// TestClearPendingTxMultipleConfs tests we wait the appropriate number of
@@ -225,12 +261,15 @@ func TestClearPendingTxMultipleConfs(t *testing.T) {
},
}, numConfs)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
// The txmgr should timeout waiting for the txn to confirm.
err := drivers.ClearPendingTx(
- "test", context.Background(), h.txMgr, h.l1Client, testWalletAddr,
- testPrivKey, testChainID,
+ "test", ctx, h.txMgr, h.l1Client, testWalletAddr, testPrivKey,
+ testChainID,
)
- require.Equal(t, txmgr.ErrPublishTimeout, err)
+ require.Equal(t, context.DeadlineExceeded, err)
// Now set the chain height to the earliest the transaction will be
// considered sufficiently confirmed.
diff --git a/go/batch-submitter/drivers/interface.go b/go/batch-submitter/drivers/interface.go
index 99c2f1f55b21..286518ef5c43 100644
--- a/go/batch-submitter/drivers/interface.go
+++ b/go/batch-submitter/drivers/interface.go
@@ -4,7 +4,6 @@ import (
"context"
"math/big"
- "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
@@ -12,12 +11,9 @@ import (
// L1Client is an abstraction over an L1 Ethereum client functionality required
// by the batch submitter.
type L1Client interface {
- // EstimateGas tries to estimate the gas needed to execute a specific
- // transaction based on the current pending state of the backend blockchain.
- // There is no guarantee that this is the true gas limit requirement as
- // other transactions may be added or removed by miners, but it should
- // provide a basis for setting a reasonable default.
- EstimateGas(context.Context, ethereum.CallMsg) (uint64, error)
+ // HeaderByNumber returns a block header from the current canonical chain.
+ // If number is nil, the latest known header is returned.
+ HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
@@ -30,6 +26,10 @@ type L1Client interface {
// method to get the contract address after the transaction has been mined.
SendTransaction(context.Context, *types.Transaction) error
+ // SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
+ // to allow a timely execution of a transaction.
+ SuggestGasTipCap(context.Context) (*big.Int, error)
+
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
diff --git a/go/batch-submitter/drivers/max_priority_fee_fallback.go b/go/batch-submitter/drivers/max_priority_fee_fallback.go
new file mode 100644
index 000000000000..76722fcbc354
--- /dev/null
+++ b/go/batch-submitter/drivers/max_priority_fee_fallback.go
@@ -0,0 +1,26 @@
+package drivers
+
+import (
+ "errors"
+ "math/big"
+ "strings"
+)
+
+var (
+ errMaxPriorityFeePerGasNotFound = errors.New(
+ "Method eth_maxPriorityFeePerGas not found",
+ )
+
+ // FallbackGasTipCap is the default fallback gasTipCap used when we are
+ // unable to query an L1 backend for a suggested gasTipCap.
+ FallbackGasTipCap = big.NewInt(1500000000)
+)
+
+// IsMaxPriorityFeePerGasNotFoundError returns true if the provided error
+// signals that the backend does not support the eth_maxPrirorityFeePerGas
+// method. In this case, the caller should fallback to using the constant above.
+func IsMaxPriorityFeePerGasNotFoundError(err error) bool {
+ return strings.Contains(
+ err.Error(), errMaxPriorityFeePerGasNotFound.Error(),
+ )
+}
diff --git a/go/batch-submitter/drivers/proposer/driver.go b/go/batch-submitter/drivers/proposer/driver.go
index cc8ff1790cd9..a735117f24cb 100644
--- a/go/batch-submitter/drivers/proposer/driver.go
+++ b/go/batch-submitter/drivers/proposer/driver.go
@@ -14,7 +14,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
"github.com/ethereum-optimism/optimism/l2geth/log"
- "github.com/ethereum-optimism/optimism/l2geth/params"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
@@ -197,22 +196,43 @@ func (d *Driver) CraftBatchTx(
}
opts.Context = ctx
opts.Nonce = nonce
- opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true
blockOffset := new(big.Int).SetUint64(d.cfg.BlockOffset)
offsetStartsAtIndex := new(big.Int).Sub(start, blockOffset)
- return d.sccContract.AppendStateBatch(opts, stateRoots, offsetStartsAtIndex)
+ tx, err := d.sccContract.AppendStateBatch(
+ opts, stateRoots, offsetStartsAtIndex,
+ )
+ switch {
+ case err == nil:
+ return tx, nil
+
+ // If the transaction failed because the backend does not support
+ // eth_maxPriorityFeePerGas, fallback to using the default constant.
+ // Currently Alchemy is the only backend provider that exposes this method,
+ // so in the event their API is unreachable we can fallback to a degraded
+ // mode of operation. This also applies to our test environments, as hardhat
+ // doesn't support the query either.
+ case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
+ log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
+ "by current backend, using fallback gasTipCap")
+ opts.GasTipCap = drivers.FallbackGasTipCap
+ return d.sccContract.AppendStateBatch(
+ opts, stateRoots, offsetStartsAtIndex,
+ )
+
+ default:
+ return nil, err
+ }
}
-// SubmitBatchTx using the passed transaction as a template, signs and publishes
-// an otherwise identical transaction after setting the provided gas price. The
-// final transaction is returned to the caller.
+// SubmitBatchTx using the passed transaction as a template, signs and
+// publishes the transaction unmodified apart from sampling the current gas
+// price. The final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
- gasPrice *big.Int,
) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID(
@@ -223,7 +243,25 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
- opts.GasPrice = gasPrice
- return d.rawSccContract.RawTransact(opts, tx.Data())
+ finalTx, err := d.rawSccContract.RawTransact(opts, tx.Data())
+ switch {
+ case err == nil:
+ return finalTx, nil
+
+ // If the transaction failed because the backend does not support
+ // eth_maxPriorityFeePerGas, fallback to using the default constant.
+ // Currently Alchemy is the only backend provider that exposes this method,
+ // so in the event their API is unreachable we can fallback to a degraded
+ // mode of operation. This also applies to our test environments, as hardhat
+ // doesn't support the query either.
+ case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
+ log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
+ "by current backend, using fallback gasTipCap")
+ opts.GasTipCap = drivers.FallbackGasTipCap
+ return d.rawSccContract.RawTransact(opts, tx.Data())
+
+ default:
+ return nil, err
+ }
}
diff --git a/go/batch-submitter/drivers/sequencer/driver.go b/go/batch-submitter/drivers/sequencer/driver.go
index 1a6f20362162..b83b2c0d98d6 100644
--- a/go/batch-submitter/drivers/sequencer/driver.go
+++ b/go/batch-submitter/drivers/sequencer/driver.go
@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/go/batch-submitter/metrics"
"github.com/ethereum-optimism/optimism/go/batch-submitter/txmgr"
l2ethclient "github.com/ethereum-optimism/optimism/l2geth/ethclient"
- "github.com/ethereum-optimism/optimism/l2geth/params"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
@@ -233,20 +232,37 @@ func (d *Driver) CraftBatchTx(
}
opts.Context = ctx
opts.Nonce = nonce
- opts.GasPrice = big.NewInt(params.GWei) // dummy
opts.NoSend = true
- return d.rawCtcContract.RawTransact(opts, batchCallData)
+ tx, err := d.rawCtcContract.RawTransact(opts, batchCallData)
+ switch {
+ case err == nil:
+ return tx, nil
+
+ // If the transaction failed because the backend does not support
+ // eth_maxPriorityFeePerGas, fallback to using the default constant.
+ // Currently Alchemy is the only backend provider that exposes this
+ // method, so in the event their API is unreachable we can fallback to a
+ // degraded mode of operation. This also applies to our test
+ // environments, as hardhat doesn't support the query either.
+ case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
+ log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
+ "by current backend, using fallback gasTipCap")
+ opts.GasTipCap = drivers.FallbackGasTipCap
+ return d.rawCtcContract.RawTransact(opts, batchCallData)
+
+ default:
+ return nil, err
+ }
}
}
// SubmitBatchTx using the passed transaction as a template, signs and publishes
-// an otherwise identical transaction after setting the provided gas price. The
+// the transaction unmodified apart from sampling the current gas price. The
// final transaction is returned to the caller.
func (d *Driver) SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
- gasPrice *big.Int,
) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID(
@@ -257,7 +273,25 @@ func (d *Driver) SubmitBatchTx(
}
opts.Context = ctx
opts.Nonce = new(big.Int).SetUint64(tx.Nonce())
- opts.GasPrice = gasPrice
- return d.rawCtcContract.RawTransact(opts, tx.Data())
+ finalTx, err := d.rawCtcContract.RawTransact(opts, tx.Data())
+ switch {
+ case err == nil:
+ return finalTx, nil
+
+ // If the transaction failed because the backend does not support
+ // eth_maxPriorityFeePerGas, fallback to using the default constant.
+ // Currently Alchemy is the only backend provider that exposes this method,
+ // so in the event their API is unreachable we can fallback to a degraded
+ // mode of operation. This also applies to our test environments, as hardhat
+ // doesn't support the query either.
+ case drivers.IsMaxPriorityFeePerGasNotFoundError(err):
+ log.Warn(d.cfg.Name + " eth_maxPriorityFeePerGas is unsupported " +
+ "by current backend, using fallback gasTipCap")
+ opts.GasTipCap = drivers.FallbackGasTipCap
+ return d.rawCtcContract.RawTransact(opts, tx.Data())
+
+ default:
+ return nil, err
+ }
}
diff --git a/go/batch-submitter/flags/flags.go b/go/batch-submitter/flags/flags.go
index f083ed39ce6f..c3f97c4e86d1 100644
--- a/go/batch-submitter/flags/flags.go
+++ b/go/batch-submitter/flags/flags.go
@@ -151,18 +151,6 @@ var (
Value: 1,
EnvVar: prefixEnvVar("BLOCK_OFFSET"),
}
- MaxGasPriceInGweiFlag = cli.Uint64Flag{
- Name: "max-gas-price-in-gwei",
- Usage: "Maximum gas price the batch submitter can use for transactions",
- Value: 100,
- EnvVar: prefixEnvVar("MAX_GAS_PRICE_IN_GWEI"),
- }
- GasRetryIncrementFlag = cli.Uint64Flag{
- Name: "gas-retry-increment",
- Usage: "Default step by which to increment gas price bumps",
- Value: 5,
- EnvVar: prefixEnvVar("GAS_RETRY_INCREMENT_FLAG"),
- }
SequencerPrivateKeyFlag = cli.StringFlag{
Name: "sequencer-private-key",
Usage: "The private key to use for sending to the sequencer contract",
@@ -240,8 +228,6 @@ var optionalFlags = []cli.Flag{
SentryDsnFlag,
SentryTraceRateFlag,
BlockOffsetFlag,
- MaxGasPriceInGweiFlag,
- GasRetryIncrementFlag,
SequencerPrivateKeyFlag,
ProposerPrivateKeyFlag,
MnemonicFlag,
diff --git a/go/batch-submitter/mock/l1client.go b/go/batch-submitter/mock/l1client.go
index 4ce9ac10c45e..cdb7df333d6c 100644
--- a/go/batch-submitter/mock/l1client.go
+++ b/go/batch-submitter/mock/l1client.go
@@ -5,7 +5,6 @@ import (
"math/big"
"sync"
- "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
@@ -16,12 +15,9 @@ type L1ClientConfig struct {
// BlockNumber returns the most recent block number.
BlockNumber func(context.Context) (uint64, error)
- // EstimateGas tries to estimate the gas needed to execute a specific
- // transaction based on the current pending state of the backend blockchain.
- // There is no guarantee that this is the true gas limit requirement as
- // other transactions may be added or removed by miners, but it should
- // provide a basis for setting a reasonable default.
- EstimateGas func(context.Context, ethereum.CallMsg) (uint64, error)
+ // HeaderByNumber returns a block header from the current canonical chain.
+ // If number is nil, the latest known header is returned.
+ HeaderByNumber func(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
@@ -34,6 +30,10 @@ type L1ClientConfig struct {
// method to get the contract address after the transaction has been mined.
SendTransaction func(context.Context, *types.Transaction) error
+ // SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
+ // to allow a timely execution of a transaction.
+ SuggestGasTipCap func(context.Context) (*big.Int, error)
+
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error)
@@ -61,12 +61,13 @@ func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) {
return c.cfg.BlockNumber(ctx)
}
-// EstimateGas executes the mock EstimateGas method.
-func (c *L1Client) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) {
+// HeaderByNumber returns a block header from the current canonical chain. If
+// number is nil, the latest known header is returned.
+func (c *L1Client) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*types.Header, error) {
c.mu.RLock()
defer c.mu.RUnlock()
- return c.cfg.EstimateGas(ctx, call)
+ return c.cfg.HeaderByNumber(ctx, blockNumber)
}
// NonceAt executes the mock NonceAt method.
@@ -85,6 +86,15 @@ func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) e
return c.cfg.SendTransaction(ctx, tx)
}
+// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to
+// allow a timely execution of a transaction.
+func (c *L1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+
+ return c.cfg.SuggestGasTipCap(ctx)
+}
+
// TransactionReceipt executes the mock TransactionReceipt method.
func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.mu.RLock()
@@ -103,17 +113,17 @@ func (c *L1Client) SetBlockNumberFunc(
c.cfg.BlockNumber = f
}
-// SetEstimateGasFunc overrwrites the mock EstimateGas method.
-func (c *L1Client) SetEstimateGasFunc(
- f func(context.Context, ethereum.CallMsg) (uint64, error)) {
+// SetHeaderByNumberFunc overwrites the mock HeaderByNumber method.
+func (c *L1Client) SetHeaderByNumberFunc(
+ f func(ctx context.Context, blockNumber *big.Int) (*types.Header, error)) {
c.mu.Lock()
defer c.mu.Unlock()
- c.cfg.EstimateGas = f
+ c.cfg.HeaderByNumber = f
}
-// SetNonceAtFunc overrwrites the mock NonceAt method.
+// SetNonceAtFunc overwrites the mock NonceAt method.
func (c *L1Client) SetNonceAtFunc(
f func(context.Context, common.Address, *big.Int) (uint64, error)) {
@@ -123,7 +133,7 @@ func (c *L1Client) SetNonceAtFunc(
c.cfg.NonceAt = f
}
-// SetSendTransactionFunc overrwrites the mock SendTransaction method.
+// SetSendTransactionFunc overwrites the mock SendTransaction method.
func (c *L1Client) SetSendTransactionFunc(
f func(context.Context, *types.Transaction) error) {
@@ -133,6 +143,16 @@ func (c *L1Client) SetSendTransactionFunc(
c.cfg.SendTransaction = f
}
+// SetSuggestGasTipCapFunc overwrites themock SuggestGasTipCap method.
+func (c *L1Client) SetSuggestGasTipCapFunc(
+ f func(context.Context) (*big.Int, error)) {
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ c.cfg.SuggestGasTipCap = f
+}
+
// SetTransactionReceiptFunc overwrites the mock TransactionReceipt method.
func (c *L1Client) SetTransactionReceiptFunc(
f func(context.Context, common.Hash) (*types.Receipt, error)) {
diff --git a/go/batch-submitter/service.go b/go/batch-submitter/service.go
index 812020f0eb1e..c172396439a0 100644
--- a/go/batch-submitter/service.go
+++ b/go/batch-submitter/service.go
@@ -55,12 +55,11 @@ type Driver interface {
) (*types.Transaction, error)
// SubmitBatchTx using the passed transaction as a template, signs and
- // publishes an otherwise identical transaction after setting the provided
- // gas price. The final transaction is returned to the caller.
+ // publishes the transaction unmodified apart from sampling the current gas
+ // price. The final transaction is returned to the caller.
SubmitBatchTx(
ctx context.Context,
tx *types.Transaction,
- gasPrice *big.Int,
) (*types.Transaction, error)
}
@@ -194,15 +193,11 @@ func (s *Service) eventLoop() {
// Construct the transaction submission clousure that will attempt
// to send the next transaction at the given nonce and gas price.
- sendTx := func(
- ctx context.Context,
- gasPrice *big.Int,
- ) (*types.Transaction, error) {
+ sendTx := func(ctx context.Context) (*types.Transaction, error) {
log.Info(name+" attempting batch tx", "start", start,
- "end", end, "nonce", nonce,
- "gasPrice", gasPrice)
+ "end", end, "nonce", nonce)
- tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx, gasPrice)
+ tx, err := s.cfg.Driver.SubmitBatchTx(ctx, tx)
if err != nil {
return nil, err
}
@@ -213,7 +208,6 @@ func (s *Service) eventLoop() {
"end", end,
"nonce", nonce,
"tx_hash", tx.Hash(),
- "gasPrice", gasPrice,
)
return tx, nil
diff --git a/go/batch-submitter/txmgr/txmgr.go b/go/batch-submitter/txmgr/txmgr.go
index f8a5750fc6ba..dd9a3c7210fd 100644
--- a/go/batch-submitter/txmgr/txmgr.go
+++ b/go/batch-submitter/txmgr/txmgr.go
@@ -2,46 +2,27 @@ package txmgr
import (
"context"
- "errors"
"math/big"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
-// ErrPublishTimeout signals that the tx manager did not receive a confirmation
-// for a given tx after publishing with the maximum gas price and waiting out a
-// resubmission timeout.
-var ErrPublishTimeout = errors.New("failed to publish tx with max gas price")
-
// SendTxFunc defines a function signature for publishing a desired tx with a
// specific gas price. Implementations of this signature should also return
// promptly when the context is canceled.
-type SendTxFunc = func(
- ctx context.Context, gasPrice *big.Int) (*types.Transaction, error)
+type SendTxFunc = func(ctx context.Context) (*types.Transaction, error)
// Config houses parameters for altering the behavior of a SimpleTxManager.
type Config struct {
+ // Name the name of the driver to appear in log lines.
Name string
- // MinGasPrice is the minimum gas price (in gwei). This is used as the
- // initial publication attempt.
- MinGasPrice *big.Int
-
- // MaxGasPrice is the maximum gas price (in gwei). This is used to clamp
- // the upper end of the range that the TxManager will ever publish when
- // attempting to confirm a transaction.
- MaxGasPrice *big.Int
-
- // GasRetryIncrement is the additive gas price (in gwei) that will be
- // used to bump each successive tx after a ResubmissionTimeout has
- // elapsed.
- GasRetryIncrement *big.Int
-
// ResubmissionTimeout is the interval at which, if no previously
// published transaction has been mined, the new tx with a bumped gas
// price will be published. Only one publication at MaxGasPrice will be
@@ -135,25 +116,29 @@ func (m *SimpleTxManager) Send(
// background, returning the first successfully mined receipt back to
// the main event loop via receiptChan.
receiptChan := make(chan *types.Receipt, 1)
- sendTxAsync := func(gasPrice *big.Int) {
+ sendTxAsync := func() {
defer wg.Done()
// Sign and publish transaction with current gas price.
- tx, err := sendTx(ctxc, gasPrice)
+ tx, err := sendTx(ctxc)
if err != nil {
if err == context.Canceled ||
strings.Contains(err.Error(), "context canceled") {
return
}
- log.Error(name+" unable to publish transaction",
- "gas_price", gasPrice, "err", err)
+ log.Error(name+" unable to publish transaction", "err", err)
+ if shouldAbortImmediately(err) {
+ cancel()
+ }
// TODO(conner): add retry?
return
}
txHash := tx.Hash()
+ gasTipCap := tx.GasTipCap()
+ gasFeeCap := tx.GasFeeCap()
log.Info(name+" transaction published successfully", "hash", txHash,
- "gas_price", gasPrice)
+ "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
// Wait for the transaction to be mined, reporting the receipt
// back to the main event loop if found.
@@ -163,7 +148,7 @@ func (m *SimpleTxManager) Send(
)
if err != nil {
log.Debug(name+" send tx failed", "hash", txHash,
- "gas_price", gasPrice, "err", err)
+ "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap, "err", err)
}
if receipt != nil {
// Use non-blocking select to ensure function can exit
@@ -171,20 +156,17 @@ func (m *SimpleTxManager) Send(
select {
case receiptChan <- receipt:
log.Trace(name+" send tx succeeded", "hash", txHash,
- "gas_price", gasPrice)
+ "gasTipCap", gasTipCap, "gasFeeCap", gasFeeCap)
default:
}
}
}
- // Initialize our initial gas price to the configured minimum.
- curGasPrice := new(big.Int).Set(m.cfg.MinGasPrice)
-
// Submit and wait for the receipt at our first gas price in the
// background, before entering the event loop and waiting out the
// resubmission timeout.
wg.Add(1)
- go sendTxAsync(curGasPrice)
+ go sendTxAsync()
for {
select {
@@ -192,24 +174,9 @@ func (m *SimpleTxManager) Send(
// Whenever a resubmission timeout has elapsed, bump the gas
// price and publish a new transaction.
case <-time.After(m.cfg.ResubmissionTimeout):
- // If our last attempt published at the max gas price,
- // return an error as we are unlikely to succeed in
- // publishing. This also indicates that the max gas
- // price should likely be adjusted higher for the
- // daemon.
- if curGasPrice.Cmp(m.cfg.MaxGasPrice) >= 0 {
- return nil, ErrPublishTimeout
- }
-
- // Bump the gas price using linear gas price increments.
- curGasPrice = NextGasPrice(
- curGasPrice, m.cfg.GasRetryIncrement,
- m.cfg.MaxGasPrice,
- )
-
// Submit and wait for the bumped traction to confirm.
wg.Add(1)
- go sendTxAsync(curGasPrice)
+ go sendTxAsync()
// The passed context has been canceled, i.e. in the event of a
// shutdown.
@@ -223,6 +190,13 @@ func (m *SimpleTxManager) Send(
}
}
+// shouldAbortImmediately returns true if the txmgr should cancel all
+// publication attempts and retry. For now, this only includes nonce errors, as
+// that error indicates that none of the transactions will ever confirm.
+func shouldAbortImmediately(err error) bool {
+ return strings.Contains(err.Error(), core.ErrNonceTooLow.Error())
+}
+
// WaitMined blocks until the backend indicates confirmation of tx and returns
// the tx receipt. Queries are made every queryInterval, regardless of whether
// the backend returns an error. This method can be canceled using the passed
@@ -289,17 +263,12 @@ func WaitMined(
}
}
-// NextGasPrice bumps the current gas price using an additive gasRetryIncrement,
-// clamping the resulting value to maxGasPrice.
-//
-// NOTE: This method does not mutate curGasPrice, but instead returns a copy.
-// This removes the possiblity of races occuring from goroutines sharing access
-// to the same underlying big.Int.
-func NextGasPrice(curGasPrice, gasRetryIncrement, maxGasPrice *big.Int) *big.Int {
- nextGasPrice := new(big.Int).Set(curGasPrice)
- nextGasPrice.Add(nextGasPrice, gasRetryIncrement)
- if nextGasPrice.Cmp(maxGasPrice) == 1 {
- nextGasPrice.Set(maxGasPrice)
- }
- return nextGasPrice
+// CalcGasFeeCap deterministically computes the recommended gas fee cap given
+// the base fee and gasTipCap. The resulting gasFeeCap is equal to:
+// gasTipCap + 2*baseFee.
+func CalcGasFeeCap(baseFee, gasTipCap *big.Int) *big.Int {
+ return new(big.Int).Add(
+ gasTipCap,
+ new(big.Int).Mul(baseFee, big.NewInt(2)),
+ )
}
diff --git a/go/batch-submitter/txmgr/txmgr_test.go b/go/batch-submitter/txmgr/txmgr_test.go
index 9a27c8529fbe..186e34bc211a 100644
--- a/go/batch-submitter/txmgr/txmgr_test.go
+++ b/go/batch-submitter/txmgr/txmgr_test.go
@@ -14,69 +14,12 @@ import (
"github.com/stretchr/testify/require"
)
-// TestNextGasPrice asserts that NextGasPrice properly bumps the passed current
-// gas price, and clamps it to the max gas price. It also tests that
-// NextGasPrice doesn't mutate the passed curGasPrice argument.
-func TestNextGasPrice(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- curGasPrice *big.Int
- gasRetryIncrement *big.Int
- maxGasPrice *big.Int
- expGasPrice *big.Int
- }{
- {
- name: "increment below max",
- curGasPrice: new(big.Int).SetUint64(5),
- gasRetryIncrement: new(big.Int).SetUint64(10),
- maxGasPrice: new(big.Int).SetUint64(20),
- expGasPrice: new(big.Int).SetUint64(15),
- },
- {
- name: "increment equal max",
- curGasPrice: new(big.Int).SetUint64(5),
- gasRetryIncrement: new(big.Int).SetUint64(10),
- maxGasPrice: new(big.Int).SetUint64(15),
- expGasPrice: new(big.Int).SetUint64(15),
- },
- {
- name: "increment above max",
- curGasPrice: new(big.Int).SetUint64(5),
- gasRetryIncrement: new(big.Int).SetUint64(10),
- maxGasPrice: new(big.Int).SetUint64(12),
- expGasPrice: new(big.Int).SetUint64(12),
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- // Copy curGasPrice, as we will later test for mutation.
- curGasPrice := new(big.Int).Set(test.curGasPrice)
-
- nextGasPrice := txmgr.NextGasPrice(
- curGasPrice, test.gasRetryIncrement,
- test.maxGasPrice,
- )
-
- require.Equal(t, nextGasPrice, test.expGasPrice)
-
- // Ensure curGasPrice hasn't been mutated. This check
- // enforces that NextGasPrice creates a copy internally.
- // Failure to do so could result in gas price bumps
- // being read concurrently from other goroutines, and
- // introduce race conditions.
- require.Equal(t, curGasPrice, test.curGasPrice)
- })
- }
-}
-
// testHarness houses the necessary resources to test the SimpleTxManager.
type testHarness struct {
- cfg txmgr.Config
- mgr txmgr.TxManager
- backend *mockBackend
+ cfg txmgr.Config
+ mgr txmgr.TxManager
+ backend *mockBackend
+ gasPricer *gasPricer
}
// newTestHarnessWithConfig initializes a testHarness with a specific
@@ -86,9 +29,10 @@ func newTestHarnessWithConfig(cfg txmgr.Config) *testHarness {
mgr := txmgr.NewSimpleTxManager("TEST", cfg, backend)
return &testHarness{
- cfg: cfg,
- mgr: mgr,
- backend: backend,
+ cfg: cfg,
+ mgr: mgr,
+ backend: backend,
+ gasPricer: newGasPricer(3),
}
}
@@ -100,17 +44,54 @@ func newTestHarness() *testHarness {
func configWithNumConfs(numConfirmations uint64) txmgr.Config {
return txmgr.Config{
- MinGasPrice: new(big.Int).SetUint64(5),
- MaxGasPrice: new(big.Int).SetUint64(50),
- GasRetryIncrement: new(big.Int).SetUint64(5),
ResubmissionTimeout: time.Second,
ReceiptQueryInterval: 50 * time.Millisecond,
NumConfirmations: numConfirmations,
}
}
+type gasPricer struct {
+ epoch int64
+ mineAtEpoch int64
+ baseGasTipFee *big.Int
+ baseBaseFee *big.Int
+ mu sync.Mutex
+}
+
+func newGasPricer(mineAtEpoch int64) *gasPricer {
+ return &gasPricer{
+ mineAtEpoch: mineAtEpoch,
+ baseGasTipFee: big.NewInt(5),
+ baseBaseFee: big.NewInt(7),
+ }
+}
+
+func (g *gasPricer) expGasFeeCap() *big.Int {
+ _, gasFeeCap := g.feesForEpoch(g.mineAtEpoch)
+ return gasFeeCap
+}
+
+func (g *gasPricer) feesForEpoch(epoch int64) (*big.Int, *big.Int) {
+ epochBaseFee := new(big.Int).Mul(g.baseBaseFee, big.NewInt(epoch))
+ epochGasTipCap := new(big.Int).Mul(g.baseGasTipFee, big.NewInt(epoch))
+ epochGasFeeCap := txmgr.CalcGasFeeCap(epochBaseFee, epochGasTipCap)
+
+ return epochGasTipCap, epochGasFeeCap
+}
+
+func (g *gasPricer) sample() (*big.Int, *big.Int, bool) {
+ g.mu.Lock()
+ defer g.mu.Unlock()
+
+ g.epoch++
+ epochGasTipCap, epochGasFeeCap := g.feesForEpoch(g.epoch)
+ shouldMine := g.epoch == g.mineAtEpoch
+
+ return epochGasTipCap, epochGasFeeCap, shouldMine
+}
+
type minedTxInfo struct {
- gasPrice *big.Int
+ gasFeeCap *big.Int
blockNumber uint64
}
@@ -133,17 +114,17 @@ func newMockBackend() *mockBackend {
}
}
-// mine records a (txHash, gasPrice) as confirmed. Subsequent calls to
+// mine records a (txHash, gasFeeCap) as confirmed. Subsequent calls to
// TransactionReceipt with a matching txHash will result in a non-nil receipt.
// If a nil txHash is supplied this has the effect of mining an empty block.
-func (b *mockBackend) mine(txHash *common.Hash, gasPrice *big.Int) {
+func (b *mockBackend) mine(txHash *common.Hash, gasFeeCap *big.Int) {
b.mu.Lock()
defer b.mu.Unlock()
b.blockHeight++
if txHash != nil {
b.minedTxs[*txHash] = minedTxInfo{
- gasPrice: gasPrice,
+ gasFeeCap: gasFeeCap,
blockNumber: b.blockHeight,
}
}
@@ -159,7 +140,7 @@ func (b *mockBackend) BlockNumber(ctx context.Context) (uint64, error) {
// TransactionReceipt queries the mockBackend for a mined txHash. If none is
// found, nil is returned for both return values. Otherwise, it retruns a
-// receipt containing the txHash and the gasPrice used in the GasUsed to make
+// receipt containing the txHash and the gasFeeCap used in the GasUsed to make
// the value accessible from our test framework.
func (b *mockBackend) TransactionReceipt(
ctx context.Context,
@@ -174,11 +155,11 @@ func (b *mockBackend) TransactionReceipt(
return nil, nil
}
- // Return the gas price for the transaction in the GasUsed field so that
+ // Return the gas fee cap for the transaction in the GasUsed field so that
// we can assert the proper tx confirmed in our tests.
return &types.Receipt{
TxHash: txHash,
- GasUsed: txInfo.gasPrice.Uint64(),
+ GasUsed: txInfo.gasFeeCap.Uint64(),
BlockNumber: big.NewInt(int64(txInfo.blockNumber)),
}, nil
}
@@ -189,15 +170,16 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
t.Parallel()
h := newTestHarness()
+
+ gasFeeCap := big.NewInt(5)
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
- tx := types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
+ tx := types.NewTx(&types.DynamicFeeTx{
+ GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
- h.backend.mine(&txHash, gasPrice)
+ h.backend.mine(&txHash, gasFeeCap)
return tx, nil
}
@@ -205,7 +187,7 @@ func TestTxMgrConfirmAtMinGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
- require.Equal(t, receipt.GasUsed, h.cfg.MinGasPrice.Uint64())
+ require.Equal(t, gasFeeCap.Uint64(), receipt.GasUsed)
}
// TestTxMgrNeverConfirmCancel asserts that a Send can be canceled even if no
@@ -218,11 +200,10 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
// Don't publish tx to backend, simulating never being mined.
- return types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
+ return types.NewTx(&types.DynamicFeeTx{
+ GasFeeCap: big.NewInt(5),
}), nil
}
@@ -236,21 +217,22 @@ func TestTxMgrNeverConfirmCancel(t *testing.T) {
// TestTxMgrConfirmsAtMaxGasPrice asserts that Send properly returns the max gas
// price receipt if none of the lower gas price txs were mined.
-func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) {
+func TestTxMgrConfirmsAtHigherGasPrice(t *testing.T) {
t.Parallel()
h := newTestHarness()
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
- tx := types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
+ gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
+ tx := types.NewTx(&types.DynamicFeeTx{
+ GasTipCap: gasTipCap,
+ GasFeeCap: gasFeeCap,
})
- if gasPrice.Cmp(h.cfg.MaxGasPrice) == 0 {
+ if shouldMine {
txHash := tx.Hash()
- h.backend.mine(&txHash, gasPrice)
+ h.backend.mine(&txHash, gasFeeCap)
}
return tx, nil
}
@@ -259,40 +241,7 @@ func TestTxMgrConfirmsAtMaxGasPrice(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
- require.Equal(t, receipt.GasUsed, h.cfg.MaxGasPrice.Uint64())
-}
-
-// TestTxMgrConfirmsAtMaxGasPriceDelayed asserts that after the maximum gas
-// price tx has been published, and a resubmission timeout has elapsed, that an
-// error is returned signaling that even our max gas price is taking too long.
-func TestTxMgrConfirmsAtMaxGasPriceDelayed(t *testing.T) {
- t.Parallel()
-
- h := newTestHarness()
-
- sendTxFunc := func(
- ctx context.Context,
- gasPrice *big.Int,
- ) (*types.Transaction, error) {
- tx := types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
- })
- // Delay mining of the max gas price tx by more than the
- // resubmission timeout. Default config uses 1 second. Send
- // should still return an error beforehand.
- if gasPrice.Cmp(h.cfg.MaxGasPrice) == 0 {
- time.AfterFunc(2*time.Second, func() {
- txHash := tx.Hash()
- h.backend.mine(&txHash, gasPrice)
- })
- }
- return tx, nil
- }
-
- ctx := context.Background()
- receipt, err := h.mgr.Send(ctx, sendTxFunc)
- require.Equal(t, err, txmgr.ErrPublishTimeout)
- require.Nil(t, receipt)
+ require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// errRpcFailure is a sentinel error used in testing to fail publications.
@@ -308,14 +257,15 @@ func TestTxMgrBlocksOnFailingRpcCalls(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
return nil, errRpcFailure
}
- ctx := context.Background()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
receipt, err := h.mgr.Send(ctx, sendTxFunc)
- require.Equal(t, err, txmgr.ErrPublishTimeout)
+ require.Equal(t, err, context.DeadlineExceeded)
require.Nil(t, receipt)
}
@@ -329,18 +279,20 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
+ gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
+
// Fail all but the final attempt.
- if gasPrice.Cmp(h.cfg.MaxGasPrice) != 0 {
+ if !shouldMine {
return nil, errRpcFailure
}
- tx := types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
+ tx := types.NewTx(&types.DynamicFeeTx{
+ GasTipCap: gasTipCap,
+ GasFeeCap: gasFeeCap,
})
txHash := tx.Hash()
- h.backend.mine(&txHash, gasPrice)
+ h.backend.mine(&txHash, gasFeeCap)
return tx, nil
}
@@ -349,7 +301,7 @@ func TestTxMgrOnlyOnePublicationSucceeds(t *testing.T) {
require.Nil(t, err)
require.NotNil(t, receipt)
- require.Equal(t, receipt.GasUsed, h.cfg.MaxGasPrice.Uint64())
+ require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestTxMgrConfirmsMinGasPriceAfterBumping delays the mining of the initial tx
@@ -362,16 +314,17 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
sendTxFunc := func(
ctx context.Context,
- gasPrice *big.Int,
) (*types.Transaction, error) {
- tx := types.NewTx(&types.LegacyTx{
- GasPrice: gasPrice,
+ gasTipCap, gasFeeCap, shouldMine := h.gasPricer.sample()
+ tx := types.NewTx(&types.DynamicFeeTx{
+ GasTipCap: gasTipCap,
+ GasFeeCap: gasFeeCap,
})
// Delay mining the tx with the min gas price.
- if gasPrice.Cmp(h.cfg.MinGasPrice) == 0 {
+ if shouldMine {
time.AfterFunc(5*time.Second, func() {
txHash := tx.Hash()
- h.backend.mine(&txHash, gasPrice)
+ h.backend.mine(&txHash, gasFeeCap)
})
}
return tx, nil
@@ -381,7 +334,7 @@ func TestTxMgrConfirmsMinGasPriceAfterBumping(t *testing.T) {
receipt, err := h.mgr.Send(ctx, sendTxFunc)
require.Nil(t, err)
require.NotNil(t, receipt)
- require.Equal(t, receipt.GasUsed, h.cfg.MinGasPrice.Uint64())
+ require.Equal(t, h.gasPricer.expGasFeeCap().Uint64(), receipt.GasUsed)
}
// TestWaitMinedReturnsReceiptOnFirstSuccess insta-mines a transaction and
diff --git a/go/batch-submitter/utils/gas_price.go b/go/batch-submitter/utils/gas_price.go
deleted file mode 100644
index 7a97c572f40e..000000000000
--- a/go/batch-submitter/utils/gas_price.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package utils
-
-import (
- "math/big"
-
- "github.com/ethereum/go-ethereum/params"
-)
-
-// GasPriceFromGwei converts an uint64 gas price in gwei to a big.Int in wei.
-func GasPriceFromGwei(gasPriceInGwei uint64) *big.Int {
- return new(big.Int).SetUint64(gasPriceInGwei * params.GWei)
-}
diff --git a/go/batch-submitter/utils/gas_price_test.go b/go/batch-submitter/utils/gas_price_test.go
deleted file mode 100644
index 284fdc511ca4..000000000000
--- a/go/batch-submitter/utils/gas_price_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package utils_test
-
-import (
- "math/big"
- "testing"
-
- "github.com/ethereum-optimism/optimism/go/batch-submitter/utils"
- "github.com/ethereum/go-ethereum/params"
- "github.com/stretchr/testify/require"
-)
-
-// TestGasPriceFromGwei asserts that the integer value is scaled properly by
-// 10^9.
-func TestGasPriceFromGwei(t *testing.T) {
- require.Equal(t, utils.GasPriceFromGwei(0), new(big.Int))
- require.Equal(t, utils.GasPriceFromGwei(1), big.NewInt(params.GWei))
- require.Equal(t, utils.GasPriceFromGwei(100), big.NewInt(100*params.GWei))
-}
diff --git a/go/proxyd/CHANGELOG.md b/go/proxyd/CHANGELOG.md
index 897c7409f17b..54c6db551d27 100644
--- a/go/proxyd/CHANGELOG.md
+++ b/go/proxyd/CHANGELOG.md
@@ -1,5 +1,11 @@
# @eth-optimism/proxyd
+## 3.7.0
+
+### Minor Changes
+
+- 3c2926b1: Add debug cache status header to proxyd responses
+
## 3.6.0
### Minor Changes
diff --git a/go/proxyd/package.json b/go/proxyd/package.json
index bd84ac5269df..295bae234f2b 100644
--- a/go/proxyd/package.json
+++ b/go/proxyd/package.json
@@ -1,6 +1,6 @@
{
"name": "@eth-optimism/proxyd",
- "version": "3.6.0",
+ "version": "3.7.0",
"private": true,
"dependencies": {}
}
diff --git a/go/proxyd/server.go b/go/proxyd/server.go
index e6d833aafcfc..f5ee57bfe455 100644
--- a/go/proxyd/server.go
+++ b/go/proxyd/server.go
@@ -25,6 +25,7 @@ const (
ContextKeyReqID = "req_id"
ContextKeyXForwardedFor = "x_forwarded_for"
MaxBatchRPCCalls = 100
+ cacheStatusHdr = "X-Proxyd-Cache-Status"
)
type Server struct {
@@ -159,6 +160,7 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
}
batchRes := make([]*RPCRes, len(reqs), len(reqs))
+ var batchContainsCached bool
for i := 0; i < len(reqs); i++ {
req, err := ParseRPCReq(reqs[i])
if err != nil {
@@ -167,9 +169,14 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
continue
}
- batchRes[i] = s.handleSingleRPC(ctx, req)
+ var cached bool
+ batchRes[i], cached = s.handleSingleRPC(ctx, req)
+ if cached {
+ batchContainsCached = true
+ }
}
+ setCacheHeader(w, batchContainsCached)
writeBatchRPCRes(ctx, w, batchRes)
return
}
@@ -181,14 +188,15 @@ func (s *Server) HandleRPC(w http.ResponseWriter, r *http.Request) {
return
}
- backendRes := s.handleSingleRPC(ctx, req)
+ backendRes, cached := s.handleSingleRPC(ctx, req)
+ setCacheHeader(w, cached)
writeRPCRes(ctx, w, backendRes)
}
-func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
+func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) (*RPCRes, bool) {
if err := ValidateRPCReq(req); err != nil {
RecordRPCError(ctx, BackendProxyd, MethodUnknown, err)
- return NewRPCErrorRes(nil, err)
+ return NewRPCErrorRes(nil, err), false
}
group := s.rpcMethodMappings[req.Method]
@@ -202,7 +210,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
"method", req.Method,
)
RecordRPCError(ctx, BackendProxyd, MethodUnknown, ErrMethodNotWhitelisted)
- return NewRPCErrorRes(req.ID, ErrMethodNotWhitelisted)
+ return NewRPCErrorRes(req.ID, ErrMethodNotWhitelisted), false
}
var backendRes *RPCRes
@@ -215,7 +223,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
)
}
if backendRes != nil {
- return backendRes
+ return backendRes, true
}
backendRes, err = s.backendGroups[group].Forward(ctx, req)
@@ -226,7 +234,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
"req_id", GetReqID(ctx),
"err", err,
)
- return NewRPCErrorRes(req.ID, err)
+ return NewRPCErrorRes(req.ID, err), false
}
if backendRes.Error == nil {
@@ -239,7 +247,7 @@ func (s *Server) handleSingleRPC(ctx context.Context, req *RPCReq) *RPCRes {
}
}
- return backendRes
+ return backendRes, false
}
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
@@ -322,6 +330,14 @@ func (s *Server) populateContext(w http.ResponseWriter, r *http.Request) context
)
}
+func setCacheHeader(w http.ResponseWriter, cached bool) {
+ if cached {
+ w.Header().Set(cacheStatusHdr, "HIT")
+ } else {
+ w.Header().Set(cacheStatusHdr, "MISS")
+ }
+}
+
func writeRPCError(ctx context.Context, w http.ResponseWriter, id json.RawMessage, err error) {
var res *RPCRes
if r, ok := err.(*RPCErr); ok {
diff --git a/integration-tests/contracts/Precompiles.sol b/integration-tests/contracts/Precompiles.sol
new file mode 100644
index 000000000000..52fccc0f9559
--- /dev/null
+++ b/integration-tests/contracts/Precompiles.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.9;
+
+contract Precompiles {
+ function expmod(uint256 base, uint256 e, uint256 m) public returns (uint256 o) {
+ assembly {
+ // define pointer
+ let p := mload(0x40)
+ // store data assembly-favouring ways
+ mstore(p, 0x20) // Length of Base
+ mstore(add(p, 0x20), 0x20) // Length of Exponent
+ mstore(add(p, 0x40), 0x20) // Length of Modulus
+ mstore(add(p, 0x60), base) // Base
+ mstore(add(p, 0x80), e) // Exponent
+ mstore(add(p, 0xa0), m) // Modulus
+ if iszero(staticcall(sub(gas(), 2000), 0x05, p, 0xc0, p, 0x20)) {
+ revert(0, 0)
+ }
+ // data
+ o := mload(p)
+ }
+ }
+}
diff --git a/integration-tests/contracts/SelfDestruction.sol b/integration-tests/contracts/SelfDestruction.sol
new file mode 100644
index 000000000000..2a9666a48565
--- /dev/null
+++ b/integration-tests/contracts/SelfDestruction.sol
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.9;
+
+contract SelfDestruction {
+ bytes32 public data = 0x0000000000000000000000000000000000000000000000000000000061626364;
+
+ function setData(bytes32 _data) public {
+ data = _data;
+ }
+
+ function destruct() public {
+ address payable self = payable(address(this));
+ selfdestruct(self);
+ }
+}
diff --git a/integration-tests/hardhat.config.ts b/integration-tests/hardhat.config.ts
index 436206583e7d..6153a8ffc8b1 100644
--- a/integration-tests/hardhat.config.ts
+++ b/integration-tests/hardhat.config.ts
@@ -4,6 +4,7 @@ import { HardhatUserConfig } from 'hardhat/types'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
import 'hardhat-gas-reporter'
+import './tasks/check-block-hashes'
import { envConfig } from './test/shared/utils'
const enableGasReport = !!process.env.ENABLE_GAS_REPORT
diff --git a/integration-tests/package.json b/integration-tests/package.json
index 22aabdbf264f..8e9e87cc16fa 100644
--- a/integration-tests/package.json
+++ b/integration-tests/package.json
@@ -28,9 +28,9 @@
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"devDependencies": {
- "@eth-optimism/contracts": "0.5.9",
- "@eth-optimism/core-utils": "0.7.4",
- "@eth-optimism/message-relayer": "0.2.13",
+ "@eth-optimism/contracts": "0.5.10",
+ "@eth-optimism/core-utils": "0.7.5",
+ "@eth-optimism/message-relayer": "0.2.14",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0",
diff --git a/integration-tests/tasks/check-block-hashes.ts b/integration-tests/tasks/check-block-hashes.ts
new file mode 100644
index 000000000000..c30d67cfdcc2
--- /dev/null
+++ b/integration-tests/tasks/check-block-hashes.ts
@@ -0,0 +1,58 @@
+import { task } from 'hardhat/config'
+import { providers } from 'ethers'
+
+import { die, logStderr } from '../test/shared/utils'
+
+task(
+ 'check-block-hashes',
+ 'Compares the block hashes of two different replicas.'
+)
+ .addPositionalParam('replicaA', 'The first replica')
+ .addPositionalParam('replicaB', 'The second replica')
+ .setAction(async ({ replicaA, replicaB }) => {
+ const providerA = new providers.JsonRpcProvider(replicaA)
+ const providerB = new providers.JsonRpcProvider(replicaB)
+
+ let netA
+ let netB
+ try {
+ netA = await providerA.getNetwork()
+ } catch (e) {
+ console.error(`Error getting network from ${replicaA}:`)
+ die(e)
+ }
+ try {
+ netB = await providerA.getNetwork()
+ } catch (e) {
+ console.error(`Error getting network from ${replicaB}:`)
+ die(e)
+ }
+
+ if (netA.chainId !== netB.chainId) {
+ die('Chain IDs do not match')
+ return
+ }
+
+ logStderr('Getting block height.')
+ const heightA = await providerA.getBlockNumber()
+ const heightB = await providerB.getBlockNumber()
+ const endHeight = Math.min(heightA, heightB)
+ logStderr(`Chose block height: ${endHeight}`)
+
+ for (let n = endHeight; n >= 1; n--) {
+ const blocks = await Promise.all([
+ providerA.getBlock(n),
+ providerB.getBlock(n),
+ ])
+
+ const hashA = blocks[0].hash
+ const hashB = blocks[1].hash
+ if (hashA !== hashB) {
+ console.log(`HASH MISMATCH! block=${n} a=${hashA} b=${hashB}`)
+ continue
+ }
+
+ console.log(`HASHES OK! block=${n} hash=${hashA}`)
+ return
+ }
+ })
diff --git a/integration-tests/test/env-specific/nightly.spec.ts b/integration-tests/test/env-specific/nightly.spec.ts
new file mode 100644
index 000000000000..0ee5e15a344f
--- /dev/null
+++ b/integration-tests/test/env-specific/nightly.spec.ts
@@ -0,0 +1,109 @@
+import { Contract } from 'ethers'
+import { ethers } from 'hardhat'
+
+import { OptimismEnv } from '../shared/env'
+import { expect } from '../shared/setup'
+import { traceToGasByOpcode } from '../hardfork.spec'
+import { envConfig } from '../shared/utils'
+
+describe('Nightly', () => {
+ before(async function () {
+ if (!envConfig.RUN_NIGHTLY_TESTS) {
+ this.skip()
+ }
+ })
+
+ describe('Berlin Hardfork', () => {
+ let env: OptimismEnv
+ let SimpleStorage: Contract
+ let Precompiles: Contract
+
+ before(async () => {
+ env = await OptimismEnv.new()
+ SimpleStorage = await ethers.getContractAt(
+ 'SimpleStorage',
+ '0xE08fFE40748367ddc29B5A154331C73B7FCC13bD',
+ env.l2Wallet
+ )
+
+ Precompiles = await ethers.getContractAt(
+ 'Precompiles',
+ '0x32E8Fbfd0C0bd1117112b249e997C27b0EC7cba2',
+ env.l2Wallet
+ )
+ })
+
+ describe('EIP-2929', () => {
+ it('should update the gas schedule', async () => {
+ const tx = await SimpleStorage.setValueNotXDomain(
+ `0x${'77'.repeat(32)}`
+ )
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ ['0x2bb346f53544c5711502fbcbd1d78dc4fb61ca5f9390b9d6d67f1a3a77de7c39']
+ )
+
+ const berlinSstoreCosts = traceToGasByOpcode(
+ berlinTrace.structLogs,
+ 'SSTORE'
+ )
+ const preBerlinSstoreCosts = traceToGasByOpcode(
+ preBerlinTrace.structLogs,
+ 'SSTORE'
+ )
+ expect(preBerlinSstoreCosts).to.eq(80000)
+ expect(berlinSstoreCosts).to.eq(5300)
+ })
+ })
+
+ describe('EIP-2565', () => {
+ it('should become cheaper', async () => {
+ const tx = await Precompiles.expmod(64, 1, 64, { gasLimit: 5_000_000 })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ ['0x7ba7d273449b0062448fe5e7426bb169a032ce189d0e3781eb21079e85c2d7d5']
+ )
+ expect(berlinTrace.gas).to.be.lt(preBerlinTrace.gas)
+ })
+ })
+
+ describe('Berlin Additional (L1 London)', () => {
+ describe('EIP-3529', () => {
+ it('should remove the refund for selfdestruct', async () => {
+ const Factory__SelfDestruction = await ethers.getContractFactory(
+ 'SelfDestruction',
+ env.l2Wallet
+ )
+
+ const SelfDestruction = await Factory__SelfDestruction.deploy()
+ const tx = await SelfDestruction.destruct({ gasLimit: 5_000_000 })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [
+ '0x948667349f00e996d9267e5c30d72fe7202a0ecdb88bab191e9a022bba6e4cb3',
+ ]
+ )
+ expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
+ })
+ })
+ })
+ })
+})
diff --git a/integration-tests/test/hardfork.spec.ts b/integration-tests/test/hardfork.spec.ts
new file mode 100644
index 000000000000..43f1d89ef440
--- /dev/null
+++ b/integration-tests/test/hardfork.spec.ts
@@ -0,0 +1,209 @@
+import { Contract, BigNumber } from 'ethers'
+import { ethers } from 'hardhat'
+
+import { expect } from './shared/setup'
+import { OptimismEnv } from './shared/env'
+
+export const traceToGasByOpcode = (structLogs, opcode) => {
+ let gas = 0
+ const opcodes = []
+ for (const log of structLogs) {
+ if (log.op === opcode) {
+ opcodes.push(opcode)
+ gas += log.gasCost
+ }
+ }
+ return gas
+}
+
+describe('Hard forks', () => {
+ let env: OptimismEnv
+ let SimpleStorage: Contract
+ let SelfDestruction: Contract
+ let Precompiles: Contract
+
+ before(async () => {
+ env = await OptimismEnv.new()
+ const Factory__SimpleStorage = await ethers.getContractFactory(
+ 'SimpleStorage',
+ env.l2Wallet
+ )
+ SimpleStorage = await Factory__SimpleStorage.deploy()
+
+ const Factory__SelfDestruction = await ethers.getContractFactory(
+ 'SelfDestruction',
+ env.l2Wallet
+ )
+ SelfDestruction = await Factory__SelfDestruction.deploy()
+
+ const Factory__Precompiles = await ethers.getContractFactory(
+ 'Precompiles',
+ env.l2Wallet
+ )
+ Precompiles = await Factory__Precompiles.deploy()
+ })
+
+ describe('Berlin', () => {
+ // https://eips.ethereum.org/EIPS/eip-2929
+ describe('EIP-2929', () => {
+ it('should update the gas schedule', async () => {
+ // Get the tip height
+ const tip = await env.l2Provider.getBlock('latest')
+
+ // send a transaction to be able to trace
+ const tx = await SimpleStorage.setValueNotXDomain(
+ `0x${'77'.repeat(32)}`
+ )
+ await tx.wait()
+
+ // Collect the traces
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+ expect(berlinTrace.gas).to.not.eq(preBerlinTrace.gas)
+
+ const berlinSstoreCosts = traceToGasByOpcode(
+ berlinTrace.structLogs,
+ 'SSTORE'
+ )
+ const preBerlinSstoreCosts = traceToGasByOpcode(
+ preBerlinTrace.structLogs,
+ 'SSTORE'
+ )
+ expect(berlinSstoreCosts).to.not.eq(preBerlinSstoreCosts)
+ })
+ })
+
+ // https://eips.ethereum.org/EIPS/eip-2565
+ describe('EIP-2565', async () => {
+ it('should become cheaper', async () => {
+ const tip = await env.l2Provider.getBlock('latest')
+
+ const tx = await Precompiles.expmod(64, 1, 64, { gasLimit: 5_000_000 })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+ expect(berlinTrace.gas).to.be.lt(preBerlinTrace.gas)
+ })
+ })
+ })
+
+ // Optimism includes EIP-3529 as part of its Berlin hardfork. It is part
+ // of the London hardfork on L1. Since it is coupled to the Berlin
+ // hardfork, some of its functionality cannot be directly tests via
+ // integration tests since we can currently only turn on all of the Berlin
+ // EIPs or none of the Berlin EIPs
+ describe('Berlin Additional (L1 London)', () => {
+ // https://eips.ethereum.org/EIPS/eip-3529
+ describe('EIP-3529', async () => {
+ const bytes32Zero = '0x' + '00'.repeat(32)
+ const bytes32NonZero = '0x' + 'ff'.repeat(32)
+
+ it('should lower the refund for storage clear', async () => {
+ const tip = await env.l2Provider.getBlock('latest')
+
+ const value = await SelfDestruction.callStatic.data()
+ // It should be non zero
+ expect(BigNumber.from(value).toNumber()).to.not.eq(0)
+
+ {
+ // Set the value to another non zero value
+ // Going from non zero to non zero
+ const tx = await SelfDestruction.setData(bytes32NonZero, {
+ gasLimit: 5_000_000,
+ })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+ // Updating a non zero value to another non zero value should not change
+ expect(berlinTrace.gas).to.deep.eq(preBerlinTrace.gas)
+ }
+
+ {
+ // Set the value to the zero value
+ // Going from non zero to zero
+ const tx = await SelfDestruction.setData(bytes32Zero, {
+ gasLimit: 5_000_000,
+ })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+
+ // Updating to a zero value from a non zero value should becomes
+ // more expensive due to this change being coupled with EIP-2929
+ expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
+ }
+
+ {
+ // Set the value to a non zero value
+ // Going from zero to non zero
+ const tx = await SelfDestruction.setData(bytes32NonZero, {
+ gasLimit: 5_000_000,
+ })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+
+ // Updating to a zero value from a non zero value should becomes
+ // more expensive due to this change being coupled with EIP-2929
+ expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
+ }
+ })
+
+ it('should remove the refund for selfdestruct', async () => {
+ const tip = await env.l2Provider.getBlock('latest')
+
+ // Send transaction with a large gas limit
+ const tx = await SelfDestruction.destruct({ gasLimit: 5_000_000 })
+ await tx.wait()
+
+ const berlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash]
+ )
+ const preBerlinTrace = await env.l2Provider.send(
+ 'debug_traceTransaction',
+ [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
+ )
+
+ // The berlin execution should use more gas than the pre Berlin
+ // execution because there is no longer a selfdestruct gas
+ // refund
+ expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
+ })
+ })
+ })
+})
diff --git a/integration-tests/test/rpc.spec.ts b/integration-tests/test/rpc.spec.ts
index b31edf6fb3d6..56d46cc83162 100644
--- a/integration-tests/test/rpc.spec.ts
+++ b/integration-tests/test/rpc.spec.ts
@@ -28,6 +28,7 @@ describe('Basic RPC tests', () => {
const provider = injectL2Context(l2Provider)
let Reverter: Contract
+ let ValueContext: Contract
let revertMessage: string
let revertingTx: TransactionRequest
let revertingDeployTx: TransactionRequest
@@ -53,6 +54,12 @@ describe('Basic RPC tests', () => {
revertingDeployTx = {
data: Factory__ConstructorReverter.bytecode,
}
+
+ // Deploy a contract to check msg.value of the call
+ const Factory__ValueContext: ContractFactory =
+ await ethers.getContractFactory('ValueContext', wallet)
+ ValueContext = await Factory__ValueContext.deploy()
+ await ValueContext.deployTransaction.wait()
})
describe('eth_sendRawTransaction', () => {
@@ -209,12 +216,6 @@ describe('Basic RPC tests', () => {
})
it('should allow eth_calls with nonzero value', async () => {
- // Deploy a contract to check msg.value of the call
- const Factory__ValueContext: ContractFactory =
- await ethers.getContractFactory('ValueContext', wallet)
- const ValueContext: Contract = await Factory__ValueContext.deploy()
- await ValueContext.deployTransaction.wait()
-
// Fund account to call from
const from = wallet.address
const value = 15
@@ -234,12 +235,6 @@ describe('Basic RPC tests', () => {
// https://github.com/ethereum-optimism/optimism/issues/1998
it('should use address(0) as the default "from" value', async () => {
- // Deploy a contract to check msg.caller
- const Factory__ValueContext: ContractFactory =
- await ethers.getContractFactory('ValueContext', wallet)
- const ValueContext: Contract = await Factory__ValueContext.deploy()
- await ValueContext.deployTransaction.wait()
-
// Do the call and check msg.sender
const data = ValueContext.interface.encodeFunctionData('getCaller')
const res = await provider.call({
@@ -256,12 +251,6 @@ describe('Basic RPC tests', () => {
})
it('should correctly use the "from" value', async () => {
- // Deploy a contract to check msg.caller
- const Factory__ValueContext: ContractFactory =
- await ethers.getContractFactory('ValueContext', wallet)
- const ValueContext: Contract = await Factory__ValueContext.deploy()
- await ValueContext.deployTransaction.wait()
-
const from = wallet.address
// Do the call and check msg.sender
@@ -278,6 +267,15 @@ describe('Basic RPC tests', () => {
)
expect(paddedRes).to.eq(from)
})
+
+ it('should be deterministic', async () => {
+ let res = await ValueContext.callStatic.getSelfBalance()
+ for (let i = 0; i < 10; i++) {
+ const next = await ValueContext.callStatic.getSelfBalance()
+ expect(res.toNumber()).to.deep.eq(next.toNumber())
+ res = next
+ }
+ })
})
describe('eth_getTransactionReceipt', () => {
@@ -450,7 +448,7 @@ describe('Basic RPC tests', () => {
})
describe('eth_estimateGas', () => {
- it('gas estimation is deterministic', async () => {
+ it('simple send gas estimation is deterministic', async () => {
let lastEstimate: BigNumber
for (let i = 0; i < 10; i++) {
const estimate = await l2Provider.estimateGas({
@@ -466,6 +464,15 @@ describe('Basic RPC tests', () => {
}
})
+ it('deterministic gas estimation for evm execution', async () => {
+ let res = await ValueContext.estimateGas.getSelfBalance()
+ for (let i = 0; i < 10; i++) {
+ const next = await ValueContext.estimateGas.getSelfBalance()
+ expect(res.toNumber()).to.deep.eq(next.toNumber())
+ res = next
+ }
+ })
+
it('should return a gas estimate for txs with empty data', async () => {
const estimate = await l2Provider.estimateGas({
to: defaultTransactionFactory().to,
diff --git a/integration-tests/test/shared/utils.ts b/integration-tests/test/shared/utils.ts
index baaebf9492f8..0a10c4adba0d 100644
--- a/integration-tests/test/shared/utils.ts
+++ b/integration-tests/test/shared/utils.ts
@@ -93,6 +93,9 @@ const procEnv = cleanEnv(process.env, {
RUN_STRESS_TESTS: bool({
default: true,
}),
+ RUN_NIGHTLY_TESTS: bool({
+ default: false,
+ }),
MOCHA_TIMEOUT: num({
default: 120_000,
@@ -264,3 +267,12 @@ export const isHardhat = async () => {
const chainId = await l1Wallet.getChainId()
return chainId === HARDHAT_CHAIN_ID
}
+
+export const die = (...args) => {
+ console.log(...args)
+ process.exit(1)
+}
+
+export const logStderr = (msg: string) => {
+ process.stderr.write(`${msg}\n`)
+}
diff --git a/l2geth/accounts/abi/bind/backends/simulated.go b/l2geth/accounts/abi/bind/backends/simulated.go
index c79a292d4e13..2d81e36fa642 100644
--- a/l2geth/accounts/abi/bind/backends/simulated.go
+++ b/l2geth/accounts/abi/bind/backends/simulated.go
@@ -592,14 +592,15 @@ type callmsg struct {
ethereum.CallMsg
}
-func (m callmsg) From() common.Address { return m.CallMsg.From }
-func (m callmsg) Nonce() uint64 { return 0 }
-func (m callmsg) CheckNonce() bool { return false }
-func (m callmsg) To() *common.Address { return m.CallMsg.To }
-func (m callmsg) GasPrice() *big.Int { return m.CallMsg.GasPrice }
-func (m callmsg) Gas() uint64 { return m.CallMsg.Gas }
-func (m callmsg) Value() *big.Int { return m.CallMsg.Value }
-func (m callmsg) Data() []byte { return m.CallMsg.Data }
+func (m callmsg) From() common.Address { return m.CallMsg.From }
+func (m callmsg) Nonce() uint64 { return 0 }
+func (m callmsg) CheckNonce() bool { return false }
+func (m callmsg) To() *common.Address { return m.CallMsg.To }
+func (m callmsg) GasPrice() *big.Int { return m.CallMsg.GasPrice }
+func (m callmsg) Gas() uint64 { return m.CallMsg.Gas }
+func (m callmsg) Value() *big.Int { return m.CallMsg.Value }
+func (m callmsg) Data() []byte { return m.CallMsg.Data }
+func (m callmsg) AccessList() types.AccessList { return m.CallMsg.AccessList }
// UsingOVM
// These getters return OVM specific fields
diff --git a/l2geth/core/evm.go b/l2geth/core/evm.go
index 65c021c728f0..1121809607dd 100644
--- a/l2geth/core/evm.go
+++ b/l2geth/core/evm.go
@@ -48,7 +48,6 @@ func NewEVMContext(msg Message, header *types.Header, chain ChainContext, author
}
if rcfg.UsingOVM {
// When using the OVM, we must:
- // - Set the BlockNumber to be the msg.L1BlockNumber
// - Set the Time to be the msg.L1Timestamp
return vm.Context{
CanTransfer: CanTransfer,
diff --git a/l2geth/core/state/access_list.go b/l2geth/core/state/access_list.go
new file mode 100644
index 000000000000..78f0799fa2ff
--- /dev/null
+++ b/l2geth/core/state/access_list.go
@@ -0,0 +1,136 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "github.com/ethereum-optimism/optimism/l2geth/common"
+)
+
+type accessList struct {
+ addresses map[common.Address]int
+ slots []map[common.Hash]struct{}
+}
+
+// ContainsAddress returns true if the address is in the access list.
+func (al *accessList) ContainsAddress(address common.Address) bool {
+ _, ok := al.addresses[address]
+ return ok
+}
+
+// Contains checks if a slot within an account is present in the access list, returning
+// separate flags for the presence of the account and the slot respectively.
+func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) {
+ idx, ok := al.addresses[address]
+ if !ok {
+ // no such address (and hence zero slots)
+ return false, false
+ }
+ if idx == -1 {
+ // address yes, but no slots
+ return true, false
+ }
+ _, slotPresent = al.slots[idx][slot]
+ return true, slotPresent
+}
+
+// newAccessList creates a new accessList.
+func newAccessList() *accessList {
+ return &accessList{
+ addresses: make(map[common.Address]int),
+ }
+}
+
+// Copy creates an independent copy of an accessList.
+func (a *accessList) Copy() *accessList {
+ cp := newAccessList()
+ for k, v := range a.addresses {
+ cp.addresses[k] = v
+ }
+ cp.slots = make([]map[common.Hash]struct{}, len(a.slots))
+ for i, slotMap := range a.slots {
+ newSlotmap := make(map[common.Hash]struct{}, len(slotMap))
+ for k := range slotMap {
+ newSlotmap[k] = struct{}{}
+ }
+ cp.slots[i] = newSlotmap
+ }
+ return cp
+}
+
+// AddAddress adds an address to the access list, and returns 'true' if the operation
+// caused a change (addr was not previously in the list).
+func (al *accessList) AddAddress(address common.Address) bool {
+ if _, present := al.addresses[address]; present {
+ return false
+ }
+ al.addresses[address] = -1
+ return true
+}
+
+// AddSlot adds the specified (addr, slot) combo to the access list.
+// Return values are:
+// - address added
+// - slot added
+// For any 'true' value returned, a corresponding journal entry must be made.
+func (al *accessList) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) {
+ idx, addrPresent := al.addresses[address]
+ if !addrPresent || idx == -1 {
+ // Address not present, or addr present but no slots there
+ al.addresses[address] = len(al.slots)
+ slotmap := map[common.Hash]struct{}{slot: {}}
+ al.slots = append(al.slots, slotmap)
+ return !addrPresent, true
+ }
+ // There is already an (address,slot) mapping
+ slotmap := al.slots[idx]
+ if _, ok := slotmap[slot]; !ok {
+ slotmap[slot] = struct{}{}
+ // Journal add slot change
+ return false, true
+ }
+ // No changes required
+ return false, false
+}
+
+// DeleteSlot removes an (address, slot)-tuple from the access list.
+// This operation needs to be performed in the same order as the addition happened.
+// This method is meant to be used by the journal, which maintains ordering of
+// operations.
+func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) {
+ idx, addrOk := al.addresses[address]
+ // There are two ways this can fail
+ if !addrOk {
+ panic("reverting slot change, address not present in list")
+ }
+ slotmap := al.slots[idx]
+ delete(slotmap, slot)
+ // If that was the last (first) slot, remove it
+ // Since additions and rollbacks are always performed in order,
+ // we can delete the item without worrying about screwing up later indices
+ if len(slotmap) == 0 {
+ al.slots = al.slots[:idx]
+ al.addresses[address] = -1
+ }
+}
+
+// DeleteAddress removes an address from the access list. This operation
+// needs to be performed in the same order as the addition happened.
+// This method is meant to be used by the journal, which maintains ordering of
+// operations.
+func (al *accessList) DeleteAddress(address common.Address) {
+ delete(al.addresses, address)
+}
diff --git a/l2geth/core/state/journal.go b/l2geth/core/state/journal.go
index b542bfdbb21a..7319881d39cc 100644
--- a/l2geth/core/state/journal.go
+++ b/l2geth/core/state/journal.go
@@ -129,6 +129,15 @@ type (
touchChange struct {
account *common.Address
}
+
+ // Changes to the access list
+ accessListAddAccountChange struct {
+ address *common.Address
+ }
+ accessListAddSlotChange struct {
+ address *common.Address
+ slot *common.Hash
+ }
)
func (ch createObjectChange) revert(s *StateDB) {
@@ -230,3 +239,28 @@ func (ch addPreimageChange) revert(s *StateDB) {
func (ch addPreimageChange) dirtied() *common.Address {
return nil
}
+
+func (ch accessListAddAccountChange) revert(s *StateDB) {
+ /*
+ One important invariant here, is that whenever a (addr, slot) is added, if the
+ addr is not already present, the add causes two journal entries:
+ - one for the address,
+ - one for the (address,slot)
+ Therefore, when unrolling the change, we can always blindly delete the
+ (addr) at this point, since no storage adds can remain when come upon
+ a single (addr) change.
+ */
+ s.accessList.DeleteAddress(*ch.address)
+}
+
+func (ch accessListAddAccountChange) dirtied() *common.Address {
+ return nil
+}
+
+func (ch accessListAddSlotChange) revert(s *StateDB) {
+ s.accessList.DeleteSlot(*ch.address, *ch.slot)
+}
+
+func (ch accessListAddSlotChange) dirtied() *common.Address {
+ return nil
+}
diff --git a/l2geth/core/state/statedb.go b/l2geth/core/state/statedb.go
index 0e50ca8392ff..a15fee1141ae 100644
--- a/l2geth/core/state/statedb.go
+++ b/l2geth/core/state/statedb.go
@@ -100,6 +100,9 @@ type StateDB struct {
preimages map[common.Hash][]byte
+ // Per-transaction access list
+ accessList *accessList
+
// Journal of state modifications. This is the backbone of
// Snapshot and RevertToSnapshot.
journal *journal
@@ -132,6 +135,7 @@ func New(root common.Hash, db Database) (*StateDB, error) {
logs: make(map[common.Hash][]*types.Log),
preimages: make(map[common.Hash][]byte),
journal: newJournal(),
+ accessList: newAccessList(),
}, nil
}
@@ -163,6 +167,7 @@ func (s *StateDB) Reset(root common.Hash) error {
s.logs = make(map[common.Hash][]*types.Log)
s.logSize = 0
s.preimages = make(map[common.Hash][]byte)
+ s.accessList = newAccessList()
s.clearJournalAndRefund()
return nil
}
@@ -673,6 +678,13 @@ func (s *StateDB) Copy() *StateDB {
for hash, preimage := range s.preimages {
state.preimages[hash] = preimage
}
+ // Do we need to copy the access list? In practice: No. At the start of a
+ // transaction, the access list is empty. In practice, we only ever copy state
+ // _between_ transactions/blocks, never in the middle of a transaction.
+ // However, it doesn't cost us much to copy an empty list, so we do it anyway
+ // to not blow up if we ever decide copy it in the middle of a transaction
+ state.accessList = s.accessList.Copy()
+
return state
}
@@ -764,6 +776,7 @@ func (s *StateDB) Prepare(thash, bhash common.Hash, ti int) {
s.thash = thash
s.bhash = bhash
s.txIndex = ti
+ s.accessList = newAccessList()
}
func (s *StateDB) clearJournalAndRefund() {
@@ -815,3 +828,63 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
return nil
})
}
+
+// PrepareAccessList handles the preparatory steps for executing a state transition with
+// regards to both EIP-2929 and EIP-2930:
+//
+// - Add sender to access list (2929)
+// - Add destination to access list (2929)
+// - Add precompiles to access list (2929)
+// - Add the contents of the optional tx access list (2930)
+//
+// This method should only be called if Berlin/2929+2930 is applicable at the current number.
+func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, precompiles []common.Address, list types.AccessList) {
+ s.AddAddressToAccessList(sender)
+ if dst != nil {
+ s.AddAddressToAccessList(*dst)
+ }
+ for _, addr := range precompiles {
+ s.AddAddressToAccessList(addr)
+ }
+ for _, el := range list {
+ s.AddAddressToAccessList(el.Address)
+ for _, key := range el.StorageKeys {
+ s.AddSlotToAccessList(el.Address, key)
+ }
+ }
+}
+
+// AddAddressToAccessList adds the given address to the access list
+func (s *StateDB) AddAddressToAccessList(addr common.Address) {
+ if s.accessList.AddAddress(addr) {
+ s.journal.append(accessListAddAccountChange{&addr})
+ }
+}
+
+// AddSlotToAccessList adds the given (address, slot)-tuple to the access list
+func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) {
+ addrMod, slotMod := s.accessList.AddSlot(addr, slot)
+ if addrMod {
+ // In practice, this should not happen, since there is no way to enter the
+ // scope of 'address' without having the 'address' become already added
+ // to the access list (via call-variant, create, etc).
+ // Better safe than sorry, though
+ s.journal.append(accessListAddAccountChange{&addr})
+ }
+ if slotMod {
+ s.journal.append(accessListAddSlotChange{
+ address: &addr,
+ slot: &slot,
+ })
+ }
+}
+
+// AddressInAccessList returns true if the given address is in the access list.
+func (s *StateDB) AddressInAccessList(addr common.Address) bool {
+ return s.accessList.ContainsAddress(addr)
+}
+
+// SlotInAccessList returns true if the given (address, slot)-tuple is in the access list.
+func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) {
+ return s.accessList.Contains(addr, slot)
+}
diff --git a/l2geth/core/state/statedb_test.go b/l2geth/core/state/statedb_test.go
index 70542c634b8a..638b304b5247 100644
--- a/l2geth/core/state/statedb_test.go
+++ b/l2geth/core/state/statedb_test.go
@@ -680,3 +680,177 @@ func TestDeleteCreateRevert(t *testing.T) {
t.Fatalf("self-destructed contract came alive")
}
}
+
+func TestStateDBAccessList(t *testing.T) {
+ // Some helpers
+ addr := func(a string) common.Address {
+ return common.HexToAddress(a)
+ }
+ slot := func(a string) common.Hash {
+ return common.HexToHash(a)
+ }
+
+ memDb := rawdb.NewMemoryDatabase()
+ db := NewDatabase(memDb)
+ state, _ := New(common.Hash{}, db)
+ state.accessList = newAccessList()
+
+ verifyAddrs := func(astrings ...string) {
+ t.Helper()
+ // convert to common.Address form
+ var addresses []common.Address
+ var addressMap = make(map[common.Address]struct{})
+ for _, astring := range astrings {
+ address := addr(astring)
+ addresses = append(addresses, address)
+ addressMap[address] = struct{}{}
+ }
+ // Check that the given addresses are in the access list
+ for _, address := range addresses {
+ if !state.AddressInAccessList(address) {
+ t.Fatalf("expected %x to be in access list", address)
+ }
+ }
+ // Check that only the expected addresses are present in the acesslist
+ for address := range state.accessList.addresses {
+ if _, exist := addressMap[address]; !exist {
+ t.Fatalf("extra address %x in access list", address)
+ }
+ }
+ }
+ verifySlots := func(addrString string, slotStrings ...string) {
+ if !state.AddressInAccessList(addr(addrString)) {
+ t.Fatalf("scope missing address/slots %v", addrString)
+ }
+ var address = addr(addrString)
+ // convert to common.Hash form
+ var slots []common.Hash
+ var slotMap = make(map[common.Hash]struct{})
+ for _, slotString := range slotStrings {
+ s := slot(slotString)
+ slots = append(slots, s)
+ slotMap[s] = struct{}{}
+ }
+ // Check that the expected items are in the access list
+ for i, s := range slots {
+ if _, slotPresent := state.SlotInAccessList(address, s); !slotPresent {
+ t.Fatalf("input %d: scope missing slot %v (address %v)", i, s, addrString)
+ }
+ }
+ // Check that no extra elements are in the access list
+ index := state.accessList.addresses[address]
+ if index >= 0 {
+ stateSlots := state.accessList.slots[index]
+ for s := range stateSlots {
+ if _, slotPresent := slotMap[s]; !slotPresent {
+ t.Fatalf("scope has extra slot %v (address %v)", s, addrString)
+ }
+ }
+ }
+ }
+
+ state.AddAddressToAccessList(addr("aa")) // 1
+ state.AddSlotToAccessList(addr("bb"), slot("01")) // 2,3
+ state.AddSlotToAccessList(addr("bb"), slot("02")) // 4
+ verifyAddrs("aa", "bb")
+ verifySlots("bb", "01", "02")
+
+ // Make a copy
+ stateCopy1 := state.Copy()
+ if exp, got := 4, state.journal.length(); exp != got {
+ t.Fatalf("journal length mismatch: have %d, want %d", got, exp)
+ }
+
+ // same again, should cause no journal entries
+ state.AddSlotToAccessList(addr("bb"), slot("01"))
+ state.AddSlotToAccessList(addr("bb"), slot("02"))
+ state.AddAddressToAccessList(addr("aa"))
+ if exp, got := 4, state.journal.length(); exp != got {
+ t.Fatalf("journal length mismatch: have %d, want %d", got, exp)
+ }
+ // some new ones
+ state.AddSlotToAccessList(addr("bb"), slot("03")) // 5
+ state.AddSlotToAccessList(addr("aa"), slot("01")) // 6
+ state.AddSlotToAccessList(addr("cc"), slot("01")) // 7,8
+ state.AddAddressToAccessList(addr("cc"))
+ if exp, got := 8, state.journal.length(); exp != got {
+ t.Fatalf("journal length mismatch: have %d, want %d", got, exp)
+ }
+
+ verifyAddrs("aa", "bb", "cc")
+ verifySlots("aa", "01")
+ verifySlots("bb", "01", "02", "03")
+ verifySlots("cc", "01")
+
+ // now start rolling back changes
+ state.journal.revert(state, 7)
+ if _, ok := state.SlotInAccessList(addr("cc"), slot("01")); ok {
+ t.Fatalf("slot present, expected missing")
+ }
+ verifyAddrs("aa", "bb", "cc")
+ verifySlots("aa", "01")
+ verifySlots("bb", "01", "02", "03")
+
+ state.journal.revert(state, 6)
+ if state.AddressInAccessList(addr("cc")) {
+ t.Fatalf("addr present, expected missing")
+ }
+ verifyAddrs("aa", "bb")
+ verifySlots("aa", "01")
+ verifySlots("bb", "01", "02", "03")
+
+ state.journal.revert(state, 5)
+ if _, ok := state.SlotInAccessList(addr("aa"), slot("01")); ok {
+ t.Fatalf("slot present, expected missing")
+ }
+ verifyAddrs("aa", "bb")
+ verifySlots("bb", "01", "02", "03")
+
+ state.journal.revert(state, 4)
+ if _, ok := state.SlotInAccessList(addr("bb"), slot("03")); ok {
+ t.Fatalf("slot present, expected missing")
+ }
+ verifyAddrs("aa", "bb")
+ verifySlots("bb", "01", "02")
+
+ state.journal.revert(state, 3)
+ if _, ok := state.SlotInAccessList(addr("bb"), slot("02")); ok {
+ t.Fatalf("slot present, expected missing")
+ }
+ verifyAddrs("aa", "bb")
+ verifySlots("bb", "01")
+
+ state.journal.revert(state, 2)
+ if _, ok := state.SlotInAccessList(addr("bb"), slot("01")); ok {
+ t.Fatalf("slot present, expected missing")
+ }
+ verifyAddrs("aa", "bb")
+
+ state.journal.revert(state, 1)
+ if state.AddressInAccessList(addr("bb")) {
+ t.Fatalf("addr present, expected missing")
+ }
+ verifyAddrs("aa")
+
+ state.journal.revert(state, 0)
+ if state.AddressInAccessList(addr("aa")) {
+ t.Fatalf("addr present, expected missing")
+ }
+ if got, exp := len(state.accessList.addresses), 0; got != exp {
+ t.Fatalf("expected empty, got %d", got)
+ }
+ if got, exp := len(state.accessList.slots), 0; got != exp {
+ t.Fatalf("expected empty, got %d", got)
+ }
+ // Check the copy
+ // Make a copy
+ state = stateCopy1
+ verifyAddrs("aa", "bb")
+ verifySlots("bb", "01", "02")
+ if got, exp := len(state.accessList.addresses), 2; got != exp {
+ t.Fatalf("expected empty, got %d", got)
+ }
+ if got, exp := len(state.accessList.slots), 1; got != exp {
+ t.Fatalf("expected empty, got %d", got)
+ }
+}
diff --git a/l2geth/core/state_transition.go b/l2geth/core/state_transition.go
index 2ed1a450b4a6..c6c7ae0c9779 100644
--- a/l2geth/core/state_transition.go
+++ b/l2geth/core/state_transition.go
@@ -79,6 +79,7 @@ type Message interface {
Nonce() uint64
CheckNonce() bool
Data() []byte
+ AccessList() types.AccessList
L1Timestamp() uint64
L1BlockNumber() *big.Int
@@ -253,6 +254,11 @@ func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bo
vmerr error
)
+ // The access list gets created here
+ if rules := st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber); rules.IsBerlin {
+ st.state.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList())
+ }
+
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
diff --git a/l2geth/core/types/access_list_tx.go b/l2geth/core/types/access_list_tx.go
new file mode 100644
index 000000000000..aa29748fab2a
--- /dev/null
+++ b/l2geth/core/types/access_list_tx.go
@@ -0,0 +1,41 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package types
+
+import (
+ "github.com/ethereum-optimism/optimism/l2geth/common"
+)
+
+//go:generate gencodec -type AccessTuple -out gen_access_tuple.go
+
+// AccessList is an EIP-2930 access list.
+type AccessList []AccessTuple
+
+// AccessTuple is the element type of an access list.
+type AccessTuple struct {
+ Address common.Address `json:"address" gencodec:"required"`
+ StorageKeys []common.Hash `json:"storageKeys" gencodec:"required"`
+}
+
+// StorageKeys returns the total number of storage keys in the access list.
+func (al AccessList) StorageKeys() int {
+ sum := 0
+ for _, tuple := range al {
+ sum += len(tuple.StorageKeys)
+ }
+ return sum
+}
diff --git a/l2geth/core/types/transaction.go b/l2geth/core/types/transaction.go
index 6d23a5c3409e..53a03836dd4d 100644
--- a/l2geth/core/types/transaction.go
+++ b/l2geth/core/types/transaction.go
@@ -479,6 +479,7 @@ type Message struct {
gasPrice *big.Int
data []byte
checkNonce bool
+ accessList AccessList
l1Timestamp uint64
l1BlockNumber *big.Int
@@ -495,6 +496,7 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b
gasPrice: gasPrice,
data: data,
checkNonce: checkNonce,
+ accessList: AccessList{},
l1Timestamp: l1Timestamp,
l1BlockNumber: l1BlockNumber,
@@ -502,14 +504,15 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b
}
}
-func (m Message) From() common.Address { return m.from }
-func (m Message) To() *common.Address { return m.to }
-func (m Message) GasPrice() *big.Int { return m.gasPrice }
-func (m Message) Value() *big.Int { return m.amount }
-func (m Message) Gas() uint64 { return m.gasLimit }
-func (m Message) Nonce() uint64 { return m.nonce }
-func (m Message) Data() []byte { return m.data }
-func (m Message) CheckNonce() bool { return m.checkNonce }
+func (m Message) From() common.Address { return m.from }
+func (m Message) To() *common.Address { return m.to }
+func (m Message) GasPrice() *big.Int { return m.gasPrice }
+func (m Message) Value() *big.Int { return m.amount }
+func (m Message) Gas() uint64 { return m.gasLimit }
+func (m Message) Nonce() uint64 { return m.nonce }
+func (m Message) Data() []byte { return m.data }
+func (m Message) CheckNonce() bool { return m.checkNonce }
+func (m Message) AccessList() AccessList { return m.accessList }
func (m Message) L1Timestamp() uint64 { return m.l1Timestamp }
func (m Message) L1BlockNumber() *big.Int { return m.l1BlockNumber }
diff --git a/l2geth/core/vm/contracts.go b/l2geth/core/vm/contracts.go
index c9737a701636..398ce36367de 100644
--- a/l2geth/core/vm/contracts.go
+++ b/l2geth/core/vm/contracts.go
@@ -77,6 +77,55 @@ var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{9}): &blake2F{},
}
+// PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum
+// contracts used in the Berlin release.
+var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{
+ common.BytesToAddress([]byte{1}): &ecrecover{},
+ common.BytesToAddress([]byte{2}): &sha256hash{},
+ common.BytesToAddress([]byte{3}): &ripemd160hash{},
+ common.BytesToAddress([]byte{4}): &dataCopy{},
+ common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true},
+ common.BytesToAddress([]byte{6}): &bn256AddIstanbul{},
+ common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{},
+ common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{},
+ common.BytesToAddress([]byte{9}): &blake2F{},
+}
+
+var (
+ PrecompiledAddressesBerlin []common.Address
+ PrecompiledAddressesIstanbul []common.Address
+ PrecompiledAddressesByzantium []common.Address
+ PrecompiledAddressesHomestead []common.Address
+)
+
+func init() {
+ for k := range PrecompiledContractsHomestead {
+ PrecompiledAddressesHomestead = append(PrecompiledAddressesHomestead, k)
+ }
+ for k := range PrecompiledContractsByzantium {
+ PrecompiledAddressesByzantium = append(PrecompiledAddressesByzantium, k)
+ }
+ for k := range PrecompiledContractsIstanbul {
+ PrecompiledAddressesIstanbul = append(PrecompiledAddressesIstanbul, k)
+ }
+ for k := range PrecompiledContractsBerlin {
+ PrecompiledAddressesBerlin = append(PrecompiledAddressesBerlin, k)
+ }
+}
+
+func ActivePrecompiles(rules params.Rules) []common.Address {
+ switch {
+ case rules.IsBerlin:
+ return PrecompiledAddressesBerlin
+ case rules.IsIstanbul:
+ return PrecompiledAddressesIstanbul
+ case rules.IsByzantium:
+ return PrecompiledAddressesByzantium
+ default:
+ return PrecompiledAddressesHomestead
+ }
+}
+
// RunPrecompiledContract runs and evaluates the output of a precompiled contract.
func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {
gas := p.RequiredGas(input)
@@ -170,13 +219,18 @@ func (c *dataCopy) Run(in []byte) ([]byte, error) {
}
// bigModExp implements a native big integer exponential modular operation.
-type bigModExp struct{}
+type bigModExp struct {
+ eip2565 bool
+}
var (
big1 = big.NewInt(1)
+ big3 = big.NewInt(3)
big4 = big.NewInt(4)
+ big7 = big.NewInt(7)
big8 = big.NewInt(8)
big16 = big.NewInt(16)
+ big20 = big.NewInt(20)
big32 = big.NewInt(32)
big64 = big.NewInt(64)
big96 = big.NewInt(96)
@@ -186,6 +240,34 @@ var (
big199680 = big.NewInt(199680)
)
+// modexpMultComplexity implements bigModexp multComplexity formula, as defined in EIP-198
+//
+// def mult_complexity(x):
+// if x <= 64: return x ** 2
+// elif x <= 1024: return x ** 2 // 4 + 96 * x - 3072
+// else: return x ** 2 // 16 + 480 * x - 199680
+//
+// where is x is max(length_of_MODULUS, length_of_BASE)
+func modexpMultComplexity(x *big.Int) *big.Int {
+ switch {
+ case x.Cmp(big64) <= 0:
+ x.Mul(x, x) // x ** 2
+ case x.Cmp(big1024) <= 0:
+ // (x ** 2 // 4 ) + ( 96 * x - 3072)
+ x = new(big.Int).Add(
+ new(big.Int).Div(new(big.Int).Mul(x, x), big4),
+ new(big.Int).Sub(new(big.Int).Mul(big96, x), big3072),
+ )
+ default:
+ // (x ** 2 // 16) + (480 * x - 199680)
+ x = new(big.Int).Add(
+ new(big.Int).Div(new(big.Int).Mul(x, x), big16),
+ new(big.Int).Sub(new(big.Int).Mul(big480, x), big199680),
+ )
+ }
+ return x
+}
+
// RequiredGas returns the gas required to execute the pre-compiled contract.
func (c *bigModExp) RequiredGas(input []byte) uint64 {
var (
@@ -220,25 +302,36 @@ func (c *bigModExp) RequiredGas(input []byte) uint64 {
adjExpLen.Mul(big8, adjExpLen)
}
adjExpLen.Add(adjExpLen, big.NewInt(int64(msb)))
-
// Calculate the gas cost of the operation
gas := new(big.Int).Set(math.BigMax(modLen, baseLen))
- switch {
- case gas.Cmp(big64) <= 0:
+ if c.eip2565 {
+ // EIP-2565 has three changes
+ // 1. Different multComplexity (inlined here)
+ // in EIP-2565 (https://eips.ethereum.org/EIPS/eip-2565):
+ //
+ // def mult_complexity(x):
+ // ceiling(x/8)^2
+ //
+ //where is x is max(length_of_MODULUS, length_of_BASE)
+ gas = gas.Add(gas, big7)
+ gas = gas.Div(gas, big8)
gas.Mul(gas, gas)
- case gas.Cmp(big1024) <= 0:
- gas = new(big.Int).Add(
- new(big.Int).Div(new(big.Int).Mul(gas, gas), big4),
- new(big.Int).Sub(new(big.Int).Mul(big96, gas), big3072),
- )
- default:
- gas = new(big.Int).Add(
- new(big.Int).Div(new(big.Int).Mul(gas, gas), big16),
- new(big.Int).Sub(new(big.Int).Mul(big480, gas), big199680),
- )
+
+ gas.Mul(gas, math.BigMax(adjExpLen, big1))
+ // 2. Different divisor (`GQUADDIVISOR`) (3)
+ gas.Div(gas, big3)
+ if gas.BitLen() > 64 {
+ return math.MaxUint64
+ }
+ // 3. Minimum price of 200 gas
+ if gas.Uint64() < 200 {
+ return 200
+ }
+ return gas.Uint64()
}
+ gas = modexpMultComplexity(gas)
gas.Mul(gas, math.BigMax(adjExpLen, big1))
- gas.Div(gas, new(big.Int).SetUint64(params.ModExpQuadCoeffDiv))
+ gas.Div(gas, big20)
if gas.BitLen() > 64 {
return math.MaxUint64
diff --git a/l2geth/core/vm/eips.go b/l2geth/core/vm/eips.go
index 2a653b314c68..84ff5965f920 100644
--- a/l2geth/core/vm/eips.go
+++ b/l2geth/core/vm/eips.go
@@ -91,7 +91,11 @@ func enable2200(jt *JumpTable) {
jt[SSTORE].dynamicGas = gasSStoreEIP2200
}
-func enableMinimal2929(jt *JumpTable) {
+// enable2929 enables "EIP-2929: Gas cost increases for state access opcodes"
+// https://eips.ethereum.org/EIPS/eip-2929
+func enable2929(jt *JumpTable) {
+ jt[SSTORE].dynamicGas = gasSStoreEIP2929
+
jt[SLOAD].constantGas = 0
jt[SLOAD].dynamicGas = gasSLoadEIP2929
@@ -124,3 +128,48 @@ func enableMinimal2929(jt *JumpTable) {
jt[SELFDESTRUCT].constantGas = params.SelfdestructGasEIP150
jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP2929
}
+
+// enable3529 enabled "EIP-3529: Reduction in refunds":
+// - Removes refunds for selfdestructs
+// - Reduces refunds for SSTORE
+// - Reduces max refunds to 20% gas
+func enable3529(jt *JumpTable) {
+ jt[SSTORE].dynamicGas = gasSStoreEIP3529
+ jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP3529
+}
+
+// UsingOVM
+// Optimism specific changes
+func enableMinimal2929(jt *JumpTable) {
+ jt[SLOAD].constantGas = 0
+ jt[SLOAD].dynamicGas = gasSLoadEIP2929Optimism
+
+ jt[EXTCODECOPY].constantGas = params.WarmStorageReadCostEIP2929
+ jt[EXTCODECOPY].dynamicGas = gasExtCodeCopyEIP2929Optimism
+
+ jt[EXTCODESIZE].constantGas = params.WarmStorageReadCostEIP2929
+ jt[EXTCODESIZE].dynamicGas = gasEip2929AccountCheckOptimism
+
+ jt[EXTCODEHASH].constantGas = params.WarmStorageReadCostEIP2929
+ jt[EXTCODEHASH].dynamicGas = gasEip2929AccountCheckOptimism
+
+ jt[BALANCE].constantGas = params.WarmStorageReadCostEIP2929
+ jt[BALANCE].dynamicGas = gasEip2929AccountCheckOptimism
+
+ jt[CALL].constantGas = params.WarmStorageReadCostEIP2929
+ jt[CALL].dynamicGas = gasCallEIP2929Optimism
+
+ jt[CALLCODE].constantGas = params.WarmStorageReadCostEIP2929
+ jt[CALLCODE].dynamicGas = gasCallCodeEIP2929Optimism
+
+ jt[STATICCALL].constantGas = params.WarmStorageReadCostEIP2929
+ jt[STATICCALL].dynamicGas = gasStaticCallEIP2929Optimism
+
+ jt[DELEGATECALL].constantGas = params.WarmStorageReadCostEIP2929
+ jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP2929Optimism
+
+ // This was previously part of the dynamic cost, but we're using it as a constantGas
+ // factor here
+ jt[SELFDESTRUCT].constantGas = params.SelfdestructGasEIP150
+ jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP2929Optimism
+}
diff --git a/l2geth/core/vm/errors.go b/l2geth/core/vm/errors.go
index 7f88f324ea13..2c7dc27ae5db 100644
--- a/l2geth/core/vm/errors.go
+++ b/l2geth/core/vm/errors.go
@@ -27,4 +27,5 @@ var (
ErrInsufficientBalance = errors.New("insufficient balance for transfer")
ErrContractAddressCollision = errors.New("contract address collision")
ErrNoCompatibleInterpreter = errors.New("no compatible interpreter")
+ ErrGasUintOverflow = errors.New("gas uint64 overflow")
)
diff --git a/l2geth/core/vm/evm.go b/l2geth/core/vm/evm.go
index 03a2f768670a..177e7c4f4c5f 100644
--- a/l2geth/core/vm/evm.go
+++ b/l2geth/core/vm/evm.go
@@ -55,6 +55,9 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
if evm.chainRules.IsIstanbul {
precompiles = PrecompiledContractsIstanbul
}
+ if evm.chainRules.IsBerlin {
+ precompiles = PrecompiledContractsBerlin
+ }
if p := precompiles[*contract.CodeAddr]; p != nil {
return RunPrecompiledContract(p, input, contract)
}
@@ -220,6 +223,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
if evm.chainRules.IsIstanbul {
precompiles = PrecompiledContractsIstanbul
}
+ if evm.chainRules.IsBerlin {
+ precompiles = PrecompiledContractsBerlin
+ }
if precompiles[addr] == nil && evm.chainRules.IsEIP158 && value.Sign() == 0 {
// Calling a non existing account, don't do anything, but ping the tracer
if evm.vmConfig.Debug && evm.depth == 0 {
@@ -413,7 +419,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
}
nonce := evm.StateDB.GetNonce(caller.Address())
evm.StateDB.SetNonce(caller.Address(), nonce+1)
-
+ // We add this to the access list _before_ taking a snapshot. Even if the creation fails,
+ // the access-list change should not be rolled back
+ if evm.chainRules.IsBerlin {
+ evm.StateDB.AddAddressToAccessList(address)
+ }
// Ensure there's no existing contract already at the designated address
contractHash := evm.StateDB.GetCodeHash(address)
if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
diff --git a/l2geth/core/vm/interface.go b/l2geth/core/vm/interface.go
index 16d8e8ee7435..83582cd02875 100644
--- a/l2geth/core/vm/interface.go
+++ b/l2geth/core/vm/interface.go
@@ -57,6 +57,16 @@ type StateDB interface {
// is defined according to EIP161 (balance = nonce = code = 0).
Empty(common.Address) bool
+ PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList)
+ AddressInAccessList(addr common.Address) bool
+ SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool)
+ // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform
+ // even if the feature/fork is not active yet
+ AddAddressToAccessList(addr common.Address)
+ // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform
+ // even if the feature/fork is not active yet
+ AddSlotToAccessList(addr common.Address, slot common.Hash)
+
RevertToSnapshot(int)
Snapshot() int
diff --git a/l2geth/core/vm/interpreter.go b/l2geth/core/vm/interpreter.go
index 596e49a5282a..fcfd032bf2f6 100644
--- a/l2geth/core/vm/interpreter.go
+++ b/l2geth/core/vm/interpreter.go
@@ -94,8 +94,13 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
if !cfg.JumpTable[STOP].valid {
var jt JumpTable
switch {
+ case evm.chainRules.IsBerlin:
+ jt = berlinInstructionSet
case evm.chainRules.IsIstanbul:
jt = istanbulInstructionSet
+ if rcfg.UsingOVM {
+ enableMinimal2929(&jt)
+ }
case evm.chainRules.IsConstantinople:
jt = constantinopleInstructionSet
case evm.chainRules.IsByzantium:
@@ -116,10 +121,6 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter {
log.Error("EIP activation failed", "eip", eip, "error", err)
}
}
- // Enable minimal eip 2929
- if rcfg.UsingOVM {
- enableMinimal2929(&jt)
- }
cfg.JumpTable = jt
}
diff --git a/l2geth/core/vm/jump_table.go b/l2geth/core/vm/jump_table.go
index 3d8cd6f859ea..a58c6327a3b8 100644
--- a/l2geth/core/vm/jump_table.go
+++ b/l2geth/core/vm/jump_table.go
@@ -61,11 +61,21 @@ var (
byzantiumInstructionSet = newByzantiumInstructionSet()
constantinopleInstructionSet = newConstantinopleInstructionSet()
istanbulInstructionSet = newIstanbulInstructionSet()
+ berlinInstructionSet = newBerlinInstructionSet()
)
// JumpTable contains the EVM opcodes supported at a given fork.
type JumpTable [256]operation
+// newBerlinInstructionSet returns the frontier, homestead, byzantium,
+// contantinople, istanbul, petersburg and berlin instructions.
+func newBerlinInstructionSet() JumpTable {
+ instructionSet := newIstanbulInstructionSet()
+ enable2929(&instructionSet) // Access lists for trie accesses https://eips.ethereum.org/EIPS/eip-2929
+ enable3529(&instructionSet) // EIP-3529: Reduction in refunds https://eips.ethereum.org/EIPS/eip-3529
+ return instructionSet
+}
+
// newIstanbulInstructionSet returns the frontier, homestead
// byzantium, contantinople and petersburg instructions.
func newIstanbulInstructionSet() JumpTable {
diff --git a/l2geth/core/vm/logger.go b/l2geth/core/vm/logger.go
index bfb0f0968f9e..9e23e04cfe58 100644
--- a/l2geth/core/vm/logger.go
+++ b/l2geth/core/vm/logger.go
@@ -21,12 +21,14 @@ import (
"fmt"
"io"
"math/big"
+ "strings"
"time"
"github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum-optimism/optimism/l2geth/common/hexutil"
"github.com/ethereum-optimism/optimism/l2geth/common/math"
"github.com/ethereum-optimism/optimism/l2geth/core/types"
+ "github.com/ethereum-optimism/optimism/l2geth/params"
)
// Storage represents a contract's storage.
@@ -49,6 +51,8 @@ type LogConfig struct {
DisableStorage bool // disable storage capture
Debug bool // print output during capture end
Limit int // maximum length of output, but zero means unlimited
+ // Chain overrides, can be used to execute a trace using future fork rules
+ Overrides *params.ChainConfig `json:"overrides,omitempty"`
}
//go:generate gencodec -type StructLog -field-override structLogMarshaling -out gen_structlog.go
@@ -254,3 +258,74 @@ func WriteLogs(writer io.Writer, logs []*types.Log) {
fmt.Fprintln(writer)
}
}
+
+type mdLogger struct {
+ out io.Writer
+ cfg *LogConfig
+}
+
+// NewMarkdownLogger creates a logger which outputs information in a format adapted
+// for human readability, and is also a valid markdown table
+func NewMarkdownLogger(cfg *LogConfig, writer io.Writer) *mdLogger {
+ l := &mdLogger{writer, cfg}
+ if l.cfg == nil {
+ l.cfg = &LogConfig{}
+ }
+ return l
+}
+
+func (t *mdLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) error {
+ if !create {
+ fmt.Fprintf(t.out, "From: `%v`\nTo: `%v`\nData: `0x%x`\nGas: `%d`\nValue `%v` wei\n",
+ from.String(), to.String(),
+ input, gas, value)
+ } else {
+ fmt.Fprintf(t.out, "From: `%v`\nCreate at: `%v`\nData: `0x%x`\nGas: `%d`\nValue `%v` wei\n",
+ from.String(), to.String(),
+ input, gas, value)
+ }
+
+ fmt.Fprintf(t.out, `
+| Pc | Op | Cost | Stack | RStack | Refund |
+|-------|-------------|------|-----------|-----------|---------|
+`)
+ return nil
+}
+
+func (t *mdLogger) CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error {
+ fmt.Fprintf(t.out, "| %4d | %10v | %3d |", pc, op, cost)
+
+ if !t.cfg.DisableStack {
+ // format stack
+ var a []string
+ for _, elem := range stack.data {
+ a = append(a, fmt.Sprintf("%v", elem.String()))
+ }
+ b := fmt.Sprintf("[%v]", strings.Join(a, ","))
+ fmt.Fprintf(t.out, "%10v |", b)
+
+ // format return stack
+ a = a[:0]
+ b = fmt.Sprintf("[%v]", strings.Join(a, ","))
+ fmt.Fprintf(t.out, "%10v |", b)
+ }
+ fmt.Fprintf(t.out, "%10v |", env.StateDB.GetRefund())
+ fmt.Fprintln(t.out, "")
+ if err != nil {
+ fmt.Fprintf(t.out, "Error: %v\n", err)
+ }
+ return nil
+}
+
+func (t *mdLogger) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, contract *Contract, depth int, err error) error {
+
+ fmt.Fprintf(t.out, "\nError: at pc=%d, op=%v: %v\n", pc, op, err)
+
+ return nil
+}
+
+func (t *mdLogger) CaptureEnd(output []byte, gasUsed uint64, tm time.Duration, err error) error {
+ fmt.Fprintf(t.out, "\nOutput: `0x%x`\nConsumed gas: `%d`\nError: `%v`\n",
+ output, gasUsed, err)
+ return nil
+}
diff --git a/l2geth/core/vm/operations_acl.go b/l2geth/core/vm/operations_acl.go
index f6f120212c2b..0238d567601c 100644
--- a/l2geth/core/vm/operations_acl.go
+++ b/l2geth/core/vm/operations_acl.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The go-ethereum Authors
+// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
@@ -24,39 +24,163 @@ import (
"github.com/ethereum-optimism/optimism/l2geth/params"
)
-// These functions are modified to work without the access list logic.
-// Access lists will be added in the future and these functions can
-// be reverted to their upstream implementations.
+func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
+ return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ // If we fail the minimum gas availability invariant, fail (0)
+ if contract.Gas <= params.SstoreSentryGasEIP2200 {
+ return 0, errors.New("not enough gas for reentrancy sentry")
+ }
+ // Gas sentry honoured, do the actual gas calculation based on the stored value
+ var (
+ y, x = stack.Back(1), stack.peek()
+ slot = common.BigToHash(x)
+ current = evm.StateDB.GetState(contract.Address(), slot)
+ cost = uint64(0)
+ )
+ // Check slot presence in the access list
+ if addrPresent, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
+ cost = params.ColdSloadCostEIP2929
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
+ if !addrPresent {
+ // Once we're done with YOLOv2 and schedule this for mainnet, might
+ // be good to remove this panic here, which is just really a
+ // canary to have during testing
+ panic("impossible case: address was not present in access list during sstore op")
+ }
+ }
+ value := common.BigToHash(y)
-// Modified dynamic gas cost to always return the cold cost
+ if current == value { // noop (1)
+ // EIP 2200 original clause:
+ // return params.SloadGasEIP2200, nil
+ return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS
+ }
+ original := evm.StateDB.GetCommittedState(contract.Address(), common.BigToHash(x))
+ if original == current {
+ if original == (common.Hash{}) { // create slot (2.1.1)
+ return cost + params.SstoreSetGasEIP2200, nil
+ }
+ if value == (common.Hash{}) { // delete slot (2.1.2b)
+ evm.StateDB.AddRefund(clearingRefund)
+ }
+ // EIP-2200 original clause:
+ // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
+ return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2)
+ }
+ if original != (common.Hash{}) {
+ if current == (common.Hash{}) { // recreate slot (2.2.1.1)
+ evm.StateDB.SubRefund(clearingRefund)
+ } else if value == (common.Hash{}) { // delete slot (2.2.1.2)
+ evm.StateDB.AddRefund(clearingRefund)
+ }
+ }
+ if original == value {
+ if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1)
+ // EIP 2200 Original clause:
+ //evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.SloadGasEIP2200)
+ evm.StateDB.AddRefund(params.SstoreSetGasEIP2200 - params.WarmStorageReadCostEIP2929)
+ } else { // reset to original existing slot (2.2.2.2)
+ // EIP 2200 Original clause:
+ // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200)
+ // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST)
+ // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST
+ // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST
+ evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929)
+ }
+ }
+ // EIP-2200 original clause:
+ //return params.SloadGasEIP2200, nil // dirty update (2.2)
+ return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2)
+ }
+}
+
+// gasSLoadEIP2929 calculates dynamic gas for SLOAD according to EIP-2929
+// For SLOAD, if the (address, storage_key) pair (where address is the address of the contract
+// whose storage is being read) is not yet in accessed_storage_keys,
+// charge 2100 gas and add the pair to accessed_storage_keys.
+// If the pair is already in accessed_storage_keys, charge 100 gas.
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
- return params.ColdSloadCostEIP2929, nil
+ loc := stack.peek()
+ slot := common.BigToHash(loc)
+ // Check slot presence in the access list
+ if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
+ // When it fails, this returns false
+ // When it succeeds, this returns true
+
+ // If the caller cannot afford the cost, this change will be rolled back
+ // If he does afford it, we can skip checking the same thing later on, during execution
+ evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
+
+ // This is what happens during actual execution
+ return params.ColdSloadCostEIP2929, nil
+ }
+
+ // Every other time, during gas estimation, we hit the bottom code path
+ // Which causes the gas estimation to be too small, and the tx runs out
+ // of gas
+ return params.WarmStorageReadCostEIP2929, nil
}
+// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
+// EIP spec:
+// > If the target is not in accessed_addresses,
+// > charge COLD_ACCOUNT_ACCESS_COST gas, and add the address to accessed_addresses.
+// > Otherwise, charge WARM_STORAGE_READ_COST gas.
func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// memory expansion first (dynamic part of pre-2929 implementation)
gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
if err != nil {
return 0, err
}
- var overflow bool
- if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
- return 0, errors.New("gas uint64 overflow")
+ addr := common.BigToAddress(stack.peek())
+ // Check slot presence in the access list
+ if !evm.StateDB.AddressInAccessList(addr) {
+ evm.StateDB.AddAddressToAccessList(addr)
+ var overflow bool
+ // We charge (cold-warm), since 'warm' is already charged as constantGas
+ if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
+ return 0, ErrGasUintOverflow
+ }
+ return gas, nil
}
return gas, nil
}
+// gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list.
+// If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it
+// is also using 'warm' as constant factor.
+// This method is used by:
+// - extcodehash,
+// - extcodesize,
+// - (ext) balance
func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
- return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil
+ addr := common.BigToAddress(stack.peek())
+ // Check slot presence in the access list
+ if !evm.StateDB.AddressInAccessList(addr) {
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddAddressToAccessList(addr)
+ // The warm storage read cost is already charged as constantGas
+ return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil
+ }
+ return 0, nil
}
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ addr := common.BigToAddress(stack.Back(1))
+ // Check slot presence in the access list
+ warmAccess := evm.StateDB.AddressInAccessList(addr)
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
// the cost to charge for cold access, if any, is Cold - Warm
coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
- if !contract.UseGas(coldCost) {
- return 0, ErrOutOfGas
+ if !warmAccess {
+ evm.StateDB.AddAddressToAccessList(addr)
+ // Charge the remaining difference here already, to correctly calculate available
+ // gas for call
+ if !contract.UseGas(coldCost) {
+ return 0, ErrOutOfGas
+ }
}
// Now call the old calculator, which takes into account
// - create new account
@@ -64,7 +188,7 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc {
// - memory expansion
// - 63/64ths rule
gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
- if err != nil {
+ if warmAccess || err != nil {
return gas, err
}
// In case of a cold access, we temporarily add the cold charge back, and also
@@ -82,14 +206,40 @@ var (
gasStaticCallEIP2929 = makeCallVariantGasCallEIP2929(gasStaticCall)
gasCallCodeEIP2929 = makeCallVariantGasCallEIP2929(gasCallCode)
gasSelfdestructEIP2929 = makeSelfdestructGasFn(true)
+ // gasSelfdestructEIP3529 implements the changes in EIP-2539 (no refunds)
+ gasSelfdestructEIP3529 = makeSelfdestructGasFn(false)
+
+ // gasSStoreEIP2929 implements gas cost for SSTORE according to EIP-2929
+ //
+ // When calling SSTORE, check if the (address, storage_key) pair is in accessed_storage_keys.
+ // If it is not, charge an additional COLD_SLOAD_COST gas, and add the pair to accessed_storage_keys.
+ // Additionally, modify the parameters defined in EIP 2200 as follows:
+ //
+ // Parameter Old value New value
+ // SLOAD_GAS 800 = WARM_STORAGE_READ_COST
+ // SSTORE_RESET_GAS 5000 5000 - COLD_SLOAD_COST
+ //
+ //The other parameters defined in EIP 2200 are unchanged.
+ // see gasSStoreEIP2200(...) in core/vm/gas_table.go for more info about how EIP 2200 is specified
+ gasSStoreEIP2929 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP2200)
+
+ // gasSStoreEIP2539 implements gas cost for SSTORE according to EPI-2539
+ // Replace `SSTORE_CLEARS_SCHEDULE` with `SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST` (4,800)
+ gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529)
)
// makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-2539
func makeSelfdestructGasFn(refundsEnabled bool) gasFunc {
gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
- address := common.BigToAddress(stack.peek())
- gas := params.ColdAccountAccessCostEIP2929
-
+ var (
+ gas uint64
+ address = common.BigToAddress(stack.peek())
+ )
+ if !evm.StateDB.AddressInAccessList(address) {
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddAddressToAccessList(address)
+ gas = params.ColdAccountAccessCostEIP2929
+ }
// if empty and transfers value
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
gas += params.CreateBySelfdestructGas
diff --git a/l2geth/core/vm/operations_acl_optimism.go b/l2geth/core/vm/operations_acl_optimism.go
new file mode 100644
index 000000000000..3c0f7b15d1b3
--- /dev/null
+++ b/l2geth/core/vm/operations_acl_optimism.go
@@ -0,0 +1,103 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package vm
+
+import (
+ "errors"
+
+ "github.com/ethereum-optimism/optimism/l2geth/common"
+ "github.com/ethereum-optimism/optimism/l2geth/common/math"
+ "github.com/ethereum-optimism/optimism/l2geth/params"
+)
+
+// These functions are modified to work without the access list logic.
+// Access lists will be added in the future and these functions can
+// be reverted to their upstream implementations.
+
+// Modified dynamic gas cost to always return the cold cost
+func gasSLoadEIP2929Optimism(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ return params.ColdSloadCostEIP2929, nil
+}
+
+func gasExtCodeCopyEIP2929Optimism(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ // memory expansion first (dynamic part of pre-2929 implementation)
+ gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return 0, err
+ }
+ var overflow bool
+ if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow {
+ return 0, errors.New("gas uint64 overflow")
+ }
+ return gas, nil
+}
+
+func gasEip2929AccountCheckOptimism(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ return params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929, nil
+}
+
+func makeCallVariantGasCallEIP2929Optimism(oldCalculator gasFunc) gasFunc {
+ return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so
+ // the cost to charge for cold access, if any, is Cold - Warm
+ coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+ if !contract.UseGas(coldCost) {
+ return 0, ErrOutOfGas
+ }
+ // Now call the old calculator, which takes into account
+ // - create new account
+ // - transfer value
+ // - memory expansion
+ // - 63/64ths rule
+ gas, err := oldCalculator(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return gas, err
+ }
+ // In case of a cold access, we temporarily add the cold charge back, and also
+ // add it to the returned gas. By adding it to the return, it will be charged
+ // outside of this function, as part of the dynamic gas, and that will make it
+ // also become correctly reported to tracers.
+ contract.Gas += coldCost
+ return gas + coldCost, nil
+ }
+}
+
+var (
+ gasCallEIP2929Optimism = makeCallVariantGasCallEIP2929Optimism(gasCall)
+ gasDelegateCallEIP2929Optimism = makeCallVariantGasCallEIP2929Optimism(gasDelegateCall)
+ gasStaticCallEIP2929Optimism = makeCallVariantGasCallEIP2929Optimism(gasStaticCall)
+ gasCallCodeEIP2929Optimism = makeCallVariantGasCallEIP2929Optimism(gasCallCode)
+ gasSelfdestructEIP2929Optimism = makeSelfdestructGasFnOptimism(true)
+)
+
+// makeSelfdestructGasFn can create the selfdestruct dynamic gas function for EIP-2929 and EIP-2539
+func makeSelfdestructGasFnOptimism(refundsEnabled bool) gasFunc {
+ gasFunc := func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ address := common.BigToAddress(stack.peek())
+ gas := params.ColdAccountAccessCostEIP2929
+
+ // if empty and transfers value
+ if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
+ gas += params.CreateBySelfdestructGas
+ }
+ if refundsEnabled && !evm.StateDB.HasSuicided(contract.Address()) {
+ evm.StateDB.AddRefund(params.SelfdestructRefundGas)
+ }
+ return gas, nil
+ }
+ return gasFunc
+}
diff --git a/l2geth/core/vm/runtime/runtime.go b/l2geth/core/vm/runtime/runtime.go
index fe942c0a104e..c195a7ca4b68 100644
--- a/l2geth/core/vm/runtime/runtime.go
+++ b/l2geth/core/vm/runtime/runtime.go
@@ -106,6 +106,9 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
vmenv = NewEnv(cfg)
sender = vm.AccountRef(cfg.Origin)
)
+ if rules := cfg.ChainConfig.Rules(vmenv.Context.BlockNumber); rules.IsBerlin {
+ cfg.State.PrepareAccessList(cfg.Origin, &address, vm.ActivePrecompiles(rules), nil)
+ }
cfg.State.CreateAccount(address)
// set the receiver's (the executing contract) code for execution.
cfg.State.SetCode(address, code)
@@ -135,7 +138,9 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
vmenv = NewEnv(cfg)
sender = vm.AccountRef(cfg.Origin)
)
-
+ if rules := cfg.ChainConfig.Rules(vmenv.Context.BlockNumber); rules.IsBerlin {
+ cfg.State.PrepareAccessList(cfg.Origin, nil, vm.ActivePrecompiles(rules), nil)
+ }
// Call the code with the given configuration.
code, address, leftOverGas, err := vmenv.Create(
sender,
@@ -157,6 +162,11 @@ func Call(address common.Address, input []byte, cfg *Config) ([]byte, uint64, er
vmenv := NewEnv(cfg)
sender := cfg.State.GetOrNewStateObject(cfg.Origin)
+ statedb := cfg.State
+
+ if rules := cfg.ChainConfig.Rules(vmenv.Context.BlockNumber); rules.IsBerlin {
+ statedb.PrepareAccessList(cfg.Origin, &address, vm.ActivePrecompiles(rules), nil)
+ }
// Call the code with the given configuration.
ret, leftOverGas, err := vmenv.Call(
sender,
diff --git a/l2geth/core/vm/runtime/runtime_test.go b/l2geth/core/vm/runtime/runtime_test.go
index 41b6aa4e1f93..c2ca4568ae58 100644
--- a/l2geth/core/vm/runtime/runtime_test.go
+++ b/l2geth/core/vm/runtime/runtime_test.go
@@ -17,12 +17,15 @@
package runtime
import (
+ "fmt"
"math/big"
+ "os"
"strings"
"testing"
"github.com/ethereum-optimism/optimism/l2geth/accounts/abi"
"github.com/ethereum-optimism/optimism/l2geth/common"
+ "github.com/ethereum-optimism/optimism/l2geth/core/asm"
"github.com/ethereum-optimism/optimism/l2geth/core/rawdb"
"github.com/ethereum-optimism/optimism/l2geth/core/state"
"github.com/ethereum-optimism/optimism/l2geth/core/vm"
@@ -203,3 +206,115 @@ func BenchmarkEVM_CREATE2_1200(bench *testing.B) {
// initcode size 1200K, repeatedly calls CREATE2 and then modifies the mem contents
benchmarkEVM_Create(bench, "5b5862124f80600080f5600152600056")
}
+
+// TestEip2929Cases contains various testcases that are used for
+// EIP-2929 about gas repricings
+func TestEip2929Cases(t *testing.T) {
+
+ id := 1
+ prettyPrint := func(comment string, code []byte) {
+
+ instrs := make([]string, 0)
+ it := asm.NewInstructionIterator(code)
+ for it.Next() {
+ if it.Arg() != nil && 0 < len(it.Arg()) {
+ instrs = append(instrs, fmt.Sprintf("%v 0x%x", it.Op(), it.Arg()))
+ } else {
+ instrs = append(instrs, fmt.Sprintf("%v", it.Op()))
+ }
+ }
+ ops := strings.Join(instrs, ", ")
+ fmt.Printf("### Case %d\n\n", id)
+ id++
+ fmt.Printf("%v\n\nBytecode: \n```\n0x%x\n```\nOperations: \n```\n%v\n```\n\n",
+ comment,
+ code, ops)
+ Execute(code, nil, &Config{
+ EVMConfig: vm.Config{
+ Debug: true,
+ Tracer: vm.NewMarkdownLogger(nil, os.Stdout),
+ ExtraEips: []int{2929},
+ },
+ })
+ }
+
+ { // First eip testcase
+ code := []byte{
+ // Three checks against a precompile
+ byte(vm.PUSH1), 1, byte(vm.EXTCODEHASH), byte(vm.POP),
+ byte(vm.PUSH1), 2, byte(vm.EXTCODESIZE), byte(vm.POP),
+ byte(vm.PUSH1), 3, byte(vm.BALANCE), byte(vm.POP),
+ // Three checks against a non-precompile
+ byte(vm.PUSH1), 0xf1, byte(vm.EXTCODEHASH), byte(vm.POP),
+ byte(vm.PUSH1), 0xf2, byte(vm.EXTCODESIZE), byte(vm.POP),
+ byte(vm.PUSH1), 0xf3, byte(vm.BALANCE), byte(vm.POP),
+ // Same three checks (should be cheaper)
+ byte(vm.PUSH1), 0xf2, byte(vm.EXTCODEHASH), byte(vm.POP),
+ byte(vm.PUSH1), 0xf3, byte(vm.EXTCODESIZE), byte(vm.POP),
+ byte(vm.PUSH1), 0xf1, byte(vm.BALANCE), byte(vm.POP),
+ // Check the origin, and the 'this'
+ byte(vm.ORIGIN), byte(vm.BALANCE), byte(vm.POP),
+ byte(vm.ADDRESS), byte(vm.BALANCE), byte(vm.POP),
+
+ byte(vm.STOP),
+ }
+ prettyPrint("This checks `EXT`(codehash,codesize,balance) of precompiles, which should be `100`, "+
+ "and later checks the same operations twice against some non-precompiles. "+
+ "Those are cheaper second time they are accessed. Lastly, it checks the `BALANCE` of `origin` and `this`.", code)
+ }
+
+ { // EXTCODECOPY
+ code := []byte{
+ // extcodecopy( 0xff,0,0,0,0)
+ byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, //length, codeoffset, memoffset
+ byte(vm.PUSH1), 0xff, byte(vm.EXTCODECOPY),
+ // extcodecopy( 0xff,0,0,0,0)
+ byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, //length, codeoffset, memoffset
+ byte(vm.PUSH1), 0xff, byte(vm.EXTCODECOPY),
+ // extcodecopy( this,0,0,0,0)
+ byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x00, //length, codeoffset, memoffset
+ byte(vm.ADDRESS), byte(vm.EXTCODECOPY),
+
+ byte(vm.STOP),
+ }
+ prettyPrint("This checks `extcodecopy( 0xff,0,0,0,0)` twice, (should be expensive first time), "+
+ "and then does `extcodecopy( this,0,0,0,0)`.", code)
+ }
+
+ { // SLOAD + SSTORE
+ code := []byte{
+
+ // Add slot `0x1` to access list
+ byte(vm.PUSH1), 0x01, byte(vm.SLOAD), byte(vm.POP), // SLOAD( 0x1) (add to access list)
+ // Write to `0x1` which is already in access list
+ byte(vm.PUSH1), 0x11, byte(vm.PUSH1), 0x01, byte(vm.SSTORE), // SSTORE( loc: 0x01, val: 0x11)
+ // Write to `0x2` which is not in access list
+ byte(vm.PUSH1), 0x11, byte(vm.PUSH1), 0x02, byte(vm.SSTORE), // SSTORE( loc: 0x02, val: 0x11)
+ // Write again to `0x2`
+ byte(vm.PUSH1), 0x11, byte(vm.PUSH1), 0x02, byte(vm.SSTORE), // SSTORE( loc: 0x02, val: 0x11)
+ // Read slot in access list (0x2)
+ byte(vm.PUSH1), 0x02, byte(vm.SLOAD), // SLOAD( 0x2)
+ // Read slot in access list (0x1)
+ byte(vm.PUSH1), 0x01, byte(vm.SLOAD), // SLOAD( 0x1)
+ }
+ prettyPrint("This checks `sload( 0x1)` followed by `sstore(loc: 0x01, val:0x11)`, then 'naked' sstore:"+
+ "`sstore(loc: 0x02, val:0x11)` twice, and `sload(0x2)`, `sload(0x1)`. ", code)
+ }
+ { // Call variants
+ code := []byte{
+ // identity precompile
+ byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0x04, byte(vm.PUSH1), 0x0, byte(vm.CALL), byte(vm.POP),
+
+ // random account - call 1
+ byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.PUSH1), 0x0, byte(vm.CALL), byte(vm.POP),
+
+ // random account - call 2
+ byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1),
+ byte(vm.PUSH1), 0xff, byte(vm.PUSH1), 0x0, byte(vm.STATICCALL), byte(vm.POP),
+ }
+ prettyPrint("This calls the `identity`-precompile (cheap), then calls an account (expensive) and `staticcall`s the same"+
+ "account (cheap)", code)
+ }
+}
diff --git a/l2geth/eth/api_backend.go b/l2geth/eth/api_backend.go
index 342163c48f59..35de825c9757 100644
--- a/l2geth/eth/api_backend.go
+++ b/l2geth/eth/api_backend.go
@@ -141,13 +141,7 @@ func (b *EthAPIBackend) SequencerClientHttp() string {
}
func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) {
- // Pending block is only known by the miner
- if number == rpc.PendingBlockNumber {
- block := b.eth.miner.PendingBlock()
- return block.Header(), nil
- }
- // Otherwise resolve and return the block
- if number == rpc.LatestBlockNumber {
+ if number == rpc.LatestBlockNumber || number == rpc.PendingBlockNumber {
return b.eth.blockchain.CurrentBlock().Header(), nil
}
return b.eth.blockchain.GetHeaderByNumber(uint64(number)), nil
@@ -175,13 +169,7 @@ func (b *EthAPIBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*ty
}
func (b *EthAPIBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) {
- // Pending block is only known by the miner
- if number == rpc.PendingBlockNumber {
- block := b.eth.miner.PendingBlock()
- return block, nil
- }
- // Otherwise resolve and return the block
- if number == rpc.LatestBlockNumber {
+ if number == rpc.LatestBlockNumber || number == rpc.PendingBlockNumber {
return b.eth.blockchain.CurrentBlock(), nil
}
return b.eth.blockchain.GetBlockByNumber(uint64(number)), nil
@@ -213,12 +201,6 @@ func (b *EthAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash r
}
func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) {
- // Pending state is only known by the miner
- if number == rpc.PendingBlockNumber {
- block, state := b.eth.miner.Pending()
- return state, block.Header(), nil
- }
- // Otherwise resolve the block number and return its state
header, err := b.HeaderByNumber(ctx, number)
if err != nil {
return nil, nil, err
@@ -271,7 +253,7 @@ func (b *EthAPIBackend) GetTd(blockHash common.Hash) *big.Int {
return b.eth.blockchain.GetTdByHash(blockHash)
}
-func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header) (*vm.EVM, func() error, error) {
+func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmCfg *vm.Config) (*vm.EVM, func() error, error) {
// This was removed upstream:
// https://github.com/ethereum-optimism/optimism/l2geth/commit/39f502329fac4640cfb71959c3496f19ea88bc85#diff-9886da3412b43831145f62cec6e895eb3613a175b945e5b026543b7463454603
// We're throwing this behind a UsingOVM flag for now as to not break
@@ -280,9 +262,11 @@ func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *sta
state.SetBalance(msg.From(), math.MaxBig256)
}
vmError := func() error { return nil }
-
+ if vmCfg == nil {
+ vmCfg = b.eth.blockchain.GetVMConfig()
+ }
context := core.NewEVMContext(msg, header, b.eth.BlockChain(), nil)
- return vm.NewEVM(context, state, b.eth.blockchain.Config(), *b.eth.blockchain.GetVMConfig()), vmError, nil
+ return vm.NewEVM(context, state, b.eth.blockchain.Config(), *vmCfg), vmError, nil
}
func (b *EthAPIBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription {
diff --git a/l2geth/eth/api_tracer.go b/l2geth/eth/api_tracer.go
index 828ace90f569..d3c3030c7705 100644
--- a/l2geth/eth/api_tracer.go
+++ b/l2geth/eth/api_tracer.go
@@ -38,6 +38,7 @@ import (
"github.com/ethereum-optimism/optimism/l2geth/eth/tracers"
"github.com/ethereum-optimism/optimism/l2geth/internal/ethapi"
"github.com/ethereum-optimism/optimism/l2geth/log"
+ "github.com/ethereum-optimism/optimism/l2geth/params"
"github.com/ethereum-optimism/optimism/l2geth/rlp"
"github.com/ethereum-optimism/optimism/l2geth/rpc"
"github.com/ethereum-optimism/optimism/l2geth/trie"
@@ -106,17 +107,15 @@ func (api *PrivateDebugAPI) TraceChain(ctx context.Context, start, end rpc.Block
var from, to *types.Block
switch start {
- case rpc.PendingBlockNumber:
- from = api.eth.miner.PendingBlock()
case rpc.LatestBlockNumber:
+ case rpc.PendingBlockNumber:
from = api.eth.blockchain.CurrentBlock()
default:
from = api.eth.blockchain.GetBlockByNumber(uint64(start))
}
switch end {
- case rpc.PendingBlockNumber:
- to = api.eth.miner.PendingBlock()
case rpc.LatestBlockNumber:
+ case rpc.PendingBlockNumber:
to = api.eth.blockchain.CurrentBlock()
default:
to = api.eth.blockchain.GetBlockByNumber(uint64(end))
@@ -357,9 +356,8 @@ func (api *PrivateDebugAPI) TraceBlockByNumber(ctx context.Context, number rpc.B
var block *types.Block
switch number {
- case rpc.PendingBlockNumber:
- block = api.eth.miner.PendingBlock()
case rpc.LatestBlockNumber:
+ case rpc.PendingBlockNumber:
block = api.eth.blockchain.CurrentBlock()
default:
block = api.eth.blockchain.GetBlockByNumber(uint64(number))
@@ -561,9 +559,30 @@ func (api *PrivateDebugAPI) standardTraceBlockToFile(ctx context.Context, block
// Execute transaction, either tracing all or just the requested one
var (
- signer = types.MakeSigner(api.eth.blockchain.Config(), block.Number())
- dumps []string
+ dumps []string
+ signer = types.MakeSigner(api.eth.blockchain.Config(), block.Number())
+ chainConfig = api.eth.blockchain.Config()
+ canon = true
)
+
+ // Check if there are any overrides: the caller may wish to enable a future
+ // fork when executing this block. Note, such overrides are only applicable to the
+ // actual specified block, not any preceding blocks that we have to go through
+ // in order to obtain the state.
+ // Therefore, it's perfectly valid to specify `"futureForkBlock": 0`, to enable `futureFork`
+
+ if config != nil && config.Overrides != nil {
+ // Copy the config, to not screw up the main config
+ // Note: the Clique-part is _not_ deep copied
+ chainConfigCopy := new(params.ChainConfig)
+ *chainConfigCopy = *chainConfig
+ chainConfig = chainConfigCopy
+ if berlin := config.LogConfig.Overrides.BerlinBlock; berlin != nil {
+ chainConfig.BerlinBlock = berlin
+ canon = false
+ }
+ }
+
for i, tx := range block.Transactions() {
// Prepare the trasaction for un-traced execution
var (
@@ -579,7 +598,9 @@ func (api *PrivateDebugAPI) standardTraceBlockToFile(ctx context.Context, block
if tx.Hash() == txHash || txHash == (common.Hash{}) {
// Generate a unique temporary file to dump it into
prefix := fmt.Sprintf("block_%#x-%d-%#x-", block.Hash().Bytes()[:4], i, tx.Hash().Bytes()[:4])
-
+ if !canon {
+ prefix = fmt.Sprintf("%valt-", prefix)
+ }
dump, err = ioutil.TempFile(os.TempDir(), prefix)
if err != nil {
return nil, err
@@ -595,7 +616,7 @@ func (api *PrivateDebugAPI) standardTraceBlockToFile(ctx context.Context, block
}
}
// Execute the transaction and flush any traces to disk
- vmenv := vm.NewEVM(vmctx, statedb, api.eth.blockchain.Config(), vmConf)
+ vmenv := vm.NewEVM(vmctx, statedb, chainConfig, vmConf)
_, _, _, err = core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()))
if writer != nil {
writer.Flush()
@@ -755,8 +776,19 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, v
default:
tracer = vm.NewStructLogger(config.LogConfig)
}
+
+ chainConfig := api.eth.blockchain.Config()
+ if config != nil && config.LogConfig != nil && config.LogConfig.Overrides != nil {
+ chainConfigCopy := new(params.ChainConfig)
+ *chainConfigCopy = *chainConfig
+ chainConfig = chainConfigCopy
+ if berlin := config.LogConfig.Overrides.BerlinBlock; berlin != nil {
+ chainConfig.BerlinBlock = berlin
+ }
+ }
+
// Run the transaction with tracing enabled.
- vmenv := vm.NewEVM(vmctx, statedb, api.eth.blockchain.Config(), vm.Config{Debug: true, Tracer: tracer})
+ vmenv := vm.NewEVM(vmctx, statedb, chainConfig, vm.Config{Debug: true, Tracer: tracer})
ret, gas, failed, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()))
if err != nil {
diff --git a/l2geth/graphql/graphql.go b/l2geth/graphql/graphql.go
index 8df50e50f2c5..730a0a5ef008 100644
--- a/l2geth/graphql/graphql.go
+++ b/l2geth/graphql/graphql.go
@@ -776,7 +776,7 @@ func (b *Block) Call(ctx context.Context, args struct {
return nil, err
}
}
- result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap())
+ result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, &vm.Config{}, 5*time.Second, b.backend.RPCGasCap())
status := hexutil.Uint64(1)
if failed {
status = 0
@@ -842,7 +842,7 @@ func (p *Pending) Call(ctx context.Context, args struct {
Data ethapi.CallArgs
}) (*CallResult, error) {
pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
- result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, pendingBlockNr, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap())
+ result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, pendingBlockNr, nil, &vm.Config{}, 5*time.Second, p.backend.RPCGasCap())
status := hexutil.Uint64(1)
if failed {
status = 0
diff --git a/l2geth/interfaces.go b/l2geth/interfaces.go
index fd6aec3bfe7a..cc05b0a7bf3c 100644
--- a/l2geth/interfaces.go
+++ b/l2geth/interfaces.go
@@ -120,6 +120,8 @@ type CallMsg struct {
Value *big.Int // amount of wei sent along with the call
Data []byte // input data, usually an ABI-encoded contract method invocation
+ AccessList types.AccessList // EIP-2930 access list.
+
L1Timestamp uint64
L1BlockNumber *big.Int
QueueOrigin types.QueueOrigin
diff --git a/l2geth/internal/ethapi/api.go b/l2geth/internal/ethapi/api.go
index 2561588d1e2f..cf62d9316cb2 100644
--- a/l2geth/internal/ethapi/api.go
+++ b/l2geth/internal/ethapi/api.go
@@ -799,7 +799,7 @@ type account struct {
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
}
-func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int) ([]byte, uint64, bool, error) {
+func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg *vm.Config, timeout time.Duration, globalGasCap *big.Int) ([]byte, uint64, bool, error) {
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
@@ -910,7 +910,7 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo
defer cancel()
// Get a new instance of the EVM.
- evm, vmError, err := b.GetEVM(ctx, msg, state, header)
+ evm, vmError, err := b.GetEVM(ctx, msg, state, header, vmCfg)
if err != nil {
return nil, 0, false, err
}
@@ -946,7 +946,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
if overrides != nil {
accounts = *overrides
}
- result, _, failed, err := DoCall(ctx, s.b, args, blockNrOrHash, accounts, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
+ result, _, failed, err := DoCall(ctx, s.b, args, blockNrOrHash, accounts, &vm.Config{}, 5*time.Second, s.b.RPCGasCap())
if err != nil {
return nil, err
}
@@ -989,11 +989,13 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
}
cap = hi
- // Set sender address or use a default if none specified
- if args.From == nil {
- if wallets := b.AccountManager().Wallets(); len(wallets) > 0 {
- if accounts := wallets[0].Accounts(); len(accounts) > 0 {
- args.From = &accounts[0].Address
+ if !rcfg.UsingOVM {
+ // Set sender address or use a default if none specified
+ if args.From == nil {
+ if wallets := b.AccountManager().Wallets(); len(wallets) > 0 {
+ if accounts := wallets[0].Accounts(); len(accounts) > 0 {
+ args.From = &accounts[0].Address
+ }
}
}
}
@@ -1005,7 +1007,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
executable := func(gas uint64) (bool, []byte) {
args.Gas = (*hexutil.Uint64)(&gas)
- res, _, failed, err := DoCall(ctx, b, args, blockNrOrHash, nil, vm.Config{}, 0, gasCap)
+ res, _, failed, err := DoCall(ctx, b, args, blockNrOrHash, nil, &vm.Config{}, 0, gasCap)
if err != nil || failed {
return false, res
}
diff --git a/l2geth/internal/ethapi/backend.go b/l2geth/internal/ethapi/backend.go
index a6a21f43339a..ca2f40098dbf 100644
--- a/l2geth/internal/ethapi/backend.go
+++ b/l2geth/internal/ethapi/backend.go
@@ -59,7 +59,7 @@ type Backend interface {
StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error)
GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)
GetTd(hash common.Hash) *big.Int
- GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header) (*vm.EVM, func() error, error)
+ GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmCfg *vm.Config) (*vm.EVM, func() error, error)
SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription
diff --git a/l2geth/les/api_backend.go b/l2geth/les/api_backend.go
index 24623f6eb5cc..98e2360be364 100644
--- a/l2geth/les/api_backend.go
+++ b/l2geth/les/api_backend.go
@@ -199,7 +199,7 @@ func (b *LesApiBackend) GetTd(hash common.Hash) *big.Int {
return b.eth.blockchain.GetTdByHash(hash)
}
-func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header) (*vm.EVM, func() error, error) {
+func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmCfg *vm.Config) (*vm.EVM, func() error, error) {
state.SetBalance(msg.From(), math.MaxBig256)
context := core.NewEVMContext(msg, header, b.eth.blockchain, nil)
return vm.NewEVM(context, state, b.eth.chainConfig, vm.Config{}), state.Error, nil
diff --git a/l2geth/params/config.go b/l2geth/params/config.go
index 42fa0a30c1b0..8a91cfe5bea6 100644
--- a/l2geth/params/config.go
+++ b/l2geth/params/config.go
@@ -215,16 +215,16 @@ var (
//
// This configuration is intentionally not using keyed fields to force anyone
// adding flags to the config to also have to set these fields.
- AllEthashProtocolChanges = &ChainConfig{big.NewInt(108), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
+ AllEthashProtocolChanges = &ChainConfig{big.NewInt(108), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
// AllCliqueProtocolChanges contains every protocol change (EIPs) introduced
// and accepted by the Ethereum core developers into the Clique consensus.
//
// This configuration is intentionally not using keyed fields to force anyone
// adding flags to the config to also have to set these fields.
- AllCliqueProtocolChanges = &ChainConfig{big.NewInt(420), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}}
+ AllCliqueProtocolChanges = &ChainConfig{big.NewInt(420), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}}
- TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
+ TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, new(EthashConfig), nil}
TestRules = TestChainConfig.Rules(new(big.Int))
)
@@ -295,7 +295,9 @@ type ChainConfig struct {
PetersburgBlock *big.Int `json:"petersburgBlock,omitempty"` // Petersburg switch block (nil = same as Constantinople)
IstanbulBlock *big.Int `json:"istanbulBlock,omitempty"` // Istanbul switch block (nil = no fork, 0 = already on istanbul)
MuirGlacierBlock *big.Int `json:"muirGlacierBlock,omitempty"` // Eip-2384 (bomb delay) switch block (nil = no fork, 0 = already activated)
- EWASMBlock *big.Int `json:"ewasmBlock,omitempty"` // EWASM switch block (nil = no fork, 0 = already activated)
+ BerlinBlock *big.Int `json:"berlinBlock,omitempty"` // Berlin switch block (nil = no fork, 0 = already on berlin)
+
+ EWASMBlock *big.Int `json:"ewasmBlock,omitempty"` // EWASM switch block (nil = no fork, 0 = already activated)
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
@@ -332,7 +334,7 @@ func (c *ChainConfig) String() string {
default:
engine = "unknown"
}
- return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Engine: %v}",
+ return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Berlin: %v, Engine: %v}",
c.ChainID,
c.HomesteadBlock,
c.DAOForkBlock,
@@ -345,6 +347,7 @@ func (c *ChainConfig) String() string {
c.PetersburgBlock,
c.IstanbulBlock,
c.MuirGlacierBlock,
+ c.BerlinBlock,
engine,
)
}
@@ -401,6 +404,11 @@ func (c *ChainConfig) IsIstanbul(num *big.Int) bool {
return isForked(c.IstanbulBlock, num)
}
+// IsBerlin returns whether num is either equal to the Berlin fork block or greater.
+func (c *ChainConfig) IsBerlin(num *big.Int) bool {
+ return isForked(c.BerlinBlock, num)
+}
+
// IsEWASM returns whether num represents a block number after the EWASM fork
func (c *ChainConfig) IsEWASM(num *big.Int) bool {
return isForked(c.EWASMBlock, num)
@@ -442,6 +450,7 @@ func (c *ChainConfig) CheckConfigForkOrder() error {
{"petersburgBlock", c.PetersburgBlock},
{"istanbulBlock", c.IstanbulBlock},
{"muirGlacierBlock", c.MuirGlacierBlock},
+ {name: "berlinBlock", block: c.BerlinBlock},
} {
if lastFork.name != "" {
// Next one must be higher number
@@ -498,6 +507,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, head *big.Int) *Confi
if isForkIncompatible(c.MuirGlacierBlock, newcfg.MuirGlacierBlock, head) {
return newCompatError("Muir Glacier fork block", c.MuirGlacierBlock, newcfg.MuirGlacierBlock)
}
+ if isForkIncompatible(c.BerlinBlock, newcfg.BerlinBlock, head) {
+ return newCompatError("Berlin fork block", c.BerlinBlock, newcfg.BerlinBlock)
+ }
if isForkIncompatible(c.EWASMBlock, newcfg.EWASMBlock, head) {
return newCompatError("ewasm fork block", c.EWASMBlock, newcfg.EWASMBlock)
}
@@ -568,6 +580,7 @@ type Rules struct {
ChainID *big.Int
IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool
IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
+ IsBerlin bool
}
// Rules ensures c's ChainID is not nil.
@@ -586,5 +599,6 @@ func (c *ChainConfig) Rules(num *big.Int) Rules {
IsConstantinople: c.IsConstantinople(num),
IsPetersburg: c.IsPetersburg(num),
IsIstanbul: c.IsIstanbul(num),
+ IsBerlin: c.IsBerlin(num),
}
}
diff --git a/l2geth/params/protocol_params.go b/l2geth/params/protocol_params.go
index 8fbd4af621c7..d1d10ff76bf7 100644
--- a/l2geth/params/protocol_params.go
+++ b/l2geth/params/protocol_params.go
@@ -52,7 +52,11 @@ const (
NetSstoreResetRefund uint64 = 4800 // Once per SSTORE operation for resetting to the original non-zero value
NetSstoreResetClearRefund uint64 = 19800 // Once per SSTORE operation for resetting to the original zero value
- SstoreSentryGasEIP2200 uint64 = 2300 // Minimum gas required to be present for an SSTORE call, not consumed
+ SstoreSentryGasEIP2200 uint64 = 2300 // Minimum gas required to be present for an SSTORE call, not consumed
+ SstoreSetGasEIP2200 uint64 = 20000 // Once per SSTORE operation from clean zero to non-zero
+ SstoreResetGasEIP2200 uint64 = 5000 // Once per SSTORE operation from clean non-zero to something else
+ SstoreClearsScheduleRefundEIP2200 uint64 = 15000 // Once per SSTORE operation for clearing an originally existing storage slot
+
SstoreNoopGasEIP2200 uint64 = 800 // Once per SSTORE operation if the value doesn't change.
SstoreDirtyGasEIP2200 uint64 = 800 // Once per SSTORE operation if a dirty value is changed.
SstoreInitGasEIP2200 uint64 = 20000 // Once per SSTORE operation from clean zero to non-zero
@@ -65,23 +69,31 @@ const (
ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST
WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST
+ // In EIP-2200: SstoreResetGas was 5000.
+ // In EIP-2929: SstoreResetGas was changed to '5000 - COLD_SLOAD_COST'.
+ // In EIP-3529: SSTORE_CLEARS_SCHEDULE is defined as SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST
+ // Which becomes: 5000 - 2100 + 1900 = 4800
+ SstoreClearsScheduleRefundEIP3529 uint64 = SstoreResetGasEIP2200 - ColdSloadCostEIP2929 + TxAccessListStorageKeyGas
+
JumpdestGas uint64 = 1 // Once per JUMPDEST operation.
EpochDuration uint64 = 30000 // Duration between proof-of-work epochs.
- CreateDataGas uint64 = 200 //
- CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack.
- ExpGas uint64 = 10 // Once per EXP instruction
- LogGas uint64 = 375 // Per LOG* operation.
- CopyGas uint64 = 3 //
- StackLimit uint64 = 1024 // Maximum size of VM stack allowed.
- TierStepGas uint64 = 0 // Once per operation, for a selection of them.
- LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas.
- CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
- Create2Gas uint64 = 32000 // Once per CREATE2 operation
- SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation.
- MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
- TxDataNonZeroGasFrontier uint64 = 68 // Per byte of data attached to a transaction that is not equal to zero. NOTE: Not payable on data of calls between transactions.
- TxDataNonZeroGasEIP2028 uint64 = 16 // Per byte of non zero data attached to a transaction after EIP 2028 (part in Istanbul)
+ CreateDataGas uint64 = 200 //
+ CallCreateDepth uint64 = 1024 // Maximum depth of call/create stack.
+ ExpGas uint64 = 10 // Once per EXP instruction
+ LogGas uint64 = 375 // Per LOG* operation.
+ CopyGas uint64 = 3 //
+ StackLimit uint64 = 1024 // Maximum size of VM stack allowed.
+ TierStepGas uint64 = 0 // Once per operation, for a selection of them.
+ LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas.
+ CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
+ Create2Gas uint64 = 32000 // Once per CREATE2 operation
+ SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation.
+ MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
+ TxDataNonZeroGasFrontier uint64 = 68 // Per byte of data attached to a transaction that is not equal to zero. NOTE: Not payable on data of calls between transactions.
+ TxDataNonZeroGasEIP2028 uint64 = 16 // Per byte of non zero data attached to a transaction after EIP 2028 (part in Istanbul)
+ TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list
+ TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list
// These have been changed during the course of the chain
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction.
diff --git a/l2geth/scripts/init.sh b/l2geth/scripts/init.sh
new file mode 100755
index 000000000000..335042274111
--- /dev/null
+++ b/l2geth/scripts/init.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# script to help simplify l2geth initialization
+# it needs a path on the filesystem to the state
+# dump
+
+set -eou pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"
+REPO=$DIR/..
+STATE_DUMP=${STATE_DUMP:-$REPO/../packages/contracts/dist/dumps/state-dump.latest.json}
+DATADIR=${DATADIR:-$HOME/.ethereum}
+
+# These are the initial key and address that must be used for the clique
+# signer on the optimism network. All nodes must be initialized with this
+# key before they are able to join the network and sync correctly.
+# The signer address needs to be in the genesis block's extradata.
+SIGNER_KEY=6587ae678cf4fc9a33000cdbf9f35226b71dcc6a4684a31203241f9bcfd55d27
+SIGNER=0x00000398232e2064f896018496b4b44b3d62751f
+
+mkdir -p $DATADIR
+
+if [[ ! -f $STATE_DUMP ]]; then
+ echo "Cannot find $STATE_DUMP"
+ exit 1
+fi
+
+# Add funds to the signer account so that it can be used to send transactions
+if [[ ! -z "$DEVELOPMENT" ]]; then
+ echo "Setting up development genesis"
+ echo "Assuming that the initial clique signer is $SIGNER, this is configured in genesis extradata"
+ DUMP=$(cat $STATE_DUMP | jq '.alloc += {"0x00000398232e2064f896018496b4b44b3d62751f": {balance: "10000000000000000000"}}')
+ TEMP=$(mktemp)
+ echo "$DUMP" | jq . > $TEMP
+ STATE_DUMP=$TEMP
+fi
+
+geth="$REPO/build/bin/geth"
+USING_OVM=true $geth init --datadir $DATADIR $STATE_DUMP
+
+echo "6587ae678cf4fc9a33000cdbf9f35226b71dcc6a4684a31203241f9bcfd55d27" \
+ > $DATADIR/keyfile
+
+echo "password" > $DATADIR/password
+
+USING_OVM=true $geth account import \
+ --datadir $DATADIR --password $DATADIR/password $DATADIR/keyfile
diff --git a/l2geth/scripts/start.sh b/l2geth/scripts/start.sh
index d48c353a1173..fc73c5aea76e 100755
--- a/l2geth/scripts/start.sh
+++ b/l2geth/scripts/start.sh
@@ -6,16 +6,18 @@ REPO=$DIR/..
IS_VERIFIER=
ROLLUP_SYNC_SERVICE_ENABLE=true
DATADIR=$HOME/.ethereum
-TARGET_GAS_LIMIT=11000000
+TARGET_GAS_LIMIT=15000000
ETH1_CTC_DEPLOYMENT_HEIGHT=12686738
ROLLUP_CLIENT_HTTP=http://localhost:7878
ROLLUP_POLL_INTERVAL=15s
-ROLLUP_TIMESTAMP_REFRESH=3m
+ROLLUP_TIMESTAMP_REFRESH=15s
CACHE=1024
RPC_PORT=8545
WS_PORT=8546
VERBOSITY=3
ROLLUP_BACKEND=l2
+CHAIN_ID=69
+BLOCK_SIGNER_ADDRESS=0x00000398232E2064F896018496b4b44b3D62751F
USAGE="
Start the Sequencer or Verifier with most configuration pre-set.
@@ -174,15 +176,22 @@ cmd="$cmd --ws"
cmd="$cmd --wsaddr 0.0.0.0"
cmd="$cmd --wsport $WS_PORT"
cmd="$cmd --wsorigins '*'"
-cmd="$cmd --rpcapi 'eth,net,rollup,web3,debug'"
+cmd="$cmd --rpcapi eth,net,rollup,web3,debug,personal"
cmd="$cmd --gasprice 0"
cmd="$cmd --nousb"
cmd="$cmd --gcmode=archive"
-cmd="$cmd --ipcdisable"
+cmd="$cmd --nodiscover"
+cmd="$cmd --mine"
+cmd="$cmd --password=$DATADIR/password"
+cmd="$cmd --allow-insecure-unlock"
+cmd="$cmd --unlock=$BLOCK_SIGNER_ADDRESS"
+cmd="$cmd --miner.etherbase=$BLOCK_SIGNER_ADDRESS"
+cmd="$cmd --txpool.pricelimit 0"
+
if [[ ! -z "$IS_VERIFIER" ]]; then
cmd="$cmd --rollup.verifier"
fi
cmd="$cmd --verbosity=$VERBOSITY"
echo -e "Running:\nTARGET_GAS_LIMIT=$TARGET_GAS_LIMIT USING_OVM=true $cmd"
-eval env TARGET_GAS_LIMIT=$TARGET_GAS_LIMIT USING_OVM=true $cmd
+TARGET_GAS_LIMIT=$TARGET_GAS_LIMIT USING_OVM=true $cmd
diff --git a/l2geth/tests/state_test_util.go b/l2geth/tests/state_test_util.go
index 4347e67942e8..789e7c12a565 100644
--- a/l2geth/tests/state_test_util.go
+++ b/l2geth/tests/state_test_util.go
@@ -181,6 +181,16 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config) (*stat
context.GetHash = vmTestBlockHash
evm := vm.NewEVM(context, statedb, config, vmconfig)
+ if config.IsBerlin(context.BlockNumber) {
+ statedb.AddAddressToAccessList(msg.From())
+ if dst := msg.To(); dst != nil {
+ statedb.AddAddressToAccessList(*dst)
+ // If it's a create-tx, the destination will be added inside evm.create
+ }
+ for _, addr := range vm.ActivePrecompiles(config.Rules(context.BlockNumber)) {
+ statedb.AddAddressToAccessList(addr)
+ }
+ }
gaspool := new(core.GasPool)
gaspool.AddGas(block.GasLimit())
snapshot := statedb.Snapshot()
diff --git a/packages/batch-submitter/CHANGELOG.md b/packages/batch-submitter/CHANGELOG.md
index cc7a469f7340..8a74d8a464c0 100644
--- a/packages/batch-submitter/CHANGELOG.md
+++ b/packages/batch-submitter/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.4.15
+
+### Patch Changes
+
+- ae4a90d9: Adds a fix for the BSS to account for new timestamp logic in L2Geth
+- ca547c4e: Import performance to not couple batch submitter to version of nodejs that has performance as a builtin
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+ - @eth-optimism/contracts@0.5.10
+
## 0.4.14
### Patch Changes
diff --git a/packages/batch-submitter/package.json b/packages/batch-submitter/package.json
index 9402406efc3a..9f329901c0e2 100644
--- a/packages/batch-submitter/package.json
+++ b/packages/batch-submitter/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@eth-optimism/batch-submitter",
- "version": "0.4.14",
+ "version": "0.4.15",
"description": "[Optimism] Service for submitting transactions and transaction results",
"main": "dist/index",
"types": "dist/index",
@@ -34,8 +34,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
- "@eth-optimism/contracts": "0.5.9",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/contracts": "0.5.10",
+ "@eth-optimism/core-utils": "0.7.5",
"@eth-optimism/ynatm": "^0.2.2",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/providers": "^5.4.5",
diff --git a/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts b/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts
index 9fe1cb2d8f12..15c452c52dbb 100644
--- a/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts
+++ b/packages/batch-submitter/src/batch-submitter/state-batch-submitter.ts
@@ -1,4 +1,6 @@
/* External Imports */
+import { performance } from 'perf_hooks'
+
import { Promise as bPromise } from 'bluebird'
import { Contract, Signer, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
diff --git a/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts b/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts
index 7679e2d337b2..7453b5a83e01 100644
--- a/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts
+++ b/packages/batch-submitter/src/batch-submitter/tx-batch-submitter.ts
@@ -1,4 +1,6 @@
/* External Imports */
+import { performance } from 'perf_hooks'
+
import { Promise as bPromise } from 'bluebird'
import { Signer, ethers, Contract, providers } from 'ethers'
import { TransactionReceipt } from '@ethersproject/abstract-provider'
@@ -683,10 +685,18 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
queued: BatchElement[]
}> = []
for (const block of blocks) {
+ // Create a new context in certain situations
if (
- (lastBlockIsSequencerTx === false && block.isSequencerTx === true) ||
+ // If there are no contexts yet, create a new context.
groupedBlocks.length === 0 ||
- (block.timestamp !== lastTimestamp && block.isSequencerTx === true) ||
+ // If the last block was an L1 to L2 transaction, but the next block is a Sequencer
+ // transaction, create a new context.
+ (lastBlockIsSequencerTx === false && block.isSequencerTx === true) ||
+ // If the timestamp of the last block differs from the timestamp of the current block,
+ // create a new context. Applies to both L1 to L2 transactions and Sequencer transactions.
+ block.timestamp !== lastTimestamp ||
+ // If the block number of the last block differs from the block number of the current block,
+ // create a new context. ONLY applies to Sequencer transactions.
(block.blockNumber !== lastBlockNumber && block.isSequencerTx === true)
) {
groupedBlocks.push({
@@ -694,6 +704,7 @@ export class TransactionBatchSubmitter extends BatchSubmitter {
queued: [],
})
}
+
const cur = groupedBlocks.length - 1
block.isSequencerTx
? groupedBlocks[cur].sequenced.push(block)
diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md
index 41358084936e..5107f8cfeb2f 100644
--- a/packages/contracts/CHANGELOG.md
+++ b/packages/contracts/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 0.5.10
+
+### Patch Changes
+
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+
## 0.5.9
### Patch Changes
diff --git a/packages/contracts/README.md b/packages/contracts/README.md
index f30d2f83d7c9..36baba86a700 100644
--- a/packages/contracts/README.md
+++ b/packages/contracts/README.md
@@ -9,8 +9,6 @@ Within each contract file you'll find a comment that lists:
1. The compiler with which a contract is intended to be compiled, `solc` or `optimistic-solc`.
2. The network upon to which the contract will be deployed, `OVM` or `EVM`.
-A more detailed overview of these contracts can be found on the [community hub](http://community.optimism.io/docs/protocol/protocol.html#system-overview).
-
## Usage (npm)
diff --git a/packages/contracts/bin/take-dump.ts b/packages/contracts/bin/take-dump.ts
index ab4cb3db4e8d..9dc3bbe60741 100644
--- a/packages/contracts/bin/take-dump.ts
+++ b/packages/contracts/bin/take-dump.ts
@@ -65,6 +65,8 @@ import { makeL2GenesisFile } from '../src/make-genesis'
const l1FeeWalletAddress = env.L1_FEE_WALLET_ADDRESS
// The L1 cross domain messenger address, used for cross domain messaging
const l1CrossDomainMessengerAddress = env.L1_CROSS_DOMAIN_MESSENGER_ADDRESS
+ // The block height at which the berlin hardfork activates
+ const berlinBlock = parseInt(env.BERLIN_BLOCK, 10) || 0
ensure(whitelistOwner, 'WHITELIST_OWNER')
ensure(gasPriceOracleOwner, 'GAS_PRICE_ORACLE_OWNER')
@@ -74,6 +76,7 @@ import { makeL2GenesisFile } from '../src/make-genesis'
ensure(l1StandardBridgeAddress, 'L1_STANDARD_BRIDGE_ADDRESS')
ensure(l1FeeWalletAddress, 'L1_FEE_WALLET_ADDRESS')
ensure(l1CrossDomainMessengerAddress, 'L1_CROSS_DOMAIN_MESSENGER_ADDRESS')
+ ensure(berlinBlock, 'BERLIN_BLOCK')
// Basic warning so users know that the whitelist will be disabled if the owner is the zero address.
if (env.WHITELIST_OWNER === '0x' + '00'.repeat(20)) {
@@ -96,6 +99,7 @@ import { makeL2GenesisFile } from '../src/make-genesis'
l1StandardBridgeAddress,
l1FeeWalletAddress,
l1CrossDomainMessengerAddress,
+ berlinBlock,
})
fs.writeFileSync(outfile, JSON.stringify(genesis, null, 4))
diff --git a/packages/contracts/package.json b/packages/contracts/package.json
index 0abf0b50ef70..7350c8aa52aa 100644
--- a/packages/contracts/package.json
+++ b/packages/contracts/package.json
@@ -1,6 +1,6 @@
{
"name": "@eth-optimism/contracts",
- "version": "0.5.9",
+ "version": "0.5.10",
"description": "[Optimism] L1 and L2 smart contracts for Optimism",
"main": "dist/index",
"types": "dist/index",
@@ -58,7 +58,7 @@
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/core-utils": "0.7.5",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/hardware-wallets": "^5.4.0"
diff --git a/packages/contracts/src/make-genesis.ts b/packages/contracts/src/make-genesis.ts
index 34526145ceab..7b3419849c3a 100644
--- a/packages/contracts/src/make-genesis.ts
+++ b/packages/contracts/src/make-genesis.ts
@@ -40,6 +40,8 @@ export interface RollupDeployConfig {
l1FeeWalletAddress: string
// Address of the L1CrossDomainMessenger contract.
l1CrossDomainMessengerAddress: string
+ // Block height to activate berlin hardfork
+ berlinBlock: number
}
/**
@@ -150,6 +152,7 @@ export const makeL2GenesisFile = async (
petersburgBlock: 0,
istanbulBlock: 0,
muirGlacierBlock: 0,
+ berlinBlock: cfg.berlinBlock,
clique: {
period: 0,
epoch: 30000,
diff --git a/packages/core-utils/CHANGELOG.md b/packages/core-utils/CHANGELOG.md
index 82a2a385582b..30d27b442de0 100644
--- a/packages/core-utils/CHANGELOG.md
+++ b/packages/core-utils/CHANGELOG.md
@@ -1,5 +1,11 @@
# @eth-optimism/core-utils
+## 0.7.5
+
+### Patch Changes
+
+- ad94b9d1: test/docs: Improve docstrings and tests for utils inside of hex-strings.ts
+
## 0.7.4
### Patch Changes
diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json
index b811ecddf6df..de0ee956ea1c 100644
--- a/packages/core-utils/package.json
+++ b/packages/core-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@eth-optimism/core-utils",
- "version": "0.7.4",
+ "version": "0.7.5",
"description": "[Optimism] Core typescript utilities",
"main": "dist/index",
"types": "dist/index",
diff --git a/packages/core-utils/src/common/hex-strings.ts b/packages/core-utils/src/common/hex-strings.ts
index 02704b0203b6..29e7b706e1f0 100644
--- a/packages/core-utils/src/common/hex-strings.ts
+++ b/packages/core-utils/src/common/hex-strings.ts
@@ -56,6 +56,12 @@ export const toHexString = (inp: Buffer | string | number | null): string => {
}
}
+/**
+ * Casts a number to a hex string without zero padding.
+ *
+ * @param n Number to cast to a hex string.
+ * @return Number cast as a hex string.
+ */
export const toRpcHexString = (n: number | BigNumber): string => {
let num
if (typeof n === 'number') {
@@ -67,10 +73,18 @@ export const toRpcHexString = (n: number | BigNumber): string => {
if (num === '0x0') {
return num
} else {
+ // BigNumber pads a single 0 to keep hex length even
return num.replace(/^0x0/, '0x')
}
}
+/**
+ * Zero pads a hex string if str.length !== 2 + length * 2. Pads to length * 2.
+ *
+ * @param str Hex string to pad
+ * @param length Half the length of the desired padded hex string
+ * @return Hex string with length of 2 + length * 2
+ */
export const padHexString = (str: string, length: number): string => {
if (str.length === 2 + length * 2) {
return str
@@ -79,9 +93,25 @@ export const padHexString = (str: string, length: number): string => {
}
}
-export const encodeHex = (val: any, len: number) =>
+/**
+ * Casts an input to hex string without '0x' prefix with conditional padding.
+ * Hex string will always start with a 0.
+ *
+ * @param val Input to cast to a hex string.
+ * @param len Desired length to pad hex string. Ignored if less than hex string length.
+ * @return Hex string with '0' prefix
+ */
+export const encodeHex = (val: any, len: number): string =>
remove0x(BigNumber.from(val).toHexString()).padStart(len, '0')
+/**
+ * Case insensitive hex string equality check
+ *
+ * @param stringA Hex string A
+ * @param stringB Hex string B
+ * @throws {Error} Inputs must be valid hex strings
+ * @return True if equal
+ */
export const hexStringEquals = (stringA: string, stringB: string): boolean => {
if (!ethers.utils.isHexString(stringA)) {
throw new Error(`input is not a hex string: ${stringA}`)
@@ -94,6 +124,12 @@ export const hexStringEquals = (stringA: string, stringB: string): boolean => {
return stringA.toLowerCase() === stringB.toLowerCase()
}
+/**
+ * Casts a number to a 32-byte, zero padded hex string.
+ *
+ * @param value Number to cast to a hex string.
+ * @return Number cast as a hex string.
+ */
export const bytes32ify = (value: number | BigNumber): string => {
return hexZeroPad(BigNumber.from(value).toHexString(), 32)
}
diff --git a/packages/core-utils/test/alias.spec.ts b/packages/core-utils/test/alias.spec.ts
index ec648ea08f34..d3d2ced10d89 100644
--- a/packages/core-utils/test/alias.spec.ts
+++ b/packages/core-utils/test/alias.spec.ts
@@ -18,7 +18,7 @@ describe('address aliasing utils', () => {
it('should throw if the input is not a valid address', () => {
expect(() => {
applyL1ToL2Alias('0x1234')
- }).to.throw
+ }).to.throw('not a valid address: 0x1234')
})
})
@@ -38,7 +38,7 @@ describe('address aliasing utils', () => {
it('should throw if the input is not a valid address', () => {
expect(() => {
undoL1ToL2Alias('0x1234')
- }).to.throw
+ }).to.throw('not a valid address: 0x1234')
})
})
})
diff --git a/packages/core-utils/test/hex-utils.spec.ts b/packages/core-utils/test/hex-utils.spec.ts
index 9d14a1e15961..ecac90f22c98 100644
--- a/packages/core-utils/test/hex-utils.spec.ts
+++ b/packages/core-utils/test/hex-utils.spec.ts
@@ -9,6 +9,9 @@ import {
fromHexString,
toHexString,
padHexString,
+ encodeHex,
+ hexStringEquals,
+ bytes32ify,
} from '../src'
describe('remove0x', () => {
@@ -52,13 +55,17 @@ describe('add0x', () => {
})
describe('toHexString', () => {
- it('should return undefined', () => {
- expect(add0x(undefined)).to.deep.equal(undefined)
+ it('should throw an error when input is null', () => {
+ expect(() => {
+ toHexString(null)
+ }).to.throw(
+ 'The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received null'
+ )
})
-
it('should return with a hex string', () => {
const cases = [
{ input: 0, output: '0x00' },
+ { input: 48, output: '0x30' },
{
input: '0',
output: '0x30',
@@ -122,3 +129,184 @@ describe('toRpcHexString', () => {
}
})
})
+
+describe('encodeHex', () => {
+ it('should throw an error when val is invalid', () => {
+ expect(() => {
+ encodeHex(null, 0)
+ }).to.throw('invalid BigNumber value')
+
+ expect(() => {
+ encodeHex(10.5, 0)
+ }).to.throw('fault="underflow", operation="BigNumber.from", value=10.5')
+
+ expect(() => {
+ encodeHex('10.5', 0)
+ }).to.throw('invalid BigNumber string')
+ })
+
+ it('should return a hex string of val with length len', () => {
+ const cases = [
+ {
+ input: {
+ val: 0,
+ len: 0,
+ },
+ output: '00',
+ },
+ {
+ input: {
+ val: 0,
+ len: 4,
+ },
+ output: '0000',
+ },
+ {
+ input: {
+ val: 1,
+ len: 0,
+ },
+ output: '01',
+ },
+ {
+ input: {
+ val: 1,
+ len: 10,
+ },
+ output: '0000000001',
+ },
+ {
+ input: {
+ val: 100,
+ len: 4,
+ },
+ output: '0064',
+ },
+ {
+ input: {
+ val: '100',
+ len: 0,
+ },
+ output: '64',
+ },
+ ]
+ for (const test of cases) {
+ expect(encodeHex(test.input.val, test.input.len)).to.deep.equal(
+ test.output
+ )
+ }
+ })
+})
+
+describe('hexStringEquals', () => {
+ it('should throw an error when input is not a hex string', () => {
+ expect(() => {
+ hexStringEquals('', '')
+ }).to.throw('input is not a hex string: ')
+
+ expect(() => {
+ hexStringEquals('0xx', '0x1')
+ }).to.throw('input is not a hex string: 0xx')
+
+ expect(() => {
+ hexStringEquals('0x1', '2')
+ }).to.throw('input is not a hex string: 2')
+
+ expect(() => {
+ hexStringEquals('-0x1', '0x1')
+ }).to.throw('input is not a hex string: -0x1')
+ })
+
+ it('should return the hex strings equality', () => {
+ const cases = [
+ {
+ input: {
+ stringA: '0x',
+ stringB: '0x',
+ },
+ output: true,
+ },
+ {
+ input: {
+ stringA: '0x1',
+ stringB: '0x1',
+ },
+ output: true,
+ },
+ {
+ input: {
+ stringA: '0x064',
+ stringB: '0x064',
+ },
+ output: true,
+ },
+ {
+ input: {
+ stringA: '0x',
+ stringB: '0x0',
+ },
+ output: false,
+ },
+ {
+ input: {
+ stringA: '0x0',
+ stringB: '0x1',
+ },
+ output: false,
+ },
+ {
+ input: {
+ stringA: '0x64',
+ stringB: '0x064',
+ },
+ output: false,
+ },
+ ]
+ for (const test of cases) {
+ expect(
+ hexStringEquals(test.input.stringA, test.input.stringB)
+ ).to.deep.equal(test.output)
+ }
+ })
+})
+
+describe('bytes32ify', () => {
+ it('should throw an error when input is invalid', () => {
+ expect(() => {
+ bytes32ify(-1)
+ }).to.throw('invalid hex string')
+ })
+
+ it('should return a zero padded, 32 bytes hex string', () => {
+ const cases = [
+ {
+ input: 0,
+ output:
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ },
+ {
+ input: BigNumber.from(0),
+ output:
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ },
+ {
+ input: 2,
+ output:
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
+ },
+ {
+ input: BigNumber.from(2),
+ output:
+ '0x0000000000000000000000000000000000000000000000000000000000000002',
+ },
+ {
+ input: 100,
+ output:
+ '0x0000000000000000000000000000000000000000000000000000000000000064',
+ },
+ ]
+ for (const test of cases) {
+ expect(bytes32ify(test.input)).to.deep.equal(test.output)
+ }
+ })
+})
diff --git a/packages/data-transport-layer/CHANGELOG.md b/packages/data-transport-layer/CHANGELOG.md
index 84f9dd8f6b44..aa31f422f03f 100644
--- a/packages/data-transport-layer/CHANGELOG.md
+++ b/packages/data-transport-layer/CHANGELOG.md
@@ -1,5 +1,13 @@
# data transport layer
+## 0.5.13
+
+### Patch Changes
+
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+ - @eth-optimism/contracts@0.5.10
+
## 0.5.12
### Patch Changes
diff --git a/packages/data-transport-layer/package.json b/packages/data-transport-layer/package.json
index 249e54e7daf9..0095fc1c65cd 100644
--- a/packages/data-transport-layer/package.json
+++ b/packages/data-transport-layer/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@eth-optimism/data-transport-layer",
- "version": "0.5.12",
+ "version": "0.5.13",
"description": "[Optimism] Service for shuttling data from L1 into L2",
"main": "dist/index",
"types": "dist/index",
@@ -37,8 +37,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
- "@eth-optimism/contracts": "0.5.9",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/contracts": "0.5.10",
+ "@eth-optimism/core-utils": "0.7.5",
"@ethersproject/providers": "^5.4.5",
"@ethersproject/transactions": "^5.4.0",
"@sentry/node": "^6.3.1",
diff --git a/packages/message-relayer/CHANGELOG.md b/packages/message-relayer/CHANGELOG.md
index d6a02e69cbf2..878370084a9d 100644
--- a/packages/message-relayer/CHANGELOG.md
+++ b/packages/message-relayer/CHANGELOG.md
@@ -1,5 +1,13 @@
# @eth-optimism/message-relayer
+## 0.2.14
+
+### Patch Changes
+
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+ - @eth-optimism/contracts@0.5.10
+
## 0.2.13
### Patch Changes
diff --git a/packages/message-relayer/package.json b/packages/message-relayer/package.json
index cb7997de7c40..e443227e3824 100644
--- a/packages/message-relayer/package.json
+++ b/packages/message-relayer/package.json
@@ -1,6 +1,6 @@
{
"name": "@eth-optimism/message-relayer",
- "version": "0.2.13",
+ "version": "0.2.14",
"description": "[Optimism] Service for automatically relaying L2 to L1 transactions",
"main": "dist/index",
"types": "dist/index",
@@ -35,8 +35,8 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
- "@eth-optimism/contracts": "0.5.9",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/contracts": "0.5.10",
+ "@eth-optimism/core-utils": "0.7.5",
"@sentry/node": "^6.3.1",
"bcfg": "^0.1.6",
"dotenv": "^10.0.0",
diff --git a/packages/regenesis-surgery/package.json b/packages/regenesis-surgery/package.json
index 258d94bc91b2..6991f58ea48b 100644
--- a/packages/regenesis-surgery/package.json
+++ b/packages/regenesis-surgery/package.json
@@ -32,7 +32,7 @@
},
"devDependencies": {
"@discoveryjs/json-ext": "^0.5.3",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/core-utils": "0.7.5",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abi": "^5.5.0",
"@ethersproject/bignumber": "^5.5.0",
diff --git a/packages/replica-healthcheck/CHANGELOG.md b/packages/replica-healthcheck/CHANGELOG.md
index dafb7425d08e..1402abfd4bf0 100644
--- a/packages/replica-healthcheck/CHANGELOG.md
+++ b/packages/replica-healthcheck/CHANGELOG.md
@@ -1,5 +1,12 @@
# @eth-optimism/replica-healthcheck
+## 0.3.5
+
+### Patch Changes
+
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+
## 0.3.4
### Patch Changes
diff --git a/packages/replica-healthcheck/package.json b/packages/replica-healthcheck/package.json
index 06c41a3d8322..5c5bef1681c4 100644
--- a/packages/replica-healthcheck/package.json
+++ b/packages/replica-healthcheck/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@eth-optimism/replica-healthcheck",
- "version": "0.3.4",
+ "version": "0.3.5",
"description": "[Optimism] Service for monitoring the health of replica nodes",
"main": "dist/index",
"types": "dist/index",
@@ -33,7 +33,7 @@
},
"dependencies": {
"@eth-optimism/common-ts": "0.2.1",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/core-utils": "0.7.5",
"dotenv": "^10.0.0",
"ethers": "^5.4.5",
"express": "^4.17.1",
diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md
index 31d24cf9ff95..eccde0405b67 100644
--- a/packages/sdk/CHANGELOG.md
+++ b/packages/sdk/CHANGELOG.md
@@ -1,5 +1,13 @@
# @eth-optimism/sdk
+## 0.0.6
+
+### Patch Changes
+
+- Updated dependencies [ad94b9d1]
+ - @eth-optimism/core-utils@0.7.5
+ - @eth-optimism/contracts@0.5.10
+
## 0.0.5
### Patch Changes
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 1e4cc7228f9c..14b8589c54db 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@eth-optimism/sdk",
- "version": "0.0.5",
+ "version": "0.0.6",
"description": "[Optimism] Tools for working with Optimism",
"main": "dist/index",
"types": "dist/index",
@@ -59,8 +59,8 @@
"typescript": "^4.3.5"
},
"dependencies": {
- "@eth-optimism/contracts": "0.5.9",
- "@eth-optimism/core-utils": "0.7.4",
+ "@eth-optimism/contracts": "0.5.10",
+ "@eth-optimism/core-utils": "0.7.5",
"@ethersproject/abstract-provider": "^5.5.1",
"@ethersproject/abstract-signer": "^5.5.0",
"ethers": "^5.5.2"
diff --git a/packages/sdk/src/cross-chain-messenger.ts b/packages/sdk/src/cross-chain-messenger.ts
new file mode 100644
index 000000000000..0745f81c2d0f
--- /dev/null
+++ b/packages/sdk/src/cross-chain-messenger.ts
@@ -0,0 +1,263 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { Overrides, Signer, BigNumber } from 'ethers'
+import {
+ TransactionRequest,
+ TransactionResponse,
+} from '@ethersproject/abstract-provider'
+import { predeploys } from '@eth-optimism/contracts'
+
+import {
+ CrossChainMessageRequest,
+ ICrossChainMessenger,
+ ICrossChainProvider,
+ MessageLike,
+ NumberLike,
+ MessageDirection,
+} from './interfaces'
+import { omit } from './utils'
+
+export class CrossChainMessenger implements ICrossChainMessenger {
+ provider: ICrossChainProvider
+ l1Signer: Signer
+ l2Signer: Signer
+
+ /**
+ * Creates a new CrossChainMessenger instance.
+ *
+ * @param opts Options for the messenger.
+ * @param opts.provider CrossChainProvider to use to send messages.
+ * @param opts.l1Signer Signer to use to send messages on L1.
+ * @param opts.l2Signer Signer to use to send messages on L2.
+ */
+ constructor(opts: {
+ provider: ICrossChainProvider
+ l1Signer: Signer
+ l2Signer: Signer
+ }) {
+ this.provider = opts.provider
+ this.l1Signer = opts.l1Signer
+ this.l2Signer = opts.l2Signer
+ }
+
+ public async sendMessage(
+ message: CrossChainMessageRequest,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise {
+ const tx = await this.populateTransaction.sendMessage(message, opts)
+ if (message.direction === MessageDirection.L1_TO_L2) {
+ return this.l1Signer.sendTransaction(tx)
+ } else {
+ return this.l2Signer.sendTransaction(tx)
+ }
+ }
+
+ public async resendMessage(
+ message: MessageLike,
+ messageGasLimit: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise {
+ return this.l1Signer.sendTransaction(
+ await this.populateTransaction.resendMessage(
+ message,
+ messageGasLimit,
+ opts
+ )
+ )
+ }
+
+ public async finalizeMessage(
+ message: MessageLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise {
+ throw new Error('Not implemented')
+ }
+
+ public async depositETH(
+ amount: NumberLike,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise {
+ return this.l1Signer.sendTransaction(
+ await this.populateTransaction.depositETH(amount, opts)
+ )
+ }
+
+ public async withdrawETH(
+ amount: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise {
+ return this.l2Signer.sendTransaction(
+ await this.populateTransaction.withdrawETH(amount, opts)
+ )
+ }
+
+ populateTransaction = {
+ sendMessage: async (
+ message: CrossChainMessageRequest,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise => {
+ if (message.direction === MessageDirection.L1_TO_L2) {
+ return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
+ this.l1Signer
+ ).populateTransaction.sendMessage(
+ message.target,
+ message.message,
+ opts?.l2GasLimit ||
+ (await this.provider.estimateL2MessageGasLimit(message)),
+ omit(opts?.overrides || {}, 'l2GasLimit')
+ )
+ } else {
+ return this.provider.contracts.l2.L2CrossDomainMessenger.connect(
+ this.l2Signer
+ ).populateTransaction.sendMessage(
+ message.target,
+ message.message,
+ 0, // Gas limit goes unused when sending from L2 to L1
+ omit(opts?.overrides || {}, 'l2GasLimit')
+ )
+ }
+ },
+
+ resendMessage: async (
+ message: MessageLike,
+ messageGasLimit: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ const resolved = await this.provider.toCrossChainMessage(message)
+ if (resolved.direction === MessageDirection.L2_TO_L1) {
+ throw new Error(`cannot resend L2 to L1 message`)
+ }
+
+ return this.provider.contracts.l1.L1CrossDomainMessenger.connect(
+ this.l1Signer
+ ).populateTransaction.replayMessage(
+ resolved.target,
+ resolved.sender,
+ resolved.message,
+ resolved.messageNonce,
+ resolved.gasLimit,
+ messageGasLimit,
+ opts?.overrides || {}
+ )
+ },
+
+ finalizeMessage: async (
+ message: MessageLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ throw new Error('Not implemented')
+ },
+
+ depositETH: async (
+ amount: NumberLike,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise => {
+ return this.provider.contracts.l1.L1StandardBridge.populateTransaction.depositETH(
+ opts?.l2GasLimit || 200000, // 200k gas is fine as a default
+ '0x', // No data
+ {
+ ...omit(opts?.overrides || {}, 'l2GasLimit', 'value'),
+ value: amount,
+ }
+ )
+ },
+
+ withdrawETH: async (
+ amount: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ return this.provider.contracts.l2.L2StandardBridge.populateTransaction.withdraw(
+ predeploys.OVM_ETH,
+ amount,
+ 0, // No need to supply gas here
+ '0x', // No data,
+ opts?.overrides || {}
+ )
+ },
+ }
+
+ estimateGas = {
+ sendMessage: async (
+ message: CrossChainMessageRequest,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise => {
+ const tx = await this.populateTransaction.sendMessage(message, opts)
+ if (message.direction === MessageDirection.L1_TO_L2) {
+ return this.provider.l1Provider.estimateGas(tx)
+ } else {
+ return this.provider.l2Provider.estimateGas(tx)
+ }
+ },
+
+ resendMessage: async (
+ message: MessageLike,
+ messageGasLimit: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ const tx = await this.populateTransaction.resendMessage(
+ message,
+ messageGasLimit,
+ opts
+ )
+ return this.provider.l1Provider.estimateGas(tx)
+ },
+
+ finalizeMessage: async (
+ message: MessageLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ throw new Error('Not implemented')
+ },
+
+ depositETH: async (
+ amount: NumberLike,
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise => {
+ const tx = await this.populateTransaction.depositETH(amount, opts)
+ return this.provider.l1Provider.estimateGas(tx)
+ },
+
+ withdrawETH: async (
+ amount: NumberLike,
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise => {
+ const tx = await this.populateTransaction.withdrawETH(amount, opts)
+ return this.provider.l2Provider.estimateGas(tx)
+ },
+ }
+}
diff --git a/packages/sdk/src/cross-chain-provider.ts b/packages/sdk/src/cross-chain-provider.ts
index 89af6e36083f..412418637545 100644
--- a/packages/sdk/src/cross-chain-provider.ts
+++ b/packages/sdk/src/cross-chain-provider.ts
@@ -12,11 +12,13 @@ import {
OEContracts,
OEContractsLike,
MessageLike,
+ MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
ProviderLike,
CrossChainMessage,
+ CrossChainMessageRequest,
MessageDirection,
MessageStatus,
TokenBridgeMessage,
@@ -41,7 +43,6 @@ export class CrossChainProvider implements ICrossChainProvider {
public l1Provider: Provider
public l2Provider: Provider
public l1ChainId: number
- public l1BlockTime: number
public contracts: OEContracts
public bridges: CustomBridges
@@ -52,7 +53,6 @@ export class CrossChainProvider implements ICrossChainProvider {
* @param opts.l1Provider Provider for the L1 chain, or a JSON-RPC url.
* @param opts.l2Provider Provider for the L2 chain, or a JSON-RPC url.
* @param opts.l1ChainId Chain ID for the L1 chain.
- * @param opts.l1BlockTime Optional L1 block time in seconds. Defaults to 15 seconds.
* @param opts.contracts Optional contract address overrides.
* @param opts.bridges Optional bridge address list.
*/
@@ -60,16 +60,12 @@ export class CrossChainProvider implements ICrossChainProvider {
l1Provider: ProviderLike
l2Provider: ProviderLike
l1ChainId: NumberLike
- l1BlockTime?: NumberLike
contracts?: DeepPartial
bridges?: Partial
}) {
this.l1Provider = toProvider(opts.l1Provider)
this.l2Provider = toProvider(opts.l2Provider)
this.l1ChainId = toBigNumber(opts.l1ChainId).toNumber()
- this.l1BlockTime = opts.l1BlockTime
- ? toBigNumber(opts.l1ChainId).toNumber()
- : 15
this.contracts = getAllOEContracts(this.l1ChainId, {
l1SignerOrProvider: this.l1Provider,
l2SignerOrProvider: this.l2Provider,
@@ -138,6 +134,7 @@ export class CrossChainProvider implements ICrossChainProvider {
sender: parsed.args.sender,
message: parsed.args.message,
messageNonce: parsed.args.messageNonce,
+ gasLimit: parsed.args.gasLimit,
logIndex: log.logIndex,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
@@ -364,9 +361,12 @@ export class CrossChainProvider implements ICrossChainProvider {
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
} else {
- const challengePeriod = await this.getChallengePeriodBlocks()
- const latestBlock = await this.l1Provider.getBlockNumber()
- if (stateRoot.blockNumber + challengePeriod > latestBlock) {
+ const challengePeriod = await this.getChallengePeriodSeconds()
+ const targetBlock = await this.l1Provider.getBlock(
+ stateRoot.blockNumber
+ )
+ const latestBlock = await this.l1Provider.getBlock('latest')
+ if (targetBlock.timestamp + challengePeriod > latestBlock.timestamp) {
return MessageStatus.IN_CHALLENGE_PERIOD
} else {
return MessageStatus.READY_FOR_RELAY
@@ -464,12 +464,21 @@ export class CrossChainProvider implements ICrossChainProvider {
}
public async estimateL2MessageGasLimit(
- message: MessageLike,
+ message: MessageRequestLike,
opts?: {
bufferPercent?: number
+ from?: string
}
): Promise {
- const resolved = await this.toCrossChainMessage(message)
+ let resolved: CrossChainMessage | CrossChainMessageRequest
+ let from: string
+ if ((message as CrossChainMessage).messageNonce === undefined) {
+ resolved = message as CrossChainMessageRequest
+ from = opts?.from
+ } else {
+ resolved = await this.toCrossChainMessage(message as MessageLike)
+ from = opts?.from || (resolved as CrossChainMessage).sender
+ }
// L2 message gas estimation is only used for L1 => L2 messages.
if (resolved.direction === MessageDirection.L2_TO_L1) {
@@ -477,7 +486,7 @@ export class CrossChainProvider implements ICrossChainProvider {
}
const estimate = await this.l2Provider.estimateGas({
- from: resolved.sender,
+ from,
to: resolved.target,
data: resolved.message,
})
@@ -493,24 +502,12 @@ export class CrossChainProvider implements ICrossChainProvider {
throw new Error('Not implemented')
}
- public async estimateMessageWaitTimeBlocks(
- message: MessageLike
- ): Promise {
- throw new Error('Not implemented')
- }
-
public async getChallengePeriodSeconds(): Promise {
const challengePeriod =
await this.contracts.l1.StateCommitmentChain.FRAUD_PROOF_WINDOW()
return challengePeriod.toNumber()
}
- public async getChallengePeriodBlocks(): Promise {
- return Math.ceil(
- (await this.getChallengePeriodSeconds()) / this.l1BlockTime
- )
- }
-
public async getMessageStateRoot(
message: MessageLike
): Promise {
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index f57d02c762aa..022bb13cefd4 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -1,3 +1,4 @@
export * from './interfaces'
export * from './utils'
export * from './cross-chain-provider'
+export * from './cross-chain-messenger'
diff --git a/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts b/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts
index afc6ae041062..ef1412a34538 100644
--- a/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts
+++ b/packages/sdk/src/interfaces/cross-chain-erc20-pair.ts
@@ -4,7 +4,7 @@ import {
TransactionResponse,
} from '@ethersproject/abstract-provider'
-import { NumberLike, L1ToL2Overrides } from './types'
+import { NumberLike } from './types'
import { ICrossChainMessenger } from './cross-chain-messenger'
/**
@@ -30,24 +30,32 @@ export interface ICrossChainERC20Pair {
* Deposits some tokens into the L2 chain.
*
* @param amount Amount of the token to deposit.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
*/
deposit(
amount: NumberLike,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
* Withdraws some tokens back to the L1 chain.
*
* @param amount Amount of the token to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
withdraw(
amount: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
@@ -59,24 +67,32 @@ export interface ICrossChainERC20Pair {
* Generates a transaction for depositing some tokens into the L2 chain.
*
* @param amount Amount of the token to deposit.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
deposit(
amount: NumberLike,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
* Generates a transaction for withdrawing some tokens back to the L1 chain.
*
* @param amount Amount of the token to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdraw(
amount: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
}
@@ -89,24 +105,32 @@ export interface ICrossChainERC20Pair {
* Estimates gas required to deposit some tokens into the L2 chain.
*
* @param amount Amount of the token to deposit.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the tokens.
*/
deposit(
amount: NumberLike,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
* Estimates gas required to withdraw some tokens back to the L1 chain.
*
* @param amount Amount of the token to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdraw(
amount: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
}
}
diff --git a/packages/sdk/src/interfaces/cross-chain-messenger.ts b/packages/sdk/src/interfaces/cross-chain-messenger.ts
index 9a1ce80bfe80..7865590c6a50 100644
--- a/packages/sdk/src/interfaces/cross-chain-messenger.ts
+++ b/packages/sdk/src/interfaces/cross-chain-messenger.ts
@@ -1,15 +1,10 @@
-import { Overrides, Signer } from 'ethers'
+import { Overrides, Signer, BigNumber } from 'ethers'
import {
TransactionRequest,
TransactionResponse,
} from '@ethersproject/abstract-provider'
-import {
- MessageLike,
- NumberLike,
- CrossChainMessageRequest,
- L1ToL2Overrides,
-} from './types'
+import { MessageLike, NumberLike, CrossChainMessageRequest } from './types'
import { ICrossChainProvider } from './cross-chain-provider'
/**
@@ -22,21 +17,31 @@ export interface ICrossChainMessenger {
provider: ICrossChainProvider
/**
- * Signer that will carry out L1/L2 transactions.
+ * Signer that will carry out L1 transactions.
*/
- signer: Signer
+ l1Signer: Signer
+
+ /**
+ * Signer that will carry out L2 transactions.
+ */
+ l2Signer: Signer
/**
* Sends a given cross chain message. Where the message is sent depends on the direction attached
* to the message itself.
*
* @param message Cross chain message to send.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message sending transaction.
*/
sendMessage(
message: CrossChainMessageRequest,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
@@ -45,13 +50,16 @@ export interface ICrossChainMessenger {
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the message resending transaction.
*/
resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
@@ -59,36 +67,47 @@ export interface ICrossChainMessenger {
* messages. Will throw an error if the message has not completed its challenge period yet.
*
* @param message Message to finalize.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the finalization transaction.
*/
finalizeMessage(
message: MessageLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
* Deposits some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit (in wei).
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the deposit transaction.
*/
depositETH(
amount: NumberLike,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
* Withdraws some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction response for the withdraw transaction.
*/
withdrawETH(
amount: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
@@ -101,13 +120,18 @@ export interface ICrossChainMessenger {
* and executed by a signer.
*
* @param message Cross chain message to send.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to send the message.
*/
sendMessage: (
message: CrossChainMessageRequest,
- overrides?: L1ToL2Overrides
- ) => Promise
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ) => Promise
/**
* Generates a transaction that resends a given cross chain message. Only applies to L1 to L2
@@ -115,13 +139,16 @@ export interface ICrossChainMessenger {
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to resend the message.
*/
resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
@@ -130,36 +157,47 @@ export interface ICrossChainMessenger {
* its challenge period yet.
*
* @param message Message to generate the finalization transaction for.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to finalize the message.
*/
finalizeMessage(
message: MessageLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
/**
* Generates a transaction for depositing some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the ETH.
*/
depositETH(
amount: NumberLike,
- overrides?: L1ToL2Overrides
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
): Promise
/**
* Generates a transaction for withdrawing some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawETH(
amount: NumberLike,
- overrides?: Overrides
+ opts?: {
+ overrides?: Overrides
+ }
): Promise
}
@@ -172,62 +210,81 @@ export interface ICrossChainMessenger {
* Estimates gas required to send a cross chain message.
*
* @param message Cross chain message to send.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to send the message.
*/
sendMessage: (
message: CrossChainMessageRequest,
- overrides?: L1ToL2Overrides
- ) => Promise
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ) => Promise
/**
* Estimates gas required to resend a cross chain message. Only applies to L1 to L2 messages.
*
* @param message Cross chain message to resend.
* @param messageGasLimit New gas limit to use for the message.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to resend the message.
*/
resendMessage(
message: MessageLike,
messageGasLimit: NumberLike,
- overrides?: Overrides
- ): Promise
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise
/**
* Estimates gas required to finalize a cross chain message. Only applies to L2 to L1 messages.
*
* @param message Message to generate the finalization transaction for.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to finalize the message.
*/
finalizeMessage(
message: MessageLike,
- overrides?: Overrides
- ): Promise
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise
/**
* Estimates gas required to deposit some ETH into the L2 chain.
*
* @param amount Amount of ETH to deposit.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.l2GasLimit Optional gas limit to use for the transaction on L2.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to deposit the ETH.
*/
depositETH(
amount: NumberLike,
- overrides?: L1ToL2Overrides
- ): Promise
+ opts?: {
+ l2GasLimit?: NumberLike
+ overrides?: Overrides
+ }
+ ): Promise
/**
* Estimates gas required to withdraw some ETH back to the L1 chain.
*
* @param amount Amount of ETH to withdraw.
- * @param overrides Optional transaction overrides.
+ * @param opts Additional options.
+ * @param opts.overrides Optional transaction overrides.
* @returns Transaction that can be signed and executed to withdraw the tokens.
*/
withdrawETH(
amount: NumberLike,
- overrides?: Overrides
- ): Promise
+ opts?: {
+ overrides?: Overrides
+ }
+ ): Promise
}
}
diff --git a/packages/sdk/src/interfaces/cross-chain-provider.ts b/packages/sdk/src/interfaces/cross-chain-provider.ts
index 0a2199d5f10d..981a79c09f5e 100644
--- a/packages/sdk/src/interfaces/cross-chain-provider.ts
+++ b/packages/sdk/src/interfaces/cross-chain-provider.ts
@@ -3,6 +3,7 @@ import { Provider, BlockTag } from '@ethersproject/abstract-provider'
import {
MessageLike,
+ MessageRequestLike,
TransactionLike,
AddressLike,
NumberLike,
@@ -205,12 +206,14 @@ export interface ICrossChainProvider {
* @param message Message get a gas estimate for.
* @param opts Options object.
* @param opts.bufferPercent Percentage of gas to add to the estimate. Defaults to 20.
+ * @param opts.from Address to use as the sender.
* @returns Estimates L2 gas limit.
*/
estimateL2MessageGasLimit(
- message: MessageLike,
+ message: MessageRequestLike,
opts?: {
bufferPercent?: number
+ from?: string
}
): Promise
@@ -225,17 +228,6 @@ export interface ICrossChainProvider {
*/
estimateMessageWaitTimeSeconds(message: MessageLike): Promise
- /**
- * Returns the estimated amount of time before the message can be executed (in L1 blocks).
- * When this is a message being sent to L1, this will return the estimated time until the message
- * will complete its challenge period. When this is a message being sent to L2, this will return
- * the estimated amount of time until the message will be picked up and executed on L2.
- *
- * @param message Message to estimate the time remaining for.
- * @returns Estimated amount of time remaining (in blocks) before the message can be executed.
- */
- estimateMessageWaitTimeBlocks(message: MessageLike): Promise
-
/**
* Queries the current challenge period in seconds from the StateCommitmentChain.
*
@@ -243,14 +235,6 @@ export interface ICrossChainProvider {
*/
getChallengePeriodSeconds(): Promise
- /**
- * Queries the current challenge period in blocks from the StateCommitmentChain. Estimation is
- * based on the challenge period in seconds divided by the L1 block time.
- *
- * @returns Current challenge period in blocks.
- */
- getChallengePeriodBlocks(): Promise
-
/**
* Returns the state root that corresponds to a given message. This is the state root for the
* block in which the transaction was included, as published to the StateCommitmentChain. If the
diff --git a/packages/sdk/src/interfaces/types.ts b/packages/sdk/src/interfaces/types.ts
index 6fd97b241bc1..31dbcbd028c5 100644
--- a/packages/sdk/src/interfaces/types.ts
+++ b/packages/sdk/src/interfaces/types.ts
@@ -4,7 +4,7 @@ import {
TransactionResponse,
} from '@ethersproject/abstract-provider'
import { Signer } from '@ethersproject/abstract-signer'
-import { Contract, BigNumber, Overrides } from 'ethers'
+import { Contract, BigNumber } from 'ethers'
/**
* L1 contract references.
@@ -143,7 +143,6 @@ export interface CrossChainMessageRequest {
direction: MessageDirection
target: string
message: string
- l2GasLimit: NumberLike
}
/**
@@ -162,6 +161,7 @@ export interface CoreCrossChainMessage {
*/
export interface CrossChainMessage extends CoreCrossChainMessage {
direction: MessageDirection
+ gasLimit: number
logIndex: number
blockNumber: number
transactionHash: string
@@ -229,28 +229,28 @@ export interface StateRootBatch {
stateRoots: string[]
}
-/**
- * Extended Ethers overrides object with an l2GasLimit field.
- * Only meant to be used for L1 to L2 messages, since L2 to L1 messages don't have a specified gas
- * limit field (gas used depends on the amount of gas provided).
- */
-export type L1ToL2Overrides = Overrides & {
- l2GasLimit: NumberLike
-}
-
/**
* Stuff that can be coerced into a transaction.
*/
export type TransactionLike = string | TransactionReceipt | TransactionResponse
/**
- * Stuff that can be coerced into a message.
+ * Stuff that can be coerced into a CrossChainMessage.
*/
export type MessageLike =
| CrossChainMessage
| TransactionLike
| TokenBridgeMessage
+/**
+ * Stuff that can be coerced into a CrossChainMessageRequest.
+ */
+export type MessageRequestLike =
+ | CrossChainMessageRequest
+ | CrossChainMessage
+ | TransactionLike
+ | TokenBridgeMessage
+
/**
* Stuff that can be coerced into a provider.
*/
diff --git a/packages/sdk/test/contracts/MockBridge.sol b/packages/sdk/test/contracts/MockBridge.sol
index 3fb00fa707d9..fd46d11537ca 100644
--- a/packages/sdk/test/contracts/MockBridge.sol
+++ b/packages/sdk/test/contracts/MockBridge.sol
@@ -3,6 +3,13 @@ pragma solidity ^0.8.9;
import { MockMessenger } from "./MockMessenger.sol";
contract MockBridge {
+ event ETHDepositInitiated(
+ address indexed _from,
+ address indexed _to,
+ uint256 _amount,
+ bytes _data
+ );
+
event ERC20DepositInitiated(
address indexed _l1Token,
address indexed _l2Token,
@@ -110,4 +117,38 @@ contract MockBridge {
) public {
emit DepositFailed(_params.l1Token, _params.l2Token, _params.from, _params.to, _params.amount, _params.data);
}
+
+ function depositETH(
+ uint32 _l2GasLimit,
+ bytes memory _data
+ )
+ public
+ payable
+ {
+ emit ETHDepositInitiated(
+ msg.sender,
+ msg.sender,
+ msg.value,
+ _data
+ );
+ }
+
+ function withdraw(
+ address _l2Token,
+ uint256 _amount,
+ uint32 _l1Gas,
+ bytes calldata _data
+ )
+ public
+ payable
+ {
+ emit WithdrawalInitiated(
+ address(0),
+ _l2Token,
+ msg.sender,
+ msg.sender,
+ _amount,
+ _data
+ );
+ }
}
diff --git a/packages/sdk/test/contracts/MockMessenger.sol b/packages/sdk/test/contracts/MockMessenger.sol
index b09dc738bbc4..c23360f427b7 100644
--- a/packages/sdk/test/contracts/MockMessenger.sol
+++ b/packages/sdk/test/contracts/MockMessenger.sol
@@ -7,13 +7,40 @@ contract MockMessenger is ICrossDomainMessenger {
return address(0);
}
+ uint256 public nonce;
+
// Empty function to satisfy the interface.
function sendMessage(
address _target,
bytes calldata _message,
uint32 _gasLimit
) public {
- return;
+ emit SentMessage(
+ _target,
+ msg.sender,
+ _message,
+ nonce,
+ _gasLimit
+ );
+ nonce++;
+ }
+
+ function replayMessage(
+ address _target,
+ address _sender,
+ bytes calldata _message,
+ uint256 _queueIndex,
+ uint32 _oldGasLimit,
+ uint32 _newGasLimit
+ ) public {
+ emit SentMessage(
+ _target,
+ _sender,
+ _message,
+ nonce,
+ _newGasLimit
+ );
+ nonce++;
}
struct SentMessageEventParams {
diff --git a/packages/sdk/test/cross-chain-messenger.spec.ts b/packages/sdk/test/cross-chain-messenger.spec.ts
index 78d6fa9837c9..8d389eb8a6ca 100644
--- a/packages/sdk/test/cross-chain-messenger.spec.ts
+++ b/packages/sdk/test/cross-chain-messenger.spec.ts
@@ -1,23 +1,195 @@
-import './setup'
+import { Contract } from 'ethers'
+import { ethers } from 'hardhat'
+import { predeploys } from '@eth-optimism/contracts'
+
+import { expect } from './setup'
+import {
+ CrossChainProvider,
+ CrossChainMessenger,
+ MessageDirection,
+} from '../src'
describe('CrossChainMessenger', () => {
+ let l1Signer: any
+ let l2Signer: any
+ before(async () => {
+ ;[l1Signer, l2Signer] = await ethers.getSigners()
+ })
+
describe('sendMessage', () => {
- describe('when no l2GasLimit is provided', () => {
- it('should send a message with an estimated l2GasLimit')
+ let l1Messenger: Contract
+ let l2Messenger: Contract
+ let provider: CrossChainProvider
+ let messenger: CrossChainMessenger
+ beforeEach(async () => {
+ l1Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+ l2Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+
+ provider = new CrossChainProvider({
+ l1Provider: ethers.provider,
+ l2Provider: ethers.provider,
+ l1ChainId: 31337,
+ contracts: {
+ l1: {
+ L1CrossDomainMessenger: l1Messenger.address,
+ },
+ l2: {
+ L2CrossDomainMessenger: l2Messenger.address,
+ },
+ },
+ })
+
+ messenger = new CrossChainMessenger({
+ provider,
+ l1Signer,
+ l2Signer,
+ })
+ })
+
+ describe('when the message is an L1 to L2 message', () => {
+ describe('when no l2GasLimit is provided', () => {
+ it('should send a message with an estimated l2GasLimit', async () => {
+ const message = {
+ direction: MessageDirection.L1_TO_L2,
+ target: '0x' + '11'.repeat(20),
+ message: '0x' + '22'.repeat(32),
+ }
+
+ const estimate = await provider.estimateL2MessageGasLimit(message)
+ await expect(messenger.sendMessage(message))
+ .to.emit(l1Messenger, 'SentMessage')
+ .withArgs(
+ message.target,
+ await l1Signer.getAddress(),
+ message.message,
+ 0,
+ estimate
+ )
+ })
+ })
+
+ describe('when an l2GasLimit is provided', () => {
+ it('should send a message with the provided l2GasLimit', async () => {
+ const message = {
+ direction: MessageDirection.L1_TO_L2,
+ target: '0x' + '11'.repeat(20),
+ message: '0x' + '22'.repeat(32),
+ }
+
+ await expect(
+ messenger.sendMessage(message, {
+ l2GasLimit: 1234,
+ })
+ )
+ .to.emit(l1Messenger, 'SentMessage')
+ .withArgs(
+ message.target,
+ await l1Signer.getAddress(),
+ message.message,
+ 0,
+ 1234
+ )
+ })
+ })
})
- describe('when an l2GasLimit is provided', () => {
- it('should send a message with the provided l2GasLimit')
+ describe('when the message is an L2 to L1 message', () => {
+ it('should send a message', async () => {
+ const message = {
+ direction: MessageDirection.L2_TO_L1,
+ target: '0x' + '11'.repeat(20),
+ message: '0x' + '22'.repeat(32),
+ }
+
+ await expect(messenger.sendMessage(message))
+ .to.emit(l2Messenger, 'SentMessage')
+ .withArgs(
+ message.target,
+ await l2Signer.getAddress(),
+ message.message,
+ 0,
+ 0
+ )
+ })
})
})
describe('resendMessage', () => {
- describe('when the message being resent exists', () => {
- it('should resend the message with the new gas limit')
+ let l1Messenger: Contract
+ let l2Messenger: Contract
+ let provider: CrossChainProvider
+ let messenger: CrossChainMessenger
+ beforeEach(async () => {
+ l1Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+ l2Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+
+ provider = new CrossChainProvider({
+ l1Provider: ethers.provider,
+ l2Provider: ethers.provider,
+ l1ChainId: 31337,
+ contracts: {
+ l1: {
+ L1CrossDomainMessenger: l1Messenger.address,
+ },
+ l2: {
+ L2CrossDomainMessenger: l2Messenger.address,
+ },
+ },
+ })
+
+ messenger = new CrossChainMessenger({
+ provider,
+ l1Signer,
+ l2Signer,
+ })
})
- describe('when the message being resent does not exist', () => {
- it('should throw an error')
+ describe('when resending an L1 to L2 message', () => {
+ it('should resend the message with the new gas limit', async () => {
+ const message = {
+ direction: MessageDirection.L1_TO_L2,
+ target: '0x' + '11'.repeat(20),
+ message: '0x' + '22'.repeat(32),
+ }
+
+ const sent = await messenger.sendMessage(message, {
+ l2GasLimit: 1234,
+ })
+
+ await expect(messenger.resendMessage(sent, 10000))
+ .to.emit(l1Messenger, 'SentMessage')
+ .withArgs(
+ message.target,
+ await l1Signer.getAddress(),
+ message.message,
+ 1, // nonce is now 1
+ 10000
+ )
+ })
+ })
+
+ describe('when resending an L2 to L1 message', () => {
+ it('should throw an error', async () => {
+ const message = {
+ direction: MessageDirection.L2_TO_L1,
+ target: '0x' + '11'.repeat(20),
+ message: '0x' + '22'.repeat(32),
+ }
+
+ const sent = await messenger.sendMessage(message, {
+ l2GasLimit: 1234,
+ })
+
+ await expect(messenger.resendMessage(sent, 10000)).to.be.rejected
+ })
})
})
@@ -40,4 +212,94 @@ describe('CrossChainMessenger', () => {
it('should throw an error')
})
})
+
+ describe('depositETH', () => {
+ let l1Messenger: Contract
+ let l1Bridge: Contract
+ let provider: CrossChainProvider
+ let messenger: CrossChainMessenger
+ beforeEach(async () => {
+ l1Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+ l1Bridge = (await (
+ await ethers.getContractFactory('MockBridge')
+ ).deploy(l1Messenger.address)) as any
+
+ provider = new CrossChainProvider({
+ l1Provider: ethers.provider,
+ l2Provider: ethers.provider,
+ l1ChainId: 31337,
+ contracts: {
+ l1: {
+ L1CrossDomainMessenger: l1Messenger.address,
+ L1StandardBridge: l1Bridge.address,
+ },
+ },
+ })
+
+ messenger = new CrossChainMessenger({
+ provider,
+ l1Signer,
+ l2Signer,
+ })
+ })
+
+ it('should trigger the deposit ETH function with the given amount', async () => {
+ await expect(messenger.depositETH(100000))
+ .to.emit(l1Bridge, 'ETHDepositInitiated')
+ .withArgs(
+ await l1Signer.getAddress(),
+ await l1Signer.getAddress(),
+ 100000,
+ '0x'
+ )
+ })
+ })
+
+ describe('withdrawETH', () => {
+ let l2Messenger: Contract
+ let l2Bridge: Contract
+ let provider: CrossChainProvider
+ let messenger: CrossChainMessenger
+ beforeEach(async () => {
+ l2Messenger = (await (
+ await ethers.getContractFactory('MockMessenger')
+ ).deploy()) as any
+ l2Bridge = (await (
+ await ethers.getContractFactory('MockBridge')
+ ).deploy(l2Messenger.address)) as any
+
+ provider = new CrossChainProvider({
+ l1Provider: ethers.provider,
+ l2Provider: ethers.provider,
+ l1ChainId: 31337,
+ contracts: {
+ l2: {
+ L2CrossDomainMessenger: l2Messenger.address,
+ L2StandardBridge: l2Bridge.address,
+ },
+ },
+ })
+
+ messenger = new CrossChainMessenger({
+ provider,
+ l1Signer,
+ l2Signer,
+ })
+ })
+
+ it('should trigger the deposit ETH function with the given amount', async () => {
+ await expect(messenger.withdrawETH(100000))
+ .to.emit(l2Bridge, 'WithdrawalInitiated')
+ .withArgs(
+ ethers.constants.AddressZero,
+ predeploys.OVM_ETH,
+ await l2Signer.getAddress(),
+ await l2Signer.getAddress(),
+ 100000,
+ '0x'
+ )
+ })
+ })
})
diff --git a/packages/sdk/test/cross-chain-provider.spec.ts b/packages/sdk/test/cross-chain-provider.spec.ts
index e05ad825ca67..e005f2694af8 100644
--- a/packages/sdk/test/cross-chain-provider.spec.ts
+++ b/packages/sdk/test/cross-chain-provider.spec.ts
@@ -270,6 +270,7 @@ describe('CrossChainProvider', () => {
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
+ gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
@@ -321,6 +322,7 @@ describe('CrossChainProvider', () => {
target: message.target,
message: message.message,
messageNonce: ethers.BigNumber.from(message.messageNonce),
+ gasLimit: ethers.BigNumber.from(message.gasLimit),
logIndex: i,
blockNumber: tx.blockNumber,
transactionHash: tx.hash,
@@ -685,6 +687,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -898,10 +901,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
- const challengePeriod = await provider.getChallengePeriodBlocks()
- for (let x = 0; x < challengePeriod + 1; x++) {
- await ethers.provider.send('evm_mine', [])
- }
+ const challengePeriod = await provider.getChallengePeriodSeconds()
+ ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
+ ethers.provider.send('evm_mine', [])
await l1Messenger.triggerRelayedMessageEvents([
hashCrossChainMessage(message),
@@ -921,10 +923,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
- const challengePeriod = await provider.getChallengePeriodBlocks()
- for (let x = 0; x < challengePeriod + 1; x++) {
- await ethers.provider.send('evm_mine', [])
- }
+ const challengePeriod = await provider.getChallengePeriodSeconds()
+ ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
+ ethers.provider.send('evm_mine', [])
await l1Messenger.triggerFailedRelayedMessageEvents([
hashCrossChainMessage(message),
@@ -944,10 +945,9 @@ describe('CrossChainProvider', () => {
await submitStateRootBatchForMessage(message)
- const challengePeriod = await provider.getChallengePeriodBlocks()
- for (let x = 0; x < challengePeriod + 1; x++) {
- await ethers.provider.send('evm_mine', [])
- }
+ const challengePeriod = await provider.getChallengePeriodSeconds()
+ ethers.provider.send('evm_increaseTime', [challengePeriod + 1])
+ ethers.provider.send('evm_mine', [])
expect(await provider.getMessageStatus(message)).to.equal(
MessageStatus.READY_FOR_RELAY
@@ -1009,6 +1009,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1039,6 +1040,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1069,6 +1071,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1104,6 +1107,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1148,6 +1152,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1179,6 +1184,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),
@@ -1211,6 +1217,7 @@ describe('CrossChainProvider', () => {
sender: '0x' + '22'.repeat(20),
message: '0x' + '33'.repeat(64),
messageNonce: 1234,
+ gasLimit: 0,
logIndex: 0,
blockNumber: 1234,
transactionHash: '0x' + '44'.repeat(32),