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),