diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..5ace4600a1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index 61c4e85bd7..c0b21ab156 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -14,11 +14,19 @@ jobs: try-to-bump: if: contains(github.event.pull_request.labels.*.name, 'bump-version') runs-on: ubuntu-latest + permissions: + # Give the default GITHUB_TOKEN write permission to commit and push the + # added or changed files to the repository. + contents: write + steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} + # note: this is needed by git-auto-commit-action below + persist-credentials: true + - name: check diff id: check_diff run: | @@ -27,7 +35,7 @@ jobs: # fetch develop branch so that we can diff against later git fetch origin develop - echo 'checking verion changes in diff...' + echo 'checking version changes in diff...' # check if version changed in version.go # note: the grep will fail if use \d instead of [0-9] @@ -45,17 +53,19 @@ jobs: echo '> yes' echo "result=bump" >> "$GITHUB_OUTPUT" fi + - name: Install Node.js 16 if: steps.check_diff.outputs.result == 'bump' - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16 + - name: bump version in params/version.go if: steps.check_diff.outputs.result == 'bump' run: node .github/scripts/bump_version_dot_go.mjs # Commits made by this Action do not trigger new Workflow runs - - uses: stefanzweifel/git-auto-commit-action@3ea6ae190baf489ba007f7c92608f33ce20ef04a + - uses: stefanzweifel/git-auto-commit-action@e348103e9026cc0eee72ae06630dbe30c8bf7a79 # v5.1.0 if: steps.check_diff.outputs.result == 'bump' with: skip_fetch: true # already did fetch in check diff diff --git a/.github/workflows/docker-arm64.yaml b/.github/workflows/docker-arm64.yaml index 5e68adfb05..00127188fa 100644 --- a/.github/workflows/docker-arm64.yaml +++ b/.github/workflows/docker-arm64.yaml @@ -11,6 +11,8 @@ on: jobs: build-and-push-arm64-image: runs-on: ubuntu-latest + permissions: {} + strategy: matrix: arch: @@ -18,21 +20,29 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up QEMU run: | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker buildx create --name multiarch --driver docker-container --use + - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + with: + cache-binary: false + - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: platforms: linux/arm64 context: . diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 64c415b6f3..c76bdda76a 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -10,16 +10,25 @@ on: jobs: build-and-push: runs-on: ubuntu-latest + permissions: {} + steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + with: + cache-binary: false + - name: Extract docker metadata id: meta - uses: docker/metadata-action@v3 + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 with: images: scrolltech/l2geth tags: | @@ -27,13 +36,15 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} flavor: | latest=false + - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . file: Dockerfile diff --git a/.github/workflows/l2geth_ci.yml b/.github/workflows/l2geth_ci.yml index d196128a69..da1f73c058 100644 --- a/.github/workflows/l2geth_ci.yml +++ b/.github/workflows/l2geth_ci.yml @@ -1,3 +1,5 @@ +name: CI + on: push: branches: # we keep this to avoid triggering `push` & `pull_request` every time we update a PR @@ -11,104 +13,144 @@ on: - reopened - synchronize - ready_for_review -name: CI + jobs: build-mock-ccc-geth: # build geth with mock circuit capacity checker if: github.event.pull_request.draft == false runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.21.x + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Build run: | make nccc_geth + build-geth: # build geth with circuit capacity checker if: github.event_name == 'push' # will only be triggered when pushing to main & staging & develop & alpha runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.21.x + - name: Install rust - uses: actions-rs/toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@9399c7bb15d4c7d47b27263d024f0a4978346ba4 # v1.11.0 with: toolchain: nightly-2023-12-03 override: true components: rustfmt, clippy + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Build run: | make libzkp sudo cp ./rollup/ccc/libzkp/libzkp.so /usr/local/lib/ make geth + check: if: github.event.pull_request.draft == false runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.21.x + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Lint run: | rm -rf $HOME/.cache/golangci-lint make lint + goimports-lint: if: github.event.pull_request.draft == false runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.x + - name: Install goimports run: go install golang.org/x/tools/cmd/goimports@v0.24.0 + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - run: goimports -local github.com/scroll-tech/go-ethereum/ -w . + # If there are any diffs from goimports, fail. - name: Verify no changes from goimports run: | if [ -n "$(git status --porcelain)" ]; then exit 1 fi + go-mod-tidy-lint: if: github.event.pull_request.draft == false runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.21.x + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - run: go mod tidy + # If there are any diffs from go mod tidy, fail. - name: Verify no changes from go mod tidy run: | if [ -n "$(git status --porcelain)" ]; then exit 1 fi + test: if: github.event.pull_request.draft == false runs-on: ubuntu-latest + permissions: {} steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.21.x + - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Test run: | make test + - name: Upload coverage report run: bash <(curl -s https://codecov.io/bash) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 6d7663154a..8b3f139d47 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -14,11 +14,14 @@ name: Semgrep jobs: semgrep: name: semgrep/ci - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 + permissions: {} env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} container: image: returntocorp/semgrep steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + persist-credentials: false - run: semgrep ci diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..689f2e66ca --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,34 @@ +name: zizmor GA Security Analysis + +on: + push: + branches: ["develop"] + pull_request: + branches: ["**"] + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1 + + - name: Run zizmor + run: uvx zizmor --format sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif + category: zizmor diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 393129a3ac..922d10648e 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -152,6 +152,15 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, if chainConfig.CurieBlock != nil && chainConfig.CurieBlock.Cmp(new(big.Int).SetUint64(pre.Env.Number)) == 0 { misc.ApplyCurieHardFork(statedb) } + // Apply EIP-2935 + if pre.Env.BlockHashes != nil && chainConfig.IsFeynman(pre.Env.Timestamp) { + var ( + prevNumber = pre.Env.Number - 1 + prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)] + evm = vm.NewEVM(vmContext, vm.TxContext{}, statedb, chainConfig, vmConfig) + ) + core.ProcessParentBlockHash(prevHash, evm, statedb) + } for i, tx := range txs { msg, err := tx.AsMessage(signer, pre.Env.BaseFee) diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 1a1535a875..d7a8002f61 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -140,15 +140,29 @@ func Transaction(ctx *cli.Context) error { r.Address = sender } // Check intrinsic gas - if gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, - chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int))); err != nil { + rules := chainConfig.Rules(common.Big0, 0) + gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + if err != nil { r.Error = err results = append(results, r) continue - } else { - r.IntrinsicGas = gas - if tx.Gas() < gas { - r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), gas) + } + r.IntrinsicGas = gas + if tx.Gas() < gas { + r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrIntrinsicGas, tx.Gas(), gas) + results = append(results, r) + continue + } + // For Feynman txs, validate the floor data gas. + if rules.IsFeynman { + floorDataGas, err := core.FloorDataGas(tx.Data()) + if err != nil { + r.Error = err + results = append(results, r) + continue + } + if tx.Gas() < floorDataGas { + r.Error = fmt.Errorf("%w: have %d, want %d", core.ErrFloorDataGas, tx.Gas(), floorDataGas) results = append(results, r) continue } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index b1b1e92642..c1fbb08746 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -29,6 +29,7 @@ import ( "github.com/scroll-tech/go-ethereum/accounts/keystore" "github.com/scroll-tech/go-ethereum/cmd/utils" "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/consensus/misc" "github.com/scroll-tech/go-ethereum/console/prompt" "github.com/scroll-tech/go-ethereum/eth" "github.com/scroll-tech/go-ethereum/eth/downloader" @@ -133,6 +134,7 @@ var ( utils.MinerNoVerifyFlag, utils.MinerStoreSkippedTxTracesFlag, utils.MinerMaxAccountsNumFlag, + utils.MinerAllowEmptyFlag, utils.NATFlag, utils.NoDiscoverFlag, utils.DiscoveryV5Flag, @@ -452,8 +454,9 @@ func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend) { utils.Fatalf("Ethereum service not running") } // Set the gas price to the limits from the CLI and start mining - gasprice := utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name) - ethBackend.TxPool().SetGasPrice(gasprice) + // gasprice := utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name) + // ethBackend.TxPool().SetGasPrice(gasprice) + ethBackend.TxPool().SetGasPrice(misc.MinBaseFee()) // override configured min gas price ethBackend.TxPool().SetIsMiner(true) // start mining threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name) diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 0e1fce891a..1d299d339f 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -161,6 +161,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.RPCGlobalEVMTimeoutFlag, utils.RPCGlobalTxFeeCapFlag, utils.AllowUnprotectedTxs, + utils.MaxBlockRangeFlag, utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag, @@ -197,6 +198,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.MinerNoVerifyFlag, utils.MinerStoreSkippedTxTracesFlag, utils.MinerMaxAccountsNumFlag, + utils.MinerAllowEmptyFlag, }, }, { @@ -226,6 +228,28 @@ var AppHelpFlagGroups = []flags.FlagGroup{ Name: "METRICS AND STATS", Flags: metricsFlags, }, + { + Name: "ROLLUP", + Flags: []cli.Flag{ + utils.L1EndpointFlag, + utils.L1ConfirmationsFlag, + utils.L1DeploymentBlockFlag, + utils.L1DisableMessageQueueV2Flag, + utils.RollupVerifyEnabledFlag, + utils.DASyncEnabledFlag, + utils.DABlobScanAPIEndpointFlag, + utils.DABlockNativeAPIEndpointFlag, + utils.DABeaconNodeAPIEndpointFlag, + utils.DARecoveryModeFlag, + utils.DARecoveryInitialL1BlockFlag, + utils.DARecoveryInitialBatchFlag, + utils.DARecoverySignBlocksFlag, + utils.DARecoveryL2EndBlockFlag, + utils.DARecoveryProduceBlocksFlag, + utils.CircuitCapacityCheckEnabledFlag, + utils.CircuitCapacityCheckWorkersFlag, + }, + }, { Name: "ALIASED (deprecated)", Flags: []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 8d8bdf6773..7371a37700 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -515,6 +515,10 @@ var ( Usage: "Maximum number of accounts that miner will fetch the pending transactions of when building a new block", Value: math.MaxInt, } + MinerAllowEmptyFlag = cli.BoolFlag{ + Name: "miner.allowempty", + Usage: "Allow sealing empty blocks", + } // Account settings UnlockedAccountFlag = cli.StringFlag{ Name: "unlock", @@ -1620,6 +1624,9 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { if ctx.GlobalIsSet(MinerMaxAccountsNumFlag.Name) { cfg.MaxAccountsNum = ctx.GlobalInt(MinerMaxAccountsNumFlag.Name) } + if ctx.GlobalIsSet(MinerAllowEmptyFlag.Name) { + cfg.AllowEmpty = ctx.GlobalBool(MinerAllowEmptyFlag.Name) + } if ctx.GlobalIsSet(LegacyMinerGasTargetFlag.Name) { log.Warn("The generic --miner.gastarget flag is deprecated and will be removed in the future!") } diff --git a/common/bytes.go b/common/bytes.go index 7827bb572e..ae45aa18c6 100644 --- a/common/bytes.go +++ b/common/bytes.go @@ -19,6 +19,9 @@ package common import ( "encoding/hex" + "errors" + + "github.com/scroll-tech/go-ethereum/common/hexutil" ) // FromHex returns the bytes represented by the hexadecimal string s. @@ -92,6 +95,15 @@ func Hex2BytesFixed(str string, flen int) []byte { return hh } +// ParseHexOrString tries to hexdecode b, but if the prefix is missing, it instead just returns the raw bytes +func ParseHexOrString(str string) ([]byte, error) { + b, err := hexutil.Decode(str) + if errors.Is(err, hexutil.ErrMissingPrefix) { + return []byte(str), nil + } + return b, err +} + // RightPadBytes zero-pads slice to the right up to length l. func RightPadBytes(slice []byte, l int) []byte { if l <= len(slice) { diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 68fadec813..8a4c8bda4a 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -226,7 +226,7 @@ func (c *Clique) VerifyHeader(chain consensus.ChainHeaderReader, header *types.H // VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers. The // method returns a quit channel to abort the operations and a results channel to // retrieve the async verifications (the order is that of the input slice). -func (c *Clique) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { +func (c *Clique) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool, parent *types.Header) (chan<- struct{}, <-chan error) { abort := make(chan struct{}) results := make(chan error, len(headers)) diff --git a/consensus/consensus.go b/consensus/consensus.go index 6416460684..5ed80dff4d 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -71,7 +71,7 @@ type Engine interface { // concurrently. The method returns a quit channel to abort the operations and // a results channel to retrieve the async verifications (the order is that of // the input slice). - VerifyHeaders(chain ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) + VerifyHeaders(chain ChainHeaderReader, headers []*types.Header, seals []bool, parent *types.Header) (chan<- struct{}, <-chan error) // VerifyUncles verifies that the given block's uncles conform to the consensus // rules of a given engine. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index a24ec37f67..d90cf613f3 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -119,7 +119,7 @@ func (ethash *Ethash) VerifyHeader(chain consensus.ChainHeaderReader, header *ty // VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers // concurrently. The method returns a quit channel to abort the operations and // a results channel to retrieve the async verifications. -func (ethash *Ethash) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { +func (ethash *Ethash) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool, parent *types.Header) (chan<- struct{}, <-chan error) { // If we're running a full engine faking, accept any input as valid if ethash.config.PowMode == ModeFullFake || len(headers) == 0 { abort, results := make(chan struct{}), make(chan error, len(headers)) diff --git a/consensus/misc/eip1559.go b/consensus/misc/eip1559.go index 483878fc0e..9990df04a7 100644 --- a/consensus/misc/eip1559.go +++ b/consensus/misc/eip1559.go @@ -19,14 +19,66 @@ package misc import ( "fmt" "math/big" + "sync" + "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/rcfg" + "github.com/scroll-tech/go-ethereum/rpc" ) -// Protocol-enforced maximum L2 base fee. -// We would only go above this if L1 base fee hits 2931 Gwei. -const MaximumL2BaseFee = 10000000000 +const ( + // Protocol-enforced maximum L2 base fee. + // We would only go above this if L1 base fee hits 2931 Gwei. + MaximumL2BaseFee = 10000000000 + + // L2 base fee fallback values, in case the L2 system contract + // is not deployed on not configured yet. + DefaultBaseFeeOverhead = 15680000 + DefaultBaseFeeScalar = 34000000000000 +) + +// L2 base fee formula constants and defaults. +// l2BaseFee = (l1BaseFee * scalar) / PRECISION + overhead. +// `scalar` accounts for finalization costs. `overhead` accounts for sequencing and proving costs. +var ( + // We use 1e18 for precision to match the contract implementation. + BaseFeePrecision = new(big.Int).SetUint64(1e18) + + // scalar and overhead are updated automatically in `Blockchain.writeBlockWithState`. + baseFeeScalar = big.NewInt(0) + baseFeeOverhead = big.NewInt(0) + + lock sync.RWMutex +) + +func ReadL2BaseFeeCoefficients() (scalar *big.Int, overhead *big.Int) { + lock.RLock() + defer lock.RUnlock() + return new(big.Int).Set(baseFeeScalar), new(big.Int).Set(baseFeeOverhead) +} + +func UpdateL2BaseFeeOverhead(newOverhead *big.Int) { + if newOverhead == nil { + log.Error("Failed to set L2 base fee overhead, new value is ") + return + } + lock.Lock() + defer lock.Unlock() + baseFeeOverhead.Set(newOverhead) +} + +func UpdateL2BaseFeeScalar(newScalar *big.Int) { + if newScalar == nil { + log.Error("Failed to set L2 base fee scalar, new value is ") + return + } + lock.Lock() + defer lock.Unlock() + baseFeeScalar.Set(newScalar) +} // VerifyEip1559Header verifies some header attributes which were changed in EIP-1559, // - gas limit check @@ -54,18 +106,22 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseF if config.Clique != nil && config.Clique.ShadowForkHeight != 0 && parent.Number.Uint64() >= config.Clique.ShadowForkHeight { return big.NewInt(10000000) // 0.01 Gwei } - l2SequencerFee := big.NewInt(1000000) // 0.001 Gwei - provingFee := big.NewInt(38200000) // 0.0382 Gwei - // L1_base_fee * 0.00017 - verificationFee := parentL1BaseFee - verificationFee = new(big.Int).Mul(verificationFee, big.NewInt(17)) - verificationFee = new(big.Int).Div(verificationFee, big.NewInt(100000)) + scalar, overhead := ReadL2BaseFeeCoefficients() + return calcBaseFee(scalar, overhead, parentL1BaseFee) +} - baseFee := big.NewInt(0) - baseFee.Add(baseFee, l2SequencerFee) - baseFee.Add(baseFee, provingFee) - baseFee.Add(baseFee, verificationFee) +// MinBaseFee calculates the minimum L2 base fee based on the current coefficients. +func MinBaseFee() *big.Int { + scalar, overhead := ReadL2BaseFeeCoefficients() + return calcBaseFee(scalar, overhead, big.NewInt(0)) +} + +func calcBaseFee(scalar, overhead, parentL1BaseFee *big.Int) *big.Int { + baseFee := new(big.Int).Set(parentL1BaseFee) + baseFee.Mul(baseFee, scalar) + baseFee.Div(baseFee, BaseFeePrecision) + baseFee.Add(baseFee, overhead) if baseFee.Cmp(big.NewInt(MaximumL2BaseFee)) > 0 { baseFee = big.NewInt(MaximumL2BaseFee) @@ -73,3 +129,60 @@ func CalcBaseFee(config *params.ChainConfig, parent *types.Header, parentL1BaseF return baseFee } + +type State interface { + GetState(addr common.Address, hash common.Hash) common.Hash +} + +func InitializeL2BaseFeeCoefficients(chainConfig *params.ChainConfig, state State) error { + overhead := common.Big0 + scalar := common.Big0 + + if l2SystemConfig := chainConfig.Scroll.L2SystemConfigAddress(); l2SystemConfig != (common.Address{}) { + overhead = state.GetState(l2SystemConfig, rcfg.L2BaseFeeOverheadSlot).Big() + scalar = state.GetState(l2SystemConfig, rcfg.L2BaseFeeScalarSlot).Big() + } else { + log.Warn("L2SystemConfig address is not configured") + } + + // fallback to default if contract is not deployed or configured yet + if overhead.Cmp(common.Big0) == 0 { + overhead = big.NewInt(DefaultBaseFeeOverhead) + } + if scalar.Cmp(common.Big0) == 0 { + scalar = big.NewInt(DefaultBaseFeeScalar) + } + + // update local view of coefficients + lock.Lock() + defer lock.Unlock() + baseFeeOverhead.Set(overhead) + baseFeeScalar.Set(scalar) + log.Info("Initialized L2 base fee coefficients", "overhead", overhead, "scalar", scalar) + return nil +} + +type API struct{} + +type L2BaseFeeConfig struct { + Scalar *big.Int `json:"scalar,omitempty"` + Overhead *big.Int `json:"overhead,omitempty"` +} + +func (api *API) GetL2BaseFeeConfig() *L2BaseFeeConfig { + scalar, overhead := ReadL2BaseFeeCoefficients() + + return &L2BaseFeeConfig{ + Scalar: scalar, + Overhead: overhead, + } +} + +func APIs() []rpc.API { + return []rpc.API{{ + Namespace: "scroll", + Version: "1.0", + Service: &API{}, + Public: false, + }} +} diff --git a/consensus/misc/eip1559_test.go b/consensus/misc/eip1559_test.go index 3b48c2e581..d41d3e5966 100644 --- a/consensus/misc/eip1559_test.go +++ b/consensus/misc/eip1559_test.go @@ -112,17 +112,55 @@ func TestCalcBaseFee(t *testing.T) { parentL1BaseFee int64 expectedL2BaseFee int64 }{ - {0, 39200000}, - {1000000000, 39370000}, - {2000000000, 39540000}, - {100000000000, 56200000}, - {111111111111, 58088888}, - {2164000000000, 407080000}, - {58592942000000, 10000000000}, // cap at max L2 base fee + {0, 1}, + {1000000000, 1}, + {2000000000, 1}, + {100000000000, 2}, + {111111111111, 2}, + {2164000000000, 22}, + {644149677419355, 6442}, } for i, test := range tests { + config := config() + UpdateL2BaseFeeScalar(big.NewInt(10000000)) + UpdateL2BaseFeeOverhead(big.NewInt(1)) + if have, want := CalcBaseFee(config, nil, big.NewInt(test.parentL1BaseFee)), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 { + t.Errorf("test %d: have %d want %d, ", i, have, want) + } + } + + testsWithDefaults := []struct { + parentL1BaseFee int64 + expectedL2BaseFee int64 + }{ + {0, 15680000}, + {1000000000, 15714000}, + {2000000000, 15748000}, + {100000000000, 19080000}, + {111111111111, 19457777}, + {2164000000000, 89256000}, + {644149677419355, 10000000000}, // cap at max L2 base fee + } + for i, test := range testsWithDefaults { + UpdateL2BaseFeeScalar(big.NewInt(34000000000000)) + UpdateL2BaseFeeOverhead(big.NewInt(15680000)) if have, want := CalcBaseFee(config(), nil, big.NewInt(test.parentL1BaseFee)), big.NewInt(test.expectedL2BaseFee); have.Cmp(want) != 0 { t.Errorf("test %d: have %d want %d, ", i, have, want) } } } + +// TestMinBaseFee assumes all blocks are 1559-blocks +func TestMinBaseFee(t *testing.T) { + UpdateL2BaseFeeScalar(big.NewInt(34000000000000)) + UpdateL2BaseFeeOverhead(big.NewInt(15680000)) + if have, want := MinBaseFee(), big.NewInt(15680000); have.Cmp(want) != 0 { + t.Errorf("have %d want %d, ", have, want) + } + + UpdateL2BaseFeeScalar(big.NewInt(10000000)) + UpdateL2BaseFeeOverhead(big.NewInt(1)) + if have, want := MinBaseFee(), big.NewInt(1); have.Cmp(want) != 0 { + t.Errorf("have %d want %d, ", have, want) + } +} diff --git a/consensus/system_contract/consensus.go b/consensus/system_contract/consensus.go index 797297870d..01d7be1850 100644 --- a/consensus/system_contract/consensus.go +++ b/consensus/system_contract/consensus.go @@ -86,13 +86,18 @@ func (s *SystemContract) VerifyHeader(chain consensus.ChainHeaderReader, header // concurrently. The method returns a quit channel to abort the operations and // a results channel to retrieve the async verifications (the order is that of // the input slice). -func (s *SystemContract) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { +func (s *SystemContract) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool, parent *types.Header) (chan<- struct{}, <-chan error) { abort := make(chan struct{}) results := make(chan error, len(headers)) go func() { for i, header := range headers { - err := s.verifyHeader(chain, header, headers[:i]) + parents := headers[:i] + if len(parents) == 0 && parent != nil { + parents = []*types.Header{parent} + } + + err := s.verifyHeader(chain, header, parents) if err != nil { log.Error("Error verifying headers", "err", err) } @@ -130,7 +135,7 @@ func (s *SystemContract) verifyHeader(chain consensus.ChainHeaderReader, header return errInvalidNonce } // Check that the BlockSignature contains signature if block is not requested - if !header.Requested && header.Number.Cmp(big.NewInt(0)) != 0 && len(header.BlockSignature) != extraSeal { + if header.Number.Cmp(big.NewInt(0)) != 0 && len(header.BlockSignature) != extraSeal { return errMissingSignature } // Ensure that the mix digest is zero @@ -197,20 +202,17 @@ func (s *SystemContract) verifyCascadingFields(chain consensus.ChainHeaderReader return err } - // only if block header has NOT been requested, then verify the signature against the current signer - if !header.Requested { - signer, err := ecrecover(header) - if err != nil { - return err - } + signer, err := ecrecover(header) + if err != nil { + return err + } - s.lock.RLock() - defer s.lock.RUnlock() + s.lock.RLock() + defer s.lock.RUnlock() - if signer != s.signerAddressL1 { - log.Error("Unauthorized signer", "Got", signer, "Expected:", s.signerAddressL1) - return ErrUnauthorizedSigner - } + if signer != s.signerAddressL1 { + log.Error("Unauthorized signer", "Got", signer, "Expected:", s.signerAddressL1) + return ErrUnauthorizedSigner } return nil diff --git a/consensus/system_contract/system_contract.go b/consensus/system_contract/system_contract.go index 8e99c9cfa3..aef24a00ba 100644 --- a/consensus/system_contract/system_contract.go +++ b/consensus/system_contract/system_contract.go @@ -3,10 +3,13 @@ package system_contract import ( "context" "fmt" + "math/big" "sync" "time" + "github.com/scroll-tech/go-ethereum" "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rollup/sync_service" @@ -48,8 +51,9 @@ func New(ctx context.Context, config *params.SystemContractConfig, client sync_s } if err := s.fetchAddressFromL1(); err != nil { - log.Error("failed to fetch signer address from L1", "err", err) + log.Error("Failed to fetch signer address from L1", "err", err) } + return s } @@ -80,7 +84,7 @@ func (s *SystemContract) Start() { return case <-syncTicker.C: if err := s.fetchAddressFromL1(); err != nil { - log.Error("failed to fetch signer address from L1", "err", err) + log.Error("Failed to fetch signer address from L1", "err", err) } } } @@ -94,16 +98,24 @@ func (s *SystemContract) fetchAddressFromL1() error { } bAddress := common.BytesToAddress(address) + s.lock.Lock() + defer s.lock.Unlock() + // Validate the address is not empty if bAddress == (common.Address{}) { - return fmt.Errorf("retrieved empty signer address from L1 System Contract: contract=%s, slot=%s", s.config.SystemContractAddress.Hex(), s.config.SystemContractSlot.Hex()) + log.Debug("Retrieved empty signer address from L1 System Contract", "contract", s.config.SystemContractAddress.Hex(), "slot", s.config.SystemContractSlot.Hex()) + + // Not initialized yet -- we don't consider this an error + if s.signerAddressL1 == (common.Address{}) { + log.Warn("System Contract signer address not initialized") + return nil + } + + return fmt.Errorf("retrieved empty signer address from L1 System Contract") } log.Debug("Read address from system contract", "address", bAddress.Hex()) - s.lock.Lock() - defer s.lock.Unlock() - if s.signerAddressL1 != bAddress { s.signerAddressL1 = bAddress log.Info("Updated new signer from L1 system contract", "signer", bAddress.Hex()) @@ -131,3 +143,53 @@ func (s *SystemContract) localSignerAddress() common.Address { return s.signer } + +// FakeEthClient implements a minimal version of sync_service.EthClient for testing purposes. +type FakeEthClient struct { + mu sync.Mutex + // Value is the fixed Value to return from StorageAt. + // We'll assume StorageAt returns a 32-byte Value representing an Ethereum address. + Value common.Address +} + +// BlockNumber returns 0. +func (f *FakeEthClient) BlockNumber(ctx context.Context) (uint64, error) { + return 0, nil +} + +// ChainID returns a zero-value chain ID. +func (f *FakeEthClient) ChainID(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} + +// FilterLogs returns an empty slice of logs. +func (f *FakeEthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { + return []types.Log{}, nil +} + +// HeaderByNumber returns nil. +func (f *FakeEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + return nil, nil +} + +// SubscribeFilterLogs returns a nil subscription. +func (f *FakeEthClient) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + return nil, nil +} + +// TransactionByHash returns (nil, false, nil). +func (f *FakeEthClient) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { + return nil, false, nil +} + +// BlockByHash returns nil. +func (f *FakeEthClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return nil, nil +} + +// StorageAt returns the byte representation of f.value. +func (f *FakeEthClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { + f.mu.Lock() + defer f.mu.Unlock() + return f.Value.Bytes(), nil +} diff --git a/consensus/system_contract/system_contract_test.go b/consensus/system_contract/system_contract_test.go index ddd6a0fc8d..fa344f5c9e 100644 --- a/consensus/system_contract/system_contract_test.go +++ b/consensus/system_contract/system_contract_test.go @@ -3,13 +3,11 @@ package system_contract import ( "context" "math/big" - "sync" "testing" "time" "github.com/stretchr/testify/require" - "github.com/scroll-tech/go-ethereum" "github.com/scroll-tech/go-ethereum/accounts" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/core/types" @@ -19,14 +17,14 @@ import ( "github.com/scroll-tech/go-ethereum/trie" ) -var _ sync_service.EthClient = &fakeEthClient{} +var _ sync_service.EthClient = &FakeEthClient{} func TestSystemContract_FetchSigner(t *testing.T) { log.Root().SetHandler(log.DiscardHandler()) expectedSigner := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") - fakeClient := &fakeEthClient{value: expectedSigner} + fakeClient := &FakeEthClient{Value: expectedSigner} config := ¶ms.SystemContractConfig{ SystemContractAddress: common.HexToAddress("0xFAKE"), @@ -55,7 +53,7 @@ func TestSystemContract_AuthorizeCheck(t *testing.T) { expectedSigner := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") - fakeClient := &fakeEthClient{value: expectedSigner} + fakeClient := &FakeEthClient{Value: expectedSigner} config := ¶ms.SystemContractConfig{ SystemContractAddress: common.HexToAddress("0xFAKE"), Period: 10, @@ -106,8 +104,8 @@ func TestSystemContract_SignsAfterUpdate(t *testing.T) { updatedSigner := common.HexToAddress("0x2222222222222222222222222222222222222222") // Create a fake client that starts by returning the wrong signer. - fakeClient := &fakeEthClient{ - value: oldSigner, + fakeClient := &FakeEthClient{ + Value: oldSigner, } config := ¶ms.SystemContractConfig{ @@ -130,7 +128,7 @@ func TestSystemContract_SignsAfterUpdate(t *testing.T) { // Now, simulate an update: change the fake client's returned value to updatedSigner. fakeClient.mu.Lock() - fakeClient.value = updatedSigner + fakeClient.Value = updatedSigner fakeClient.mu.Unlock() // fetch new value from L1 (simulating a background poll) @@ -171,53 +169,3 @@ func TestSystemContract_SignsAfterUpdate(t *testing.T) { t.Fatal("Timed out waiting for Seal to return a sealed block") } } - -// fakeEthClient implements a minimal version of sync_service.EthClient for testing purposes. -type fakeEthClient struct { - mu sync.Mutex - // value is the fixed value to return from StorageAt. - // We'll assume StorageAt returns a 32-byte value representing an Ethereum address. - value common.Address -} - -// BlockNumber returns 0. -func (f *fakeEthClient) BlockNumber(ctx context.Context) (uint64, error) { - return 0, nil -} - -// ChainID returns a zero-value chain ID. -func (f *fakeEthClient) ChainID(ctx context.Context) (*big.Int, error) { - return big.NewInt(0), nil -} - -// FilterLogs returns an empty slice of logs. -func (f *fakeEthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return []types.Log{}, nil -} - -// HeaderByNumber returns nil. -func (f *fakeEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - return nil, nil -} - -// SubscribeFilterLogs returns a nil subscription. -func (f *fakeEthClient) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { - return nil, nil -} - -// TransactionByHash returns (nil, false, nil). -func (f *fakeEthClient) TransactionByHash(ctx context.Context, txHash common.Hash) (*types.Transaction, bool, error) { - return nil, false, nil -} - -// BlockByHash returns nil. -func (f *fakeEthClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return nil, nil -} - -// StorageAt returns the byte representation of f.value. -func (f *fakeEthClient) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { - f.mu.Lock() - defer f.mu.Unlock() - return f.value.Bytes(), nil -} diff --git a/consensus/wrapper/consensus.go b/consensus/wrapper/consensus.go index 3c5aa5cb3e..16ea46f0d8 100644 --- a/consensus/wrapper/consensus.go +++ b/consensus/wrapper/consensus.go @@ -2,7 +2,6 @@ package wrapper import ( "math/big" - "time" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/consensus" @@ -61,7 +60,7 @@ func (ue *UpgradableEngine) VerifyHeader(chain consensus.ChainHeaderReader, head // VerifyHeaders verifies a batch of headers concurrently. In our use-case, // headers can only be all system, all clique, or start with clique and then switch once to system. -func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { +func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header, seals []bool, parent *types.Header) (chan<- struct{}, <-chan error) { abort := make(chan struct{}) results := make(chan error, len(headers)) @@ -77,12 +76,12 @@ func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, hea // If the first header is system, then all headers must be system. if firstEngine == ue.system { - return firstEngine.VerifyHeaders(chain, headers, seals) + return firstEngine.VerifyHeaders(chain, headers, seals, nil) } // If first and last headers are both clique, then all headers are clique. if firstEngine == lastEngine { - return firstEngine.VerifyHeaders(chain, headers, seals) + return firstEngine.VerifyHeaders(chain, headers, seals, nil) } // Otherwise, headers start as clique then switch to system. Since we assume @@ -110,7 +109,7 @@ func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, hea // Verify clique headers. log.Info("Start EuclidV2 transition verification in Clique section", "startBlockNumber", cliqueHeaders[0].Number, "endBlockNumber", cliqueHeaders[len(cliqueHeaders)-1].Number) - abortClique, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals) + abortClique, cliqueResults := ue.clique.VerifyHeaders(chain, cliqueHeaders, cliqueSeals, nil) // Note: cliqueResults is not closed so we cannot directly iterate over it for i := 0; i < len(cliqueHeaders); i++ { @@ -124,14 +123,13 @@ func (ue *UpgradableEngine) VerifyHeaders(chain consensus.ChainHeaderReader, hea } } - // Not sure why we need this here, but without this we get err="unknown ancestor" - // at the 1st Euclid block. It seems that `VerifyHeaders` start processing the next - // header before the previous one was written into `chain`. - time.Sleep(2 * time.Second) + // Since the Clique part of the header chain might not yet be stored in the local chain, + // provide a hint to the SystemContract consensus engine. + lastCliqueHeader := cliqueHeaders[len(cliqueHeaders)-1] // Verify system contract headers. log.Info("Start EuclidV2 transition verification in SystemContract section", "startBlockNumber", systemHeaders[0].Number, "endBlockNumber", systemHeaders[len(systemHeaders)-1].Number) - abortSystem, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals) + abortSystem, systemResults := ue.system.VerifyHeaders(chain, systemHeaders, systemSeals, lastCliqueHeader) // Note: systemResults is not closed so we cannot directly iterate over it for i := 0; i < len(systemHeaders); i++ { diff --git a/core/block_validator_test.go b/core/block_validator_test.go index c94132d001..12e599921c 100644 --- a/core/block_validator_test.go +++ b/core/block_validator_test.go @@ -51,10 +51,10 @@ func TestHeaderVerification(t *testing.T) { if valid { engine := ethash.NewFaker() - _, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]}, []bool{true}) + _, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]}, []bool{true}, nil) } else { engine := ethash.NewFakeFailer(headers[i].Number.Uint64()) - _, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]}, []bool{true}) + _, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]}, []bool{true}, nil) } // Wait for the verification result select { @@ -107,11 +107,11 @@ func testHeaderConcurrentVerification(t *testing.T, threads int) { if valid { chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, nil) - _, results = chain.engine.VerifyHeaders(chain, headers, seals) + _, results = chain.engine.VerifyHeaders(chain, headers, seals, nil) chain.Stop() } else { chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeFailer(uint64(len(headers)-1)), vm.Config{}, nil, nil) - _, results = chain.engine.VerifyHeaders(chain, headers, seals) + _, results = chain.engine.VerifyHeaders(chain, headers, seals, nil) chain.Stop() } // Wait for all the verification results @@ -176,7 +176,7 @@ func testHeaderConcurrentAbortion(t *testing.T, threads int) { chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeDelayer(time.Millisecond), vm.Config{}, nil, nil) defer chain.Stop() - abort, results := chain.engine.VerifyHeaders(chain, headers, seals) + abort, results := chain.engine.VerifyHeaders(chain, headers, seals, nil) close(abort) // Deplete the results channel diff --git a/core/blockchain.go b/core/blockchain.go index aa3d74197f..57a803198f 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -34,6 +34,7 @@ import ( "github.com/scroll-tech/go-ethereum/common/mclock" "github.com/scroll-tech/go-ethereum/common/prque" "github.com/scroll-tech/go-ethereum/consensus" + "github.com/scroll-tech/go-ethereum/consensus/misc" "github.com/scroll-tech/go-ethereum/core/rawdb" "github.com/scroll-tech/go-ethereum/core/state" "github.com/scroll-tech/go-ethereum/core/state/snapshot" @@ -45,6 +46,7 @@ import ( "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/metrics" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rollup/l2_system_config" "github.com/scroll-tech/go-ethereum/trie" ) @@ -53,8 +55,10 @@ var ( headHeaderGauge = metrics.NewRegisteredGauge("chain/head/header", nil) headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) headTimeGapGauge = metrics.NewRegisteredGauge("chain/head/timegap", nil) + headL1MessageGauge = metrics.NewRegisteredGauge("chain/head/l1msg", nil) - l2BaseFeeGauge = metrics.NewRegisteredGauge("chain/fees/l2basefee", nil) + l2BaseFeeGauge = metrics.NewRegisteredGauge("chain/fees/l2basefee", nil) + l2BaseFeeUpdateTimer = metrics.NewRegisteredTimer("chain/fees/updates", nil) accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil) accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) @@ -148,6 +152,51 @@ var defaultCacheConfig = &CacheConfig{ SnapshotWait: true, } +func updateHeadL1msgGauge(block *types.Block) { + for _, tx := range block.Transactions() { + if msg := tx.AsL1MessageTx(); msg != nil { + // Queue index is guaranteed to fit into int64. + headL1MessageGauge.Update(int64(msg.QueueIndex)) + } else { + // No more L1 messages in this block. + break + } + } +} + +// updateL2BaseFeeCoefficients updates the global L2 base fee coefficients. +// Coefficient updates are written into L2 state and emit an event. +// We could use either here; we read from the event to avoid state reads. +// In the future, if the base fee setting becomes part of block validation, +// reading from state will be more appropriate. +func updateL2BaseFeeCoefficients(l2SystemConfigAddress common.Address, logs []*types.Log) { + defer func(start time.Time) { l2BaseFeeUpdateTimer.Update(time.Since(start)) }(time.Now()) + + for _, l := range logs { + if l.Address != l2SystemConfigAddress { + continue + } + switch l.Topics[0] { + case l2_system_config.BaseFeeOverheadUpdatedTopic: + event, err := l2_system_config.UnpackBaseFeeOverheadUpdatedEvent(*l) + if err != nil { + log.Error("failed to unpack base fee overhead updated event log", "err", err, "log", *l) + break // break from switch, continue loop + } + misc.UpdateL2BaseFeeOverhead(event.NewBaseFeeOverhead) + log.Info("Updated L2 base fee overhead", "blockNumber", l.BlockNumber, "blockHash", l.BlockHash.Hex(), "old", event.OldBaseFeeOverhead, "new", event.NewBaseFeeOverhead) + case l2_system_config.BaseFeeScalarUpdatedTopic: + event, err := l2_system_config.UnpackBaseFeeScalarUpdatedEvent(*l) + if err != nil { + log.Error("failed to unpack base fee scalar updated event log", "err", err, "log", *l) + break // break from switch, continue loop + } + misc.UpdateL2BaseFeeScalar(event.NewBaseFeeScalar) + log.Info("Updated L2 base fee scalar", "blockNumber", l.BlockNumber, "blockHash", l.BlockHash.Hex(), "old", event.OldBaseFeeScalar, "new", event.NewBaseFeeScalar) + } + } +} + // BlockChain represents the canonical chain given a database with a genesis // block. The Blockchain manages chain imports, reverts, chain reorganisations. // @@ -422,6 +471,9 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par triedb.SaveCachePeriodically(bc.cacheConfig.TrieCleanJournal, bc.cacheConfig.TrieCleanRejournal, bc.quit) }() } + + updateHeadL1msgGauge(bc.CurrentBlock()) + return bc, nil } @@ -1253,6 +1305,12 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. l2BaseFeeGauge.Update(0) } + // Note the latest relayed L1 message queue index (if any) + updateHeadL1msgGauge(block) + + // Execute L2 base fee coefficient updates (if any) + updateL2BaseFeeCoefficients(bc.Config().Scroll.L2SystemConfigAddress(), logs) + parent := bc.GetHeaderByHash(block.ParentHash()) // block.Time is guaranteed to be larger than parent.Time, // and the time gap should fit into int64. @@ -1283,7 +1341,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. if queueIndex == nil { // We expect that we only insert contiguous chain segments, // so the parent will always be inserted first. - log.Crit("Queue index in DB is nil", "parent", block.ParentHash(), "hash", block.Hash()) + log.Crit("Queue index in DB is nil", "parent", block.ParentHash().Hex(), "hash", block.Hash().Hex()) } numProcessed := uint64(block.NumL1MessagesProcessed(*queueIndex)) // do not overwrite the index written by the miner worker @@ -1527,7 +1585,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er headers[i] = block.Header() seals[i] = verifySeals } - abort, results := bc.engine.VerifyHeaders(bc, headers, seals) + abort, results := bc.engine.VerifyHeaders(bc, headers, seals, nil) defer close(abort) // Peek the error for the first block to decide the directing import logic @@ -1680,7 +1738,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er } // Enable prefetching to pull in trie node paths while processing transactions - statedb.StartPrefetcher("chain", nil) + statedb.StartPrefetcher("chain") activeState = statedb // If we have a followup block, run that against the current state to pre-cache @@ -1817,7 +1875,7 @@ func (bc *BlockChain) BuildAndWriteBlock(parentBlock *types.Block, header *types return nil, NonStatTy, err } - statedb.StartPrefetcher("l1sync", nil) + statedb.StartPrefetcher("l1sync") defer statedb.StopPrefetcher() header.ParentHash = parentBlock.Hash() diff --git a/core/error.go b/core/error.go index 54ec1b188d..98a637e958 100644 --- a/core/error.go +++ b/core/error.go @@ -81,6 +81,10 @@ var ( // than required to start the invocation. ErrIntrinsicGas = errors.New("intrinsic gas too low") + // ErrFloorDataGas is returned if the transaction is specified to use less gas + // than required for the data floor cost. + ErrFloorDataGas = errors.New("insufficient gas for floor data gas cost") + // ErrTxTypeNotSupported is returned if a transaction is not supported in the // current network configuration. ErrTxTypeNotSupported = types.ErrTxTypeNotSupported diff --git a/core/genesis.go b/core/genesis.go index 9554be0ac3..a5eea6f30c 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -500,6 +500,8 @@ func DeveloperGenesisBlock(period uint64, gasLimit uint64, faucet common.Address common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b + // Pre-deploy EIP-2935 history contract. + params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0}, // LSH 250 due to finite field limitation faucet: {Balance: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 250), big.NewInt(9))}, }, diff --git a/core/headerchain.go b/core/headerchain.go index f047d654d9..c01eb7ba8c 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -339,7 +339,7 @@ func (hc *HeaderChain) ValidateHeaderChain(chain []*types.Header, checkFreq int) seals[len(seals)-1] = true } - abort, results := hc.engine.VerifyHeaders(hc, chain, seals) + abort, results := hc.engine.VerifyHeaders(hc, chain, seals, nil) defer close(abort) // Iterate over the headers and ensure they all check out diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index d89a7d2e21..d9aceeca09 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -26,7 +26,6 @@ import ( "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/core/types" - "github.com/scroll-tech/go-ethereum/crypto" "github.com/scroll-tech/go-ethereum/ethdb" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" @@ -301,11 +300,9 @@ func WriteFastTxLookupLimit(db ethdb.KeyValueWriter, number uint64) { func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { var data []byte db.ReadAncients(func(reader ethdb.AncientReader) error { - // First try to look up the data in ancient database. Extra hash - // comparison is necessary since ancient database only maintains - // the canonical data. + // First try to look up the data in ancient database. data, _ = reader.Ancient(freezerHeaderTable, number) - if len(data) > 0 && crypto.Keccak256Hash(data) == hash { + if len(data) > 0 { return nil } // If not, try reading from leveldb @@ -337,6 +334,14 @@ func ReadHeader(db ethdb.Reader, hash common.Hash, number uint64) *types.Header log.Error("Invalid block header RLP", "hash", hash, "err", err) return nil } + + // Extra hash + // comparison is necessary since ancient database only maintains + // the canonical data. + if header.Hash() != hash { + return nil + } + return header } diff --git a/core/state/state_object.go b/core/state/state_object.go index 4fb9e82c2e..2b615d9408 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "math/big" + "slices" "time" "github.com/scroll-tech/go-ethereum/common" @@ -344,8 +345,17 @@ func (s *stateObject) updateTrie(db Database) Trie { tr := s.getTrie(db) hasher := s.db.hasher + sortedPendingStorages := make([]common.Hash, 0, len(s.pendingStorage)) + for key := range s.pendingStorage { + sortedPendingStorages = append(sortedPendingStorages, key) + } + slices.SortFunc(sortedPendingStorages, func(a, b common.Hash) int { + return bytes.Compare(a[:], b[:]) + }) + usedStorage := make([][]byte, 0, len(s.pendingStorage)) - for key, value := range s.pendingStorage { + for _, key := range sortedPendingStorages { + value := s.pendingStorage[key] // Skip noop changes, persist actual changes if value == s.originStorage[key] { continue diff --git a/core/state/statedb.go b/core/state/statedb.go index 7affd81fc4..5302263d36 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,9 +18,11 @@ package state import ( + "bytes" "errors" "fmt" "math/big" + "slices" "sort" "time" @@ -160,18 +162,25 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return sdb, nil } +// WithWitness sets the witness for the state database. +func (s *StateDB) WithWitness(witness *stateless.Witness) { + s.witness = witness +} + +// Witness returns the current witness for the state database. +func (s *StateDB) Witness() *stateless.Witness { + return s.witness +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. -func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) { +func (s *StateDB) StartPrefetcher(namespace string) { if s.prefetcher != nil { s.prefetcher.close() s.prefetcher = nil } - // Enable witness collection if requested - s.witness = witness - if s.snap != nil { s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace) } @@ -967,8 +976,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { s.trie = trie } } - usedAddrs := make([][]byte, 0, len(s.stateObjectsPending)) + + sortedPendingAddrs := make([]common.Address, 0, len(s.stateObjectsPending)) for addr := range s.stateObjectsPending { + sortedPendingAddrs = append(sortedPendingAddrs, addr) + } + slices.SortFunc(sortedPendingAddrs, func(a, b common.Address) int { + return bytes.Compare(a[:], b[:]) + }) + + usedAddrs := make([][]byte, 0, len(s.stateObjectsPending)) + for _, addr := range sortedPendingAddrs { if obj := s.stateObjects[addr]; obj.deleted { s.deleteStateObject(obj) s.AccountDeleted += 1 diff --git a/core/state_processor.go b/core/state_processor.go index 7a6c5d0d97..ddbc9d43bd 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -94,6 +94,10 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg blockContext := NewEVMBlockContext(header, p.bc, p.config, nil) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) processorBlockTransactionGauge.Update(int64(block.Transactions().Len())) + // Apply EIP-2935 + if p.config.IsFeynman(block.Time()) { + ProcessParentBlockHash(block.ParentHash(), vmenv, statedb) + } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number, header.Time), header.BaseFee) @@ -199,3 +203,30 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) return applyTransaction(msg, config, bc, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } + +// ProcessParentBlockHash stores the parent block hash in the history storage contract +// as per EIP-2935. +func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, statedb *state.StateDB) { + msg := types.NewMessage( + params.SystemAddress, // from + ¶ms.HistoryStorageAddress, // to + 0, // nonce + common.Big0, // amount + 30_000_000, // gasLimit + common.Big0, // gasPrice + common.Big0, // gasFeeCap + common.Big0, // gasTipCap + prevHash.Bytes(), // data + nil, // accessList + false, // isFake + nil, // setCodeAuthorizations + ) + + evm.Reset(NewEVMTxContext(msg), statedb) + statedb.AddAddressToAccessList(params.HistoryStorageAddress) + _, _, err := evm.Call(vm.AccountRef(msg.From()), *msg.To(), msg.Data(), 30_000_000, common.Big0, nil) + if err != nil { + panic(err) + } + statedb.Finalise(true) +} diff --git a/core/state_processor_test.go b/core/state_processor_test.go index a2585941ce..7fbc812cb6 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -18,6 +18,7 @@ package core import ( "crypto/ecdsa" + "encoding/binary" "math/big" "testing" @@ -31,9 +32,11 @@ import ( "github.com/scroll-tech/go-ethereum/consensus/ethash" "github.com/scroll-tech/go-ethereum/consensus/misc" "github.com/scroll-tech/go-ethereum/core/rawdb" + "github.com/scroll-tech/go-ethereum/core/state" "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/core/vm" "github.com/scroll-tech/go-ethereum/crypto" + "github.com/scroll-tech/go-ethereum/ethdb/memorydb" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/trie" ) @@ -64,6 +67,7 @@ func TestStateProcessorErrors(t *testing.T) { DarwinV2Time: new(uint64), EuclidTime: new(uint64), EuclidV2Time: new(uint64), + FeynmanTime: new(uint64), Ethash: new(params.EthashConfig), } signer = types.LatestSigner(config) @@ -387,15 +391,15 @@ func TestStateProcessorErrors(t *testing.T) { }{ { // ErrMaxInitCodeSizeExceeded txs: []*types.Transaction{ - mkDynamicCreationTx(0, 500000, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee), tooBigInitCode[:]), + mkDynamicCreationTx(0, 520000, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee), tooBigInitCode[:]), }, - want: "could not apply tx 0 [0xa31de6e26bd5ffba0ca91a2bc29fc2eaad6a6cfc5ad9ab6ffb69cac121e0125c]: max initcode size exceeded: code size 49153 limit 49152", + want: "could not apply tx 0 [0xe0d03426cecc04467410064cb4de02012fc069d2462282735d7dfcb9dea9f63b]: max initcode size exceeded: code size 49153 limit 49152", }, { // ErrIntrinsicGas: Not enough gas to cover init code txs: []*types.Transaction{ mkDynamicCreationTx(0, 54299, common.Big0, misc.CalcBaseFee(config, genesis.Header(), parentL1BaseFee), smallInitCode[:]), }, - want: "could not apply tx 0 [0xf36b7d68cf239f956f7c36be26688a97aaa317ea5f5230d109bb30dbc8598ccb]: intrinsic gas too low: have 54299, want 54300", + want: "could not apply tx 0 [0x98e54c5ecfa7986a66480d65ba32f2c6a2a6aedc3a67abb91b1e118b0717ed2d]: intrinsic gas too low: have 54299, want 54300", }, } { block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config) @@ -453,3 +457,68 @@ func GenerateBadBlock(parent *types.Block, engine consensus.Engine, txs types.Tr // Assemble and return the final block for sealing return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)) } + +func TestProcessParentBlockHash(t *testing.T) { + var ( + chainConfig = ¶ms.ChainConfig{ + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ShanghaiBlock: big.NewInt(0), + BernoulliBlock: big.NewInt(0), + CurieBlock: big.NewInt(0), + DarwinTime: new(uint64), + DarwinV2Time: new(uint64), + EuclidTime: new(uint64), + EuclidV2Time: new(uint64), + FeynmanTime: new(uint64), + Ethash: new(params.EthashConfig), + } + hashA = common.Hash{0x01} + hashB = common.Hash{0x02} + header = &types.Header{ParentHash: hashA, Number: big.NewInt(2), Difficulty: big.NewInt(0)} + parent = &types.Header{ParentHash: hashB, Number: big.NewInt(1), Difficulty: big.NewInt(0)} + coinbase = common.Address{} + ) + test := func(statedb *state.StateDB) { + statedb.SetNonce(params.HistoryStorageAddress, 1) + statedb.SetCode(params.HistoryStorageAddress, params.HistoryStorageCode) + statedb.IntermediateRoot(true) + + vmContext := NewEVMBlockContext(header, nil, chainConfig, &coinbase) + evm := vm.NewEVM(vmContext, vm.TxContext{}, statedb, chainConfig, vm.Config{}) + ProcessParentBlockHash(header.ParentHash, evm, statedb) + + vmContext = NewEVMBlockContext(parent, nil, chainConfig, &coinbase) + evm = vm.NewEVM(vmContext, vm.TxContext{}, statedb, chainConfig, vm.Config{}) + ProcessParentBlockHash(parent.ParentHash, evm, statedb) + + // make sure that the state is correct + if have := getParentBlockHash(statedb, 1); have != hashA { + t.Errorf("want parent hash %v, have %v", hashA, have) + } + if have := getParentBlockHash(statedb, 0); have != hashB { + t.Errorf("want parent hash %v, have %v", hashB, have) + } + } + t.Run("MPT", func(t *testing.T) { + statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewDatabase(memorydb.New())), nil) + test(statedb) + }) +} + +func getParentBlockHash(statedb *state.StateDB, number uint64) common.Hash { + ringIndex := number % params.HistoryServeWindow + var key common.Hash + binary.BigEndian.PutUint64(key[24:], ringIndex) + return statedb.GetState(params.HistoryStorageAddress, key) +} diff --git a/core/state_transition.go b/core/state_transition.go index 0f480d52a3..0b01945927 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "fmt" "math" "math/big" @@ -141,12 +142,9 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set // Bump the required gas by the amount of transactional data if dataLen > 0 { // Zero and non-zero bytes are priced differently - var nz uint64 - for _, byt := range data { - if byt != 0 { - nz++ - } - } + z := uint64(bytes.Count(data, []byte{0})) + nz := dataLen - z + // Make sure we don't exceed uint64 for all data combinations nonZeroGas := params.TxDataNonZeroGasFrontier if isEIP2028 { @@ -157,7 +155,6 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set } gas += nz * nonZeroGas - z := dataLen - nz if (math.MaxUint64-gas)/params.TxDataZeroGas < z { return 0, ErrGasUintOverflow } @@ -181,6 +178,21 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set return gas, nil } +// FloorDataGas computes the minimum gas required for a transaction based on its data tokens (EIP-7623). +func FloorDataGas(data []byte) (uint64, error) { + var ( + z = uint64(bytes.Count(data, []byte{0})) + nz = uint64(len(data)) - z + tokens = nz*params.TxTokenPerNonZeroByte + z + ) + // Check for overflow + if (math.MaxUint64-params.TxGas)/params.TxCostFloorPerToken < tokens { + return 0, ErrGasUintOverflow + } + // Minimum gas required for a transaction based on its data tokens (EIP-7623). + return params.TxGas + tokens*params.TxCostFloorPerToken, nil +} + // toWordSize returns the ceiled word size required for init code payment calculation. func toWordSize(size uint64) uint64 { if size > math.MaxUint64-31 { @@ -269,8 +281,21 @@ func (st *StateTransition) buyGas() error { } func (st *StateTransition) preCheck() error { + // Only check transactions that are not fake + if !st.msg.IsFake() { + // Make sure the sender is an EOA + code := st.state.GetCode(st.msg.From()) + _, delegated := types.ParseDelegation(code) + if len(code) > 0 && !delegated { + // If the sender on L1 is a (delegated) EOA, then it must be a (delegated) EOA on L2, too. + // If the sender on L1 is a contract, then we apply address aliasing. + // The probability that the aliased address happens to be a smart contract on L2 is negligible. + return fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, st.msg.From().Hex(), len(code)) + } + } + if st.msg.IsL1MessageTx() { - // No fee fields to check, no nonce to check, and no need to check if EOA (L1 already verified it for us) + // No fee fields to check, no nonce to check // Gas is free, but no refunds! st.gas += st.msg.Gas() st.initialGas = st.msg.Gas() @@ -291,12 +316,6 @@ func (st *StateTransition) preCheck() error { return fmt.Errorf("%w: address %v, nonce: %d", ErrNonceMax, st.msg.From().Hex(), stNonce) } - // Make sure the sender is an EOA - code := st.state.GetCode(st.msg.From()) - _, delegated := types.ParseDelegation(code) - if len(code) > 0 && !delegated { - return fmt.Errorf("%w: address %v, len(code): %d", ErrSenderNoEOA, st.msg.From().Hex(), len(code)) - } } // Make sure that transaction gasFeeCap is greater than the baseFee (post london) // Note: Logically, this should be `IsCurie`, but we keep `IsLondon` to ensure backward compatibility. @@ -373,16 +392,30 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { sender = vm.AccountRef(msg.From()) rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Time.Uint64()) contractCreation = msg.To() == nil + floorDataGas uint64 ) // Check clauses 4-5, subtract intrinsic gas if everything is correct gas, err := IntrinsicGas(st.data, st.msg.AccessList(), st.msg.SetCodeAuthorizations(), contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) if err != nil { + // Note: The L1 message queue contract ensures that this cannot happen for L1 messages. return nil, err } if st.gas < gas { + // Note: The L1 message queue contract ensures that this cannot happen for L1 messages. return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas) } + // Gas limit suffices for the floor data cost (EIP-7623) + if rules.IsFeynman { + floorDataGas, err = FloorDataGas(st.data) + if err != nil { + return nil, err + } + if st.gas < floorDataGas { + // Note: The L1 message queue contract ensures that this cannot happen for L1 messages. + return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, st.gas, floorDataGas) + } + } st.gas -= gas // Check clause 6 @@ -448,13 +481,16 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { }, nil } - if !rules.IsLondon { - // Before EIP-3529: refunds were capped to gasUsed / 2 - st.refundGas(params.RefundQuotient) - } else { - // After EIP-3529: refunds are capped to gasUsed / 5 - st.refundGas(params.RefundQuotientEIP3529) + // Compute refund counter, capped to a refund quotient. + st.gas += st.calcRefund() + if rules.IsFeynman { + // After EIP-7623: Data-heavy transactions pay the floor gas. + if st.gasUsed() < floorDataGas { + st.gas = st.initialGas - floorDataGas + } } + st.returnGas() + effectiveTip := st.gasPrice // only burn the base fee if the fee vault is not enabled @@ -479,7 +515,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { // validateAuthorization validates an EIP-7702 authorization against the state. func (st *StateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, preCode []byte, err error) { - // Verify chain ID is 0 or equal to current chain ID. + // Verify chain ID is null or equal to current chain ID. if !auth.ChainID.IsZero() && auth.ChainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 { return authority, nil, ErrAuthorizationWrongChainID } @@ -538,14 +574,25 @@ func (st *StateTransition) applyAuthorization(auth *types.SetCodeAuthorization) return types.AuthorizationResult{Authority: authority, PreCode: preCode, Success: true} } -func (st *StateTransition) refundGas(refundQuotient uint64) { - // Apply refund counter, capped to a refund quotient - refund := st.gasUsed() / refundQuotient +// calcRefund computes refund counter, capped to a refund quotient. +func (st *StateTransition) calcRefund() uint64 { + var refund uint64 + if !st.evm.ChainConfig().IsLondon(st.evm.Context.BlockNumber) { + // Before EIP-3529: refunds were capped to gasUsed / 2 + refund = st.gasUsed() / params.RefundQuotient + } else { + // After EIP-3529: refunds are capped to gasUsed / 5 + refund = st.gasUsed() / params.RefundQuotientEIP3529 + } if refund > st.state.GetRefund() { refund = st.state.GetRefund() } - st.gas += refund + return refund +} +// returnGas returns ETH for remaining gas, +// exchanged at the original rate. +func (st *StateTransition) returnGas() { // Return ETH for remaining gas, exchanged at the original rate. remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice) st.state.AddBalance(st.msg.From(), remaining) diff --git a/core/tx_pool.go b/core/tx_pool.go index b9828789cd..3d49800e01 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -79,10 +79,6 @@ var ( // with a different one without the required price bump. ErrReplaceUnderpriced = errors.New("replacement transaction underpriced") - // ErrAccountLimitExceeded is returned if a transaction would exceed the number - // allowed by a pool for a single account. - ErrAccountLimitExceeded = errors.New("account limit exceeded") - // ErrGasLimit is returned if a transaction's requested gas limit exceeds the // maximum allowance of the current block. ErrGasLimit = errors.New("exceeds block gas limit") @@ -96,6 +92,14 @@ var ( // making the transaction invalid, rather a DOS protection. ErrOversizedData = errors.New("oversized data") + // ErrInflightTxLimitReached is returned when the maximum number of in-flight + // transactions is reached for specific accounts. + ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts") + + // ErrOutOfOrderTxFromDelegated is returned when the transaction with gapped + // nonce received from the accounts with delegation or pending delegation. + ErrOutOfOrderTxFromDelegated = errors.New("gapped-nonce tx from delegated accounts") + // ErrAuthorityReserved is returned if a transaction has an authorization // signed by an address which already has in-flight transactions known to the // pool. @@ -106,7 +110,7 @@ var ( evictionInterval = time.Minute // Time interval to check for evictable transactions statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats - dumpReorgTxHashThreshold = 100 // Number of transaction hashse to dump when runReorg + dumpReorgTxHashThreshold = 100 // Number of transaction hashes to dump when runReorg ) var ( @@ -293,6 +297,7 @@ type TxPool struct { eip1559 bool // Fork indicator whether we are using EIP-1559 type transactions. shanghai bool // Fork indicator whether we are in the Shanghai stage. eip7702 bool // Fork indicator whether we are using EIP-7702 type transactions. + feynman bool // Fork indicator whether we are in the Feynman stage. currentState *state.StateDB // Current state in the blockchain head currentHead *big.Int // Current blockchain head @@ -420,6 +425,8 @@ func (pool *TxPool) loop() { journal = time.NewTicker(pool.config.Rejournal) // Track the previous head headers for transaction reorgs head = pool.chain.CurrentBlock() + // Track the min L2 base fee + minBaseFee = big.NewInt(0) ) defer report.Stop() defer evict.Stop() @@ -434,6 +441,16 @@ func (pool *TxPool) loop() { if ev.Block != nil { pool.requestReset(head.Header(), ev.Block.Header()) head = ev.Block + for _, tx := range head.Transactions() { + log.Debug("TX is included in a block", "hash", tx.Hash().Hex()) + } + } + + newMinBaseFee := misc.MinBaseFee() + if minBaseFee.Cmp(newMinBaseFee) != 0 { + log.Debug("Updating min base fee in txpool", "old", minBaseFee, "new", newMinBaseFee) + minBaseFee = newMinBaseFee + pool.SetGasPrice(minBaseFee) } // System shutdown. @@ -467,7 +484,7 @@ func (pool *TxPool) loop() { if time.Since(pool.beats[addr]) > pool.config.Lifetime { list := pool.queue[addr].Flatten() for _, tx := range list { - log.Trace("Evicting queued transaction due to timeout", "account", addr.Hex(), "hash", tx.Hash().Hex(), "lifetime sec", time.Since(pool.beats[addr]).Seconds(), "lifetime limit sec", pool.config.Lifetime.Seconds()) + log.Debug("Evicting queued transaction due to timeout", "account", addr.Hex(), "hash", tx.Hash().Hex(), "lifetime sec", time.Since(pool.beats[addr]).Seconds(), "lifetime limit sec", pool.config.Lifetime.Seconds()) pool.removeTx(tx.Hash(), true) } queuedEvictionMeter.Mark(int64(len(list))) @@ -483,7 +500,7 @@ func (pool *TxPool) loop() { if time.Since(pool.beats[addr]) > pool.config.Lifetime { list := pool.pending[addr].Flatten() for _, tx := range list { - log.Trace("Evicting pending transaction due to timeout", "account", addr.Hex(), "hash", tx.Hash().Hex(), "lifetime sec", time.Since(pool.beats[addr]).Seconds(), "lifetime limit sec", pool.config.Lifetime.Seconds()) + log.Debug("Evicting pending transaction due to timeout", "account", addr.Hex(), "hash", tx.Hash().Hex(), "lifetime sec", time.Since(pool.beats[addr]).Seconds(), "lifetime limit sec", pool.config.Lifetime.Seconds()) pool.removeTx(tx.Hash(), true) } pendingEvictionMeter.Mark(int64(len(list))) @@ -546,6 +563,7 @@ func (pool *TxPool) SetGasPrice(price *big.Int) { // pool.priced is sorted by GasFeeCap, so we have to iterate through pool.all instead drop := pool.all.RemotesBelowTip(price) for _, tx := range drop { + log.Debug("Removing transaction below price limit", "hash", tx.Hash().Hex(), "price", tx.GasFeeCap().String(), "limit", price.String()) pool.removeTx(tx.Hash(), false) } pool.priced.Removed(len(drop)) @@ -809,19 +827,8 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 { return ErrInsufficientFunds } - list := pool.pending[from] - if list == nil || !list.Overlaps(tx) { - // Transaction takes a new nonce value out of the pool. Ensure it doesn't - // overflow the number of permitted transactions from a single account - // (i.e. max cancellable via out-of-bound transaction). - if used, left := usedAndLeftSlots(pool, from); left <= 0 { - return fmt.Errorf("%w: pooled %d txs", ErrAccountLimitExceeded, used) - } - // Verify no authorizations will invalidate existing transactions known to - // the pool. - if conflicts := knownConflicts(pool, tx.SetCodeAuthorities()); len(conflicts) > 0 { - return fmt.Errorf("%w: authorization conflicts with other known tx", ErrAuthorityReserved) - } + if err := pool.validateAuth(from, tx); err != nil { + return err } if tx.Type() == types.SetCodeTxType { if len(tx.SetCodeAuthorizations()) == 0 { @@ -835,6 +842,11 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if err != nil { return fmt.Errorf("failed to calculate L1 data fee, err: %w", err) } + // Reject transactions that require the max data fee amount. + // This can only happen if the L1 gas oracle is updated incorrectly. + if l1DataFee.Cmp(fees.MaxL1DataFee()) >= 0 { + return errors.New("invalid transaction: invalid L1 data fee") + } // Transactor should have enough funds to cover the costs // cost == L1 data fee + V + GP * GL if b := pool.currentState.GetBalance(from); b.Cmp(new(big.Int).Add(tx.Cost(), l1DataFee)) < 0 { @@ -849,6 +861,16 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if tx.Gas() < intrGas { return ErrIntrinsicGas } + // Ensure the transaction can cover floor data gas. + if pool.feynman { + floorDataGas, err := FloorDataGas(tx.Data()) + if err != nil { + return err + } + if tx.Gas() < floorDataGas { + return fmt.Errorf("%w: gas %v, minimum needed %v", ErrFloorDataGas, tx.Gas(), floorDataGas) + } + } return nil } @@ -863,13 +885,13 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e // If the transaction is already known, discard it hash := tx.Hash() if pool.all.Get(hash) != nil { - log.Trace("Discarding already known transaction", "hash", hash.Hex()) + log.Debug("Discarding already known transaction when adding the transaction", "hash", hash.Hex()) knownTxMeter.Mark(1) return false, ErrAlreadyKnown } if pool.IsMiner() && rawdb.IsSkippedTransaction(pool.chain.Database(), hash) { - log.Trace("Discarding already known skipped transaction", "hash", hash.Hex()) + log.Debug("Discarding already known skipped transaction when adding the transaction", "hash", hash.Hex()) knownSkippedTxMeter.Mark(1) return false, ErrAlreadyKnown } @@ -880,7 +902,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e // If the transaction fails basic validation, discard it if err := pool.validateTx(tx, isLocal); err != nil { - log.Trace("Discarding invalid transaction", "hash", hash.Hex(), "err", err) + log.Debug("Discarding invalid transaction when adding the transaction", "hash", hash.Hex(), "err", err) invalidTxMeter.Mark(1) return false, err } @@ -888,7 +910,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e if uint64(pool.all.Slots()+numSlots(tx)) > pool.config.GlobalSlots+pool.config.GlobalQueue { // If the new transaction is underpriced, don't accept it if !isLocal && pool.priced.Underpriced(tx) { - log.Trace("Discarding underpriced transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) + log.Debug("Discarding underpriced transaction when transaction pool is full", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) underpricedTxMeter.Mark(1) return false, ErrUnderpriced } @@ -908,7 +930,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e // Special case, we still can't make the room for the new remote one. if !isLocal && !success { - log.Trace("Discarding overflown transaction", "hash", hash.Hex()) + log.Debug("Discarding overflown transaction when transaction pool is full", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) overflowedTxMeter.Mark(1) return false, ErrTxPoolOverflow } @@ -916,7 +938,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e pool.changesSinceReorg += len(drop) // Kick out the underpriced remote transactions. for _, tx := range drop { - log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) + log.Debug("Discarding underpriced transaction when transaction pool is full", "hash", tx.Hash().Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) underpricedTxMeter.Mark(1) pool.removeTx(tx.Hash(), false) } @@ -927,11 +949,13 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e // Nonce already pending, check if required price bump is met inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead) if !inserted { + log.Debug("Discarding underpriced pending transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) pendingDiscardMeter.Mark(1) return false, ErrReplaceUnderpriced } // New transaction is better, replace old one if old != nil { + log.Debug("Replacing pending transaction", "hash", tx.Hash().Hex(), "old", old.Hash().Hex()) pool.all.Remove(old.Hash()) pool.calculateTxsLifecycle(types.Transactions{old}, time.Now()) pool.priced.Removed(1) @@ -941,7 +965,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e pool.priced.Put(tx, isLocal) pool.journalTx(from, tx) pool.queueTxEvent(tx) - log.Trace("Pooled new executable transaction", "hash", hash.Hex(), "from", from.Hex(), "to", tx.To()) + log.Debug("Pooled new executable transaction", "hash", hash.Hex(), "from", from.Hex(), "to", tx.To(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) // Successful promotion, bump the heartbeat pool.beats[from] = time.Now() @@ -963,7 +987,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e } pool.journalTx(from, tx) - log.Trace("Pooled new future transaction", "hash", hash.Hex(), "from", from.Hex(), "to", tx.To()) + log.Debug("Pooled new future transaction", "hash", hash.Hex(), "from", from.Hex(), "to", tx.To(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) return replaced, nil } @@ -980,11 +1004,13 @@ func (pool *TxPool) enqueueTx(hash common.Hash, tx *types.Transaction, local boo inserted, old := pool.queue[from].Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead) if !inserted { // An older transaction was better, discard this + log.Debug("Discarding underpriced queued transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) queuedDiscardMeter.Mark(1) return false, ErrReplaceUnderpriced } // Discard any previous transaction and mark this if old != nil { + log.Debug("Replacing queued transaction", "hash", tx.Hash().Hex(), "old", old.Hash().Hex()) pool.all.Remove(old.Hash()) pool.priced.Removed(1) pool.calculateTxsLifecycle(types.Transactions{old}, time.Now()) @@ -1034,6 +1060,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T // Account pending list is full if uint64(list.Len()) >= pool.config.AccountPendingLimit { + log.Debug("Discarding pending transaction when account pending limit is reached", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) pool.priced.Removed(1) @@ -1044,6 +1071,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T inserted, old := list.Add(tx, pool.currentState, pool.config.PriceBump, pool.chainconfig, pool.currentHead) if !inserted { // An older transaction was better, discard this + log.Debug("Discarding underpriced pending transaction", "hash", hash.Hex(), "gasTipCap", tx.GasTipCap(), "gasFeeCap", tx.GasFeeCap()) pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) pool.priced.Removed(1) @@ -1052,6 +1080,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T } // Otherwise discard any previous transaction and mark this if old != nil { + log.Debug("Replacing pending transaction", "hash", tx.Hash().Hex(), "old", old.Hash().Hex()) pool.all.Remove(old.Hash()) pool.calculateTxsLifecycle(types.Transactions{old}, time.Now()) pool.priced.Removed(1) @@ -1060,7 +1089,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T // Nothing was replaced, bump the pending counter pendingGauge.Inc(1) } - log.Trace("Transaction promoted from future queue to pending", "hash", hash.Hex(), "from", addr.Hex(), "to", tx.To()) + log.Debug("Transaction promoted from future queue to pending", "hash", hash.Hex(), "from", addr.Hex(), "to", tx.To()) // Set the potentially new pending nonce and notify any subsystems of the new tx pool.pendingNonces.set(addr, tx.Nonce()+1) @@ -1233,7 +1262,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) { return } - log.Trace("Removing transaction from pool", "hash", hash.Hex()) + log.Debug("Removing transaction from pool", "hash", hash.Hex()) addr, _ := types.Sender(pool.signer, tx) // already validated during insertion @@ -1463,10 +1492,10 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt } pool.txFeed.Send(NewTxsEvent{txs}) - log.Debug("runReorg", "len(txs)", len(txs)) + log.Trace("runReorg", "len(txs)", len(txs)) if len(txs) > dumpReorgTxHashThreshold { for _, txs := range txs { - log.Debug("dumping runReorg tx hashes", "txHash", txs.Hash().Hex()) + log.Trace("dumping runReorg tx hashes", "txHash", txs.Hash().Hex()) } } } @@ -1535,6 +1564,10 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { } } reinject = types.TxDifference(discarded, included) + + for _, tx := range reinject { + log.Debug("TX is removed from old chain due to reorg", "hash", tx.Hash().Hex()) + } } } } @@ -1552,7 +1585,9 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.currentMaxGas = newHead.GasLimit // Inject any transactions discarded due to reorgs - log.Debug("Reinjecting stale transactions", "count", len(reinject)) + if len(reinject) > 0 { + log.Debug("Reinjecting stale transactions", "count", len(reinject)) + } senderCacher.recover(pool.signer, reinject) pool.addTxsLocked(reinject, false) @@ -1564,6 +1599,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.eip1559 = pool.chainconfig.IsCurie(next) pool.shanghai = pool.chainconfig.IsShanghai(next) pool.eip7702 = pool.chainconfig.IsEuclidV2(newHead.Time) + pool.feynman = pool.chainconfig.IsFeynman(newHead.Time) // Update current head pool.currentHead = next @@ -1588,6 +1624,9 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans hash := tx.Hash() pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) + // This is a special case where the transaction was already included in a block, + // thus no need to mark it as removed, setting a trace log level to avoid confusion. + log.Trace("Removed queued transaction with low nonce", "hash", hash.Hex()) } log.Trace("Removed old queued transactions", "count", len(forwards)) @@ -1598,6 +1637,7 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans hash := tx.Hash() pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) + log.Debug("Removed unpayable queued transaction", "hash", hash.Hex()) } log.Trace("Removed unpayable queued transactions", "count", len(drops)) queuedNofundsMeter.Mark(int64(len(drops))) @@ -1621,7 +1661,7 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans hash := tx.Hash() pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) - log.Trace("Removed cap-exceeding queued transaction", "hash", hash.Hex()) + log.Debug("Removed cap-exceeding queued transaction", "hash", hash.Hex()) } queuedRateLimitMeter.Mark(int64(len(caps))) } @@ -1679,7 +1719,7 @@ func (pool *TxPool) truncatePending() { // Update the account nonce to the dropped transaction // note: this will set pending nonce to the min nonce from the discarded txs pool.pendingNonces.setIfLower(addr, tx.Nonce()) - log.Trace("Removed pending transaction to comply with hard limit", "hash", hash.Hex()) + log.Debug("Removed pending transaction to comply with hard limit", "hash", hash.Hex()) } pool.priced.Removed(len(caps)) pendingGauge.Dec(int64(len(caps))) @@ -1732,7 +1772,7 @@ func (pool *TxPool) truncatePending() { // Update the account nonce to the dropped transaction pool.pendingNonces.setIfLower(offenders[i], tx.Nonce()) - log.Trace("Removed fairness-exceeding pending transaction", "hash", hash.Hex()) + log.Debug("Removed fairness-exceeding pending transaction", "hash", hash.Hex()) } pool.priced.Removed(len(caps)) pendingGauge.Dec(int64(len(caps))) @@ -1760,7 +1800,7 @@ func (pool *TxPool) truncatePending() { // Update the account nonce to the dropped transaction pool.pendingNonces.setIfLower(addr, tx.Nonce()) - log.Trace("Removed fairness-exceeding pending transaction", "hash", hash) + log.Debug("Removed fairness-exceeding pending transaction", "hash", hash.Hex()) } pool.priced.Removed(len(caps)) pendingGauge.Dec(int64(len(caps))) @@ -1803,6 +1843,7 @@ func (pool *TxPool) truncateQueue() { // Drop all transactions if they are less than the overflow if size := uint64(list.Len()); size <= drop { for _, tx := range list.Flatten() { + log.Debug("Removed fairness-exceeding queued transaction", "hash", tx.Hash().Hex()) pool.removeTx(tx.Hash(), true) } drop -= size @@ -1812,6 +1853,7 @@ func (pool *TxPool) truncateQueue() { // Otherwise drop only last few transactions txs := list.Flatten() for i := len(txs) - 1; i >= 0 && drop > 0; i-- { + log.Debug("Removed fairness-exceeding queued transaction", "hash", txs[i].Hash().Hex()) pool.removeTx(txs[i].Hash(), true) drop-- queuedRateLimitMeter.Mark(1) @@ -1837,22 +1879,24 @@ func (pool *TxPool) demoteUnexecutables() { hash := tx.Hash() pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) - log.Trace("Removed old pending transaction", "hash", hash.Hex()) + // This is a special case where the transaction was already included in a block, + // thus no need to mark it as removed, setting a trace log level to avoid confusion. + log.Trace("Removed pending transaction with low nonce", "hash", hash.Hex()) } // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later costLimit := pool.currentState.GetBalance(addr) drops, invalids := list.FilterF(costLimit, pool.currentMaxGas, pool.executableTxFilter(costLimit)) for _, tx := range drops { hash := tx.Hash() - log.Trace("Removed unpayable pending transaction", "hash", hash.Hex()) pool.all.Remove(hash) pool.calculateTxsLifecycle(types.Transactions{tx}, time.Now()) + log.Debug("Removed unpayable pending transaction", "hash", hash.Hex()) } pendingNofundsMeter.Mark(int64(len(drops))) for _, tx := range invalids { hash := tx.Hash() - log.Trace("Demoting pending transaction", "hash", hash.Hex()) + log.Debug("Demoting pending transaction", "hash", hash.Hex()) // Internal shuffle shouldn't touch the lookup set. pool.enqueueTx(hash, tx, false, false) @@ -1866,7 +1910,7 @@ func (pool *TxPool) demoteUnexecutables() { gapped := list.Cap(0) for _, tx := range gapped { hash := tx.Hash() - log.Error("Demoting invalidated transaction", "hash", hash) + log.Error("Demoting invalidated transaction", "hash", hash.Hex()) // Internal shuffle shouldn't touch the lookup set. pool.enqueueTx(hash, tx, false, false) @@ -1895,6 +1939,57 @@ func (pool *TxPool) calculateTxsLifecycle(txs types.Transactions, t time.Time) { } } +// checkDelegationLimit determines if the tx sender is delegated or has a +// pending delegation, and if so, ensures they have at most one in-flight +// **executable** transaction, e.g. disallow stacked and gapped transactions +// from the account. +func (pool *TxPool) checkDelegationLimit(from common.Address, tx *types.Transaction) error { + // Short circuit if the sender has neither delegation nor pending delegation. + if pool.currentState.GetKeccakCodeHash(from) == codehash.EmptyKeccakCodeHash && len(pool.all.auths[from]) == 0 { + return nil + } + pending := pool.pending[from] + if pending == nil { + // Transaction with gapped nonce is not supported for delegated accounts + if pool.pendingNonces.get(from) != tx.Nonce() { + return ErrOutOfOrderTxFromDelegated + } + return nil + } + // Transaction replacement is supported + if pending.Overlaps(tx) { + return nil + } + return ErrInflightTxLimitReached +} + +// validateAuth verifies that the transaction complies with code authorization +// restrictions brought by SetCode transaction type. +func (pool *TxPool) validateAuth(from common.Address, tx *types.Transaction) error { + // Allow at most one in-flight tx for delegated accounts or those with a + // pending authorization. + if err := pool.checkDelegationLimit(from, tx); err != nil { + return err + } + // For symmetry, allow at most one in-flight tx for any authority with a + // pending transaction. + if auths := tx.SetCodeAuthorities(); len(auths) > 0 { + for _, auth := range auths { + var count int + if pending := pool.pending[auth]; pending != nil { + count += pending.Len() + } + if queue := pool.queue[auth]; queue != nil { + count += queue.Len() + } + if count > 1 { + return ErrAuthorityReserved + } + } + } + return nil +} + // PauseReorgs stops any new reorg jobs to be started but doesn't interrupt any existing ones that are in flight // Keep in mind this function might block, although it is not expected to block for any significant amount of time func (pool *TxPool) PauseReorgs() { @@ -2132,7 +2227,6 @@ func (t *txLookup) Remove(hash common.Hash) { t.lock.Lock() defer t.lock.Unlock() - t.removeAuthorities(hash) tx, ok := t.locals[hash] if !ok { tx, ok = t.remotes[hash] @@ -2141,6 +2235,7 @@ func (t *txLookup) Remove(hash common.Hash) { log.Error("No transaction found to be deleted", "hash", hash) return } + t.removeAuthorities(tx) t.slots -= numSlots(tx) slotsGauge.Update(int64(t.slots)) @@ -2196,8 +2291,9 @@ func (t *txLookup) addAuthorities(tx *types.Transaction) { // removeAuthorities stops tracking the supplied tx in relation to its // authorities. -func (t *txLookup) removeAuthorities(hash common.Hash) { - for addr := range t.auths { +func (t *txLookup) removeAuthorities(tx *types.Transaction) { + hash := tx.Hash() + for _, addr := range tx.SetCodeAuthorities() { list := t.auths[addr] // Remove tx from tracker. if i := slices.Index(list, hash); i >= 0 { @@ -2218,34 +2314,3 @@ func (t *txLookup) removeAuthorities(hash common.Hash) { func numSlots(tx *types.Transaction) int { return int((tx.Size() + txSlotSize - 1) / txSlotSize) } - -// usedAndLeftSlots returns the number of slots used and left for the given address. -func usedAndLeftSlots(pool *TxPool, addr common.Address) (int, int) { - var have int - if list := pool.pending[addr]; list != nil { - have += list.Len() - } - if list := pool.queue[addr]; list != nil { - have += list.Len() - } - if pool.currentState.GetKeccakCodeHash(addr) != codehash.EmptyKeccakCodeHash || len(pool.all.auths[addr]) != 0 { - // Allow at most one in-flight tx for delegated accounts or those with - // a pending authorization. - return have, max(0, 1-have) - } - return have, math.MaxInt -} - -// knownConflicts returns a list of addresses that conflict with the given authorities. -func knownConflicts(pool *TxPool, auths []common.Address) []common.Address { - var conflicts []common.Address - // Authorities cannot conflict with any pending or queued transactions. - for _, addr := range auths { - if list := pool.pending[addr]; list != nil { - conflicts = append(conflicts, addr) - } else if list := pool.queue[addr]; list != nil { - conflicts = append(conflicts, addr) - } - } - return conflicts -} diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index e97b8be96a..7d9501a807 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -24,6 +24,7 @@ import ( "math/big" "math/rand" "os" + "slices" "sync/atomic" "testing" "time" @@ -156,6 +157,10 @@ func pricedSetCodeTx(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, ke }) authList = append(authList, auth) } + return pricedSetCodeTxWithAuth(nonce, gaslimit, gasFee, tip, key, authList) +} + +func pricedSetCodeTxWithAuth(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, authList []types.SetCodeAuthorization) *types.Transaction { return types.MustSignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.SetCodeTx{ ChainID: uint256.MustFromBig(params.TestChainConfig.ChainID), Nonce: nonce, @@ -214,6 +219,34 @@ func validateTxPoolInternals(pool *TxPool) error { return fmt.Errorf("pending nonce mismatch: have %v, want %v", nonce, last+1) } } + // Ensure all auths in pool are tracked + for _, tx := range pool.all.locals { + for _, addr := range tx.SetCodeAuthorities() { + list := pool.all.auths[addr] + if i := slices.Index(list, tx.Hash()); i < 0 { + return fmt.Errorf("authority not tracked: addr %s, tx %s", addr, tx.Hash()) + } + } + } + for _, tx := range pool.all.remotes { + for _, addr := range tx.SetCodeAuthorities() { + list := pool.all.auths[addr] + if i := slices.Index(list, tx.Hash()); i < 0 { + return fmt.Errorf("authority not tracked: addr %s, tx %s", addr, tx.Hash()) + } + } + } + // Ensure all auths in pool have an associated tx in locals or remotes. + for addr, hashes := range pool.all.auths { + for _, hash := range hashes { + _, inLocals := pool.all.locals[hash] + _, inRemotes := pool.all.remotes[hash] + + if !inLocals && !inRemotes { + return fmt.Errorf("dangling authority, missing originating tx: addr %s, hash %s", addr, hash.Hex()) + } + } + } return nil } @@ -2689,29 +2722,98 @@ func TestSetCodeTransactions(t *testing.T) { }{ { // Check that only one in-flight transaction is allowed for accounts - // with delegation set. Also verify the accepted transaction can be - // replaced by fee. - name: "only-one-in-flight", + // with delegation set. + name: "accept-one-inflight-tx-of-delegated-account", pending: 1, run: func(name string, pool *TxPool, statedb *state.StateDB) { aa := common.Address{0xaa, 0xaa} statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...)) statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)}) + + // Send gapped transaction, it should be rejected. + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrOutOfOrderTxFromDelegated) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrOutOfOrderTxFromDelegated, err) + } // Send transactions. First is accepted, second is rejected. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyA)); err != nil { t.Fatalf("%s: failed to add remote transaction: %v", name, err) } - if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrAccountLimitExceeded, err) + // Second and further transactions shall be rejected + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } - // Also check gapped transaction. - if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrAccountLimitExceeded, err) + // Check gapped transaction again. + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } // Replace by fee. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyA)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } + + // Reset the delegation, avoid leaking state into the other tests + statedb.SetCode(addrA, nil) + }, + }, + { + // This test is analogous to the previous one, but the delegation is pending + // instead of set. + name: "allow-one-tx-from-pooled-delegation", + pending: 2, + run: func(name string, pool *TxPool, statedb *state.StateDB) { + // Create a pending delegation request from B. + if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyB}})); err != nil { + t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) + } + // First transaction from B is accepted. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyB)); err != nil { + t.Fatalf("%s: failed to add remote transaction: %v", name, err) + } + // Second transaction fails due to limit. + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) + } + // Replace by fee for first transaction from B works. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(2), keyB)); err != nil { + t.Fatalf("%s: failed to add remote transaction: %v", name, err) + } + }, + }, + { + // This is the symmetric case of the previous one, where the delegation request + // is received after the transaction. The resulting state shall be the same. + name: "accept-authorization-from-sender-of-one-inflight-tx", + pending: 2, + run: func(name string, pool *TxPool, statedb *state.StateDB) { + // The first in-flight transaction is accepted. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyB)); err != nil { + t.Fatalf("%s: failed to add with pending delegation: %v", name, err) + } + // Delegation is accepted. + if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyB}})); err != nil { + t.Fatalf("%s: failed to add remote transaction: %v", name, err) + } + // The second in-flight transaction is rejected. + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) + } + }, + }, + { + name: "reject-authorization-from-sender-with-more-than-one-inflight-tx", + pending: 2, + run: func(name string, pool *TxPool, statedb *state.StateDB) { + // Submit two transactions. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyB)); err != nil { + t.Fatalf("%s: failed to add with pending delegation: %v", name, err) + } + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); err != nil { + t.Fatalf("%s: failed to add with pending delegation: %v", name, err) + } + // Delegation rejected since two txs are already in-flight. + if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyB}})); !errors.Is(err, ErrAuthorityReserved) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrAuthorityReserved, err) + } }, }, { @@ -2719,7 +2821,7 @@ func TestSetCodeTransactions(t *testing.T) { pending: 2, run: func(name string, pool *TxPool, statedb *state.StateDB) { // Send two transactions where the first has no conflicting delegations and - // the second should be allowed despite conflicting with the authorities in 1). + // the second should be allowed despite conflicting with the authorities in the first. if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } @@ -2728,28 +2830,10 @@ func TestSetCodeTransactions(t *testing.T) { } }, }, - { - name: "allow-one-tx-from-pooled-delegation", - pending: 2, - run: func(name string, pool *TxPool, statedb *state.StateDB) { - // Verify C cannot originate another transaction when it has a pooled delegation. - if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyC}})); err != nil { - t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) - } - if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyC)); err != nil { - t.Fatalf("%s: failed to add with pending delegation: %v", name, err) - } - // Also check gapped transaction is rejected. - if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyC)); !errors.Is(err, ErrAccountLimitExceeded) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrAccountLimitExceeded, err) - } - }, - }, { name: "replace-by-fee-setcode-tx", pending: 1, run: func(name string, pool *TxPool, statedb *state.StateDB) { - // 4. Fee bump the setcode tx send. if err := pool.addRemoteSync(setCodeTx(0, keyB, []unsignedAuth{{1, keyC}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } @@ -2760,42 +2844,87 @@ func TestSetCodeTransactions(t *testing.T) { }, { name: "allow-tx-from-replaced-authority", - pending: 2, + pending: 3, run: func(name string, pool *TxPool, statedb *state.StateDB) { - // Fee bump with a different auth list. Make sure that unlocks the authorities. + // Send transaction from A with B as an authority. if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyB}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } + // Replace transaction with another having C as an authority. if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(3000), uint256.NewInt(300), keyA, []unsignedAuth{{0, keyC}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } - // Now send a regular tx from B. + // B should not be considred as having an in-flight delegation, so + // should allow more than one pooled transaction. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(10), keyB)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(10), keyB)); err != nil { + t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) + } }, }, { + // This test is analogous to the previous one, but the the replaced + // transaction is self-sponsored. name: "allow-tx-from-replaced-self-sponsor-authority", - pending: 2, + pending: 3, run: func(name string, pool *TxPool, statedb *state.StateDB) { + // Reset the delegation + statedb.SetCode(addrA, nil) + + // Send transaction from A with A as an authority. if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyA}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } + // Replace transaction with a transaction with B as an authority. if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(30), uint256.NewInt(30), keyA, []unsignedAuth{{0, keyB}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } - // Now send a regular tx from keyA. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyA)); err != nil { + // The one in-flight transaction limit from A no longer applies, so we + // can stack a second transaction for the account. + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyA)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } - // Make sure we can still send from keyB. + // B should still be able to send transactions. if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyB)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } + // However B still has the limitation to one in-flight transaction. + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) + } }, }, { + name: "replacements-respect-inflight-tx-count", + pending: 2, + run: func(name string, pool *TxPool, statedb *state.StateDB) { + // Send transaction from A with B as an authority. + if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, []unsignedAuth{{0, keyB}})); err != nil { + t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) + } + // Send two transactions from B. Only the first should be accepted due + // to in-flight limit. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), keyB)); err != nil { + t.Fatalf("%s: failed to add remote transaction: %v", name, err) + } + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) + } + // Replace the in-flight transaction from B. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(30), keyB)); err != nil { + t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) + } + // Ensure the in-flight limit for B is still in place. + if err := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1), keyB)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) + } + }, + }, + { + // Since multiple authorizations can be pending simultaneously, replacing + // one of them should not break the one in-flight-transaction limit. name: "track-multiple-conflicting-delegations", pending: 3, run: func(name string, pool *TxPool, statedb *state.StateDB) { @@ -2815,20 +2944,7 @@ func TestSetCodeTransactions(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil { t.Fatalf("%s: failed to added single pooled for account with pending delegation: %v", name, err) } - if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyC)), ErrAccountLimitExceeded; !errors.Is(err, want) { - t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err) - } - }, - }, - { - name: "reject-delegation-from-pending-account", - pending: 1, - run: func(name string, pool *TxPool, statedb *state.StateDB) { - // Attempt to submit a delegation from an account with a pending tx. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keyC)); err != nil { - t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) - } - if err, want := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})), ErrAuthorityReserved; !errors.Is(err, want) { + if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, big.NewInt(1000), keyC)), ErrInflightTxLimitReached; !errors.Is(err, want) { t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err) } }, @@ -2846,6 +2962,32 @@ func TestSetCodeTransactions(t *testing.T) { } }, }, + { + name: "remove-hash-from-authority-tracker", + pending: 10, + run: func(name string, pool *TxPool, statedb *state.StateDB) { + var keys []*ecdsa.PrivateKey + for i := 0; i < 30; i++ { + key, _ := crypto.GenerateKey() + keys = append(keys, key) + addr := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, addr, big.NewInt(params.Ether)) + } + // Create a transactions with 3 unique auths so the lookup's auth map is + // filled with addresses. + for i := 0; i < 30; i += 3 { + if err := pool.addRemoteSync(pricedSetCodeTx(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keys[i], []unsignedAuth{{0, keys[i]}, {0, keys[i+1]}, {0, keys[i+2]}})); err != nil { + t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) + } + } + // Replace one of the transactions with a normal transaction so that the + // original hash is removed from the tracker. The hash should be + // associated with 3 different authorities. + if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1000), keys[0])); err != nil { + t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) + } + }, + }, } { // Create the pool to test the status retrievals with statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) @@ -2875,6 +3017,64 @@ func TestSetCodeTransactions(t *testing.T) { } } +func TestSetCodeTransactionsReorg(t *testing.T) { + t.Parallel() + + // Create the pool to test the status retrievals with + statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + blockchain := &testBlockChain{1000000, statedb, new(event.Feed)} + + pool := NewTxPool(testTxPoolConfig, params.TestChainConfig, blockchain) + defer pool.Stop() + + // Create the test accounts + var ( + keyA, _ = crypto.GenerateKey() + addrA = crypto.PubkeyToAddress(keyA.PublicKey) + ) + testAddBalance(pool, addrA, big.NewInt(params.Ether)) + // Send an authorization for 0x42 + var authList []types.SetCodeAuthorization + auth, _ := types.SignSetCode(keyA, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(params.TestChainConfig.ChainID), + Address: common.Address{0x42}, + Nonce: 0, + }) + authList = append(authList, auth) + if err := pool.addRemoteSync(pricedSetCodeTxWithAuth(0, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, authList)); err != nil { + t.Fatalf("failed to add with remote setcode transaction: %v", err) + } + // Simulate the chain moving + blockchain.statedb.SetNonce(addrA, 1) + blockchain.statedb.SetCode(addrA, types.AddressToDelegation(auth.Address)) + <-pool.requestReset(nil, nil) + // Set an authorization for 0x00 + auth, _ = types.SignSetCode(keyA, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(params.TestChainConfig.ChainID), + Address: common.Address{}, + Nonce: 0, + }) + authList = append(authList, auth) + if err := pool.addRemoteSync(pricedSetCodeTxWithAuth(1, 250000, uint256.NewInt(10), uint256.NewInt(3), keyA, authList)); err != nil { + t.Fatalf("failed to add with remote setcode transaction: %v", err) + } + // Try to add a transactions in + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1000), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + t.Fatalf("unexpected error %v, expecting %v", err, ErrInflightTxLimitReached) + } + // Simulate the chain moving + blockchain.statedb.SetNonce(addrA, 2) + blockchain.statedb.SetCode(addrA, nil) + <-pool.requestReset(nil, nil) + // Now send two transactions from addrA + if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1000), keyA)); err != nil { + t.Fatalf("failed to added single transaction: %v", err) + } + if err := pool.addRemoteSync(pricedTransaction(3, 100000, big.NewInt(1000), keyA)); err != nil { + t.Fatalf("failed to added single transaction: %v", err) + } +} + // Benchmarks the speed of validating the contents of the pending queue of the // transaction pool. func BenchmarkPendingDemotion100(b *testing.B) { benchmarkPendingDemotion(b, 100) } diff --git a/core/types/transaction.go b/core/types/transaction.go index 6a5a50f8b2..4f88df833d 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -477,15 +477,23 @@ func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization { return setcodetx.AuthList } -// SetCodeAuthorities returns a list of each authorization's corresponding authority. +// SetCodeAuthorities returns a list of unique authorities from the +// authorization list. func (tx *Transaction) SetCodeAuthorities() []common.Address { setcodetx, ok := tx.inner.(*SetCodeTx) if !ok { return nil } - auths := make([]common.Address, 0, len(setcodetx.AuthList)) + var ( + marks = make(map[common.Address]bool) + auths = make([]common.Address, 0, len(setcodetx.AuthList)) + ) for _, auth := range setcodetx.AuthList { if addr, err := auth.Authority(); err == nil { + if marks[addr] { + continue + } + marks[addr] = true auths = append(auths, addr) } } diff --git a/core/vm/access_list_tracer.go b/core/vm/access_list_tracer.go index 997b169ec5..6342dfd3ad 100644 --- a/core/vm/access_list_tracer.go +++ b/core/vm/access_list_tracer.go @@ -115,16 +115,10 @@ type AccessListTracer struct { // NewAccessListTracer creates a new tracer that can generate AccessLists. // An optional AccessList can be specified to occupy slots and addresses in // the resulting accesslist. -func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompiles []common.Address) *AccessListTracer { - excl := map[common.Address]struct{}{ - from: {}, to: {}, - } - for _, addr := range precompiles { - excl[addr] = struct{}{} - } +func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer { list := newAccessList() for _, al := range acl { - if _, ok := excl[al.Address]; !ok { + if _, ok := addressesToExclude[al.Address]; !ok { list.addAddress(al.Address) } for _, slot := range al.StorageKeys { @@ -132,7 +126,7 @@ func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompi } } return &AccessListTracer{ - excl: excl, + excl: addressesToExclude, list: list, } } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index d115182d49..97b2297700 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -80,7 +80,7 @@ var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{ common.BytesToAddress([]byte{5}): &bigModExp{eip2565: false}, common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, - common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{limitInputLength: false}, common.BytesToAddress([]byte{9}): &blake2F{}, } @@ -94,7 +94,7 @@ var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{ 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{8}): &bn256PairingIstanbul{limitInputLength: false}, common.BytesToAddress([]byte{9}): &blake2F{}, } @@ -108,7 +108,7 @@ var PrecompiledContractsArchimedes = map[common.Address]PrecompiledContract{ 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{8}): &bn256PairingIstanbul{limitInputLength: true}, common.BytesToAddress([]byte{9}): &blake2FDisabled{}, } @@ -122,7 +122,7 @@ var PrecompiledContractsBernoulli = map[common.Address]PrecompiledContract{ 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{8}): &bn256PairingIstanbul{limitInputLength: true}, common.BytesToAddress([]byte{9}): &blake2FDisabled{}, } @@ -136,7 +136,22 @@ var PrecompiledContractsEuclidV2 = map[common.Address]PrecompiledContract{ 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{8}): &bn256PairingIstanbul{limitInputLength: true}, + common.BytesToAddress([]byte{9}): &blake2FDisabled{}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, +} + +// PrecompiledContractsFeynman contains the default set of pre-compiled Ethereum +// contracts used in the Feynman release. +var PrecompiledContractsFeynman = map[common.Address]PrecompiledContract{ + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hashDisabled{}, + 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{limitInputLength: false}, common.BytesToAddress([]byte{9}): &blake2FDisabled{}, common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, } @@ -156,6 +171,7 @@ var PrecompiledContractsBLS = map[common.Address]PrecompiledContract{ } var ( + PrecompiledAddressesFeynman []common.Address PrecompiledAddressesEuclidV2 []common.Address PrecompiledAddressesBernoulli []common.Address PrecompiledAddressesArchimedes []common.Address @@ -187,11 +203,16 @@ func init() { for k := range PrecompiledContractsEuclidV2 { PrecompiledAddressesEuclidV2 = append(PrecompiledAddressesEuclidV2, k) } + for k := range PrecompiledContractsFeynman { + PrecompiledAddressesFeynman = append(PrecompiledAddressesFeynman, k) + } } // ActivePrecompiles returns the precompiles enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsFeynman: + return PrecompiledAddressesFeynman case rules.IsEuclidV2: return PrecompiledAddressesEuclidV2 case rules.IsBernoulli: @@ -600,10 +621,6 @@ var ( // runBn256Pairing implements the Bn256Pairing precompile, referenced by both // Byzantium and Istanbul operations. func runBn256Pairing(input []byte) ([]byte, error) { - // Allow at most 4 inputs - if len(input) > 4*192 { - return nil, errBadPairingInput - } // Handle some corner cases cheaply if len(input)%192 > 0 { return nil, errBadPairingInput @@ -634,7 +651,9 @@ func runBn256Pairing(input []byte) ([]byte, error) { // bn256PairingIstanbul implements a pairing pre-compile for the bn256 curve // conforming to Istanbul consensus rules. -type bn256PairingIstanbul struct{} +type bn256PairingIstanbul struct { + limitInputLength bool +} // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bn256PairingIstanbul) RequiredGas(input []byte) uint64 { @@ -642,6 +661,13 @@ func (c *bn256PairingIstanbul) RequiredGas(input []byte) uint64 { } func (c *bn256PairingIstanbul) Run(input []byte) ([]byte, error) { + if c.limitInputLength { + // Allow at most 4 inputs + if len(input) > 4*192 { + return nil, errBadPairingInput + } + } + return runBn256Pairing(input) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index e028d45f65..f6217b0ffd 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -65,6 +65,7 @@ var allPrecompiles = map[common.Address]PrecompiledContract{ common.BytesToAddress([]byte{16}): &bls12381Pairing{}, common.BytesToAddress([]byte{17}): &bls12381MapG1{}, common.BytesToAddress([]byte{18}): &bls12381MapG2{}, + common.BytesToAddress([]byte{0xf8}): &bn256PairingIstanbul{limitInputLength: true}, common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, } @@ -264,7 +265,7 @@ func BenchmarkPrecompiledBn256ScalarMul(b *testing.B) { benchJson("bn256ScalarMu // Tests the sample inputs from the elliptic curve pairing check EIP 197. func TestPrecompiledBn256Pairing(t *testing.T) { testJson("bn256Pairing", "08", t) } -func TestPrecompiledBn256PairingFail(t *testing.T) { testJsonFail("bn256Pairing", "08", t) } +func TestPrecompiledBn256PairingFail(t *testing.T) { testJsonFail("bn256Pairing", "f8", t) } func BenchmarkPrecompiledBn256Pairing(b *testing.B) { benchJson("bn256Pairing", "08", b) } func TestPrecompiledBlake2F(t *testing.T) { testJson("blake2F", "09", t) } diff --git a/core/vm/evm.go b/core/vm/evm.go index afb5c7cb39..0514677c61 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -47,6 +47,8 @@ type ( func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { var precompiles map[common.Address]PrecompiledContract switch { + case evm.chainRules.IsFeynman: + precompiles = PrecompiledContractsFeynman case evm.chainRules.IsEuclidV2: precompiles = PrecompiledContractsEuclidV2 case evm.chainRules.IsBernoulli: diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index fe7c34759c..cc1cdcee39 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -100,6 +100,10 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi y, x = stack.Back(1), stack.Back(0) current = evm.StateDB.GetState(contract.Address(), x.Bytes32()) ) + + // Try updating the witness of SSTORE at first to align with reth's witness implementation. + original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) + // The legacy gas metering only takes into consideration the current state // Legacy rules should be applied if we are in Petersburg (removal of EIP-1283) // OR Constantinople is not active @@ -137,7 +141,6 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi if current == value { // noop (1) return params.NetSstoreNoopGas, nil } - original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { if original == (common.Hash{}) { // create slot (2.1.1) return params.NetSstoreInitGas, nil @@ -178,10 +181,6 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi // 2.2.2.1. If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. // 2.2.2.2. Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. func gasSStoreEIP2200(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.Back(0) @@ -189,10 +188,17 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m ) value := common.Hash(y.Bytes32()) + // Try updating the witness of SSTORE at first to align with reth's witness implementation. + original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) + + // 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") + } + if current == value { // noop (1) return params.SloadGasEIP2200, nil } - original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { if original == (common.Hash{}) { // create slot (2.1.1) return params.SstoreSetGasEIP2200, nil diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 36a467a8f3..8027afae76 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -470,6 +470,32 @@ func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ( return nil, nil } +func opBlockhashPostFeynman(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + num := scope.Stack.peek() + num64, overflow := num.Uint64WithOverflow() + if overflow { + num.Clear() + return nil, nil + } + var upper, lower uint64 + upper = interpreter.evm.Context.BlockNumber.Uint64() + if upper < 257 { + lower = 0 + } else { + lower = upper - 256 + } + if num64 >= lower && num64 < upper { + res := interpreter.evm.Context.GetHash(num64) + if witness := interpreter.evm.StateDB.Witness(); witness != nil { + witness.AddBlockHash(num64) + } + num.SetBytes(res[:]) + } else { + num.Clear() + } + return nil, nil +} + func opCoinbase(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetBytes(interpreter.evm.FeeRecipient().Bytes())) return nil, nil diff --git a/core/vm/interface.go b/core/vm/interface.go index fe27905e91..2d19515c37 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/stateless" "github.com/scroll-tech/go-ethereum/core/types" "github.com/scroll-tech/go-ethereum/params" ) @@ -85,6 +86,8 @@ type StateDB interface { AddPreimage(common.Hash, []byte) ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error + + Witness() *stateless.Witness } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 81de5f3c05..28b5972e7d 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -74,6 +74,8 @@ func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter { if cfg.JumpTable[STOP] == nil { var jt JumpTable switch { + case evm.chainRules.IsFeynman: + jt = feynmanInstructionSet case evm.chainRules.IsEuclidV2: jt = euclidV2InstructionSet case evm.chainRules.IsDarwin: diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 5c46758dbf..92a1a8c790 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -62,11 +62,24 @@ var ( curieInstructionSet = newCurieInstructionSet() darwinInstructionSet = newDarwinInstructionSet() euclidV2InstructionSet = newEuclidV2InstructionSet() + feynmanInstructionSet = newFeynmanInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. type JumpTable [256]*operation +// newFeynmanInstructionSet returns the frontier, homestead, byzantium, +// contantinople, istanbul, petersburg, berlin, london, shanghai, curie, darwin, euclidV2, +// and feynman instructions. +func newFeynmanInstructionSet() JumpTable { + instructionSet := newEuclidV2InstructionSet() + + // change block hash opcode implementation + instructionSet[BLOCKHASH].execute = opBlockhashPostFeynman + + return instructionSet +} + // newEuclidV2InstructionSet returns the frontier, homestead, byzantium, // contantinople, istanbul, petersburg, berlin, london, shanghai, curie, darwin and euclidV2 instructions. func newEuclidV2InstructionSet() JumpTable { diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index d9a8b44102..0a53d62ada 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -27,10 +27,6 @@ import ( 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() @@ -38,6 +34,15 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { current = evm.StateDB.GetState(contract.Address(), slot) cost = uint64(0) ) + + // Try updating the witness of SSTORE at first to align with reth's witness implementation. + original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) + + // 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") + } + // Check slot presence in the access list if addrPresent, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { cost = params.ColdSloadCostEIP2929 @@ -57,7 +62,6 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { // return params.SloadGasEIP2200, nil return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS } - original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { if original == (common.Hash{}) { // create slot (2.1.1) return cost + params.SstoreSetGasEIP2200, nil @@ -109,6 +113,14 @@ func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me // 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) + + // Try updating the witness of SLOAD to align with reth's witness implementation. + // Another place that needs to change is when calculating the gas cost after Frontier and before EIP-2929. + // Frontier gas cost simply uses params.SloadGasFrontier (i.e., 50 gas), so changing the gas cost there + // might affect code cleanliness. Usually, this won't be a problem because EIP-2929 is enabled by default. + // Thus, adding the SLOAD witness before EIP-2929 is left as a TODO here. + evm.StateDB.GetCommittedState(contract.Address(), loc.Bytes32()) + return params.ColdSloadCostEIP2929, nil } return params.WarmStorageReadCostEIP2929, nil diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 7e4029aec4..2d21fd37c1 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -74,6 +74,8 @@ func setDefaults(cfg *Config) { CurieBlock: new(big.Int), DarwinTime: new(uint64), DarwinV2Time: new(uint64), + EuclidV2Time: new(uint64), + FeynmanTime: new(uint64), } } diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index fa1556fa59..a1b5a9d5b2 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -337,17 +337,13 @@ func TestBlockhash(t *testing.T) { if zero.BitLen() != 0 { t.Fatalf("expected zeroes, got %x", ret[0:32]) } - firstExpectedHash := new(big.Int) - firstExpectedHash.SetString("13215081625009140218242111988507489764601005198286886925088730931502473149599", 10) - if first.Uint64() != firstExpectedHash.Uint64() { - t.Fatalf("first hash should be 13215081625009140218242111988507489764601005198286886925088730931502473149599, got %d (%x)", first, ret[32:64]) + if first.Uint64() != 999 { + t.Fatalf("second block should be 999, got %d (%x)", first, ret[32:64]) } - lastExpectedHash := new(big.Int) - lastExpectedHash.SetString("2851160567348483005169712516804956111111231427377973738952179767509712807467", 10) - if last.Uint64() != lastExpectedHash.Uint64() { - t.Fatalf("last hash should be 2851160567348483005169712516804956111111231427377973738952179767509712807467, got %d (%x)", last, ret[64:96]) + if last.Uint64() != 744 { + t.Fatalf("last block should be 744, got %d (%x)", last, ret[64:96]) } - if exp, got := 0, chain.counter; exp != got { + if exp, got := 255, chain.counter; exp != got { t.Errorf("suboptimal; too much chain iteration, expected %d, got %d", exp, got) } } diff --git a/core/vm/testdata/precompiles/bn256Pairing.json b/core/vm/testdata/precompiles/bn256Pairing.json index bd06969aa8..3fbed6b87c 100644 --- a/core/vm/testdata/precompiles/bn256Pairing.json +++ b/core/vm/testdata/precompiles/bn256Pairing.json @@ -76,6 +76,20 @@ "Gas": 113000, "NoBenchmark": false }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d", + "Expected": "0000000000000000000000000000000000000000000000000000000000000001", + "Name": "ten_point_match_1", + "Gas": 385000, + "NoBenchmark": false + }, + { + "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd31a76dae6d3272396d0cbe61fced2bc532edac647851e3ac53ce1cc9c7e645a83198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd31a76dae6d3272396d0cbe61fced2bc532edac647851e3ac53ce1cc9c7e645a83198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd31a76dae6d3272396d0cbe61fced2bc532edac647851e3ac53ce1cc9c7e645a83198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd31a76dae6d3272396d0cbe61fced2bc532edac647851e3ac53ce1cc9c7e645a83198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd31a76dae6d3272396d0cbe61fced2bc532edac647851e3ac53ce1cc9c7e645a83198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", + "Expected": "0000000000000000000000000000000000000000000000000000000000000001", + "Name": "ten_point_match_2", + "Gas": 385000, + "NoBenchmark": false + }, { "Input": "105456a333e6d636854f987ea7bb713dfd0ae8371a72aea313ae0c32c0bf10160cf031d41b41557f3e7e3ba0c51bebe5da8e6ecd855ec50fc87efcdeac168bcc0476be093a6d2b4bbf907172049874af11e1b6267606e00804d3ff0037ec57fd3010c68cb50161b7d1d96bb71edfec9880171954e56871abf3d93cc94d745fa114c059d74e5b6c4ec14ae5864ebe23a71781d86c29fb8fb6cce94f70d3de7a2101b33461f39d9e887dbb100f170a2345dde3c07e256d1dfa2b657ba5cd030427000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000021a2c3013d2ea92e13c800cde68ef56a294b883f6ac35d25f587c09b1b3c635f7290158a80cd3d66530f74dc94c94adb88f5cdb481acca997b6e60071f08a115f2f997f3dbd66a7afe07fe7862ce239edba9e05c5afff7f8a1259c9733b2dfbb929d1691530ca701b4a106054688728c9972c8512e9789e9567aae23e302ccd75", "Expected": "0000000000000000000000000000000000000000000000000000000000000001", diff --git a/eth/api.go b/eth/api.go index 8a0f094eef..0752a3cb15 100644 --- a/eth/api.go +++ b/eth/api.go @@ -29,6 +29,8 @@ import ( "strings" "time" + "github.com/syndtr/goleveldb/leveldb" + "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/common/hexutil" "github.com/scroll-tech/go-ethereum/core" @@ -131,6 +133,8 @@ func (api *PrivateMinerAPI) SetGasPrice(gasPrice hexutil.Big) bool { api.e.gasPrice = (*big.Int)(&gasPrice) api.e.lock.Unlock() + // This will override the min base fee configuration. + // That is fine, it only happens if we explicitly set gas price via the console. api.e.txPool.SetGasPrice((*big.Int)(&gasPrice)) return true } @@ -353,13 +357,11 @@ func generateWitness(blockchain *core.BlockChain, block *types.Block) (*stateles if err != nil { return nil, fmt.Errorf("failed to retrieve parent state: %w", err) } + statedb.WithWitness(witness) // Collect storage locations that prover needs but sequencer might not touch necessarily statedb.GetState(rcfg.L2MessageQueueAddress, rcfg.WithdrawTrieRootSlot) - statedb.StartPrefetcher("debug_execution_witness", witness) - defer statedb.StopPrefetcher() - receipts, _, usedGas, err := blockchain.Processor().Process(block, statedb, *blockchain.GetVMConfig()) if err != nil { return nil, fmt.Errorf("failed to process block %d: %w", block.Number(), err) @@ -368,36 +370,57 @@ func generateWitness(blockchain *core.BlockChain, block *types.Block) (*stateles if err := blockchain.Validator().ValidateState(block, statedb, receipts, usedGas); err != nil { return nil, fmt.Errorf("failed to validate block %d: %w", block.Number(), err) } - return witness, testWitness(blockchain, block, witness) + + if err = testWitness(blockchain, block, witness); err != nil { + return nil, err + } + return witness, nil } func testWitness(blockchain *core.BlockChain, block *types.Block, witness *stateless.Witness) error { stateRoot := witness.Root() - if diskRoot, _ := rawdb.ReadDiskStateRoot(blockchain.Database(), stateRoot); diskRoot != (common.Hash{}) { + diskRoot, err := rawdb.ReadDiskStateRoot(blockchain.Database(), stateRoot) + if err != nil && !errors.Is(err, leveldb.ErrNotFound) { + return fmt.Errorf("failed to read disk state root for stateRoot %s: %w", stateRoot.Hex(), err) + } + if diskRoot != (common.Hash{}) { + log.Debug("Using disk root for state root", "stateRoot", stateRoot.Hex(), "diskRoot", diskRoot.Hex()) stateRoot = diskRoot } // Create and populate the state database to serve as the stateless backend statedb, err := state.New(stateRoot, state.NewDatabase(witness.MakeHashDB()), nil) if err != nil { - return fmt.Errorf("failed to create state database: %w", err) + return fmt.Errorf("failed to create state database with stateRoot %s: %w", stateRoot.Hex(), err) } receipts, _, usedGas, err := blockchain.Processor().Process(block, statedb, *blockchain.GetVMConfig()) if err != nil { - return fmt.Errorf("failed to process block %d: %w", block.Number(), err) + return fmt.Errorf("failed to process block %d (hash: %s): %w", block.Number(), block.Hash().Hex(), err) } if err := blockchain.Validator().ValidateState(block, statedb, receipts, usedGas); err != nil { - return fmt.Errorf("failed to validate block %d: %w", block.Number(), err) + return fmt.Errorf("failed to validate block %d (hash: %s): %w", block.Number(), block.Hash().Hex(), err) } postStateRoot := block.Root() - if diskRoot, _ := rawdb.ReadDiskStateRoot(blockchain.Database(), postStateRoot); diskRoot != (common.Hash{}) { + diskRoot, err = rawdb.ReadDiskStateRoot(blockchain.Database(), postStateRoot) + if err != nil && !errors.Is(err, leveldb.ErrNotFound) { + return fmt.Errorf("failed to read disk state root for postStateRoot %s: %w", postStateRoot.Hex(), err) + } + if diskRoot != (common.Hash{}) { + log.Debug("Using disk root for post state root", "postStateRoot", postStateRoot.Hex(), "diskRoot", diskRoot.Hex()) postStateRoot = diskRoot } - if statedb.GetRootHash() != postStateRoot { - return fmt.Errorf("failed to commit statelessly %d: %w", block.Number(), err) + computedRoot := statedb.GetRootHash() + if computedRoot != postStateRoot { + log.Debug("State root mismatch", "block", block.Number(), "expected", postStateRoot.Hex(), "got", computedRoot) + executionWitness := ToExecutionWitness(witness) + jsonStr, err := json.Marshal(executionWitness) + if err != nil { + return fmt.Errorf("state root mismatch after processing block %d (hash: %s): expected %s, got %s, but failed to marshal witness: %w", block.Number(), block.Hash().Hex(), postStateRoot.Hex(), computedRoot, err) + } + return fmt.Errorf("state root mismatch after processing block %d (hash: %s): expected %s, got %s, witness: %s", block.Number(), block.Hash().Hex(), postStateRoot.Hex(), computedRoot, string(jsonStr)) } return nil } diff --git a/eth/backend.go b/eth/backend.go index d6902f089d..ad765b835b 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -32,6 +32,7 @@ import ( "github.com/scroll-tech/go-ethereum/common/hexutil" "github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus/clique" + "github.com/scroll-tech/go-ethereum/consensus/misc" "github.com/scroll-tech/go-ethereum/consensus/system_contract" "github.com/scroll-tech/go-ethereum/consensus/wrapper" "github.com/scroll-tech/go-ethereum/core" @@ -213,6 +214,12 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client l1.Client) (*Ether eth.blockchain.Validator().WithAsyncValidator(eth.asyncChecker.Check) } + state, err := eth.blockchain.State() + if err != nil { + return nil, err + } + misc.InitializeL2BaseFeeCoefficients(chainConfig, state) + // Rewind the chain in case of an incompatible config upgrade. if compat, ok := genesisErr.(*params.ConfigCompatError); ok { log.Warn("Rewinding chain to upgrade configuration", "err", compat) @@ -225,6 +232,7 @@ func New(stack *node.Node, config *ethconfig.Config, l1Client l1.Client) (*Ether config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal) } eth.txPool = core.NewTxPool(config.TxPool, chainConfig, eth.blockchain) + eth.txPool.SetGasPrice(misc.MinBaseFee()) // Initialize and start DA syncing pipeline before SyncService as SyncService is blocking until all L1 messages are loaded. // We need SyncService to load the L1 messages for DA syncing, but since both sync from last known L1 state, we can @@ -354,6 +362,9 @@ func (s *Ethereum) APIs() []rpc.API { // Append any APIs exposed explicitly by the consensus engine apis = append(apis, s.engine.APIs(s.BlockChain())...) + // Append L2 base fee APIs. + apis = append(apis, misc.APIs()...) + if !s.config.EnableDASyncing { apis = append(apis, rpc.API{ Namespace: "eth", @@ -520,10 +531,11 @@ func (s *Ethereum) StartMining(threads int) error { // If the miner was not running, initialize it if !s.IsMining() { // Propagate the initial price point to the transaction pool - s.lock.RLock() - price := s.gasPrice - s.lock.RUnlock() - s.txPool.SetGasPrice(price) + // Disabled, we now update min gas price automatically via L2 base fee. + // s.lock.RLock() + // price := s.gasPrice + // s.lock.RUnlock() + // s.txPool.SetGasPrice(price) // Configure the local mining address eb, err := s.Etherbase() diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go index 24fec71020..dba52701f9 100644 --- a/eth/fetcher/tx_fetcher.go +++ b/eth/fetcher/tx_fetcher.go @@ -794,7 +794,7 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, return true // continue in the for-each }) - log.Debug("Scheduling transaction retrieval", "peer", peer, "len(f.announces[peer])", len(f.announces[peer]), "len(hashes)", len(hashes)) + log.Trace("Scheduling transaction retrieval", "peer", peer, "len(f.announces[peer])", len(f.announces[peer]), "len(hashes)", len(hashes)) peerAnnounceTxsLenGauge.Update(int64(len(f.announces[peer]))) peerRetrievalTxsLenGauge.Update(int64(len(hashes))) diff --git a/eth/gasprice/feehistory.go b/eth/gasprice/feehistory.go index 0d38d7628e..571dfa3d74 100644 --- a/eth/gasprice/feehistory.go +++ b/eth/gasprice/feehistory.go @@ -84,7 +84,7 @@ func (s sortGasAndReward) Less(i, j int) bool { // processBlock takes a blockFees structure with the blockNumber, the header and optionally // the block field filled in, retrieves the block from the backend if not present yet and // fills in the rest of the fields. -func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { +func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64, nonCongestedPrice *big.Int) { chainconfig := oracle.backend.ChainConfig() if bf.results.baseFee = bf.header.BaseFee; bf.results.baseFee == nil { bf.results.baseFee = new(big.Int) @@ -121,7 +121,12 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { sorter := make(sortGasAndReward, len(bf.block.Transactions())) for i, tx := range bf.block.Transactions() { - reward, _ := tx.EffectiveGasTip(bf.block.BaseFee()) + var reward *big.Int + if nonCongestedPrice != nil { + reward = nonCongestedPrice + } else { + reward, _ = tx.EffectiveGasTip(bf.block.BaseFee()) + } sorter[i] = txGasAndReward{gasUsed: bf.receipts[i].GasUsed, reward: reward} } sort.Sort(sorter) @@ -144,23 +149,25 @@ func (oracle *Oracle) processBlock(bf *blockFees, percentiles []float64) { // also returned if requested and available. // Note: an error is only returned if retrieving the head header has failed. If there are no // retrievable blocks in the specified range then zero block count is returned with no error. -func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.BlockNumber, blocks int) (*types.Block, []*types.Receipt, uint64, int, error) { +func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.BlockNumber, blocks int) (*types.Block, []*types.Receipt, uint64, int, *types.Header, error) { var ( headBlock rpc.BlockNumber pendingBlock *types.Block pendingReceipts types.Receipts + headHeader *types.Header ) // query either pending block or head header and set headBlock if lastBlock == rpc.PendingBlockNumber { if pendingBlock, pendingReceipts = oracle.backend.PendingBlockAndReceipts(); pendingBlock != nil { lastBlock = rpc.BlockNumber(pendingBlock.NumberU64()) headBlock = lastBlock - 1 + headHeader = pendingBlock.Header() } else { // pending block not supported by backend, process until latest block lastBlock = rpc.LatestBlockNumber blocks-- if blocks == 0 { - return nil, nil, 0, 0, nil + return nil, nil, 0, 0, nil, nil } } } @@ -168,27 +175,28 @@ func (oracle *Oracle) resolveBlockRange(ctx context.Context, lastBlock rpc.Block // if pending block is not fetched then we retrieve the head header to get the head block number if latestHeader, err := oracle.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber); err == nil { headBlock = rpc.BlockNumber(latestHeader.Number.Uint64()) + headHeader = latestHeader } else { - return nil, nil, 0, 0, err + return nil, nil, 0, 0, nil, err } } if lastBlock == rpc.LatestBlockNumber { lastBlock = headBlock } else if pendingBlock == nil && lastBlock > headBlock { - return nil, nil, 0, 0, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, lastBlock, headBlock) + return nil, nil, 0, 0, nil, fmt.Errorf("%w: requested %d, head %d", errRequestBeyondHead, lastBlock, headBlock) } if lastBlock == rpc.FinalizedBlockNumber { if latestFinalizedHeader, err := oracle.backend.HeaderByNumber(ctx, rpc.FinalizedBlockNumber); err == nil { lastBlock = rpc.BlockNumber(latestFinalizedHeader.Number.Uint64()) } else { - return nil, nil, 0, 0, err + return nil, nil, 0, 0, nil, err } } // ensure not trying to retrieve before genesis if rpc.BlockNumber(blocks) > lastBlock+1 { blocks = int(lastBlock + 1) } - return pendingBlock, pendingReceipts, uint64(lastBlock), blocks, nil + return pendingBlock, pendingReceipts, uint64(lastBlock), blocks, headHeader, nil } // FeeHistory returns data relevant for fee estimation based on the specified range of blocks. @@ -230,12 +238,26 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast pendingReceipts []*types.Receipt err error ) - pendingBlock, pendingReceipts, lastBlock, blocks, err := oracle.resolveBlockRange(ctx, unresolvedLastBlock, blocks) + pendingBlock, pendingReceipts, lastBlock, blocks, headHeader, err := oracle.resolveBlockRange(ctx, unresolvedLastBlock, blocks) if err != nil || blocks == 0 { return common.Big0, nil, nil, nil, err } oldestBlock := lastBlock + 1 - uint64(blocks) + // If pending txs are less than oracle.congestedThreshold, we consider the network to be non-congested and suggest + // a minimal tip cap. This is to prevent users from overpaying for gas when the network is not congested and a few + // high-priced txs are causing the suggested tip cap to be high. + var nonCongestedPrice *big.Int + pendingTxCount, _ := oracle.backend.StatsWithMinBaseFee(headHeader.BaseFee) + if pendingTxCount < oracle.congestedThreshold { + // Before Curie (EIP-1559), we need to return the total suggested gas price. After Curie we return defaultGasTipCap wei as the tip cap, + // as the base fee is set separately or added manually for legacy transactions. + nonCongestedPrice = oracle.defaultGasTipCap + if !oracle.backend.ChainConfig().IsCurie(headHeader.Number) { + nonCongestedPrice = oracle.defaultBasePrice + } + } + var ( next = oldestBlock results = make(chan *blockFees, blocks) @@ -244,6 +266,7 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast for i, p := range rewardPercentiles { binary.LittleEndian.PutUint64(percentileKey[i*8:(i+1)*8], math.Float64bits(p)) } + for i := 0; i < maxBlockFetchers && i < blocks; i++ { go func() { for { @@ -257,7 +280,7 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast if pendingBlock != nil && blockNumber >= pendingBlock.NumberU64() { fees.block, fees.receipts = pendingBlock, pendingReceipts fees.header = fees.block.Header() - oracle.processBlock(fees, rewardPercentiles) + oracle.processBlock(fees, rewardPercentiles, nonCongestedPrice) results <- fees } else { cacheKey := struct { @@ -279,7 +302,7 @@ func (oracle *Oracle) FeeHistory(ctx context.Context, blocks int, unresolvedLast fees.header, fees.err = oracle.backend.HeaderByNumber(ctx, rpc.BlockNumber(blockNumber)) } if fees.header != nil && fees.err == nil { - oracle.processBlock(fees, rewardPercentiles) + oracle.processBlock(fees, rewardPercentiles, nonCongestedPrice) if fees.err == nil { oracle.historyCache.Add(cacheKey, fees.results) } diff --git a/eth/handler.go b/eth/handler.go index f77de69d0b..4755a0c7b7 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -455,6 +455,8 @@ func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { hash := block.Hash() peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutBlock(hash)) + log.Debug("Broadcasting block", "hash", hash.Hex(), "number", block.NumberU64(), "size", block.Size()) + // If propagation is requested, send to a subset of the peer if propagate { // Calculate the TD of the block (it's not imported yet, so block.Td is not valid) @@ -470,7 +472,7 @@ func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { for _, peer := range transfer { peer.AsyncSendNewBlock(block, td) } - log.Trace("Propagated block", "hash", hash, "recipients", len(transfer), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) + log.Trace("Propagated block", "hash", hash.Hex(), "recipients", len(transfer), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) return } // Otherwise if the block is indeed in out own chain, announce it @@ -478,7 +480,7 @@ func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { for _, peer := range peers { peer.AsyncSendNewBlockHash(block) } - log.Trace("Announced block", "hash", hash, "recipients", len(peers), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) + log.Trace("Announced block", "hash", hash.Hex(), "recipients", len(peers), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) } } @@ -503,6 +505,7 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { if tx.IsL1MessageTx() { continue } + log.Debug("Broadcasting transaction", "hash", tx.Hash().Hex(), "size", tx.Size()) peers := onlyShadowForkPeers(h.shadowForkPeerIDs, h.peers.peersWithoutTransaction(tx.Hash())) // Send the tx unconditionally to a subset of our peers numDirect := int(math.Sqrt(float64(len(peers)))) @@ -518,13 +521,13 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) { directPeers++ directCount += len(hashes) peer.AsyncSendTransactions(hashes) - log.Debug("Transactions being broadcasted to", "peer", peer.String(), "len", len(hashes)) + log.Trace("Transactions being broadcasted to", "peer", peer.String(), "len", len(hashes)) } for peer, hashes := range annos { annoPeers++ annoCount += len(hashes) peer.AsyncSendPooledTransactionHashes(hashes) - log.Debug("Transactions being announced to", "peer", peer.String(), "len", len(hashes)) + log.Trace("Transactions being announced to", "peer", peer.String(), "len", len(hashes)) } log.Debug("Transaction broadcast", "txs", len(txs), "announce packs", annoPeers, "announced hashes", annoCount, diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index fd10725968..c0b8f2c9dc 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -294,6 +294,7 @@ func handleNewBlock(backend Backend, msg Decoder, peer *Peer) error { // Mark the peer as owning the block peer.markBlock(ann.Block.Hash()) + log.Debug("Received new block via gossip", "blockHash", ann.Block.Hash().Hex(), "blockNumber", ann.Block.NumberU64(), "peer", peer.String()) return backend.Handle(peer, ann) } @@ -362,12 +363,12 @@ func handleNewPooledTransactionHashes(backend Backend, msg Decoder, peer *Peer) } ann := new(NewPooledTransactionHashesPacket) if err := msg.Decode(ann); err != nil { - log.Debug("Failed to decode `NewPooledTransactionHashesPacket`", "peer", peer.String(), "err", err) + log.Trace("Failed to decode `NewPooledTransactionHashesPacket`", "peer", peer.String(), "err", err) newPooledTxHashesFailMeter.Mark(1) return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } // Schedule all the unknown hashes for retrieval - log.Debug("handleNewPooledTransactionHashes", "peer", peer.String(), "len(ann)", len(*ann)) + log.Trace("handleNewPooledTransactionHashes", "peer", peer.String(), "len(ann)", len(*ann)) newPooledTxHashesLenGauge.Update(int64(len(*ann))) for _, hash := range *ann { peer.markTransaction(hash) @@ -379,12 +380,15 @@ func handleGetPooledTransactions66(backend Backend, msg Decoder, peer *Peer) err // Decode the pooled transactions retrieval message var query GetPooledTransactionsPacket66 if err := msg.Decode(&query); err != nil { - log.Debug("Failed to decode `GetPooledTransactionsPacket66`", "peer", peer.String(), "err", err) + log.Trace("Failed to decode `GetPooledTransactionsPacket66`", "peer", peer.String(), "err", err) getPooledTxsFailMeter.Mark(1) return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsPacket, peer) - log.Debug("handleGetPooledTransactions", "peer", peer.String(), "RequestId", query.RequestId, "len(query)", len(query.GetPooledTransactionsPacket), "retrieved", len(hashes)) + log.Trace("handleGetPooledTransactions", "peer", peer.String(), "RequestId", query.RequestId, "len(query)", len(query.GetPooledTransactionsPacket), "retrieved", len(hashes)) + for _, hash := range hashes { + log.Debug("Received new pooled transaction", "hash", hash.Hex(), "peer", peer.String()) + } getPooledTxsQueryLenGauge.Update(int64(len(query.GetPooledTransactionsPacket))) getPooledTxsRetrievedLenGauge.Update(int64(len(hashes))) return peer.ReplyPooledTransactionsRLP(query.RequestId, hashes, txs) @@ -427,16 +431,16 @@ func handleTransactions(backend Backend, msg Decoder, peer *Peer) error { var txs TransactionsPacket if err := msg.Decode(&txs); err != nil { handleTxsFailMeter.Mark(1) - log.Debug("Failed to decode `TransactionsPacket`", "peer", peer.String(), "err", err) + log.Trace("Failed to decode `TransactionsPacket`", "peer", peer.String(), "err", err) return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } - log.Debug("handleTransactions", "peer", peer.String(), "len(txs)", len(txs)) + log.Trace("handleTransactions", "peer", peer.String(), "len(txs)", len(txs)) handleTxsLenGauge.Update(int64(len(txs))) for i, tx := range txs { // Validate and mark the remote transaction if tx == nil { handleTxsNilMeter.Mark(1) - log.Debug("handleTransactions: transaction is nil", "peer", peer.String(), "i", i) + log.Trace("handleTransactions: transaction is nil", "peer", peer.String(), "i", i) return fmt.Errorf("%w: transaction %d is nil", errDecode, i) } peer.markTransaction(tx.Hash()) @@ -453,16 +457,16 @@ func handlePooledTransactions66(backend Backend, msg Decoder, peer *Peer) error var txs PooledTransactionsPacket66 if err := msg.Decode(&txs); err != nil { pooledTxs66FailMeter.Mark(1) - log.Debug("Failed to decode `PooledTransactionsPacket66`", "peer", peer.String(), "err", err) + log.Trace("Failed to decode `PooledTransactionsPacket66`", "peer", peer.String(), "err", err) return fmt.Errorf("%w: message %v: %v", errDecode, msg, err) } - log.Debug("handlePooledTransactions66", "peer", peer.String(), "len(txs)", len(txs.PooledTransactionsPacket)) + log.Trace("handlePooledTransactions66", "peer", peer.String(), "len(txs)", len(txs.PooledTransactionsPacket)) pooledTxs66LenGauge.Update(int64(len(txs.PooledTransactionsPacket))) for i, tx := range txs.PooledTransactionsPacket { // Validate and mark the remote transaction if tx == nil { pooledTxs66NilMeter.Mark(1) - log.Debug("handlePooledTransactions: transaction is nil", "peer", peer.String(), "i", i) + log.Trace("handlePooledTransactions: transaction is nil", "peer", peer.String(), "i", i) return fmt.Errorf("%w: transaction %d is nil", errDecode, i) } peer.markTransaction(tx.Hash()) diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index d7ab789227..838232051c 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -436,7 +436,7 @@ func (p *Peer) RequestTxs(hashes []common.Hash) error { p.Log().Debug("Fetching batch of transactions", "count", len(hashes)) id := rand.Uint64() - log.Debug("Requesting transactions", "RequestId", id, "Peer.id", p.id, "count", len(hashes)) + log.Trace("Requesting transactions", "RequestId", id, "Peer.id", p.id, "count", len(hashes)) peerRequestTxsCntGauge.Update(int64(len(hashes))) requestTracker.Track(p.id, p.version, GetPooledTransactionsMsg, PooledTransactionsMsg, id) diff --git a/eth/state_accessor.go b/eth/state_accessor.go index c610edd85d..13b80ded03 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -176,6 +176,12 @@ func (eth *Ethereum) stateAtTransaction(block *types.Block, txIndex int, reexec if err != nil { return nil, vm.BlockContext{}, nil, err } + // If feynman hardfork, insert parent block hash in the state as per EIP-2935. + if eth.blockchain.Config().IsFeynman(block.Time()) { + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, eth.blockchain.Config(), nil) + vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, eth.blockchain.Config(), vm.Config{}) + core.ProcessParentBlockHash(block.ParentHash(), vmenv, statedb) + } if txIndex == 0 && len(block.Transactions()) == 0 { return nil, vm.BlockContext{}, statedb, nil } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 271ddf9632..7b66326c09 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -277,6 +277,11 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config for task := range tasks { signer := types.MakeSigner(api.backend.ChainConfig(), task.block.Number(), task.block.Time()) blockCtx := core.NewEVMBlockContext(task.block.Header(), api.chainContext(localctx), api.backend.ChainConfig(), nil) + // EIP-2935: Insert parent hash in history contract. + if api.backend.ChainConfig().IsFeynman(task.block.Time()) { + evm := vm.NewEVM(blockCtx, vm.TxContext{}, task.statedb, api.backend.ChainConfig(), vm.Config{}) + core.ProcessParentBlockHash(task.block.ParentHash(), evm, task.statedb) + } // Trace all the transactions contained within for i, tx := range task.block.Transactions() { msg, _ := tx.AsMessage(signer, task.block.BaseFee()) @@ -539,6 +544,11 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), api.backend.ChainConfig(), nil) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) ) + // EIP-2935: Insert parent hash in history contract. + if api.backend.ChainConfig().IsFeynman(block.Time()) { + vmenv := vm.NewEVM(vmctx, vm.TxContext{}, statedb, chainConfig, vm.Config{}) + core.ProcessParentBlockHash(block.ParentHash(), vmenv, statedb) + } for i, tx := range block.Transactions() { var ( msg, _ = tx.AsMessage(signer, block.BaseFee()) @@ -614,6 +624,11 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac threads = len(txs) } blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), api.backend.ChainConfig(), nil) + // EIP-2935: Insert parent hash in history contract. + if api.backend.ChainConfig().IsFeynman(block.Time()) { + evm := vm.NewEVM(blockCtx, vm.TxContext{}, statedb, api.backend.ChainConfig(), vm.Config{}) + core.ProcessParentBlockHash(block.ParentHash(), evm, statedb) + } blockHash := block.Hash() blockNumber := block.NumberU64() for th := 0; th < threads; th++ { @@ -748,6 +763,13 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block canon = false } } + + // EIP-2935: Insert parent hash in history contract. + if api.backend.ChainConfig().IsFeynman(block.Time()) { + evm := vm.NewEVM(vmctx, vm.TxContext{}, statedb, api.backend.ChainConfig(), vm.Config{}) + core.ProcessParentBlockHash(block.ParentHash(), evm, statedb) + } + for i, tx := range block.Transactions() { // Prepare the trasaction for un-traced execution var ( diff --git a/eth/tracers/native/call.go b/eth/tracers/native/call.go index b2bebb15ce..36a3417d8f 100644 --- a/eth/tracers/native/call.go +++ b/eth/tracers/native/call.go @@ -18,7 +18,6 @@ package native import ( "encoding/json" - "errors" "math/big" "strconv" "strings" @@ -60,21 +59,21 @@ type callTracer struct { func newCallTracer(ctx *tracers.Context) tracers.Tracer { // First callframe contains tx context info // and is populated on start and end. - t := &callTracer{callstack: make([]callFrame, 1)} + t := &callTracer{callstack: make([]callFrame, 0, 1)} return t } // CaptureStart implements the EVMLogger interface to initialize the tracing operation. func (t *callTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int, authorizationResults []types.AuthorizationResult) { t.env = env - t.callstack[0] = callFrame{ + t.callstack = append(t.callstack, callFrame{ Type: "CALL", From: addrToHex(from), To: addrToHex(to), Input: bytesToHex(input), Gas: uintToHex(gas), Value: bigToHex(value), - } + }) if create { t.callstack[0].Type = "CREATE" } @@ -152,7 +151,10 @@ func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) { // error arising from the encoding or forceful termination (via `Stop`). func (t *callTracer) GetResult() (json.RawMessage, error) { if len(t.callstack) != 1 { - return nil, errors.New("incorrect number of top-level calls") + // If the callstack is empty, return an empty JSON object instead of erroring + // Callstack can be empty due to an edge case where an L1 message is reverted even + // before entering the first call. + return json.RawMessage("{}"), nil } res, err := json.Marshal(t.callstack[0]) if err != nil { diff --git a/eth/tracers/native/call_flat.go b/eth/tracers/native/call_flat.go index b82398c78a..721882b0c9 100644 --- a/eth/tracers/native/call_flat.go +++ b/eth/tracers/native/call_flat.go @@ -18,7 +18,6 @@ package native import ( "encoding/json" - "errors" "fmt" "math/big" "strings" @@ -115,7 +114,7 @@ type flatCallTracer struct { // newFlatCallTracer returns a new flatCallTracer. func newFlatCallTracer(ctx *tracers.Context) tracers.Tracer { - t := &callTracer{callstack: make([]callFrame, 1)} + t := &callTracer{callstack: make([]callFrame, 0, 1)} return &flatCallTracer{callTracer: t, ctx: ctx} @@ -133,7 +132,10 @@ func (t *flatCallTracer) CaptureEnter(typ vm.OpCode, from common.Address, to com // error arising from the encoding or forceful termination (via `Stop`). func (t *flatCallTracer) GetResult() (json.RawMessage, error) { if len(t.callTracer.callstack) < 1 { - return nil, errors.New("invalid number of calls") + // If the callstack is empty, return an empty JSON list instead of erroring + // Callstack can be empty due to an edge case where an L1 message is reverted even + // before entering the first call. + return json.RawMessage("[]"), nil } flat, err := flatFromNested(&t.callTracer.callstack[0], []int{}, true, t.ctx) diff --git a/go.mod b/go.mod index ea4204d370..1c3397b960 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/prometheus/tsdb v0.7.1 github.com/rjeczalik/notify v0.9.1 github.com/rs/cors v1.7.0 - github.com/scroll-tech/da-codec v0.1.3-0.20250226072559-f8a8d3898f54 + github.com/scroll-tech/da-codec v0.1.3-0.20250313120912-344f2d5e33e1 github.com/scroll-tech/zktrie v0.8.4 github.com/shirou/gopsutil v3.21.11+incompatible github.com/sourcegraph/conc v0.3.0 diff --git a/go.sum b/go.sum index 6e028c2e64..a6c9f2ad2a 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/scroll-tech/da-codec v0.1.3-0.20250226072559-f8a8d3898f54 h1:qVpsVu1J91opTn6HYeuzWcBRVhQmPR8g05i+PlOjlI4= -github.com/scroll-tech/da-codec v0.1.3-0.20250226072559-f8a8d3898f54/go.mod h1:xECEHZLVzbdUn+tNbRJhRIjLGTOTmnFQuTgUTeVLX58= +github.com/scroll-tech/da-codec v0.1.3-0.20250313120912-344f2d5e33e1 h1:Dhd58LE1D+dnoxpgLVeQBMF9uweL/fhQfZHWtWSiOlE= +github.com/scroll-tech/da-codec v0.1.3-0.20250313120912-344f2d5e33e1/go.mod h1:yhTS9OVC0xQGhg7DN5iV5KZJvnSIlFWAxDdp+6jxQtY= github.com/scroll-tech/zktrie v0.8.4 h1:UagmnZ4Z3ITCk+aUq9NQZJNAwnWl4gSxsLb2Nl7IgRE= github.com/scroll-tech/zktrie v0.8.4/go.mod h1:XvNo7vAk8yxNyTjBDj5WIiFzYW4bx/gJ78+NK6Zn6Uk= github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 83b04f4a61..d9978b7ac5 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -692,7 +692,6 @@ func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Addre storageHash = types.EmptyRootHash } keccakCodeHash := state.GetKeccakCodeHash(address) - poseidonCodeHash := state.GetPoseidonCodeHash(address) storageProof := make([]StorageResult, len(storageKeys)) // if we have a storageTrie, (which means the account exists), we can update the storagehash @@ -701,7 +700,6 @@ func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Addre } else { // no storageTrie means the account does not exist, so the codeHash is the hash of an empty bytearray. keccakCodeHash = codehash.EmptyKeccakCodeHash - poseidonCodeHash = codehash.EmptyPoseidonCodeHash } // create the proof for the storageKeys @@ -723,17 +721,26 @@ func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Addre return nil, proofErr } - return &AccountResult{ - Address: address, - AccountProof: toHexSlice(accountProof), - Balance: (*hexutil.Big)(state.GetBalance(address)), - KeccakCodeHash: keccakCodeHash, - PoseidonCodeHash: poseidonCodeHash, - CodeSize: hexutil.Uint64(state.GetCodeSize(address)), - Nonce: hexutil.Uint64(state.GetNonce(address)), - StorageHash: storageHash, - StorageProof: storageProof, - }, state.Error() + result := &AccountResult{ + Address: address, + AccountProof: toHexSlice(accountProof), + Balance: (*hexutil.Big)(state.GetBalance(address)), + KeccakCodeHash: keccakCodeHash, + CodeSize: hexutil.Uint64(state.GetCodeSize(address)), + Nonce: hexutil.Uint64(state.GetNonce(address)), + StorageHash: storageHash, + StorageProof: storageProof, + } + + if state.IsZktrie() { + if storageTrie != nil { + result.PoseidonCodeHash = state.GetPoseidonCodeHash(address) + } else { + result.PoseidonCodeHash = codehash.EmptyPoseidonCodeHash + } + } + + return result, state.Error() } // GetHeaderByNumber returns the requested canonical block header. @@ -1529,10 +1536,33 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH // Retrieve the precompiles since they don't need to be added to the access list precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, header.Time)) + // addressesToExclude contains sender, receiver, precompiles and valid authorizations + addressesToExclude := map[common.Address]struct{}{args.from(): {}, to: {}} + for _, addr := range precompiles { + addressesToExclude[addr] = struct{}{} + } + + // Prevent redundant operations if args contain more authorizations than EVM may handle + maxAuthorizations := uint64(*args.Gas) / params.CallNewAccountGas + if uint64(len(args.AuthorizationList)) > maxAuthorizations { + return nil, 0, nil, errors.New("insufficient gas to process all authorizations") + } + + for _, auth := range args.AuthorizationList { + // Duplicating stateTransition.validateAuthorization() logic + if (!auth.ChainID.IsZero() && auth.ChainID.CmpBig(b.ChainConfig().ChainID) != 0) || auth.Nonce+1 < auth.Nonce { + continue + } + + if authority, err := auth.Authority(); err == nil { + addressesToExclude[authority] = struct{}{} + } + } + // Create an initial tracer - prevTracer := vm.NewAccessListTracer(nil, args.from(), to, precompiles) + prevTracer := vm.NewAccessListTracer(nil, addressesToExclude) if args.AccessList != nil { - prevTracer = vm.NewAccessListTracer(*args.AccessList, args.from(), to, precompiles) + prevTracer = vm.NewAccessListTracer(*args.AccessList, addressesToExclude) } for { // Retrieve the current access list to expand @@ -1558,7 +1588,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH } // Apply the transaction with the access list tracer - tracer := vm.NewAccessListTracer(accessList, args.from(), to, precompiles) + tracer := vm.NewAccessListTracer(accessList, addressesToExclude) config := vm.Config{Tracer: tracer, Debug: true, NoBaseFee: true} vmenv, _, err := b.GetEVM(ctx, msg, statedb, header, &config) if err != nil { diff --git a/internal/ethapi/dbapi.go b/internal/ethapi/dbapi.go new file mode 100644 index 0000000000..d8433c2abe --- /dev/null +++ b/internal/ethapi/dbapi.go @@ -0,0 +1,43 @@ +// Copyright 2022 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 ethapi + +import ( + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/common/hexutil" +) + +// DbGet returns the raw value of a key stored in the database. +func (api *PublicDebugAPI) DbGet(key string) (hexutil.Bytes, error) { + blob, err := common.ParseHexOrString(key) + if err != nil { + return nil, err + } + return api.b.ChainDb().Get(blob) +} + +// DbAncient retrieves an ancient binary blob from the append-only immutable files. +// It is a mapping to the `AncientReaderOp.Ancient` method +func (api *PublicDebugAPI) DbAncient(kind string, number uint64) (hexutil.Bytes, error) { + return api.b.ChainDb().Ancient(kind, number) +} + +// DbAncients returns the ancient item numbers in the ancient store. +// It is a mapping to the `AncientReaderOp.Ancients` method +func (api *PublicDebugAPI) DbAncients() (uint64, error) { + return api.b.ChainDb().Ancients() +} diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 3255100341..0fc93fec84 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -482,6 +482,21 @@ web3._extend({ params: 2, inputFormatter:[web3._extend.formatters.inputBlockNumberFormatter, web3._extend.formatters.inputBlockNumberFormatter], }), + new web3._extend.Method({ + name: 'dbGet', + call: 'debug_dbGet', + params: 1 + }), + new web3._extend.Method({ + name: 'dbAncient', + call: 'debug_dbAncient', + params: 2 + }), + new web3._extend.Method({ + name: 'dbAncients', + call: 'debug_dbAncients', + params: 0 + }), new web3._extend.Method({ name: 'executionWitness', call: 'debug_executionWitness', @@ -983,6 +998,10 @@ web3._extend({ name: 'localSigner', getter: 'scroll_getLocalSigner', }), + new web3._extend.Property({ + name: 'l2BaseFeeConfig', + getter: 'scroll_getL2BaseFeeConfig', + }), ] }); ` diff --git a/miner/miner.go b/miner/miner.go index 0d483643df..a7b5b0cc81 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -60,6 +60,7 @@ type Config struct { StoreSkippedTxTraces bool // Whether store the wrapped traces when storing a skipped tx MaxAccountsNum int // Maximum number of accounts that miner will fetch the pending transactions of when building a new block CCCMaxWorkers int // Maximum number of workers to use for async CCC tasks + AllowEmpty bool // If true, then we allow sealing empty blocks SigningDisabled bool // Whether to disable signing blocks with consensus enginek } diff --git a/miner/scroll_worker.go b/miner/scroll_worker.go index c2b97b5fbf..8af97f1cd7 100644 --- a/miner/scroll_worker.go +++ b/miner/scroll_worker.go @@ -21,6 +21,7 @@ import ( "fmt" "math" "math/big" + "runtime/debug" "sync" "sync/atomic" "time" @@ -341,6 +342,7 @@ func (w *worker) mainLoop() { p := recover() if p != nil { log.Error("worker mainLoop panic", "panic", p) + fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) } }() @@ -400,6 +402,9 @@ func (w *worker) mainLoop() { w.current.deadlineReached = true if len(w.current.txs) > 0 { _, err = w.commit() + } else if w.config.AllowEmpty { + log.Warn("Committing empty block", "number", w.current.header.Number) + _, err = w.commit() } case ev := <-w.txsCh: idleTimer.UpdateSince(idleStart) @@ -622,15 +627,25 @@ func (w *worker) tryCommitNewWork(now time.Time, parent common.Hash, reorging bo // handleForks func (w *worker) handleForks() (bool, error) { + // Apply Curie predeployed contract update if w.chainConfig.CurieBlock != nil && w.chainConfig.CurieBlock.Cmp(w.current.header.Number) == 0 { misc.ApplyCurieHardFork(w.current.state) return true, nil } + // Stop/start miner at Euclid fork boundary on zktrie/mpt nodes if w.chainConfig.IsEuclid(w.current.header.Time) { parent := w.chain.GetBlockByHash(w.current.header.ParentHash) return parent != nil && !w.chainConfig.IsEuclid(parent.Time()), nil } + + // Apply EIP-2935 + if w.chainConfig.IsFeynman(w.current.header.Time) { + context := core.NewEVMBlockContext(w.current.header, w.chain, w.chainConfig, nil) + vmenv := vm.NewEVM(context, vm.TxContext{}, w.current.state, w.chainConfig, vm.Config{}) + core.ProcessParentBlockHash(w.current.header.ParentHash, vmenv, w.current.state) + } + return false, nil } diff --git a/miner/scroll_worker_test.go b/miner/scroll_worker_test.go index db18e86738..74f1371c91 100644 --- a/miner/scroll_worker_test.go +++ b/miner/scroll_worker_test.go @@ -17,6 +17,7 @@ package miner import ( + "context" "fmt" "math" "math/big" @@ -33,6 +34,8 @@ import ( "github.com/scroll-tech/go-ethereum/consensus" "github.com/scroll-tech/go-ethereum/consensus/clique" "github.com/scroll-tech/go-ethereum/consensus/ethash" + "github.com/scroll-tech/go-ethereum/consensus/system_contract" + "github.com/scroll-tech/go-ethereum/consensus/wrapper" "github.com/scroll-tech/go-ethereum/core" "github.com/scroll-tech/go-ethereum/core/rawdb" "github.com/scroll-tech/go-ethereum/core/types" @@ -77,6 +80,14 @@ var ( MaxAccountsNum: math.MaxInt, CCCMaxWorkers: 2, } + + testConfigAllowEmpty = &Config{ + Recommit: time.Second, + GasCeil: params.GenesisGasLimit, + MaxAccountsNum: math.MaxInt, + CCCMaxWorkers: 2, + AllowEmpty: true, + } ) func init() { @@ -138,6 +149,15 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine e.Authorize(testBankAddress, func(account accounts.Account, s string, data []byte) ([]byte, error) { return crypto.Sign(crypto.Keccak256(data), testBankKey) }) + case *wrapper.UpgradableEngine: + gspec.ExtraData = make([]byte, 32+common.AddressLength+crypto.SignatureLength) + gspec.Timestamp = uint64(time.Now().Unix()) + copy(gspec.ExtraData[32:32+common.AddressLength], testBankAddress.Bytes()) + e.Authorize(testBankAddress, func(account accounts.Account, s string, data []byte) ([]byte, error) { + return crypto.Sign(crypto.Keccak256(data), testBankKey) + }, func(account accounts.Account, s string, data []byte) ([]byte, error) { + return crypto.Sign(crypto.Keccak256(data), testBankKey) + }) case *ethash.Ethash: default: t.Fatalf("unexpected consensus engine type: %T", engine) @@ -207,14 +227,26 @@ func (b *testWorkerBackend) newRandomTx(creation bool) *types.Transaction { return tx } -func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*worker, *testWorkerBackend) { +func testWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int, allowEmpty bool) (*worker, *testWorkerBackend) { backend := newTestWorkerBackend(t, chainConfig, engine, db, blocks) backend.txPool.AddLocals(pendingTxs) - w := newWorker(testConfig, chainConfig, engine, backend, new(event.TypeMux), nil, false, false) + config := testConfig + if allowEmpty { + config = testConfigAllowEmpty + } + w := newWorker(config, chainConfig, engine, backend, new(event.TypeMux), nil, false, false) w.setEtherbase(testBankAddress) return w, backend } +func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*worker, *testWorkerBackend) { + return testWorker(t, chainConfig, engine, db, blocks, false) +} + +func newTestWorkerWithEmptyBlock(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*worker, *testWorkerBackend) { + return testWorker(t, chainConfig, engine, db, blocks, true) +} + func TestGenerateBlockAndImportClique(t *testing.T) { testGenerateBlockAndImport(t, true) } @@ -1355,3 +1387,84 @@ func TestEuclidV2HardForkMessageQueue(t *testing.T) { } } } + +// TestEuclidV2TransitionVerification tests that the upgradable consensus engine +// can successfully verify the EuclidV2 transition chain. +func TestEuclidV2TransitionVerification(t *testing.T) { + // patch time.Now() to be able to simulate hard fork time + patches := gomonkey.NewPatches() + defer patches.Reset() + var timeCount int64 + patches.ApplyFunc(time.Now, func() time.Time { + timeCount++ + return time.Unix(timeCount, 0) + }) + + // init chain config + chainConfig := params.AllCliqueProtocolChanges.Clone() + chainConfig.EuclidTime = newUint64(0) + chainConfig.EuclidV2Time = newUint64(10000) + chainConfig.FeynmanTime = nil + chainConfig.Clique = ¶ms.CliqueConfig{Period: 1, Epoch: 30000} + chainConfig.SystemContract = ¶ms.SystemContractConfig{Period: 1} + + // init worker + db := rawdb.NewMemoryDatabase() + cliqueEngine := clique.New(chainConfig.Clique, db) + sysEngine := system_contract.New(context.Background(), chainConfig.SystemContract, &system_contract.FakeEthClient{Value: testBankAddress}) + engine := wrapper.NewUpgradableEngine(chainConfig.IsEuclidV2, cliqueEngine, sysEngine) + w, b := newTestWorkerWithEmptyBlock(t, chainConfig, engine, db, 0) + defer w.close() + b.genesis.MustCommit(db) + + // collect mined blocks + sub := w.mux.Subscribe(core.NewMinedBlockEvent{}) + defer sub.Unsubscribe() + w.start() + + blocks := []*types.Block{} + headers := []*types.Header{} + + for i := 0; i < 6; i++ { + select { + case ev := <-sub.Chan(): + // activate EuclidV2 at next block + if i == 2 { + timeCount = int64(*chainConfig.EuclidV2Time) + } + + block := ev.Data.(core.NewMinedBlockEvent).Block + blocks = append(blocks, block) + headers = append(headers, block.Header()) + + case <-time.After(3 * time.Second): + t.Fatalf("timeout") + } + } + + // sanity check: we generated the EuclidV2 transition block + assert.False(t, chainConfig.IsEuclidV2(headers[0].Time)) + assert.True(t, chainConfig.IsEuclidV2(headers[len(headers)-1].Time)) + + // import headers into new chain + chainDb := rawdb.NewMemoryDatabase() + b.genesis.MustCommit(chainDb) + chain, err := core.NewBlockChain(chainDb, nil, b.chain.Config(), engine, vm.Config{}, nil, nil) + assert.NoError(t, err) + defer chain.Stop() + + // previously this would fail with `unknown ancestor` + _, err = chain.InsertHeaderChain(headers, 0) + assert.NoError(t, err) + + // import headers into new chain + chainDb = rawdb.NewMemoryDatabase() + b.genesis.MustCommit(chainDb) + chain, err = core.NewBlockChain(chainDb, nil, b.chain.Config(), engine, vm.Config{}, nil, nil) + assert.NoError(t, err) + defer chain.Stop() + + // previously this would fail with `unknown ancestor` + _, err = chain.InsertChain(blocks) + assert.NoError(t, err) +} diff --git a/params/config.go b/params/config.go index 1684a1bc3f..eda4340e80 100644 --- a/params/config.go +++ b/params/config.go @@ -328,20 +328,31 @@ var ( CurieBlock: big.NewInt(4740239), DarwinTime: newUint64(1723622400), DarwinV2Time: newUint64(1724832000), + EuclidTime: newUint64(1741680000), + EuclidV2Time: newUint64(1741852800), + FeynmanTime: nil, Clique: &CliqueConfig{ Period: 3, Epoch: 30000, }, + SystemContract: &SystemContractConfig{ + Period: 3, + SystemContractAddress: common.HexToAddress("0xC706Ba9fa4fedF4507CB7A898b4766c1bbf9be57"), + SystemContractSlot: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000067"), + }, Scroll: ScrollConfig{ UseZktrie: true, MaxTxPerBlock: &ScrollMaxTxPerBlock, MaxTxPayloadBytesPerBlock: &ScrollMaxTxPayloadBytesPerBlock, FeeVaultAddress: &rcfg.ScrollFeeVaultAddress, L1Config: &L1Config{ - L1ChainId: 11155111, - L1MessageQueueAddress: common.HexToAddress("0xF0B2293F5D834eAe920c6974D50957A1732de763"), - NumL1MessagesPerBlock: 10, - ScrollChainAddress: common.HexToAddress("0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0"), + L1ChainId: 11155111, + L1MessageQueueAddress: common.HexToAddress("0xF0B2293F5D834eAe920c6974D50957A1732de763"), + L1MessageQueueV2Address: common.HexToAddress("0xA0673eC0A48aa924f067F1274EcD281A10c5f19F"), + L1MessageQueueV2DeploymentBlock: 7773746, + NumL1MessagesPerBlock: 10, + ScrollChainAddress: common.HexToAddress("0x2D567EcE699Eabe5afCd141eDB7A4f2D0D6ce8a0"), + L2SystemConfigAddress: common.HexToAddress("0xF444cF06A3E3724e20B35c2989d3942ea8b59124"), }, GenesisStateRoot: &ScrollSepoliaGenesisState, }, @@ -369,20 +380,31 @@ var ( CurieBlock: big.NewInt(7096836), DarwinTime: newUint64(1724227200), DarwinV2Time: newUint64(1725264000), + EuclidTime: newUint64(1744815600), + EuclidV2Time: newUint64(1745305200), + FeynmanTime: nil, Clique: &CliqueConfig{ Period: 3, Epoch: 30000, }, + SystemContract: &SystemContractConfig{ + Period: 3, + SystemContractAddress: common.HexToAddress("0x8432728A257646449245558B8b7Dbe51A16c7a4D"), + SystemContractSlot: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000067"), + }, Scroll: ScrollConfig{ UseZktrie: true, MaxTxPerBlock: &ScrollMaxTxPerBlock, MaxTxPayloadBytesPerBlock: &ScrollMaxTxPayloadBytesPerBlock, FeeVaultAddress: &rcfg.ScrollFeeVaultAddress, L1Config: &L1Config{ - L1ChainId: 1, - L1MessageQueueAddress: common.HexToAddress("0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B"), - NumL1MessagesPerBlock: 10, - ScrollChainAddress: common.HexToAddress("0xa13BAF47339d63B743e7Da8741db5456DAc1E556"), + L1ChainId: 1, + L1MessageQueueAddress: common.HexToAddress("0x0d7E906BD9cAFa154b048cFa766Cc1E54E39AF9B"), + L1MessageQueueV2Address: common.HexToAddress("0x56971da63A3C0205184FEF096E9ddFc7A8C2D18a"), + L1MessageQueueV2DeploymentBlock: 22088276, + NumL1MessagesPerBlock: 10, + ScrollChainAddress: common.HexToAddress("0xa13BAF47339d63B743e7Da8741db5456DAc1E556"), + L2SystemConfigAddress: common.HexToAddress("0x331A873a2a85219863d80d248F9e2978fE88D0Ea"), }, GenesisStateRoot: &ScrollMainnetGenesisState, }, @@ -501,6 +523,7 @@ var ( DarwinV2Time: new(uint64), EuclidTime: new(uint64), EuclidV2Time: new(uint64), + FeynmanTime: new(uint64), TerminalTotalDifficulty: nil, Ethash: new(EthashConfig), Clique: nil, @@ -642,6 +665,7 @@ type ChainConfig struct { DarwinV2Time *uint64 `json:"darwinv2Time,omitempty"` // DarwinV2 switch time (nil = no fork, 0 = already on darwinv2) EuclidTime *uint64 `json:"euclidTime,omitempty"` // Euclid switch time (nil = no fork, 0 = already on euclid) EuclidV2Time *uint64 `json:"euclidv2Time,omitempty"` // EuclidV2 switch time (nil = no fork, 0 = already on euclidv2) + FeynmanTime *uint64 `json:"feynmanTime,omitempty"` // Feynman switch time (nil = no fork, 0 = already on feynman) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. @@ -692,10 +716,11 @@ type ScrollConfig struct { type L1Config struct { L1ChainId uint64 `json:"l1ChainId,string,omitempty"` L1MessageQueueAddress common.Address `json:"l1MessageQueueAddress,omitempty"` - L1MessageQueueV2Address common.Address `json:"l1MessageQueueV2Address,omitempty"` // TODO: set address once known - L1MessageQueueV2DeploymentBlock uint64 `json:"l1MessageQueueV2DeploymentBlock,omitempty"` // TODO: set block number once known + L1MessageQueueV2Address common.Address `json:"l1MessageQueueV2Address,omitempty"` + L1MessageQueueV2DeploymentBlock uint64 `json:"l1MessageQueueV2DeploymentBlock,omitempty"` NumL1MessagesPerBlock uint64 `json:"numL1MessagesPerBlock,string,omitempty"` ScrollChainAddress common.Address `json:"scrollChainAddress,omitempty"` + L2SystemConfigAddress common.Address `json:"l2SystemConfigAddress,omitempty"` } func (c *L1Config) String() string { @@ -703,8 +728,8 @@ func (c *L1Config) String() string { return "" } - return fmt.Sprintf("{l1ChainId: %v, l1MessageQueueAddress: %v, numL1MessagesPerBlock: %v, ScrollChainAddress: %v}", - c.L1ChainId, c.L1MessageQueueAddress.Hex(), c.NumL1MessagesPerBlock, c.ScrollChainAddress.Hex()) + return fmt.Sprintf("{l1ChainId: %v, l1MessageQueueAddress: %v, l1MessageQueueV2Address: %v, l1MessageQueueV2DeploymentBlock: %v, numL1MessagesPerBlock: %v, ScrollChainAddress: %v, L2SystemConfigAddress: %v}", + c.L1ChainId, c.L1MessageQueueAddress.Hex(), c.L1MessageQueueV2Address.Hex(), c.L1MessageQueueV2DeploymentBlock, c.NumL1MessagesPerBlock, c.ScrollChainAddress.Hex(), c.L2SystemConfigAddress.Hex()) } func (s ScrollConfig) FeeVaultEnabled() bool { @@ -730,8 +755,13 @@ func (s ScrollConfig) String() string { maxTxPayloadBytesPerBlock = fmt.Sprintf("%v", *s.MaxTxPayloadBytesPerBlock) } - return fmt.Sprintf("{useZktrie: %v, maxTxPerBlock: %v, MaxTxPayloadBytesPerBlock: %v, feeVaultAddress: %v, l1Config: %v}", - s.UseZktrie, maxTxPerBlock, maxTxPayloadBytesPerBlock, s.FeeVaultAddress, s.L1Config.String()) + genesisStateRoot := "" + if s.GenesisStateRoot != nil { + genesisStateRoot = fmt.Sprintf("%v", *s.GenesisStateRoot) + } + + return fmt.Sprintf("{useZktrie: %v, maxTxPerBlock: %v, MaxTxPayloadBytesPerBlock: %v, feeVaultAddress: %v, l1Config: %v, genesisStateRoot: %v}", + s.UseZktrie, maxTxPerBlock, maxTxPayloadBytesPerBlock, s.FeeVaultAddress, s.L1Config.String(), genesisStateRoot) } // IsValidTxCount returns whether the given block's transaction count is below the limit. @@ -750,6 +780,14 @@ func (s ScrollConfig) IsValidBlockSizeForMining(size common.StorageSize) bool { return s.IsValidBlockSize(size * (1.0 / 0.95)) } +// L2SystemConfigAddress returns the configured l2 system config address, or the zero address if it is not configured. +func (s ScrollConfig) L2SystemConfigAddress() common.Address { + if s.L1Config == nil { + return common.Address{} // only in tests + } + return s.L1Config.L2SystemConfigAddress +} + // EthashConfig is the consensus engine configs for proof-of-work based sealing. type EthashConfig struct{} @@ -818,7 +856,11 @@ func (c *ChainConfig) String() string { if c.EuclidV2Time != nil { euclidV2Time = fmt.Sprintf("@%v", *c.EuclidV2Time) } - 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, London: %v, Arrow Glacier: %v, Archimedes: %v, Shanghai: %v, Bernoulli: %v, Curie: %v, Darwin: %v, DarwinV2: %v, Euclid: %v, EuclidV2: %v, Engine: %v, Scroll config: %v}", + feynmanTime := "" + if c.FeynmanTime != nil { + feynmanTime = fmt.Sprintf("@%v", *c.FeynmanTime) + } + 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, London: %v, Arrow Glacier: %v, Archimedes: %v, Shanghai: %v, Bernoulli: %v, Curie: %v, Darwin: %v, DarwinV2: %v, Euclid: %v, EuclidV2: %v, Feynman: %v, Engine: %v, Scroll config: %v}", c.ChainID, c.HomesteadBlock, c.DAOForkBlock, @@ -842,6 +884,7 @@ func (c *ChainConfig) String() string { darwinV2Time, euclidTime, euclidV2Time, + feynmanTime, engine, c.Scroll, ) @@ -954,6 +997,11 @@ func (c *ChainConfig) IsEuclidV2(now uint64) bool { return isForkedTime(now, c.EuclidV2Time) } +// IsFeynman returns whether time is either equal to the Feynman fork time or greater. +func (c *ChainConfig) IsFeynman(now uint64) bool { + return isForkedTime(now, c.FeynmanTime) +} + // IsTerminalPoWBlock returns whether the given block is the last block of PoW stage. func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool { if c.TerminalTotalDifficulty == nil { @@ -1167,6 +1215,7 @@ type Rules struct { IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon, IsArchimedes, IsShanghai bool IsBernoulli, IsCurie, IsDarwin, IsEuclid, IsEuclidV2 bool + IsFeynman bool } // Rules ensures c's ChainID is not nil. @@ -1194,5 +1243,6 @@ func (c *ChainConfig) Rules(num *big.Int, time uint64) Rules { IsDarwin: c.IsDarwin(time), IsEuclid: c.IsEuclid(time), IsEuclidV2: c.IsEuclidV2(time), + IsFeynman: c.IsFeynman(time), } } diff --git a/params/protocol_params.go b/params/protocol_params.go index a541c86a0f..7fa848db83 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -16,7 +16,11 @@ package params -import "math/big" +import ( + "math/big" + + "github.com/scroll-tech/go-ethereum/common" +) const ( GasLimitBoundDivisor uint64 = 1024 // The bound divisor of the gas limit, used in update calculations. @@ -87,6 +91,8 @@ const ( 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) + TxTokenPerNonZeroByte uint64 = 4 // Token cost per non-zero byte as specified by EIP-7623. + TxCostFloorPerToken uint64 = 10 // Cost floor per byte of data as specified by EIP-7623. TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702 @@ -165,17 +171,30 @@ const ( BlobTxBlobGasPerBlob = 1 << 17 // Gas consumption of a single data blob (== blob byte size) BlobTxMinBlobGasprice = 1 // Minimum gas price for data blobs - BlobTxBlobGaspriceUpdateFraction = 3338477 // Controls the maximum rate of change for blob gas price + BlobTxBlobGaspriceUpdateFraction = 5007716 // Controls the maximum rate of change for blob gas price, using Prague parameters - BlobTxTargetBlobGasPerBlock = 3 * BlobTxBlobGasPerBlob // Target consumable blob gas for data blobs per block (for 1559-like pricing) + BlobTxTargetBlobGasPerBlock = 6 * BlobTxBlobGasPerBlob // Target consumable blob gas for data blobs per block (for 1559-like pricing), using Prague parameters + + HistoryServeWindow = 8192 // Number of blocks to serve historical block hashes for, EIP-2935. ) // Gas discount table for BLS12-381 G1 and G2 multi exponentiation operations var Bls12381MultiExpDiscountTable = [128]uint64{1200, 888, 764, 641, 594, 547, 500, 453, 438, 423, 408, 394, 379, 364, 349, 334, 330, 326, 322, 318, 314, 310, 306, 302, 298, 294, 289, 285, 281, 277, 273, 269, 268, 266, 265, 263, 262, 260, 259, 257, 256, 254, 253, 251, 250, 248, 247, 245, 244, 242, 241, 239, 238, 236, 235, 233, 232, 231, 229, 228, 226, 225, 223, 222, 221, 220, 219, 219, 218, 217, 216, 216, 215, 214, 213, 213, 212, 211, 211, 210, 209, 208, 208, 207, 206, 205, 205, 204, 203, 202, 202, 201, 200, 199, 199, 198, 197, 196, 196, 195, 194, 193, 193, 192, 191, 191, 190, 189, 188, 188, 187, 186, 185, 185, 184, 183, 182, 182, 181, 180, 179, 179, 178, 177, 176, 176, 175, 174} +// Difficulty parameters. var ( DifficultyBoundDivisor = big.NewInt(2048) // The bound divisor of the difficulty, used in the update calculations. GenesisDifficulty = big.NewInt(131072) // Difficulty of the Genesis block. MinimumDifficulty = big.NewInt(131072) // The minimum that the difficulty may ever be. DurationLimit = big.NewInt(13) // The decision boundary on the blocktime duration used to determine whether difficulty should go up or not. ) + +// System contracts. +var ( + // SystemAddress is where the system-transaction is sent from as per EIP-4788 + SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe") + + // EIP-2935 - Serve historical block hashes from state + HistoryStorageAddress = common.HexToAddress("0x0000F90827F1C53a10cb7A02335B175320002935") + HistoryStorageCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500") +) diff --git a/params/version.go b/params/version.go index 6badac58f8..0a9f4e2b00 100644 --- a/params/version.go +++ b/params/version.go @@ -24,7 +24,7 @@ import ( const ( VersionMajor = 5 // Major version component of the current release VersionMinor = 8 // Minor version component of the current release - VersionPatch = 21 // Patch version component of the current release + VersionPatch = 56 // Patch version component of the current release VersionMeta = "mainnet" // Version metadata to append to the version string ) diff --git a/rollup/da_syncer/batch_queue.go b/rollup/da_syncer/batch_queue.go index 1850f53c19..b0b2614004 100644 --- a/rollup/da_syncer/batch_queue.go +++ b/rollup/da_syncer/batch_queue.go @@ -159,7 +159,7 @@ func (bq *BatchQueue) processAndDeleteBatch(batch da.Entry) da.EntryWithBlocks { // In case of a restart or crash we can continue from the last processed batch (and its metadata). rawdb.WriteDAProcessedBatchMeta(bq.db, bq.previousBatch) - log.Info("processing batch", "batchIndex", entryWithBlocks.BatchIndex(), "L1BlockNumber", entryWithBlocks.L1BlockNumber(), "totalL1MessagesPopped", entryWithBlocks.TotalL1MessagesPopped(), "previousBatch", bq.previousBatch.BatchIndex, "previousL1BlockNumber", bq.previousBatch.L1BlockNumber, "previous TotalL1MessagesPopped", bq.previousBatch.TotalL1MessagesPopped) + log.Debug("processing batch", "batchIndex", entryWithBlocks.BatchIndex(), "L1BlockNumber", entryWithBlocks.L1BlockNumber(), "totalL1MessagesPopped", entryWithBlocks.TotalL1MessagesPopped(), "previousBatch", bq.previousBatch.BatchIndex, "previousL1BlockNumber", bq.previousBatch.L1BlockNumber, "previous TotalL1MessagesPopped", bq.previousBatch.TotalL1MessagesPopped) bq.previousBatch = &rawdb.DAProcessedBatchMeta{ L1BlockNumber: entryWithBlocks.L1BlockNumber(), diff --git a/rollup/da_syncer/blob_client/beacon_node_client.go b/rollup/da_syncer/blob_client/beacon_node_client.go index adb61a4199..f7129af76d 100644 --- a/rollup/da_syncer/blob_client/beacon_node_client.go +++ b/rollup/da_syncer/blob_client/beacon_node_client.go @@ -9,12 +9,18 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/crypto/kzg4844" ) +const ( + BeaconNodeDefaultTimeout = 15 * time.Second +) + type BeaconNodeClient struct { + client *http.Client apiEndpoint string genesisTime uint64 secondsPerSlot uint64 @@ -27,12 +33,14 @@ var ( ) func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) { + client := &http.Client{Timeout: BeaconNodeDefaultTimeout} + // get genesis time genesisPath, err := url.JoinPath(apiEndpoint, beaconNodeGenesisEndpoint) if err != nil { return nil, fmt.Errorf("failed to join path, err: %w", err) } - resp, err := http.Get(genesisPath) + resp, err := client.Get(genesisPath) if err != nil { return nil, fmt.Errorf("cannot do request, err: %w", err) } @@ -62,7 +70,7 @@ func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) { if err != nil { return nil, fmt.Errorf("failed to join path, err: %w", err) } - resp, err = http.Get(specPath) + resp, err = client.Get(specPath) if err != nil { return nil, fmt.Errorf("cannot do request, err: %w", err) } @@ -91,6 +99,7 @@ func NewBeaconNodeClient(apiEndpoint string) (*BeaconNodeClient, error) { } return &BeaconNodeClient{ + client: client, apiEndpoint: apiEndpoint, genesisTime: genesisTime, secondsPerSlot: secondsPerSlot, @@ -105,7 +114,7 @@ func (c *BeaconNodeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Contex if err != nil { return nil, fmt.Errorf("failed to join path, err: %w", err) } - resp, err := http.Get(blobSidecarPath) + resp, err := c.client.Get(blobSidecarPath) if err != nil { return nil, fmt.Errorf("cannot do request, err: %w", err) } diff --git a/rollup/da_syncer/blob_client/blob_scan_client.go b/rollup/da_syncer/blob_client/blob_scan_client.go index 0185cc9dc9..c4756ed695 100644 --- a/rollup/da_syncer/blob_client/blob_scan_client.go +++ b/rollup/da_syncer/blob_client/blob_scan_client.go @@ -8,12 +8,17 @@ import ( "fmt" "net/http" "net/url" + "time" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/common/hexutil" "github.com/scroll-tech/go-ethereum/crypto/kzg4844" ) +const ( + BlobScanDefaultTimeout = 15 * time.Second +) + type BlobScanClient struct { client *http.Client apiEndpoint string @@ -21,7 +26,7 @@ type BlobScanClient struct { func NewBlobScanClient(apiEndpoint string) *BlobScanClient { return &BlobScanClient{ - client: http.DefaultClient, + client: &http.Client{Timeout: BlobScanDefaultTimeout}, apiEndpoint: apiEndpoint, } } @@ -59,6 +64,12 @@ func (c *BlobScanClient) GetBlobByVersionedHashAndBlockTime(ctx context.Context, if err != nil { return nil, fmt.Errorf("failed to decode result into struct, err: %w", err) } + + // check that blob data is not empty + if len(result.Data) < 2 { + return nil, fmt.Errorf("blob data is too short to be valid, expected at least 2 characters, got: %s, versioned hash: %s", result.Data, versionedHash.String()) + } + blobBytes, err := hex.DecodeString(result.Data[2:]) if err != nil { return nil, fmt.Errorf("failed to decode data to bytes, err: %w", err) diff --git a/rollup/da_syncer/blob_client/block_native_client.go b/rollup/da_syncer/blob_client/block_native_client.go index 1fe6efbbab..b3ec31ba62 100644 --- a/rollup/da_syncer/blob_client/block_native_client.go +++ b/rollup/da_syncer/blob_client/block_native_client.go @@ -8,18 +8,25 @@ import ( "fmt" "net/http" "net/url" + "time" "github.com/scroll-tech/go-ethereum/common" "github.com/scroll-tech/go-ethereum/common/hexutil" "github.com/scroll-tech/go-ethereum/crypto/kzg4844" ) +const ( + BlockNativeDefaultTimeout = 15 * time.Second +) + type BlockNativeClient struct { + client *http.Client apiEndpoint string } func NewBlockNativeClient(apiEndpoint string) *BlockNativeClient { return &BlockNativeClient{ + client: &http.Client{Timeout: BlockNativeDefaultTimeout}, apiEndpoint: apiEndpoint, } } @@ -34,7 +41,7 @@ func (c *BlockNativeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Conte if err != nil { return nil, fmt.Errorf("cannot create request, err: %w", err) } - resp, err := http.DefaultClient.Do(req) + resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("cannot do request, err: %w", err) } @@ -52,6 +59,12 @@ func (c *BlockNativeClient) GetBlobByVersionedHashAndBlockTime(ctx context.Conte if err != nil { return nil, fmt.Errorf("failed to decode result into struct, err: %w", err) } + + // check that blob data is not empty + if len(result.Blob.Data) < 2 { + return nil, fmt.Errorf("blob data is too short to be valid, expected at least 2 characters, got: %s, versioned hash: %s", result.Blob.Data, versionedHash.String()) + } + blobBytes, err := hex.DecodeString(result.Blob.Data[2:]) if err != nil { return nil, fmt.Errorf("failed to decode data to bytes, err: %w", err) diff --git a/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go index 2c46aeedbf..ac5fa9fa81 100644 --- a/rollup/fees/rollup_fee.go +++ b/rollup/fees/rollup_fee.go @@ -20,8 +20,15 @@ var ( // to be non-zero. // - tx length prefix: 4 bytes txExtraDataBytes = uint64(4) + + // L1 data fee cap. + l1DataFeeCap = new(big.Int).SetUint64(math.MaxUint64) ) +func MaxL1DataFee() *big.Int { + return new(big.Int).Set(l1DataFeeCap) +} + // Message represents the interface of a message. // It should be a subset of the methods found on // types.Message @@ -248,8 +255,8 @@ func CalculateL1DataFee(tx *types.Transaction, state StateDB, config *params.Cha // ensure l1DataFee fits into uint64 for circuit compatibility // (note: in practice this value should never be this big) - if !l1DataFee.IsUint64() { - l1DataFee.SetUint64(math.MaxUint64) + if l1DataFee.Cmp(l1DataFeeCap) > 0 { + l1DataFee = new(big.Int).Set(l1DataFeeCap) } return l1DataFee, nil diff --git a/rollup/l1/abi.go b/rollup/l1/abi.go index bcc35b5a75..17b00c7cd1 100644 --- a/rollup/l1/abi.go +++ b/rollup/l1/abi.go @@ -24,7 +24,7 @@ func init() { // ScrollChainMetaData contains ABI of the ScrollChain contract. var ScrollChainMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"_chainId\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"_messageQueueV1\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_messageQueueV2\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_verifier\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_systemConfig\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"addProver\",\"inputs\":[{\"name\":\"_account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"addSequencer\",\"inputs\":[{\"name\":\"_account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitAndFinalizeBatch\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"finalizeStruct\",\"type\":\"tuple\",\"internalType\":\"struct IScrollChain.FinalizeStruct\",\"components\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"lastProcessedQueueIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"zkProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitBatchWithBlobProof\",\"inputs\":[{\"name\":\"_version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"_parentBatchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_chunks\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"},{\"name\":\"_skippedL1MessageBitmap\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_blobDataProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitBatches\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"lastBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"committedBatches\",\"inputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"disableEnforcedBatchMode\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundlePostEuclidV2\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"lastProcessedQueueIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundleWithProof\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizedStateRoots\",\"inputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"importGenesisBatch\",\"inputs\":[{\"name\":\"_batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_stateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"initialEuclidBatchIndex\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_messageQueue\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_verifier\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_maxNumTxInChunk\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"initializeV2\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"isBatchFinalized\",\"inputs\":[{\"name\":\"_batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isEnforcedModeEnabled\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isProver\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"isSequencer\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lastFinalizedBatchIndex\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"layer2ChainId\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"maxNumTxInChunk\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"messageQueueV1\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"messageQueueV2\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"miscData\",\"inputs\":[],\"outputs\":[{\"name\":\"lastCommittedBatchIndex\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"lastFinalizedBatchIndex\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"lastFinalizeTimestamp\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"flags\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"reserved\",\"type\":\"uint88\",\"internalType\":\"uint88\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"paused\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"removeProver\",\"inputs\":[{\"name\":\"_account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"removeSequencer\",\"inputs\":[{\"name\":\"_account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"revertBatch\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setPause\",\"inputs\":[{\"name\":\"_status\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"systemConfig\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"updateMaxNumTxInChunk\",\"inputs\":[{\"name\":\"_maxNumTxInChunk\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"verifier\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"withdrawRoots\",\"inputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"CommitBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"FinalizeBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Paused\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RevertBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RevertBatch\",\"inputs\":[{\"name\":\"startBatchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"finishBatchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Unpaused\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UpdateEnforcedBatchMode\",\"inputs\":[{\"name\":\"enabled\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"},{\"name\":\"lastCommittedBatchIndex\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UpdateMaxNumTxInChunk\",\"inputs\":[{\"name\":\"oldMaxNumTxInChunk\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newMaxNumTxInChunk\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UpdateProver\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"status\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UpdateSequencer\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"status\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"ErrorAccountIsNotEOA\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchHeaderV0LengthTooSmall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchHeaderV1LengthTooSmall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchHeaderV3LengthMismatch\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchHeaderV7LengthMismatch\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchIsAlreadyCommitted\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchIsAlreadyVerified\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchIsEmpty\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorBatchNotCommitted\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorCallPointEvaluationPrecompileFailed\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorCallerIsNotProver\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorCallerIsNotSequencer\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorCannotDowngradeVersion\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorEuclidForkEnabled\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorFinalizePreAndPostEuclidBatchInOneBundle\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorFoundMultipleBlobs\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorGenesisBatchHasNonZeroField\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorGenesisBatchImported\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorGenesisDataHashIsZero\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorGenesisParentBatchHashIsNonZero\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorInEnforcedBatchMode\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectBatchHash\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectBatchVersion\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectBitmapLength\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectBitmapLengthV0\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectBitmapLengthV1\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorIncorrectChunkLengthV1\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorLastL1MessageSkipped\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorNoBlobFound\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorNoBlockInChunkV1\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorNotAllV1MessagesAreFinalized\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorNotInEnforcedBatchMode\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorNumTxsLessThanNumL1Msgs\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorRevertFinalizedBatch\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorStateRootIsZero\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorTooManyTxsInOneChunk\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorUnexpectedPointEvaluationPrecompileOutput\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorV5BatchContainsTransactions\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorV5BatchNotContainsOnlyOneBlock\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorV5BatchNotContainsOnlyOneChunk\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ErrorZeroAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InconsistentBatchHash\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"expected\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"actual\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]}]", + ABI: "[{\"type\":\"function\",\"name\":\"commitAndFinalizeBatch\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"finalizeStruct\",\"type\":\"tuple\",\"internalType\":\"struct ScrollChainInterface.FinalizeStruct\",\"components\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"totalL1MessagesPoppedOverall\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"zkProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitBatch\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"chunks\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"},{\"name\":\"skippedL1MessageBitmap\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitBatchWithBlobProof\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"chunks\",\"type\":\"bytes[]\",\"internalType\":\"bytes[]\"},{\"name\":\"skippedL1MessageBitmap\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"blobDataProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"commitBatches\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"parentBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"lastBatchHash\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"committedBatches\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"finalizeBatch\",\"inputs\":[{\"name\":\"_batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_prevStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBatch4844\",\"inputs\":[{\"name\":\"_batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_blobDataProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBatchWithProof\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"prevStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBatchWithProof4844\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"prevStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"blobDataProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundle\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundlePostEuclidV2\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"totalL1MessagesPoppedOverall\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundlePostEuclidV2NoProof\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"totalL1MessagesPoppedOverall\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeBundleWithProof\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"aggrProof\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizeEuclidInitialBatch\",\"inputs\":[{\"name\":\"postStateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"finalizedStateRoots\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"importGenesisBatch\",\"inputs\":[{\"name\":\"_batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_stateRoot\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"isBatchFinalized\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"lastFinalizedBatchIndex\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"revertBatch\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"count\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"revertBatch\",\"inputs\":[{\"name\":\"batchHeader\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"withdrawRoots\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"CommitBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"FinalizeBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"},{\"name\":\"stateRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"withdrawRoot\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RevertBatch\",\"inputs\":[{\"name\":\"batchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"batchHash\",\"type\":\"bytes32\",\"indexed\":true,\"internalType\":\"bytes32\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"RevertBatch\",\"inputs\":[{\"name\":\"startBatchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"},{\"name\":\"finishBatchIndex\",\"type\":\"uint256\",\"indexed\":true,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"UpdateEnforcedBatchMode\",\"inputs\":[{\"name\":\"enabled\",\"type\":\"bool\",\"indexed\":false,\"internalType\":\"bool\"},{\"name\":\"lastCommittedBatchIndex\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false}]", } // L1MessageQueueMetaDataManual contains all meta data concerning the L1MessageQueue contract. @@ -45,6 +45,7 @@ const ( commitBatchMethodName = "commitBatch" commitBatchWithBlobProofMethodName = "commitBatchWithBlobProof" commitBatchesV7MethodName = "commitBatches" + commitAndFinalizeBatch = "commitAndFinalizeBatch" finalizeBundlePostEuclidV2MethodName = "finalizeBundlePostEuclidV2" @@ -362,3 +363,19 @@ func newFinalizeBatchArgs(method *abi.Method, values []any) (*FinalizeBatchArgs, } return &args, nil } + +type commitAndFinalizeBatchArgs struct { + Version uint8 + ParentBatchHash common.Hash + FinalizeStruct FinalizeBatchArgs +} + +func newCommitAndFinalizeBatchArgs(method *abi.Method, values []any) (*commitAndFinalizeBatchArgs, error) { + var args commitAndFinalizeBatchArgs + err := method.Inputs.Copy(&args, values) + if err != nil { + return nil, err + } + + return &args, nil +} diff --git a/rollup/l1/abi_test.go b/rollup/l1/abi_test.go index 59fcb21ae5..43533bd68e 100644 --- a/rollup/l1/abi_test.go +++ b/rollup/l1/abi_test.go @@ -18,6 +18,14 @@ func TestEventSignatures(t *testing.T) { assert.Equal(t, crypto.Keccak256Hash([]byte("FinalizeBatch(uint256,bytes32,bytes32,bytes32)")), ScrollChainABI.Events[finalizeBatchEventName].ID) } +func TestMethodSignatures(t *testing.T) { + assert.Equal(t, crypto.Keccak256Hash([]byte("commitBatch(uint8,bytes,bytes[],bytes)")).Bytes()[:4], ScrollChainABI.Methods[commitBatchMethodName].ID) + assert.Equal(t, crypto.Keccak256Hash([]byte("commitBatchWithBlobProof(uint8,bytes,bytes[],bytes,bytes)")).Bytes()[:4], ScrollChainABI.Methods[commitBatchWithBlobProofMethodName].ID) + assert.Equal(t, crypto.Keccak256Hash([]byte("commitBatches(uint8,bytes32,bytes32)")).Bytes()[:4], ScrollChainABI.Methods[commitBatchesV7MethodName].ID) + assert.Equal(t, crypto.Keccak256Hash([]byte("commitAndFinalizeBatch(uint8,bytes32,(bytes,uint256,bytes32,bytes32,bytes))")).Bytes()[:4], ScrollChainABI.Methods[commitAndFinalizeBatch].ID) + assert.Equal(t, crypto.Keccak256Hash([]byte("finalizeBundlePostEuclidV2(bytes,uint256,bytes32,bytes32,bytes)")).Bytes()[:4], ScrollChainABI.Methods[finalizeBundlePostEuclidV2MethodName].ID) +} + func TestUnpackLog(t *testing.T) { mockBatchIndex := big.NewInt(123) finishMockBatchIndex := big.NewInt(125) diff --git a/rollup/l1/reader.go b/rollup/l1/reader.go index 6c39621d91..e479b7dd9d 100644 --- a/rollup/l1/reader.go +++ b/rollup/l1/reader.go @@ -6,6 +6,8 @@ import ( "fmt" "math/big" + "github.com/scroll-tech/da-codec/encoding" + "github.com/scroll-tech/go-ethereum" "github.com/scroll-tech/go-ethereum/accounts/abi" "github.com/scroll-tech/go-ethereum/common" @@ -21,6 +23,7 @@ const ( finalizeBatchEventName = "FinalizeBatch" nextUnfinalizedQueueIndex = "nextUnfinalizedQueueIndex" lastFinalizedBatchIndex = "lastFinalizedBatchIndex" + finalizedStateRoots = "finalizedStateRoots" defaultRollupEventsFetchBlockRange = 100 ) @@ -70,7 +73,7 @@ func NewReader(ctx context.Context, config Config, l1Client Client) (*Reader, er return &reader, nil } -func (r *Reader) FinalizedL1MessageQueueIndex(blockNumber uint64) (uint64, error) { +func (r *Reader) NextUnfinalizedL1MessageQueueIndex(blockNumber uint64) (uint64, error) { data, err := r.l1MessageQueueABI.Pack(nextUnfinalizedQueueIndex) if err != nil { return 0, fmt.Errorf("failed to pack %s: %w", nextUnfinalizedQueueIndex, err) @@ -90,14 +93,10 @@ func (r *Reader) FinalizedL1MessageQueueIndex(blockNumber uint64) (uint64, error } next := parsedResult.Uint64() - if next == 0 { - return 0, nil - } - - return next - 1, nil + return next, nil } -func (r *Reader) LatestFinalizedBatch(blockNumber uint64) (uint64, error) { +func (r *Reader) LatestFinalizedBatchIndex(blockNumber uint64) (uint64, error) { data, err := r.scrollChainABI.Pack(lastFinalizedBatchIndex) if err != nil { return 0, fmt.Errorf("failed to pack %s: %w", lastFinalizedBatchIndex, err) @@ -119,6 +118,28 @@ func (r *Reader) LatestFinalizedBatch(blockNumber uint64) (uint64, error) { return parsedResult.Uint64(), nil } +func (r *Reader) GetFinalizedStateRootByBatchIndex(blockNumber uint64, batchIndex uint64) (common.Hash, error) { + data, err := r.scrollChainABI.Pack(finalizedStateRoots, big.NewInt(int64(batchIndex))) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to pack %s: %w", finalizedStateRoots, err) + } + + result, err := r.client.CallContract(r.ctx, ethereum.CallMsg{ + To: &r.config.ScrollChainAddress, + Data: data, + }, new(big.Int).SetUint64(blockNumber)) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to call %s: %w", finalizedStateRoots, err) + } + + var parsedResult common.Hash + if err = r.scrollChainABI.UnpackIntoInterface(&parsedResult, finalizedStateRoots, result); err != nil { + return common.Hash{}, fmt.Errorf("failed to unpack result: %w", err) + } + + return parsedResult, nil +} + // GetLatestFinalizedBlockNumber fetches the block number of the latest finalized block from the L1 chain. func (r *Reader) GetLatestFinalizedBlockNumber() (uint64, error) { header, err := r.client.HeaderByNumber(r.ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) @@ -398,6 +419,28 @@ func (r *Reader) FetchCommitTxData(commitEvent *CommitBatchEvent) (*CommitBatchA if err != nil { return nil, fmt.Errorf("failed to decode calldata into commitBatch args %s, values: %+v, err: %w", commitBatchesV7MethodName, values, err) } + } else if method.Name == commitAndFinalizeBatch { + commitAndFinalizeArgs, err := newCommitAndFinalizeBatchArgs(method, values) + if err != nil { + return nil, fmt.Errorf("failed to decode calldata into commitAndFinalizeBatch args %s, values: %+v, err: %w", commitAndFinalizeBatch, values, err) + } + + // in commitAndFinalizeBatch, the last batch hash is encoded in the finalize struct as this is the only batch we're + // committing when calling this function. + codec, err := encoding.CodecFromVersion(encoding.CodecVersion(commitAndFinalizeArgs.Version)) + if err != nil { + return nil, fmt.Errorf("failed to get codec from version %d, err: %w", commitAndFinalizeArgs.Version, err) + } + daBatch, err := codec.NewDABatchFromBytes(commitAndFinalizeArgs.FinalizeStruct.BatchHeader) + if err != nil { + return nil, fmt.Errorf("failed to decode daBatch from bytes, err: %w", err) + } + + args = &CommitBatchArgs{ + Version: commitAndFinalizeArgs.Version, + ParentBatchHash: commitAndFinalizeArgs.ParentBatchHash, + LastBatchHash: daBatch.Hash(), + } } else { return nil, fmt.Errorf("unknown method name for commit transaction: %s", method.Name) } diff --git a/rollup/l2_system_config/abi.go b/rollup/l2_system_config/abi.go new file mode 100644 index 0000000000..b30361f079 --- /dev/null +++ b/rollup/l2_system_config/abi.go @@ -0,0 +1,54 @@ +package l2_system_config + +import ( + "math/big" + + "github.com/scroll-tech/go-ethereum/accounts/abi" + "github.com/scroll-tech/go-ethereum/accounts/abi/bind" + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/rollup/l1" +) + +var ( + l2SystemConfigABI *abi.ABI + + baseFeeOverheadUpdatedEventName = "BaseFeeOverheadUpdated" + baseFeeScalarUpdatedEventName = "BaseFeeScalarUpdated" + + BaseFeeOverheadUpdatedTopic common.Hash + BaseFeeScalarUpdatedTopic common.Hash +) + +func init() { + l2SystemConfigABI, _ = l2SystemConfigMetaData.GetAbi() + + BaseFeeOverheadUpdatedTopic = l2SystemConfigABI.Events[baseFeeOverheadUpdatedEventName].ID + BaseFeeScalarUpdatedTopic = l2SystemConfigABI.Events[baseFeeScalarUpdatedEventName].ID +} + +var l2SystemConfigMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"event\",\"name\":\"BaseFeeOverheadUpdated\",\"inputs\":[{\"name\":\"oldBaseFeeOverhead\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newBaseFeeOverhead\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"BaseFeeScalarUpdated\",\"inputs\":[{\"name\":\"oldBaseFeeScalar\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"},{\"name\":\"newBaseFeeScalar\",\"type\":\"uint256\",\"indexed\":false,\"internalType\":\"uint256\"}],\"anonymous\":false}]", +} + +type BaseFeeOverheadUpdatedEventUnpacked struct { + OldBaseFeeOverhead *big.Int + NewBaseFeeOverhead *big.Int +} + +type BaseFeeScalarUpdatedEventUnpacked struct { + OldBaseFeeScalar *big.Int + NewBaseFeeScalar *big.Int +} + +func UnpackBaseFeeOverheadUpdatedEvent(log types.Log) (*BaseFeeOverheadUpdatedEventUnpacked, error) { + event := &BaseFeeOverheadUpdatedEventUnpacked{} + err := l1.UnpackLog(l2SystemConfigABI, event, baseFeeOverheadUpdatedEventName, log) + return event, err +} + +func UnpackBaseFeeScalarUpdatedEvent(log types.Log) (*BaseFeeScalarUpdatedEventUnpacked, error) { + event := &BaseFeeScalarUpdatedEventUnpacked{} + err := l1.UnpackLog(l2SystemConfigABI, event, baseFeeScalarUpdatedEventName, log) + return event, err +} diff --git a/rollup/l2_system_config/abi_test.go b/rollup/l2_system_config/abi_test.go new file mode 100644 index 0000000000..db4059599b --- /dev/null +++ b/rollup/l2_system_config/abi_test.go @@ -0,0 +1,51 @@ +package l2_system_config + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/crypto" +) + +func TestEventSignatures(t *testing.T) { + assert.Equal(t, crypto.Keccak256Hash([]byte("BaseFeeOverheadUpdated(uint256,uint256)")), BaseFeeOverheadUpdatedTopic) + assert.Equal(t, crypto.Keccak256Hash([]byte("BaseFeeScalarUpdated(uint256,uint256)")), BaseFeeScalarUpdatedTopic) +} + +func bigToBytesPadded(num *big.Int) []byte { + return common.BigToHash(num).Bytes() +} + +func TestUnpackBaseFeeOverheadUpdatedLog(t *testing.T) { + old := common.Big1 + new := common.Big2 + + log := types.Log{ + Topics: []common.Hash{BaseFeeOverheadUpdatedTopic}, + Data: append(bigToBytesPadded(old), bigToBytesPadded(new)...), + } + + event, err := UnpackBaseFeeOverheadUpdatedEvent(log) + assert.NoError(t, err) + assert.Equal(t, common.Big1, event.OldBaseFeeOverhead) + assert.Equal(t, common.Big2, event.NewBaseFeeOverhead) +} + +func TestUnpackBaseFeeScalarUpdatedLog(t *testing.T) { + old := common.Big32 + new := common.Big256 + + log := types.Log{ + Topics: []common.Hash{BaseFeeScalarUpdatedTopic}, + Data: append(bigToBytesPadded(old), bigToBytesPadded(new)...), + } + + event, err := UnpackBaseFeeScalarUpdatedEvent(log) + assert.NoError(t, err) + assert.Equal(t, common.Big32, event.OldBaseFeeScalar) + assert.Equal(t, common.Big256, event.NewBaseFeeScalar) +} diff --git a/rollup/pipeline/pipeline.go b/rollup/pipeline/pipeline.go index 77ac3aee24..90c6149b38 100644 --- a/rollup/pipeline/pipeline.go +++ b/rollup/pipeline/pipeline.go @@ -228,7 +228,7 @@ func sendCancellable[T any, C comparable](resCh chan T, msg T, cancelCh <-chan C } func (p *Pipeline) traceAndApplyStage(txsIn <-chan *types.Transaction) (<-chan error, <-chan *BlockCandidate, error) { - p.state.StartPrefetcher("miner", nil) + p.state.StartPrefetcher("miner") downstreamCh := make(chan *BlockCandidate, p.downstreamChCapacity()) resCh := make(chan error) p.wg.Add(1) diff --git a/rollup/rcfg/config.go b/rollup/rcfg/config.go index 8f7931b745..3bac185c83 100644 --- a/rollup/rcfg/config.go +++ b/rollup/rcfg/config.go @@ -49,3 +49,20 @@ var ( // cat artifacts/src/L1GasPriceOracle.sol/L1GasPriceOracle.json | jq -r .deployedBytecode.object CurieL1GasPriceOracleBytecode = common.Hex2Bytes("608060405234801561000f575f80fd5b5060043610610132575f3560e01c8063715018a6116100b4578063a911d77f11610079578063a911d77f1461024c578063bede39b514610254578063de26c4a114610267578063e88a60ad1461027a578063f2fde38b1461028d578063f45e65d8146102a0575f80fd5b8063715018a6146101eb57806384189161146101f35780638da5cb5b146101fc57806393e59dc114610226578063944b247f14610239575f80fd5b80633d0f963e116100fa5780633d0f963e146101a057806349948e0e146101b3578063519b4bd3146101c65780636a5e67e5146101cf57806370465597146101d8575f80fd5b80630c18c1621461013657806313dad5be1461015257806323e524ac1461016f5780633577afc51461017857806339455d3a1461018d575b5f80fd5b61013f60025481565b6040519081526020015b60405180910390f35b60085461015f9060ff1681565b6040519015158152602001610149565b61013f60065481565b61018b6101863660046109b3565b6102a9565b005b61018b61019b3660046109ca565b61033b565b61018b6101ae3660046109ea565b610438565b61013f6101c1366004610a2b565b6104bb565b61013f60015481565b61013f60075481565b61018b6101e63660046109b3565b6104e0565b61018b61056e565b61013f60055481565b5f5461020e906001600160a01b031681565b6040516001600160a01b039091168152602001610149565b60045461020e906001600160a01b031681565b61018b6102473660046109b3565b6105a2565b61018b61062e565b61018b6102623660046109b3565b61068a565b61013f610275366004610a2b565b610747565b61018b6102883660046109b3565b610764565b61018b61029b3660046109ea565b6107f0565b61013f60035481565b5f546001600160a01b031633146102db5760405162461bcd60e51b81526004016102d290610ad6565b60405180910390fd5b621c9c388111156102ff57604051635742c80560e11b815260040160405180910390fd5b60028190556040518181527f32740b35c0ea213650f60d44366b4fb211c9033b50714e4a1d34e65d5beb9bb4906020015b60405180910390a150565b6004805460405163efc7840160e01b815233928101929092526001600160a01b03169063efc7840190602401602060405180830381865afa158015610382573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103a69190610b0d565b6103c3576040516326b3506d60e11b815260040160405180910390fd5b600182905560058190556040518281527f351fb23757bb5ea0546c85b7996ddd7155f96b939ebaa5ff7bc49c75f27f2c449060200160405180910390a16040518181527f9a14bfb5d18c4c3cf14cae19c23d7cf1bcede357ea40ca1f75cd49542c71c214906020015b60405180910390a15050565b5f546001600160a01b031633146104615760405162461bcd60e51b81526004016102d290610ad6565b600480546001600160a01b038381166001600160a01b031983168117909355604080519190921680825260208201939093527f22d1c35fe072d2e42c3c8f9bd4a0d34aa84a0101d020a62517b33fdb3174e5f7910161042c565b6008545f9060ff16156104d7576104d18261087b565b92915050565b6104d1826108c1565b5f546001600160a01b031633146105095760405162461bcd60e51b81526004016102d290610ad6565b610519633b9aca006103e8610b40565b81111561053957604051631e44fdeb60e11b815260040160405180910390fd5b60038190556040518181527f3336cd9708eaf2769a0f0dc0679f30e80f15dcd88d1921b5a16858e8b85c591a90602001610330565b5f546001600160a01b031633146105975760405162461bcd60e51b81526004016102d290610ad6565b6105a05f610904565b565b5f546001600160a01b031633146105cb5760405162461bcd60e51b81526004016102d290610ad6565b6105d9633b9aca0080610b40565b8111156105f95760405163874f603160e01b815260040160405180910390fd5b60068190556040518181527f2ab3f5a4ebbcbf3c24f62f5454f52f10e1a8c9dcc5acac8f19199ce881a6a10890602001610330565b5f546001600160a01b031633146106575760405162461bcd60e51b81526004016102d290610ad6565b60085460ff161561067b576040516379f9c57560e01b815260040160405180910390fd5b6008805460ff19166001179055565b6004805460405163efc7840160e01b815233928101929092526001600160a01b03169063efc7840190602401602060405180830381865afa1580156106d1573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906106f59190610b0d565b610712576040516326b3506d60e11b815260040160405180910390fd5b60018190556040518181527f351fb23757bb5ea0546c85b7996ddd7155f96b939ebaa5ff7bc49c75f27f2c4490602001610330565b6008545f9060ff161561075b57505f919050565b6104d182610953565b5f546001600160a01b0316331461078d5760405162461bcd60e51b81526004016102d290610ad6565b61079b633b9aca0080610b40565b8111156107bb5760405163f37ec21560e01b815260040160405180910390fd5b60078190556040518181527f6b332a036d8c3ead57dcb06c87243bd7a2aed015ddf2d0528c2501dae56331aa90602001610330565b5f546001600160a01b031633146108195760405162461bcd60e51b81526004016102d290610ad6565b6001600160a01b03811661086f5760405162461bcd60e51b815260206004820152601d60248201527f6e6577206f776e657220697320746865207a65726f206164647265737300000060448201526064016102d2565b61087881610904565b50565b5f633b9aca0060055483516007546108939190610b40565b61089d9190610b40565b6001546006546108ad9190610b40565b6108b79190610b57565b6104d19190610b6a565b5f806108cc83610953565b90505f600154826108dd9190610b40565b9050633b9aca00600354826108f29190610b40565b6108fc9190610b6a565b949350505050565b5f80546001600160a01b038381166001600160a01b0319831681178455604051919092169283917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e09190a35050565b80515f908190815b818110156109a45784818151811061097557610975610b89565b01602001516001600160f81b0319165f036109955760048301925061099c565b6010830192505b60010161095b565b50506002540160400192915050565b5f602082840312156109c3575f80fd5b5035919050565b5f80604083850312156109db575f80fd5b50508035926020909101359150565b5f602082840312156109fa575f80fd5b81356001600160a01b0381168114610a10575f80fd5b9392505050565b634e487b7160e01b5f52604160045260245ffd5b5f60208284031215610a3b575f80fd5b813567ffffffffffffffff80821115610a52575f80fd5b818401915084601f830112610a65575f80fd5b813581811115610a7757610a77610a17565b604051601f8201601f19908116603f01168101908382118183101715610a9f57610a9f610a17565b81604052828152876020848701011115610ab7575f80fd5b826020860160208301375f928101602001929092525095945050505050565b60208082526017908201527f63616c6c6572206973206e6f7420746865206f776e6572000000000000000000604082015260600190565b5f60208284031215610b1d575f80fd5b81518015158114610a10575f80fd5b634e487b7160e01b5f52601160045260245ffd5b80820281158282048414176104d1576104d1610b2c565b808201808211156104d1576104d1610b2c565b5f82610b8457634e487b7160e01b5f52601260045260245ffd5b500490565b634e487b7160e01b5f52603260045260245ffdfea26469706673582212200c2ac583f18be4f94ab169ae6f2ea3a708a7c0d4424746b120b177adb39e626064736f6c63430008180033") ) + +// L2SystemConfig constants. +var ( + // > forge inspect src/L2/L2SystemConfig.sol:L2SystemConfig storageLayout + // ╭-----------------+-------------+------+--------+-------+------------------------------------------╮ + // | Name | Type | Slot | Offset | Bytes | Contract | + // +==================================================================================================+ + // ... + // |-----------------+-------------+------+--------+-------+------------------------------------------| + // | baseFeeOverhead | uint256 | 101 | 0 | 32 | src/L2/L2SystemConfig.sol:L2SystemConfig | + // |-----------------+-------------+------+--------+-------+------------------------------------------| + // | baseFeeScalar | uint256 | 102 | 0 | 32 | src/L2/L2SystemConfig.sol:L2SystemConfig | + // ╰-----------------+-------------+------+--------+-------+------------------------------------------╯ + + L2BaseFeeOverheadSlot = common.BigToHash(big.NewInt(101)) + L2BaseFeeScalarSlot = common.BigToHash(big.NewInt(102)) +) diff --git a/rollup/rollup_sync_service/rollup_sync_service.go b/rollup/rollup_sync_service/rollup_sync_service.go index c0d79d8339..8aba65a1d4 100644 --- a/rollup/rollup_sync_service/rollup_sync_service.go +++ b/rollup/rollup_sync_service/rollup_sync_service.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" "sync" "time" @@ -16,6 +17,7 @@ import ( "github.com/scroll-tech/go-ethereum/core/rawdb" "github.com/scroll-tech/go-ethereum/ethdb" "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/metrics" "github.com/scroll-tech/go-ethereum/node" "github.com/scroll-tech/go-ethereum/params" "github.com/scroll-tech/go-ethereum/rollup/da_syncer" @@ -40,6 +42,15 @@ const ( // defaultLogInterval is the frequency at which we print the latest processed block. defaultLogInterval = 5 * time.Minute + + // rewindL1Height is the number of blocks to rewind the L1 sync height when a missing batch event is detected. + rewindL1Height = 100 +) + +var ( + finalizedBlockGauge = metrics.NewRegisteredGauge("chain/head/finalized", nil) + ErrShouldResetSyncHeight = errors.New("ErrShouldResetSyncHeight") + ErrMissingBatchEvent = errors.New("ErrMissingBatchEvent") ) // RollupSyncService collects ScrollChain batch commit/revert/finalize events and stores metadata into db. @@ -132,6 +143,11 @@ func (s *RollupSyncService) Start() { log.Info("Starting rollup event sync background service", "latest processed block", s.callDataBlobSource.L1Height()) + finalizedBlockHeightPtr := rawdb.ReadFinalizedL2BlockNumber(s.db) + if finalizedBlockHeightPtr != nil { + finalizedBlockGauge.Update(int64(*finalizedBlockHeightPtr)) + } + go func() { syncTicker := time.NewTicker(defaultSyncInterval) defer syncTicker.Stop() @@ -205,6 +221,28 @@ func (s *RollupSyncService) fetchRollupEvents() error { } if err = s.updateRollupEvents(daEntries); err != nil { + if errors.Is(err, ErrShouldResetSyncHeight) { + log.Warn("Resetting sync height to L1 block 7892668 to fix L1 message queue hash calculation") + s.callDataBlobSource.SetL1Height(7892668) + + return nil + } + if errors.Is(err, ErrMissingBatchEvent) { + // make sure no underflow + var rewindTo uint64 + if prevL1Height > rewindL1Height { + rewindTo = prevL1Height - rewindL1Height + } + + // If there's a missing batch event, rewind the L1 sync height by some blocks to re-fetch from L1 RPC and + // replay creating corresponding CommittedBatchMeta in local DB. + // This happens recursively until the missing event has been recovered as we will call fetchRollupEvents again + // with the `L1Height = prevL1Height - rewindL1Height`. + s.callDataBlobSource.SetL1Height(rewindTo) + + return fmt.Errorf("missing batch event, rewinding L1 sync height by %d blocks to %d: %w", rewindL1Height, rewindTo, err) + } + // Reset the L1 height to the previous value to retry fetching the same data. s.callDataBlobSource.SetL1Height(prevL1Height) return fmt.Errorf("failed to parse and update rollup event logs: %w", err) @@ -279,12 +317,18 @@ func (s *RollupSyncService) updateRollupEvents(daEntries da.Entries) error { var err error if index > 0 { if parentCommittedBatchMeta, err = rawdb.ReadCommittedBatchMeta(s.db, index-1); err != nil { - return fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", index-1, err) + return fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", index-1, errors.Join(ErrMissingBatchEvent, err)) + } + if parentCommittedBatchMeta == nil { + return fmt.Errorf("parent committed batch meta = nil, batch index: %v, err: %w", index-1, ErrMissingBatchEvent) } } committedBatchMeta, err := rawdb.ReadCommittedBatchMeta(s.db, index) if err != nil { - return fmt.Errorf("failed to read committed batch meta, batch index: %v, err: %w", index, err) + return fmt.Errorf("failed to read committed batch meta, batch index: %v, err: %w", index, errors.Join(ErrMissingBatchEvent, err)) + } + if committedBatchMeta == nil { + return fmt.Errorf("committed batch meta = nil, batch index: %v, err: %w", index, ErrMissingBatchEvent) } chunks, err := s.getLocalChunksForBatch(committedBatchMeta.ChunkBlockRanges) @@ -312,6 +356,7 @@ func (s *RollupSyncService) updateRollupEvents(daEntries da.Entries) error { return fmt.Errorf("failed to batch write finalized batch meta to database: %w", err) } rawdb.WriteFinalizedL2BlockNumber(s.db, highestFinalizedBlockNumber) + finalizedBlockGauge.Update(int64(highestFinalizedBlockNumber)) rawdb.WriteLastFinalizedBatchIndex(s.db, batchIndex) log.Debug("write finalized l2 block number", "batch index", batchIndex, "finalized l2 block height", highestFinalizedBlockNumber) @@ -385,16 +430,20 @@ func (s *RollupSyncService) getLocalChunksForBatch(chunkBlockRanges []*rawdb.Chu return nil, fmt.Errorf("failed to get block by number: %v", i) } txData := encoding.TxsToTxsData(block.Transactions()) - state, err := s.bc.StateAt(block.Root()) - if err != nil { - return nil, fmt.Errorf("failed to get block state, block: %v, err: %w", block.Hash().Hex(), err) - } - withdrawRoot := withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, state) chunks[i].Blocks[j-cr.StartBlockNumber] = &encoding.Block{ Header: block.Header(), Transactions: txData, - WithdrawRoot: withdrawRoot, } + + // read withdraw root, if available + // note: historical state is not available on full nodes + state, err := s.bc.StateAt(block.Root()) + if err != nil { + log.Trace("State is not available, skipping withdraw trie validation", "blockNumber", block.NumberU64(), "blockHash", block.Hash().Hex(), "err", err) + continue + } + withdrawRoot := withdrawtrie.ReadWTRSlot(rcfg.L2MessageQueueAddress, state) + chunks[i].Blocks[j-cr.StartBlockNumber].WithdrawRoot = withdrawRoot } } @@ -424,7 +473,10 @@ func (s *RollupSyncService) getCommittedBatchMeta(commitedBatch da.EntryWithBloc if commitedBatch.Version() == encoding.CodecV7 { parentCommittedBatchMeta, err := rawdb.ReadCommittedBatchMeta(s.db, commitedBatch.BatchIndex()-1) if err != nil { - return nil, fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", commitedBatch.BatchIndex()-1, err) + return nil, fmt.Errorf("failed to read parent committed batch meta, batch index: %v, err: %w", commitedBatch.BatchIndex()-1, errors.Join(ErrMissingBatchEvent, err)) + } + if parentCommittedBatchMeta == nil { + return nil, fmt.Errorf("parent committed batch meta = nil, batch index: %v, err: %w", commitedBatch.BatchIndex()-1, ErrMissingBatchEvent) } // If parent batch has a lower version this means this is the first batch of CodecV7. @@ -535,6 +587,16 @@ func validateBatch(batchIndex uint64, event *l1.FinalizeBatchEvent, parentFinali daBatch, err := codec.NewDABatch(batch) if err != nil { + // This is hotfix for the L1 message hash mismatch issue which lead to wrong committedBatchMeta.PostL1MessageQueueHash hashes. + // These in turn lead to a wrongly computed batch hash locally. This happened after upgrading to EuclidV2 + // where da-codec was not updated to the latest version in l2geth. + // If the error message due to mismatching PostL1MessageQueueHash contains the same hash as the hardcoded one, + // this means the node ran into this issue. + // We need to reset the sync height to 1 block before the L1 block in which the last batch in CodecV6 was committed. + // The node will overwrite the wrongly computed message queue hashes. + if strings.Contains(err.Error(), "0xaa16faf2a1685fe1d7e0f2810b1a0e98c2841aef96596d10456a6d0f00000000") { + return 0, nil, ErrShouldResetSyncHeight + } return 0, nil, fmt.Errorf("failed to create DA batch, batch index: %v, codec version: %v, err: %w", batchIndex, codecVersion, err) } localBatchHash := daBatch.Hash() @@ -555,7 +617,9 @@ func validateBatch(batchIndex uint64, event *l1.FinalizeBatchEvent, parentFinali os.Exit(1) } - if localWithdrawRoot != event.WithdrawRoot() { + // note: this check is optional, + // withdraw root correctness is already implied by state root correctness. + if localWithdrawRoot != (common.Hash{}) && localWithdrawRoot != event.WithdrawRoot() { log.Error("Withdraw root mismatch", "batch index", event.BatchIndex().Uint64(), "start block", startBlock.Header.Number.Uint64(), "end block", endBlock.Header.Number.Uint64(), "parent batch hash", parentFinalizedBatchMeta.BatchHash.Hex(), "l1 finalized withdraw root", event.WithdrawRoot().Hex(), "l2 withdraw root", localWithdrawRoot.Hex()) stack.Close() os.Exit(1) @@ -584,7 +648,7 @@ func validateBatch(batchIndex uint64, event *l1.FinalizeBatchEvent, parentFinali BatchHash: localBatchHash, TotalL1MessagePopped: totalL1MessagePopped, StateRoot: localStateRoot, - WithdrawRoot: localWithdrawRoot, + WithdrawRoot: event.WithdrawRoot(), } return endBlock.Header.Number.Uint64(), finalizedBatchMeta, nil } diff --git a/rollup/sync_service/sync_service.go b/rollup/sync_service/sync_service.go index 65743b5e7e..a8edba793b 100644 --- a/rollup/sync_service/sync_service.go +++ b/rollup/sync_service/sync_service.go @@ -1,6 +1,7 @@ package sync_service import ( + "bytes" "context" "fmt" "reflect" @@ -15,6 +16,7 @@ import ( "github.com/scroll-tech/go-ethereum/metrics" "github.com/scroll-tech/go-ethereum/node" "github.com/scroll-tech/go-ethereum/params" + "github.com/scroll-tech/go-ethereum/rlp" ) const ( @@ -272,19 +274,43 @@ func (s *SyncService) fetchMessages() { if len(msgs) > 0 { log.Debug("Received new L1 events", "fromBlock", from, "toBlock", to, "count", len(msgs)) - rawdb.WriteL1Messages(batchWriter, msgs) // collect messages in memory - numMsgsCollected += len(msgs) } for _, msg := range msgs { if msg.QueueIndex > 0 { queueIndex++ } + // check if received queue index matches expected queue index - if msg.QueueIndex != queueIndex { + if msg.QueueIndex > queueIndex { log.Error("Unexpected queue index in SyncService", "expected", queueIndex, "got", msg.QueueIndex, "msg", msg) return // do not flush inconsistent data to disk } + + // compare with stored message in database, abort if not equal, ignore if already exists + if msg.QueueIndex < queueIndex { + log.Warn("Duplicate queue index in SyncService", "expected", queueIndex, "got", msg.QueueIndex) + + receivedMsgBytes, err := rlp.EncodeToBytes(msg) + if err != nil { + log.Error("Failed to encode message", "err", err) + return + } + storedMsgBytes := rawdb.ReadL1MessageRLP(s.db, msg.QueueIndex) + if !bytes.Equal(storedMsgBytes, receivedMsgBytes) { + storedL1Message := rawdb.ReadL1Message(s.db, msg.QueueIndex) + log.Error("Stored message at same queue index does not match received message", "queueIndex", msg.QueueIndex, "expected", storedL1Message, "got", msg) + return + } + + // already exists, ignore + queueIndex-- + continue + } + + // store message to database (collected in memory and flushed periodically) + rawdb.WriteL1Message(batchWriter, msg) + numMsgsCollected++ } numBlocksPendingDbWrite += to - from + 1