mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-08 03:43:56 -05:00
3822 rejected transaction api service (#101)
* feat: first commit for transaction exclusion api service * feat: removed debug logs and improved params error handling * fix: jacocoRootReport error * feat: improved json request param parsings * feat: added docker container and github workflow pipeline for transaction exclusion api * feat: added rejection stage in response and use txHash and rejectReason as primary key of tables * feat: separate database into read and write config and each with dedicated connection * fix: e2e testing error * feat: removed redundant commands in Makefile * feat: updated transaction exclusion api default image tag in compose file * feat: added metric and change param name from reasonMessage to reason * feat: added integration and unit tests and use reasonMessage for both request and response * fix: transaction-exclusion-api unit test * feat: added e2e tests and remove reasonMessage from get response and redundant codes * feat: updated README.md and removed abi file * feat: updated image version of transaction exclusion api service in compose file * feat: updated README and added more test cases * feat: updated transaction exclusion api default image tag in compose file * feat: decoupled transaction exclusion api from coordinator package * feat: removed unnecessary dependencies to prover client * feat: moved persistence:db package to jvm-libs * feat: removed migration file dir location config from transaction exclusion api * fix: db migration location for fee history integration test * changed db column name timestamp to reject_timestamp and add dto for ModuleOverflow to remove all jackson dependencies in core module * feat: rejected transaction dao and config refactoring * feat: removed repository service and using persistence retryer * feat: updated transaction exclusion api default image tag in compose file * feat: updated log and increase retry backoff delay to avoid repetitive error logs * feat: added support of list request on save method and added dto for RejectedTransaction * feat: revised gradle.build dependencies * feat: switch from shadow jar to zipped jar * feat: updated transaction exclusion api default image tag in compose file * feat: updated sql and tables and changes for PR comments * feat: improved log message for duplicate key error * feat: updated transaction exclusion api default image tag in compose file * feat: avoid redundant logs on periodic db cleanup * feat: revised request handlers plus better test assertions on insertion * fix: test case * feat: parse save method json request with jackson * feat: extracted db migrations from the coordinator and transaction-exclusion app * feat: decoupled coordinator modules from jvm-libs persistence db test module * feat: updated dockerfile of transaction-exclusion-api * feat: removed the find check before metric increment on save rejected transaction * feat: updated docker base image for tx-exclusion-api image buid and queryable window config * feat: skip migration scripts on read db instance * feat: updated more percise jvm-libs change filtering on transaction-exclusion-api * feat: updated coordinator config for geth node l2 gas pricing recipients * feat: update runners with specific version and removed the use of retry for transaction exclusion api testing * feat: add integration test for transaction exclusion app * feat: update local stack docker compose and workflow for transaction exclusion * feat: add e2e test for transaction exclusion * feat: skip the sequencer test in transaction exclusion e2e test * feat: revert sequencer config poa-block-txs-selection-max-time * feat: remove incorrect comment * feat: added explicitly assertion if tx exclusion is not defined and simplify the localStackPostgresDbOnly in build.gradle * feat: remove beforeAll in test suite with it.concurrent * feat: set coordinator config blob-compressor-version as V1_0_1 explicitly for traces-v2 * feat: update coordinator config test * feat: change default prefix not to be coordinator specific * feat: place persistence:db under jvm-libs:generic and fixed conflicts from latest main * fix: remove dependency to resolve circular dependency issue * test: switch from localStackPostgresDbOnlyComposeUp to localStackComposeUp * feat: replace GITHUB_SHA with github.event.pull_request.head.sha in computing commit tag * feat: update filter change file lists for transaction exclusion api
This commit is contained in:
18
.github/workflows/build-and-publish.yml
vendored
18
.github/workflows/build-and-publish.yml
vendored
@@ -30,6 +30,9 @@ on:
|
||||
traces_api_facade_changed:
|
||||
required: true
|
||||
type: string
|
||||
transaction_exclusion_api_changed:
|
||||
required: true
|
||||
type: string
|
||||
coordinator_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
@@ -42,6 +45,9 @@ on:
|
||||
traces_api_facade_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
transaction_exclusion_api_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
@@ -96,3 +102,15 @@ jobs:
|
||||
untested_tag_suffix: ${{ inputs.untested_tag_suffix }}
|
||||
image_name: consensys/linea-traces-api-facade
|
||||
secrets: inherit
|
||||
|
||||
transaction_exclusion_api:
|
||||
uses: ./.github/workflows/transaction-exclusion-api-build-and-publish.yml
|
||||
if: ${{ always() && (inputs.transaction_exclusion_api_changed == 'true' || inputs.transaction_exclusion_api_image_tagged != 'true') }}
|
||||
with:
|
||||
commit_tag: ${{ inputs.commit_tag }}
|
||||
last_commit_tag: ${{ inputs.last_commit_tag }}
|
||||
common_ancestor_tag: ${{ inputs.common_ancestor_tag }}
|
||||
develop_tag: ${{ inputs.develop_tag }}
|
||||
untested_tag_suffix: ${{ inputs.untested_tag_suffix }}
|
||||
image_name: consensys/linea-transaction-exclusion-api
|
||||
secrets: inherit
|
||||
|
||||
29
.github/workflows/main.yml
vendored
29
.github/workflows/main.yml
vendored
@@ -18,8 +18,9 @@ jobs:
|
||||
postman: ${{ steps.filter.outputs.postman }}
|
||||
prover: ${{ steps.filter.outputs.prover }}
|
||||
traces-api-facade: ${{ steps.filter.outputs.traces-api-facade }}
|
||||
transaction-exclusion-api: ${{ steps.filter.outputs.transaction-exclusion-api }}
|
||||
finalized-tag-updater: ${{ steps.filter.outputs.finalized-tag-updater }}
|
||||
no-changes: ${{ steps.filter.outputs.coordinator == 'false' && steps.filter.outputs.postman == 'false' && steps.filter.outputs.prover == 'false' && steps.filter.outputs.traces-api-facade == 'false' }}
|
||||
no-changes: ${{ steps.filter.outputs.coordinator == 'false' && steps.filter.outputs.postman == 'false' && steps.filter.outputs.prover == 'false' && steps.filter.outputs.traces-api-facade == 'false' && steps.filter.outputs.transaction-exclusion-api == 'false' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -83,6 +84,25 @@ jobs:
|
||||
- 'build.gradle'
|
||||
- 'gradle.properties'
|
||||
- 'settings.gradle'
|
||||
transaction-exclusion-api:
|
||||
- 'transaction-exclusion-api/**'
|
||||
- 'jvm-libs/generic/extensions/futures/**'
|
||||
- 'jvm-libs/generic/extensions/kotlin/**'
|
||||
- 'jvm-libs/generic/json-rpc/**'
|
||||
- 'jvm-libs/generic/persistence/**'
|
||||
- 'jvm-libs/generic/vertx-helper/**'
|
||||
- 'jvm-libs/linea/core/long-running-service/**'
|
||||
- 'jvm-libs/linea/core/metrics/**'
|
||||
- 'jvm-libs/linea/metrics/**'
|
||||
- '.github/workflows/transaction-exclusion-api-*.yml'
|
||||
- '.github/workflows/build-and-publish.yml'
|
||||
- '.github/workflows/main.yml'
|
||||
- '.github/workflows/reuse-*.yml'
|
||||
- 'buildSrc/**'
|
||||
- 'gradle/**'
|
||||
- 'build.gradle'
|
||||
- 'gradle.properties'
|
||||
- 'settings.gradle'
|
||||
finalized-tag-updater:
|
||||
- 'jvm-libs/linea/core/long-running-service/**'
|
||||
- 'jvm-libs/linea/web3j-extensions/**'
|
||||
@@ -105,6 +125,7 @@ jobs:
|
||||
postman_changed: ${{ needs.filter-commit-changes.outputs.postman }}
|
||||
prover_changed: ${{ needs.filter-commit-changes.outputs.prover }}
|
||||
traces_api_facade_changed: ${{ needs.filter-commit-changes.outputs.traces-api-facade }}
|
||||
transaction_exclusion_api_changed: ${{ needs.filter-commit-changes.outputs.transaction-exclusion-api }}
|
||||
secrets: inherit
|
||||
|
||||
manual-docker-build-and-e2e-tests:
|
||||
@@ -129,10 +150,12 @@ jobs:
|
||||
postman_changed: ${{ needs.filter-commit-changes.outputs.postman }}
|
||||
prover_changed: ${{ needs.filter-commit-changes.outputs.prover }}
|
||||
traces_api_facade_changed: ${{ needs.filter-commit-changes.outputs.traces-api-facade }}
|
||||
transaction_exclusion_api_changed: ${{ needs.filter-commit-changes.outputs.transaction-exclusion-api }}
|
||||
coordinator_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_coordinator }}
|
||||
postman_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_postman }}
|
||||
prover_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_prover }}
|
||||
traces_api_facade_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_traces_api_facade }}
|
||||
transaction_exclusion_api_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_transaction_exclusion_api }}
|
||||
secrets: inherit
|
||||
|
||||
# Comment out the auto build and release step below as the plugin release should be
|
||||
@@ -154,9 +177,11 @@ jobs:
|
||||
postman_changed: ${{ needs.filter-commit-changes.outputs.postman }}
|
||||
prover_changed: ${{ needs.filter-commit-changes.outputs.prover }}
|
||||
traces_api_facade_changed: ${{ needs.filter-commit-changes.outputs.traces-api-facade }}
|
||||
transaction_exclusion_api_changed: ${{ needs.filter-commit-changes.outputs.transaction-exclusion-api }}
|
||||
coordinator_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_coordinator }}
|
||||
postman_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_postman }}
|
||||
traces_api_facade_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_traces_api_facade }}
|
||||
transaction_exclusion_api_image_tagged: ${{ needs.check-and-tag-images.outputs.image_tagged_transaction_exclusion_api }}
|
||||
secrets: inherit
|
||||
|
||||
run-e2e-tests-geth-tracing:
|
||||
@@ -195,7 +220,7 @@ jobs:
|
||||
commit_tag: ${{ needs.store-image-name-and-tags.outputs.commit_tag }}
|
||||
develop_tag: ${{ needs.store-image-name-and-tags.outputs.develop_tag }}
|
||||
untested_tag_suffix: ${{ needs.store-image-name-and-tags.outputs.untested_tag_suffix }}
|
||||
image_names: '["consensys/linea-coordinator", "consensys/linea-postman", "consensys/linea-prover", "consensys/linea-traces-api-facade"]'
|
||||
image_names: '["consensys/linea-coordinator", "consensys/linea-postman", "consensys/linea-prover", "consensys/linea-traces-api-facade", "consensys/linea-transaction-exclusion-api"]'
|
||||
secrets: inherit
|
||||
|
||||
cleanup-deployments:
|
||||
|
||||
@@ -29,6 +29,9 @@ on:
|
||||
traces_api_facade_changed:
|
||||
required: true
|
||||
type: string
|
||||
transaction_exclusion_api_changed:
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
image_tagged_coordinator:
|
||||
value: ${{ jobs.image_tag_push.outputs.image_tagged_coordinator }}
|
||||
@@ -38,6 +41,8 @@ on:
|
||||
value: ${{ jobs.image_tag_push.outputs.image_tagged_postman }}
|
||||
image_tagged_traces_api_facade:
|
||||
value: ${{ jobs.image_tag_push.outputs.image_tagged_traces_api_facade }}
|
||||
image_tagged_transaction_exclusion_api:
|
||||
value: ${{ jobs.image_tag_push.outputs.image_tagged_transaction_exclusion_api }}
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
@@ -61,6 +66,8 @@ jobs:
|
||||
common_ancestor_commit_tag_exists_prover: ${{ steps.check_image_tags_exist_prover.outputs.common_ancestor_commit_tag_exists }}
|
||||
last_commit_tag_exists_traces_api_facade: ${{ steps.check_image_tags_exist_traces_api_facade.outputs.last_commit_tag_exists }}
|
||||
common_ancestor_commit_tag_exists_traces_api_facade: ${{ steps.check_image_tags_exist_traces_api_facade.outputs.common_ancestor_commit_tag_exists }}
|
||||
last_commit_tag_exists_transaction_exclusion_api: ${{ steps.check_image_tags_exist_transaction_exclusion_api.outputs.last_commit_tag_exists }}
|
||||
common_ancestor_commit_tag_exists_transaction_exclusion_api: ${{ steps.check_image_tags_exist_transaction_exclusion_api.outputs.common_ancestor_commit_tag_exists }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -108,6 +115,17 @@ jobs:
|
||||
image_name: consensys/linea-traces-api-facade
|
||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Check image tags exist for transaction-exclusion-api
|
||||
uses: ./.github/actions/check-image-tags-exist
|
||||
if: ${{ inputs.transaction_exclusion_api_changed == 'false' }}
|
||||
id: check_image_tags_exist_transaction_exclusion_api
|
||||
with:
|
||||
last_commit_tag: ${{ inputs.last_commit_tag }}
|
||||
common_ancestor_tag: ${{ inputs.common_ancestor_tag }}
|
||||
image_name: consensys/linea-transaction-exclusion-api
|
||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
image_tag_push:
|
||||
runs-on: [self-hosted, ubuntu-20.04, X64, small]
|
||||
@@ -118,6 +136,7 @@ jobs:
|
||||
image_tagged_prover: ${{ steps.image_tag_push_prover.outputs.image_tagged }}
|
||||
image_tagged_postman: ${{ steps.image_tag_push_postman.outputs.image_tagged }}
|
||||
image_tagged_traces_api_facade: ${{ steps.image_tag_push_traces_api_facade.outputs.image_tagged }}
|
||||
image_tagged_transaction_exclusion_api: ${{ steps.image_tag_push_transaction_exclusion_api.outputs.image_tagged }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -185,3 +204,19 @@ jobs:
|
||||
common_ancestor_commit_tag_exists: ${{ needs.check_image_tags_exist.outputs.common_ancestor_commit_tag_exists_traces_api_facade }}
|
||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Tag and push transaction exclusion api image
|
||||
id: image_tag_push_transaction_exclusion_api
|
||||
uses: ./.github/actions/image-tag-and-push
|
||||
if: ${{ inputs.transaction_exclusion_api_changed == 'false' }}
|
||||
with:
|
||||
commit_tag: ${{ inputs.commit_tag }}
|
||||
last_commit_tag: ${{ inputs.last_commit_tag }}
|
||||
common_ancestor_tag: ${{ inputs.common_ancestor_tag }}
|
||||
develop_tag: ${{ inputs.develop_tag }}
|
||||
untested_tag_suffix: ${{ inputs.untested_tag_suffix }}
|
||||
image_name: consensys/linea-transaction-exclusion-api
|
||||
last_commit_tag_exists: ${{ needs.check_image_tags_exist.outputs.last_commit_tag_exists_transaction_exclusion_api }}
|
||||
common_ancestor_commit_tag_exists: ${{ needs.check_image_tags_exist.outputs.common_ancestor_commit_tag_exists_transaction_exclusion_api }}
|
||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
3
.github/workflows/reuse-run-e2e-tests.yml
vendored
3
.github/workflows/reuse-run-e2e-tests.yml
vendored
@@ -72,6 +72,7 @@ jobs:
|
||||
POSTMAN_TAG: ${{ inputs.commit_tag }}-${{ inputs.untested_tag_suffix }}
|
||||
PROVER_TAG: ${{ inputs.commit_tag }}-${{ inputs.untested_tag_suffix }}
|
||||
TRACES_API_TAG: ${{ inputs.commit_tag }}-${{ inputs.untested_tag_suffix }}
|
||||
TRANSACTION_EXCLUSION_API_TAG: ${{ inputs.commit_tag }}-${{ inputs.untested_tag_suffix }}
|
||||
GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN_RELEASE_ACCESS }}
|
||||
outputs:
|
||||
tests_outcome: ${{ steps.run_e2e_tests.outcome }}
|
||||
@@ -165,6 +166,8 @@ jobs:
|
||||
docker logs postman --since 1h &>> docker_logs/postman.txt
|
||||
docker logs traces-node --since 1h &>> docker_logs/traces-node.txt
|
||||
docker logs traces-node-v2 --since 1h &>> docker_logs/traces-node-v2.txt;
|
||||
docker logs l2-node-besu --since 1h &>> docker_logs/l2-node-besu.txt;
|
||||
docker logs transaction-exclusion-api --since 1h &>> docker_logs/transaction-exclusion-api.txt
|
||||
docker logs sequencer --since 1h &>> docker_logs/sequencer.txt
|
||||
- name: Archive debug logs
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Compute version tags
|
||||
id: step2
|
||||
run: |
|
||||
echo COMMIT_TAG=$(git rev-parse --short "$GITHUB_SHA") >> $GITHUB_OUTPUT
|
||||
echo COMMIT_TAG=$(git rev-parse --short "${{ github.event.pull_request.head.sha }}") >> $GITHUB_OUTPUT
|
||||
echo LAST_COMMIT_TAG=$(git rev-parse --short "${{ env.EVENT_BEFORE }}") >> $GITHUB_OUTPUT
|
||||
echo DEVELOP_TAG=develop >> $GITHUB_OUTPUT
|
||||
echo COMMON_ANCESTOR_TAG=$(git rev-parse --short "${{ env.COMMON_ANCESTOR }}") >> $GITHUB_OUTPUT
|
||||
|
||||
11
.github/workflows/testing.yml
vendored
11
.github/workflows/testing.yml
vendored
@@ -18,6 +18,9 @@ on:
|
||||
traces_api_facade_changed:
|
||||
required: true
|
||||
type: string
|
||||
transaction_exclusion_api_changed:
|
||||
required: true
|
||||
type: string
|
||||
coordinator_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
@@ -27,6 +30,9 @@ on:
|
||||
traces_api_facade_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
transaction_exclusion_api_image_tagged:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
@@ -55,3 +61,8 @@ jobs:
|
||||
uses: ./.github/workflows/traces-api-facade-testing.yml
|
||||
if: ${{ always() && (inputs.traces_api_facade_changed == 'true' || inputs.traces_api_facade_image_tagged != 'true') }}
|
||||
secrets: inherit
|
||||
|
||||
transaction-exclusion-api:
|
||||
uses: ./.github/workflows/transaction-exclusion-api-testing.yml
|
||||
if: ${{ always() && (inputs.transaction_exclusion_api_changed == 'true' || inputs.transaction_exclusion_api_image_tagged != 'true') }}
|
||||
secrets: inherit
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
retry_on: error
|
||||
timeout_minutes: 20
|
||||
command: |
|
||||
./gradlew traces-api-facade:app:buildNeeded jacocoRootReport
|
||||
./gradlew -V traces-api-facade:app:buildNeeded jacocoRootReport
|
||||
- name: Run tests without coverage
|
||||
uses: nick-fields/retry@v2
|
||||
if: ${{ !inputs.coverage }}
|
||||
@@ -52,4 +52,4 @@ jobs:
|
||||
retry_on: error
|
||||
timeout_minutes: 20
|
||||
command: |
|
||||
./gradlew traces-api-facade:app:buildNeeded
|
||||
./gradlew -V traces-api-facade:app:buildNeeded
|
||||
|
||||
80
.github/workflows/transaction-exclusion-api-build-and-publish.yml
vendored
Normal file
80
.github/workflows/transaction-exclusion-api-build-and-publish.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Transaction-Exclusion-Api build and publish CI
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit_tag:
|
||||
required: true
|
||||
type: string
|
||||
last_commit_tag:
|
||||
required: true
|
||||
type: string
|
||||
common_ancestor_tag:
|
||||
required: true
|
||||
type: string
|
||||
develop_tag:
|
||||
required: true
|
||||
type: string
|
||||
untested_tag_suffix:
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
DOCKERHUB_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: [self-hosted, ubuntu-20.04, X64, small]
|
||||
name: Transaction exclusion api build
|
||||
env:
|
||||
COMMIT_TAG: ${{ inputs.commit_tag }}
|
||||
DEVELOP_TAG: ${{ inputs.develop_tag }}
|
||||
UNTESTED_TAG_SUFFIX: ${{ inputs.untested_tag_suffix }}
|
||||
IMAGE_NAME: ${{ inputs.image_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Build dist
|
||||
run: |
|
||||
./gradlew transaction-exclusion-api:app:distZip --no-daemon
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: transaction-exclusion-api
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: consensys/linea-transaction-exclusion-api
|
||||
- name: Build & push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-contexts: zip=./transaction-exclusion-api/app/build/distributions/
|
||||
file: ./transaction-exclusion-api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ env.COMMIT_TAG }}-${{ env.UNTESTED_TAG_SUFFIX }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
41
.github/workflows/transaction-exclusion-api-testing.yml
vendored
Normal file
41
.github/workflows/transaction-exclusion-api-testing.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: transaction-exclusion-api-testing
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
DOCKERHUB_USERNAME:
|
||||
required: true
|
||||
DOCKERHUB_TOKEN:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
coverage:
|
||||
description: To generate test report
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: [self-hosted, ubuntu-20.04, X64, small]
|
||||
name: Transaction exclusion api tests
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Run tests with coverage
|
||||
if: ${{ inputs.coverage }}
|
||||
run: |
|
||||
./gradlew transaction-exclusion-api:app:buildNeeded jacocoRootReport
|
||||
- name: Run tests without coverage
|
||||
if: ${{ !inputs.coverage }}
|
||||
run: |
|
||||
./gradlew transaction-exclusion-api:app:buildNeeded
|
||||
23
.run/TransactionExclusionApi.run.xml.template
Normal file
23
.run/TransactionExclusionApi.run.xml.template
Normal file
@@ -0,0 +1,23 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TransactionExclusionApi" type="Application" factoryName="Application">
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="21" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.consensys.linea.transactionexclusion.app.TransactionExclusionAppMain" />
|
||||
<module name="zkevm.transaction-exclusion-api.app.main" />
|
||||
<option name="PROGRAM_PARAMETERS" value="config/transaction-exclusion-api/transaction-exclusion-app-docker.config.toml config/transaction-exclusion-api/transaction-exclusion-app-local-dev.config.overrides.toml" />
|
||||
<option name="VM_PARAMETERS" value="-Dvertx.configurationFile=config/transaction-exclusion-api/vertx-options.json -Dlog4j2.configurationFile=config/transaction-exclusion-api/log4j2-dev.xml" />
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
18
Makefile
18
Makefile
@@ -48,15 +48,9 @@ start-whole-environment:
|
||||
# docker compose -f docker/compose.yml -f docker/compose-local-dev.overrides.yml build prover
|
||||
docker compose -f docker/compose.yml -f docker/compose-local-dev.overrides.yml --profile l1 --profile l2 up -d
|
||||
|
||||
start-whole-environment-with-finalized-tag-updater:
|
||||
docker compose -f docker/compose.yml -f docker/compose-local-dev.overrides.yml -f docker/compose-local-dev-finalized-tag-updater.overrides.yml --profile l1 --profile l2 up -d
|
||||
|
||||
start-whole-environment-traces-v2:
|
||||
docker compose -f docker/compose.yml -f docker/compose-local-dev-traces-v2.overrides.yml --profile l1 --profile l2 up -d
|
||||
|
||||
start-whole-environment-traces-v2-with-finalized-tag-updater:
|
||||
docker compose -f docker/compose.yml -f docker/compose-local-dev-traces-v2.overrides.yml -f docker/compose-local-dev-finalized-tag-updater.overrides.yml --profile l1 --profile l2 up -d
|
||||
|
||||
pull-all-images:
|
||||
docker compose -f docker/compose.yml -f docker/compose-local-dev-traces-v2.overrides.yml --profile l1 --profile l2 pull
|
||||
|
||||
@@ -122,10 +116,6 @@ fresh-start-all-traces-v2:
|
||||
make clean-environment
|
||||
make start-all-traces-v2
|
||||
|
||||
fresh-start-all-traces-v2-with-finalized-tag-updater:
|
||||
make clean-environment
|
||||
make start-all-traces-v2-with-finalized-tag-updater
|
||||
|
||||
start-all-smc-v4:
|
||||
L1_GENESIS_TIME=$(get_future_time) make start-whole-environment
|
||||
make deploy-contracts-v4
|
||||
@@ -134,18 +124,10 @@ start-all:
|
||||
L1_GENESIS_TIME=$(get_future_time) make start-whole-environment
|
||||
make deploy-contracts
|
||||
|
||||
start-all-with-finalized-tag-updater:
|
||||
L1_GENESIS_TIME=$(get_future_time) make start-whole-environment-with-finalized-tag-updater
|
||||
make deploy-contracts
|
||||
|
||||
start-all-traces-v2:
|
||||
L1_GENESIS_TIME=$(get_future_time) make start-whole-environment-traces-v2
|
||||
make deploy-contracts
|
||||
|
||||
start-all-traces-v2-with-finalized-tag-updater:
|
||||
L1_GENESIS_TIME=$(get_future_time) make start-whole-environment-traces-v2-with-finalized-tag-updater
|
||||
make deploy-contracts
|
||||
|
||||
deploy-contracts-v4:
|
||||
make compile-contracts
|
||||
$(MAKE) -j2 deploy-linea-rollup-v4 deploy-l2messageservice
|
||||
|
||||
12
build.gradle
12
build.gradle
@@ -147,6 +147,18 @@ dockerCompose {
|
||||
projectName = "docker"
|
||||
environment.put("L1_GENESIS_TIME", "${Instant.now().plusSeconds(3).getEpochSecond()}")
|
||||
}
|
||||
|
||||
localStackPostgresDbOnly {
|
||||
startedServices = [
|
||||
"postgres"
|
||||
]
|
||||
useComposeFiles = ["${project.rootDir.path}/docker/compose.yml"]
|
||||
waitForHealthyStateTimeout = Duration.ofMinutes(3)
|
||||
waitForTcpPorts = false
|
||||
removeOrphans = true
|
||||
noRecreate = true
|
||||
projectName = "docker"
|
||||
}
|
||||
}
|
||||
|
||||
static Boolean hasKotlinPlugin(Project proj) {
|
||||
|
||||
@@ -14,6 +14,7 @@ eth-api="http://traces-node-v2:8545"
|
||||
|
||||
[traces]
|
||||
switch-to-linea-besu=true
|
||||
blob-compressor-version="V1_0_1"
|
||||
expected-traces-api-version-v2="v0.8.0-rc3"
|
||||
[traces.counters-v2]
|
||||
endpoints=["http://traces-node-v2:8545/"]
|
||||
@@ -26,7 +27,7 @@ request-limit-per-endpoint=2
|
||||
request-retry.backoff-delay="PT1S"
|
||||
request-retry.failures-warning-threshold=2
|
||||
|
||||
[dynamic-gas-price-service]
|
||||
[l2-network-gas-pricing.json-rpc-pricing-propagation]
|
||||
geth-gas-price-update-recipients=[
|
||||
"http://l2-node:8545/"
|
||||
]
|
||||
|
||||
@@ -3,12 +3,6 @@ output_traces_directory = "/data/traces/v1/conflated"
|
||||
traces_api_version = "0.2.0"
|
||||
traces_file_extension = "json.gz"
|
||||
|
||||
# Inmemory cache of the traces read from the file system
|
||||
# Save IO and CPU unzipping and JSON parsing
|
||||
[read_traces_cache]
|
||||
size = 20
|
||||
expiration_duration = "PT20M"
|
||||
|
||||
[api]
|
||||
port = 8080
|
||||
path = "/"
|
||||
|
||||
21
config/transaction-exclusion-api/log4j2-dev.xml
Normal file
21
config/transaction-exclusion-api/log4j2-dev.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="warn" shutdownHook="disable" monitorInterval="2">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %m%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Logger name="net.consensys.linea.jsonrpc" level="TRACE" additivity="false">
|
||||
<appender-ref ref="console" level="trace"/>
|
||||
</Logger>
|
||||
<Logger name="net.consensys.linea" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="console" level="trace"/>
|
||||
</Logger>
|
||||
<Root level="info" additivity="true">
|
||||
<appender-ref ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -0,0 +1,30 @@
|
||||
data-queryable-window-since-rejected-timestamp="P7D"
|
||||
|
||||
[database]
|
||||
schema="linea_transaction_exclusion"
|
||||
read_pool_size=10
|
||||
read_pipelining_limit=10
|
||||
transactional_pool_size=10
|
||||
[database.read]
|
||||
host="postgres"
|
||||
port="5432"
|
||||
username="postgres"
|
||||
password="postgres"
|
||||
[database.write]
|
||||
host="postgres"
|
||||
port="5432"
|
||||
username="postgres"
|
||||
password="postgres"
|
||||
[database.cleanup]
|
||||
polling-interval="PT60S"
|
||||
storage-period="P7D"
|
||||
[database.persistence-retry]
|
||||
#max-retries = 10 commented as can be null
|
||||
backoff-delay = "PT5S"
|
||||
timeout = "PT20S"
|
||||
|
||||
[api]
|
||||
port=8080
|
||||
path="/"
|
||||
number-of-verticles=1 #if 0 will create one verticle per core (or hyperthread if supported)
|
||||
observability-port=8090
|
||||
@@ -0,0 +1,9 @@
|
||||
[database]
|
||||
[database.read]
|
||||
host="localhost"
|
||||
[database.write]
|
||||
host="localhost"
|
||||
|
||||
[api]
|
||||
port=8082
|
||||
number-of-verticles=1
|
||||
16
config/transaction-exclusion-api/vertx-options.json
Normal file
16
config/transaction-exclusion-api/vertx-options.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"preferNativeTransport": true,
|
||||
"logStacktraceThreshold": 500,
|
||||
"maxEventLoopExecuteTime": 2,
|
||||
"maxEventLoopExecuteTimeUnit": "MINUTES",
|
||||
"warnEventLoopBlocked": 5000,
|
||||
"maxWorkerExecuteTime": 2,
|
||||
"maxWorkerExecuteTimeUnit": "MINUTES",
|
||||
"metricsOptions": {
|
||||
"enabled": true,
|
||||
"prometheusOptions": {
|
||||
"enabled": true,
|
||||
"publishQuantiles": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ dependencies {
|
||||
implementation project(':jvm-libs:generic:http-rest')
|
||||
implementation project(':jvm-libs:generic:vertx-helper')
|
||||
implementation project(':jvm-libs:generic:extensions:futures')
|
||||
implementation project(':jvm-libs:generic:persistence:db')
|
||||
implementation project(':jvm-libs:linea:web3j-extensions')
|
||||
implementation project(':jvm-libs:linea:core:metrics')
|
||||
implementation project(':jvm-libs:linea:metrics:micrometer')
|
||||
@@ -31,11 +32,11 @@ dependencies {
|
||||
implementation project(':coordinator:ethereum:blob-submitter')
|
||||
implementation project(':coordinator:ethereum:message-anchoring')
|
||||
implementation project(':coordinator:clients:web3signer-client')
|
||||
implementation project(':coordinator:persistence:db')
|
||||
implementation project(':coordinator:persistence:blob')
|
||||
implementation project(':coordinator:persistence:aggregation')
|
||||
implementation project(':coordinator:persistence:batch')
|
||||
implementation project(':coordinator:persistence:feehistory')
|
||||
implementation project(':coordinator:persistence:db-common')
|
||||
implementation project(":jvm-libs:linea:teku-execution-client")
|
||||
implementation "tech.pegasys.teku.internal:bytes:${libs.versions.teku.get()}"
|
||||
|
||||
|
||||
@@ -833,6 +833,7 @@ class CoordinatorConfigTest {
|
||||
),
|
||||
traces = tracesConfig.copy(
|
||||
switchToLineaBesu = true,
|
||||
blobCompressorVersion = BlobCompressorVersion.V1_0_1,
|
||||
expectedTracesApiVersionV2 = "v0.8.0-rc3",
|
||||
conflationV2 = tracesConfig.conflation.copy(
|
||||
endpoints = listOf(URI("http://traces-node-v2:8545/").toURL())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
plugins {
|
||||
id 'net.consensys.zkevm.kotlin-library-conventions'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.consensys.zkevm.ethereum.type2statemanagerjsonrpcclient
|
||||
package net.consensys.zkevm.coordinator.clients
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
@@ -17,9 +17,6 @@ import net.consensys.linea.async.get
|
||||
import net.consensys.linea.errors.ErrorResponse
|
||||
import net.consensys.linea.jsonrpc.client.RequestRetryConfig
|
||||
import net.consensys.linea.jsonrpc.client.VertxHttpJsonRpcClientFactory
|
||||
import net.consensys.zkevm.coordinator.clients.GetZkEVMStateMerkleProofResponse
|
||||
import net.consensys.zkevm.coordinator.clients.Type2StateManagerErrorType
|
||||
import net.consensys.zkevm.coordinator.clients.Type2StateManagerJsonRpcClient
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
@@ -12,7 +12,6 @@ dependencies {
|
||||
implementation(project(':coordinator:ethereum:gas-pricing'))
|
||||
implementation(project(':coordinator:ethereum:test-utils'))
|
||||
implementation(project(":coordinator:persistence:blob"))
|
||||
implementation(project(":coordinator:persistence:db"))
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}")
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${libs.versions.jackson.get()}")
|
||||
@@ -25,10 +24,11 @@ dependencies {
|
||||
|
||||
testImplementation(project(":jvm-libs:linea:testing:l1-blob-and-proof-submission"))
|
||||
testImplementation(project(":coordinator:persistence:aggregation"))
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
testImplementation(project(":coordinator:persistence:db-common"))
|
||||
testImplementation(testFixtures(project(":coordinator:ethereum:gas-pricing")))
|
||||
testImplementation(testFixtures(project(":coordinator:core")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}")
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}")
|
||||
|
||||
@@ -23,7 +23,7 @@ import net.consensys.zkevm.persistence.dao.aggregation.PostgresAggregationsDao
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsPostgresDao
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsRepositoryImpl
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.awaitility.Awaitility.waitAtMost
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -36,6 +36,10 @@ import kotlin.time.toJavaDuration
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class BlobAndAggregationFinalizationIntTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("coordinator-tests-submission-int-test")
|
||||
private val fakeClock = FakeFixedClock()
|
||||
private lateinit var lineaRollupContractForAggregationSubmission: LineaRollupSmartContractClient
|
||||
|
||||
@@ -4,14 +4,16 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
api(project(":coordinator:core"))
|
||||
api project(':coordinator:clients:prover-client:serialization')
|
||||
implementation(project(":coordinator:persistence:db"))
|
||||
api(project(":coordinator:clients:prover-client:serialization"))
|
||||
implementation(project(":jvm-libs:generic:persistence:db"))
|
||||
implementation(project(":coordinator:persistence:batch"))
|
||||
|
||||
testImplementation(project(":coordinator:persistence:batch"))
|
||||
testImplementation(project(":coordinator:persistence:blob"))
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
testImplementation(project(":coordinator:persistence:db-common"))
|
||||
testImplementation(testFixtures(project(":coordinator:core")))
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db-common")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import net.consensys.zkevm.persistence.dao.batch.persistence.BatchesPostgresDao
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsPostgresDao
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.test.DbQueries
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.DbQueries
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -39,6 +39,10 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class AggregationsPostgresDaoTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("coordinator-tests-aggregations-dao")
|
||||
|
||||
private val maxBlobReturnLimit = 10u
|
||||
|
||||
@@ -21,7 +21,7 @@ import net.consensys.zkevm.persistence.dao.batch.persistence.PostgresBatchesRepo
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsPostgresDao
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsRepositoryImpl
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -31,6 +31,9 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class RecordsCleanupFinalizationHandlerTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("records-cleanup-on-finalization")
|
||||
private var fakeClock = FakeFixedClock(Clock.System.now())
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
plugins {
|
||||
id "net.consensys.zkevm.kotlin-library-conventions"
|
||||
id "net.consensys.zkevm.linea-contracts-helper"
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":coordinator:core"))
|
||||
implementation(project(":coordinator:persistence:db"))
|
||||
implementation(project(":jvm-libs:generic:persistence:db"))
|
||||
implementation(project(":jvm-libs:linea:core:metrics"))
|
||||
implementation(project(":coordinator:clients:prover-client:file-based-client")) {
|
||||
because "ProverResponseIndex is a part of it"
|
||||
}
|
||||
|
||||
testImplementation(project(":coordinator:persistence:db-common"))
|
||||
testImplementation(testFixtures(project(":coordinator:core")))
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db-common")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
|
||||
testFixturesImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -12,8 +12,8 @@ import net.consensys.zkevm.domain.createBatch
|
||||
import net.consensys.zkevm.persistence.dao.batch.persistence.BatchesDao.Companion.batchesDaoTableName
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.test.DbQueries
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.DbQueries
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -26,6 +26,10 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class BatchesPostgresDaoTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("coordinator-tests-batches")
|
||||
private var fakeClockTime = Instant.parse("2023-12-11T00:00:00.000Z")
|
||||
private var fakeClock = FakeFixedClock(fakeClockTime)
|
||||
|
||||
@@ -10,8 +10,8 @@ import net.consensys.zkevm.domain.Batch
|
||||
import net.consensys.zkevm.domain.createBatch
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.test.DbQueries
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.DbQueries
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -23,6 +23,10 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class PostgresBatchesRepositoryTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
private var fakeClockTime = Instant.parse("2023-12-11T00:00:00.000Z")
|
||||
override val databaseName = DbHelper.generateUniqueDbName("coordinator-tests-batches-repository")
|
||||
|
||||
|
||||
@@ -3,29 +3,22 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
plugins {
|
||||
id "net.consensys.zkevm.kotlin-library-conventions"
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":coordinator:core"))
|
||||
api project(':coordinator:clients:prover-client:serialization')
|
||||
implementation(project(":coordinator:persistence:db"))
|
||||
api(project(":coordinator:clients:prover-client:serialization"))
|
||||
implementation(project(":jvm-libs:generic:persistence:db"))
|
||||
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}")
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}")
|
||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:${libs.versions.jackson.get()}")
|
||||
testImplementation("tech.pegasys.teku.internal:executionclient:${libs.versions.teku.get()}")
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
testImplementation(project(":coordinator:persistence:db-common"))
|
||||
testImplementation(testFixtures(project(":coordinator:core")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
|
||||
testFixturesImplementation('tech.pegasys.teku.internal:async:23.1.1')
|
||||
testFixturesImplementation('org.apache.tuweni:tuweni-units:2.3.1')
|
||||
testFixturesImplementation('com.michael-bull.kotlin-result:kotlin-result:1.1.16')
|
||||
testFixturesImplementation('org.mockito.kotlin:mockito-kotlin:5.1.0')
|
||||
testFixturesApi(platform("org.junit:junit-bom:${libs.versions.junit.get()}"))
|
||||
testFixturesApi("io.vertx:vertx-junit5:${libs.versions.vertx.get()}")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -23,7 +23,7 @@ import net.consensys.zkevm.persistence.BlobsRepository
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsPostgresDao
|
||||
import net.consensys.zkevm.persistence.dao.blob.BlobsRepositoryImpl
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.apache.tuweni.bytes.Bytes32
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.awaitility.Awaitility.waitAtMost
|
||||
@@ -47,6 +47,10 @@ import kotlin.time.toJavaDuration
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class BlobCompressionProofCoordinatorIntTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName(
|
||||
"blob-compression-proof-coordinator"
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import net.consensys.zkevm.domain.BlockIntervals
|
||||
import net.consensys.zkevm.domain.createBlobRecord
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -32,6 +32,10 @@ import kotlin.time.toJavaDuration
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class BlobsPostgresDaoTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("coordinator-tests-blobs-dao")
|
||||
private val maxBlobsToReturn = 6u
|
||||
private fun blobsContentQuery(): PreparedQuery<RowSet<Row>> =
|
||||
|
||||
38
coordinator/persistence/db-common/build.gradle
Normal file
38
coordinator/persistence/db-common/build.gradle
Normal file
@@ -0,0 +1,38 @@
|
||||
plugins {
|
||||
id "net.consensys.zkevm.kotlin-library-conventions"
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(project(":jvm-libs:generic:extensions:futures"))
|
||||
testImplementation(project(":jvm-libs:generic:extensions:kotlin"))
|
||||
testImplementation(project(":jvm-libs:generic:persistence:db"))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
|
||||
testFixturesImplementation(project(":coordinator:core"))
|
||||
testFixturesImplementation("io.vertx:vertx-pg-client:${libs.versions.vertx.get()}")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + sourceSets.test.compileClasspath
|
||||
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
test ->
|
||||
description = "Runs integration tests."
|
||||
group = "verification"
|
||||
useJUnitPlatform()
|
||||
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
|
||||
dependsOn(":localStackComposeUp")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.consensys.zkevm.persistence.db
|
||||
package net.consensys.zkevm.persistence.db.test
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.VertxExtension
|
||||
@@ -7,7 +7,8 @@ import io.vertx.sqlclient.SqlClient
|
||||
import kotlinx.datetime.Clock
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.async.get
|
||||
import net.consensys.zkevm.persistence.test.DbQueries
|
||||
import net.consensys.zkevm.persistence.db.Db
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -26,8 +27,8 @@ class DbSchemaUpdatesIntTest {
|
||||
private val databaseName = DbHelper.generateUniqueDbName("coordinator-db-migration-tests")
|
||||
private val username = "postgres"
|
||||
private val password = "postgres"
|
||||
private lateinit var dataSource: DataSource
|
||||
|
||||
private lateinit var dataSource: DataSource
|
||||
private lateinit var pool: Pool
|
||||
private lateinit var sqlClient: SqlClient
|
||||
|
||||
@@ -73,7 +74,10 @@ class DbSchemaUpdatesIntTest {
|
||||
val schemaTarget = "1"
|
||||
|
||||
DbHelper.dropAllTables(dataSource)
|
||||
Db.applyDbMigrations(dataSource, schemaTarget)
|
||||
Db.applyDbMigrations(
|
||||
dataSource = dataSource,
|
||||
target = schemaTarget
|
||||
)
|
||||
|
||||
val paramsV1 = listOf(
|
||||
Clock.System.now().toEpochMilliseconds(),
|
||||
@@ -106,7 +110,10 @@ class DbSchemaUpdatesIntTest {
|
||||
val schemaTarget = "2"
|
||||
|
||||
DbHelper.dropAllTables(dataSource)
|
||||
Db.applyDbMigrations(dataSource, schemaTarget)
|
||||
Db.applyDbMigrations(
|
||||
dataSource = dataSource,
|
||||
target = schemaTarget
|
||||
)
|
||||
|
||||
val batchParamsV2 = listOf(
|
||||
Clock.System.now().toEpochMilliseconds(),
|
||||
@@ -133,7 +140,7 @@ class DbSchemaUpdatesIntTest {
|
||||
Clock.System.now().toEpochMilliseconds(),
|
||||
3,
|
||||
ByteArray(32).encodeHex(),
|
||||
""
|
||||
"{}"
|
||||
)
|
||||
|
||||
DbQueries.insertBlob(sqlClient, DbQueries.insertBlobQuery, blobParams).get()
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.consensys.zkevm.persistence.test
|
||||
package net.consensys.zkevm.persistence.db.test
|
||||
|
||||
import io.vertx.core.Future
|
||||
import io.vertx.sqlclient.PreparedQuery
|
||||
@@ -46,7 +46,7 @@ object DbQueries {
|
||||
(created_epoch_milli, start_block_number, end_block_number,
|
||||
conflation_calculator_version, blob_hash, status, start_block_timestamp, end_block_timestamp,
|
||||
batches_count, expected_shnarf, blob_compression_proof)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CAST($11::text as jsonb))
|
||||
""".trimIndent()
|
||||
|
||||
val insertBatchQueryV1 =
|
||||
@@ -6,15 +6,18 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':coordinator:clients:prover-client:serialization')
|
||||
implementation(project(":coordinator:persistence:db"))
|
||||
implementation(project(":coordinator:ethereum:gas-pricing:dynamic-cap"))
|
||||
implementation(project(":jvm-libs:generic:extensions:futures"))
|
||||
implementation(project(":jvm-libs:generic:extensions:kotlin"))
|
||||
implementation(project(":jvm-libs:generic:persistence:db"))
|
||||
implementation(project(":jvm-libs:linea:core:domain-models"))
|
||||
|
||||
testImplementation(project(":coordinator:persistence:db-common"))
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}")
|
||||
testImplementation("com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}")
|
||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:${libs.versions.jackson.get()}")
|
||||
testImplementation("tech.pegasys.teku.internal:executionclient:${libs.versions.teku.get()}")
|
||||
testImplementation(testFixtures(project(":coordinator:persistence:db")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import net.consensys.FakeFixedClock
|
||||
import net.consensys.linea.FeeHistory
|
||||
import net.consensys.linea.async.get
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.test.CleanDbTestSuiteParallel
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -17,6 +17,9 @@ import org.junit.jupiter.api.extension.ExtendWith
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class FeeHistoriesPostgresDaoTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "4"
|
||||
}
|
||||
|
||||
fun createFeeHistory(
|
||||
oldestBlockNumber: ULong,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="net.consensys.zkevm.persistence.feehistory" level="trace" additivity="false">
|
||||
<Logger name="net.consensys.zkevm.persistence.dao.feehistory" level="trace" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Root level="info" additivity="false">
|
||||
|
||||
@@ -7,9 +7,15 @@ services:
|
||||
JAVA_OPTS: -XX:+UnlockExperimentalVMOptions -XX:-UseG1GC -XX:+UseZGC
|
||||
volumes:
|
||||
- ../config/common/traces-limits-besu-v2.toml:/var/lib/besu/traces-limits.toml:ro
|
||||
|
||||
l2-node-besu:
|
||||
environment:
|
||||
JAVA_OPTS: -XX:+UnlockExperimentalVMOptions -XX:-UseG1GC -XX:+UseZGC
|
||||
volumes:
|
||||
- ../config/common/traces-limits-besu-v2.toml:/var/lib/besu/traces-limits.toml:ro
|
||||
|
||||
linea-besu-sequencer-plugin-downloader:
|
||||
command: [ "sh", "/file-downloader.sh", "https://github.com/Consensys/linea-sequencer/releases/download/v0.8.0-rc1.1/linea-sequencer-v0.8.0-rc1.1.jar", "/linea-besu-sequencer" ]
|
||||
command: [ "sh", "/file-downloader.sh", "https://github.com/Consensys/linea-sequencer/releases/download/v0.8.0-rc4.1/linea-sequencer-v0.8.0-rc4.1.jar", "/linea-besu-sequencer" ]
|
||||
|
||||
traces-node:
|
||||
command: ['echo', 'forced exit as replaced by traces-node-v2']
|
||||
@@ -40,6 +46,7 @@ services:
|
||||
retries: 120
|
||||
restart: "no"
|
||||
environment:
|
||||
JAVA_OPTS: -XX:+UnlockExperimentalVMOptions -XX:-UseG1GC -XX:+UseZGC
|
||||
LOG4J_CONFIGURATION_FILE: /var/lib/besu/log4j.xml
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
@@ -51,7 +58,6 @@ services:
|
||||
--bootnodes=enode://14408801a444dafc44afbccce2eb755f902aed3b5743fed787b3c790e021fef28b8c827ed896aa4e8fb46e22bd67c39f994a73768b4b382f8597b0d44370e15d@11.11.11.101:30303
|
||||
volumes:
|
||||
- ./config/traces-node-v2/traces-node-v2-config.toml:/var/lib/besu/traces-node-v2.config.toml:ro
|
||||
- ./config/traces-node-v2/key:/var/lib/besu/key:ro
|
||||
- ./config/traces-node-v2/log4j.xml:/var/lib/besu/log4j.xml:ro
|
||||
- ./config/linea-local-dev-genesis-PoA-besu.json/:/var/lib/besu/genesis.json:ro
|
||||
- ../tmp/traces-node-v2/plugins:/opt/besu/plugins/
|
||||
|
||||
@@ -48,3 +48,7 @@ services:
|
||||
l1-el-node:
|
||||
environment:
|
||||
JAVA_OPTS: -XX:+UnlockExperimentalVMOptions -XX:-UseG1GC -XX:+UseZGC
|
||||
|
||||
l2-node-besu:
|
||||
environment:
|
||||
JAVA_OPTS: -XX:+UnlockExperimentalVMOptions -XX:-UseG1GC -XX:+UseZGC
|
||||
|
||||
@@ -54,7 +54,9 @@ services:
|
||||
--node-private-key-file="/var/lib/besu/key" \
|
||||
--plugin-linea-l1-polling-interval="PT12S" \
|
||||
--plugin-linea-l1-smart-contract-address="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" \
|
||||
--plugin-linea-l1-rpc-endpoint="http://l1-el-node:8545"
|
||||
--plugin-linea-l1-rpc-endpoint="http://l1-el-node:8545" \
|
||||
--plugin-linea-rejected-tx-endpoint="http://transaction-exclusion-api:8080" \
|
||||
--plugin-linea-node-type="SEQUENCER"
|
||||
volumes:
|
||||
- ./config/linea-besu-sequencer/sequencer.config.toml:/var/lib/besu/sequencer.config.toml:ro
|
||||
- ./config/linea-besu-sequencer/deny-list.txt:/var/lib/besu/deny-list.txt:ro
|
||||
@@ -71,7 +73,7 @@ services:
|
||||
linea-besu-sequencer-plugin-downloader:
|
||||
image: busybox:1.36.1
|
||||
# profiles: ["l2", "l2-bc"] this works locally but breakes on CI, maybe Docker compose version issue
|
||||
command: [ "sh", "/file-downloader.sh", "https://github.com/Consensys/linea-sequencer/releases/download/v0.1.4-test34/besu-sequencer-plugins-v0.1.4-test34.jar", "/linea-besu-sequencer" ]
|
||||
command: [ "sh", "/file-downloader.sh", "https://github.com/Consensys/linea-sequencer/releases/download/v0.1.4-test35/besu-sequencer-plugins-v0.1.4-test35.jar", "/linea-besu-sequencer" ]
|
||||
volumes:
|
||||
- ./scripts/file-downloader.sh:/file-downloader.sh:ro
|
||||
- ../tmp/linea-besu-sequencer/plugins:/linea-besu-sequencer/
|
||||
@@ -116,6 +118,54 @@ services:
|
||||
networks:
|
||||
linea:
|
||||
ipv4_address: 11.11.11.209
|
||||
|
||||
l2-node-besu:
|
||||
hostname: l2-node-besu
|
||||
container_name: l2-node-besu
|
||||
image: consensys/linea-besu:24.10-delivery34
|
||||
profiles: [ "l2", "l2-bc", "debug" ]
|
||||
depends_on:
|
||||
sequencer:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "9045:8545"
|
||||
- "9046:8546"
|
||||
- "9050:8550"
|
||||
- "9051:8548"
|
||||
- "30309:30303"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "bash -c \"[ -f /tmp/pid ]\"" ]
|
||||
interval: 1s
|
||||
timeout: 1s
|
||||
retries: 120
|
||||
restart: "no"
|
||||
environment:
|
||||
LOG4J_CONFIGURATION_FILE: /var/lib/besu/log4j.xml
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
/opt/besu/bin/besu \
|
||||
--config-file=/var/lib/besu/l2-node-besu.config.toml \
|
||||
--genesis-file=/var/lib/besu/genesis.json \
|
||||
--plugin-linea-l1-polling-interval="PT12S" \
|
||||
--plugin-linea-l1-smart-contract-address="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" \
|
||||
--plugin-linea-l1-rpc-endpoint="http://l1-el-node:8545" \
|
||||
--plugin-linea-rejected-tx-endpoint="http://transaction-exclusion-api:8080" \
|
||||
--plugin-linea-node-type="RPC" \
|
||||
--bootnodes=enode://14408801a444dafc44afbccce2eb755f902aed3b5743fed787b3c790e021fef28b8c827ed896aa4e8fb46e22bd67c39f994a73768b4b382f8597b0d44370e15d@11.11.11.101:30303
|
||||
volumes:
|
||||
- ./config/l2-node-besu/l2-node-besu-config.toml:/var/lib/besu/l2-node-besu.config.toml:ro
|
||||
- ./config/linea-besu-sequencer/deny-list.txt:/var/lib/besu/deny-list.txt:ro
|
||||
- ./config/l2-node-besu/log4j.xml:/var/lib/besu/log4j.xml:ro
|
||||
- ./config/linea-local-dev-genesis-PoA-besu.json/:/var/lib/besu/genesis.json:ro
|
||||
- ../config/common/traces-limits-besu-v1.toml:/var/lib/besu/traces-limits.toml:ro
|
||||
- ../tmp/linea-besu-sequencer/plugins:/opt/besu/plugins/
|
||||
- ../tmp/local/:/data/:rw
|
||||
networks:
|
||||
l1-network:
|
||||
linea:
|
||||
ipv4_address: 11.11.11.119
|
||||
|
||||
traces-node:
|
||||
container_name: traces-node
|
||||
@@ -255,6 +305,27 @@ services:
|
||||
linea:
|
||||
ipv4_address: 11.11.11.105
|
||||
|
||||
transaction-exclusion-api:
|
||||
hostname: transaction-exclusion-api
|
||||
container_name: transaction-exclusion-api
|
||||
image: consensys/linea-transaction-exclusion-api:${TRANSACTION_EXCLUSION_API_TAG:-d227ddf}
|
||||
profiles: [ "l2", "debug" ]
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "8082:8080"
|
||||
command: [ 'java', '-Dvertx.configurationFile=config/vertx-options.json', '-Dlog4j2.configurationFile=config/log4j2-dev.xml', '-jar', 'libs/transaction-exclusion-api.jar', 'config/transaction-exclusion-app-docker.config.toml', ]
|
||||
volumes:
|
||||
- ../config/transaction-exclusion-api/transaction-exclusion-app-docker.config.toml:/opt/consensys/linea/transaction-exclusion-api/config/transaction-exclusion-app-docker.config.toml:ro
|
||||
- ../config/transaction-exclusion-api/vertx-options.json:/opt/consensys/linea/transaction-exclusion-api/config/vertx-options.json:ro
|
||||
- ../config/transaction-exclusion-api/log4j2-dev.xml:/opt/consensys/linea/transaction-exclusion-api/config/log4j2-dev.xml:ro
|
||||
- local-dev:/data/
|
||||
networks:
|
||||
linea:
|
||||
ipv4_address: 11.11.11.110
|
||||
|
||||
coordinator:
|
||||
hostname: coordinator
|
||||
container_name: coordinator
|
||||
@@ -330,7 +401,6 @@ services:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
PGDATA: /data/postgres
|
||||
POSTGRES_DB: ${POSTGRES_DB:-coordinator}
|
||||
command: postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||
# uncomment command below if you need to log and debug queries to PG
|
||||
# command:
|
||||
@@ -500,7 +570,6 @@ services:
|
||||
|
||||
volumes:
|
||||
- ./config/zkbesu-shomei/zkbesu-config.toml:/var/lib/besu/zkbesu-config.toml:ro
|
||||
- ./config/zkbesu-shomei/key:/var/lib/besu/key:ro
|
||||
- ./config/zkbesu-shomei/log4j.xml:/var/lib/besu/log4j.xml:ro
|
||||
- ./config/linea-local-dev-genesis-PoA-besu.json/:/var/lib/besu/genesis.json:ro
|
||||
- ../tmp/zkbesu-shomei/plugins:/opt/besu/plugins/
|
||||
|
||||
50
docker/config/l2-node-besu/l2-node-besu-config.toml
Normal file
50
docker/config/l2-node-besu/l2-node-besu-config.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
data-path="/opt/besu/data"
|
||||
host-whitelist=["*"]
|
||||
sync-mode="FULL"
|
||||
p2p-port=30303
|
||||
|
||||
min-gas-price=0
|
||||
|
||||
# engine
|
||||
engine-host-allowlist=["*"]
|
||||
engine-rpc-port=8550
|
||||
|
||||
# rpc
|
||||
rpc-http-enabled=true
|
||||
rpc-http-host="0.0.0.0"
|
||||
rpc-http-port=8545
|
||||
rpc-http-cors-origins=["*"]
|
||||
rpc-http-api=["ADMIN","DEBUG","NET","ETH","WEB3","PLUGINS","LINEA"]
|
||||
rpc-http-max-active-connections=200
|
||||
|
||||
# ws
|
||||
rpc-ws-enabled=true
|
||||
rpc-ws-host="0.0.0.0"
|
||||
rpc-ws-port=8546
|
||||
rpc-ws-api=["ADMIN","TXPOOL","WEB3","ETH","NET","PERM"]
|
||||
rpc-ws-max-active-connections=200
|
||||
|
||||
# graphql
|
||||
graphql-http-enabled=false
|
||||
|
||||
# metrics
|
||||
metrics-enabled=true
|
||||
metrics-host="0.0.0.0"
|
||||
metrics-port=9545
|
||||
|
||||
# database
|
||||
data-storage-format="BONSAI"
|
||||
|
||||
# plugins
|
||||
plugin-linea-module-limit-file-path="/var/lib/besu/traces-limits.toml"
|
||||
plugin-linea-deny-list-path="/var/lib/besu/deny-list.txt"
|
||||
plugin-linea-l1l2-bridge-contract="0xe537D669CA013d86EBeF1D64e40fC74CADC91987"
|
||||
plugin-linea-l1l2-bridge-topic="e856c2b8bd4eb0027ce32eeaf595c21b0b6b4644b326e5b7bd80a1cf8db72e6c"
|
||||
plugin-linea-tx-pool-profitability-check-p2p-enabled=false
|
||||
plugin-linea-tx-pool-profitability-check-api-enabled=false
|
||||
plugin-linea-tx-pool-simulation-check-api-enabled=true
|
||||
plugin-linea-tx-pool-simulation-check-p2p-enabled=false
|
||||
plugin-linea-estimate-gas-compatibility-mode-enabled=false
|
||||
|
||||
Xbonsai-limit-trie-logs-enabled=false
|
||||
bonsai-historical-block-limit=1024
|
||||
44
docker/config/l2-node-besu/log4j.xml
Normal file
44
docker/config/l2-node-besu/log4j.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="INFO" monitorInterval="2">
|
||||
<Properties>
|
||||
<Property name="root.log.level">WARN</Property>
|
||||
</Properties>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSSZZZ} | %t | %-5level | %c{1} | %msg %throwable%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<!-- edit the package name/level below to add more logging to specific classes -->
|
||||
<!-- no need to restart Besu as it will detect changes every 2s -->
|
||||
<Logger name="org.hyperledger.besu" level="WARN" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<!-- to avoid annoying message "INFO ... No sync target, waiting for peers. Current peers: 0" change to WARN-->
|
||||
<Logger name="org.hyperledger.besu.ethereum.eth.sync.fullsync.FullSyncTargetManager" level="INFO" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="org.hyperledger.besu.ethereum.blockcreation" level="INFO" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="org.hyperledger.besu.consensus.merge.blockcreation" level="INFO" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="org.hyperledger.besu.ethereum.api.jsonrpc" level="TRACE" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="io.opentelemetry" level="WARN" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="net.consensys.linea.sequencer.txselection.selectors" level="DEBUG">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Logger name="org.hyperledger.besu.ethereum.eth.transactions.TransactionPool" level="TRACE" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Root level="${sys:root.log.level}">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -2,4 +2,4 @@ CREATE DATABASE linea_coordinator;
|
||||
CREATE DATABASE postman_db;
|
||||
CREATE DATABASE l1_blockscout_db;
|
||||
CREATE DATABASE l2_blockscout_db;
|
||||
|
||||
CREATE DATABASE linea_transaction_exclusion;
|
||||
|
||||
306
e2e/src/abi/TestContract.json
Normal file
306
e2e/src/abi/TestContract.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
import assert from "assert";
|
||||
import { BaseContract, BlockTag, TransactionReceipt, Wallet, ethers } from "ethers";
|
||||
import { BaseContract, BlockTag, TransactionReceipt, TransactionRequest, Wallet, ethers } from "ethers";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { L2MessageService, LineaRollup } from "../typechain";
|
||||
@@ -72,6 +72,68 @@ export class RollupGetZkEVMBlockNumberClient {
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExclusionClient {
|
||||
private endpoint: URL;
|
||||
|
||||
public constructor(endpoint: URL) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public async getTransactionExclusionStatusV1(txHash: String): Promise<any> {
|
||||
const request = {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "linea_getTransactionExclusionStatusV1",
|
||||
params: [txHash],
|
||||
id: 1,
|
||||
}),
|
||||
};
|
||||
const response = await fetch(this.endpoint, request);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async saveRejectedTransactionV1(
|
||||
txRejectionStage: String,
|
||||
timestamp: String, // ISO-8601
|
||||
blockNumber: Number | null,
|
||||
transactionRLP: String,
|
||||
reasonMessage: String,
|
||||
overflows: { module: String; count: Number; limit: Number }[],
|
||||
): Promise<any> {
|
||||
let params: any = {
|
||||
txRejectionStage,
|
||||
timestamp,
|
||||
transactionRLP,
|
||||
reasonMessage,
|
||||
overflows,
|
||||
};
|
||||
if (blockNumber != null) {
|
||||
params = {
|
||||
...params,
|
||||
blockNumber,
|
||||
};
|
||||
}
|
||||
const request = {
|
||||
method: "post",
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "linea_saveRejectedTransactionV1",
|
||||
params: params,
|
||||
id: 1,
|
||||
}),
|
||||
};
|
||||
const response = await fetch(this.endpoint, request);
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTransactionHash(txRequest: TransactionRequest, signer: Wallet): Promise<string> {
|
||||
const rawTransaction = await signer.populateTransaction(txRequest);
|
||||
const signature = await signer.signTransaction(rawTransaction);
|
||||
return ethers.keccak256(signature);
|
||||
}
|
||||
|
||||
export async function getBlockByNumberOrBlockTag(rpcUrl: URL, blockTag: BlockTag): Promise<ethers.Block | null> {
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl.href);
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-var */
|
||||
import { config } from "../tests-config";
|
||||
import { deployContract } from "../../common/deployments";
|
||||
import { DummyContract__factory } from "../../typechain";
|
||||
import { DummyContract__factory, TestContract__factory } from "../../typechain";
|
||||
import { etherToWei, sendTransactionsToGenerateTrafficWithInterval } from "../../common/utils";
|
||||
|
||||
declare global {
|
||||
@@ -16,10 +16,12 @@ export default async (): Promise<void> => {
|
||||
deployContract(new DummyContract__factory(), account),
|
||||
deployContract(new DummyContract__factory(), l2Account),
|
||||
]);
|
||||
|
||||
console.log(`L1 Dummy contract deployed at address: ${await dummyContract.getAddress()}`);
|
||||
console.log(`L2 Dummy contract deployed at address: ${await l2DummyContract.getAddress()}`);
|
||||
|
||||
const l2TestContract = await deployContract(new TestContract__factory(), l2Account);
|
||||
console.log(`L2 Test contract deployed at address: ${await l2TestContract.getAddress()}`);
|
||||
|
||||
// Send ETH to the LineaRollup contract
|
||||
const lineaRollup = config.getLineaRollupContract(account);
|
||||
const l1JsonRpcProvider = config.getL1Provider();
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Config } from "../types";
|
||||
|
||||
const L1_RPC_URL = new URL("http://localhost:8445");
|
||||
const L2_RPC_URL = new URL("http://localhost:8845");
|
||||
const L2_BESU_NODE_RPC_URL = new URL("http://localhost:9045");
|
||||
const SHOMEI_ENDPOINT = new URL("http://localhost:8998");
|
||||
const SHOMEI_FRONTEND_ENDPOINT = new URL("http://localhost:8889");
|
||||
const SEQUENCER_ENDPOINT = new URL("http://localhost:8545");
|
||||
const TRANSACTION_EXCLUSION_ENDPOINT = new URL("http://localhost:8082");
|
||||
|
||||
const config: Config = {
|
||||
L1: {
|
||||
@@ -25,8 +27,10 @@ const config: Config = {
|
||||
},
|
||||
L2: {
|
||||
rpcUrl: L2_RPC_URL,
|
||||
besuNodeRpcUrl: L2_BESU_NODE_RPC_URL,
|
||||
chainId: 1337,
|
||||
l2MessageServiceAddress: "0xe537D669CA013d86EBeF1D64e40fC74CADC91987",
|
||||
l2TestContractAddress: "0xeB0b0a14F92e3BA35aEF3a2B6A24D7ED1D11631B",
|
||||
dummyContractAddress: "0x2f6dAaF8A81AB675fbD37Ca6Ed5b72cf86237453",
|
||||
accountManager: new GenesisBasedAccountManager(
|
||||
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),
|
||||
@@ -38,6 +42,7 @@ const config: Config = {
|
||||
shomeiEndpoint: SHOMEI_ENDPOINT,
|
||||
shomeiFrontendEndpoint: SHOMEI_FRONTEND_ENDPOINT,
|
||||
sequencerEndpoint: SEQUENCER_ENDPOINT,
|
||||
transactionExclusionEndpoint: TRANSACTION_EXCLUSION_ENDPOINT,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
L2MessageService__factory,
|
||||
LineaRollup,
|
||||
LineaRollup__factory,
|
||||
TestContract,
|
||||
TestContract__factory,
|
||||
} from "../../typechain";
|
||||
import { AccountManager } from "./accounts/account-manager";
|
||||
|
||||
@@ -21,6 +23,22 @@ export default class TestSetup {
|
||||
return new JsonRpcProvider(this.config.L2.rpcUrl.toString());
|
||||
}
|
||||
|
||||
public getL2SequencerProvider(): JsonRpcProvider | undefined {
|
||||
if (this.config.L2.sequencerEndpoint) {
|
||||
return new JsonRpcProvider(this.config.L2.sequencerEndpoint.toString());
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public getL2BesuNodeProvider(): JsonRpcProvider | undefined {
|
||||
if (this.config.L2.besuNodeRpcUrl) {
|
||||
return new JsonRpcProvider(this.config.L2.besuNodeRpcUrl.toString());
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public getL1ChainId(): number {
|
||||
return this.config.L1.chainId;
|
||||
}
|
||||
@@ -41,6 +59,10 @@ export default class TestSetup {
|
||||
return this.config.L2.sequencerEndpoint;
|
||||
}
|
||||
|
||||
public getTransactionExclusionEndpoint(): URL | undefined {
|
||||
return this.config.L2.transactionExclusionEndpoint;
|
||||
}
|
||||
|
||||
public getLineaRollupContract(signer?: Wallet): LineaRollup {
|
||||
const lineaRollup: LineaRollup = LineaRollup__factory.connect(
|
||||
this.config.L1.lineaRollupAddress,
|
||||
@@ -87,6 +109,20 @@ export default class TestSetup {
|
||||
return dummyContract;
|
||||
}
|
||||
|
||||
public getL2TestContract(signer?: Wallet): TestContract | undefined {
|
||||
if (this.config.L2.l2TestContractAddress) {
|
||||
const testContract = TestContract__factory.connect(this.config.L2.l2TestContractAddress, this.getL2Provider());
|
||||
|
||||
if (signer) {
|
||||
return testContract.connect(signer);
|
||||
}
|
||||
|
||||
return testContract;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public getL1AccountManager(): AccountManager {
|
||||
return this.config.L1.accountManager;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@ export type L1Config = BaseConfig & {
|
||||
|
||||
export type L2Config = BaseConfig & {
|
||||
l2MessageServiceAddress: string;
|
||||
l2TestContractAddress?: string;
|
||||
besuNodeRpcUrl?: URL;
|
||||
shomeiEndpoint?: URL;
|
||||
shomeiFrontendEndpoint?: URL;
|
||||
sequencerEndpoint?: URL;
|
||||
transactionExclusionEndpoint?: URL;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { JsonRpcProvider, ethers } from "ethers";
|
||||
import { beforeAll, describe, expect, it } from "@jest/globals";
|
||||
import { ethers } from "ethers";
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { config } from "./config/tests-config";
|
||||
import { RollupGetZkEVMBlockNumberClient, etherToWei } from "./common/utils";
|
||||
import { TRANSACTION_CALLDATA_LIMIT } from "./common/constants";
|
||||
@@ -7,12 +7,6 @@ import { TRANSACTION_CALLDATA_LIMIT } from "./common/constants";
|
||||
const l2AccountManager = config.getL2AccountManager();
|
||||
|
||||
describe("Layer 2 test suite", () => {
|
||||
let l2Provider: JsonRpcProvider;
|
||||
|
||||
beforeAll(() => {
|
||||
l2Provider = config.getL2Provider();
|
||||
});
|
||||
|
||||
it.concurrent("Should revert if transaction data size is above the limit", async () => {
|
||||
const account = await l2AccountManager.generateAccount();
|
||||
const dummyContract = config.getL2DummyContract(account);
|
||||
@@ -35,7 +29,7 @@ describe("Layer 2 test suite", () => {
|
||||
it.concurrent("Should successfully send a legacy transaction", async () => {
|
||||
const account = await l2AccountManager.generateAccount();
|
||||
|
||||
const { gasPrice } = await l2Provider.getFeeData();
|
||||
const { gasPrice } = await config.getL2Provider().getFeeData();
|
||||
|
||||
const receipt = await (
|
||||
await account.sendTransaction({
|
||||
@@ -54,7 +48,7 @@ describe("Layer 2 test suite", () => {
|
||||
it.concurrent("Should successfully send an EIP1559 transaction", async () => {
|
||||
const account = await l2AccountManager.generateAccount();
|
||||
|
||||
const { maxPriorityFeePerGas, maxFeePerGas } = await l2Provider.getFeeData();
|
||||
const { maxPriorityFeePerGas, maxFeePerGas } = await config.getL2Provider().getFeeData();
|
||||
|
||||
const receipt = await (
|
||||
await account.sendTransaction({
|
||||
@@ -74,7 +68,7 @@ describe("Layer 2 test suite", () => {
|
||||
it.concurrent("Should successfully send an access list transaction with empty access list", async () => {
|
||||
const account = await l2AccountManager.generateAccount();
|
||||
|
||||
const { gasPrice } = await l2Provider.getFeeData();
|
||||
const { gasPrice } = await config.getL2Provider().getFeeData();
|
||||
|
||||
const receipt = await (
|
||||
await account.sendTransaction({
|
||||
@@ -93,7 +87,7 @@ describe("Layer 2 test suite", () => {
|
||||
it.concurrent("Should successfully send an access list transaction with access list", async () => {
|
||||
const account = await l2AccountManager.generateAccount();
|
||||
|
||||
const { gasPrice } = await l2Provider.getFeeData();
|
||||
const { gasPrice } = await config.getL2Provider().getFeeData();
|
||||
const accessList = {
|
||||
"0x8D97689C9818892B700e27F316cc3E41e17fBeb9": [
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
@@ -131,7 +125,7 @@ describe("Layer 2 test suite", () => {
|
||||
const shomeiFrontendClient = new RollupGetZkEVMBlockNumberClient(shomeiFrontendEndpoint);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { maxPriorityFeePerGas, maxFeePerGas } = await l2Provider.getFeeData();
|
||||
const { maxPriorityFeePerGas, maxFeePerGas } = await config.getL2Provider().getFeeData();
|
||||
|
||||
await (
|
||||
await account.sendTransaction({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { JsonRpcProvider } from "ethers";
|
||||
import {
|
||||
getMessageSentEventFromLogs,
|
||||
sendMessage,
|
||||
@@ -12,12 +11,6 @@ import { config } from "./config/tests-config";
|
||||
import { LineaRollup } from "./typechain";
|
||||
|
||||
describe("Submission and finalization test suite", () => {
|
||||
let l1Provider: JsonRpcProvider;
|
||||
|
||||
beforeAll(() => {
|
||||
l1Provider = config.getL1Provider();
|
||||
});
|
||||
|
||||
const sendMessages = async () => {
|
||||
const messageFee = etherToWei("0.0001");
|
||||
const messageValue = etherToWei("0.0051");
|
||||
@@ -32,8 +25,8 @@ describe("Submission and finalization test suite", () => {
|
||||
const l1MessagesPromises = [];
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [l1MessageSenderNonce, { maxPriorityFeePerGas, maxFeePerGas }] = await Promise.all([
|
||||
l1Provider.getTransactionCount(l1MessageSender.address),
|
||||
l1Provider.getFeeData(),
|
||||
config.getL1Provider().getTransactionCount(l1MessageSender.address),
|
||||
config.getL1Provider().getFeeData(),
|
||||
]);
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
||||
74
e2e/src/transaction-exclusion.spec.ts
Normal file
74
e2e/src/transaction-exclusion.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
import { config } from "./config/tests-config";
|
||||
import { etherToWei, getTransactionHash, getWallet, TransactionExclusionClient, wait } from "./common/utils";
|
||||
import { TransactionRequest } from "ethers";
|
||||
|
||||
const l2AccountManager = config.getL2AccountManager();
|
||||
|
||||
describe("Transaction exclusion test suite", () => {
|
||||
it.concurrent(
|
||||
"Should get the status of the rejected transaction reported from Besu RPC node",
|
||||
async () => {
|
||||
expect(config.getTransactionExclusionEndpoint()).toBeDefined();
|
||||
|
||||
const transactionExclusionClient = new TransactionExclusionClient(config.getTransactionExclusionEndpoint()!!);
|
||||
const l2Account = await l2AccountManager.generateAccount();
|
||||
const l2AccountLocal = getWallet(l2Account.privateKey, config.getL2BesuNodeProvider()!!);
|
||||
const testContract = config.getL2TestContract(l2AccountLocal)!!;
|
||||
|
||||
// This shall be rejected by the Besu node due to traces module limit overflow
|
||||
let rejectedTxHash;
|
||||
try {
|
||||
const txRequest: TransactionRequest = {
|
||||
to: await testContract.getAddress(),
|
||||
data: testContract.interface.encodeFunctionData("testAddmod", [13000, 31]),
|
||||
maxPriorityFeePerGas: etherToWei("0.000000001"), // 1 Gwei
|
||||
maxFeePerGas: etherToWei("0.00000001"), // 10 Gwei
|
||||
};
|
||||
rejectedTxHash = await getTransactionHash(txRequest, l2AccountLocal);
|
||||
await l2AccountLocal.sendTransaction(txRequest);
|
||||
} catch (err) {
|
||||
// This shall return error with traces limit overflow
|
||||
console.debug(`sendTransaction expected err: ${JSON.stringify(err)}`);
|
||||
}
|
||||
|
||||
expect(rejectedTxHash).toBeDefined();
|
||||
console.log(`rejectedTxHash (RPC): ${rejectedTxHash}`);
|
||||
|
||||
let getResponse;
|
||||
do {
|
||||
await wait(1_000);
|
||||
getResponse = await transactionExclusionClient.getTransactionExclusionStatusV1(rejectedTxHash!);
|
||||
} while (!getResponse?.result);
|
||||
|
||||
expect(getResponse.result.txHash).toStrictEqual(rejectedTxHash);
|
||||
expect(getResponse.result.txRejectionStage).toStrictEqual("RPC");
|
||||
expect(getResponse.result.from.toLowerCase()).toStrictEqual(l2AccountLocal.address.toLowerCase());
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
it.skip("Should get the status of the rejected transaction reported from Besu SEQUENCER node", async () => {
|
||||
expect(config.getTransactionExclusionEndpoint()).toBeDefined();
|
||||
|
||||
const transactionExclusionClient = new TransactionExclusionClient(config.getTransactionExclusionEndpoint()!!);
|
||||
const l2Account = await l2AccountManager.generateAccount();
|
||||
const l2AccountLocal = getWallet(l2Account.privateKey, config.getL2SequencerProvider()!!);
|
||||
const testContract = config.getL2TestContract(l2AccountLocal);
|
||||
|
||||
// This shall be rejected by sequencer due to traces module limit overflow
|
||||
const tx = await testContract!!.connect(l2AccountLocal).testAddmod(13000, 31);
|
||||
const rejectedTxHash = tx.hash;
|
||||
console.log(`rejectedTxHash (SEQUENCER): ${rejectedTxHash}`);
|
||||
|
||||
let getResponse;
|
||||
do {
|
||||
await wait(1_000);
|
||||
getResponse = await transactionExclusionClient.getTransactionExclusionStatusV1(rejectedTxHash);
|
||||
} while (!getResponse?.result);
|
||||
|
||||
expect(getResponse.result.txHash).toStrictEqual(rejectedTxHash);
|
||||
expect(getResponse.result.txRejectionStage).toStrictEqual("SEQUENCER");
|
||||
expect(getResponse.result.from.toLowerCase()).toStrictEqual(l2AccountLocal.address.toLowerCase());
|
||||
}, 120_000);
|
||||
});
|
||||
@@ -4,8 +4,9 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":coordinator:core"))
|
||||
api("io.vertx:vertx-pg-client")
|
||||
implementation(project(":jvm-libs:generic:extensions:futures"))
|
||||
implementation(project(":jvm-libs:generic:extensions:kotlin"))
|
||||
api("io.vertx:vertx-pg-client:${libs.versions.vertx.get()}")
|
||||
api("com.ongres.scram:common:2.1") {
|
||||
because("Vertx pg client fails without it")
|
||||
}
|
||||
@@ -19,8 +20,9 @@ dependencies {
|
||||
}
|
||||
|
||||
testImplementation testFixtures(project(':jvm-libs:generic:extensions:kotlin'))
|
||||
testFixturesApi(platform("org.junit:junit-bom:${libs.versions.junit.get()}"))
|
||||
testFixturesApi("io.vertx:vertx-junit5:${libs.versions.vertx.get()}")
|
||||
|
||||
testFixturesImplementation(platform("org.junit:junit-bom:${libs.versions.junit.get()}"))
|
||||
testFixturesImplementation("io.vertx:vertx-junit5:${libs.versions.vertx.get()}")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -27,7 +27,8 @@ object Db {
|
||||
database: String,
|
||||
target: String,
|
||||
username: String,
|
||||
password: String
|
||||
password: String,
|
||||
migrationLocations: String = "classpath:db/"
|
||||
) {
|
||||
val dataSource =
|
||||
PGSimpleDataSource().apply {
|
||||
@@ -37,7 +38,7 @@ object Db {
|
||||
this.password = password
|
||||
this.databaseName = database
|
||||
}
|
||||
applyDbMigrations(dataSource, target)
|
||||
applyDbMigrations(dataSource, target, migrationLocations)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,11 +46,15 @@ object Db {
|
||||
*
|
||||
* @param dataSource datasource
|
||||
*/
|
||||
fun applyDbMigrations(dataSource: DataSource, target: String) {
|
||||
fun applyDbMigrations(
|
||||
dataSource: DataSource,
|
||||
target: String,
|
||||
migrationLocations: String = "classpath:db/"
|
||||
) {
|
||||
LOG.info("Migrating coordinator database")
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("classpath:db/")
|
||||
.locations(migrationLocations)
|
||||
.baselineDescription("Migration baseline from legacy database generated ${Instant.now()}")
|
||||
.table("schema_version")
|
||||
.target(target)
|
||||
@@ -12,7 +12,7 @@ object DbHelper {
|
||||
private var formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
|
||||
|
||||
fun generateUniqueDbName(
|
||||
prefix: String = "coordinator_test",
|
||||
prefix: String = "test",
|
||||
clock: Clock = Clock.systemUTC()
|
||||
): String {
|
||||
// Just time is not enough, as we can have multiple tests running in parallel
|
||||
@@ -20,8 +20,21 @@ open class PersistenceRetryer(
|
||||
|
||||
fun <T> retryQuery(
|
||||
action: () -> SafeFuture<T>,
|
||||
stopRetriesOnErrorPredicate: (Throwable) -> Boolean = ::stopRetriesOnErrorPredicate
|
||||
|
||||
stopRetriesOnErrorPredicate: (Throwable) -> Boolean = Companion::stopRetriesOnErrorPredicate,
|
||||
exceptionConsumer: (Throwable) -> Unit = { error ->
|
||||
when {
|
||||
isDuplicateKeyException(error) -> log.info(
|
||||
"Persistence errorMessage={}",
|
||||
error.message
|
||||
)
|
||||
else -> log.info(
|
||||
"Persistence errorMessage={}, it will retry again in {}",
|
||||
error.message,
|
||||
config.backoffDelay,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
): SafeFuture<T> {
|
||||
return AsyncRetryer.retry(
|
||||
vertx = vertx,
|
||||
@@ -29,14 +42,7 @@ open class PersistenceRetryer(
|
||||
backoffDelay = config.backoffDelay,
|
||||
maxRetries = config.maxRetries,
|
||||
stopRetriesOnErrorPredicate = stopRetriesOnErrorPredicate,
|
||||
exceptionConsumer = { error ->
|
||||
log.info(
|
||||
"Persistence errorMessage={}, it will retry again in {}",
|
||||
error.message,
|
||||
config.backoffDelay,
|
||||
error
|
||||
)
|
||||
},
|
||||
exceptionConsumer = exceptionConsumer,
|
||||
action = action
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.consensys.zkevm.persistence.test
|
||||
package net.consensys.zkevm.persistence.db.test
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.sqlclient.Pool
|
||||
@@ -15,13 +15,14 @@ import javax.sql.DataSource
|
||||
abstract class CleanDbTestSuiteParallel {
|
||||
private val host = "localhost"
|
||||
private val port = 5432
|
||||
abstract val databaseName: String
|
||||
private val username = "postgres"
|
||||
private val password = "postgres"
|
||||
abstract val databaseName: String
|
||||
private lateinit var dataSource: DataSource
|
||||
lateinit var pool: Pool
|
||||
lateinit var sqlClient: SqlClient
|
||||
val target: String = "4"
|
||||
var target: String = "1"
|
||||
var migrationLocations: String = "classpath:db/"
|
||||
|
||||
private fun createDataSource(databaseName: String): DataSource {
|
||||
return PGSimpleDataSource().also {
|
||||
@@ -43,7 +44,7 @@ abstract class CleanDbTestSuiteParallel {
|
||||
sqlClient = vertxSqlClient(vertx, host, port, databaseName, username, password)
|
||||
// drop flyway db metadata table as well
|
||||
// to recreate new db tables;
|
||||
Db.applyDbMigrations(dataSource, target)
|
||||
Db.applyDbMigrations(dataSource, target, migrationLocations)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -9,7 +9,8 @@ enum class LineaMetricsCategory {
|
||||
BATCH,
|
||||
BLOB,
|
||||
AGGREGATION,
|
||||
GAS_PRICE_CAP;
|
||||
GAS_PRICE_CAP,
|
||||
TX_EXCLUSION_API;
|
||||
|
||||
override fun toString(): String {
|
||||
return this.name.replace('_', '.').lowercase()
|
||||
|
||||
@@ -8,6 +8,7 @@ include 'jvm-libs:generic:logging'
|
||||
include 'jvm-libs:generic:vertx-helper'
|
||||
include 'jvm-libs:generic:extensions:futures'
|
||||
include 'jvm-libs:generic:errors'
|
||||
include 'jvm-libs:generic:persistence:db'
|
||||
|
||||
include 'jvm-libs:linea:core:domain-models'
|
||||
include 'jvm-libs:linea:core:metrics'
|
||||
@@ -41,11 +42,11 @@ include 'coordinator:ethereum:finalization-monitor'
|
||||
include 'coordinator:ethereum:blob-submitter'
|
||||
include 'coordinator:ethereum:message-anchoring'
|
||||
include 'coordinator:ethereum:test-utils'
|
||||
include 'coordinator:persistence:db'
|
||||
include 'coordinator:persistence:blob'
|
||||
include 'coordinator:persistence:aggregation'
|
||||
include 'coordinator:persistence:batch'
|
||||
include 'coordinator:persistence:feehistory'
|
||||
include 'coordinator:persistence:db-common'
|
||||
include 'coordinator:ethereum:gas-pricing'
|
||||
include 'coordinator:ethereum:gas-pricing:static-cap'
|
||||
include 'coordinator:ethereum:gas-pricing:dynamic-cap'
|
||||
@@ -56,3 +57,6 @@ include 'traces-api-facade:app'
|
||||
include 'traces-api-facade:core'
|
||||
include 'traces-api-facade:conflation'
|
||||
include 'transaction-decoder-tool'
|
||||
include 'transaction-exclusion-api:app'
|
||||
include 'transaction-exclusion-api:core'
|
||||
include 'transaction-exclusion-api:persistence:rejectedtransaction'
|
||||
|
||||
33
transaction-exclusion-api/Dockerfile
Normal file
33
transaction-exclusion-api/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# BUILDER image
|
||||
FROM eclipse-temurin:21-jre-alpine AS builder
|
||||
WORKDIR /opt/consensys/linea/transaction-exclusion-api
|
||||
|
||||
RUN apk add --no-cache unzip
|
||||
|
||||
# copy application
|
||||
COPY --from=zip ./transaction-exclusion-api.zip libs/
|
||||
RUN unzip libs/transaction-exclusion-api.zip -d libs/ \
|
||||
&& mv libs/transaction-exclusion-api/lib/** libs/ \
|
||||
&& rm -R libs/transaction-exclusion-api/
|
||||
|
||||
# FINAL image
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /opt/consensys/linea/transaction-exclusion-api/
|
||||
|
||||
RUN mkdir -p /opt/consensys/linea/transaction-exclusion-api/logs/
|
||||
|
||||
COPY --from=builder /opt/consensys/linea/transaction-exclusion-api/libs libs/
|
||||
|
||||
# Build-time metadata as defined at http://label-schema.org
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
LABEL org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.name="transaction-exclusion-api" \
|
||||
org.label-schema.description="Linea Transaction Exclusion API" \
|
||||
org.label-schema.url="https://consensys.io/" \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vcs-url="https://github.com/ConsenSys/linea-monorepo" \
|
||||
org.label-schema.vendor="ConsenSys" \
|
||||
org.label-schema.version=$VERSION \
|
||||
org.label-schema.schema-version="1.0"
|
||||
163
transaction-exclusion-api/README.md
Normal file
163
transaction-exclusion-api/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Transaction Exclusion API Service
|
||||
This micro-service will receive the transactions rejected by sequencer or other Linea Besu nodes,
|
||||
persist them into a local database and expose a JSON-RPC v2 API to allow Linea users to query
|
||||
why their transactions were not included.
|
||||
|
||||
## V1 API Methods
|
||||
### linea_saveRejectedTransactionV1
|
||||
```bash
|
||||
curl -H 'content-type:application/json' --data '{
|
||||
"id": "1",
|
||||
"jsonrpc": "2.0",
|
||||
"method": "linea_saveRejectedTransactionV1",
|
||||
"params": {
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"timestamp": "2024-08-22T09:18:51Z",
|
||||
"blockNumber": 12345,
|
||||
"transactionRLP": "0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c496d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8",
|
||||
"reasonMessage": "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
|
||||
}
|
||||
]
|
||||
}
|
||||
}' http://127.0.0.1:8082
|
||||
```
|
||||
|
||||
|
||||
### linea_getTransactionExclusionStatusV1
|
||||
```bash
|
||||
curl -H 'content-type:application/json' --data '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "53",
|
||||
"method": "linea_getTransactionExclusionStatusV1",
|
||||
"params": [
|
||||
"0xf5bf951edfefbaa6d9ed78c88942147cf98c8ef1f3d3416f99d2534675096569"
|
||||
]
|
||||
}' http://127.0.0.1:8082
|
||||
```
|
||||
|
||||
|
||||
### linea_saveRejectedTransactionV1 Response Examples:
|
||||
The rejected transaction was successfully saved:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"status": "SAVED",
|
||||
"txHash": "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"
|
||||
}
|
||||
}
|
||||
```
|
||||
The rejected transaction with the same tx hash and reason message was saved before:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"status": "DUPLICATE_ALREADY_SAVED_BEFORE",
|
||||
"txHash": "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"
|
||||
}
|
||||
}
|
||||
```
|
||||
Invalid request params errors:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Missing [timestamp] from the given request params"
|
||||
}
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Timestamp is not in ISO-8601: Text '2024-09-08T09:23:56Zdd' could not be parsed, unparsed text found at index 20"
|
||||
}
|
||||
}
|
||||
```
|
||||
Other error:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Server error",
|
||||
"data": "Database connection refused: localhost/127.0.0.1:5432"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### linea_getTransactionExclusionStatusV1 Response Examples:
|
||||
The rejected transaction was successfully found:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"result": {
|
||||
"txHash": "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7",
|
||||
"from": "0x4d144d7b9c96b26361d6ac74dd1d8267edca4fc2",
|
||||
"nonce": "0x64",
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70",
|
||||
"blockNumber": "0x3039",
|
||||
"timestamp": "2024-08-22T09:18:51Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
The rejected transaction was not found:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"result": null
|
||||
}
|
||||
```
|
||||
Invalid request params errors:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Hex string of transaction hash cannot be parsed: expected to have 32 bytes, but got 33"
|
||||
}
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Hex string of transaction hash cannot be parsed: For input string: \"tt\" under radix 16"
|
||||
}
|
||||
}
|
||||
```
|
||||
Other error:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 53,
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": "Server error",
|
||||
"data": "Database connection refused: localhost/127.0.0.1:5432"
|
||||
}
|
||||
}
|
||||
```
|
||||
112
transaction-exclusion-api/app/build.gradle
Normal file
112
transaction-exclusion-api/app/build.gradle
Normal file
@@ -0,0 +1,112 @@
|
||||
plugins {
|
||||
id 'net.consensys.zkevm.kotlin-application-conventions'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':transaction-exclusion-api:core')
|
||||
implementation project(':transaction-exclusion-api:persistence:rejectedtransaction')
|
||||
implementation project(':jvm-libs:generic:extensions:futures')
|
||||
implementation project(':jvm-libs:generic:extensions:kotlin')
|
||||
implementation project(':jvm-libs:generic:json-rpc')
|
||||
implementation project(':jvm-libs:generic:persistence:db')
|
||||
implementation project(':jvm-libs:generic:vertx-helper')
|
||||
implementation project(':jvm-libs:linea:core:long-running-service')
|
||||
implementation project(':jvm-libs:linea:core:metrics')
|
||||
implementation project(':jvm-libs:linea:metrics:micrometer')
|
||||
|
||||
implementation "com.github.ben-manes.caffeine:caffeine:${libs.versions.caffeine.get()}"
|
||||
implementation "io.vertx:vertx-core"
|
||||
implementation "io.vertx:vertx-web"
|
||||
implementation "io.vertx:vertx-health-check"
|
||||
implementation "io.vertx:vertx-lang-kotlin"
|
||||
implementation "io.vertx:vertx-config"
|
||||
implementation "io.vertx:vertx-micrometer-metrics"
|
||||
implementation "info.picocli:picocli:${libs.versions.picoli.get()}"
|
||||
implementation "com.sksamuel.hoplite:hoplite-core:${libs.versions.hoplite.get()}"
|
||||
implementation "com.sksamuel.hoplite:hoplite-toml:${libs.versions.hoplite.get()}"
|
||||
implementation "io.micrometer:micrometer-registry-prometheus:${libs.versions.micrometer.get()}"
|
||||
implementation "com.fasterxml.jackson.core:jackson-annotations:${libs.versions.jackson.get()}"
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}"
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:${libs.versions.jackson.get()}"
|
||||
|
||||
implementation "org.hyperledger.besu:besu-datatypes:${libs.versions.besu.get()}"
|
||||
implementation "org.hyperledger.besu:evm:${libs.versions.besu.get()}"
|
||||
implementation "org.hyperledger.besu.internal:rlp:${libs.versions.besu.get()}"
|
||||
implementation "org.hyperledger.besu.internal:core:${libs.versions.besu.get()}"
|
||||
implementation "org.hyperledger.besu.internal:crypto:${libs.versions.besu.get()}"
|
||||
|
||||
api("io.netty:netty-transport-native-epoll:${libs.versions.netty.get()}:linux-x86_64") {
|
||||
because "It enables native transport for Linux."
|
||||
// Note that its version should match netty version used in Vertx
|
||||
}
|
||||
api("io.netty:netty-transport-native-kqueue:${libs.versions.netty.get()}:osx-x86_64") {
|
||||
because "It enables native transport for Mac OSX."
|
||||
// Note that its version should match netty version used in Vertx
|
||||
}
|
||||
|
||||
testImplementation "io.vertx:vertx-junit5"
|
||||
testImplementation "io.rest-assured:rest-assured:${libs.versions.restassured.get()}"
|
||||
testImplementation "io.rest-assured:json-schema-validator:${libs.versions.restassured.get()}"
|
||||
testImplementation "net.javacrumbs.json-unit:json-unit-assertj:${libs.versions.jsonUnit.get()}"
|
||||
testImplementation testFixtures(project(":jvm-libs:generic:extensions:kotlin"))
|
||||
testImplementation testFixtures(project(":jvm-libs:generic:persistence:db"))
|
||||
testImplementation testFixtures(project(":transaction-exclusion-api:core"))
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'net.consensys.linea.transactionexclusion.app.TransactionExclusionAppMain'
|
||||
}
|
||||
|
||||
jar {
|
||||
archiveBaseName = 'transaction-exclusion-api'
|
||||
manifest {
|
||||
attributes(
|
||||
'Class-Path': configurations.runtimeClasspath.collect { it.getName() }.findAll {it.endsWith('jar') }.join(' '),
|
||||
'Main-Class': 'net.consensys.linea.transactionexclusion.app.TransactionExclusionAppMain',
|
||||
'Multi-Release': 'true'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
distributions {
|
||||
main {
|
||||
distributionBaseName = 'transaction-exclusion-api'
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
workingDir = rootProject.projectDir
|
||||
jvmArgs = [
|
||||
"-Dvertx.configurationFile=config/transaction-exclusion-api/vertx.json",
|
||||
"-Dlog4j2.configurationFile=config/transaction-exclusion-api/log4j2-dev.xml"
|
||||
] + System.properties.entrySet()
|
||||
.findAll { it.key.startsWith("config") }
|
||||
.collect { "-D${it.key}=${it.value}" }
|
||||
args = ["config/transaction-exclusion-api/transaction-exclusion-app-docker.config.toml",
|
||||
"config/transaction-exclusion-api/transaction-exclusion-app-local-dev.config.overrides.toml"]
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + sourceSets.test.compileClasspath
|
||||
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
test ->
|
||||
systemProperty "vertx.configurationFile", "vertx-options.json"
|
||||
|
||||
description = "Runs integration tests."
|
||||
group = "verification"
|
||||
useJUnitPlatform()
|
||||
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
|
||||
dependsOn(":localStackComposeUp")
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package net.consensys.linea.transactionexclusion
|
||||
|
||||
import com.sksamuel.hoplite.Masked
|
||||
import io.restassured.RestAssured
|
||||
import io.restassured.builder.RequestSpecBuilder
|
||||
import io.restassured.http.ContentType
|
||||
import io.restassured.specification.RequestSpecification
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import kotlinx.datetime.Clock
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.async.get
|
||||
import net.consensys.linea.transactionexclusion.app.AppConfig
|
||||
import net.consensys.linea.transactionexclusion.app.DatabaseConfig
|
||||
import net.consensys.linea.transactionexclusion.app.DbCleanupConfig
|
||||
import net.consensys.linea.transactionexclusion.app.DbConnectionConfig
|
||||
import net.consensys.linea.transactionexclusion.app.PersistenceRetryConfig
|
||||
import net.consensys.linea.transactionexclusion.app.TransactionExclusionApp
|
||||
import net.consensys.linea.transactionexclusion.app.api.ApiConfig
|
||||
import net.consensys.trimToMillisecondPrecision
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
|
||||
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 java.time.Duration
|
||||
import kotlin.random.Random
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class TransactionExclusionAppTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "1"
|
||||
}
|
||||
|
||||
override var databaseName = DbHelper.generateUniqueDbName("tx-exclusion-api-app-tests")
|
||||
|
||||
private val dbConfig = DatabaseConfig(
|
||||
read = DbConnectionConfig(
|
||||
host = "localhost",
|
||||
port = 5432,
|
||||
username = "postgres",
|
||||
password = Masked("postgres")
|
||||
),
|
||||
write = DbConnectionConfig(
|
||||
host = "localhost",
|
||||
port = 5432,
|
||||
username = "postgres",
|
||||
password = Masked("postgres")
|
||||
),
|
||||
cleanup = DbCleanupConfig(
|
||||
pollingInterval = Duration.parse("PT60S"),
|
||||
storagePeriod = Duration.parse("P7D")
|
||||
),
|
||||
persistenceRetry = PersistenceRetryConfig(
|
||||
backoffDelay = Duration.parse("PT5S"),
|
||||
timeout = Duration.parse("PT20S")
|
||||
),
|
||||
schema = databaseName
|
||||
)
|
||||
|
||||
private lateinit var requestSpecification: RequestSpecification
|
||||
private lateinit var app: TransactionExclusionApp
|
||||
|
||||
@BeforeEach()
|
||||
fun beforeEach() {
|
||||
app = TransactionExclusionApp(
|
||||
config = AppConfig(
|
||||
api = ApiConfig(
|
||||
port = 0, // port will be assigned under os
|
||||
observabilityPort = 0, // port will be assigned under os
|
||||
numberOfVerticles = 1
|
||||
),
|
||||
database = dbConfig,
|
||||
dataQueryableWindowSinceRejectedTimestamp = Duration.parse("P7D")
|
||||
)
|
||||
)
|
||||
app.start().get()
|
||||
|
||||
requestSpecification = RequestSpecBuilder()
|
||||
.setBaseUri("http://localhost:${app.apiBindedPort}/")
|
||||
.build()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun afterEach() {
|
||||
app.stop().get()
|
||||
}
|
||||
|
||||
private fun makeRequestJsonResponse(request: String): String {
|
||||
return RestAssured.given()
|
||||
.spec(requestSpecification)
|
||||
.accept(ContentType.JSON)
|
||||
.body(request)
|
||||
.`when`()
|
||||
.post("/")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType("application/json")
|
||||
.extract()
|
||||
.asString()
|
||||
}
|
||||
|
||||
private fun saveFirstRejectedTransaction() {
|
||||
val saveTxJonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 123,
|
||||
"method": "linea_saveRejectedTransactionV1",
|
||||
"params": {
|
||||
"txRejectionStage": "P2P",
|
||||
"timestamp": "${Clock.System.now().trimToMillisecondPrecision()}",
|
||||
"transactionRLP": "0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c496d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70",
|
||||
"overflows": [
|
||||
{ "module": "ADD", "count": 402, "limit": 70 },
|
||||
{ "module": "MUL", "count": 587, "limit": 401 }
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Check the save response and ensure the rejected txn was saved
|
||||
assertThatJson(makeRequestJsonResponse(saveTxJonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 123,
|
||||
"result": {"status":"SAVED","txHash":"0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"}
|
||||
}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should save the rejected tx from P2P and then from SEQUENCER with same txHash but different reason message`() {
|
||||
// Save the first rejected tx from P2P without rejected block number
|
||||
saveFirstRejectedTransaction()
|
||||
|
||||
// Save the rejected tx from SEQUENCER with rejected block number and different
|
||||
// rejected reason and a more recent rejected timestamp
|
||||
val rejectionTimeStamp = Clock.System.now()
|
||||
.trimToMillisecondPrecision()
|
||||
.toString()
|
||||
|
||||
val saveTxJonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"method": "linea_saveRejectedTransactionV1",
|
||||
"params": [{
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"timestamp": "$rejectionTimeStamp",
|
||||
"transactionRLP": "0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c496d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8",
|
||||
"blockNumber": "10000",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70 (from Sequencer)",
|
||||
"overflows": [
|
||||
{ "module": "ADD", "count": 402, "limit": 70 },
|
||||
{ "module": "MUL", "count": 587, "limit": 401 }
|
||||
]
|
||||
}]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Check the save response and ensure the rejected txn was saved
|
||||
assertThatJson(makeRequestJsonResponse(saveTxJonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"result": {"status":"SAVED","txHash":"0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"}
|
||||
}"""
|
||||
)
|
||||
|
||||
// Send the get request for the rejected transaction
|
||||
val getTxJsonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 125,
|
||||
"method": "linea_getTransactionExclusionStatusV1",
|
||||
"params": ["0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Check the get response is corresponding to the rejected txn from SEQUENCER
|
||||
assertThatJson(makeRequestJsonResponse(getTxJsonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 125,
|
||||
"result": {
|
||||
"txHash": "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7",
|
||||
"from": "0x4d144d7b9c96b26361d6ac74dd1d8267edca4fc2",
|
||||
"nonce": "0x64",
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70 (from Sequencer)",
|
||||
"timestamp": "$rejectionTimeStamp",
|
||||
"blockNumber": "0x2710"
|
||||
}
|
||||
}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should return DUPLICATE_ALREADY_SAVED_BEFORE when saving rejected tx with same txHash and reason message`() {
|
||||
// Save the first rejected tx from P2P without rejected block number
|
||||
saveFirstRejectedTransaction()
|
||||
|
||||
// Save the same rejected tx from SEQUENCER with rejected block number and a more recent rejected timestamp
|
||||
val rejectionTimeStamp = Clock.System.now()
|
||||
.trimToMillisecondPrecision()
|
||||
.toString()
|
||||
|
||||
val saveTxJonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"method": "linea_saveRejectedTransactionV1",
|
||||
"params": [{
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"timestamp": "$rejectionTimeStamp",
|
||||
"transactionRLP": "0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c496d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8",
|
||||
"blockNumber": "10000",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70",
|
||||
"overflows": [
|
||||
{ "module": "ADD", "count": 402, "limit": 70 },
|
||||
{ "module": "MUL", "count": 587, "limit": 401 }
|
||||
]
|
||||
}]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Check the save response and ensure the status is "duplicated already saved before"
|
||||
assertThatJson(makeRequestJsonResponse(saveTxJonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"result": {"status":"DUPLICATE_ALREADY_SAVED_BEFORE","txHash":"0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"}
|
||||
}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should return result as null when getting the rejected tx with random transaction hash`() {
|
||||
// Save the first rejected tx from P2P without rejected block number
|
||||
saveFirstRejectedTransaction()
|
||||
|
||||
// Send the get request with a random txn hash
|
||||
val getTxJsonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"method": "linea_getTransactionExclusionStatusV1",
|
||||
"params": ["${Random.nextBytes(32).encodeHex()}"]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// Check the get response and ensure the result is null
|
||||
assertThatJson(makeRequestJsonResponse(getTxJsonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 124,
|
||||
"result": null
|
||||
}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when transaction request is invalid shall return error`() {
|
||||
val saveTxJonRequest = """{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 123,
|
||||
"method": "linea_saveRejectedTransactionV1",
|
||||
"params": [{
|
||||
"txRejectionStage": "SEQUENCER",
|
||||
"transactionRLP": "0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c496d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8",
|
||||
"blockNumber": "10000",
|
||||
"reasonMessage": "Transaction line count for module ADD=402 is above the limit 70"
|
||||
}]
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
assertThatJson(makeRequestJsonResponse(saveTxJonRequest))
|
||||
.isEqualTo(
|
||||
"""{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 123,
|
||||
"error": {
|
||||
"code": -32602,
|
||||
"message": "Missing [timestamp,overflows] from the given request params"
|
||||
}
|
||||
}"""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="net.consensys.linea.transactionexclusion" level="trace" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Root level="info" additivity="false">
|
||||
<appender-ref ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"metricsOptions": {
|
||||
"enabled": true,
|
||||
"jvmMetricsEnabled": true,
|
||||
"prometheusOptions": {
|
||||
"publishQuantiles": true,
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package net.consensys.linea.transactionexclusion.app
|
||||
|
||||
import com.sksamuel.hoplite.Masked
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
import io.vertx.core.Future
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.micrometer.backends.BackendRegistries
|
||||
import io.vertx.sqlclient.SqlClient
|
||||
import net.consensys.linea.async.toSafeFuture
|
||||
import net.consensys.linea.async.toVertxFuture
|
||||
import net.consensys.linea.metrics.micrometer.MicrometerMetricsFacade
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.transactionexclusion.app.api.Api
|
||||
import net.consensys.linea.transactionexclusion.app.api.ApiConfig
|
||||
import net.consensys.linea.transactionexclusion.service.RejectedTransactionCleanupService
|
||||
import net.consensys.linea.transactionexclusion.service.TransactionExclusionServiceV1Impl
|
||||
import net.consensys.linea.vertx.loadVertxConfig
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsDao
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsPostgresDao
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RetryingRejectedTransactionsPostgresDao
|
||||
import net.consensys.zkevm.persistence.db.Db
|
||||
import net.consensys.zkevm.persistence.db.PersistenceRetryer
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import java.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
import kotlin.time.toKotlinDuration
|
||||
|
||||
data class DbConnectionConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val username: String,
|
||||
val password: Masked
|
||||
)
|
||||
|
||||
data class DbCleanupConfig(
|
||||
val pollingInterval: Duration,
|
||||
val storagePeriod: Duration
|
||||
)
|
||||
|
||||
data class DatabaseConfig(
|
||||
val read: DbConnectionConfig,
|
||||
val write: DbConnectionConfig,
|
||||
val cleanup: DbCleanupConfig,
|
||||
val persistenceRetry: PersistenceRetryConfig,
|
||||
val schema: String = "linea_transaction_exclusion",
|
||||
val readPoolSize: Int = 10,
|
||||
val readPipeliningLimit: Int = 10,
|
||||
val transactionalPoolSize: Int = 10
|
||||
)
|
||||
|
||||
data class AppConfig(
|
||||
val api: ApiConfig,
|
||||
val database: DatabaseConfig,
|
||||
val dataQueryableWindowSinceRejectedTimestamp: Duration
|
||||
)
|
||||
|
||||
data class PersistenceRetryConfig(
|
||||
val maxRetries: Int? = null,
|
||||
val backoffDelay: Duration = 1.seconds.toJavaDuration(),
|
||||
val timeout: Duration? = 20.seconds.toJavaDuration()
|
||||
)
|
||||
|
||||
class TransactionExclusionApp(config: AppConfig) {
|
||||
private val log = LogManager.getLogger(TransactionExclusionApp::class.java)
|
||||
private val meterRegistry: MeterRegistry
|
||||
private val vertx: Vertx
|
||||
private val api: Api
|
||||
private val sqlReadClient: SqlClient
|
||||
private val sqlWriteClient: SqlClient
|
||||
private val rejectedTransactionsRepository: RejectedTransactionsDao
|
||||
private val transactionExclusionService: TransactionExclusionServiceV1
|
||||
private val rejectedTransactionCleanupService: RejectedTransactionCleanupService
|
||||
private val micrometerMetricsFacade: MicrometerMetricsFacade
|
||||
val apiBindedPort: Int
|
||||
get() = api.bindedPort
|
||||
|
||||
init {
|
||||
log.debug("System properties: {}", System.getProperties())
|
||||
val vertxConfig = loadVertxConfig()
|
||||
log.debug("Vertx full configs: {}", vertxConfig)
|
||||
log.info("App configs: {}", config)
|
||||
this.vertx = Vertx.vertx(vertxConfig)
|
||||
this.meterRegistry = BackendRegistries.getDefaultNow()
|
||||
this.micrometerMetricsFacade = MicrometerMetricsFacade(meterRegistry, "linea")
|
||||
this.sqlReadClient = initDb(
|
||||
connectionConfig = config.database.read,
|
||||
schema = config.database.schema,
|
||||
transactionalPoolSize = config.database.transactionalPoolSize,
|
||||
readPipeliningLimit = config.database.readPipeliningLimit,
|
||||
skipMigration = true
|
||||
)
|
||||
this.sqlWriteClient = initDb(
|
||||
connectionConfig = config.database.write,
|
||||
schema = config.database.schema,
|
||||
transactionalPoolSize = config.database.transactionalPoolSize,
|
||||
readPipeliningLimit = config.database.readPipeliningLimit
|
||||
)
|
||||
this.rejectedTransactionsRepository = RetryingRejectedTransactionsPostgresDao(
|
||||
delegate = RejectedTransactionsPostgresDao(
|
||||
readConnection = this.sqlReadClient,
|
||||
writeConnection = this.sqlWriteClient
|
||||
),
|
||||
persistenceRetryer = PersistenceRetryer(
|
||||
vertx = vertx,
|
||||
config = PersistenceRetryer.Config(
|
||||
backoffDelay = config.database.persistenceRetry.backoffDelay.toKotlinDuration(),
|
||||
maxRetries = config.database.persistenceRetry.maxRetries,
|
||||
timeout = config.database.persistenceRetry.timeout?.toKotlinDuration()
|
||||
)
|
||||
)
|
||||
)
|
||||
this.transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = TransactionExclusionServiceV1Impl.Config(
|
||||
config.dataQueryableWindowSinceRejectedTimestamp.toKotlinDuration()
|
||||
),
|
||||
repository = this.rejectedTransactionsRepository,
|
||||
metricsFacade = this.micrometerMetricsFacade
|
||||
)
|
||||
this.rejectedTransactionCleanupService = RejectedTransactionCleanupService(
|
||||
config = RejectedTransactionCleanupService.Config(
|
||||
pollingInterval = config.database.cleanup.pollingInterval.toKotlinDuration(),
|
||||
storagePeriod = config.database.cleanup.storagePeriod.toKotlinDuration()
|
||||
),
|
||||
repository = this.rejectedTransactionsRepository,
|
||||
vertx = this.vertx
|
||||
)
|
||||
this.api =
|
||||
Api(
|
||||
configs = config.api,
|
||||
vertx = vertx,
|
||||
meterRegistry = meterRegistry,
|
||||
transactionExclusionService = transactionExclusionService
|
||||
)
|
||||
}
|
||||
|
||||
fun start(): Future<*> {
|
||||
log.info("Starting up app..")
|
||||
return api.start().toSafeFuture()
|
||||
.thenCompose { rejectedTransactionCleanupService.start() }
|
||||
.thenPeek {
|
||||
log.info("App successfully started")
|
||||
}.toVertxFuture()
|
||||
}
|
||||
|
||||
fun stop(): Future<*> {
|
||||
log.info("Shooting down app..")
|
||||
return api.stop().toSafeFuture()
|
||||
.thenCompose { rejectedTransactionCleanupService.stop() }
|
||||
.thenPeek {
|
||||
log.info("App successfully stopped")
|
||||
}.toVertxFuture()
|
||||
}
|
||||
|
||||
private fun initDb(
|
||||
connectionConfig: DbConnectionConfig,
|
||||
schema: String,
|
||||
transactionalPoolSize: Int,
|
||||
readPipeliningLimit: Int,
|
||||
skipMigration: Boolean = false
|
||||
): SqlClient {
|
||||
val dbVersion = "1"
|
||||
if (!skipMigration) {
|
||||
Db.applyDbMigrations(
|
||||
host = connectionConfig.host,
|
||||
port = connectionConfig.port,
|
||||
database = schema,
|
||||
target = dbVersion,
|
||||
username = connectionConfig.username,
|
||||
password = connectionConfig.password.value
|
||||
)
|
||||
}
|
||||
return Db.vertxSqlClient(
|
||||
vertx = vertx,
|
||||
host = connectionConfig.host,
|
||||
port = connectionConfig.port,
|
||||
database = schema,
|
||||
username = connectionConfig.username,
|
||||
password = connectionConfig.password.value,
|
||||
maxPoolSize = transactionalPoolSize,
|
||||
pipeliningLimit = readPipeliningLimit
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.consensys.linea.transactionexclusion.app
|
||||
|
||||
import com.sksamuel.hoplite.ConfigFailure
|
||||
import com.sksamuel.hoplite.ConfigLoaderBuilder
|
||||
import com.sksamuel.hoplite.addFileSource
|
||||
import com.sksamuel.hoplite.fp.Validated
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Parameters
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.nio.charset.Charset
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
@Command(
|
||||
name = TransactionExclusionAppCli.COMMAND_NAME,
|
||||
showDefaultValues = true,
|
||||
abbreviateSynopsis = true,
|
||||
description = ["Runs Transaction Exclusion API Service"],
|
||||
version = ["0.0.1"],
|
||||
synopsisHeading = "%n",
|
||||
descriptionHeading = "%nDescription:%n%n",
|
||||
optionListHeading = "%nOptions:%n",
|
||||
footerHeading = "%n"
|
||||
)
|
||||
class TransactionExclusionAppCli
|
||||
internal constructor(private val errorWriter: PrintWriter, private val startAction: StartAction) :
|
||||
Callable<Int> {
|
||||
@Parameters(paramLabel = "CONFIG.toml", description = ["Configuration files"])
|
||||
private val configFiles: List<File>? = null
|
||||
|
||||
override fun call(): Int {
|
||||
return try {
|
||||
if (configFiles == null) {
|
||||
errorWriter.println("Please provide a configuration file!")
|
||||
printUsage(errorWriter)
|
||||
return 1
|
||||
}
|
||||
for (configFile in configFiles) {
|
||||
if (!canReadFile(configFile)) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
val configs: Validated<ConfigFailure, AppConfig> = configs(configFiles)
|
||||
if (configs.isInvalid()) {
|
||||
errorWriter.println(configs.getInvalidUnsafe().description())
|
||||
return 1
|
||||
}
|
||||
startAction.start(configs.getUnsafe())
|
||||
0
|
||||
} catch (e: Exception) {
|
||||
reportUserError(e)
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
private fun canReadFile(file: File): Boolean {
|
||||
if (!file.canRead()) {
|
||||
errorWriter.println("Cannot read configuration file '${file.absolutePath}'")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun configs(configFiles: List<File>): Validated<ConfigFailure, AppConfig> {
|
||||
val confBuilder: ConfigLoaderBuilder = ConfigLoaderBuilder.Companion.empty().addDefaults()
|
||||
for (i in configFiles.indices.reversed()) {
|
||||
// files must be added in reverse order for overriding
|
||||
|
||||
// files must be added in reverse order for overriding
|
||||
confBuilder.addFileSource(configFiles[i], false)
|
||||
}
|
||||
val config: Validated<ConfigFailure, AppConfig> =
|
||||
confBuilder.build().loadConfig<AppConfig>(emptyList())
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun reportUserError(ex: Throwable) {
|
||||
logger.fatal(ex.message, ex)
|
||||
errorWriter.println(ex.message)
|
||||
printUsage(errorWriter)
|
||||
}
|
||||
|
||||
private fun printUsage(outputWriter: PrintWriter) {
|
||||
outputWriter.println()
|
||||
outputWriter.println("To display full help:")
|
||||
outputWriter.println(COMMAND_NAME + " --help")
|
||||
}
|
||||
|
||||
/**
|
||||
* Not using a static field for this log instance because some code in this class executes prior
|
||||
* to the logging configuration being applied so it's not always safe to use the logger.
|
||||
*
|
||||
* Where this is used we also ensure the messages are printed to the error writer so they will be
|
||||
* printed even if logging is not yet configured.
|
||||
*
|
||||
* @return the logger for this class
|
||||
*/
|
||||
private val logger: Logger = LogManager.getLogger()
|
||||
|
||||
fun interface StartAction {
|
||||
fun start(configs: AppConfig)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COMMAND_NAME = "transaction-exclusion"
|
||||
fun withAction(startAction: StartAction): TransactionExclusionAppCli {
|
||||
val errorWriter = PrintWriter(System.err, true, Charset.defaultCharset())
|
||||
return TransactionExclusionAppCli(errorWriter, startAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.consensys.linea.transactionexclusion.app
|
||||
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.core.LoggerContext
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
import picocli.CommandLine
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class TransactionExclusionAppMain {
|
||||
companion object {
|
||||
private val log = LogManager.getLogger(TransactionExclusionAppMain::class)
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val cmd = CommandLine(TransactionExclusionAppCli.withAction(::startApp))
|
||||
cmd.execute(*args)
|
||||
}
|
||||
|
||||
private fun startApp(configs: AppConfig) {
|
||||
try {
|
||||
val app = TransactionExclusionApp(configs)
|
||||
Runtime.getRuntime()
|
||||
.addShutdownHook(
|
||||
Thread {
|
||||
app.stop()
|
||||
if (LogManager.getContext() is LoggerContext) {
|
||||
// Disable log4j auto shutdown hook is not used otherwise
|
||||
// Messages in App.stop won't appear in the logs
|
||||
Configurator.shutdown(LogManager.getContext() as LoggerContext)
|
||||
}
|
||||
}
|
||||
)
|
||||
app.start()
|
||||
} catch (t: Throwable) {
|
||||
log.error("Startup failure: ", t)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
import io.vertx.core.DeploymentOptions
|
||||
import io.vertx.core.Future
|
||||
import io.vertx.core.Vertx
|
||||
import net.consensys.linea.jsonrpc.HttpRequestHandler
|
||||
import net.consensys.linea.jsonrpc.JsonRpcMessageHandler
|
||||
import net.consensys.linea.jsonrpc.JsonRpcMessageProcessor
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestRouter
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.vertx.ObservabilityServer
|
||||
|
||||
data class ApiConfig(
|
||||
val port: Int = 0,
|
||||
val observabilityPort: Int = 0,
|
||||
val numberOfVerticles: Int = 0,
|
||||
val path: String = "/"
|
||||
)
|
||||
|
||||
class Api(
|
||||
private val configs: ApiConfig,
|
||||
private val vertx: Vertx,
|
||||
private val meterRegistry: MeterRegistry,
|
||||
private val transactionExclusionService: TransactionExclusionServiceV1
|
||||
) {
|
||||
private var jsonRpcServerId: String? = null
|
||||
private var observabilityServerId: String? = null
|
||||
private var serverPort: Int = -1
|
||||
val bindedPort: Int
|
||||
get() = if (serverPort > 0) {
|
||||
serverPort
|
||||
} else {
|
||||
throw IllegalStateException("Http server not started")
|
||||
}
|
||||
|
||||
fun start(): Future<*> {
|
||||
val requestHandlersV1 =
|
||||
mapOf(
|
||||
ApiMethod.LINEA_SAVE_REJECTED_TRANSACTION_V1.method to
|
||||
SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionService = transactionExclusionService
|
||||
),
|
||||
ApiMethod.LINEA_GET_TRANSACTION_EXCLUSION_STATUS_V1.method to
|
||||
GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionService = transactionExclusionService
|
||||
)
|
||||
)
|
||||
|
||||
val messageHandler: JsonRpcMessageHandler =
|
||||
JsonRpcMessageProcessor(JsonRpcRequestRouter(requestHandlersV1), meterRegistry)
|
||||
|
||||
val numberOfVerticles: Int =
|
||||
if (configs.numberOfVerticles > 0) {
|
||||
configs.numberOfVerticles
|
||||
} else {
|
||||
Runtime.getRuntime().availableProcessors()
|
||||
}
|
||||
|
||||
val observabilityServer =
|
||||
ObservabilityServer(
|
||||
ObservabilityServer.Config(
|
||||
"transaction-exclusion-api",
|
||||
configs.observabilityPort
|
||||
)
|
||||
)
|
||||
var httpServer: HttpJsonRpcServer? = null
|
||||
return vertx
|
||||
.deployVerticle(
|
||||
{
|
||||
HttpJsonRpcServer(configs.port.toUInt(), configs.path, HttpRequestHandler(messageHandler))
|
||||
.also {
|
||||
httpServer = it
|
||||
}
|
||||
},
|
||||
DeploymentOptions().setInstances(numberOfVerticles)
|
||||
)
|
||||
.compose { verticleId: String ->
|
||||
jsonRpcServerId = verticleId
|
||||
serverPort = httpServer!!.bindedPort
|
||||
vertx.deployVerticle(observabilityServer).onSuccess { monitorVerticleId ->
|
||||
this.observabilityServerId = monitorVerticleId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stop(): Future<*> {
|
||||
return Future.all(
|
||||
this.jsonRpcServerId?.let { vertx.undeploy(it) } ?: Future.succeededFuture(null),
|
||||
this.observabilityServerId?.let { vertx.undeploy(it) } ?: Future.succeededFuture(null)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
enum class ApiMethod(val method: String) {
|
||||
LINEA_SAVE_REJECTED_TRANSACTION_V1("linea_saveRejectedTransactionV1"),
|
||||
LINEA_GET_TRANSACTION_EXCLUSION_STATUS_V1("linea_getTransactionExclusionStatusV1")
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.assertIs32Bytes
|
||||
import net.consensys.decodeHex
|
||||
import net.consensys.linea.transactionexclusion.ModuleOverflow
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionInfo
|
||||
import net.consensys.linea.transactionexclusion.dto.ModuleOverflowJsonDto
|
||||
import org.apache.tuweni.bytes.Bytes
|
||||
import org.hyperledger.besu.ethereum.core.encoding.TransactionDecoder
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
object ArgumentParser {
|
||||
fun getTransactionRLPInRawBytes(transactionRLP: String): ByteArray {
|
||||
try {
|
||||
return transactionRLP.decodeHex()
|
||||
} catch (ex: Exception) {
|
||||
throw IllegalArgumentException("Hex string of RLP-encoded transaction cannot be parsed: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTxHashInRawBytes(txHash: String): ByteArray {
|
||||
try {
|
||||
return txHash.decodeHex().assertIs32Bytes()
|
||||
} catch (ex: Exception) {
|
||||
throw IllegalArgumentException("Hex string of transaction hash cannot be parsed: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTransactionInfoFromRLP(rlp: ByteArray): TransactionInfo {
|
||||
try {
|
||||
return TransactionDecoder.decodeOpaqueBytes(
|
||||
Bytes.wrap(rlp)
|
||||
).run {
|
||||
TransactionInfo(
|
||||
hash = this.hash.toArray(),
|
||||
to = this.to.get().toArray(),
|
||||
from = this.sender.toArray(),
|
||||
nonce = this.nonce.toULong()
|
||||
)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
throw IllegalArgumentException("RLP-encoded transaction cannot be parsed: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getOverflows(target: Any): List<ModuleOverflow> {
|
||||
try {
|
||||
return ModuleOverflowJsonDto.parseListFrom(target).map { it.toDomainObject() }
|
||||
} catch (ex: Exception) {
|
||||
throw IllegalArgumentException("Overflows cannot be parsed: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getReasonMessage(reasonMessage: String): String {
|
||||
if (reasonMessage.length > 256) {
|
||||
throw IllegalArgumentException("Reason message should not be more than 256 characters: $reasonMessage")
|
||||
}
|
||||
return reasonMessage
|
||||
}
|
||||
|
||||
fun getBlockNumber(blockNumberStr: String?): ULong? {
|
||||
try {
|
||||
return blockNumberStr?.toULong()
|
||||
} catch (ex: NumberFormatException) {
|
||||
throw IllegalArgumentException("Block number cannot be parsed to an unsigned number: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimestampFromISO8601(timestamp: String): Instant {
|
||||
try {
|
||||
DateTimeFormatter.ISO_DATE_TIME.parse(timestamp)
|
||||
return Instant.parse(timestamp)
|
||||
} catch (ex: DateTimeParseException) {
|
||||
throw IllegalArgumentException("Timestamp is not in ISO-8601: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTxRejectionStage(txRejectionStage: String): RejectedTransaction.Stage {
|
||||
try {
|
||||
return RejectedTransaction.Stage.valueOf(txRejectionStage)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
throw IllegalArgumentException("Unsupported transaction rejection stage: $txRejectionStage")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import io.vertx.core.AbstractVerticle
|
||||
import io.vertx.core.AsyncResult
|
||||
import io.vertx.core.Handler
|
||||
import io.vertx.core.Promise
|
||||
import io.vertx.core.http.HttpServer
|
||||
import io.vertx.core.http.HttpServerOptions
|
||||
import io.vertx.ext.web.Router
|
||||
import io.vertx.ext.web.RoutingContext
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
|
||||
class HttpJsonRpcServer(
|
||||
private val port: UInt,
|
||||
private val path: String,
|
||||
private val requestHandler: Handler<RoutingContext>
|
||||
) : AbstractVerticle() {
|
||||
private val log: Logger = LogManager.getLogger(this.javaClass)
|
||||
private lateinit var httpServer: HttpServer
|
||||
val bindedPort: Int
|
||||
get() = if (this::httpServer.isInitialized) {
|
||||
httpServer.actualPort()
|
||||
} else {
|
||||
throw IllegalStateException("Http server not started")
|
||||
}
|
||||
|
||||
override fun start(startPromise: Promise<Void>) {
|
||||
val options = HttpServerOptions().setPort(port.toInt()).setReusePort(true)
|
||||
log.debug("Creating Http server on port {}", port)
|
||||
httpServer = vertx.createHttpServer(options)
|
||||
httpServer.requestHandler(buildRouter())
|
||||
httpServer.listen { res: AsyncResult<HttpServer> ->
|
||||
if (res.succeeded()) {
|
||||
log.info("Http server started and listening on port {}", res.result().actualPort())
|
||||
startPromise.complete()
|
||||
} else {
|
||||
log.error("Creating Http server: {}", res.cause())
|
||||
startPromise.fail(res.cause())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRouter(): Router {
|
||||
val router = Router.router(vertx)
|
||||
router.route(path).produces("application/json").handler(requestHandler)
|
||||
return router
|
||||
}
|
||||
|
||||
override fun stop(endFuture: Promise<Void>) {
|
||||
httpServer.close(endFuture)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.flatMap
|
||||
import com.github.michaelbull.result.get
|
||||
import com.github.michaelbull.result.map
|
||||
import com.github.michaelbull.result.mapError
|
||||
import io.vertx.core.Future
|
||||
import io.vertx.core.json.JsonObject
|
||||
import io.vertx.ext.auth.User
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.async.toVertxFuture
|
||||
import net.consensys.linea.jsonrpc.JsonRpcErrorResponse
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequest
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestHandler
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestListParams
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestMapParams
|
||||
import net.consensys.linea.jsonrpc.JsonRpcSuccessResponse
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.transactionexclusion.dto.RejectedTransactionJsonDto
|
||||
import net.consensys.toHexString
|
||||
|
||||
private fun validateIsMapOrListParams(request: JsonRpcRequest): Result<JsonRpcRequest, JsonRpcErrorResponse> {
|
||||
if (request.params !is Map<*, *> && request.params !is List<*>) {
|
||||
return Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"params should be either an object or a list"
|
||||
)
|
||||
)
|
||||
}
|
||||
return Ok(request)
|
||||
}
|
||||
|
||||
private fun validateIsListParams(request: JsonRpcRequest): Result<JsonRpcRequest, JsonRpcErrorResponse> {
|
||||
if (request.params !is List<*>) {
|
||||
return Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"params should be a list"
|
||||
)
|
||||
)
|
||||
}
|
||||
return Ok(request)
|
||||
}
|
||||
|
||||
class SaveRejectedTransactionRequestHandlerV1(
|
||||
private val transactionExclusionService: TransactionExclusionServiceV1
|
||||
) : JsonRpcRequestHandler {
|
||||
enum class RequestParams(val paramName: String) {
|
||||
TX_REJECTION_STAGE("txRejectionStage"),
|
||||
TIMESTAMP("timestamp"),
|
||||
REASON_MESSAGE("reasonMessage"),
|
||||
TRANSACTION_RLP("transactionRLP"),
|
||||
BLOCK_NUMBER("blockNumber"),
|
||||
OVERFLOWS("overflows")
|
||||
}
|
||||
|
||||
private fun validateMapParamsPresence(requestMapParams: Map<*, *>) {
|
||||
RequestParams.entries
|
||||
.filter { requestParam ->
|
||||
requestParam != RequestParams.BLOCK_NUMBER && requestMapParams[requestParam.paramName] == null
|
||||
}
|
||||
.run {
|
||||
if (this.isNotEmpty()) {
|
||||
throw IllegalArgumentException(
|
||||
"Missing ${this.joinToString(",", "[", "]") { it.paramName }} " +
|
||||
"from the given request params"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMapParamsToRejectedTransaction(requestMapParams: Map<*, *>): RejectedTransaction {
|
||||
return validateMapParamsPresence(requestMapParams).run {
|
||||
RejectedTransactionJsonDto.parseFrom(requestMapParams).toDomainObject()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseListParamsToRejectedTransaction(requestListParams: List<Any?>): RejectedTransaction {
|
||||
if (requestListParams.size != 1) {
|
||||
throw IllegalArgumentException(
|
||||
"The given request params list should have one argument"
|
||||
)
|
||||
} else if (requestListParams.first() !is Map<*, *>) {
|
||||
throw IllegalArgumentException(
|
||||
"The argument in the request params list should be an object"
|
||||
)
|
||||
}
|
||||
return parseMapParamsToRejectedTransaction(requestListParams[0] as Map<*, *>)
|
||||
}
|
||||
|
||||
override fun invoke(
|
||||
user: User?,
|
||||
request: JsonRpcRequest,
|
||||
requestJson: JsonObject
|
||||
): Future<Result<JsonRpcSuccessResponse, JsonRpcErrorResponse>> {
|
||||
val rejectedTransaction = try {
|
||||
val parsingResult = validateIsMapOrListParams(request).flatMap { validatedRequest ->
|
||||
val parsedRejectedTransaction =
|
||||
when (validatedRequest) {
|
||||
is JsonRpcRequestMapParams -> parseMapParamsToRejectedTransaction(validatedRequest.params)
|
||||
is JsonRpcRequestListParams -> parseListParamsToRejectedTransaction(validatedRequest.params)
|
||||
else -> throw IllegalStateException(
|
||||
"JsonRpcRequest should be as JsonRpcRequestMapParams or JsonRpcRequestListParams"
|
||||
)
|
||||
}
|
||||
Ok(parsedRejectedTransaction)
|
||||
}
|
||||
if (parsingResult is Err) {
|
||||
return Future.succeededFuture(parsingResult)
|
||||
} else {
|
||||
parsingResult.get()!!
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Future.succeededFuture(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return transactionExclusionService
|
||||
.saveRejectedTransaction(rejectedTransaction)
|
||||
.thenApply { result ->
|
||||
result.map {
|
||||
val rpcResult =
|
||||
JsonObject()
|
||||
.put("status", it.name)
|
||||
.put("txHash", rejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
JsonRpcSuccessResponse(request.id, rpcResult)
|
||||
}.mapError { error ->
|
||||
JsonRpcErrorResponse(request.id, jsonRpcError(error))
|
||||
}
|
||||
}.toVertxFuture()
|
||||
}
|
||||
}
|
||||
|
||||
class GetTransactionExclusionStatusRequestHandlerV1(
|
||||
private val transactionExclusionService: TransactionExclusionServiceV1
|
||||
) : JsonRpcRequestHandler {
|
||||
private fun parseListParamsToTxHash(requestListParams: List<Any?>): ByteArray {
|
||||
if (requestListParams.size != 1) {
|
||||
throw IllegalArgumentException(
|
||||
"The given request params list should have one argument"
|
||||
)
|
||||
}
|
||||
return ArgumentParser.getTxHashInRawBytes(requestListParams[0].toString())
|
||||
}
|
||||
|
||||
override fun invoke(
|
||||
user: User?,
|
||||
request: JsonRpcRequest,
|
||||
requestJson: JsonObject
|
||||
): Future<Result<JsonRpcSuccessResponse, JsonRpcErrorResponse>> {
|
||||
val txHash = try {
|
||||
val parsingResult = validateIsListParams(request).flatMap { validatedRequest ->
|
||||
val parsedTxHash =
|
||||
when (validatedRequest) {
|
||||
is JsonRpcRequestListParams -> parseListParamsToTxHash(validatedRequest.params)
|
||||
else -> throw IllegalStateException("JsonRpcRequest should be as JsonRpcRequestListParams")
|
||||
}
|
||||
Ok(parsedTxHash)
|
||||
}
|
||||
if (parsingResult is Err) {
|
||||
return Future.succeededFuture(parsingResult)
|
||||
} else {
|
||||
parsingResult.get()!!
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return Future.succeededFuture(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return transactionExclusionService
|
||||
.getTransactionExclusionStatus(txHash)
|
||||
.thenApply { result ->
|
||||
result.map {
|
||||
val rpcResult = if (it == null) { null } else {
|
||||
JsonObject()
|
||||
.put("txHash", it.transactionInfo.hash.encodeHex())
|
||||
.put("from", it.transactionInfo.from.encodeHex())
|
||||
.put("nonce", it.transactionInfo.nonce.toHexString())
|
||||
.put("txRejectionStage", it.txRejectionStage.name)
|
||||
.put("reasonMessage", it.reasonMessage)
|
||||
.put("timestamp", it.timestamp.toString())
|
||||
.also { jsonObject ->
|
||||
if (it.blockNumber != null) {
|
||||
jsonObject.put("blockNumber", it.blockNumber!!.toHexString())
|
||||
}
|
||||
}
|
||||
}
|
||||
JsonRpcSuccessResponse(request.id, rpcResult)
|
||||
}.mapError { error ->
|
||||
JsonRpcErrorResponse(request.id, jsonRpcError(error))
|
||||
}
|
||||
}.toVertxFuture()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import net.consensys.linea.jsonrpc.JsonRpcError
|
||||
import net.consensys.linea.transactionexclusion.ErrorType
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionError
|
||||
|
||||
enum class TransactionExclusionErrorCodes(val code: Int, val message: String) {
|
||||
// App/System/Server' error codes
|
||||
SERVER_ERROR(-32000, "Server error");
|
||||
|
||||
fun toErrorObject(data: Any? = null): JsonRpcError {
|
||||
return JsonRpcError(this.code, this.message, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun jsonRpcError(appError: TransactionExclusionError): JsonRpcError {
|
||||
return when (appError.errorType) {
|
||||
ErrorType.SERVER_ERROR ->
|
||||
TransactionExclusionErrorCodes.SERVER_ERROR.toErrorObject(appError.errorDetail)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package net.consensys.linea.transactionexclusion.dto
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.consensys.linea.transactionexclusion.ModuleOverflow
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.app.api.ArgumentParser
|
||||
|
||||
data class ModuleOverflowJsonDto(
|
||||
val count: Long,
|
||||
val limit: Long,
|
||||
val module: String
|
||||
) {
|
||||
// Jackson ObjectMapper requires a default constructor
|
||||
constructor() : this(0L, 0L, "")
|
||||
|
||||
companion object {
|
||||
fun parseListFrom(target: Any): List<ModuleOverflowJsonDto> {
|
||||
return ObjectMapper().readValue(
|
||||
ObjectMapper().writeValueAsString(target),
|
||||
Array<ModuleOverflowJsonDto>::class.java
|
||||
).toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun toDomainObject(): ModuleOverflow {
|
||||
return ModuleOverflow(
|
||||
count = count,
|
||||
limit = limit,
|
||||
module = module
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class RejectedTransactionJsonDto(
|
||||
val txRejectionStage: String,
|
||||
val timestamp: String,
|
||||
val blockNumber: String?,
|
||||
val transactionRLP: String,
|
||||
val reasonMessage: String,
|
||||
val overflows: Any
|
||||
) {
|
||||
// Jackson ObjectMapper requires a default constructor
|
||||
constructor() : this("", "", null, "", "", Any())
|
||||
|
||||
companion object {
|
||||
fun parseFrom(target: Any): RejectedTransactionJsonDto {
|
||||
return ObjectMapper().readValue(
|
||||
ObjectMapper().writeValueAsString(target),
|
||||
RejectedTransactionJsonDto::class.java
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RejectedTransactionJsonDto
|
||||
|
||||
if (txRejectionStage != other.txRejectionStage) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (blockNumber != other.blockNumber) return false
|
||||
if (transactionRLP != other.transactionRLP) return false
|
||||
if (reasonMessage != other.reasonMessage) return false
|
||||
return overflows == other.overflows
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = txRejectionStage.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + (blockNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + transactionRLP.hashCode()
|
||||
result = 31 * result + reasonMessage.hashCode()
|
||||
result = 31 * result + overflows.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun toDomainObject(): RejectedTransaction {
|
||||
return ArgumentParser.getTransactionRLPInRawBytes(transactionRLP)
|
||||
.let { parsedTransactionRLP ->
|
||||
RejectedTransaction(
|
||||
txRejectionStage = ArgumentParser.getTxRejectionStage(txRejectionStage),
|
||||
timestamp = ArgumentParser.getTimestampFromISO8601(timestamp),
|
||||
blockNumber = ArgumentParser.getBlockNumber(blockNumber),
|
||||
transactionRLP = parsedTransactionRLP,
|
||||
reasonMessage = ArgumentParser.getReasonMessage(reasonMessage),
|
||||
overflows = ArgumentParser.getOverflows(overflows),
|
||||
transactionInfo = ArgumentParser.getTransactionInfoFromRLP(parsedTransactionRLP)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.consensys.linea.transactionexclusion.service
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import kotlinx.datetime.Clock
|
||||
import net.consensys.zkevm.PeriodicPollingService
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsDao
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import kotlin.time.Duration
|
||||
|
||||
class RejectedTransactionCleanupService(
|
||||
vertx: Vertx,
|
||||
private val config: Config,
|
||||
private val repository: RejectedTransactionsDao,
|
||||
private val clock: Clock = Clock.System,
|
||||
private val log: Logger = LogManager.getLogger(RejectedTransactionCleanupService::class.java)
|
||||
) : PeriodicPollingService(
|
||||
vertx = vertx,
|
||||
pollingIntervalMs = config.pollingInterval.inWholeMilliseconds,
|
||||
log = log
|
||||
) {
|
||||
data class Config(
|
||||
val pollingInterval: Duration,
|
||||
val storagePeriod: Duration
|
||||
)
|
||||
|
||||
override fun action(): SafeFuture<*> {
|
||||
return this.repository.deleteRejectedTransactions(
|
||||
clock.now().minus(config.storagePeriod)
|
||||
).thenPeek { deletedRows ->
|
||||
if (deletedRows > 0) {
|
||||
log.debug("deletedRows=$deletedRows")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleError(error: Throwable) {
|
||||
log.error(
|
||||
"Error with rejected transaction cleanup service: errorMessage={}",
|
||||
error.message,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.consensys.linea.transactionexclusion.service
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import kotlinx.datetime.Clock
|
||||
import net.consensys.linea.metrics.LineaMetricsCategory
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.linea.transactionexclusion.ErrorType
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionError
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1.SaveRejectedTransactionStatus
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsDao
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import kotlin.time.Duration
|
||||
|
||||
class TransactionExclusionServiceV1Impl(
|
||||
private val config: Config,
|
||||
private val repository: RejectedTransactionsDao,
|
||||
metricsFacade: MetricsFacade,
|
||||
private val clock: Clock = Clock.System
|
||||
) : TransactionExclusionServiceV1 {
|
||||
data class Config(
|
||||
val rejectedTimestampWithinDuration: Duration
|
||||
)
|
||||
|
||||
private val txRejectionCounter = metricsFacade.createCounter(
|
||||
LineaMetricsCategory.TX_EXCLUSION_API,
|
||||
"transactions.rejected",
|
||||
"Counter of rejected transactions reported to Transaction Exclusion API service"
|
||||
)
|
||||
|
||||
override fun saveRejectedTransaction(
|
||||
rejectedTransaction: RejectedTransaction
|
||||
): SafeFuture<
|
||||
Result<SaveRejectedTransactionStatus, TransactionExclusionError>
|
||||
> {
|
||||
return this.repository.saveNewRejectedTransaction(rejectedTransaction)
|
||||
.handleComposed { _, error ->
|
||||
if (error != null) {
|
||||
if (error is DuplicatedRecordException) {
|
||||
SafeFuture.completedFuture(
|
||||
Ok(SaveRejectedTransactionStatus.DUPLICATE_ALREADY_SAVED_BEFORE)
|
||||
)
|
||||
} else {
|
||||
SafeFuture.completedFuture(
|
||||
Err(TransactionExclusionError(ErrorType.SERVER_ERROR, error.message ?: ""))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
txRejectionCounter.increment()
|
||||
SafeFuture.completedFuture(Ok(SaveRejectedTransactionStatus.SAVED))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTransactionExclusionStatus(
|
||||
txHash: ByteArray
|
||||
): SafeFuture<Result<RejectedTransaction?, TransactionExclusionError>> {
|
||||
return this.repository.findRejectedTransactionByTxHash(
|
||||
txHash = txHash,
|
||||
notRejectedBefore = clock.now().minus(config.rejectedTimestampWithinDuration)
|
||||
)
|
||||
.handleComposed { result, error ->
|
||||
if (error != null) {
|
||||
SafeFuture.completedFuture(
|
||||
Err(TransactionExclusionError(ErrorType.SERVER_ERROR, error.message ?: ""))
|
||||
)
|
||||
} else {
|
||||
SafeFuture.completedFuture(Ok(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import io.vertx.core.json.JsonObject
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.decodeHex
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.transactionexclusion.ModuleOverflow
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionInfo
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.random.Random
|
||||
|
||||
class ArgumentParserTest {
|
||||
@Test
|
||||
fun getTransactionRLPInRawBytes_should_return_correct_byte_array() {
|
||||
val transactionRLPInHexStr =
|
||||
"0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8"
|
||||
Assertions.assertTrue(
|
||||
ArgumentParser.getTransactionRLPInRawBytes(transactionRLPInHexStr)
|
||||
.contentEquals(transactionRLPInHexStr.decodeHex())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionRLPInRawBytes_should_throw_error_for_invalid_hex_string() {
|
||||
// odd number of hex character
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTransactionRLPInRawBytes(
|
||||
"0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b820"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("RLP-encoded transaction cannot be parsed")
|
||||
)
|
||||
}
|
||||
|
||||
// invalid hex character
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTransactionRLPInRawBytes(
|
||||
"yyf8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201xx"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("RLP-encoded transaction cannot be parsed")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTxHashInRawBytes_should_return_correct_byte_array() {
|
||||
val txHashInHexStr = "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7"
|
||||
Assertions.assertTrue(
|
||||
ArgumentParser.getTxHashInRawBytes(txHashInHexStr)
|
||||
.contentEquals(txHashInHexStr.decodeHex())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTxHashInRawBytes_should_throw_error_for_invalid_hex_string() {
|
||||
// hex string of less than 64 hex characters
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTxHashInRawBytes(
|
||||
"0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350a"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Hex string of transaction hash cannot be parsed")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionInfoFromRLP_should_return_correct_transactionInfo() {
|
||||
val transactionRLP =
|
||||
(
|
||||
"0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8"
|
||||
).decodeHex()
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getTransactionInfoFromRLP(transactionRLP),
|
||||
TransactionInfo(
|
||||
hash = "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7".decodeHex(),
|
||||
from = "0x4d144d7b9c96b26361d6ac74dd1d8267edca4fc2".decodeHex(),
|
||||
to = "0x1195cf65f83b3a5768f3c496d3a05ad6412c64b3".decodeHex(),
|
||||
nonce = 100UL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionInfoFromRLP_should_throw_error_for_invalid_transactionRLP() {
|
||||
// hex string of less than 64 hex characters
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTransactionInfoFromRLP(
|
||||
(
|
||||
"0xaaf8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8"
|
||||
).decodeHex()
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("RLP-encoded transaction cannot be parsed")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getOverflows_should_return_correct_list_of_ModuleOverflow() {
|
||||
val expectedModuleOverflowList = listOf(
|
||||
ModuleOverflow(
|
||||
module = "ADD",
|
||||
count = 402,
|
||||
limit = 70
|
||||
),
|
||||
ModuleOverflow(
|
||||
module = "MUL",
|
||||
count = 587,
|
||||
limit = 400
|
||||
)
|
||||
)
|
||||
|
||||
// valid module overflow as json request params
|
||||
val moduleOverflowJsonRequestParams =
|
||||
listOf(
|
||||
mapOf(
|
||||
"module" to "ADD",
|
||||
"count" to "402",
|
||||
"limit" to "70"
|
||||
),
|
||||
mapOf(
|
||||
"module" to "MUL",
|
||||
"count" to "587",
|
||||
"limit" to "400"
|
||||
)
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getOverflows(moduleOverflowJsonRequestParams),
|
||||
expectedModuleOverflowList
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getOverflows_should_throw_error_for_invalid_moduleOverflowJsonRequestParams() {
|
||||
// invalid module overflow as json request params (invalid field name xxx)
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getOverflows(
|
||||
listOf(
|
||||
mapOf(
|
||||
"module" to "ADD",
|
||||
"count" to "402",
|
||||
"xxx" to "70"
|
||||
),
|
||||
mapOf(
|
||||
"module" to "MUL",
|
||||
"count" to "587",
|
||||
"limit" to "400"
|
||||
)
|
||||
)
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Overflows cannot be parsed")
|
||||
)
|
||||
}
|
||||
|
||||
// invalid module overflow as json request params (invalid module value)
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getOverflows(
|
||||
listOf(
|
||||
mapOf(
|
||||
"module" to null,
|
||||
"count" to "402",
|
||||
"limit" to "70"
|
||||
),
|
||||
mapOf(
|
||||
"module" to "MUL",
|
||||
"count" to "587",
|
||||
"limit" to "400"
|
||||
)
|
||||
)
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Overflows cannot be parsed")
|
||||
)
|
||||
}
|
||||
|
||||
// invalid json string (invalid input)
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getOverflows(JsonObject())
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Overflows cannot be parsed")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getReasonMessage_should_return_correct_reason_message() {
|
||||
val reasonMessage = "Transaction line count for module ADD=402 is above the limit 70"
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getReasonMessage(reasonMessage),
|
||||
reasonMessage
|
||||
)
|
||||
|
||||
val reasonMessageWithMaxLen = Random.Default.nextBytes(128).encodeHex(prefix = false)
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getReasonMessage(reasonMessageWithMaxLen),
|
||||
reasonMessageWithMaxLen
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getReasonMessage_should_throw_error_for_string_length_longer_than_256() {
|
||||
// reason message string with more than 256 characters
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getReasonMessage(
|
||||
Random.Default.nextBytes(128).encodeHex(prefix = false) + "0"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Reason message should not be more than 256 characters")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBlockNumber_should_return_correct_unsigned_long_or_null() {
|
||||
// 10-base number
|
||||
val blockNumberStr = "12345"
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getBlockNumber(blockNumberStr)!!,
|
||||
blockNumberStr.toULong()
|
||||
)
|
||||
|
||||
Assertions.assertNull(
|
||||
ArgumentParser.getBlockNumber(null)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBlockNumber_should_throw_error_for_invalid_blockNumberStr() {
|
||||
// block number string with hex string
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getBlockNumber(
|
||||
"0x12345"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Block number cannot be parsed to an unsigned number")
|
||||
)
|
||||
}
|
||||
|
||||
// block number string with random characters
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getBlockNumber(
|
||||
"xxyyzz"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Block number cannot be parsed to an unsigned number")
|
||||
)
|
||||
}
|
||||
|
||||
// empty block number string
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getBlockNumber("")
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Block number cannot be parsed to an unsigned number")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTimestampFromISO8601_should_return_correct_instant() {
|
||||
// timestamp in ISO-8601
|
||||
val timestampStr = "2024-09-05T09:22:52Z"
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getTimestampFromISO8601(timestampStr),
|
||||
Instant.parse(timestampStr)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTimestampFromISO8601_should_throw_error_for_invalid_timestampStr() {
|
||||
// timestamp string not in ISO-8601
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTimestampFromISO8601(
|
||||
"2024-09-05_09:22:52"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Timestamp is not in ISO-8601")
|
||||
)
|
||||
}
|
||||
|
||||
// timestamp string in epoch time millisecond
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTimestampFromISO8601(
|
||||
"1725543970103"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Timestamp is not in ISO-8601")
|
||||
)
|
||||
}
|
||||
|
||||
// empty timestamp string
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTimestampFromISO8601("")
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Timestamp is not in ISO-8601")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTxRejectionStage_should_return_correct_rejection_stage() {
|
||||
val txRejectionStageStr = "SEQUENCER"
|
||||
Assertions.assertEquals(
|
||||
ArgumentParser.getTxRejectionStage(txRejectionStageStr),
|
||||
RejectedTransaction.Stage.SEQUENCER
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTxRejectionStage_should_throw_error_for_invalid_txRejectionStageStr() {
|
||||
// rejection stage string in lower case
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTxRejectionStage(
|
||||
"sequencer"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Unsupported transaction rejection stage")
|
||||
)
|
||||
}
|
||||
|
||||
// rejection stage string in random characters
|
||||
assertThrows<IllegalArgumentException> {
|
||||
ArgumentParser.getTxRejectionStage(
|
||||
"helloworld"
|
||||
)
|
||||
}.also { error ->
|
||||
Assertions.assertTrue(
|
||||
error.message!!.contains("Unsupported transaction rejection stage")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package net.consensys.linea.transactionexclusion.app.api
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.get
|
||||
import com.github.michaelbull.result.getError
|
||||
import io.vertx.core.json.JsonObject
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.async.get
|
||||
import net.consensys.linea.jsonrpc.JsonRpcErrorResponse
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestListParams
|
||||
import net.consensys.linea.jsonrpc.JsonRpcRequestMapParams
|
||||
import net.consensys.linea.jsonrpc.JsonRpcSuccessResponse
|
||||
import net.consensys.linea.transactionexclusion.ErrorType
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionError
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.transactionexclusion.test.defaultRejectedTransaction
|
||||
import net.consensys.toHexString
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
class RequestHandlersTest {
|
||||
private lateinit var transactionExclusionServiceMock: TransactionExclusionServiceV1
|
||||
|
||||
private val mapParams = mapOf(
|
||||
"txRejectionStage" to "SEQUENCER",
|
||||
"timestamp" to "2024-09-05T09:22:52Z",
|
||||
"transactionRLP" to defaultRejectedTransaction.transactionRLP.encodeHex(),
|
||||
"reasonMessage" to defaultRejectedTransaction.reasonMessage,
|
||||
"overflows" to defaultRejectedTransaction.overflows
|
||||
)
|
||||
|
||||
private val mapRequest = JsonRpcRequestMapParams(
|
||||
"2.0",
|
||||
"1",
|
||||
"linea_saveRejectedTransactionV1",
|
||||
mapParams
|
||||
)
|
||||
|
||||
private val listRequest = JsonRpcRequestListParams(
|
||||
"2.0",
|
||||
"1",
|
||||
"linea_saveRejectedTransactionV1",
|
||||
listOf(mapParams)
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
transactionExclusionServiceMock = mock<TransactionExclusionServiceV1>(
|
||||
defaultAnswer = Mockito.RETURNS_DEEP_STUBS
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_rejectsEmptyMap() {
|
||||
val request = JsonRpcRequestMapParams("", "", "", emptyMap<String, Any>())
|
||||
|
||||
val saveRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val result = saveRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"Missing [txRejectionStage,timestamp,reasonMessage,transactionRLP,overflows] " +
|
||||
"from the given request params"
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_rejectsEmptyList() {
|
||||
val request = JsonRpcRequestListParams("", "", "", emptyList())
|
||||
|
||||
val saveRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val result = saveRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"The given request params list should have one argument"
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_rejectsListWithInvalidArgument() {
|
||||
val request = JsonRpcRequestListParams("", "", "", listOf("invalid_argument"))
|
||||
|
||||
val saveRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val result = saveRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"The argument in the request params list should be an object"
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_invoke_acceptsValidRequestMap() {
|
||||
whenever(transactionExclusionServiceMock.saveRejectedTransaction(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
)
|
||||
)
|
||||
|
||||
val saveRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonObject()
|
||||
.put("status", TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
.put("txHash", defaultRejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
.let {
|
||||
JsonRpcSuccessResponse(mapRequest.id, it)
|
||||
}
|
||||
|
||||
val result = saveRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = mapRequest,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_invoke_acceptsValidRequestList() {
|
||||
whenever(transactionExclusionServiceMock.saveRejectedTransaction(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
)
|
||||
)
|
||||
|
||||
val saveRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonObject()
|
||||
.put("status", TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
.put("txHash", defaultRejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
.let {
|
||||
JsonRpcSuccessResponse(listRequest.id, it)
|
||||
}
|
||||
|
||||
val result = saveRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = listRequest,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_invoke_acceptsValidRequestMap_without_blockNumber() {
|
||||
whenever(transactionExclusionServiceMock.saveRejectedTransaction(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
)
|
||||
)
|
||||
|
||||
val saveTxRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonObject()
|
||||
.put("status", TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED)
|
||||
.put("txHash", defaultRejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
.let {
|
||||
JsonRpcSuccessResponse(mapRequest.id, it)
|
||||
}
|
||||
|
||||
val result = saveTxRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = mapRequest,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_invoke_return_success_result_with_duplicate_status() {
|
||||
whenever(transactionExclusionServiceMock.saveRejectedTransaction(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.DUPLICATE_ALREADY_SAVED_BEFORE)
|
||||
)
|
||||
)
|
||||
|
||||
val saveTxRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonObject()
|
||||
.put("status", TransactionExclusionServiceV1.SaveRejectedTransactionStatus.DUPLICATE_ALREADY_SAVED_BEFORE)
|
||||
.put("txHash", defaultRejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
.let {
|
||||
JsonRpcSuccessResponse(mapRequest.id, it)
|
||||
}
|
||||
|
||||
val result = saveTxRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = mapRequest,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun SaveRejectedTransactionRequestHandlerV1_invoke_return_failure_result() {
|
||||
whenever(transactionExclusionServiceMock.saveRejectedTransaction(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Err(
|
||||
TransactionExclusionError(
|
||||
ErrorType.SERVER_ERROR,
|
||||
"error for unit test"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val saveTxRequestHandlerV1 = SaveRejectedTransactionRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonRpcErrorResponse(
|
||||
mapRequest.id,
|
||||
jsonRpcError(
|
||||
TransactionExclusionError(
|
||||
ErrorType.SERVER_ERROR,
|
||||
"error for unit test"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = saveTxRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = mapRequest,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.getError())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GetTransactionExclusionStatusRequestHandlerV1_rejectsEmptyList() {
|
||||
val request = JsonRpcRequestListParams("", "", "", emptyList())
|
||||
|
||||
val getRequestHandlerV1 = GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val result = getRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"The given request params list should have one argument"
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GetTransactionExclusionStatusRequestHandlerV1_rejectsListWithInvalidArgument() {
|
||||
val request = JsonRpcRequestListParams("", "", "", listOf("0x123"))
|
||||
|
||||
val getRequestHandlerV1 = GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val result = getRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
JsonRpcErrorResponse.invalidParams(
|
||||
request.id,
|
||||
"Hex string of transaction hash cannot be parsed: Must have an even length"
|
||||
)
|
||||
),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GetTransactionExclusionStatusRequestHandlerV1_invoke_acceptsValidRequestList() {
|
||||
whenever(transactionExclusionServiceMock.getTransactionExclusionStatus(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Ok(defaultRejectedTransaction)
|
||||
)
|
||||
)
|
||||
|
||||
val request = JsonRpcRequestListParams(
|
||||
"2.0",
|
||||
"1",
|
||||
"linea_getTransactionExclusionStatusV1",
|
||||
listOf(
|
||||
defaultRejectedTransaction.transactionInfo.hash.encodeHex()
|
||||
)
|
||||
)
|
||||
|
||||
val getTxStatusRequestHandlerV1 = GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonObject()
|
||||
.put("txHash", defaultRejectedTransaction.transactionInfo.hash.encodeHex())
|
||||
.put("from", defaultRejectedTransaction.transactionInfo.from.encodeHex())
|
||||
.put("nonce", defaultRejectedTransaction.transactionInfo.nonce.toHexString())
|
||||
.put("txRejectionStage", defaultRejectedTransaction.txRejectionStage.name)
|
||||
.put("reasonMessage", defaultRejectedTransaction.reasonMessage)
|
||||
.put("timestamp", defaultRejectedTransaction.timestamp.toString())
|
||||
.put("blockNumber", defaultRejectedTransaction.blockNumber!!.toHexString())
|
||||
.let {
|
||||
JsonRpcSuccessResponse(request.id, it)
|
||||
}
|
||||
|
||||
val result = getTxStatusRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GetTransactionExclusionStatusRequestHandlerV1_invoke_return_null_result() {
|
||||
whenever(transactionExclusionServiceMock.getTransactionExclusionStatus(any()))
|
||||
.thenReturn(SafeFuture.completedFuture(Ok(null)))
|
||||
|
||||
val request = JsonRpcRequestListParams(
|
||||
"2.0",
|
||||
"1",
|
||||
"linea_getTransactionExclusionStatusV1",
|
||||
listOf(
|
||||
defaultRejectedTransaction.transactionInfo.hash.encodeHex()
|
||||
)
|
||||
)
|
||||
|
||||
val getTxStatusRequestHandlerV1 = GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonRpcSuccessResponse(request.id, null)
|
||||
|
||||
val result = getTxStatusRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GetTransactionExclusionStatusRequestHandlerV1_invoke_return_failure_result() {
|
||||
whenever(transactionExclusionServiceMock.getTransactionExclusionStatus(any()))
|
||||
.thenReturn(
|
||||
SafeFuture.completedFuture(
|
||||
Err(
|
||||
TransactionExclusionError(
|
||||
ErrorType.SERVER_ERROR,
|
||||
"error for unit test"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val request = JsonRpcRequestListParams(
|
||||
"2.0",
|
||||
"1",
|
||||
"linea_getTransactionExclusionStatusV1",
|
||||
listOf(
|
||||
defaultRejectedTransaction.transactionInfo.hash.encodeHex()
|
||||
)
|
||||
)
|
||||
|
||||
val getTxStatusRequestHandlerV1 = GetTransactionExclusionStatusRequestHandlerV1(
|
||||
transactionExclusionServiceMock
|
||||
)
|
||||
|
||||
val expectedResult = JsonRpcErrorResponse(
|
||||
request.id,
|
||||
jsonRpcError(
|
||||
TransactionExclusionError(
|
||||
ErrorType.SERVER_ERROR,
|
||||
"error for unit test"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = getTxStatusRequestHandlerV1.invoke(
|
||||
user = null,
|
||||
request = request,
|
||||
requestJson = JsonObject()
|
||||
).get()
|
||||
|
||||
Assertions.assertEquals(expectedResult, result.getError())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.consensys.linea.transactionexclusion.service
|
||||
|
||||
import io.vertx.core.Vertx
|
||||
import io.vertx.junit5.Timeout
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.junit5.VertxTestContext
|
||||
import kotlinx.datetime.Clock
|
||||
import net.consensys.FakeFixedClock
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsDao
|
||||
import org.awaitility.Awaitility
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.atLeastOnce
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class RejectedTransactionCleanupServiceTest {
|
||||
private lateinit var rejectedTransactionCleanupService: RejectedTransactionCleanupService
|
||||
private lateinit var rejectedTransactionsRepositoryMock: RejectedTransactionsDao
|
||||
private var fakeClock = FakeFixedClock(Clock.System.now())
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
fakeClock.setTimeTo(Clock.System.now())
|
||||
rejectedTransactionsRepositoryMock = mock<RejectedTransactionsDao>(
|
||||
defaultAnswer = Mockito.RETURNS_DEEP_STUBS
|
||||
).also {
|
||||
whenever(it.deleteRejectedTransactions(any()))
|
||||
.thenReturn(SafeFuture.completedFuture(1))
|
||||
}
|
||||
rejectedTransactionCleanupService =
|
||||
RejectedTransactionCleanupService(
|
||||
config = RejectedTransactionCleanupService.Config(
|
||||
pollingInterval = 100.milliseconds,
|
||||
storagePeriod = 24.hours
|
||||
),
|
||||
clock = fakeClock,
|
||||
vertx = Vertx.vertx(),
|
||||
repository = rejectedTransactionsRepositoryMock
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(2, timeUnit = TimeUnit.SECONDS)
|
||||
fun `when rejectedTransactionCleanupService starts, deleteRejectedTransaction should be called`
|
||||
(testContext: VertxTestContext) {
|
||||
rejectedTransactionCleanupService.start()
|
||||
.thenApply {
|
||||
Awaitility.await()
|
||||
.pollInterval(50.milliseconds.toJavaDuration())
|
||||
.untilAsserted {
|
||||
verify(rejectedTransactionsRepositoryMock, atLeastOnce())
|
||||
.deleteRejectedTransactions(eq(fakeClock.now().minus(24.hours)))
|
||||
}
|
||||
testContext.completeNow()
|
||||
}.whenException(testContext::failNow)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package net.consensys.linea.transactionexclusion.service
|
||||
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import net.consensys.linea.metrics.MetricsFacade
|
||||
import net.consensys.linea.transactionexclusion.ErrorType
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionError
|
||||
import net.consensys.linea.transactionexclusion.TransactionExclusionServiceV1
|
||||
import net.consensys.linea.transactionexclusion.test.defaultRejectedTransaction
|
||||
import net.consensys.zkevm.persistence.dao.rejectedtransaction.RejectedTransactionsDao
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
class TransactionExclusionServiceTest {
|
||||
private val metricsFacadeMock = mock<MetricsFacade>(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
|
||||
private val config = TransactionExclusionServiceV1Impl.Config(
|
||||
rejectedTimestampWithinDuration = 24.hours
|
||||
)
|
||||
private lateinit var rejectedTransactionsRepositoryMock: RejectedTransactionsDao
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
rejectedTransactionsRepositoryMock = mock<RejectedTransactionsDao>(
|
||||
defaultAnswer = Mockito.RETURNS_DEEP_STUBS
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveRejectedTransaction_return_success_result_with_saved_status() {
|
||||
whenever(rejectedTransactionsRepositoryMock.findRejectedTransactionByTxHash(any(), any()))
|
||||
.thenReturn(SafeFuture.completedFuture(defaultRejectedTransaction))
|
||||
whenever(rejectedTransactionsRepositoryMock.saveNewRejectedTransaction(any()))
|
||||
.thenReturn(SafeFuture.completedFuture(Unit))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.SAVED),
|
||||
transactionExclusionService.saveRejectedTransaction(defaultRejectedTransaction).get()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveRejectedTransaction_return_success_result_with_duplicated_already_saved_status() {
|
||||
whenever(rejectedTransactionsRepositoryMock.saveNewRejectedTransaction(any()))
|
||||
.thenReturn(SafeFuture.failedFuture(DuplicatedRecordException()))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Ok(TransactionExclusionServiceV1.SaveRejectedTransactionStatus.DUPLICATE_ALREADY_SAVED_BEFORE),
|
||||
transactionExclusionService.saveRejectedTransaction(defaultRejectedTransaction).get()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveRejectedTransaction_return_error_result_when_saveRejectedTransaction_failed() {
|
||||
whenever(rejectedTransactionsRepositoryMock.findRejectedTransactionByTxHash(any(), any()))
|
||||
.thenReturn(SafeFuture.completedFuture(defaultRejectedTransaction))
|
||||
whenever(rejectedTransactionsRepositoryMock.saveNewRejectedTransaction(any()))
|
||||
.thenReturn(SafeFuture.failedFuture(RuntimeException()))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(TransactionExclusionError(ErrorType.SERVER_ERROR, "")),
|
||||
transactionExclusionService.saveRejectedTransaction(defaultRejectedTransaction).get()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionExclusionStatus_return_success_result_with_rejected_txn() {
|
||||
whenever(rejectedTransactionsRepositoryMock.findRejectedTransactionByTxHash(any(), any()))
|
||||
.thenReturn(SafeFuture.completedFuture(defaultRejectedTransaction))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Ok(defaultRejectedTransaction),
|
||||
transactionExclusionService.getTransactionExclusionStatus(
|
||||
defaultRejectedTransaction.transactionInfo.hash
|
||||
).get()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionExclusionStatus_return_error_result_with_transaction_unavailable() {
|
||||
whenever(rejectedTransactionsRepositoryMock.findRejectedTransactionByTxHash(any(), any()))
|
||||
.thenReturn(SafeFuture.completedFuture(null))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Ok(null),
|
||||
transactionExclusionService.getTransactionExclusionStatus(
|
||||
defaultRejectedTransaction.transactionInfo.hash
|
||||
).get()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTransactionExclusionStatus_return_error_result_with_other_error() {
|
||||
whenever(rejectedTransactionsRepositoryMock.findRejectedTransactionByTxHash(any(), any()))
|
||||
.thenReturn(SafeFuture.failedFuture(RuntimeException()))
|
||||
|
||||
val transactionExclusionService = TransactionExclusionServiceV1Impl(
|
||||
config = config,
|
||||
repository = rejectedTransactionsRepositoryMock,
|
||||
metricsFacade = metricsFacadeMock
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
Err(
|
||||
TransactionExclusionError(
|
||||
ErrorType.SERVER_ERROR,
|
||||
""
|
||||
)
|
||||
),
|
||||
transactionExclusionService.getTransactionExclusionStatus(
|
||||
defaultRejectedTransaction.transactionInfo.hash
|
||||
).get()
|
||||
)
|
||||
}
|
||||
}
|
||||
10
transaction-exclusion-api/core/build.gradle
Normal file
10
transaction-exclusion-api/core/build.gradle
Normal file
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
id 'net.consensys.zkevm.kotlin-library-conventions'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":jvm-libs:generic:extensions:kotlin")
|
||||
|
||||
testFixtures implementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.consensys.linea.transactionexclusion
|
||||
|
||||
/** For simplicity, placing all error codes into single enum */
|
||||
enum class ErrorType {
|
||||
SERVER_ERROR
|
||||
}
|
||||
|
||||
data class TransactionExclusionError(val errorType: ErrorType, val errorDetail: String)
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.consensys.linea.transactionexclusion
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.encodeHex
|
||||
|
||||
data class ModuleOverflow(
|
||||
val count: Long,
|
||||
val limit: Long,
|
||||
val module: String
|
||||
) {
|
||||
// Jackson ObjectMapper requires a default constructor
|
||||
constructor() : this(0L, 0L, "")
|
||||
|
||||
override fun toString(): String {
|
||||
return "module=$module count=$count limit=$limit"
|
||||
}
|
||||
}
|
||||
|
||||
data class TransactionInfo(
|
||||
val hash: ByteArray,
|
||||
val from: ByteArray,
|
||||
val to: ByteArray,
|
||||
val nonce: ULong
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TransactionInfo
|
||||
|
||||
if (!hash.contentEquals(other.hash)) return false
|
||||
if (!from.contentEquals(other.from)) return false
|
||||
if (!to.contentEquals(other.to)) return false
|
||||
return nonce == other.nonce
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = hash.contentHashCode()
|
||||
result = 31 * result + from.contentHashCode()
|
||||
result = 31 * result + to.contentHashCode()
|
||||
result = 31 * result + nonce.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "hash=${hash.encodeHex()} from=${from.encodeHex()} " +
|
||||
"to=${to.encodeHex()} nonce=$nonce"
|
||||
}
|
||||
}
|
||||
|
||||
data class RejectedTransaction(
|
||||
val txRejectionStage: Stage,
|
||||
val timestamp: Instant,
|
||||
val blockNumber: ULong?,
|
||||
val transactionRLP: ByteArray,
|
||||
val reasonMessage: String,
|
||||
val overflows: List<ModuleOverflow>,
|
||||
val transactionInfo: TransactionInfo
|
||||
) {
|
||||
enum class Stage {
|
||||
SEQUENCER,
|
||||
RPC,
|
||||
P2P
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as RejectedTransaction
|
||||
|
||||
if (txRejectionStage != other.txRejectionStage) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (blockNumber != other.blockNumber) return false
|
||||
if (!transactionRLP.contentEquals(other.transactionRLP)) return false
|
||||
if (reasonMessage != other.reasonMessage) return false
|
||||
return overflows == other.overflows
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = txRejectionStage.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + blockNumber.hashCode()
|
||||
result = 31 * result + transactionRLP.contentHashCode()
|
||||
result = 31 * result + reasonMessage.hashCode()
|
||||
result = 31 * result + overflows.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "txRejectionStage=$txRejectionStage timestamp=${timestamp.toEpochMilliseconds()} blockNumber=$blockNumber" +
|
||||
" transactionRLP=${transactionRLP.encodeHex()}" +
|
||||
" transactionInfo=$transactionInfo" +
|
||||
" reasonMessage=\"$reasonMessage\"" +
|
||||
" overflows=[${overflows.joinToString(",") { "{$it}" }}]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.consensys.linea.transactionexclusion
|
||||
|
||||
import com.github.michaelbull.result.Result
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
interface TransactionExclusionServiceV1 {
|
||||
enum class SaveRejectedTransactionStatus {
|
||||
SAVED,
|
||||
DUPLICATE_ALREADY_SAVED_BEFORE
|
||||
}
|
||||
|
||||
fun saveRejectedTransaction(
|
||||
rejectedTransaction: RejectedTransaction
|
||||
): SafeFuture<Result<SaveRejectedTransactionStatus, TransactionExclusionError>>
|
||||
|
||||
fun getTransactionExclusionStatus(
|
||||
txHash: ByteArray
|
||||
): SafeFuture<Result<RejectedTransaction?, TransactionExclusionError>>
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.consensys.linea.transactionexclusion.test
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.decodeHex
|
||||
import net.consensys.linea.transactionexclusion.ModuleOverflow
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionInfo
|
||||
|
||||
val defaultRejectedTransaction = RejectedTransaction(
|
||||
txRejectionStage = RejectedTransaction.Stage.SEQUENCER,
|
||||
timestamp = Instant.parse("2024-08-31T09:18:51Z"),
|
||||
blockNumber = 10000UL,
|
||||
transactionRLP =
|
||||
(
|
||||
"0x02f8388204d2648203e88203e88203e8941195cf65f83b3a5768f3c4" +
|
||||
"96d3a05ad6412c64b38203e88c666d93e9cc5f73748162cea9c0017b8201c8"
|
||||
)
|
||||
.decodeHex(),
|
||||
reasonMessage = "Transaction line count for module ADD=402 is above the limit 70",
|
||||
overflows = listOf(
|
||||
ModuleOverflow(
|
||||
module = "ADD",
|
||||
count = 402,
|
||||
limit = 70
|
||||
),
|
||||
ModuleOverflow(
|
||||
module = "MUL",
|
||||
count = 587,
|
||||
limit = 401
|
||||
),
|
||||
ModuleOverflow(
|
||||
module = "EXP",
|
||||
count = 9000,
|
||||
limit = 8192
|
||||
)
|
||||
),
|
||||
transactionInfo = TransactionInfo(
|
||||
hash = "0x526e56101cf39c1e717cef9cedf6fdddb42684711abda35bae51136dbb350ad7".decodeHex(),
|
||||
from = "0x4d144d7b9c96b26361d6ac74dd1d8267edca4fc2".decodeHex(),
|
||||
to = "0x1195cf65f83b3a5768f3c496d3a05ad6412c64b3".decodeHex(),
|
||||
nonce = 100UL
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
plugins {
|
||||
id "net.consensys.zkevm.kotlin-library-conventions"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":jvm-libs:generic:extensions:futures")
|
||||
implementation project(":jvm-libs:generic:extensions:kotlin")
|
||||
implementation project(':jvm-libs:generic:persistence:db')
|
||||
implementation project(":transaction-exclusion-api:core")
|
||||
implementation "com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.get()}"
|
||||
implementation "io.vertx:vertx-pg-client:${libs.versions.vertx.get()}"
|
||||
|
||||
testImplementation(testFixtures(project(":transaction-exclusion-api:core")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:extensions:kotlin")))
|
||||
testImplementation(testFixtures(project(":jvm-libs:generic:persistence:db")))
|
||||
testImplementation("io.vertx:vertx-junit5")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
}
|
||||
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath + sourceSets.test.compileClasspath
|
||||
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
test ->
|
||||
testLogging {
|
||||
events TestLogEvent.FAILED,
|
||||
TestLogEvent.SKIPPED,
|
||||
TestLogEvent.STANDARD_ERROR,
|
||||
TestLogEvent.STANDARD_OUT
|
||||
exceptionFormat TestExceptionFormat.FULL
|
||||
showCauses true
|
||||
showExceptions true
|
||||
showStackTraces true
|
||||
showStandardStreams false
|
||||
}
|
||||
description = "Runs integration tests."
|
||||
group = "verification"
|
||||
useJUnitPlatform()
|
||||
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
|
||||
dependsOn(":localStackComposeUp")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package net.consensys.zkevm.persistence.dao.rejectedtransaction
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.vertx.junit5.VertxExtension
|
||||
import io.vertx.sqlclient.PreparedQuery
|
||||
import io.vertx.sqlclient.Row
|
||||
import io.vertx.sqlclient.RowSet
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.FakeFixedClock
|
||||
import net.consensys.decodeHex
|
||||
import net.consensys.encodeHex
|
||||
import net.consensys.linea.async.get
|
||||
import net.consensys.linea.transactionexclusion.ModuleOverflow
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import net.consensys.linea.transactionexclusion.TransactionInfo
|
||||
import net.consensys.linea.transactionexclusion.test.defaultRejectedTransaction
|
||||
import net.consensys.trimToMillisecondPrecision
|
||||
import net.consensys.zkevm.persistence.db.DbHelper
|
||||
import net.consensys.zkevm.persistence.db.DuplicatedRecordException
|
||||
import net.consensys.zkevm.persistence.db.test.CleanDbTestSuiteParallel
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@ExtendWith(VertxExtension::class)
|
||||
class RejectedTransactionsPostgresDaoTest : CleanDbTestSuiteParallel() {
|
||||
init {
|
||||
target = "1"
|
||||
}
|
||||
|
||||
override val databaseName = DbHelper.generateUniqueDbName("tx-exclusion-api-rejectedtxns-dao-tests")
|
||||
private var fakeClock = FakeFixedClock(Clock.System.now())
|
||||
private lateinit var rejectedTransactionsPostgresDao: RejectedTransactionsPostgresDao
|
||||
private lateinit var notRejectedBefore: Instant
|
||||
|
||||
// Helper functions
|
||||
private fun createRejectedTransaction(
|
||||
txRejectionStage: RejectedTransaction.Stage = defaultRejectedTransaction.txRejectionStage,
|
||||
timestamp: Instant = fakeClock.now().minus(10.seconds),
|
||||
blockNumber: ULong? = defaultRejectedTransaction.blockNumber,
|
||||
transactionRLP: ByteArray = defaultRejectedTransaction.transactionRLP,
|
||||
reasonMessage: String = defaultRejectedTransaction.reasonMessage,
|
||||
overflows: List<ModuleOverflow> = defaultRejectedTransaction.overflows,
|
||||
transactionInfo: TransactionInfo = defaultRejectedTransaction.transactionInfo
|
||||
): RejectedTransaction {
|
||||
return RejectedTransaction(
|
||||
txRejectionStage = txRejectionStage,
|
||||
timestamp = timestamp.trimToMillisecondPrecision(),
|
||||
blockNumber = blockNumber,
|
||||
transactionRLP = transactionRLP,
|
||||
reasonMessage = reasonMessage,
|
||||
overflows = overflows,
|
||||
transactionInfo = transactionInfo
|
||||
)
|
||||
}
|
||||
|
||||
private fun dbTableContentQuery(dbTableName: String): PreparedQuery<RowSet<Row>> =
|
||||
sqlClient.preparedQuery("select * from $dbTableName")
|
||||
|
||||
private fun rejectedTransactionsTotalRows(): Int =
|
||||
dbTableContentQuery(RejectedTransactionsPostgresDao.rejectedTransactionsTable).execute().get().size()
|
||||
|
||||
private fun fullTransactionsTotalRows(): Int =
|
||||
dbTableContentQuery(RejectedTransactionsPostgresDao.fullTransactionsTable).execute().get().size()
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
fakeClock.setTimeTo(Clock.System.now())
|
||||
notRejectedBefore = fakeClock.now().minus(1.hours)
|
||||
rejectedTransactionsPostgresDao =
|
||||
RejectedTransactionsPostgresDao(
|
||||
readConnection = sqlClient,
|
||||
writeConnection = sqlClient,
|
||||
clock = fakeClock
|
||||
)
|
||||
}
|
||||
|
||||
private fun performInsertTest(
|
||||
rejectedTransaction: RejectedTransaction
|
||||
) {
|
||||
rejectedTransactionsPostgresDao.saveNewRejectedTransaction(rejectedTransaction).get()
|
||||
|
||||
// assert the corresponding record was inserted into the full_transactions table
|
||||
val newlyInsertedFullTxnsRows = dbTableContentQuery(RejectedTransactionsPostgresDao.fullTransactionsTable)
|
||||
.execute().get().filter { row ->
|
||||
row.getBuffer("tx_hash").bytes.contentEquals(rejectedTransaction.transactionInfo.hash)
|
||||
}
|
||||
assertThat(newlyInsertedFullTxnsRows.size).isEqualTo(1)
|
||||
assertThat(newlyInsertedFullTxnsRows.first().getBuffer("tx_rlp").bytes).isEqualTo(
|
||||
rejectedTransaction.transactionRLP
|
||||
)
|
||||
|
||||
// assert the corresponding record was inserted into the rejected_transactions table
|
||||
val newlyInsertedRejectedTxnsRows = dbTableContentQuery(RejectedTransactionsPostgresDao.rejectedTransactionsTable)
|
||||
.execute().get().filter { row ->
|
||||
row.getBuffer("tx_hash").bytes.contentEquals(rejectedTransaction.transactionInfo.hash) &&
|
||||
row.getString("reject_reason") == rejectedTransaction.reasonMessage
|
||||
}
|
||||
assertThat(newlyInsertedRejectedTxnsRows.size).isEqualTo(1)
|
||||
val insertedRow = newlyInsertedRejectedTxnsRows.first()
|
||||
assertThat(insertedRow.getLong("created_epoch_milli")).isEqualTo(
|
||||
fakeClock.now().toEpochMilliseconds()
|
||||
)
|
||||
assertThat(insertedRow.getString("reject_stage")).isEqualTo(
|
||||
RejectedTransactionsPostgresDao.rejectedStageToDbValue(rejectedTransaction.txRejectionStage)
|
||||
)
|
||||
assertThat(insertedRow.getLong("block_number") == rejectedTransaction.blockNumber?.toLong()).isTrue()
|
||||
assertThat(insertedRow.getJsonArray("overflows").encode()).isEqualTo(
|
||||
ObjectMapper().writeValueAsString(rejectedTransaction.overflows)
|
||||
)
|
||||
assertThat(insertedRow.getLong("reject_timestamp")).isEqualTo(
|
||||
rejectedTransaction.timestamp.toEpochMilliseconds()
|
||||
)
|
||||
assertThat(insertedRow.getBuffer("tx_from").bytes).isEqualTo(
|
||||
rejectedTransaction.transactionInfo.from
|
||||
)
|
||||
assertThat(insertedRow.getBuffer("tx_to").bytes).isEqualTo(
|
||||
rejectedTransaction.transactionInfo.to
|
||||
)
|
||||
assertThat(insertedRow.getLong("tx_nonce")).isEqualTo(
|
||||
rejectedTransaction.transactionInfo.nonce.toLong()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveNewRejectedTransaction inserts new rejected transaction to db`() {
|
||||
// insert a new rejected transaction
|
||||
performInsertTest(createRejectedTransaction())
|
||||
|
||||
// assert that the total number of rows in the two tables are correct
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(1)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveNewRejectedTransaction inserts new rejected transactions with same txHash but different reason to db`() {
|
||||
// insert a new rejected transaction
|
||||
performInsertTest(createRejectedTransaction())
|
||||
|
||||
// insert another rejected transaction with same txHash but different reason
|
||||
performInsertTest(
|
||||
createRejectedTransaction(
|
||||
txRejectionStage = RejectedTransaction.Stage.P2P,
|
||||
blockNumber = null,
|
||||
reasonMessage = "Transaction line count for module MUL=587 is above the limit 401",
|
||||
overflows = listOf(
|
||||
ModuleOverflow(
|
||||
module = "MUL",
|
||||
count = 587,
|
||||
limit = 401
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// assert that the total number of rows in the two tables are correct
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(2)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveNewRejectedTransaction throws error when inserting rejected transactions with same txHash and reason`() {
|
||||
// insert a new rejected transaction
|
||||
performInsertTest(createRejectedTransaction())
|
||||
|
||||
// another rejected transaction with same txHash and reason
|
||||
val duplicatedRejectedTransaction = createRejectedTransaction(
|
||||
txRejectionStage = RejectedTransaction.Stage.P2P,
|
||||
blockNumber = null,
|
||||
overflows = listOf(
|
||||
ModuleOverflow(
|
||||
module = "ADD",
|
||||
count = 587,
|
||||
limit = 401
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// assert that the insertion of duplicatedRejectedTransaction would trigger DuplicatedRecordException error
|
||||
assertThrows<ExecutionException> {
|
||||
rejectedTransactionsPostgresDao.saveNewRejectedTransaction(duplicatedRejectedTransaction).get()
|
||||
}.also { executionException ->
|
||||
assertThat(executionException.cause).isInstanceOf(DuplicatedRecordException::class.java)
|
||||
assertThat(executionException.cause!!.message)
|
||||
.isEqualTo(
|
||||
"RejectedTransaction ${duplicatedRejectedTransaction.transactionInfo.hash.encodeHex()} " +
|
||||
"is already persisted!"
|
||||
)
|
||||
}
|
||||
|
||||
// assert that the total number of rows in the two tables are correct
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(1)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findRejectedTransactionByTxHash returns rejected transaction with most recent timestamp from db`() {
|
||||
// insert a new rejected transaction
|
||||
val oldestRejectedTransaction = createRejectedTransaction(
|
||||
timestamp = fakeClock.now().minus(10.seconds)
|
||||
)
|
||||
performInsertTest(oldestRejectedTransaction)
|
||||
|
||||
// insert another rejected transaction with same txHash but different reason and
|
||||
// with a more recent timestamp
|
||||
performInsertTest(
|
||||
createRejectedTransaction(
|
||||
reasonMessage = "Transaction line count for module MUL=587 is above the limit 401",
|
||||
timestamp = fakeClock.now().minus(9.seconds)
|
||||
)
|
||||
)
|
||||
|
||||
// insert another rejected transaction with same txHash but different reason
|
||||
// and with the most recent timestamp
|
||||
val newestRejectedTransaction = createRejectedTransaction(
|
||||
reasonMessage = "Transaction line count for module EXP=9000 is above the limit 8192",
|
||||
timestamp = fakeClock.now().minus(8.seconds)
|
||||
)
|
||||
performInsertTest(newestRejectedTransaction)
|
||||
|
||||
// find the rejected transaction with the txHash
|
||||
val foundRejectedTransaction = rejectedTransactionsPostgresDao.findRejectedTransactionByTxHash(
|
||||
oldestRejectedTransaction.transactionInfo.hash
|
||||
).get()
|
||||
|
||||
// assert that the found rejected transaction is the same as the one with most recent timestamp
|
||||
assertThat(foundRejectedTransaction).isEqualTo(newestRejectedTransaction)
|
||||
|
||||
// assert that the total number of rows in the two tables are correct
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(3)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `findRejectedTransactionByTxHash returns null as rejected timestamp exceeds queryable window`() {
|
||||
// insert a new rejected transaction with timestamp exceeds the 1-hour queryable window
|
||||
val rejectedTransaction = createRejectedTransaction(
|
||||
timestamp = fakeClock.now().minus(1.hours).minus(1.seconds)
|
||||
)
|
||||
performInsertTest(rejectedTransaction)
|
||||
|
||||
// find the rejected transaction with the txHash
|
||||
val foundRejectedTransaction = rejectedTransactionsPostgresDao.findRejectedTransactionByTxHash(
|
||||
rejectedTransaction.transactionInfo.hash,
|
||||
notRejectedBefore
|
||||
).get()
|
||||
|
||||
// assert that null is returned from the find method
|
||||
assertThat(foundRejectedTransaction).isNull()
|
||||
|
||||
// assert that the total number of rows in the two tables are both one which
|
||||
// implies the rejected transaction is still present in db
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(1)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteRejectedTransactions returns 2 row deleted as created timestamp exceeds storage window`() {
|
||||
// insert a new rejected transaction
|
||||
performInsertTest(
|
||||
createRejectedTransaction(
|
||||
timestamp = fakeClock.now()
|
||||
)
|
||||
)
|
||||
// advance the fake clock to make its created timestamp exceeds the 10-hours storage window
|
||||
fakeClock.advanceBy(1.hours)
|
||||
|
||||
// insert another rejected transaction with same txHash but different reason
|
||||
performInsertTest(
|
||||
createRejectedTransaction(
|
||||
reasonMessage = "Transaction line count for module EXP=9000 is above the limit 8192",
|
||||
timestamp = fakeClock.now()
|
||||
)
|
||||
)
|
||||
// advance the fake clock to make its created timestamp just within the 10-hours storage window
|
||||
fakeClock.advanceBy(1.hours)
|
||||
|
||||
// insert another rejected transaction with different txHash and reason
|
||||
performInsertTest(
|
||||
createRejectedTransaction(
|
||||
transactionInfo = TransactionInfo(
|
||||
hash = "0x078ecd6f00bff4beca9116ca85c65ddd265971e415d7df7a96b3c10424b031e2".decodeHex(),
|
||||
from = "0x4d144d7b9c96b26361d6ac74dd1d8267edca4fc2".decodeHex(),
|
||||
to = "0x1195cf65f83b3a5768f3c496d3a05ad6412c64b3".decodeHex(),
|
||||
nonce = 101UL
|
||||
),
|
||||
reasonMessage = "Transaction line count for module EXP=10000 is above the limit 8192",
|
||||
timestamp = fakeClock.now()
|
||||
)
|
||||
)
|
||||
// advance the fake clock to make its created timestamp within the 10-hours storage window
|
||||
fakeClock.advanceBy(9.hours)
|
||||
|
||||
// assert that the total number of rows in the two tables are both three which
|
||||
// implies all the rejected transactions above are present in db
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(3)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(2)
|
||||
|
||||
// delete the rejected transactions with storage window as 10 hours from now
|
||||
val deletedRows = rejectedTransactionsPostgresDao.deleteRejectedTransactions(
|
||||
fakeClock.now().minus(10.hours)
|
||||
).get()
|
||||
|
||||
// assert that number of total deleted rows is just one
|
||||
assertThat(deletedRows).isEqualTo(1)
|
||||
|
||||
// assert that the total number of rows in the two tables are both two which
|
||||
// implies only the rejected transactions with created timestamp exceeds
|
||||
// the storage window was deleted
|
||||
assertThat(rejectedTransactionsTotalRows()).isEqualTo(2)
|
||||
assertThat(fullTransactionsTotalRows()).isEqualTo(2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="warn">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="net.consensys.zkevm.persistence.dao.rejectedtransaction" level="trace" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Root level="info" additivity="false">
|
||||
<appender-ref ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.consensys.zkevm.persistence.dao.rejectedtransaction
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import net.consensys.linea.transactionexclusion.RejectedTransaction
|
||||
import tech.pegasys.teku.infrastructure.async.SafeFuture
|
||||
|
||||
interface RejectedTransactionsDao {
|
||||
fun saveNewRejectedTransaction(
|
||||
rejectedTransaction: RejectedTransaction
|
||||
): SafeFuture<Unit>
|
||||
|
||||
fun findRejectedTransactionByTxHash(
|
||||
txHash: ByteArray,
|
||||
notRejectedBefore: Instant = Instant.DISTANT_PAST
|
||||
): SafeFuture<RejectedTransaction?>
|
||||
|
||||
fun deleteRejectedTransactions(
|
||||
createdBefore: Instant
|
||||
): SafeFuture<Int>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user