diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..9b28445ce04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + + # Maintain dependencies for Docker + - package-ecosystem: docker + directory: / + schedule: + interval: daily \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index dbfec017604..a40d4e4690a 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -10,14 +10,17 @@ on: branches: - "**" +# Declare default permissions as read only. +permissions: read-all + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' @@ -28,7 +31,7 @@ jobs: curl -sSL https://secchannel.rsk.co/SUPPORT.asc | gpg2 --import - gpg2 --verify SHA256SUMS.asc && sha256sum --check SHA256SUMS.asc - - uses: actions/cache@v4 + - uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Cache Gradle Wrapper id: cache-gradle-wrapper with: @@ -46,7 +49,7 @@ jobs: ./gradlew --no-daemon --stacktrace build -x test - name: Archive build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 #v4.4.3 with: name: build-files path: | @@ -56,18 +59,18 @@ jobs: needs: unit-tests-java17 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 with: fetch-depth: 0 - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Restore Gradle Wrapper with: path: | @@ -76,21 +79,21 @@ jobs: fail-on-cache-miss: true - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 with: name: build-files path: | rskj-core/build - name: Download test results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 with: name: test-results path: | rskj-core/build/test-results/ - name: Download test reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 with: name: test-reports path: | @@ -104,18 +107,23 @@ jobs: GH_PR_HEAD_REF: ${{ github.head_ref }} GH_REF: ${{ github.ref }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + IS_FORK: ${{ github.event.pull_request.head.repo.fork }} run: | - if [ "$GH_EVENT" = pull_request ]; then - ./gradlew -Dorg.gradle.jvmargs=-Xmx5g sonarqube --no-daemon -x build -x test \ - -Dsonar.pullrequest.base="$GH_PR_BASE_REF" \ - -Dsonar.pullrequest.branch="$GH_PR_HEAD_REF" \ - -Dsonar.pullrequest.key="$GH_PR_NUMBER" \ - -Dsonar.organization=rsksmart \ - -Dsonar.projectKey=rskj \ - -Dsonar.host.url="https://sonarcloud.io" \ - -Dsonar.junit.reportPaths=rskj-core/build/test-results/ \ - -Dsonar.coverage.jacoco.xmlReportPaths=rskj-core/build/reports/jacoco/test/jacocoTestReport.xml \ - -Dsonar.token="$SONAR_TOKEN" + if [ "$GH_EVENT" = "pull_request" ]; then + if [ "$IS_FORK" != "true" ]; then + ./gradlew -Dorg.gradle.jvmargs=-Xmx5g sonarqube --no-daemon -x build -x test \ + -Dsonar.pullrequest.base="$GH_PR_BASE_REF" \ + -Dsonar.pullrequest.branch="$GH_PR_HEAD_REF" \ + -Dsonar.pullrequest.key="$GH_PR_NUMBER" \ + -Dsonar.organization=rsksmart \ + -Dsonar.projectKey=rskj \ + -Dsonar.host.url="https://sonarcloud.io" \ + -Dsonar.junit.reportPaths=rskj-core/build/test-results/ \ + -Dsonar.coverage.jacoco.xmlReportPaths=rskj-core/build/reports/jacoco/test/jacocoTestReport.xml \ + -Dsonar.token="$SONAR_TOKEN" + else + echo "Skipping SonarQube analysis for pull request from a forked repo." + fi else ./gradlew -Dorg.gradle.jvmargs=-Xmx5g sonarqube --no-daemon -x build -x test \ -Dsonar.branch.name="$GH_REF" \ @@ -148,17 +156,17 @@ jobs: options: --name bitcoind2 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 #v4.0.4 with: node-version: '12.x' - name: Check Node.js version run: node --version - name: Checkout Mining Integration Tests Repository - uses: actions/checkout@v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 with: repository: rsksmart/mining-integration-tests ref: ${{ secrets.MINING_INTEGRATION_TESTS_REF }} @@ -181,13 +189,13 @@ jobs: node --unhandled-rejections=strict generateBtcBlocks.js - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Restore Gradle Wrapper with: path: | @@ -196,7 +204,7 @@ jobs: fail-on-cache-miss: true - name: Download build artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #v4.1.8 with: name: build-files path: | @@ -231,16 +239,16 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Restore Gradle Wrapper with: path: | @@ -253,14 +261,14 @@ jobs: ./gradlew --no-daemon --stacktrace test - name: Persist test results for sonar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 #v4.4.3 with: name: test-results path: | rskj-core/build/test-results/ - name: Persist test reports for sonar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 #v4.4.3 with: name: test-reports path: | @@ -270,16 +278,16 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '21' distribution: 'temurin' cache: 'gradle' - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Restore Gradle Wrapper with: path: | @@ -295,16 +303,16 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Java & Gradle - uses: actions/setup-java@v4 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - - uses: actions/cache/restore@v4 + - uses: actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 #v4.1.1 name: Restore Gradle Wrapper with: path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 637f0af13e5..5fefc04bd26 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,13 +8,14 @@ on: schedule: - cron: "0 0 * * *" +# Declare default permissions as read only. +permissions: read-all + jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: - actions: read - contents: read security-events: write strategy: @@ -24,11 +25,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Setup Java JDK if: ${{ matrix.language == 'java' }} - uses: actions/setup-java@v3 + uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 #v4.4.0 with: java-version: '17' distribution: 'temurin' @@ -38,15 +39,15 @@ jobs: run: ./configure.sh - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@f779452ac5af1c261dce0346a8f964149f49322b #v3.26.13 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@f779452ac5af1c261dce0346a8f964149f49322b #v3.26.13 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@f779452ac5af1c261dce0346a8f964149f49322b #v3.26.13 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000000..e513e55f8ed --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: read-all + +jobs: + dependency-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: 'Checkout Repository' + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + + - name: 'Dependency Review' + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + with: + fail-on-severity: high + comment-summary-in-pr: true \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a05727d7ed9..24642b3453f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,17 +7,20 @@ on: tags: - '*' +# Declare default permissions as read only. +permissions: read-all + jobs: docker: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 #v5.5.1 with: images: rsksmart/rskj tags: | @@ -28,13 +31,13 @@ jobs: type=match,pattern=(\w+-\d+)\.\d+\.\d+.*,group=1 - name: DockerHub login - uses: docker/login-action@v2 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 #v6.9.0 with: context: . push: true diff --git a/.github/workflows/rit.yml b/.github/workflows/rit.yml index 889ea657130..98e6c8dfb77 100644 --- a/.github/workflows/rit.yml +++ b/.github/workflows/rit.yml @@ -1,6 +1,8 @@ name: Rootstock Integration Tests on: + schedule: + - cron: '0 0 * * *' pull_request: types: [ opened, synchronize, reopened ] branches: ["master", "*-rc"] @@ -15,6 +17,9 @@ on: required: false default: 'master' +# Declare default permissions as read only. +permissions: read-all + jobs: rootstock-integration-tests: name: Rootstock Integration Tests @@ -22,7 +27,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout Repository # Step needed to access the PR description using github CLI - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 - name: Set Branch Variables id: set-branch-variables @@ -34,10 +39,12 @@ jobs: github_event_pull_request_number: ${{ github.event.pull_request.number }} github_head_ref: ${{ github.head_ref }} github_ref_name: ${{ github.ref_name }} + github_event_pull_request_head_repo_owner_login: ${{ github.event.pull_request.head.repo.owner.login }} + github_repository_owner: ${{ github.repository_owner }} run: | PR_DESCRIPTION=pr-description.txt - ALLOWED_BRANCH_CHARACTERS='[-./0-9A-Z_a-z]' + ALLOWED_BRANCH_CHARACTERS='[-+./0-9A-Z_a-z]' default_rskj_branch=master default_powpeg_branch=master @@ -94,6 +101,11 @@ jobs: exit 1 fi + # Set the Repo Owner + REPO_OWNER="${github_event_pull_request_head_repo_owner_login:-$github_repository_owner}" + + + echo "REPO_OWNER=$REPO_OWNER" >> $GITHUB_ENV echo "RSKJ_BRANCH=$RSKJ_BRANCH" >> $GITHUB_ENV echo "RIT_BRANCH=$RIT_BRANCH" >> $GITHUB_ENV echo "POWPEG_BRANCH=$POWPEG_BRANCH" >> $GITHUB_ENV @@ -104,27 +116,29 @@ jobs: BUILD_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" echo "BUILD_URL=$BUILD_URL" >> $GITHUB_ENV - - name: Sanitize Branch Name - id: sanitize-branch-name + - name: Sanitize Github Variables + id: sanitize-github-variables env: - GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_EVENT_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} run: | - # Delete non-alphanumeric characters and limit to 255 chars which is the branch limit in GitHub - SAFE_BRANCH_NAME=$(echo "${GITHUB_HEAD_REF}" | tr -cd '[:alnum:]_-' | cut -c1-255) - echo "SAFE_BRANCH_NAME=$SAFE_BRANCH_NAME" >> $GITHUB_ENV + # Delete non-alphanumeric characters and limit to 75 chars which is the branch title limit in GitHub + SAFE_PULL_REQUEST_TITLE=$(echo "${GITHUB_EVENT_PULL_REQUEST_TITLE}" | tr -cd '[:alnum:]_ -' | cut -c1-75) + echo "SAFE_PULL_REQUEST_TITLE=$SAFE_PULL_REQUEST_TITLE" >> $GITHUB_ENV - name: Run Rootstock Integration Tests - uses: rsksmart/rootstock-integration-tests@497172fd38dcfaf48c77f9bb1eeb6617eef5eed6 #v1 + uses: rsksmart/rootstock-integration-tests@e86332474179a63f027d0fe969687d3d24f34c29 #v1 with: rskj-branch: ${{ env.RSKJ_BRANCH }} powpeg-node-branch: ${{ env.POWPEG_BRANCH }} rit-branch: ${{ env.RIT_BRANCH }} + repo-owner: ${{ env.REPO_OWNER }} - name: Send Slack Notification on Success if: success() && github.event.pull_request.head.repo.owner.login == 'rsksmart' uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 env: SLACK_BOT_TOKEN: ${{ secrets.GHA_SLACK_NOTIFICATION_TOKEN }} + GITHUB_EVENT_PULL_REQUEST_HTML_URL: ${{ github.event.pull_request.html_url }} with: channel-id: ${{ vars.GHA_SLACK_NOTIFICATION_CHANNEL }} payload: | @@ -132,7 +146,7 @@ jobs: "attachments": [ { "color": "good", - "text": "OK: :+1: *Pull request*: ${{ env.SAFE_BRANCH_NAME }} - [#${{ github.run_number }}] - (${{ env.BUILD_URL }}) - *Branches used* [rskj:`rsksmart#${{ env.RSKJ_BRANCH }}`] [fed:`${{ env.POWPEG_BRANCH }}`] [rootstock-integration-tests:`${{ env.RIT_BRANCH }}`]" + "text": "*PASSED*: :white_check_mark: - *${{ env.SAFE_PULL_REQUEST_TITLE }}* \n*Pull request*: ${{ env.GITHUB_EVENT_PULL_REQUEST_HTML_URL }} \n*Pipeline*: ${{ env.BUILD_URL }} \n*Branches used*: [ rskj:`${{ env.RSKJ_BRANCH }}` ] [ fed:`${{ env.POWPEG_BRANCH }}` ] [ rit:`${{ env.RIT_BRANCH }}` ]" } ] } @@ -142,6 +156,7 @@ jobs: uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 env: SLACK_BOT_TOKEN: ${{ secrets.GHA_SLACK_NOTIFICATION_TOKEN }} + GITHUB_EVENT_PULL_REQUEST_HTML_URL: ${{ github.event.pull_request.html_url }} with: channel-id: ${{ vars.GHA_SLACK_NOTIFICATION_CHANNEL }} payload: | @@ -149,7 +164,7 @@ jobs: "attachments": [ { "color": "danger", - "text": "FAILED: :robot_face: *Pull request*: ${{ env.SAFE_BRANCH_NAME }} - [#${{ github.run_number }}] - (${{ env.BUILD_URL }}) - *Branches used* [rskj:`rsksmart#${{ env.RSKJ_BRANCH }}`] [fed:`${{ env.POWPEG_BRANCH }}`] [rootstock-integration-tests:`${{ env.RIT_BRANCH }}`]" + "text": "*FAILED*: :x: - *${{ env.SAFE_PULL_REQUEST_TITLE }}* \n*Pull request*: ${{ env.GITHUB_EVENT_PULL_REQUEST_HTML_URL }} \n*Pipeline*: ${{ env.BUILD_URL }} \n*Branches used*: [ rskj:`${{ env.RSKJ_BRANCH }}` ] [ fed:`${{ env.POWPEG_BRANCH }}` ] [ rit:`${{ env.RIT_BRANCH }}` ]" } ] } diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..d357502f8fa --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,47 @@ +name: Scorecard supply-chain security +on: + branch_protection_rule: + schedule: + - cron: '33 2 * * 2' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 #v4.2.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 #v4.4.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + with: + sarif_file: results.sarif diff --git a/Dockerfile b/Dockerfile index 8d75e05395c..e5cb31e7aab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk AS build +FROM eclipse-temurin:17-jdk@sha256:08295ab0f5007a37cbcc6679a8447a7278d9403f9f82acd80ed08cd10921e026 AS build RUN apt-get update -y && \ apt-get install -y git curl gnupg @@ -19,8 +19,8 @@ RUN gpg --keyserver https://secchannel.rsk.co/SUPPORT.asc --recv-keys 1DC9157991 modifier=$(sed -n 's/^modifier=//p' "$file" | tr -d "\"'") && \ cp "rskj-core/build/libs/rskj-core-$version_number-$modifier-all.jar" rsk.jar -FROM eclipse-temurin:17-jre -LABEL org.opencontainers.image.authors="ops@iovlabs.org" +FROM eclipse-temurin:17-jre@sha256:f1515395c0695910a3ca665e973cc11013d1f50d265e61cb8c9156e999d914b4 +LABEL org.opencontainers.image.authors="ops@rootstocklabs.com" RUN useradd -ms /sbin/nologin -d /var/lib/rsk rsk USER rsk diff --git a/README.md b/README.md index 5c3c672e3f9..a671b8378e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Welcome to RskJ -[![CircleCI](https://circleci.com/gh/rsksmart/rskj/tree/master.svg?style=svg)](https://circleci.com/gh/rsksmart/rskj/tree/master) +[![Build and Test](https://github.com/rsksmart/rskj/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/rsksmart/rskj/actions/workflows/build_and_test.yml) +[![Rootstock Integration Tests](https://github.com/rsksmart/rskj/actions/workflows/rit.yml/badge.svg)](https://github.com/rsksmart/rskj/actions/workflows/rit.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=rskj&metric=alert_status)](https://sonarcloud.io/dashboard?id=rskj) [![CodeQL](https://github.com/rsksmart/rskj/workflows/CodeQL/badge.svg)](https://github.com/rsksmart/rskj/actions?query=workflow%3ACodeQL) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/rsksmart/rskj/badge)](https://scorecard.dev/viewer/?uri=github.com/rsksmart/rskj) # About RskJ is a Java implementation of the Rootstock node. For more information about Rootstock, visit [rootstock.io](https://rootstock.io/). The [Rootstock white paper](https://rootstock.io/rsk-white-paper-updated.pdf) provides a complete conceptual overview of the platform. diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 08400597a2e..510e633ddef 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -46,14 +46,6 @@ - - - - - - - - diff --git a/rskj-core/build.gradle b/rskj-core/build.gradle index 8deb7a17fdc..0078115a127 100644 --- a/rskj-core/build.gradle +++ b/rskj-core/build.gradle @@ -123,7 +123,7 @@ ext { jaxwsRtVer : '2.3.5', picocliVer : '4.6.3', - bitcoinjThinVer: '0.14.4-rsk-15', + bitcoinjThinVer: '0.14.4-rsk-16-SNAPSHOT', rskjNativeVer: '1.3.0', ] diff --git a/rskj-core/src/integrationTest/java/co/rsk/util/OkHttpClientTestFixture.java b/rskj-core/src/integrationTest/java/co/rsk/util/OkHttpClientTestFixture.java index 7c779a74416..e9c660549cd 100644 --- a/rskj-core/src/integrationTest/java/co/rsk/util/OkHttpClientTestFixture.java +++ b/rskj-core/src/integrationTest/java/co/rsk/util/OkHttpClientTestFixture.java @@ -40,6 +40,31 @@ public class OkHttpClientTestFixture { " \"jsonrpc\": \"2.0\"\n" + "}]"; + public static final String ETH_GET_BLOCK_BY_NUMBER = + "{\n" + + " \"method\": \"eth_getBlockByNumber\",\n" + + " \"params\": [\n" + + " \"\",\n" + + " true\n" + + " ],\n" + + " \"id\": 1,\n" + + " \"jsonrpc\": \"2.0\"\n" + + "}"; + + public static final String ETH_SEND_TRANSACTION = + "{\n" + + " \"jsonrpc\": \"2.0\",\n" + + " \"method\": \"eth_sendTransaction\",\n" + + " \"id\": 1,\n" + + " \"params\": [{\n" + + " \"from\": \"\",\n" + + " \"to\": \"\",\n" + + " \"gas\": \"\",\n" + + " \"gasPrice\": \"\",\n" + + " \"value\": \"\"\n" + + " }]\n" + + "}"; + private OkHttpClientTestFixture() { } @@ -105,4 +130,9 @@ public static JsonNode getJsonResponseForGetBestBlockMessage(int port, String bl Response response = sendJsonRpcGetBlockMessage(port, blockNumOrTag); return new ObjectMapper().readTree(response.body().string()); } + + public static String getEnvelopedMethodCalls(String... methodCall) { + return "[\n" + String.join(",\n", methodCall) + "]"; + } + } diff --git a/rskj-core/src/integrationTest/java/pte/PteIntegrationTest.java b/rskj-core/src/integrationTest/java/pte/PteIntegrationTest.java new file mode 100644 index 00000000000..fefc75b6d83 --- /dev/null +++ b/rskj-core/src/integrationTest/java/pte/PteIntegrationTest.java @@ -0,0 +1,244 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package pte; + +import co.rsk.util.OkHttpClientTestFixture; +import co.rsk.util.cli.CommandLineFixture; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.squareup.okhttp.Response; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Stream; + +import static co.rsk.util.OkHttpClientTestFixture.*; + +public class PteIntegrationTest { + + /* + When running this test locally, don't forget to build the .jar for the code you're trying to + test ('./gradlew clean' and './gradlew assemble' should be sufficient for most cases). + */ + + private static final int RPC_PORT = 9999; + private static final int MAX_BLOCKS_TO_GET = 20; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private String buildLibsPath; + private String jarName; + private String strBaseArgs; + private String baseJavaCmd; + + @TempDir + private Path tempDir; + + @BeforeEach + public void setup() throws IOException { + String projectPath = System.getProperty("user.dir"); + buildLibsPath = String.format("%s/build/libs", projectPath); + String integrationTestResourcesPath = String.format("%s/src/integrationTest/resources", projectPath); + String logbackXmlFile = String.format("%s/logback.xml", integrationTestResourcesPath); + String rskConfFile = String.format("%s/pte-integration-test-rskj.conf", integrationTestResourcesPath); + Stream pathsStream = Files.list(Paths.get(buildLibsPath)); + jarName = pathsStream.filter(p -> !p.toFile().isDirectory()) + .map(p -> p.getFileName().toString()) + .filter(fn -> fn.endsWith("-all.jar")) + .findFirst() + .orElse(""); + + Path databaseDirPath = tempDir.resolve("database"); + String databaseDir = databaseDirPath.toString(); + String[] baseArgs = new String[]{ + String.format("-Xdatabase.dir=%s", databaseDir), + "--regtest", + String.format("-Xrpc.providers.web.http.port=%s", RPC_PORT) + }; + strBaseArgs = String.join(" ", baseArgs); + baseJavaCmd = String.format("java %s %s", String.format("-Dlogback.configurationFile=%s", logbackXmlFile), String.format("-Drsk.conf.file=%s", rskConfFile)); + } + + @Test + void whenParallelizableTransactionsAreSent_someAreExecutedInParallel() throws Exception { + + // Given + + // Pre-funded Test Accounts on Regtest + List accounts = Arrays.asList( + "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "0x7986b3df570230288501eea3d890bd66948c9b79", + "0x0a3aa774752ec2042c46548456c094a76c7f3a79", + "0xcf7cdbbb5f7ba79d3ffe74a0bba13fc0295f6036", + "0x39b12c05e8503356e3a7df0b7b33efa4c054c409", + "0xc354d97642faa06781b76ffb6786f72cd7746c97", + "0xdebe71e1de41fc77c44df4b6db940026e31b0e71", + "0x7857288e171c6159c5576d1bd9ac40c0c48a771c", + "0xa4dea4d5c954f5fd9e87f0e9752911e83a3d18b3", + "0x09a1eda29f664ac8f68106f6567276df0c65d859", + "0xec4ddeb4380ad69b3e509baad9f158cdf4e4681d" + ); + + Map txResponseMap = new HashMap<>(); + Map> blocksResponseMap = new HashMap<>(); + + String cmd = String.format( + "%s -cp %s/%s co.rsk.Start --reset %s", + baseJavaCmd, + buildLibsPath, + jarName, + strBaseArgs); + + // When + + CommandLineFixture.runCommand( + cmd, + 1, + TimeUnit.MINUTES, + proc -> { + try { + + // Send bulk transactions + + Response txResponse = sendBulkTransactions( + accounts.get(0), accounts.get(1), accounts.get(2), accounts.get(3), + accounts.get(4), accounts.get(5), accounts.get(6), accounts.get(7)); + + txResponseMap.put("bulkTransactionsResponse", txResponse); + + // Await for n blocks to be mined and return them + + Future> future = getBlocksAsync(); + + try { + + blocksResponseMap.put("asyncBlocksResult", future.get()); + + } catch (ExecutionException | InterruptedException e) { + Assertions.fail(e); + } + + } catch (IOException e) { + Assertions.fail(e); + } + } + ); + + // Then + + Assertions.assertEquals(200, txResponseMap.get("bulkTransactionsResponse").code()); + + Map blocksResult = blocksResponseMap.get("asyncBlocksResult"); + Assertions.assertEquals(MAX_BLOCKS_TO_GET, blocksResult.size()); + + boolean pteFound = false; + int i = blocksResult.size(); // Start from the last element (optimization) + while (!pteFound && i >= 0) { + i -= 1; + + JsonNode blockResponse = objectMapper.readTree(blocksResult.get(i)); + JsonNode result = blockResponse.get(0).get("result"); + + if (!result.isNull()) { + JsonNode pteEdges = result.get("rskPteEdges"); + if (pteEdges.isArray() && pteEdges.size() > 0) { + Assertions.assertTrue(result.get("transactions").isArray()); + Assertions.assertTrue(result.get("transactions").size() > 1); + pteFound = true; + } + } + } + + Assertions.assertTrue(pteFound); + + } + + private Response getBlockByNumber(String number) throws IOException { + String content = getEnvelopedMethodCalls( + ETH_GET_BLOCK_BY_NUMBER.replace( + "", + number) + ); + + System.out.println(content); + + return OkHttpClientTestFixture.sendJsonRpcMessage(content, RPC_PORT); + } + + private Response sendBulkTransactions( + String addressFrom1, String addressTo1, + String addressFrom2, String addressTo2, + String addressFrom3, String addressTo3, + String addressFrom4, String addressTo4) throws IOException { + + String gas = "0x9C40"; + String gasPrice = "0x10"; + String value = "0x500"; + + String[] placeholders = new String[]{ + "", "", "", + "", "" + }; + + String content = getEnvelopedMethodCalls( + StringUtils.replaceEach(ETH_SEND_TRANSACTION, placeholders, + new String[]{addressFrom1, addressTo1, gas, gasPrice, value}), + StringUtils.replaceEach(ETH_SEND_TRANSACTION, placeholders, + new String[]{addressFrom2, addressTo2, gas, gasPrice, value}), + StringUtils.replaceEach(ETH_SEND_TRANSACTION, placeholders, + new String[]{addressFrom3, addressTo3, gas, gasPrice, value}), + StringUtils.replaceEach(ETH_SEND_TRANSACTION, placeholders, + new String[]{addressFrom4, addressTo4, gas, gasPrice, value}) + ); + + System.out.println(content); + + return OkHttpClientTestFixture.sendJsonRpcMessage(content, RPC_PORT); + } + + private Future> getBlocksAsync() { + CompletableFuture> completableFuture = new CompletableFuture<>(); + + Executors.newCachedThreadPool().submit(() -> { + Map results = new HashMap<>(); + + for (int i = 0; i < MAX_BLOCKS_TO_GET; i++) { + String response = getBlockByNumber("0x" + String.format("%02x", i)).body().string(); + + results.put(i, response); + Thread.sleep(500); + } + + completableFuture.complete(results); + return null; + }); + + return completableFuture; + } + +} \ No newline at end of file diff --git a/rskj-core/src/integrationTest/resources/pte-integration-test-rskj.conf b/rskj-core/src/integrationTest/resources/pte-integration-test-rskj.conf new file mode 100644 index 00000000000..3e6f988dfb3 --- /dev/null +++ b/rskj-core/src/integrationTest/resources/pte-integration-test-rskj.conf @@ -0,0 +1,34 @@ +miner { + + server { + enabled = true + updateWorkOnNewTransaction = true + } + + client { + enabled = true + automine = false + delayBetweenBlocks = 5 seconds + delayBetweenRefreshes = 1 second + } + +} + +peer { + + discovery = { + + # if peer discovery is off + # the peer window will show + # only what retrieved by active + # peer [true/false] + enabled = false + + # List of the peers to start + # the search of the online peers + # values: [ip:port] + ip.list = [] + + } + +} \ No newline at end of file diff --git a/rskj-core/src/jmh/java/co/rsk/jmh/web3/BenchmarkWeb3.java b/rskj-core/src/jmh/java/co/rsk/jmh/web3/BenchmarkWeb3.java index d3e722a02c8..02968cc727a 100644 --- a/rskj-core/src/jmh/java/co/rsk/jmh/web3/BenchmarkWeb3.java +++ b/rskj-core/src/jmh/java/co/rsk/jmh/web3/BenchmarkWeb3.java @@ -170,13 +170,25 @@ public void debugTraceTransaction_Params_ContractCall(DebugPlan plan) throws Ben @Benchmark @Timeout(time = 30) public void debugTraceBlockByHash(DebugPlan plan) throws BenchmarkWeb3Exception { - plan.getWeb3Connector().debugTraceBlockByHash(plan.getBlock()); + plan.getWeb3Connector().debugTraceBlockByHash(plan.getBlockHash()); } @Benchmark @Timeout(time = 30) public void debugTraceBlockByHash_Params(DebugPlan plan) throws BenchmarkWeb3Exception { - plan.getWeb3Connector().debugTraceBlockByHash(plan.getBlock()); + plan.getWeb3Connector().debugTraceBlockByHash(plan.getBlockHash(), plan.getDebugParams()); + } + + @Benchmark + @Timeout(time = 30) + public void debugTraceBlockByNumber(DebugPlan plan) throws BenchmarkWeb3Exception { + plan.getWeb3Connector().debugTraceBlockByNumber(plan.getBlockNumber()); + } + + @Benchmark + @Timeout(time = 30) + public void debugTraceBlockByNumber_Params(DebugPlan plan) throws BenchmarkWeb3Exception { + plan.getWeb3Connector().debugTraceBlockByNumber(plan.getBlockTag(), plan.getDebugParams()); } @Benchmark diff --git a/rskj-core/src/jmh/java/co/rsk/jmh/web3/Web3Connector.java b/rskj-core/src/jmh/java/co/rsk/jmh/web3/Web3Connector.java index ef030d1e1c1..482b5e61d01 100644 --- a/rskj-core/src/jmh/java/co/rsk/jmh/web3/Web3Connector.java +++ b/rskj-core/src/jmh/java/co/rsk/jmh/web3/Web3Connector.java @@ -91,6 +91,10 @@ public interface Web3Connector { JsonNode debugTraceBlockByHash(String txHash, Map params) throws HttpRpcException; + JsonNode debugTraceBlockByNumber(String bnOrId) throws HttpRpcException; + + JsonNode debugTraceBlockByNumber(String bnOrId, Map params) throws HttpRpcException; + EthAccounts ethAccounts() throws HttpRpcException; EthHashrate ethHashrate() throws HttpRpcException; diff --git a/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/RskDebugModuleWeb3j.java b/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/RskDebugModuleWeb3j.java index 609538fdc93..8d783f53a4d 100644 --- a/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/RskDebugModuleWeb3j.java +++ b/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/RskDebugModuleWeb3j.java @@ -47,4 +47,12 @@ public Request debugTraceBlockByHash(Stri public Request debugTraceBlockByHash(String txHash, Map params) { return new Request<>("debug_traceBlockByHash", Arrays.asList(txHash, params), web3jService, RskModuleWeb3j.GenericJsonResponse.class); } + + public Request debugTraceBlockByNumber(String bnOrId) { + return new Request<>("debug_traceBlockByNumber", Collections.singletonList(bnOrId), web3jService, RskModuleWeb3j.GenericJsonResponse.class); + } + + public Request debugTraceBlockByNumber(String bnOrId, Map params) { + return new Request<>("debug_traceBlockByNumber", Arrays.asList(bnOrId, params), web3jService, RskModuleWeb3j.GenericJsonResponse.class); + } } diff --git a/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/Web3ConnectorE2E.java b/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/Web3ConnectorE2E.java index 3693fc7b85d..6c3e5a10959 100644 --- a/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/Web3ConnectorE2E.java +++ b/rskj-core/src/jmh/java/co/rsk/jmh/web3/e2e/Web3ConnectorE2E.java @@ -354,6 +354,28 @@ public JsonNode debugTraceBlockByHash(String txHash, Map params) } } + @Override + public JsonNode debugTraceBlockByNumber(String bnOrId) throws HttpRpcException { + try { + RskModuleWeb3j.GenericJsonResponse response = sendRequest(() -> debugModuleWeb3j.debugTraceBlockByNumber(bnOrId)); + return response.getJson(); + } catch (Exception e) { + e.printStackTrace(); + throw new HttpRpcException(e); + } + } + + @Override + public JsonNode debugTraceBlockByNumber(String bnOrId, Map params) throws HttpRpcException { + try { + RskModuleWeb3j.GenericJsonResponse response = sendRequest(() -> debugModuleWeb3j.debugTraceBlockByNumber(bnOrId, params)); + return response.getJson(); + } catch (Exception e) { + e.printStackTrace(); + throw new HttpRpcException(e); + } + } + @Override public EthAccounts ethAccounts() throws HttpRpcException { try { diff --git a/rskj-core/src/jmh/java/co/rsk/jmh/web3/plan/DebugPlan.java b/rskj-core/src/jmh/java/co/rsk/jmh/web3/plan/DebugPlan.java index 328e2727f8b..78155497d67 100644 --- a/rskj-core/src/jmh/java/co/rsk/jmh/web3/plan/DebugPlan.java +++ b/rskj-core/src/jmh/java/co/rsk/jmh/web3/plan/DebugPlan.java @@ -36,7 +36,9 @@ public class DebugPlan extends BasePlan { private String transactionVT; private String transactionContractCreation; private String transactionContractCall; - private String block; + private String blockHash; + private String blockNumber; + private String blockTag; private final Map debugParams = new HashMap<>(); @@ -87,10 +89,13 @@ public void setUp(BenchmarkParams params) throws BenchmarkWeb3Exception { } } - block = configuration.getNullableProperty("debug.block"); - if (block == null) { - block = web3Connector.ethGetBlockHashByNumber(BigInteger.ONE); // naive, valid only for regtest mode + blockHash = configuration.getNullableProperty("debug.block"); + if (blockHash == null) { + blockHash = web3Connector.ethGetBlockHashByNumber(BigInteger.ONE); // naive, valid only for regtest mode } + + blockNumber = "0x1"; + blockTag = "latest"; } @TearDown(Level.Trial) @@ -111,8 +116,16 @@ public String getTransactionContractCall() { return transactionContractCall; } - public String getBlock() { - return block; + public String getBlockHash() { + return blockHash; + } + + public String getBlockNumber() { + return blockNumber; + } + + public String getBlockTag() { + return blockTag; } public Map getDebugParams() { diff --git a/rskj-core/src/main/java/co/rsk/RskContext.java b/rskj-core/src/main/java/co/rsk/RskContext.java index a50e758671e..db130d7069a 100644 --- a/rskj-core/src/main/java/co/rsk/RskContext.java +++ b/rskj-core/src/main/java/co/rsk/RskContext.java @@ -482,9 +482,9 @@ public synchronized BlockExecutor getBlockExecutor() { if (blockExecutor == null) { blockExecutor = new BlockExecutor( - getRskSystemProperties().getActivationConfig(), getRepositoryLocator(), - getTransactionExecutorFactory() + getTransactionExecutorFactory(), + getRskSystemProperties() ); } @@ -819,7 +819,8 @@ public synchronized DebugModule getDebugModule() { getReceiptStore(), getNodeMessageHandler(), getBlockExecutor(), - getTxQuotaChecker() + getTxQuotaChecker(), + getWeb3InformationRetriever() ); } @@ -1097,7 +1098,7 @@ public synchronized BlockValidationRule getBlockValidationRule() { rskSystemProperties.getActivationConfig(), rskSystemProperties.getNetworkConstants() ); - blockValidationRule = new BlockValidatorRule( + blockValidationRule = new BlockCompositeRule( new TxsMinGasPriceRule(), new BlockTxsMaxGasPriceRule(rskSystemProperties.getActivationConfig()), new BlockUnclesValidationRule( @@ -1124,7 +1125,8 @@ public synchronized BlockValidationRule getBlockValidationRule() { blockTimeStampValidationRule, new GasLimitRule(commonConstants.getMinGasLimit()), new ExtraDataRule(commonConstants.getMaximumExtraDataSize()), - getForkDetectionDataRule() + getForkDetectionDataRule(), + new ValidTxExecutionSublistsEdgesRule(getRskSystemProperties().getActivationConfig()) ); } diff --git a/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java b/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java index 2f80fafbed7..61ca94672e4 100644 --- a/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java +++ b/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java @@ -70,7 +70,7 @@ private void executeBlocks(BlockExecutor blockExecutor, BlockStore blockStore, T Block block = blockStore.getChainBlockByNumber(n); Block parent = blockStore.getBlockByHash(block.getParentHash().getBytes()); - BlockResult blockResult = blockExecutor.execute(block, parent.getHeader(), false, false, true); + BlockResult blockResult = blockExecutor.execute(null, 0, block, parent.getHeader(), false, false, true); Keccak256 stateRootHash = stateRootHandler.translate(block.getHeader()); if (!Arrays.equals(blockResult.getFinalState().getHash().getBytes(), stateRootHash.getBytes())) { diff --git a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java index b2388537591..1638f7f155c 100644 --- a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java +++ b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java @@ -21,6 +21,7 @@ import co.rsk.core.RskAddress; import co.rsk.net.discovery.table.KademliaOptions; import co.rsk.rpc.ModuleDescription; +import com.google.common.annotations.VisibleForTesting; import com.typesafe.config.Config; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigValue; @@ -32,7 +33,9 @@ import org.ethereum.crypto.HashUtil; import org.ethereum.listener.GasPriceCalculator; import org.ethereum.net.client.Capability; +import org.ethereum.vm.PrecompiledContracts; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -76,6 +79,10 @@ public class RskSystemProperties extends SystemProperties { //TODO: REMOVE THIS WHEN THE LocalBLockTests starts working with REMASC private boolean remascEnabled = true; + private Set concurrentContractsDisallowed = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + PrecompiledContracts.REMASC_ADDR, PrecompiledContracts.BRIDGE_ADDR + ))); + private List moduleDescriptions; public RskSystemProperties(ConfigLoader loader) { @@ -250,6 +257,15 @@ public boolean isRemascEnabled() { return remascEnabled; } + public Set concurrentContractsDisallowed() { + return concurrentContractsDisallowed; + } + + @VisibleForTesting + public void setConcurrentContractsDisallowed(@Nonnull Set disallowed) { + this.concurrentContractsDisallowed = Collections.unmodifiableSet(new HashSet<>(disallowed)); + } + //TODO: REMOVE THIS WHEN THE LocalBLockTests starts working with REMASC public void setRemascEnabled(boolean remascEnabled) { this.remascEnabled = remascEnabled; @@ -262,7 +278,7 @@ public boolean skipRemasc() { public boolean usePeersFromLastSession() { return getBoolean(USE_PEERS_FROM_LAST_SESSION, false); } - + public long peerDiscoveryMessageTimeOut() { return getLong("peer.discovery.msg.timeout", PD_DEFAULT_TIMEOUT_MESSAGE); } diff --git a/rskj-core/src/main/java/co/rsk/core/TransactionExecutorFactory.java b/rskj-core/src/main/java/co/rsk/core/TransactionExecutorFactory.java index fb500c9afcd..c8b951f5e43 100644 --- a/rskj-core/src/main/java/co/rsk/core/TransactionExecutorFactory.java +++ b/rskj-core/src/main/java/co/rsk/core/TransactionExecutorFactory.java @@ -24,6 +24,7 @@ import org.ethereum.db.BlockStore; import org.ethereum.db.ReceiptStore; import org.ethereum.vm.DataWord; +import org.ethereum.vm.GasCost; import org.ethereum.vm.PrecompiledContracts; import org.ethereum.vm.program.invoke.ProgramInvokeFactory; @@ -66,6 +67,7 @@ public TransactionExecutor newInstance( long totalGasUsed) { return newInstance(tx, txindex, coinbase, track, block, totalGasUsed, false, 0, new HashSet<>()); } + // TODO(JULI): newInstance calls a second newInstance hardcoding Postpone payment fees and sublist gas limit as block.gasLimit() public TransactionExecutor newInstance( Transaction tx, @@ -76,7 +78,9 @@ public TransactionExecutor newInstance( long totalGasUsed, boolean vmTrace, int vmTraceOptions, - Set deletedAccounts) { + Set deletedAccounts, + boolean postponeFeePayment, + long sublistGasLimit) { // Tracing configuration is scattered across different files (VM, DetailedProgramTrace, etc.) and // TransactionExecutor#extractTrace doesn't work when called independently. // It would be great to decouple from VmConfig#vmTrace, but sadly that's a major refactor we can't do now. @@ -109,7 +113,23 @@ public TransactionExecutor newInstance( config.isRemascEnabled(), precompiledContracts, deletedAccounts, - blockTxSignatureCache + blockTxSignatureCache, + postponeFeePayment, + sublistGasLimit ); } + + public TransactionExecutor newInstance( + Transaction tx, + int txindex, + RskAddress coinbase, + Repository track, + Block block, + long totalGasUsed, + boolean vmTrace, + int vmTraceOptions, + Set deletedAccounts) { + return newInstance(tx, txindex, coinbase, track, block, totalGasUsed, vmTrace, vmTraceOptions, deletedAccounts, false, GasCost.toGas(block.getGasLimit())); + } + // TODO(JULI): set the sublist gas limit as the whole block is wrong. However, this method is just used either when RSKIP144 is deactivated or for testing. } diff --git a/rskj-core/src/main/java/co/rsk/core/TransactionListExecutor.java b/rskj-core/src/main/java/co/rsk/core/TransactionListExecutor.java new file mode 100644 index 00000000000..208da85832d --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/TransactionListExecutor.java @@ -0,0 +1,250 @@ +package co.rsk.core; + +import co.rsk.crypto.Keccak256; +import org.ethereum.core.*; +import org.ethereum.vm.DataWord; +import org.ethereum.vm.PrecompiledContracts; +import org.ethereum.vm.program.ProgramResult; +import org.ethereum.vm.trace.ProgramTraceProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.Callable; + +public class TransactionListExecutor implements Callable { + + private static final Logger logger = LoggerFactory.getLogger("transactionlistexecutor"); + + private final TransactionExecutorFactory transactionExecutorFactory; + private final List transactions; + private final Block block; + private final Repository track; + private final boolean vmTrace; + private final int vmTraceOptions; + private final Set deletedAccounts; + private final boolean discardInvalidTxs; + private final boolean acceptInvalidTransactions; + private final Map executedTransactions; + private final Map receipts; + private final Map transactionResults; + private final ProgramTraceProcessor programTraceProcessor; + private long sublistGasLimit; + private final boolean remascEnabled; + private long totalGas; + private int i; + private final boolean registerProgramResults; + private final Set concurrentContractsDisallowed; + private Coin totalPaidFees; + + private volatile boolean stopped; + + public TransactionListExecutor( + List transactions, + Block block, + TransactionExecutorFactory transactionExecutorFactory, + Repository track, + boolean vmTrace, + int vmTraceOptions, + Set deletedAccounts, + boolean discardInvalidTxs, + boolean acceptInvalidTransactions, + Map receipts, + Map executedTransactions, + Map transactionResults, + boolean registerProgramResults, + @Nullable ProgramTraceProcessor programTraceProcessor, + int firstTxIndex, + Coin totalPaidFees, + boolean remascEnabled, + Set concurrentContractsDisallowed, + long sublistGasLimit) { + this.block = block; + this.transactionExecutorFactory = transactionExecutorFactory; + this.track = track; + this.vmTrace = vmTrace; + this.vmTraceOptions = vmTraceOptions; + this.transactions = new ArrayList<>(transactions); + this.deletedAccounts = deletedAccounts; + this.discardInvalidTxs = discardInvalidTxs; + this.acceptInvalidTransactions = acceptInvalidTransactions; + this.executedTransactions = executedTransactions; + this.receipts = receipts; + this.registerProgramResults = registerProgramResults; + this.transactionResults = transactionResults; + this.programTraceProcessor = programTraceProcessor; + this.totalGas = 0L; + this.i = firstTxIndex; + this.totalPaidFees = totalPaidFees; + this.remascEnabled = remascEnabled; + this.concurrentContractsDisallowed = Collections.unmodifiableSet(new HashSet<>(concurrentContractsDisallowed)); + this.sublistGasLimit = sublistGasLimit; + } + + @Override + public Boolean call() { + if (stopped) { + return false; + } + + long totalGasUsed = 0; + for (Transaction tx : transactions) { + + int numberOfTransactions = block.getTransactionsList().size(); + boolean isRemascTransaction = tx.isRemascTransaction(this.i, numberOfTransactions); + + addFeesToRemascIfEnabled(isRemascTransaction); + + TransactionExecutor txExecutor = transactionExecutorFactory.newInstance( + tx, + i, + block.getCoinbase(), + track, + block, + totalGasUsed, + vmTrace, + vmTraceOptions, + deletedAccounts, + true, + sublistGasLimit); + boolean transactionSucceeded = txExecutor.executeTransaction(); + if (stopped) { + return false; + } + + if (!this.concurrentContractsDisallowed.isEmpty() && txExecutor.precompiledContractsCalled().stream().anyMatch(this.concurrentContractsDisallowed::contains)) { + transactionSucceeded = false; + } + + if (!acceptInvalidTransactions && !transactionSucceeded) { + if (discardIfInvalid(tx, numberOfTransactions, isRemascTransaction)) { + return false; + } + continue; + } + + executedTransactions.put(i, tx); + + if (this.registerProgramResults) { + this.transactionResults.put(tx.getHash(), txExecutor.getResult()); + } + + if (vmTrace) { + txExecutor.extractTrace(programTraceProcessor); + } + + logger.trace("tx[{}] executed", i + 1); + logger.trace("track commit"); + + long txGasUsed = txExecutor.getGasConsumed(); + totalGasUsed += txGasUsed; + + addPaidFeesToToal(txExecutor); + + // It's used just for testing, the last tx should be always the REMASC. + payToRemascWhenThereIsNoRemascTx(numberOfTransactions, isRemascTransaction); + + deletedAccounts.addAll(txExecutor.getResult().getDeleteAccounts()); + + TransactionReceipt receipt = createTransactionReceipt(totalGasUsed, tx, txExecutor, txGasUsed); + + logger.trace("block: [{}] executed tx: [{}]", block.getNumber(), tx.getHash()); + + i++; + + logger.trace("tx[{}].receipt", i); + + receipts.put(i, receipt); + + logger.trace("tx[{}] done", i); + } + totalGas += totalGasUsed; + return true; + } + + private boolean discardIfInvalid(Transaction tx, int numberOfTransactions, boolean isRemascTransaction) { + // It's used just for testing, the last tx should be always the REMASC. + payToRemascWhenThereIsNoRemascTx(numberOfTransactions, isRemascTransaction); + if (!discardInvalidTxs) { + logger.warn("block: [{}] execution interrupted because of invalid tx: [{}]", + block.getNumber(), tx.getHash() + ); + return true; + } + + logger.warn("block: [{}] discarded tx: [{}]", block.getNumber(), tx.getHash()); + return false; + } + + private TransactionReceipt createTransactionReceipt(long totalGasUsed, Transaction tx, TransactionExecutor txExecutor, long txGasUsed) { + TransactionReceipt receipt = new TransactionReceipt(); + receipt.setGasUsed(txGasUsed); + receipt.setCumulativeGas(totalGasUsed); + + receipt.setTxStatus(txExecutor.getReceipt().isSuccessful()); + receipt.setTransaction(tx); + receipt.setLogInfoList(txExecutor.getVMLogs()); + receipt.setStatus(txExecutor.getReceipt().getStatus()); + return receipt; + } + + private void addPaidFeesToToal(TransactionExecutor txExecutor) { + Coin txPaidFees = txExecutor.getPaidFees(); + if (txPaidFees != null) { + totalPaidFees = totalPaidFees.add(txPaidFees); + } + } + + private void addFeesToRemascIfEnabled(boolean isRemascTransaction) { + if (this.remascEnabled && isRemascTransaction) { + addFeesToRemasc(); + } + } + + private void payToRemascWhenThereIsNoRemascTx(int numberOfTransactions, boolean isRemascTransaction) { + boolean isLastTx = this.i == numberOfTransactions - 1; + if (this.remascEnabled && isLastTx && !isRemascTransaction) { + addFeesToRemasc(); + } + } + + private void addFeesToRemasc() { + if (this.totalPaidFees.compareTo(Coin.ZERO) > 0) { + logger.trace("Adding fee to remasc contract account"); + track.addBalance(PrecompiledContracts.REMASC_ADDR, this.totalPaidFees); + } + } + + public Repository getRepository() { + return this.track; + } + + public Set getDeletedAccounts() { + return new HashSet<>(this.deletedAccounts); + } + + public Map getReceipts() { + return this.receipts; + } + + public Map getExecutedTransactions() { + return this.executedTransactions; + } + + public Map getTransactionResults() { + return this.transactionResults; + } + + public Coin getTotalFees() { + return this.totalPaidFees; + } + + public long getTotalGas() { + return this.totalGas; + } + + public void stop() { + this.stopped = true; + } +} diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java index 2a369dd61ce..6bed3f10649 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java @@ -249,7 +249,7 @@ private ImportResult internalTryToConnect(Block block) { long saveTime = System.nanoTime(); logger.trace("execute start"); - result = blockExecutor.execute(block, parent.getHeader(), false, noValidation, true); + result = blockExecutor.execute(null, 0, block, parent.getHeader(), false, noValidation, true); logger.trace("execute done"); diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java index 7f41ae18bd7..afff40d822b 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java @@ -18,18 +18,22 @@ package co.rsk.core.bc; +import co.rsk.config.RskSystemProperties; import co.rsk.core.Coin; import co.rsk.core.RskAddress; import co.rsk.core.TransactionExecutorFactory; +import co.rsk.core.TransactionListExecutor; import co.rsk.crypto.Keccak256; import co.rsk.db.RepositoryLocator; import co.rsk.metrics.profilers.Metric; import co.rsk.metrics.profilers.Profiler; import co.rsk.metrics.profilers.ProfilerFactory; import com.google.common.annotations.VisibleForTesting; +import org.ethereum.config.Constants; import org.ethereum.config.blockchain.upgrades.ActivationConfig; import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.ethereum.core.*; +import org.ethereum.util.ByteUtil; import org.ethereum.vm.DataWord; import org.ethereum.vm.PrecompiledContracts; import org.ethereum.vm.program.ProgramResult; @@ -37,8 +41,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP126; import static org.ethereum.config.blockchain.upgrades.ConsensusRule.RSKIP85; @@ -48,7 +55,7 @@ * There are two main use cases: * - execute and validate the block final state * - execute and complete the block final state - * + *

