diff --git a/.github/workflows/linea-sequencer-plugin-release.yml b/.github/workflows/linea-sequencer-plugin-release.yml new file mode 100644 index 00000000..2a130281 --- /dev/null +++ b/.github/workflows/linea-sequencer-plugin-release.yml @@ -0,0 +1,69 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version (must start with "v", e.g., "v1.2.3")' + required: true + type: string + +jobs: + build: + runs-on: gha-runner-scale-set-ubuntu-22.04-amd64-small + steps: + - name: Validate version + run: | + if [[ ! "${{ github.event.inputs.version }}" =~ ^v.+ ]]; then + echo "Error: version must start with 'v'" + exit 1 + fi + + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + cache: 'gradle' + + - name: Build artifacts + run: ./gradlew besu-plugins:linea-sequencer:artifacts -PreleaseVersion=${{ github.event.inputs.version }} + env: + JAVA_OPTS: -Xmx2g -Dorg.gradle.daemon=false + + # TODO in later ticket - actions/create-release and actions/upload-release-asset are both archived + # ncipollo/release-action@v1 is an actively-maintained alternative that can combine release creation and artifact upload + - name: Draft Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: 'Draft release of version ${{ github.event.inputs.version }}.' + draft: true + prerelease: false + + - name: Upload Release Lib Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./besu-plugins/linea-sequencer/sequencer/build/libs/linea-sequencer-${{ github.event.inputs.version }}.jar + asset_name: linea-sequencer-${{ github.event.inputs.version }}.jar + asset_content_type: application/octet-stream + + - name: Upload Release Dist Asset + id: upload-release-dist-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./besu-plugins/linea-sequencer/sequencer/build/distributions/linea-sequencer-${{ github.event.inputs.version }}.zip + asset_name: linea-sequencer-${{ github.event.inputs.version }}.zip + asset_content_type: application/octet-stream diff --git a/.github/workflows/linea-sequencer-plugin-testing.yml b/.github/workflows/linea-sequencer-plugin-testing.yml new file mode 100644 index 00000000..5bee53e9 --- /dev/null +++ b/.github/workflows/linea-sequencer-plugin-testing.yml @@ -0,0 +1,79 @@ +name: linea-sequencer-plugin-testing + +on: + workflow_call: + +concurrency: + group: linea-sequencer-plugin-testing-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + validation: + name: "Gradlew Wrapper Checksum Validation" + runs-on: gha-runner-scale-set-ubuntu-22.04-amd64-small + steps: + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v4 + + build: + name: "Linea Sequencer Plugin Build" + runs-on: gha-runner-scale-set-ubuntu-22.04-amd64-small + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Build and test + run: ./gradlew besu-plugins:linea-sequencer:build + env: + JAVA_OPTS: -Xmx2g -Dorg.gradle.daemon=false + + - name: Store distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: distributions + path: besu-plugins/linea-sequencer/sequencer/build/libs + + acceptanceTest: + name: "Linea Sequencer Plugin Acceptance Tests" + runs-on: gha-runner-scale-set-ubuntu-22.04-amd64-large + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Run acceptance tests + run: ./gradlew besu-plugins:linea-sequencer:acceptance-tests:acceptanceTests + env: + JAVA_OPTS: -Dorg.gradle.daemon=false + + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: acceptance-test-report + path: besu-plugins/linea-sequencer/acceptance-tests/build/reports/tests/ + + spotless: + name: "Linea Sequencer Plugin Spotless Check" + runs-on: gha-runner-scale-set-ubuntu-22.04-amd64-small + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: spotless + run: ./gradlew --no-daemon --parallel clean besu-plugins:linea-sequencer:spotlessCheck diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e243877c..586bcf3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,7 @@ jobs: contracts-excluding-local-deployment-artifacts: ${{ steps.exclusion-filter.outputs.contracts-excluding-local-deployment-artifacts }} contracts-excluding-local-deployment-artifacts-count: ${{ steps.exclusion-filter.outputs.contracts-excluding-local-deployment-artifacts_count }} smart-contracts: ${{ steps.filter.outputs.smart-contracts }} + linea-sequencer-plugin: ${{ steps.filter.outputs.linea-sequencer-plugin }} steps: - name: Checkout uses: actions/checkout@v4 @@ -105,6 +106,9 @@ jobs: - 'prover/**' - '.github/workflows/main.yml' - '.github/workflows/run-smc-tests.yml' + linea-sequencer-plugin: + - 'besu-plugins/linea-sequencer/**' + - '.github/workflows/linea-sequencer-plugin-testing.yml' - name: Filter out commit changes uses: dorny/paths-filter@v3 @@ -147,6 +151,7 @@ jobs: with: commit_tag: ${{ needs.store-image-name-and-tags.outputs.commit_tag }} coordinator_changed: ${{ needs.filter-commit-changes.outputs.coordinator }} + linea_sequencer_changed: ${{ needs.filter-commit-changes.outputs.linea-sequencer-plugin }} postman_changed: ${{ needs.filter-commit-changes.outputs.postman }} prover_changed: ${{ needs.filter-commit-changes.outputs.prover }} smart_contracts_changed: ${{ needs.filter-commit-changes.outputs.smart-contracts }} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f93f0129..9383eae2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,6 +24,10 @@ on: smart_contracts_changed: required: true type: string + linea_sequencer_changed: + required: true + type: string + jobs: # Cache for pulling Docker images is disabled as we empirically found that this # (retrieving cache and loading Docker images) actually increased test time-to-completion @@ -63,6 +67,11 @@ jobs: commit_tag: ${{ inputs.commit_tag }} secrets: inherit + linea-sequencer: + uses: ./.github/workflows/linea-sequencer-plugin-testing.yml + if: ${{ inputs.linea_sequencer_changed == 'true' }} + secrets: inherit + # If all jobs are skipped, the workflow will still succeed. always_succeed: runs-on: ubuntu-24.04 diff --git a/besu-plugins/linea-sequencer/.gitignore b/besu-plugins/linea-sequencer/.gitignore new file mode 100644 index 00000000..a79920eb --- /dev/null +++ b/besu-plugins/linea-sequencer/.gitignore @@ -0,0 +1,34 @@ +*.bak +*.swp +*.tmp +*~.nib +*.iml +*.launch +*.swp +*.log +*.out +nohup.out +.classpath +.DS_Store +.externalToolBuilders/ +.gradle/ +.idea/ +.loadpath +.metadata +.prefs +.project +.recommenders/ +.settings +.springBeans +.vertx +.java-version +./bin +local.properties +target/ +tmp/ +build/ +out/ +site/ +/.direnv/ +/.envrc +acceptance-tests/bin/ diff --git a/besu-plugins/linea-sequencer/CHANGELOG.md b/besu-plugins/linea-sequencer/CHANGELOG.md new file mode 100644 index 00000000..fdf2d21e --- /dev/null +++ b/besu-plugins/linea-sequencer/CHANGELOG.md @@ -0,0 +1,220 @@ +# Changelog + +## Next release +* feat: Report rejected transactions only due to trace limit overflows to an external service. +* feat: Report rejected transactions to an external service for validators used by LineaTransactionPoolValidatorPlugin [#85](https://github.com/Consensys/linea-sequencer/pull/85) +* feat: Report rejected transactions to an external service for LineaTransactionSelector used by LineaTransactionSelectorPlugin [#69](https://github.com/Consensys/linea-sequencer/pull/69) + +## 0.6.0-rc1.1 +* bump linea-arithmetization version to 0.6.0-rc1 [#71](https://github.com/Consensys/linea-sequencer/pull/71) +* bump Linea-Besu version to 24.9-delivery32 [#71](https://github.com/Consensys/linea-sequencer/pull/71) + +## 0.3.0-rc2.1 +* bump linea-arithmetization version to 0.3.0-rc2 [#62](https://github.com/Consensys/linea-sequencer/pull/62) +* bump Linea-Besu version to 24.7-develop-c0029e6 (delivery-28) [#62](https://github.com/Consensys/linea-sequencer/pull/62) + +## 0.3.0-rc1.1 +* bump linea-arithmetization version to 0.3.0-rc1 [#54](https://github.com/Consensys/linea-sequencer/pull/54) +* bump Linea-Besu version to 24.7-develop-f812936 (delivery-27) [#54](https://github.com/Consensys/linea-sequencer/pull/54) +* Fix linea_estimateGas when called with gasPrice set [#58](https://github.com/Consensys/linea-sequencer/pull/58) + +## 0.2.0-rc5.1 +* bump linea-arithmetization version to 0.2.0-rc5 [#55](https://github.com/Consensys/linea-sequencer/pull/55) + +## 0.2.0-rc4.1 +* feat: bump linea-arithmetization version to 0.2.0-rc4 [#47](https://github.com/Consensys/linea-sequencer/pull/47) +* Option to disable setting minGasPrice via extra data [#50](https://github.com/Consensys/linea-sequencer/pull/50) +* Remove the check that profitable priority fee must be greater than minGasPrice [#49](https://github.com/Consensys/linea-sequencer/pull/49) +* Fix and enable unit tests in CI [#45](https://github.com/Consensys/linea-sequencer/pull/45) +* Documentation using javadoc [#33](https://github.com/Consensys/linea-sequencer/pull/33) +* Improve error log when setting pricing conf via extra data fails [#44](https://github.com/Consensys/linea-sequencer/pull/44) + +## 0.1.5-test1 +First release of the new series that uses on the ZkTracer as dependency from `linea-arithmetization` repo +* arithmetizationVersion=0.1.5-rc3 [#29](https://github.com/Consensys/linea-sequencer/pull/29) +* Align linea_estimateGas behavior to geth [#25](https://github.com/Consensys/linea-sequencer/pull/25) +* Implement linea_setExtraData [#19](https://github.com/Consensys/linea-sequencer/pull/19) +* Set plugin-linea-tx-pool-simulation-check-api-enabled=false by default [#23](https://github.com/Consensys/linea-sequencer/pull/23) + +## 0.1.4-test28 +Test pre-release 28 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Extra data based pricing [#10](https://github.com/Consensys/linea-sequencer/pull/10) +* Remove check that minGasPrice need to decrease to retry unprofitable tx [#17](https://github.com/Consensys/linea-sequencer/pull/17) + +## 0.1.4-test27 +Test pre-release 27 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Calculate line count only once in linea_estimateGas [#13](https://github.com/Consensys/linea-sequencer/pull/13) + +## 0.1.4-test26 +Test pre-release 26 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Improve ZkTracer initialization time [#11](https://github.com/Consensys/linea-sequencer/pull/11) +* Add more log to txpool simulation validator [#12](https://github.com/Consensys/linea-sequencer/pull/12) + +## 0.1.4-test25 +Test pre-release 25 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Extend Module Line Count Verification to linea_estimateGas RPC Method [#1](https://github.com/Consensys/linea-sequencer/pull/1) +* In the txpool, reject a tx if its simulation fails [#2](https://github.com/Consensys/linea-sequencer/pull/2) + +## 0.1.4-test24 +Test pre-release 24 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Improve linea_estimateGas error response [#650](https://github.com/Consensys/besu-sequencer-plugins/pull/650) +* On Windows also build Linux native lib so it can run on WSL [#651](https://github.com/Consensys/besu-sequencer-plugins/pull/651) + +## 0.1.4-test23 +Test pre-release 23 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* linea_estimateGas compatibility mode multiplier https://github.com/Consensys/besu-sequencer-plugins/pull/646 + +## 0.1.4-test22 +Test pre-release 22 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* linea_estimateGas compatibility switch https://github.com/Consensys/besu-sequencer-plugins/pull/634 +* Update profitability formula with gas price adjustment option https://github.com/Consensys/besu-sequencer-plugins/pull/638 +* Update code to latest plugin API https://github.com/Consensys/besu-sequencer-plugins/pull/640 +* Txpool profitability check https://github.com/Consensys/besu-sequencer-plugins/pull/603 +* Fix price adjustment in profitability formula https://github.com/Consensys/besu-sequencer-plugins/pull/642 + +## 0.1.4-test21 +Test pre-release 21 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: capture SSTORE-touched storage slots for correct gas computations [#606](https://github.com/Consensys/besu-sequencer-plugins/pull/606) +* build: make the build script portable, explicit dependency on Go & GCC, test libcompress build [#621](https://github.com/Consensys/besu-sequencer-plugins/pull/621) +* Update after the refactor of transaction selection service [#626](https://github.com/Consensys/besu-sequencer-plugins/pull/626) +* Use the right classloader to load the native library [#628](https://github.com/Consensys/besu-sequencer-plugins/pull/628) + +## 0.1.4-test20 +Test pre-release 20 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Get L2L1 settings from CLI options [#591](https://github.com/Consensys/besu-sequencer-plugins/pull/591) +* feat: add a replay capture script [#600](https://github.com/Consensys/besu-sequencer-plugins/pull/600) +* move compress native into plugin repo [#604](https://github.com/Consensys/besu-sequencer-plugins/pull/604) +* Add compression [#605](https://github.com/Consensys/besu-sequencer-plugins/pull/605) +* Update for the new bad block manager [#607](https://github.com/Consensys/besu-sequencer-plugins/pull/607) + +## 0.1.4-test19 +Test pre-release 19 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Avoid returning an estimated priority fee that is less than the min gas price [#598](https://github.com/Consensys/besu-sequencer-plugins/pull/598) + +## 0.1.4-test18 +Test pre-release 18 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: check that spilling and limits file contain all counted modules [#592](https://github.com/Consensys/besu-sequencer-plugins/pull/592) + +## 0.1.4-test18-RC3 +Test pre-release 18-RC3 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Use compressed tx size also when selecting txs from block creation [#590](https://github.com/Consensys/besu-sequencer-plugins/pull/590) + +## 0.1.4-test18-RC2 +Test pre-release 18-RC2 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Fix linea_estimateGas reports Internal error when value or gas price is missing [#587](https://github.com/Consensys/besu-sequencer-plugins/pull/587) + +## 0.1.4-test18-RC1 +Test pre-release 18-RC1 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Linea estimate gas endpoint [#585](https://github.com/Consensys/besu-sequencer-plugins/pull/585) + +## 0.1.4-test17 +Test pre-release 17 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* tests: drop huge random tests [#563](https://github.com/Consensys/besu-sequencer-plugins/pull/563) +* feat(modexp-data): implement MODEXP_DATA module [#547](https://github.com/Consensys/besu-sequencer-plugins/pull/547) +* feat: mechanics to capture conflations & replay them as test cases [#561](https://github.com/Consensys/besu-sequencer-plugins/pull/561) +* perf(EUC): one less column [#570](https://github.com/Consensys/besu-sequencer-plugins/pull/570) +* docs: Add basic plugins doc [#509](https://github.com/Consensys/besu-sequencer-plugins/pull/509) +* Check upfront profitability + Unprofitable txs cache and retry limit [#565](https://github.com/Consensys/besu-sequencer-plugins/pull/565) +* Avoid reprocessing txs that go over line count limit [#571](https://github.com/Consensys/besu-sequencer-plugins/pull/571) + +## 0.1.4-test16 +Test pre-release 16 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: bug-compatibility with Geth +* fix: PubHash 16 factor + +Full changeset https://github.com/Consensys/besu-sequencer-plugins/compare/v0.1.4-test15...v0.1.4-test16 + +## 0.1.4-test15 +release rebase off of main +* add option to adjust the tx size used to calculate the profitability of a tx during block creation(#562)[https://github.com/Consensys/besu-sequencer-plugins/pull/562] + +## 0.1.4-test14 +release rebase off of main +Test pre-release 14 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Fix log of line counts in case of block limit reached + minor changes [#555](https://github.com/Consensys/besu-sequencer-plugins/pull/555) +* build: update Corset to 9.3.0 [#554](https://github.com/Consensys/besu-sequencer-plugins/pull/554) + +## 0.1.4-test13 +Test pre-release 13 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix stackedSet [c3f226775f24508b93a758e4226a51ae386d76a5](https://github.com/Consensys/besu-sequencer-plugins/commit/c3f226775f24508b93a758e4226a51ae386d76a5) + +## 0.1.4-test12 +Test pre-release 12 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: stacked set multiple insertions in a single transaction (#548) + +## 0.1.4-test11 +Test pre-release 11 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* same as 0.1.4-test10 + +## 0.1.4-test10 +Test pre-release 10 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: semantics of LinkedList (#544) +* refactor: add @EqualsAndHashCode annotations and remove corresponding methods (#541) + +## 0.1.4-test9 +Test pre-release 9 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Bump Linea Besu to 24.1.1-SNAPSHOT + +## 0.1.4-test8 +Test pre-release 8 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Add profitable transaction selector [#530](https://github.com/Consensys/besu-sequencer-plugins/pull/530) +* temp: geth-compatibily hacks [820918a](https://github.com/Consensys/besu-sequencer-plugins/commit/820918a39e8d394e73b8de85a46391ffe7d314b1) + +## 0.1.4-test7 +Test pre-release 7 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* fix: invalid SStore gas computation [#532](https://github.com/Consensys/besu-sequencer-plugins/pull/532) + +## 0.1.4-test6 +Test pre-release 6, fix: [make precompile counters work](https://github.com/Consensys/besu-sequencer-plugins/commit/10f03ead5207746f253703a328f13988ed9b9305) +* feat: implement fake hashdata/info [Franklin Delehelle] +* temp: geth-compatibily hacks [Franklin Delehelle] +* refactor: group RLPs modules, use retro-compatible module keys [#508](https://github.com/Consensys/besu-sequencer-plugins/pull/508) +* [MINOR] Add javadoc [#507](https://github.com/Consensys/besu-sequencer-plugins/pull/507) +* style: update name of prec limits to avoid confusion with old geth name [#506](https://github.com/Consensys/besu-sequencer-plugins/pull/506) +* perf: cache tx-specific line counter [#497](https://github.com/Consensys/besu-sequencer-plugins/pull/497) +* fix: continuous tracing plugin start check [#500](https://github.com/Consensys/besu-sequencer-plugins/pull/500) +* fix: lookup txndata <-> wcp [#488](https://github.com/Consensys/besu-sequencer-plugins/pull/488) +* fix(romLex): wrong stack arg for extcodecopy address [#498](https://github.com/Consensys/besu-sequencer-plugins/pull/498) + +## 0.1.4-test3 +Test pre-release 3 from [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) +* Log ZkTracer counters for every produced block [#485](https://github.com/Consensys/besu-sequencer-plugins/pull/485) +* fix: overflow for modexp arg [#489](https://github.com/Consensys/besu-sequencer-plugins/pull/489) +* bin reimplementation [#473](https://github.com/Consensys/besu-sequencer-plugins/pull/473) +* applyMavenExclusions=false [#477](https://github.com/Consensys/besu-sequencer-plugins/pull/477) + +## 0.1.4-test2 +Testing pre-release from branch test-release/v0.1.4-test2 + +* revert make loginfo counts closer to Geth +* head: disable stp & txndata + +## 0.1.4-test +Temporary line counting release for testnet. + +* count stack temporary impl: make loginfo counts closer to Geth [temp/issue-248/count-stack-only](https://github.com/Consensys/besu-sequencer-plugins/tree/temp/issue-248/count-stack-only) + -- +* fix: `Bytes.toUnsignedInteger` [#484](https://github.com/Consensys/besu-sequencer-plugins/pull/484) +* perf: delay computations at trace time [#483](https://github.com/Consensys/besu-sequencer-plugins/pull/483) + +## 0.1.3 +- perf: improve `StackedSet` performances [#466](https://github.com/Consensys/besu-sequencer-plugins/pull/466) +- feat: implement L1 block & Keccak limits [#445](https://github.com/Consensys/besu-sequencer-plugins/pull/445) +- feat: partially implement EC_DATA [#475](https://github.com/Consensys/besu-sequencer-plugins/pull/475) +- fix: ensure trace files are always deleted [#462](https://github.com/Consensys/besu-sequencer-plugins/pull/462) + + +## 0.1.2 +Release 8 for 23.10.4-SNAPSHOT of linea-besu +- changed default file name to toml [#476](https://github.com/Consensys/besu-sequencer-plugins/pull/476) +- feat: implement `BIN` counting [#471](https://github.com/Consensys/besu-sequencer-plugins/pull/471) +- Upgrade Linea Besu to 23.10.4-SNAPSHOT [#469](https://github.com/Consensys/besu-sequencer-plugins/pull/469) +- fix: incorrect address comparison [#470](https://github.com/Consensys/besu-sequencer-plugins/pull/470) +- fix: line count discrepancy [#468](https://github.com/Consensys/besu-sequencer-plugins/pull/468) + +## 0.1.1 +Release for 23.10.3-SNAPSHOT of linea-besu + +## 0.1.0 +- Initial build of besu-sequencer-plugins +- uses 23.10.3-SNAPSHOT as linea-besu version diff --git a/besu-plugins/linea-sequencer/LICENSE b/besu-plugins/linea-sequencer/LICENSE new file mode 100644 index 00000000..5c5c6254 --- /dev/null +++ b/besu-plugins/linea-sequencer/LICENSE @@ -0,0 +1,15 @@ +# License + +Copyright 2024 Consensys Software Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/README.md b/besu-plugins/linea-sequencer/README.md new file mode 100644 index 00000000..7477515a --- /dev/null +++ b/besu-plugins/linea-sequencer/README.md @@ -0,0 +1,65 @@ +# Besu Plugins related to tracer and sequencer functionality + +This repository hosts the implementation of the sequencer, the component of the Linea stack responsible for ordering transactions and building blocks, as well as executing them. It provides a set of [Besu](https://github.com/hyperledger/besu):Linea plugins. + +It serves developers by making the Linea tech stack open source under +the [Apache 2.0 license](LICENSE). + +## What is Linea? + +[Linea](https://linea.build) is a developer-ready layer 2 network scaling Ethereum. It's secured with a zero-knowledge rollup, built on lattice-based cryptography, and powered by [Consensys](https://consensys.io). + +## Get started + +If you already have an understanding of the tech stack, use our [Quickstart](docs/quickstart.md) guide. + +### Looking for Plugins? + +Discover [existing plugins](docs/plugins.md) and understand the [plugin release process](docs/plugin-release.md). + +## Looking for the Linea code? + +Linea's stack is made up of multiple repositories, these include: +- This repo, [linea-monorepo](https://github.com/Consensys/linea-monorepo): main repository for the Linea stack & network +- [besu](https://github.com/hyperledger/besu): Besu client +- [linea-tracer](https://github.com/Consensys/linea-tracer): Linea-Besu plugin which produces the traces that the constraint system applies and that serve as inputs to the prover +- [linea-constraints](https://github.com/Consensys/linea-constraints): Implementation of the constraint system from the specification +- [linea-specification](https://github.com/Consensys/linea-specification): Specification of the constraint system defining Linea's zkEVM + +Linea abstracts away the complexity of this technical architecture to allow developers to: + +- [Bridge tokens](https://docs.linea.build/developers/guides/bridge) +- [Deploy a contract](https://docs.linea.build/developers/quickstart/deploy-smart-contract) +- [Run a node](https://docs.linea.build/developers/guides/run-a-node) + +... and more. + +## How to contribute + +Contributions are welcome! + +### Guidelines for Non-Code and other Trivial Contributions +Please keep in mind that we do not accept non-code contributions like fixing comments, typos or some other trivial fixes. Although we appreciate the extra help, managing lots of these small contributions is unfeasible, and puts extra pressure in our continuous delivery systems (running all tests, etc). Feel free to open an issue pointing any of those errors, and we will batch them into a single change. + +1. [Create an issue](https://github.com/Consensys/linea-monorepo/issues). +> If the proposed update requires input, also tag us for discussion. +2. Submit the update as a pull request from your [fork of this repo](https://github.com/Consensys/linea-monorepo/fork), and tag us for review. +> Include the issue number in the pull request description and (optionally) in the branch name. + +Consider starting with a ["good first issue"](https://github.com/ConsenSys/linea-monorepo/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). + +Before contributing, ensure you're familiar with: + +- Our [Linea contribution guide](https://github.com/Consensys/linea-monorepo/blob/main/docs/contribute.md) +- Our [Linea code of conduct](https://github.com/Consensys/linea-monorepo/blob/main/docs/code-of-conduct.md) +- The [Besu contribution guide](https://github.com/hyperledger/besu/blob/main/CONTRIBUTING.md), for Besu:Linea related contributions +- Our [Security policy](https://github.com/Consensys/linea-monorepo/blob/main/docs/security.md) + + +### Useful links + +- [Linea docs](https://docs.linea.build) +- [Linea blog](https://linea.mirror.xyz) +- [Support](https://support.linea.build) +- [Discord](https://discord.gg/linea) +- [X](https://x.com/LineaBuild) diff --git a/besu-plugins/linea-sequencer/acceptance-tests/build.gradle b/besu-plugins/linea-sequencer/acceptance-tests/build.gradle new file mode 100644 index 00000000..7073fea8 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/build.gradle @@ -0,0 +1,86 @@ +import java.time.LocalTime + +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + alias(libs.plugins.web3j) + alias(libs.plugins.web3jSolidity) + alias(libs.plugins.lombok) + alias(libs.plugins.gradleVersions) + alias(libs.plugins.dependencyManagement) + alias(libs.plugins.download) +} + +def lineaSequencerProject = project(lineaSequencerProjectPath) +apply from: lineaSequencerProject.file("gradle/java.gradle") +apply from: lineaSequencerProject.file("gradle/dependency-management.gradle") +apply from: lineaSequencerProject.file('gradle/common-dependencies.gradle') +apply from: lineaSequencerProject.file("gradle/lint.gradle") + +web3j { + generatedPackageName = 'linea.plugin.acc.test.tests.web3j.generated' +} + +solidity { + resolvePackages = false + optimizeRuns = 1 + // TODO: remove the forced version, when DEV network is upgraded to support latest forks + version '0.8.19' +} + +processTestResources.dependsOn("${lineaSequencerProjectPath}:acceptance-tests:generateTestContractWrappers") + +tasks.register('acceptanceTests', Test) { + description = 'Runs acceptance tests.' + + inputs.property("integration.date", LocalTime.now()) // so it runs on every invocation + + useJUnitPlatform { + includeTags("AcceptanceTest") + } + + maxParallelForks Runtime.runtime.availableProcessors() + systemProperties["junit.jupiter.execution.parallel.enabled"] = false +} + +dependencies { + annotationProcessor 'org.apache.logging.log4j:log4j-core' + + implementation 'org.apache.logging.log4j:log4j-core' + + testImplementation project("${lineaSequencerProjectPath}:sequencer") + + testImplementation "${besuArtifactGroup}:besu-datatypes" + testImplementation "${besuArtifactGroup}.internal:clique" + testImplementation "${besuArtifactGroup}.internal:api" + testImplementation "${besuArtifactGroup}.internal:core" + testImplementation "${besuArtifactGroup}.internal:dsl" + testImplementation "${besuArtifactGroup}.internal:eth" + testImplementation "${besuArtifactGroup}.internal:metrics-core" + testImplementation "${besuArtifactGroup}.internal:services" + testImplementation group: "${besuArtifactGroup}.internal", name: "core", classifier: "test-support" + + testImplementation 'net.consensys.linea.zktracer:arithmetization' + + testImplementation 'org.awaitility:awaitility' +} + +// Do not run acceptance tests with ./gradlew test, only with ./gradlew acceptanceTests +test.enabled = false + +jar { + enabled = false +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/main/java/linea/plugin/acc/test/utils/MemoryAppender.java b/besu-plugins/linea-sequencer/acceptance-tests/src/main/java/linea/plugin/acc/test/utils/MemoryAppender.java new file mode 100644 index 00000000..f81a2fce --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/main/java/linea/plugin/acc/test/utils/MemoryAppender.java @@ -0,0 +1,61 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.utils; + +import java.io.ByteArrayOutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; + +import lombok.SneakyThrows; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +@Plugin(name = "Memory", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class MemoryAppender extends AbstractAppender { + private static final ByteArrayOutputStream collectingOutput = new ByteArrayOutputStream(); + + protected MemoryAppender(String name, Layout layout) { + super(name, null, layout, true, Property.EMPTY_ARRAY); + } + + @PluginFactory + public static MemoryAppender createAppender( + @PluginAttribute("name") String name, + @PluginElement("Layout") Layout layout) { + return new MemoryAppender(name, layout); + } + + @SneakyThrows + @Override + public void append(LogEvent event) { + collectingOutput.write(getLayout().toByteArray(event)); + } + + public static void reset() { + collectingOutput.reset(); + } + + public static String getLog() { + return collectingOutput.toString(StandardCharsets.UTF_8); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java new file mode 100644 index 00000000..b1decd2c --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java @@ -0,0 +1,103 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import java.math.BigInteger; +import java.util.List; + +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; + +/** This class tests the block gas limit functionality of the plugin. */ +public class BlockGasLimitTest extends LineaPluginTestBase { + + private static final BigInteger GAS_PRICE = BigInteger.TEN.pow(9); + private static final BigInteger VALUE = BigInteger.TWO; + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-max-block-gas=", "300000") + .build(); + } + + @Override + @BeforeEach + public void setup() throws Exception { + super.setup(); + minerNode.execute(minerTransactions.minerStop()); + } + + /** + * if we have a list of transactions [t_0.3, t_0.3, t_0.66, t_0.4], just two blocks are created, + * where t_x fills X% of a limit. + */ + @Test + public void multipleBlocksFilledRespectingUserBlockGasLimit() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials1 = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager1 = new RawTransactionManager(web3j, credentials1, CHAIN_ID); + final Credentials credentials2 = Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY); + TransactionManager txManager2 = new RawTransactionManager(web3j, credentials2, CHAIN_ID); + + final var tx100kGas1 = + txManager1.sendTransaction( + GAS_PRICE, + BigInteger.valueOf(MAX_TX_GAS_LIMIT).divide(BigInteger.TEN), + accounts.getSecondaryBenefactor().getAddress(), + "a".repeat(10000), + VALUE); + + final var tx100kGas2 = + txManager1.sendTransaction( + GAS_PRICE.multiply(BigInteger.TWO), + BigInteger.valueOf(MAX_TX_GAS_LIMIT).divide(BigInteger.TEN), + accounts.getSecondaryBenefactor().getAddress(), + "b".repeat(10000), + VALUE); + + final var tx200kGas = + txManager2.sendTransaction( + GAS_PRICE.multiply(BigInteger.TEN), + BigInteger.valueOf(MAX_TX_GAS_LIMIT).divide(BigInteger.TEN), + accounts.getPrimaryBenefactor().getAddress(), + "c".repeat(20000), + VALUE); + + final var tx125kGas = + txManager1.sendTransaction( + GAS_PRICE.multiply(BigInteger.TWO), + BigInteger.valueOf(MAX_TX_GAS_LIMIT).divide(BigInteger.TEN), + accounts.getSecondaryBenefactor().getAddress(), + "d".repeat(12500), + VALUE); + + startMining(); + + assertTransactionsMinedInSameBlock( + web3j, List.of(tx100kGas1.getTransactionHash(), tx200kGas.getTransactionHash())); + assertTransactionsMinedInSameBlock( + web3j, List.of(tx100kGas2.getTransactionHash(), tx125kGas.getTransactionHash())); + } + + private void startMining() { + minerNode.execute(minerTransactions.minerStart()); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/EcDataLimitsTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/EcDataLimitsTest.java new file mode 100644 index 00000000..52bb7c17 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/EcDataLimitsTest.java @@ -0,0 +1,564 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import linea.plugin.acc.test.tests.web3j.generated.EcAdd; +import linea.plugin.acc.test.tests.web3j.generated.EcMul; +import linea.plugin.acc.test.tests.web3j.generated.EcPairing; +import linea.plugin.acc.test.tests.web3j.generated.EcRecover; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.utils.Numeric; + +public class EcDataLimitsTest extends LineaPluginTestBase { + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + // disable line count validation to accept excluded precompile txs in the txpool + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "false") + // set the module limits file + .set("--plugin-linea-module-limit-file-path=", getResourcePath("/moduleLimits.toml")) + .build(); + } + + @Override + protected GenesisConfigurationFactory.CliqueOptions getCliqueOptions() { + return new GenesisConfigurationFactory.CliqueOptions( + BLOCK_PERIOD_SECONDS + 1, + GenesisConfigurationFactory.CliqueOptions.DEFAULT.epochLength(), + false); + } + + /** + * Tests the EcPairing limits, that are the number of times a certain circuit may be invoked in a + * single block. + * + * @param nTransactions the number of transactions to try to include in the same block. The last + * one is not supposed to fit as it exceeds the limit, thus it is included in the next block + * @param input a function that generates the input data for each transaction + * @param target the expected string to be found in the blocks log + * @throws Exception if an error occurs during the test + */ + @ParameterizedTest + @MethodSource("ecPairingLimitsTestSource") + public void ecPairingLimitsTest( + int nTransactions, BiFunction input, String target) + throws Exception { + + // Deploy the EcPairing contract + final EcPairing ecPairing = deployEcPairing(); + + // Create an account to send the transactions + Account ecPairingSender = accounts.createAccount("ecPairingSender"); + + // Fund the account using secondary benefactor + String fundTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), ecPairingSender, 1, BigInteger.ZERO) + .execute(minerNode.nodeRequests()) + .toHexString(); + // Verify that the transaction for transferring funds was successful + minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash)); + + String[] txHashes = new String[nTransactions]; + for (int i = 0; i < nTransactions; i++) { + // With decreasing nonce we force the transactions to be included in the same block + // i = 0 , 1 , ..., nTransactions - 1 + // nonce = nTransactions - 1, nTransactions - 2, ..., 0 + int nonce = nTransactions - 1 - i; + + // Craft the transaction data + final byte[] encodedCallEcPairing = + encodedCallEcPairing( + ecPairing, + ecPairingSender, + nonce, + Bytes.fromHexString(input.apply(i, nTransactions))); + + // Send the transaction + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(encodedCallEcPairing)).send(); + + // Store the transaction hash + txHashes[nonce] = resp.getTransactionHash(); + } + + // Transfer used as sentry to ensure a new block is mined + final Hash transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.ONE) // nonce is 1 as primary benefactor also deploys the contract + .execute(minerNode.nodeRequests()); + // Wait for the sentry to be mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + + // Assert that all the transactions involving the EcPairing precompile, but the last one, were + // included in the same block + assertTransactionsMinedInSameBlock( + minerNode.nodeRequests().eth(), Arrays.asList(txHashes).subList(0, nTransactions - 1)); + + // Assert that the last transaction was included in another block + assertTransactionsMinedInSeparateBlocks( + minerNode.nodeRequests().eth(), List.of(txHashes[0], txHashes[nTransactions - 1])); + + // Assert that the target string is contained in the blocks log + final String blockLog = getAndResetLog(); + assertThat(blockLog).contains(target); + } + + private static Stream ecPairingLimitsTestSource() { + Map moduleLimits = + ModuleLineCountValidator.createLimitModules( + new LineaTracerConfiguration(getResourcePath("/moduleLimits.toml"))); + final int PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = + moduleLimits.get("PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS"); + final int PRECOMPILE_ECPAIRING_MILLER_LOOPS = + moduleLimits.get("PRECOMPILE_ECPAIRING_MILLER_LOOPS"); + final int PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = + moduleLimits.get("PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS"); + + /* + Structure of the input: + Ax + Ay + BxIm + BxRe + ByIm + ByRe + */ + + // Valid pair requiring 1 Miller Loop and 1 final exponentiation + final String nonTrivial = + "01395d002b3ca9180fb924650ef0656ead838fd027d487fed681de0d674c30da097c3a9a072f9c85edf7a36812f8ee05e2cc73140749dcd7d29ceb34a8412188" + + "2bd3295ff81c577fe772543783411c36f463676d9692ca4250588fbad0b44dc707d8d8329e62324af8091e3a4ffe5a57cb8664d1f5f6838c55261177118e9313" + + "230f1851ba0d3d7d36c8603c7118c86bd2b6a7a1610c4af9e907cb702beff1d812843e703009c1c1a2f1088dcf4d91e9ed43189aa6327cae9a68be22a1aee5cb"; + + // Valid pair requiring 1 G2 membership test + final String leftTrivialValid = + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "266152e278e5dab4e14f0d93a3e54550d08dc30ef4fe911257bd3e313864b85922cebabf989f812c0a6e67362bcb83d55c6378a4f500ecc8a6a5518b3d1695e0" + + "070a5a339edbbb67c35d0d44b3ffff6b5803b198af7645c892f6af2fa8abf6f2117f82e731f61e688908fa2c831c6a1c7775e6f9cfd49e06d1d24d3d13e5936a"; + + List arguments = new ArrayList<>(); + + /* + This test case produces PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + 1 transactions performing one ECPAIRING. + All ECPAIRING are well-formed and none of the points is ever the point at infinity, + leading in total to: + - 1 Miller loops + - 0 G2 membership tests + - 1 final exponentiations + per pairing. + + In total: + - PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + 1 Miller loops (< PRECOMPILE_ECPAIRING_MILLER_LOOPS) + - 0 G2 membership tests + - PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + 1 final exponentiations + + This final transaction exceeds the PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS and must be included in the next block. + */ + arguments.add( + Arguments.of( + PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + + 1, // 1 final exponentiation per transaction + (BiFunction) + (i, nTransactions) -> nonTrivial, // 1 pair per transaction + "Cumulated line count for module PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS=" + + (PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + 1) + + " is above the limit " + + PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS + + ", stopping selection")); + + final int nPairsPerTransaction = 8; + + /* + This test case produces PRECOMPILE_ECPAIRING_MILLER_LOOPS / nPairsPerTransaction + 1 + transactions each performing nPairsPerTransaction ECPAIRING's + except the last transaction which requires only 1. + All ECPAIRING are well-formed and none of the points is ever the point at infinity, + leading in total to: + - 1 Miller loops + - 0 G2 membership tests + - 1 final exponentiations + per pairing. + + In total: + - PRECOMPILE_ECPAIRING_MILLER_LOOPS + 1 Miller loops + - 0 G2 membership tests + - PRECOMPILE_ECPAIRING_MILLER_LOOPS / nPairsPerTransaction + 1 final exponentiations (< PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS) + + This final transaction exceeds the PRECOMPILE_ECPAIRING_MILLER_LOOPS and must be included in the next block. + */ + arguments.add( + Arguments.of( + PRECOMPILE_ECPAIRING_MILLER_LOOPS / nPairsPerTransaction + 1, + // nPairsPerTransaction Miller Loops per transaction except the last one which has 1 + (BiFunction) + (i, nTransactions) -> + nonTrivial.repeat(i < nTransactions - 1 ? nPairsPerTransaction : 1), + // nPairsPerTransaction pairs per transaction except the last one which has 1 + "Cumulated line count for module PRECOMPILE_ECPAIRING_MILLER_LOOPS=" + + (PRECOMPILE_ECPAIRING_MILLER_LOOPS + 1) + + " is above the limit " + + PRECOMPILE_ECPAIRING_MILLER_LOOPS + + ", stopping selection")); + + /* + This test case produces PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS / nPairsPerTransaction + 1 + transactions each performing nPairsPerTransaction ECPAIRING's + except the last transaction which requires only 1. + All ECPAIRING are well-formed and the small point is always the point at infinity + leading to: + - 0 Miller loops + - 1 G2 membership tests + - 0 final exponentiations + per pairing. + + In total: + - 0 Miller loops + - PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS + 1 G2 membership tests + - 0 final exponentiations + + This final transaction exceeds the PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS and must be included in the next block. + */ + arguments.add( + Arguments.of( + PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS / nPairsPerTransaction + 1, + // nPairsPerTransaction g2 membership test per transaction except the last one which has + // 1 + (BiFunction) + (i, nTransactions) -> + leftTrivialValid.repeat(i < nTransactions - 1 ? nPairsPerTransaction : 1), + // nPairsPerTransaction pairs per transaction except the last one which has 1 + "Cumulated line count for module PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS=" + + (PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS + 1) + + " is above the limit " + + PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS + + ", stopping selection")); + + /* + Description of the test cases: + + - This method defines 3 test cases. + - Each test case is defined by a tuple (nTransactions, input, target). See the test method signature for more details. + - Each test case goal is crossing a limit independently. Specifically: + * The first test case crosses the limit of final exponentiations. + * The second test case crosses the limit of Miller loops. + * The third test case crosses the limit of G2 membership tests. + Note that while the first two test cases requires 2 circuits, the limits are crossed independently. + - Each test cases generates at least two blocks, one for the transactions that fit in the limit and another for the + transaction that exceeds the limit. + - Due to how the corresponding test is structured, we observe exactly 4 blocks: + * 1 including 1 transaction to deploy the EcPairing contract calling the precompile + * 1 including 1 transaction to fund ecPairing sender + * 1 including all the transactions that fit in the limit and the sentry transaction + * 1 including the transaction that exceeds the limit + */ + return arguments.stream(); + } + + /** + * Tests the EcAdd PRECOMPILE_ECADD_EFFECTIVE_CALLS limit, that is the number of times the + * corresponding circuit may be invoked in a single block. + */ + @Test + public void ecAddLimitTest() throws Exception { + Map moduleLimits = + ModuleLineCountValidator.createLimitModules( + new LineaTracerConfiguration(getResourcePath("/moduleLimits.toml"))); + final int PRECOMPILE_ECADD_EFFECTIVE_CALLS = + moduleLimits.get("PRECOMPILE_ECADD_EFFECTIVE_CALLS"); + + /* + * nTransactions: the number of transactions to try to include in the same block. The last + * one is not supposed to fit as it exceeds the limit, thus it is included in the next block. Note + * that in this specific test more than one call to the ECADD precompile is executed within the + * same transaction to reach the limit with a smaller number of transaction + * + * input: input data for each transaction + * target: the expected string to be found in the blocks log + */ + final int callsPerTransaction = 32; + final int nTransactions = PRECOMPILE_ECADD_EFFECTIVE_CALLS / callsPerTransaction + 1; + final String input = + "070375d4eec4f22aa3ad39cb40ccd73d2dbab6de316e75f81dc2948a996795d5041b98f07f44aa55ce8bd97e32cacf55f1e42229d540d5e7a767d1138a5da656185f6f5cf93c8afa0461a948c2da7c403b6f8477c488155dfa8d2da1c62517b813d83d7a51eb18fdb51225873c87d44f883e770ce2ca56c305d02d6cb99ca5b8"; + final String target = + "Cumulated line count for module PRECOMPILE_ECADD_EFFECTIVE_CALLS=" + + (PRECOMPILE_ECADD_EFFECTIVE_CALLS + callsPerTransaction) + + " is above the limit " + + PRECOMPILE_ECADD_EFFECTIVE_CALLS + + ", stopping selection"; + + // Deploy the EcAdd contract + final EcAdd ecAdd = deployEcAdd(); + + // Create an account to send the transactions + Account ecAddSender = accounts.createAccount("ecAddSender"); + + // Fund the account using secondary benefactor + String fundTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), ecAddSender, 1, BigInteger.ZERO) + .execute(minerNode.nodeRequests()) + .toHexString(); + // Verify that the transaction for transferring funds was successful + minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash)); + + String[] txHashes = new String[nTransactions]; + for (int i = 0; i < nTransactions; i++) { + // With decreasing nonce we force the transactions to be included in the same block + // i = 0 , 1 , ..., nTransactions - 1 + // nonce = nTransactions - 1, nTransactions - 2, ..., 0 + int nonce = nTransactions - 1 - i; + + // Craft the transaction data + final byte[] encodedCallEcAdd = + encodedCallEcAdd(ecAdd, ecAddSender, nonce, Bytes.fromHexString(input)); + + // Send the transaction + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(encodedCallEcAdd)).send(); + + // Store the transaction hash + txHashes[nonce] = resp.getTransactionHash(); + } + + // Transfer used as sentry to ensure a new block is mined + final Hash transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.ONE) // nonce is 1 as primary benefactor also deploys the contract + .execute(minerNode.nodeRequests()); + // Wait for the sentry to be mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + + // Assert that all the transactions involving the EcPairing precompile, but the last one, were + // included in the same block + assertTransactionsMinedInSameBlock( + minerNode.nodeRequests().eth(), Arrays.asList(txHashes).subList(0, nTransactions - 1)); + + // Assert that the last transaction was included in another block + assertTransactionsMinedInSeparateBlocks( + minerNode.nodeRequests().eth(), List.of(txHashes[0], txHashes[nTransactions - 1])); + + // Assert that the target string is contained in the blocks log + final String blockLog = getAndResetLog(); + assertThat(blockLog).contains(target); + } + + /** + * Tests the EcMul PRECOMPILE_ECMUL_EFFECTIVE_CALLS limit, that is the number of times the + * corresponding circuit may be invoked in a single block. + */ + @Test + public void ecMulLimitTest() throws Exception { + Map moduleLimits = + ModuleLineCountValidator.createLimitModules( + new LineaTracerConfiguration(getResourcePath("/moduleLimits.toml"))); + final int PRECOMPILE_ECMUL_EFFECTIVE_CALLS = + moduleLimits.get("PRECOMPILE_ECMUL_EFFECTIVE_CALLS"); + + /* + * nTransactions: the number of transactions to try to include in the same block. The last + * one is not supposed to fit as it exceeds the limit, thus it is included in the next block + * input: input data for each transaction + * target: the expected string to be found in the blocks log + */ + final int nTransactions = PRECOMPILE_ECMUL_EFFECTIVE_CALLS + 1; + final String input = + "030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd315ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c40000000000000000000000000000000000000000000000000000000000000001"; + final String target = + "Cumulated line count for module PRECOMPILE_ECMUL_EFFECTIVE_CALLS=" + + (PRECOMPILE_ECMUL_EFFECTIVE_CALLS + 1) + + " is above the limit " + + PRECOMPILE_ECMUL_EFFECTIVE_CALLS + + ", stopping selection"; + + // Deploy the EcMul contract + final EcMul ecMul = deployEcMul(); + + // Create an account to send the transactions + Account ecMulSender = accounts.createAccount("ecMulSender"); + + // Fund the account using secondary benefactor + String fundTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), ecMulSender, 1, BigInteger.ZERO) + .execute(minerNode.nodeRequests()) + .toHexString(); + // Verify that the transaction for transferring funds was successful + minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash)); + + String[] txHashes = new String[nTransactions]; + for (int i = 0; i < nTransactions; i++) { + // With decreasing nonce we force the transactions to be included in the same block + // i = 0 , 1 , ..., nTransactions - 1 + // nonce = nTransactions - 1, nTransactions - 2, ..., 0 + int nonce = nTransactions - 1 - i; + + // Craft the transaction data + final byte[] encodedCallEcMul = + encodedCallEcMul(ecMul, ecMulSender, nonce, Bytes.fromHexString(input)); + + // Send the transaction + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(encodedCallEcMul)).send(); + + // Store the transaction hash + txHashes[nonce] = resp.getTransactionHash(); + } + + // Transfer used as sentry to ensure a new block is mined + final Hash transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.ONE) // nonce is 1 as primary benefactor also deploys the contract + .execute(minerNode.nodeRequests()); + // Wait for the sentry to be mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + + // Assert that all the transactions involving the EcPairing precompile, but the last one, were + // included in the same block + assertTransactionsMinedInSameBlock( + minerNode.nodeRequests().eth(), Arrays.asList(txHashes).subList(0, nTransactions - 1)); + + // Assert that the last transaction was included in another block + assertTransactionsMinedInSeparateBlocks( + minerNode.nodeRequests().eth(), List.of(txHashes[0], txHashes[nTransactions - 1])); + + // Assert that the target string is contained in the blocks log + final String blockLog = getAndResetLog(); + assertThat(blockLog).contains(target); + } + + /** + * Tests the EcRecover PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS limit, that is the number of times the + * corresponding circuit may be invoked in a single block. + */ + @Test + public void ecRecoverLimitTest() throws Exception { + Map moduleLimits = + ModuleLineCountValidator.createLimitModules( + new LineaTracerConfiguration(getResourcePath("/moduleLimits.toml"))); + final int PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = + moduleLimits.get("PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS"); + + /* + * nTransactions: the number of transactions to try to include in the same block. The last + * one is not supposed to fit as it exceeds the limit, thus it is included in the next block + * input: input data for each transaction + * target: the expected string to be found in the blocks log + */ + final int nTransactions = PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS + 1; + final String input = + "279d94621558f755796898fc4bd36b6d407cae77537865afe523b79c74cc680b000000000000000000000000000000000000000000000000000000000000001bc2ff96feed8749a5ad1c0714f950b5ac939d8acedbedcbc2949614ab8af063121feecd50adc6273fdd5d11c6da18c8cfe14e2787f5a90af7c7c1328e7d0a2c42"; + final String target = + "Cumulated line count for module PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS=" + + (PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS + 1) + + " is above the limit " + + PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS + + ", stopping selection"; + + // Deploy the EcRecover contract + final EcRecover ecRecover = deployEcRecover(); + + // Create an account to send the transactions + Account ecRecoverSender = accounts.createAccount("ecRecoverSender"); + + // Fund the account using secondary benefactor + String fundTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), ecRecoverSender, 1, BigInteger.ZERO) + .execute(minerNode.nodeRequests()) + .toHexString(); + // Verify that the transaction for transferring funds was successful + minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash)); + + String[] txHashes = new String[nTransactions]; + for (int i = 0; i < nTransactions; i++) { + // With decreasing nonce we force the transactions to be included in the same block + // i = 0 , 1 , ..., nTransactions - 1 + // nonce = nTransactions - 1, nTransactions - 2, ..., 0 + int nonce = nTransactions - 1 - i; + + // Craft the transaction data + final byte[] encodedCallEcRecover = + encodedCallEcRecover(ecRecover, ecRecoverSender, nonce, Bytes.fromHexString(input)); + + // Send the transaction + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(encodedCallEcRecover)).send(); + + // Store the transaction hash + txHashes[nonce] = resp.getTransactionHash(); + } + + // Transfer used as sentry to ensure a new block is mined + final Hash transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.ONE) // nonce is 1 as primary benefactor also deploys the contract + .execute(minerNode.nodeRequests()); + // Wait for the sentry to be mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + + // Assert that all the transactions involving the EcPairing precompile, but the last one, were + // included in the same block + assertTransactionsMinedInSameBlock( + minerNode.nodeRequests().eth(), Arrays.asList(txHashes).subList(0, nTransactions - 1)); + + // Assert that the last transaction was included in another block + assertTransactionsMinedInSeparateBlocks( + minerNode.nodeRequests().eth(), List.of(txHashes[0], txHashes[nTransactions - 1])); + + // Assert that the target string is contained in the blocks log + final String blockLog = getAndResetLog(); + assertThat(blockLog).contains(target); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExamplePragueTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExamplePragueTest.java new file mode 100644 index 00000000..c78cd8f2 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExamplePragueTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import java.math.BigInteger; + +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.DefaultGasProvider; + +/** + * Example test using Besu node configured for Prague. Note that block building must be triggered + * explicitly through `this.buildNewBlock()` + */ +public class ExamplePragueTest extends LineaPluginTestBasePrague { + private static final BigInteger GAS_PRICE = DefaultGasProvider.GAS_PRICE; + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + private static final String DATA = "0x"; + + private Web3j web3j; + private Credentials credentials; + private TransactionManager txManager; + private String recipient; + + @Override + @BeforeEach + public void setup() throws Exception { + super.setup(); + web3j = minerNode.nodeRequests().eth(); + credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + recipient = accounts.getSecondaryBenefactor().getAddress(); + } + + @Test + public void legacyTransactionsAreAccepted() throws Exception { + // Act - Send a legacy transaction + String txHash = + txManager + .sendTransaction(GAS_PRICE, GAS_LIMIT, recipient, DATA, VALUE) + .getTransactionHash(); + + this.buildNewBlock(); + + // Assert + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash)); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExcludedPrecompilesTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExcludedPrecompilesTest.java new file mode 100644 index 00000000..206ebe3d --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ExcludedPrecompilesTest.java @@ -0,0 +1,234 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import linea.plugin.acc.test.tests.web3j.generated.ExcludedPrecompiles; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.abi.datatypes.generated.Bytes8; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Hash; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; + +public class ExcludedPrecompilesTest extends LineaPluginTestBase { + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger GAS_PRICE = DefaultGasProvider.GAS_PRICE; + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + // disable line count validation to accept excluded precompile txs in the txpool + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "false") + .build(); + } + + @Test + public void transactionsWithExcludedPrecompilesAreNotAccepted() throws Exception { + final ExcludedPrecompiles excludedPrecompiles = deployExcludedPrecompiles(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = excludedPrecompiles.getContractAddress(); + + // fund a new account + final var recipient = accounts.createAccount("recipient"); + final var txHashFundRecipient = + accountTransactions + .createTransfer(accounts.getPrimaryBenefactor(), recipient, 10, BigInteger.valueOf(1)) + .execute(minerNode.nodeRequests()); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHashFundRecipient.toHexString())); + + record InvalidCall( + String senderPrivateKey, int nonce, String encodedContractCall, String expectedTraceLog) {} + + final InvalidCall[] invalidCalls = { + new InvalidCall( + Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY, + 2, + excludedPrecompiles + .callRIPEMD160("I am not allowed here".getBytes(StandardCharsets.UTF_8)) + .encodeFunctionCall(), + "Tx 0xe4648fd59d4289e59b112bf60931336440d306c85c2aac5a8b0c64ab35bc55b7 line count for module PRECOMPILE_RIPEMD_BLOCKS=1 is above the limit 0"), + new InvalidCall( + Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY, + 0, + encodedCallBlake2F(excludedPrecompiles), + "Tx 0x9f457b1b5244b03c54234f7f9e8225d4253135dd3c99a46dc527d115e7ea5dac line count for module PRECOMPILE_BLAKE_ROUNDS=12 is above the limit 0") + }; + + final var invalidTxHashes = + Arrays.stream(invalidCalls) + .map( + invalidCall -> { + // this tx must not be accepted but not mined + final RawTransaction txInvalid = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(invalidCall.nonce), + GAS_LIMIT.divide(BigInteger.TEN), + contractAddress, + BigInteger.ZERO, + invalidCall.encodedContractCall, + GAS_PRICE, + GAS_PRICE); + + final byte[] signedTxInvalid = + TransactionEncoder.signMessage( + txInvalid, Credentials.create(invalidCall.senderPrivateKey)); + + final EthSendTransaction signedTxInvalidResp; + try { + signedTxInvalidResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTxInvalid)).send(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + assertThat(signedTxInvalidResp.hasError()).isFalse(); + return signedTxInvalidResp.getTransactionHash(); + }) + .toList(); + + assertThat(getTxPoolContent()).hasSize(invalidTxHashes.size()); + + // transfer used as sentry to ensure a new block is mined without the invalid txs + final var transferTxHash1 = + accountTransactions + .createTransfer(recipient, accounts.getSecondaryBenefactor(), 1) + .execute(minerNode.nodeRequests()); + + // first sentry is mined and no tx of the bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash1.toHexString())); + Arrays.stream(invalidCalls) + .forEach( + invalidCall -> + minerNode.verify( + eth.expectNoTransactionReceipt(Hash.sha3(invalidCall.encodedContractCall)))); + + final String log = getLog(); + // verify trace log contains the exclusion cause + Arrays.stream(invalidCalls) + .forEach(invalidCall -> assertThat(log).contains(invalidCall.expectedTraceLog)); + } + + @Test + public void invalidModExpCallsAreNotMined() throws Exception { + final var modExp = deployModExp(); + + final var modExpSenders = new Account[3]; + final var foundTxHashes = new String[3]; + for (int i = 0; i < 3; i++) { + modExpSenders[i] = accounts.createAccount("sender" + i); + foundTxHashes[i] = + accountTransactions + .createTransfer( + accounts.getSecondaryBenefactor(), modExpSenders[i], 1, BigInteger.valueOf(i)) + .execute(minerNode.nodeRequests()) + .toHexString(); + } + Arrays.stream(foundTxHashes) + .forEach( + fundTxHash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash))); + + final Bytes[][] invalidInputs = { + {Bytes.fromHexString("0000000000000000000000000000000000000000000000000000000000000201")}, + { + Bytes.fromHexString("00000000000000000000000000000000000000000000000000000000000003"), + Bytes.fromHexString("ff") + }, + { + Bytes.fromHexString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + Bytes.fromHexString("00000000000000000000000000000000000000000000000000000000000003"), + Bytes.fromHexString("ff") + } + }; + + for (int i = 0; i < invalidInputs.length; i++) { + final var invalidCallTxHashes = new String[invalidInputs[i].length]; + for (int j = 0; j < invalidInputs[i].length; j++) { + + // use always the same nonce since we expect this tx not to be mined + final var mulmodOverflow = + encodedCallModExp(modExp, modExpSenders[j], 0, invalidInputs[i][j]); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(mulmodOverflow)).send(); + invalidCallTxHashes[j] = resp.getTransactionHash(); + } + + // transfer used as sentry to ensure a new block is mined without the invalid modexp call + final var transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.valueOf(i + 1)) + .execute(minerNode.nodeRequests()); + + // sentry is mined and the invalid modexp txs are not + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + final var blockLog = getAndResetLog(); + Arrays.stream(invalidCallTxHashes) + .forEach( + invalidCallTxHash -> { + minerNode.verify(eth.expectNoTransactionReceipt(invalidCallTxHash)); + assertThat(blockLog) + .contains( + "Tx " + + invalidCallTxHash + + " line count for module PRECOMPILE_MODEXP_EFFECTIVE_CALLS=2147483647 is above the limit 10000, removing from the txpool"); + }); + } + } + + private String encodedCallBlake2F(final ExcludedPrecompiles excludedPrecompiles) { + return excludedPrecompiles + .callBlake2f( + BigInteger.valueOf(12), + List.of( + Bytes32.fromHexString( + "0x48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5") + .toArrayUnsafe(), + Bytes32.fromHexString( + "0xd182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b") + .toArrayUnsafe()), + List.of( + Bytes32.fromHexString( + "0x6162630000000000000000000000000000000000000000000000000000000000") + .toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe()), + List.of(Bytes8.DEFAULT.getValue(), Bytes8.DEFAULT.getValue()), + true) + .encodeFunctionCall(); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java new file mode 100644 index 00000000..59b59a90 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java @@ -0,0 +1,593 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package linea.plugin.acc.test; + +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; +import static net.consensys.linea.metrics.LineaMetricCategory.SEQUENCER_PROFITABILITY; +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import linea.plugin.acc.test.tests.web3j.generated.AcceptanceTestToken; +import linea.plugin.acc.test.tests.web3j.generated.DummyAdder; +import linea.plugin.acc.test.tests.web3j.generated.EcAdd; +import linea.plugin.acc.test.tests.web3j.generated.EcMul; +import linea.plugin.acc.test.tests.web3j.generated.EcPairing; +import linea.plugin.acc.test.tests.web3j.generated.EcRecover; +import linea.plugin.acc.test.tests.web3j.generated.ExcludedPrecompiles; +import linea.plugin.acc.test.tests.web3j.generated.ModExp; +import linea.plugin.acc.test.tests.web3j.generated.MulmodExecutor; +import linea.plugin.acc.test.tests.web3j.generated.RevertExample; +import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import linea.plugin.acc.test.utils.MemoryAppender; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt32; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; +import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.condition.txpool.TxPoolConditions; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.BesuNodeConfigurationBuilder; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.NodeConfigurationFactory; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory.CliqueOptions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.txpool.TxPoolTransactions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.protocol.exceptions.TransactionException; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.tx.response.PollingTransactionReceiptProcessor; +import org.web3j.tx.response.TransactionReceiptProcessor; + +/** Base class for plugin tests. */ +@Slf4j +public abstract class LineaPluginTestBase extends AcceptanceTestBase { + public static final int MAX_CALLDATA_SIZE = 1188; // contract has a call data size of 1160 + public static final int MAX_TX_GAS_LIMIT = DefaultGasProvider.GAS_LIMIT.intValue(); + public static final long CHAIN_ID = 1337L; + public static final int BLOCK_PERIOD_SECONDS = 5; + public static final CliqueOptions DEFAULT_LINEA_CLIQUE_OPTIONS = + new CliqueOptions(BLOCK_PERIOD_SECONDS, CliqueOptions.DEFAULT.epochLength(), false); + protected static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + protected BesuNode minerNode; + + @BeforeEach + public void setup() throws Exception { + minerNode = + createCliqueNodeWithExtraCliOptionsAndRpcApis( + "miner1", getCliqueOptions(), getTestCliOptions(), Set.of("LINEA", "MINER"), false); + minerNode.setTransactionPoolConfiguration( + ImmutableTransactionPoolConfiguration.builder() + .from(TransactionPoolConfiguration.DEFAULT) + .noLocalPriority(true) + .build()); + cluster.start(minerNode); + } + + protected List getTestCliOptions() { + return new TestCommandLineOptionsBuilder().build(); + } + + protected CliqueOptions getCliqueOptions() { + return DEFAULT_LINEA_CLIQUE_OPTIONS; + } + + @AfterEach + public void stop() { + cluster.stop(); + cluster.close(); + MemoryAppender.reset(); + } + + protected Optional maybeCustomGenesisExtraData() { + return Optional.empty(); + } + + protected BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( + final String name, + final CliqueOptions cliqueOptions, + final List extraCliOptions, + final Set extraRpcApis, + final boolean isEngineRpcEnabled) + throws IOException { + final NodeConfigurationFactory node = new NodeConfigurationFactory(); + + final var nodeConfBuilder = + new BesuNodeConfigurationBuilder() + .name(name) + .miningEnabled() + .jsonRpcConfiguration(node.createJsonRpcWithCliqueEnabledConfig(extraRpcApis)) + .webSocketConfiguration(node.createWebSocketEnabledConfig()) + .inProcessRpcConfiguration(node.createInProcessRpcConfiguration(extraRpcApis)) + .devMode(false) + .jsonRpcTxPool() + .engineRpcEnabled(isEngineRpcEnabled) + .genesisConfigProvider( + validators -> Optional.of(provideGenesisConfig(validators, cliqueOptions))) + .extraCLIOptions(extraCliOptions) + .metricsConfiguration( + MetricsConfiguration.builder() + .enabled(true) + .port(0) + .metricCategories( + Set.of(PRICING_CONF, SEQUENCER_PROFITABILITY, TX_POOL_PROFITABILITY)) + .build()) + .requestedPlugins( + List.of( + "LineaExtraDataPlugin", + "LineaEstimateGasEndpointPlugin", + "LineaSetExtraDataEndpointPlugin", + "LineaTransactionPoolValidatorPlugin", + "LineaTransactionSelectorPlugin", + "LineaBundleEndpointsPlugin", + "ForwardBundlesPlugin")); + + return besu.create(nodeConfBuilder.build()); + } + + protected String provideGenesisConfig( + final Collection validators, final CliqueOptions cliqueOptions) { + final var genesis = + GenesisConfigurationFactory.createCliqueGenesisConfig(validators, cliqueOptions).get(); + + return maybeCustomGenesisExtraData() + .map(ed -> setGenesisCustomExtraData(genesis, ed)) + .orElse(genesis); + } + + protected String setGenesisCustomExtraData(final String genesis, final Bytes32 customExtraData) { + final var om = new ObjectMapper(); + final ObjectNode root; + try { + root = (ObjectNode) om.readTree(genesis); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + final var existingExtraData = Bytes.fromHexString(root.get("extraData").asText()); + final var updatedExtraData = Bytes.concatenate(customExtraData, existingExtraData.slice(32)); + root.put("extraData", updatedExtraData.toHexString()); + return root.toPrettyString(); + } + + protected void sendTransactionsWithGivenLengthPayload( + final SimpleStorage simpleStorage, + final List accounts, + final Web3j web3j, + final int num) { + final String contractAddress = simpleStorage.getContractAddress(); + final String txData = + simpleStorage.set(RandomStringUtils.secure().nextAlphabetic(num)).encodeFunctionCall(); + final List hashes = new ArrayList<>(); + accounts.forEach( + a -> { + final Credentials credentials = Credentials.create(a); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + for (int i = 0; i < 5; i++) { + try { + hashes.add( + txManager + .sendTransaction( + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_LIMIT, + contractAddress, + txData, + BigInteger.ZERO) + .getTransactionHash()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + + assertTransactionsInCorrectBlocks(web3j, hashes, num); + } + + private void assertTransactionsInCorrectBlocks(Web3j web3j, List hashes, int num) { + final HashMap txMap = new HashMap<>(); + TransactionReceiptProcessor receiptProcessor = createReceiptProcessor(web3j); + + // CallData for the transaction for empty String is 68 and grows in steps of 32 with (String + // size / 32) + final int maxTxs = MAX_CALLDATA_SIZE / (68 + ((num + 31) / 32) * 32); + + // Wait for transaction to be mined and check that there are no more than maxTxs per block + hashes.forEach( + h -> { + final TransactionReceipt transactionReceipt; + try { + transactionReceipt = receiptProcessor.waitForTransactionReceipt(h); + } catch (IOException | TransactionException e) { + throw new RuntimeException(e); + } + + final long blockNumber = transactionReceipt.getBlockNumber().longValue(); + txMap.compute(blockNumber, (b, n) -> n == null ? 1 : n + 1); + + // make sure that no block contained more than maxTxs + assertThat(txMap.get(blockNumber)).isLessThanOrEqualTo(maxTxs); + }); + // make sure that at least one block has maxTxs + assertThat(txMap).containsValue(maxTxs); + } + + protected SimpleStorage deploySimpleStorage() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + SimpleStorage.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected DummyAdder deployDummyAdder() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + DummyAdder.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected MulmodExecutor deployMulmodExecutor() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + MulmodExecutor.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected ModExp deployModExp() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = ModExp.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected EcPairing deployEcPairing() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + EcPairing.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected EcAdd deployEcAdd() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = EcAdd.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected EcMul deployEcMul() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = EcMul.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected EcRecover deployEcRecover() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + EcRecover.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected RevertExample deployRevertExample() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + RevertExample.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + protected AcceptanceTestToken deployAcceptanceTestToken() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + // 1000 AT tokens will be assigned to this account on deploy + final Credentials credentials = accounts.getPrimaryBenefactor().web3jCredentialsOrThrow(); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + AcceptanceTestToken.deploy(web3j, txManager, new DefaultGasProvider()); + final var contract = deploy.send(); + final var balance = contract.balanceOf(accounts.getPrimaryBenefactor().getAddress()).send(); + assertThat(balance).isEqualTo(1000); + return contract; + } + + protected ExcludedPrecompiles deployExcludedPrecompiles() throws Exception { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = + new RawTransactionManager(web3j, credentials, CHAIN_ID, createReceiptProcessor(web3j)); + + final RemoteCall deploy = + ExcludedPrecompiles.deploy(web3j, txManager, new DefaultGasProvider()); + return deploy.send(); + } + + public static String getResourcePath(String resource) { + return Objects.requireNonNull(LineaPluginTestBase.class.getResource(resource)).getPath(); + } + + protected void assertTransactionsMinedInSeparateBlocks(Web3j web3j, List hashes) + throws Exception { + TransactionReceiptProcessor receiptProcessor = createReceiptProcessor(web3j); + + final HashSet blockNumbers = new HashSet<>(); + for (String hash : hashes) { + TransactionReceipt receipt = receiptProcessor.waitForTransactionReceipt(hash); + assertThat(receipt).isNotNull(); + boolean isAdded = blockNumbers.add(receipt.getBlockNumber().longValue()); + assertThat(isAdded).isEqualTo(true); + } + } + + protected void assertTransactionsMinedInSameBlock(Web3j web3j, List hashes) { + TransactionReceiptProcessor receiptProcessor = createReceiptProcessor(web3j); + Set blockNumbers = + hashes.stream() + .map( + hash -> { + try { + TransactionReceipt receipt = receiptProcessor.waitForTransactionReceipt(hash); + assertThat(receipt).isNotNull(); + return receipt.getBlockNumber().longValue(); + } catch (IOException | TransactionException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); + + assertThat(blockNumbers.size()).isEqualTo(1); + } + + protected void assertTransactionNotInThePool(String hash) { + minerNode.verify( + new TxPoolConditions(new TxPoolTransactions()) + .notInTransactionPool(Hash.fromHexString(hash))); + } + + protected List> getTxPoolContent() { + return minerNode.execute(new TxPoolTransactions().getTxPoolContents()); + } + + private TransactionReceiptProcessor createReceiptProcessor(Web3j web3j) { + return new PollingTransactionReceiptProcessor( + web3j, + Math.max(1000, DEFAULT_LINEA_CLIQUE_OPTIONS.blockPeriodSeconds() * 1000 / 5), + DEFAULT_LINEA_CLIQUE_OPTIONS.blockPeriodSeconds() * 3); + } + + protected String sendTransactionWithGivenLengthPayload( + final String account, final Web3j web3j, final int num) throws IOException { + String to = Address.fromHexString("fe3b557e8fb62b89f4916b721be55ceb828dbd73").toString(); + TransactionManager txManager = new RawTransactionManager(web3j, Credentials.create(account)); + + return txManager + .sendTransaction( + DefaultGasProvider.GAS_PRICE, + BigInteger.valueOf(MAX_TX_GAS_LIMIT), + to, + RandomStringUtils.secure().nextAlphabetic(num), + BigInteger.ZERO) + .getTransactionHash(); + } + + protected Bytes32 createExtraDataPricingField( + final long fixedCostKWei, final long variableCostKWei, final long minGasPriceKWei) { + final UInt32 fixed = UInt32.valueOf(BigInteger.valueOf(fixedCostKWei)); + final UInt32 variable = UInt32.valueOf(BigInteger.valueOf(variableCostKWei)); + final UInt32 min = UInt32.valueOf(BigInteger.valueOf(minGasPriceKWei)); + + return Bytes32.rightPad( + Bytes.concatenate(Bytes.of((byte) 1), fixed.toBytes(), variable.toBytes(), min.toBytes())); + } + + protected double getMetricValue( + final MetricCategory category, + final String metricName, + final List> labelValues) + throws IOException, InterruptedException { + + final var metricsReq = + HttpRequest.newBuilder().GET().uri(URI.create(minerNode.metricsHttpUrl().get())).build(); + + final var respLines = HTTP_CLIENT.send(metricsReq, HttpResponse.BodyHandlers.ofLines()); + + final var searchString = + category.getApplicationPrefix().orElse("") + + category.getName() + + "_" + + metricName + + labelValues.stream() + .map(lv -> lv.getKey() + "=\"" + lv.getValue() + "\"") + .collect(Collectors.joining(",", "{", "}")); + + final var foundMetric = + respLines.body().filter(line -> line.startsWith(searchString)).findFirst(); + + return foundMetric + .map(line -> line.substring(searchString.length()).trim()) + .map(Double::valueOf) + .orElse(Double.NaN); + } + + protected String getLog() { + return MemoryAppender.getLog(); + } + + protected String getAndResetLog() { + final var log = MemoryAppender.getLog(); + MemoryAppender.reset(); + return log; + } + + protected byte[] encodedCallModExp( + final ModExp modExp, final Account sender, final int nonce, final Bytes input) { + final var modExpCalldata = modExp.callModExp(input.toArrayUnsafe()).encodeFunctionCall(); + + final var modExpCall = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + modExp.getContractAddress(), + BigInteger.ZERO, + modExpCalldata, + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(modExpCall, sender.web3jCredentialsOrThrow()); + } + + protected byte[] encodedCallEcPairing( + final EcPairing ecPairing, final Account sender, final int nonce, final Bytes input) { + final var ecPairingCalldata = + ecPairing.callEcPairing(input.toArrayUnsafe()).encodeFunctionCall(); + + final var ecPairingCall = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + ecPairing.getContractAddress(), + BigInteger.ZERO, + ecPairingCalldata, + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(ecPairingCall, sender.web3jCredentialsOrThrow()); + } + + protected byte[] encodedCallEcAdd( + final EcAdd ecAdd, final Account sender, final int nonce, final Bytes input) { + final var ecAddCalldata = ecAdd.callEcAdd(input.toArrayUnsafe()).encodeFunctionCall(); + + final var ecAddCall = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + ecAdd.getContractAddress(), + BigInteger.ZERO, + ecAddCalldata, + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(ecAddCall, sender.web3jCredentialsOrThrow()); + } + + protected byte[] encodedCallEcMul( + final EcMul ecMul, final Account sender, final int nonce, final Bytes input) { + final var ecMulCalldata = ecMul.callEcMul(input.toArrayUnsafe()).encodeFunctionCall(); + + final var ecMulCall = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + ecMul.getContractAddress(), + BigInteger.ZERO, + ecMulCalldata, + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(ecMulCall, sender.web3jCredentialsOrThrow()); + } + + protected byte[] encodedCallEcRecover( + final EcRecover ecRecover, final Account sender, final int nonce, final Bytes input) { + final var ecRecoverCalldata = + ecRecover.callEcRecover(input.toArrayUnsafe()).encodeFunctionCall(); + + final var ecRecoverCall = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + DefaultGasProvider.GAS_LIMIT, + ecRecover.getContractAddress(), + BigInteger.ZERO, + ecRecoverCalldata, + DefaultGasProvider.GAS_PRICE, + DefaultGasProvider.GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(ecRecoverCall, sender.web3jCredentialsOrThrow()); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBasePrague.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBasePrague.java new file mode 100644 index 00000000..f917924a --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBasePrague.java @@ -0,0 +1,104 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package linea.plugin.acc.test; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.consensus.clique.CliqueExtraData; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; +import org.hyperledger.besu.tests.acceptance.dsl.EngineAPIService; +import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory.CliqueOptions; +import org.junit.jupiter.api.BeforeEach; + +// This file initializes a Besu node configured for the Prague fork and makes it available to +// acceptance tests. +@Slf4j +public abstract class LineaPluginTestBasePrague extends LineaPluginTestBase { + private EngineAPIService engineApiService; + private ObjectMapper mapper; + private final String GENESIS_FILE_TEMPLATE_PATH = "/clique/clique-prague.json.tpl"; + + @BeforeEach + @Override + public void setup() throws Exception { + minerNode = + createCliqueNodeWithExtraCliOptionsAndRpcApis( + "miner1", getCliqueOptions(), getTestCliOptions(), Set.of("LINEA", "MINER"), true); + minerNode.setTransactionPoolConfiguration( + ImmutableTransactionPoolConfiguration.builder() + .from(TransactionPoolConfiguration.DEFAULT) + .noLocalPriority(true) + .build()); + cluster.start(minerNode); + mapper = new ObjectMapper(); + this.engineApiService = new EngineAPIService(minerNode, ethTransactions, mapper); + } + + // Ideally GenesisConfigurationFactory.createCliqueGenesisConfig would support a custom genesis + // file + // path. We have resorted to inlining its logic here to allow a flexible genesis file path. + @Override + protected String provideGenesisConfig( + final Collection validators, final CliqueOptions cliqueOptions) { + // Target state + final String genesisTemplate = + GenesisConfigurationFactory.readGenesisFile(GENESIS_FILE_TEMPLATE_PATH); + final String hydratedGenesisTemplate = + genesisTemplate + .replace("%blockperiodseconds%", String.valueOf(cliqueOptions.blockPeriodSeconds())) + .replace("%epochlength%", String.valueOf(cliqueOptions.epochLength())) + .replace("%createemptyblocks%", String.valueOf(cliqueOptions.createEmptyBlocks())); + + final List
addresses = + validators.stream().map(RunnableNode::getAddress).collect(Collectors.toList()); + final String extraDataString = CliqueExtraData.createGenesisExtraDataString(addresses); + final String genesis = hydratedGenesisTemplate.replaceAll("%extraData%", extraDataString); + + return maybeCustomGenesisExtraData() + .map(ed -> setGenesisCustomExtraData(genesis, ed)) + .orElse(genesis); + } + + // No-arg override for simple test cases, we take sensible defaults from the genesis config + protected void buildNewBlock() throws IOException, InterruptedException { + var latestTimestamp = this.minerNode.execute(ethTransactions.block()).getTimestamp(); + var genesisConfigSerialized = this.minerNode.getGenesisConfig().get(); + JsonNode genesisConfig = mapper.readTree(genesisConfigSerialized); + long defaultSlotTimeSeconds = + genesisConfig.path("config").path("clique").path("blockperiodseconds").asLong(); + this.engineApiService.buildNewBlock( + latestTimestamp.longValue() + defaultSlotTimeSeconds, defaultSlotTimeSeconds * 1000); + } + + // @param blockTimestampSeconds The Unix timestamp (in seconds) to assign to the new block. + // @param blockBuildingTimeMs The duration (in milliseconds) allocated for the Besu node to + // build the block. + protected void buildNewBlock(long blockTimestampSeconds, long blockBuildingTimeMs) + throws IOException, InterruptedException { + this.engineApiService.buildNewBlock(blockTimestampSeconds, blockBuildingTimeMs); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ProfitableTransactionTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ProfitableTransactionTest.java new file mode 100644 index 00000000..ca2cf265 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/ProfitableTransactionTest.java @@ -0,0 +1,101 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import java.math.BigInteger; +import java.util.List; + +import org.bouncycastle.crypto.digests.KeccakDigest; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; + +public class ProfitableTransactionTest extends LineaPluginTestBase { + private static final double MIN_MARGIN = 1.5; + private static final Wei MIN_GAS_PRICE = Wei.of(1_000_000_000); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-min-margin=", String.valueOf(MIN_MARGIN)) + .build(); + } + + @BeforeEach + public void setMinGasPrice() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + } + + @Test + public void transactionIsNotMinedWhenUnprofitable() throws Exception { + + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + final TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final KeccakDigest keccakDigest = new KeccakDigest(256); + final StringBuilder txData = new StringBuilder(); + txData.append("0x"); + for (int i = 0; i < 10; i++) { + keccakDigest.update(new byte[] {(byte) i}, 0, 1); + final byte[] out = new byte[32]; + keccakDigest.doFinal(out, 0); + txData.append(new BigInteger(out)); + } + + final var txUnprofitable = + txManager.sendTransaction( + MIN_GAS_PRICE.getAsBigInteger().divide(BigInteger.valueOf(100)), + BigInteger.valueOf(MAX_TX_GAS_LIMIT / 2), + credentials.getAddress(), + txData.toString(), + BigInteger.ZERO); + + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + + // assert that tx below margin is not confirmed + minerNode.verify(eth.expectNoTransactionReceipt(txUnprofitable.getTransactionHash())); + } + + /** + * if we have a list of transactions [t_small, t_tooBig, t_small, ..., t_small] where t_tooBig is + * too big to fit in a block, we have blocks created that contain all t_small transactions. + * + * @throws Exception if send transaction fails + */ + @Test + public void transactionIsMinedWhenProfitable() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TestCommandLineOptionsBuilder.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TestCommandLineOptionsBuilder.java new file mode 100644 index 00000000..138ca23e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TestCommandLineOptionsBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static linea.plugin.acc.test.LineaPluginTestBase.getResourcePath; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.web3j.tx.gas.DefaultGasProvider; + +/** This class is used to build a list of command line options for testing. */ +public class TestCommandLineOptionsBuilder { + private final Properties cliOptions = new Properties(); + + private static final String MAX_VALUE = String.valueOf(Integer.MAX_VALUE); + + public TestCommandLineOptionsBuilder() { + cliOptions.setProperty("--plugin-linea-max-tx-calldata-size=", MAX_VALUE); + cliOptions.setProperty("--plugin-linea-max-block-calldata-size=", MAX_VALUE); + cliOptions.setProperty( + "--plugin-linea-max-tx-gas-limit=", DefaultGasProvider.GAS_LIMIT.toString()); + cliOptions.setProperty("--plugin-linea-deny-list-path=", getResourcePath("/emptyDenyList.txt")); + cliOptions.setProperty( + "--plugin-linea-module-limit-file-path=", getResourcePath("/noModuleLimits.toml")); + cliOptions.setProperty("--plugin-linea-max-block-gas=", MAX_VALUE); + cliOptions.setProperty( + "--plugin-linea-l1l2-bridge-contract=", "0x00000000000000000000000000000000DEADBEEF"); + cliOptions.setProperty("--plugin-linea-l1l2-bridge-topic=", "0x123456"); + } + + public TestCommandLineOptionsBuilder set(String option, String value) { + cliOptions.setProperty(option, value); + return this; + } + + public List build() { + List optionsList = new ArrayList<>(cliOptions.size()); + for (String key : cliOptions.stringPropertyNames()) { + optionsList.add(key + cliOptions.getProperty(key)); + } + return optionsList; + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionCallDataSizeLimitTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionCallDataSizeLimitTest.java new file mode 100644 index 00000000..ba14f97e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionCallDataSizeLimitTest.java @@ -0,0 +1,139 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.DefaultGasProvider; + +public class TransactionCallDataSizeLimitTest extends LineaPluginTestBase { + + public static final int MAX_CALLDATA_SIZE = 1188; // contract has a call data size of 1160 + private static final BigInteger GAS_PRICE = DefaultGasProvider.GAS_PRICE; + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-max-tx-calldata-size=", String.valueOf(MAX_CALLDATA_SIZE)) + .set("--plugin-linea-max-block-calldata-size=", String.valueOf(MAX_CALLDATA_SIZE)) + .build(); + } + + @Test + public void shouldMineTransactions() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + + List accounts = + List.of(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY, Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final List numCharactersInStringList = List.of(150, 200, 400); + + numCharactersInStringList.forEach( + num -> sendTransactionsWithGivenLengthPayload(simpleStorage, accounts, web3j, num)); + } + + @Test + public void transactionIsMinedWhenNotTooBig() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final String txDataGood = simpleStorage.set("a".repeat(1200 - 80)).encodeFunctionCall(); + final String hashGood = + txManager + .sendTransaction(GAS_PRICE, GAS_LIMIT, contractAddress, txDataGood, VALUE) + .getTransactionHash(); + + // make sure that a transaction that is not too big was mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(hashGood)); + } + + @Test + public void transactionIsNotMinedWhenTooBig() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final String txDataTooBig = simpleStorage.set("a".repeat(1200 - 79)).encodeFunctionCall(); + final var txTooBigResp = + txManager.sendTransaction(GAS_PRICE, GAS_LIMIT, contractAddress, txDataTooBig, VALUE); + + assertThat(txTooBigResp.hasError()).isTrue(); + assertThat(txTooBigResp.getError().getMessage()) + .isEqualTo("Calldata of transaction is greater than the allowed max of 1188"); + } + + /** + * if we have a list of transactions [t_small, t_tooBig, t_small, ..., t_small] where t_tooBig is + * too big to fit in a block, we have blocks created that contain all t_small transactions. + * + * @throws Exception if send transaction fails + */ + @Test + public void multipleSmallTxsMinedWhileTxTooBigNot() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final Account smallCalldataSender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final List expectedConfirmedTxs = new ArrayList<>(4); + + expectedConfirmedTxs.add( + minerNode.execute(accountTransactions.createTransfer(smallCalldataSender, recipient, 1))); + + final String txDataTooBig = simpleStorage.set("a".repeat(1200 - 79)).encodeFunctionCall(); + + final var txTooBigResp = + txManager.sendTransaction( + GAS_PRICE, BigInteger.valueOf(MAX_TX_GAS_LIMIT), contractAddress, txDataTooBig, VALUE); + + expectedConfirmedTxs.addAll( + minerNode.execute( + accountTransactions.createIncrementalTransfers(smallCalldataSender, recipient, 3))); + + assertThat(txTooBigResp.hasError()).isTrue(); + assertThat(txTooBigResp.getError().getMessage()) + .isEqualTo("Calldata of transaction is greater than the allowed max of 1188"); + + expectedConfirmedTxs.stream() + .map(Hash::toHexString) + .forEach(hash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(hash))); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionGasLimitTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionGasLimitTest.java new file mode 100644 index 00000000..41b46ca2 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionGasLimitTest.java @@ -0,0 +1,129 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.DefaultGasProvider; + +public class TransactionGasLimitTest extends LineaPluginTestBase { + + public static final int MAX_TX_GAS_LIMIT = DefaultGasProvider.GAS_LIMIT.intValue(); + private static final BigInteger GAS_PRICE = DefaultGasProvider.GAS_PRICE; + private static final BigInteger VALUE = BigInteger.ZERO; + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-max-tx-gas-limit=", String.valueOf(MAX_TX_GAS_LIMIT)) + .build(); + } + + @Test + public void transactionIsMinedWhenGasLimitIsNotExceeded() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final String txData = simpleStorage.set("hello").encodeFunctionCall(); + + final String hashGood = + txManager + .sendTransaction( + GAS_PRICE, BigInteger.valueOf(MAX_TX_GAS_LIMIT), contractAddress, txData, VALUE) + .getTransactionHash(); + + // make sure that a transaction that is not too big was mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(hashGood)); + } + + @Test + public void transactionIsNotMinedWhenGasLimitIsExceeded() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final String txData = simpleStorage.set("hello").encodeFunctionCall(); + + final var txTooBigResp = + txManager.sendTransaction( + GAS_PRICE, BigInteger.valueOf(MAX_TX_GAS_LIMIT + 1), contractAddress, txData, VALUE); + + assertThat(txTooBigResp.hasError()).isTrue(); + assertThat(txTooBigResp.getError().getMessage()) + .isEqualTo("Gas limit of transaction is greater than the allowed max of 9000000"); + } + + /** + * if we have a list of transactions [t_small, t_tooBig, t_small, ..., t_small] where t_tooBig is + * too big to fit in a block, we have blocks created that contain all t_small transactions. + * + * @throws Exception if send transaction fails + */ + @Test + public void multipleSmallTxsMinedWhileTxTooBigNot() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = simpleStorage.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final Account lowGasSender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final List expectedConfirmedTxs = new ArrayList<>(4); + + expectedConfirmedTxs.add( + minerNode.execute(accountTransactions.createTransfer(lowGasSender, recipient, 1))); + + final String txData = simpleStorage.set("too BIG").encodeFunctionCall(); + + final var txTooBigResp = + txManager.sendTransaction( + GAS_PRICE, BigInteger.valueOf(MAX_TX_GAS_LIMIT + 1), contractAddress, txData, VALUE); + + expectedConfirmedTxs.addAll( + minerNode.execute( + accountTransactions.createIncrementalTransfers(lowGasSender, recipient, 3))); + + assertThat(txTooBigResp.hasError()).isTrue(); + assertThat(txTooBigResp.getError().getMessage()) + .isEqualTo("Gas limit of transaction is greater than the allowed max of 9000000"); + + expectedConfirmedTxs.stream() + .map(Hash::toHexString) + .forEach(hash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(hash))); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionPoolDenialTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionPoolDenialTest.java new file mode 100644 index 00000000..3e539db6 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionPoolDenialTest.java @@ -0,0 +1,96 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.math.BigInteger; +import java.util.List; +import java.util.stream.IntStream; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.RawTransactionManager; +import org.web3j.utils.Convert; + +public class TransactionPoolDenialTest extends LineaPluginTestBase { + + private static final BigInteger GAS_PRICE = Convert.toWei("20", Convert.Unit.GWEI).toBigInteger(); + private static final BigInteger GAS_LIMIT = BigInteger.valueOf(210000); + private static final BigInteger VALUE = BigInteger.ONE; // 1 wei + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-deny-list-path=", getResourcePath("/denyList.txt")) + .build(); + } + + @Test + public void senderOnDenyListCannotAddTransactionToPool() throws Exception { + final Credentials notDenied = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + final Credentials denied = Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY); + final Web3j miner = minerNode.nodeRequests().eth(); + + RawTransactionManager transactionManager = new RawTransactionManager(miner, denied, CHAIN_ID); + EthSendTransaction transactionResponse = + transactionManager.sendTransaction(GAS_PRICE, GAS_LIMIT, notDenied.getAddress(), "", VALUE); + + assertThat(transactionResponse.getTransactionHash()).isNull(); + assertThat(transactionResponse.getError().getMessage()) + .isEqualTo( + "sender 0x627306090abab3a6e1400e9345bc60c78a8bef57 is blocked as appearing on the SDN or other legally prohibited list"); + } + + @Test + public void transactionWithRecipientOnDenyListCannotBeAddedToPool() throws Exception { + final Credentials notDenied = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + final Credentials denied = Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY); + final Web3j miner = minerNode.nodeRequests().eth(); + + RawTransactionManager transactionManager = + new RawTransactionManager(miner, notDenied, CHAIN_ID); + EthSendTransaction transactionResponse = + transactionManager.sendTransaction(GAS_PRICE, GAS_LIMIT, denied.getAddress(), "", VALUE); + + assertThat(transactionResponse.getTransactionHash()).isNull(); + assertThat(transactionResponse.getError().getMessage()) + .isEqualTo( + "recipient 0x627306090abab3a6e1400e9345bc60c78a8bef57 is blocked as appearing on the SDN or other legally prohibited list"); + } + + @Test + public void transactionThatTargetPrecompileIsNotAccepted() { + IntStream.rangeClosed(1, 9) + .mapToObj( + index -> + accountTransactions.createTransfer( + accounts.getPrimaryBenefactor(), + accounts.createAccount(Address.precompiled(index)), + 1, + BigInteger.valueOf(1))) + .forEach( + txWithPrecompileRecipient -> + assertThatThrownBy( + () -> txWithPrecompileRecipient.execute(minerNode.nodeRequests())) + .hasMessage( + "Error sending transaction: destination address is a precompile address and cannot receive transactions")); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java new file mode 100644 index 00000000..6965173e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitOverflowTest.java @@ -0,0 +1,93 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import linea.plugin.acc.test.tests.web3j.generated.DummyAdder; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; + +public class TransactionTraceLimitOverflowTest extends LineaPluginTestBase { + + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + private static final BigInteger GAS_PRICE = BigInteger.TEN.pow(11); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set( + "--plugin-linea-module-limit-file-path=", + getResourcePath("/txOverflowModuleLimits.toml")) + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "false") + .build(); + } + + @Test + public void transactionOverModuleLineCountRemoved() throws Exception { + final DummyAdder dummyAdder = deployDummyAdder(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = dummyAdder.getContractAddress(); + final String txData = dummyAdder.add(BigInteger.valueOf(100)).encodeFunctionCall(); + + // this tx will not be selected since it goes above the line count limit + // but selection should go on and select the next one + final RawTransaction txModuleLineCountTooBig = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(1), + GAS_LIMIT.divide(BigInteger.TEN), + contractAddress, + VALUE, + txData, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + final byte[] signedTxContractInteraction = + TransactionEncoder.signMessage( + txModuleLineCountTooBig, Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY)); + final EthSendTransaction signedTxContractInteractionResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTxContractInteraction)).send(); + + // these are under the line count limit and should be selected + final Account fewLinesSender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + final List expectedConfirmedTxs = new ArrayList<>(4); + + expectedConfirmedTxs.addAll( + minerNode.execute( + accountTransactions.createIncrementalTransfers(fewLinesSender, recipient, 4))); + + expectedConfirmedTxs.stream() + .map(Hash::toHexString) + .forEach(hash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(hash))); + + // assert that tx over line count limit is not confirmed and is removed from the pool + minerNode.verify( + eth.expectNoTransactionReceipt(signedTxContractInteractionResp.getTransactionHash())); + assertTransactionNotInThePool(signedTxContractInteractionResp.getTransactionHash()); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitTest.java new file mode 100644 index 00000000..30b32b9c --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/TransactionTraceLimitTest.java @@ -0,0 +1,75 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import linea.plugin.acc.test.tests.web3j.generated.DummyAdder; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; + +public class TransactionTraceLimitTest extends LineaPluginTestBase { + + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + private static final BigInteger GAS_PRICE = BigInteger.TEN.pow(9); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-module-limit-file-path=", getResourcePath("/strictModuleLimits.toml")) + .build(); + } + + @Test + public void transactionsMinedInSeparateBlocksTest() throws Exception { + final DummyAdder dummyAdder = deployDummyAdder(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = dummyAdder.getContractAddress(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + final String txData = dummyAdder.add(BigInteger.valueOf(100)).encodeFunctionCall(); + + final ArrayList hashes = new ArrayList<>(5); + for (int i = 0; i < 5; i++) { + final RawTransaction transaction = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(i + 1), + GAS_LIMIT, + contractAddress, + VALUE, + txData, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN)); + final byte[] signedTransaction = TransactionEncoder.signMessage(transaction, credentials); + final EthSendTransaction response = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTransaction)).send(); + hashes.add(response.getTransactionHash()); + } + + // make sure that there are no more than one transaction per block, because the limit for the + // add module only allows for one of these transactions. + assertTransactionsMinedInSeparateBlocks(web3j, hashes); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java new file mode 100644 index 00000000..fce533ba --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java @@ -0,0 +1,167 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.extradata; + +import static java.util.Map.entry; +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.List; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.utils.Numeric; + +public class ExtraDataPricingTest extends LineaPluginTestBase { + protected static final Wei MIN_GAS_PRICE = Wei.of(1_000_000_000); + protected static final int WEI_IN_KWEI = 1000; + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder().build(); + } + + protected TestCommandLineOptionsBuilder getTestCommandLineOptionsBuilder() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-extra-data-pricing-enabled=", Boolean.TRUE.toString()); + } + + @Test + public void updateMinGasPriceViaExtraData() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + final var doubleMinGasPrice = MIN_GAS_PRICE.multiply(2); + + final var extraData = + createExtraDataPricingField( + 0, MIN_GAS_PRICE.toLong() / WEI_IN_KWEI, doubleMinGasPrice.toLong() / WEI_IN_KWEI); + final var reqSetExtraData = new MinerSetExtraDataRequest(extraData); + final var respSetExtraData = reqSetExtraData.execute(minerNode.nodeRequests()); + + assertThat(respSetExtraData).isTrue(); + + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + + assertThat(minerNode.getMiningParameters().getMinTransactionGasPrice()) + .isEqualTo(doubleMinGasPrice); + } + + @Test + public void updateProfitabilityParamsViaExtraData() throws IOException, InterruptedException { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + + final var extraData = + createExtraDataPricingField( + MIN_GAS_PRICE.multiply(2).toLong() / WEI_IN_KWEI, + MIN_GAS_PRICE.toLong() / WEI_IN_KWEI, + MIN_GAS_PRICE.toLong() / WEI_IN_KWEI); + final var reqSetExtraData = new ExtraDataPricingTest.MinerSetExtraDataRequest(extraData); + final var respSetExtraData = reqSetExtraData.execute(minerNode.nodeRequests()); + + assertThat(respSetExtraData).isTrue(); + + // when this first tx is mined the above extra data pricing will have effect on following txs + final TransferTransaction profitableTx = + accountTransactions.createTransfer(sender, recipient, 1); + final var profitableTxHash = minerNode.execute(profitableTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(profitableTxHash.toHexString())); + + // this tx will be evaluated with the previously set extra data pricing to be unprofitable + final RawTransaction unprofitableTx = + RawTransaction.createTransaction( + BigInteger.ZERO, + MIN_GAS_PRICE.getAsBigInteger(), + BigInteger.valueOf(21000), + recipient.getAddress(), + ""); + + final byte[] signedUnprofitableTx = + TransactionEncoder.signMessage( + unprofitableTx, Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY)); + + final EthSendTransaction signedUnprofitableTxResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedUnprofitableTx)).send(); + + assertThat(signedUnprofitableTxResp.hasError()).isTrue(); + assertThat(signedUnprofitableTxResp.getError().getMessage()).isEqualTo("Gas price too low"); + + assertThat(getTxPoolContent()).isEmpty(); + + final var fixedCostMetric = + getMetricValue(PRICING_CONF, "values", List.of(entry("field", "fixed_cost_wei"))); + + assertThat(fixedCostMetric).isEqualTo(MIN_GAS_PRICE.multiply(2).getValue().doubleValue()); + + final var variableCostMetric = + getMetricValue(PRICING_CONF, "values", List.of(entry("field", "variable_cost_wei"))); + + assertThat(variableCostMetric).isEqualTo(MIN_GAS_PRICE.getValue().doubleValue()); + + final var ethGasPriceMetric = + getMetricValue(PRICING_CONF, "values", List.of(entry("field", "eth_gas_price_wei"))); + + assertThat(ethGasPriceMetric).isEqualTo(MIN_GAS_PRICE.getValue().doubleValue()); + } + + static class MinerSetExtraDataRequest implements Transaction { + private final Bytes32 extraData; + + public MinerSetExtraDataRequest(final Bytes32 extraData) { + this.extraData = extraData; + } + + @Override + public Boolean execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "miner_setExtraData", + List.of(extraData.toHexString()), + nodeRequests.getWeb3jService(), + MinerSetExtraDataResponse.class) + .send() + .getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class MinerSetExtraDataResponse extends org.web3j.protocol.core.Response {} + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTestWithoutSetMinGasPrice.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTestWithoutSetMinGasPrice.java new file mode 100644 index 00000000..7856c40a --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTestWithoutSetMinGasPrice.java @@ -0,0 +1,68 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.extradata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class ExtraDataPricingTestWithoutSetMinGasPrice extends ExtraDataPricingTest { + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder().build(); + } + + protected TestCommandLineOptionsBuilder getTestCommandLineOptionsBuilder() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-extra-data-pricing-enabled=", Boolean.TRUE.toString()) + .set("--plugin-linea-extra-data-set-min-gas-price-enabled=", Boolean.FALSE.toString()); + } + + @Disabled("disable since minGasPrice is not updated with this test") + @Test + public void updateMinGasPriceViaExtraData() {} + + @Test + public void minGasPriceNotUpdatedViaExtraData() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + final var doubleMinGasPrice = MIN_GAS_PRICE.multiply(2); + + final var extraData = + createExtraDataPricingField( + 0, MIN_GAS_PRICE.toLong() / WEI_IN_KWEI, doubleMinGasPrice.toLong() / WEI_IN_KWEI); + final var reqSetExtraData = new MinerSetExtraDataRequest(extraData); + final var respSetExtraData = reqSetExtraData.execute(minerNode.nodeRequests()); + + assertThat(respSetExtraData).isTrue(); + + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + + assertThat(minerNode.getMiningParameters().getMinTransactionGasPrice()) + .isEqualTo(MIN_GAS_PRICE); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/StartupExtraDataPricingTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/StartupExtraDataPricingTest.java new file mode 100644 index 00000000..f9719eaa --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/StartupExtraDataPricingTest.java @@ -0,0 +1,68 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.extradata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.Test; + +public class StartupExtraDataPricingTest extends LineaPluginTestBase { + private static final Wei VARIABLE_GAS_COST = Wei.of(1_200_300_000); + private static final Wei MIN_GAS_PRICE = VARIABLE_GAS_COST.divide(2); + private static final int WEI_IN_KWEI = 1000; + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder().build(); + } + + protected TestCommandLineOptionsBuilder getTestCommandLineOptionsBuilder() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-extra-data-pricing-enabled=", Boolean.TRUE.toString()); + } + + @Override + protected Optional maybeCustomGenesisExtraData() { + final var genesisExtraData = + createExtraDataPricingField( + 0, VARIABLE_GAS_COST.toLong() / WEI_IN_KWEI, MIN_GAS_PRICE.toLong() / WEI_IN_KWEI); + + return Optional.of(genesisExtraData); + } + + @Test + public void minGasPriceSetFromChainHeadExtraDataAtStartup() { + // at startup the min gas price should be set from the current chain head block extra data + assertThat(minerNode.getMiningParameters().getMinTransactionGasPrice()) + .isEqualTo(MIN_GAS_PRICE); + + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/AbstractSendBundleTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/AbstractSendBundleTest.java new file mode 100644 index 00000000..47fe21ba --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/AbstractSendBundleTest.java @@ -0,0 +1,122 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.web3j.crypto.Hash.sha3; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.tests.web3j.generated.AcceptanceTestToken; +import linea.plugin.acc.test.tests.web3j.generated.MulmodExecutor; +import lombok.RequiredArgsConstructor; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.core.Request; +import org.web3j.utils.Numeric; + +public class AbstractSendBundleTest extends LineaPluginTestBase { + protected static final BigInteger TRANSFER_GAS_LIMIT = BigInteger.valueOf(100_000L); + protected static final BigInteger MULMOD_GAS_LIMIT = BigInteger.valueOf(10_000_000L); + protected static final BigInteger GAS_PRICE = BigInteger.TEN.pow(9); + + protected TokenTransfer transferTokens( + final AcceptanceTestToken token, + final Account sender, + final int nonce, + final Account recipient, + final int amount) { + final var transferCalldata = + token.transfer(recipient.getAddress(), BigInteger.valueOf(amount)).encodeFunctionCall(); + + final var transferTx = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + TRANSFER_GAS_LIMIT, + token.getContractAddress(), + BigInteger.ZERO, + transferCalldata, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + final String signedTransferTx = + Numeric.toHexString( + TransactionEncoder.signMessage(transferTx, sender.web3jCredentialsOrThrow())); + + final String hashTx = sha3(signedTransferTx); + + return new TokenTransfer(recipient, hashTx, signedTransferTx); + } + + protected MulmodCall mulmodOperation( + final MulmodExecutor executor, final Account sender, final int nonce, final int iterations) { + final var operationCalldata = + executor.executeMulmod(BigInteger.valueOf(iterations)).encodeFunctionCall(); + + final var operationTx = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + MULMOD_GAS_LIMIT, + executor.getContractAddress(), + BigInteger.ZERO, + operationCalldata, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + final String signedTransferTx = + Numeric.toHexString( + TransactionEncoder.signMessage(operationTx, sender.web3jCredentialsOrThrow())); + + final String hashTx = sha3(signedTransferTx); + + return new MulmodCall(hashTx, signedTransferTx); + } + + @RequiredArgsConstructor + static class SendBundleRequest implements Transaction { + private final BundleParams bundleParams; + + @Override + public SendBundleResponse execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "linea_sendBundle", + Arrays.asList(bundleParams), + nodeRequests.getWeb3jService(), + SendBundleResponse.class) + .send(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class SendBundleResponse extends org.web3j.protocol.core.Response {} + + record Response(String bundleHash) {} + } + + record BundleParams(String[] txs, String blockNumber) {} + + record TokenTransfer(Account recipient, String txHash, String rawTx) {} + + record MulmodCall(String txHash, String rawTx) {} +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasCompatibilityModeTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasCompatibilityModeTest.java new file mode 100644 index 00000000..9fcf28fe --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasCompatibilityModeTest.java @@ -0,0 +1,85 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.junit.jupiter.api.Test; + +public class EstimateGasCompatibilityModeTest extends EstimateGasTest { + private static final BigDecimal PRICE_MULTIPLIER = BigDecimal.valueOf(1.2); + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder() + .set("--plugin-linea-estimate-gas-compatibility-mode-enabled=", "true") + .set( + "--plugin-linea-estimate-gas-compatibility-mode-multiplier=", + PRICE_MULTIPLIER.toPlainString()) + .build(); + } + + @Override + protected void assertIsProfitable( + final Transaction tx, + final Wei baseFee, + final Wei estimatedMaxGasPrice, + final long estimatedGasLimit) { + final var minGasPrice = minerNode.getMiningParameters().getMinTransactionGasPrice(); + final var minPriorityFee = minGasPrice.subtract(baseFee); + final var compatibilityMinPriorityFee = + Wei.of( + PRICE_MULTIPLIER + .multiply(new BigDecimal(minPriorityFee.getAsBigInteger())) + .setScale(0, RoundingMode.CEILING) + .toBigInteger()); + + // since we are in compatibility mode, we want to check that returned profitable priority fee is + // the min priority fee per gas * multiplier + base fee + final var expectedMaxGasPrice = baseFee.add(compatibilityMinPriorityFee); + assertThat(estimatedMaxGasPrice).isEqualTo(expectedMaxGasPrice); + } + + @Override + protected void assertMinGasPriceLowerBound(final Wei baseFee, final Wei estimatedMaxGasPrice) { + // since we are in compatibility mode, we want to check that returned profitable priority fee is + // the min priority fee per gas * multiplier + base fee + assertIsProfitable(null, baseFee, estimatedMaxGasPrice, 0); + } + + @Test + public void lineaEstimateGasPriorityFeeMinGasPriceLowerBound() { + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams(null, sender.getAddress(), null, null, "", "", "0", null, null, null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()).getResult(); + + final var baseFee = Wei.fromHexString(respLinea.baseFeePerGas()); + final var estimatedPriorityFee = Wei.fromHexString(respLinea.priorityFeePerGas()); + final var estimatedMaxGasPrice = baseFee.add(estimatedPriorityFee); + + assertMinGasPriceLowerBound(baseFee, estimatedMaxGasPrice); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasModuleLimitOverflowTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasModuleLimitOverflowTest.java new file mode 100644 index 00000000..8100bd15 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasModuleLimitOverflowTest.java @@ -0,0 +1,66 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.List; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.DummyAdder; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.junit.jupiter.api.Test; +import org.web3j.tx.gas.DefaultGasProvider; + +public class EstimateGasModuleLimitOverflowTest extends LineaPluginTestBase { + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set( + "--plugin-linea-module-limit-file-path=", + getResourcePath("/txOverflowModuleLimits.toml")) + .build(); + } + + @Test + public void estimateGasFailsForExceedingModuleLineCountTest() throws Exception { + + final Account sender = accounts.getPrimaryBenefactor(); + + final DummyAdder dummyAdder = deployDummyAdder(); + final String txData = dummyAdder.add(BigInteger.valueOf(1)).encodeFunctionCall(); + + final EstimateGasTest.CallParams callParams = + new EstimateGasTest.CallParams( + null, + sender.getAddress(), + null, + dummyAdder.getContractAddress(), + null, + txData, + "0", + DefaultGasProvider.GAS_PRICE.toString(), + null, + null); + + final var reqLinea = new EstimateGasTest.BadLineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getCode()).isEqualTo(-32000); + assertThat(respLinea.getMessage()) + .isEqualTo("Transaction line count for module WCP=349 is above the limit 306"); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasTest.java new file mode 100644 index 00000000..9966dc86 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EstimateGasTest.java @@ -0,0 +1,560 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityCliOptions; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.rpc.methods.LineaEstimateGas; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt64; +import org.bouncycastle.crypto.digests.KeccakDigest; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.http.HttpService; + +public class EstimateGasTest extends LineaPluginTestBase { + protected static final int FIXED_GAS_COST_WEI = 0; + protected static final int VARIABLE_GAS_COST_WEI = 1_000_000_000; + protected static final double MIN_MARGIN = 1.0; + protected static final double ESTIMATE_GAS_MIN_MARGIN = 1.1; + protected static final Wei MIN_GAS_PRICE = Wei.of(1_000_000_000); + protected static final int MAX_TRANSACTION_GAS_LIMIT = 30_000_000; + protected LineaProfitabilityConfiguration profitabilityConf; + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder().build(); + } + + protected TestCommandLineOptionsBuilder getTestCommandLineOptionsBuilder() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-fixed-gas-cost-wei=", String.valueOf(FIXED_GAS_COST_WEI)) + .set("--plugin-linea-variable-gas-cost-wei=", String.valueOf(VARIABLE_GAS_COST_WEI)) + .set("--plugin-linea-min-margin=", String.valueOf(MIN_MARGIN)) + .set("--plugin-linea-estimate-gas-min-margin=", String.valueOf(ESTIMATE_GAS_MIN_MARGIN)) + .set("--plugin-linea-max-tx-gas-limit=", String.valueOf(MAX_TRANSACTION_GAS_LIMIT)); + } + + @BeforeEach + public void setMinGasPrice() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + } + + @BeforeEach + public void createDefaultConfigurations() { + profitabilityConf = + LineaProfitabilityCliOptions.create().toDomainObject().toBuilder() + .fixedCostWei(FIXED_GAS_COST_WEI) + .variableCostWei(VARIABLE_GAS_COST_WEI) + .minMargin(MIN_MARGIN) + .estimateGasMinMargin(ESTIMATE_GAS_MIN_MARGIN) + .build(); + } + + @Test + public void lineaEstimateGasMatchesEthEstimateGas() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + null, + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + null, + null, + null); + + final var reqEth = new RawEstimateGasRequest(callParams); + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respEth = reqEth.execute(minerNode.nodeRequests()); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respEth).isEqualTo(respLinea.getResult().gasLimit()); + } + + @Test + public void passingGasPriceFieldWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + null, + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + "0x1234", + null, + null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isFalse(); + assertThat(respLinea.getResult()).isNotNull(); + } + + @Test + public void passingChainIdFieldWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + "0x539", + sender.getAddress(), + null, + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + "0x1234", + null, + null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isFalse(); + assertThat(respLinea.getResult()).isNotNull(); + } + + @Test + public void passingEIP1559FieldsWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + null, + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + null, + "0x1234", + "0x1"); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isFalse(); + assertThat(respLinea.getResult()).isNotNull(); + } + + @Test + public void passingChainIdAndEIP1559FieldsWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + "0x539", + sender.getAddress(), + null, + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + null, + "0x1234", + null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isFalse(); + assertThat(respLinea.getResult()).isNotNull(); + } + + @Test + public void passingStateOverridesWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final var actualBalance = minerNode.execute(ethTransactions.getBalance(sender)); + + assertThat(actualBalance).isGreaterThan(BigInteger.ONE); + + final CallParams callParams = + new CallParams( + "0x539", + sender.getAddress(), + null, + sender.getAddress(), + "1", + Bytes.EMPTY.toHexString(), + "0", + "0x1234", + null, + null); + + final var zeroBalance = Map.of("balance", Wei.ZERO.toHexString()); + + final var stateOverrides = Map.of(accounts.getSecondaryBenefactor().getAddress(), zeroBalance); + + final var reqLinea = new LineaEstimateGasRequest(callParams, stateOverrides); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isTrue(); + assertThat(respLinea.getError().getCode()).isEqualTo(-32004); + assertThat(respLinea.getError().getMessage()) + .isEqualTo( + "Upfront cost exceeds account balance (transaction up-front cost 0x208cbab601 exceeds transaction sender account balance 0x0)"); + } + + @Test + public void passingNonceWorks() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + "0", + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + null, + "0x1234", + null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.hasError()).isFalse(); + assertThat(respLinea.getResult()).isNotNull(); + + // try with a future nonce + final CallParams callParamsFuture = + new CallParams( + null, + sender.getAddress(), + "10", + sender.getAddress(), + null, + Bytes.EMPTY.toHexString(), + "0", + null, + "0x1234", + null); + + final var reqLineaFuture = new LineaEstimateGasRequest(callParamsFuture); + final var respLineaFuture = reqLineaFuture.execute(minerNode.nodeRequests()); + assertThat(respLineaFuture.hasError()).isFalse(); + assertThat(respLineaFuture.getResult()).isNotNull(); + } + + @Test + public void lineaEstimateGasIsProfitable() { + + final Account sender = accounts.getSecondaryBenefactor(); + + final KeccakDigest keccakDigest = new KeccakDigest(256); + final StringBuilder txData = new StringBuilder(); + txData.append("0x"); + for (int i = 0; i < 5; i++) { + keccakDigest.update(new byte[] {(byte) i}, 0, 1); + final byte[] out = new byte[32]; + keccakDigest.doFinal(out, 0); + txData.append(new BigInteger(out).abs()); + } + final var payload = Bytes.wrap(txData.toString().getBytes(StandardCharsets.UTF_8)); + + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + null, + sender.getAddress(), + null, + payload.toHexString(), + "0", + null, + null, + null); + + final var reqLinea = new LineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()).getResult(); + + final var estimatedGasLimit = UInt64.fromHexString(respLinea.gasLimit()).toLong(); + final var baseFee = Wei.fromHexString(respLinea.baseFeePerGas()); + final var estimatedPriorityFee = Wei.fromHexString(respLinea.priorityFeePerGas()); + final var estimatedMaxGasPrice = baseFee.add(estimatedPriorityFee); + + final var tx = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(Address.fromHexString(sender.getAddress())) + .to(Address.fromHexString(sender.getAddress())) + .gasLimit(estimatedGasLimit) + .gasPrice(estimatedMaxGasPrice) + .chainId(BigInteger.valueOf(CHAIN_ID)) + .value(Wei.ZERO) + .payload(payload) + .signature(LineaEstimateGas.FAKE_SIGNATURE_FOR_SIZE_CALCULATION) + .build(); + + assertIsProfitable(tx, baseFee, estimatedMaxGasPrice, estimatedGasLimit); + } + + protected void assertIsProfitable( + final org.hyperledger.besu.ethereum.core.Transaction tx, + final Wei baseFee, + final Wei estimatedMaxGasPrice, + final long estimatedGasLimit) { + + final var minGasPrice = minerNode.getMiningParameters().getMinTransactionGasPrice(); + + final var profitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + + assertThat( + profitabilityCalculator.isProfitable( + "Test", + tx, + profitabilityConf.minMargin(), + baseFee, + estimatedMaxGasPrice, + estimatedGasLimit, + minGasPrice)) + .isTrue(); + } + + @Test + public void invalidParametersLineaEstimateGasRequestReturnErrorResponse() { + final Account sender = accounts.getSecondaryBenefactor(); + final CallParams callParams = + new CallParams( + null, + sender.getAddress(), + null, + null, + "", + "", + String.valueOf(Integer.MAX_VALUE), + null, + null, + null); + final var reqLinea = new BadLineaEstimateGasRequest(callParams); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getCode()).isEqualTo(RpcErrorType.INVALID_PARAMS.getCode()); + assertThat(respLinea.getMessage()).isEqualTo(RpcErrorType.INVALID_PARAMS.getMessage()); + } + + @Test + public void revertedTransactionReturnErrorResponse() throws Exception { + final SimpleStorage simpleStorage = deploySimpleStorage(); + final Account sender = accounts.getSecondaryBenefactor(); + final var reqLinea = + new BadLineaEstimateGasRequest( + new CallParams( + null, + sender.getAddress(), + null, + simpleStorage.getContractAddress(), + "", + "", + "0", + null, + null, + null)); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getCode()).isEqualTo(-32000); + assertThat(respLinea.getMessage()).isEqualTo("Execution reverted"); + assertThat(respLinea.getData()).isEqualTo("\"0x\""); + } + + @Test + public void failedTransactionReturnErrorResponse() { + final Account sender = accounts.getSecondaryBenefactor(); + final var reqLinea = + new BadLineaEstimateGasRequest( + new CallParams( + null, + sender.getAddress(), + null, + null, + "", + Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY, + "0", + null, + null, + null)); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getCode()).isEqualTo(-32000); + assertThat(respLinea.getMessage()) + .isEqualTo( + "Transaction processing could not be completed due to an exception (Invalid opcode: 0xc8)"); + } + + @Test + public void parseErrorLineaEstimateGasRequestReturnErrorResponse() + throws IOException, InterruptedException { + final var httpService = (HttpService) minerNode.nodeRequests().getWeb3jService(); + final var httpClient = HttpClient.newHttpClient(); + final var badJsonRequest = + HttpRequest.newBuilder(URI.create(httpService.getUrl())) + .headers("Content-Type", "application/json") + .POST( + HttpRequest.BodyPublishers.ofString( + """ + {"jsonrpc":"2.0","method":"linea_estimateGas","params":[malformed json],"id":53} + """)) + .build(); + final var errorResponse = httpClient.send(badJsonRequest, HttpResponse.BodyHandlers.ofString()); + assertThat(errorResponse.body()) + .isEqualTo( + """ + {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}"""); + } + + protected void assertMinGasPriceLowerBound(final Wei baseFee, final Wei estimatedMaxGasPrice) { + final var minGasPrice = minerNode.getMiningParameters().getMinTransactionGasPrice(); + assertThat(estimatedMaxGasPrice).isEqualTo(minGasPrice); + } + + static class LineaEstimateGasRequest + implements Transaction { + private final CallParams callParams; + private final Map> stateOverrides; + + public LineaEstimateGasRequest(final CallParams callParams) { + this(callParams, null); + } + + public LineaEstimateGasRequest( + final CallParams callParams, final Map> stateOverrides) { + this.callParams = callParams; + this.stateOverrides = stateOverrides; + } + + @Override + public LineaEstimateGasResponse execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "linea_estimateGas", + Arrays.asList(callParams, stateOverrides), + nodeRequests.getWeb3jService(), + LineaEstimateGasResponse.class) + .send(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class LineaEstimateGasResponse extends org.web3j.protocol.core.Response {} + + record Response(String gasLimit, String baseFeePerGas, String priorityFeePerGas) {} + } + + static class BadLineaEstimateGasRequest + implements Transaction { + private final CallParams badCallParams; + + public BadLineaEstimateGasRequest(final CallParams badCallParams) { + this.badCallParams = badCallParams; + } + + @Override + public org.web3j.protocol.core.Response.Error execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "linea_estimateGas", + List.of(badCallParams), + nodeRequests.getWeb3jService(), + BadLineaEstimateGasResponse.class) + .send() + .getError(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class BadLineaEstimateGasResponse extends org.web3j.protocol.core.Response {} + + record Response(String gasLimit, String baseFeePerGas, String priorityFeePerGas) {} + } + + static class RawEstimateGasRequest implements Transaction { + private final CallParams callParams; + + public RawEstimateGasRequest(final CallParams callParams) { + this.callParams = callParams; + } + + @Override + public String execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "eth_estimateGas", + List.of(callParams), + nodeRequests.getWeb3jService(), + RawEstimateGasResponse.class) + .send() + .getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class RawEstimateGasResponse extends org.web3j.protocol.core.Response {} + } + + @JsonInclude(NON_NULL) + record CallParams( + String chainId, + String from, + String nonce, + String to, + String value, + String data, + String gas, + String gasPrice, + String maxFeePerGas, + String maxPriorityFeePerGas) {} + + record StateOverride(String account, String balance) {} +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java new file mode 100644 index 00000000..b4b2c621 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationCheckTest.java @@ -0,0 +1,234 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.ExcludedPrecompiles; +import linea.plugin.acc.test.tests.web3j.generated.MulmodExecutor; +import linea.plugin.acc.test.tests.web3j.generated.RevertExample; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransactionSet; +import org.junit.jupiter.api.Test; +import org.web3j.abi.datatypes.generated.Bytes8; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.tx.gas.DefaultGasProvider; +import org.web3j.utils.Numeric; + +public class EthSendRawTransactionSimulationCheckTest extends LineaPluginTestBase { + private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT; + private static final BigInteger VALUE = BigInteger.ZERO; + private static final BigInteger GAS_PRICE = BigInteger.TEN.pow(9); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set( + "--plugin-linea-module-limit-file-path=", + getResourcePath("/moduleLimits_sendRawTx.toml")) + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "true") + .build(); + } + + @Test + public void transactionOverModuleLineCountNotAccepted() throws Exception { + final var mulmodExecutor = deployMulmodExecutor(); + + final var mulmodOverflow = + encodedCallMulmodOperation(mulmodExecutor, accounts.getPrimaryBenefactor(), 1, 5_000); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final var resp = web3j.ethSendRawTransaction(Numeric.toHexString(mulmodOverflow)).send(); + assertThat(resp.hasError()).isTrue(); + assertThat(resp.getError().getMessage()) + .isEqualTo( + "Transaction 0x6928439fd82ddf40709238e2df0f54ab2e51b252404fbf0efeebb515e6a405e0 line count for module EXT=33939 is above the limit 20"); + + assertThat(getTxPoolContent()).isEmpty(); + } + + @Test + public void validTransactionsAreAccepted() { + // these are under the line count limit and should be accepted and selected + final Account recipient = accounts.createAccount("recipient"); + final List expectedConfirmedTxs = new ArrayList<>(4); + + final var transfers = + IntStream.range(0, 4) + .mapToObj( + i -> + accountTransactions.createTransfer( + accounts.getSecondaryBenefactor(), recipient, i + 1, BigInteger.valueOf(i))) + .toList() + .reversed(); + // reversed, so we are sure no tx is selected before all are sent due to the nonce gap, + // otherwise a block can be built with some txs before we can check the txpool content + + expectedConfirmedTxs.addAll(minerNode.execute(new TransferTransactionSet(transfers))); + + final var txPoolContentByHash = getTxPoolContent().stream().map(e -> e.get("hash")).toList(); + assertThat(txPoolContentByHash) + .containsExactlyInAnyOrderElementsOf( + expectedConfirmedTxs.stream().map(Hash::toHexString).toList()); + + expectedConfirmedTxs.stream() + .map(Hash::toHexString) + .forEach(hash -> minerNode.verify(eth.expectSuccessfulTransactionReceipt(hash))); + } + + @Test + public void transactionsThatRevertAreAccepted() throws Exception { + final RevertExample revertExample = deployRevertExample(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = revertExample.getContractAddress(); + final String txData = revertExample.setValue(BigInteger.ZERO).encodeFunctionCall(); + + // this tx reverts but nevertheless it is accepted in the pool + final RawTransaction txThatReverts = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.ZERO, + GAS_LIMIT.divide(BigInteger.TEN), + contractAddress, + VALUE, + txData, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + final byte[] signedTxContractInteraction = + TransactionEncoder.signMessage( + txThatReverts, Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY)); + + final EthSendTransaction signedTxContractInteractionResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTxContractInteraction)).send(); + + assertThat(signedTxContractInteractionResp.hasError()).isFalse(); + + final var expectedConfirmedTxHash = signedTxContractInteractionResp.getTransactionHash(); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(expectedConfirmedTxHash)); + } + + @Test + public void transactionsWithExcludedPrecompilesAreNotAccepted() throws Exception { + final ExcludedPrecompiles excludedPrecompiles = deployExcludedPrecompiles(); + final Web3j web3j = minerNode.nodeRequests().eth(); + final String contractAddress = excludedPrecompiles.getContractAddress(); + + record InvalidCall(String encodedContractCall, String expectedErrorMessage) {} + + final InvalidCall[] invalidCalls = { + new InvalidCall( + excludedPrecompiles + .callRIPEMD160("I am not allowed here".getBytes(StandardCharsets.UTF_8)) + .encodeFunctionCall(), + "Transaction 0x35451c83b480b45df19105a30f22704df8750b7e328e1ebc646e6442f2f426f9 line count for module PRECOMPILE_RIPEMD_BLOCKS=1 is above the limit 0"), + new InvalidCall( + encodedCallBlake2F(excludedPrecompiles), + "Transaction 0xfd447b2b688f7448c875f68d9c85ffcb976e1cc722b70dae53e4f2e30d871be8 line count for module PRECOMPILE_BLAKE_ROUNDS=12 is above the limit 0") + }; + + Arrays.stream(invalidCalls) + .forEach( + invalidCall -> { + // this tx must not be accepted + final RawTransaction txInvalid = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.ZERO, + GAS_LIMIT.divide(BigInteger.TEN), + contractAddress, + VALUE, + invalidCall.encodedContractCall, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + final byte[] signedTxInvalid = + TransactionEncoder.signMessage( + txInvalid, Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY)); + + final EthSendTransaction signedTxContractInteractionResp; + try { + signedTxContractInteractionResp = + web3j.ethSendRawTransaction(Numeric.toHexString(signedTxInvalid)).send(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + assertThat(signedTxContractInteractionResp.hasError()).isTrue(); + assertThat(signedTxContractInteractionResp.getError().getMessage()) + .isEqualTo(invalidCall.expectedErrorMessage); + }); + assertThat(getTxPoolContent()).isEmpty(); + } + + protected byte[] encodedCallMulmodOperation( + final MulmodExecutor executor, final Account sender, final int nonce, final int iterations) { + final var operationCalldata = + executor.executeMulmod(BigInteger.valueOf(iterations)).encodeFunctionCall(); + + final var operationTx = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.valueOf(nonce), + GAS_LIMIT, + executor.getContractAddress(), + BigInteger.ZERO, + operationCalldata, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + + return TransactionEncoder.signMessage(operationTx, sender.web3jCredentialsOrThrow()); + } + + private String encodedCallBlake2F(final ExcludedPrecompiles excludedPrecompiles) { + return excludedPrecompiles + .callBlake2f( + BigInteger.valueOf(12), + List.of( + Bytes32.fromHexString( + "0x48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5") + .toArrayUnsafe(), + Bytes32.fromHexString( + "0xd182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b") + .toArrayUnsafe()), + List.of( + Bytes32.fromHexString( + "0x6162630000000000000000000000000000000000000000000000000000000000") + .toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe(), + Bytes32.ZERO.toArrayUnsafe()), + List.of(Bytes8.DEFAULT.getValue(), Bytes8.DEFAULT.getValue()), + true) + .encodeFunctionCall(); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationModExpTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationModExpTest.java new file mode 100644 index 00000000..c5b7e0bc --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/EthSendRawTransactionSimulationModExpTest.java @@ -0,0 +1,96 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.crypto.Hash.sha3; + +import java.util.List; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.utils.Numeric; + +public class EthSendRawTransactionSimulationModExpTest extends LineaPluginTestBase { + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-module-limit-file-path=", getResourcePath("/noModuleLimits.toml")) + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "true") + .build(); + } + + @Test + public void validModExpCallsAreAccepted() throws Exception { + final var modExp = deployModExp(); + + final Bytes[] validInputs = { + Bytes.EMPTY, + Bytes.fromHexString("0000000000000000000000000000000000000000000000000000000000000000"), + Bytes.fromHexString("000000000000000000000000000000000000000000000000000000000000013f"), + Bytes.fromHexString("0000000000000000000000000000000000000000000000000000000000000200"), + Bytes.fromHexString("00000000000000000000000000000000000000000000000000000000000002") + }; + + for (int i = 0; i < validInputs.length; i++) { + + final var mulmodOverflow = + encodedCallModExp(modExp, accounts.getSecondaryBenefactor(), i, validInputs[i]); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(mulmodOverflow)).send(); + assertThat(resp.hasError()).isFalse(); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(resp.getTransactionHash())); + } + } + + @Test + public void invalidModExpCallsAreRejected() throws Exception { + final var modExp = deployModExp(); + + final Bytes[] invalidInputs = { + Bytes.fromHexString("0000000000000000000000000000000000000000000000000000000000000201"), + Bytes.fromHexString("00000000000000000000000000000000000000000000000000000000000003"), + Bytes.fromHexString("ff"), + Bytes.fromHexString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + }; + + for (int i = 0; i < invalidInputs.length; i++) { + + final var mulmodOverflow = + encodedCallModExp(modExp, accounts.getSecondaryBenefactor(), i, invalidInputs[i]); + + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(mulmodOverflow)).send(); + + assertThat(resp.hasError()).isTrue(); + assertThat(resp.getError().getMessage()) + .isEqualTo( + "Transaction " + + Numeric.toHexString(sha3(mulmodOverflow)) + + " line count for module PRECOMPILE_MODEXP_EFFECTIVE_CALLS=2147483647 is above the limit 10000"); + + assertThat(getTxPoolContent()).isEmpty(); + } + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ForwardBundleTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ForwardBundleTest.java new file mode 100644 index 00000000..880c39de --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ForwardBundleTest.java @@ -0,0 +1,238 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.getAllServeEvents; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.request; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.consensys.linea.bundles.BundleForwarder.RETRY_COUNT_HEADER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.http.LoggedResponse; +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.matching.MatchResult; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@WireMockTest +public class ForwardBundleTest extends AbstractSendBundleTest { + protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static WireMockRuntimeInfo wireMockRuntimeInfo; + + @BeforeAll + public static void beforeAll(final WireMockRuntimeInfo wireMockRuntimeInfo) { + ForwardBundleTest.wireMockRuntimeInfo = wireMockRuntimeInfo; + } + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-bundles-forward-urls=", wireMockRuntimeInfo.getHttpBaseUrl()) + .set( + "--plugin-linea-bundles-forward-timeout=", + String.valueOf(Duration.ofSeconds(1).toMillis())) + .build(); + } + + @Test + public void bundleIsForwarded() { + final var bundleParams = sendBundle(1); + stubSuccessResponseFor(bundleParams, 0); + verifyRequestForwarded(bundleParams); + } + + @Test + public void forwardIsRetriedAfterTimeout() { + final var bundleParams = sendBundle(2); + stubSuccessResponseFor(bundleParams, 0, Duration.ofSeconds(2)); + + verifyResponseSent(bundleParams); + + verifyRequestForwarded(bundleParams, 1); + } + + @Test + public void forwardIsRetriedAfterNetworkFailure() { + final var bundleParams = sendBundle(3); + stubFailureFor(bundleParams); + stubSuccessResponseFor(bundleParams, 1); + + verifyRequestForwarded(bundleParams); + verifyRequestForwarded(bundleParams, 1); + } + + private void stubSuccessResponseFor(final BundleParams bundleParams, final int retryCount) { + stubSuccessResponseFor(bundleParams, retryCount, Duration.ZERO); + } + + private void stubSuccessResponseFor( + final BundleParams bundleParams, final int retryCount, final Duration delay) { + final var requestMatcher = post(urlEqualTo("/")); + + if (retryCount > 0) { + requestMatcher.withHeader(RETRY_COUNT_HEADER, equalTo(String.valueOf(retryCount))); + } else { + requestMatcher.andMatching(this::noRetryCountHeader); + } + + stubFor( + requestMatcher + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(matchingBlockNumber(bundleParams.blockNumber())) + .willReturn( + aResponse() + .withFixedDelay((int) delay.toMillis()) + .withTransformers("response-template") + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + """ + { + "jsonrpc": "2.0", + "result": { + "bundleHash": "" + }, + "id": {{jsonPath request.body '$.id'}} + }""" + .replace("", "0xb" + bundleParams.blockNumber())))); + } + + private void stubFailureFor(final BundleParams bundleParams) { + stubFor( + post(urlEqualTo("/")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(matchingBlockNumber(bundleParams.blockNumber())) + .andMatching(this::noRetryCountHeader) + .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); + } + + private MatchResult noRetryCountHeader(final Request request) { + return new MatchResult() { + @Override + public boolean isExactMatch() { + return !request.getAllHeaderKeys().contains(RETRY_COUNT_HEADER); + } + + @Override + public double getDistance() { + return 0; + } + }; + } + + private static StringValuePattern matchingBlockNumber(final String blockNumber) { + return matchingJsonPath("$.params[?(@.blockNumber == %s)]".formatted(blockNumber)); + } + + private static void verifyRequestForwarded(final BundleParams bundleParams) { + verifyRequestForwarded(bundleParams, 0); + } + + private static void verifyRequestForwarded( + final BundleParams bundleParams, final int retryCount) { + + final var patternBuilder = + postRequestedFor(urlEqualTo("/")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(matchingBundleParams(bundleParams)); + + if (retryCount > 0) { + patternBuilder.withHeader(RETRY_COUNT_HEADER, equalTo(String.valueOf(retryCount))); + } else { + patternBuilder.withoutHeader(RETRY_COUNT_HEADER); + } + + await().atMost(2, SECONDS).untilAsserted(() -> verify(exactly(1), patternBuilder)); + } + + private static void verifyResponseSent(final BundleParams bundleParams) { + await() + .atMost(2, SECONDS) + .until( + () -> + getAllServeEvents().stream() + .map(ServeEvent::getResponse) + .map(LoggedResponse::getBodyAsString) + .map( + body -> { + try { + return OBJECT_MAPPER.readTree(body); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .anyMatch( + jsonNode -> + jsonNode + .findPath("bundleHash") + .textValue() + .equals("0xb" + bundleParams.blockNumber()))); + } + + private static StringValuePattern matchingBundleParams(final BundleParams bundleParams) { + return matchingJsonPath( + "$.params[?(@.blockNumber == %s)]".formatted(bundleParams.blockNumber())) + .and( + matchingJsonPath( + "$.params[?(@.txs == [%s])]" + .formatted( + Arrays.stream(bundleParams.txs()).collect(Collectors.joining(","))))); + } + + private BundleParams sendBundle(final int blockNumber) { + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.getPrimaryBenefactor(); + + final TransferTransaction tx = accountTransactions.createTransfer(sender, recipient, 1); + + final String bundleRawTx = tx.signedTransactionData(); + + final var bundleParams = + new BundleParams(new String[] {bundleRawTx}, Integer.toHexString(blockNumber)); + + final var sendBundleRequest = new SendBundleRequest(bundleParams); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + return bundleParams; + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ModExpLimitsTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ModExpLimitsTest.java new file mode 100644 index 00000000..808a849e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/ModExpLimitsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.ModExp; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.junit.jupiter.api.Test; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.utils.Numeric; + +public class ModExpLimitsTest extends LineaPluginTestBase { + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + // disable line count validation to accept excluded precompile txs in the txpool + .set("--plugin-linea-tx-pool-simulation-check-api-enabled=", "false") + // set the module limits file + .set("--plugin-linea-module-limit-file-path=", getResourcePath("/moduleLimits.toml")) + .build(); + } + + /** + * Tests the ModExp PRECOMPILE_MODEXP_EFFECTIVE_CALLS limit, that is the number of times the + * corresponding circuit may be invoked in a single block. + */ + @Test + public void modExpLimitTest() throws Exception { + Map moduleLimits = + ModuleLineCountValidator.createLimitModules( + new LineaTracerConfiguration(getResourcePath("/moduleLimits.toml"))); + final int PRECOMPILE_MODEXP_EFFECTIVE_CALLS = + moduleLimits.get("PRECOMPILE_MODEXP_EFFECTIVE_CALLS"); + + /* + * nTransactions: the number of transactions to try to include in the same block. The last + * one is not supposed to fit as it exceeds the limit, thus it is included in the next block + * input: input data for each transaction + * target: the expected string to be found in the blocks log + */ + final int nTransactions = PRECOMPILE_MODEXP_EFFECTIVE_CALLS + 1; + final String input = + "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001aabbcc"; + final String target = + "Cumulated line count for module PRECOMPILE_MODEXP_EFFECTIVE_CALLS=" + + (PRECOMPILE_MODEXP_EFFECTIVE_CALLS + 1) + + " is above the limit " + + PRECOMPILE_MODEXP_EFFECTIVE_CALLS + + ", stopping selection"; + + // Deploy the ModExp contract + final ModExp modExp = deployModExp(); + + // Create an account to send the transactions + Account modExpSender = accounts.createAccount("modExpSender"); + + // Fund the account using secondary benefactor + String fundTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), modExpSender, 1, BigInteger.ZERO) + .execute(minerNode.nodeRequests()) + .toHexString(); + // Verify that the transaction for transferring funds was successful + minerNode.verify(eth.expectSuccessfulTransactionReceipt(fundTxHash)); + + String[] txHashes = new String[nTransactions]; + for (int i = 0; i < nTransactions; i++) { + // With decreasing nonce we force the transactions to be included in the same block + // i = 0 , 1 , ..., nTransactions - 1 + // nonce = nTransactions - 1, nTransactions - 2, ..., 0 + int nonce = nTransactions - 1 - i; + + // Craft the transaction data + final byte[] encodedCallEcRecover = + encodedCallModExp(modExp, modExpSender, nonce, Bytes.fromHexString(input)); + + // Send the transaction + final Web3j web3j = minerNode.nodeRequests().eth(); + final EthSendTransaction resp = + web3j.ethSendRawTransaction(Numeric.toHexString(encodedCallEcRecover)).send(); + + // Store the transaction hash + txHashes[nonce] = resp.getTransactionHash(); + } + + // Transfer used as sentry to ensure a new block is mined + final Hash transferTxHash = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.ONE) // nonce is 1 as primary benefactor also deploys the contract + .execute(minerNode.nodeRequests()); + // Wait for the sentry to be mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + + // Assert that all the transactions involving the EcPairing precompile, but the last one, were + // included in the same block + assertTransactionsMinedInSameBlock( + minerNode.nodeRequests().eth(), Arrays.asList(txHashes).subList(0, nTransactions - 1)); + + // Assert that the last transaction was included in another block + assertTransactionsMinedInSeparateBlocks( + minerNode.nodeRequests().eth(), List.of(txHashes[0], txHashes[nTransactions - 1])); + + // Assert that the target string is contained in the blocks log + final String blockLog = getAndResetLog(); + assertThat(blockLog).contains(target); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleMaxBlockGasTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleMaxBlockGasTest.java new file mode 100644 index 00000000..93efc3aa --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleMaxBlockGasTest.java @@ -0,0 +1,97 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import linea.plugin.acc.test.tests.web3j.generated.AcceptanceTestToken; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.Test; + +public class SendBundleMaxBlockGasTest extends AbstractSendBundleTest { + private static final BigInteger BUNDLE_BLOCK_GAS_LIMIT = BigInteger.valueOf(100_000L); + + @Override + public List getTestCliOptions() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-max-bundle-block-gas=", BUNDLE_BLOCK_GAS_LIMIT.toString()) + .build(); + } + + @Test + public void maxBlockGasForBundlesIsRespected() throws Exception { + final AcceptanceTestToken token = deployAcceptanceTestToken(); + + final int numOfTransfers = 2; + + // each token transfer has a gas limit of 100k so the bundle does not fit in the max block gas + // reserved for bundles + final TokenTransfer[] tokenTransfers = new TokenTransfer[numOfTransfers]; + for (int i = 0; i < numOfTransfers; i++) { + tokenTransfers[i] = + transferTokens( + token, + accounts.getPrimaryBenefactor(), + i + 1, + accounts.createAccount("recipient " + i), + 1); + } + + final var tokenTransferBundleRawTxs = + Arrays.stream(tokenTransfers).map(TokenTransfer::rawTx).toArray(String[]::new); + + final var tokenTransferSendBundleRequest = + new SendBundleRequest(new BundleParams(tokenTransferBundleRawTxs, Integer.toHexString(2))); + final var tokenTransferSendBundleResponse = + tokenTransferSendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(tokenTransferSendBundleResponse.hasError()).isFalse(); + assertThat(tokenTransferSendBundleResponse.getResult().bundleHash()).isNotBlank(); + + // while 2 simple transfers each with a gas limit of 21k fit + final TransferTransaction tx1 = + accountTransactions.createTransfer( + accounts.getSecondaryBenefactor(), accounts.getPrimaryBenefactor(), 1); + final TransferTransaction tx2 = + accountTransactions.createTransfer( + accounts.getSecondaryBenefactor(), accounts.getPrimaryBenefactor(), 1); + + final String[] bundleRawTxs = + new String[] {tx1.signedTransactionData(), tx2.signedTransactionData()}; + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(2))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + // verify simple transfers are mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx1.transactionHash())); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx2.transactionHash())); + + // but token transfers are not + Arrays.stream(tokenTransfers) + .forEach( + tokenTransfer -> { + minerNode.verify(eth.expectNoTransactionReceipt(tokenTransfer.txHash())); + }); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleTest.java new file mode 100644 index 00000000..a5958a9d --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SendBundleTest.java @@ -0,0 +1,379 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.crypto.Hash.sha3; + +import java.math.BigInteger; +import java.util.Arrays; + +import linea.plugin.acc.test.tests.web3j.generated.AcceptanceTestToken; +import linea.plugin.acc.test.tests.web3j.generated.RevertExample; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Amount; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.utils.Numeric; + +public class SendBundleTest extends AbstractSendBundleTest { + + @Test + public void singleTxBundleIsAcceptedAndMined() { + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.getPrimaryBenefactor(); + + final TransferTransaction tx = accountTransactions.createTransfer(sender, recipient, 1); + + final String bundleRawTx = tx.signedTransactionData(); + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(new String[] {bundleRawTx}, Integer.toHexString(1))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx.transactionHash())); + } + + @Test + public void bundleIsAcceptedAndMined() { + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.getPrimaryBenefactor(); + + final TransferTransaction tx1 = accountTransactions.createTransfer(sender, recipient, 1); + final TransferTransaction tx2 = accountTransactions.createTransfer(recipient, sender, 1); + + final String[] bundleRawTxs = + new String[] {tx1.signedTransactionData(), tx2.signedTransactionData()}; + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(1))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx1.transactionHash())); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx2.transactionHash())); + } + + @Test + public void distributeTokensInBundle() throws Exception { + final AcceptanceTestToken token = deployAcceptanceTestToken(); + + final int numOfTransfers = 10; + + final TokenTransfer[] tokenTransfers = new TokenTransfer[numOfTransfers]; + for (int i = 0; i < numOfTransfers; i++) { + tokenTransfers[i] = + transferTokens( + token, + accounts.getPrimaryBenefactor(), + i + 1, + accounts.createAccount("recipient " + i), + 1); + } + + final var bundleRawTxs = + Arrays.stream(tokenTransfers).map(TokenTransfer::rawTx).toArray(String[]::new); + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(2))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + Arrays.stream(tokenTransfers) + .forEach( + tokenTransfer -> { + minerNode.verify(eth.expectSuccessfulTransactionReceipt(tokenTransfer.txHash())); + try { + assertThat(token.balanceOf(tokenTransfer.recipient().getAddress()).send()) + .isEqualTo(1); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test + public void payGasWithTokensInBundle() throws Exception { + final AcceptanceTestToken token = deployAcceptanceTestToken(); + + final var recipient = accounts.createAccount("recipient"); + final var transferReceipt = token.transfer(recipient.getAddress(), BigInteger.TEN).send(); + assertThat(transferReceipt.isStatusOK()).isTrue(); + assertThat(token.balanceOf(recipient.getAddress()).send()).isEqualTo(10); + + final var transferGasTx = + accountTransactions.createTransfer(accounts.getSecondaryBenefactor(), recipient, 1); + final var payGasWithTokenRawTx = + transferTokens(token, recipient, 0, accounts.getSecondaryBenefactor(), 1); + + final var bundleRawTxs = + new String[] {transferGasTx.signedTransactionData(), payGasWithTokenRawTx.rawTx()}; + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(3))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferGasTx.transactionHash())); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(payGasWithTokenRawTx.txHash())); + + final var payGasWithTokenReceipt = + ethTransactions + .getTransactionReceipt(payGasWithTokenRawTx.txHash()) + .execute(minerNode.nodeRequests()) + .orElseThrow(); + final var gasPrice = + Wei.fromHexString(payGasWithTokenReceipt.getEffectiveGasPrice()).toBigInteger(); + + final var expectedBalance = + Amount.ether(1) + .subtract(Amount.wei(gasPrice.multiply(payGasWithTokenReceipt.getGasUsed()))); + + minerNode.verify(recipient.balanceEquals(expectedBalance)); + + assertThat(token.balanceOf(recipient.getAddress()).send()).isEqualTo(9); + assertThat(token.balanceOf(accounts.getSecondaryBenefactor().getAddress()).send()).isEqualTo(1); + } + + @Test + public void singleNotSelectedTxBundleIsNotMined() throws Exception { + final var mulmodExecutor = deployMulmodExecutor(); + + final var mulmodOverflow = + mulmodOperation(mulmodExecutor, accounts.getPrimaryBenefactor(), 1, 5_000); + + final var sendBundleRequest = + new SendBundleRequest( + new BundleParams(new String[] {mulmodOverflow.rawTx()}, Integer.toHexString(2))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + // transfer used as sentry to ensure a new block is mined without the bundles + final var transferTxHash = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), accounts.getPrimaryBenefactor(), 1) + .execute(minerNode.nodeRequests()); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash.toHexString())); + minerNode.verify(eth.expectNoTransactionReceipt(mulmodOverflow.txHash())); + } + + @Test + public void bundleWithNotSelectedTxIsNotMined() throws Exception { + final var mulmodExecutor = deployMulmodExecutor(); + final var recipient = accounts.createAccount("recipient"); + + final var mulmodOverflow = + mulmodOperation(mulmodExecutor, accounts.getPrimaryBenefactor(), 1, 5_000); + final var inBundleTransferTx = + accountTransactions.createTransfer(recipient, accounts.getPrimaryBenefactor(), 1); + + // first is not selected because exceeds line count limit + final var bundleRawTxs = + new String[] {mulmodOverflow.rawTx(), inBundleTransferTx.signedTransactionData()}; + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(2))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + // transfer used as sentry to ensure a new block is mined without the bundles + final var transferTxHash1 = + accountTransactions + .createTransfer(accounts.getSecondaryBenefactor(), recipient, 10) + .execute(minerNode.nodeRequests()); + + // first sentry is mined and no tx of the bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash1.toHexString())); + minerNode.verify(eth.expectNoTransactionReceipt(mulmodOverflow.txHash())); + minerNode.verify(eth.expectNoTransactionReceipt(inBundleTransferTx.transactionHash())); + + // try with a bundle where first is selected but second no + final var reverseBundleRawTxs = + new String[] {inBundleTransferTx.signedTransactionData(), mulmodOverflow.rawTx()}; + final var sendReverseBundleRequest = + new SendBundleRequest(new BundleParams(reverseBundleRawTxs, Integer.toHexString(3))); + final var sendReverseBundleResponse = + sendReverseBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendReverseBundleResponse.hasError()).isFalse(); + assertThat(sendReverseBundleResponse.getResult().bundleHash()).isNotBlank(); + + // transfer used as sentry to ensure a new block is mined without the bundles + final var transferTxHash2 = + accountTransactions + .createTransfer( + accounts.getSecondaryBenefactor(), + accounts.getPrimaryBenefactor(), + 1, + BigInteger.valueOf(1)) + .execute(minerNode.nodeRequests()); + + // second sentry is mined and no tx of the bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash2.toHexString())); + minerNode.verify(eth.expectNoTransactionReceipt(mulmodOverflow.txHash())); + minerNode.verify(eth.expectNoTransactionReceipt(inBundleTransferTx.transactionHash())); + } + + @Test + public void mixOfSelectedNotSelectedBundles() throws Exception { + final var mulmodExecutor = deployMulmodExecutor(); + + final var mulmodOverflow = + mulmodOperation(mulmodExecutor, accounts.getPrimaryBenefactor(), 1, 5_000); + final var inBundleTransferTx1 = + accountTransactions.createTransfer( + accounts.getSecondaryBenefactor(), accounts.getPrimaryBenefactor(), 1, BigInteger.ZERO); + + // first is not selected because exceeds line count limit + final var notSelectedBundleRawTxs = + new String[] {mulmodOverflow.rawTx(), inBundleTransferTx1.signedTransactionData()}; + + final var sendNotSelectedBundleRequest = + new SendBundleRequest(new BundleParams(notSelectedBundleRawTxs, Integer.toHexString(2))); + final var sendNotSelectedBundleResponse = + sendNotSelectedBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendNotSelectedBundleResponse.hasError()).isFalse(); + assertThat(sendNotSelectedBundleResponse.getResult().bundleHash()).isNotBlank(); + + final var mulmodOk = mulmodOperation(mulmodExecutor, accounts.getPrimaryBenefactor(), 1, 1_000); + final var inBundleTransferTx2 = + accountTransactions.createTransfer( + accounts.getSecondaryBenefactor(), accounts.getPrimaryBenefactor(), 2, BigInteger.ZERO); + + // both txs are valid + final var selectedBundleRawTxs = + new String[] {mulmodOk.rawTx(), inBundleTransferTx2.signedTransactionData()}; + + final var sendSelectedBundleRequest = + new SendBundleRequest(new BundleParams(selectedBundleRawTxs, Integer.toHexString(2))); + final var sendSelectedBundleResponse = + sendSelectedBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendSelectedBundleResponse.hasError()).isFalse(); + assertThat(sendSelectedBundleResponse.getResult().bundleHash()).isNotBlank(); + + // assert second bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(mulmodOk.txHash())); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(inBundleTransferTx2.transactionHash())); + + // while first bundle is not selected + minerNode.verify(eth.expectNoTransactionReceipt(mulmodOverflow.txHash())); + minerNode.verify(eth.expectNoTransactionReceipt(inBundleTransferTx1.transactionHash())); + } + + @Test + public void bundleWithRevertedTxIsNotMined() throws Exception { + final RevertExample revertExample = deployRevertExample(); + + // fund a new account + final var recipient = accounts.createAccount("recipient"); + final var txHashFundRecipient = + accountTransactions + .createTransfer(accounts.getPrimaryBenefactor(), recipient, 10, BigInteger.valueOf(1)) + .execute(minerNode.nodeRequests()); + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHashFundRecipient.toHexString())); + + // create a tx that reverts + final String contractAddress = revertExample.getContractAddress(); + final String txData = revertExample.setValue(BigInteger.ZERO).encodeFunctionCall(); + + final RawTransaction txThatReverts = + RawTransaction.createTransaction( + CHAIN_ID, + BigInteger.ZERO, + TRANSFER_GAS_LIMIT, + contractAddress, + BigInteger.ZERO, + txData, + GAS_PRICE, + GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE)); + final var signedTxThatReverts = + Numeric.toHexString( + TransactionEncoder.signMessage( + txThatReverts, Credentials.create(Accounts.GENESIS_ACCOUNT_TWO_PRIVATE_KEY))); + final var txThatRevertsHash = sha3(signedTxThatReverts); + + final var inBundleTransferTx = + accountTransactions.createTransfer(recipient, accounts.getSecondaryBenefactor(), 1); + + // first tx reverts and bundle is not selected + final var bundleRawTxs = + new String[] {signedTxThatReverts, inBundleTransferTx.signedTransactionData()}; + + final var sendBundleRequest = + new SendBundleRequest(new BundleParams(bundleRawTxs, Integer.toHexString(3))); + final var sendBundleResponse = sendBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendBundleResponse.hasError()).isFalse(); + assertThat(sendBundleResponse.getResult().bundleHash()).isNotBlank(); + + // transfer used as sentry to ensure a new block is mined without the bundles + final var transferTxHash1 = + accountTransactions + .createTransfer( + accounts.getPrimaryBenefactor(), + accounts.getSecondaryBenefactor(), + 1, + BigInteger.valueOf(2)) + .execute(minerNode.nodeRequests()); + + // first sentry is mined and no tx of the bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash1.toHexString())); + minerNode.verify(eth.expectNoTransactionReceipt(txThatRevertsHash)); + minerNode.verify(eth.expectNoTransactionReceipt(inBundleTransferTx.transactionHash())); + + // try with a bundle where first is selected but second reverts + final var reverseBundleRawTxs = + new String[] {inBundleTransferTx.signedTransactionData(), signedTxThatReverts}; + final var sendReverseBundleRequest = + new SendBundleRequest(new BundleParams(reverseBundleRawTxs, Integer.toHexString(4))); + final var sendReverseBundleResponse = + sendReverseBundleRequest.execute(minerNode.nodeRequests()); + + assertThat(sendReverseBundleResponse.hasError()).isFalse(); + assertThat(sendReverseBundleResponse.getResult().bundleHash()).isNotBlank(); + + // transfer used as sentry to ensure a new block is mined without the bundles + final var transferTxHash2 = + accountTransactions + .createTransfer(accounts.getPrimaryBenefactor(), recipient, 1, BigInteger.valueOf(3)) + .execute(minerNode.nodeRequests()); + + // second sentry is mined and no tx of the bundle is mined + minerNode.verify(eth.expectSuccessfulTransactionReceipt(transferTxHash2.toHexString())); + minerNode.verify(eth.expectNoTransactionReceipt(txThatRevertsHash)); + minerNode.verify(eth.expectNoTransactionReceipt(inBundleTransferTx.transactionHash())); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SetExtraDataTest.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SetExtraDataTest.java new file mode 100644 index 00000000..8fccf961 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/linea/plugin/acc/test/rpc/linea/SetExtraDataTest.java @@ -0,0 +1,264 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package linea.plugin.acc.test.rpc.linea; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import com.google.common.base.Strings; +import linea.plugin.acc.test.LineaPluginTestBase; +import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import net.consensys.linea.config.LineaProfitabilityCliOptions; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.crypto.digests.KeccakDigest; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.NodeRequests; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.Transaction; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.TransferTransaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.Request; +import org.web3j.protocol.core.Response; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.RawTransactionManager; +import org.web3j.tx.TransactionManager; + +public class SetExtraDataTest extends LineaPluginTestBase { + protected static final int FIXED_GAS_COST_WEI = 0; + protected static final int VARIABLE_GAS_COST_WEI = 1_000_000_000; + protected static final double MIN_MARGIN = 1.5; + protected static final Wei MIN_GAS_PRICE = Wei.of(1_000_000); + protected static final int MAX_TRANSACTION_GAS_LIMIT = 30_000_000; + protected LineaProfitabilityConfiguration profitabilityConf; + + @Override + public List getTestCliOptions() { + return getTestCommandLineOptionsBuilder().build(); + } + + protected TestCommandLineOptionsBuilder getTestCommandLineOptionsBuilder() { + return new TestCommandLineOptionsBuilder() + .set("--plugin-linea-fixed-gas-cost-wei=", String.valueOf(FIXED_GAS_COST_WEI)) + .set("--plugin-linea-variable-gas-cost-wei=", String.valueOf(VARIABLE_GAS_COST_WEI)) + .set("--plugin-linea-min-margin=", String.valueOf(MIN_MARGIN)) + .set("--plugin-linea-max-tx-gas-limit=", String.valueOf(MAX_TRANSACTION_GAS_LIMIT)) + .set("--plugin-linea-extra-data-pricing-enabled=", "true"); + } + + @BeforeEach + public void setMinGasPrice() { + minerNode.getMiningParameters().setMinTransactionGasPrice(MIN_GAS_PRICE); + } + + @BeforeEach + public void createDefaultConfigurations() { + profitabilityConf = + LineaProfitabilityCliOptions.create().toDomainObject().toBuilder() + .fixedCostWei(FIXED_GAS_COST_WEI) + .variableCostWei(VARIABLE_GAS_COST_WEI) + .minMargin(MIN_MARGIN) + .build(); + } + + @Test + public void setUnsupportedExtraDataReturnsError() { + final var unsupportedExtraData = Bytes32.ZERO; + + final var reqLinea = new FailingLineaSetExtraDataRequest(unsupportedExtraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getMessage()) + .isEqualTo( + "Unsupported extra data field 0x0000000000000000000000000000000000000000000000000000000000000000"); + } + + @Test + public void setTooLongExtraDataReturnsError() { + final var tooLongExtraData = Bytes.concatenate(Bytes.of(1), Bytes32.ZERO); + + final var reqLinea = new FailingLineaSetExtraDataRequest(tooLongExtraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getMessage()).isEqualTo("Expected 32 bytes but got 33"); + } + + @Test + public void setTooShortExtraDataReturnsError() { + final var tooShortExtraData = Bytes32.ZERO.slice(1); + + final var reqLinea = new FailingLineaSetExtraDataRequest(tooShortExtraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea.getMessage()).isEqualTo("Expected 32 bytes but got 31"); + } + + @Test + public void successfulSetExtraData() { + final var extraData = + Bytes32.fromHexString("0x0100000000000000000000000000000000000000000000000000000000000000"); + + final var reqLinea = new LineaSetExtraDataRequest(extraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea).isTrue(); + } + + @Test + public void successfulUpdateMinGasPrice() { + final var doubledMinGasPriceKWei = MIN_GAS_PRICE.multiply(2).divide(1000); + final var hexMinGasPrice = + Strings.padStart(doubledMinGasPriceKWei.toShortHexString().substring(2), 8, '0'); + final var extraData = + Bytes32.fromHexString( + "0x010000000000000000" + hexMinGasPrice + "00000000000000000000000000000000000000"); + + final var reqLinea = new LineaSetExtraDataRequest(extraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea).isTrue(); + assertThat(minerNode.getMiningParameters().getMinTransactionGasPrice()) + .isEqualTo(MIN_GAS_PRICE.multiply(2)); + } + + @Test + public void successfulUpdatePricingParameters() throws IOException { + final Web3j web3j = minerNode.nodeRequests().eth(); + final Credentials credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY); + final TransactionManager txManager = new RawTransactionManager(web3j, credentials, CHAIN_ID); + + final KeccakDigest keccakDigest = new KeccakDigest(256); + final StringBuilder txData = new StringBuilder(); + txData.append("0x"); + for (int i = 0; i < 10; i++) { + keccakDigest.update(new byte[] {(byte) i}, 0, 1); + final byte[] out = new byte[32]; + keccakDigest.doFinal(out, 0); + txData.append(new BigInteger(out)); + } + + final var txUnprofitable = + txManager.sendTransaction( + MIN_GAS_PRICE.getAsBigInteger(), + BigInteger.valueOf(MAX_TX_GAS_LIMIT / 2), + credentials.getAddress(), + txData.toString(), + BigInteger.ZERO); + + final Account sender = accounts.getSecondaryBenefactor(); + final Account recipient = accounts.createAccount("recipient"); + final TransferTransaction transferTx = accountTransactions.createTransfer(sender, recipient, 1); + final var txHash = minerNode.execute(transferTx); + + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txHash.toHexString())); + + // assert that tx below margin is not confirmed + minerNode.verify(eth.expectNoTransactionReceipt(txUnprofitable.getTransactionHash())); + + final var zeroFixedCostKWei = "00000000"; + final var minimalVariableCostKWei = "00000001"; + final var minimalMinGasPriceKWei = "00000002"; + final var extraData = + Bytes32.fromHexString( + "0x01" + + zeroFixedCostKWei + + minimalVariableCostKWei + + minimalMinGasPriceKWei + + "00000000000000000000000000000000000000"); + + final var reqLinea = new LineaSetExtraDataRequest(extraData); + final var respLinea = reqLinea.execute(minerNode.nodeRequests()); + assertThat(respLinea).isTrue(); + assertThat(minerNode.getMiningParameters().getMinTransactionGasPrice()).isEqualTo(Wei.of(2000)); + // assert that tx is confirmed now + minerNode.verify(eth.expectSuccessfulTransactionReceipt(txUnprofitable.getTransactionHash())); + } + + @Test + public void parseErrorLineaEstimateGasRequestReturnErrorResponse() + throws IOException, InterruptedException { + final var httpService = (HttpService) minerNode.nodeRequests().getWeb3jService(); + final var httpClient = HttpClient.newHttpClient(); + final var badJsonRequest = + HttpRequest.newBuilder(URI.create(httpService.getUrl())) + .headers("Content-Type", "application/json") + .POST( + HttpRequest.BodyPublishers.ofString( + """ + {"jsonrpc":"2.0","method":"linea_setExtraData","params":[malformed json],"id":53} + """)) + .build(); + final var errorResponse = httpClient.send(badJsonRequest, HttpResponse.BodyHandlers.ofString()); + assertThat(errorResponse.body()) + .isEqualTo( + """ + {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}"""); + } + + static class LineaSetExtraDataRequest implements Transaction { + private final Bytes32 extraData; + + public LineaSetExtraDataRequest(final Bytes32 extraData) { + this.extraData = extraData; + } + + @Override + public Boolean execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "linea_setExtraData", + List.of(extraData.toHexString()), + nodeRequests.getWeb3jService(), + LineaSetExtraDataResponse.class) + .send() + .getResult(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + static class FailingLineaSetExtraDataRequest implements Transaction { + private final Bytes extraData; + + public FailingLineaSetExtraDataRequest(final Bytes extraData) { + this.extraData = extraData; + } + + @Override + public Response.Error execute(final NodeRequests nodeRequests) { + try { + return new Request<>( + "linea_setExtraData", + List.of(extraData.toHexString()), + nodeRequests.getWeb3jService(), + LineaSetExtraDataResponse.class) + .send() + .getError(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + static class LineaSetExtraDataResponse extends org.web3j.protocol.core.Response {} +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java new file mode 100644 index 00000000..45de6aff --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java @@ -0,0 +1,177 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.tests.acceptance.dsl; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.ProcessBuilder.Redirect; +import java.math.BigInteger; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; +import org.hyperledger.besu.tests.acceptance.dsl.blockchain.Blockchain; +import org.hyperledger.besu.tests.acceptance.dsl.condition.admin.AdminConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.bft.BftConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.clique.CliqueConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.eth.EthConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.login.LoginConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.net.NetConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.perm.PermissioningConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.process.ExitedWithCode; +import org.hyperledger.besu.tests.acceptance.dsl.condition.txpool.TxPoolConditions; +import org.hyperledger.besu.tests.acceptance.dsl.condition.web3.Web3Conditions; +import org.hyperledger.besu.tests.acceptance.dsl.contract.ContractVerifier; +import org.hyperledger.besu.tests.acceptance.dsl.node.Node; +import org.hyperledger.besu.tests.acceptance.dsl.node.cluster.Cluster; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.BesuNodeFactory; +import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.permissioning.PermissionedNodeBuilder; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.account.AccountTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.admin.AdminTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.bft.BftTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.clique.CliqueTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.contract.ContractTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.miner.MinerTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.perm.PermissioningTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.txpool.TxPoolTransactions; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.web3.Web3Transactions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Base class for acceptance tests. */ +@ExtendWith(AcceptanceTestBaseTestWatcher.class) +@Tag("AcceptanceTest") +@Slf4j +public abstract class AcceptanceTestBase { + protected final Accounts accounts; + protected final AccountTransactions accountTransactions; + protected final AdminConditions admin; + protected final AdminTransactions adminTransactions; + protected final Blockchain blockchain; + protected final CliqueConditions clique; + protected final CliqueTransactions cliqueTransactions; + protected final Cluster cluster; + protected final ContractVerifier contractVerifier; + protected final ContractTransactions contractTransactions; + protected final EthConditions eth; + protected final EthTransactions ethTransactions; + protected final BftTransactions bftTransactions; + protected final BftConditions bft; + protected final LoginConditions login; + protected final NetConditions net; + protected final BesuNodeFactory besu; + protected final PermissioningConditions perm; + protected final PermissionedNodeBuilder permissionedNodeBuilder; + protected final PermissioningTransactions permissioningTransactions; + protected final MinerTransactions minerTransactions; + protected final Web3Conditions web3; + protected final TxPoolConditions txPoolConditions; + protected final TxPoolTransactions txPoolTransactions; + protected final ExitedWithCode exitedSuccessfully; + + private final ExecutorService outputProcessorExecutor = Executors.newCachedThreadPool(); + + protected AcceptanceTestBase() { + ethTransactions = new EthTransactions(); + accounts = new Accounts(ethTransactions); + adminTransactions = new AdminTransactions(); + cliqueTransactions = new CliqueTransactions(); + bftTransactions = new BftTransactions(); + accountTransactions = new AccountTransactions(accounts); + permissioningTransactions = new PermissioningTransactions(); + contractTransactions = new ContractTransactions(); + minerTransactions = new MinerTransactions(); + + blockchain = new Blockchain(ethTransactions); + clique = new CliqueConditions(ethTransactions, cliqueTransactions); + eth = new EthConditions(ethTransactions); + bft = new BftConditions(bftTransactions); + login = new LoginConditions(); + net = new NetConditions(new NetTransactions()); + cluster = new Cluster(net); + perm = new PermissioningConditions(permissioningTransactions); + admin = new AdminConditions(adminTransactions); + web3 = new Web3Conditions(new Web3Transactions()); + besu = new BesuNodeFactory(); + txPoolTransactions = new TxPoolTransactions(); + txPoolConditions = new TxPoolConditions(txPoolTransactions); + contractVerifier = new ContractVerifier(accounts.getPrimaryBenefactor()); + permissionedNodeBuilder = new PermissionedNodeBuilder(); + exitedSuccessfully = new ExitedWithCode(0); + } + + @AfterEach + public void tearDownAcceptanceTestBase() { + reportMemory(); + cluster.close(); + } + + /** Report memory usage after test execution. */ + public void reportMemory() { + String os = System.getProperty("os.name"); + String[] command = null; + if (os.contains("Linux")) { + command = new String[] {"/usr/bin/top", "-n", "1", "-o", "%MEM", "-b", "-c", "-w", "180"}; + } + if (os.contains("Mac")) { + command = new String[] {"/usr/bin/top", "-l", "1", "-o", "mem", "-n", "20"}; + } + if (command != null) { + log.info("Memory usage at end of test:"); + final ProcessBuilder processBuilder = + new ProcessBuilder(command).redirectErrorStream(true).redirectInput(Redirect.INHERIT); + try { + final Process memInfoProcess = processBuilder.start(); + outputProcessorExecutor.execute(() -> printOutput(memInfoProcess)); + memInfoProcess.waitFor(); + log.debug("Memory info process exited with code {}", memInfoProcess.exitValue()); + } catch (final Exception e) { + log.warn("Error running memory information process", e); + } + } else { + log.info("Don't know how to report memory for OS {}", os); + } + } + + private void printOutput(final Process process) { + try (final BufferedReader in = + new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8))) { + String line = in.readLine(); + while (line != null) { + log.info(line); + line = in.readLine(); + } + } catch (final IOException e) { + log.warn("Failed to read output from memory information process: ", e); + } + } + + protected void waitForBlockHeight(final Node node, final long blockchainHeight) { + WaitUtils.waitFor( + 120, + () -> + assertThat(node.execute(ethTransactions.blockNumber())) + .isGreaterThanOrEqualTo(BigInteger.valueOf(blockchainHeight))); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/EngineAPIService.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/EngineAPIService.java new file mode 100644 index 00000000..14a36185 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/EngineAPIService.java @@ -0,0 +1,218 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.tests.acceptance.dsl; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode; +import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions; +import org.web3j.protocol.core.methods.response.EthBlock; + +/* + * Inspired by PragueAcceptanceTestHelper class in Besu codebase. We use this class to + * emulate Engine API calls to the Besu Node, so that we can run tests for post-merge EVM forks. + */ +public class EngineAPIService { + private final OkHttpClient httpClient; + private final ObjectMapper mapper; + private final BesuNode node; + private final EthTransactions ethTransactions; + + private static final String JSONRPC_VERSION = "2.0"; + private static final long JSONRPC_REQUEST_ID = 67; + private static final String SUGGESTED_BLOCK_FEE_RECIPIENT = + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"; + + public EngineAPIService(BesuNode node, EthTransactions ethTransactions, ObjectMapper mapper) { + httpClient = new OkHttpClient(); + this.mapper = mapper; + this.node = node; + this.ethTransactions = ethTransactions; + } + + /* + * See https://hackmd.io/@danielrachi/engine_api + * + * The flow to build a block with the Engine API is as follows: + * 1. Send engine_forkchoiceUpdated(EngineForkchoiceUpdatedParameter, EnginePayloadAttributesParameter) request to Besu node + * 2. Besu node responds with payloadId + * The Besu Node will start building a proposed block + * + * 3. Send engine_getPayload(payloadId) request to Besu node + * 4. Besu node responds with executionPayload + * Get the proposed block from the Besu node + * + * 5. Send engine_newPayload request to Besu node + * Validate the proposed block. Then store the validated block for future reference. + * Unsure why the proposed block is not stored in the previous steps where it was built. + * + * 6. Send engine_forkchoiceUpdated(EngineForkchoiceUpdatedParameter) request to Besu node + * Add validated block to blockchain head. + * + * @param blockTimestampSeconds The Unix timestamp (in seconds) to assign to the new block. + * @param blockBuildingTimeMs The duration (in milliseconds) allocated for the Besu node to build the block. + */ + public void buildNewBlock(long blockTimestampSeconds, long blockBuildingTimeMs) + throws IOException, InterruptedException { + final EthBlock.Block latestBlock = node.execute(ethTransactions.block()); + + final Call buildBlockRequest = + createForkChoiceRequest(latestBlock.getHash(), blockTimestampSeconds); + + final String payloadId; + try (final Response buildBlockResponse = buildBlockRequest.execute()) { + // Ideally, we would deserialize directly into Besu native types such as + // EngineUpdateForkchoiceResult and JsonRpcSuccessResponse. However, neither class + // provides a default constructor or a constructor annotated with @JsonCreator. + // As a result, deserializing them would require hefty boilerplate code (custom + // deserializers and DTOs). To keep things simple and lightweight, we instead + // parse the relevant fields manually from the expected JSON structure. + payloadId = + mapper + .readTree(buildBlockResponse.body().string()) + .get("result") + .get("payloadId") + .asText(); + assertThat(payloadId).isNotEmpty(); + } + + // This is required to give the Besu node time to build the block. As per the Engine API spec, + // engine_forkChoice will begin the payload build process and engine_getPayload may stop the + // payload build process. Besu node behaviour is to stop the payload build process on + // engine_getPayload. So unfortunately we lack a means to inspect a payload in-building without + // interrupting it. Hence we must be conservative and wait for the 'SECONDS_PER_SLOT' time, + // especially for slower machines running the tests. + // See - https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md + Thread.sleep(blockBuildingTimeMs); + + final Call getPayloadRequest = createGetPayloadRequest(payloadId); + + final ObjectNode executionPayload; + final ArrayNode executionRequests; + final String newBlockHash; + final String parentBeaconBlockRoot; + try (final Response getPayloadResponse = getPayloadRequest.execute()) { + assertThat(getPayloadResponse.code()).isEqualTo(200); + JsonNode result = mapper.readTree(getPayloadResponse.body().string()).get("result"); + executionPayload = (ObjectNode) result.get("executionPayload"); + executionRequests = (ArrayNode) result.get("executionRequests"); + newBlockHash = executionPayload.get("blockHash").asText(); + parentBeaconBlockRoot = executionPayload.remove("parentBeaconBlockRoot").asText(); + assertThat(newBlockHash).isNotEmpty(); + } + + final Call newPayloadRequest = + createNewPayloadRequest(executionPayload, parentBeaconBlockRoot, executionRequests); + + try (final Response newPayloadResponse = newPayloadRequest.execute()) { + assertThat(newPayloadResponse.code()).isEqualTo(200); + final String responseStatus = + mapper.readTree(newPayloadResponse.body().string()).get("result").get("status").asText(); + assertThat(responseStatus).isEqualTo("VALID"); + } + + final Call moveChainAheadRequest = createForkChoiceRequest(newBlockHash); + + try (final Response moveChainAheadResponse = moveChainAheadRequest.execute()) { + assertThat(moveChainAheadResponse.code()).isEqualTo(200); + } + } + + private Call createForkChoiceRequest(final String blockHash) { + return createForkChoiceRequest(blockHash, null); + } + + private Call createForkChoiceRequest(final String parentBlockHash, final Long blockTimestamp) { + final Optional maybeTimeStamp = Optional.ofNullable(blockTimestamp); + + // Construct the first param - EngineForkchoiceUpdatedParameter + ArrayNode params = mapper.createArrayNode(); + ObjectNode forkchoiceState = mapper.createObjectNode(); + forkchoiceState.put("headBlockHash", parentBlockHash); + forkchoiceState.put("safeBlockHash", parentBlockHash); + forkchoiceState.put("finalizedBlockHash", parentBlockHash); + params.add(forkchoiceState); + + // Optionally construct the second param - EnginePayloadAttributesParameter + if (maybeTimeStamp.isPresent()) { + ObjectNode payloadAttributes = mapper.createObjectNode(); + payloadAttributes.put("timestamp", blockTimestamp); + payloadAttributes.put("prevRandao", Hash.ZERO.toString()); + payloadAttributes.put("suggestedFeeRecipient", SUGGESTED_BLOCK_FEE_RECIPIENT); + payloadAttributes.set("withdrawals", mapper.createArrayNode()); + payloadAttributes.put("parentBeaconBlockRoot", Hash.ZERO.toString()); + params.add(payloadAttributes); + } + return createEngineCall("engine_forkchoiceUpdatedV3", params); + } + + private Call createGetPayloadRequest(final String payloadId) { + ArrayNode params = mapper.createArrayNode(); + params.add(payloadId); + return createEngineCall("engine_getPayloadV4", params); + } + + private Call createNewPayloadRequest( + final ObjectNode executionPayload, + final String parentBeaconBlockRoot, + final ArrayNode executionRequests) { + ArrayNode params = mapper.createArrayNode(); + params.add(executionPayload); + params.add(mapper.createArrayNode()); // empty withdrawals + params.add(parentBeaconBlockRoot); + params.add(executionRequests); + + return createEngineCall("engine_newPayloadV4", params); + } + + private Call createEngineCall(final String rpcMethod, ArrayNode params) { + ObjectNode request = mapper.createObjectNode(); + request.put("jsonrpc", JSONRPC_VERSION); + request.put("method", rpcMethod); + request.set("params", params); + request.put("id", JSONRPC_REQUEST_ID); + + String requestString; + try { + requestString = mapper.writeValueAsString(request); + } catch (Exception e) { + throw new RuntimeException( + "Failed to serialize JSON-RPC request for method " + rpcMethod + ":", e); + } + + return httpClient.newCall( + new Request.Builder() + .url(node.engineRpcUrl().get()) + .post( + RequestBody.create( + requestString, MediaType.parse("application/json; charset=utf-8"))) + .build()); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/StaticNodesUtils.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/StaticNodesUtils.java new file mode 100644 index 00000000..2e288f00 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/StaticNodesUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.tests.acceptance.dsl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +public class StaticNodesUtils { + + public static Path createStaticNodesFile(final Path directory, final List staticNodes) { + try { + final Path tempFile = Files.createTempFile(directory, "", ""); + tempFile.toFile().deleteOnExit(); + + final Path staticNodesFile = tempFile.getParent().resolve("static-nodes.json"); + Files.move(tempFile, staticNodesFile); + staticNodesFile.toFile().deleteOnExit(); + + final String json = + staticNodes.stream() + .map(s -> String.format("\"%s\"", s)) + .collect(Collectors.joining(",", "[", "]")); + + Files.writeString(staticNodesFile, json); + + return staticNodesFile; + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/WaitUtils.java b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/WaitUtils.java new file mode 100644 index 00000000..0f03633e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/WaitUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.tests.acceptance.dsl; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; + +/** Contains functionality for timeouts. */ +public class WaitUtils { + public static void waitFor(final ThrowingRunnable condition) { + waitFor(30, condition); + } + + public static void waitFor(final int timeout, final ThrowingRunnable condition) { + Awaitility.await() + .pollInterval(5, TimeUnit.SECONDS) + .ignoreExceptions() + .atMost(timeout, TimeUnit.SECONDS) + .untilAsserted(condition); + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique-prague.json.tpl b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique-prague.json.tpl new file mode 100644 index 00000000..fd6bef6a --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique-prague.json.tpl @@ -0,0 +1,117 @@ +{ + "config": { + "chainId": 1337, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "terminalTotalDifficulty":0, + "cancunTime":0, + "pragueTime":0, + "blobSchedule": { + "cancun": { + "target": 0, + "max": 0, + "baseFeeUpdateFraction": 3338477 + }, + "prague": { + "target": 0, + "max": 0, + "baseFeeUpdateFraction": 5007716 + }, + "osaka": { + "target": 0, + "max": 0, + "baseFeeUpdateFraction": 5007716 + } + }, + "clique": { + "blockperiodseconds": %blockperiodseconds%, + "epochlength": %epochlength%, + "createemptyblocks": %createemptyblocks% + }, + "depositContractAddress": "0x4242424242424242424242424242424242424242", + "withdrawalRequestContractAddress": "0x00A3ca265EBcb825B45F985A16CEFB49958cE017", + "consolidationRequestContractAddress": "0x00b42dbF2194e931E80326D950320f7d9Dbeac02" + }, + "zeroBaseFee": false, + "baseFeePerGas": "7", + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "%extraData%", + "gasLimit": "0x1C9C380", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "a05b21E5186Ce93d2a226722b85D6e550Ac7D6E3": { + "privateKey": "3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453", + "balance": "90000000000000000000000" + }, + "8da48afC965480220a3dB9244771bd3afcB5d895": { + "comment": "This account has signed a authorization for contract 0x0000000000000000000000000000000000009999 to send a 7702 transaction", + "privateKey": "11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1", + "balance": "90000000000000000000000" + }, + "0x0000000000000000000000000000000000000666": { + "comment": "Contract reverts immediately when called", + "balance": "0", + "code": "5F5FFD", + "codeDecompiled": "PUSH0 PUSH0 REVERT", + "storage": {} + }, + "0x0000000000000000000000000000000000009999": { + "comment": "Contract sends all its Ether to the address provided as a call data.", + "balance": "0", + "code": "5F5F5F5F475F355AF100", + "codeDecompiled": "PUSH0 PUSH0 PUSH0 PUSH0 SELFBALANCE PUSH0 CALLDATALOAD GAS CALL STOP", + "storage": {} + }, + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { + "comment": "This is the account used to sign the transaction that creates a validator exit", + "balance": "1000000000000000000000000000" + }, + "0x00A3ca265EBcb825B45F985A16CEFB49958cE017": { + "comment": "This is the runtime bytecode for the Withdrawal Request Smart Contract. It was created from the generated alloc section of fork_Prague_blockchain_test_engine_single_block_single_withdrawal_request_from_contract spec test", + "balance": "0", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000004": "000000000000000000000000a4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb", + "0x0000000000000000000000000000000000000000000000000000000000000005": "b10a4a15bf67b328c9b101d09e5c6ee6672978fdad9ef0d9e2ceffaee9922355", + "0x0000000000000000000000000000000000000000000000000000000000000006": "5d8601f0cb3bcc4ce1af9864779a416e00000000000000000000000000000000" + } + }, + "0x00b42dbF2194e931E80326D950320f7d9Dbeac02": { + "comment": "This is the runtime bytecode for the Consolidation Request Smart Contract", + "nonce": "0x01", + "balance": "0x00", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "storage": {} + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique.json.tpl b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique.json.tpl new file mode 100644 index 00000000..5ea3e53d --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/clique/clique.json.tpl @@ -0,0 +1,43 @@ +{ + "config": { + "chainId": 1337, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "clique": { + "blockperiodseconds": %blockperiodseconds%, + "epochlength": %epochlength%, + "createemptyblocks": %createemptyblocks% + } + }, + "zeroBaseFee": false, + "baseFeePerGas": "7", + "nonce": "0x0", + "timestamp": "0x6391BFF3", + "extraData": "%extraData%", + "gasLimit": "0x1C9C380", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "fe3b557e8fb62b89f4916b721be55ceb828dbd73": { + "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "0xad78ebc5ac6200000" + }, + "627306090abaB3A6e1400e9345bC60c78a8BEf57": { + "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + }, + "f17f52151EbEF6C7334FAD080c5704D77216b732": { + "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f", + "comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored", + "balance": "90000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/denyList.txt b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/denyList.txt new file mode 100644 index 00000000..f90c984a --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/denyList.txt @@ -0,0 +1 @@ +0x627306090abab3a6e1400e9345bc60c78a8bef57 \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/emptyDenyList.txt b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/emptyDenyList.txt new file mode 100644 index 00000000..e69de29b diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/log4j2-test.xml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/log4j2-test.xml new file mode 100644 index 00000000..dc5a8144 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/log4j2-test.xml @@ -0,0 +1,25 @@ + + + + TRACE + + + + + + + + + + + + + + + + + + + + + diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits.toml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits.toml new file mode 100644 index 00000000..a5af4d1f --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits.toml @@ -0,0 +1,82 @@ +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/Consensys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 524288 +BIN = 262144 +BLAKE_MODEXP_DATA = 16384 +BLOCK_DATA = 4096 +BLOCK_HASH = 2048 +EC_DATA = 262144 +EUC = 65536 +EXP = 8192 +EXT = 65536 +GAS = 65536 +HUB = 2097152 +LOG_DATA = 65536 +LOG_INFO = 4096 +MMIO = 4194304 +MMU = 4194304 +MOD = 131072 +MUL = 65536 +MXP = 524288 +OOB = 262144 +RLP_ADDR = 4096 +RLP_TXN = 131072 +RLP_TXN_RCPT = 65536 +ROM = 4194304 +ROM_LEX = 1024 +SHAKIRA_DATA = 32768 +SHF = 65536 +STP = 16384 +TRM = 32768 +TXN_DATA = 8192 +WCP = 262144 +# NOTE: in the original file the limits were just shy of powers of 2, e.g. ADD = 524286 +# Question: this seemed deliberate; it could be related to spillings; if so we may want +# to reduce all limits above by the corresponding spillings value (or 16 for simplicity) + +# +# Precompiles limits +# compare with https://github.com/Consensys/linea-arithmetization/issues/257 +# +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 128 +PRECOMPILE_SHA2_BLOCKS = 671 +PRECOMPILE_RIPEMD_BLOCKS = 671 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 4 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 512 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 32 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 16 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 64 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 64 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 # there are no gnarks circuit atm +PRECOMPILE_BLAKE_ROUNDS = 0 # it is possible to call BLAKE2f with r = 0; this is a nontrivial operation ... +# # Notes: +# - there are no IDENTITY related limits +# - we used to have the following limits +# * PRECOMPILE_SHA2_EFFECTIVE_CALLS = 1000000 +# * PRECOMPILE_RIPEMD_EFFECTIVE_CALLS = 1000000 +# (the values are nonsensical); +# as per Alex they are not required by the prover; + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits_sendRawTx.toml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits_sendRawTx.toml new file mode 100644 index 00000000..a3073761 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/moduleLimits_sendRawTx.toml @@ -0,0 +1,75 @@ + +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/Consensys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 247 +BIN = 262144 +BLAKE_MODEXP_DATA = 262144 +BLOCK_DATA = 26 +BLOCK_HASH = 6 +EC_DATA = 4096 +EUC = 16384 # can probably be lower +EXP = 32760 +EXT = 20 +GAS = 262144 +HUB = 321 +MMIO = 1048576 +MMU = 524288 +MOD = 23 +MUL = 20 +MXP = 311 +PHONEY_RLP = 65536 # can probably get lower +ROM = 3009 +ROM_LEX = 20 +SHF = 63 +TX_RLP = 131072 +TRM = 120 +WCP = 700 +LOG_DATA = 20 +LOG_INFO = 20 +RLP_ADDR = 20 +RLP_TXN = 1590 +RLP_TXN_RCPT = 100 +TXN_DATA = 30 +SHAKIRA_DATA = 262144 +STP = 20 +OOB = 262144 + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row + +# +# Precompiles limits +# +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 10000 +PRECOMPILE_SHA2_BLOCKS = 10000 +PRECOMPILE_RIPEMD_BLOCKS = 0 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 10000 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 # there are no gnarks circuit atm +PRECOMPILE_BLAKE_ROUNDS = 0 \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/noModuleLimits.toml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/noModuleLimits.toml new file mode 100644 index 00000000..b115525c --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/noModuleLimits.toml @@ -0,0 +1,74 @@ +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/Consensys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 262144 +BIN = 262144 +BIN_RT = 262144 +BLAKE_MODEXP_DATA = 262144 +BLOCK_DATA = 26 +BLOCK_HASH = 6 +EC_DATA = 4096 +EUC = 16384 # can probably be lower +EXP = 32760 +EXT = 32768 +GAS = 262144 +HUB = 2097152 +LOG_DATA = 262144 +LOG_INFO = 262144 +MMIO = 1048576 +MMU = 524288 +MOD = 131072 +MUL = 65536 +MXP = 524288 +OOB = 262144 +PHONEY_RLP = 65536 # can probably get lower +RLP_ADDR = 262144 +RLP_TXN = 262144 +RLP_TXN_RCPT = 262144 +ROM = 1048576 +ROM_LEX = 1048576 +SHAKIRA_DATA = 262144 +SHF = 65536 +STP = 262144 +TRM = 131072 +TXN_DATA = 262144 +TX_RLP = 131072 +WCP = 262144 + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Precompiles limits +# +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 10000 +PRECOMPILE_SHA2_BLOCKS = 10000 +PRECOMPILE_RIPEMD_BLOCKS = 0 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 10000 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 +PRECOMPILE_BLAKE_ROUNDS = 0 \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/strictModuleLimits.toml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/strictModuleLimits.toml new file mode 100644 index 00000000..37f7bcb0 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/strictModuleLimits.toml @@ -0,0 +1,76 @@ +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/Consensys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 7000 +BIN = 262144 +BLAKE_MODEXP_DATA = 262144 +EC_DATA = 4096 +EUC = 16384 # can probably be lower +EXP = 32760 +EXT = 32768 +GAS = 262144 +HUB = 10000 +MMIO = 1048576 +MMU = 524288 +MOD = 131072 +MUL = 65536 +MXP = 524288 +PHONEY_RLP = 65536 # can probably get lower +PUB_LOG = 16384 +PUB_LOG_INFO = 16384 +ROM = 1048576 +ROM_LEX = 1048576 +SHF = 65536 +TX_RLP = 131072 +TRM = 131072 +WCP = 262144 +LOG_DATA = 262144 +LOG_INFO = 262144 +RLP_ADDR = 262144 +RLP_TXN = 262144 +RLP_TXN_RCPT = 262144 +TXN_DATA = 262144 +SHAKIRA_DATA = 262144 +STP = 262144 +OOB = 262144 +BLOCK_DATA = 26 +BLOCK_HASH = 6 + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row + +# +# Precompiles limits +# +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 10000 +PRECOMPILE_SHA2_BLOCKS = 10000 +PRECOMPILE_RIPEMD_BLOCKS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 64 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 10000 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 +PRECOMPILE_BLAKE_ROUNDS = 0 \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/txOverflowModuleLimits.toml b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/txOverflowModuleLimits.toml new file mode 100644 index 00000000..ae7fca3f --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/resources/txOverflowModuleLimits.toml @@ -0,0 +1,75 @@ + +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/Consensys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 70 +BIN = 262144 +BLAKE_MODEXP_DATA = 262144 +BLOCK_DATA = 26 +BLOCK_HASH = 6 +EC_DATA = 4096 +EUC = 16384 # can probably be lower +EXP = 32760 +EXT = 20 +GAS = 262144 +HUB = 52 +MMIO = 1048576 +MMU = 524288 +MOD = 20 +MUL = 20 +MXP = 20 +PHONEY_RLP = 65536 # can probably get lower +ROM = 2402 +ROM_LEX = 20 +SHF = 20 +TX_RLP = 131072 +TRM = 120 +WCP = 306 +LOG_DATA = 20 +LOG_INFO = 20 +RLP_ADDR = 20 +RLP_TXN = 1300 +RLP_TXN_RCPT = 100 +TXN_DATA = 30 +SHAKIRA_DATA = 262144 +STP = 20 +OOB = 262144 + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row + +# +# Precompiles limits +# +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 10000 +PRECOMPILE_SHA2_BLOCKS = 10000 +PRECOMPILE_RIPEMD_BLOCKS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 10000 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 10000 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 10000 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 10000 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 +PRECOMPILE_BLAKE_ROUNDS = 0 \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/AcceptanceTestToken.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/AcceptanceTestToken.sol new file mode 100644 index 00000000..b086950a --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/AcceptanceTestToken.sol @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +pragma solidity >=0.7.0 <0.9.0; + +import "./libs/ERC20/ERC20.sol"; + +contract AcceptanceTestToken is ERC20 { + constructor() public ERC20("Acceptance Test", "AT") { + _mint(msg.sender, 1000); + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/DummyAdder.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/DummyAdder.sol new file mode 100644 index 00000000..843d3050 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/DummyAdder.sol @@ -0,0 +1,25 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +pragma solidity >=0.7.0 <0.9.0; + +contract DummyAdder { + uint sum; + + function add(int count) public { + for (int i = 0; i < count; i++) { + sum++; + } + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcAdd.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcAdd.sol new file mode 100644 index 00000000..994bd796 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcAdd.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract EcAdd { + function callEcAdd(bytes memory input) + public + view + returns (bytes memory) + { + uint256 callDataSize = input.length; + bytes memory output = new bytes(32); // Allocate memory for the output + + // Several calls to ECADD per transaction + uint callsPerTransaction = 32; + for (uint i = 0; i < callsPerTransaction; i++) { + assembly { + let callDataOffset := add(input, 0x20) // Move pointer past length prefix to actual input + let returnAtOffset := add(output, 0x20) // Move pointer past length prefix to store output + + let success := staticcall( + gas(), + 0x06, // ECADD address + callDataOffset, + callDataSize, + returnAtOffset, + 0 // returnAtCapacity + ) + } + } + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcMul.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcMul.sol new file mode 100644 index 00000000..cb3b6bed --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcMul.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract EcMul { + function callEcMul(bytes memory input) + public + view + returns (bytes memory) + { + uint256 callDataSize = input.length; + bytes memory output = new bytes(32); // Allocate memory for the output + + assembly { + let callDataOffset := add(input, 0x20) // Move pointer past length prefix to actual input + let returnAtOffset := add(output, 0x20) // Move pointer past length prefix to store output + + let success := staticcall( + gas(), + 0x07, // ECMUL address + callDataOffset, + callDataSize, + returnAtOffset, + 0 // returnAtCapacity + ) + } + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcPairing.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcPairing.sol new file mode 100644 index 00000000..4321a522 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcPairing.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract EcPairing { + function callEcPairing(bytes memory input) + public + view + returns (bytes memory) + { + uint256 callDataSize = input.length; + bytes memory output = new bytes(32); // Allocate memory for the output + + assembly { + let callDataOffset := add(input, 0x20) // Move pointer past length prefix to actual input + let returnAtOffset := add(output, 0x20) // Move pointer past length prefix to store output + + let success := staticcall( + gas(), + 0x08, // ECPAIRING address + callDataOffset, + callDataSize, + returnAtOffset, + 0 // returnAtCapacity + ) + } + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcRecover.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcRecover.sol new file mode 100644 index 00000000..ef3f6507 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/EcRecover.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract EcRecover { + function callEcRecover(bytes memory input) + public + view + returns (bytes memory) + { + uint256 callDataSize = input.length; + bytes memory output = new bytes(32); // Allocate memory for the output + + assembly { + let callDataOffset := add(input, 0x20) // Move pointer past length prefix to actual input + let returnAtOffset := add(output, 0x20) // Move pointer past length prefix to store output + + let success := staticcall( + gas(), + 0x01, // ECRECOVER address + callDataOffset, + callDataSize, + returnAtOffset, + 0 // returnAtCapacity + ) + } + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ExcludedPrecompiles.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ExcludedPrecompiles.sol new file mode 100644 index 00000000..bf850219 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ExcludedPrecompiles.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ExcludedPrecompiles { + function callRIPEMD160(bytes memory data) public view returns (bytes20 result) { + // The RIPEMD-160 precompile is located at address 0x3 + address ripemdPrecompile = address(0x3); + + // Prepare the input data + bytes memory input = data; + + // Prepare variables for the assembly call + bool success; + + // Use inline assembly to call the precompile + assembly { + // Call the precompile + // Arguments: gas, address, input offset, input size, output offset, output size + success := staticcall(gas(), ripemdPrecompile, add(input, 32), mload(input), result, 20) + } + + // Check if the call was successful + require(success, "RIPEMD-160 call failed"); + + return result; + } + + function callBlake2f( + uint32 rounds, + bytes32[2] memory h, + bytes32[4] memory m, + bytes8[2] memory t, + bool f + ) public view returns (bytes32[2] memory) { + // Blake2f precompile address + address BLAKE2F_PRECOMPILE = address(0x09); + + bytes memory input = abi.encodePacked( + rounds, + h[0], h[1], + m[0], m[1], m[2], m[3], + t[0], t[1], + f ? bytes1(0x01) : bytes1(0x00) + ); + + (bool success, bytes memory result) = BLAKE2F_PRECOMPILE.staticcall(input); + require(success, "Blake2f precompile call failed"); + + bytes32[2] memory output; + assembly { + mstore(output, mload(add(result, 32))) + mstore(add(output, 32), mload(add(result, 64))) + } + + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ModExp.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ModExp.sol new file mode 100644 index 00000000..3332b7b5 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/ModExp.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// example of input: +// 0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001aabbcc +contract ModExp { + function callModExp(bytes memory input) + public + view + returns (bytes memory) + { + uint256 callDataSize = input.length; + bytes memory output = new bytes(32); // Allocate memory for the output + + assembly { + let callDataOffset := add(input, 0x20) // Move pointer past length prefix to actual input + let returnAtOffset := add(output, 0x20) // Move pointer past length prefix to store output + + let success := staticcall( + gas(), + 0x05, // MODEXP address + callDataOffset, + callDataSize, + returnAtOffset, + 0 // returnAtCapacity + ) + } + return output; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/MulmodExecutor.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/MulmodExecutor.sol new file mode 100644 index 00000000..c9e0b5a0 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/MulmodExecutor.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract MulmodExecutor { + + // This function executes a loop with a high number of mulmod operations. + // The input parameter `iterations` controls how many times the loop runs. + function executeMulmod(uint256 iterations) public pure returns (uint256) { + uint256 result = 1; // Start with 1 to avoid multiplying by zero + uint256 result2 = 1; + uint256 result3 = 1; + uint256 result4 = 1; + uint256 result5 = 1; + for (uint256 i = 1; i <= iterations; i++) { + // Perform the mulmod operation + result = mulmod(result, i, 2**255); // MULMOD opcode + result2 = mulmod(result, i, 2**254); + result3 = mulmod(result, i, 2**253); + result4 = mulmod(result, i, 2**252); + result5 = mulmod(result, i, 2**251); + } + + return result; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/RevertExample.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/RevertExample.sol new file mode 100644 index 00000000..28b97678 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/RevertExample.sol @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +pragma solidity >=0.7.0 <0.9.0; + +contract RevertExample { + uint256 public value; + + function setValue(uint256 _newValue) public { + require(_newValue != 0, "Value cannot be zero"); + value = _newValue; + } + + function forceRevert() public pure { + revert("This function always reverts"); + } + + function conditionalRevert(uint256 _input) public pure returns (uint256) { + if (_input < 10) { + revert("Input must be 10 or greater"); + } + return _input * 2; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/SimpleStorage.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/SimpleStorage.sol new file mode 100644 index 00000000..05ed3b1b --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/SimpleStorage.sol @@ -0,0 +1,28 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +pragma solidity >=0.7.0 <0.9.0; + +contract SimpleStorage { + string data; + + function set(string memory value) public { + require(bytes(value).length != 0); + data = value; + } + + function get() public view returns (string memory) { + return data; + } +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/Context.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/Context.sol new file mode 100644 index 00000000..8e410bda --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/Context.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.4) (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/ERC20.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/ERC20.sol new file mode 100644 index 00000000..815341e3 --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/ERC20.sol @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import "./IERC20.sol"; +import "./IERC20Metadata.sol"; +import "./Context.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20.sol new file mode 100644 index 00000000..6d5b4e9f --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20Metadata.sol b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20Metadata.sol new file mode 100644 index 00000000..982bc39e --- /dev/null +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/solidity/libs/ERC20/IERC20Metadata.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.0; + +import "./IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} diff --git a/besu-plugins/linea-sequencer/build.gradle b/besu-plugins/linea-sequencer/build.gradle new file mode 100644 index 00000000..1587a114 --- /dev/null +++ b/besu-plugins/linea-sequencer/build.gradle @@ -0,0 +1,42 @@ +import com.github.jk1.license.filter.LicenseBundleNormalizer + +buildscript { + ext { + distributionIdentifier = "linea-sequencer" + releaseVersion = "v2.1-rc16.2.4" + distributionBaseUrl = "https://github.com/Consensys/linea-besu-upstream/releases/download/" + besuIdentifier = "besu-${libs.versions.besu.get()}" + besuFilename = "${besuIdentifier}.tar.gz" + besuUrl = "${distributionBaseUrl}${libs.versions.besu.get()}/${besuFilename}" + besuPluginsIdentifier = "${distributionIdentifier}-${version}" + besuPluginDir = File.createTempDir("plugins") + } +} + +plugins { + id 'java-library' + alias(libs.plugins.dependencyLicenseReport) +} + +licenseReport { + // This is for the allowed-licenses-file in checkLicense Task + // Accepts File, URL or String path to local or remote file + allowedLicensesFile = project.file("gradle/allowed-licenses.json") + + excludes = [] + + // If set to true, then all boms will be excluded from the report + excludeBoms = true + + filters = [ + new LicenseBundleNormalizer(bundlePath: project.file("gradle/license-normalizer-bundle.json")) + ] +} + +build { + dependsOn checkLicense +} + +jar { + enabled = false +} diff --git a/besu-plugins/linea-sequencer/docs/plugin-release.md b/besu-plugins/linea-sequencer/docs/plugin-release.md new file mode 100644 index 00000000..886fed28 --- /dev/null +++ b/besu-plugins/linea-sequencer/docs/plugin-release.md @@ -0,0 +1,9 @@ +## Plugin release process + +Here are the steps for releasing a new version of the plugins: + 1. Create a tag with the release version number in the format vX.Y.Z (e.g., v0.2.0 creates a release version 0.2.0). + 2. Push the tag to the repository. + 3. GitHub Actions will automatically create a draft release for the release tag. + 4. Once the release workflow completes, update the release notes, uncheck "Draft", and publish the release. + +Note: Release tags (of the form v*) are protected and can only be pushed by organization and/or repository owners. diff --git a/besu-plugins/linea-sequencer/docs/plugins.md b/besu-plugins/linea-sequencer/docs/plugins.md new file mode 100644 index 00000000..e74e85a3 --- /dev/null +++ b/besu-plugins/linea-sequencer/docs/plugins.md @@ -0,0 +1,145 @@ +# Linea plugins + +## Shared components + +### Profitability calculator +The profitability calculator is a shared component, that is used to check if a tx is profitable. +It's applied, with different configuration to: +1. `linea_estimateGas` endpoint +2. Tx validation for the txpool (if tx profitability check is enabled) +3. Tx selection during block creation + +#### CLI options + +| Command Line Argument | Default Value | +|-------------------------------------------------------|---------------| +| `--plugin-linea-fixed-gas-cost-wei` | 0 | +| `--plugin-linea-variable-gas-cost-wei` | 1_000_000_000 | +| `--plugin-linea-extra-data-pricing-enabled` | false | +| `--plugin-linea-min-margin` | 1.0 | +| `--plugin-linea-estimate-gas-min-margin` | 1.0 | +| `--plugin-linea-tx-pool-min-margin` | 0.5 | +| `--plugin-linea-extra-data-set-min-gas-price-enabled` | true | + + +### Module line count validator +The Module line count validator is a shared component, that is used to check if a tx exceeds any of the configured line count limits. +It is used in: +1. `linea_estimateGas` endpoint +2. Tx validation for the txpool (if tx simulation is enabled) +3. Tx selection during block creation + +#### CLI options + +| Command Line Argument | Default Value | +|-------------------------------------------------------|----------------------| +| `--plugin-linea-module-limit-file-path` | moduleLimitFile.toml | +| `--plugin-linea-over-line-count-limit-cache-size` | 10_000 | + + +### L1<>L2 bridge + +These values are just passed to the ZkTracer + +#### CLI Options + +| Command Line Argument | Default Value | +|----------------------------------------|---------------| +| `--plugin-linea-l1l2-bridge-contract` | | +| `--plugin-linea-l1l2-bridge-topic` | | + + +## Sequencer + +### Transaction selection - LineaTransactionSelectorPlugin +This plugin extends the standard transaction selection protocols employed by Besu for block creation. +It leverages the `TransactionSelectionService` to manage and customize the process of transaction selection. +This includes setting limits such as `TraceLineLimit`, `maxBlockGas`, and `maxCallData`, and check the profitability +of a transaction. +The selectors are in the package `net.consensys.linea.sequencer.txselection.selectors`. + +#### CLI options + +| Command Line Argument | Default Value | +|--------------------------------------------------------|----------------------| +| `--plugin-linea-max-block-calldata-size` | 70000 | +| `--plugin-linea-module-limit-file-path` | moduleLimitFile.toml | +| `--plugin-linea-over-line-count-limit-cache-size` | 10_000 | +| `--plugin-linea-max-block-gas` | 30_000_000L | +| `--plugin-linea-unprofitable-cache-size` | 100_000 | +| `--plugin-linea-unprofitable-retry-limit` | 10 | + + +### Transaction validation - LineaTransactionPoolValidatorPlugin + +This plugin extends the default transaction validation rules for adding transactions to the +transaction pool. It leverages the `PluginTransactionValidatorService` to manage and customize the +process of transaction validation. +This includes setting limits such as `TraceLineLimit`, `maxTxGasLimit`, and `maxTxCallData`, and checking the profitability +of a transaction. +The validators are in the package `net.consensys.linea.sequencer.txpoolvalidation.validators`. + +#### CLI options + +| Command Line Argument | Default Value | +|----------------------------------------------------------|-------------------| +| `--plugin-linea-deny-list-path` | lineaDenyList.txt | +| `--plugin-linea-max-tx-gas-limit` | 30_000_000 | +| `--plugin-linea-max-tx-calldata-size` | 60_000 | +| `--plugin-linea-tx-pool-simulation-check-api-enabled` | false | +| `--plugin-linea-tx-pool-simulation-check-p2p-enabled` | false | +| `--plugin-linea-tx-pool-profitability-check-api-enabled` | true | +| `--plugin-linea-tx-pool-profitability-check-p2p-enabled` | false | + +### Reporting rejected transactions +The transaction selection and validation plugins can report rejected transactions as JSON-RPC calls to an external +service. This feature can be enabled by setting the following CLI options: + +| Command Line Argument | Default Value | Expected Values | +|---------------------------------------|---------------|--------------------------------------------------------------| +| `--plugin-linea-rejected-tx-endpoint` | `null` | A valid URL e.g. `http://localhost:9363` to enable reporting | +| `--plugin-linea-node-type` | `null` | One of `SEQUENCER`, `RPC`, `P2P` | + +## RPC methods + +### Linea Estimate Gas +#### `linea_estimateGas` + +This endpoint simulates a transaction, including line count limit validation, and returns the estimated gas used +(as the standard `eth_estimateGas` with `strict=true`) plus the estimated gas price to be used when submitting the tx. + +#### Parameters +same as `eth_estimateGas` + +#### Result +```json +{ + "jsonrpc": "2.0", + "id": 53, + "result": { + "gasLimit": "0x5208", + "baseFeePerGas": "0x7", + "priorityFeePerGas": "0x123456" + } +} +``` + +### Linea Set Extra Data +#### `linea_setExtraData` + +This endpoint is used to configure the extra data based pricing, and it only makes sense to call it on the sequencer. +Internally it sets runtime pricing configuration and then calls, via the in-process RPC service, `miner_setExtraData` +and `miner_setMinGasPrice` to update internal Besu configuration, and add the extra data pricing to the future built blocks. + +#### Parameters +same as `miner_setExtraData` with the added constraint that the number of bytes must be 32 + +#### Result +```json +{ + "jsonrpc": "2.0", + "id": 53, + "result": "true" +} +``` + diff --git a/besu-plugins/linea-sequencer/docs/quickstart.md b/besu-plugins/linea-sequencer/docs/quickstart.md new file mode 100644 index 00000000..12e73d3a --- /dev/null +++ b/besu-plugins/linea-sequencer/docs/quickstart.md @@ -0,0 +1,83 @@ +## Quickstart: running [Linea Besu](https://github.com/Consensys/linea-besu) with plugins + +- compile linea-plugins `gradlew installDist` +- copy jar file to besu runtime plugins/ directory (where you will run Besu from, not where you're building Besu) +- add `LINEA` to besu config to enable the plugin RPC methods + - rpc-http-api=\["ADMIN","ETH","NET","WEB3","LINEA"\] +- start besu (command line or from IDE) and you should see plugins registered at startup +- call the RPC endpoint eg: + +```shell + curl --location --request POST 'http://localhost:8545' --data-raw '{ + "jsonrpc": "2.0", + "method": "linea_estimateGas", + "params": [ + "from": "0x73b2e0E54510239E22cC936F0b4a6dE1acf0AbdE", + "to": "0xBb977B2EE8a111D788B3477D242078d0B837E72b", + "value": "0x123" + ], + "id": 1 + }' +``` + +## Development Setup + +### Install Java 21 + +### Native Lib Prerequisites + +Linux/MacOs +* Install the relevant CGo compiler for your platform +* Install the Go toolchain + +Windows +* Requirement [Docker Desktop WSL 2 backend on Windows](https://docs.docker.com/desktop/wsl/) + +On release native libs are built for all the supported platforms, +if you want to test this process locally run `./gradlew -PreleaseNativeLibs jar`, +jar is generated in `sequencer/build/libs`. + +### Run tests + +```shell +# Run all tests +./gradlew clean test + +# Run only acceptance tests +./gradlew clean acceptanceTests +``` + +## IntelliJ IDEA Setup + +### Enable Annotation Processing + +- Go to `Settings | Build, Execution, Deployment | Compiler | Annotation Processors` and tick the following + checkbox: + + ![idea_enable_annotation_processing_setting.png](images/idea_enable_annotation_processing_setting.png) + +______________________________________________________________________ + +NOTE + +> This setting is required to avoid IDE compilation errors because of the [Lombok](https://projectlombok.org/features/) +> library used for code generation of boilerplate Java code such as: +> +> - Getters/Setters (via [`@Getter/@Setter`](https://projectlombok.org/features/GetterSetter)) +> - Class log instances (via [`@Slf4j`](https://projectlombok.org/features/log)) +> - Builder classes (via [`@Builder`](https://projectlombok.org/features/Builder)) +> - Constructors ( +> via [`@NoArgsConstructor/@RequiredArgsConstructor/@AllArgsConstructor`](https://projectlombok.org/features/constructor)) +> - etc. +> +> Learn more about how Java annotation processing +> works [here](https://www.baeldung.com/java-annotation-processing-builder). + +______________________________________________________________________ + +### Install Optional Plugins + +- Install [Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle) plugin to re-format through + the IDE according to spotless configuration. + +Learn more [about available plugins](plugins.md). \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/allowed-licenses.json b/besu-plugins/linea-sequencer/gradle/allowed-licenses.json new file mode 100644 index 00000000..febfaaf9 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/allowed-licenses.json @@ -0,0 +1,83 @@ +{ + "allowedLicenses": [ + { + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, + { + "moduleLicense": "BSD Zero Clause License", + "moduleLicenseUrl": "https://opensource.org/licenses/0BSD" + }, + { + "moduleLicense" : "The 2-Clause BSD License", + "moduleLicenseUrl" : "https://opensource.org/licenses/BSD-2-Clause" + }, + { + "moduleLicense": "The 3-Clause BSD License", + "moduleLicenseUrl": "https://opensource.org/licenses/BSD-3-Clause" + }, + { + "moduleLicense": "Bouncy Castle Licence", + "moduleLicenseUrl": "https://www.bouncycastle.org/licence.html" + }, + { + "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0", + "moduleLicenseUrl": "https://oss.oracle.com/licenses/CDDL" + }, + { + "moduleLicense" : "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1", + "moduleLicenseUrl" : "https://oss.oracle.com/licenses/CDDL-1.1" + }, + { + "moduleLicense": "Eclipse Distribution License - v 1.0", + "moduleLicenseUrl": "http://www.eclipse.org/legal/epl-v10.html" + }, + { + "moduleLicense": "Eclipse Public License - v 1.0", + "moduleLicenseUrl": "http://www.eclipse.org/legal/epl-v10.html" + }, + { + "moduleLicense": "Eclipse Public License - v 2.0", + "moduleLicenseUrl": "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt" + }, + { + "moduleLicense": "MIT License", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleLicense": "CC0-1.0", + "moduleLicenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + { + "moduleLicense": "Public-Domain" + }, + { + "moduleLicense": "Unicode/ICU License", + "moduleLicenseUrl": "https://raw.githubusercontent.com/unicode-org/icu/main/icu4c/LICENSE" + }, + { + "moduleLicense": "Creative Commons Legal Code", + "moduleVersion": "1.0.3", + "moduleName": "org.reactivestreams:reactive-streams" + }, + { + "moduleLicense": "MIT-0", + "moduleVersion": "1.0.4", + "moduleName": "org.reactivestreams:reactive-streams" + }, + { + "moduleName": "org.jetbrains.kotlin:kotlin-stdlib-common", + "moduleVersion": "1.9.22" + }, + { + "moduleLicense": "CDDL-1.0", + "moduleVersion": "3.0.3", + "moduleName": "org.jupnp:org.jupnp" + }, + { + "moduleLicense": "CDDL-1.0", + "moduleVersion": "3.0.3", + "moduleName": "org.jupnp:org.jupnp.support" + } + ] +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/build-aliases.gradle b/besu-plugins/linea-sequencer/gradle/build-aliases.gradle new file mode 100644 index 00000000..62e93cee --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/build-aliases.gradle @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// Default tasks and build aliases +defaultTasks 'build', 'checkLicense', 'javadoc', 'artifacts' + +def buildAliases = [ + 'dev': [ + 'spotlessApply', + 'build', + 'checkLicenses' + ], + 'artifacts' : [ + 'jar', + 'distPlugin' + ] +] + +def expandedTaskList = [] +gradle.startParameter.taskNames.each { + expandedTaskList << (buildAliases[it] ? buildAliases[it] : it) +} +gradle.startParameter.taskNames = expandedTaskList.flatten() \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/common-dependencies.gradle b/besu-plugins/linea-sequencer/gradle/common-dependencies.gradle new file mode 100644 index 00000000..40860b82 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/common-dependencies.gradle @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +dependencies { + implementation 'org.slf4j:slf4j-api' + + testImplementation 'org.apache.commons:commons-lang3' + testImplementation 'com.google.guava:guava' + testImplementation 'org.assertj:assertj-core' + + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + + testImplementation 'org.wiremock:wiremock' + + testRuntimeOnly 'org.apache.logging.log4j:log4j-core' + testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/besu-plugins/linea-sequencer/gradle/dependency-management.gradle b/besu-plugins/linea-sequencer/gradle/dependency-management.gradle new file mode 100644 index 00000000..e0d13ba8 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/dependency-management.gradle @@ -0,0 +1,76 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +repositories { + maven { + url "https://artifacts.consensys.net/public/linea-besu/maven/" + content { + includeGroupByRegex('io\\.consensys\\..*') + includeGroupByRegex('org\\.hyperledger\\..*') + } + } + maven { + url 'https://hyperledger.jfrog.io/hyperledger/besu-maven' + content { includeGroupByRegex('org\\.hyperledger\\..*') } + } + maven { + url 'https://artifacts.consensys.net/public/maven/maven/' + content { + includeGroupByRegex('tech\\.pegasys(\\..*)?') + } + } + maven { + url "https://artifacts.consensys.net/public/linea-arithmetization/maven/" + content { + includeGroupByRegex('net\\.consensys\\.linea\\..*?') + } + } + maven { + url 'https://splunk.jfrog.io/splunk/ext-releases-local' + content { includeGroupByRegex('com\\.splunk\\..*') } + } + mavenCentral() + mavenLocal() +} + +apply plugin: 'io.spring.dependency-management' + +dependencyManagement { + applyMavenExclusions = false + generatedPomCustomization { + enabled = false + } + imports { + mavenBom "${besuArtifactGroup}:bom:${libs.versions.besu.get()}" + } + + dependencies { + dependencySet(group: "net.consensys.linea.zktracer", version: "${libs.versions.arithmetization.get()}") { + entry "arithmetization" + } + + dependency "com.google.code.gson:gson:${libs.versions.gson.get()}" + + dependency "io.tmio:tuweni-bytes:${libs.versions.tuweni.get()}" + dependency "io.tmio:tuweni-units:${libs.versions.tuweni.get()}" + dependency "io.tmio:tuweni-toml:${libs.versions.tuweni.get()}" + + // ToDo: remove when fixed in Besu, force version to avoid conflict with previous version + dependency "org.apache.logging.log4j:log4j-api:${libs.versions.log4j.get()}" + dependency "org.apache.logging.log4j:log4j-core:${libs.versions.log4j.get()}" + + dependency "org.wiremock:wiremock:${libs.versions.wiremock.get()}" + } +} diff --git a/besu-plugins/linea-sequencer/gradle/dist.gradle b/besu-plugins/linea-sequencer/gradle/dist.gradle new file mode 100644 index 00000000..919aac48 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/dist.gradle @@ -0,0 +1,139 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +def lineaSequencerProject = project(lineaSequencerProjectPath) + +tasks.register('sourcesJar', Jar) { + dependsOn classes + archiveClassifier = 'sources' + from sourceSets.main.allSource +} + +tasks.register('javadocJar', Jar) { + dependsOn javadoc + archiveClassifier = 'javadoc' + from javadoc.destinationDir +} + +def besuDistTar = new File(new File(buildDir, "downloads"), lineaSequencerProject.besuFilename) +task downloadBesu { + outputs.file(besuDistTar) + doLast { + try { + download.run { + src lineaSequencerProject.besuUrl + dest besuDistTar + onlyIfModified true + } + } catch (Exception e) { + def localBesuDir = + project.hasProperty('useLocalBesuDir') + ? file("${findProperty('useLocalBesuDir')}".replaceFirst('^~', System.getProperty('user.home'))) + : new File(projectDir, "../../besu") + + def localBesuFile = new File("${localBesuDir.canonicalPath}/build/distributions/${lineaSequencerProject.besuFilename}") + + logger.warn("Could not download " + lineaSequencerProject.besuUrl + " trying local copy from " + localBesuFile + " as fallback") + if (!file(localBesuFile).exists()) { + throw new GradleException("Could not download Besu distribution from: " + lineaSequencerProject.besuUrl + + ", and could not find it locally at ${localBesuFile} either") + } + + copy { + from localBesuFile + into besuDistTar.parentFile + } + } + } +} + +task unTarBesu(type: Copy) { + dependsOn downloadBesu + + from tarTree(besuDistTar) + into besuDistTar.parentFile +} + +def besuLibDir = new File(besuDistTar.parentFile, lineaSequencerProject.besuIdentifier + '/lib') +def besuLibs = [] + +def excludeBesuProvidedDeps = { + if(besuLibs.isEmpty()) { + // Get all the dependencies that are provided by Besu + fileTree(dir: besuLibDir, include: '*.jar').visit { + FileVisitDetails details -> + besuLibs << details.file.name + } + } + // include the dependency in the jar only if it is not already provided by Besu + !besuLibs.any { artifactName -> + if(artifactName == it.name) { + return true + } + // exclude Besu group + if(it.toString().contains(besuArtifactGroup)) { + return true + } + // try ignoring the version + def libName = it.name =~ dependencyNamePattern() + def artName = artifactName =~ dependencyNamePattern() + libName[0][1] == artName[0][1] + } +} + +jar { + dependsOn unTarBesu + archiveBaseName = lineaSequencerProject.distributionIdentifier + version = calculateVersion() + + manifest { + attributes( + 'Specification-Title': 'arithmetization', + 'Specification-Version': "${libs.versions.arithmetization.get()}", + 'Implementation-Title': 'arithmetization', + 'Implementation-Version': "${libs.versions.arithmetization.get()}" + ) + } + + from { + configurations.runtimeClasspath.filter(excludeBesuProvidedDeps).collect { + it.isDirectory() ? it : zipTree(it) + } + } + + duplicatesStrategy('exclude') +} + +/** + * Create a distribution of the plugin, that only contains the plugin jar and the + * dependencies that are not provided by Besu itself, so that is can be simply + * extracted in the Besu plugins dir. + */ +tasks.register('distPlugin', Zip) { + dependsOn installDist + + archiveBaseName = lineaSequencerProject.distributionIdentifier + + from("${buildDir}/libs/${lineaSequencerProject.distributionIdentifier}-${calculateVersion()}.jar") + from { + configurations.runtimeClasspath.filter( + excludeBesuProvidedDeps) + + } +} + +static def dependencyNamePattern() { + /(.*)(\-.*?)\.jar/ +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/java.gradle b/besu-plugins/linea-sequencer/gradle/java.gradle new file mode 100644 index 00000000..758dbed5 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/java.gradle @@ -0,0 +1,57 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'java-library' + +if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + throw new GradleException("Java 21 or later is required to build Besu.\n" + + " Detected version ${JavaVersion.current()}") +} + +sourceCompatibility = 21 +targetCompatibility = 21 + +tasks.withType(JavaCompile) { + options.compilerArgs += [ + '-Xlint:unchecked', + '-Xlint:cast', + '-Xlint:rawtypes', + '-Xlint:overloads', + '-Xlint:divzero', + '-Xlint:finally', + '-Xlint:static', + '-Werror', + ] + + options.encoding = 'UTF-8' +} + +def lineaSequencerProject = project(lineaSequencerProjectPath) + +// Takes the version, and if -SNAPSHOT is part of it replaces SNAPSHOT +// with the git commit version. +ext.calculateVersion = { -> + String version = lineaSequencerProject.releaseVersion + if (version.endsWith("-SNAPSHOT")) { + version = version.replace("-SNAPSHOT", "-dev-${getCheckedOutGitCommitHash()}") + } + + return version +} + +static def getCheckedOutGitCommitHash() { + def hashLength = 8 + "git rev-parse HEAD".execute().text.take(hashLength) +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/license-normalizer-bundle.json b/besu-plugins/linea-sequencer/gradle/license-normalizer-bundle.json new file mode 100644 index 00000000..8c0af450 --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/license-normalizer-bundle.json @@ -0,0 +1,106 @@ +/** + * Copyright 2018 Evgeny Naumenko + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +{ + "bundles" : [ + { "bundleName" : "Apache-1.1", "licenseName" : "Apache Software License, Version 1.1", "licenseUrl" : "https://www.apache.org/licenses/LICENSE-1.1" }, + { "bundleName" : "Apache-2.0", "licenseName" : "Apache License, Version 2.0", "licenseUrl" : "https://www.apache.org/licenses/LICENSE-2.0" }, + { "bundleName" : "0BSD", "licenseName" : "BSD Zero Clause License", "licenseUrl" : "https://opensource.org/licenses/0BSD" }, + { "bundleName" : "BSD-2-Clause", "licenseName" : "The 2-Clause BSD License", "licenseUrl" : "https://opensource.org/licenses/BSD-2-Clause" }, + { "bundleName" : "BSD-3-Clause", "licenseName" : "The 3-Clause BSD License", "licenseUrl" : "https://opensource.org/licenses/BSD-3-Clause" }, + { "bundleName" : "CC0-1.0", "licenseName" : "Creative Commons Legal Code", "licenseUrl" : "https://creativecommons.org/publicdomain/zero/1.0/legalcode" }, + { "bundleName" : "CDDL-1.0", "licenseName" : "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0", "licenseUrl" : "https://oss.oracle.com/licenses/CDDL" }, + { "bundleName" : "CDDL-1.1", "licenseName" : "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1", "licenseUrl" : "https://oss.oracle.com/licenses/CDDL-1.1" }, + { "bundleName" : "CPL-1.0", "licenseName" : "Common Public License - v 1.0", "licenseUrl" : "https://www.eclipse.org/legal/cpl-v10.html" }, + { "bundleName" : "EPL-1.0", "licenseName" : "Eclipse Public License - v 1.0", "licenseUrl" : "http://www.eclipse.org/legal/epl-v10.html" }, + { "bundleName" : "EPL-2.0", "licenseName" : "Eclipse Public License - v 2.0", "licenseUrl" : "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt" }, + { "bundleName" : "EDL-1.0", "licenseName" : "Eclipse Distribution License - v 1.0", "licenseUrl" : "https://www.eclipse.org/org/documents/edl-v10.html" }, + { "bundleName" : "GPL-1.0", "licenseName" : "GNU GENERAL PUBLIC LICENSE, Version 1", "licenseUrl" : "https://www.gnu.org/licenses/gpl-1.0" }, + { "bundleName" : "GPL-2.0-only", "licenseName" : "GNU GENERAL PUBLIC LICENSE, Version 2", "licenseUrl" : "https://www.gnu.org/licenses/gpl-2.0" }, + { "bundleName" : "GPL-3.0-only", "licenseName" : "GNU GENERAL PUBLIC LICENSE, Version 3", "licenseUrl" : "https://www.gnu.org/licenses/gpl-3.0" }, + { "bundleName" : "GPL-2.0 WITH Classpath-exception-2.0", "licenseName" : "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception", "licenseUrl" : "https://openjdk.java.net/legal/gplv2+ce.html" }, + { "bundleName" : "LGPL-2.1-only", "licenseName" : "GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1", "licenseUrl" : "https://www.gnu.org/licenses/lgpl-2.1" }, + { "bundleName" : "LGPL-3.0-only", "licenseName" : "GNU LESSER GENERAL PUBLIC LICENSE, Version 3", "licenseUrl" : "https://www.gnu.org/licenses/lgpl-3.0" }, + { "bundleName" : "MIT", "licenseName" : "MIT License", "licenseUrl" : "https://opensource.org/licenses/MIT" }, + { "bundleName" : "MPL-1.1", "licenseName" : "Mozilla Public License Version 1.1", "licenseUrl" : "https://www.mozilla.org/en-US/MPL/1.1" }, + { "bundleName" : "MPL-2.0", "licenseName" : "Mozilla Public License, Version 2.0", "licenseUrl" : "https://www.mozilla.org/en-US/MPL/2.0" }, + { "bundleName" : "Public-Domain", "licenseName" : "PUBLIC DOMAIN", "licenseUrl" : "" } + ], + "transformationRules" : [ + { "bundleName" : "0BSD", "licenseNamePattern" : "BSD Zero Clause License" }, + { "bundleName" : "0BSD", "licenseNamePattern" : "BSD$" }, + { "bundleName" : "0BSD", "licenseNamePattern" : "BSD( |-)clause.*" }, + { "bundleName" : "0BSD", "licenseNamePattern" : "(The )?BSD( |-)(l|L)icen(s|c)e.*" }, + { "bundleName" : "Apache-2.0", "licenseNamePattern" : ".*The Apache Software License, Version 2\\.0.*" }, + { "bundleName" : "Apache-2.0", "licenseNamePattern" : ".*?Apache( |-|_)2.*" }, + { "bundleName" : "Apache-2.0", "licenseNamePattern" : "ASL 2\\.0" }, + { "bundleName" : "Apache-2.0", "licenseNamePattern" : ".*Apache License,?( Version)? 2.*" }, + { "bundleName" : "Apache-2.0", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/Apache-2\\.0.*" }, + { "bundleName" : "Apache-2.0", "licenseUrlPattern" : ".*www\\.apache\\.org/licenses/LICENSE-2\\.0.*" }, + { "bundleName" : "Apache-2.0", "licenseFileContentPattern" : ".*Apache License,?( Version)? 2.*" }, + { "bundleName" : "Apache-1.1", "licenseFileContentPattern" : ".*Apache Software License, Version 1\\.1.*" }, + { "bundleName" : "LGPL-2.1-only", "licenseUrlPattern" : ".*www\\.gnu\\.org/licenses/old-licenses/lgpl-2\\.1\\.html" }, + { "bundleName" : "CC0-1.0", "licenseNamePattern" : "CC0(( |-)1(\\.0)?)?" }, + { "bundleName" : "CC0-1.0", "licenseUrlPattern" : ".*(www\\.)?creativecommons\\.org/publicdomain/zero/1\\.0/" }, + { "bundleName" : "CDDL-1.0", "licenseFileContentPattern" : ".*CDDL.*1\\.0" }, + { "bundleName" : "CDDL-1.0", "licenseUrlPattern" : ".*CDDL.*.?1\\.0" }, + { "bundleName" : "CDDL-1.1", "licenseUrlPattern" : ".*CDDL.*.?1\\.1" }, + { "bundleName" : "CDDL-1.0", "licenseNamePattern" : "Common Development and Distribution License( \\(CDDL\\),?)? (version )?(.?\\s?)?1\\.0" }, + { "bundleName" : "CDDL-1.1", "licenseNamePattern" : "Common Development and Distribution License( \\(CDDL\\),?)? (version )?(.?\\s?)?1\\.1" }, + { "bundleName" : "BSD-3-Clause", "licenseNamePattern" : ".*BSD( |-)3-clause.*" }, + { "bundleName" : "BSD-3-Clause", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/BSD-3-Clause" }, + { "bundleName" : "BSD-3-Clause", "licenseNamePattern" : ".*?(The )New BSD License.*" }, + { "bundleName" : "BSD-3-Clause", "licenseNamePattern" : ".*?Modified BSD License.*" }, + { "bundleName" : "BSD-2-Clause", "licenseNamePattern" : "BSD( |-)2-clause.*" }, + { "bundleName" : "BSD-2-Clause", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/BSD-2-Clause" }, + { "bundleName" : "BSD-2-Clause", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/bsd-license(\\.php)?" }, + { "bundleName" : "CDDL-1.0", "licenseNamePattern" : "Common Development and Distribution( License)?" }, + { "bundleName" : "CDDL-1.0", "licenseNamePattern" : "CDDL 1(\\.0)" }, + { "bundleName" : "CDDL-1.1", "licenseNamePattern" : "CDDL 1\\.1" }, + { "bundleName" : "CDDL-1.1", "licenseUrlPattern" : ".*(www\\.).opensource\\.org/licenses/CDDL-1\\.0" }, + { "bundleName" : "EPL-1.0", "licenseNamePattern" : "Eclipse Publish License.*(v|version)\\.?\\s?1(\\.?0)?" }, + { "bundleName" : "EPL-1.0", "licenseNamePattern" : "Eclipse Public License.*(v|version)\\.?\\s?1(\\.?0)?" }, + { "bundleName" : "EPL-2.0", "licenseNamePattern" : "Eclipse Public License.*(v|version)\\.?\\s?2(\\.?0)?" }, + { "bundleName" : "EPL-2.0", "licenseUrlPattern" : ".*(www\\.).opensource\\.org/licenses/EPL-2\\.0" }, + { "bundleName" : "EPL-2.0", "licenseUrlPattern" : ".*http.?://www\\.eclipse\\.org/legal/epl-.?2\\.?0.*" }, + { "bundleName" : "EPL-2.0", "licenseUrlPattern" : ".*http.?://www\\.eclipse\\.org/org.*/epl-.?2\\.?0.*" }, + { "bundleName" : "EPL-2.0", "licenseUrlPattern" : ".*http.?://projects\\.eclipse\\.org/.*/epl-.?2\\.?0.*" }, + { "bundleName" : "EDL-1.0", "licenseNamePattern" : "Eclipse Distribution License.*(v|version)\\.?\\s?1(\\.0)?" }, + { "bundleName" : "EDL-1.0", "licenseNamePattern" : "Eclipse Distribution License \\(New BSD License\\)" }, + { "bundleName" : "EDL-1.0", "licenseUrlPattern" : ".*http.?://(www\\.)?eclipse\\.org/org.*/edl-.?1\\.?0.*" }, + { "bundleName" : "GPL-2.0-only", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/GPL-2\\.0" }, + { "bundleName" : "GPL-2.0 WITH Classpath-exception-2.0", "licenseNamePattern" : "GNU General Public License, version 2.*classpath exception" }, + { "bundleName" : "GPL-2.0 WITH Classpath-exception-2.0", "licenseNamePattern" : "GNU General Public License, version 2.*cp?e" }, + { "bundleName" : "GPL-2.0 WITH Classpath-exception-2.0", "licenseNamePattern" : "GNU General Public License, version 2.*, with the classpath exception" }, + { "bundleName" : "GPL-2.0 WITH Classpath-exception-2.0", "licenseNamePattern" : "GPL2 w/ CPE" }, + { "bundleName" : "GPL-3.0-only", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/GPL-3\\.0" }, + { "bundleName" : "LGPL-2.1-only", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/LGPL-2\\.1" }, + { "bundleName" : "LGPL-2.1-only", "licenseUrlPattern" : ".*(www\\.)?gnu\\.org/licenses(/old-licenses)?/lgpl-2\\.1(\\.(html|txt))?" }, + { "bundleName" : "LGPL-2.1-only", "licenseNamePattern" : "LGPL 2\\.1" }, + { "bundleName" : "LGPL-2.1-only", "licenseNamePattern" : "LGPL.*(v|version)\\.?\\s?2\\.1" }, + { "bundleName" : "LGPL-2.1-only", "licenseUrlPattern" : ".*(www\\.)?repository.jboss.org/licenses/lgpl-2.1\\.txt" }, + { "bundleName" : "LGPL-3.0-only", "licenseUrlPattern" : ".*(www\\.).opensource\\.org/licenses/LGPL-3\\.0" }, + { "bundleName" : "LGPL-3.0-only", "licenseNamePattern" : "lgplv?3" }, + { "bundleName" : "LGPL-3.0-only", "licenseUrlPattern" : ".*(www\\.)?gnu\\.org/licenses(/old-licenses)?/lgpl(-3)?(\\.(html|txt))?" }, + { "bundleName" : "MIT", "licenseNamePattern" : "(The\\s)?MIT(\\slicen(c|s)e)?(\\s\\(MIT\\))?" }, + { "bundleName" : "MIT", "licenseUrlPattern" : ".*(www\\.)?opensource\\.org/licenses/MIT(\\.php)?" }, + { "bundleName" : "MPL-1.1", "licenseNamePattern" : "MPL 1\\.1" }, + { "bundleName" : "MPL-2.0", "licenseUrlPattern" : ".*(www\\.).opensource\\.org/licenses/MPL-2\\.0" }, + { "bundleName" : "Public-Domain", "licenseNamePattern" : "((public)\\s(domain)).*", "transformUrl" : false }, + { "bundleName" : "Public-Domain", "licenseFileContentPattern" : ".*(Creative)\\s(Commons).*", "transformUrl" : false }, + { "bundleName" : "Public-Domain", "licenseFileContentPattern" : ".*((Public)\\s(Domain)).*", "transformUrl" : false } + ] +} \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/gradle/lint.gradle b/besu-plugins/linea-sequencer/gradle/lint.gradle new file mode 100644 index 00000000..e124413f --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/lint.gradle @@ -0,0 +1,104 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import groovy.io.FileType +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.tooling.BuildException + +class CheckSpdxHeader extends DefaultTask { + private String rootPath + private String spdxHeader + private String filesRegex + private String excludeRegex + + @Input + String getRootPath() { + return rootPath + } + + void setRootPath(final String rootPath) { + this.rootPath = rootPath + } + + @Input + String getSpdxHeader() { + return spdxHeader + } + + void setSpdxHeader(final String spdxHeader) { + this.spdxHeader = spdxHeader + } + + @Input + String getFilesRegex() { + return filesRegex + } + + void setFilesRegex(final String filesRegex) { + this.filesRegex = filesRegex + } + + @Input + String getExcludeRegex() { + return excludeRegex + } + + void setExcludeRegex(final String excludeRegex) { + this.excludeRegex = excludeRegex + } + + @TaskAction + void checkHeaders() { + def filesWithoutHeader = [] + + new File(rootPath).traverse( + type: FileType.FILES, + nameFilter: ~/${filesRegex}/, + excludeFilter: ~/${excludeRegex}/ + ) { + f -> + if (!f.getText().contains(spdxHeader)) { + filesWithoutHeader.add(f) + } + } + + if (!filesWithoutHeader.isEmpty()) { + throw new BuildException("Files without headers: " + filesWithoutHeader.join('\n'), null) + } + } +} + +javadoc { + options.addStringOption('Xdoclint:all', '-quiet') + options.addStringOption('Xwerror', '-html5') + options.encoding = 'UTF-8' +} + +task checkSpdxHeader(type: CheckSpdxHeader) { + rootPath = "${projectDir}" + spdxHeader = "* SPDX-License-Identifier: Apache-2.0" + filesRegex = "(.*.java)|(.*.groovy)" + excludeRegex = [ + "(.*/.gradle/.*)", + "(.*/.idea/.*)", + "(.*/out/.*)", + "(.*/build/.*)", + "(.*/src/[^/]+/generated/.*)", + ].join("|") +} + +tasks.check.dependsOn(checkSpdxHeader) diff --git a/besu-plugins/linea-sequencer/gradle/spotless.java.license b/besu-plugins/linea-sequencer/gradle/spotless.java.license new file mode 100644 index 00000000..d8be39ff --- /dev/null +++ b/besu-plugins/linea-sequencer/gradle/spotless.java.license @@ -0,0 +1,14 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ \ No newline at end of file diff --git a/besu-plugins/linea-sequencer/images/idea_enable_annotation_processing_setting.png b/besu-plugins/linea-sequencer/images/idea_enable_annotation_processing_setting.png new file mode 100644 index 00000000..4b9d618d Binary files /dev/null and b/besu-plugins/linea-sequencer/images/idea_enable_annotation_processing_setting.png differ diff --git a/besu-plugins/linea-sequencer/sequencer/build.gradle b/besu-plugins/linea-sequencer/sequencer/build.gradle new file mode 100644 index 00000000..4fb9c9f2 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/build.gradle @@ -0,0 +1,73 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'java' + id 'java-library-distribution' + alias(libs.plugins.lombok) + alias(libs.plugins.gradleVersions) + alias(libs.plugins.dependencyManagement) + alias(libs.plugins.download) +} + +group = 'net.consensys.linea.besu.plugin' + +def lineaSequencerProject = project(lineaSequencerProjectPath) +apply from: lineaSequencerProject.file("gradle/java.gradle") +apply from: lineaSequencerProject.file("gradle/dependency-management.gradle") +apply from: lineaSequencerProject.file('gradle/common-dependencies.gradle') +apply from: lineaSequencerProject.file("gradle/build-aliases.gradle") +apply from: lineaSequencerProject.file("gradle/lint.gradle") + +dependencies { + // annotationProcessor generates the file META-INF/services/org.hyperledger.besu.plugin.BesuPlugin + annotationProcessor 'com.google.auto.service:auto-service' + compileOnly 'com.google.auto.service:auto-service' + + api "build.linea:blob-compressor:${libs.versions.blobCompressor.get()}" + api "build.linea.internal:kotlin:${libs.versions.lineaKotlin.get()}" + + implementation "${besuArtifactGroup}:besu-datatypes" + implementation "${besuArtifactGroup}:evm" + implementation "${besuArtifactGroup}:plugin-api" + implementation "${besuArtifactGroup}.internal:algorithms" + implementation "${besuArtifactGroup}.internal:api" + implementation "${besuArtifactGroup}.internal:core" + implementation "${besuArtifactGroup}.internal:eth" + implementation "${besuArtifactGroup}.internal:rlp" + + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + + implementation 'com.github.ben-manes.caffeine:caffeine' + + implementation 'com.google.code.gson:gson' + + implementation "io.tmio:tuweni-bytes" + implementation "io.tmio:tuweni-units" + implementation "io.tmio:tuweni-toml" + + implementation 'info.picocli:picocli' + + implementation 'net.consensys.linea.zktracer:arithmetization' + + implementation 'org.hibernate.validator:hibernate-validator' + + testImplementation "${besuArtifactGroup}.internal:besu" + testImplementation group: "${besuArtifactGroup}.internal", name: "core", classifier: "test-support" + + testImplementation 'org.awaitility:awaitility' +} + +apply from: lineaSequencerProject.file("gradle/dist.gradle") diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java new file mode 100644 index 00000000..4fbd749a --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java @@ -0,0 +1,72 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea; + +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; + +/** + * Linea plugins extending this class will halt startup of Besu in case of exception during + * registration. + * + *

If that's NOT desired, the plugin should implement {@link BesuPlugin} directly. + */ +@Slf4j +public abstract class AbstractLineaRequiredPlugin extends AbstractLineaSharedPrivateOptionsPlugin { + + @Override + public void register(final ServiceManager serviceManager) { + super.register(serviceManager); + try { + log.info("Registering Linea plugin {}", this.getClass().getName()); + + doRegister(serviceManager); + + } catch (Exception e) { + log.error("Halting Besu startup: exception in plugin registration: ", e); + e.printStackTrace(); + // System.exit will cause besu to exit + System.exit(1); + } + } + + /** + * Linea plugins need to implement this method. Called by {@link BesuPlugin} register method + * + * @param serviceManager the ServiceManager to be used. + */ + public abstract void doRegister(final ServiceManager serviceManager); + + @Override + public void start() { + super.start(); + try { + log.info("Starting Linea plugin {}", this.getClass().getName()); + + doStart(); + + } catch (Exception e) { + log.error("Halting Besu startup: exception in plugin startup: ", e); + e.printStackTrace(); + // System.exit will cause besu to exit + System.exit(1); + } + } + + /** Linea plugins can implement this method. Called by {@link BesuPlugin} start method */ + public abstract void doStart(); +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java new file mode 100644 index 00000000..9550a7f5 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java @@ -0,0 +1,240 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundlePoolService; +import net.consensys.linea.bundles.LineaLimitedBundlePool; +import net.consensys.linea.config.LineaBundleCliOptions; +import net.consensys.linea.config.LineaBundleConfiguration; +import net.consensys.linea.config.LineaProfitabilityCliOptions; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaRejectedTxReportingCliOptions; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import net.consensys.linea.config.LineaRpcCliOptions; +import net.consensys.linea.config.LineaRpcConfiguration; +import net.consensys.linea.config.LineaTracerCliOptions; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorCliOptions; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorCliOptions; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.plugins.AbstractLineaSharedOptionsPlugin; +import net.consensys.linea.plugins.LineaOptionsPluginConfiguration; +import net.consensys.linea.utils.Compressor; +import org.hyperledger.besu.plugin.ServiceManager; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.RpcEndpointService; +import org.hyperledger.besu.plugin.services.metrics.MetricCategoryRegistry; + +/** + * This abstract class is used as superclass for all the plugins that share one or more + * configuration options, services and common initializations. + * + *

Configuration options that are exclusive of a single plugin, are not required to be added + * here, but they could stay in the class that implement a plugin, but in case that configuration + * becomes to be used by multiple plugins, then to avoid code duplications and possible different + * management of the options, it is better to move the configuration here so all plugins will + * automatically see it. + * + *

Same for services and other initialization tasks, that are shared by more than one plugin, + * like registration of metrics categories or check to perform once at startup + */ +@Slf4j +public abstract class AbstractLineaSharedPrivateOptionsPlugin + extends AbstractLineaSharedOptionsPlugin { + protected static BesuConfiguration besuConfiguration; + protected static BlockchainService blockchainService; + protected static MetricsSystem metricsSystem; + protected static BesuEvents besuEvents; + protected static BundlePoolService bundlePoolService; + protected static MetricCategoryRegistry metricCategoryRegistry; + protected static RpcEndpointService rpcEndpointService; + + private static final AtomicBoolean sharedRegisterTasksDone = new AtomicBoolean(false); + private static final AtomicBoolean sharedStartTasksDone = new AtomicBoolean(false); + + static { + // force the initialization of the gnark compress native library to fail fast in case of issues + Compressor.instance.compressedSize(new byte[1024]); + } + + private ServiceManager serviceManager; + + @Override + public Map getLineaPluginConfigMap() { + final var configMap = new HashMap<>(super.getLineaPluginConfigMap()); + + configMap.put( + LineaTransactionSelectorCliOptions.CONFIG_KEY, + LineaTransactionSelectorCliOptions.create().asPluginConfig()); + configMap.put( + LineaTransactionPoolValidatorCliOptions.CONFIG_KEY, + LineaTransactionPoolValidatorCliOptions.create().asPluginConfig()); + configMap.put(LineaRpcCliOptions.CONFIG_KEY, LineaRpcCliOptions.create().asPluginConfig()); + configMap.put( + LineaProfitabilityCliOptions.CONFIG_KEY, + LineaProfitabilityCliOptions.create().asPluginConfig()); + configMap.put( + LineaTracerCliOptions.CONFIG_KEY, LineaTracerCliOptions.create().asPluginConfig()); + configMap.put( + LineaRejectedTxReportingCliOptions.CONFIG_KEY, + LineaRejectedTxReportingCliOptions.create().asPluginConfig()); + configMap.put( + LineaBundleCliOptions.CONFIG_KEY, LineaBundleCliOptions.create().asPluginConfig()); + return configMap; + } + + public LineaTransactionSelectorConfiguration transactionSelectorConfiguration() { + return (LineaTransactionSelectorConfiguration) + getConfigurationByKey(LineaTransactionSelectorCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaTransactionPoolValidatorConfiguration transactionPoolValidatorConfiguration() { + return (LineaTransactionPoolValidatorConfiguration) + getConfigurationByKey(LineaTransactionPoolValidatorCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaRpcConfiguration lineaRpcConfiguration() { + return (LineaRpcConfiguration) + getConfigurationByKey(LineaRpcCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaProfitabilityConfiguration profitabilityConfiguration() { + return (LineaProfitabilityConfiguration) + getConfigurationByKey(LineaProfitabilityCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaTracerConfiguration tracerConfiguration() { + return (LineaTracerConfiguration) + getConfigurationByKey(LineaTracerCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaRejectedTxReportingConfiguration rejectedTxReportingConfiguration() { + return (LineaRejectedTxReportingConfiguration) + getConfigurationByKey(LineaRejectedTxReportingCliOptions.CONFIG_KEY).optionsConfig(); + } + + public LineaBundleConfiguration bundleConfiguration() { + return (LineaBundleConfiguration) + getConfigurationByKey(LineaBundleCliOptions.CONFIG_KEY).optionsConfig(); + } + + @Override + public synchronized void register(final ServiceManager serviceManager) { + super.register(serviceManager); + + this.serviceManager = serviceManager; + + if (sharedRegisterTasksDone.compareAndSet(false, true)) { + performSharedRegisterTasksOnce(serviceManager); + } + } + + protected static void performSharedRegisterTasksOnce(final ServiceManager serviceManager) { + besuConfiguration = + serviceManager + .getService(BesuConfiguration.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BesuConfiguration from the ServiceManager.")); + blockchainService = + serviceManager + .getService(BlockchainService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BlockchainService from the ServiceManager.")); + + metricCategoryRegistry = + serviceManager + .getService(MetricCategoryRegistry.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain MetricCategoryRegistry from the ServiceManager.")); + + rpcEndpointService = + serviceManager + .getService(RpcEndpointService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain RpcEndpointService from the ServiceManager.")); + } + + @Override + public void start() { + super.start(); + + if (sharedStartTasksDone.compareAndSet(false, true)) { + performSharedStartTasksOnce(serviceManager); + } + } + + private void performSharedStartTasksOnce(final ServiceManager serviceManager) { + + blockchainService + .getChainId() + .ifPresentOrElse( + chainId -> { + if (chainId.signum() <= 0) { + throw new IllegalArgumentException("Chain id must be greater than zero."); + } + }, + () -> { + throw new IllegalArgumentException("Chain id required"); + }); + + metricsSystem = + serviceManager + .getService(MetricsSystem.class) + .orElseThrow( + () -> + new RuntimeException("Failed to obtain MetricSystem from the ServiceManager.")); + + besuEvents = + serviceManager + .getService(BesuEvents.class) + .orElseThrow( + () -> new RuntimeException("Failed to obtain BesuEvents from the ServiceManager.")); + + bundlePoolService = + new LineaLimitedBundlePool( + besuConfiguration.getDataPath(), + transactionSelectorConfiguration().maxBundlePoolSizeBytes(), + besuEvents, + blockchainService); + bundlePoolService.loadFromDisk(); + } + + @Override + public void stop() { + super.stop(); + sharedRegisterTasksDone.set(false); + sharedStartTasksDone.set(false); + blockchainService = null; + metricsSystem = null; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java new file mode 100644 index 00000000..578c14ea --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java @@ -0,0 +1,216 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.bl; + +import java.math.BigDecimal; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.utils.Compressor; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.slf4j.spi.LoggingEventBuilder; + +/** + * This class implements the profitability formula, and it is used both to check if a tx is + * profitable and to give an estimation of the profitable priorityFeePerGas for a given tx. The + * profitability depends on the context, so it could mean that it is priced enough to have a chance: + * to be accepted in the txpool and to be a candidate for new block creation, it is also used to + * give an estimated priorityFeePerGas in response to a linea_estimateGas call. Each context has it + * own minMargin configuration, so that is possible to accept in the txpool txs, that are not yet + * profitable for block inclusion, but could be in future if the gas price decrease and likewise, it + * is possible to return an estimated priorityFeePerGas that has a profitability buffer to address + * small fluctuations in the gas market. + */ +@Slf4j +public class TransactionProfitabilityCalculator { + private final LineaProfitabilityConfiguration profitabilityConf; + + public TransactionProfitabilityCalculator( + final LineaProfitabilityConfiguration profitabilityConf) { + this.profitabilityConf = profitabilityConf; + } + + /** + * Calculate the estimation of priorityFeePerGas that is considered profitable for the given tx, + * according to the current pricing config and the minMargin. + * + * @param transaction the tx we want to get the estimated priorityFeePerGas for + * @param minMargin the min margin to use for this calculation + * @param gas the gas to use for this calculation, could be the gasUsed of the tx, if it has been + * processed/simulated, otherwise the gasLimit of the tx + * @param minGasPriceWei the current minGasPrice, only used in place of the variable cost from the + * config, in case the extra data pricing is disabled + * @return the estimation of priorityFeePerGas that is considered profitable for the given tx + */ + public Wei profitablePriorityFeePerGas( + final Transaction transaction, + final double minMargin, + final long gas, + final Wei minGasPriceWei) { + final int compressedTxSize = getCompressedTxSize(transaction); + + final long variableCostWei = + profitabilityConf.extraDataPricingEnabled() + ? profitabilityConf.variableCostWei() + : minGasPriceWei.toLong(); + + final var profitAt = + minMargin * (variableCostWei * compressedTxSize / gas + profitabilityConf.fixedCostWei()); + + final var profitAtWei = Wei.ofNumber(BigDecimal.valueOf(profitAt).toBigInteger()); + + log.atDebug() + .setMessage( + "Estimated profitable priorityFeePerGas: {}; minMargin={}, fixedCostWei={}, " + + "variableCostWei={}, gas={}, txSize={}, compressedTxSize={}") + .addArgument(profitAtWei::toHumanReadableString) + .addArgument(minMargin) + .addArgument(profitabilityConf.fixedCostWei()) + .addArgument(variableCostWei) + .addArgument(gas) + .addArgument(transaction::getSize) + .addArgument(compressedTxSize) + .log(); + + return profitAtWei; + } + + /** + * Checks if then given gas price is considered profitable for the given tx, according to the + * current pricing config, the minMargin and gas used, or gasLimit of the tx. + * + * @param context a string to name the context in which it is called, used for logs + * @param transaction the tx we want to check if profitable + * @param minMargin the min margin to use for this check + * @param payingGasPrice the gas price the tx is willing to pay + * @param gas the gas to use for this check, could be the gasUsed of the tx, if it has been + * processed/simulated, otherwise the gasLimit of the tx + * @param minGasPriceWei the current minGasPrice, only used in place of the variable cost from the + * config, in case the extra data pricing is disabled + * @return true if the tx is priced enough to be profitable, false otherwise + */ + public boolean isProfitable( + final String context, + final Transaction transaction, + final double minMargin, + final Wei baseFee, + final Wei payingGasPrice, + final long gas, + final Wei minGasPriceWei) { + + final Wei profitablePriorityFee = + profitablePriorityFeePerGas(transaction, minMargin, gas, minGasPriceWei); + + return isProfitable( + context, + profitablePriorityFee, + transaction, + minMargin, + baseFee, + payingGasPrice, + gas, + minGasPriceWei); + } + + public boolean isProfitable( + final String context, + final Wei profitablePriorityFee, + final Transaction transaction, + final double minMargin, + final Wei baseFee, + final Wei payingGasPrice, + final long gas, + final Wei minGasPriceWei) { + + final Wei profitableGasPrice = baseFee.add(profitablePriorityFee); + + if (payingGasPrice.lessThan(profitableGasPrice)) { + log( + log.atDebug(), + context, + transaction, + minMargin, + payingGasPrice, + baseFee, + profitablePriorityFee, + profitableGasPrice, + gas, + minGasPriceWei); + return false; + } + + log( + log.atTrace(), + context, + transaction, + minMargin, + payingGasPrice, + baseFee, + profitablePriorityFee, + profitableGasPrice, + gas, + minGasPriceWei); + return true; + } + + /** + * This method calculates the compressed size of a tx using the native lib + * + * @param transaction the tx + * @return the compressed size + */ + private int getCompressedTxSize(final Transaction transaction) { + final byte[] bytes = transaction.encoded().toArrayUnsafe(); + return Compressor.instance.compressedSize(bytes); + } + + private void log( + final LoggingEventBuilder leb, + final String context, + final Transaction transaction, + final double minMargin, + final Wei payingGasPrice, + final Wei baseFee, + final Wei profitablePriorityFee, + final Wei profitableGasPrice, + final long gasUsed, + final Wei minGasPriceWei) { + + leb.setMessage( + "Context {}. Transaction {} has a margin of {}, minMargin={}, payingGasPrice={}," + + " profitableGasPrice={}, baseFee={}, profitablePriorityFee={}, fixedCostWei={}, variableCostWei={}, " + + " gasUsed={}") + .addArgument(context) + .addArgument(transaction::getHash) + .addArgument( + () -> + payingGasPrice.toBigInteger().doubleValue() + / profitablePriorityFee.toBigInteger().doubleValue()) + .addArgument(minMargin) + .addArgument(payingGasPrice::toHumanReadableString) + .addArgument(profitableGasPrice::toHumanReadableString) + .addArgument(baseFee::toHumanReadableString) + .addArgument(profitablePriorityFee::toHumanReadableString) + .addArgument(profitabilityConf::fixedCostWei) + .addArgument( + () -> + profitabilityConf.extraDataPricingEnabled() + ? profitabilityConf.variableCostWei() + : minGasPriceWei.toLong()) + .addArgument(gasUsed) + .log(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleForwarder.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleForwarder.java new file mode 100644 index 00000000..5533ea99 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleForwarder.java @@ -0,0 +1,209 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.bundles; + +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; + +import java.io.IOException; +import java.net.URL; +import java.util.OptionalLong; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundlePoolService.TransactionBundleAddedListener; +import net.consensys.linea.bundles.BundlePoolService.TransactionBundleRemovedListener; +import net.consensys.linea.config.LineaBundleConfiguration; +import net.consensys.linea.utils.PriorityThreadPoolExecutor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.hyperledger.besu.plugin.services.BlockchainService; + +@Slf4j +@RequiredArgsConstructor +public class BundleForwarder + implements TransactionBundleAddedListener, TransactionBundleRemovedListener { + public static final String RETRY_COUNT_HEADER = "X-Retry-Count"; + private final AtomicLong reqIdProvider = new AtomicLong(0L); + private final LineaBundleConfiguration config; + private final PriorityThreadPoolExecutor executor; + private final ScheduledExecutorService retryScheduler; + private final BlockchainService blockchainService; + private final OkHttpClient rpcClient; + private final URL recipientUrl; + + @Override + public void onTransactionBundleAdded(final TransactionBundle bundle) { + executor.submit(new SendBundleTask(bundle, 0)); + } + + @Override + public void onTransactionBundleRemoved(final TransactionBundle transactionBundle) { + executor.remove(new SendBundleTask(transactionBundle, 0)); + } + + void retry(final TransactionBundle bundle, final int retry) { + retryScheduler.schedule( + () -> executor.submit(new SendBundleTask(bundle, retry)), + config.retryDelayMillis(), + TimeUnit.MILLISECONDS); + } + + @RequiredArgsConstructor + @EqualsAndHashCode(onlyExplicitlyIncluded = true) + class SendBundleTask implements Callable, Comparable { + private static final ObjectMapper OBJECT_MAPPER = + new ObjectMapper().registerModule(new Jdk8Module()); + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + @Getter @EqualsAndHashCode.Include private final TransactionBundle bundle; + private final int retryCount; + + @Override + public SendBundleResponse call() throws BundleForwarderException { + final var chainHeadBlockNumber = blockchainService.getChainHeadHeader().getNumber(); + if (bundle.blockNumber() <= chainHeadBlockNumber) { + throw new BundleForwarderException( + "Skip forwarding bundle for past block number " + + bundle.blockNumber() + + " since chain head block number is " + + chainHeadBlockNumber, + bundle); + } + + final long reqId = reqIdProvider.getAndIncrement(); + final var jsonRpcRequest = new JsonRpcEnvelope(reqId, bundle.toBundleParameter(false)); + + log.trace("Forwarding request {}, retry count {}", jsonRpcRequest, retryCount); + + final RequestBody body; + try { + body = RequestBody.create(OBJECT_MAPPER.writeValueAsString(jsonRpcRequest), JSON); + } catch (JsonProcessingException e) { + log.error("Error creating send bundle request body", e); + throw new BundleForwarderException( + "Error creating send bundle request body", e, bundle, reqId); + } + + final var requestBuilder = new Request.Builder().url(recipientUrl).post(body); + + if (retryCount > 0) { + requestBuilder.addHeader(RETRY_COUNT_HEADER, String.valueOf(retryCount)); + } + + try (final Response response = rpcClient.newCall(requestBuilder.build()).execute()) { + final var result = + new SendBundleResponse(reqId, bundle, response, response.body().string()); + if (response.isSuccessful()) { + log.trace( + "Bundle {} forwarded successfully at retry count {}", jsonRpcRequest, retryCount); + } else { + log.error( + "Bundle {} forward failed with status {} at retry count {}", + jsonRpcRequest, + response.code(), + retryCount); + } + return result; + } catch (IOException e) { + log.warn( + "Error forwarding bundle request {}, at retry count {}, retrying later", + jsonRpcRequest, + retryCount, + e); + retry(bundle, retryCount + 1); + throw new BundleForwarderException( + "Error send bundle request, retrying later", e, bundle, reqId); + } + } + + @Override + public int compareTo(final SendBundleTask o) { + final int blockNumberPlusRetriesComp = + Long.compare(this.blockNumberPlusRetries(), o.blockNumberPlusRetries()); + if (blockNumberPlusRetriesComp == 0) { + // put retries at the end + final int retryCountComp = Integer.compare(this.retryCount, o.retryCount); + if (retryCountComp == 0) { + // at last disambiguate by sequence + return Long.compare(this.bundle.sequence(), o.bundle.sequence()); + } + return retryCountComp; + } + return blockNumberPlusRetriesComp; + } + + private long blockNumberPlusRetries() { + return this.bundle.blockNumber() + retryCount; + } + } + + record SendBundleResponse(long reqId, TransactionBundle bundle, Response response, String body) {} + + @JsonAutoDetect(fieldVisibility = ANY) + @ToString(onlyExplicitlyIncluded = true) + private static class JsonRpcEnvelope { + private final String jsonrpc = "2.0"; + private final String method = "linea_sendBundle"; + @ToString.Include private final long id; + @ToString.Include private final BundleParameter[] params; + + public JsonRpcEnvelope(final long id, final BundleParameter params) { + this.id = id; + this.params = new BundleParameter[] {params}; + } + } + + @Accessors(fluent = true) + @Getter + public class BundleForwarderException extends RuntimeException { + private final OptionalLong reqId; + private final TransactionBundle bundle; + + public BundleForwarderException(final String message, final TransactionBundle bundle) { + super(message); + this.reqId = OptionalLong.empty(); + this.bundle = bundle; + } + + public BundleForwarderException( + final String message, + final Throwable cause, + final TransactionBundle bundle, + final long reqId) { + super(message, cause); + this.reqId = OptionalLong.of(reqId); + this.bundle = bundle; + } + + @Override + public String getMessage() { + return "[ReqId:" + reqId + "] " + super.getMessage(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleParameter.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleParameter.java new file mode 100644 index 00000000..1004fda6 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundleParameter.java @@ -0,0 +1,64 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.bundles; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT; + +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; + +@JsonInclude(NON_ABSENT) +@JsonPropertyOrder({"blockNumber", "minTimestamp", "maxTimestamp"}) +public record BundleParameter( + /* array of signed transactions to execute in a bundle */ + List txs, + /* block number for which this bundle is valid */ + Long blockNumber, + /* Optional minimum timestamp from which this bundle is valid */ + Optional minTimestamp, + /* Optional max timestamp for which this bundle is valid */ + Optional maxTimestamp, + /* Optional list of transaction hashes which are allowed to revert */ + Optional> revertingTxHashes, + /* Optional UUID which can be used to replace or cancel this bundle */ + Optional replacementUUID, + /* Optional list of builders to share this bundle with */ + Optional> builders) { + @JsonCreator + public BundleParameter( + @JsonProperty("txs") final List txs, + @JsonProperty("blockNumber") final UnsignedLongParameter blockNumber, + @JsonProperty("minTimestamp") final Optional minTimestamp, + @JsonProperty("maxTimestamp") final Optional maxTimestamp, + @JsonProperty("revertingTxHashes") final Optional> revertingTxHashes, + @JsonProperty("replacementUUID") final Optional replacementUUID, + @JsonProperty("builders") final Optional> builders) { + this( + txs, + blockNumber.getValue(), + minTimestamp, + maxTimestamp, + revertingTxHashes, + replacementUUID, + builders); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundlePoolService.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundlePoolService.java new file mode 100644 index 00000000..8a21dfd0 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/BundlePoolService.java @@ -0,0 +1,129 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.bundles; + +import java.util.List; +import java.util.UUID; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.plugin.services.BesuService; + +public interface BundlePoolService extends BesuService { + @FunctionalInterface + interface TransactionBundleAddedListener { + void onTransactionBundleAdded(TransactionBundle transactionBundle); + } + + @FunctionalInterface + interface TransactionBundleRemovedListener { + void onTransactionBundleRemoved(TransactionBundle transactionBundle); + } + + /** + * Retrieves a list of TransactionBundles associated with a block number. + * + * @param blockNumber The block number to look up. + * @return A list of TransactionBundles for the given block number, or an empty list if none are + * found. + */ + List getBundlesByBlockNumber(long blockNumber); + + /** + * Retrieves a TransactionBundle by its unique hash identifier. + * + * @param hash The hash identifier of the TransactionBundle. + * @return The TransactionBundle associated with the hash, or null if not found. + */ + TransactionBundle get(Hash hash); + + /** + * Retrieves a TransactionBundle by its replacement UUID + * + * @param replacementUUID identifier of the TransactionBundle. + * @return The TransactionBundle associated with the uuid, or null if not found. + */ + TransactionBundle get(UUID replacementUUID); + + /** + * Puts or replaces an existing TransactionBundle in the cache and updates the block index. + * + * @param hash The hash identifier of the TransactionBundle. + * @param bundle The new TransactionBundle to replace the existing one. + * @throws IllegalStateException if the pool is frozen + */ + void putOrReplace(Hash hash, TransactionBundle bundle); + + /** + * Puts or replaces an existing TransactionBundle by UUIDin the cache and updates the block index. + * + * @param replacementUUID identifier of the TransactionBundle. + * @param bundle The new TransactionBundle to replace the existing one. + * @throws IllegalStateException if the pool is frozen + */ + void putOrReplace(UUID replacementUUID, TransactionBundle bundle); + + /** + * Removes an existing TransactionBundle in the cache and updates the block index. + * + * @param replacementUUID identifier of the TransactionBundle. + * @return boolean indicating if bundle was found and removed + * @throws IllegalStateException if the pool is frozen + */ + boolean remove(UUID replacementUUID); + + /** + * Removes an existing TransactionBundle in the cache and updates the block index. + * + * @param hash The hash identifier of the TransactionBundle. + * @return boolean indicating if bundle was found and removed + * @throws IllegalStateException if the pool is frozen + */ + boolean remove(Hash hash); + + /** + * Get the number of bundles in the pool + * + * @return the number of bundles in the pool + */ + long size(); + + /** + * Return true if the pool does not accept modifications anymore + * + * @return true if the pool does not accept modifications anymore + */ + boolean isFrozen(); + + /** + * Save the content of the pool to disk. Note that once this operation starts, the pool will be + * frozen and will not be possible to modify it anymore. + */ + void saveToDisk(); + + /** + * Load the content of the pool from disk. + * + * @throws IllegalStateException if the pool is frozen + */ + void loadFromDisk(); + + long subscribeTransactionBundleAdded(TransactionBundleAddedListener listener); + + long subscribeTransactionBundleRemoved(TransactionBundleRemovedListener listener); + + void unsubscribeTransactionBundleAdded(long listenerId); + + void unsubscribeTransactionBundleRemoved(long listenerId); +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/ForwardBundlesPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/ForwardBundlesPlugin.java new file mode 100644 index 00000000..dc55921a --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/ForwardBundlesPlugin.java @@ -0,0 +1,85 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.google.auto.service.AutoService; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.utils.PriorityThreadPoolExecutor; +import okhttp3.OkHttpClient; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; + +@AutoService(BesuPlugin.class) +public class ForwardBundlesPlugin extends AbstractLineaRequiredPlugin { + + @Override + public void doRegister(final ServiceManager serviceManager) {} + + @Override + public void doStart() { + final var config = bundleConfiguration(); + final var forwardUrls = config.forwardUrls(); + if (!forwardUrls.isEmpty()) { + final var rpcClient = createRpcClient(config.timeoutMillis()); + final var retryScheduler = createRetryScheduler(); + forwardUrls.stream() + .map( + url -> + new BundleForwarder( + config, + createExecutor(url), + retryScheduler, + blockchainService, + rpcClient, + url)) + .peek(bundlePoolService::subscribeTransactionBundleAdded) + .toList(); + } + } + + private OkHttpClient createRpcClient(final int timeoutMillis) { + return new OkHttpClient.Builder() + .retryOnConnectionFailure(false) + .callTimeout(Duration.ofMillis(timeoutMillis)) + .build(); + } + + private PriorityThreadPoolExecutor createExecutor(final URL recipientUrl) { + return new PriorityThreadPoolExecutor( + 0, + 1, + 10, + TimeUnit.MINUTES, + Thread.ofVirtual().name("BundleForwarder[" + recipientUrl.toString() + "]", 0L).factory()); + } + + private ScheduledExecutorService createRetryScheduler() { + return Executors.newSingleThreadScheduledExecutor( + Thread.ofPlatform().name("BundleForwarderRetry", 0L).factory()); + } + + @Override + public void stop() { + // stop forwarders? + super.stop(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/LineaLimitedBundlePool.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/LineaLimitedBundlePool.java new file mode 100644 index 00000000..ee988d58 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/LineaLimitedBundlePool.java @@ -0,0 +1,416 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import static java.util.Collections.emptyList; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SequenceWriter; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.Scheduler; +import com.google.auto.service.AutoService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.plugin.data.AddedBlockContext; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BesuService; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.util.Subscribers; + +/** + * A pool for managing TransactionBundles with limited size and FIFO eviction. Provides access via + * hash identifiers or block numbers. + */ +@AutoService(BesuService.class) +@Slf4j +public class LineaLimitedBundlePool implements BundlePoolService, BesuEvents.BlockAddedListener { + public static final String BUNDLE_SAVE_FILENAME = "bundles.ndjson"; + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module()); + private final BlockchainService blockchainService; + private final Cache cache; + private final Map> blockIndex; + private final Path saveFilePath; + private final AtomicBoolean isFrozen = new AtomicBoolean(false); + private final Subscribers transactionBundleAddedListeners = + Subscribers.create(); + private final Subscribers transactionBundleRemovedListeners = + Subscribers.create(); + + /** + * Initializes the LineaLimitedBundlePool with a maximum size and expiration time, and registers + * as a blockAddedEvent listener. + * + * @param maxSizeInBytes The maximum size in bytes of the pool objects. + */ + @VisibleForTesting + public LineaLimitedBundlePool( + final Path dataDir, + final long maxSizeInBytes, + final BesuEvents eventService, + final BlockchainService blockchainService) { + this.saveFilePath = dataDir.resolve(BUNDLE_SAVE_FILENAME); + this.blockchainService = blockchainService; + this.cache = + Caffeine.newBuilder() + .maximumWeight(maxSizeInBytes) // Maximum size in bytes + .scheduler( + Scheduler + .systemScheduler()) // To ensure maintenance operation are not delayed too much + .weigher( + (Hash key, TransactionBundle value) -> { + // Calculate the size of a TransactionBundle in bytes + return calculateWeight(value); + }) + .removalListener( + (Hash key, TransactionBundle bundle, RemovalCause cause) -> { + if (bundle != null) { + if (cause.wasEvicted()) { + log.atTrace() + .setMessage("Dropping transaction bundle {}:{} due to {}") + .addArgument(bundle::blockNumber) + .addArgument(() -> bundle.bundleIdentifier().toHexString()) + .addArgument(cause::name) + .log(); + removeFromBlockIndex(bundle); + } + + transactionBundleRemovedListeners.forEach( + listener -> listener.onTransactionBundleRemoved(bundle)); + } + }) + .build(); + this.blockIndex = new ConcurrentHashMap<>(); + + // register ourselves as a block added listener: + eventService.addBlockAddedListener(this); + } + + /** + * Retrieves a list of TransactionBundles associated with a block number. + * + * @param blockNumber The block number to look up. + * @return A list of TransactionBundles for the given block number, or an empty list if none are + * found. The returned list if safe for modification since it is not backed by the original + * one + */ + public List getBundlesByBlockNumber(long blockNumber) { + return List.copyOf(blockIndex.getOrDefault(blockNumber, emptyList())); + } + + /** + * Retrieves a TransactionBundle by its unique hash identifier. + * + * @param hash The hash identifier of the TransactionBundle. + * @return The TransactionBundle associated with the hash, or null if not found. + */ + public TransactionBundle get(Hash hash) { + return cache.getIfPresent(hash); + } + + /** + * Retrieves a TransactionBundle by its replacement UUID + * + * @param replacementUUID identifier of the TransactionBundle. + * @return The TransactionBundle associated with the uuid, or null if not found. + */ + public TransactionBundle get(UUID replacementUUID) { + return cache.getIfPresent(UUIDToHash(replacementUUID)); + } + + /** + * Puts or replaces an existing TransactionBundle in the cache and updates the block index. + * + * @param hash The hash identifier of the TransactionBundle. + * @param bundle The new TransactionBundle to replace the existing one. + */ + public void putOrReplace(Hash hash, TransactionBundle bundle) { + failIfFrozen( + () -> { + TransactionBundle existing = cache.getIfPresent(hash); + if (existing != null) { + removeFromBlockIndex(existing); + } + cache.put(hash, bundle); + addToBlockIndex(bundle); + return null; + }); + } + + /** + * Puts or replaces an existing TransactionBundle by UUIDin the cache and updates the block index. + * + * @param replacementUUID identifier of the TransactionBundle. + * @param bundle The new TransactionBundle to replace the existing one. + */ + public void putOrReplace(UUID replacementUUID, TransactionBundle bundle) { + putOrReplace(UUIDToHash(replacementUUID), bundle); + } + + /** + * removes an existing TransactionBundle in the cache and updates the block index. + * + * @param replacementUUID identifier of the TransactionBundle. + * @return boolean indicating if bundle was found and removed + */ + public boolean remove(UUID replacementUUID) { + return remove(UUIDToHash(replacementUUID)); + } + + /** + * removes an existing TransactionBundle in the cache and updates the block index. + * + * @param hash The hash identifier of the TransactionBundle. + * @return boolean indicating if bundle was found and removed + */ + public boolean remove(Hash hash) { + return failIfFrozen( + () -> { + var existingBundle = cache.getIfPresent(hash); + if (existingBundle != null) { + cache.invalidate(hash); + removeFromBlockIndex(existingBundle); + return true; + } + return false; + }); + } + + @Override + public long size() { + return cache.estimatedSize(); + } + + @Override + public boolean isFrozen() { + return isFrozen.get(); + } + + @Override + public void saveToDisk() { + synchronized (isFrozen) { + isFrozen.set(true); + log.info("Saving bundles to {}", saveFilePath); + + try (final BufferedWriter bw = Files.newBufferedWriter(saveFilePath, StandardCharsets.UTF_8); + final SequenceWriter sequenceWriter = + objectMapper + .writerFor(TransactionBundle.class) + .withRootValueSeparator(System.lineSeparator()) + .writeValues(bw)) { + + // write the header + bw.write(objectMapper.writeValueAsString(Map.of("version", 1))); + bw.newLine(); + + // write the bundles sorted by block number + final var savedCount = + blockIndex.values().stream() + .flatMap(List::stream) + .sorted(Comparator.comparing(TransactionBundle::blockNumber)) + .peek( + bundle -> { + try { + sequenceWriter.write(bundle); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .count(); + log.info("Saved {} bundles to {}", savedCount, saveFilePath); + } catch (final Throwable ioe) { + log.error("Error while saving bundles to {}", saveFilePath, ioe); + } + } + } + + @Override + public void loadFromDisk() { + failIfFrozen( + () -> { + if (saveFilePath.toFile().exists()) { + log.info("Loading bundles from {}", saveFilePath); + final var chainHeadBlockNumber = blockchainService.getChainHeadHeader().getNumber(); + final var loadedCount = new AtomicLong(0L); + final var skippedCount = new AtomicLong(0L); + + try (final BufferedReader br = + Files.newBufferedReader(saveFilePath, StandardCharsets.UTF_8)) { + + // read header and check version + final var headerNode = objectMapper.readTree(br.readLine()); + if (!headerNode.has("version") || headerNode.get("version").asInt() != 1) { + throw new IllegalArgumentException( + "Unsupported bundle serialization header " + headerNode); + } + log.info("Loading bundles from {}, header {}", saveFilePath, headerNode); + + try (final MappingIterator iterator = + objectMapper.readerFor(TransactionBundle.class).readValues(br)) { + iterator.forEachRemaining( + bundle -> { + if (bundle.blockNumber() > chainHeadBlockNumber) { + this.putOrReplace(bundle.bundleIdentifier(), bundle); + loadedCount.incrementAndGet(); + } else { + log.debug( + "Skipping bundle {} at location {}, since its block number {} is not greater than chain head block number {}", + bundle.bundleIdentifier(), + iterator.getCurrentLocation(), + bundle.blockNumber(), + chainHeadBlockNumber); + skippedCount.incrementAndGet(); + } + }); + log.info("Loaded {} bundles from {}", loadedCount.get(), saveFilePath); + } + } catch (final Throwable t) { + log.error( + "Error while reading bundles from {}, partially loaded {} bundles", + saveFilePath, + loadedCount.get(), + t); + } + saveFilePath.toFile().delete(); + } + return null; + }); + } + + @Override + public long subscribeTransactionBundleAdded(final TransactionBundleAddedListener listener) { + return transactionBundleAddedListeners.subscribe(listener); + } + + @Override + public long subscribeTransactionBundleRemoved(final TransactionBundleRemovedListener listener) { + return transactionBundleRemovedListeners.subscribe(listener); + } + + @Override + public void unsubscribeTransactionBundleAdded(final long listenerId) { + transactionBundleAddedListeners.unsubscribe(listenerId); + } + + @Override + public void unsubscribeTransactionBundleRemoved(final long listenerId) { + transactionBundleRemovedListeners.unsubscribe(listenerId); + } + + /** + * Adds a TransactionBundle to the block index. + * + * @param bundle The TransactionBundle to add. + */ + private void addToBlockIndex(TransactionBundle bundle) { + long blockNumber = bundle.blockNumber(); + blockIndex.computeIfAbsent(blockNumber, k -> new ArrayList<>()).add(bundle); + transactionBundleAddedListeners.forEach(listener -> listener.onTransactionBundleAdded(bundle)); + } + + /** + * Removes a TransactionBundle from the block index. + * + * @param bundle The TransactionBundle to remove. + */ + private void removeFromBlockIndex(TransactionBundle bundle) { + long blockNumber = bundle.blockNumber(); + List bundles = blockIndex.get(blockNumber); + if (bundles != null) { + bundles.remove(bundle); + if (bundles.isEmpty()) { + blockIndex.remove(blockNumber); + } + } + } + + private int calculateWeight(TransactionBundle bundle) { + return bundle.pendingTransactions().stream().mapToInt(PendingTransaction::memorySize).sum(); + } + + /** + * convert a UUID into a hash used by the bundle pool. + * + * @param uuid the uuid to hash + * @return Hash identifier for the uuid + */ + public static Hash UUIDToHash(UUID uuid) { + return Hash.hash( + Bytes.concatenate( + Bytes.ofUnsignedLong(uuid.getMostSignificantBits()), + Bytes.ofUnsignedLong(uuid.getLeastSignificantBits()))); + } + + /** + * Cull the bundle pool on the basis of blocks added. + * + * @param addedBlockContext + */ + @Override + public void onBlockAdded(final AddedBlockContext addedBlockContext) { + synchronized (isFrozen) { + if (!isFrozen.get()) { // do nothing if frozen + final var lastSeen = addedBlockContext.getBlockHeader().getNumber(); + final var latest = Math.max(lastSeen, blockchainService.getChainHeadHeader().getNumber()); + // keep it simple regarding reorgs and, cull the pool for any block numbers lower than + // latest + blockIndex.keySet().stream() + .filter(k -> k < latest) + // collecting to a set in order to not mutate the collection we are streaming: + .collect(Collectors.toSet()) + .forEach( + k -> { + blockIndex.get(k).forEach(bundle -> cache.invalidate(bundle.bundleIdentifier())); + // dropping from the cache should inherently remove from blockIndex, but this + // is cheap insurance against blockIndex map leaking due to cache evictions + blockIndex.remove(k); + }); + } + } + } + + private R failIfFrozen(Supplier modificationAction) { + synchronized (isFrozen) { + if (isFrozen.get()) { + throw new IllegalStateException("Bundle pool is not accepting modifications"); + } + return modificationAction.get(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/TransactionBundle.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/TransactionBundle.java new file mode 100644 index 00000000..eda7d35d --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/bundles/TransactionBundle.java @@ -0,0 +1,134 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SequencedMap; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +/** TransactionBundle class representing a collection of pending transactions with metadata. */ +@Accessors(fluent = true) +@Getter +@EqualsAndHashCode +@ToString +public class TransactionBundle { + private static final AtomicLong BUNDLE_COUNT = new AtomicLong(0L); + private final long sequence = BUNDLE_COUNT.incrementAndGet(); + private final Hash bundleIdentifier; + private final List pendingTransactions; + private final Long blockNumber; + private final Optional minTimestamp; + private final Optional maxTimestamp; + private final Optional> revertingTxHashes; + private final Optional replacementUUID; + + public TransactionBundle( + final Hash bundleIdentifier, + final List transactions, + final Long blockNumber, + final Optional minTimestamp, + final Optional maxTimestamp, + final Optional> revertingTxHashes, + final Optional replacementUUID) { + this.bundleIdentifier = bundleIdentifier; + this.pendingTransactions = transactions.stream().map(PendingBundleTx::new).toList(); + this.blockNumber = blockNumber; + this.minTimestamp = minTimestamp; + this.maxTimestamp = maxTimestamp; + this.revertingTxHashes = revertingTxHashes; + this.replacementUUID = replacementUUID; + } + + public BundleParameter toBundleParameter(final boolean compact) { + return new BundleParameter( + pendingTransactions.stream() + .map( + ptx -> + compact ? ptx.toBase64String() : ptx.getTransaction().encoded().toHexString()) + .toList(), + new UnsignedLongParameter(blockNumber), + minTimestamp, + maxTimestamp, + revertingTxHashes, + replacementUUID.map(UUID::toString), + Optional.empty()); + } + + @JsonValue + public Map serialize() { + return Map.of(bundleIdentifier, toBundleParameter(true)); + } + + @JsonCreator + public static TransactionBundle deserialize( + final SequencedMap serialized) { + final var entry = serialized.firstEntry(); + final var hash = entry.getKey(); + final var parameters = entry.getValue(); + + return new TransactionBundle( + hash, + parameters.txs().stream().map(Bytes::fromBase64String).map(Transaction::readFrom).toList(), + parameters.blockNumber(), + parameters.minTimestamp(), + parameters.maxTimestamp(), + parameters.revertingTxHashes(), + parameters.replacementUUID().map(UUID::fromString)); + } + + /** A pending transaction contained in a bundle. */ + public class PendingBundleTx + extends org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction.Local { + + public PendingBundleTx(final Transaction transaction) { + super(transaction); + } + + public TransactionBundle getBundle() { + return TransactionBundle.this; + } + + public boolean isBundleStart() { + return getBundle().pendingTransactions().getFirst().equals(this); + } + + @Override + public String toTraceLog() { + return "Bundle tx: " + super.toTraceLog(); + } + + String toBase64String() { + final var rlpOutput = new BytesValueRLPOutput(); + getTransaction().writeTo(rlpOutput); + return rlpOutput.encoded().toBase64String(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleCliOptions.java new file mode 100644 index 00000000..d729ff23 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleCliOptions.java @@ -0,0 +1,110 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.net.URL; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +/** The Linea Bundle CLI options. */ +public class LineaBundleCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "bundle-sequencer"; + + private static final String BUNDLES_FORWARD_URLS = "--plugin-linea-bundles-forward-urls"; + private static final Set DEFAULT_BUNDLES_FORWARD_URLS = Set.of(); + + private static final String BUNDLES_FORWARD_RETRY_DELAY = + "--plugin-linea-bundles-forward-retry-delay"; + private static final int DEFAULT_BUNDLES_FORWARD_RETRY_DELAY_MILLIS = 1000; + + private static final String BUNDLES_FORWARD_TIMEOUT = "--plugin-linea-bundles-forward-timeout"; + private static final int DEFAULT_BUNDLES_FORWARD_TIMEOUT_MILLIS = 5000; + + @CommandLine.Option( + names = {BUNDLES_FORWARD_URLS}, + paramLabel = ">", + description = + "A comma separated list of endpoint to which incoming bundles will be forwarded (default: ${DEFAULT-VALUE})") + private Set forwardUrls = DEFAULT_BUNDLES_FORWARD_URLS; + + @CommandLine.Option( + names = {BUNDLES_FORWARD_RETRY_DELAY}, + paramLabel = "", + description = + "Number of milliseconds to wait before retrying a failed forward (default: ${DEFAULT-VALUE})") + private int retryDelayMillis = DEFAULT_BUNDLES_FORWARD_RETRY_DELAY_MILLIS; + + @CommandLine.Option( + names = {BUNDLES_FORWARD_TIMEOUT}, + paramLabel = "", + description = + "Number of milliseconds to wait before a forward times out (default: ${DEFAULT-VALUE})") + private int timeoutMillis = DEFAULT_BUNDLES_FORWARD_TIMEOUT_MILLIS; + + private LineaBundleCliOptions() {} + + /** + * Create Linea Bundle CLI options. + * + * @return the Linea RPC Bundle options + */ + public static LineaBundleCliOptions create() { + return new LineaBundleCliOptions(); + } + + /** + * Linea Bundle CLI options from config. + * + * @param config the config + * @return the Linea Bundle CLI options + */ + public static LineaBundleCliOptions fromConfig(final LineaBundleConfiguration config) { + final LineaBundleCliOptions options = create(); + options.forwardUrls = config.forwardUrls(); + options.retryDelayMillis = config.retryDelayMillis(); + options.timeoutMillis = config.timeoutMillis(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaBundleConfiguration toDomainObject() { + return LineaBundleConfiguration.builder() + .forwardUrls(forwardUrls) + .retryDelayMillis(retryDelayMillis) + .timeoutMillis(timeoutMillis) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add( + BUNDLES_FORWARD_URLS, + forwardUrls.stream().map(URL::toString).collect(Collectors.joining(","))) + .add(BUNDLES_FORWARD_RETRY_DELAY, retryDelayMillis) + .add(BUNDLES_FORWARD_TIMEOUT, timeoutMillis) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleConfiguration.java new file mode 100644 index 00000000..b2e951b7 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaBundleConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.net.URL; +import java.util.Set; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.Accessors; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** The Linea Bundle configuration. */ +@Builder(toBuilder = true) +@Accessors(fluent = true) +@Getter +@ToString +public class LineaBundleConfiguration implements LineaOptionsConfiguration { + private Set forwardUrls; + private int retryDelayMillis; + private int timeoutMillis; +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java new file mode 100644 index 00000000..5bdb40fe --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +/** Linea node type that is used when reporting rejected transactions. */ +public enum LineaNodeType { + SEQUENCER, + RPC, + P2P +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java new file mode 100644 index 00000000..36ed3637 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java @@ -0,0 +1,224 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.math.BigDecimal; + +import com.google.common.base.MoreObjects; +import jakarta.validation.constraints.Positive; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +/** The Linea profitability calculator CLI options. */ +public class LineaProfitabilityCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "profitability-config"; + + public static final String FIXED_GAS_COST_WEI = "--plugin-linea-fixed-gas-cost-wei"; + public static final long DEFAULT_FIXED_GAS_COST_WEI = 0; + + public static final String VARIABLE_GAS_COST_WEI = "--plugin-linea-variable-gas-cost-wei"; + public static final long DEFAULT_VARIABLE_GAS_COST_WEI = 1_000_000_000; + + public static final String MIN_MARGIN = "--plugin-linea-min-margin"; + public static final BigDecimal DEFAULT_MIN_MARGIN = BigDecimal.ONE; + + public static final String ESTIMATE_GAS_MIN_MARGIN = "--plugin-linea-estimate-gas-min-margin"; + public static final BigDecimal DEFAULT_ESTIMATE_GAS_MIN_MARGIN = BigDecimal.ONE; + + public static final String TX_POOL_MIN_MARGIN = "--plugin-linea-tx-pool-min-margin"; + public static final BigDecimal DEFAULT_TX_POOL_MIN_MARGIN = BigDecimal.valueOf(0.5); + + public static final String TX_POOL_ENABLE_CHECK_API = + "--plugin-linea-tx-pool-profitability-check-api-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_CHECK_API = true; + + public static final String TX_POOL_ENABLE_CHECK_P2P = + "--plugin-linea-tx-pool-profitability-check-p2p-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_CHECK_P2P = false; + + public static final String EXTRA_DATA_PRICING_ENABLED = + "--plugin-linea-extra-data-pricing-enabled"; + public static final boolean DEFAULT_EXTRA_DATA_PRICING_ENABLED = false; + + public static final String EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED = + "--plugin-linea-extra-data-set-min-gas-price-enabled"; + public static final boolean DEFAULT_EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED = true; + + public static final String PROFITABILITY_METRICS_BUCKETS = + "--plugin-linea-profitability-metrics-buckets"; + public static final double[] DEFAULT_PROFITABILITY_METRICS_BUCKETS = { + 0.1, 0.3, 0.5, 0.7, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 5.0, 10.0 + }; + + @Positive + @CommandLine.Option( + names = {FIXED_GAS_COST_WEI}, + hidden = true, + paramLabel = "", + description = "Fixed gas cost in Wei (default: ${DEFAULT-VALUE})") + private long fixedGasCostWei = DEFAULT_FIXED_GAS_COST_WEI; + + @Positive + @CommandLine.Option( + names = {VARIABLE_GAS_COST_WEI}, + hidden = true, + paramLabel = "", + description = "Variable gas cost in Wei (default: ${DEFAULT-VALUE})") + private long variableGasCostWei = DEFAULT_VARIABLE_GAS_COST_WEI; + + @Positive + @CommandLine.Option( + names = {MIN_MARGIN}, + hidden = true, + paramLabel = "", + description = "Minimum margin of a transaction to be selected (default: ${DEFAULT-VALUE})") + private BigDecimal minMargin = DEFAULT_MIN_MARGIN; + + @Positive + @CommandLine.Option( + names = {ESTIMATE_GAS_MIN_MARGIN}, + hidden = true, + paramLabel = "", + description = + "Recommend a specific gas price when using linea_estimateGas (default: ${DEFAULT-VALUE})") + private BigDecimal estimateGasMinMargin = DEFAULT_ESTIMATE_GAS_MIN_MARGIN; + + @Positive + @CommandLine.Option( + names = {TX_POOL_MIN_MARGIN}, + hidden = true, + paramLabel = "", + description = + "The min margin an incoming tx must have to be accepted in the txpool (default: ${DEFAULT-VALUE})") + private BigDecimal txPoolMinMargin = DEFAULT_TX_POOL_MIN_MARGIN; + + @CommandLine.Option( + names = {TX_POOL_ENABLE_CHECK_API}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the profitability check for txs received via API? (default: ${DEFAULT-VALUE})") + private boolean txPoolCheckApiEnabled = DEFAULT_TX_POOL_ENABLE_CHECK_API; + + @CommandLine.Option( + names = {TX_POOL_ENABLE_CHECK_P2P}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the profitability check for txs received via p2p? (default: ${DEFAULT-VALUE})") + private boolean txPoolCheckP2pEnabled = DEFAULT_TX_POOL_ENABLE_CHECK_P2P; + + @CommandLine.Option( + names = {EXTRA_DATA_PRICING_ENABLED}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable setting pricing parameters via extra data field (default: ${DEFAULT-VALUE})") + private boolean extraDataPricingEnabled = DEFAULT_EXTRA_DATA_PRICING_ENABLED; + + @CommandLine.Option( + names = {EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable setting min gas price runtime value via extra data field (default: ${DEFAULT-VALUE})") + private boolean extraDataSetMinGasPriceEnabled = DEFAULT_EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED; + + @CommandLine.Option( + names = {PROFITABILITY_METRICS_BUCKETS}, + arity = "1..*", + split = ",", + hidden = true, + paramLabel = "", + description = + "List of buckets to use to create the histogram for ratio between the effective priority fee " + + "and the calculate profitable priority of the tx (default: ${DEFAULT-VALUE})") + private double[] profitabilityMetricsBuckets = DEFAULT_PROFITABILITY_METRICS_BUCKETS; + + private LineaProfitabilityCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaProfitabilityCliOptions create() { + return new LineaProfitabilityCliOptions(); + } + + /** + * Linea cli options from config. + * + * @param config the config + * @return the Linea cli options + */ + public static LineaProfitabilityCliOptions fromConfig( + final LineaProfitabilityConfiguration config) { + final LineaProfitabilityCliOptions options = create(); + options.fixedGasCostWei = config.fixedCostWei(); + options.variableGasCostWei = config.variableCostWei(); + options.minMargin = BigDecimal.valueOf(config.minMargin()); + options.estimateGasMinMargin = BigDecimal.valueOf(config.estimateGasMinMargin()); + options.txPoolMinMargin = BigDecimal.valueOf(config.txPoolMinMargin()); + options.txPoolCheckApiEnabled = config.txPoolCheckApiEnabled(); + options.txPoolCheckP2pEnabled = config.txPoolCheckP2pEnabled(); + options.extraDataPricingEnabled = config.extraDataPricingEnabled(); + options.extraDataSetMinGasPriceEnabled = config.extraDataSetMinGasPriceEnabled(); + options.profitabilityMetricsBuckets = config.profitabilityMetricsBuckets(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaProfitabilityConfiguration toDomainObject() { + return LineaProfitabilityConfiguration.builder() + .fixedCostWei(fixedGasCostWei) + .variableCostWei(variableGasCostWei) + .minMargin(minMargin.doubleValue()) + .estimateGasMinMargin(estimateGasMinMargin.doubleValue()) + .txPoolMinMargin(txPoolMinMargin.doubleValue()) + .txPoolCheckApiEnabled(txPoolCheckApiEnabled) + .txPoolCheckP2pEnabled(txPoolCheckP2pEnabled) + .extraDataPricingEnabled(extraDataPricingEnabled) + .extraDataSetMinGasPriceEnabled(extraDataSetMinGasPriceEnabled) + .profitabilityMetricsBuckets(profitabilityMetricsBuckets) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(FIXED_GAS_COST_WEI, fixedGasCostWei) + .add(VARIABLE_GAS_COST_WEI, variableGasCostWei) + .add(MIN_MARGIN, minMargin) + .add(ESTIMATE_GAS_MIN_MARGIN, estimateGasMinMargin) + .add(TX_POOL_MIN_MARGIN, txPoolMinMargin) + .add(TX_POOL_ENABLE_CHECK_API, txPoolCheckApiEnabled) + .add(TX_POOL_ENABLE_CHECK_P2P, txPoolCheckP2pEnabled) + .add(EXTRA_DATA_PRICING_ENABLED, extraDataPricingEnabled) + .add(EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED, extraDataSetMinGasPriceEnabled) + .add(PROFITABILITY_METRICS_BUCKETS, profitabilityMetricsBuckets) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java new file mode 100644 index 00000000..012ad4d7 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.Accessors; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** The Linea profitability calculator configuration. */ +@Builder(toBuilder = true) +@Accessors(fluent = true) +@Getter +@ToString +public class LineaProfitabilityConfiguration implements LineaOptionsConfiguration { + /** It is safe to keep this as long, since it will store value <= max_int * 1000 */ + private long fixedCostWei; + + /** It is safe to keep this as long, since it will store value <= max_int * 1000 */ + private long variableCostWei; + + /** It is safe to keep this as long, since it will store value <= max_int * 1000 */ + private long ethGasPriceWei; + + private double minMargin; + private double estimateGasMinMargin; + private double txPoolMinMargin; + private boolean txPoolCheckApiEnabled; + private boolean txPoolCheckP2pEnabled; + private boolean extraDataPricingEnabled; + private boolean extraDataSetMinGasPriceEnabled; + private double[] profitabilityMetricsBuckets; + + /** + * These 2 parameters must be atomically updated + * + * @param fixedCostWei fixed cost in Wei + * @param variableCostWei variable cost in Wei + * @param ethGasPriceWei gas price in Wei + */ + public synchronized void updateFixedVariableAndGasPrice( + final long fixedCostWei, final long variableCostWei, final long ethGasPriceWei) { + this.fixedCostWei = fixedCostWei; + this.variableCostWei = variableCostWei; + this.ethGasPriceWei = ethGasPriceWei; + } + + public synchronized long fixedCostWei() { + return fixedCostWei; + } + + public synchronized long variableCostWei() { + return variableCostWei; + } + + public synchronized long ethGasPriceWei() { + return ethGasPriceWei; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java new file mode 100644 index 00000000..cbdce8af --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java @@ -0,0 +1,100 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config; + +import java.net.URL; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine.Option; + +/** The Linea Rejected Transaction Reporting CLI options. */ +public class LineaRejectedTxReportingCliOptions implements LineaCliOptions { + /** + * The configuration key used in AbstractLineaPrivateOptionsPlugin to identify the cli options. + */ + public static final String CONFIG_KEY = "rejected-tx-reporting-config"; + + /** The rejected transaction endpoint. */ + public static final String REJECTED_TX_ENDPOINT = "--plugin-linea-rejected-tx-endpoint"; + + /** The Linea node type. */ + public static final String LINEA_NODE_TYPE = "--plugin-linea-node-type"; + + @Option( + names = {REJECTED_TX_ENDPOINT}, + hidden = true, + paramLabel = "", + description = + "Endpoint URI for reporting rejected transactions. Specify a valid URI to enable reporting.") + URL rejectedTxEndpoint = null; + + @Option( + names = {LINEA_NODE_TYPE}, + hidden = true, + paramLabel = "", + description = + "Linea Node type to use when reporting rejected transactions. (Valid values: ${COMPLETION-CANDIDATES})") + LineaNodeType lineaNodeType = null; + + /** Default constructor. */ + private LineaRejectedTxReportingCliOptions() {} + + /** + * Create Linea Rejected Transaction Reporting CLI options. + * + * @return the Linea Rejected Transaction Reporting CLI options + */ + public static LineaRejectedTxReportingCliOptions create() { + return new LineaRejectedTxReportingCliOptions(); + } + + /** + * Instantiates a new Linea rejected tx reporting cli options from Configuration object + * + * @param config An instance of LineaRejectedTxReportingConfiguration + */ + public static LineaRejectedTxReportingCliOptions fromConfig( + final LineaRejectedTxReportingConfiguration config) { + final LineaRejectedTxReportingCliOptions options = create(); + options.rejectedTxEndpoint = config.rejectedTxEndpoint(); + options.lineaNodeType = config.lineaNodeType(); + return options; + } + + @Override + public LineaRejectedTxReportingConfiguration toDomainObject() { + // perform validation here, if endpoint is specified then node type is required. + // We can ignore node type if endpoint is not specified. + if (rejectedTxEndpoint != null && lineaNodeType == null) { + throw new IllegalArgumentException( + "Error: Missing required argument(s): " + LINEA_NODE_TYPE + "="); + } + + return LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(rejectedTxEndpoint) + .lineaNodeType(lineaNodeType) + .build(); + } + + @Override + public String toString() { + + return MoreObjects.toStringHelper(this) + .add(REJECTED_TX_ENDPOINT, rejectedTxEndpoint) + .add(LINEA_NODE_TYPE, lineaNodeType) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java new file mode 100644 index 00000000..607d0410 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.net.URL; + +import lombok.Builder; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** Linea Rejected Transactions Reporting Configuration */ +@Builder(toBuilder = true) +public record LineaRejectedTxReportingConfiguration( + URL rejectedTxEndpoint, LineaNodeType lineaNodeType) implements LineaOptionsConfiguration {} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java new file mode 100644 index 00000000..97b99f15 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcCliOptions.java @@ -0,0 +1,96 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.math.BigDecimal; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +/** The Linea RPC CLI options. */ +public class LineaRpcCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "rpc-config-sequencer"; + + private static final String ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED = + "--plugin-linea-estimate-gas-compatibility-mode-enabled"; + private static final boolean DEFAULT_ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED = false; + private static final String ESTIMATE_GAS_COMPATIBILITY_MODE_MULTIPLIER = + "--plugin-linea-estimate-gas-compatibility-mode-multiplier"; + private static final BigDecimal DEFAULT_ESTIMATE_GAS_COMPATIBILITY_MODE_MULTIPLIER = + BigDecimal.valueOf(1.2); + + @CommandLine.Option( + names = {ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED}, + paramLabel = "", + description = + "Set to true to return the min mineable gas price * multiplier, instead of the profitable price (default: ${DEFAULT-VALUE})") + private boolean estimateGasCompatibilityModeEnabled = + DEFAULT_ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED; + + @CommandLine.Option( + names = {ESTIMATE_GAS_COMPATIBILITY_MODE_MULTIPLIER}, + paramLabel = "", + description = + "Set to multiplier to apply to the min priority fee per gas when the compatibility mode is enabled (default: ${DEFAULT-VALUE})") + private BigDecimal estimateGasCompatibilityMultiplier = + DEFAULT_ESTIMATE_GAS_COMPATIBILITY_MODE_MULTIPLIER; + + private LineaRpcCliOptions() {} + + /** + * Create Linea RPC CLI options. + * + * @return the Linea RPC CLI options + */ + public static LineaRpcCliOptions create() { + return new LineaRpcCliOptions(); + } + + /** + * Linea RPC CLI options from config. + * + * @param config the config + * @return the Linea RPC CLI options + */ + public static LineaRpcCliOptions fromConfig(final LineaRpcConfiguration config) { + final LineaRpcCliOptions options = create(); + options.estimateGasCompatibilityModeEnabled = config.estimateGasCompatibilityModeEnabled(); + options.estimateGasCompatibilityMultiplier = config.estimateGasCompatibilityMultiplier(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaRpcConfiguration toDomainObject() { + return LineaRpcConfiguration.builder() + .estimateGasCompatibilityModeEnabled(estimateGasCompatibilityModeEnabled) + .estimateGasCompatibilityMultiplier(estimateGasCompatibilityMultiplier) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(ESTIMATE_GAS_COMPATIBILITY_MODE_ENABLED, estimateGasCompatibilityModeEnabled) + .add(ESTIMATE_GAS_COMPATIBILITY_MODE_MULTIPLIER, estimateGasCompatibilityMultiplier) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java new file mode 100644 index 00000000..79debd95 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaRpcConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.math.BigDecimal; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** The Linea RPC configuration. */ +@Builder(toBuilder = true) +@Accessors(fluent = true) +@Getter +@ToString +public class LineaRpcConfiguration implements LineaOptionsConfiguration { + @Setter private volatile boolean estimateGasCompatibilityModeEnabled; + private BigDecimal estimateGasCompatibilityMultiplier; +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java new file mode 100644 index 00000000..54ad124e --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerCliOptions.java @@ -0,0 +1,74 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +public class LineaTracerCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "tracer-config"; + + public static final String MODULE_LIMIT_FILE_PATH = "--plugin-linea-module-limit-file-path"; + public static final String DEFAULT_MODULE_LIMIT_FILE_PATH = "moduleLimitFile.toml"; + + @CommandLine.Option( + names = {MODULE_LIMIT_FILE_PATH}, + hidden = true, + paramLabel = "", + description = + "Path to the toml file containing the module limits (default: ${DEFAULT-VALUE})") + private String moduleLimitFilePath = DEFAULT_MODULE_LIMIT_FILE_PATH; + + private LineaTracerCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaTracerCliOptions create() { + return new LineaTracerCliOptions(); + } + + /** + * Linea cli options from config. + * + * @param config the config + * @return the Linea cli options + */ + public static LineaTracerCliOptions fromConfig(final LineaTracerConfiguration config) { + final LineaTracerCliOptions options = create(); + options.moduleLimitFilePath = config.moduleLimitsFilePath(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaTracerConfiguration toDomainObject() { + return LineaTracerConfiguration.builder().moduleLimitsFilePath(moduleLimitFilePath).build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(MODULE_LIMIT_FILE_PATH, moduleLimitFilePath) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java new file mode 100644 index 00000000..909817c2 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTracerConfiguration.java @@ -0,0 +1,24 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import lombok.Builder; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** The Linea tracer configuration. */ +@Builder(toBuilder = true) +public record LineaTracerConfiguration(String moduleLimitsFilePath) + implements LineaOptionsConfiguration {} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java new file mode 100644 index 00000000..20f6bb99 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorCliOptions.java @@ -0,0 +1,142 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +/** The Linea CLI options. */ +public class LineaTransactionPoolValidatorCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "transaction-pool-validator-config"; + + public static final String DENY_LIST_PATH = "--plugin-linea-deny-list-path"; + public static final String DEFAULT_DENY_LIST_PATH = "lineaDenyList.txt"; + + public static final String MAX_TX_GAS_LIMIT_OPTION = "--plugin-linea-max-tx-gas-limit"; + public static final int DEFAULT_MAX_TRANSACTION_GAS_LIMIT = 30_000_000; + + public static final String MAX_TX_CALLDATA_SIZE = "--plugin-linea-max-tx-calldata-size"; + public static final int DEFAULT_MAX_TX_CALLDATA_SIZE = 60_000; + + public static final String TX_POOL_ENABLE_SIMULATION_CHECK_API = + "--plugin-linea-tx-pool-simulation-check-api-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_API = false; + + public static final String TX_POOL_ENABLE_SIMULATION_CHECK_P2P = + "--plugin-linea-tx-pool-simulation-check-p2p-enabled"; + public static final boolean DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_P2P = false; + + @CommandLine.Option( + names = {DENY_LIST_PATH}, + hidden = true, + paramLabel = "", + description = + "Path to the file containing the deny list (default: " + DEFAULT_DENY_LIST_PATH + ")") + private String denyListPath = DEFAULT_DENY_LIST_PATH; + + @CommandLine.Option( + names = {MAX_TX_GAS_LIMIT_OPTION}, + hidden = true, + paramLabel = "", + description = + "Maximum gas limit for a transaction (default: " + + DEFAULT_MAX_TRANSACTION_GAS_LIMIT + + ")") + private int maxTxGasLimit = DEFAULT_MAX_TRANSACTION_GAS_LIMIT; + + @CommandLine.Option( + names = {MAX_TX_CALLDATA_SIZE}, + hidden = true, + paramLabel = "", + description = + "Maximum size for the calldata of a Transaction (default: " + + DEFAULT_MAX_TX_CALLDATA_SIZE + + ")") + private int maxTxCallDataSize = DEFAULT_MAX_TX_CALLDATA_SIZE; + + @CommandLine.Option( + names = {TX_POOL_ENABLE_SIMULATION_CHECK_API}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the simulation check for txs received via API? (default: ${DEFAULT-VALUE})") + private boolean txPoolSimulationCheckApiEnabled = DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_API; + + @CommandLine.Option( + names = {TX_POOL_ENABLE_SIMULATION_CHECK_P2P}, + arity = "0..1", + hidden = true, + paramLabel = "", + description = + "Enable the simulation check for txs received via p2p? (default: ${DEFAULT-VALUE})") + private boolean txPoolSimulationCheckP2pEnabled = DEFAULT_TX_POOL_ENABLE_SIMULATION_CHECK_P2P; + + private LineaTransactionPoolValidatorCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaTransactionPoolValidatorCliOptions create() { + return new LineaTransactionPoolValidatorCliOptions(); + } + + /** + * Cli options from config. + * + * @param config the config + * @return the cli options + */ + public static LineaTransactionPoolValidatorCliOptions fromConfig( + final LineaTransactionPoolValidatorConfiguration config) { + final LineaTransactionPoolValidatorCliOptions options = create(); + options.denyListPath = config.denyListPath(); + options.maxTxGasLimit = config.maxTxGasLimit(); + options.maxTxCallDataSize = config.maxTxCalldataSize(); + options.txPoolSimulationCheckApiEnabled = config.txPoolSimulationCheckApiEnabled(); + options.txPoolSimulationCheckP2pEnabled = config.txPoolSimulationCheckP2pEnabled(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaTransactionPoolValidatorConfiguration toDomainObject() { + return new LineaTransactionPoolValidatorConfiguration( + denyListPath, + maxTxGasLimit, + maxTxCallDataSize, + txPoolSimulationCheckApiEnabled, + txPoolSimulationCheckP2pEnabled); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(DENY_LIST_PATH, denyListPath) + .add(MAX_TX_GAS_LIMIT_OPTION, maxTxGasLimit) + .add(MAX_TX_CALLDATA_SIZE, maxTxCallDataSize) + .add(TX_POOL_ENABLE_SIMULATION_CHECK_API, txPoolSimulationCheckApiEnabled) + .add(TX_POOL_ENABLE_SIMULATION_CHECK_P2P, txPoolSimulationCheckP2pEnabled) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java new file mode 100644 index 00000000..f08b57e0 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionPoolValidatorConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import lombok.Builder; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** + * The Linea transaction pool validation configuration. + * + * @param denyListPath the path to the file containing the addresses that are denied. + * @param maxTxGasLimit the maximum gas limit allowed for transactions + * @param maxTxCalldataSize the maximum size of calldata allowed for transactions + */ +@Builder(toBuilder = true) +public record LineaTransactionPoolValidatorConfiguration( + String denyListPath, + int maxTxGasLimit, + int maxTxCalldataSize, + boolean txPoolSimulationCheckApiEnabled, + boolean txPoolSimulationCheckP2pEnabled) + implements LineaOptionsConfiguration {} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java new file mode 100644 index 00000000..d343a5d2 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java @@ -0,0 +1,166 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import com.google.common.base.MoreObjects; +import jakarta.validation.constraints.Positive; +import net.consensys.linea.plugins.LineaCliOptions; +import picocli.CommandLine; + +/** The Linea Transaction Selector CLI options. */ +public class LineaTransactionSelectorCliOptions implements LineaCliOptions { + public static final String CONFIG_KEY = "transaction-selector-config"; + + public static final String MAX_BLOCK_CALLDATA_SIZE = "--plugin-linea-max-block-calldata-size"; + public static final int DEFAULT_MAX_BLOCK_CALLDATA_SIZE = 70_000; + public static final String OVER_LINE_COUNT_LIMIT_CACHE_SIZE = + "--plugin-linea-over-line-count-limit-cache-size"; + public static final int DEFAULT_OVER_LINE_COUNT_LIMIT_CACHE_SIZE = 10_000; + + public static final String MAX_GAS_PER_BLOCK = "--plugin-linea-max-block-gas"; + public static final long DEFAULT_MAX_GAS_PER_BLOCK = 30_000_000L; + public static final String MAX_BUNDLE_POOL_SIZE_BYTES = + "--plugin-linea-max-bundle-pool-size-bytes"; + public static final long DEFAULT_MAX_BUNDLE_POOL_SIZE_BYTES = 1024 * 1024 * 16L; + public static final String MAX_BUNDLE_GAS_PER_BLOCK = "--plugin-linea-max-bundle-block-gas"; + public static final long DEFAULT_MAX_BUNDLE_GAS_PER_BLOCK = 15_000_000L; + + public static final String UNPROFITABLE_CACHE_SIZE = "--plugin-linea-unprofitable-cache-size"; + public static final int DEFAULT_UNPROFITABLE_CACHE_SIZE = 100_000; + + public static final String UNPROFITABLE_RETRY_LIMIT = "--plugin-linea-unprofitable-retry-limit"; + public static final int DEFAULT_UNPROFITABLE_RETRY_LIMIT = 10; + + @Positive + @CommandLine.Option( + names = {MAX_BLOCK_CALLDATA_SIZE}, + hidden = true, + paramLabel = "", + description = "Maximum size for the calldata of a block (default: ${DEFAULT-VALUE})") + private int maxBlockCallDataSize = DEFAULT_MAX_BLOCK_CALLDATA_SIZE; + + @Positive + @CommandLine.Option( + names = {OVER_LINE_COUNT_LIMIT_CACHE_SIZE}, + hidden = true, + paramLabel = "", + description = + "Max number of transactions that go over the line count limit we keep track of (default: ${DEFAULT-VALUE})") + private int overLineCountLimitCacheSize = DEFAULT_OVER_LINE_COUNT_LIMIT_CACHE_SIZE; + + @Positive + @CommandLine.Option( + names = {MAX_GAS_PER_BLOCK}, + hidden = true, + paramLabel = "", + description = "Sets max gas per block (default: ${DEFAULT-VALUE})") + private Long maxGasPerBlock = DEFAULT_MAX_GAS_PER_BLOCK; + + @Positive + @CommandLine.Option( + names = {MAX_BUNDLE_POOL_SIZE_BYTES}, + hidden = true, + paramLabel = "", + description = + "Sets max memory size, in bytes, that the bundle txpool can occupy (default: ${DEFAULT-VALUE})") + public Long maxBundlePoolSizeBytes = DEFAULT_MAX_BUNDLE_POOL_SIZE_BYTES; + + @Positive + @CommandLine.Option( + names = {MAX_BUNDLE_GAS_PER_BLOCK}, + hidden = true, + paramLabel = "", + description = + "Sets max amount of block gas bundle transactions can use (default: ${DEFAULT-VALUE})") + public Long maxBundleGasPerBlock = DEFAULT_MAX_BUNDLE_GAS_PER_BLOCK; + + @Positive + @CommandLine.Option( + names = {UNPROFITABLE_CACHE_SIZE}, + hidden = true, + paramLabel = "", + description = + "Max number of unprofitable transactions we keep track of (default: ${DEFAULT-VALUE})") + private int unprofitableCacheSize = DEFAULT_UNPROFITABLE_CACHE_SIZE; + + @Positive + @CommandLine.Option( + names = {UNPROFITABLE_RETRY_LIMIT}, + hidden = true, + paramLabel = "", + description = + "Max number of unprofitable transactions we retry on each block creation (default: ${DEFAULT-VALUE})") + private int unprofitableRetryLimit = DEFAULT_UNPROFITABLE_RETRY_LIMIT; + + private LineaTransactionSelectorCliOptions() {} + + /** + * Create Linea cli options. + * + * @return the Linea cli options + */ + public static LineaTransactionSelectorCliOptions create() { + return new LineaTransactionSelectorCliOptions(); + } + + /** + * Linea cli options from config. + * + * @param config the config + * @return the Linea cli options + */ + public static LineaTransactionSelectorCliOptions fromConfig( + final LineaTransactionSelectorConfiguration config) { + final LineaTransactionSelectorCliOptions options = create(); + options.maxBlockCallDataSize = config.maxBlockCallDataSize(); + options.overLineCountLimitCacheSize = config.overLinesLimitCacheSize(); + options.maxGasPerBlock = config.maxGasPerBlock(); + options.unprofitableCacheSize = config.unprofitableCacheSize(); + options.unprofitableRetryLimit = config.unprofitableRetryLimit(); + return options; + } + + /** + * To domain object Linea factory configuration. + * + * @return the Linea factory configuration + */ + @Override + public LineaTransactionSelectorConfiguration toDomainObject() { + return LineaTransactionSelectorConfiguration.builder() + .maxBlockCallDataSize(maxBlockCallDataSize) + .overLinesLimitCacheSize(overLineCountLimitCacheSize) + .maxGasPerBlock(maxGasPerBlock) + .unprofitableCacheSize(unprofitableCacheSize) + .unprofitableRetryLimit(unprofitableRetryLimit) + .maxBundleGasPerBlock(maxBundleGasPerBlock) + .maxBundlePoolSizeBytes(maxBundlePoolSizeBytes) + .build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add(MAX_BLOCK_CALLDATA_SIZE, maxBlockCallDataSize) + .add(OVER_LINE_COUNT_LIMIT_CACHE_SIZE, overLineCountLimitCacheSize) + .add(MAX_GAS_PER_BLOCK, maxGasPerBlock) + .add(UNPROFITABLE_CACHE_SIZE, unprofitableCacheSize) + .add(UNPROFITABLE_RETRY_LIMIT, unprofitableRetryLimit) + .add(MAX_BUNDLE_GAS_PER_BLOCK, maxBundleGasPerBlock) + .add(MAX_BUNDLE_POOL_SIZE_BYTES, maxBundlePoolSizeBytes) + .toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java new file mode 100644 index 00000000..0f0c4613 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import lombok.Builder; +import net.consensys.linea.plugins.LineaOptionsConfiguration; + +/** The Linea transaction selectors configuration. */ +@Builder(toBuilder = true) +public record LineaTransactionSelectorConfiguration( + int maxBlockCallDataSize, + int overLinesLimitCacheSize, + long maxGasPerBlock, + int unprofitableCacheSize, + int unprofitableRetryLimit, + long maxBundleGasPerBlock, + long maxBundlePoolSizeBytes) + implements LineaOptionsConfiguration {} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/AddressConverter.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/AddressConverter.java new file mode 100644 index 00000000..a1c3c457 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/AddressConverter.java @@ -0,0 +1,25 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config.converters; + +import org.hyperledger.besu.datatypes.Address; +import picocli.CommandLine; + +public class AddressConverter implements CommandLine.ITypeConverter

{ + @Override + public Address convert(final String s) throws Exception { + return Address.fromHexString(s); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/BytesConverter.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/BytesConverter.java new file mode 100644 index 00000000..28d73c01 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/BytesConverter.java @@ -0,0 +1,25 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config.converters; + +import org.apache.tuweni.bytes.Bytes; +import picocli.CommandLine; + +public class BytesConverter implements CommandLine.ITypeConverter { + @Override + public Bytes convert(final String s) throws Exception { + return Bytes.fromHexStringLenient(s); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/WeiConverter.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/WeiConverter.java new file mode 100644 index 00000000..33ea6618 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/converters/WeiConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config.converters; + +import java.math.BigInteger; + +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Wei; +import picocli.CommandLine; + +public class WeiConverter implements CommandLine.ITypeConverter { + @Override + public Bytes convert(final String s) throws Exception { + return Wei.of(new BigInteger(s)); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataException.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataException.java new file mode 100644 index 00000000..8cec405a --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataException.java @@ -0,0 +1,49 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.extradata; + +public class LineaExtraDataException extends RuntimeException { + public enum ErrorType { + INVALID_ARGUMENT(-32602), + FAILED_CALLING_SET_MIN_GAS_PRICE(-32000), + FAILED_CALLING_SET_EXTRA_DATA(-32000); + + private final int code; + + ErrorType(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + } + + private final ErrorType errorType; + + public LineaExtraDataException(final ErrorType errorType, final String message) { + super(message); + this.errorType = errorType; + } + + public ErrorType getErrorType() { + return errorType; + } + + @Override + public String toString() { + return "errorType=" + errorType + ", message=" + getMessage(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataHandler.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataHandler.java new file mode 100644 index 00000000..32f05c75 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataHandler.java @@ -0,0 +1,171 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.extradata; + +import java.util.function.Consumer; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import org.apache.commons.lang3.mutable.MutableLong; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.units.bigints.UInt32; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.services.RpcEndpointService; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +/** + * Handles the Linea extra data custom extension. + * + *

In Linea the extra data field is used to distribute pricing config, it has a standard format, + * it is versioned to support for changes in the format. + * + *

The version is the first byte of the extra data, currently on version 1 exists, in case the + * version byte is not recognized as supported, then the extra data is simply ignored. + */ +@Slf4j +public class LineaExtraDataHandler { + private final RpcEndpointService rpcEndpointService; + private final ExtraDataConsumer[] extraDataConsumers; + + public LineaExtraDataHandler( + final RpcEndpointService rpcEndpointService, + final LineaProfitabilityConfiguration profitabilityConf) { + this.rpcEndpointService = rpcEndpointService; + this.extraDataConsumers = new ExtraDataConsumer[] {new Version1Consumer(profitabilityConf)}; + } + + /** + * Handles the extra data, first tries to see if it has a supported format, if so the bytes are + * processed according to that format. + * + * @param rawExtraData the extra data bytes + * @throws LineaExtraDataException if the format of the extra data is invalid + */ + public void handle(final Bytes rawExtraData) throws LineaExtraDataException { + + if (!Bytes.EMPTY.equals(rawExtraData)) { + for (final ExtraDataConsumer extraDataConsumer : extraDataConsumers) { + if (extraDataConsumer.canConsume(rawExtraData)) { + // strip first byte since it is the version already used to select the actual consumer + final var extraData = rawExtraData.slice(1); + extraDataConsumer.accept(extraData); + return; + } + } + throw new LineaExtraDataException( + LineaExtraDataException.ErrorType.INVALID_ARGUMENT, + "Unsupported extra data field " + rawExtraData.toHexString()); + } + } + + /** A consumer of a specific version of the extra data format */ + private interface ExtraDataConsumer extends Consumer { + + /** + * Is this consumer able to process the given extra data? + * + * @param extraData extra data bytes + * @return true if this consumer can process the extra data + */ + boolean canConsume(Bytes extraData); + + static Long toLong(final Bytes fieldBytes) { + return UInt32.fromBytes(fieldBytes).toLong(); + } + } + + /** + * Handles a version 1 extra data field and on successful parsing it updates the pricing config + * and the min gas price + * + *

Version 1 has this format: + * + *

VERSION (1 byte) FIXED_COST (4 bytes) VARIABLE_COST (4 bytes) ETH_GAS_PRICE (4 bytes) + */ + @SuppressWarnings("rawtypes") + private class Version1Consumer implements ExtraDataConsumer { + private static final int WEI_IN_KWEI = 1_000; + private final LineaProfitabilityConfiguration profitabilityConf; + private final FieldConsumer[] fieldsSequence; + private final MutableLong currFixedCostKWei = new MutableLong(); + private final MutableLong currVariableCostKWei = new MutableLong(); + private final MutableLong currEthGasPriceKWei = new MutableLong(); + + public Version1Consumer(final LineaProfitabilityConfiguration profitabilityConf) { + this.profitabilityConf = profitabilityConf; + + final FieldConsumer fixedGasCostField = + new FieldConsumer<>( + "fixedGasCost", 4, ExtraDataConsumer::toLong, currFixedCostKWei::setValue); + final FieldConsumer variableGasCostField = + new FieldConsumer<>( + "variableGasCost", 4, ExtraDataConsumer::toLong, currVariableCostKWei::setValue); + final FieldConsumer ethGasPriceField = + new FieldConsumer<>("ethGasPrice", 4, ExtraDataConsumer::toLong, this::updateEthGasPrice); + + this.fieldsSequence = + new FieldConsumer[] {fixedGasCostField, variableGasCostField, ethGasPriceField}; + } + + public boolean canConsume(final Bytes rawExtraData) { + return rawExtraData.get(0) == (byte) 1; + } + + public synchronized void accept(final Bytes extraData) { + log.debug("Parsing extra data version 1: {}", extraData.toHexString()); + int startIndex = 0; + for (final FieldConsumer fieldConsumer : fieldsSequence) { + fieldConsumer.accept(extraData.slice(startIndex, fieldConsumer.length)); + startIndex += fieldConsumer.length; + } + + profitabilityConf.updateFixedVariableAndGasPrice( + currFixedCostKWei.longValue() * WEI_IN_KWEI, + currVariableCostKWei.longValue() * WEI_IN_KWEI, + currEthGasPriceKWei.longValue() * WEI_IN_KWEI); + } + + void updateEthGasPrice(final Long ethGasPriceKWei) { + currEthGasPriceKWei.setValue(ethGasPriceKWei); + if (profitabilityConf.extraDataSetMinGasPriceEnabled()) { + final var minGasPriceWei = Wei.of(ethGasPriceKWei).multiply(WEI_IN_KWEI); + final var resp = + rpcEndpointService.call( + "miner_setMinGasPrice", new Object[] {minGasPriceWei.toShortHexString()}); + if (!resp.getType().equals(RpcResponseType.SUCCESS)) { + throw new LineaExtraDataException( + LineaExtraDataException.ErrorType.FAILED_CALLING_SET_MIN_GAS_PRICE, + "Internal setMinGasPrice method failed: " + resp); + } + } else { + log.trace("Setting minGasPrice from extraData is disabled by conf"); + } + } + } + + private record FieldConsumer( + String name, int length, Function converter, Consumer consumer) + implements Consumer { + + @Override + public void accept(final Bytes fieldBytes) { + final var converted = converter.apply(fieldBytes); + log.debug("Field {}={} (raw bytes: {})", name, converted, fieldBytes.toHexString()); + consumer.accept(converted); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java new file mode 100644 index 00000000..9dee7fd8 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java @@ -0,0 +1,152 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.extradata; + +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; + +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; +import org.hyperledger.besu.plugin.data.AddedBlockContext; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BesuEvents.InitialSyncCompletionListener; + +/** This plugin registers handlers that are activated when new blocks are imported */ +@Slf4j +@AutoService(BesuPlugin.class) +public class LineaExtraDataPlugin extends AbstractLineaRequiredPlugin { + private ServiceManager serviceManager; + + @Override + public void doRegister(final ServiceManager context) { + serviceManager = context; + + metricCategoryRegistry.addMetricCategory(PRICING_CONF); + } + + /** + * Starts this plugin and in case the extra data pricing is enabled, as first thing it tries to + * extract extra data pricing configuration from the chain head, then it starts listening for new + * imported block, in order to update the extra data pricing on every incoming block. + */ + @Override + public void doStart() { + if (profitabilityConfiguration().extraDataPricingEnabled()) { + final var besuEventsService = + serviceManager + .getService(BesuEvents.class) + .orElseThrow( + () -> + new RuntimeException("Failed to obtain BesuEvents from the ServiceManager.")); + + // assume that we are in sync by default to support reading extra data at genesis + final AtomicBoolean inSync = new AtomicBoolean(true); + + besuEventsService.addSyncStatusListener( + maybeSyncStatus -> + inSync.set( + maybeSyncStatus + .map( + syncStatus -> + syncStatus.getHighestBlock() - syncStatus.getCurrentBlock() < 5) + .orElse(true))); + + // wait for the initial sync phase to complete before starting parsing extra data + // to avoid parsing errors + besuEventsService.addInitialSyncCompletionListener( + new InitialSyncCompletionListener() { + long blockAddedListenerId = -1; + + @Override + public synchronized void onInitialSyncCompleted() { + blockAddedListenerId = enableExtraDataHandling(besuEventsService, inSync); + } + + @Override + public synchronized void onInitialSyncRestart() { + besuEventsService.removeBlockAddedListener(blockAddedListenerId); + blockAddedListenerId = -1; + } + }); + } + + if (metricCategoryRegistry.isMetricCategoryEnabled(PRICING_CONF)) { + initMetrics(profitabilityConfiguration()); + } + } + + private void initMetrics(final LineaProfitabilityConfiguration lineaProfitabilityConfiguration) { + final var confLabelledGauge = + metricsSystem.createLabelledSuppliedGauge( + PRICING_CONF, "values", "Profitability configuration values at runtime", "field"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::fixedCostWei, "fixed_cost_wei"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::variableCostWei, "variable_cost_wei"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::ethGasPriceWei, "eth_gas_price_wei"); + } + + private long enableExtraDataHandling( + final BesuEvents besuEventsService, final AtomicBoolean inSync) { + + final var extraDataHandler = + new LineaExtraDataHandler(rpcEndpointService, profitabilityConfiguration()); + + if (inSync.get()) { + final var chainHeadHeader = blockchainService.getChainHeadHeader(); + final var initialExtraData = chainHeadHeader.getExtraData(); + try { + extraDataHandler.handle(initialExtraData); + } catch (final Exception e) { + // this could normally happen if for example the genesis block has not a valid extra data + // field so we keep this log at debug + log.debug( + "Failed setting initial pricing conf from extra data field ({}) of the chain head block {}({})", + initialExtraData, + chainHeadHeader.getNumber(), + chainHeadHeader.getBlockHash(), + e); + } + } + + return besuEventsService.addBlockAddedListener( + addedBlockContext -> { + if (inSync.get()) { + processNewBlock(extraDataHandler, addedBlockContext); + } + }); + } + + private void processNewBlock( + final LineaExtraDataHandler extraDataHandler, final AddedBlockContext addedBlockContext) { + final var importedBlockHeader = addedBlockContext.getBlockHeader(); + final var latestExtraData = importedBlockHeader.getExtraData(); + + try { + extraDataHandler.handle(latestExtraData); + } catch (final Exception e) { + log.warn( + "Failed setting pricing conf from extra data field ({}) of latest imported block {}({})", + latestExtraData, + importedBlockHeader.getNumber(), + importedBlockHeader.getBlockHash(), + e); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java new file mode 100644 index 00000000..7110bcc9 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -0,0 +1,315 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.jsonrpc; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.Map; +import java.util.TreeSet; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** This class is responsible for managing JSON-RPC requests for reporting rejected transactions */ +@Slf4j +public class JsonRpcManager { + private static final Duration INITIAL_RETRY_DELAY_DURATION = Duration.ofSeconds(1); + private static final Duration MAX_RETRY_DURATION = Duration.ofHours(2); + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + static final String JSON_RPC_DIR = "rej-tx-rpc"; + static final String DISCARDED_DIR = "discarded"; + + private final OkHttpClient client = new OkHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final Map fileStartTimes = new ConcurrentHashMap<>(); + + private final Path jsonRpcDir; + private final LineaRejectedTxReportingConfiguration reportingConfiguration; + private final ExecutorService executorService; + private final ScheduledExecutorService retrySchedulerService; + + /** + * Creates a new JSON-RPC manager. + * + * @param pluginIdentifier The plugin identifier will be created as a sub-directory under + * rej-tx-rpc. The rejected transactions will be stored under it for each plugin that uses it. + * @param besuDataDir Path to Besu data directory. The json-rpc files will be stored here under + * rej-tx-rpc subdirectory. + * @param reportingConfiguration Instance of LineaRejectedTxReportingConfiguration containing the + * endpoint URI and node type. + */ + public JsonRpcManager( + @NonNull final String pluginIdentifier, + @NonNull final Path besuDataDir, + @NonNull final LineaRejectedTxReportingConfiguration reportingConfiguration) { + if (reportingConfiguration.rejectedTxEndpoint() == null) { + throw new IllegalStateException("Rejected transaction endpoint URI is required"); + } + this.jsonRpcDir = besuDataDir.resolve(JSON_RPC_DIR).resolve(pluginIdentifier); + this.reportingConfiguration = reportingConfiguration; + this.executorService = Executors.newVirtualThreadPerTaskExecutor(); + this.retrySchedulerService = Executors.newSingleThreadScheduledExecutor(); + } + + /** Load existing JSON-RPC and submit them. */ + public JsonRpcManager start() { + try { + // Create the rej-tx-rpc/pluginIdentifier/discarded directories if it doesn't exist + Files.createDirectories(jsonRpcDir.resolve(DISCARDED_DIR)); + + // Load existing JSON files + processExistingJsonFiles(); + return this; + } catch (final IOException e) { + log.error("Failed to create or access directories under: {}", jsonRpcDir, e); + throw new UncheckedIOException(e); + } + } + + /** Shuts down the executor service and scheduler service. */ + public void shutdown() { + executorService.shutdown(); + retrySchedulerService.shutdown(); + } + + /** + * Submits a new JSON-RPC call. + * + * @param jsonContent The JSON content to submit + */ + public void submitNewJsonRpcCallAsync(final String jsonContent) { + CompletableFuture.supplyAsync( + () -> { + try { + Path jsonFile = saveJsonToDir(jsonContent, jsonRpcDir); + fileStartTimes.put(jsonFile, Instant.now()); + return jsonFile; + } catch (final IOException e) { + log.error("Failed to save JSON-RPC content", e); + throw new CompletionException(e); + } + }, + executorService) + .thenAcceptAsync( + jsonFile -> submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION), executorService) + .exceptionally( + e -> { + log.error("Error in submitNewJsonRpcCall", e); + return null; + }); + } + + public LineaNodeType getNodeType() { + return reportingConfiguration.lineaNodeType(); + } + + private void processExistingJsonFiles() { + try { + final TreeSet sortedFiles = new TreeSet<>(Comparator.comparing(Path::getFileName)); + + try (DirectoryStream stream = Files.newDirectoryStream(jsonRpcDir, "rpc_*.json")) { + for (Path path : stream) { + sortedFiles.add(path); + } + } + + log.info("Loaded {} existing JSON-RPC files for reporting", sortedFiles.size()); + + for (Path path : sortedFiles) { + fileStartTimes.put(path, Instant.now()); + submitJsonRpcCall(path, INITIAL_RETRY_DELAY_DURATION); + } + } catch (final IOException e) { + log.error("Failed to load existing JSON-RPC files", e); + } + } + + private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { + executorService.submit( + () -> { + if (!Files.exists(jsonFile)) { + log.debug("JSON-RPC file {} no longer exists, skipping processing.", jsonFile); + fileStartTimes.remove(jsonFile); + return; + } + try { + final String jsonContent = new String(Files.readAllBytes(jsonFile)); + final boolean success = sendJsonRpcCall(jsonContent); + if (success) { + Files.deleteIfExists(jsonFile); + fileStartTimes.remove(jsonFile); + } else { + log.error( + "Failed to send JSON-RPC file {} to {}, Scheduling retry ...", + jsonFile, + reportingConfiguration.rejectedTxEndpoint()); + scheduleRetry(jsonFile, nextDelay); + } + } catch (final Exception e) { + log.error( + "Failed to process JSON-RPC file {} due to unexpected error: {}. Scheduling retry ...", + jsonFile, + e.getMessage()); + scheduleRetry(jsonFile, nextDelay); + } + }); + } + + private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { + final Instant startTime = fileStartTimes.get(jsonFile); + if (startTime == null) { + log.debug("No start time found for JSON-RPC file: {}. Skipping retry.", jsonFile); + return; + } + + // Check if we're still within the maximum retry duration + if (Duration.between(startTime, Instant.now()).compareTo(MAX_RETRY_DURATION) < 0) { + // Calculate next delay with exponential backoff, capped at 1 minute + final Duration nextDelay = + Duration.ofMillis( + Math.min(currentDelay.multipliedBy(2).toMillis(), Duration.ofMinutes(1).toMillis())); + + // Schedule a retry + retrySchedulerService.schedule( + () -> submitJsonRpcCall(jsonFile, nextDelay), + currentDelay.toMillis(), + TimeUnit.MILLISECONDS); + } else { + log.error("Exceeded maximum retry duration for JSON-RPC file: {}.", jsonFile); + final Path destination = jsonRpcDir.resolve(DISCARDED_DIR).resolve(jsonFile.getFileName()); + + try { + Files.move(jsonFile, destination, StandardCopyOption.REPLACE_EXISTING); + log.error( + "The JSON-RPC file {} has been moved to: {}. The tx notification has been discarded.", + jsonFile, + destination); + } catch (final IOException e) { + log.error("Failed to move JSON-RPC file to discarded directory: {}", jsonFile, e); + } finally { + fileStartTimes.remove(jsonFile); + } + } + } + + private boolean sendJsonRpcCall(final String jsonContent) { + final RequestBody body = RequestBody.create(jsonContent, JSON); + final Request request = + new Request.Builder().url(reportingConfiguration.rejectedTxEndpoint()).post(body).build(); + + try (final Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + log.error("Unexpected response code from rejected-tx endpoint: {}", response.code()); + return false; + } + + // process the response body here ... + final String responseBody = response.body() != null ? response.body().string() : null; + if (responseBody == null) { + log.error("Unexpected empty response body from rejected-tx endpoint"); + return false; + } + + final JsonNode jsonNode = objectMapper.readTree(responseBody); + if (jsonNode == null) { + log.error("Failed to parse JSON response from rejected-tx endpoint: {}", responseBody); + return false; + } + if (jsonNode.has("error")) { + log.error("Error response from rejected-tx endpoint: {}", jsonNode.get("error")); + return false; + } + // Check for result + if (jsonNode.has("result")) { + final String status = jsonNode.get("result").get("status").asText(); + log.debug("Rejected-tx JSON-RPC call successful. Status: {}", status); + return true; + } + + log.warn("Unexpected rejected-tx JSON-RPC response format: {}", responseBody); + return false; + } catch (final IOException e) { + log.error( + "Failed to send JSON-RPC call to rejected-tx endpoint {}", + reportingConfiguration.rejectedTxEndpoint(), + e); + return false; + } + } + + /** + * Saves the given JSON content to a file in the rejected transactions RPC directory. The filename + * is generated using a high-precision timestamp and a UUID to ensure uniqueness. + * + *

The file naming format is: rpc_[timestamp]_[uuid].json + * + * @param jsonContent The JSON string to be written to the file. + * @param rejTxRpcDirectory The directory where the file should be saved. + * @return The Path object representing the newly created file. + * @throws IOException If an I/O error occurs while writing the file, including unexpected file + * collisions. + */ + @VisibleForTesting + static Path saveJsonToDir(final String jsonContent, final Path rejTxRpcDirectory) + throws IOException { + final String timestamp = generateTimestampWithNanos(); + final String uuid = UUID.randomUUID().toString(); + final String fileName = String.format("rpc_%s_%s.json", timestamp, uuid); + final Path filePath = rejTxRpcDirectory.resolve(fileName); + + try { + return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); + } catch (final FileAlreadyExistsException e) { + // This should never happen with UUID, but just in case + log.warn("Unexpected JSON-RPC filename collision occurred: {}", filePath); + throw new IOException("Unexpected file name collision", e); + } + } + + static String generateTimestampWithNanos() { + final Instant now = Instant.now(); + final long seconds = now.getEpochSecond(); + final int nanos = now.getNano(); + return String.format("%d%09d", seconds, nanos); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java new file mode 100644 index 00000000..4bd4dc69 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -0,0 +1,100 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.jsonrpc; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import org.hyperledger.besu.datatypes.Transaction; + +/** + * Helper class to build JSON-RPC requests for rejected transactions. + * + *

+ * {@code linea_saveRejectedTransactionV1({
+ *         "txRejectionStage": "SEQUENCER/RPC/P2P",
+ *         "timestamp": "2024-08-22T09:18:51Z", # ISO8601 UTC+0 when tx was rejected by node, usefull if P2P edge node.
+ *         "blockNumber": "base 10 number",
+ *         "transactionRLP": "transaction as the user sent in eth_sendRawTransaction",
+ *         "reason": "Transaction line count for module ADD=402 is above the limit 70"
+ *         "overflows": [{
+ *           "module": "ADD",
+ *           "count": 402,
+ *           "limit": 70
+ *         }, {
+ *           "module": "MUL",
+ *           "count": 587,
+ *           "limit": 400
+ *         }]
+ *     })
+ * }
+ * 
+ */ +public class JsonRpcRequestBuilder { + private static final AtomicLong idCounter = new AtomicLong(1); + + /** + * Generate linea_saveRejectedTransactionV1 JSON-RPC request from given arguments. + * + * @param lineaNodeType Linea node type which is reporting the rejected transaction. + * @param transaction The rejected transaction. The encoded transaction RLP is used in the + * JSON-RPC request. + * @param timestamp The timestamp when the transaction was rejected. + * @param blockNumber Optional block number where the transaction was rejected. Used for sequencer + * node. + * @param reasonMessage The reason message for the rejection. + * @return JSON-RPC request as a string. + */ + public static String generateSaveRejectedTxJsonRpc( + final LineaNodeType lineaNodeType, + final Transaction transaction, + final Instant timestamp, + final Optional blockNumber, + final String reasonMessage, + final List overflowValidationResults) { + final JsonObject params = new JsonObject(); + params.addProperty("txRejectionStage", lineaNodeType.name()); + params.addProperty("timestamp", timestamp.toString()); + blockNumber.ifPresent(number -> params.addProperty("blockNumber", number)); + params.addProperty("transactionRLP", transaction.encoded().toHexString()); + params.addProperty("reasonMessage", reasonMessage); + + // overflows + final JsonArray overflows = new JsonArray(); + for (ModuleLimitsValidationResult result : overflowValidationResults) { + JsonObject overflow = new JsonObject(); + overflow.addProperty("module", result.getModuleName()); + overflow.addProperty("count", result.getModuleLineCount()); + overflow.addProperty("limit", result.getModuleLineLimit()); + overflows.add(overflow); + } + params.add("overflows", overflows); + + // request + final JsonObject request = new JsonObject(); + request.addProperty("jsonrpc", "2.0"); + request.addProperty("method", "linea_saveRejectedTransactionV1"); + request.add("params", params); + request.addProperty("id", idCounter.getAndIncrement()); + return request.toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/HistogramMetrics.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/HistogramMetrics.java new file mode 100644 index 00000000..a7702d38 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/HistogramMetrics.java @@ -0,0 +1,154 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.metrics; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.DoubleSupplier; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Histogram; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; + +@Slf4j +public class HistogramMetrics { + public interface LabelValue { + String value(); + } + + private static final String LABEL_VALUES_SEPARATOR = "\u2060"; + private final LabelledMetric histogram; + private final Map mins; + private final Map maxs; + + @SafeVarargs + public HistogramMetrics( + final MetricsSystem metricsSystem, + final LineaMetricCategory category, + final String name, + final String help, + final double[] buckets, + final Class... labels) { + + final var labelNames = getLabelNames(labels); + + final LabelledSuppliedMetric minRatio = + metricsSystem.createLabelledSuppliedGauge( + category, name + "_min", "Lowest " + help, labelNames); + + final LabelledSuppliedMetric maxRatio = + metricsSystem.createLabelledSuppliedGauge( + category, name + "_max", "Highest " + help, labelNames); + + final var combinations = getLabelValuesCombinations(labels); + mins = HashMap.newHashMap(combinations.size()); + maxs = HashMap.newHashMap(combinations.size()); + for (final var combination : combinations) { + final var key = String.join(LABEL_VALUES_SEPARATOR, combination); + final var minSupplier = new MutableDoubleSupplier(Double.POSITIVE_INFINITY); + mins.put(key, minSupplier); + minRatio.labels(minSupplier, combination); + final var maxSupplier = new MutableDoubleSupplier(Double.NEGATIVE_INFINITY); + maxs.put(key, maxSupplier); + maxRatio.labels(maxSupplier, combination); + } + + this.histogram = + metricsSystem.createLabelledHistogram( + category, name, StringUtils.capitalize(help) + " buckets", buckets, labelNames); + } + + @SafeVarargs + private String[] getLabelNames(final Class... labels) { + return Arrays.stream(labels) + .map(Class::getSimpleName) + .map(sn -> sn.toLowerCase(Locale.ROOT)) + .toArray(String[]::new); + } + + @SafeVarargs + private List getLabelValuesCombinations(final Class... labels) { + if (labels.length == 0) { + return Collections.singletonList(new String[0]); + } + if (labels.length == 1) { + return Arrays.stream(labels[0].getEnumConstants()) + .map(lv -> new String[] {lv.value()}) + .toList(); + } + final var head = labels[0]; + final var tail = Arrays.copyOfRange(labels, 1, labels.length); + final var tailCombinations = getLabelValuesCombinations(tail); + final int newSize = tailCombinations.size() * head.getEnumConstants().length; + final List combinations = new ArrayList<>(newSize); + for (final var headValue : head.getEnumConstants()) { + for (final var tailValues : tailCombinations) { + final var combination = new String[tailValues.length + 1]; + combination[0] = headValue.value(); + System.arraycopy(tailValues, 0, combination, 1, tailValues.length); + combinations.add(combination); + } + } + return combinations; + } + + public void track(final double value, final String... labelValues) { + + // Record the observation + histogram.labels(labelValues).observe(value); + } + + public void setMinMax(final double min, final double max, final String... labelValues) { + final var key = String.join(LABEL_VALUES_SEPARATOR, labelValues); + + // Update lowest seen + mins.get(key).set(min); + + // Update highest seen + maxs.get(key).set(max); + } + + private static class MutableDoubleSupplier implements DoubleSupplier { + private final double initialValue; + private volatile double value; + + public MutableDoubleSupplier(final double initialValue) { + this.initialValue = initialValue; + this.value = initialValue; + } + + @Override + public double getAsDouble() { + return value; + } + + public void set(final double value) { + this.value = value; + } + + public void reset() { + value = initialValue; + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java new file mode 100644 index 00000000..849e7ddb --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java @@ -0,0 +1,47 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.metrics; + +import java.util.Locale; +import java.util.Optional; + +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +public enum LineaMetricCategory implements MetricCategory { + /** Sequencer profitability metric category */ + SEQUENCER_PROFITABILITY, + /** Tx pool profitability metric category */ + TX_POOL_PROFITABILITY, + /** Runtime pricing configuration */ + PRICING_CONF; + + private static final Optional APPLICATION_PREFIX = Optional.of("linea_"); + + private final String name; + + LineaMetricCategory() { + this.name = name().toLowerCase(Locale.ROOT); + } + + @Override + public String getName() { + return name; + } + + @Override + public Optional getApplicationPrefix() { + return APPLICATION_PREFIX; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaCancelBundle.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaCancelBundle.java new file mode 100644 index 00000000..605845dc --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaCancelBundle.java @@ -0,0 +1,63 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.rpc.methods; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundlePoolService; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; + +@Slf4j +public class LineaCancelBundle { + private static final AtomicInteger LOG_SEQUENCE = new AtomicInteger(); + private final JsonRpcParameter parameterParser = new JsonRpcParameter(); + private BundlePoolService bundlePool; + + public LineaCancelBundle init(BundlePoolService bundlePoolService) { + this.bundlePool = bundlePoolService; + return this; + } + + public String getNamespace() { + return "linea"; + } + + public String getName() { + return "cancelBundle"; + } + + public Boolean execute(final PluginRpcRequest request) { + // sequence id for correlating error messages in logs: + final int logId = log.isDebugEnabled() ? LOG_SEQUENCE.incrementAndGet() : -1; + try { + final UUID replacementUUID = parameterParser.required(request.getParams(), 0, UUID.class); + + return bundlePool.remove(replacementUUID); + + } catch (final Exception e) { + log.atError() + .setMessage("[{}] failed to parse linea_cancelBundle request") + .addArgument(logId) + .setCause(e) + .log(); + throw new PluginRpcEndpointException( + new LineaSendBundle.LineaSendBundleError("malformed linea_cancelBundle json param")); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaEstimateGas.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaEstimateGas.java new file mode 100644 index 00000000..0f28bb72 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaEstimateGas.java @@ -0,0 +1,459 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.rpc.methods; + +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.MODULE_NOT_DEFINED; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import static net.consensys.linea.zktracer.Fork.LONDON; +import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity.create; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaRpcConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.zktracer.ZkTracer; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.StateOverrideMap; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcRequestException; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.transaction.CallParameter; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.RpcEndpointService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +@Slf4j +public class LineaEstimateGas { + @VisibleForTesting public static final SECPSignature FAKE_SIGNATURE_FOR_SIZE_CALCULATION; + + private static final AtomicInteger LOG_SEQUENCE = new AtomicInteger(); + + static { + final X9ECParameters params = SECNamedCurves.getByName("secp256k1"); + final ECDomainParameters curve = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + FAKE_SIGNATURE_FOR_SIZE_CALCULATION = + SECPSignature.create( + new BigInteger( + "66397251408932042429874251838229702988618145381408295790259650671563847073199"), + new BigInteger( + "24729624138373455972486746091821238755870276413282629437244319694880507882088"), + (byte) 0, + curve.getN()); + } + + private final JsonRpcParameter parameterParser = new JsonRpcParameter(); + private final BesuConfiguration besuConfiguration; + private final TransactionSimulationService transactionSimulationService; + private final BlockchainService blockchainService; + private final RpcEndpointService rpcEndpointService; + private LineaRpcConfiguration rpcConfiguration; + private LineaTransactionPoolValidatorConfiguration txValidatorConf; + private LineaProfitabilityConfiguration profitabilityConf; + private TransactionProfitabilityCalculator txProfitabilityCalculator; + private LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private ModuleLineCountValidator moduleLineCountValidator; + + public LineaEstimateGas( + final BesuConfiguration besuConfiguration, + final TransactionSimulationService transactionSimulationService, + final BlockchainService blockchainService, + final RpcEndpointService rpcEndpointService) { + this.besuConfiguration = besuConfiguration; + this.transactionSimulationService = transactionSimulationService; + this.blockchainService = blockchainService; + this.rpcEndpointService = rpcEndpointService; + } + + public void init( + final LineaRpcConfiguration rpcConfiguration, + final LineaTransactionPoolValidatorConfiguration transactionValidatorConfiguration, + final LineaProfitabilityConfiguration profitabilityConf, + final Map limitsMap, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration) { + this.rpcConfiguration = rpcConfiguration; + this.txValidatorConf = transactionValidatorConfiguration; + this.profitabilityConf = profitabilityConf; + this.txProfitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.moduleLineCountValidator = new ModuleLineCountValidator(limitsMap); + } + + public String getNamespace() { + return "linea"; + } + + public String getName() { + return "estimateGas"; + } + + public LineaEstimateGas.Response execute(final PluginRpcRequest request) { + try { + final long logId; + if (log.isDebugEnabled()) { + // no matter if it overflows, since it is only used to correlate logs for this request, + // so we only print callParameters once at the beginning, and we can reference them using + // the logId. + logId = LOG_SEQUENCE.incrementAndGet(); + } else { + logId = 0; + } + + final var callParameters = parseCallParameters(request.getParams()); + final var maybeStateOverrides = getStateOverrideMap(request.getParams()); + final var minGasPrice = besuConfiguration.getMinGasPrice(); + final Wei baseFee = + blockchainService + .getNextBlockBaseFee() + .orElseThrow( + () -> + new PluginRpcEndpointException( + RpcErrorType.INVALID_REQUEST, "Not on a baseFee market")); + + log.debug("[{}] Parsed call parameters: {}", logId, callParameters); + final long gasEstimation = getGasEstimation(callParameters, maybeStateOverrides, logId); + + final var transaction = + createTransactionForSimulation(callParameters, gasEstimation, baseFee, logId); + log.atDebug() + .setMessage("[{}] Transaction: {}; Gas estimation {}") + .addArgument(logId) + .addArgument(transaction::toTraceLog) + .addArgument(gasEstimation) + .log(); + + validateLineCounts(maybeStateOverrides, transaction, logId); + + final Wei estimatedPriorityFee = + getEstimatedPriorityFee(transaction, baseFee, minGasPrice, gasEstimation); + + final var response = + new Response(create(gasEstimation), create(baseFee), create(estimatedPriorityFee)); + log.atDebug() + .setMessage("[{}] Response for call params {} is {}") + .addArgument(logId) + .addArgument(callParameters) + .addArgument(response) + .log(); + + return response; + } catch (PluginRpcEndpointException | InvalidJsonRpcRequestException e) { + throw e; + } catch (Exception e) { + throw new PluginRpcEndpointException(new EstimateGasError(e.getMessage()), null, e); + } + } + + private Long getGasEstimation( + final CallParameter callParameter, + final Optional maybeStateOverrides, + final long logId) { + final var params = + maybeStateOverrides.isPresent() + ? new Object[] {callParameter, "pending", maybeStateOverrides.get()} + : new Object[] {callParameter, "pending"}; + + final var resp = rpcEndpointService.call("eth_estimateGas", params); + if (!resp.getType().equals(RpcResponseType.SUCCESS)) { + var errorResponse = (JsonRpcError) resp.getResult(); + throw new PluginRpcEndpointException( + new EstimateGasError(errorResponse.getCode(), errorResponse.getMessage()), + errorResponse.getData()); + } + + final Long gasEstimation = Long.decode((String) resp.getResult()); + log.atTrace() + .setMessage("[{}] eth_estimateGas response is {}") + .addArgument(logId) + .addArgument(gasEstimation) + .log(); + return gasEstimation; + } + + private Wei getEstimatedPriorityFee( + final Transaction transaction, + final Wei baseFee, + final Wei minGasPrice, + final long estimatedGasUsed) { + final Wei priorityFeeLowerBound = minGasPrice.subtract(baseFee); + + if (rpcConfiguration.estimateGasCompatibilityModeEnabled()) { + return Wei.of( + rpcConfiguration + .estimateGasCompatibilityMultiplier() + .multiply(new BigDecimal(priorityFeeLowerBound.getAsBigInteger())) + .setScale(0, RoundingMode.CEILING) + .toBigInteger()); + } + + return txProfitabilityCalculator.profitablePriorityFeePerGas( + transaction, profitabilityConf.estimateGasMinMargin(), estimatedGasUsed, minGasPrice); + } + + private void validateLineCounts( + final Optional maybeStateOverrides, + final Transaction transaction, + final long logId) { + + final var pendingBlockHeader = transactionSimulationService.simulatePendingBlockHeader(); + final var zkTracer = createZkTracer(pendingBlockHeader, blockchainService.getChainId().get()); + + final var maybeSimulationResults = + transactionSimulationService.simulate( + transaction, maybeStateOverrides, pendingBlockHeader, zkTracer, false, true); + + ModuleLimitsValidationResult moduleLimit = + moduleLineCountValidator.validate(zkTracer.getModulesLineCount()); + + if (moduleLimit.getResult() != ModuleLineCountValidator.ModuleLineCountResult.VALID) { + handleModuleOverLimit(moduleLimit); + } + + maybeSimulationResults.ifPresentOrElse( + r -> { + // if the transaction is invalid or doesn't have enough gas with the max it never will + if (r.isInvalid()) { + log.atDebug() + .setMessage("[{}] Invalid transaction {}, reason {}") + .addArgument(logId) + .addArgument(transaction::toTraceLog) + .addArgument(r.result()) + .log(); + throw new PluginRpcEndpointException( + new EstimateGasError(r.result().getInvalidReason().orElse(""))); + } + if (!r.isSuccessful()) { + log.atDebug() + .setMessage("[{}] Failed transaction {}, reason {}") + .addArgument(logId) + .addArgument(transaction::toTraceLog) + .addArgument(r.result()) + .log(); + r.getRevertReason() + .ifPresent( + rr -> { + throw new PluginRpcEndpointException( + RpcErrorType.REVERT_ERROR, rr.toHexString()); + }); + final var invalidReason = r.result().getInvalidReason(); + throw new PluginRpcEndpointException( + new EstimateGasError( + "Failed transaction" + invalidReason.map(ir -> ", reason: " + ir).orElse(""))); + } + }, + () -> + new PluginRpcEndpointException( + RpcErrorType.PLUGIN_INTERNAL_ERROR, "Empty result from simulation")); + } + + private CallParameter parseCallParameters(final Object[] params) { + final CallParameter callParameters; + try { + callParameters = parameterParser.required(params, 0, CallParameter.class); + } catch (JsonRpcParameter.JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid call parameters (index 0)", RpcErrorType.INVALID_CALL_PARAMS); + } + validateCallParameters(callParameters); + return callParameters; + } + + private void validateCallParameters(final CallParameter callParameters) { + if (callParameters.getGasPrice().isPresent() && isBaseFeeTransaction(callParameters)) { + throw new InvalidJsonRpcParameters( + "gasPrice cannot be used with maxFeePerGas or maxPriorityFeePerGas or maxFeePerBlobGas"); + } + + final var gasLimit = callParameters.getGas().orElse(0L); + if (gasLimit > txValidatorConf.maxTxGasLimit()) { + throw new InvalidJsonRpcParameters( + "gasLimit above maximum of: " + txValidatorConf.maxTxGasLimit()); + } + } + + protected Optional getStateOverrideMap(final Object[] params) { + try { + return parameterParser.optional(params, 1, StateOverrideMap.class); + } catch (JsonRpcParameter.JsonRpcParameterException e) { + throw new InvalidJsonRpcRequestException( + "Invalid account overrides parameter (index 1)", RpcErrorType.INVALID_CALL_PARAMS, e); + } + } + + private boolean isBaseFeeTransaction(final CallParameter callParameters) { + return (callParameters.getMaxFeePerGas().isPresent() + || callParameters.getMaxPriorityFeePerGas().isPresent() + || callParameters.getMaxFeePerBlobGas().isPresent()); + } + + private Transaction createTransactionForSimulation( + final CallParameter callParameters, + final long gasEstimation, + final Wei baseFee, + final long logId) { + + final var txBuilder = + Transaction.builder() + .sender(callParameters.getSender().orElse(Address.ZERO)) + .nonce(callParameters.getNonce().orElseGet(() -> getSenderNonce(callParameters, logId))) + .gasLimit(gasEstimation) + .payload(callParameters.getPayload().orElse(Bytes.EMPTY)) + .value(callParameters.getValue().orElse(Wei.ZERO)) + .signature(FAKE_SIGNATURE_FOR_SIZE_CALCULATION); + + callParameters.getTo().ifPresent(txBuilder::to); + + if (isBaseFeeTransaction(callParameters)) { + txBuilder.maxFeePerGas(callParameters.getMaxFeePerGas().orElse(Wei.ZERO)); + txBuilder.maxPriorityFeePerGas(callParameters.getMaxPriorityFeePerGas().orElse(Wei.ZERO)); + } else { + txBuilder.gasPrice(callParameters.getGasPrice().orElse(baseFee)); + } + + callParameters.getAccessList().ifPresent(txBuilder::accessList); + + final var txType = txBuilder.guessType().getTransactionType(); + + if (txType.supportsBlob()) { + txBuilder.maxFeePerBlobGas(callParameters.getMaxFeePerBlobGas().orElse(Wei.ZERO)); + } + + callParameters + .getChainId() + .ifPresentOrElse( + txBuilder::chainId, + () -> { + if (txType.requiresChainId()) { + blockchainService.getChainId().ifPresent(txBuilder::chainId); + } + }); + + return txBuilder.build(); + } + + private long getSenderNonce(final CallParameter callParameters, final long logId) { + + return callParameters + .getSender() + .map( + sender -> { + final var resp = + rpcEndpointService.call( + "eth_getTransactionCount", new Object[] {sender.toHexString(), "latest"}); + + if (!resp.getType().equals(RpcResponseType.SUCCESS)) { + throw new PluginRpcEndpointException( + new EstimateGasError("Unable to query sender nonce")); + } + + final Long nonce = Long.decode((String) resp.getResult()); + + log.atTrace() + .setMessage("[{}] eth_getTransactionCount response for {} is {}, nonce {}") + .addArgument(logId) + .addArgument(sender) + .addArgument(resp::getResult) + .addArgument(nonce) + .log(); + + return nonce; + }) + .orElse(0L); + } + + private ZkTracer createZkTracer( + final ProcessableBlockHeader pendingBlockHeader, final BigInteger chainId) { + var zkTracer = new ZkTracer(LONDON, l1L2BridgeConfiguration, chainId); + zkTracer.traceStartConflation(1L); + zkTracer.traceStartBlock(pendingBlockHeader, pendingBlockHeader.getCoinbase()); + return zkTracer; + } + + private void handleModuleOverLimit(ModuleLimitsValidationResult moduleLimitResult) { + // Throw specific exceptions based on the type of limit exceeded + if (moduleLimitResult.getResult() == MODULE_NOT_DEFINED) { + String moduleNotDefinedMsg = + String.format( + "Module %s does not exist in the limits file.", moduleLimitResult.getModuleName()); + log.error(moduleNotDefinedMsg); + throw new PluginRpcEndpointException(new EstimateGasError(moduleNotDefinedMsg)); + } + if (moduleLimitResult.getResult() == TX_MODULE_LINE_COUNT_OVERFLOW) { + String txOverflowMsg = + String.format( + "Transaction line count for module %s=%s is above the limit %s", + moduleLimitResult.getModuleName(), + moduleLimitResult.getModuleLineCount(), + moduleLimitResult.getModuleLineLimit()); + log.warn(txOverflowMsg); + throw new PluginRpcEndpointException(new EstimateGasError(txOverflowMsg)); + } + + final String internalErrorMsg = + String.format("Do not know what to do with result %s", moduleLimitResult.getResult()); + log.error(internalErrorMsg); + throw new PluginRpcEndpointException(RpcErrorType.PLUGIN_INTERNAL_ERROR, internalErrorMsg); + } + + public record Response( + @JsonProperty String gasLimit, + @JsonProperty String baseFeePerGas, + @JsonProperty String priorityFeePerGas) {} + + private record EstimateGasError(int errorCode, String errorReason) implements RpcMethodError { + public EstimateGasError(String errorReason) { + this(-32000, errorReason); + } + + @Override + public int getCode() { + return errorCode; + } + + @Override + public String getMessage() { + return errorReason; + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSendBundle.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSendBundle.java new file mode 100644 index 00000000..e613b229 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSendBundle.java @@ -0,0 +1,190 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.rpc.methods; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundleParameter; +import net.consensys.linea.bundles.BundlePoolService; +import net.consensys.linea.bundles.LineaLimitedBundlePool; +import net.consensys.linea.bundles.TransactionBundle; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; +import org.hyperledger.besu.ethereum.api.util.DomainObjectDecodeUtils; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; + +@Slf4j +@RequiredArgsConstructor +public class LineaSendBundle { + private static final AtomicInteger LOG_SEQUENCE = new AtomicInteger(); + private static final int MAX_TRACKED_SEEN_REQUESTS = 1_000; + private final JsonRpcParameter parameterParser = new JsonRpcParameter(); + private final Cache recentlySeenRequestsCache = + Caffeine.newBuilder() + .maximumSize(MAX_TRACKED_SEEN_REQUESTS) + .expireAfterAccess(Duration.ofMinutes(1)) + .build(); + private final BlockchainService blockchainService; + private BundlePoolService bundlePool; + + public LineaSendBundle init(BundlePoolService bundlePoolService) { + this.bundlePool = bundlePoolService; + return this; + } + + public String getNamespace() { + return "linea"; + } + + public String getName() { + return "sendBundle"; + } + + public BundleResponse execute(final PluginRpcRequest request) { + // sequence id for correlating error messages in logs: + final int logId = log.isDebugEnabled() ? LOG_SEQUENCE.incrementAndGet() : -1; + + try { + final BundleParameter bundleParams = parseRequest(logId, request.getParams()); + + validateParameters(bundleParams); + + final var optBundleUUID = bundleParams.replacementUUID().map(UUID::fromString); + + // use replacement UUID hashed if present, otherwise the hash of the transactions themselves + final var optBundleHash = + optBundleUUID + .map(LineaLimitedBundlePool::UUIDToHash) + .or( + () -> + bundleParams.txs().stream() + .map(Bytes::fromHexString) + .reduce(Bytes::concatenate) + .map(Hash::hash)); + + return optBundleHash + .map( + bundleHash -> { + final List txs = + bundleParams.txs().stream() + .map(DomainObjectDecodeUtils::decodeRawTransaction) + .toList(); + + bundlePool.putOrReplace( + bundleHash, + new TransactionBundle( + bundleHash, + txs, + bundleParams.blockNumber(), + bundleParams.minTimestamp(), + bundleParams.maxTimestamp(), + bundleParams.revertingTxHashes(), + optBundleUUID)); + return new BundleResponse(bundleHash.toHexString()); + }) + .orElseThrow( + () -> + // otherwise boom. + new RuntimeException("Malformed bundle, no bundle transactions present")); + + } catch (final Exception e) { + throw new PluginRpcEndpointException(new LineaSendBundleError(e.getMessage())); + } + } + + private void validateParameters(final BundleParameter bundleParams) { + // synchronized to avoid that 2 parallel requests with the same parameters + // will be both processed + synchronized (recentlySeenRequestsCache) { + final var alreadySeenAt = recentlySeenRequestsCache.getIfPresent(bundleParams); + if (alreadySeenAt != null) { + throw new IllegalArgumentException( + "request already seen " + Duration.between(alreadySeenAt, Instant.now()) + " ago"); + } + recentlySeenRequestsCache.put(bundleParams, Instant.now()); + } + + final var chainHeadBlockNumber = blockchainService.getChainHeadHeader().getNumber(); + if (bundleParams.blockNumber() <= chainHeadBlockNumber) { + throw new IllegalArgumentException( + "bundle block number " + + bundleParams.blockNumber() + + " is not greater than current chain head block number " + + chainHeadBlockNumber); + } + + bundleParams + .maxTimestamp() + .ifPresent( + maxTimestamp -> { + final var now = Instant.now().getEpochSecond(); + if (maxTimestamp < now) { + throw new IllegalArgumentException( + "bundle max timestamp " + + maxTimestamp + + " is in the past, current timestamp is " + + now); + } + }); + } + + private BundleParameter parseRequest(final int logId, final Object[] params) { + try { + BundleParameter param = parameterParser.required(params, 0, BundleParameter.class); + return param; + } catch (Exception e) { + log.atError() + .setMessage("[{}] failed to parse linea_sendBundle request") + .addArgument(logId) + .setCause(e) + .log(); + throw new RuntimeException("malformed linea_sendBundle json param"); + } + } + + public record BundleResponse(String bundleHash) {} + + static class LineaSendBundleError implements RpcMethodError { + + final String errMessage; + + LineaSendBundleError(String errMessage) { + this.errMessage = errMessage; + } + + @Override + public int getCode() { + return INVALID_PARAMS_ERROR_CODE; + } + + @Override + public String getMessage() { + return errMessage; + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSetExtraData.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSetExtraData.java new file mode 100644 index 00000000..4a02c8a0 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/methods/LineaSetExtraData.java @@ -0,0 +1,122 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.rpc.methods; + +import static net.consensys.linea.extradata.LineaExtraDataException.ErrorType.FAILED_CALLING_SET_EXTRA_DATA; +import static net.consensys.linea.extradata.LineaExtraDataException.ErrorType.INVALID_ARGUMENT; + +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.extradata.LineaExtraDataException; +import net.consensys.linea.extradata.LineaExtraDataHandler; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; +import org.hyperledger.besu.plugin.services.RpcEndpointService; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.hyperledger.besu.plugin.services.rpc.RpcMethodError; +import org.hyperledger.besu.plugin.services.rpc.RpcResponseType; + +@Slf4j +public class LineaSetExtraData { + + private static final AtomicInteger LOG_SEQUENCE = new AtomicInteger(); + private final JsonRpcParameter parameterParser = new JsonRpcParameter(); + private final RpcEndpointService rpcEndpointService; + private LineaExtraDataHandler extraDataHandler; + + public LineaSetExtraData(final RpcEndpointService rpcEndpointService) { + this.rpcEndpointService = rpcEndpointService; + } + + public void init(final LineaExtraDataHandler extraDataHandler) { + this.extraDataHandler = extraDataHandler; + } + + public String getNamespace() { + return "linea"; + } + + public String getName() { + return "setExtraData"; + } + + public Boolean execute(final PluginRpcRequest request) { + // no matter if it overflows, since it is only used to correlate logs for this request, + // so we only print callParameters once at the beginning, and we can reference them using the + // sequence. + final int logId = log.isDebugEnabled() ? LOG_SEQUENCE.incrementAndGet() : -1; + + try { + final var extraData = parseRequest(logId, request.getParams()); + + updatePricingConf(logId, extraData); + + updateStandardExtraData(extraData); + + return Boolean.TRUE; + } catch (final LineaExtraDataException lede) { + throw new PluginRpcEndpointException(new ExtraDataPricingError(lede)); + } + } + + private void updateStandardExtraData(final Bytes32 extraData) { + final var resp = + rpcEndpointService.call("miner_setExtraData", new Object[] {extraData.toHexString()}); + if (!resp.getType().equals(RpcResponseType.SUCCESS)) { + throw new LineaExtraDataException( + FAILED_CALLING_SET_EXTRA_DATA, "Internal setExtraData method failed: " + resp); + } + } + + private void updatePricingConf(final int logId, final Bytes32 extraData) { + extraDataHandler.handle(extraData); + log.atDebug() + .setMessage("[{}] Successfully handled extra data pricing") + .addArgument(logId) + .log(); + } + + private Bytes32 parseRequest(final int logId, final Object[] params) { + try { + final var rawParam = parameterParser.required(params, 0, String.class); + final var extraData = Bytes32.wrap(Bytes.fromHexStringLenient(rawParam)); + log.atDebug() + .setMessage("[{}] set extra data, raw=[{}] parsed=[{}]") + .addArgument(logId) + .addArgument(rawParam) + .addArgument(extraData::toHexString) + .log(); + return extraData; + } catch (Exception e) { + throw new LineaExtraDataException(INVALID_ARGUMENT, e.getMessage()); + } + } + + private record ExtraDataPricingError(LineaExtraDataException ex) implements RpcMethodError { + @Override + public int getCode() { + return ex.getErrorType().getCode(); + } + + @Override + public String getMessage() { + return ex.getMessage(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaBundleEndpointsPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaBundleEndpointsPlugin.java new file mode 100644 index 00000000..18499a90 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaBundleEndpointsPlugin.java @@ -0,0 +1,69 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.rpc.services; + +import com.google.auto.service.AutoService; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.rpc.methods.LineaCancelBundle; +import net.consensys.linea.rpc.methods.LineaSendBundle; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; + +@AutoService(BesuPlugin.class) +public class LineaBundleEndpointsPlugin extends AbstractLineaRequiredPlugin { + private LineaSendBundle lineaSendBundleMethod; + private LineaCancelBundle lineaCancelBundleMethod; + + /** + * Register the bundle RPC service. + * + * @param serviceManager the ServiceManager to be used. + */ + @Override + public void doRegister(final ServiceManager serviceManager) { + lineaSendBundleMethod = new LineaSendBundle(blockchainService); + + rpcEndpointService.registerRPCEndpoint( + lineaSendBundleMethod.getNamespace(), + lineaSendBundleMethod.getName(), + lineaSendBundleMethod::execute); + + lineaCancelBundleMethod = new LineaCancelBundle(); + + rpcEndpointService.registerRPCEndpoint( + lineaCancelBundleMethod.getNamespace(), + lineaCancelBundleMethod.getName(), + lineaCancelBundleMethod::execute); + } + + /** + * Starts this plugin and in case the extra data pricing is enabled, as first thing it tries to + * extract extra data pricing configuration from the chain head, then it starts listening for new + * imported block, in order to update the extra data pricing on every incoming block. + */ + @Override + public void doStart() { + // set the pool + lineaSendBundleMethod.init(bundlePoolService); + lineaCancelBundleMethod.init(bundlePoolService); + } + + @Override + public void stop() { + bundlePoolService.saveToDisk(); + super.stop(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java new file mode 100644 index 00000000..c95d3a69 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java @@ -0,0 +1,79 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.rpc.services; + +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.rpc.methods.LineaEstimateGas; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; + +/** Registers RPC endpoints. This class provides RPC endpoints under the 'linea' namespace. */ +@AutoService(BesuPlugin.class) +@Slf4j +public class LineaEstimateGasEndpointPlugin extends AbstractLineaRequiredPlugin { + + private TransactionSimulationService transactionSimulationService; + private LineaEstimateGas lineaEstimateGasMethod; + + /** + * Register the RPC service. + * + * @param serviceManager the BesuContext to be used. + */ + @Override + public void doRegister(final ServiceManager serviceManager) { + transactionSimulationService = + serviceManager + .getService(TransactionSimulationService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionSimulatorService from the ServiceManager.")); + + lineaEstimateGasMethod = + new LineaEstimateGas( + besuConfiguration, transactionSimulationService, blockchainService, rpcEndpointService); + + rpcEndpointService.registerRPCEndpoint( + lineaEstimateGasMethod.getNamespace(), + lineaEstimateGasMethod.getName(), + lineaEstimateGasMethod::execute); + } + + @Override + public void beforeExternalServices() { + super.beforeExternalServices(); + lineaEstimateGasMethod.init( + lineaRpcConfiguration(), + transactionPoolValidatorConfiguration(), + profitabilityConfiguration(), + createLimitModules(tracerConfiguration()), + l1L2BridgeSharedConfiguration()); + } + + @Override + public void doStart() { + if (l1L2BridgeSharedConfiguration().equals(LineaL1L2BridgeSharedConfiguration.TEST_DEFAULT)) { + throw new IllegalArgumentException("L1L2 bridge settings have not been defined."); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSetExtraDataEndpointPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSetExtraDataEndpointPlugin.java new file mode 100644 index 00000000..541f6756 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaSetExtraDataEndpointPlugin.java @@ -0,0 +1,59 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.rpc.services; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.extradata.LineaExtraDataHandler; +import net.consensys.linea.rpc.methods.LineaSetExtraData; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; + +/** Registers RPC endpoints. This class provides RPC endpoints under the 'linea' namespace. */ +@AutoService(BesuPlugin.class) +@Slf4j +public class LineaSetExtraDataEndpointPlugin extends AbstractLineaRequiredPlugin { + private LineaSetExtraData lineaSetExtraDataMethod; + + /** + * Register the RPC service. + * + * @param serviceManager the ServiceManager to be used. + */ + @Override + public void doRegister(final ServiceManager serviceManager) { + + lineaSetExtraDataMethod = new LineaSetExtraData(rpcEndpointService); + + rpcEndpointService.registerRPCEndpoint( + lineaSetExtraDataMethod.getNamespace(), + lineaSetExtraDataMethod.getName(), + lineaSetExtraDataMethod::execute); + } + + @Override + public void beforeExternalServices() { + super.beforeExternalServices(); + lineaSetExtraDataMethod.init( + new LineaExtraDataHandler(rpcEndpointService, profitabilityConfiguration())); + } + + @Override + public void doStart() { + // no-op + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/TracerAggregator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/TracerAggregator.java new file mode 100644 index 00000000..14d18868 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/TracerAggregator.java @@ -0,0 +1,161 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.log.Log; +import org.hyperledger.besu.evm.operation.Operation; +import org.hyperledger.besu.evm.tracing.OperationTracer; +import org.hyperledger.besu.evm.worldstate.WorldView; + +/** + * Aggregates multiple {@link OperationTracer} instances, allowing them to be treated as a single + * tracer. This class facilitates the registration and delegation of tracing operations to multiple + * tracers. + */ +public class TracerAggregator implements OperationTracer { + private final List tracers = new ArrayList<>(); + + /** + * Registers an {@link OperationTracer} instance with the aggregator. If a tracer of the same + * class is already registered, an {@link IllegalArgumentException} is thrown. + * + * @param tracer the tracer to register + * @throws IllegalArgumentException if a tracer of the same class is already registered + */ + public void register(OperationTracer tracer) { + // Check if a tracer of the same class is already registered + for (OperationTracer existingTracer : tracers) { + if (existingTracer.getClass().equals(tracer.getClass())) { + throw new IllegalArgumentException( + "A tracer of class " + tracer.getClass().getName() + " is already registered."); + } + } + tracers.add(tracer); + } + + /** + * Creates a {@link TracerAggregator} instance and registers the provided tracers. + * + * @param tracers the tracers to register with the aggregator + * @return a new {@link TracerAggregator} instance with the provided tracers registered + */ + public static TracerAggregator create(OperationTracer... tracers) { + TracerAggregator aggregator = new TracerAggregator(); + for (OperationTracer tracer : tracers) { + aggregator.register(tracer); + } + return aggregator; + } + + @Override + public void tracePreExecution(MessageFrame frame) { + for (OperationTracer tracer : tracers) { + tracer.tracePreExecution(frame); + } + } + + @Override + public void tracePostExecution(MessageFrame frame, Operation.OperationResult operationResult) { + for (OperationTracer tracer : tracers) { + tracer.tracePostExecution(frame, operationResult); + } + } + + @Override + public void tracePrecompileCall(MessageFrame frame, long gasRequirement, Bytes output) { + for (OperationTracer tracer : tracers) { + tracer.tracePrecompileCall(frame, gasRequirement, output); + } + } + + @Override + public void traceAccountCreationResult( + MessageFrame frame, Optional haltReason) { + for (OperationTracer tracer : tracers) { + tracer.traceAccountCreationResult(frame, haltReason); + } + } + + @Override + public void tracePrepareTransaction(WorldView worldView, Transaction transaction) { + for (OperationTracer tracer : tracers) { + tracer.tracePrepareTransaction(worldView, transaction); + } + } + + @Override + public void traceStartTransaction(WorldView worldView, Transaction transaction) { + for (OperationTracer tracer : tracers) { + tracer.traceStartTransaction(worldView, transaction); + } + } + + @Override + public void traceEndTransaction( + WorldView worldView, + Transaction tx, + boolean status, + Bytes output, + List logs, + long gasUsed, + Set
selfDestructs, + long timeNs) { + for (OperationTracer tracer : tracers) { + tracer.traceEndTransaction( + worldView, tx, status, output, logs, gasUsed, selfDestructs, timeNs); + } + } + + @Override + public void traceContextEnter(MessageFrame frame) { + for (OperationTracer tracer : tracers) { + tracer.traceContextEnter(frame); + } + } + + @Override + public void traceContextReEnter(MessageFrame frame) { + for (OperationTracer tracer : tracers) { + tracer.traceContextReEnter(frame); + } + } + + @Override + public void traceContextExit(MessageFrame frame) { + for (OperationTracer tracer : tracers) { + tracer.traceContextExit(frame); + } + } + + @Override + public boolean isExtendedTracing() { + for (OperationTracer tracer : tracers) { + if (tracer.isExtendedTracing()) { + return true; + } + } + return false; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLimitsValidationResult.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLimitsValidationResult.java new file mode 100644 index 00000000..618aab95 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLimitsValidationResult.java @@ -0,0 +1,119 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.modulelimit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** Represents the result of verifying module line counts against their limits. */ +@Getter +@EqualsAndHashCode +public class ModuleLimitsValidationResult { + private final ModuleLineCountValidator.ModuleLineCountResult result; + private final String moduleName; + private final Integer moduleLineCount; + private final Integer moduleLineLimit; + private final Integer cumulativeModuleLineCount; + private final Integer cumulativeModuleLineLimit; + + public static final ModuleLimitsValidationResult VALID = + new ModuleLimitsValidationResult( + ModuleLineCountValidator.ModuleLineCountResult.VALID, null, null, null, null, null); + + private ModuleLimitsValidationResult( + final ModuleLineCountValidator.ModuleLineCountResult result, + final String moduleName, + final Integer moduleLineCount, + final Integer moduleLineLimit, + final Integer cumulativeModuleLineCount, + final Integer cumulativeModuleLineLimit) { + this.result = result; + this.moduleName = moduleName; + this.moduleLineCount = moduleLineCount; + this.moduleLineLimit = moduleLineLimit; + this.cumulativeModuleLineCount = cumulativeModuleLineCount; + this.cumulativeModuleLineLimit = cumulativeModuleLineLimit; + } + + public static ModuleLimitsValidationResult moduleNotDefined(final String moduleName) { + return new ModuleLimitsValidationResult( + ModuleLineCountValidator.ModuleLineCountResult.MODULE_NOT_DEFINED, + moduleName, + null, + null, + null, + null); + } + + public static ModuleLimitsValidationResult invalidLineCount( + final String moduleName, final Integer moduleLineCount) { + return new ModuleLimitsValidationResult( + ModuleLineCountValidator.ModuleLineCountResult.INVALID_LINE_COUNT, + moduleName, + moduleLineCount, + null, + null, + null); + } + + public static ModuleLimitsValidationResult txModuleLineCountOverflow( + final String moduleName, + final Integer moduleLineCount, + final Integer moduleLineLimit, + final Integer cumulativeModuleLineCount, + final Integer cumulativeModuleLineLimit) { + return new ModuleLimitsValidationResult( + ModuleLineCountValidator.ModuleLineCountResult.TX_MODULE_LINE_COUNT_OVERFLOW, + moduleName, + moduleLineCount, + moduleLineLimit, + cumulativeModuleLineCount, + cumulativeModuleLineLimit); + } + + public static ModuleLimitsValidationResult blockModuleLineCountFull( + final String moduleName, + final Integer moduleLineCount, + final Integer moduleLineLimit, + final Integer cumulativeModuleLineCount, + final Integer cumulativeModuleLineLimit) { + + return new ModuleLimitsValidationResult( + ModuleLineCountValidator.ModuleLineCountResult.BLOCK_MODULE_LINE_COUNT_FULL, + moduleName, + moduleLineCount, + moduleLineLimit, + cumulativeModuleLineCount, + cumulativeModuleLineLimit); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(result.name()); + if (moduleName != null) { + sb.append("[module=").append(moduleName); + + if (moduleLineCount != null) { + sb.append(",lineCount=").append(moduleLineCount); + sb.append(",lineLimit=").append(moduleLineLimit); + sb.append(",cumulativeLineCount=").append(cumulativeModuleLineCount); + sb.append(",cumulativeLineLimit=").append(cumulativeModuleLineLimit); + } + + sb.append(']'); + } + return sb.toString(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidator.java new file mode 100644 index 00000000..15a59521 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidator.java @@ -0,0 +1,150 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.modulelimit; + +import java.io.File; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTracerConfiguration; +import org.apache.tuweni.toml.Toml; +import org.apache.tuweni.toml.TomlParseResult; +import org.apache.tuweni.toml.TomlTable; + +/** + * Verifies line counts for modules based on provided limits. It supports verifying whether current + * transaction exceed these limits. + */ +@Slf4j +public class ModuleLineCountValidator { + private final Map moduleLineCountLimits; + + /** + * Constructs a new accumulator with specified module line count limits. + * + * @param moduleLineCountLimits A map of module names to their respective line count limits. + */ + public ModuleLineCountValidator(Map moduleLineCountLimits) { + this.moduleLineCountLimits = Map.copyOf(moduleLineCountLimits); + } + + /** + * Verifies if the current accumulated line counts for modules exceed the predefined limits. + * + * @param currentAccumulatedLineCounts A map of module names to their current accumulated line + * counts. + * @return A {@link ModuleLimitsValidationResult} indicating the outcome of the verification. + */ + public ModuleLimitsValidationResult validate( + final Map currentAccumulatedLineCounts) { + return validate(currentAccumulatedLineCounts, initialLineCountLimits()); + } + + /** + * Verifies whether the current accumulated line counts, against previous accumulation line + * counts, for modules exceed the predefined limits. + * + * @param currentAccumulatedLineCounts A map of module names to their current accumulated line + * counts. + * @param prevAccumulatedLineCounts A map with previous accumulated line counts. + * @return A {@link ModuleLimitsValidationResult} indicating the outcome of the verification. + */ + public ModuleLimitsValidationResult validate( + final Map currentAccumulatedLineCounts, + final Map prevAccumulatedLineCounts) { + for (Map.Entry moduleEntry : currentAccumulatedLineCounts.entrySet()) { + final String moduleName = moduleEntry.getKey(); + final int currentTotalLineCountForModule = moduleEntry.getValue(); + if (currentTotalLineCountForModule < 0) { + log.error( + "Negative line count {} returned for module '{}'.", + currentAccumulatedLineCounts, + moduleName); + return ModuleLimitsValidationResult.invalidLineCount( + moduleName, currentTotalLineCountForModule); + } + final Integer lineCountLimitForModule = moduleLineCountLimits.get(moduleName); + + if (lineCountLimitForModule == null) { + log.error("Module '{}' is not defined in limits config.", moduleName); + return ModuleLimitsValidationResult.moduleNotDefined(moduleName); + } + + final int lineCountAddedByCurrentTx = + currentTotalLineCountForModule - prevAccumulatedLineCounts.get(moduleName); + + if (lineCountAddedByCurrentTx > lineCountLimitForModule) { + return ModuleLimitsValidationResult.txModuleLineCountOverflow( + moduleName, + lineCountAddedByCurrentTx, + lineCountLimitForModule, + currentTotalLineCountForModule, + lineCountLimitForModule); + } + + if (currentTotalLineCountForModule > lineCountLimitForModule) { + return ModuleLimitsValidationResult.blockModuleLineCountFull( + moduleName, + lineCountAddedByCurrentTx, + lineCountLimitForModule, + currentTotalLineCountForModule, + lineCountLimitForModule); + } + } + return ModuleLimitsValidationResult.VALID; + } + + private Map initialLineCountLimits() { + return moduleLineCountLimits.keySet().stream() + .collect(Collectors.toMap(Function.identity(), unused -> 0)); + } + + /** Enumerates possible outcomes of verifying module line counts against their limits. */ + public enum ModuleLineCountResult { + VALID, + TX_MODULE_LINE_COUNT_OVERFLOW, + BLOCK_MODULE_LINE_COUNT_FULL, + MODULE_NOT_DEFINED, + INVALID_LINE_COUNT + } + + public static Map createLimitModules( + LineaTracerConfiguration lineaTracerConfiguration) { + try { + URL url = new File(lineaTracerConfiguration.moduleLimitsFilePath()).toURI().toURL(); + final String tomlString = Resources.toString(url, StandardCharsets.UTF_8); + TomlParseResult result = Toml.parse(tomlString); + final TomlTable table = result.getTable("traces-limits"); + final Map limitsMap = + table.toMap().entrySet().stream() + .collect( + Collectors.toUnmodifiableMap( + Map.Entry::getKey, e -> Math.toIntExact((Long) e.getValue()))); + + return limitsMap; + } catch (final Exception e) { + final String errorMsg = + "Problem reading the toml file containing the limits for the modules: " + + lineaTracerConfiguration.moduleLimitsFilePath(); + log.error(errorMsg); + throw new RuntimeException(errorMsg, e); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java new file mode 100644 index 00000000..18bf1193 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java @@ -0,0 +1,103 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.txpoolvalidation.validators.AllowedAddressValidator; +import net.consensys.linea.sequencer.txpoolvalidation.validators.CalldataValidator; +import net.consensys.linea.sequencer.txpoolvalidation.validators.GasLimitValidator; +import net.consensys.linea.sequencer.txpoolvalidation.validators.ProfitabilityValidator; +import net.consensys.linea.sequencer.txpoolvalidation.validators.SimulationValidator; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidatorFactory; + +/** Represents a factory for creating transaction pool validators. */ +public class LineaTransactionPoolValidatorFactory implements PluginTransactionPoolValidatorFactory { + + private final BesuConfiguration besuConfiguration; + private final BlockchainService blockchainService; + private final TransactionSimulationService transactionSimulationService; + private final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + private final LineaProfitabilityConfiguration profitabilityConf; + private final Set
denied; + private final Map moduleLineLimitsMap; + private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private final Optional rejectedTxJsonRpcManager; + + public LineaTransactionPoolValidatorFactory( + final BesuConfiguration besuConfiguration, + final BlockchainService blockchainService, + final TransactionSimulationService transactionSimulationService, + final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf, + final LineaProfitabilityConfiguration profitabilityConf, + final Set
deniedAddresses, + final Map moduleLineLimitsMap, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final Optional rejectedTxJsonRpcManager) { + this.besuConfiguration = besuConfiguration; + this.blockchainService = blockchainService; + this.transactionSimulationService = transactionSimulationService; + this.txPoolValidatorConf = txPoolValidatorConf; + this.profitabilityConf = profitabilityConf; + this.denied = deniedAddresses; + this.moduleLineLimitsMap = moduleLineLimitsMap; + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; + } + + /** + * Creates a new transaction pool validator, that simply calls in sequence all the actual + * validators, in a fail-fast mode. + * + * @return the new transaction pool validator + */ + @Override + public PluginTransactionPoolValidator createTransactionValidator() { + final var validators = + new PluginTransactionPoolValidator[] { + new AllowedAddressValidator(denied), + new GasLimitValidator(txPoolValidatorConf), + new CalldataValidator(txPoolValidatorConf), + new ProfitabilityValidator(besuConfiguration, blockchainService, profitabilityConf), + new SimulationValidator( + blockchainService, + transactionSimulationService, + txPoolValidatorConf, + moduleLineLimitsMap, + l1L2BridgeConfiguration, + rejectedTxJsonRpcManager) + }; + + return (transaction, isLocal, hasPriority) -> + Arrays.stream(validators) + .map(v -> v.validateTransaction(transaction, isLocal, hasPriority)) + .filter(Optional::isPresent) + .findFirst() + .map(Optional::get); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java new file mode 100644 index 00000000..2f8e0ee5 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java @@ -0,0 +1,168 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation; + +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.txpoolvalidation.metrics.TransactionPoolProfitabilityMetrics; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.TransactionPoolValidatorService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.transactionpool.TransactionPoolService; + +/** + * This class extends the default transaction validation rules for adding transactions to the + * transaction pool. It leverages the PluginTransactionValidatorService to manage and customize the + * process of transaction validation. This includes, for example, setting a deny list of addresses + * that are not allowed to add transactions to the pool. + */ +@Slf4j +@AutoService(BesuPlugin.class) +public class LineaTransactionPoolValidatorPlugin extends AbstractLineaRequiredPlugin { + private ServiceManager serviceManager; + private TransactionPoolValidatorService transactionPoolValidatorService; + private TransactionSimulationService transactionSimulationService; + private Optional rejectedTxJsonRpcManager = Optional.empty(); + + @Override + public void doRegister(final ServiceManager serviceManager) { + this.serviceManager = serviceManager; + + transactionPoolValidatorService = + serviceManager + .getService(TransactionPoolValidatorService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionPoolValidationService from the ServiceManager.")); + + transactionSimulationService = + serviceManager + .getService(TransactionSimulationService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionSimulatorService from the ServiceManager.")); + + metricCategoryRegistry.addMetricCategory(TX_POOL_PROFITABILITY); + } + + @Override + public void doStart() { + if (l1L2BridgeSharedConfiguration().equals(LineaL1L2BridgeSharedConfiguration.TEST_DEFAULT)) { + throw new IllegalArgumentException("L1L2 bridge settings have not been defined."); + } + + try (Stream lines = + Files.lines( + Path.of(new File(transactionPoolValidatorConfiguration().denyListPath()).toURI()))) { + final Set
deniedAddresses = + lines.map(l -> Address.fromHexString(l.trim())).collect(Collectors.toUnmodifiableSet()); + + // start the optional json rpc manager for rejected tx reporting + final LineaRejectedTxReportingConfiguration lineaRejectedTxReportingConfiguration = + rejectedTxReportingConfiguration(); + rejectedTxJsonRpcManager = + Optional.ofNullable(lineaRejectedTxReportingConfiguration.rejectedTxEndpoint()) + .map( + endpoint -> + new JsonRpcManager( + "linea-tx-pool-validator-plugin", + besuConfiguration.getDataPath(), + lineaRejectedTxReportingConfiguration) + .start()); + + transactionPoolValidatorService.registerPluginTransactionValidatorFactory( + new LineaTransactionPoolValidatorFactory( + besuConfiguration, + blockchainService, + transactionSimulationService, + transactionPoolValidatorConfiguration(), + profitabilityConfiguration(), + deniedAddresses, + createLimitModules(tracerConfiguration()), + l1L2BridgeSharedConfiguration(), + rejectedTxJsonRpcManager)); + + if (metricCategoryRegistry.isMetricCategoryEnabled(TX_POOL_PROFITABILITY)) { + final var besuEventsService = + serviceManager + .getService(BesuEvents.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BesuEvents from the ServiceManager.")); + + final var transactionPoolService = + serviceManager + .getService(TransactionPoolService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionPoolService from the ServiceManager.")); + + final var transactionPoolProfitabilityMetrics = + new TransactionPoolProfitabilityMetrics( + besuConfiguration, + metricsSystem, + profitabilityConfiguration(), + transactionPoolService, + blockchainService); + + besuEventsService.addBlockAddedListener( + addedBlockContext -> { + try { + // on new block let's calculate profitability for every txs in the pool + transactionPoolProfitabilityMetrics.update(); + } catch (final Exception e) { + log.warn( + "Error calculating transaction profitability for block {}({})", + addedBlockContext.getBlockHeader().getNumber(), + addedBlockContext.getBlockHeader().getBlockHash(), + e); + } + }); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void stop() { + super.stop(); + rejectedTxJsonRpcManager.ifPresent(JsonRpcManager::shutdown); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java new file mode 100644 index 00000000..fccc65fa --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java @@ -0,0 +1,126 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.metrics; + +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; + +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; +import org.apache.tuweni.units.bigints.UInt256s; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.transactionpool.TransactionPoolService; + +/** + * Tracks profitability metrics for transactions in the transaction pool. Specifically monitors the + * ratio of profitable priority fee to actual priority fee: + * profitablePriorityFeePerGas/transaction.priorityFeePerGas + * + *

Provides: - Lowest ratio seen (minimum profitability) - Highest ratio seen (maximum + * profitability) - Distribution histogram of ratios + */ +@Slf4j +public class TransactionPoolProfitabilityMetrics { + private final TransactionProfitabilityCalculator profitabilityCalculator; + private final LineaProfitabilityConfiguration profitabilityConf; + private final BesuConfiguration besuConfiguration; + private final TransactionPoolService transactionPoolService; + private final BlockchainService blockchainService; + private final HistogramMetrics histogramMetrics; + + public TransactionPoolProfitabilityMetrics( + final BesuConfiguration besuConfiguration, + final MetricsSystem metricsSystem, + final LineaProfitabilityConfiguration profitabilityConf, + final TransactionPoolService transactionPoolService, + final BlockchainService blockchainService) { + + this.besuConfiguration = besuConfiguration; + this.profitabilityConf = profitabilityConf; + this.profitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + this.transactionPoolService = transactionPoolService; + this.blockchainService = blockchainService; + this.histogramMetrics = + new HistogramMetrics( + metricsSystem, + TX_POOL_PROFITABILITY, + "ratio", + "transaction pool profitability ratio", + profitabilityConf.profitabilityMetricsBuckets()); + } + + public void update() { + final long startTime = System.currentTimeMillis(); + final var txPoolContent = transactionPoolService.getPendingTransactions(); + + final var ratioStats = + txPoolContent.parallelStream() + .map(PendingTransaction::getTransaction) + .map( + tx -> { + final var ratio = handleTransaction(tx); + histogramMetrics.track(ratio); + log.trace("Recorded profitability ratio {} for tx {}", ratio, tx.getHash()); + return ratio; + }) + .collect(Collectors.summarizingDouble(Double::doubleValue)); + + histogramMetrics.setMinMax(ratioStats.getMin(), ratioStats.getMax()); + + log.atDebug() + .setMessage("Transaction pool profitability metrics processed {}txs in {}ms, statistics {}") + .addArgument(txPoolContent::size) + .addArgument(() -> System.currentTimeMillis() - startTime) + .addArgument(ratioStats) + .log(); + } + + private double handleTransaction(final Transaction transaction) { + final Wei actualPriorityFeePerGas; + if (transaction.getMaxPriorityFeePerGas().isEmpty()) { + actualPriorityFeePerGas = + Wei.fromQuantity(transaction.getGasPrice().orElseThrow()) + .subtract(blockchainService.getNextBlockBaseFee().orElseThrow()); + } else { + final Wei maxPriorityFeePerGas = + Wei.fromQuantity(transaction.getMaxPriorityFeePerGas().get()); + actualPriorityFeePerGas = + UInt256s.min( + maxPriorityFeePerGas.add(blockchainService.getNextBlockBaseFee().orElseThrow()), + Wei.fromQuantity(transaction.getMaxFeePerGas().orElseThrow())); + } + + final Wei profitablePriorityFeePerGas = + profitabilityCalculator.profitablePriorityFeePerGas( + transaction, + profitabilityConf.txPoolMinMargin(), + transaction.getGasLimit(), + besuConfiguration.getMinGasPrice()); + + final double ratio = + actualPriorityFeePerGas.toBigInteger().doubleValue() + / profitablePriorityFeePerGas.toBigInteger().doubleValue(); + + return ratio; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java new file mode 100644 index 00000000..a30cab93 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java @@ -0,0 +1,85 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +/** + * Validator that checks if the sender or the recipient are accepted. By default, precompiles are + * not valid recipient. + */ +@Slf4j +@RequiredArgsConstructor +public class AllowedAddressValidator implements PluginTransactionPoolValidator { + private static final Set

PRECOMPILES = + Set.of( + Address.fromHexString("0x0000000000000000000000000000000000000001"), + Address.fromHexString("0x0000000000000000000000000000000000000002"), + Address.fromHexString("0x0000000000000000000000000000000000000003"), + Address.fromHexString("0x0000000000000000000000000000000000000004"), + Address.fromHexString("0x0000000000000000000000000000000000000005"), + Address.fromHexString("0x0000000000000000000000000000000000000006"), + Address.fromHexString("0x0000000000000000000000000000000000000007"), + Address.fromHexString("0x0000000000000000000000000000000000000008"), + Address.fromHexString("0x0000000000000000000000000000000000000009"), + Address.fromHexString("0x000000000000000000000000000000000000000a")); + + private final Set
denied; + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + return validateSender(transaction).or(() -> validateRecipient(transaction)); + } + + private Optional validateRecipient(final Transaction transaction) { + if (transaction.getTo().isPresent()) { + final Address to = transaction.getTo().get(); + if (denied.contains(to)) { + final String errMsg = + String.format( + "recipient %s is blocked as appearing on the SDN or other legally prohibited list", + to); + log.debug(errMsg); + return Optional.of(errMsg); + } else if (PRECOMPILES.contains(to)) { + final String errMsg = + "destination address is a precompile address and cannot receive transactions"; + log.debug(errMsg); + return Optional.of(errMsg); + } + } + return Optional.empty(); + } + + private Optional validateSender(final Transaction transaction) { + if (denied.contains(transaction.getSender())) { + final String errMsg = + String.format( + "sender %s is blocked as appearing on the SDN or other legally prohibited list", + transaction.getSender()); + log.debug(errMsg); + return Optional.of(errMsg); + } + return Optional.empty(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java new file mode 100644 index 00000000..44eba275 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java @@ -0,0 +1,43 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +/** Validator that checks if the calldata is below the configured max size. */ +@Slf4j +@RequiredArgsConstructor +public class CalldataValidator implements PluginTransactionPoolValidator { + final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + if (transaction.getPayload().size() > txPoolValidatorConf.maxTxCalldataSize()) { + final String errMsg = + "Calldata of transaction is greater than the allowed max of " + + txPoolValidatorConf.maxTxCalldataSize(); + log.debug(errMsg); + return Optional.of(errMsg); + } + return Optional.empty(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java new file mode 100644 index 00000000..f86e2844 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java @@ -0,0 +1,46 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +/** + * Validator that checks if the gas limit is below the configured max amount. This means that max + * gas limit of a transaction could be less than the block gas limit. + */ +@Slf4j +@RequiredArgsConstructor +public class GasLimitValidator implements PluginTransactionPoolValidator { + final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + if (transaction.getGasLimit() > txPoolValidatorConf.maxTxGasLimit()) { + final String errMsg = + "Gas limit of transaction is greater than the allowed max of " + + txPoolValidatorConf.maxTxGasLimit(); + log.debug(errMsg); + return Optional.of(errMsg); + } + return Optional.empty(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java new file mode 100644 index 00000000..74040fdb --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java @@ -0,0 +1,91 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import org.apache.tuweni.units.bigints.UInt256s; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +/** + * Validator that checks if the upfront gas price, that the transaction is willing to pay, is + * profitable. This check does not apply to transaction with priority and can be enabled/disabled + * independently for transactions received via API or P2P. + */ +@Slf4j +public class ProfitabilityValidator implements PluginTransactionPoolValidator { + final BesuConfiguration besuConfiguration; + final BlockchainService blockchainService; + final LineaProfitabilityConfiguration profitabilityConf; + final TransactionProfitabilityCalculator profitabilityCalculator; + + public ProfitabilityValidator( + final BesuConfiguration besuConfiguration, + final BlockchainService blockchainService, + final LineaProfitabilityConfiguration profitabilityConf) { + this.besuConfiguration = besuConfiguration; + this.blockchainService = blockchainService; + this.profitabilityConf = profitabilityConf; + this.profitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + } + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + + if (!hasPriority + && (isLocal && profitabilityConf.txPoolCheckApiEnabled() + || !isLocal && profitabilityConf.txPoolCheckP2pEnabled())) { + + final Wei baseFee = + blockchainService + .getNextBlockBaseFee() + .orElseThrow(() -> new RuntimeException("We only support a base fee market")); + + return profitabilityCalculator.isProfitable( + "Txpool", + transaction, + profitabilityConf.txPoolMinMargin(), + baseFee, + calculateUpfrontGasPrice(transaction, baseFee), + transaction.getGasLimit(), + besuConfiguration.getMinGasPrice()) + ? Optional.empty() + : Optional.of("Gas price too low"); + } + + return Optional.empty(); + } + + private Wei calculateUpfrontGasPrice(final Transaction transaction, final Wei baseFee) { + + return transaction + .getMaxFeePerGas() + .map(Wei::fromQuantity) + .map( + maxFee -> + UInt256s.min( + maxFee, + baseFee.add(Wei.fromQuantity(transaction.getMaxPriorityFeePerGas().get())))) + .orElseGet(() -> Wei.fromQuantity(transaction.getGasPrice().get())); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java new file mode 100644 index 00000000..848324f3 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java @@ -0,0 +1,194 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.MODULE_NOT_DEFINED; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import static net.consensys.linea.zktracer.Fork.LONDON; + +import java.math.BigInteger; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionSimulationResult; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; + +/** + * Validator that checks if transaction simulation completes successfully, including line counting. + * This check can be enabled/disabled independently for transactions received via API or P2P. + */ +@Slf4j +public class SimulationValidator implements PluginTransactionPoolValidator { + private final BlockchainService blockchainService; + private final TransactionSimulationService transactionSimulationService; + private final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + private final Map moduleLineLimitsMap; + private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private final Optional rejectedTxJsonRpcManager; + + public SimulationValidator( + final BlockchainService blockchainService, + final TransactionSimulationService transactionSimulationService, + final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf, + final Map moduleLineLimitsMap, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final Optional rejectedTxJsonRpcManager) { + this.blockchainService = blockchainService; + this.transactionSimulationService = transactionSimulationService; + this.txPoolValidatorConf = txPoolValidatorConf; + this.moduleLineLimitsMap = moduleLineLimitsMap; + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; + } + + @Override + public Optional validateTransaction( + final Transaction transaction, final boolean isLocal, final boolean hasPriority) { + + final boolean isLocalAndApiEnabled = + isLocal && txPoolValidatorConf.txPoolSimulationCheckApiEnabled(); + final boolean isRemoteAndP2pEnabled = + !isLocal && txPoolValidatorConf.txPoolSimulationCheckP2pEnabled(); + if (isRemoteAndP2pEnabled || isLocalAndApiEnabled) { + log.atTrace() + .setMessage( + "Starting simulation validation for tx with hash={}, isLocal={}, hasPriority={}") + .addArgument(transaction::getHash) + .addArgument(isLocal) + .addArgument(hasPriority) + .log(); + + final ModuleLineCountValidator moduleLineCountValidator = + new ModuleLineCountValidator(moduleLineLimitsMap); + final var pendingBlockHeader = transactionSimulationService.simulatePendingBlockHeader(); + + final var zkTracer = createZkTracer(pendingBlockHeader, blockchainService.getChainId().get()); + final var maybeSimulationResults = + transactionSimulationService.simulate( + transaction, Optional.empty(), pendingBlockHeader, zkTracer, false, true); + + ModuleLimitsValidationResult moduleLimitResult = + moduleLineCountValidator.validate(zkTracer.getModulesLineCount()); + + logSimulationResult( + transaction, isLocal, hasPriority, maybeSimulationResults, moduleLimitResult); + + if (moduleLimitResult.getResult() != ModuleLineCountValidator.ModuleLineCountResult.VALID) { + final String reason = handleModuleOverLimit(transaction, moduleLimitResult); + reportRejectedTransaction(transaction, reason); + return Optional.of(reason); + } + + if (maybeSimulationResults.isPresent()) { + final var simulationResult = maybeSimulationResults.get(); + if (simulationResult.isInvalid()) { + final String errMsg = + "Invalid transaction" + + simulationResult.getInvalidReason().map(ir -> ": " + ir).orElse(""); + log.debug(errMsg); + return Optional.of(errMsg); + } + } + } else { + log.atTrace() + .setMessage( + "Simulation validation not enabled for tx with hash={}, isLocal={}, hasPriority={}") + .addArgument(transaction::getHash) + .addArgument(isLocal) + .addArgument(hasPriority) + .log(); + } + + return Optional.empty(); + } + + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } + + private void logSimulationResult( + final Transaction transaction, + final boolean isLocal, + final boolean hasPriority, + final Optional maybeSimulationResults, + final ModuleLimitsValidationResult moduleLimitResult) { + log.atTrace() + .setMessage( + "Result of simulation validation for tx with hash={}, isLocal={}, hasPriority={}, is {}, module line counts {}") + .addArgument(transaction::getHash) + .addArgument(isLocal) + .addArgument(hasPriority) + .addArgument(maybeSimulationResults) + .addArgument(moduleLimitResult) + .log(); + } + + private ZkTracer createZkTracer( + final ProcessableBlockHeader pendingBlockHeader, BigInteger chainId) { + var zkTracer = new ZkTracer(LONDON, l1L2BridgeConfiguration, chainId); + zkTracer.traceStartConflation(1L); + zkTracer.traceStartBlock(pendingBlockHeader, pendingBlockHeader.getCoinbase()); + return zkTracer; + } + + private String handleModuleOverLimit( + Transaction transaction, ModuleLimitsValidationResult moduleLimitResult) { + if (moduleLimitResult.getResult() == MODULE_NOT_DEFINED) { + String moduleNotDefinedMsg = + String.format( + "Module %s does not exist in the limits file.", moduleLimitResult.getModuleName()); + log.error(moduleNotDefinedMsg); + return moduleNotDefinedMsg; + } + if (moduleLimitResult.getResult() == TX_MODULE_LINE_COUNT_OVERFLOW) { + String txOverflowMsg = + String.format( + "Transaction %s line count for module %s=%s is above the limit %s", + transaction.getHash(), + moduleLimitResult.getModuleName(), + moduleLimitResult.getModuleLineCount(), + moduleLimitResult.getModuleLineLimit()); + log.warn(txOverflowMsg); + log.trace("Transaction details: {}", transaction); + return txOverflowMsg; + } + return "Internal Error: do not know what to do with result: " + moduleLimitResult.getResult(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectionResult.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectionResult.java new file mode 100644 index 00000000..20b9af0f --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectionResult.java @@ -0,0 +1,89 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection; + +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; + +public class LineaTransactionSelectionResult extends TransactionSelectionResult { + private enum LineaStatus implements TransactionSelectionResult.Status { + BLOCK_CALLDATA_OVERFLOW(false, false, false), + BLOCK_MODULE_LINE_COUNT_FULL(true, false, false), + TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS(false, true, true), + TX_TOO_LARGE_FOR_REMAINING_USER_GAS(false, false, false), + TX_MODULE_LINE_COUNT_OVERFLOW(false, true, true), + TX_MODULE_LINE_COUNT_OVERFLOW_CACHED(false, true, true), + TX_MODULE_LINE_INVALID_COUNT(false, true, true), + TX_UNPROFITABLE(false, false, true), + TX_UNPROFITABLE_UPFRONT(false, false, true), + TX_UNPROFITABLE_RETRY_LIMIT(false, false, false), + BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS(false, true, true), + BUNDLE_TOO_LARGE_FOR_REMAINING_BUNDLE_BLOCK_GAS(false, false, false); + + private final boolean stop; + private final boolean discard; + private final boolean penalize; + + LineaStatus(boolean stop, boolean discard, boolean penalize) { + this.stop = stop; + this.discard = discard; + this.penalize = penalize; + } + + @Override + public boolean stop() { + return stop; + } + + @Override + public boolean discard() { + return discard; + } + + @Override + public boolean penalize() { + return penalize; + } + } + + protected LineaTransactionSelectionResult(LineaStatus status) { + super(status); + } + + public static final TransactionSelectionResult BLOCK_CALLDATA_OVERFLOW = + new LineaTransactionSelectionResult(LineaStatus.BLOCK_CALLDATA_OVERFLOW); + public static final TransactionSelectionResult BLOCK_MODULE_LINE_COUNT_FULL = + new LineaTransactionSelectionResult(LineaStatus.BLOCK_MODULE_LINE_COUNT_FULL); + public static final TransactionSelectionResult TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS = + new LineaTransactionSelectionResult(LineaStatus.TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS); + public static final TransactionSelectionResult TX_TOO_LARGE_FOR_REMAINING_USER_GAS = + new LineaTransactionSelectionResult(LineaStatus.TX_TOO_LARGE_FOR_REMAINING_USER_GAS); + public static final TransactionSelectionResult TX_MODULE_LINE_COUNT_OVERFLOW = + new LineaTransactionSelectionResult(LineaStatus.TX_MODULE_LINE_COUNT_OVERFLOW); + public static final TransactionSelectionResult TX_MODULE_LINE_COUNT_OVERFLOW_CACHED = + new LineaTransactionSelectionResult(LineaStatus.TX_MODULE_LINE_COUNT_OVERFLOW_CACHED); + public static final TransactionSelectionResult TX_MODULE_LINE_INVALID_COUNT = + new LineaTransactionSelectionResult(LineaStatus.TX_MODULE_LINE_INVALID_COUNT); + public static final TransactionSelectionResult TX_UNPROFITABLE = + new LineaTransactionSelectionResult(LineaStatus.TX_UNPROFITABLE); + public static final TransactionSelectionResult TX_UNPROFITABLE_UPFRONT = + new LineaTransactionSelectionResult(LineaStatus.TX_UNPROFITABLE_UPFRONT); + public static final TransactionSelectionResult TX_UNPROFITABLE_RETRY_LIMIT = + new LineaTransactionSelectionResult(LineaStatus.TX_UNPROFITABLE_RETRY_LIMIT); + public static final TransactionSelectionResult BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS = + new LineaTransactionSelectionResult(LineaStatus.BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS); + public static final TransactionSelectionResult BUNDLE_TOO_LARGE_FOR_REMAINING_BUNDLE_BLOCK_GAS = + new LineaTransactionSelectionResult( + LineaStatus.BUNDLE_TOO_LARGE_FOR_REMAINING_BUNDLE_BLOCK_GAS); +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java new file mode 100644 index 00000000..d10c2a07 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java @@ -0,0 +1,137 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txselection; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundlePoolService; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.txselection.selectors.LineaTransactionSelector; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; + +/** + * Represents a factory for creating transaction selectors. Note that a new instance of the + * transaction selector is created everytime a new block creation time is started. + * + *

Also provides an entrypoint for bundle transactions + */ +@Slf4j +public class LineaTransactionSelectorFactory implements PluginTransactionSelectorFactory { + private final BlockchainService blockchainService; + private final Optional rejectedTxJsonRpcManager; + private final LineaTransactionSelectorConfiguration txSelectorConfiguration; + private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private final LineaProfitabilityConfiguration profitabilityConfiguration; + private final LineaTracerConfiguration tracerConfiguration; + private final Optional maybeProfitabilityMetrics; + private final BundlePoolService bundlePoolService; + private final Map limitsMap; + private final AtomicReference currSelector = new AtomicReference<>(); + + public LineaTransactionSelectorFactory( + final BlockchainService blockchainService, + final LineaTransactionSelectorConfiguration txSelectorConfiguration, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final LineaProfitabilityConfiguration profitabilityConfiguration, + final LineaTracerConfiguration tracerConfiguration, + final Map limitsMap, + final Optional rejectedTxJsonRpcManager, + final Optional maybeProfitabilityMetrics, + final BundlePoolService bundlePoolService) { + this.blockchainService = blockchainService; + this.txSelectorConfiguration = txSelectorConfiguration; + this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.profitabilityConfiguration = profitabilityConfiguration; + this.tracerConfiguration = tracerConfiguration; + this.limitsMap = limitsMap; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; + this.maybeProfitabilityMetrics = maybeProfitabilityMetrics; + this.bundlePoolService = bundlePoolService; + } + + @Override + public PluginTransactionSelector create(final SelectorsStateManager selectorsStateManager) { + final var selector = + new LineaTransactionSelector( + selectorsStateManager, + blockchainService, + txSelectorConfiguration, + l1L2BridgeConfiguration, + profitabilityConfiguration, + tracerConfiguration, + bundlePoolService, + limitsMap, + rejectedTxJsonRpcManager, + maybeProfitabilityMetrics); + currSelector.set(selector); + return selector; + } + + public void selectPendingTransactions( + final BlockTransactionSelectionService bts, final ProcessableBlockHeader pendingBlockHeader) { + final var bundlesByBlockNumber = + bundlePoolService.getBundlesByBlockNumber(pendingBlockHeader.getNumber()); + + log.atDebug() + .setMessage("Bundle pool stats: total={}, for block #{}={}") + .addArgument(bundlePoolService::size) + .addArgument(pendingBlockHeader::getNumber) + .addArgument(bundlesByBlockNumber::size) + .log(); + + bundlesByBlockNumber.forEach( + bundle -> { + log.trace("Starting evaluation of bundle {}", bundle); + var badBundleRes = + bundle.pendingTransactions().stream() + .map(bts::evaluatePendingTransaction) + .filter(evalRes -> !evalRes.selected()) + .findFirst(); + + if (badBundleRes.isPresent()) { + log.trace("Failed bundle {}, reason {}", bundle, badBundleRes); + rollback(bts); + } else { + log.trace("Selected bundle {}", bundle); + commit(bts); + } + }); + currSelector.set(null); + } + + private void commit(final BlockTransactionSelectionService bts) { + currSelector.get().getOperationTracer().commitTransactionBundle(); + bts.commit(); + } + + private void rollback(final BlockTransactionSelectionService bts) { + currSelector.get().getOperationTracer().popTransactionBundle(); + bts.rollback(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java new file mode 100644 index 00000000..77e14b6b --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java @@ -0,0 +1,111 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txselection; + +import static net.consensys.linea.metrics.LineaMetricCategory.SEQUENCER_PROFITABILITY; +import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; + +import java.util.Optional; + +import com.google.auto.service.AutoService; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.txselection.selectors.ProfitableTransactionSelector; +import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.ServiceManager; +import org.hyperledger.besu.plugin.services.TransactionSelectionService; + +/** + * This class extends the default transaction selection rules used by Besu. It leverages the + * TransactionSelectionService to manage and customize the process of transaction selection. This + * includes setting limits such as 'TraceLineLimit', 'maxBlockGas', and 'maxCallData'. + */ +@Slf4j +@AutoService(BesuPlugin.class) +public class LineaTransactionSelectorPlugin extends AbstractLineaRequiredPlugin { + private TransactionSelectionService transactionSelectionService; + private Optional rejectedTxJsonRpcManager = Optional.empty(); + + @Override + public void doRegister(final ServiceManager serviceManager) { + transactionSelectionService = + serviceManager + .getService(TransactionSelectionService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionSelectionService from the ServiceManager.")); + + metricCategoryRegistry.addMetricCategory(SEQUENCER_PROFITABILITY); + } + + @Override + public void doStart() { + if (l1L2BridgeSharedConfiguration().equals(LineaL1L2BridgeSharedConfiguration.TEST_DEFAULT)) { + throw new IllegalArgumentException("L1L2 bridge settings have not been defined."); + } + + final LineaTransactionSelectorConfiguration txSelectorConfiguration = + transactionSelectorConfiguration(); + + final LineaRejectedTxReportingConfiguration lineaRejectedTxReportingConfiguration = + rejectedTxReportingConfiguration(); + rejectedTxJsonRpcManager = + Optional.ofNullable(lineaRejectedTxReportingConfiguration.rejectedTxEndpoint()) + .map( + endpoint -> + new JsonRpcManager( + "linea-tx-selector-plugin", + besuConfiguration.getDataPath(), + lineaRejectedTxReportingConfiguration) + .start()); + + final Optional maybeProfitabilityMetrics = + metricCategoryRegistry.isMetricCategoryEnabled(SEQUENCER_PROFITABILITY) + ? Optional.of( + new HistogramMetrics( + metricsSystem, + SEQUENCER_PROFITABILITY, + "ratio", + "sequencer profitability ratio", + profitabilityConfiguration().profitabilityMetricsBuckets(), + ProfitableTransactionSelector.Phase.class)) + : Optional.empty(); + + transactionSelectionService.registerPluginTransactionSelectorFactory( + new LineaTransactionSelectorFactory( + blockchainService, + txSelectorConfiguration, + l1L2BridgeSharedConfiguration(), + profitabilityConfiguration(), + tracerConfiguration(), + createLimitModules(tracerConfiguration()), + rejectedTxJsonRpcManager, + maybeProfitabilityMetrics, + bundlePoolService)); + } + + @Override + public void stop() { + super.stop(); + rejectedTxJsonRpcManager.ifPresent(JsonRpcManager::shutdown); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelector.java new file mode 100644 index 00000000..564093e5 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelector.java @@ -0,0 +1,78 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static java.lang.Boolean.TRUE; + +import java.time.Instant; + +import net.consensys.linea.bundles.TransactionBundle; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +public class BundleConstraintTransactionSelector implements PluginTransactionSelector { + + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext txContext) { + + // short circuit if we are not a PendingBundleTx + if (!(txContext.getPendingTransaction() + instanceof TransactionBundle.PendingBundleTx pendingBundleTx)) { + return TransactionSelectionResult.SELECTED; + } + + final var bundle = pendingBundleTx.getBundle(); + + final var satisfiesCriteria = + bundle.minTimestamp().map(minTime -> minTime < Instant.now().getEpochSecond()).orElse(TRUE) + && bundle + .maxTimestamp() + .map(maxTime -> maxTime > Instant.now().getEpochSecond()) + .orElse(TRUE); + + if (!satisfiesCriteria) { + return TransactionSelectionResult.invalid("Failed Bundled Transaction Criteria"); + } + return TransactionSelectionResult.SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext txContext, + final TransactionProcessingResult transactionProcessingResult) { + + // short circuit if we are not a PendingBundleTx + if (!(txContext.getPendingTransaction() + instanceof TransactionBundle.PendingBundleTx pendingBundleTx)) { + return TransactionSelectionResult.SELECTED; + } + + if (transactionProcessingResult.isFailed()) { + final var revertableList = pendingBundleTx.getBundle().revertingTxHashes(); + + // if a bundle tx failed, but was not in a revertable list, we unselect and fail the bundle + if (revertableList.isEmpty() + || !revertableList + .get() + .contains(txContext.getPendingTransaction().getTransaction().getHash())) { + return TransactionSelectionResult.invalid("Failed non revertable transaction in bundle"); + } + } + return TransactionSelectionResult.SELECTED; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java new file mode 100644 index 00000000..142e04f7 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java @@ -0,0 +1,242 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW_CACHED; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.BundlePoolService; +import net.consensys.linea.bundles.TransactionBundle; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.zktracer.ZkTracer; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +/** Class for transaction selection using a list of selectors. */ +@Slf4j +public class LineaTransactionSelector implements PluginTransactionSelector { + + private TraceLineLimitTransactionSelector traceLineLimitTransactionSelector; + private final List selectors; + private final Optional rejectedTxJsonRpcManager; + private final Set rejectedTransactionReasonsMap = new HashSet<>(); + + public LineaTransactionSelector( + final SelectorsStateManager selectorsStateManager, + final BlockchainService blockchainService, + final LineaTransactionSelectorConfiguration txSelectorConfiguration, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final LineaProfitabilityConfiguration profitabilityConfiguration, + final LineaTracerConfiguration tracerConfiguration, + final BundlePoolService bundlePoolService, + final Map limitsMap, + final Optional rejectedTxJsonRpcManager, + final Optional maybeProfitabilityMetrics) { + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; + + // only report rejected transaction selection result from TraceLineLimitTransactionSelector + if (rejectedTxJsonRpcManager.isPresent()) { + rejectedTransactionReasonsMap.add(TX_MODULE_LINE_COUNT_OVERFLOW.toString()); + rejectedTransactionReasonsMap.add(TX_MODULE_LINE_COUNT_OVERFLOW_CACHED.toString()); + } + + selectors = + createTransactionSelectors( + selectorsStateManager, + blockchainService, + txSelectorConfiguration, + l1L2BridgeConfiguration, + profitabilityConfiguration, + tracerConfiguration, + bundlePoolService, + limitsMap, + maybeProfitabilityMetrics); + } + + /** + * Creates a list of selectors based on Linea configuration. + * + * @param selectorsStateManager + * @param blockchainService Blockchain service. + * @param txSelectorConfiguration The configuration to use. + * @param profitabilityConfiguration The profitability configuration. + * @param tracerConfiguration the tracer config + * @param bundlePoolService bundle pool for transaction bundle selector + * @param limitsMap The limits map. + * @param maybeProfitabilityMetrics The optional profitability metrics + * @return A list of selectors. + */ + private List createTransactionSelectors( + final SelectorsStateManager selectorsStateManager, + final BlockchainService blockchainService, + final LineaTransactionSelectorConfiguration txSelectorConfiguration, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final LineaProfitabilityConfiguration profitabilityConfiguration, + final LineaTracerConfiguration tracerConfiguration, + final BundlePoolService bundlePoolService, + final Map limitsMap, + final Optional maybeProfitabilityMetrics) { + + traceLineLimitTransactionSelector = + new TraceLineLimitTransactionSelector( + selectorsStateManager, + blockchainService.getChainId().get(), + limitsMap, + txSelectorConfiguration, + l1L2BridgeConfiguration, + tracerConfiguration); + + List selectors = + List.of( + new MaxBlockCallDataTransactionSelector( + selectorsStateManager, txSelectorConfiguration.maxBlockCallDataSize()), + new MaxBlockGasTransactionSelector( + selectorsStateManager, txSelectorConfiguration.maxGasPerBlock()), + new ProfitableTransactionSelector( + blockchainService, + txSelectorConfiguration, + profitabilityConfiguration, + maybeProfitabilityMetrics), + new BundleConstraintTransactionSelector(), + new MaxBundleGasPerBlockTransactionSelector( + selectorsStateManager, txSelectorConfiguration.maxBundleGasPerBlock()), + traceLineLimitTransactionSelector); + + return selectors; + } + + /** + * Evaluates a transaction before processing using all selectors. Stops if any selector doesn't + * select the transaction. + * + * @param evaluationContext The current selection context. + * @return The first non-SELECTED result or SELECTED if all selectors select the transaction. + */ + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + return selectors.stream() + .map(selector -> selector.evaluateTransactionPreProcessing(evaluationContext)) + .filter(result -> !result.equals(TransactionSelectionResult.SELECTED)) + .findFirst() + .orElse(TransactionSelectionResult.SELECTED); + } + + /** + * Evaluates a transaction considering its processing result. Stops if any selector doesn't select + * the transaction. + * + * @param evaluationContext The current selection context. + * @param processingResult The result of the transaction processing. + * @return The first non-SELECTED result or SELECTED if all selectors select the transaction. + */ + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + for (var selector : selectors) { + TransactionSelectionResult result = + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + if (!result.equals(TransactionSelectionResult.SELECTED)) { + return result; + } + } + return TransactionSelectionResult.SELECTED; + } + + /** + * Notifies all selectors when a transaction is selected. + * + * @param evaluationContext The current selection context. + * @param processingResult The transaction processing result. + */ + @Override + public void onTransactionSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + + // if pending tx is not from a bundle, then we need to commit now + if (!(evaluationContext.getPendingTransaction() instanceof TransactionBundle.PendingBundleTx)) { + getOperationTracer().commitTransactionBundle(); + } + + selectors.forEach( + selector -> selector.onTransactionSelected(evaluationContext, processingResult)); + } + + /** + * Notifies all selectors when a transaction is not selected. + * + * @param evaluationContext The current selection context. + * @param transactionSelectionResult The reason for not selecting the transaction. + */ + @Override + public void onTransactionNotSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionSelectionResult transactionSelectionResult) { + + // if pending tx is not from a bundle, then we need to rollback now + if (!(evaluationContext.getPendingTransaction() instanceof TransactionBundle.PendingBundleTx)) { + getOperationTracer().popTransactionBundle(); + } + + selectors.forEach( + selector -> + selector.onTransactionNotSelected(evaluationContext, transactionSelectionResult)); + + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + if (transactionSelectionResult.discard() + && rejectedTransactionReasonsMap.contains(transactionSelectionResult.toString())) { + jsonRpcManager.submitNewJsonRpcCallAsync( + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + evaluationContext.getPendingTransaction().getTransaction(), + Instant.now(), + Optional.of(evaluationContext.getPendingBlockHeader().getNumber()), + transactionSelectionResult.toString(), + List.of())); + } + }); + } + + /** + * Returns the operation tracer to be used while processing the transactions for the block. + * + * @return the operation tracer + */ + @Override + public ZkTracer getOperationTracer() { + return traceLineLimitTransactionSelector.getOperationTracer(); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataTransactionSelector.java new file mode 100644 index 00000000..a1472ee0 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataTransactionSelector.java @@ -0,0 +1,105 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BLOCK_CALLDATA_OVERFLOW; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; + +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.AbstractStatefulPluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +/** + * This class implements TransactionSelector and provides a specific implementation for evaluating + * transactions based on the size of the call data. It checks if adding a transaction to the block + * pushes the call data size of the block over the limit. + */ +@Slf4j +public class MaxBlockCallDataTransactionSelector + extends AbstractStatefulPluginTransactionSelector { + + private final int maxBlockCallDataSize; + + public MaxBlockCallDataTransactionSelector( + final SelectorsStateManager stateManager, final int maxBlockCallDataSize) { + super(stateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); + this.maxBlockCallDataSize = maxBlockCallDataSize; + } + + /** + * Evaluates a transaction before processing. Checks if adding the transaction to the block pushes + * the call data size of the block over the limit. + * + * @param evaluationContext The current selection context. + * @return BLOCK_CALLDATA_FULL if the call data size of a transactions pushes the size for the + * block over the limit, otherwise SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + + final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); + final int transactionCallDataSize = transaction.getPayload().size(); + + final var stateCumulativeBlockCallDataSize = getWorkingState(); + + final long newCumulativeBlockCallDataSize = + Math.addExact(stateCumulativeBlockCallDataSize, transactionCallDataSize); + + if (newCumulativeBlockCallDataSize > maxBlockCallDataSize) { + log.atTrace() + .setMessage( + "Cumulative block calldata size including tx {} is {} greater than the max allowed {}, skipping tx") + .addArgument(transaction::getHash) + .addArgument(newCumulativeBlockCallDataSize) + .addArgument(maxBlockCallDataSize) + .log(); + return BLOCK_CALLDATA_OVERFLOW; + } + + log.atTrace() + .setMessage("Cumulative block calldata size including tx {} is {}") + .addArgument(transaction::getHash) + .addArgument(newCumulativeBlockCallDataSize) + .log(); + + return SELECTED; + } + + /** + * No evaluation is performed post-processing. + * + * @param evaluationContext The current selection context. + * @param processingResult The result of the transaction processing. + * @return Always returns SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + final long newCumulativeBlockCallDataSize = + Math.addExact( + getWorkingState(), + evaluationContext.getPendingTransaction().getTransaction().getPayload().size()); + + setWorkingState(newCumulativeBlockCallDataSize); + + return SELECTED; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelector.java new file mode 100644 index 00000000..db519cd5 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelector.java @@ -0,0 +1,105 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_USER_GAS; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; + +import lombok.extern.slf4j.Slf4j; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.AbstractStatefulPluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +/** + * This class implements TransactionSelector and provides a specific implementation for evaluating + * if the cumulative gas used by the block, including the current pending transaction, is below the + * user configured max amount, if not the transaction is not selected. This means that the user can + * configure a max gas per block that is below the limit defined by the protocol. + */ +@Slf4j +public class MaxBlockGasTransactionSelector + extends AbstractStatefulPluginTransactionSelector { + + private final long maxGasPerBlock; + + public MaxBlockGasTransactionSelector( + final SelectorsStateManager selectorsStateManager, final long maxGasPerBlock) { + super(selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); + this.maxGasPerBlock = maxGasPerBlock; + } + + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + // Evaluation done in post-processing, no action needed here. + return SELECTED; + } + + /** + * Evaluates a transaction post-processing. Checks if adding the gas used of the transaction, to + * the cumulative gas used of the block till now, is below the configured max gas used per block + * specified by the operator of the node. + * + * @param evaluationContext The current selection context. + * @return TX_TOO_LARGE_FOR_REMAINING_USER_GAS if adding this transaction pushes the gas used by + * the block over the limit, TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS if the gas used by this + * transaction alone is greater than the max gas used per block limit, otherwise SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + + final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); + final long gasUsedByTransaction = processingResult.getEstimateGasUsedByTransaction(); + + if (gasUsedByTransaction > maxGasPerBlock) { + log.atTrace() + .setMessage( + "Not selecting transaction {}, its gas used {} is greater than max user gas per block {}," + + " removing it from the txpool") + .addArgument(transaction::getHash) + .addArgument(gasUsedByTransaction) + .addArgument(maxGasPerBlock) + .log(); + return TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS; + } + + final var stateCumulativeBlockGasUsed = getWorkingState(); + + final long newCumulativeBlockGasUsed = + Math.addExact(stateCumulativeBlockGasUsed, gasUsedByTransaction); + + if (newCumulativeBlockGasUsed > maxGasPerBlock) { + log.atTrace() + .setMessage( + "Not selecting transaction {}, its cumulative block gas used {} exceeds max user gas per block {}," + + " skipping it") + .addArgument(transaction::getHash) + .addArgument(newCumulativeBlockGasUsed) + .addArgument(maxGasPerBlock) + .log(); + return TX_TOO_LARGE_FOR_REMAINING_USER_GAS; + } + + setWorkingState(newCumulativeBlockGasUsed); + + return SELECTED; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleGasPerBlockTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleGasPerBlockTransactionSelector.java new file mode 100644 index 00000000..39708536 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleGasPerBlockTransactionSelector.java @@ -0,0 +1,115 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BUNDLE_TOO_LARGE_FOR_REMAINING_BUNDLE_BLOCK_GAS; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bundles.TransactionBundle; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.AbstractStatefulPluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +@Slf4j +public class MaxBundleGasPerBlockTransactionSelector + extends AbstractStatefulPluginTransactionSelector< + MaxBundleGasPerBlockTransactionSelector.BundleGasTracker> { + private final long maxBundleGasPerBlock; + + public MaxBundleGasPerBlockTransactionSelector( + final SelectorsStateManager selectorsStateManager, final long maxBundleGasPerBlock) { + super(selectorsStateManager, new BundleGasTracker(0L, 0L), BundleGasTracker::duplicate); + this.maxBundleGasPerBlock = maxBundleGasPerBlock; + } + + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext txContext) { + return SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext txContext, + final TransactionProcessingResult transactionProcessingResult) { + + // short circuit if we are not a PendingBundleTx + if (!(txContext.getPendingTransaction() + instanceof TransactionBundle.PendingBundleTx pendingBundleTx)) { + return SELECTED; + } + + final long gasUsedByTransaction = transactionProcessingResult.getEstimateGasUsedByTransaction(); + + final long currentBundleGasUsed = + pendingBundleTx.isBundleStart() ? 0L : getWorkingState().currentBundleGasUsed(); + final long newCurrentBundleGasUsed = currentBundleGasUsed + gasUsedByTransaction; + + final long cumulativeBlockBundleGasUsed = getWorkingState().cumulativeBlockBundleGasUsed(); + final long newCumulativeBlockBundleGasUsed = + cumulativeBlockBundleGasUsed + gasUsedByTransaction; + + setWorkingState(new BundleGasTracker(newCumulativeBlockBundleGasUsed, newCurrentBundleGasUsed)); + + if (newCurrentBundleGasUsed > maxBundleGasPerBlock) { + log.atTrace() + .setMessage( + "Not selecting bundle transaction {} since the current gas used by the bundle is greater than the max {};" + + " gas used by tx {} + gas already used by the bundle {} = {}") + .addArgument(pendingBundleTx::toTraceLog) + .addArgument(maxBundleGasPerBlock) + .addArgument(gasUsedByTransaction) + .addArgument(currentBundleGasUsed) + .addArgument(newCurrentBundleGasUsed) + .log(); + return BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS; + } + + if (newCumulativeBlockBundleGasUsed > maxBundleGasPerBlock) { + log.atTrace() + .setMessage( + "Not selecting bundle transaction {} since the cumulative gas used by bundles in this block is greater than the max {};" + + " gas used by tx {} + gas already used by the bundle {} = {}") + .addArgument(pendingBundleTx::toTraceLog) + .addArgument(maxBundleGasPerBlock) + .addArgument(gasUsedByTransaction) + .addArgument(cumulativeBlockBundleGasUsed) + .addArgument(newCumulativeBlockBundleGasUsed) + .log(); + return BUNDLE_TOO_LARGE_FOR_REMAINING_BUNDLE_BLOCK_GAS; + } + return SELECTED; + } + + /** + * Track the gas used by transactions in bundles + * + * @param cumulativeBlockBundleGasUsed the gas used by selected bundle transactions since the + * beginning of the block + * @param currentBundleGasUsed the gas used only by transactions belonging to the current bundle + */ + public record BundleGasTracker(long cumulativeBlockBundleGasUsed, long currentBundleGasUsed) { + + static BundleGasTracker duplicate(final BundleGasTracker bundleGasTracker) { + // since the record is immutable there is no need to create another instance, and we can just + // return the same + return bundleGasTracker; + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java new file mode 100644 index 00000000..03cbc476 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java @@ -0,0 +1,319 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE_RETRY_LIMIT; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE_UPFRONT; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; + +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.metrics.HistogramMetrics.LabelValue; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +/** + * This class implements TransactionSelector and provides a specific implementation for evaluating + * if the transaction is profitable, according to the current config and the min margin defined for + * this context. Profitability check is done upfront using the gas limit, to avoid processing the + * transaction at all, and if it passes it is done after the processing this time using the actual + * gas used by the transaction. This selector keeps a cache of the unprofitable transactions to + * avoid reprocessing all of them everytime, and only allows for a configurable limited number of + * unprofitable transaction to be retried on every new block creation. + */ +@Slf4j +public class ProfitableTransactionSelector implements PluginTransactionSelector { + public enum Phase implements LabelValue { + PRE_PROCESSING, + POST_PROCESSING; + + final String value; + + Phase() { + this.value = name().toLowerCase(Locale.ROOT); + } + + @Override + public String value() { + return value; + } + } + + @VisibleForTesting protected static Set unprofitableCache = new LinkedHashSet<>(); + protected static Map lastBlockMinRatios = new EnumMap<>(Phase.class); + protected static Map lastBlockMaxRatios = new EnumMap<>(Phase.class); + + static { + resetMinMaxRatios(); + } + + private final LineaTransactionSelectorConfiguration txSelectorConf; + private final LineaProfitabilityConfiguration profitabilityConf; + private final TransactionProfitabilityCalculator transactionProfitabilityCalculator; + private final Optional maybeProfitabilityMetrics; + private final Wei baseFee; + + private int unprofitableRetries; + + public ProfitableTransactionSelector( + final BlockchainService blockchainService, + final LineaTransactionSelectorConfiguration txSelectorConf, + final LineaProfitabilityConfiguration profitabilityConf, + final Optional maybeProfitabilityMetrics) { + this.txSelectorConf = txSelectorConf; + this.profitabilityConf = profitabilityConf; + this.transactionProfitabilityCalculator = + new TransactionProfitabilityCalculator(profitabilityConf); + this.maybeProfitabilityMetrics = maybeProfitabilityMetrics; + maybeProfitabilityMetrics.ifPresent( + histogramMetrics -> { + // temporary solution to update min and max metrics + // we should do this just after the block is created, but we do not have any API for that + // so we postponed the update asap the next block creation starts. + histogramMetrics.setMinMax( + lastBlockMinRatios.get(Phase.PRE_PROCESSING), + lastBlockMaxRatios.get(Phase.PRE_PROCESSING), + Phase.PRE_PROCESSING.value()); + histogramMetrics.setMinMax( + lastBlockMinRatios.get(Phase.POST_PROCESSING), + lastBlockMaxRatios.get(Phase.POST_PROCESSING), + Phase.POST_PROCESSING.value()); + log.atTrace() + .setMessage("Setting profitability ratio metrics for last block to min={}, max={}") + .addArgument(lastBlockMinRatios) + .addArgument(lastBlockMaxRatios) + .log(); + resetMinMaxRatios(); + }); + + this.baseFee = + blockchainService + .getNextBlockBaseFee() + .orElseThrow(() -> new RuntimeException("We only support a base fee market")); + } + + /** + * Evaluates a transaction before processing. Checks if it is profitable using its gas limit. If + * the transaction was found to be unprofitable during a previous block creation process, it is + * retried, since the gas price market could now make it profitable, but only a configurable + * amount of these transactions is retried each time, to avoid that they could potentially consume + * all the time allocated to block creation. + * + * @param evaluationContext The current selection context. + * @return TX_UNPROFITABLE_UPFRONT if the transaction is not profitable upfront, + * TX_UNPROFITABLE_RETRY_LIMIT if the transaction was already found to be unprofitable, and + * there are no more slot to retry past unprofitable transactions during this block creation + * process, otherwise SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + + final Wei minGasPrice = evaluationContext.getMinGasPrice(); + + if (!evaluationContext.getPendingTransaction().hasPriority()) { + final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); + final long gasLimit = transaction.getGasLimit(); + + final var profitablePriorityFeePerGas = + transactionProfitabilityCalculator.profitablePriorityFeePerGas( + transaction, profitabilityConf.minMargin(), gasLimit, minGasPrice); + + updateMetric( + Phase.PRE_PROCESSING, evaluationContext, transaction, profitablePriorityFeePerGas); + + // check the upfront profitability using the gas limit of the tx + if (!transactionProfitabilityCalculator.isProfitable( + "PreProcessing", + profitablePriorityFeePerGas, + transaction, + profitabilityConf.minMargin(), + baseFee, + evaluationContext.getTransactionGasPrice(), + gasLimit, + minGasPrice)) { + return TX_UNPROFITABLE_UPFRONT; + } + + if (unprofitableCache.contains(transaction.getHash())) { + if (unprofitableRetries >= txSelectorConf.unprofitableRetryLimit()) { + log.atTrace() + .setMessage("Limit of unprofitable tx retries reached: {}/{}") + .addArgument(unprofitableRetries) + .addArgument(txSelectorConf.unprofitableRetryLimit()); + return TX_UNPROFITABLE_RETRY_LIMIT; + } + + log.atTrace() + .setMessage("Retrying unprofitable tx. Retry: {}/{}") + .addArgument(unprofitableRetries) + .addArgument(txSelectorConf.unprofitableRetryLimit()); + unprofitableCache.remove(transaction.getHash()); + unprofitableRetries++; + } + } + + return SELECTED; + } + + /** + * Evaluates a transaction post-processing. Checks if it is profitable according to its gas used. + * If unprofitable, the transaction is added to the unprofitable cache, to be retried in the + * future, since gas price market fluctuations can make it profitable again. + * + * @param evaluationContext The current selection context. + * @return TX_UNPROFITABLE if the transaction is not profitable after execution, otherwise + * SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + + if (!evaluationContext.getPendingTransaction().hasPriority()) { + final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); + final long gasUsed = processingResult.getEstimateGasUsedByTransaction(); + + final var profitablePriorityFeePerGas = + transactionProfitabilityCalculator.profitablePriorityFeePerGas( + transaction, + profitabilityConf.minMargin(), + gasUsed, + evaluationContext.getMinGasPrice()); + + updateMetric( + Phase.POST_PROCESSING, evaluationContext, transaction, profitablePriorityFeePerGas); + + if (!transactionProfitabilityCalculator.isProfitable( + "PostProcessing", + profitablePriorityFeePerGas, + transaction, + profitabilityConf.minMargin(), + baseFee, + evaluationContext.getTransactionGasPrice(), + gasUsed, + evaluationContext.getMinGasPrice())) { + rememberUnprofitable(transaction); + return TX_UNPROFITABLE; + } + } + return SELECTED; + } + + /** + * If the transaction has been selected for block inclusion, then we remove it from the + * unprofitable cache. + * + * @param evaluationContext The current selection context + * @param processingResult The result of processing the selected transaction. + */ + @Override + public void onTransactionSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + unprofitableCache.remove(evaluationContext.getPendingTransaction().getTransaction().getHash()); + } + + /** + * If the transaction has not been selected and has been discarded from the transaction pool, then + * we remove it from the unprofitable cache. + * + * @param evaluationContext The current selection context + * @param transactionSelectionResult The transaction selection result + */ + @Override + public void onTransactionNotSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionSelectionResult transactionSelectionResult) { + final var txHash = evaluationContext.getPendingTransaction().getTransaction().getHash(); + if (transactionSelectionResult.discard()) { + unprofitableCache.remove(txHash); + } + } + + private void rememberUnprofitable(final Transaction transaction) { + while (unprofitableCache.size() >= txSelectorConf.unprofitableCacheSize()) { + final var it = unprofitableCache.iterator(); + if (it.hasNext()) { + it.next(); + it.remove(); + } + } + unprofitableCache.add(transaction.getHash()); + log.atTrace().setMessage("unprofitableCache={}").addArgument(unprofitableCache::size).log(); + } + + private void updateMetric( + final Phase label, + final TransactionEvaluationContext evaluationContext, + final Transaction tx, + final Wei profitablePriorityFeePerGas) { + + final var effectivePriorityFee = evaluationContext.getTransactionGasPrice().subtract(baseFee); + final var ratio = + effectivePriorityFee.getValue().doubleValue() + / profitablePriorityFeePerGas.getValue().doubleValue(); + + maybeProfitabilityMetrics.ifPresent( + histogramMetrics -> { + histogramMetrics.track(ratio, label.value()); + + if (ratio < lastBlockMinRatios.get(label)) { + lastBlockMinRatios.put(label, ratio); + } + if (ratio > lastBlockMaxRatios.get(label)) { + lastBlockMaxRatios.put(label, ratio); + } + }); + + log.atTrace() + .setMessage( + "{}: block[{}] tx {} , baseFee {}, effectiveGasPrice {}, ratio (effectivePayingPriorityFee {} / calculatedProfitablePriorityFee {}) {}") + .addArgument(label.name()) + .addArgument(evaluationContext.getPendingBlockHeader().getNumber()) + .addArgument(tx.getHash()) + .addArgument(baseFee::toHumanReadableString) + .addArgument(evaluationContext.getTransactionGasPrice()::toHumanReadableString) + .addArgument(effectivePriorityFee::toHumanReadableString) + .addArgument(profitablePriorityFeePerGas::toHumanReadableString) + .addArgument(ratio) + .log(); + } + + private static void resetMinMaxRatios() { + lastBlockMinRatios.put(Phase.PRE_PROCESSING, Double.POSITIVE_INFINITY); + lastBlockMinRatios.put(Phase.POST_PROCESSING, Double.POSITIVE_INFINITY); + lastBlockMaxRatios.put(Phase.PRE_PROCESSING, Double.NEGATIVE_INFINITY); + lastBlockMaxRatios.put(Phase.POST_PROCESSING, Double.NEGATIVE_INFINITY); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelector.java new file mode 100644 index 00000000..6263ecef --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelector.java @@ -0,0 +1,244 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BLOCK_MODULE_LINE_COUNT_FULL; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW_CACHED; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_INVALID_COUNT; +import static net.consensys.linea.zktracer.Fork.LONDON; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; + +import java.math.BigInteger; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.zktracer.ZkTracer; +import net.consensys.linea.zktracer.container.module.Module; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.BlockBody; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.AbstractStatefulPluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +/** + * This class implements TransactionSelector and provides a specific implementation for evaluating + * transactions based on the number of trace lines per module created by a transaction. It checks if + * adding a transaction to the block pushes the trace lines for a module over the limit. + */ +@Slf4j +public class TraceLineLimitTransactionSelector + extends AbstractStatefulPluginTransactionSelector> { + private static final Marker BLOCK_LINE_COUNT_MARKER = MarkerFactory.getMarker("BLOCK_LINE_COUNT"); + @VisibleForTesting protected static Set overLineCountLimitCache = new LinkedHashSet<>(); + private final ZkTracer zkTracer; + private final BigInteger chainId; + private final String limitFilePath; + private final Map moduleLimits; + private final int overLimitCacheSize; + private final ModuleLineCountValidator moduleLineCountValidator; + + public TraceLineLimitTransactionSelector( + final SelectorsStateManager stateManager, + final BigInteger chainId, + final Map moduleLimits, + final LineaTransactionSelectorConfiguration txSelectorConfiguration, + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final LineaTracerConfiguration tracerConfiguration) { + super( + stateManager, + moduleLimits.keySet().stream().collect(Collectors.toMap(Function.identity(), unused -> 0)), + Map::copyOf); + + this.chainId = chainId; + this.moduleLimits = moduleLimits; + this.limitFilePath = tracerConfiguration.moduleLimitsFilePath(); + this.overLimitCacheSize = txSelectorConfiguration.overLinesLimitCacheSize(); + + zkTracer = new ZkTracerWithLog(l1L2BridgeConfiguration); + for (Module m : zkTracer.getHub().getModulesToCount()) { + if (!moduleLimits.containsKey(m.moduleKey())) { + throw new IllegalStateException( + "Limit for module %s not defined in %s".formatted(m.moduleKey(), this.limitFilePath)); + } + } + zkTracer.traceStartConflation(1L); + moduleLineCountValidator = new ModuleLineCountValidator(moduleLimits); + } + + /** + * Check if the tx is already known to go over the limit to avoid reprocessing it + * + * @param evaluationContext The current selection context. + * @return transaction selection result + */ + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + if (overLineCountLimitCache.contains( + evaluationContext.getPendingTransaction().getTransaction().getHash())) { + log.atTrace() + .setMessage( + "Transaction {} was already identified to go over line count limit, dropping it") + .addArgument(evaluationContext.getPendingTransaction().getTransaction()::getHash) + .log(); + return TX_MODULE_LINE_COUNT_OVERFLOW_CACHED; + } + return SELECTED; + } + + /** + * Checking the created trace lines is performed post-processing. + * + * @param evaluationContext The current selection context. + * @param processingResult The result of the transaction processing. + * @return BLOCK_MODULE_LINE_COUNT_FULL if the trace lines for a module are over the limit for the + * block, TX_MODULE_LINE_COUNT_OVERFLOW if the trace lines are over the limit for the single + * tx, otherwise SELECTED. + */ + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + + final var prevCumulatedLineCountMap = getWorkingState(); + + // check that we are not exceeding line number for any module + final var newCumulatedLineCountMap = zkTracer.getModulesLineCount(); + final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); + log.atTrace() + .setMessage("Tx {} line count per module: {}") + .addArgument(transaction::getHash) + .addArgument(() -> logTxLineCount(newCumulatedLineCountMap, prevCumulatedLineCountMap)) + .log(); + + ModuleLimitsValidationResult result = + moduleLineCountValidator.validate(newCumulatedLineCountMap, prevCumulatedLineCountMap); + + switch (result.getResult()) { + case MODULE_NOT_DEFINED: + log.error("Module {} does not exist in the limits file.", result.getModuleName()); + throw new RuntimeException( + "Module " + result.getModuleName() + " does not exist in the limits file."); + case INVALID_LINE_COUNT: + log.warn( + "Tx {} line count for module {}={} is invalid, removing from the txpool", + transaction.getHash(), + result.getModuleName(), + result.getModuleLineCount()); + return TX_MODULE_LINE_INVALID_COUNT; + case TX_MODULE_LINE_COUNT_OVERFLOW: + log.warn( + "Tx {} line count for module {}={} is above the limit {}, removing from the txpool", + transaction.getHash(), + result.getModuleName(), + result.getModuleLineCount(), + result.getModuleLineLimit()); + rememberOverLineCountLimitTransaction(transaction); + return TX_MODULE_LINE_COUNT_OVERFLOW; + case BLOCK_MODULE_LINE_COUNT_FULL: + log.atTrace() + .setMessage( + "Cumulated line count for module {}={} is above the limit {}, stopping selection") + .addArgument(result.getModuleName()) + .addArgument(result.getCumulativeModuleLineCount()) + .addArgument(result.getCumulativeModuleLineLimit()) + .log(); + return BLOCK_MODULE_LINE_COUNT_FULL; + default: + break; + } + + setWorkingState(newCumulatedLineCountMap); + + return SELECTED; + } + + @Override + public ZkTracer getOperationTracer() { + return zkTracer; + } + + private void rememberOverLineCountLimitTransaction(final Transaction transaction) { + while (overLineCountLimitCache.size() >= overLimitCacheSize) { + final var it = overLineCountLimitCache.iterator(); + if (it.hasNext()) { + it.next(); + it.remove(); + } + } + overLineCountLimitCache.add(transaction.getHash()); + log.atTrace() + .setMessage("overLineCountLimitCache={}") + .addArgument(overLineCountLimitCache::size) + .log(); + } + + private String logTxLineCount( + final Map currCumulatedLineCount, + final Map stateLineLimitMap) { + return currCumulatedLineCount.entrySet().stream() + .map( + e -> + // tx line count / cumulated line count / line count limit + e.getKey() + + "=" + + (e.getValue() - stateLineLimitMap.getOrDefault(e.getKey(), 0)) + + "/" + + e.getValue() + + "/" + + moduleLimits.get(e.getKey())) + .collect(Collectors.joining(",", "[", "]")); + } + + private class ZkTracerWithLog extends ZkTracer { + public ZkTracerWithLog(final LineaL1L2BridgeSharedConfiguration bridgeConfiguration) { + super(LONDON, bridgeConfiguration, chainId); + } + + @Override + public void traceEndBlock(final BlockHeader blockHeader, final BlockBody blockBody) { + super.traceEndBlock(blockHeader, blockBody); + log.atDebug() + .addMarker(BLOCK_LINE_COUNT_MARKER) + .addKeyValue("blockNumber", blockHeader::getNumber) + .addKeyValue("blockHash", blockHeader::getBlockHash) + .addKeyValue( + "traceCounts", + () -> + getCommitedState().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> '"' + e.getKey() + "\":" + e.getValue()) + .collect(Collectors.joining(","))) + .log(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/Compressor.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/Compressor.java new file mode 100644 index 00000000..38dcbfd7 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/Compressor.java @@ -0,0 +1,40 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.utils; + +import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorVersion; +import linea.blob.GoBackedBlobCompressor; +import org.apache.logging.log4j.LogManager; + +public class Compressor { + public static BlobCompressor instance; + + static { + try { + instance = + GoBackedBlobCompressor.getInstance( + BlobCompressorVersion.V1_2, + // 100KB to match coordinator config. + // However, is not relevant for the sequencer because it does not create blobs. + 102400); + } catch (Throwable t) { + LogManager.getLogger(Compressor.class) + .error("Failed to load GoBackedBlobCompressor. errorMessage={}", t.getMessage(), t); + throw new RuntimeException("Failed to load GoBackedBlobCompressor", t); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/PriorityThreadPoolExecutor.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/PriorityThreadPoolExecutor.java new file mode 100644 index 00000000..3d5a9442 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/PriorityThreadPoolExecutor.java @@ -0,0 +1,84 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.utils; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +public class PriorityThreadPoolExecutor extends ThreadPoolExecutor { + public PriorityThreadPoolExecutor( + final int corePoolSize, + final int maximumPoolSize, + final long keepAliveTime, + final TimeUnit unit, + final ThreadFactory threadFactory) { + super( + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + new PriorityBlockingQueue<>(), + threadFactory); + } + + @Override + protected RunnableFuture newTaskFor(final Runnable runnable, final T value) { + return new PriorityFuture<>(runnable, value); + } + + @Override + protected RunnableFuture newTaskFor(final Callable callable) { + return new PriorityFuture<>(callable); + } + + public boolean remove(final Callable callable) { + return super.remove(new PriorityFuture<>(callable)); + } + + public boolean remove(final Runnable runnable) { + return super.remove(new PriorityFuture<>(runnable, null)); + } + + // we delegate equality to source class so the remove works + @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) + public static class PriorityFuture extends FutureTask + implements Comparable> { + @Getter @EqualsAndHashCode.Include private Object sourceTask; + + public PriorityFuture(final Runnable runnable, final T result) { + super(runnable, result); + sourceTask = runnable; + } + + public PriorityFuture(final Callable callable) { + super(callable); + sourceTask = callable; + } + + @Override + @SuppressWarnings("unchecked") + public int compareTo(PriorityFuture o) { + return ((Comparable) sourceTask).compareTo(o.sourceTask); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/AbstractBundleTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/AbstractBundleTest.java new file mode 100644 index 00000000..2f56a92c --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/AbstractBundleTest.java @@ -0,0 +1,58 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPPrivateKey; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; + +abstract class AbstractBundleTest { + protected static final ObjectMapper OBJECT_MAPPER = + new ObjectMapper().registerModule(new Jdk8Module()); + protected static final KeyPair KEY_PAIR = + new KeyPair( + SECPPrivateKey.create(BigInteger.valueOf(Long.MAX_VALUE), SignatureAlgorithm.ALGORITHM), + SECPPublicKey.create(BigInteger.valueOf(Long.MIN_VALUE), SignatureAlgorithm.ALGORITHM)); + + protected static final Transaction TX1 = + new TransactionTestFixture().nonce(0).gasLimit(21000).createTransaction(KEY_PAIR); + protected static final Transaction TX2 = + new TransactionTestFixture().nonce(1).gasLimit(21000).createTransaction(KEY_PAIR); + protected static final Transaction TX3 = + new TransactionTestFixture().nonce(2).gasLimit(21000).createTransaction(KEY_PAIR); + + protected TransactionBundle createBundle( + Hash hash, long blockNumber, List maybeTxs) { + return new TransactionBundle( + hash, + maybeTxs, + blockNumber, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/BundleForwarderTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/BundleForwarderTest.java new file mode 100644 index 00000000..e4d94531 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/BundleForwarderTest.java @@ -0,0 +1,422 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.getAllServeEvents; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.consensys.linea.bundles.BundleForwarder.RETRY_COUNT_HEADER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.lenient; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.time.Duration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.matching.StringValuePattern; +import net.consensys.linea.bundles.BundleForwarder.SendBundleResponse; +import net.consensys.linea.config.LineaBundleConfiguration; +import net.consensys.linea.utils.PriorityThreadPoolExecutor; +import net.consensys.linea.utils.TestablePriorityThreadPoolExecutor; +import okhttp3.OkHttpClient; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@WireMockTest +@ExtendWith(MockitoExtension.class) +class BundleForwarderTest extends AbstractBundleTest { + private static final AtomicLong REQ_ID_COUNT = new AtomicLong(0); + private static final Duration RPC_CALL_TIMEOUT = Duration.ofSeconds(2); + private static final long CHAIN_HEAD_BLOCK_NUMBER = 5L; + private LineaBundleConfiguration bundleConfiguration; + private BundleForwarder bundleForwarder; + private TestablePriorityThreadPoolExecutor executor; + private ResponseCollector responseCollector; + + @Mock(answer = RETURNS_DEEP_STUBS) + private BlockchainService blockchainService; + + @BeforeEach + void init(final WireMockRuntimeInfo wmInfo) throws MalformedURLException { + REQ_ID_COUNT.set(0); + lenient() + .when(blockchainService.getChainHeadHeader().getNumber()) + .thenReturn(CHAIN_HEAD_BLOCK_NUMBER); + responseCollector = new ResponseCollector(); + executor = + new TestablePriorityThreadPoolExecutor(0, 1, 1, SECONDS, Thread.ofVirtual().factory()); + executor.addAfterExecuteListener(responseCollector); + bundleConfiguration = + LineaBundleConfiguration.builder() + .forwardUrls(Set.of(URI.create(wmInfo.getHttpBaseUrl()).toURL())) + .timeoutMillis(5000) + .retryDelayMillis(100) + .build(); + + bundleForwarder = + new BundleForwarder( + bundleConfiguration, + executor, + Executors.newSingleThreadScheduledExecutor(), + blockchainService, + new OkHttpClient.Builder().callTimeout(RPC_CALL_TIMEOUT).build(), + URI.create(wmInfo.getHttpBaseUrl()).toURL()); + } + + @AfterEach + void cleanup() { + executor.shutdown(); + } + + @Test + void bundleIsForwarded() throws IOException { + final var bundle = + createBundle( + Hash.fromHexStringLenient("0x1234"), CHAIN_HEAD_BLOCK_NUMBER + 1, List.of(TX1, TX2)); + stubSuccessResponseFor(bundle, 0); + + bundleForwarder.onTransactionBundleAdded(bundle); + + verifyRequestSentFor(bundle, 0); + responseCollector.assertSuccessResponse(0); + } + + @Test + void timeoutRequestIsRetried() throws IOException { + final var bundle = + createBundle( + Hash.fromHexStringLenient("0x1234"), CHAIN_HEAD_BLOCK_NUMBER + 1, List.of(TX1, TX2)); + stubTimeoutResponseFor(bundle, 0); + + bundleForwarder.onTransactionBundleAdded(bundle); + + executor.addAfterExecuteListener( + (r, t) -> + // now change the response to success at the second attempt + stubSuccessResponseFor(bundle, 1)); + + verifyRequestSentFor(bundle, 0); + responseCollector.assertFailedResponse(0, InterruptedIOException.class); + verifyRequestSentFor(bundle, 1, 1); + responseCollector.assertSuccessResponse(1); + } + + @Test + void bundleWithLowerBlockNumberIsSentFirst() + throws IOException, ExecutionException, InterruptedException { + final var bundle_bn1 = + createBundle( + Hash.fromHexStringLenient("0x9abc"), CHAIN_HEAD_BLOCK_NUMBER + 1, List.of(TX1, TX3)); + final var bundle_bn2 = + createBundle( + Hash.fromHexStringLenient("0x5678"), CHAIN_HEAD_BLOCK_NUMBER + 2, List.of(TX2, TX3)); + + stubSuccessResponseFor(bundle_bn1, 0); + stubSuccessResponseFor(bundle_bn2, 1); + + // since we want to form a queue of bundle to form, we just wait before executing the send + // bundle task that all the expected tasks are queued + executor.waitForQueueTaskCount(2, true); + + // we submit bundle_bn2, bundle_bn1 in this order + // we expect they are sent in this order: bundle_bn1, bundle_bn2 + // with lower block number bundles sent first + bundleForwarder.onTransactionBundleAdded(bundle_bn2); + bundleForwarder.onTransactionBundleAdded(bundle_bn1); + + // reqId ensures the correct order + verifyRequestSentFor(bundle_bn1, 0); + responseCollector.assertSuccessResponse(0); + verifyRequestSentFor(bundle_bn2, 1); + responseCollector.assertSuccessResponse(1); + } + + @Test + void requestForAlreadyImportedBlockIsSkipped() { + final var bundle = + createBundle( + Hash.fromHexStringLenient("0x1234"), CHAIN_HEAD_BLOCK_NUMBER, List.of(TX1, TX2)); + bundleForwarder.onTransactionBundleAdded(bundle); + + responseCollector.assertSkippedBundle(bundle); + + // ensure no rpc call was sent + assertThat(getAllServeEvents()).isEmpty(); + } + + @Test + void failedRequestIsRetriedAfterBundlesForTheNextBlock() + throws IOException, ExecutionException, InterruptedException { + final var bundle_bn1 = + createBundle( + Hash.fromHexStringLenient("0x9abc"), CHAIN_HEAD_BLOCK_NUMBER + 1, List.of(TX1, TX3)); + final var bundle_bn2 = + createBundle( + Hash.fromHexStringLenient("0x5678"), CHAIN_HEAD_BLOCK_NUMBER + 2, List.of(TX2, TX3)); + + stubTimeoutResponseFor(bundle_bn1, 0); + stubSuccessResponseFor(bundle_bn2, 1); + + // since we want to form a queue of bundle to form, we just wait before executing the send + // bundle task that all the expected tasks are queued + executor.waitForQueueTaskCount(2, true); + + // we submit bundle_bn2, bundle_bn1 in this order + // we expect they are sent in this order: bundle_bn1, bundle_bn2 + // but bundle_bn1 timeout at first attempt and is retried after bundle_bn2 + bundleForwarder.onTransactionBundleAdded(bundle_bn2); + bundleForwarder.onTransactionBundleAdded(bundle_bn1); + + final var isFirstExecution = new AtomicBoolean(false); + executor.addAfterExecuteListener( + (r, t) -> { + // we expect bundle_bn1 to be processed first and retried so we can wait for the task + // queue to be filled again, but this time bundle_bn2 should have priority over bundle_bn1 + if (isFirstExecution.compareAndSet(false, true)) { + // change the response to success at the second attempt + stubSuccessResponseFor(bundle_bn1, 2); + + try { + executor.waitForQueueTaskCount(2, false); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + }); + + final var allEvents = getAllServeEvents(); + + allEvents.forEach(event -> System.out.println(event)); + + // reqId ensures the correct order + verifyRequestSentFor(bundle_bn1, 0); + responseCollector.assertFailedResponse(0, InterruptedIOException.class); + verifyRequestSentFor(bundle_bn2, 1); + responseCollector.assertSuccessResponse(1); + verifyRequestSentFor(bundle_bn1, 2, 1); + responseCollector.assertSuccessResponse(2); + } + + @Test + void cancelledBundleIsNotForwarded() throws InterruptedException { + final var bundle = + createBundle( + Hash.fromHexStringLenient("0x9abc"), CHAIN_HEAD_BLOCK_NUMBER + 1, List.of(TX1, TX3)); + + // since we want that the bundle is not forwarded before we have a chance to remove it + // we pause the execution + final var semaphore = executor.pauseExecution(); + + bundleForwarder.onTransactionBundleAdded(bundle); + assertThat(executor.getQueue()).map(this::extractBundle).containsExactly(bundle); + + bundleForwarder.onTransactionBundleRemoved(bundle); + assertThat(executor.getQueue()).isEmpty(); + + // resume execution + semaphore.release(); + + executor.executeSomething(); + + // expect that both internal tasks are completed + await().until(() -> executor.getCompletedTaskCount() == 2); + + // ensure no rpc call was sent + assertThat(getAllServeEvents()).isEmpty(); + } + + private static String getExpectedRequest(final TransactionBundle bundle, final long reqId) + throws JsonProcessingException { + final var expectedRequest = + """ + { + "jsonrpc": "2.0", + "id": , + "method": "linea_sendBundle", + "params": [] + } + """ + .replace("", OBJECT_MAPPER.writeValueAsString(bundle.toBundleParameter(false))) + .replace("", String.valueOf(reqId)); + return expectedRequest; + } + + private static void stubSuccessResponseFor(final TransactionBundle bundle, final long reqId) { + stubFor( + post(urlEqualTo("/")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(matchingBlockNumberAndReqId(bundle.blockNumber(), reqId)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + """ + { + "jsonrpc": "2.0", + "result": { + "bundleHash": "" + }, + "id": + }""" + .replace("", bundle.bundleIdentifier().toHexString()) + .replace("", String.valueOf(reqId))))); + } + + private static void stubTimeoutResponseFor(final TransactionBundle bundle, final long reqId) { + stubFor( + post(urlEqualTo("/")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(matchingBlockNumberAndReqId(bundle.blockNumber(), reqId)) + .willReturn(aResponse().withFixedDelay(10_000))); + } + + private static StringValuePattern matchingBlockNumberAndReqId( + final long blockNumber, final long reqId) { + return matchingJsonPath("$.params[?(@.blockNumber == %d)]".formatted(blockNumber)) + .and(matchingJsonPath("$[?(@.id == %d)]".formatted(reqId))); + } + + private static void verifyRequestSentFor(final TransactionBundle bundle, final long reqId) + throws JsonProcessingException { + verifyRequestSentFor(bundle, reqId, 0); + } + + private static void verifyRequestSentFor( + final TransactionBundle bundle, final long reqId, final int retryCount) + throws JsonProcessingException { + final var expectedRequest = getExpectedRequest(bundle, reqId); + + final var patterBuilder = + postRequestedFor(urlEqualTo("/")) + .withHeader("Content-Type", equalTo("application/json; charset=UTF-8")) + .withRequestBody(equalToJson(expectedRequest)); + + if (retryCount > 0) { + patterBuilder.withHeader(RETRY_COUNT_HEADER, equalTo(String.valueOf(retryCount))); + } else { + patterBuilder.withoutHeader(RETRY_COUNT_HEADER); + } + + await().atMost(2, SECONDS).untilAsserted(() -> verify(exactly(1), patterBuilder)); + } + + @SuppressWarnings("unchecked") + private PriorityThreadPoolExecutor.PriorityFuture extractPriorityFuture( + final Runnable r) { + return (PriorityThreadPoolExecutor.PriorityFuture) r; + } + + private BundleForwarder.SendBundleTask extractSendBundleTask(final Runnable r) { + return (BundleForwarder.SendBundleTask) extractPriorityFuture(r).getSourceTask(); + } + + private TransactionBundle extractBundle(final Runnable r) { + return extractSendBundleTask(r).getBundle(); + } + + private class ResponseCollector + implements TestablePriorityThreadPoolExecutor.AfterExecuteListener { + private final Map responses = new HashMap<>(); + private final Map failures = new HashMap<>(); + private final Set skipped = new HashSet<>(); + + @Override + public void onAfterExecute(final Runnable r, final Throwable t) { + try { + final var response = extractPriorityFuture(r).get(); + responses.put(response.reqId(), response); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof BundleForwarder.BundleForwarderException bfe) { + bfe.reqId() + .ifPresentOrElse(reqId -> failures.put(reqId, e), () -> skipped.add(bfe.bundle())); + } else { + fail(e); + } + } + } + + public void assertSuccessResponse(final long reqId) throws IOException { + await().until(() -> responses.containsKey(reqId) || failures.containsKey(reqId)); + if (failures.containsKey(reqId)) { + fail(failures.get(reqId)); + } + + final var response = responses.get(reqId); + assertTrue(response.response().isSuccessful()); + assertThat(getReqId(response)).isEqualTo(reqId); + assertThat(getBundleHash(response)) + .isEqualTo(response.bundle().bundleIdentifier().toHexString()); + } + + public void assertFailedResponse( + final long reqId, final Class expectedException) { + await().until(() -> responses.containsKey(reqId) || failures.containsKey(reqId)); + + if (responses.containsKey(reqId)) { + assertFalse(responses.get(reqId).response().isSuccessful()); + } + + assertThat(failures.get(reqId).getCause().getCause()).isInstanceOf(expectedException); + } + + public void assertSkippedBundle(final TransactionBundle bundle) { + await().until(() -> skipped.contains(bundle)); + } + + private String getBundleHash(final SendBundleResponse response) throws JsonProcessingException { + return OBJECT_MAPPER.readTree(response.body()).findValue("bundleHash").asText(); + } + + private long getReqId(final SendBundleResponse response) throws JsonProcessingException { + return OBJECT_MAPPER.readTree(response.body()).get("id").asLong(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/LineaLimitedBundlePoolTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/LineaLimitedBundlePoolTest.java new file mode 100644 index 00000000..d2e7a28a --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/LineaLimitedBundlePoolTest.java @@ -0,0 +1,456 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static net.consensys.linea.bundles.LineaLimitedBundlePool.BUNDLE_SAVE_FILENAME; +import static net.consensys.linea.bundles.LineaLimitedBundlePool.UUIDToHash; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import lombok.Getter; +import net.consensys.linea.bundles.BundlePoolService.TransactionBundleAddedListener; +import net.consensys.linea.bundles.BundlePoolService.TransactionBundleRemovedListener; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.plugin.data.AddedBlockContext; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class LineaLimitedBundlePoolTest extends AbstractBundleTest { + @TempDir Path dataDir; + private LineaLimitedBundlePool pool; + private BesuEvents eventService; + private AddedBlockContext addedBlockContext; + private BlockHeader blockHeader; + private BlockchainService blockchainService; + private NotificationCollector notificationCollector; + + @BeforeEach + void setUp() { + eventService = mock(BesuEvents.class); + addedBlockContext = mock(AddedBlockContext.class); + blockchainService = mock(BlockchainService.class, RETURNS_DEEP_STUBS); + when(blockchainService.getChainHeadHeader().getNumber()).thenReturn(10L); + pool = + new LineaLimitedBundlePool( + dataDir, 10_000L, eventService, blockchainService); // Max 100 entries, 10 KB size + blockHeader = mock(BlockHeader.class); + notificationCollector = new NotificationCollector(); + pool.subscribeTransactionBundleRemoved(notificationCollector); + pool.subscribeTransactionBundleAdded(notificationCollector); + } + + @Test + void smokeTestPutAndGetByHash() { + Hash hash = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle = createBundle(hash, 1); + + pool.putOrReplace(hash, bundle); + TransactionBundle retrieved = pool.get(hash); + + notificationCollector.assertAddNotificationReceived(bundle); + assertNotNull(retrieved, "Bundle should be retrieved by hash"); + assertEquals(hash, retrieved.bundleIdentifier(), "Retrieved bundle hash should match"); + } + + @Test + void smokeTestGetBundlesByBlockNumber() { + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + TransactionBundle bundle1 = createBundle(hash1, 1); + TransactionBundle bundle2 = createBundle(hash2, 1); + + pool.putOrReplace(hash1, bundle1); + notificationCollector.assertAddNotificationReceived(bundle1); + + pool.putOrReplace(hash2, bundle2); + notificationCollector.assertAddNotificationReceived(bundle2); + + List bundles = pool.getBundlesByBlockNumber(1); + + assertEquals(2, bundles.size(), "There should be two bundles for block 1"); + assertTrue(bundles.contains(bundle1), "Bundles should contain bundle1"); + assertTrue(bundles.contains(bundle2), "Bundles should contain bundle2"); + } + + @Test + void testPutAndGetByUUID() { + UUID uuid = UUID.randomUUID(); + TransactionBundle bundle = createBundle(Hash.ZERO, 1L); + + pool.putOrReplace(uuid, bundle); + + notificationCollector.assertAddNotificationReceived(bundle); + assertNotNull(pool.get(uuid)); + assertEquals(bundle, pool.get(uuid)); + } + + @Test + void testPutAndGetByHash() { + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0x1234")); + TransactionBundle bundle = createBundle(hash, 1L); + + pool.putOrReplace(hash, bundle); + + notificationCollector.assertAddNotificationReceived(bundle); + assertNotNull(pool.get(hash)); + assertEquals(bundle, pool.get(hash)); + } + + @Test + void testPutAndGet_ReplaceExistingBundle() { + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0x1234")); + TransactionBundle bundleOrig = createBundle(hash, 1L); + + pool.putOrReplace(hash, bundleOrig); + + notificationCollector.assertAddNotificationReceived(bundleOrig); + assertNotNull(pool.get(hash)); + assertEquals(bundleOrig, pool.get(hash)); + + TransactionBundle bundleReplace = createBundle(hash, 2L); + pool.putOrReplace(hash, bundleReplace); + notificationCollector.assertAddNotificationReceived(bundleReplace); + notificationCollector.assertRemoveNotificationReceived(bundleOrig); + assertEquals(bundleReplace, pool.get(hash)); + assertThat(pool.getBundlesByBlockNumber(1L)).isEmpty(); + } + + @Test + void testPutAndGet_ThrowsException_WhenFrozen() { + // saving to disk freeze the pool + pool.saveToDisk(); + assertTrue(pool.isFrozen()); + + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0x1234")); + TransactionBundle bundle = createBundle(hash, 1L); + + assertThatThrownBy(() -> pool.putOrReplace(hash, bundle)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Bundle pool is not accepting modifications"); + } + + @Test + void testRemoveByUUID() { + UUID uuid = UUID.randomUUID(); + TransactionBundle bundle = createBundle(UUIDToHash(uuid), 1L); + + pool.putOrReplace(uuid, bundle); + + notificationCollector.assertAddNotificationReceived(bundle); + + assertTrue(pool.remove(uuid)); + notificationCollector.assertRemoveNotificationReceived(bundle); + } + + @Test + void testRemoveByHash() { + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0x5678")); + TransactionBundle bundle = createBundle(hash, 1L); + + pool.putOrReplace(hash, bundle); + notificationCollector.assertAddNotificationReceived(bundle); + + assertTrue(pool.remove(hash)); + notificationCollector.assertRemoveNotificationReceived(bundle); + } + + @Test + void testGetByUUID_NotFound() { + UUID uuid = UUID.randomUUID(); + assertNull(pool.get(uuid)); + } + + @Test + void testGetByHash_NotFound() { + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0x9876")); + assertNull(pool.get(hash)); + } + + @Test + void testRemoveByUUID_NotFound() { + UUID uuid = UUID.randomUUID(); + assertFalse(pool.remove(uuid)); + } + + @Test + void testRemoveByHash_NotFound() { + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0xabcd")); + assertFalse(pool.remove(hash)); + } + + @Test + void testRemove_ThrowsException_WhenFrozen() { + // saving to disk freeze the pool + pool.saveToDisk(); + assertTrue(pool.isFrozen()); + + Hash hash = Hash.hash(Bytes.fromHexStringLenient("0xabcd")); + assertThatThrownBy(() -> pool.remove(hash)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Bundle pool is not accepting modifications"); + } + + @Test + void testOnBlockAdded_RemovesOldBundles() { + // Prepare old block number + long oldBlockNumber = 10L; + long newBlockNumber = 15L; + Hash mockOldHash = Hash.ZERO; + + // Mock block header behavior + when(addedBlockContext.getBlockHeader()).thenReturn(blockHeader); + when(blockHeader.getNumber()).thenReturn(newBlockNumber); + + // Create a fake transaction bundle + TransactionBundle oldBundle = createBundle(mockOldHash, oldBlockNumber); + + // Manually insert old bundle into the block index + pool.putOrReplace(mockOldHash, oldBundle); + notificationCollector.assertAddNotificationReceived(oldBundle); + + // Ensure bundle exists before adding new block + assert !pool.getBundlesByBlockNumber(oldBlockNumber).isEmpty(); + + // Call the method under test + pool.onBlockAdded(addedBlockContext); + + // Verify that the old bundle is removed + assert pool.getBundlesByBlockNumber(oldBlockNumber).isEmpty(); + notificationCollector.assertRemoveNotificationReceived(oldBundle); + } + + @Test + void testOnBlockAdded_DoesNothing_WhenFrozen() { + // Prepare old block number + long oldBlockNumber = 10L; + long newBlockNumber = 15L; + Hash mockOldHash = Hash.ZERO; + + // Mock block header behavior + when(addedBlockContext.getBlockHeader()).thenReturn(blockHeader); + when(blockHeader.getNumber()).thenReturn(newBlockNumber); + + // Create a fake transaction bundle + TransactionBundle oldBundle = createBundle(mockOldHash, oldBlockNumber); + + // Manually insert old bundle into the block index + pool.putOrReplace(mockOldHash, oldBundle); + + // Ensure bundle exists before adding new block + assert !pool.getBundlesByBlockNumber(oldBlockNumber).isEmpty(); + + // saving to disk freeze the pool + pool.saveToDisk(); + assertTrue(pool.isFrozen()); + + // Call the method under test + pool.onBlockAdded(addedBlockContext); + + // Verify that the old bundle is still there + assertThat(pool.getBundlesByBlockNumber(oldBlockNumber)).containsExactly(oldBundle); + } + + @Test + void saveToDisk() throws IOException { + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = createBundle(hash1, 1, List.of(TX1, TX2)); + pool.putOrReplace(hash1, bundle1); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + TransactionBundle bundle2 = createBundle(hash2, 2, List.of(TX3)); + pool.putOrReplace(hash2, bundle2); + + pool.saveToDisk(); + + final var saved = Files.readString(dataDir.resolve(BUNDLE_SAVE_FILENAME), US_ASCII); + assertThat(saved) + .isEqualTo( + """ + {"version":1} + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":1,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}} + {"0x0000000000000000000000000000000000000000000000000000000000005678":{"blockNumber":2,"txs":["+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx"]}}"""); + } + + @Test + void loadFromDisk() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + {"version":1} + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":11,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}} + {"0x0000000000000000000000000000000000000000000000000000000000005678":{"blockNumber":12,"txs":["+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx"]}}""", + US_ASCII); + + pool.loadFromDisk(); + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = pool.get(hash1); + + notificationCollector.assertAddNotificationReceived(bundle1); + assertThat(bundle1.blockNumber()).isEqualTo(11); + assertThat(bundle1.bundleIdentifier()).isEqualTo(hash1); + assertThat(bundle1.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + + TransactionBundle bundle2 = pool.get(hash2); + + notificationCollector.assertAddNotificationReceived(bundle2); + assertThat(bundle2.blockNumber()).isEqualTo(12); + assertThat(bundle2.bundleIdentifier()).isEqualTo(hash2); + assertThat(bundle2.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX3.getHash()); + } + + @Test + void loadFromDisk_UnsupportedVersion() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + {"version":0} + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":11,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}}""", + US_ASCII); + + pool.loadFromDisk(); + + // no bundle should be restored + assertThat(pool.size()).isEqualTo(0); + } + + @Test + void partialLoadFromDisk_DueToInvalidLine() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + {"version":1} + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":11,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}} + {"0x0000000000000000000000000000000000000000000000000000000000005678":{"blockNumber":"not a number","txs":["+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx"]}}""", + US_ASCII); + + pool.loadFromDisk(); + + assertThat(pool.size()).isEqualTo(1); + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = pool.get(hash1); + + notificationCollector.assertAddNotificationReceived(bundle1); + assertThat(bundle1.blockNumber()).isEqualTo(11); + assertThat(bundle1.bundleIdentifier()).isEqualTo(hash1); + assertThat(bundle1.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + + assertThat(pool.get(hash2)).isNull(); + } + + @Test + void partialLoadFromDisk_DueOldBlockNumber() throws IOException { + Files.writeString( + dataDir.resolve(BUNDLE_SAVE_FILENAME), + """ + {"version":1} + {"0x0000000000000000000000000000000000000000000000000000000000005678":{"blockNumber":10,"txs":["+E8CghOIglIIgASAggqVoMmdnUf+4fBBE+l/IAxacTZhj5elWnFdplP+s4jg92yyoHUWAGDUZ5Vo6dg3q7e9+PyBAkwlk4Fprh1UFmyQhhjx"]}} + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":11,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}}""", + US_ASCII); + + pool.loadFromDisk(); + + assertThat(pool.size()).isEqualTo(1); + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = pool.get(hash1); + + notificationCollector.assertAddNotificationReceived(bundle1); + assertThat(bundle1.blockNumber()).isEqualTo(11); + assertThat(bundle1.bundleIdentifier()).isEqualTo(hash1); + assertThat(bundle1.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + + Hash hash2 = Hash.fromHexStringLenient("0x5678"); + + assertThat(pool.get(hash2)).isNull(); + } + + private TransactionBundle createBundle(Hash hash, long blockNumber) { + return createBundle(hash, blockNumber, Collections.emptyList()); + } + + private static class NotificationCollector + implements TransactionBundleRemovedListener, TransactionBundleAddedListener { + @Getter private final Map events = new LinkedHashMap<>(); + + enum EventType { + ADDED, + REMOVED + } + + @Override + public void onTransactionBundleAdded(final TransactionBundle transactionBundle) { + events.put(transactionBundle, EventType.ADDED); + } + + @Override + public void onTransactionBundleRemoved(final TransactionBundle transactionBundle) { + events.put(transactionBundle, EventType.REMOVED); + } + + public void assertAddNotificationReceived(final TransactionBundle bundle) { + await().until(() -> events.get(bundle) == EventType.ADDED); + } + + public void assertRemoveNotificationReceived(final TransactionBundle bundle) { + await().until(() -> events.get(bundle) == EventType.REMOVED); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/TransactionBundleTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/TransactionBundleTest.java new file mode 100644 index 00000000..585d6102 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/bundles/TransactionBundleTest.java @@ -0,0 +1,85 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.bundles; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.junit.jupiter.api.Test; + +class TransactionBundleTest extends AbstractBundleTest { + + @Test + void serializeToJson() throws JsonProcessingException { + + Hash hash1 = Hash.fromHexStringLenient("0x1234"); + TransactionBundle bundle1 = createBundle(hash1, 1, List.of(TX1, TX2)); + + assertThat(OBJECT_MAPPER.writeValueAsString(bundle1)) + .isEqualTo( + """ + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":1,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}}"""); + } + + @Test + void deserializeFromJson() throws JsonProcessingException { + TransactionBundle bundle = + OBJECT_MAPPER.readValue( + """ + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"blockNumber":1,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}}""", + TransactionBundle.class); + + assertThat(bundle.blockNumber()).isEqualTo(1); + assertThat(bundle.bundleIdentifier()).isEqualTo(Hash.fromHexStringLenient("0x1234")); + assertThat(bundle.pendingTransactions()) + .map(PendingTransaction::getTransaction) + .map(Transaction::getHash) + .containsExactly(TX1.getHash(), TX2.getHash()); + } + + @Test + void deserializedMalformed() { + assertThatThrownBy( + () -> + OBJECT_MAPPER.readValue( + """ + {"0x0000000000000000000000000000000000000000000000000000000000001234":{"wrong":1,"txs":["+E+AghOIglIIgASAggqWoHNvbkX5jC5D+Q0GW88l7bP45W+b8oubebJsfXgE+lRzoAVzHPSnS/zQmUxq3Hg9UHQ3p51KWM6dyYuqKVM7HYz7","+E8BghOIglIIgASAggqVoGgwjcqbkx9qWzUse4MmYxq5fGYo617lp3j9YAj74GDhoFrjtX1uTIbDgflVrS1EPJv2jmbGV2NbxukBL0sNVpBf"]}}""", + TransactionBundle.class)) + .isInstanceOf(ValueInstantiationException.class) + .hasMessageContaining("because \"blockNumber\" is null"); + } + + @Test + void restoreFromSerializedParseError() { + assertThatThrownBy( + () -> + OBJECT_MAPPER.readValue( + """ + {{wrong=json}}""", + TransactionBundle.class)) + .isInstanceOf(JsonParseException.class) + .hasMessageStartingWith( + "Unexpected character ('{' (code 123)): was expecting double-quote to start field name"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java new file mode 100644 index 00000000..12124f17 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java @@ -0,0 +1,129 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import java.net.MalformedURLException; +import java.net.URI; + +import org.hyperledger.besu.services.PicoCLIOptionsImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +class LineaRejectedTxReportingCliOptionsTest { + + @Command + static final class MockLineaBesuCommand { + @Option(names = "--mock-option") + String mockOption; + } + + private MockLineaBesuCommand command; + private CommandLine commandLine; + private LineaRejectedTxReportingCliOptions txReportingCliOptions; + + @BeforeEach + public void setup() { + command = new MockLineaBesuCommand(); + commandLine = new CommandLine(command); + + // add mixin option before parseArgs is called + final PicoCLIOptionsImpl picoCliService = new PicoCLIOptionsImpl(commandLine); + txReportingCliOptions = LineaRejectedTxReportingCliOptions.create(); + picoCliService.addPicoCLIOptions("linea", txReportingCliOptions); + } + + @Test + void emptyLineaRejectedTxReportingCliOptions() { + commandLine.parseArgs("--mock-option", "mockValue"); + + assertThat(command.mockOption).isEqualTo("mockValue"); + assertThat(txReportingCliOptions.rejectedTxEndpoint).isNull(); + assertThat(txReportingCliOptions.lineaNodeType).isNull(); + } + + @ParameterizedTest + @EnumSource(LineaNodeType.class) + void lineaRejectedTxOptionBothOptionsRequired(final LineaNodeType lineaNodeType) + throws MalformedURLException { + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + "http://localhost:8080", + "--plugin-linea-node-type", + lineaNodeType.name()); + + // parse args would not throw an exception, toDomainObject will perform the validation + assertThat(txReportingCliOptions.rejectedTxEndpoint) + .isEqualTo(URI.create("http://localhost:8080").toURL()); + assertThat(txReportingCliOptions.lineaNodeType).isEqualTo(lineaNodeType); + assertThatNoException().isThrownBy(() -> txReportingCliOptions.toDomainObject()); + } + + @Test + void lineaRejectedTxReportingCliOptionsOnlyEndpointCauseException() { + commandLine.parseArgs("--plugin-linea-rejected-tx-endpoint", "http://localhost:8080"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> txReportingCliOptions.toDomainObject()) + .withMessageContaining( + "Error: Missing required argument(s): --plugin-linea-node-type="); + } + + @Test + void lineaRejectedTxReportingCliOptionsOnlyNodeTypeParsesWithoutProblem() { + commandLine.parseArgs("--plugin-linea-node-type", LineaNodeType.SEQUENCER.name()); + assertThatNoException().isThrownBy(() -> txReportingCliOptions.toDomainObject()); + } + + @Test + void lineaRejectedTxReportingInvalidNodeTypeCauseException() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy( + () -> + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + "http://localhost:8080", + "--plugin-linea-node-type", + "INVALID_NODE_TYPE")) + .withMessageContaining( + "Invalid value for option '--plugin-linea-node-type': expected one of [SEQUENCER, RPC, P2P] (case-sensitive) but was 'INVALID_NODE_TYPE'"); + } + + @ParameterizedTest + @ValueSource(strings = {"", "http://localhost:8080:8080", "invalid"}) + void lineaRejectedTxReportingCliOptionsInvalidEndpointCauseException(final String endpoint) { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy( + () -> + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + endpoint, + "--plugin-linea-node-type", + "SEQUENCER")) + .withMessageContaining( + "Invalid value for option '--plugin-linea-rejected-tx-endpoint': cannot convert '" + + endpoint + + "' to URL (java.net.MalformedURLException:"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java new file mode 100644 index 00000000..5601d35b --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java @@ -0,0 +1,118 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.jsonrpc; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@WireMockTest +@ExtendWith(MockitoExtension.class) +public class JsonRpcManagerStartTest { + @TempDir private Path tempDataDir; + private JsonRpcManager jsonRpcManager; + private final Bytes randomEncodedBytes = Bytes.random(32); + @Mock private Transaction transaction; + static final String PLUGIN_IDENTIFIER = "linea-start-test-plugin"; + + @BeforeEach + void init(final WireMockRuntimeInfo wmInfo) throws IOException { + // create temp directories + final Path jsonRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); + Files.createDirectories(jsonRpcDir); + + // mock stubbing + when(transaction.encoded()).thenReturn(randomEncodedBytes); + + // save rejected transaction in tempDataDir so that they are processed by the + // JsonRpcManager.start + for (int i = 0; i < 3; i++) { + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test" + i); + final Instant timestamp = Instant.now(); + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.toString(), + List.of()); + + JsonRpcManager.saveJsonToDir(jsonRpcCall, jsonRpcDir); + } + + final LineaRejectedTxReportingConfiguration config = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.SEQUENCER) + .build(); + jsonRpcManager = new JsonRpcManager(PLUGIN_IDENTIFIER, tempDataDir, config); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); + } + + @Test + void existingJsonRpcFilesAreProcessedOnStart() { + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + // method under test + jsonRpcManager.start(); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted(() -> verify(exactly(3), postRequestedFor(urlEqualTo("/")))); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java new file mode 100644 index 00000000..b73845aa --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -0,0 +1,297 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.jsonrpc; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.stubbing.Scenario; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@WireMockTest +@ExtendWith(MockitoExtension.class) +class JsonRpcManagerTest { + static final String PLUGIN_IDENTIFIER = "linea-json-test-plugin"; + @TempDir private Path tempDataDir; + private JsonRpcManager jsonRpcManager; + private final Bytes randomEncodedBytes = Bytes.random(32); + @Mock private Transaction transaction; + + @BeforeEach + void init(final WireMockRuntimeInfo wmInfo) throws MalformedURLException { + // mock stubbing + when(transaction.encoded()).thenReturn(randomEncodedBytes); + final LineaRejectedTxReportingConfiguration config = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.SEQUENCER) + .build(); + jsonRpcManager = new JsonRpcManager(PLUGIN_IDENTIFIER, tempDataDir, config); + jsonRpcManager.start(); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); + } + + @Test + void rejectedTxIsReported() { + // json-rpc stubbing + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // method under test + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); + + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); + } + + @Test + void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOException { + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}")) + .willSetStateTo("Second Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Second Call") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + + // Prepare test data + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); + + // Submit the call, the scheduler will retry the failed call + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(2), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); + + // Verify that the JSON file no longer exists in the directory (as the second call was + // successful) + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); + try (Stream files = Files.list(rejTxRpcDir)) { + long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); + assertThat(fileCount).isEqualTo(0); + } + } + + @Test + void serverRespondingWithErrorScenario() throws IOException { + // Stub for error response + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}"))); + + // Prepare test data + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); + + // Submit the call + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); + + // Verify that the JSON file still exists in the directory (as the call was unsuccessful) + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); + try (Stream files = Files.list(rejTxRpcDir)) { + long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); + assertThat(fileCount).as("JSON file should exist as server responded with error").isOne(); + } + } + + @Test + void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException, IOException { + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)) + .willSetStateTo("Second Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Second Call") + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}")) + .willSetStateTo("Third Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Third Call") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + + // Prepare test data + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); + + // Submit the call, the scheduler will retry the failed calls + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(6, SECONDS) + .untilAsserted( + () -> + verify( + exactly(3), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); + + // Verify that the JSON file no longer exists in the directory (as the second call was + // successful) + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); + try (Stream files = Files.list(rejTxRpcDir)) { + long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); + assertThat(fileCount).isEqualTo(0); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaCancelBundleTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaCancelBundleTest.java new file mode 100644 index 00000000..ef7f77b4 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaCancelBundleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.rpc.methods; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import net.consensys.linea.bundles.LineaLimitedBundlePool; +import org.hyperledger.besu.plugin.services.exception.PluginRpcEndpointException; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LineaCancelBundleTest { + + private LineaCancelBundle lineaCancelBundle; + private LineaLimitedBundlePool bundlePool; + private PluginRpcRequest request; + + @BeforeEach + void setup() { + bundlePool = mock(LineaLimitedBundlePool.class); + request = mock(PluginRpcRequest.class); + lineaCancelBundle = new LineaCancelBundle().init(bundlePool); + } + + @Test + void testExecute_ValidUUID_RemovesBundle() { + // Mock UUID input + UUID replacementUUID = UUID.randomUUID(); + when(request.getParams()).thenReturn(new Object[] {replacementUUID}); + when(bundlePool.remove(replacementUUID)).thenReturn(true); // Simulate successful removal + + // Execute method + boolean result = lineaCancelBundle.execute(request); + + // Verify behavior + assertTrue(result, "Bundle should be successfully removed"); + verify(bundlePool).remove(replacementUUID); // Ensure remove() was called + } + + @Test + void testExecute_InvalidParams_ThrowsException() { + // Mock invalid parameters (not a UUID) + when(request.getParams()).thenReturn(new Object[] {"invalid_uuid"}); + + Exception exception = + assertThrows( + PluginRpcEndpointException.class, + () -> { + lineaCancelBundle.execute(request); + }); + + assertTrue(exception.getMessage().contains("malformed linea_cancelBundle json param")); + } + + @Test + void testExecute_BundleNotFound_ReturnsFalse() { + // Mock a valid UUID but simulate that the bundle doesn't exist + UUID replacementUUID = UUID.randomUUID(); + when(request.getParams()).thenReturn(new Object[] {replacementUUID}); + when(bundlePool.remove(replacementUUID)).thenReturn(false); // Simulate bundle not found + + boolean result = lineaCancelBundle.execute(request); + + assertFalse(result, "Bundle should not be found in the pool"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java new file mode 100644 index 00000000..d05429cf --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/rpc/methods/LineaSendBundleTest.java @@ -0,0 +1,268 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.rpc.methods; + +import static java.util.Optional.empty; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hyperledger.besu.ethereum.core.PrivateTransactionDataFixture.SIGNATURE_ALGORITHM; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import net.consensys.linea.bundles.BundleParameter; +import net.consensys.linea.bundles.LineaLimitedBundlePool; +import net.consensys.linea.bundles.TransactionBundle; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.rpc.PluginRpcRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class LineaSendBundleTest { + private static final long CHAIN_HEAD_BLOCK_NUMBER = 100L; + @TempDir Path dataDir; + private LineaSendBundle lineaSendBundle; + private BesuEvents mockEvents; + private LineaLimitedBundlePool bundlePool; + private BlockchainService blockchainService; + + private Transaction mockTX1 = + new TransactionTestFixture() + .nonce(1) + .gasLimit(21000) + .createTransaction(SIGNATURE_ALGORITHM.get().generateKeyPair()); + + private Transaction mockTX2 = + new TransactionTestFixture() + .nonce(1) + .gasLimit(21000) + .createTransaction(SIGNATURE_ALGORITHM.get().generateKeyPair()); + + @BeforeEach + void setup() { + mockEvents = mock(BesuEvents.class); + blockchainService = mock(BlockchainService.class, RETURNS_DEEP_STUBS); + when(blockchainService.getChainHeadHeader().getNumber()).thenReturn(CHAIN_HEAD_BLOCK_NUMBER); + bundlePool = spy(new LineaLimitedBundlePool(dataDir, 4096L, mockEvents, blockchainService)); + lineaSendBundle = new LineaSendBundle(blockchainService).init(bundlePool); + } + + @Test + void testExecute_ValidBundle() { + List transactions = List.of(mockTX1.encoded().toHexString()); + var expectedTxBundleHash = Hash.hash(mockTX1.encoded()); + + Optional minTimestamp = Optional.of(1000L); + Optional maxTimestamp = Optional.of(System.currentTimeMillis() + 5000L); + + BundleParameter bundleParams = + new BundleParameter( + transactions, 123L, minTimestamp, maxTimestamp, empty(), empty(), empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + // Execute + LineaSendBundle.BundleResponse response = lineaSendBundle.execute(request); + + // Validate response + assertNotNull(response); + assertEquals(expectedTxBundleHash.toHexString(), response.bundleHash()); + } + + @Test + void testExecute_ValidBundle_withReplacement() { + List transactions = List.of(mockTX1.encoded().toHexString()); + UUID replId = UUID.randomUUID(); + var expectedUUIDBundleHash = LineaLimitedBundlePool.UUIDToHash(replId); + + Optional minTimestamp = Optional.of(1000L); + Optional maxTimestamp = Optional.of(System.currentTimeMillis() + 5000L); + + BundleParameter bundleParams = + new BundleParameter( + transactions, + 123L, + minTimestamp, + maxTimestamp, + empty(), + Optional.of(replId.toString()), + empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + // Execute + LineaSendBundle.BundleResponse response = lineaSendBundle.execute(request); + + // Validate response + assertNotNull(response); + assertEquals(expectedUUIDBundleHash.toHexString(), response.bundleHash()); + + // Replace bundle: + transactions = List.of(mockTX2.encoded().toHexString(), mockTX1.encoded().toHexString()); + bundleParams = + new BundleParameter( + transactions, + 12345L, + minTimestamp, + maxTimestamp, + empty(), + Optional.of(replId.toString()), + empty()); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + // re-execute + response = lineaSendBundle.execute(request); + + // Validate response + assertNotNull(response); + assertEquals(expectedUUIDBundleHash.toHexString(), response.bundleHash()); + + // assert the new block number: + assertTrue(bundlePool.get(expectedUUIDBundleHash).blockNumber().equals(12345L)); + List pts = + bundlePool.get(expectedUUIDBundleHash).pendingTransactions(); + // assert the new tx2 is present + assertTrue(pts.stream().map(pt -> pt.getTransaction()).anyMatch(t -> t.equals(mockTX2))); + } + + @Test + void testExecute_ExpiredBundle() { + List transactions = List.of(mockTX1.encoded().toHexString()); + Optional maxTimestamp = Optional.of(5000L); + BundleParameter bundleParams = + new BundleParameter(transactions, 123L, empty(), maxTimestamp, empty(), empty(), empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + assertThatThrownBy(() -> lineaSendBundle.execute(request)) + .isInstanceOf(RuntimeException.class) + .hasMessageMatching( + "bundle max timestamp [0-9]+ is in the past, current timestamp is [0-9]+"); + } + + @Test + void testExecute_BundleForBlockAlreadyInChain_ThrowsException() { + List transactions = List.of(mockTX1.encoded().toHexString()); + BundleParameter bundleParams = + new BundleParameter( + transactions, CHAIN_HEAD_BLOCK_NUMBER, empty(), empty(), empty(), empty(), empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + assertThatThrownBy(() -> lineaSendBundle.execute(request)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining( + "bundle block number 100 is not greater than current chain head block number 100"); + } + + @Test + void testExecute_InvalidRequest_ThrowsException() { + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {"invalid_param"}); + + Exception exception = + assertThrows( + RuntimeException.class, + () -> { + lineaSendBundle.execute(request); + }); + + assertTrue(exception.getMessage().contains("malformed linea_sendBundle json param")); + } + + @Test + void testExecute_DuplicateRequest_ThrowsException() { + List transactions = List.of(mockTX1.encoded().toHexString()); + BundleParameter bundleParams1 = + new BundleParameter( + transactions, CHAIN_HEAD_BLOCK_NUMBER + 1, empty(), empty(), empty(), empty(), empty()); + + PluginRpcRequest request1 = mock(PluginRpcRequest.class); + when(request1.getParams()).thenReturn(new Object[] {bundleParams1}); + + LineaSendBundle.BundleResponse response1 = lineaSendBundle.execute(request1); + + // first time we send the request it works + assertThat(response1.bundleHash()).isEqualTo(Hash.hash(mockTX1.encoded()).toHexString()); + + BundleParameter bundleParams2 = + new BundleParameter( + transactions, CHAIN_HEAD_BLOCK_NUMBER + 1, empty(), empty(), empty(), empty(), empty()); + + PluginRpcRequest request2 = mock(PluginRpcRequest.class); + when(request2.getParams()).thenReturn(new Object[] {bundleParams2}); + + // same request sent again return already seen + assertThatThrownBy(() -> lineaSendBundle.execute(request2)) + .isInstanceOf(RuntimeException.class) + .hasMessageMatching("request already seen PT[0-9]+\\.[0-9]+S ago"); + } + + @Test + void testExecute_EmptyTransactions_ThrowsException() { + BundleParameter bundleParams = + new BundleParameter(List.of(), 123L, empty(), empty(), empty(), empty(), empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + Exception exception = + assertThrows( + RuntimeException.class, + () -> { + lineaSendBundle.execute(request); + }); + + assertTrue(exception.getMessage().contains("Malformed bundle, no bundle transactions present")); + } + + @Test + void testExecute_FrozenPool_ThrowsException() { + List transactions = List.of(mockTX1.encoded().toHexString()); + BundleParameter bundleParams = + new BundleParameter(transactions, 123L, empty(), empty(), empty(), empty(), empty()); + + PluginRpcRequest request = mock(PluginRpcRequest.class); + when(request.getParams()).thenReturn(new Object[] {bundleParams}); + + // saving to disk freeze the pool + bundlePool.saveToDisk(); + + assertTrue(bundlePool.isFrozen()); + + assertThatThrownBy(() -> lineaSendBundle.execute(request)) + .isInstanceOf(RuntimeException.class) + .hasMessageMatching("Bundle pool is not accepting modifications"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidatorTest.java new file mode 100644 index 00000000..7387bcba --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/modulelimit/ModuleLineCountValidatorTest.java @@ -0,0 +1,78 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.modulelimit; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class ModuleLineCountValidatorTest { + + @Test + void successfulValidation() { + final var moduleLineCountValidator = + new ModuleLineCountValidator(Map.of("MOD1", 1, "MOD2", 2, "MOD3", 3)); + final var lineCountTx = Map.of("MOD1", 1, "MOD2", 1, "MOD3", 1); + + assertThat(moduleLineCountValidator.validate(lineCountTx)) + .isEqualTo(ModuleLimitsValidationResult.VALID); + } + + @Test + void failedValidationTransactionOverLimit() { + final var moduleLineCountValidator = + new ModuleLineCountValidator(Map.of("MOD1", 1, "MOD2", 2, "MOD3", 3)); + final var lineCountTx = Map.of("MOD1", 3, "MOD2", 2, "MOD3", 3); + + assertThat(moduleLineCountValidator.validate(lineCountTx)) + .isEqualTo(ModuleLimitsValidationResult.txModuleLineCountOverflow("MOD1", 3, 1, 3, 1)); + } + + @Test + void failedValidationBlockOverLimit() { + final var moduleLineCountValidator = + new ModuleLineCountValidator(Map.of("MOD1", 1, "MOD2", 2, "MOD3", 3)); + final var prevLineCountTx = Map.of("MOD1", 1, "MOD2", 1, "MOD3", 1); + + final var lineCountTx = Map.of("MOD1", 1, "MOD2", 3, "MOD3", 3); + + assertThat(moduleLineCountValidator.validate(lineCountTx, prevLineCountTx)) + .isEqualTo(ModuleLimitsValidationResult.blockModuleLineCountFull("MOD2", 2, 2, 3, 2)); + } + + @Test + void failedValidationModuleNotFound() { + final var moduleLineCountValidator = + new ModuleLineCountValidator(Map.of("MOD1", 1, "MOD2", 2, "MOD3", 3)); + + final var lineCountTx = Map.of("MOD4", 1, "MOD2", 1, "MOD3", 1); + + assertThat(moduleLineCountValidator.validate(lineCountTx, lineCountTx)) + .isEqualTo(ModuleLimitsValidationResult.moduleNotDefined("MOD4")); + } + + @Test + void failedValidationInvalidLineCount() { + final var moduleLineCountValidator = + new ModuleLineCountValidator(Map.of("MOD1", 1, "MOD2", 2, "MOD3", 3)); + + final var lineCountTx = Map.of("MOD1", 1, "MOD2", -2, "MOD3", 1); + + assertThat(moduleLineCountValidator.validate(lineCountTx, lineCountTx)) + .isEqualTo(ModuleLimitsValidationResult.invalidLineCount("MOD2", -2)); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java new file mode 100644 index 00000000..c2f25f68 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java @@ -0,0 +1,88 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +@RequiredArgsConstructor +public class AllowedAddressValidatorTest { + public static final Address DENIED = + Address.fromHexString("0x0000000000000000000000000000000000001000"); + public static final Address NOT_DENIED = + Address.fromHexString("0x0000000000000000000000000000000000001001"); + public static final Address PRECOMPILED = Address.precompiled(0xa); + private AllowedAddressValidator allowedAddressValidator; + + @BeforeEach + public void initialize() { + Set

denied = Set.of(DENIED); + allowedAddressValidator = new AllowedAddressValidator(denied); + } + + @Test + public void validatedIfNoneOnList() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.sender(NOT_DENIED).to(NOT_DENIED).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + allowedAddressValidator.validateTransaction(transaction, false, false), Optional.empty()); + } + + @Test + public void deniedIfFromAddressIsOnList() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.sender(DENIED).to(NOT_DENIED).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + allowedAddressValidator.validateTransaction(transaction, false, false).orElseThrow(), + "sender 0x0000000000000000000000000000000000001000 is blocked as appearing on the SDN or other legally prohibited list"); + } + + @Test + public void deniedIfToAddressIsOnList() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.sender(NOT_DENIED).to(DENIED).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + allowedAddressValidator.validateTransaction(transaction, false, false).orElseThrow(), + "recipient 0x0000000000000000000000000000000000001000 is blocked as appearing on the SDN or other legally prohibited list"); + } + + @Test + public void deniedIfToAddressIsPrecompiled() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.sender(NOT_DENIED).to(PRECOMPILED).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + allowedAddressValidator.validateTransaction(transaction, false, false).orElseThrow(), + "destination address is a precompile address and cannot receive transactions"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java new file mode 100644 index 00000000..fc775580 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTransactionPoolValidatorCliOptions; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Wei; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +@RequiredArgsConstructor +public class CalldataValidatorTest { + public static final int MAX_TX_CALLDATA_SIZE = 10_000; + private CalldataValidator calldataValidator; + + @BeforeEach + public void initialize() { + calldataValidator = + new CalldataValidator( + LineaTransactionPoolValidatorCliOptions.create().toDomainObject().toBuilder() + .maxTxCalldataSize(MAX_TX_CALLDATA_SIZE) + .build()); + } + + @Test + public void validatedWithValidCalldata() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.gasPrice(Wei.ZERO).payload(Bytes.random(MAX_TX_CALLDATA_SIZE)).build(); + Assertions.assertEquals( + calldataValidator.validateTransaction(transaction, false, false), Optional.empty()); + } + + @Test + public void rejectedWithTooBigCalldata() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.gasPrice(Wei.ZERO).payload(Bytes.random(MAX_TX_CALLDATA_SIZE + 1)).build(); + Assertions.assertEquals( + calldataValidator.validateTransaction(transaction, false, false).orElseThrow(), + "Calldata of transaction is greater than the allowed max of " + MAX_TX_CALLDATA_SIZE); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java new file mode 100644 index 00000000..219e62f4 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaTransactionPoolValidatorCliOptions; +import org.apache.tuweni.bytes.Bytes; +import org.hyperledger.besu.datatypes.Wei; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +@RequiredArgsConstructor +public class GasLimitValidatorTest { + public static final int MAX_TX_GAS_LIMIT = 9_000_000; + private GasLimitValidator gasLimitValidator; + + @BeforeEach + public void initialize() { + gasLimitValidator = + new GasLimitValidator( + LineaTransactionPoolValidatorCliOptions.create().toDomainObject().toBuilder() + .maxTxGasLimit(MAX_TX_GAS_LIMIT) + .build()); + } + + @Test + public void validatedWithValidGasLimit() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.gasLimit(MAX_TX_GAS_LIMIT).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + gasLimitValidator.validateTransaction(transaction, false, false), Optional.empty()); + } + + @Test + public void rejectedWithMaxGasLimitPlusOne() { + final org.hyperledger.besu.ethereum.core.Transaction.Builder builder = + org.hyperledger.besu.ethereum.core.Transaction.builder(); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + builder.gasLimit(MAX_TX_GAS_LIMIT + 1).gasPrice(Wei.ZERO).payload(Bytes.EMPTY).build(); + Assertions.assertEquals( + gasLimitValidator.validateTransaction(transaction, false, false).orElseThrow(), + "Gas limit of transaction is greater than the allowed max of " + MAX_TX_GAS_LIMIT); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java new file mode 100644 index 00000000..3178a5ec --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java @@ -0,0 +1,255 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.math.BigInteger; +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaProfitabilityCliOptions; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@Slf4j +@RequiredArgsConstructor +@ExtendWith(MockitoExtension.class) +public class ProfitabilityValidatorTest { + public static final Address SENDER = + Address.fromHexString("0x0000000000000000000000000000000000001000"); + public static final Address RECIPIENT = + Address.fromHexString("0x0000000000000000000000000000000000001001"); + private static Wei PROFITABLE_GAS_PRICE = Wei.of(11_000_000); + private static Wei UNPROFITABLE_GAS_PRICE = Wei.of(200_000); + private static final SECPSignature FAKE_SIGNATURE; + + static { + final X9ECParameters params = SECNamedCurves.getByName("secp256k1"); + final ECDomainParameters curve = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + FAKE_SIGNATURE = + SECPSignature.create( + new BigInteger( + "66397251408932042429874251838229702988618145381408295790259650671563847073199"), + new BigInteger( + "24729624138373455972486746091821238755870276413282629437244319694880507882088"), + (byte) 0, + curve.getN()); + } + + public static final double TX_POOL_MIN_MARGIN = 0.5; + private ProfitabilityValidator profitabilityValidatorAlways; + private ProfitabilityValidator profitabilityValidatorOnlyApi; + private ProfitabilityValidator profitabilityValidatorOnlyP2p; + private ProfitabilityValidator profitabilityValidatorNever; + + @Mock BesuConfiguration besuConfiguration; + @Mock BlockchainService blockchainService; + + @BeforeEach + public void initialize() { + final var profitabilityConfBuilder = + LineaProfitabilityCliOptions.create().toDomainObject().toBuilder() + .txPoolMinMargin(TX_POOL_MIN_MARGIN); + + profitabilityValidatorAlways = + new ProfitabilityValidator( + besuConfiguration, + blockchainService, + profitabilityConfBuilder + .txPoolCheckP2pEnabled(true) + .txPoolCheckApiEnabled(true) + .build()); + + profitabilityValidatorNever = + new ProfitabilityValidator( + besuConfiguration, + blockchainService, + profitabilityConfBuilder + .txPoolCheckP2pEnabled(false) + .txPoolCheckApiEnabled(false) + .build()); + + profitabilityValidatorOnlyApi = + new ProfitabilityValidator( + besuConfiguration, + blockchainService, + profitabilityConfBuilder + .txPoolCheckP2pEnabled(false) + .txPoolCheckApiEnabled(true) + .build()); + + profitabilityValidatorOnlyP2p = + new ProfitabilityValidator( + besuConfiguration, + blockchainService, + profitabilityConfBuilder + .txPoolCheckP2pEnabled(true) + .txPoolCheckApiEnabled(false) + .build()); + } + + @Test + public void acceptPriorityRemoteWhenBelowMinProfitability() { + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(PROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorAlways.validateTransaction(transaction, false, true)) + .isEmpty(); + } + + @Test + public void rejectRemoteWhenBelowMinProfitability() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorAlways.validateTransaction(transaction, false, false)) + .isPresent() + .contains("Gas price too low"); + } + + @Test + public void acceptRemoteWhenBelowMinProfitabilityIfCheckNeverEnabled() { + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorNever.validateTransaction(transaction, false, false)) + .isEmpty(); + } + + @Test + public void acceptLocalWhenBelowMinProfitabilityIfCheckNeverEnabled() { + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorNever.validateTransaction(transaction, true, false)).isEmpty(); + } + + @Test + public void acceptRemoteWhenBelowMinProfitabilityIfCheckDisabledForP2p() { + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorOnlyApi.validateTransaction(transaction, false, false)) + .isEmpty(); + } + + @Test + public void rejectRemoteWhenBelowMinProfitabilityIfCheckEnableForP2p() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorOnlyP2p.validateTransaction(transaction, false, false)) + .isPresent() + .contains("Gas price too low"); + } + + @Test + public void acceptLocalWhenBelowMinProfitabilityIfCheckDisabledForApi() { + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorOnlyP2p.validateTransaction(transaction, true, false)) + .isEmpty(); + } + + @Test + public void rejectLocalWhenBelowMinProfitabilityIfCheckEnableForApi() { + when(besuConfiguration.getMinGasPrice()).thenReturn(Wei.of(100_000_000)); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(UNPROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(profitabilityValidatorOnlyApi.validateTransaction(transaction, true, false)) + .isPresent() + .contains("Gas price too low"); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java new file mode 100644 index 00000000..d1af7304 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java @@ -0,0 +1,231 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txpoolvalidation.validators; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.sequencer.txselection.selectors.TraceLineLimitTransactionSelectorTest; +import org.apache.tuweni.bytes.Bytes; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@Slf4j +@RequiredArgsConstructor +@WireMockTest +@ExtendWith(MockitoExtension.class) +public class SimulationValidatorTest { + private static final String MODULE_LINE_LIMITS_RESOURCE_NAME = "/sequencer/line-limits.toml"; + public static final Address SENDER = + Address.fromHexString("0x0000000000000000000000000000000000001000"); + public static final Address RECIPIENT = + Address.fromHexString("0x0000000000000000000000000000000000001001"); + private static final Wei BASE_FEE = Wei.of(7); + private static final Wei PROFITABLE_GAS_PRICE = Wei.of(11000000); + private static final SECPSignature FAKE_SIGNATURE; + private static final Address BRIDGE_CONTRACT = + Address.fromHexString("0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec"); + private static final Bytes BRIDGE_LOG_TOPIC = + Bytes.fromHexString("e856c2b8bd4eb0027ce32eeaf595c21b0b6b4644b326e5b7bd80a1cf8db72e6c"); + + static { + final X9ECParameters params = SECNamedCurves.getByName("secp256k1"); + final ECDomainParameters curve = + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + FAKE_SIGNATURE = + SECPSignature.create( + new BigInteger( + "66397251408932042429874251838229702988618145381408295790259650671563847073199"), + new BigInteger( + "24729624138373455972486746091821238755870276413282629437244319694880507882088"), + (byte) 0, + curve.getN()); + } + + private Map lineCountLimits; + + @Mock BlockchainService blockchainService; + @Mock TransactionSimulationService transactionSimulationService; + private JsonRpcManager jsonRpcManager; + @TempDir private Path tempDataDir; + @TempDir static Path tempDir; + static Path lineLimitsConfPath; + + @BeforeAll + public static void beforeAll() throws IOException { + lineLimitsConfPath = tempDir.resolve("line-limits.toml"); + Files.copy( + TraceLineLimitTransactionSelectorTest.class.getResourceAsStream( + MODULE_LINE_LIMITS_RESOURCE_NAME), + lineLimitsConfPath); + } + + @BeforeEach + public void initialize(final WireMockRuntimeInfo wmInfo) throws MalformedURLException { + final var tracerConf = + LineaTracerConfiguration.builder() + .moduleLimitsFilePath(lineLimitsConfPath.toString()) + .build(); + lineCountLimits = new HashMap<>(ModuleLineCountValidator.createLimitModules(tracerConf)); + final var pendingBlockHeader = mock(BlockHeader.class); + when(pendingBlockHeader.getBaseFee()).thenReturn(Optional.of(BASE_FEE)); + when(pendingBlockHeader.getCoinbase()).thenReturn(Address.ZERO); + when(transactionSimulationService.simulatePendingBlockHeader()).thenReturn(pendingBlockHeader); + when(blockchainService.getChainId()).thenReturn(Optional.of(BigInteger.ONE)); + + final var rejectedTxReportingConf = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.P2P) + .build(); + jsonRpcManager = + new JsonRpcManager("simulation-test", tempDataDir, rejectedTxReportingConf).start(); + + // rejected tx json-rpc stubbing + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); + } + + private SimulationValidator createSimulationValidator( + final Map lineCountLimits, + final boolean enableForApi, + final boolean enableForP2p) { + return new SimulationValidator( + blockchainService, + transactionSimulationService, + LineaTransactionPoolValidatorConfiguration.builder() + .txPoolSimulationCheckApiEnabled(enableForApi) + .txPoolSimulationCheckP2pEnabled(enableForP2p) + .build(), + lineCountLimits, + LineaL1L2BridgeSharedConfiguration.builder() + .contract(BRIDGE_CONTRACT) + .topic(BRIDGE_LOG_TOPIC) + .build(), + Optional.of(jsonRpcManager)); + } + + @Test + public void successfulTransactionIsValid() { + final var simulationValidator = createSimulationValidator(lineCountLimits, true, false); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(PROFITABLE_GAS_PRICE) + .payload(Bytes.EMPTY) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + assertThat(simulationValidator.validateTransaction(transaction, true, false)).isEmpty(); + } + + @Test + public void moduleLineCountOverflowTransactionIsInvalidAndReported() { + lineCountLimits.put("EXT", 5); + final var simulationValidator = createSimulationValidator(lineCountLimits, true, false); + final org.hyperledger.besu.ethereum.core.Transaction transaction = + org.hyperledger.besu.ethereum.core.Transaction.builder() + .sender(SENDER) + .to(RECIPIENT) + .gasLimit(21000) + .gasPrice(PROFITABLE_GAS_PRICE) + .payload(Bytes.repeat((byte) 1, 1000)) + .value(Wei.ONE) + .signature(FAKE_SIGNATURE) + .build(); + final var expectedReasonMessage = + "Transaction 0xbf668c5dc926c008d5b34f347e1842b94911b46f4a36b668812f821e20303322 line count for module EXT=7 is above the limit 5"; + assertThat(simulationValidator.validateTransaction(transaction, true, false)) + .contains(expectedReasonMessage); + + // assert that wiremock received 1 post request for rejected tx. + // Use Awaitility to wait for the condition to be met + await() + .atMost(6, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + postRequestedFor(urlEqualTo("/")) + .withRequestBody( + matchingJsonPath( + "$.params.txRejectionStage", equalTo(LineaNodeType.P2P.name()))) + .withRequestBody( + matchingJsonPath( + "$.params.reasonMessage", equalTo(expectedReasonMessage))))); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java new file mode 100644 index 00000000..ab88deb1 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java @@ -0,0 +1,207 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import net.consensys.linea.bundles.BundlePoolService; +import net.consensys.linea.bundles.LineaLimitedBundlePool; +import net.consensys.linea.bundles.TransactionBundle; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import net.consensys.linea.sequencer.txselection.selectors.TraceLineLimitTransactionSelectorTest; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.BesuEvents; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +class LineaTransactionSelectorFactoryTest { + private static final String MODULE_LINE_LIMITS_RESOURCE_NAME = "/sequencer/line-limits.toml"; + + private static final Address BRIDGE_CONTRACT = + Address.fromHexString("0x508Ca82Df566dCD1B0DE8296e70a96332cD644ec"); + private static final Bytes BRIDGE_LOG_TOPIC = + Bytes.fromHexString("e856c2b8bd4eb0027ce32eeaf595c21b0b6b4644b326e5b7bd80a1cf8db72e6c"); + + private BlockchainService mockBlockchainService; + private LineaTransactionSelectorConfiguration mockTxSelectorConfiguration; + private LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private LineaProfitabilityConfiguration mockProfitabilityConfiguration; + private Map lineCountLimits; + private BesuEvents mockEvents; + private LineaLimitedBundlePool bundlePool; + private BundlePoolService mockBundlePool; + private LineaTracerConfiguration lineaTracerConfiguration; + private LineaTransactionSelectorFactory factory; + + @TempDir static Path tempDir; + @TempDir Path dataDir; + static Path lineLimitsConfPath; + + @BeforeAll + public static void beforeAll() throws IOException { + lineLimitsConfPath = tempDir.resolve("line-limits.toml"); + Files.copy( + TraceLineLimitTransactionSelectorTest.class.getResourceAsStream( + MODULE_LINE_LIMITS_RESOURCE_NAME), + lineLimitsConfPath); + } + + @BeforeEach + void setUp() { + lineaTracerConfiguration = + LineaTracerConfiguration.builder() + .moduleLimitsFilePath(lineLimitsConfPath.toString()) + .build(); + lineCountLimits = + new HashMap<>(ModuleLineCountValidator.createLimitModules(lineaTracerConfiguration)); + + mockBlockchainService = mock(BlockchainService.class); + when(mockBlockchainService.getChainId()).thenReturn(Optional.of(BigInteger.ONE)); + when(mockBlockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(Wei.of(7))); + mockTxSelectorConfiguration = mock(LineaTransactionSelectorConfiguration.class); + l1L2BridgeConfiguration = + new LineaL1L2BridgeSharedConfiguration(BRIDGE_CONTRACT, BRIDGE_LOG_TOPIC); + mockProfitabilityConfiguration = mock(LineaProfitabilityConfiguration.class); + mockEvents = mock(BesuEvents.class); + bundlePool = spy(new LineaLimitedBundlePool(dataDir, 4096, mockEvents, mockBlockchainService)); + + factory = + new LineaTransactionSelectorFactory( + mockBlockchainService, + mockTxSelectorConfiguration, + l1L2BridgeConfiguration, + mockProfitabilityConfiguration, + lineaTracerConfiguration, + lineCountLimits, + Optional.empty(), + Optional.empty(), + bundlePool); + factory.create(new SelectorsStateManager()); + } + + @Test + void testSelectPendingTransactions_WithBundles() { + var mockBts = mock(BlockTransactionSelectionService.class); + var mockPendingBlockHeader = mock(ProcessableBlockHeader.class); + when(mockPendingBlockHeader.getNumber()).thenReturn(1L); + + var mockHash = Hash.wrap(Bytes32.random()); + var mockBundle = createBundle(mockHash, 1L, Optional.empty()); + bundlePool.putOrReplace(mockHash, mockBundle); + + when(mockBts.evaluatePendingTransaction(any())).thenReturn(TransactionSelectionResult.SELECTED); + + factory.selectPendingTransactions(mockBts, mockPendingBlockHeader); + + verify(mockBts).commit(); + } + + @ParameterizedTest() + @ArgumentsSource(FailedTransactionSelectionResultProvider.class) + void testSelectPendingTransactions_WithFailedBundle(TransactionSelectionResult failStatus) { + var mockBts = mock(BlockTransactionSelectionService.class); + var mockPendingBlockHeader = mock(ProcessableBlockHeader.class); + when(mockPendingBlockHeader.getNumber()).thenReturn(1L); + + var mockHash = Hash.wrap(Bytes32.random()); + var mockBundle = createBundle(mockHash, 1L, Optional.empty()); + bundlePool.putOrReplace(mockHash, mockBundle); + + when(mockBts.evaluatePendingTransaction(any())).thenReturn(failStatus); + + factory.selectPendingTransactions(mockBts, mockPendingBlockHeader); + + verify(mockBts).rollback(); + } + + @Test + void testSelectPendingTransactions_WithoutBundles() { + var mockBts = mock(BlockTransactionSelectionService.class); + var mockPendingBlockHeader = mock(ProcessableBlockHeader.class); + when(mockPendingBlockHeader.getNumber()).thenReturn(1L); + + factory.selectPendingTransactions(mockBts, mockPendingBlockHeader); + + verifyNoInteractions(mockBts); + } + + private TransactionBundle createBundle( + Hash hash, long blockNumber, Optional optPendingTx) { + return new TransactionBundle( + hash, + List.of( + optPendingTx.isPresent() + ? optPendingTx.get() + : mock(Transaction.class, RETURNS_DEEP_STUBS)), + blockNumber, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + } + + static class FailedTransactionSelectionResultProvider implements ArgumentsProvider { + @Override + public Stream provideArguments( + org.junit.jupiter.api.extension.ExtensionContext context) { + return Stream.of( + Arguments.of(TransactionSelectionResult.BLOCK_FULL), + Arguments.of(TransactionSelectionResult.BLOBS_FULL), + Arguments.of(TransactionSelectionResult.BLOCK_SELECTION_TIMEOUT), + Arguments.of(TransactionSelectionResult.BLOCK_SELECTION_TIMEOUT_INVALID_TX), + Arguments.of(TransactionSelectionResult.TX_EVALUATION_TOO_LONG), + Arguments.of(TransactionSelectionResult.INVALID_TX_EVALUATION_TOO_LONG), + Arguments.of(TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD), + Arguments.of(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS), + Arguments.of(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS)); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelectorTest.java new file mode 100644 index 00000000..838cff0b --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/BundleConstraintTransactionSelectorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.sequencer.txselection.selectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import net.consensys.linea.bundles.TransactionBundle; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.plugin.data.BlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BundleConstraintTransactionSelectorTest { + + private BundleConstraintTransactionSelector selector; + + @BeforeEach + void setUp() { + selector = new BundleConstraintTransactionSelector(); + } + + @Test + void testEvaluateTransactionPreProcessing_Selected() { + var blockHeader = mockBlockHeader(1); + TransactionBundle bundle = createBundle(List.of(mock(Transaction.class)), 1, null, null); + var pendingTransaction = bundle.pendingTransactions().getFirst(); + var txContext = mockTransactionEvaluationContext(blockHeader, pendingTransaction); + + var result = selector.evaluateTransactionPreProcessing(txContext); + + assertEquals(TransactionSelectionResult.SELECTED, result); + } + + @Test + void testEvaluateTransactionPreProcessing_FailedCriteria() { + var blockHeader = mockBlockHeader(1); + TransactionBundle bundle = + createBundle( + List.of(mock(Transaction.class)), 1, Instant.now().getEpochSecond() + 10000000, null); + var pendingTransaction = bundle.pendingTransactions().getFirst(); + var txContext = mockTransactionEvaluationContext(blockHeader, pendingTransaction); + + var result = selector.evaluateTransactionPreProcessing(txContext); + + assertEquals(TransactionSelectionResult.invalid("Failed Bundled Transaction Criteria"), result); + } + + @Test + void testEvaluateTransactionPostProcessing_FailedNonRevertable() { + var blockHeader = mockBlockHeader(1); + TransactionBundle bundle = createBundle(List.of(mock(Transaction.class)), 1, null, null); + var pendingTransaction = bundle.pendingTransactions().getFirst(); + var txContext = mockTransactionEvaluationContext(blockHeader, pendingTransaction); + + var transactionProcessingResult = mock(TransactionProcessingResult.class); + when(transactionProcessingResult.isFailed()).thenReturn(true); + + var result = selector.evaluateTransactionPostProcessing(txContext, transactionProcessingResult); + + assertEquals( + TransactionSelectionResult.invalid("Failed non revertable transaction in bundle"), result); + } + + @Test + void testEvaluateTransactionPostProcessing_Selected() { + var blockHeader = mockBlockHeader(1); + var pendingTransaction = mock(TransactionBundle.PendingBundleTx.class); + var txContext = mockTransactionEvaluationContext(blockHeader, pendingTransaction); + + var transactionProcessingResult = mock(TransactionProcessingResult.class); + when(transactionProcessingResult.isFailed()).thenReturn(false); + + var result = selector.evaluateTransactionPostProcessing(txContext, transactionProcessingResult); + + assertEquals(TransactionSelectionResult.SELECTED, result); + } + + private TransactionBundle createBundle( + List txs, long blockNumber, Long minTimestamp, Long maxTimestamp) { + return new TransactionBundle( + Hash.fromHexStringLenient("0x1234"), + txs, + blockNumber, + Optional.ofNullable(minTimestamp), + Optional.ofNullable(maxTimestamp), + Optional.empty(), + Optional.empty()); + } + + private TransactionEvaluationContext mockTransactionEvaluationContext( + BlockHeader blockHeader, TransactionBundle.PendingBundleTx pendingTransaction) { + return new TestTransactionEvaluationContext(blockHeader, pendingTransaction, Wei.ONE, Wei.ONE); + } + + private BlockHeader mockBlockHeader(long blockNumber) { + var blockHeader = mock(BlockHeader.class); + when(blockHeader.getNumber()).thenReturn(blockNumber); + return blockHeader; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataSizeTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataSizeTransactionSelectorTest.java new file mode 100644 index 00000000..3db536f4 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockCallDataSizeTransactionSelectorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BLOCK_CALLDATA_OVERFLOW; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MaxBlockCallDataSizeTransactionSelectorTest { + private static final int BLOCK_CALL_DATA_MAX_SIZE = 100; + private static final int BLOCK_CALL_DATA_HALF_SIZE = 50; + private static final int TX_CALL_DATA_SIZE = BLOCK_CALL_DATA_HALF_SIZE + 1; + private static int seq = 1; + private PluginTransactionSelector transactionSelector; + private SelectorsStateManager selectorsStateManager; + + @BeforeEach + public void initialize() { + selectorsStateManager = new SelectorsStateManager(); + transactionSelector = + new MaxBlockCallDataTransactionSelector(selectorsStateManager, BLOCK_CALL_DATA_MAX_SIZE); + selectorsStateManager.blockSelectionStarted(); + } + + @Test + public void shouldSelectTransactionWhen_BlockCallDataSize_IsLessThan_MaxBlockCallDataSize() { + final var evaluationContext1 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext1, SELECTED); + + final var evaluationContext2 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE - 1); + verifyTransactionSelection(transactionSelector, evaluationContext2, SELECTED); + } + + @Test + public void shouldSelectTransactionWhen_BlockCallDataSize_IsEqualTo_MaxBlockCallDataSize() { + final var evaluationContext1 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext1, SELECTED); + + final var evaluationContext2 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext2, SELECTED); + } + + @Test + public void + shouldNotSelectTransactionWhen_BlockCallDataSize_IsGreaterThan_MaxBlockCallDataSize() { + final var evaluationContext1 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext1, SELECTED); + + final var evaluationContext2 = mockTransactionOfCallDataSize(BLOCK_CALL_DATA_HALF_SIZE + 1); + verifyTransactionSelection(transactionSelector, evaluationContext2, BLOCK_CALLDATA_OVERFLOW); + } + + @Test + public void shouldNotSelectAdditionalTransactionOnceBlockIsFull() { + final var evaluationContext1 = mockTransactionOfCallDataSize(TX_CALL_DATA_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext1, SELECTED); + + final var evaluationContext2 = mockTransactionOfCallDataSize(TX_CALL_DATA_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext2, BLOCK_CALLDATA_OVERFLOW); + + final var evaluationContext3 = mockTransactionOfCallDataSize(TX_CALL_DATA_SIZE); + verifyTransactionSelection(transactionSelector, evaluationContext3, BLOCK_CALLDATA_OVERFLOW); + } + + private void verifyTransactionSelection( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionSelectionResult expectedPreProcessedResult) { + final var preProcessedResult = selector.evaluateTransactionPreProcessing(evaluationContext); + assertThat(preProcessedResult).isEqualTo(expectedPreProcessedResult); + final var processingResult = mock(TransactionProcessingResult.class); + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + notifySelector(selector, evaluationContext, preProcessedResult, processingResult); + } + + private void notifySelector( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionSelectionResult selectionResult, + final TransactionProcessingResult processingResult) { + if (selectionResult.equals(SELECTED)) { + selector.onTransactionSelected(evaluationContext, processingResult); + } else { + selector.onTransactionNotSelected(evaluationContext, selectionResult); + } + } + + private TestTransactionEvaluationContext mockTransactionOfCallDataSize(final int size) { + PendingTransaction pendingTransaction = mock(PendingTransaction.class); + Transaction transaction = mock(Transaction.class); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + when(transaction.getPayload()).thenReturn(Bytes.repeat((byte) 1, size)); + when(transaction.getHash()).thenReturn(Hash.wrap(Bytes32.repeat((byte) seq++))); + return new TestTransactionEvaluationContext( + mock(ProcessableBlockHeader.class), pendingTransaction); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelectorTest.java new file mode 100644 index 00000000..ae1503e6 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBlockGasTransactionSelectorTest.java @@ -0,0 +1,150 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_USER_GAS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class MaxBlockGasTransactionSelectorTest { + private static final int MAX_GAS_PER_BLOCK = 1000; + private static final int MAX_GAS_PER_BLOCK_20_PERCENTAGE = 200; + private static final int MAX_GAS_PER_BLOCK_80_PERCENTAGE = 800; + private static int seq = 1; + private PluginTransactionSelector transactionSelector; + private SelectorsStateManager selectorsStateManager; + + @BeforeEach + public void initialize() { + selectorsStateManager = new SelectorsStateManager(); + transactionSelector = + new MaxBlockGasTransactionSelector(selectorsStateManager, MAX_GAS_PER_BLOCK); + selectorsStateManager.blockSelectionStarted(); + } + + @Test + public void shouldSelectWhen_GasUsedByTransaction_IsLessThan_MaxGasPerBlock() { + final var mockTransactionProcessingResult = + mockTransactionProcessingResult(MAX_GAS_PER_BLOCK - 1); + final var evaluationContext = mockEvaluationContext(); + verifyTransactionSelection( + transactionSelector, evaluationContext, mockTransactionProcessingResult, SELECTED); + } + + @Test + public void shouldSelectWhen_GasUsedByTransaction_IsEqual_MaxGasPerBlock() { + final var mockTransactionProcessingResult = mockTransactionProcessingResult(MAX_GAS_PER_BLOCK); + final var evaluationContext = mockEvaluationContext(); + verifyTransactionSelection( + transactionSelector, evaluationContext, mockTransactionProcessingResult, SELECTED); + } + + @Test + public void shouldNotSelectWhen_GasUsedByTransaction_IsGreaterThan_MaxGasPerBlock() { + final var mockTransactionProcessingResult = + mockTransactionProcessingResult(MAX_GAS_PER_BLOCK + 1); + final var evaluationContext = mockEvaluationContext(); + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mockTransactionProcessingResult, + TX_GAS_EXCEEDS_USER_MAX_BLOCK_GAS); + } + + @Test + public void shouldNotSelectWhen_CumulativeGasUsed_IsGreaterThan_MaxGasPerBlock() { + final var evaluationContext1 = mockEvaluationContext(); + + // block empty, transaction 80% max gas, should select + verifyTransactionSelection( + transactionSelector, + evaluationContext1, + mockTransactionProcessingResult(MAX_GAS_PER_BLOCK_80_PERCENTAGE), + SELECTED); + + final var evaluationContext2 = mockEvaluationContext(); + + // block 80% full, transaction 80% max gas, should not select + verifyTransactionSelection( + transactionSelector, + evaluationContext2, + mockTransactionProcessingResult(MAX_GAS_PER_BLOCK_80_PERCENTAGE), + TX_TOO_LARGE_FOR_REMAINING_USER_GAS); + + final var evaluationContext3 = mockEvaluationContext(); + + // block 80% full, transaction 20% max gas, should select + verifyTransactionSelection( + transactionSelector, + evaluationContext3, + mockTransactionProcessingResult(MAX_GAS_PER_BLOCK_20_PERCENTAGE), + SELECTED); + } + + private void verifyTransactionSelection( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult expectedSelectionResult) { + var selectionResult = + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + assertThat(selectionResult).isEqualTo(expectedSelectionResult); + notifySelector(selector, evaluationContext, processingResult, selectionResult); + } + + private TestTransactionEvaluationContext mockEvaluationContext() { + PendingTransaction pendingTransaction = mock(PendingTransaction.class); + Transaction transaction = mock(Transaction.class); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + when(transaction.getHash()).thenReturn(Hash.wrap(Bytes32.repeat((byte) seq++))); + return new TestTransactionEvaluationContext( + mock(ProcessableBlockHeader.class), pendingTransaction); + } + + private TransactionProcessingResult mockTransactionProcessingResult(long gasUsedByTransaction) { + TransactionProcessingResult mockTransactionProcessingResult = + mock(TransactionProcessingResult.class); + when(mockTransactionProcessingResult.getEstimateGasUsedByTransaction()) + .thenReturn(gasUsedByTransaction); + return mockTransactionProcessingResult; + } + + private void notifySelector( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult selectionResult) { + if (selectionResult.equals(SELECTED)) { + selector.onTransactionSelected(evaluationContext, processingResult); + } else { + selector.onTransactionNotSelected(evaluationContext, selectionResult); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleBlockGasTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleBlockGasTransactionSelectorTest.java new file mode 100644 index 00000000..b7b4dbbd --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/MaxBundleBlockGasTransactionSelectorTest.java @@ -0,0 +1,181 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS; +import static net.consensys.linea.sequencer.txselection.selectors.MaxBundleBlockGasTransactionSelectorTest.TestParams.concat; +import static net.consensys.linea.sequencer.txselection.selectors.MaxBundleBlockGasTransactionSelectorTest.TestParams.notSelected; +import static net.consensys.linea.sequencer.txselection.selectors.MaxBundleBlockGasTransactionSelectorTest.TestParams.selected; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import net.consensys.linea.bundles.TransactionBundle; +import net.consensys.linea.sequencer.txselection.selectors.MaxBundleGasPerBlockTransactionSelector.BundleGasTracker; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; + +public class MaxBundleBlockGasTransactionSelectorTest { + private static final int MAX_BUNDLE_GAS_PER_BLOCK = 1000; + private static int seq = 1; + private MaxBundleGasPerBlockTransactionSelector transactionSelector; + private SelectorsStateManager selectorsStateManager; + + @BeforeEach + public void initialize() { + selectorsStateManager = new SelectorsStateManager(); + transactionSelector = + new MaxBundleGasPerBlockTransactionSelector( + selectorsStateManager, MAX_BUNDLE_GAS_PER_BLOCK); + selectorsStateManager.blockSelectionStarted(); + } + + static final List> bundleGasLessThanEqual = + List.of( + selected(MAX_BUNDLE_GAS_PER_BLOCK - 1), + selected(MAX_BUNDLE_GAS_PER_BLOCK), + selected(MAX_BUNDLE_GAS_PER_BLOCK / 2, MAX_BUNDLE_GAS_PER_BLOCK / 2), + selected(MAX_BUNDLE_GAS_PER_BLOCK - 100, 100)); + + static final List> bundleGasGreaterThan = + List.of( + notSelected( + MAX_BUNDLE_GAS_PER_BLOCK + 1, + BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS, + MAX_BUNDLE_GAS_PER_BLOCK + 1), + concat( + selected(MAX_BUNDLE_GAS_PER_BLOCK / 2, MAX_BUNDLE_GAS_PER_BLOCK / 2), + notSelected( + MAX_BUNDLE_GAS_PER_BLOCK / 2, + BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS, + (MAX_BUNDLE_GAS_PER_BLOCK / 2) * 3)), + concat( + selected(MAX_BUNDLE_GAS_PER_BLOCK - 100), + notSelected( + 101, + BUNDLE_GAS_EXCEEDS_MAX_BUNDLE_BLOCK_GAS, + MAX_BUNDLE_GAS_PER_BLOCK - 100 + 101))); + + @ParameterizedTest + @FieldSource("bundleGasLessThanEqual") + @FieldSource("bundleGasGreaterThan") + public void shouldSelectWhen_GasUsedByBundle_IsLessThanEqual_MaxBundleGasPerBlock( + final List params) { + + final var mockTxs = params.stream().map(__ -> mockTransaction()).toList(); + final var bundle = + new TransactionBundle( + Hash.wrap(Bytes32.repeat((byte) seq++)), + mockTxs, + 1L, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + final var evaluationContexts = + bundle.pendingTransactions().stream() + .map(pt -> new TestTransactionEvaluationContext(mock(ProcessableBlockHeader.class), pt)) + .toList(); + + for (int i = 0; i < params.size(); i++) { + final var mockTransactionProcessingResult = + mockTransactionProcessingResult(params.get(i).gasUsedByTransaction); + verifyTransactionSelection( + transactionSelector, + evaluationContexts.get(i), + mockTransactionProcessingResult, + params.get(i).selectionResult); + assertThat( + (BundleGasTracker) selectorsStateManager.getSelectorWorkingState(transactionSelector)) + .isEqualTo( + new BundleGasTracker( + params.get(i).expectedBlockBundleGasUsed, params.get(i).expectedBundleGasUsed)); + } + } + + private void verifyTransactionSelection( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult expectedSelectionResult) { + var selectionResult = + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + assertThat(selectionResult).isEqualTo(expectedSelectionResult); + } + + private Transaction mockTransaction() { + Transaction transaction = mock(Transaction.class); + when(transaction.getHash()).thenReturn(Hash.wrap(Bytes32.repeat((byte) seq++))); + return transaction; + } + + private TransactionProcessingResult mockTransactionProcessingResult(long gasUsedByTransaction) { + TransactionProcessingResult mockTransactionProcessingResult = + mock(TransactionProcessingResult.class); + when(mockTransactionProcessingResult.getEstimateGasUsedByTransaction()) + .thenReturn(gasUsedByTransaction); + return mockTransactionProcessingResult; + } + + record TestParams( + long gasUsedByTransaction, + TransactionSelectionResult selectionResult, + long expectedBlockBundleGasUsed, + long expectedBundleGasUsed) { + + static List selected(final long... gasUsedByTransactions) { + final List params = new ArrayList<>(gasUsedByTransactions.length); + + long cumulativeBundleGasUsed = 0; + + for (final long gasUsedByTransaction : gasUsedByTransactions) { + cumulativeBundleGasUsed += gasUsedByTransaction; + params.add( + new TestParams( + gasUsedByTransaction, SELECTED, cumulativeBundleGasUsed, cumulativeBundleGasUsed)); + } + return params; + } + + static List notSelected( + final long gasUsedByTransaction, + final TransactionSelectionResult selectionResult, + final long expectedBundleGasUsed) { + return List.of( + new TestParams( + gasUsedByTransaction, selectionResult, expectedBundleGasUsed, expectedBundleGasUsed)); + } + + @SafeVarargs + static List concat(final List... lists) { + return Arrays.stream(lists).flatMap(List::stream).toList(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java new file mode 100644 index 00000000..ff3b08b9 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java @@ -0,0 +1,427 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE_RETRY_LIMIT; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE_UPFRONT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Optional; + +import net.consensys.linea.config.LineaProfitabilityCliOptions; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorCliOptions; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.bouncycastle.crypto.digests.KeccakDigest; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ProfitableTransactionSelectorTest { + private static final int FIXED_GAS_COST_WEI = 600_000; + private static final int VARIABLE_GAS_COST_WEI = 1_000_000; + private static final double MIN_MARGIN = 1.5; + private static final int UNPROFITABLE_CACHE_SIZE = 2; + private static final Wei BASE_FEE = Wei.of(7); + private static final int UNPROFITABLE_RETRY_LIMIT = 1; + private final LineaTransactionSelectorConfiguration txSelectorConf = + LineaTransactionSelectorCliOptions.create().toDomainObject().toBuilder() + .unprofitableCacheSize(UNPROFITABLE_CACHE_SIZE) + .unprofitableRetryLimit(UNPROFITABLE_RETRY_LIMIT) + .build(); + private final LineaProfitabilityConfiguration profitabilityConf = + LineaProfitabilityCliOptions.create().toDomainObject().toBuilder() + .minMargin(MIN_MARGIN) + .fixedCostWei(FIXED_GAS_COST_WEI) + .variableCostWei(VARIABLE_GAS_COST_WEI) + .build(); + private TestableProfitableTransactionSelector transactionSelector; + + @BeforeEach + public void initialize() { + transactionSelector = newSelectorForNewBlock(); + transactionSelector.reset(); + } + + private TestableProfitableTransactionSelector newSelectorForNewBlock() { + final var blockchainService = mock(BlockchainService.class); + when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(BASE_FEE)); + return new TestableProfitableTransactionSelector( + blockchainService, txSelectorConf, profitabilityConf, Optional.empty()); + } + + @Test + public void shouldSelectWhenProfitable() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000), + mockTransactionProcessingResult, + SELECTED, + SELECTED); + } + + @Test + public void shouldNotSelectWhenUnprofitableUpfront() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext(false, 10000, Wei.of(1_000_100), Wei.of(1_000_000), 21000), + mockTransactionProcessingResult, + TX_UNPROFITABLE_UPFRONT, + null); + } + + @Test + public void shouldNotSelectWhenUnprofitable() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext(false, 10000, Wei.of(1_000_100), Wei.of(1_000_000), 210000), + mockTransactionProcessingResult, + SELECTED, + TX_UNPROFITABLE); + } + + @Test + public void shouldSelectPrevUnprofitableAfterGasPriceBump() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext( + false, 1000, Wei.of(1_100_000_000).multiply(9), Wei.of(1_000_000_000), 210000), + mockTransactionProcessingResult, + SELECTED, + SELECTED); + } + + @Test + public void shouldSelectPriorityTxEvenWhenUnprofitable() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext(true, 1000, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000), + mockTransactionProcessingResult, + SELECTED, + SELECTED); + } + + @Test + public void shouldRetryUnprofitableTxWhenBelowLimit() { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + var mockEvaluationContext = + mockEvaluationContext(false, 10000, Wei.of(1_000_010), Wei.of(1_000_000), 210000); + // first try + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext, + mockTransactionProcessingResult, + SELECTED, + TX_UNPROFITABLE); + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + // simulate another block + newSelectorForNewBlock(); + // we should remember of the unprofitable tx + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + // second try because we are below the retry limit + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext, + mockTransactionProcessingResult, + SELECTED, + TX_UNPROFITABLE); + } + + @Test + public void shouldEvictWhenUnprofitableCacheIsFull() { + final TestTransactionEvaluationContext[] evaluationContexts = + new TestTransactionEvaluationContext[UNPROFITABLE_CACHE_SIZE + 1]; + for (int i = 0; i <= UNPROFITABLE_CACHE_SIZE; i++) { + var mockTransactionProcessingResult = mockTransactionProcessingResult(21000); + var mockEvaluationContext = + mockEvaluationContext(false, 10000, Wei.of(1_000_010), Wei.of(1_000_000), 210000); + evaluationContexts[i] = mockEvaluationContext; + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext, + mockTransactionProcessingResult, + SELECTED, + TX_UNPROFITABLE); + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + // only the last two txs must be in the unprofitable cache, since the first one was evicted + assertThat( + transactionSelector.isUnprofitableTxCached( + evaluationContexts[0].getPendingTransaction().getTransaction().getHash())) + .isFalse(); + assertThat( + transactionSelector.isUnprofitableTxCached( + evaluationContexts[1].getPendingTransaction().getTransaction().getHash())) + .isTrue(); + assertThat( + transactionSelector.isUnprofitableTxCached( + evaluationContexts[2].getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + + @Test + public void shouldNotRetryUnprofitableTxWhenRetryLimitReached() { + var minGasPriceBlock1 = Wei.of(1_000_000); + var mockTransactionProcessingResult1 = mockTransactionProcessingResult(21000); + var mockEvaluationContext1 = + mockEvaluationContext(false, 10000, Wei.of(1_000_010), minGasPriceBlock1, 210000); + // first try of first tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext1, + mockTransactionProcessingResult1, + SELECTED, + TX_UNPROFITABLE); + + var mockTransactionProcessingResult2 = mockTransactionProcessingResult(21000); + var mockEvaluationContext2 = + mockEvaluationContext(false, 10000, Wei.of(1_000_010), minGasPriceBlock1, 210000); + // first try of second tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext2, + mockTransactionProcessingResult2, + SELECTED, + TX_UNPROFITABLE); + + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext1.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext2.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + + // simulate another block + transactionSelector = newSelectorForNewBlock(); + // we need to decrease the min gas price in order to allow a retry + var minGasPriceBlock2 = minGasPriceBlock1.subtract(1); + + // we should remember of the unprofitable txs for the new block + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext1.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext2.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + + // second try of the first tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext1.setMinGasPrice(minGasPriceBlock2), + mockTransactionProcessingResult1, + SELECTED, + TX_UNPROFITABLE); + + // second try of the second tx is not retried since we reached the retry limit + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext2.setMinGasPrice(minGasPriceBlock2), + mockTransactionProcessingResult2, + TX_UNPROFITABLE_RETRY_LIMIT, + null); + } + + @Test + public void profitableAndUnprofitableTxsMix() { + var minGasPriceBlock1 = Wei.of(1_000_000); + var mockTransactionProcessingResult1 = mockTransactionProcessingResult(21000); + var mockEvaluationContext1 = + mockEvaluationContext(false, 10000, Wei.of(1_000_010), minGasPriceBlock1, 210000); + // first try of first tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext1, + mockTransactionProcessingResult1, + SELECTED, + TX_UNPROFITABLE); + + var mockTransactionProcessingResult2 = mockTransactionProcessingResult(21000); + var mockEvaluationContext2 = + mockEvaluationContext(false, 1000, Wei.of(1_000_010), minGasPriceBlock1, 210000); + // first try of second tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext2, + mockTransactionProcessingResult2, + SELECTED, + SELECTED); + + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext1.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext2.getPendingTransaction().getTransaction().getHash())) + .isFalse(); + + // simulate another block + transactionSelector = newSelectorForNewBlock(); + // we keep the min gas price the same + var minGasPriceBlock2 = minGasPriceBlock1; + + // we should remember of the unprofitable txs for the new block + assertThat( + transactionSelector.isUnprofitableTxCached( + mockEvaluationContext1.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + + // second try of the first tx + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext1.setMinGasPrice(minGasPriceBlock2), + mockTransactionProcessingResult1, + SELECTED, + TX_UNPROFITABLE); + + var mockTransactionProcessingResult3 = mockTransactionProcessingResult(21000); + var mockEvaluationContext3 = + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), minGasPriceBlock1, 21000); + + // new profitable tx is selected + verifyTransactionSelection( + transactionSelector, + mockEvaluationContext3, + mockTransactionProcessingResult3, + SELECTED, + SELECTED); + } + + private void verifyTransactionSelection( + final ProfitableTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult expectedPreProcessingResult, + final TransactionSelectionResult expectedPostProcessingResult) { + var preProcessingResult = selector.evaluateTransactionPreProcessing(evaluationContext); + assertThat(preProcessingResult).isEqualTo(expectedPreProcessingResult); + if (preProcessingResult.equals(SELECTED)) { + var postProcessingResult = + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + assertThat(postProcessingResult).isEqualTo(expectedPostProcessingResult); + notifySelector(selector, evaluationContext, processingResult, postProcessingResult); + } else { + notifySelector(selector, evaluationContext, processingResult, preProcessingResult); + } + } + + private TestTransactionEvaluationContext mockEvaluationContext( + final boolean hasPriority, + final int size, + final Wei effectiveGasPrice, + final Wei minGasPrice, + final long gasLimit) { + PendingTransaction pendingTransaction = mock(PendingTransaction.class); + Transaction transaction = mock(Transaction.class); + when(transaction.getHash()).thenReturn(Hash.wrap(Bytes32.random())); + when(transaction.getGasLimit()).thenReturn(gasLimit); + when(transaction.encoded()).thenReturn(Bytes.wrap(pseudoRandomBytes(size))); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + when(pendingTransaction.hasPriority()).thenReturn(hasPriority); + return new TestTransactionEvaluationContext( + mock(ProcessableBlockHeader.class), pendingTransaction, effectiveGasPrice, minGasPrice); + } + + private byte[] pseudoRandomBytes(int size) { + final int expectedCompressedSize = + (size - 58) / 5; // This emulates old behaviour of compression ratio and size adjustment + byte[] bytes = new byte[expectedCompressedSize]; + final KeccakDigest keccakDigest = new KeccakDigest(256); + + final byte[] out = new byte[32]; + int offset = 0; + int i = 0; + do { + keccakDigest.update(new byte[] {(byte) i++}, 0, 1); + keccakDigest.doFinal(out, 0); + System.arraycopy(out, 0, bytes, offset, Math.min(expectedCompressedSize - offset, 32)); + offset += 32; + } while (offset < expectedCompressedSize); + + return bytes; + } + + private TransactionProcessingResult mockTransactionProcessingResult(long gasUsedByTransaction) { + TransactionProcessingResult mockTransactionProcessingResult = + mock(TransactionProcessingResult.class); + when(mockTransactionProcessingResult.getEstimateGasUsedByTransaction()) + .thenReturn(gasUsedByTransaction); + return mockTransactionProcessingResult; + } + + private void notifySelector( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult selectionResult) { + if (selectionResult.equals(SELECTED)) { + selector.onTransactionSelected(evaluationContext, processingResult); + } else { + selector.onTransactionNotSelected(evaluationContext, selectionResult); + } + } + + private static class TestableProfitableTransactionSelector extends ProfitableTransactionSelector { + + TestableProfitableTransactionSelector( + final BlockchainService blockchainService, + final LineaTransactionSelectorConfiguration txSelectorConf, + final LineaProfitabilityConfiguration profitabilityConf, + final Optional maybeProfitabilityMetrics) { + super(blockchainService, txSelectorConf, profitabilityConf, maybeProfitabilityMetrics); + } + + boolean isUnprofitableTxCached(final Hash txHash) { + return unprofitableCache.contains(txHash); + } + + void reset() { + unprofitableCache.clear(); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java new file mode 100644 index 00000000..d3d978a3 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java @@ -0,0 +1,75 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import com.google.common.base.Stopwatch; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; + +public class TestTransactionEvaluationContext implements TransactionEvaluationContext { + private ProcessableBlockHeader processableBlockHeader; + private PendingTransaction pendingTransaction; + private Wei transactionGasPrice; + private Wei minGasPrice; + + public TestTransactionEvaluationContext( + final ProcessableBlockHeader processableBlockHeader, + final PendingTransaction pendingTransaction, + final Wei transactionGasPrice, + final Wei minGasPrice) { + this.processableBlockHeader = processableBlockHeader; + this.pendingTransaction = pendingTransaction; + this.transactionGasPrice = transactionGasPrice; + this.minGasPrice = minGasPrice; + } + + public TestTransactionEvaluationContext( + final ProcessableBlockHeader processableBlockHeader, + final PendingTransaction pendingTransaction) { + this(processableBlockHeader, pendingTransaction, Wei.ONE, Wei.ONE); + } + + @Override + public ProcessableBlockHeader getPendingBlockHeader() { + return processableBlockHeader; + } + + @Override + public PendingTransaction getPendingTransaction() { + return pendingTransaction; + } + + @Override + public Stopwatch getEvaluationTimer() { + return Stopwatch.createStarted(); + } + + @Override + public Wei getTransactionGasPrice() { + return transactionGasPrice; + } + + @Override + public Wei getMinGasPrice() { + return minGasPrice; + } + + public TestTransactionEvaluationContext setMinGasPrice(final Wei minGasPrice) { + this.minGasPrice = minGasPrice; + return this; + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelectorTest.java new file mode 100644 index 00000000..6258aed6 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TraceLineLimitTransactionSelectorTest.java @@ -0,0 +1,287 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.sequencer.txselection.selectors; + +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_MODULE_LINE_COUNT_OVERFLOW_CACHED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import net.consensys.linea.config.LineaTracerConfiguration; +import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class TraceLineLimitTransactionSelectorTest { + private static final int OVER_LINE_COUNT_LIMIT_CACHE_SIZE = 2; + private static final String MODULE_LINE_LIMITS_RESOURCE_NAME = "/sequencer/line-limits.toml"; + private Map lineCountLimits; + private LineaTracerConfiguration lineaTracerConfiguration; + private SelectorsStateManager selectorsStateManager; + + @TempDir static Path tempDir; + static Path lineLimitsConfPath; + + @BeforeAll + public static void beforeAll() throws IOException { + lineLimitsConfPath = tempDir.resolve("line-limits.toml"); + Files.copy( + TraceLineLimitTransactionSelectorTest.class.getResourceAsStream( + MODULE_LINE_LIMITS_RESOURCE_NAME), + lineLimitsConfPath); + } + + @BeforeEach + public void initialize() { + lineaTracerConfiguration = + LineaTracerConfiguration.builder() + .moduleLimitsFilePath(lineLimitsConfPath.toString()) + .build(); + lineCountLimits = + new HashMap<>(ModuleLineCountValidator.createLimitModules(lineaTracerConfiguration)); + } + + private TestableTraceLineLimitTransactionSelector newSelectorForNewBlock( + final Map lineCountLimits) { + selectorsStateManager = new SelectorsStateManager(); + final var selector = + new TestableTraceLineLimitTransactionSelector( + selectorsStateManager, + lineaTracerConfiguration, + lineCountLimits, + OVER_LINE_COUNT_LIMIT_CACHE_SIZE); + selectorsStateManager.blockSelectionStarted(); + return selector; + } + + @Test + public void shouldSelectWhenBelowLimits() { + final var transactionSelector = newSelectorForNewBlock(lineCountLimits); + transactionSelector.resetCache(); + + final var evaluationContext = + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000, 0); + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mock(TransactionProcessingResult.class), + SELECTED, + SELECTED); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isFalse(); + } + + @Test + public void shouldNotSelectWhenOverLimits() { + lineCountLimits.put("EXT", 5); + final var transactionSelector = newSelectorForNewBlock(lineCountLimits); + transactionSelector.resetCache(); + + final var evaluationContext = + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000, 0); + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mock(TransactionProcessingResult.class), + SELECTED, + TX_MODULE_LINE_COUNT_OVERFLOW); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + + @Test + public void shouldNotReprocessedWhenOverLimits() { + lineCountLimits.put("EXT", 5); + var transactionSelector = newSelectorForNewBlock(lineCountLimits); + transactionSelector.resetCache(); + + var evaluationContext = + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000, 0); + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mock(TransactionProcessingResult.class), + SELECTED, + TX_MODULE_LINE_COUNT_OVERFLOW); + + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + transactionSelector = newSelectorForNewBlock(lineCountLimits); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + // retrying the same tx should avoid reprocessing + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mock(TransactionProcessingResult.class), + TX_MODULE_LINE_COUNT_OVERFLOW_CACHED, + null); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + + @Test + public void shouldEvictWhenCacheIsFull() { + lineCountLimits.put("EXT", 5); + final var transactionSelector = newSelectorForNewBlock(lineCountLimits); + transactionSelector.resetCache(); + + final TestTransactionEvaluationContext[] evaluationContexts = + new TestTransactionEvaluationContext[OVER_LINE_COUNT_LIMIT_CACHE_SIZE + 1]; + for (int i = 0; i <= OVER_LINE_COUNT_LIMIT_CACHE_SIZE; i++) { + var evaluationContext = + mockEvaluationContext(false, 100, Wei.of(1_100_000_000), Wei.of(1_000_000_000), 21000, 0); + verifyTransactionSelection( + transactionSelector, + evaluationContext, + mock(TransactionProcessingResult.class), + SELECTED, + TX_MODULE_LINE_COUNT_OVERFLOW); + evaluationContexts[i] = evaluationContext; + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContext.getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + + // only the last two txs must be in the over limit cache, since the first one was evicted + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContexts[0].getPendingTransaction().getTransaction().getHash())) + .isFalse(); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContexts[1].getPendingTransaction().getTransaction().getHash())) + .isTrue(); + assertThat( + transactionSelector.isOverLineCountLimitTxCached( + evaluationContexts[2].getPendingTransaction().getTransaction().getHash())) + .isTrue(); + } + + private void verifyTransactionSelection( + final TestableTraceLineLimitTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult expectedPreProcessingResult, + final TransactionSelectionResult expectedPostProcessingResult) { + var preProcessingResult = selector.evaluateTransactionPreProcessing(evaluationContext); + assertThat(preProcessingResult).isEqualTo(expectedPreProcessingResult); + if (preProcessingResult.equals(SELECTED)) { + var postProcessingResult = + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); + assertThat(postProcessingResult).isEqualTo(expectedPostProcessingResult); + notifySelector(selector, evaluationContext, processingResult, postProcessingResult); + } else { + notifySelector(selector, evaluationContext, processingResult, preProcessingResult); + } + } + + private void notifySelector( + final PluginTransactionSelector selector, + final TestTransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult, + final TransactionSelectionResult selectionResult) { + if (selectionResult.equals(SELECTED)) { + selector.onTransactionSelected(evaluationContext, processingResult); + } else { + selector.onTransactionNotSelected(evaluationContext, selectionResult); + } + } + + private TestTransactionEvaluationContext mockEvaluationContext( + final boolean hasPriority, + final int size, + final Wei effectiveGasPrice, + final Wei minGasPrice, + final long gasLimit, + final int payloadSize) { + PendingTransaction pendingTransaction = mock(PendingTransaction.class); + Transaction transaction = mock(Transaction.class); + when(transaction.getHash()).thenReturn(Hash.wrap(Bytes32.random())); + when(transaction.getSize()).thenReturn(size); + when(transaction.getGasLimit()).thenReturn(gasLimit); + when(transaction.getPayload()).thenReturn(Bytes.repeat((byte) 1, payloadSize)); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + when(pendingTransaction.hasPriority()).thenReturn(hasPriority); + return new TestTransactionEvaluationContext( + mock(ProcessableBlockHeader.class), pendingTransaction, effectiveGasPrice, minGasPrice); + } + + private class TestableTraceLineLimitTransactionSelector + extends TraceLineLimitTransactionSelector { + TestableTraceLineLimitTransactionSelector( + final SelectorsStateManager selectorsStateManager, + final LineaTracerConfiguration lineaTracerConfiguration, + final Map moduleLimits, + final int overLimitCacheSize) { + super( + selectorsStateManager, + BigInteger.ONE, + moduleLimits, + LineaTransactionSelectorConfiguration.builder() + .overLinesLimitCacheSize(overLimitCacheSize) + .build(), + LineaL1L2BridgeSharedConfiguration.builder() + .contract(Address.fromHexString("0xDEADBEEF")) + .topic(Bytes.fromHexString("0x012345")) + .build(), + lineaTracerConfiguration); + } + + void resetCache() { + overLineCountLimitCache.clear(); + } + + boolean isOverLineCountLimitTxCached(final Hash txHash) { + return overLineCountLimitCache.contains(txHash); + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/utils/TestablePriorityThreadPoolExecutor.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/utils/TestablePriorityThreadPoolExecutor.java new file mode 100644 index 00000000..6f9fc10e --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/utils/TestablePriorityThreadPoolExecutor.java @@ -0,0 +1,184 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.utils; + +import static org.awaitility.Awaitility.await; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import lombok.RequiredArgsConstructor; +import org.hyperledger.besu.util.Subscribers; + +public class TestablePriorityThreadPoolExecutor extends PriorityThreadPoolExecutor { + public interface BeforeExecuteListener { + void onBeforeExecute(Thread t, Runnable r); + } + + public interface AfterExecuteListener { + void onAfterExecute(Runnable r, final Throwable t); + } + + private final Subscribers beforeExecuteListeners = Subscribers.create(); + private final Subscribers afterExecuteListeners = Subscribers.create(); + private final Subscribers internalBeforeExecuteListeners = + Subscribers.create(); + private final Subscribers internalAfterExecuteListeners = + Subscribers.create(); + + public TestablePriorityThreadPoolExecutor( + final int corePoolSize, + final int maximumPoolSize, + final long keepAliveTime, + final TimeUnit unit, + final ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, threadFactory); + } + + @Override + protected void beforeExecute(final Thread t, final Runnable r) { + super.beforeExecute(t, r); + if (isInternalTask(r)) { + internalBeforeExecuteListeners.forEach(l -> l.onBeforeExecute(t, r)); + } else { + beforeExecuteListeners.forEach(l -> l.onBeforeExecute(t, r)); + } + } + + @Override + protected void afterExecute(final Runnable r, final Throwable t) { + super.afterExecute(r, t); + if (isInternalTask(r)) { + internalAfterExecuteListeners.forEach(l -> l.onAfterExecute(r, t)); + } else { + afterExecuteListeners.forEach(l -> l.onAfterExecute(r, t)); + } + } + + public long addBeforeExecuteListener(final BeforeExecuteListener l) { + return beforeExecuteListeners.subscribe(l); + } + + public long addAfterExecuteListener(final AfterExecuteListener l) { + return afterExecuteListeners.subscribe(l); + } + + public void removeBeforeExecuteListeners(final long id) { + beforeExecuteListeners.unsubscribe(id); + } + + public void removeAfterExecuteListeners(final long id) { + afterExecuteListeners.unsubscribe(id); + } + + public void waitForQueueTaskCount(final int expectedCount, final boolean blocking) + throws ExecutionException, InterruptedException { + final var waitTask = new WaitForQueueTaskCount(expectedCount); + if (blocking) { + ensureInternalTaskIsSelectedForExecution(waitTask); + } else { + submit(waitTask); + } + } + + public Semaphore pauseExecution() throws InterruptedException { + final var resumeSemaphore = new Semaphore(0); + final var pauseTask = new PauseExecution(resumeSemaphore); + ensureInternalTaskIsSelectedForExecution(pauseTask); + return resumeSemaphore; + } + + public void executeSomething() { + submit(new FillerTask()); + } + + private void ensureInternalTaskIsSelectedForExecution(final InternalTask task) + throws InterruptedException { + final var returnSemaphore = new Semaphore(0); + final var lid = + internalBeforeExecuteListeners.subscribe( + (t, r) -> { + // wait that this pause task is being executed before returning + if (extractPriorityFuture(r).getSourceTask() == task) { + returnSemaphore.release(); + } + }); + submit(task); + returnSemaphore.acquire(); + internalBeforeExecuteListeners.unsubscribe(lid); + } + + @SuppressWarnings("unchecked") + private PriorityThreadPoolExecutor.PriorityFuture extractPriorityFuture(final Runnable r) { + return (PriorityThreadPoolExecutor.PriorityFuture) r; + } + + private boolean isInternalTask(final Runnable r) { + return extractPriorityFuture(r).getSourceTask() instanceof InternalTask; + } + + private interface InternalTask extends Callable, Comparable {} + + @RequiredArgsConstructor + private class WaitForQueueTaskCount implements InternalTask { + private final int expectedCount; + + @Override + public int compareTo(final Object o) { + // force the internal task to always have priority + return -1; + } + + @Override + public Void call() { + await().until(() -> getQueue().size() == expectedCount); + return null; + } + } + + @RequiredArgsConstructor + private class PauseExecution implements InternalTask { + private final Semaphore semaphore; + + @Override + public int compareTo(final Object o) { + // force the internal task to always have priority + return -1; + } + + @Override + public Void call() throws Exception { + semaphore.acquire(); + return null; + } + } + + private class FillerTask implements InternalTask { + + @Override + public int compareTo(final Object o) { + // execute after all other queued tasks + return 1; + } + + @Override + public Void call() { + return null; + } + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/resources/sequencer/line-limits.toml b/besu-plugins/linea-sequencer/sequencer/src/test/resources/sequencer/line-limits.toml new file mode 100644 index 00000000..26f4a9b7 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/resources/sequencer/line-limits.toml @@ -0,0 +1,86 @@ +## +# This file specifies prover limit by each EVM module +# WARN: The prover/arithmetization team has the owneship of this. +# Changing this values may compromise the system. +# issue: https://github.com/ConsenSys/zkevm-monorepo/issues/525 +## + +[traces-limits] +# +# Arithmetization module limits +# +ADD = 524288 +BIN = 262144 +BLAKE_MODEXP_DATA = 16384 +BLOCK_DATA = 4096 +BLOCK_HASH = 2048 +EC_DATA = 262144 +EUC = 65536 +EXP = 8192 +EXT = 65536 +GAS = 65536 +HUB = 2097152 +LOG_DATA = 65536 +LOG_INFO = 4096 +MMIO = 4194304 +MMU = 4194304 +MOD = 131072 +MUL = 65536 +MXP = 524288 +OOB = 262144 +RLP_ADDR = 4096 +RLP_TXN = 131072 +RLP_TXN_RCPT = 65536 +ROM = 4194304 +ROM_LEX = 1024 +SHAKIRA_DATA = 32768 +SHF = 65536 +STP = 16384 +TRM = 32768 +TXN_DATA = 8192 +WCP = 262144 + +# NOTE: in the original file the limits were just shy of powers of 2, e.g. ADD = 524286 +# Question: this seemed deliberate; it could be related to spillings; if so we may want +# to reduce all limits above by the corresponding spillings value (or 16 for simplicity) + +# +# Fixed size, static tables +# +BIN_REFERENCE_TABLE = 262144 # contains 3 * 256^2 + 256 data rows + 1 padding row +SHF_REFERENCE_TABLE = 4096 # contains 9 * 256 data rows + 1 padding row +INSTRUCTION_DECODER = 512 # contains 256 data rows + 1 padding row + +# +# Block-specific limits +# +BLOCK_KECCAK = 8192 +BLOCK_L1_SIZE = 1000000 +BLOCK_L2_L1_LOGS = 16 +BLOCK_TRANSACTIONS = 200 # max number of tx in an L2 block + +# +# Precompiles limits +# compare with https://github.com/Consensys/linea-arithmetization/issues/257 +# + + + +PRECOMPILE_ECRECOVER_EFFECTIVE_CALLS = 128 +PRECOMPILE_SHA2_BLOCKS = 671 +PRECOMPILE_RIPEMD_BLOCKS = 671 +PRECOMPILE_MODEXP_EFFECTIVE_CALLS = 4 +PRECOMPILE_ECADD_EFFECTIVE_CALLS = 16384 +PRECOMPILE_ECMUL_EFFECTIVE_CALLS = 32 +PRECOMPILE_ECPAIRING_FINAL_EXPONENTIATIONS = 16 +PRECOMPILE_ECPAIRING_G2_MEMBERSHIP_CALLS = 64 +PRECOMPILE_ECPAIRING_MILLER_LOOPS = 64 +PRECOMPILE_BLAKE_EFFECTIVE_CALLS = 0 # there are no gnarks circuit atm +PRECOMPILE_BLAKE_ROUNDS = 0 # it is possible to call BLAKE2f with r = 0; this is a nontrivial operation ... +# # Notes: +# - there are no IDENTITY related limits +# - we used to have the following limits +# * PRECOMPILE_SHA2_EFFECTIVE_CALLS = 1000000 +# * PRECOMPILE_RIPEMD_EFFECTIVE_CALLS = 1000000 +# (the values are nonsensical); +# as per Alex they are not required by the prover; diff --git a/build.gradle b/build.gradle index 0922f4ee..2d57354c 100644 --- a/build.gradle +++ b/build.gradle @@ -113,11 +113,25 @@ allprojects { } // spotless check applied to build.gradle (groovy) files groovyGradle { + target '*.gradle' greclipse() indentWithSpaces(2) endWithNewline() } } + + // TODO in later ticket - apply these linting rules to all monorepo projects + if (subproject.path.startsWith(':besu-plugins:linea-sequencer')) { + subproject.spotless { + java { + target 'src/**/*.java' + targetExclude '**/src/test/java/**ReferenceTest**', '**/src/main/generated/**', '**/src/test/generated/**', '**/src/jmh/generated/**' + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } + } + } } } } diff --git a/gradle.properties b/gradle.properties index 00fca394..049a0b3a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,9 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 group=build.linea +besuArtifactGroup=org.hyperledger.besu +lineaSequencerProjectPath=:besu-plugins:linea-sequencer + # Version of the project, at release override this value with # /gradlew jvm-libs:blob-compressor:publish -Pversion=0.0.3 # version=0.0.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f533ea79..8143ce4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,14 @@ [plugins] docker = { id = "com.avast.gradle.docker-compose", version = "0.17.7" } web3j = { id = "org.web3j", version.ref = "web3j" } -spotless = { id = "com.diffplug.spotless", version = "6.16.0" } +web3jSolidity = { id = "org.web3j.solidity", version = "0.5.0" } +spotless = { id = "com.diffplug.spotless", version = "6.16.0" } # TODO in later ticket - Update to >=6.25.0, which breaks the CI jreleaser = {id = "org.jreleaser", version = "1.15.0"} +dependencyLicenseReport = { id = "com.github.jk1.dependency-license-report", version = "2.9" } +lombok = { id = "io.freefair.lombok", version = "8.6" } +gradleVersions = { id = "com.github.ben-manes.versions", version = "0.51.0" } +dependencyManagement = { id = "io.spring.dependency-management", version = "1.1.5" } +download = { id = "de.undercouch.download", version = "5.6.0" } [libraries] jreleaser = { group = "org.jreleaser", name = "jreleaser-gradle-plugin", version = "1.15.0" } @@ -20,18 +26,21 @@ wiremock = "3.13.0" logcaptor = "2.11.0" # Runtime -besu = "25.2.1-linea1" -blobCompressor = "1.2.1" +arithmetization="beta-v2.1-rc16.2" +besu = "25.5.0-linea3" +blobCompressor = "1.2.2" blobShnarfCalculator = "1.2.0" bouncycastle = "1.79" caffeine = "3.1.6" +gson = "2.11.0" hoplite = "2.9.0" jackson = "2.19.0" jna = "5.14.0" kotlinResult = "1.1.16" kotlinxDatetime = "0.6.2" ktlint = "0.47.0" -log4j = "2.20.0" +lineaKotlin = "0.1.0" +log4j = "2.24.1" micrometer = "1.8.4" netty = "4.1.92.Final" picoli = "4.7.6" @@ -40,3 +49,6 @@ teku = "23.1.1" tuweni = "2.4.2" vertx = "4.5.14" web3j = "4.12.2" + +# Other +dependencyLicenseReport = "2.9" \ No newline at end of file diff --git a/jvm-libs/linea/besu-libs/build.gradle b/jvm-libs/linea/besu-libs/build.gradle index 6ff212eb..c495d15e 100644 --- a/jvm-libs/linea/besu-libs/build.gradle +++ b/jvm-libs/linea/besu-libs/build.gradle @@ -3,7 +3,6 @@ plugins { id 'java-library' } -def besuArtifactGroup="org.hyperledger.besu" def besuVersion="${libs.versions.besu.get()}" dependencies { diff --git a/settings.gradle b/settings.gradle index 74a41421..acce9006 100644 --- a/settings.gradle +++ b/settings.gradle @@ -62,6 +62,9 @@ include 'transaction-exclusion-api:app' include 'transaction-exclusion-api:core' include 'transaction-exclusion-api:persistence:rejectedtransaction' include 'besu-plugins:finalized-tag-updater' +include 'besu-plugins:linea-sequencer' +include 'besu-plugins:linea-sequencer:acceptance-tests' +include 'besu-plugins:linea-sequencer:sequencer' include 'besu-plugins:state-recovery:appcore:clients-interfaces' include 'besu-plugins:state-recovery:appcore:domain-models' include 'besu-plugins:state-recovery:appcore:logic'