* Note that this class IS NOT guaranteed to be thread safe because its dependencies might hold state. */ public class BlockExecutor { @@ -58,40 +65,96 @@ public class BlockExecutor { private final RepositoryLocator repositoryLocator; private final TransactionExecutorFactory transactionExecutorFactory; private final ActivationConfig activationConfig; + private final boolean remascEnabled; + private final Set concurrentContractsDisallowed; - private final Map transactionResults = new HashMap<>(); + private final Map transactionResults = new ConcurrentHashMap<>(); private boolean registerProgramResults; + private long minSequentialSetGasLimit; + + /** + * An array of ExecutorService's of size `Constants.getTransactionExecutionThreads()`. Each parallel list uses an executor + * at specific index of this array, so that threads chosen by thread pools cannot be "reused" for executing parallel + * lists from same block. Otherwise, that could lead to non-determinism, when trie keys collision may not be detected + * on some circumstances. + */ + private final ExecutorService[] execServices; public BlockExecutor( - ActivationConfig activationConfig, RepositoryLocator repositoryLocator, - TransactionExecutorFactory transactionExecutorFactory) { + TransactionExecutorFactory transactionExecutorFactory, + RskSystemProperties systemProperties) { this.repositoryLocator = repositoryLocator; this.transactionExecutorFactory = transactionExecutorFactory; - this.activationConfig = activationConfig; + this.activationConfig = systemProperties.getActivationConfig(); + this.remascEnabled = systemProperties.isRemascEnabled(); + this.concurrentContractsDisallowed = Collections.unmodifiableSet(new HashSet<>(systemProperties.concurrentContractsDisallowed())); + this.minSequentialSetGasLimit = systemProperties.getNetworkConstants().getMinSequentialSetGasLimit(); + + int numOfParallelList = Constants.getTransactionExecutionThreads(); + this.execServices = new ExecutorService[numOfParallelList]; + for (int i = 0; i < numOfParallelList; i++) { + execServices[i] = new ThreadPoolExecutorImpl(i); + } + } + + /** + * Precompiled contracts storage is setup like any other contract for consistency. Here, we apply this logic on the + * exact activation block. + * This method is called automatically for every block except for the Genesis (which makes an explicit call). + */ + public static void maintainPrecompiledContractStorageRoots(Repository track, ActivationConfig.ForBlock activations) { + if (activations.isActivating(RSKIP126)) { + for (RskAddress addr : PrecompiledContracts.GENESIS_ADDRESSES) { + if (!track.isExist(addr)) { + track.createAccount(addr); + } + track.setupContract(addr); + } + } + + for (Map.Entry e : PrecompiledContracts.CONSENSUS_ENABLED_ADDRESSES.entrySet()) { + ConsensusRule contractActivationRule = e.getValue(); + if (activations.isActivating(contractActivationRule)) { + RskAddress addr = e.getKey(); + track.createAccount(addr); + track.setupContract(addr); + } + } + } + + @VisibleForTesting + public static byte[] calculateLogsBloom(List receipts) { + Bloom logBloom = new Bloom(); + + for (TransactionReceipt receipt : receipts) { + logBloom.or(receipt.getBloomFilter()); + } + + return logBloom.getData(); } /** * Execute and complete a block. * - * @param block A block to execute and complete - * @param parent The parent of the block. + * @param block A block to execute and complete + * @param parent The parent of the block. */ public BlockResult executeAndFill(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, true, false, false); + BlockResult result = executeForMining(block, parent, true, false, false); fill(block, result); return result; } @VisibleForTesting public void executeAndFillAll(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, true, false); + BlockResult result = executeForMining(block, parent, false, true, false); fill(block, result); } @VisibleForTesting public void executeAndFillReal(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, false, false); + BlockResult result = executeForMining(block, parent, false, false, false); if (result != BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT) { fill(block, result); } @@ -108,6 +171,7 @@ private void fill(Block block, BlockResult result) { header.setGasUsed(result.getGasUsed()); header.setPaidFees(result.getPaidFees()); header.setLogsBloom(calculateLogsBloom(result.getTransactionReceipts())); + header.setTxExecutionSublistsEdges(result.getTxEdges()); block.flushRLP(); profiler.stop(metric); @@ -116,13 +180,13 @@ private void fill(Block block, BlockResult result) { /** * Execute and validate the final state of a block. * - * @param block A block to execute and complete - * @param parent The parent of the block. + * @param block A block to execute and complete + * @param parent The parent of the block. * @return true if the block final state is equalBytes to the calculated final state. */ @VisibleForTesting public boolean executeAndValidate(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, false, false); + BlockResult result = execute(null, 0, block, parent, false, false, false); return this.validate(block, result); } @@ -130,8 +194,8 @@ public boolean executeAndValidate(Block block, BlockHeader parent) { /** * Validate the final state of a block. * - * @param block A block to validate - * @param result A block result (state root, receipts root, etc...) + * @param block A block to validate + * @param result A block result (state root, receipts root, etc...) * @return true if the block final state is equalBytes to the calculated final state. */ public boolean validate(Block block, BlockResult result) { @@ -172,7 +236,7 @@ public boolean validate(Block block, BlockResult result) { Coin paidFees = result.getPaidFees(); Coin feesPaidToMiner = block.getFeesPaidToMiner(); - if (!paidFees.equals(feesPaidToMiner)) { + if (!paidFees.equals(feesPaidToMiner)) { logger.error("Block {} [{}] given paidFees doesn't match: {} != {}", block.getNumber(), block.getPrintableHash(), feesPaidToMiner, paidFees); profiler.stop(metric); return false; @@ -181,7 +245,7 @@ public boolean validate(Block block, BlockResult result) { List executedTransactions = result.getExecutedTransactions(); List transactionsList = block.getTransactionsList(); - if (!executedTransactions.equals(transactionsList)) { + if (!executedTransactions.equals(transactionsList)) { logger.error("Block {} [{}] given txs doesn't match: {} != {}", block.getNumber(), block.getPrintableHash(), transactionsList, executedTransactions); profiler.stop(metric); return false; @@ -217,30 +281,45 @@ private boolean validateLogsBloom(BlockHeader header, BlockResult result) { return Arrays.equals(calculateLogsBloom(result.getTransactionReceipts()), header.getLogsBloom()); } - @VisibleForTesting - BlockResult execute(Block block, BlockHeader parent, boolean discardInvalidTxs) { - return execute(block, parent, discardInvalidTxs, false, true); - } - - public BlockResult execute(Block block, BlockHeader parent, boolean discardInvalidTxs, boolean ignoreReadyToExecute, boolean saveState) { - return executeInternal(null, 0, block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + public BlockResult executeForMining(Block block, BlockHeader parent, boolean discardInvalidTxs, boolean ignoreReadyToExecute, boolean saveState) { + if (activationConfig.isActive(ConsensusRule.RSKIP144, block.getHeader().getNumber())) { + return executeForMiningAfterRSKIP144(block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + } else { + return executeInternal(null, 0, block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + } } /** * Execute a block while saving the execution trace in the trace processor */ - public void traceBlock( - ProgramTraceProcessor programTraceProcessor, - int vmTraceOptions, - Block block, - BlockHeader parent, - boolean discardInvalidTxs, - boolean ignoreReadyToExecute) { - executeInternal( - Objects.requireNonNull(programTraceProcessor), vmTraceOptions, block, parent, discardInvalidTxs, ignoreReadyToExecute, false - ); + public void traceBlock(ProgramTraceProcessor programTraceProcessor, + int vmTraceOptions, + Block block, + BlockHeader parent, + boolean discardInvalidTxs, + boolean ignoreReadyToExecute) { + execute(Objects.requireNonNull(programTraceProcessor), vmTraceOptions, block, parent, discardInvalidTxs, + ignoreReadyToExecute, false); } + public BlockResult execute(@Nullable ProgramTraceProcessor programTraceProcessor, + int vmTraceOptions, + Block block, + BlockHeader parent, + boolean discardInvalidTxs, + boolean acceptInvalidTransactions, + boolean saveState) { + if (activationConfig.isActive(ConsensusRule.RSKIP144, block.getHeader().getNumber())) { + return executeParallel(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); + } else { + return executeInternal(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); + } + } + + // executes the block before RSKIP 144 + // when RSKIP 144 is active the block is in executed parallel + // miners use executeForMiningAfterRSKIP144 to create the parallel schedule + // new blocks are executed with executeParallel private BlockResult executeInternal( @Nullable ProgramTraceProcessor programTraceProcessor, int vmTraceOptions, @@ -250,8 +329,8 @@ private BlockResult executeInternal( boolean acceptInvalidTransactions, boolean saveState) { boolean vmTrace = programTraceProcessor != null; - logger.trace("Start executeInternal."); - logger.trace("applyBlock: block: [{}] tx.list: [{}]", block.getNumber(), block.getTransactionsList().size()); + logger.trace("Start execute pre RSKIP144."); + loggingApplyBlock(block); // Forks the repo, does not change "repository". It will have a completely different // image of the repo, where the middle caches are immediately ignored. @@ -261,7 +340,7 @@ private BlockResult executeInternal( // in the next block processed. // Note that creating a snapshot is important when the block is executed twice // (e.g. once while building the block in tests/mining, and the other when trying - // to conect the block). This is because the first execution will change the state + // to connect the block). This is because the first execution will change the state // of the repository to the state post execution, so it's necessary to get it to // the state prior execution again. Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_EXECUTE); @@ -280,7 +359,7 @@ private BlockResult executeInternal( int txindex = 0; for (Transaction tx : block.getTransactionsList()) { - logger.trace("apply block: [{}] tx: [{}] ", block.getNumber(), i); + loggingApplyBlockToTx(block, i); TransactionExecutor txExecutor = transactionExecutorFactory.newInstance( tx, @@ -295,121 +374,465 @@ private BlockResult executeInternal( boolean transactionExecuted = txExecutor.executeTransaction(); if (!acceptInvalidTransactions && !transactionExecuted) { - if (discardInvalidTxs) { - logger.warn("block: [{}] discarded tx: [{}]", block.getNumber(), tx.getHash()); - continue; - } else { - logger.warn("block: [{}] execution interrupted because of invalid tx: [{}]", - block.getNumber(), tx.getHash()); - profiler.stop(metric); - return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + if (!discardInvalidTxs) { + return getBlockResultAndLogExecutionInterrupted(block, metric, tx); } + loggingDiscardedBlock(block, tx); + continue; } - executedTransactions.add(tx); + registerExecutedTx(programTraceProcessor, vmTrace, executedTransactions, tx, txExecutor); + + long gasUsed = txExecutor.getGasConsumed(); + totalGasUsed += gasUsed; + + totalPaidFees = addTotalPaidFees(totalPaidFees, txExecutor); + + deletedAccounts.addAll(txExecutor.getResult().getDeleteAccounts()); + + TransactionReceipt receipt = buildTransactionReceipt(tx, txExecutor, gasUsed, totalGasUsed); + + loggingExecuteTxAndReceipt(block, i, tx); + + i++; + + receipts.add(receipt); + + loggingTxDone(); + } + + + saveOrCommitTrackState(saveState, track); + + BlockResult result = new BlockResult( + block, + executedTransactions, + receipts, + null, + totalGasUsed, + totalPaidFees, + vmTrace ? null : track.getTrie() + + ); + profiler.stop(metric); + logger.trace("End execute pre RSKIP144."); + return result; + } + + private BlockResult executeParallel( + @Nullable ProgramTraceProcessor programTraceProcessor, + int vmTraceOptions, + Block block, + BlockHeader parent, + boolean discardInvalidTxs, + boolean acceptInvalidTransactions, + boolean saveState) { + boolean vmTrace = programTraceProcessor != null; + logger.trace("Start executeParallel."); + loggingApplyBlock(block); + + // Forks the repo, does not change "repository". It will have a completely different + // image of the repo, where the middle caches are immediately ignored. + // In fact, while cloning everything, it asserts that no cache elements remains. + // (see assertNoCache()) + // Which means that you must commit changes and save them to be able to recover + // in the next block processed. + // Note that creating a snapshot is important when the block is executed twice + // (e.g. once while building the block in tests/mining, and the other when trying + // to conect the block). This is because the first execution will change the state + // of the repository to the state post execution, so it's necessary to get it to + // the state prior execution again. + Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_EXECUTE); + + ReadWrittenKeysTracker readWrittenKeysTracker = new ReadWrittenKeysTracker(); + Repository track = repositoryLocator.startTrackingAt(parent, readWrittenKeysTracker); + + maintainPrecompiledContractStorageRoots(track, activationConfig.forBlock(block.getNumber())); + readWrittenKeysTracker.clear(); + + short[] txExecutionEdges = block.getHeader().getTxExecutionSublistsEdges(); + + // if the number of parallel lists is less than 2, then there's no need to execute in another thread. The work can + // be done in the same thread (in-line) without any threads switching. + ExecutorCompletionService completionService = new ExecutorCompletionService<>(txExecutionEdges.length > 1 ? new Executor() { + private final AtomicInteger parallelListIndex = new AtomicInteger(0); // 'parallelListIndex' should not exceed Constants.getTransactionExecutionThreads() - if (this.registerProgramResults) { - this.transactionResults.put(tx.getHash(), txExecutor.getResult()); + @Override + public void execute(@Nonnull Runnable command) { + execServices[parallelListIndex.getAndIncrement()].execute(command); } + } : Runnable::run); + List transactionListExecutors = new ArrayList<>(); - if (vmTrace) { - txExecutor.extractTrace(programTraceProcessor); + short start = 0; + + for (short end : txExecutionEdges) { + List sublist = block.getTransactionsList().subList(start, end); + TransactionListExecutor txListExecutor = new TransactionListExecutor( + sublist, + block, + transactionExecutorFactory, + track.startTracking(), + vmTrace, + vmTraceOptions, + new HashSet<>(), + discardInvalidTxs, + acceptInvalidTransactions, + new HashMap<>(), + new HashMap<>(), + new HashMap<>(), + registerProgramResults, + programTraceProcessor, + start, + Coin.ZERO, + remascEnabled, + concurrentContractsDisallowed, + BlockUtils.getSublistGasLimit(block, false, minSequentialSetGasLimit) + ); + completionService.submit(txListExecutor); + transactionListExecutors.add(txListExecutor); + start = end; + } + + for (int i = 0; i < transactionListExecutors.size(); i++) { + try { + Future success = completionService.take(); + if (!Boolean.TRUE.equals(success.get())) { + transactionListExecutors.forEach(TransactionListExecutor::stop); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } + } catch (InterruptedException e) { + logger.warn("block: [{}]/[{}] execution was interrupted", block.getNumber(), block.getHash()); + logger.trace("", e); + Thread.currentThread().interrupt(); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } catch (ExecutionException e) { + logger.warn("block: [{}]/[{}] execution failed", block.getNumber(), block.getHash()); + logger.trace("", e); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; } + } - logger.trace("tx executed"); + // Review collision + if (readWrittenKeysTracker.detectCollision()) { + logger.warn("block: [{}]/[{}] execution failed. Block data: [{}]", block.getNumber(), block.getHash(), ByteUtil.toHexString(block.getEncoded())); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } - // No need to commit the changes here. track.commit(); + // Merge maps. + Map executedTransactions = new HashMap<>(); + Set deletedAccounts = new HashSet<>(); + Map receipts = new HashMap<>(); + Map mergedTransactionResults = new HashMap<>(); + Coin totalPaidFees = Coin.ZERO; + long totalGasUsed = 0; - logger.trace("track commit"); + for (TransactionListExecutor tle : transactionListExecutors) { + tle.getRepository().commit(); + deletedAccounts.addAll(tle.getDeletedAccounts()); + executedTransactions.putAll(tle.getExecutedTransactions()); + receipts.putAll(tle.getReceipts()); + mergedTransactionResults.putAll(tle.getTransactionResults()); + totalPaidFees = totalPaidFees.add(tle.getTotalFees()); + totalGasUsed += tle.getTotalGas(); + } - long gasUsed = txExecutor.getGasUsed(); - totalGasUsed += gasUsed; - Coin paidFees = txExecutor.getPaidFees(); - if (paidFees != null) { - totalPaidFees = totalPaidFees.add(paidFees); + // execute remaining transactions after the parallel subsets + List sublist = block.getTransactionsList().subList(start, block.getTransactionsList().size()); + TransactionListExecutor txListExecutor = new TransactionListExecutor( + sublist, + block, + transactionExecutorFactory, + track, + vmTrace, + vmTraceOptions, + deletedAccounts, + discardInvalidTxs, + acceptInvalidTransactions, + receipts, + executedTransactions, + mergedTransactionResults, + registerProgramResults, + programTraceProcessor, + start, + totalPaidFees, + remascEnabled, + Collections.emptySet(), // precompiled contracts are always allowed in a sequential list, as there's no concurrency in it + BlockUtils.getSublistGasLimit(block, true, minSequentialSetGasLimit) + ); + Boolean success = txListExecutor.call(); + if (!Boolean.TRUE.equals(success)) { + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } + + Coin totalBlockPaidFees = txListExecutor.getTotalFees(); + totalGasUsed += txListExecutor.getTotalGas(); + + saveOrCommitTrackState(saveState, track); + + BlockResult result = new BlockResult( + block, + new LinkedList<>(executedTransactions.values()), + new LinkedList<>(receipts.values()), + txExecutionEdges, + totalGasUsed, + totalBlockPaidFees, + vmTrace ? null : track.getTrie() + ); + profiler.stop(metric); + logger.trace("End executeParallel."); + return result; + } + + private BlockResult executeForMiningAfterRSKIP144( + Block block, + BlockHeader parent, + boolean discardInvalidTxs, + boolean acceptInvalidTransactions, + boolean saveState) { + logger.trace("Start executeForMining."); + List transactionsList = block.getTransactionsList(); + loggingApplyBlock(block); + + // Forks the repo, does not change "repository". It will have a completely different + // image of the repo, where the middle caches are immediately ignored. + // In fact, while cloning everything, it asserts that no cache elements remains. + // (see assertNoCache()) + // Which means that you must commit changes and save them to be able to recover + // in the next block processed. + // Note that creating a snapshot is important when the block is executed twice + // (e.g. once while building the block in tests/mining, and the other when trying + // to conect the block). This is because the first execution will change the state + // of the repository to the state post execution, so it's necessary to get it to + // the state prior execution again. + Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_EXECUTE); + + IReadWrittenKeysTracker readWrittenKeysTracker = new ReadWrittenKeysTracker(); + Repository track = repositoryLocator.startTrackingAt(parent, readWrittenKeysTracker); + + maintainPrecompiledContractStorageRoots(track, activationConfig.forBlock(block.getNumber())); + readWrittenKeysTracker.clear(); + + int i = 1; + long totalGasUsed = 0; + Coin totalPaidFees = Coin.ZERO; + Map receiptsByTx = new HashMap<>(); + Set deletedAccounts = new HashSet<>(); + + int txindex = 0; + + int transactionExecutionThreads = Constants.getTransactionExecutionThreads(); + ParallelizeTransactionHandler parallelizeTransactionHandler = new ParallelizeTransactionHandler((short) transactionExecutionThreads, block, minSequentialSetGasLimit); + + for (Transaction tx : transactionsList) { + loggingApplyBlockToTx(block, i); + + TransactionExecutor txExecutor = transactionExecutorFactory.newInstance( + tx, + txindex, + block.getCoinbase(), + track, + block, + parallelizeTransactionHandler.getGasUsedInSequential(), + false, + 0, + deletedAccounts, + true, + Math.max(BlockUtils.getSublistGasLimit(block, true, minSequentialSetGasLimit), BlockUtils.getSublistGasLimit(block, false, minSequentialSetGasLimit)) + ); + boolean transactionExecuted = txExecutor.executeTransaction(); + + if (!acceptInvalidTransactions && !transactionExecuted) { + if (!discardInvalidTxs) { + return getBlockResultAndLogExecutionInterrupted(block, metric, tx); + } + + loggingDiscardedBlock(block, tx); + txindex++; + continue; } - deletedAccounts.addAll(txExecutor.getResult().getDeleteAccounts()); + Optional sublistGasAccumulated = addTxToSublistAndGetAccumulatedGas( + readWrittenKeysTracker, + parallelizeTransactionHandler, + tx, + tx.isRemascTransaction(txindex, transactionsList.size()), + txExecutor.getGasConsumed(), + txExecutor.precompiledContractsCalled().stream().anyMatch(this.concurrentContractsDisallowed::contains)); + + if (!acceptInvalidTransactions && !sublistGasAccumulated.isPresent()) { + if (!discardInvalidTxs) { + return getBlockResultAndLogExecutionInterrupted(block, metric, tx); + } - TransactionReceipt receipt = new TransactionReceipt(); - receipt.setGasUsed(gasUsed); - receipt.setCumulativeGas(totalGasUsed); + loggingDiscardedBlock(block, tx); + txindex++; + continue; + } + + registerTxExecutedForMiningAfterRSKIP144(readWrittenKeysTracker, tx, txExecutor); - receipt.setTxStatus(txExecutor.getReceipt().isSuccessful()); - receipt.setTransaction(tx); - receipt.setLogInfoList(txExecutor.getVMLogs()); - receipt.setStatus(txExecutor.getReceipt().getStatus()); + long gasUsed = txExecutor.getGasConsumed(); + totalGasUsed += gasUsed; + totalPaidFees = addTotalPaidFees(totalPaidFees, txExecutor); + + deletedAccounts.addAll(txExecutor.getResult().getDeleteAccounts()); - logger.trace("block: [{}] executed tx: [{}]", block.getNumber(), tx.getHash()); + //orElseGet is used for testing only when acceptInvalidTransactions is set. + long cumulativeGas = sublistGasAccumulated + .orElseGet(() -> parallelizeTransactionHandler.getGasUsedIn((short) Constants.getTransactionExecutionThreads())); + TransactionReceipt receipt = buildTransactionReceipt(tx, txExecutor, gasUsed, cumulativeGas); - logger.trace("tx[{}].receipt", i); + loggingExecuteTxAndReceipt(block, i, tx); i++; + txindex++; - receipts.add(receipt); + receiptsByTx.put(tx, receipt); - logger.trace("tx done"); + loggingTxDone(); } - logger.trace("End txs executions."); - if (saveState) { - logger.trace("Saving track."); - track.save(); - logger.trace("End saving track."); - } else { - logger.trace("Committing track."); - track.commit(); - logger.trace("End committing track."); + if (totalPaidFees.compareTo(Coin.ZERO) > 0) { + if (remascEnabled) { + track.addBalance(PrecompiledContracts.REMASC_ADDR, totalPaidFees); + } else { + track.addBalance(block.getCoinbase(), totalPaidFees); + } } - logger.trace("Building execution results."); + saveOrCommitTrackState(saveState, track); + + List executedTransactions = parallelizeTransactionHandler.getTransactionsInOrder(); + short[] sublistOrder = parallelizeTransactionHandler.getTransactionsPerSublistInOrder(); + List receipts = getTransactionReceipts(receiptsByTx, executedTransactions); + BlockResult result = new BlockResult( block, executedTransactions, receipts, + sublistOrder, totalGasUsed, totalPaidFees, - vmTrace ? null : track.getTrie() + track.getTrie() ); profiler.stop(metric); - logger.trace("End executeInternal."); + logger.trace("End executeForMining."); return result; } - /** - * Precompiled contracts storage is setup like any other contract for consistency. Here, we apply this logic on the - * exact activation block. - * This method is called automatically for every block except for the Genesis (which makes an explicit call). - */ - public static void maintainPrecompiledContractStorageRoots(Repository track, ActivationConfig.ForBlock activations) { - if (activations.isActivating(RSKIP126)) { - for (RskAddress addr : PrecompiledContracts.GENESIS_ADDRESSES) { - if (!track.isExist(addr)) { - track.createAccount(addr); - } - track.setupContract(addr); - } + private void registerExecutedTx(ProgramTraceProcessor programTraceProcessor, boolean vmTrace, List executedTransactions, Transaction tx, TransactionExecutor txExecutor) { + executedTransactions.add(tx); + + if (this.registerProgramResults) { + this.transactionResults.put(tx.getHash(), txExecutor.getResult()); } - for (Map.Entry e : PrecompiledContracts.CONSENSUS_ENABLED_ADDRESSES.entrySet()) { - ConsensusRule contractActivationRule = e.getValue(); - if (activations.isActivating(contractActivationRule)) { - RskAddress addr = e.getKey(); - track.createAccount(addr); - track.setupContract(addr); - } + if (vmTrace) { + txExecutor.extractTrace(programTraceProcessor); } + + loggingTxExecuted(); } - @VisibleForTesting - public static byte[] calculateLogsBloom(List receipts) { - Bloom logBloom = new Bloom(); + private Coin addTotalPaidFees(Coin totalPaidFees, TransactionExecutor txExecutor) { + Coin paidFees = txExecutor.getPaidFees(); + if (paidFees != null) { + totalPaidFees = totalPaidFees.add(paidFees); + } + return totalPaidFees; + } - for (TransactionReceipt receipt : receipts) { - logBloom.or(receipt.getBloomFilter()); + private void registerTxExecutedForMiningAfterRSKIP144(IReadWrittenKeysTracker readWrittenKeysTracker, Transaction tx, TransactionExecutor txExecutor) { + readWrittenKeysTracker.clear(); + + if (this.registerProgramResults) { + this.transactionResults.put(tx.getHash(), txExecutor.getResult()); } - return logBloom.getData(); + loggingTxExecuted(); + } + + private List getTransactionReceipts(Map receiptsByTx, List executedTransactions) { + List receipts = new ArrayList<>(); + + for (Transaction tx : executedTransactions) { + receipts.add(receiptsByTx.get(tx)); + } + return receipts; + } + + private Optional addTxToSublistAndGetAccumulatedGas(IReadWrittenKeysTracker readWrittenKeysTracker, ParallelizeTransactionHandler parallelizeTransactionHandler, Transaction tx, boolean isRemascTransaction, long gasUsed, boolean isSequentialSublistRequired) { + Optional sublistGasAccumulated; + + if (isRemascTransaction) { + sublistGasAccumulated = parallelizeTransactionHandler.addRemascTransaction(tx, gasUsed); + } else if (isSequentialSublistRequired) { + sublistGasAccumulated = parallelizeTransactionHandler.addTxToSequentialSublist(tx, gasUsed); + } else { + sublistGasAccumulated = parallelizeTransactionHandler.addTransaction(tx, readWrittenKeysTracker.getThisThreadReadKeys(), readWrittenKeysTracker.getThisThreadWrittenKeys(), gasUsed); + } + return sublistGasAccumulated; + } + + private void saveOrCommitTrackState(boolean saveState, Repository track) { + logger.trace("End txs executions."); + if (saveState) { + logger.trace("Saving track."); + track.save(); + logger.trace("End saving track."); + } else { + logger.trace("Committing track."); + track.commit(); + logger.trace("End committing track."); + } + } + + private TransactionReceipt buildTransactionReceipt(Transaction tx, TransactionExecutor txExecutor, long gasUsed, long cumulativeGas) { + TransactionReceipt receipt = new TransactionReceipt(); + receipt.setGasUsed(gasUsed); + receipt.setTxStatus(txExecutor.getReceipt().isSuccessful()); + receipt.setTransaction(tx); + receipt.setLogInfoList(txExecutor.getVMLogs()); + receipt.setStatus(txExecutor.getReceipt().getStatus()); + receipt.setCumulativeGas(cumulativeGas); + return receipt; + } + + private BlockResult getBlockResultAndLogExecutionInterrupted(Block block, Metric metric, Transaction tx) { + logger.warn("block: [{}]/[{}] execution interrupted because of invalid tx: [{}]", + block.getNumber(), block.getHash(), tx.getHash()); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } + + private void loggingTxExecuted() { + logger.trace("tx executed"); + } + + private void loggingTxDone() { + logger.trace("tx done"); + } + + private void loggingDiscardedBlock(Block block, Transaction tx) { + logger.warn("block: [{}] discarded tx: [{}]", block.getNumber(), tx.getHash()); + } + + private void loggingApplyBlock(Block block) { + logger.trace("applyBlock: block: [{}] tx.list: [{}]", block.getNumber(), block.getTransactionsList().size()); + } + + private void loggingApplyBlockToTx(Block block, int i) { + logger.trace("apply block: [{}] tx: [{}] ", block.getNumber(), i); + } + + private void loggingExecuteTxAndReceipt(Block block, int i, Transaction tx) { + logger.trace("block: [{}] executed tx: [{}]", block.getNumber(), tx.getHash()); + logger.trace("tx[{}].receipt", i); } public ProgramResult getProgramResult(Keccak256 txhash) { @@ -420,4 +843,53 @@ public void setRegisterProgramResults(boolean value) { this.registerProgramResults = value; this.transactionResults.clear(); } + + /** + * This implementation mimics a thread pool returned by {@link Executors#newCachedThreadPool()}, meaning that + * its core pool size is zero and maximum pool size is unbounded, while each thead created has keep-alive time - 15 mins. + */ + private static final class ThreadPoolExecutorImpl extends ThreadPoolExecutor { + private static final long KEEP_ALIVE_TIME_IN_SECS = 15*60L; /* 15 minutes */ + + public ThreadPoolExecutorImpl(int parallelListIndex) { + super(0, Integer.MAX_VALUE, + KEEP_ALIVE_TIME_IN_SECS, TimeUnit.SECONDS, + new SynchronousQueue<>(), new ThreadFactoryImpl(parallelListIndex)); + } + + @Override + protected void beforeExecute(Thread t, Runnable r) { + logger.debug("[{}]: before execution", t); + super.beforeExecute(t, r); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null) { + logger.debug("[{}]: after execution", Thread.currentThread()); + } else { + logger.warn("[{}]: failed execution", Thread.currentThread(), t); + } + } + } + + /** + * A utility class that helps to specify a proper thread name in the form of `BlockExecutorWorker--`. + */ + private static final class ThreadFactoryImpl implements ThreadFactory { + private final AtomicInteger cnt = new AtomicInteger(0); + private final int parallelListIndex; + + ThreadFactoryImpl(int parallelListIndex) { + this.parallelListIndex = parallelListIndex; + } + + @Override + public Thread newThread(@Nonnull Runnable r) { + String threadName = "BlockExecutorWorker-" + parallelListIndex + "-" + cnt.getAndIncrement(); + logger.info("New block execution thread [{}] for parallel list [{}] has been created", threadName, parallelListIndex); + return new Thread(r, threadName); + } + } } diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java index bb52ec2e364..b8f3468b9d3 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java @@ -24,6 +24,7 @@ import org.ethereum.core.Transaction; import org.ethereum.core.TransactionReceipt; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -35,7 +36,7 @@ public class BlockResult { null, Collections.emptyList(), Collections.emptyList(), - 0, + new short[0], 0, Coin.ZERO, null ); @@ -49,11 +50,13 @@ public class BlockResult { // It is for optimizing switching between states. Instead of using the "stateRoot" field, // which requires regenerating the trie, using the finalState field does not. private final Trie finalState; + private final short[] txEdges; public BlockResult( Block block, List executedTransactions, List transactionReceipts, + short[] txEdges, long gasUsed, Coin paidFees, Trie finalState) { @@ -63,12 +66,14 @@ public BlockResult( this.gasUsed = gasUsed; this.paidFees = paidFees; this.finalState = finalState; + this.txEdges = txEdges != null? Arrays.copyOf(txEdges, txEdges.length) : null; } - public Block getBlock() { return block; } + public short[] getTxEdges() { return this.txEdges != null ? Arrays.copyOf(txEdges, txEdges.length) : null; } + public List getExecutedTransactions() { return executedTransactions; } public List getTransactionReceipts() { diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockUtils.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockUtils.java index afe66fd0a4e..f8a305799f2 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockUtils.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockUtils.java @@ -20,10 +20,12 @@ import co.rsk.crypto.Keccak256; import co.rsk.net.NetBlockStore; +import org.ethereum.config.Constants; import org.ethereum.core.Block; import org.ethereum.core.BlockHeader; import org.ethereum.core.Blockchain; import org.ethereum.db.BlockInformation; +import org.ethereum.vm.GasCost; import java.util.*; import java.util.stream.Collectors; @@ -116,4 +118,77 @@ public static List sortBlocksByNumber(List blocks) { .collect(Collectors.toList()); } + /** + * Calculate the gas limit of a sublist, depending on the sublist type (sequential and parallel), from the block + * gas limit. The distribution can be performed one of two ways: + * + * 1. The block gas limit is divided equally among all sublists. If the division was not even (results in a decimal + * number) then the extra gas limit gets added to the sequential sublist. + * + * 2. The sequential sublist gets assigned a fixed value, determined by minSequentialSetGasLimit and additional + * gas limit is calculated by subtracting minSequentialSetGasLimit form block gas limit, the result is then divided + * by the amount of transaction execution threads. If the division for the parallel sublists was not even (results + * in a decimal number) then the extra gas limit gets added to the sequential sublist. + * + * + * @param block the block to get the gas limit from + * @param forSequentialTxSet a boolean the indicates if the gas limit beign calculated is for a sequential + * sublist or a paralle one. + * @param minSequentialSetGasLimit The minimum gas limit value the sequential sublist can have, configured by + * network in {@link Constants}. + * + * @return set of ancestors block hashes + */ + public static long getSublistGasLimit(Block block, boolean forSequentialTxSet, long minSequentialSetGasLimit) { + long blockGasLimit = GasCost.toGas(block.getGasLimit()); + int transactionExecutionThreadCount = Constants.getTransactionExecutionThreads(); + + /* + This if determines which distribution approach will be performed. If the result of multiplying the minSequentialSetGasLimit + by transactionExecutionThreadCount + 1 (where transactionExecutionThreadCount is the parallel sublist count and + the + 1 represents the sequential sublist) is less than the block gas limit then the equal division approach is performed, + otherwise the second approach, where the parallel sublists get less gas limit than the sequential sublist, is executed. + */ + if((transactionExecutionThreadCount + 1) * minSequentialSetGasLimit <= blockGasLimit) { + long parallelTxSetGasLimit = blockGasLimit / (transactionExecutionThreadCount + 1); + + if (forSequentialTxSet) { + /* + Subtract the total parallel sublist gas limit (parallelTxSetGasLimit) from the block gas limit to get + the sequential sublist gas limit + the possible extra gas limit and return it. + */ + return blockGasLimit - (transactionExecutionThreadCount * parallelTxSetGasLimit); + } + + return parallelTxSetGasLimit; + } else { + // Check if the block gas limit is less than the sequential gas limit. + if (blockGasLimit <= minSequentialSetGasLimit) { + /* + If this method execution is for a sequential sublist then return the total block gas limit. This will + skip the parallel sublist gas limit calculation since there will not be any gas limit left. + */ + if (forSequentialTxSet) { + return blockGasLimit; + } + + // If this method execution is NOT for a sequential sublist then return 0. + return 0; + } + + long additionalGasLimit = (blockGasLimit - minSequentialSetGasLimit); + long parallelTxSetGasLimit = additionalGasLimit / (transactionExecutionThreadCount); + + /* + If this method execution is for a sequential sublist then calculate the possible extra gas limit by subtracting + the total parallel sublist gas limit (parallelTxSetGasLimit) from additionalGasLimit and add the result to + minSequentialSetGasLimit + */ + if (forSequentialTxSet) { + long extraGasLimit = additionalGasLimit - (parallelTxSetGasLimit * transactionExecutionThreadCount); + return minSequentialSetGasLimit + extraGasLimit; + } + return parallelTxSetGasLimit; + } + } } diff --git a/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java b/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java new file mode 100644 index 00000000000..fdc29fef320 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java @@ -0,0 +1,42 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import org.ethereum.db.ByteArrayWrapper; + +import java.util.Map; +import java.util.Set; + +public interface IReadWrittenKeysTracker { + Set getThisThreadReadKeys(); + + Set getThisThreadWrittenKeys(); + + Map> getReadKeysByThread(); + + Map> getWrittenKeysByThread(); + + void addNewReadKey(ByteArrayWrapper key); + + void addNewWrittenKey(ByteArrayWrapper key); + + boolean detectCollision(); + + void clear(); +} diff --git a/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java b/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java new file mode 100644 index 00000000000..814554f3e33 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java @@ -0,0 +1,290 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import co.rsk.core.RskAddress; +import org.ethereum.core.Block; +import org.ethereum.core.Transaction; +import org.ethereum.db.ByteArrayWrapper; +import org.ethereum.vm.GasCost; + +import java.util.*; + +public class ParallelizeTransactionHandler { + private final HashMap sublistsHavingWrittenToKey; + private final HashMap> sublistsHavingReadFromKey; + private final Map sublistOfSender; + private final ArrayList sublists; + + public ParallelizeTransactionHandler(short numberOfSublists, Block block, long minSequentialSetGasLimit) { + this.sublistOfSender = new HashMap<>(); + this.sublistsHavingWrittenToKey = new HashMap<>(); + this.sublistsHavingReadFromKey = new HashMap<>(); + this.sublists = new ArrayList<>(); + for (short i = 0; i < numberOfSublists; i++){ + this.sublists.add(new TransactionSublist(BlockUtils.getSublistGasLimit(block, false, minSequentialSetGasLimit), false)); + } + this.sublists.add(new TransactionSublist(BlockUtils.getSublistGasLimit(block, true, minSequentialSetGasLimit), true)); + } + + public Optional addTransaction(Transaction tx, Set newReadKeys, Set newWrittenKeys, long gasUsedByTx) { + TransactionSublist sublistCandidate = getSublistCandidates(tx, newReadKeys, newWrittenKeys); + + if (sublistDoesNotHaveEnoughGas(tx, sublistCandidate)) { + if (sublistCandidate.isSequential()) { + return Optional.empty(); + } + sublistCandidate = getSequentialSublist(); + + if (sublistDoesNotHaveEnoughGas(tx, sublistCandidate)) { + return Optional.empty(); + } + } + + sublistCandidate.addTransaction(tx, gasUsedByTx); + addNewKeysToMaps(tx.getSender(), sublistCandidate, newReadKeys, newWrittenKeys); + return Optional.of(sublistCandidate.getGasUsed()); + } + + private boolean sublistDoesNotHaveEnoughGas(Transaction tx, TransactionSublist sublistCandidate) { + return !sublistCandidate.hasGasAvailable(GasCost.toGas(tx.getGasLimit())); + } + + public Optional addRemascTransaction(Transaction tx, long gasUsedByTx) { + TransactionSublist sequentialSublist = getSequentialSublist(); + sequentialSublist.addTransaction(tx, gasUsedByTx); + return Optional.of(sequentialSublist.getGasUsed()); + } + + // Unlike the smart contracts execution, Precompiled contracts persist the changes in the repository when they + // complete the execution. So, there is no way for the ParallelTransactionHandler to track the keys if the execution + // fails since the Precompiled contract rolls back the repository. + // Therefore, the tx is added to the sequential sublist to avoid possible race conditions. + public Optional addTxToSequentialSublist(Transaction tx, long gasUsedByTx) { + TransactionSublist sequentialSublist = getSequentialSublist(); + + if (sublistDoesNotHaveEnoughGas(tx, sequentialSublist)) { + return Optional.empty(); + } + + sequentialSublist.addTransaction(tx, gasUsedByTx); + addNewKeysToMaps(tx.getSender(), sequentialSublist, new HashSet<>(), new HashSet<>()); + + return Optional.of(sequentialSublist.getGasUsed()); + } + + public long getGasUsedIn(Short sublistId) { + + if (sublistId < 0 || sublistId >= sublists.size()) { + throw new NoSuchElementException(); + } + + return this.sublists.get(sublistId).getGasUsed(); + } + + public List getTransactionsInOrder() { + List txs = new ArrayList<>(); + for (TransactionSublist sublist: this.sublists) { + txs.addAll(sublist.getTransactions()); + } + return txs; + } + + public short[] getTransactionsPerSublistInOrder() { + List sublistSizes = new ArrayList<>(); + short sublistEdges = 0; + + for (TransactionSublist sublist: this.sublists) { + if (sublist.getTransactions().isEmpty() || sublist.isSequential()) { + continue; + } + sublistEdges += (short) sublist.getTransactions().size(); + sublistSizes.add(sublistEdges); + } + + short[] sublistOrder = new short[sublistSizes.size()]; + int i = 0; + for (Short size: sublistSizes) { + sublistOrder[i] = size; + i++; + } + + return sublistOrder; + } + + public long getGasUsedInSequential() { + return getSequentialSublist().getGasUsed(); + } + + private void addNewKeysToMaps(RskAddress sender, TransactionSublist sublist, Set newReadKeys, Set newWrittenKeys) { + for (ByteArrayWrapper key : newReadKeys) { + Set sublistsAlreadyRead = sublistsHavingReadFromKey.getOrDefault(key, new HashSet<>()); + sublistsAlreadyRead.add(sublist); + sublistsHavingReadFromKey.put(key, sublistsAlreadyRead); + } + + if (sublist.isSequential()) { + sublistOfSender.put(sender, sublist); + } else { + sublistOfSender.putIfAbsent(sender, sublist); + } + + for (ByteArrayWrapper key: newWrittenKeys) { + sublistsHavingWrittenToKey.put(key, sublist); + } + } + + private Optional getSublistBySender(Transaction tx) { + return Optional.ofNullable(sublistOfSender.get(tx.getSender())); + } + + private Optional getAvailableSublistWithLessUsedGas(long txGasLimit) { + long gasUsed = Long.MAX_VALUE; + Optional sublistCandidate = Optional.empty(); + + for (TransactionSublist sublist : sublists) { + if (!sublist.isSequential() && sublist.hasGasAvailable(txGasLimit) && sublist.getGasUsed() < gasUsed) { + sublistCandidate = Optional.of(sublist); + gasUsed = sublist.getGasUsed(); + } + } + + return sublistCandidate; + } + + private TransactionSublist getSublistCandidates(Transaction tx, Set newReadKeys, Set newWrittenKeys) { + Optional sublistCandidate = getSublistBySender(tx); + + if (sublistCandidate.isPresent() && sublistCandidate.get().isSequential()) { + // there is a tx with the same sender in the sequential sublist + return sublistCandidate.get(); + } + + // analyze reads + for (ByteArrayWrapper newReadKey : newReadKeys) { + // read - written + if (sublistsHavingWrittenToKey.containsKey(newReadKey)) { + TransactionSublist sublist = sublistsHavingWrittenToKey.get(newReadKey); + + if (sublist.isSequential()) { + // there is a tx with read-written collision in sequential sublist + return sublist; + } + if (!sublistCandidate.isPresent()) { + // this is the new candidate + sublistCandidate = Optional.of(sublist); + } else if (!sublistCandidate.get().equals(sublist)) { + // use the sequential sublist (greedy decision) + return getSequentialSublist(); + } + } + } + + // analyze writes + for (ByteArrayWrapper newWrittenKey : newWrittenKeys) { + // write - written + if (sublistsHavingWrittenToKey.containsKey(newWrittenKey)) { + TransactionSublist sublist = sublistsHavingWrittenToKey.get(newWrittenKey); + + if (sublist.isSequential()) { + // there is a tx with write-written collision in sequential sublist + return sublist; + } + if (!sublistCandidate.isPresent()) { + // this is the new candidate + sublistCandidate = Optional.of(sublist); + } else if (!sublistCandidate.get().equals(sublist)) { + // use the sequential sublist (greedy decision) + return getSequentialSublist(); + } + } + + // write - read + if (sublistsHavingReadFromKey.containsKey(newWrittenKey)) { + Set setOfsublists = sublistsHavingReadFromKey.get(newWrittenKey); + if (setOfsublists.size() > 1) { + // there is a write-read collision with multiple sublists + return getSequentialSublist(); + } + + // there is only one colluded sublist + TransactionSublist sublist = getNextSublist(setOfsublists); + if (!sublistCandidate.isPresent()) { + // if there is no candidate, take the colluded sublist + sublistCandidate = Optional.of(sublist); + } else if (!sublistCandidate.get().equals(sublist)) { + // otherwise, check if the sublist is different from the candidate and return the sequential + return getSequentialSublist(); + } + } + } + + // if there is no candidate use the sublist with more gas available + // if the is no more gas available in any parallel sublist use the sequential + return sublistCandidate + .orElseGet(() -> getAvailableSublistWithLessUsedGas(GasCost.toGas(tx.getGasLimit())) + .orElseGet(this::getSequentialSublist)); + } + + private TransactionSublist getNextSublist(Set sublist) { + return sublist.iterator().next(); + } + + private TransactionSublist getSequentialSublist() { + return this.sublists.get(this.sublists.size()-1); + } + + private static class TransactionSublist { + + private final long gasLimit; + private final boolean isSequential; + private final List transactions; + private long gasUsedInSublist; + + public TransactionSublist(long sublistGasLimit, boolean isSequential) { + this.gasLimit = sublistGasLimit; + this.isSequential = isSequential; + this.transactions = new ArrayList<>(); + this.gasUsedInSublist = 0; + } + + private void addTransaction(Transaction tx, long gasUsedByTx) { + transactions.add(tx); + gasUsedInSublist = gasUsedInSublist + gasUsedByTx; + } + + private boolean hasGasAvailable(long txGasLimit) { + //TODO(JULI): Re-check a thousand of times this line. + long cumulativeGas = GasCost.add(gasUsedInSublist, txGasLimit); + return cumulativeGas <= gasLimit; + } + + public long getGasUsed() { + return gasUsedInSublist; + } + + public List getTransactions() { + return new ArrayList<>(this.transactions); + } + + public boolean isSequential() { + return isSequential; + } + } +} diff --git a/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java b/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java new file mode 100644 index 00000000000..968465ba20d --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java @@ -0,0 +1,135 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import org.ethereum.db.ByteArrayWrapper; + +import java.util.*; + +public class ReadWrittenKeysTracker implements IReadWrittenKeysTracker { + + private Map> readKeysByThread; + + private Map> writtenKeysByThread; + + + public ReadWrittenKeysTracker() { + this.readKeysByThread = new HashMap<>(); + this.writtenKeysByThread = new HashMap<>(); + } + + @Override + public Set getThisThreadReadKeys(){ + long threadId = Thread.currentThread().getId(); + if (this.readKeysByThread.containsKey(threadId)) { + return new HashSet<>(this.readKeysByThread.get(threadId)); + } else { + return new HashSet<>(); + } + } + + @Override + public Set getThisThreadWrittenKeys(){ + long threadId = Thread.currentThread().getId(); + if (this.writtenKeysByThread.containsKey(threadId)) { + return new HashSet<>(this.writtenKeysByThread.get(threadId)); + } else { + return new HashSet<>(); + } + } + + @Override + public Map> getReadKeysByThread() { + return new HashMap<>(this.readKeysByThread); + } + + @Override + public Map> getWrittenKeysByThread() { + return new HashMap<>(this.writtenKeysByThread); + } + + @Override + public synchronized void addNewReadKey(ByteArrayWrapper key) { + long threadId = Thread.currentThread().getId(); + addNewReadKeyToThread(threadId,key); + } + + public synchronized void addNewReadKeyToThread(long threadId,ByteArrayWrapper key) { + Set readKeys = readKeysByThread.containsKey(threadId)? readKeysByThread.get(threadId) : new HashSet<>(); + readKeys.add(key); + readKeysByThread.put(threadId, readKeys); + } + public synchronized void removeReadKeyToThread(long threadId,ByteArrayWrapper key) { + Set readKeys = readKeysByThread.containsKey(threadId)? readKeysByThread.get(threadId) : new HashSet<>(); + readKeys.remove(key); + readKeysByThread.put(threadId, readKeys); + } + + @Override + public synchronized void addNewWrittenKey(ByteArrayWrapper key) { + long threadId = Thread.currentThread().getId(); + addNewWrittenKeyToThread(threadId,key); + + } + public synchronized void removeWrittenKeyToThread(long threadId,ByteArrayWrapper key) { + Set writtenKeys = writtenKeysByThread.containsKey(threadId)? writtenKeysByThread.get(threadId) : new HashSet<>(); + writtenKeys.remove(key); + writtenKeysByThread.put(threadId, writtenKeys); + } + + public synchronized void addNewWrittenKeyToThread(long threadId,ByteArrayWrapper key) { + Set writtenKeys = writtenKeysByThread.containsKey(threadId)? writtenKeysByThread.get(threadId) : new HashSet<>(); + writtenKeys.add(key); + writtenKeysByThread.put(threadId, writtenKeys); + } + + @Override + public synchronized void clear() { + this.readKeysByThread = new HashMap<>(); + this.writtenKeysByThread = new HashMap<>(); + } + + public boolean detectCollision() { + Set threads = new HashSet<>(); + threads.addAll(readKeysByThread.keySet()); + threads.addAll(writtenKeysByThread.keySet()); + + for (Long threadId : threads) { + Set baseReadKeys = readKeysByThread.getOrDefault(threadId, new HashSet<>()); + Set baseWrittenKeys = writtenKeysByThread.getOrDefault(threadId, new HashSet<>()); + + for (Long threadId2 : threads) { + if (threadId >= threadId2) { + continue; + } + + Set temporalReadKeys = readKeysByThread.getOrDefault(threadId2, new HashSet<>()); + Set temporalWrittenKeys = writtenKeysByThread.getOrDefault(threadId2, new HashSet<>()); + + boolean isDisjoint = Collections.disjoint(baseWrittenKeys, temporalWrittenKeys) && Collections.disjoint(baseWrittenKeys, temporalReadKeys) + && Collections.disjoint(baseReadKeys, temporalWrittenKeys); + + if (!isDisjoint) { + return true; + } + } + } + return false; + } +} diff --git a/rskj-core/src/main/java/co/rsk/core/types/bytes/Bytes.java b/rskj-core/src/main/java/co/rsk/core/types/bytes/Bytes.java index ce988888068..cb4c16d17d3 100644 --- a/rskj-core/src/main/java/co/rsk/core/types/bytes/Bytes.java +++ b/rskj-core/src/main/java/co/rsk/core/types/bytes/Bytes.java @@ -23,7 +23,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Arrays; import java.util.Objects; /** @@ -140,8 +139,8 @@ public byte byteAt(int index) { } @Override - public byte[] copyArrayOfRange(int from, int to) { - return Arrays.copyOfRange(byteArray, from, to); + public void arraycopy(int srcPos, byte[] dest, int destPos, int length) { + System.arraycopy(byteArray, srcPos, dest, destPos, length); } @Override diff --git a/rskj-core/src/main/java/co/rsk/core/types/bytes/BytesSlice.java b/rskj-core/src/main/java/co/rsk/core/types/bytes/BytesSlice.java index 5210ff8eea0..ac3616fd8f0 100644 --- a/rskj-core/src/main/java/co/rsk/core/types/bytes/BytesSlice.java +++ b/rskj-core/src/main/java/co/rsk/core/types/bytes/BytesSlice.java @@ -18,11 +18,62 @@ package co.rsk.core.types.bytes; +import java.util.Arrays; + /** * A {@link BytesSlice} is a subsequence of bytes backed by another broader byte sequence. */ public interface BytesSlice extends HexPrintableBytes { + /** + * Copies an array from the {@link BytesSlice} source, beginning at the + * specified position, to the specified position of the destination array. + * A subsequence of array components are copied from this instance to the + * destination array referenced by {@code dest}. The number of components + * copied is equal to the {@code length} argument. The components at + * positions {@code srcPos} through {@code srcPos+length-1} in the source + * array are copied into positions {@code destPos} through + * {@code destPos+length-1}, respectively, of the destination + * array. + *

+ * If the underlying byte array and {@code dest} argument refer to the + * same array object, then the copying is performed as if the + * components at positions {@code srcPos} through + * {@code srcPos+length-1} were first copied to a temporary + * array with {@code length} components and then the contents of + * the temporary array were copied into positions + * {@code destPos} through {@code destPos+length-1} of the + * destination array. + *

+ * If {@code dest} is {@code null}, then a + * {@code NullPointerException} is thrown. + *

+ * Otherwise, if any of the following is true, an + * {@code IndexOutOfBoundsException} is + * thrown and the destination is not modified: + *

    + *
  • The {@code srcPos} argument is negative. + *
  • The {@code destPos} argument is negative. + *
  • The {@code length} argument is negative. + *
  • {@code srcPos+length} is greater than + * {@code src.length}, the length of the source array. + *
  • {@code destPos+length} is greater than + * {@code dest.length}, the length of the destination array. + *
+ * + *

+ * Note: this method mimics behaviour of {@link System#arraycopy(Object, int, Object, int, int)} + * + * @param srcPos starting position in the source array. + * @param dest the destination array. + * @param destPos starting position in the destination data. + * @param length the number of array elements to be copied. + * @throws IndexOutOfBoundsException if copying would cause + * access of data outside array bounds. + * @throws NullPointerException if {@code dest} is {@code null}. + */ + void arraycopy(int srcPos, byte[] dest, int destPos, int length); + /** * Copies the specified range of the specified array into a new array. * The initial index of the range (from) must lie between zero @@ -37,17 +88,30 @@ public interface BytesSlice extends HexPrintableBytes { * greater than or equal to original.length - from. The length * of the returned array will be to - from. * + *

+ * Note: this method mimics behaviour of {@link Arrays#copyOfRange(Object[], int, int)} + * * @param from the initial index of the range to be copied, inclusive * @param to the final index of the range to be copied, exclusive. * (This index may lie outside the array.) * @return a new array containing the specified range from the original array, * truncated or padded with zeros to obtain the required length - * @throws ArrayIndexOutOfBoundsException if {@code from < 0} + * @throws IndexOutOfBoundsException if {@code from < 0} * or {@code from > original.length} * @throws IllegalArgumentException if from > to - * @throws NullPointerException if original is null */ - byte[] copyArrayOfRange(int from, int to); + default byte[] copyArrayOfRange(int from, int to) { + if (from < 0 || from > length()) { + throw new IndexOutOfBoundsException("invalid 'from': " + from); + } + int newLength = to - from; + if (newLength < 0) { + throw new IllegalArgumentException(from + " > " + to); + } + byte[] copy = new byte[newLength]; + arraycopy(from, copy, 0, Math.min(length() - from, newLength)); + return copy; + } default byte[] copyArray() { return copyArrayOfRange(0, length()); @@ -104,11 +168,17 @@ public byte byteAt(int index) { } @Override - public byte[] copyArrayOfRange(int from, int to) { - if (from < 0 || from > to || to > length()) { - throw new IndexOutOfBoundsException("invalid 'from' and/or 'to': [" + from + ";" + to + ")"); + public void arraycopy(int srcPos, byte[] dest, int destPos, int length) { + if (length < 0) { + throw new IndexOutOfBoundsException("invalid 'length': " + length); + } + if (srcPos < 0 || srcPos + length > length()) { + throw new IndexOutOfBoundsException("invalid 'srcPos' and/or 'length': [" + srcPos + ";" + length + ")"); + } + if (destPos < 0 || destPos + length > dest.length) { + throw new IndexOutOfBoundsException("invalid 'destPos' and/or 'length': [" + destPos + ";" + length + ")"); } - return originBytes.copyArrayOfRange(this.from + from, this.from + to); + originBytes.arraycopy(this.from + srcPos, dest, destPos, length); } @Override diff --git a/rskj-core/src/main/java/co/rsk/core/types/bytes/HexPrintableBytes.java b/rskj-core/src/main/java/co/rsk/core/types/bytes/HexPrintableBytes.java index d700c0cc6a6..3f91f918eda 100644 --- a/rskj-core/src/main/java/co/rsk/core/types/bytes/HexPrintableBytes.java +++ b/rskj-core/src/main/java/co/rsk/core/types/bytes/HexPrintableBytes.java @@ -77,7 +77,7 @@ public String toFormattedString(@Nonnull HexPrintableBytes printableBytes, int o } if (length > 32) { - return printableBytes.toHexString(off, 15) + ".." + printableBytes.toHexString(off + length - 15, 15); + return printableBytes.toHexString(off, 16) + ".." + printableBytes.toHexString(off + length - 15, 15); } return printableBytes.toHexString(off, length); } diff --git a/rskj-core/src/main/java/co/rsk/db/MutableTrieCache.java b/rskj-core/src/main/java/co/rsk/db/MutableTrieCache.java index 997b07defd2..64f8bc729e9 100644 --- a/rskj-core/src/main/java/co/rsk/db/MutableTrieCache.java +++ b/rskj-core/src/main/java/co/rsk/db/MutableTrieCache.java @@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; public class MutableTrieCache implements MutableTrie { @@ -40,14 +41,14 @@ public class MutableTrieCache implements MutableTrie { private MutableTrie trie; // We use a single cache to mark both changed elements and removed elements. // null value means the element has been removed. - private final Map> cache; + private final Map>> cache; // this logs recursive delete operations to be performed at commit time private final Set deleteRecursiveLog; public MutableTrieCache(MutableTrie parentTrie) { trie = parentTrie; - cache = new HashMap<>(); + cache = new ConcurrentHashMap<>(); deleteRecursiveLog = new HashSet<>(); } @@ -75,7 +76,7 @@ private Optional internalGet( ByteArrayWrapper wrapper = new ByteArrayWrapper(key); ByteArrayWrapper accountWrapper = getAccountWrapper(wrapper); - Map accountItems = cache.get(accountWrapper); + Map> accountItems = cache.get(accountWrapper); boolean isDeletedAccount = deleteRecursiveLog.contains(accountWrapper); if (accountItems == null || !accountItems.containsKey(wrapper)) { if (isDeletedAccount) { @@ -85,14 +86,14 @@ private Optional internalGet( return Optional.ofNullable(trieRetriever.apply(key)); } - byte[] cacheItem = accountItems.get(wrapper); - if (cacheItem == null) { + Optional cacheItem = accountItems.get(wrapper); + if (!cacheItem.isPresent()) { // deleted account key return Optional.empty(); } // cached account key - return Optional.ofNullable(cacheTransformer.apply(cacheItem)); + return Optional.ofNullable(cacheTransformer.apply(cacheItem.get())); } public Iterator getStorageKeys(RskAddress addr) { @@ -100,7 +101,7 @@ public Iterator getStorageKeys(RskAddress addr) { ByteArrayWrapper accountWrapper = getAccountWrapper(new ByteArrayWrapper(accountStoragePrefixKey)); boolean isDeletedAccount = deleteRecursiveLog.contains(accountWrapper); - Map accountItems = cache.get(accountWrapper); + Map> accountItems = cache.get(accountWrapper); if (accountItems == null && isDeletedAccount) { return Collections.emptyIterator(); } @@ -139,8 +140,8 @@ public void put(ByteArrayWrapper wrapper, byte[] value) { // in cache with null or in deleteCache. Here we have the choice to // to add it to cache with null value or to deleteCache. ByteArrayWrapper accountWrapper = getAccountWrapper(wrapper); - Map accountMap = cache.computeIfAbsent(accountWrapper, k -> new HashMap<>()); - accountMap.put(wrapper, value); + Map> accountMap = cache.computeIfAbsent(accountWrapper, k -> new ConcurrentHashMap<>()); + accountMap.put(wrapper, Optional.ofNullable(value)); } @Override @@ -179,7 +180,7 @@ public void commit() { cache.forEach((accountKey, accountData) -> { if (accountData != null) { // cached account - accountData.forEach((realKey, value) -> this.trie.put(realKey, value)); + accountData.forEach((realKey, value) -> this.trie.put(realKey, value.orElse(null))); } }); @@ -242,7 +243,7 @@ public Optional getValueHash(byte[] key) { private static class StorageKeysIterator implements Iterator { private final Iterator keysIterator; - private final Map accountItems; + private final Map> accountItems; private final RskAddress address; private final int storageKeyOffset = ( TrieKeyMapper.domainPrefix().length + @@ -252,11 +253,11 @@ private static class StorageKeysIterator implements Iterator { * Byte.SIZE; private final TrieKeyMapper trieKeyMapper; private DataWord currentStorageKey; - private Iterator> accountIterator; + private Iterator>> accountIterator; StorageKeysIterator( Iterator keysIterator, - Map accountItems, + Map> accountItems, RskAddress addr, TrieKeyMapper trieKeyMapper) { this.keysIterator = keysIterator; @@ -275,8 +276,8 @@ public boolean hasNext() { DataWord item = keysIterator.next(); ByteArrayWrapper fullKey = getCompleteKey(item); if (accountItems.containsKey(fullKey)) { - byte[] value = accountItems.remove(fullKey); - if (value == null){ + Optional value = accountItems.remove(fullKey); + if (!value.isPresent()){ continue; } } @@ -289,9 +290,9 @@ public boolean hasNext() { } while (accountIterator.hasNext()) { - Map.Entry entry = accountIterator.next(); + Map.Entry> entry = accountIterator.next(); byte[] key = entry.getKey().getData(); - if (entry.getValue() != null && key.length * Byte.SIZE > storageKeyOffset) { + if (entry.getValue().isPresent() && key.length * Byte.SIZE > storageKeyOffset) { // cached account key currentStorageKey = getPartialKey(key); return true; diff --git a/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java b/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java index 3a3eef66fec..7d328039bc5 100644 --- a/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java +++ b/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java @@ -18,6 +18,7 @@ package co.rsk.db; +import co.rsk.core.bc.IReadWrittenKeysTracker; import co.rsk.crypto.Keccak256; import co.rsk.trie.MutableTrie; import co.rsk.trie.Trie; @@ -77,6 +78,13 @@ public Repository startTrackingAt(BlockHeader header) { .orElseThrow(() -> trieNotFoundException(header)); } + public Repository startTrackingAt(BlockHeader header, IReadWrittenKeysTracker tracker) { + return mutableTrieSnapshotAt(header) + .map(MutableTrieCache::new) + .map(mutableTrieCache -> new MutableRepository(mutableTrieCache, tracker)) + .orElseThrow(() -> trieNotFoundException(header)); + } + private IllegalArgumentException trieNotFoundException(BlockHeader header) { return new IllegalArgumentException(String.format( "The trie with root %s is missing in this store", header.getHash() diff --git a/rskj-core/src/main/java/co/rsk/mine/BlockToMineBuilder.java b/rskj-core/src/main/java/co/rsk/mine/BlockToMineBuilder.java index 42b895dd20c..dfa07fed3a0 100644 --- a/rskj-core/src/main/java/co/rsk/mine/BlockToMineBuilder.java +++ b/rskj-core/src/main/java/co/rsk/mine/BlockToMineBuilder.java @@ -249,6 +249,7 @@ private BlockHeader createHeader( .setMinimumGasPrice(minimumGasPrice) .setUncleCount(uncles.size()) .setUmmRoot(ummRoot) + .setCreateParallelCompliantHeader(activationConfig.isActive(ConsensusRule.RSKIP144, blockNumber)) .build(); newHeader.setDifficulty(difficultyCalculator.calcDifficulty(newHeader, newBlockParentHeader)); diff --git a/rskj-core/src/main/java/co/rsk/net/NodeBlockProcessor.java b/rskj-core/src/main/java/co/rsk/net/NodeBlockProcessor.java index 2e2a303dc6c..1a3a7b52afc 100644 --- a/rskj-core/src/main/java/co/rsk/net/NodeBlockProcessor.java +++ b/rskj-core/src/main/java/co/rsk/net/NodeBlockProcessor.java @@ -227,7 +227,7 @@ public void processBodyRequest(@Nonnull final Peer sender, long requestId, @Nonn return; } - Message responseMessage = new BodyResponseMessage(requestId, block.getTransactionsList(), block.getUncleList()); + Message responseMessage = new BodyResponseMessage(requestId, block.getTransactionsList(), block.getUncleList(), block.getHeader().getExtension()); sender.sendMessage(responseMessage); } diff --git a/rskj-core/src/main/java/co/rsk/net/handler/TxPendingValidator.java b/rskj-core/src/main/java/co/rsk/net/handler/TxPendingValidator.java index 24416ac005f..54b81c17b5a 100644 --- a/rskj-core/src/main/java/co/rsk/net/handler/TxPendingValidator.java +++ b/rskj-core/src/main/java/co/rsk/net/handler/TxPendingValidator.java @@ -18,11 +18,13 @@ package co.rsk.net.handler; import co.rsk.core.Coin; +import co.rsk.core.bc.BlockUtils; import co.rsk.net.TransactionValidationResult; import co.rsk.net.handler.txvalidator.*; import org.bouncycastle.util.BigIntegers; import org.ethereum.config.Constants; import org.ethereum.config.blockchain.upgrades.ActivationConfig; +import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.ethereum.core.AccountState; import org.ethereum.core.Block; import org.ethereum.core.SignatureCache; @@ -69,10 +71,14 @@ public TxPendingValidator(Constants constants, ActivationConfig activationConfig } public TransactionValidationResult isValid(Transaction tx, Block executionBlock, @Nullable AccountState state) { - BigInteger blockGasLimit = BigIntegers.fromUnsignedByteArray(executionBlock.getGasLimit()); + long executionBlockNumber = executionBlock.getNumber(); + ActivationConfig.ForBlock activations = activationConfig.forBlock(executionBlockNumber); + BigInteger gasLimit = activations.isActive(ConsensusRule.RSKIP144) + ? BigInteger.valueOf(Math.max(BlockUtils.getSublistGasLimit(executionBlock, true, constants.getMinSequentialSetGasLimit()), BlockUtils.getSublistGasLimit(executionBlock, false, constants.getMinSequentialSetGasLimit()))) + : BigIntegers.fromUnsignedByteArray(executionBlock.getGasLimit()); Coin minimumGasPrice = executionBlock.getMinimumGasPrice(); long bestBlockNumber = executionBlock.getNumber(); - long basicTxCost = tx.transactionCost(constants, activationConfig.forBlock(bestBlockNumber), signatureCache); + long basicTxCost = tx.transactionCost(constants, activations, signatureCache); if (state == null && basicTxCost != 0) { if (logger.isTraceEnabled()) { @@ -81,7 +87,7 @@ public TransactionValidationResult isValid(Transaction tx, Block executionBlock, return TransactionValidationResult.withError("the sender account doesn't exist"); } - if(tx.isInitCodeSizeInvalidForTx(activationConfig.forBlock(bestBlockNumber))) { + if (tx.isInitCodeSizeInvalidForTx(activationConfig.forBlock(bestBlockNumber))) { return TransactionValidationResult.withError("transaction's init code size is invalid"); } @@ -90,7 +96,7 @@ public TransactionValidationResult isValid(Transaction tx, Block executionBlock, } for (TxValidatorStep step : validatorSteps) { - TransactionValidationResult validationResult = step.validate(tx, state, blockGasLimit, minimumGasPrice, bestBlockNumber, basicTxCost == 0); + TransactionValidationResult validationResult = step.validate(tx, state, gasLimit, minimumGasPrice, executionBlockNumber, basicTxCost == 0); if (!validationResult.transactionIsValid()) { logger.info("[tx={}] validation failed with error: {}", tx.getHash(), validationResult.getErrorMessage()); return validationResult; diff --git a/rskj-core/src/main/java/co/rsk/net/messages/BlockHeadersResponseMessage.java b/rskj-core/src/main/java/co/rsk/net/messages/BlockHeadersResponseMessage.java index ada7749f442..d38324cb303 100644 --- a/rskj-core/src/main/java/co/rsk/net/messages/BlockHeadersResponseMessage.java +++ b/rskj-core/src/main/java/co/rsk/net/messages/BlockHeadersResponseMessage.java @@ -32,7 +32,7 @@ public BlockHeadersResponseMessage(long id, List headers) { @Override protected byte[] getEncodedMessageWithoutId() { byte[][] rlpHeaders = this.blockHeaders.stream() - .map(BlockHeader::getFullEncoded) + .map(BlockHeader::getEncodedCompressed) .toArray(byte[][]::new); return RLP.encodeList(RLP.encodeList(rlpHeaders)); diff --git a/rskj-core/src/main/java/co/rsk/net/messages/BodyResponseMessage.java b/rskj-core/src/main/java/co/rsk/net/messages/BodyResponseMessage.java index 41c73c76de7..b857f262fc9 100644 --- a/rskj-core/src/main/java/co/rsk/net/messages/BodyResponseMessage.java +++ b/rskj-core/src/main/java/co/rsk/net/messages/BodyResponseMessage.java @@ -1,6 +1,8 @@ package co.rsk.net.messages; +import com.google.common.collect.Lists; import org.ethereum.core.BlockHeader; +import org.ethereum.core.BlockHeaderExtension; import org.ethereum.core.Transaction; import org.ethereum.util.RLP; @@ -10,14 +12,16 @@ * Created by ajlopez on 25/08/2017. */ public class BodyResponseMessage extends MessageWithId { - private long id; - private List transactions; - private List uncles; + private final long id; + private final List transactions; + private final List uncles; + private final BlockHeaderExtension blockHeaderExtension; - public BodyResponseMessage(long id, List transactions, List uncles) { + public BodyResponseMessage(long id, List transactions, List uncles, BlockHeaderExtension blockHeaderExtension) { this.id = id; this.transactions = transactions; this.uncles = uncles; + this.blockHeaderExtension = blockHeaderExtension; } @Override @@ -26,6 +30,7 @@ public BodyResponseMessage(long id, List transactions, List getTransactions() { return this.transactions; } public List getUncles() { return this.uncles; } + public BlockHeaderExtension getBlockHeaderExtension() { return this.blockHeaderExtension; } @Override protected byte[] getEncodedMessageWithoutId() { @@ -40,7 +45,13 @@ protected byte[] getEncodedMessageWithoutId() { rlpUncles[k] = this.uncles.get(k).getFullEncoded(); } - return RLP.encodeList(RLP.encodeList(rlpTransactions), RLP.encodeList(rlpUncles)); + List elements = Lists.newArrayList(RLP.encodeList(rlpTransactions), RLP.encodeList(rlpUncles)); + + if (this.blockHeaderExtension != null) { + elements.add(BlockHeaderExtension.toEncoded(blockHeaderExtension)); + } + + return RLP.encodeList(elements.toArray(new byte[][]{})); } @Override diff --git a/rskj-core/src/main/java/co/rsk/net/messages/MessageType.java b/rskj-core/src/main/java/co/rsk/net/messages/MessageType.java index ddb5ac7534e..2b8da7a78d0 100644 --- a/rskj-core/src/main/java/co/rsk/net/messages/MessageType.java +++ b/rskj-core/src/main/java/co/rsk/net/messages/MessageType.java @@ -143,7 +143,7 @@ public Message createMessage(BlockFactory blockFactory, RLPList list) { for (int k = 0; k < rlpHeaders.size(); k++) { RLPElement element = rlpHeaders.get(k); - BlockHeader header = blockFactory.decodeHeader(element.getRLPData()); + BlockHeader header = blockFactory.decodeHeader(element.getRLPData(), true); headers.add(header); } @@ -229,10 +229,14 @@ public Message createMessage(BlockFactory blockFactory, RLPList list) { for (int j = 0; j < rlpUncles.size(); j++) { RLPElement element = rlpUncles.get(j); - uncles.add(blockFactory.decodeHeader(element.getRLPData())); + uncles.add(blockFactory.decodeHeader(element.getRLPData(), false)); } - return new BodyResponseMessage(id, transactions, uncles); + BlockHeaderExtension blockHeaderExtension = message.size() == 3 + ? BlockHeaderExtension.fromEncoded(message.get(2).getRLPData()) + : null; + + return new BodyResponseMessage(id, transactions, uncles, blockHeaderExtension); } }, SKELETON_REQUEST_MESSAGE(16) { diff --git a/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBackwardsBodiesSyncState.java b/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBackwardsBodiesSyncState.java index e3a663c4939..e59da6fcaac 100644 --- a/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBackwardsBodiesSyncState.java +++ b/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBackwardsBodiesSyncState.java @@ -94,6 +94,7 @@ public void newBody(BodyResponseMessage body, Peer peer) { return; } + requestedHeader.setExtension(body.getBlockHeaderExtension()); Block block = blockFactory.newBlock(requestedHeader, body.getTransactions(), body.getUncles()); block.seal(); diff --git a/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBodiesSyncState.java b/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBodiesSyncState.java index dc1eedf0dd4..1eb1d3e1255 100644 --- a/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBodiesSyncState.java +++ b/rskj-core/src/main/java/co/rsk/net/sync/DownloadingBodiesSyncState.java @@ -116,6 +116,7 @@ public void newBody(BodyResponseMessage message, Peer peer) { // we already checked that this message was expected BlockHeader header = pendingBodyResponses.remove(requestId).header; + header.setExtension(message.getBlockHeaderExtension()); Block block; try { block = blockFactory.newBlock(header, message.getTransactions(), message.getUncles()); diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java b/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java index 4683068e18c..9202aebebfe 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeEvents.java @@ -5,110 +5,80 @@ public enum BridgeEvents { - LOCK_BTC("lock_btc", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.RECEIVER, SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(false, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, "senderBtcAddress", SolidityType.getType(SolidityType.STRING)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.INT256)) - } - ), - PEGIN_BTC("pegin_btc", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.RECEIVER, SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.INT256)), - new CallTransaction.Param(false, "protocolVersion", SolidityType.getType(SolidityType.INT256)) - } - ), - REJECTED_PEGIN("rejected_pegin", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) - } - ), - UNREFUNDABLE_PEGIN("unrefundable_pegin", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) - } - ), - UPDATE_COLLECTIONS("update_collections", - new CallTransaction.Param[]{ - new CallTransaction.Param(false, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)) - } - ), - ADD_SIGNATURE("add_signature", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.RELEASE_RSK_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(true, "federatorRskAddress", SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(false, "federatorBtcPublicKey", SolidityType.getType(SolidityType.BYTES)) - } - ), - RELEASE_BTC("release_btc", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.RELEASE_RSK_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, "btcRawTransaction", SolidityType.getType(SolidityType.BYTES)) - } - ), - COMMIT_FEDERATION("commit_federation", - new CallTransaction.Param[]{ - new CallTransaction.Param(false, "oldFederationBtcPublicKeys", SolidityType.getType(SolidityType.BYTES)), - new CallTransaction.Param(false, "oldFederationBtcAddress", SolidityType.getType(SolidityType.STRING)), - new CallTransaction.Param(false, "newFederationBtcPublicKeys", SolidityType.getType(SolidityType.BYTES)), - new CallTransaction.Param(false, "newFederationBtcAddress", SolidityType.getType(SolidityType.STRING)), - new CallTransaction.Param(false, "activationHeight", SolidityType.getType(SolidityType.INT256)) - } - ), - RELEASE_REQUESTED("release_requested", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, "rskTxHash", SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) - } - ), - RELEASE_REQUEST_RECEIVED_LEGACY("release_request_received", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(false, "btcDestinationAddress", SolidityType.getType(SolidityType.BYTES)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) - } - ), - RELEASE_REQUEST_REJECTED("release_request_rejected", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)), - new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) - } - ), - BATCH_PEGOUT_CREATED("batch_pegout_created", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.RELEASE_RSK_TX_HASHES, SolidityType.getType(SolidityType.BYTES)) - } - ), - RELEASE_REQUEST_RECEIVED("release_request_received", - new CallTransaction.Param[]{ + LOCK_BTC("lock_btc", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.RECEIVER, SolidityType.getType(SolidityType.ADDRESS)), + new CallTransaction.Param(false, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, "senderBtcAddress", SolidityType.getType(SolidityType.STRING)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.INT256)) + }), + PEGIN_BTC("pegin_btc", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.RECEIVER, SolidityType.getType(SolidityType.ADDRESS)), + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.INT256)), + new CallTransaction.Param(false, "protocolVersion", SolidityType.getType(SolidityType.INT256)) + }), + REJECTED_PEGIN("rejected_pegin", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) + }), + UNREFUNDABLE_PEGIN("unrefundable_pegin", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) + }), + UPDATE_COLLECTIONS("update_collections", new CallTransaction.Param[] { + new CallTransaction.Param(false, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)) + }), + ADD_SIGNATURE("add_signature", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.RELEASE_RSK_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(true, "federatorRskAddress", SolidityType.getType(SolidityType.ADDRESS)), + new CallTransaction.Param(false, "federatorBtcPublicKey", SolidityType.getType(SolidityType.BYTES)) + }), + RELEASE_BTC("release_btc", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.RELEASE_RSK_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, "btcRawTransaction", SolidityType.getType(SolidityType.BYTES)) + }), + COMMIT_FEDERATION("commit_federation", new CallTransaction.Param[] { + new CallTransaction.Param(false, "oldFederationBtcPublicKeys", SolidityType.getType(SolidityType.BYTES)), + new CallTransaction.Param(false, "oldFederationBtcAddress", SolidityType.getType(SolidityType.STRING)), + new CallTransaction.Param(false, "newFederationBtcPublicKeys", SolidityType.getType(SolidityType.BYTES)), + new CallTransaction.Param(false, "newFederationBtcAddress", SolidityType.getType(SolidityType.STRING)), + new CallTransaction.Param(false, "activationHeight", SolidityType.getType(SolidityType.INT256)) + }), + RELEASE_REQUESTED("release_requested", new CallTransaction.Param[] { + new CallTransaction.Param(true, "rskTxHash", SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) + }), + RELEASE_REQUEST_RECEIVED_LEGACY("release_request_received", new CallTransaction.Param[] { new CallTransaction.Param(true, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)), - new CallTransaction.Param(false, "btcDestinationAddress", SolidityType.getType(SolidityType.STRING)), - new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) - } - ), - PEGOUT_CONFIRMED("pegout_confirmed", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, "pegoutCreationRskBlockNumber", SolidityType.getType(SolidityType.UINT256)) - } - ), - PEGOUT_TRANSACTION_CREATED("pegout_transaction_created", - new CallTransaction.Param[]{ - new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), - new CallTransaction.Param(false, Fields.UTXO_OUTPOINT_VALUES, SolidityType.getType(SolidityType.BYTES)) - } - ); + new CallTransaction.Param(false, Fields.BTC_DESTINATION_ADDRESS, SolidityType.getType(SolidityType.BYTES)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) + }), + RELEASE_REQUEST_REJECTED("release_request_rejected", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)), + new CallTransaction.Param(false, Fields.REASON, SolidityType.getType(SolidityType.INT256)) + }), + BATCH_PEGOUT_CREATED("batch_pegout_created", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.RELEASE_RSK_TX_HASHES, SolidityType.getType(SolidityType.BYTES)) + }), + RELEASE_REQUEST_RECEIVED("release_request_received", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.SENDER, SolidityType.getType(SolidityType.ADDRESS)), + new CallTransaction.Param(false, Fields.BTC_DESTINATION_ADDRESS, SolidityType.getType(SolidityType.STRING)), + new CallTransaction.Param(false, Fields.AMOUNT, SolidityType.getType(SolidityType.UINT256)) + }), + PEGOUT_CONFIRMED("pegout_confirmed", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, "pegoutCreationRskBlockNumber", SolidityType.getType(SolidityType.UINT256)) + }), + PEGOUT_TRANSACTION_CREATED("pegout_transaction_created", new CallTransaction.Param[] { + new CallTransaction.Param(true, Fields.BTC_TX_HASH, SolidityType.getType(SolidityType.BYTES32)), + new CallTransaction.Param(false, Fields.UTXO_OUTPOINT_VALUES, SolidityType.getType(SolidityType.BYTES)) + }); - private String eventName; - private CallTransaction.Param[] params; + private final String eventName; + private final CallTransaction.Param[] params; BridgeEvents(String eventName, CallTransaction.Param[] params) { this.eventName = eventName; @@ -128,5 +98,6 @@ private static class Fields { private static final String RELEASE_RSK_TX_HASH = "releaseRskTxHash"; private static final String RELEASE_RSK_TX_HASHES = "releaseRskTxHashes"; private static final String UTXO_OUTPOINT_VALUES = "utxoOutpointValues"; + private static final String BTC_DESTINATION_ADDRESS = "btcDestinationAddress"; } } diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java b/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java index 2fbdb688a55..80458a14139 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeMethods.java @@ -19,6 +19,7 @@ import static org.ethereum.config.blockchain.upgrades.ConsensusRule.*; +import java.math.BigInteger; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -31,7 +32,11 @@ import org.ethereum.vm.MessageCall.MsgType; /** - * This enum holds the basic information of the Bridge contract methods: the ABI, the cost and the implementation. + * Represents the methods of the Bridge contract, encapsulating details such as + * the Application Binary Interface (ABI), execution costs, and method implementations. + * + * Each enum constant corresponds to a specific method of the Bridge contract, + * defining its signature and providing the necessary information for execution. */ public enum BridgeMethods { ADD_FEDERATOR_PUBLIC_KEY( @@ -41,7 +46,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(13000L), - (BridgeMethodExecutorTyped) Bridge::addFederatorPublicKey, + (BridgeMethodExecutorTyped) Bridge::addFederatorPublicKey, activations -> !activations.isActive(RSKIP123), fixedPermission(false) ), @@ -52,7 +57,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(13000L), - (BridgeMethodExecutorTyped) Bridge::addFederatorPublicKeyMultikey, + (BridgeMethodExecutorTyped) Bridge::addFederatorPublicKeyMultikey, activations -> activations.isActive(RSKIP123), fixedPermission(false) ), @@ -63,7 +68,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(25000L), - (BridgeMethodExecutorTyped) Bridge::addOneOffLockWhitelistAddress, + (BridgeMethodExecutorTyped) Bridge::addOneOffLockWhitelistAddress, activations -> !activations.isActive(RSKIP87), fixedPermission(false) ), @@ -74,7 +79,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(25000L), // using same gas estimation as ADD_LOCK_WHITELIST_ADDRESS - (BridgeMethodExecutorTyped) Bridge::addOneOffLockWhitelistAddress, + (BridgeMethodExecutorTyped) Bridge::addOneOffLockWhitelistAddress, activations -> activations.isActive(RSKIP87), fixedPermission(false) ), @@ -85,7 +90,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(25000L), // using same gas estimation as ADD_LOCK_WHITELIST_ADDRESS - (BridgeMethodExecutorTyped) Bridge::addUnlimitedLockWhitelistAddress, + (BridgeMethodExecutorTyped) Bridge::addUnlimitedLockWhitelistAddress, activations -> activations.isActive(RSKIP87), fixedPermission(false) ), @@ -106,7 +111,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(38000L), - (BridgeMethodExecutorTyped) Bridge::commitFederation, + (BridgeMethodExecutorTyped) Bridge::commitFederation, fixedPermission(false) ), CREATE_FEDERATION( @@ -116,7 +121,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(11000L), - (BridgeMethodExecutorTyped) Bridge::createFederation, + (BridgeMethodExecutorTyped) Bridge::createFederation, fixedPermission(false) ), GET_BTC_BLOCKCHAIN_BEST_CHAIN_HEIGHT( @@ -126,7 +131,7 @@ public enum BridgeMethods { new String[]{"int"} ), fixedCost(19000L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBestChainHeight, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBestChainHeight, fromMethod(Bridge::getBtcBlockchainBestChainHeightOnlyAllowsLocalCalls), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -137,7 +142,7 @@ public enum BridgeMethods { new String[]{"int"} ), fixedCost(20000L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainInitialBlockHeight, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainInitialBlockHeight, activations -> activations.isActive(RSKIP89), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -149,7 +154,7 @@ public enum BridgeMethods { new String[]{"string[]"} ), fixedCost(76000L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockLocator, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockLocator, activations -> !activations.isActive(RSKIP89), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -161,7 +166,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(20000L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHashAtDepth, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHashAtDepth, activations -> activations.isActive(RSKIP89), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -173,7 +178,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fromMethod(Bridge::getBtcTransactionConfirmationsGetCost), - (BridgeMethodExecutorTyped) Bridge::getBtcTransactionConfirmations, + (BridgeMethodExecutorTyped) Bridge::getBtcTransactionConfirmations, activations -> activations.isActive(RSKIP122), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -185,7 +190,7 @@ public enum BridgeMethods { new String[]{"int64"} ), fixedCost(22000L), - (BridgeMethodExecutorTyped) Bridge::getBtcTxHashProcessedHeight, + (BridgeMethodExecutorTyped) Bridge::getBtcTxHashProcessedHeight, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -196,7 +201,7 @@ public enum BridgeMethods { new String[]{"string"} ), fixedCost(11000L), - (BridgeMethodExecutorTyped) Bridge::getFederationAddress, + (BridgeMethodExecutorTyped) Bridge::getFederationAddress, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -207,7 +212,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::getFederationCreationBlockNumber, + (BridgeMethodExecutorTyped) Bridge::getFederationCreationBlockNumber, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -218,7 +223,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::getFederationCreationTime, + (BridgeMethodExecutorTyped) Bridge::getFederationCreationTime, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -229,7 +234,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::getFederationSize, + (BridgeMethodExecutorTyped) Bridge::getFederationSize, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -240,7 +245,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(11000L), - (BridgeMethodExecutorTyped) Bridge::getFederationThreshold, + (BridgeMethodExecutorTyped) Bridge::getFederationThreshold, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -251,7 +256,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::getFederatorPublicKey, + (BridgeMethodExecutorTyped) Bridge::getFederatorPublicKey, activations -> !activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -263,7 +268,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::getFederatorPublicKeyOfType, + (BridgeMethodExecutorTyped) Bridge::getFederatorPublicKeyOfType, activations -> activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -275,7 +280,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(2000L), - (BridgeMethodExecutorTyped) Bridge::getFeePerKb, + (BridgeMethodExecutorTyped) Bridge::getFeePerKb, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -286,7 +291,7 @@ public enum BridgeMethods { new String[]{"string"} ), fixedCost(16000L), - (BridgeMethodExecutorTyped) Bridge::getLockWhitelistAddress, + (BridgeMethodExecutorTyped) Bridge::getLockWhitelistAddress, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -297,7 +302,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(16000L), - (BridgeMethodExecutorTyped) Bridge::getLockWhitelistEntryByAddress, + (BridgeMethodExecutorTyped) Bridge::getLockWhitelistEntryByAddress, activations -> activations.isActive(RSKIP87), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -309,7 +314,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(16000L), - (BridgeMethodExecutorTyped) Bridge::getLockWhitelistSize, + (BridgeMethodExecutorTyped) Bridge::getLockWhitelistSize, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -320,7 +325,7 @@ public enum BridgeMethods { new String[]{"int"} ), fixedCost(2000L), - (BridgeMethodExecutorTyped) Bridge::getMinimumLockTxValue, + (BridgeMethodExecutorTyped) Bridge::getMinimumLockTxValue, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -331,7 +336,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getPendingFederationHashSerialized, + (BridgeMethodExecutorTyped) Bridge::getPendingFederationHashSerialized, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -342,7 +347,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getPendingFederationSize, + (BridgeMethodExecutorTyped) Bridge::getPendingFederationSize, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -353,7 +358,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKey, + (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKey, activations -> !activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -365,7 +370,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKeyOfType, + (BridgeMethodExecutorTyped) Bridge::getPendingFederatorPublicKeyOfType, activations -> activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -377,7 +382,7 @@ public enum BridgeMethods { new String[]{"string"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederationAddress, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederationAddress, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -388,7 +393,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationBlockNumber, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationBlockNumber, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -399,7 +404,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationTime, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederationCreationTime, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -410,7 +415,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederationSize, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederationSize, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -421,7 +426,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederationThreshold, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederationThreshold, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -432,7 +437,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKey, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKey, activations -> !activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -444,7 +449,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3000L), - (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKeyOfType, + (BridgeMethodExecutorTyped) Bridge::getRetiringFederatorPublicKeyOfType, activations -> activations.isActive(RSKIP123), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -456,7 +461,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(4000L), - (BridgeMethodExecutorTyped) Bridge::getStateForBtcReleaseClient, + (BridgeMethodExecutorTyped) Bridge::getStateForBtcReleaseClient, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -467,7 +472,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3_000_000L), - (BridgeMethodExecutorTyped) Bridge::getStateForDebugging, + (BridgeMethodExecutorTyped) Bridge::getStateForDebugging, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -478,7 +483,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(3_000L), - (BridgeMethodExecutorTyped) Bridge::getLockingCap, + (BridgeMethodExecutorTyped) Bridge::getLockingCap, activations -> activations.isActive(RSKIP134), fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL @@ -490,7 +495,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(30_000L), - (BridgeMethodExecutorTyped) Bridge::getActivePowpegRedeemScript, + (BridgeMethodExecutorTyped) Bridge::getActivePowpegRedeemScript, activations -> activations.isActive(RSKIP293), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -502,7 +507,7 @@ public enum BridgeMethods { new String[]{"uint256"} ), fixedCost(3_000L), - (BridgeMethodExecutorTyped) Bridge::getActiveFederationCreationBlockHeight, + (BridgeMethodExecutorTyped) Bridge::getActiveFederationCreationBlockHeight, activations -> activations.isActive(RSKIP186), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -514,7 +519,7 @@ public enum BridgeMethods { new String[]{"bool"} ), fixedCost(8_000L), - (BridgeMethodExecutorTyped) Bridge::increaseLockingCap, + (BridgeMethodExecutorTyped) Bridge::increaseLockingCap, activations -> activations.isActive(RSKIP134), fixedPermission(false) ), @@ -525,7 +530,7 @@ public enum BridgeMethods { new String[]{"bool"} ), fixedCost(23000L), - (BridgeMethodExecutorTyped) Bridge::isBtcTxHashAlreadyProcessed, + (BridgeMethodExecutorTyped) Bridge::isBtcTxHashAlreadyProcessed, fixedPermission(true), CallTypeHelper.ALLOW_STATIC_CALL ), @@ -550,7 +555,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(10_600L), - (BridgeMethodExecutorTyped) Bridge::receiveHeader, + (BridgeMethodExecutorTyped) Bridge::receiveHeader, activations -> activations.isActive(RSKIP200), fixedPermission(false) ), @@ -585,7 +590,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(24000L), - (BridgeMethodExecutorTyped) Bridge::removeLockWhitelistAddress, + (BridgeMethodExecutorTyped) Bridge::removeLockWhitelistAddress, fixedPermission(false) ), ROLLBACK_FEDERATION( @@ -595,7 +600,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(12000L), - (BridgeMethodExecutorTyped) Bridge::rollbackFederation, + (BridgeMethodExecutorTyped) Bridge::rollbackFederation, fixedPermission(false) ), SET_LOCK_WHITELIST_DISABLE_BLOCK_DELAY( @@ -605,7 +610,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(24000L), - (BridgeMethodExecutorTyped) Bridge::setLockWhitelistDisableBlockDelay, + (BridgeMethodExecutorTyped) Bridge::setLockWhitelistDisableBlockDelay, fixedPermission(false) ), UPDATE_COLLECTIONS( @@ -625,7 +630,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(10000L), - (BridgeMethodExecutorTyped) Bridge::voteFeePerKbChange, + (BridgeMethodExecutorTyped) Bridge::voteFeePerKbChange, fixedPermission(false) ), REGISTER_BTC_COINBASE_TRANSACTION( @@ -646,7 +651,7 @@ public enum BridgeMethods { new String[]{"bool"} ), fixedCost(5000L), - (BridgeMethodExecutorTyped) Bridge::hasBtcBlockCoinbaseTransactionInformation, + (BridgeMethodExecutorTyped) Bridge::hasBtcBlockCoinbaseTransactionInformation, activations -> activations.isActive(RSKIP143), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -658,7 +663,7 @@ public enum BridgeMethods { new String[]{"int256"} ), fixedCost(25_000L), - (BridgeMethodExecutorTyped) Bridge::registerFlyoverBtcTransaction, + (BridgeMethodExecutorTyped) Bridge::registerFlyoverBtcTransaction, activations -> activations.isActive(RSKIP176), fixedPermission(false) ), @@ -669,7 +674,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(3_800L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBestBlockHeader, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBestBlockHeader, activations -> activations.isActive(RSKIP220), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -681,7 +686,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(4_600L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHeaderByHash, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHeaderByHash, activations -> activations.isActive(RSKIP220), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -693,7 +698,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(5_000L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHeaderByHeight, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainBlockHeaderByHeight, activations -> activations.isActive(RSKIP220), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -705,7 +710,7 @@ public enum BridgeMethods { new String[]{"bytes"} ), fixedCost(4_900L), - (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainParentBlockHeaderByHash, + (BridgeMethodExecutorTyped) Bridge::getBtcBlockchainParentBlockHeaderByHash, activations -> activations.isActive(RSKIP220), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -717,7 +722,7 @@ public enum BridgeMethods { new String[]{"uint256"} ), fixedCost(3_000L), - (BridgeMethodExecutorTyped) Bridge::getNextPegoutCreationBlockNumber, + (BridgeMethodExecutorTyped) Bridge::getNextPegoutCreationBlockNumber, activations -> activations.isActive(RSKIP271), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -729,7 +734,7 @@ public enum BridgeMethods { new String[]{"uint256"} ), fixedCost(3_000L), - (BridgeMethodExecutorTyped) Bridge::getQueuedPegoutsCount, + (BridgeMethodExecutorTyped) Bridge::getQueuedPegoutsCount, activations -> activations.isActive(RSKIP271), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -741,7 +746,7 @@ public enum BridgeMethods { new String[]{"uint256"} ), fixedCost(10_000L), - (BridgeMethodExecutorTyped) Bridge::getEstimatedFeesForNextPegOutEvent, + (BridgeMethodExecutorTyped) Bridge::getEstimatedFeesForNextPegOutEvent, activations -> activations.isActive(RSKIP271), fixedPermission(false), CallTypeHelper.ALLOW_STATIC_CALL @@ -858,10 +863,33 @@ public interface BridgeCondition { boolean isTrue(Bridge bridge); } + /** + * Interface for executing methods in the Bridge context. + * + *

+ * This interface defines a single method, {@code execute}, which takes a + * {@link Bridge} instance and an array of arguments, returning an + * {@code Optional} result. Implementations of this interface should handle + * the execution logic and manage potential exceptions. + *

+ */ public interface BridgeMethodExecutor { Optional execute(Bridge self, Object[] args) throws Exception; } + /** + * A typed variant of {@link BridgeMethodExecutor} that allows for specific + * return types. + * + *

+ * This interface extends {@code BridgeMethodExecutor} and provides a default + * implementation of the {@code execute} method, delegating the call to a typed + * execution method {@code executeTyped}. Implementations must define this + * method to specify the expected return type. + *

+ * + * @param the return type of the executed method + */ private interface BridgeMethodExecutorTyped extends BridgeMethodExecutor { @Override default Optional execute(Bridge self, Object[] args) throws Exception { @@ -871,6 +899,16 @@ default Optional execute(Bridge self, Object[] args) throws Exception { T executeTyped(Bridge self, Object[] args) throws Exception; } + /** + * A variant of {@link BridgeMethodExecutor} for void methods. + * + *

+ * This interface overrides the {@code execute} method to perform an action + * without returning a result. Implementations should define the + * {@code executeVoid} method, which executes the intended action using the + * provided {@link Bridge} instance and arguments. + *

+ */ private interface BridgeMethodExecutorVoid extends BridgeMethodExecutor { @Override default Optional execute(Bridge self, Object[] args) throws Exception { diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java index 9db8b111270..342d13cdd43 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeSupport.java @@ -274,7 +274,7 @@ private boolean cannotProcessNextBlock(StoredBlock previousBlock) { * Get the wallet for the currently active federation * @return A BTC wallet for the currently active federation * - * @param shouldConsiderFlyoverUTXOs + * @param shouldConsiderFlyoverUTXOs Whether to consider flyover UTXOs */ public Wallet getActiveFederationWallet(boolean shouldConsiderFlyoverUTXOs) { Federation federation = getActiveFederation(); @@ -294,7 +294,7 @@ public Wallet getActiveFederationWallet(boolean shouldConsiderFlyoverUTXOs) { * or null if there's currently no retiring federation * @return A BTC wallet for the currently active federation * - * @param shouldConsiderFlyoverUTXOs + * @param shouldConsiderFlyoverUTXOs Whether to consider flyover UTXOs */ protected Wallet getRetiringFederationWallet(boolean shouldConsiderFlyoverUTXOs) { List retiringFederationBtcUTXOs = federationSupport.getRetiringFederationBtcUTXOs(); @@ -351,8 +351,8 @@ public Wallet getNoSpendWalletForLiveFederations(boolean isFlyoverCompatible) { * @param btcTxSerialized The raw BTC tx * @param height The height of the BTC block that contains the tx * @param pmtSerialized The raw partial Merkle tree - * @throws BlockStoreException - * @throws IOException + * @throws BlockStoreException If there's an error while executing validations + * @throws IOException If there's an error while processing the tx */ public void registerBtcTransaction( Transaction rskTx, @@ -804,11 +804,10 @@ private void saveNewUTXOs(BtcTransaction btcTx) { * The funds will be sent to the bitcoin address controlled by the private key that signed the rsk tx. * The amount sent to the bridge in this tx will be the amount sent in the btc network minus fees. * @param rskTx The rsk tx being executed. - * @throws IOException + * @throws IOException If there's an error while processing the release request. */ public void releaseBtc(Transaction rskTx) throws IOException { final co.rsk.core.Coin pegoutValueInWeis = rskTx.getValue(); - final Coin pegoutValueInSatoshis = pegoutValueInWeis.toBitcoin(); final RskAddress senderAddress = rskTx.getSender(signatureCache); logger.debug( "[releaseBtc] Releasing {} weis from RSK address {} in tx {}", @@ -824,7 +823,7 @@ public void releaseBtc(Transaction rskTx) throws IOException { senderAddress ); if (activations.isActive(ConsensusRule.RSKIP185)) { - emitRejectEvent(pegoutValueInSatoshis, senderAddress, RejectedPegoutReason.CALLER_CONTRACT); + emitRejectEvent(pegoutValueInWeis, senderAddress, RejectedPegoutReason.CALLER_CONTRACT); return; } else { String message = "Contract calling releaseBTC"; @@ -837,26 +836,37 @@ public void releaseBtc(Transaction rskTx) throws IOException { Address btcDestinationAddress = BridgeUtils.recoverBtcAddressFromEthTransaction(rskTx, networkParameters); logger.debug("[releaseBtc] BTC destination address: {}", btcDestinationAddress); - requestRelease(btcDestinationAddress, pegoutValueInSatoshis, rskTx); + requestRelease(btcDestinationAddress, pegoutValueInWeis, rskTx); } - private void refundAndEmitRejectEvent(Coin value, RskAddress senderAddress, RejectedPegoutReason reason) { + private void refundAndEmitRejectEvent( + co.rsk.core.Coin releaseRequestedValueInWeis, + RskAddress senderAddress, + RejectedPegoutReason reason + ) { logger.trace( - "[refundAndEmitRejectEvent] Executing a refund of {} to {}. Reason: {}", - value, + "[refundAndEmitRejectEvent] Executing a refund of {} weis to {}. Reason: {}", + releaseRequestedValueInWeis, senderAddress, reason ); + + // Prior to RSKIP427, the value was converted to BTC before doing the refund + // This could cause the original value to be rounded down to fit in satoshis value + co.rsk.core.Coin refundValue = activations.isActive(RSKIP427) ? + releaseRequestedValueInWeis : + co.rsk.core.Coin.fromBitcoin(releaseRequestedValueInWeis.toBitcoin()); + rskRepository.transfer( PrecompiledContracts.BRIDGE_ADDR, senderAddress, - co.rsk.core.Coin.fromBitcoin(value) + refundValue ); - emitRejectEvent(value, senderAddress, reason); + emitRejectEvent(releaseRequestedValueInWeis, senderAddress, reason); } - private void emitRejectEvent(Coin value, RskAddress senderAddress, RejectedPegoutReason reason) { - eventLogger.logReleaseBtcRequestRejected(senderAddress.toHexString(), value, reason); + private void emitRejectEvent(co.rsk.core.Coin releaseRequestedValueInWeis, RskAddress senderAddress, RejectedPegoutReason reason) { + eventLogger.logReleaseBtcRequestRejected(senderAddress, releaseRequestedValueInWeis, reason); } /** @@ -865,10 +875,11 @@ private void emitRejectEvent(Coin value, RskAddress senderAddress, RejectedPegou * to be processed later. * * @param destinationAddress the destination BTC address. - * @param value the amount of BTC to release. - * @throws IOException + * @param releaseRequestedValueInWeis the amount of RBTC requested to be released, represented in weis + * @throws IOException if there is an error getting the release request queue from storage */ - private void requestRelease(Address destinationAddress, Coin value, Transaction rskTx) throws IOException { + private void requestRelease(Address destinationAddress, co.rsk.core.Coin releaseRequestedValueInWeis, Transaction rskTx) throws IOException { + Coin valueToReleaseInSatoshis = releaseRequestedValueInWeis.toBitcoin(); Optional optionalRejectedPegoutReason = Optional.empty(); if (activations.isActive(RSKIP219)) { int pegoutSize = getRegularPegoutTxSize(activations, getActiveFederation()); @@ -886,11 +897,11 @@ private void requestRelease(Address destinationAddress, Coin value, Transaction .divide(100) ); // add the gap - // The pegout value should be greater or equals than the max of these two values + // The pegout releaseRequestedValueInWeis should be greater or equals than the max of these two values Coin minValue = Coin.valueOf(Math.max(bridgeConstants.getMinimumPegoutTxValue().value, requireFundsForFee.value)); // Since Iris the peg-out the rule is that the minimum is inclusive - if (value.isLessThan(minValue)) { + if (valueToReleaseInSatoshis.isLessThan(minValue)) { optionalRejectedPegoutReason = Optional.of( Objects.equals(minValue, requireFundsForFee) ? RejectedPegoutReason.FEE_ABOVE_VALUE: @@ -899,37 +910,47 @@ private void requestRelease(Address destinationAddress, Coin value, Transaction } } else { // For legacy peg-outs the rule stated that the minimum was exclusive - if (!value.isGreaterThan(bridgeConstants.getLegacyMinimumPegoutTxValue())) { + if (!valueToReleaseInSatoshis.isGreaterThan(bridgeConstants.getLegacyMinimumPegoutTxValue())) { optionalRejectedPegoutReason = Optional.of(RejectedPegoutReason.LOW_AMOUNT); } } if (optionalRejectedPegoutReason.isPresent()) { logger.warn( - "[requestRelease] releaseBtc ignored. To {}. Tx {}. Value {}. Reason: {}", + "[requestRelease] releaseBtc ignored. To {}. Tx {}. Value {} weis. Reason: {}", destinationAddress, rskTx, - value, + releaseRequestedValueInWeis, optionalRejectedPegoutReason.get() ); if (activations.isActive(ConsensusRule.RSKIP185)) { refundAndEmitRejectEvent( - value, + releaseRequestedValueInWeis, rskTx.getSender(signatureCache), optionalRejectedPegoutReason.get() ); } } else { if (activations.isActive(ConsensusRule.RSKIP146)) { - provider.getReleaseRequestQueue().add(destinationAddress, value, rskTx.getHash()); + provider.getReleaseRequestQueue().add(destinationAddress, valueToReleaseInSatoshis, rskTx.getHash()); } else { - provider.getReleaseRequestQueue().add(destinationAddress, value); + provider.getReleaseRequestQueue().add(destinationAddress, valueToReleaseInSatoshis); } + RskAddress sender = rskTx.getSender(signatureCache); if (activations.isActive(ConsensusRule.RSKIP185)) { - eventLogger.logReleaseBtcRequestReceived(rskTx.getSender(signatureCache).toHexString(), destinationAddress, value); + eventLogger.logReleaseBtcRequestReceived( + sender, + destinationAddress, + releaseRequestedValueInWeis + ); } - logger.info("[requestRelease] releaseBtc successful to {}. Tx {}. Value {}.", destinationAddress, rskTx, value); + logger.info( + "[requestRelease] releaseBtc successful to {}. Tx {}. Value {} weis.", + destinationAddress, + rskTx, + releaseRequestedValueInWeis + ); } } @@ -2431,17 +2452,18 @@ protected FlyoverFederationInformation createFlyoverFederationInformation(Keccak } protected FlyoverFederationInformation createFlyoverFederationInformation(Keccak256 flyoverDerivationHash, Federation federation) { - Script flyoverScript = FastBridgeRedeemScriptParser.createMultiSigFastBridgeRedeemScript( - federation.getRedeemScript(), - Sha256Hash.wrap(flyoverDerivationHash.getBytes()) + Script federationRedeemScript = federation.getRedeemScript(); + Script flyoverRedeemScript = FlyoverRedeemScriptBuilderImpl.builder().of( + flyoverDerivationHash, + federationRedeemScript ); - Script flyoverScriptHash = ScriptBuilder.createP2SHOutputScript(flyoverScript); + Script flyoverP2shOutputScript = ScriptBuilder.createP2SHOutputScript(flyoverRedeemScript); return new FlyoverFederationInformation( flyoverDerivationHash, federation.getP2SHScript().getPubKeyHash(), - flyoverScriptHash.getPubKeyHash() + flyoverP2shOutputScript.getPubKeyHash() ); } diff --git a/rskj-core/src/main/java/co/rsk/peg/BridgeUtils.java b/rskj-core/src/main/java/co/rsk/peg/BridgeUtils.java index 6e854f8c88a..5487d8afb82 100644 --- a/rskj-core/src/main/java/co/rsk/peg/BridgeUtils.java +++ b/rskj-core/src/main/java/co/rsk/peg/BridgeUtils.java @@ -19,8 +19,6 @@ import co.rsk.bitcoinj.core.*; import co.rsk.bitcoinj.crypto.TransactionSignature; -import co.rsk.bitcoinj.script.RedeemScriptParser; -import co.rsk.bitcoinj.script.RedeemScriptParser.MultiSigType; import co.rsk.bitcoinj.script.RedeemScriptParserFactory; import co.rsk.bitcoinj.script.Script; import co.rsk.bitcoinj.script.ScriptChunk; @@ -319,20 +317,14 @@ public static int countMissingSignatures(Context btcContext, BtcTransaction btcT TransactionInput input = btcTx.getInput(0); Script scriptSig = input.getScriptSig(); List chunks = scriptSig.getChunks(); - Script redeemScript = new Script(chunks.get(chunks.size() - 1).data); - RedeemScriptParser parser = RedeemScriptParserFactory.get(redeemScript.getChunks()); - MultiSigType multiSigType; int lastChunk; + Script redeemScript = new Script(chunks.get(chunks.size() - 1).data); - multiSigType = parser.getMultiSigType(); - - if (multiSigType == MultiSigType.STANDARD_MULTISIG || - multiSigType == MultiSigType.FAST_BRIDGE_MULTISIG - ) { - lastChunk = chunks.size() - 1; - } else { + if (isErpType(redeemScript)) { lastChunk = chunks.size() - 2; + } else { + lastChunk = chunks.size() - 1; } for (int i = 1; i < lastChunk; i++) { @@ -344,6 +336,11 @@ public static int countMissingSignatures(Context btcContext, BtcTransaction btcT return unsigned; } + private static boolean isErpType(Script redeemScript) { + List redeemScriptChunks = redeemScript.getChunks(); + return RedeemScriptParserFactory.get(redeemScriptChunks).hasErpFormat(); + } + /** * Checks whether a btc tx has been signed by the required number of federators. * @param btcContext Btc context @@ -357,23 +354,18 @@ public static boolean hasEnoughSignatures(Context btcContext, BtcTransaction btc Script scriptSig; List chunks; Script redeemScript; - RedeemScriptParser parser; - MultiSigType multiSigType; int lastChunk; for (TransactionInput input : btcTx.getInputs()) { scriptSig = input.getScriptSig(); chunks = scriptSig.getChunks(); redeemScript = new Script(chunks.get(chunks.size() - 1).data); - parser = RedeemScriptParserFactory.get(redeemScript.getChunks()); - multiSigType = parser.getMultiSigType(); - if (multiSigType == MultiSigType.STANDARD_MULTISIG || - multiSigType == MultiSigType.FAST_BRIDGE_MULTISIG + if (isErpType(redeemScript) ) { - lastChunk = chunks.size() - 1; - } else { lastChunk = chunks.size() - 2; + } else { + lastChunk = chunks.size() - 1; } for (int i = 1; i < lastChunk; i++) { diff --git a/rskj-core/src/main/java/co/rsk/peg/FlyoverCompatibleBtcWallet.java b/rskj-core/src/main/java/co/rsk/peg/FlyoverCompatibleBtcWallet.java index 6cf1e261013..82f1b506ffc 100644 --- a/rskj-core/src/main/java/co/rsk/peg/FlyoverCompatibleBtcWallet.java +++ b/rskj-core/src/main/java/co/rsk/peg/FlyoverCompatibleBtcWallet.java @@ -1,14 +1,9 @@ package co.rsk.peg; import co.rsk.bitcoinj.core.Context; -import co.rsk.bitcoinj.core.Sha256Hash; -import co.rsk.bitcoinj.script.FastBridgeErpRedeemScriptParser; -import co.rsk.bitcoinj.script.FastBridgeRedeemScriptParser; -import co.rsk.bitcoinj.script.RedeemScriptParser; -import co.rsk.bitcoinj.script.RedeemScriptParser.MultiSigType; -import co.rsk.bitcoinj.script.RedeemScriptParserFactory; import co.rsk.bitcoinj.script.Script; import co.rsk.bitcoinj.wallet.RedeemData; +import co.rsk.peg.bitcoin.FlyoverRedeemScriptBuilderImpl; import co.rsk.peg.federation.Federation; import co.rsk.peg.flyover.FlyoverFederationInformation; import java.util.List; @@ -44,26 +39,10 @@ public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) { Federation destinationFederationInstance = destinationFederation.get(); Script fedRedeemScript = destinationFederationInstance.getRedeemScript(); - RedeemScriptParser parser = RedeemScriptParserFactory.get(fedRedeemScript.getChunks()); - Script flyoverRedeemScript; - - if (parser.getMultiSigType() == MultiSigType.ERP_FED) { - flyoverRedeemScript = FastBridgeErpRedeemScriptParser.createFastBridgeErpRedeemScript( - fedRedeemScript, - Sha256Hash.wrap(flyoverFederationInformationInstance - .getDerivationHash() - .getBytes() - ) - ); - } else { - flyoverRedeemScript = FastBridgeRedeemScriptParser.createMultiSigFastBridgeRedeemScript( - fedRedeemScript, - Sha256Hash.wrap(flyoverFederationInformationInstance - .getDerivationHash() - .getBytes() - ) - ); - } + Script flyoverRedeemScript = FlyoverRedeemScriptBuilderImpl.builder().of( + flyoverFederationInformationInstance.getDerivationHash(), + fedRedeemScript + ); return RedeemData.of(destinationFederationInstance.getBtcPublicKeys(), flyoverRedeemScript); } diff --git a/rskj-core/src/main/java/co/rsk/peg/PegUtilsLegacy.java b/rskj-core/src/main/java/co/rsk/peg/PegUtilsLegacy.java index f57e070ae87..f0d2e4ec691 100644 --- a/rskj-core/src/main/java/co/rsk/peg/PegUtilsLegacy.java +++ b/rskj-core/src/main/java/co/rsk/peg/PegUtilsLegacy.java @@ -73,17 +73,17 @@ protected static boolean isPegOutTx(BtcTransaction btcTx, ActivationConfig.ForBl int inputsSize = btcTx.getInputs().size(); for (int i = 0; i < inputsSize; i++) { TransactionInput txInput = btcTx.getInput(i); - Optional