diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e17dfbdbc0c0..bbdda457c70d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,7 +15,8 @@ ##### HAPI protobuf ##### ######################### -/hapi/ @hashgraph/hedera-base @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core @hashgraph/platform-hashgraph @hashgraph/platform-data @hashgraph/platform-base @hashgraph/platform-architects +/hapi/ @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core @hashgraph/platform-hashgraph @hashgraph/platform-data @hashgraph/platform-base @hashgraph/platform-architects +/hapi/hedera-protobufs/services @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core @jsync-swirlds ######################### @@ -23,31 +24,31 @@ ######################### # Hedera Node Root Protections -/hedera-node/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/README.md @hashgraph/hedera-base @hashgraph/hedera-services @hashgraph/devops-ci @hashgraph/release-engineering-managers +/hedera-node/ @hashgraph/hedera-services +/hedera-node/README.md @hashgraph/hedera-services @hashgraph/devops-ci @hashgraph/release-engineering-managers # Hedera Node Deployments - Configuration & Grafana Dashboards /hedera-node/configuration/** @rbair23 @dalvizu @poulok @netopyr @Nana-EC @SimiHunjan @steven-sheehy @nathanklick -/hedera-node/configuration/dev/** @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/infrastructure/** @hashgraph/release-engineering-managers @hashgraph/devops-ci @hashgraph/devops @hashgraph/hedera-base @hashgraph/hedera-services +/hedera-node/configuration/dev/** @hashgraph/hedera-services +/hedera-node/infrastructure/** @hashgraph/release-engineering-managers @hashgraph/devops-ci @hashgraph/devops @hashgraph/hedera-services # Hedera Node Docker Definitions -/hedera-node/docker/ @hashgraph/hedera-base @hashgraph/hedera-services @hashgraph/devops-ci @hashgraph/release-engineering @hashgraph/release-engineering-managers +/hedera-node/docker/ @hashgraph/hedera-services @hashgraph/devops-ci @hashgraph/release-engineering @hashgraph/release-engineering-managers # Hedera Node Modules -/hedera-node/hapi*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-admin*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-app*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-consensus*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-file*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-network*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-schedule*/ @hashgraph/hedera-base @hashgraph/hedera-services +/hedera-node/hapi*/ @hashgraph/hedera-services +/hedera-node/hedera-admin*/ @hashgraph/hedera-services +/hedera-node/hedera-app*/ @hashgraph/hedera-services +/hedera-node/hedera-consensus*/ @hashgraph/hedera-services +/hedera-node/hedera-file*/ @hashgraph/hedera-services +/hedera-node/hedera-network*/ @hashgraph/hedera-services +/hedera-node/hedera-schedule*/ @hashgraph/hedera-services /hedera-node/hedera-smart-contract*/ @hashgraph/hedera-smart-contracts-core @tinker-michaelj -/hedera-node/hedera-token*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-util*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/hedera-staking*/ @hashgraph/hedera-base @hashgraph/hedera-services -/hedera-node/test-clients/ @hashgraph/hedera-base @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core -/hedera-node/**/module-info.java @hashgraph/hedera-base @hashgraph/hedera-services @hashgraph/devops-ci +/hedera-node/hedera-token*/ @hashgraph/hedera-services +/hedera-node/hedera-util*/ @hashgraph/hedera-services +/hedera-node/hedera-staking*/ @hashgraph/hedera-services +/hedera-node/test-clients/ @hashgraph/hedera-services @hashgraph/hedera-smart-contracts-core +/hedera-node/**/module-info.java @hashgraph/hedera-services @hashgraph/devops-ci ############################### ##### Hedera Cryptography ##### diff --git a/.github/workflows/flow-node-performance-tests.yaml b/.github/workflows/flow-node-performance-tests.yaml index f21f2c77ee88..3d630eed6b2f 100644 --- a/.github/workflows/flow-node-performance-tests.yaml +++ b/.github/workflows/flow-node-performance-tests.yaml @@ -53,7 +53,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Authenticate to Google Cloud uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 diff --git a/.github/workflows/node-flow-deploy-release-artifact.yaml b/.github/workflows/node-flow-deploy-release-artifact.yaml index f92c74d21ae1..3e565adf4844 100644 --- a/.github/workflows/node-flow-deploy-release-artifact.yaml +++ b/.github/workflows/node-flow-deploy-release-artifact.yaml @@ -95,7 +95,6 @@ jobs: sdk-gpg-key-contents: ${{ secrets.PLATFORM_GPG_KEY_CONTENTS }} sdk-gpg-key-passphrase: ${{ secrets.PLATFORM_GPG_KEY_PASSPHRASE }} slack-webhook-url: ${{ secrets.PLATFORM_SLACK_RELEASE_WEBHOOK }} - jenkins-integration-url: ${{ secrets.RELEASE_JENKINS_INTEGRATION_URL }} jf-url: ${{ vars.JF_URL }} jf-docker-registry: ${{ vars.JF_DOCKER_REGISTRY }} jf-user-name: ${{ vars.JF_USER_NAME }} @@ -122,7 +121,6 @@ jobs: sdk-gpg-key-contents: ${{ secrets.PLATFORM_GPG_KEY_CONTENTS }} sdk-gpg-key-passphrase: ${{ secrets.PLATFORM_GPG_KEY_PASSPHRASE }} slack-webhook-url: ${{ secrets.PLATFORM_SLACK_RELEASE_WEBHOOK }} - jenkins-integration-url: ${{ secrets.RELEASE_JENKINS_INTEGRATION_URL }} jf-url: ${{ vars.JF_URL }} jf-docker-registry: ${{ vars.JF_DOCKER_REGISTRY }} jf-user-name: ${{ vars.JF_USER_NAME }} diff --git a/.github/workflows/node-zxc-build-release-artifact.yaml b/.github/workflows/node-zxc-build-release-artifact.yaml index d131535c5171..e69b762a916e 100644 --- a/.github/workflows/node-zxc-build-release-artifact.yaml +++ b/.github/workflows/node-zxc-build-release-artifact.yaml @@ -99,8 +99,6 @@ on: required: true jf-access-token: required: true - jenkins-integration-url: - required: false defaults: run: @@ -177,7 +175,7 @@ jobs: fi - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Setup Java uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 @@ -277,7 +275,7 @@ jobs: echo "::endgroup::" - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Authenticate to Google Cloud uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2 @@ -389,40 +387,6 @@ jobs: destination: ${{ secrets.cdn-bucket-name }}/node/software/v${{ needs.validate.outputs.version-prefix }}/ parent: false - - name: Notify Jenkins of Release (Integration) - id: jenkins-integration - uses: fjogeleit/http-request-action@0bd00a33db6f82063a3c6befd41f232f61d66583 # v1.15.2 - if: ${{ inputs.dry-run-enabled != true && inputs.trigger-env-deploy == 'integration' && !cancelled() && !failure() }} - with: - url: ${{ secrets.jenkins-integration-url }} - data: ${{ toJSON(github.event) }} - - - name: Display Jenkins Payload - env: - JSON_RESPONSE: ${{ steps.jenkins-integration.outputs.response || steps.jenkins-preview.outputs.response }} - if: ${{ inputs.trigger-env-deploy == 'integration' }} - run: | - jq '.' <<<"${JSON_RESPONSE}" - printf "### Jenkins Response Payload\n\`\`\`json\n%s\n\`\`\`\n" "$(jq '.' <<<"${JSON_RESPONSE}")" >>"${GITHUB_STEP_SUMMARY}" - - - name: Check for Jenkins Failures (Integration) - if: ${{ inputs.trigger-env-deploy == 'integration' }} - env: - JSON_RESPONSE: ${{ steps.jenkins-integration.outputs.response }} - run: | - INTEGRATION_TRIGGERED="$(jq '.jobs."build-upgrade-integration".triggered' <<<"${JSON_RESPONSE}")" - DOCKER_TRIGGERED="$(jq '.jobs."build-upgrade-integration-docker".triggered' <<<"${JSON_RESPONSE}")" - - if [[ "${INTEGRATION_TRIGGERED}" != true ]]; then - echo "::error title=Jenkins Trigger Failure::Failed to trigger the 'build-upgrade-integration' job via the Jenkins 'integration' pipeline!" - exit 1 - fi - - if [[ "${DOCKER_TRIGGERED}" != true ]]; then - echo "::error title=Jenkins Trigger Failure::Failed to trigger the 'build-upgrade-integration-docker' job via the Jenkins 'integration' pipeline!" - exit 1 - fi - local-node-images: name: Publish Local Node Images runs-on: network-node-linux-large @@ -437,7 +401,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Authenticate to Google Cloud id: google-auth @@ -612,7 +576,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Authenticate to Google Cloud id: google-auth @@ -706,7 +670,7 @@ jobs: fi - name: Upload Manifests - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ (steps.gcp.outcome == 'success' || steps.jfrog.outcome == 'success') && !cancelled() && always() }} with: name: Production Image Manifests @@ -725,7 +689,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Install GnuPG Tools if: ${{ inputs.dry-run-enabled != true }} @@ -871,7 +835,7 @@ jobs: NEXUS_PASSWORD: ${{ secrets.sdk-ossrh-password }} with: gradle-version: ${{ inputs.gradle-version }} - arguments: "release${{ inputs.release-profile }} -PpublishingPackageGroup=com.swirlds --scan -PpublishSigningEnabled=true --no-configuration-cache" + arguments: "release${{ inputs.release-profile }} -PpublishingPackageGroup=com.swirlds -Ps01SonatypeHost=true -PpublishSigningEnabled=true --scan --no-configuration-cache" - name: Gradle Publish Services to ${{ inputs.version-policy == 'specified' && 'Maven Central' || 'Google Artifact Registry' }} (${{ inputs.release-profile }}) uses: gradle/gradle-build-action@29c0906b64b8fc82467890bfb7a0a7ef34bda89e # v3.1.0 @@ -881,7 +845,7 @@ jobs: NEXUS_PASSWORD: ${{ secrets.svcs-ossrh-password }} with: gradle-version: ${{ inputs.gradle-version }} - arguments: "release${{ inputs.release-profile }} -PpublishingPackageGroup=com.hedera --scan -PpublishSigningEnabled=true --no-configuration-cache" + arguments: "release${{ inputs.release-profile }} -PpublishingPackageGroup=com.hedera.hashgraph -PpublishSigningEnabled=true --scan --no-configuration-cache" - name: Upload SDK Release Archives if: ${{ inputs.dry-run-enabled != true && inputs.version-policy == 'specified' && !cancelled() && !failure() }} diff --git a/.github/workflows/node-zxc-compile-application-code.yaml b/.github/workflows/node-zxc-compile-application-code.yaml index f1ab37565dd9..ab81a1ade5d7 100644 --- a/.github/workflows/node-zxc-compile-application-code.yaml +++ b/.github/workflows/node-zxc-compile-application-code.yaml @@ -167,7 +167,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref || '' }} @@ -206,7 +206,7 @@ jobs: - name: Unit Testing id: gradle-test if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} - run: ${GRADLE_EXEC} :reports:testCodeCoverageReport -x :test-clients:test --continue --scan + run: ${GRADLE_EXEC} :aggregation:testCodeCoverageReport -x :test-clients:test --continue --scan - name: Publish Unit Test Report @@ -219,7 +219,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report @@ -241,7 +241,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test (Timing Sensitive) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-timing-sensitive-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report (Timing Sensitive) @@ -263,7 +263,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Unit Test (Time Consuming) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-time-consuming-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Unit Test Report (Time Consuming) @@ -285,7 +285,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload Hammer Test Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hammer-tests && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: Hammer Test Report @@ -311,7 +311,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Misc) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-misc && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Misc) Reports @@ -319,7 +319,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Misc) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-misc && inputs.enable-network-log-capture && steps.gradle-hapi-misc.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Misc) Network Logs @@ -345,7 +345,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Crypto) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-crypto && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Crypto) Report @@ -353,7 +353,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (crypto) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-crypto && inputs.enable-network-log-capture && steps.gradle-hapi-crypto.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Crypto) Network Logs @@ -379,7 +379,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Token) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-token && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Token) Report @@ -387,7 +387,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Token) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-token && inputs.enable-network-log-capture && steps.gradle-hapi-token.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Token) Network Logs @@ -413,7 +413,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Smart Contract) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-smart-contract && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Smart Contract) Report @@ -421,7 +421,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Smart Contract) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-smart-contract && inputs.enable-network-log-capture && steps.gradle-hapi-smart-contract.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Smart Contract) Network Logs @@ -447,7 +447,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Time Consuming) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-time-consuming && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Time Consuming) Report @@ -455,7 +455,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Time Consuming) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-time-consuming && inputs.enable-network-log-capture && steps.gradle-hapi-time-consuming.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Time Consuming) Network Logs @@ -481,7 +481,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Restart) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-restart && steps.gradle-build.conclusion == 'success' && !cancelled() }} with: name: HAPI Test (Restart) Report @@ -489,7 +489,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Restart) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-restart && inputs.enable-network-log-capture && steps.gradle-hapi-restart.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Restart) Network Logs @@ -516,7 +516,7 @@ jobs: comment_mode: errors # only comment if we could not find or parse the JUnit XML files - name: Upload HAPI Test (Node Death Reconnect) Report Artifacts - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-nd-reconnect && steps.gradle-build.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Node Death Reconnect) Report @@ -524,7 +524,7 @@ jobs: retention-days: 7 - name: Upload HAPI Test (Node Death Reconnect) Network Logs - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-hapi-tests-nd-reconnect && inputs.enable-network-log-capture && steps.gradle-hapi-nd-reconnect.conclusion == 'failure' && !cancelled() }} with: name: HAPI Test (Node Death Reconnect) Network Logs @@ -538,16 +538,16 @@ jobs: uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.codecov-token }} - files: gradle/reports/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml + files: gradle/aggregation/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml - name: Publish to Codacy env: CODACY_PROJECT_TOKEN: ${{ secrets.codacy-project-token }} if: ${{ inputs.enable-unit-tests && !cancelled() }} - run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Java -r gradle/reports/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml + run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Java -r gradle/aggregation/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml - name: Upload Test Reports - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ inputs.enable-unit-tests && !cancelled() }} with: name: Test Reports diff --git a/.github/workflows/node-zxcron-release-branching.yaml b/.github/workflows/node-zxcron-release-branching.yaml index bf61a668ee19..a8b9f59cb53f 100644 --- a/.github/workflows/node-zxcron-release-branching.yaml +++ b/.github/workflows/node-zxcron-release-branching.yaml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Read Trigger Time id: time @@ -101,7 +101,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Branch Creation Check id: branch-creation @@ -124,7 +124,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 token: ${{ secrets.GH_ACCESS_TOKEN }} @@ -219,7 +219,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ needs.check-branch.outputs.branch-name }} fetch-depth: 0 diff --git a/.github/workflows/node-zxcron-release-fsts-regression.yaml b/.github/workflows/node-zxcron-release-fsts-regression.yaml index 97d06977a43a..b8c49ce71771 100644 --- a/.github/workflows/node-zxcron-release-fsts-regression.yaml +++ b/.github/workflows/node-zxcron-release-fsts-regression.yaml @@ -25,6 +25,9 @@ defaults: env: BRANCH_LIST_FILE: "${{ github.workspace }}/branches.lst" +permissions: + contents: read + jobs: cron: name: Cron / Launch Workflows @@ -36,7 +39,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 diff --git a/.github/workflows/node-zxf-deploy-integration.yaml b/.github/workflows/node-zxf-deploy-integration.yaml new file mode 100644 index 000000000000..3f73a85132a0 --- /dev/null +++ b/.github/workflows/node-zxf-deploy-integration.yaml @@ -0,0 +1,73 @@ +## +# Copyright (C) 2022-2024 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +name: "ZXF: [Node] Deploy Integration Network Release" +on: + workflow_dispatch: + + workflow_run: + workflows: + - "ZXC: [Node] Deploy Release Artifacts" + types: + - completed + branches: + - develop + +permissions: + contents: read + +jobs: + jenkins-checks: + name: Build Artifact + runs-on: network-node-linux-medium + if: ${{ false }} + + steps: + - name: Harden Runner + uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0 + with: + egress-policy: audit + + - name: Notify Jenkins of Release (Integration) + id: jenkins-integration + uses: fjogeleit/http-request-action@0bd00a33db6f82063a3c6befd41f232f61d66583 # v1.15.2 + with: + url: ${{ secrets.RELEASE_JENKINS_INTEGRATION_URL }} + data: ${{ toJSON(github.event) }} + + - name: Display Jenkins Payload + env: + JSON_RESPONSE: ${{ steps.jenkins-integration.outputs.response }} + run: | + jq '.' <<<"${JSON_RESPONSE}" + printf "### Jenkins Response Payload\n\`\`\`json\n%s\n\`\`\`\n" "$(jq '.' <<<"${JSON_RESPONSE}")" >>"${GITHUB_STEP_SUMMARY}" + + - name: Check for Jenkins Failures (Integration) + env: + JSON_RESPONSE: ${{ steps.jenkins-integration.outputs.response }} + run: | + INTEGRATION_TRIGGERED="$(jq '.jobs."build-upgrade-integration".triggered' <<<"${JSON_RESPONSE}")" + DOCKER_TRIGGERED="$(jq '.jobs."build-upgrade-integration-docker".triggered' <<<"${JSON_RESPONSE}")" + + if [[ "${INTEGRATION_TRIGGERED}" != true ]]; then + echo "::error title=Jenkins Trigger Failure::Failed to trigger the 'build-upgrade-integration' job via the Jenkins 'integration' pipeline!" + exit 1 + fi + + if [[ "${DOCKER_TRIGGERED}" != true ]]; then + echo "::error title=Jenkins Trigger Failure::Failed to trigger the 'build-upgrade-integration-docker' job via the Jenkins 'integration' pipeline!" + exit 1 + fi diff --git a/.github/workflows/node-zxf-snyk-monitor.yaml b/.github/workflows/node-zxf-snyk-monitor.yaml index 4eca527429d2..af41861bc274 100644 --- a/.github/workflows/node-zxf-snyk-monitor.yaml +++ b/.github/workflows/node-zxf-snyk-monitor.yaml @@ -37,7 +37,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Setup Java uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 diff --git a/.github/workflows/platform-zxc-launch-jrs-workflow.yaml b/.github/workflows/platform-zxc-launch-jrs-workflow.yaml index 5e56e127ddb0..4d1a1ae3961b 100644 --- a/.github/workflows/platform-zxc-launch-jrs-workflow.yaml +++ b/.github/workflows/platform-zxc-launch-jrs-workflow.yaml @@ -43,6 +43,9 @@ on: description: "The Github access token used to checkout the repository, submodules, and make GitHub API calls." required: true +permissions: + contents: read + defaults: run: shell: bash diff --git a/.github/workflows/platform-zxcron-release-jrs-regression.yaml b/.github/workflows/platform-zxcron-release-jrs-regression.yaml index 1caf53a94bed..a4d11e470368 100644 --- a/.github/workflows/platform-zxcron-release-jrs-regression.yaml +++ b/.github/workflows/platform-zxcron-release-jrs-regression.yaml @@ -20,6 +20,9 @@ on: - cron: '0 9 * * *' workflow_dispatch: +permissions: + contents: read + defaults: run: shell: bash @@ -38,7 +41,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 diff --git a/.github/workflows/zxc-jrs-regression.yaml b/.github/workflows/zxc-jrs-regression.yaml index 5d14a27d5775..b5a5eb5777b6 100644 --- a/.github/workflows/zxc-jrs-regression.yaml +++ b/.github/workflows/zxc-jrs-regression.yaml @@ -192,7 +192,7 @@ jobs: egress-policy: audit - name: Checkout Platform Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref || inputs.branch-name || '' }} fetch-depth: 0 @@ -215,7 +215,7 @@ jobs: echo "branch-name=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}" - name: Checkout Regression Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: path: platform-sdk/regression repository: swirlds/swirlds-platform-regression diff --git a/.github/workflows/zxc-publish-production-image.yaml b/.github/workflows/zxc-publish-production-image.yaml index 836d0d61321a..8dca5090d4b3 100644 --- a/.github/workflows/zxc-publish-production-image.yaml +++ b/.github/workflows/zxc-publish-production-image.yaml @@ -92,7 +92,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Restore Build Artifacts uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 diff --git a/.github/workflows/zxc-verify-docker-build-determinism.yaml b/.github/workflows/zxc-verify-docker-build-determinism.yaml index 9ada99e1a7b5..8f46eb8ae6a1 100644 --- a/.github/workflows/zxc-verify-docker-build-determinism.yaml +++ b/.github/workflows/zxc-verify-docker-build-determinism.yaml @@ -79,7 +79,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref }} @@ -299,7 +299,7 @@ jobs: git config --global core.eol lf - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref }} @@ -495,7 +495,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Docker Manifests [${{ join(matrix.os, ', ') }}] diff --git a/.github/workflows/zxc-verify-gradle-build-determinism.yaml b/.github/workflows/zxc-verify-gradle-build-determinism.yaml index 0f7dcb9a973b..3c88169e9fc8 100644 --- a/.github/workflows/zxc-verify-gradle-build-determinism.yaml +++ b/.github/workflows/zxc-verify-gradle-build-determinism.yaml @@ -73,7 +73,7 @@ jobs: egress-policy: audit - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref }} @@ -165,7 +165,7 @@ jobs: git config --global core.eol lf - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.ref }} @@ -247,7 +247,7 @@ jobs: fi - name: Publish Manifests - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 if: ${{ steps.regen-manifest.conclusion == 'success' && failure() && !cancelled() }} with: name: Gradle Manifests [${{ join(matrix.os, ', ') }}] diff --git a/.github/workflows/zxcron-extended-test-suite.yaml b/.github/workflows/zxcron-extended-test-suite.yaml index e9fbf04ba538..619044caabb1 100644 --- a/.github/workflows/zxcron-extended-test-suite.yaml +++ b/.github/workflows/zxcron-extended-test-suite.yaml @@ -41,7 +41,7 @@ jobs: steps: # Checkout the latest from dev - name: Checkout Code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: '0' @@ -49,6 +49,7 @@ jobs: # the command git branch --contains xts_tag_commit | grep -q # will return an exit code of 1 if the tag commit is not found on the develop # branch. + # TODO: Should we delete the tag as part of this job? Or should it occur after XTS passes? - name: Check for tags id: check_tags_exist run: | @@ -140,7 +141,9 @@ jobs: - extended-test-suite - fetch-xts-candidate - hedera-node-jrs-panel - if: ${{ needs.extended-test-suite.result == 'success' }} + if: ${{ needs.abbreviated-panel.result == 'success' || + needs.extended-test-suite.result == 'success' || + needs.hedera-node-jrs-panel.result == 'success' }} steps: - name: Harden Runner uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0 @@ -150,7 +153,7 @@ jobs: - name: Checkout Tagged Code id: checkout_tagged_code if: ${{ needs.fetch-xts-candidate.outputs.xts_tag_exists == 'true' }} - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ needs.fetch-xts-candidate.outputs.xts_tag_commit }} # this becomes an input to the reusable flow diff --git a/.github/workflows/zxf-prepare-extended-test-suite.yaml b/.github/workflows/zxf-prepare-extended-test-suite.yaml index cb3be74659cc..42158896471a 100644 --- a/.github/workflows/zxf-prepare-extended-test-suite.yaml +++ b/.github/workflows/zxf-prepare-extended-test-suite.yaml @@ -38,7 +38,7 @@ jobs: tag-for-xts: name: Tag for XTS promotion runs-on: network-node-linux-medium - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.ref_type == 'branch' }} + if: ${{ github.event.workflow_run.conclusion == 'success' && !github.event.workflow_run.head_repository.fork && github.event.workflow_run.head_branch == 'develop'}} steps: - name: Harden Runner uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0 @@ -46,12 +46,27 @@ jobs: egress-policy: audit - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + fetch-depth: '0' + ref: ${{ github.event.workflow_run.head_sha }} + token: ${{ secrets.GH_ACCESS_TOKEN }} + persist-credentials: 'true' + + - name: Import GPG Key + id: gpg_importer + uses: step-security/ghaction-import-gpg@6c8fe4d0126a59d57c21f87c9ae5dd3451fa3cca # v6.1.0 + with: + git_commit_gpgsign: true + git_tag_gpgsign: true + git_user_signingkey: true + gpg_private_key: ${{ secrets.SVCS_GPG_KEY_CONTENTS }} + passphrase: ${{ secrets.SVCS_GPG_KEY_PASSPHRASE }} # move the tag if successful - name: Tag Code and push run: | - git tag --force --annotate ${XTS_CANDIDATE_TAG} + git tag --force --sign ${XTS_CANDIDATE_TAG} --message "Tagging commit for XTS promotion" git push --set-upstream origin --tags - name: Report failure diff --git a/developers.properties b/developers.properties new file mode 100644 index 000000000000..d029ffc679cd --- /dev/null +++ b/developers.properties @@ -0,0 +1,6 @@ +# This file is here for 'hapi', because that "product" is missing the toplevel folder +# (it contains the 'hapi' module directly) +hedera-base@hashgraph.com=Hedera Base Team +hedera-services@hashgraph.com=Hedera Services Team +hedera-smart-contracts@hashgraph.com=Hedera Smart Contracts Team +release-engineering@hashgraph.com=Release Engineering Team diff --git a/example-apps/swirlds-platform-base-example/src/main/java/com/swirlds/platform/base/example/ext/BaseContextFactory.java b/example-apps/swirlds-platform-base-example/src/main/java/com/swirlds/platform/base/example/ext/BaseContextFactory.java index 5f0847826c97..7d1764cda594 100644 --- a/example-apps/swirlds-platform-base-example/src/main/java/com/swirlds/platform/base/example/ext/BaseContextFactory.java +++ b/example-apps/swirlds-platform-base-example/src/main/java/com/swirlds/platform/base/example/ext/BaseContextFactory.java @@ -16,6 +16,8 @@ package com.swirlds.platform.base.example.ext; +import static com.swirlds.base.utility.FileSystemUtils.waitForPathPresence; + import com.swirlds.common.metrics.platform.DefaultMetricsProvider; import com.swirlds.common.platform.NodeId; import com.swirlds.config.api.Configuration; @@ -64,7 +66,7 @@ private static Configuration getConfiguration() { .withSource(new ClasspathFileConfigSource(Path.of(APPLICATION_PROPERTIES))) .autoDiscoverExtensions(); - if (EXTERNAL_PROPERTIES.toFile().exists()) { + if (waitForPathPresence(EXTERNAL_PROPERTIES)) { configurationBuilder.withSources(new PropertyFileConfigSource(EXTERNAL_PROPERTIES)); } diff --git a/gradle/aggregation/build.gradle.kts b/gradle/aggregation/build.gradle.kts new file mode 100644 index 000000000000..77c10f61781f --- /dev/null +++ b/gradle/aggregation/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { id("com.hedera.gradle.reports") } + +dependencies { + implementation(project(":app")) + implementation(project(":swirlds-platform-base-example")) + implementation(project(":AddressBookTestingTool")) + implementation(project(":ConsistencyTestingTool")) + implementation(project(":ISSTestingTool")) + implementation(project(":MigrationTestingTool")) + implementation(project(":PlatformTestingTool")) + implementation(project(":StatsSigningTestingTool")) + implementation(project(":StressTestingTool")) + implementation(project(":test-clients")) +} + +// As the standard 'src/test' folder of 'test-clients' does not contain unit tests, +// do not include it in the aggregated code coverage report. +configurations.aggregateCodeCoverageReportResults { exclude(module = "test-clients") } diff --git a/gradle/modules.properties b/gradle/modules.properties index 0ad3a96f484d..82f927ee2791 100644 --- a/gradle/modules.properties +++ b/gradle/modules.properties @@ -5,3 +5,4 @@ # Consider contributing the missing entry back to the plugin by creating a PR for the 'modules.properties' linked above. com.hedera.cryptography.pairings.api=com.hedera.cryptography:hedera-cryptography-pairings-api com.hedera.cryptography.pairings.signatures=com.hedera.cryptography:hedera-cryptography-pairings-signatures +jmh.core=org.openjdk.jmh:jmh-core diff --git a/gradle/plugins/build.gradle.kts b/gradle/plugins/build.gradle.kts index 516916b559d4..90529500d033 100644 --- a/gradle/plugins/build.gradle.kts +++ b/gradle/plugins/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { implementation("io.github.gradle-nexus:publish-plugin:1.3.0") implementation("me.champeau.jmh:jmh-gradle-plugin:0.7.2") implementation("net.swiftzer.semver:semver:1.3.0") - implementation("org.gradlex:extra-java-module-info:1.8") + implementation("org.gradlex:extra-java-module-info:1.9") implementation("org.gradlex:jvm-dependency-conflict-resolution:2.1.2") - implementation("org.gradlex:java-module-dependencies:1.7") + implementation("org.gradlex:java-module-dependencies:1.7.1") } diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.java.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.java.gradle.kts index baac0c50e528..5a4e2fd4cd2c 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.java.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.java.gradle.kts @@ -69,11 +69,37 @@ configurations.all { jvmDependencyConflicts { consistentResolution { - providesVersions(":app") + providesVersions(":aggregation") platform(":hedera-dependency-versions") } } +val consistentResolutionAttribute = Attribute.of("consistent-resolution", String::class.java) + +configurations.create("allDependencies") { + isCanBeConsumed = true + isCanBeResolved = false + sourceSets.all { + extendsFrom( + configurations[this.implementationConfigurationName], + configurations[this.compileOnlyConfigurationName], + configurations[this.runtimeOnlyConfigurationName], + configurations[this.annotationProcessorConfigurationName] + ) + } + attributes { + attribute(consistentResolutionAttribute, "global") + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + } +} + +configurations.getByName("mainRuntimeClasspath") { + attributes.attribute(consistentResolutionAttribute, "global") +} + tasks.buildDependents { setGroup(null) } tasks.buildNeeded { setGroup(null) } @@ -210,6 +236,9 @@ testing.suites { maxHeapSize = "4g" // Some tests overlap due to using the same temp folders within one project // maxParallelForks = 4 <- set this, once tests can run in parallel + + // Enable dynamic agent loading for tests - eg: Mockito, ByteBuddy + jvmArgs("-XX:+EnableDynamicAgentLoading") } } } diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.jpms-modules.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.jpms-modules.gradle.kts index c0f66286f971..f5c2ca21b350 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.jpms-modules.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.jpms-modules.gradle.kts @@ -92,6 +92,7 @@ jvmDependencyConflicts.patch { // Jar needs to have this file. If it is missing, it is added by what is configured here. extraJavaModuleInfo { failOnAutomaticModules = true // Only allow Jars with 'module-info' on all module paths + versionsProvidingConfiguration = "mainRuntimeClasspath" module("io.grpc:grpc-api", "io.grpc") module("io.grpc:grpc-core", "io.grpc.internal") @@ -106,15 +107,11 @@ extraJavaModuleInfo { requireAllDefinedDependencies() requires("java.logging") } - module("io.grpc:grpc-testing", "io.grpc.testing") - module("io.grpc:grpc-services", "io.grpc.services") module("io.grpc:grpc-util", "io.grpc.util") module("io.grpc:grpc-protobuf", "io.grpc.protobuf") module("io.grpc:grpc-protobuf-lite", "io.grpc.protobuf.lite") module("com.github.spotbugs:spotbugs-annotations", "com.github.spotbugs.annotations") module("com.google.code.findbugs:jsr305", "java.annotation") - module("com.google.errorprone:error_prone_annotations", "com.google.errorprone.annotations") - module("com.google.j2objc:j2objc-annotations", "com.google.j2objc.annotations") module("com.google.protobuf:protobuf-java", "com.google.protobuf") { exportAllPackages() requireAllDefinedDependencies() @@ -131,8 +128,6 @@ extraJavaModuleInfo { module("io.perfmark:perfmark-api", "io.perfmark") module("javax.inject:javax.inject", "javax.inject") module("commons-codec:commons-codec", "org.apache.commons.codec") - module("org.apache.commons:commons-math3", "org.apache.commons.math3") - module("org.apache.commons:commons-collections4", "org.apache.commons.collections4") module("com.esaulpaugh:headlong", "headlong") module("org.checkerframework:checker-qual", "org.checkerframework.checker.qual") module("org.connid:framework", "org.connid.framework") @@ -174,7 +169,6 @@ extraJavaModuleInfo { requireAllDefinedDependencies() requiresStatic("com.fasterxml.jackson.annotation") } - module("org.hyperledger.besu:plugin-api", "org.hyperledger.besu.plugin.api") module("org.hyperledger.besu:secp256k1", "org.hyperledger.besu.nativelib.secp256k1") module("org.hyperledger.besu:secp256r1", "org.hyperledger.besu.nativelib.secp256r1") module("com.goterl:resource-loader", "resource.loader") @@ -197,11 +191,11 @@ extraJavaModuleInfo { // Need to use Jar file names here as there is currently no other way to address Jar with // classifier directly for patching module( - "netty-transport-native-epoll-4.1.110.Final-linux-x86_64.jar", + "io.netty:netty-transport-native-epoll|linux-x86_64", "io.netty.transport.epoll.linux.x86_64" ) module( - "netty-transport-native-epoll-4.1.110.Final-linux-aarch_64.jar", + "io.netty:netty-transport-native-epoll|linux-aarch_64", "io.netty.transport.epoll.linux.aarch_64" ) @@ -227,15 +221,8 @@ extraJavaModuleInfo { module("uk.org.webcompere:system-stubs-core", "uk.org.webcompere.systemstubs.core") module("uk.org.webcompere:system-stubs-jupiter", "uk.org.webcompere.systemstubs.jupiter") - // JMH only - module("net.sf.jopt-simple:jopt-simple", "jopt.simple") - module("org.openjdk.jmh:jmh-core", "jmh.core") - module("org.openjdk.jmh:jmh-generator-asm", "jmh.generator.asm") - module("org.openjdk.jmh:jmh-generator-bytecode", "jmh.generator.bytecode") - module("org.openjdk.jmh:jmh-generator-reflection", "jmh.generator.reflection") - // Test clients only - module("com.github.docker-java:docker-java-api", "com.github.docker.java.api") + module("com.github.docker-java:docker-java-api", "com.github.dockerjava.api") module("com.github.docker-java:docker-java-transport", "com.github.docker.java.transport") module( "com.github.docker-java:docker-java-transport-zerodep", @@ -252,7 +239,6 @@ extraJavaModuleInfo { module("org.mockito:mockito-core", "org.mockito") module("org.objenesis:objenesis", "org.objenesis") module("org.rnorth.duct-tape:duct-tape", "org.rnorth.ducttape") - module("org.testcontainers:junit-jupiter", "org.testcontainers.junit.jupiter") module("org.testcontainers:testcontainers", "org.testcontainers") module("org.mockito:mockito-junit-jupiter", "org.mockito.junit.jupiter") } diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.maven-publish.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.maven-publish.gradle.kts index fbc9dd85100f..293350cfc431 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.maven-publish.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.maven-publish.gradle.kts @@ -14,12 +14,21 @@ * limitations under the License. */ +import java.util.Properties + plugins { id("java") id("maven-publish") id("signing") } +tasks.withType().configureEach { + // Publishing tasks are only enabled if we publish to the matching group. + // Otherwise, Nexus configuration and credentials do not fit. + val publishingPackageGroup = providers.gradleProperty("publishingPackageGroup").orNull + enabled = publishingPackageGroup == project.group +} + java { withJavadocJar() withSourcesJar() @@ -42,32 +51,69 @@ val maven = allVariants { fromResolutionResult() } } + suppressAllPomMetadataWarnings() + pom { - packaging = findProperty("maven.project.packaging")?.toString() ?: "jar" - name.set(project.name) - url.set("https://www.swirlds.com/") - inceptionYear.set("2016") + val devGroups = Properties() + val developerProperties = layout.projectDirectory.file("../developers.properties") + devGroups.load( + providers + .fileContents(developerProperties) + .asText + .orElse( + provider { + throw RuntimeException("${developerProperties.asFile} does not exist") + } + ) + .get() + .reader() + ) + + url = "https://www.hashgraph.com/" + inceptionYear = "2016" - description.set(provider(project::getDescription)) + description = + providers + .fileContents(layout.projectDirectory.file("../description.txt")) + .asText + .orElse(provider { project.description }) + .map { it.replace("\n", " ").trim() } + .orNull organization { - name.set("Hedera Hashgraph, LLC") - url.set("https://www.hedera.com") + name = "Hedera Hashgraph, LLC" + url = "https://www.hedera.com" + } + + val repoName = isolated.rootProject.name + + issueManagement { + system = "GitHub" + url = "https://github.com/hashgraph/$repoName/issues" } licenses { license { - name.set("Apache License, Version 2.0") - url.set( - "https://raw.githubusercontent.com/hashgraph/hedera-services/main/LICENSE" - ) + name = "Apache License, Version 2.0" + url = "https://raw.githubusercontent.com/hashgraph/$repoName/main/LICENSE" } } scm { - connection.set("scm:git:git://github.com/hashgraph/hedera-services.git") - developerConnection.set("scm:git:ssh://github.com:hashgraph/hedera-services.git") - url.set("https://github.com/hashgraph/hedera-services") + connection = "scm:git:git://github.com/hashgraph/$repoName.git" + developerConnection = "scm:git:ssh://github.com:hashgraph/$repoName.git" + url = "https://github.com/hashgraph/$repoName" + } + + developers { + devGroups.forEach { mail, team -> + developer { + name = team as String + email = mail as String + organization = "Hedera Hashgraph" + organizationUrl = "https://www.hedera.com" + } + } } } } diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.nexus-publish.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.nexus-publish.gradle.kts index 182be0a43f10..429566cb793a 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.nexus-publish.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.nexus-publish.gradle.kts @@ -19,16 +19,15 @@ plugins { id("io.github.gradle-nexus.publish-plugin") } -val publishingPackageGroup = providers.gradleProperty("publishingPackageGroup").getOrElse("") -val isPlatformPublish = publishingPackageGroup == "com.swirlds" - nexusPublishing { - packageGroup = publishingPackageGroup + val s01SonatypeHost = providers.gradleProperty("s01SonatypeHost").getOrElse("false").toBoolean() + packageGroup = providers.gradleProperty("publishingPackageGroup").getOrElse("") + repositories { sonatype { username = System.getenv("NEXUS_USERNAME") password = System.getenv("NEXUS_PASSWORD") - if (isPlatformPublish) { + if (s01SonatypeHost) { nexusUrl = uri("https://s01.oss.sonatype.org/service/local/") snapshotRepositoryUrl = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.platform-publish.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.platform-publish.gradle.kts index 79fd23a0ad23..e2983169da5f 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.platform-publish.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.platform-publish.gradle.kts @@ -31,50 +31,6 @@ if ( apply(plugin = "com.google.cloud.artifactregistry.gradle-plugin") } -// Publishing tasks are only enabled if we publish to the matching group. -// Otherwise, Nexus configuration and credentials do not fit. -val publishingPackageGroup = providers.gradleProperty("publishingPackageGroup").getOrElse("") - -tasks.withType().configureEach { - enabled = publishingPackageGroup == "com.swirlds" -} - -publishing.publications.named("maven") { - pom.description = - "Swirlds is a software platform designed to build fully-distributed " + - "applications that harness the power of the cloud without servers. " + - "Now you can develop applications with fairness in decision making, " + - "speed, trust and reliability, at a fraction of the cost of " + - "traditional server-based platforms." - - pom.developers { - developer { - name = "Platform Base Team" - email = "platform-base@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Platform Hashgraph Team" - email = "platform-hashgraph@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Platform Data Team" - email = "platform-data@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Release Engineering Team" - email = "release-engineering@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - } -} - publishing.repositories { maven("artifactregistry://us-maven.pkg.dev/swirlds-registry/maven-prerelease-channel") { name = "prereleaseChannel" diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.reports.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.reports.gradle.kts index 22a013a951d3..3cef369974a5 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.reports.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.reports.gradle.kts @@ -14,36 +14,14 @@ * limitations under the License. */ +import org.gradlex.javamodule.dependencies.tasks.ModuleDirectivesScopeCheck + plugins { - id("jvm-ecosystem") + id("com.hedera.gradle.java") id("jacoco-report-aggregation") - id("com.hedera.gradle.lifecycle") - id("com.hedera.gradle.repositories") - id("com.hedera.gradle.jpms-modules") } -dependencies { - rootProject.subprojects - // exclude the 'reports' project itself - .filter { prj -> prj != project } - // exclude 'test-clients' as it contains test sources in 'main' - // see also 'codecov.yml' - .filter { prj -> prj.name != "test-clients" } - .forEach { - if (it.name == "hedera-dependency-versions") { - jacocoAggregation(platform(project(it.path))) - } else { - jacocoAggregation(project(it.path)) - } - } -} +tasks.withType { enabled = false } -// Use Gradle's 'jacoco-report-aggregation' plugin to create an aggregated report independent of the -// platform (Codecov, Codacy, ...) that picks it up later on. -// See: -// https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage_standalone.html -reporting { - reports.create("testCodeCoverageReport") { - testType = TestSuiteType.UNIT_TEST - } -} +// Make aggregation "classpath" use the platform for versions (gradle/versions) +configurations.aggregateCodeCoverageReportResults { extendsFrom(configurations["internal"]) } diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.services-publish.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.services-publish.gradle.kts index 4bae581a0fd3..57ed149ed3de 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.services-publish.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.services-publish.gradle.kts @@ -18,44 +18,3 @@ plugins { id("java") id("com.hedera.gradle.maven-publish") } - -// Publishing tasks are only enabled if we publish to the matching group. -// Otherwise, Nexus configuration and credentials do not fit. -val publishingPackageGroup = providers.gradleProperty("publishingPackageGroup").getOrElse("") - -tasks.withType().configureEach { - enabled = publishingPackageGroup == "com.hedera" -} - -publishing { - publications { - named("maven") { - pom.developers { - developer { - name = "Hedera Base Team" - email = "hedera-base@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Hedera Services Team" - email = "hedera-services@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Hedera Smart Contracts Team" - email = "hedera-smart-contracts@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - developer { - name = "Release Engineering Team" - email = "release-engineering@swirldslabs.com" - organization = "Hedera Hashgraph" - organizationUrl = "https://www.hedera.com" - } - } - } - } -} diff --git a/gradle/plugins/src/main/kotlin/com.hedera.gradle.settings.settings.gradle.kts b/gradle/plugins/src/main/kotlin/com.hedera.gradle.settings.settings.gradle.kts index 295dbbe739e2..5da00ecb2321 100644 --- a/gradle/plugins/src/main/kotlin/com.hedera.gradle.settings.settings.gradle.kts +++ b/gradle/plugins/src/main/kotlin/com.hedera.gradle.settings.settings.gradle.kts @@ -73,7 +73,7 @@ includeBuild(".") configure { // Project to aggregate code coverage data for the whole repository into one report - module("gradle/reports") + module("gradle/aggregation") // "BOM" with versions of 3rd party dependencies versions("hedera-dependency-versions") diff --git a/gradle/reports/build.gradle.kts b/gradle/reports/build.gradle.kts deleted file mode 100644 index c4ea91df8da6..000000000000 --- a/gradle/reports/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("com.hedera.gradle.reports") -} diff --git a/hapi/hedera-protobufs/services/address_book_service.proto b/hapi/hedera-protobufs/services/address_book_service.proto index 021b5a48be70..bcabbd5c4858 100644 --- a/hapi/hedera-protobufs/services/address_book_service.proto +++ b/hapi/hedera-protobufs/services/address_book_service.proto @@ -33,7 +33,7 @@ import "transaction.proto"; * but each node operator may update their own operational attributes without * additional approval, reducing overhead for routine operations. * - * Most operations are `privileged operations` and require governing council + * All operations are `privileged operations` and require governing council * approval. * * ### For a node creation transaction. @@ -91,22 +91,18 @@ import "transaction.proto"; * - If the transaction changes the value of the "node account" the * node operator MUST _also_ sign this transaction with the active `key` * for the account to be assigned as the new "node account". - * - The node operator SHALL submit the transaction to the - * network. Hedera council approval SHALL NOT be sought for this - * transaction - * - If the Hedera council representative creates the transaction - * - The Hedera council representative SHALL arrange for council members - * to review and sign the transaction. - * - Once sufficient council members have signed the transaction, the - * Hedera council representative SHALL submit the transaction to the - * network. + * - The node operator MUST deliver the signed transaction to the Hedera + * council representative. + * - The Hedera council representative SHALL arrange for council members to + * review and sign the transaction. + * - Once sufficient council members have signed the transaction, the + * Hedera council representative SHALL submit the transaction to the + * network. * - Upon receipt of a valid and signed node update transaction the network * software SHALL - * - If the transaction is signed by the Hedera governing council - * - Validate the threshold signature for the Hedera governing council - * - If the transaction is signed by the active `key` for the node account - * - Validate the signature of the active `key` for the account assigned - * as the "node account". + * - Validate the threshold signature for the Hedera governing council + * - Validate the signature of the active `key` for the account to be + * assigned as the "node account". * - If the transaction modifies the value of the "node account", * - Validate the signature of the _new_ `key` for the account to be * assigned as the new "node account". @@ -151,8 +147,7 @@ service AddressBookService { * This transaction, once complete, SHALL modify the identified consensus * node state as requested. *

- * This transaction MAY be authorized by either the node operator OR the - * Hedera governing council. + * Hedera governing council authorization is REQUIRED for this transaction. */ rpc updateNode (proto.Transaction) returns (proto.TransactionResponse); } diff --git a/hapi/hedera-protobufs/services/tss_message.proto b/hapi/hedera-protobufs/services/auxiliary/tss/tss_message.proto similarity index 52% rename from hapi/hedera-protobufs/services/tss_message.proto rename to hapi/hedera-protobufs/services/auxiliary/tss/tss_message.proto index 2af5e1ed49c1..921652ba12cb 100644 --- a/hapi/hedera-protobufs/services/tss_message.proto +++ b/hapi/hedera-protobufs/services/auxiliary/tss/tss_message.proto @@ -10,7 +10,7 @@ */ syntax = "proto3"; -package proto; +package com.hedera.hapi.services.auxiliary.tss; /* * Copyright (C) 2024 Hedera Hashgraph, LLC @@ -28,23 +28,29 @@ package proto; * limitations under the License. */ -option java_package = "com.hederahashgraph.api.proto.java"; -// <<>> This comment is special code for setting PBJ Compiler java package +option java_package = "com.hedera.hapi.services.auxiliary.tss.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package option java_multiple_files = true; -/** The TssMessageTransaction is used to send a TssMessage to the network for a - * candidate roster. - *
- * This transaction contains the information to identify the source and target - * rosters, as well as the specific TssMessage being sent. - */ +/** A transaction body to to send a Threshold Signature Scheme (TSS) + * Message.
+ * This is a wrapper around several different TSS message types that a node + * might communicate with other nodes in the network. + * + * - A `TssMessageTransactionBody` MUST identify the hash of the roster + * containing the node generating this TssMessage + * - A `TssMessageTransactionBody` MUST identify the hash of the roster that + * the TSS messages is for + * - A `TssMessageTransactionBody` SHALL contain the specificc TssMessage data + * that has been generated by the node for the share_index. + */ message TssMessageTransactionBody { /** - * A hash of the roster containing the node generating the TssMessage. - *
- * This hash uniquely identifies the source roster. + * A hash of the roster containing the node generating the TssMessage.
+ * This hash uniquely identifies the source roster, which will include + * an entry for the node generating this TssMessage. *

* This value MUST be set.
* This value MUST NOT be empty.
@@ -62,17 +68,21 @@ message TssMessageTransactionBody { bytes target_roster_hash = 2; /** - * An index to order shares.
- * This establishes a global ordering of shares across all shares in - * the network.
- * It corresponds to the index of the public share in the list returned from - * the TSS library when the share was created for the source roster. + * An index to order shares. + *

+ * A share index SHALL establish a global ordering of shares across all + * shares in the network.
+ * A share index MUST correspond to the index of the public share in the list + * returned from the TSS library when the share was created for the source + * roster. */ uint64 share_index = 3; /** - * A byte array containing the TssMessage data generated by the node for the - * share_index. + * A byte array. + *

+ * This field SHALL contain the TssMessage data generated by the node + * for the specified `share_index`. */ bytes tss_message = 4; } diff --git a/hapi/hedera-protobufs/services/auxiliary/tss/tss_vote.proto b/hapi/hedera-protobufs/services/auxiliary/tss/tss_vote.proto new file mode 100644 index 000000000000..97eb559a43b2 --- /dev/null +++ b/hapi/hedera-protobufs/services/auxiliary/tss/tss_vote.proto @@ -0,0 +1,98 @@ +/** + * # Tss Vote Transaction + * + * ### Keywords + * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + * document are to be interpreted as described in + * [RFC2119](https://www.ietf.org/rfc/rfc2119) and clarified in + * [RFC8174](https://www.ietf.org/rfc/rfc8174). + */ +syntax = "proto3"; + +package com.hedera.hapi.services.auxiliary.tss; + +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +option java_package = "com.hedera.hapi.services.auxiliary.tss.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +/** + * A transaction body to vote on the validity of Threshold Signature Scheme + * (TSS) Messages for a candidate roster. + * + * - A `TssVoteTransactionBody` MUST identify the hash of the roster containing + * the node generating this TssVote + * - A `TssVoteTransactionBody` MUST identify the hash of the roster that the + * TSS messages is for + * - If the candidate roster has received enough yes votes, the candidate + * roster SHALL be adopted. + * - Switching to the candidate roster MUST not happen until enough nodes have + * voted that they have verified a threshold number of TSS messages from the + * active roster. + * - A vote consists of a bit vector of message statuses where each bit + * corresponds to the order of TssMessages as they have come through + * consensus. + * - The threshold for votes to adopt a candidate roster SHALL be at least 1/3 + * of the consensus weight of the active roster to ensure that at least 1 + * honest node has validated the TSS key material. + */ +message TssVoteTransactionBody { + + /** + * A hash of the roster containing the node generating this TssVote. + */ + bytes source_roster_hash = 1; + + /** + * A hash of the roster that this TssVote is for. + */ + bytes target_roster_hash = 2; + + /** + * An identifier (and public key) computed from the TssMessages for the target + * roster. + */ + bytes ledger_id = 3; + + /** + * A signature produced by the node. + *

+ * This signature SHALL be produced using the node RSA signing key to sign + * the ledger_id.
+ * This signature SHALL be used to establish a chain of trust in the ledger id. + */ + bytes node_signature = 4; + + /** + * A bit vector of message statuses. + *

+ * #### Example + *

  • The least significant bit of byte[0] SHALL be the 0th item in the sequence.
  • + *
  • The most significant bit of byte[0] SHALL be the 7th item in the sequence.
  • + *
  • The least significant bit of byte[1] SHALL be the 8th item in the sequence.
  • + *
  • The most significant bit of byte[1] SHALL be the 15th item in the sequence.
  • + *
+ * A bit SHALL be set if the `TssMessage` for the `TssMessageTransaction` + * with a sequence number matching that bit index has been + * received, and is valid.
+ * A bit SHALL NOT be set if the `TssMessage` has not been received or was + * received but not valid. + */ + bytes tss_vote = 5; +} diff --git a/hapi/hedera-protobufs/services/node_update.proto b/hapi/hedera-protobufs/services/node_update.proto index f899f9cc5ca8..834c6b9bd93b 100644 --- a/hapi/hedera-protobufs/services/node_update.proto +++ b/hapi/hedera-protobufs/services/node_update.proto @@ -28,9 +28,10 @@ import "basic_types.proto"; /** * Transaction body to modify address book node attributes. * - * - This transaction SHALL enable the node operator, as identified by the - * `admin_key`, to modify operational attributes of the node. - * - This transaction MUST be signed by the active `admin_key` for the node. + * This transaction body SHALL be considered a "privileged transaction". + * + * - This transaction MUST be signed by the governing council and + * - the active `admin_key` for the node. * - If this transaction sets a new value for the `admin_key`, then both the * current `admin_key`, and the new `admin_key` MUST sign this transaction. * - This transaction SHALL NOT change any field that is not set (is null) in diff --git a/hapi/hedera-protobufs/services/state/blockstream/block_stream_info.proto b/hapi/hedera-protobufs/services/state/blockstream/block_stream_info.proto index 6e58506613ea..f7b19530eb86 100644 --- a/hapi/hedera-protobufs/services/state/blockstream/block_stream_info.proto +++ b/hapi/hedera-protobufs/services/state/blockstream/block_stream_info.proto @@ -31,6 +31,7 @@ package com.hedera.hapi.node.state.blockstream; */ import "timestamp.proto"; +import "basic_types.proto"; option java_package = "com.hederahashgraph.api.proto.java"; // <<>> This comment is special code for setting PBJ Compiler java package @@ -73,7 +74,7 @@ message BlockStreamInfo { * A concatenation of hash values.
* This combines several trailing output block item hashes and * is used as a seed value for a pseudo-random number generator.
- * This is also requiried to implement the EVM `PREVRANDAO` opcode. + * This is also required to implement the EVM `PREVRANDAO` opcode. * This MUST contain at least 256 bits of entropy. */ bytes trailing_output_hashes = 3; @@ -131,4 +132,27 @@ message BlockStreamInfo { * the current block. */ proto.Timestamp block_end_time = 9; + + /** + * Whether the post-upgrade work has been done. + *

+ * This MUST be false if and only if the network just restarted + * after an upgrade and has not yet done the post-upgrade work. + */ + bool post_upgrade_work_done = 10; + + /** + * A version describing the version of application software. + *

+ * This SHALL be the software version that created this block. + */ + proto.SemanticVersion creation_software_version = 11; + + /** + * The time stamp at which the last interval process was done. + *

+ * This field SHALL hold the consensus time for the last time + * at which an interval of time-dependent events were processed. + */ + proto.Timestamp last_interval_process_time = 12; } diff --git a/hapi/hedera-protobufs/services/state/roster/ledger_id.proto b/hapi/hedera-protobufs/services/state/roster/ledger_id.proto index 7b4564359fd8..fe96db96e396 100644 --- a/hapi/hedera-protobufs/services/state/roster/ledger_id.proto +++ b/hapi/hedera-protobufs/services/state/roster/ledger_id.proto @@ -39,19 +39,21 @@ option java_multiple_files = true; /** * A ledger identifier.
- * This message identifies a ledger and can be used to verify ledger signatures. - * A LedgerId may change, but does so only in rare circumstances. + * This message identifies a ledger and is used to verify ledger + * signatures in a Threshold Signature Scheme (TSS). * * A ledger identifier SHALL be a public key defined according to the TSS * process.
- * A ledger identifier SHOULD NOT change, but MAY do so in rare circumstances. - * Clients SHOULD always check for the correct ledger identifier, according to the - * network roster, before attempting to verify any state proof or other ledger - * signature. + * A ledger identifier SHOULD NOT change, but MAY do so in rare + * circumstances.
+ * Clients SHOULD always check for the correct ledger identifier, according to + * the network roster, before attempting to verify any state proof or other + * ledger signature. * * ### Block Stream Effects - * Every Block Stream `BlockProof` item SHALL be signed via TSS and MUST be - * verified with the ledger identifier current at the _start_ of that block. + * Every block in the Block Stream `BlockProof` SHALL be signed via TSS and + * MUST be verified with the ledger identifier current at the _start_ of that + * block. * If the ledger identifier changes, the new value MUST be used to validate * Block Proof items after the change. * A change to the ledger identifier SHALL be reported in a State Change for @@ -62,7 +64,8 @@ message LedgerId { /** * A public key.
- * This key both identifies the ledger and can be used to verify ledger signatures. + * This key both identifies the ledger and can be used to verify ledger + * signatures. *

* This value MUST be set.
* This value MUST NOT be empty.
@@ -72,25 +75,25 @@ message LedgerId { /** * A round number.
- * This identifies when this ledger id becomes active. - *

+ * This identifies when this ledger id becomes active.
* This value is REQUIRED. */ uint64 round = 2; /** * A signature from the prior ledger key.
- * This signature is the _previous_ ledger ID signing _this_ ledger ID. - *

- * This value MAY be unset, if there is no prior ledger ID.
+ * This signature is the _previous_ ledger ID signing _this_ ledger ID.
+ * This value MAY be unset, if there is no prior ledger ID.
* This value SHOULD be set if a prior ledger ID exists * to generate the signature. */ bytes ledger_signature = 3; /** - * The signatures from nodes in the active roster signing the new ledger id.
- * These signatures establish a chain of trust from the network to the new ledger id. + * The signatures from nodes in the active roster signing the new + * ledger id.
+ * These signatures establish a chain of trust from the network to the new + * ledger id. *

* This value MUST be present when the ledger signature of a previous ledger * id is absent. @@ -121,11 +124,17 @@ message RosterSignatures { message NodeSignature { /** * The node id of the node that created the _RSA_ signature. + * This value MUST be set.
+ * This value MUST NOT be empty.
+ * This value is REQUIRED. */ uint64 node_id = 1; /** * The bytes of an _RSA_ signature. + * This value MUST be set.
+ * This value MUST NOT be empty.
+ * This value MUST contain a valid signature. */ bytes node_signature = 2; } diff --git a/hapi/hedera-protobufs/services/state/tss/tss_message_map_key.proto b/hapi/hedera-protobufs/services/state/tss/tss_message_map_key.proto index 4996b3ba39a0..bd33431563d8 100644 --- a/hapi/hedera-protobufs/services/state/tss/tss_message_map_key.proto +++ b/hapi/hedera-protobufs/services/state/tss/tss_message_map_key.proto @@ -13,8 +13,6 @@ syntax = "proto3"; package com.hedera.hapi.node.state.tss; /* - * Hedera Network Services Protobuf - * * Copyright (C) 2024 Hedera Hashgraph, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,14 +28,15 @@ package com.hedera.hapi.node.state.tss; * limitations under the License. */ -option java_package = "com.hederahashgraph.api.proto.java"; +option java_package = "com.hedera.hapi.node.state.tss.legacy"; // <<>> This comment is special code for setting PBJ Compiler java package option java_multiple_files = true; /** - * A key for use in the TssMessageMaps.
- * This key is used to uniquely identify entries in the TssMessageMaps. - */ + * A key for use in the Threshold Signature Scheme (TSS) TssMessageMaps. + * + * This key SHALL be used to uniquely identify entries in the Message Maps. + */ message TssMessageMapKey { /** diff --git a/hapi/hedera-protobufs/services/state/tss/tss_vote_map_key.proto b/hapi/hedera-protobufs/services/state/tss/tss_vote_map_key.proto index c7100dc64839..6ea669c73d99 100644 --- a/hapi/hedera-protobufs/services/state/tss/tss_vote_map_key.proto +++ b/hapi/hedera-protobufs/services/state/tss/tss_vote_map_key.proto @@ -13,8 +13,6 @@ syntax = "proto3"; package com.hedera.hapi.node.state.tss; /* - * Hedera Network Services Protobuf - * * Copyright (C) 2024 Hedera Hashgraph, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,13 +28,14 @@ package com.hedera.hapi.node.state.tss; * limitations under the License. */ -option java_package = "com.hederahashgraph.api.proto.java"; +option java_package = "com.hedera.hapi.node.state.tss.legacy"; // <<>> This comment is special code for setting PBJ Compiler java package option java_multiple_files = true; /** - * A key for use in the TssVoteMaps.
- * This key is used to uniquely identify entries in the TssVoteMaps. + * A key for use in the Threshold Signature Scheme (TSS) TssVoteMaps. + * + * This key SHALL be used to uniquely identify entries in the Vote Maps. */ message TssVoteMapKey { @@ -49,8 +48,7 @@ message TssVoteMapKey { */ bytes roster_hash = 1; - /* The node id of the node that created the TssVote. - *
+ /** The node id of the node that created the TssVote.
* This id uniquely identifies the node. *

* This value MUST be set.
diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index 4746467407e7..75557d43e6b7 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -89,8 +89,8 @@ import "node_create.proto"; import "node_update.proto"; import "node_delete.proto"; -import "tss_message.proto"; -import "tss_vote.proto"; +import "auxiliary/tss/tss_message.proto"; +import "auxiliary/tss/tss_vote.proto"; /** * A single transaction. All transaction types are possible here. @@ -425,11 +425,11 @@ message TransactionBody { /** * A transaction body for a `tssMessage` request. */ - TssMessageTransactionBody tssMessage = 61; + com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody tssMessage = 61; /** * A transaction body for a `tssVote` request. */ - TssVoteTransactionBody tssVote = 62; + com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody tssVote = 62; } } diff --git a/hapi/hedera-protobufs/services/tss_vote.proto b/hapi/hedera-protobufs/services/tss_vote.proto deleted file mode 100644 index 30134c5c2f1e..000000000000 --- a/hapi/hedera-protobufs/services/tss_vote.proto +++ /dev/null @@ -1,80 +0,0 @@ -/** - * # Tss Vote Transaction - * - * ### Keywords - * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", - * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this - * document are to be interpreted as described in - * [RFC2119](https://www.ietf.org/rfc/rfc2119) and clarified in - * [RFC8174](https://www.ietf.org/rfc/rfc8174). - */ -syntax = "proto3"; - -package proto; - -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -option java_package = "com.hederahashgraph.api.proto.java"; -// <<>> This comment is special code for setting PBJ Compiler java package -option java_multiple_files = true; - -/** - * A transaction used to vote on the validity of TssMessages for a candidate roster. - */ -message TssVoteTransactionBody { - - /** - * A hash of the roster containing the node generating this TssVote. - */ - bytes source_roster_hash = 1; - - /** - * A hash of the roster that this TssVote is for. - */ - bytes target_roster_hash = 2; - - /** - * An identifier (and public key) computed from the TssMessages for the target roster. - */ - bytes ledger_id = 3; - - /** - * A signature produced by the node.
- * This is produced using the node RSA signing key to sign the ledger_id.
- * This signature is used to establish a chain of trust in the ledger id. - */ - bytes node_signature = 4; - - /** - * A bit vector of message statuses.
- * Each bit in this vector indicates receipt (1) or non-receipt (0) of a - * _valid_ `TssMessage` for a corresponding `TssMessageTransaction`. - *

- * #### Example
- *

  • The least significant bit of byte[0] SHALL be sequence 0.
  • - *
  • The most significant bit of byte[0] SHALL be sequence 7.
  • - *
  • The least significant bit of byte[1] SHALL be sequence 8.
  • - *
  • The most significant bit of byte[0] SHALL be sequence 15.
  • - *
- * A bit SHALL be set if the `TssMessage` for the `TssMessageTransaction` - * with a sequence number matching that bit index has been - * received, and is valid.
- * A bit SHALL NOT be set if the `TssMessage` has not been received or was - * received but not valid. - */ - bytes tss_vote = 5; -} diff --git a/hapi/src/main/java/module-info.java b/hapi/src/main/java/module-info.java index 2137b5b8c832..676ba5935f50 100644 --- a/hapi/src/main/java/module-info.java +++ b/hapi/src/main/java/module-info.java @@ -23,9 +23,6 @@ exports com.hedera.hapi.node.token; exports com.hedera.hapi.node.token.codec; exports com.hedera.hapi.node.token.schema; - exports com.hedera.hapi.node.tss; - exports com.hedera.hapi.node.tss.codec; - exports com.hedera.hapi.node.tss.schema; exports com.hedera.hapi.node.transaction; exports com.hedera.hapi.node.transaction.codec; exports com.hedera.hapi.node.transaction.schema; @@ -66,6 +63,7 @@ exports com.hedera.hapi.node.state.roster; exports com.hedera.hapi.block.stream.schema; exports com.hedera.hapi.node.state.tss; + exports com.hedera.hapi.services.auxiliary.tss; requires transitive com.google.common; requires transitive com.google.protobuf; diff --git a/hedera-dependency-versions/build.gradle.kts b/hedera-dependency-versions/build.gradle.kts index 60dcfcf13aa5..a3963d6c8408 100644 --- a/hedera-dependency-versions/build.gradle.kts +++ b/hedera-dependency-versions/build.gradle.kts @@ -41,7 +41,7 @@ dependencies.constraints { because("com.github.benmanes.caffeine") } api("com.github.docker-java:docker-java-api:3.2.13") { - because("com.github.docker.java.api") + because("com.github.dockerjava.api") } api("com.github.spotbugs:spotbugs-annotations:4.7.3") { because("com.github.spotbugs.annotations") @@ -52,9 +52,12 @@ dependencies.constraints { api("com.google.auto.service:auto-service:1.1.1") { because("com.google.auto.service.processor") } - api("com.google.guava:guava:31.1-jre") { + api("com.google.guava:guava:33.1.0-jre") { because("com.google.common") } + api("com.google.j2objc:j2objc-annotations:3.0.0") { + because("com.google.j2objc.annotations") + } api("com.google.jimfs:jimfs:1.2") { because("com.google.jimfs") } diff --git a/hedera-node/developers.properties b/hedera-node/developers.properties new file mode 100644 index 000000000000..078a762f8735 --- /dev/null +++ b/hedera-node/developers.properties @@ -0,0 +1,4 @@ +hedera-base@hashgraph.com=Hedera Base Team +hedera-services@hashgraph.com=Hedera Services Team +hedera-smart-contracts@hashgraph.com=Hedera Smart Contracts Team +release-engineering@hashgraph.com=Release Engineering Team diff --git a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/CommonUtils.java b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/CommonUtils.java index 288ab958b114..48a4b7380c1c 100644 --- a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/CommonUtils.java +++ b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/CommonUtils.java @@ -28,12 +28,14 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.hedera.hapi.util.HapiUtils; import com.hedera.hapi.util.UnknownHederaFunctionality; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.SignatureMap; import com.hederahashgraph.api.proto.java.SignedTransaction; import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionOrBuilder; +import com.swirlds.common.crypto.DigestType; import edu.umd.cs.findbugs.annotations.NonNull; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -94,6 +96,23 @@ public static SignatureMap extractSignatureMap(final TransactionOrBuilder transa return transaction.getSigMap(); } + /** + * Returns a {@link MessageDigest} instance for the SHA-384 algorithm, throwing an unchecked exception if the + * algorithm is not found. + * @return a {@link MessageDigest} instance for the SHA-384 algorithm + */ + public static MessageDigest sha384DigestOrThrow() { + try { + return MessageDigest.getInstance(DigestType.SHA_384.algorithmName()); + } catch (final NoSuchAlgorithmException fatal) { + throw new IllegalStateException(fatal); + } + } + + public static Bytes noThrowSha384HashOf(final Bytes bytes) { + return Bytes.wrap(noThrowSha384HashOf(bytes.toByteArray())); + } + public static byte[] noThrowSha384HashOf(final byte[] byteArray) { try { return MessageDigest.getInstance(sha384HashTag).digest(byteArray); diff --git a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/RecordStreamEntry.java b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/RecordStreamEntry.java index 7539dcf45a26..4ebff38607e8 100644 --- a/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/RecordStreamEntry.java +++ b/hedera-node/hapi-utils/src/main/java/com/hedera/node/app/hapi/utils/forensics/RecordStreamEntry.java @@ -18,6 +18,7 @@ import static com.hedera.node.app.hapi.utils.CommonUtils.timestampToInstant; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.FileID; import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.services.stream.proto.RecordStreamItem; @@ -72,6 +73,15 @@ public ResponseCodeEnum finalStatus() { return txnRecord.getReceipt().getStatus(); } + /** + * Returns the account ID created by the transaction, if any. + * + * @return the created account ID + */ + public AccountID createdAccountId() { + return CommonPbjConverters.toPbj(txnRecord.getReceipt().getAccountID()); + } + /** * Returns the file ID created by the transaction, if any. * diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeCreateHandler.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeCreateHandler.java index 73a0258c5c49..ce83ac33e636 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeCreateHandler.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeCreateHandler.java @@ -72,7 +72,7 @@ public NodeCreateHandler(@NonNull final AddressBookValidator addressBookValidato @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { requireNonNull(txn); - final var op = txn.nodeCreate(); + final var op = txn.nodeCreateOrThrow(); addressBookValidator.validateAccountId(op.accountId()); validateFalsePreCheck(op.gossipEndpoint().isEmpty(), INVALID_GOSSIP_ENDPOINT); validateFalsePreCheck(op.serviceEndpoint().isEmpty(), INVALID_SERVICE_ENDPOINT); @@ -95,7 +95,7 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx @Override public void handle(@NonNull final HandleContext handleContext) { requireNonNull(handleContext); - final var op = handleContext.body().nodeCreate(); + final var op = handleContext.body().nodeCreateOrThrow(); final var nodeConfig = handleContext.configuration().getConfigData(NodesConfig.class); final var storeFactory = handleContext.storeFactory(); final var nodeStore = storeFactory.writableStore(WritableNodeStore.class); diff --git a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeUpdateHandler.java b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeUpdateHandler.java index f174aa68dcf8..15f239a3679f 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeUpdateHandler.java +++ b/hedera-node/hedera-addressbook-service-impl/src/main/java/com/hedera/node/app/service/addressbook/impl/handlers/NodeUpdateHandler.java @@ -65,7 +65,7 @@ public NodeUpdateHandler(@NonNull final AddressBookValidator addressBookValidato @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { requireNonNull(txn); - final var op = txn.nodeUpdate(); + final var op = txn.nodeUpdateOrThrow(); validateFalsePreCheck(op.nodeId() < 0, INVALID_NODE_ID); if (op.hasGossipCaCertificate()) { validateFalsePreCheck(op.gossipCaCertificate().equals(Bytes.EMPTY), INVALID_GOSSIP_CA_CERTIFICATE); diff --git a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java index 5e3b77b7be52..22f02182ee70 100644 --- a/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java +++ b/hedera-node/hedera-addressbook-service-impl/src/test/java/com/hedera/node/app/service/addressbook/impl/test/schemas/V053AddressBookSchemaTest.java @@ -243,6 +243,14 @@ void migrateAsExpected4() { writableNodes.get(EntityNumber.newBuilder().number(3).build())); } + @Test + void failedNullNetworkinfo() { + given(migrationContext.genesisNetworkInfo()).willReturn(null); + assertThatCode(() -> subject.migrate(migrationContext)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Genesis network info is not found"); + } + private void setupMigrationContext() { writableStates = MapWritableStates.builder().state(writableNodes).build(); given(migrationContext.newStates()).willReturn(writableStates); diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java index 2836df6cef59..56e0abbdc990 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java @@ -16,7 +16,12 @@ package com.hedera.node.app.spi; +import static com.hedera.hapi.node.base.ResponseCodeEnum.FAIL_INVALID; + +import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.spi.signatures.SignatureVerifier; +import edu.umd.cs.findbugs.annotations.NonNull; import java.time.InstantSource; /** @@ -24,6 +29,28 @@ * shared functions like verifying signatures or computing the current instant. */ public interface AppContext { + /** + * The {@link Gossip} interface is used to submit transactions to the network. + */ + interface Gossip { + /** + * A {@link Gossip} that throws an exception indicating it should never have been used; for example, + * if the client code was running in a standalone mode. + */ + Gossip UNAVAILABLE_GOSSIP = body -> { + throw new IllegalArgumentException("" + FAIL_INVALID); + }; + + /** + * Attempts to submit the given transaction to the network. + * @param body the transaction to submit + * @throws IllegalStateException if the network is not active; the client should retry later + * @throws IllegalArgumentException if body is invalid; so the client can retry immediately with a + * different transaction id if the exception's message is {@link ResponseCodeEnum#DUPLICATE_TRANSACTION} + */ + void submit(@NonNull TransactionBody body); + } + /** * The source of the current instant. * @return the instant source @@ -35,4 +62,10 @@ public interface AppContext { * @return the signature verifier */ SignatureVerifier signatureVerifier(); + + /** + * The {@link Gossip} can be used to submit transactions to the network when it is active. + * @return the gossip interface + */ + Gossip gossip(); } diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeContext.java index d06f399e32e5..409c30dd4450 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/fees/FeeContext.java @@ -67,7 +67,7 @@ public interface FeeContext { * * @return the {@code Configuration} */ - @Nullable + @NonNull Configuration configuration(); /** diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyComparator.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyComparator.java index 737e87439e21..2783feeeef04 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyComparator.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyComparator.java @@ -30,10 +30,10 @@ *
These include maps, sets, lists, arrays, etc... *
The methods in this class are used in hot spot code, so allocation must be kept to a bare * minimum, and anything likely to have performance questions should be avoided. - *
Note that comparing keys is unavoidably costly. We try to exit as early as possible throughout - * this class, but worst case we're comparing every simple key byte-by-byte for the entire tree, which - * may be up to 15 levels deep with any number of keys per level. We haven't seen a key with - * several million "simple" keys included, but that does not mean nobody will create one. + *
Note that comparing keys can be fairly costly, as in principle a key structure can have a + * serialized size up to about {@code TransactionConfig#transactionMaxBytes()}. We try to exit as + * early as possible throughout this class, but worst case we're comparing every simple key + * byte-by-byte for the entire tree. */ public class KeyComparator implements Comparator { @Override diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyVerifier.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyVerifier.java index 3c74db270cc0..3269c95a0416 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyVerifier.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/key/KeyVerifier.java @@ -16,15 +16,20 @@ package com.hedera.node.app.spi.key; +import static java.util.Collections.unmodifiableSortedSet; + import com.hedera.hapi.node.base.Key; import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.signatures.VerificationAssistant; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.SortedSet; +import java.util.TreeSet; /** * Helper class that contains all functionality for verifying signatures during handle. */ public interface KeyVerifier { + SortedSet NO_CRYPTO_KEYS = unmodifiableSortedSet(new TreeSet<>(new KeyComparator())); /** * Gets the {@link SignatureVerification} for the given key. If this key was not provided during pre-handle, then @@ -60,4 +65,20 @@ public interface KeyVerifier { */ @NonNull SignatureVerification verificationFor(@NonNull Key key, @NonNull VerificationAssistant callback); + + /** + * If this verifier is based on cryptographic verification of signatures on a transaction submitted from + * outside the blockchain, returns the set of cryptographic keys that had valid signatures, ordered by the + * {@link KeyComparator}. + *

+ * Default is an empty set, for verifiers that use a more abstract concept of signing, such as, + *

    + *
  1. Whether a key references the contract whose EVM address is the recipient address of the active frame.
  2. + *
  3. Whether a key is present in the signatories list of a scheduled transaction.
  4. + *
+ * @return the set of cryptographic keys that had valid signatures for this transaction. + */ + default SortedSet signingCryptoKeys() { + return NO_CRYPTO_KEYS; + } } diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordCache.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordCache.java index b0803f62dd57..0c458cae8a98 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordCache.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordCache.java @@ -21,6 +21,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.UNKNOWN; import static com.hedera.hapi.util.HapiUtils.TIMESTAMP_COMPARATOR; import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ResponseCodeEnum; @@ -53,7 +54,8 @@ public interface RecordCache { * For mono-service fidelity, records with these statuses do not prevent valid transactions with * the same id from reaching consensus and being handled. */ - Set DUE_DILIGENCE_FAILURES = EnumSet.of(INVALID_NODE_ACCOUNT, INVALID_PAYER_SIGNATURE); + Set NODE_FAILURES = EnumSet.of(INVALID_NODE_ACCOUNT, INVALID_PAYER_SIGNATURE); + /** * And when ordering records for queries, we treat records with unclassifiable statuses as the * lowest "priority"; so that e.g. if a transaction with id {@code X} resolves to {@link ResponseCodeEnum#SUCCESS} @@ -65,11 +67,82 @@ public interface RecordCache { @SuppressWarnings("java:S3358") Comparator RECORD_COMPARATOR = Comparator.comparing( rec -> rec.receiptOrThrow().status(), - (a, b) -> DUE_DILIGENCE_FAILURES.contains(a) == DUE_DILIGENCE_FAILURES.contains(b) + (a, b) -> NODE_FAILURES.contains(a) == NODE_FAILURES.contains(b) ? 0 - : (DUE_DILIGENCE_FAILURES.contains(b) ? -1 : 1)) + : (NODE_FAILURES.contains(b) ? -1 : 1)) .thenComparing(rec -> rec.consensusTimestampOrElse(Timestamp.DEFAULT), TIMESTAMP_COMPARATOR); + /** + * Returns true if the two transaction IDs are equal in all fields except for the nonce. + * @param aTxnId the first transaction ID + * @param bTxnId the second transaction ID + * @return true if the two transaction IDs are equal in all fields except for the nonce + */ + static boolean matchesExceptNonce(@NonNull final TransactionID aTxnId, @NonNull final TransactionID bTxnId) { + requireNonNull(aTxnId); + requireNonNull(bTxnId); + return aTxnId.accountIDOrElse(AccountID.DEFAULT).equals(bTxnId.accountIDOrElse(AccountID.DEFAULT)) + && aTxnId.transactionValidStartOrElse(Timestamp.DEFAULT) + .equals(bTxnId.transactionValidStartOrElse(Timestamp.DEFAULT)) + && aTxnId.scheduled() == bTxnId.scheduled(); + } + + /** + * Returns true if the second transaction ID is a child of the first. + * @param aTxnId the first transaction ID + * @param bTxnId the second transaction ID + * @return true if the second transaction ID is a child of the first + */ + static boolean isChild(@NonNull final TransactionID aTxnId, @NonNull final TransactionID bTxnId) { + requireNonNull(aTxnId); + requireNonNull(bTxnId); + return aTxnId.nonce() == 0 && bTxnId.nonce() != 0 && matchesExceptNonce(aTxnId, bTxnId); + } + + /** + * Just the receipts for a source of one or more {@link TransactionID}s instead of the full records. + */ + interface ReceiptSource { + /** + * This receipt is returned whenever we know there is a transaction pending (i.e. we have a history for a + * transaction ID), but we do not yet have a record for it. + */ + TransactionReceipt PENDING_RECEIPT = + TransactionReceipt.newBuilder().status(UNKNOWN).build(); + + /** + * The "priority" receipt for the transaction id, if known; or {@link ReceiptSource#PENDING_RECEIPT} if there are no + * consensus receipts with this id. (The priority receipt is the first receipt in the id's history that had a + * status not in {@link RecordCache#NODE_FAILURES}; or if all its receipts have such statuses, the first + * one to have reached consensus.) + * @return the priority receipt, if known + */ + @NonNull + TransactionReceipt priorityReceipt(@NonNull TransactionID txnId); + + /** + * The child receipt with this transaction id, if any; or null otherwise. + * @return the child receipt, if known + */ + @Nullable + TransactionReceipt childReceipt(@NonNull TransactionID txnId); + + /** + * All the duplicate receipts for the transaction id, if any, with the statuses in + * {@link RecordCache#NODE_FAILURES} coming last. The list is otherwise ordered by consensus timestamp. + * @return the duplicate receipts, if any + */ + @NonNull + List duplicateReceipts(@NonNull TransactionID txnId); + + /** + * All the child receipts for the transaction id, if any. The list is ordered by consensus timestamp. + * @return the child receipts, if any + */ + @NonNull + List childReceipts(@NonNull TransactionID txnId); + } + /** * An item stored in the cache. * @@ -92,13 +165,6 @@ record History( @NonNull List records, @NonNull List childRecords) { - /** - * This receipt is returned whenever we know there is a transaction pending (i.e. we have a history for a - * transaction ID), but we do not yet have a record for it. - */ - private static final TransactionReceipt PENDING_RECEIPT = - TransactionReceipt.newBuilder().status(UNKNOWN).build(); - /** * Create a new {@link History} instance with empty lists. */ @@ -117,17 +183,10 @@ public TransactionRecord userTransactionRecord() { return records.isEmpty() ? null : sortedRecords().getFirst(); } - /** - * Gets the primary receipt, that is, the receipt associated with the user transaction itself. This receipt will - * be null if there is no such record. - * - * @return The primary receipt, if there is one. - */ - @Nullable - public TransactionReceipt userTransactionReceipt() { + public @NonNull TransactionReceipt priorityReceipt() { return records.isEmpty() - ? PENDING_RECEIPT - : sortedRecords().getFirst().receipt(); + ? ReceiptSource.PENDING_RECEIPT + : sortedRecords().getFirst().receiptOrThrow(); } /** @@ -181,6 +240,14 @@ private List sortedRecords() { @Nullable History getHistory(@NonNull TransactionID transactionID); + /** + * Gets the receipts for the given {@link TransactionID}, if known. + * @param transactionID The transaction ID to look up + * @return the receipts, if any, stored in this cache for the given transaction ID + */ + @Nullable + ReceiptSource getReceipts(@NonNull TransactionID transactionID); + /** * Gets a list of all records for the given {@link AccountID}. The {@link AccountID} is the account of the Payer of * the transaction. diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordSource.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordSource.java new file mode 100644 index 000000000000..e377746ff2c3 --- /dev/null +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/records/RecordSource.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.spi.records; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.spi.workflows.HandleContext.SavepointStack; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.function.Consumer; + +/** + * A source of {@link TransactionRecord}s and {@link TransactionReceipt}s for one or more {@link TransactionID}'s. + *

+ * (FUTURE) Important: It would be much simpler if this interface was scoped to a single {@link TransactionID}, + * but that adds overhead in the current system where a single {@link SavepointStack} commit flushes builders for + * unrelated ids. + *

+ * Once we refactor to use a separate {@link SavepointStack} for each id, we can simplify this interface. + */ +public interface RecordSource { + /** + * A receipt with its originating {@link TransactionID}. + * @param txnId the transaction id + * @param receipt the matching receipt + */ + record IdentifiedReceipt(@NonNull TransactionID txnId, @NonNull TransactionReceipt receipt) { + public IdentifiedReceipt { + requireNonNull(txnId); + requireNonNull(receipt); + } + } + + /** + * Returns all identified receipts known to this source. + * @return the receipts + */ + List identifiedReceipts(); + + /** + * Perform the given action on each transaction record known to this source. + * @param action the action to perform + */ + void forEachTxnRecord(@NonNull Consumer action); + + /** + * Returns the priority receipt for the given transaction id. + * @throws IllegalArgumentException if the transaction id is unknown + */ + TransactionReceipt receiptOf(@NonNull TransactionID txnId); + + /** + * Returns all child receipts for the given transaction id. + */ + List childReceiptsOf(@NonNull TransactionID txnId); +} diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java index 40dbcd8b7adf..78a4207bde59 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/HandleContext.java @@ -16,6 +16,7 @@ package com.hedera.node.app.spi.workflows; +import static com.hedera.node.app.spi.AppContext.*; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.SCHEDULED; import com.hedera.hapi.node.base.AccountID; @@ -63,16 +64,26 @@ public interface HandleContext { * Category of the current transaction. */ enum TransactionCategory { - /** The original transaction submitted by a user. */ + /** + * A transaction submitted by a user via HAPI or by a node via {@link com.hedera.node.app.spi.AppContext.Gossip}. + * */ USER, - - /** An independent, top-level transaction that is executed before the user transaction. */ + /** + * An independent, top-level transaction that is executed before the user transaction. + * */ PRECEDING, - - /** A child transaction that is executed as part of a user transaction. */ + /** + * A child transaction that is executed as part of a user transaction. + * */ CHILD, - /** A transaction executed via the schedule service. */ - SCHEDULED + /** + * A transaction executed via the schedule service. + * */ + SCHEDULED, + /** + * A transaction submitted by Node for TSS service + */ + NODE } /** @@ -498,28 +509,31 @@ interface SavepointStack { * Adds a child record builder to the list of record builders. If the current {@link HandleContext} (or any parent * context) is rolled back, all child record builders will be reverted. * + * @param the record type * @param recordBuilderClass the record type + * @param functionality the functionality of the record * @return the new child record builder - * @param the record type * @throws NullPointerException if {@code recordBuilderClass} is {@code null} * @throws IllegalArgumentException if the record builder type is unknown to the app */ @NonNull - T addChildRecordBuilder(@NonNull Class recordBuilderClass); + T addChildRecordBuilder(@NonNull Class recordBuilderClass, @NonNull HederaFunctionality functionality); /** * Adds a removable child record builder to the list of record builders. Unlike a regular child record builder, * a removable child record builder is removed, if the current {@link HandleContext} (or any parent context) is * rolled back. * + * @param the record type * @param recordBuilderClass the record type + * @param functionality the functionality of the record * @return the new child record builder - * @param the record type * @throws NullPointerException if {@code recordBuilderClass} is {@code null} * @throws IllegalArgumentException if the record builder type is unknown to the app */ @NonNull - T addRemovableChildRecordBuilder(@NonNull Class recordBuilderClass); + T addRemovableChildRecordBuilder( + @NonNull Class recordBuilderClass, @NonNull HederaFunctionality functionality); } static void throwIfMissingPayerId(@NonNull final TransactionBody body) { diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/PreHandleContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/PreHandleContext.java index dcd70e7d8060..5ed85aa6ba7e 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/PreHandleContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/PreHandleContext.java @@ -276,13 +276,13 @@ PreHandleContext requireKeyIfReceiverSigRequired( /** * Returns all (required and optional) keys of a nested transaction. * - * @param nestedTxn the {@link TransactionBody} which keys are needed - * @param payerForNested the payer for the nested transaction + * @param body the {@link TransactionBody} which keys are needed + * @param payerId the payer for the nested transaction * @return the set of keys * @throws PreCheckException If there is a problem with the nested transaction */ @NonNull - TransactionKeys allKeysForTransaction(@NonNull TransactionBody nestedTxn, @NonNull AccountID payerForNested) + TransactionKeys allKeysForTransaction(@NonNull TransactionBody body, @NonNull AccountID payerId) throws PreCheckException; /** diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/TransactionKeys.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/TransactionKeys.java index 80d5ebc78d11..810283412a53 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/TransactionKeys.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/TransactionKeys.java @@ -25,7 +25,6 @@ * Contains all keys and hollow accounts (required and optional) of a transaction. */ public interface TransactionKeys { - /** * Getter for the payer key * diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/record/StreamBuilder.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/record/StreamBuilder.java index 8ad7946cac08..3ab4061cc03e 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/record/StreamBuilder.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/workflows/record/StreamBuilder.java @@ -17,21 +17,23 @@ package com.hedera.node.app.spi.workflows.record; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.NODE; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.PRECEDING; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.SCHEDULED; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.block.stream.output.StateChange; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.SignatureMap; import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.transaction.ExchangeRateSet; import com.hedera.hapi.node.transaction.SignedTransaction; import com.hedera.hapi.node.transaction.TransactionBody; -import com.hedera.hapi.node.transaction.TransactionRecord; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; @@ -136,8 +138,8 @@ default StreamBuilder stateChanges(@NonNull List stateChanges) { StreamBuilder status(@NonNull ResponseCodeEnum status); /** - * Returns the {@link TransactionRecord.Builder} of the record. It can be PRECEDING, CHILD, USER or SCHEDULED. - * @return the {@link TransactionRecord.Builder} of the record + * Returns the category of the builder's transaction. + * @return the category */ HandleContext.TransactionCategory category(); @@ -235,23 +237,29 @@ default boolean isInternalDispatch() { * @return true if this transaction is internal */ default boolean isUserDispatch() { - return category() == USER || category() == SCHEDULED; + return category() == USER || category() == SCHEDULED || category() == NODE; } /** - * Convenience method to package as {@link TransactionBody} as a {@link Transaction} . - * + * Convenience method to package as {@link TransactionBody} as a {@link Transaction} whose + * {@link SignedTransaction} has an unset {@link SignatureMap}. * @param body the transaction body * @return the transaction */ - static Transaction transactionWith(@NonNull TransactionBody body) { - final var bodyBytes = TransactionBody.PROTOBUF.toBytes(body); - final var signedTransaction = - SignedTransaction.newBuilder().bodyBytes(bodyBytes).build(); - final var signedTransactionBytes = SignedTransaction.PROTOBUF.toBytes(signedTransaction); - return Transaction.newBuilder() - .signedTransactionBytes(signedTransactionBytes) - .build(); + static Transaction transactionWith(@NonNull final TransactionBody body) { + requireNonNull(body); + return transactionWith(body, null); + } + + /** + * Convenience method to package a {@link TransactionBody} as a {@link Transaction} whose + * {@link SignedTransaction} has an empty {@link SignatureMap}. + * @param body the transaction body + * @return the transaction + */ + static Transaction nodeTransactionWith(@NonNull final TransactionBody body) { + requireNonNull(body); + return transactionWith(body, SignatureMap.DEFAULT); } /** @@ -283,4 +291,17 @@ enum ReversingBehavior { */ IRREVERSIBLE } + + private static Transaction transactionWith( + @NonNull final TransactionBody body, @Nullable final SignatureMap sigMap) { + final var bodyBytes = TransactionBody.PROTOBUF.toBytes(body); + final var signedTransaction = SignedTransaction.newBuilder() + .sigMap(sigMap) + .bodyBytes(bodyBytes) + .build(); + final var signedTransactionBytes = SignedTransaction.PROTOBUF.toBytes(signedTransaction); + return Transaction.newBuilder() + .signedTransactionBytes(signedTransactionBytes) + .build(); + } } diff --git a/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/records/RecordCacheTest.java b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/records/RecordCacheTest.java new file mode 100644 index 000000000000..4507227a6739 --- /dev/null +++ b/hedera-node/hedera-app-spi/src/test/java/com/hedera/node/app/spi/records/RecordCacheTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.spi.records; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ACCOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_SIGNATURE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.hapi.node.base.ResponseCodeEnum.UNKNOWN; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class RecordCacheTest { + private static final TransactionID USER_TXN_ID = TransactionID.newBuilder() + .accountID(AccountID.newBuilder().accountNum(666L).build()) + .transactionValidStart(new Timestamp(1, 0)) + .scheduled(true) + .build(); + + @Test + void constantsAsExpected() { + assertThat(RecordCache.NODE_FAILURES).containsExactlyInAnyOrder(INVALID_PAYER_SIGNATURE, INVALID_NODE_ACCOUNT); + assertThat(RecordCache.ReceiptSource.PENDING_RECEIPT) + .isEqualTo(TransactionReceipt.newBuilder().status(UNKNOWN).build()); + } + + @Test + void comparatorPutsAnyPossiblyUniqueReceiptFirst() { + final var earlyNonUniqueRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(INVALID_PAYER_SIGNATURE) + .build()) + .consensusTimestamp(new Timestamp(1234, 0)) + .build(); + final var latePossiblyUniqueRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(DUPLICATE_TRANSACTION) + .build()) + .consensusTimestamp(new Timestamp(5678, 0)) + .build(); + assertThat(RecordCache.RECORD_COMPARATOR.compare(latePossiblyUniqueRecord, earlyNonUniqueRecord)) + .isLessThan(0); + } + + @Test + void twoPossiblyUniqueReceiptsAreOrderedByConsTime() { + final var earlyUniqueRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder().status(SUCCESS).build()) + .consensusTimestamp(new Timestamp(1234, 0)) + .build(); + final var lateUniqueRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(DUPLICATE_TRANSACTION) + .build()) + .consensusTimestamp(new Timestamp(5678, 0)) + .build(); + assertThat(RecordCache.RECORD_COMPARATOR.compare(lateUniqueRecord, earlyUniqueRecord)) + .isGreaterThan(0); + } + + @Test + void idsMatchEvenIfNonceDiffers() { + assertThat(RecordCache.matchesExceptNonce( + USER_TXN_ID, USER_TXN_ID.copyBuilder().nonce(1).build())) + .isTrue(); + } + + @Test + void idsDontMatchIfAnythingButNonceDiffers() { + assertThat(RecordCache.matchesExceptNonce( + USER_TXN_ID, + USER_TXN_ID + .copyBuilder() + .transactionValidStart(Timestamp.DEFAULT) + .build())) + .isFalse(); + assertThat(RecordCache.matchesExceptNonce( + USER_TXN_ID, + USER_TXN_ID.copyBuilder().accountID(AccountID.DEFAULT).build())) + .isFalse(); + assertThat(RecordCache.matchesExceptNonce( + USER_TXN_ID, USER_TXN_ID.copyBuilder().scheduled(false).build())) + .isFalse(); + } + + @Test + void childDetectionRequiresUserTxnIdAsParent() { + assertThat(RecordCache.isChild( + USER_TXN_ID, USER_TXN_ID.copyBuilder().nonce(2).build())) + .isTrue(); + assertThat(RecordCache.isChild( + USER_TXN_ID.copyBuilder().nonce(1).build(), + USER_TXN_ID.copyBuilder().nonce(2).build())) + .isFalse(); + } + + @Test + void childDetectionRequiresMatchingChildTxnIdAsChild() { + assertThat(RecordCache.isChild(USER_TXN_ID, USER_TXN_ID)).isFalse(); + assertThat(RecordCache.isChild( + USER_TXN_ID, + USER_TXN_ID + .copyBuilder() + .accountID(AccountID.DEFAULT) + .nonce(1) + .build())) + .isFalse(); + } + + @Test + void emptyHistoryAsExpected() { + final var subject = new RecordCache.History(); + assertThat(subject.userTransactionRecord()).isNull(); + assertThat(subject.priorityReceipt()).isSameAs(RecordCache.ReceiptSource.PENDING_RECEIPT); + assertThat(subject.duplicateRecords()).isEmpty(); + assertThat(subject.duplicateCount()).isZero(); + assertThat(subject.orderedRecords()).isEmpty(); + } + + @Test + void nonEmptyHistoryAsExpected() { + final var userRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder().status(SUCCESS).build()) + .consensusTimestamp(new Timestamp(456, 0)) + .build(); + final var invalidRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(INVALID_NODE_ACCOUNT) + .build()) + .consensusTimestamp(new Timestamp(123, 0)) + .build(); + final var childRecord = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder().status(SUCCESS).build()) + .consensusTimestamp(new Timestamp(456, 1)) + .build(); + final var subject = + new RecordCache.History(Set.of(0L), List.of(invalidRecord, userRecord), List.of(childRecord)); + assertThat(subject.userTransactionRecord()).isSameAs(userRecord); + assertThat(subject.priorityReceipt()).isSameAs(userRecord.receiptOrThrow()); + assertThat(subject.duplicateRecords()).containsExactly(invalidRecord); + assertThat(subject.duplicateCount()).isEqualTo(1); + assertThat(subject.orderedRecords()).containsExactly(userRecord, childRecord, invalidRecord); + } +} diff --git a/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/info/FakeNetworkInfo.java b/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/info/FakeNetworkInfo.java index 81a306d912a2..83fe2f3bfb6c 100644 --- a/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/info/FakeNetworkInfo.java +++ b/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/info/FakeNetworkInfo.java @@ -34,7 +34,7 @@ */ public class FakeNetworkInfo implements NetworkInfo { private static final Bytes DEV_LEDGER_ID = Bytes.wrap(new byte[] {0x03}); - private static final List FAKE_NODE_INFO_IDS = List.of(new NodeId(2), new NodeId(4), new NodeId(8)); + private static final List FAKE_NODE_INFO_IDS = List.of(NodeId.of(2), NodeId.of(4), NodeId.of(8)); private static final List FAKE_NODE_INFOS = List.of( fakeInfoWith( 2L, @@ -81,7 +81,7 @@ public NodeInfo nodeInfo(long nodeId) { @Override public boolean containsNode(final long nodeId) { - return FAKE_NODE_INFO_IDS.contains(new NodeId(nodeId)); + return FAKE_NODE_INFO_IDS.contains(NodeId.of(nodeId)); } @Override diff --git a/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/workflows/FakePreHandleContext.java b/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/workflows/FakePreHandleContext.java index 912a28791d56..472bc7d5edd3 100644 --- a/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/workflows/FakePreHandleContext.java +++ b/hedera-node/hedera-app-spi/src/testFixtures/java/com/hedera/node/app/spi/fixtures/workflows/FakePreHandleContext.java @@ -423,8 +423,7 @@ public PreHandleContext requireSignatureForHollowAccountCreation(@NonNull final @NonNull @Override - public TransactionKeys allKeysForTransaction( - @NonNull TransactionBody nestedTxn, @NonNull AccountID payerForNested) { + public TransactionKeys allKeysForTransaction(@NonNull TransactionBody body, @NonNull AccountID payerId) { throw new UnsupportedOperationException("Not yet implemented"); } diff --git a/hedera-node/hedera-app/build.gradle.kts b/hedera-node/hedera-app/build.gradle.kts index 9ba5fd9c9d9d..ca9f0ba73598 100644 --- a/hedera-node/hedera-app/build.gradle.kts +++ b/hedera-node/hedera-app/build.gradle.kts @@ -45,8 +45,10 @@ testModuleInfo { requires("com.hedera.node.app.spi.test.fixtures") requires("com.hedera.node.config.test.fixtures") requires("com.swirlds.config.extensions.test.fixtures") + requires("com.swirlds.common.test.fixtures") requires("com.swirlds.platform.core.test.fixtures") requires("com.swirlds.state.api.test.fixtures") + requires("com.swirlds.base.test.fixtures") requires("headlong") requires("org.assertj.core") requires("org.bouncycastle.provider") @@ -63,11 +65,17 @@ testModuleInfo { jmhModuleInfo { requires("com.hedera.node.app") requires("com.hedera.node.app.hapi.utils") + requires("com.hedera.node.app.spi") requires("com.hedera.node.app.spi.test.fixtures") requires("com.hedera.node.app.test.fixtures") + requires("com.hedera.node.config") requires("com.hedera.node.hapi") requires("com.hedera.pbj.runtime") requires("com.swirlds.common") + requires("com.swirlds.config.api") + requires("com.swirlds.metrics.api") + requires("com.swirlds.platform.core") + requires("com.swirlds.state.api") requires("jmh.core") } diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java new file mode 100644 index 000000000000..d309f4057bc8 --- /dev/null +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks; + +import static com.hedera.hapi.block.stream.output.SingletonUpdateChange.NewValueOneOfType.BLOCK_STREAM_INFO_VALUE; +import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_BLOCK_STREAM_INFO; +import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_PLATFORM_STATE; +import static com.hedera.node.app.blocks.BlockStreamManager.ZERO_BLOCK_HASH; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.CompletableFuture.completedFuture; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.block.stream.output.SingletonUpdateChange; +import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; +import com.hedera.hapi.platform.state.PlatformState; +import com.hedera.node.app.blocks.impl.BlockStreamManagerImpl; +import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; +import com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema; +import com.hedera.node.app.config.ConfigProviderImpl; +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.node.app.services.AppContextImpl; +import com.hedera.node.app.spi.signatures.SignatureVerifier; +import com.hedera.node.app.tss.TssBaseServiceImpl; +import com.hedera.node.config.ConfigProvider; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.BufferedData; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.crypto.Hash; +import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.state.notifications.StateHashedNotification; +import com.swirlds.state.spi.Schema; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.zip.GZIPInputStream; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 1) +@Measurement(iterations = 3) +public class BlockStreamManagerBenchmark { + private static final long FIRST_ROUND_NO = 123L; + private static final Bytes FAKE_START_OF_BLOCK_STATE_HASH = Bytes.fromHex("ab".repeat(48)); + private static final Hash FAKE_STATE_HASH = new Hash(FAKE_START_OF_BLOCK_STATE_HASH.toByteArray()); + private static final String SAMPLE_BLOCK = "sample.blk.gz"; + private static final Instant FAKE_CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + private static final Timestamp FAKE_CONSENSUS_TIME = new Timestamp(1_234_567L, 890); + private static final SemanticVersion VERSION = new SemanticVersion(0, 56, 0, "", ""); + + public static void main(String... args) throws Exception { + org.openjdk.jmh.Main.main(new String[] {"com.hedera.node.app.blocks.BlockStreamManagerBenchmark.manageRound"}); + } + + private final Round round = new FakeRound(); + private final ConfigProvider configProvider = new ConfigProviderImpl( + false, + null, + Map.of( + "blockStream.hashCombineBatchSize", "64", + "blockStream.serializationBatchSize", "32")); + private final List roundItems = new ArrayList<>(); + private final TssBaseServiceImpl tssBaseService = new TssBaseServiceImpl( + new AppContextImpl(Instant::now, fakeSignatureVerifier(), UNAVAILABLE_GOSSIP), + ForkJoinPool.commonPool(), + ForkJoinPool.commonPool()); + private final BlockStreamManagerImpl subject = new BlockStreamManagerImpl( + NoopBlockItemWriter::new, + // BaosBlockItemWriter::new, + ForkJoinPool.commonPool(), + configProvider, + tssBaseService, + new FakeBoundaryStateChangeListener(), + new InitialStateHash(completedFuture(FAKE_START_OF_BLOCK_STATE_HASH), FIRST_ROUND_NO - 1), + VERSION); + + @Param({"10"}) + private int numEvents; + + @Param({"100"}) + private int numTxnsPerEvent; + + private long roundNum = FIRST_ROUND_NO; + private FakeState state; + private BlockItem boundaryStateChanges; + private PlatformState platformState; + + @Setup(Level.Trial) + public void setup() throws IOException, ParseException { + loadSampleItems(); + state = new FakeState(); + addServiceSingleton(new V0560BlockStreamSchema(ignore -> {}), BlockStreamService.NAME, BlockStreamInfo.DEFAULT); + addServiceSingleton(new V0540PlatformStateSchema(), PlatformStateService.NAME, platformState); + subject.initLastBlockHash(ZERO_BLOCK_HASH); + tssBaseService.registerLedgerSignatureConsumer(subject); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void manageRound() { + subject.startRound(round, state); + roundItems.forEach(subject::writeItem); + subject.notify(new StateHashedNotification(roundNum, FAKE_STATE_HASH)); + subject.endRound(state, roundNum); + roundNum++; + } + + private void addServiceSingleton( + @NonNull final Schema schema, @NonNull final String serviceName, @NonNull final T singletonValue) { + final Map stateDataSources = new HashMap<>(); + schema.statesToCreate(configProvider.getConfiguration()).forEach(def -> { + if (def.singleton()) { + stateDataSources.put(def.stateKey(), new AtomicReference<>(singletonValue)); + } + }); + state.addService(serviceName, stateDataSources); + } + + private void loadSampleItems() throws IOException, ParseException { + BlockItem blockHeader = null; + BlockItem roundHeader = null; + BlockItem lastStateChanges = null; + BlockItem penultimateStateChanges = null; + BlockItem sampleEventHeader = null; + BlockItem sampleEventTxn = null; + BlockItem sampleTxnResult = null; + BlockItem sampleTxnStateChanges = null; + try (final var fin = SerializationBenchmark.class.getClassLoader().getResourceAsStream(SAMPLE_BLOCK)) { + try (final var in = new GZIPInputStream(fin)) { + final var block = Block.PROTOBUF.parse(Bytes.wrap(in.readAllBytes())); + for (final var item : block.items()) { + switch (item.item().kind()) { + case BLOCK_HEADER -> blockHeader = item; + case ROUND_HEADER -> roundHeader = item; + case EVENT_HEADER -> { + if (sampleEventHeader == null) { + sampleEventHeader = item; + } + } + case EVENT_TRANSACTION -> { + if (sampleEventTxn == null) { + sampleEventTxn = item; + } + } + case TRANSACTION_RESULT -> { + if (sampleTxnResult == null) { + sampleTxnResult = item; + } + } + case STATE_CHANGES -> { + penultimateStateChanges = lastStateChanges; + lastStateChanges = item; + if (sampleTxnStateChanges == null) { + sampleTxnStateChanges = item; + } + } + } + } + roundItems.add(requireNonNull(blockHeader)); + roundItems.add(requireNonNull(roundHeader)); + for (int i = 0; i < numEvents; i++) { + roundItems.add(requireNonNull(sampleEventHeader)); + for (int j = 0; j < numTxnsPerEvent; j++) { + roundItems.add(requireNonNull(sampleEventTxn)); + roundItems.add(requireNonNull(sampleTxnResult)); + roundItems.add(requireNonNull(sampleTxnStateChanges)); + } + } + } + boundaryStateChanges = requireNonNull(penultimateStateChanges); + platformState = boundaryStateChanges.stateChangesOrThrow().stateChanges().stream() + .filter(stateChange -> stateChange.stateId() == STATE_ID_PLATFORM_STATE.protoOrdinal()) + .findFirst() + .map(StateChange::singletonUpdateOrThrow) + .map(SingletonUpdateChange::platformStateValueOrThrow) + .orElseThrow(); + } + } + + private class FakeBoundaryStateChangeListener extends BoundaryStateChangeListener { + private boolean nextChangesAreFromState = false; + + @Override + public BlockItem flushChanges() { + if (nextChangesAreFromState) { + final var blockStreamInfo = state.getReadableStates(BlockStreamService.NAME) + .getSingleton(BLOCK_STREAM_INFO_KEY) + .get(); + requireNonNull(blockStreamInfo); + final var stateChanges = new StateChanges( + FAKE_CONSENSUS_TIME, + List.of(StateChange.newBuilder() + .stateId(STATE_ID_BLOCK_STREAM_INFO.protoOrdinal()) + .singletonUpdate(new SingletonUpdateChange( + new OneOf<>(BLOCK_STREAM_INFO_VALUE, blockStreamInfo))) + .build())); + nextChangesAreFromState = false; + return BlockItem.newBuilder().stateChanges(stateChanges).build(); + } else { + nextChangesAreFromState = true; + return boundaryStateChanges; + } + } + + @NonNull + @Override + public Timestamp boundaryTimestampOrThrow() { + return FAKE_CONSENSUS_TIME; + } + } + + private class FakeRound implements Round { + @NonNull + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public long getRoundNum() { + return roundNum; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public int getEventCount() { + return 0; + } + + @NonNull + @Override + public AddressBook getConsensusRoster() { + throw new UnsupportedOperationException(); + } + + @NonNull + @Override + public Instant getConsensusTimestamp() { + return FAKE_CONSENSUS_NOW; + } + } + + private static class NoopBlockItemWriter implements BlockItemWriter { + @Override + public void openBlock(final long blockNumber) { + // No-op + } + + @Override + public BlockItemWriter writeItem(@NonNull final byte[] bytes) { + return this; + } + + @Override + public BlockItemWriter writeItems(@NonNull BufferedData data) { + return this; + } + + @Override + public void closeBlock() { + // No-op + } + } + + private static class BaosBlockItemWriter implements BlockItemWriter { + private static final int BLOCKS_TO_CHECK = 10; + private static final String BLOCKS_DIR = "other-blocks"; + + private static int numBlocksToWrite = BLOCKS_TO_CHECK; + + private ByteArrayOutputStream baos; + + @Override + public void openBlock(final long blockNumber) { + baos = new ByteArrayOutputStream(); + } + + @Override + public BlockItemWriter writeItem(@NonNull final byte[] bytes) { + try { + baos.write(bytes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return this; + } + + @Override + public BlockItemWriter writeItems(@NonNull final BufferedData data) { + try { + final var block = Block.PROTOBUF.parse(data); + for (final var item : block.items()) { + writePbjItem(BlockItem.PROTOBUF.toBytes(item)); + } + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + return this; + } + + @Override + public void closeBlock() { + if (numBlocksToWrite > 0) { + final var blockNo = BLOCKS_TO_CHECK - numBlocksToWrite + 1; + final var path = Paths.get(BLOCKS_DIR, "block" + blockNo + ".blk"); + try { + Files.createDirectories(path.getParent()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (final var fout = Files.newOutputStream(path)) { + baos.writeTo(fout); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + numBlocksToWrite--; + if (numBlocksToWrite == 0) { + System.exit(0); + } + } + } + } + + private SignatureVerifier fakeSignatureVerifier() { + return new SignatureVerifier() { + @Override + public boolean verifySignature( + @NonNull Key key, + @NonNull Bytes bytes, + @NonNull MessageType messageType, + @NonNull SignatureMap signatureMap, + @Nullable Function simpleKeyVerifier) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public KeyCounts countSimpleKeys(@NonNull Key key) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + } +} diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/HashingBenchmark.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/HashingBenchmark.java index d07f6ab8e7c1..a76f6e6e1443 100644 --- a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/HashingBenchmark.java +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/HashingBenchmark.java @@ -17,7 +17,7 @@ package com.hedera.node.app.blocks; import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_ACCOUNTS; -import static com.hedera.node.app.blocks.impl.NaiveStreamingTreeHasher.hashNaively; +import static com.hedera.node.app.hapi.utils.CommonUtils.sha384DigestOrThrow; import com.hedera.hapi.block.stream.BlockItem; import com.hedera.hapi.block.stream.output.MapChangeKey; @@ -29,8 +29,11 @@ import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.state.token.Account; import com.hedera.node.app.blocks.impl.ConcurrentStreamingTreeHasher; +import com.hedera.node.app.blocks.impl.NaiveStreamingTreeHasher; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.nio.ByteBuffer; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -64,28 +67,31 @@ public static void main(String... args) throws Exception { } @Param({"10000"}) - private int numLeaves; + private int numLeafHashes; - private List leaves; + private List leafHashes; private Bytes expectedAnswer; @Setup(Level.Trial) - public void setup() { - leaves = new ArrayList<>(numLeaves); - for (int i = 0; i < numLeaves; i++) { - leaves.add(BlockItem.PROTOBUF.toBytes(randomBlockItem())); + public void setup() throws IOException { + final var digest = sha384DigestOrThrow(); + leafHashes = new ArrayList<>(numLeafHashes); + for (int i = 0; i < numLeafHashes; i++) { + final var item = randomBlockItem(); + final var hash = digest.digest(BlockItem.PROTOBUF.toBytes(item).toByteArray()); + leafHashes.add(hash); } - expectedAnswer = hashNaively(leaves); + expectedAnswer = NaiveStreamingTreeHasher.computeRootHash(leafHashes); } @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void hashItemTree(@NonNull final Blackhole blackhole) { - // final var subject = new NaiveStreamingTreeHasher(); + // final var subject = new NaiveStreamingTreeHasher(); final var subject = new ConcurrentStreamingTreeHasher(ForkJoinPool.commonPool()); - for (final var item : leaves) { - subject.addLeaf(item); + for (final var hash : leafHashes) { + subject.addLeaf(ByteBuffer.wrap(hash)); } final var rootHash = subject.rootHash().join(); if (!rootHash.equals(expectedAnswer)) { diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/SerializationBenchmark.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/SerializationBenchmark.java new file mode 100644 index 000000000000..6114e06c0fe0 --- /dev/null +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/SerializationBenchmark.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 1) +@Measurement(iterations = 3) +public class SerializationBenchmark { + public static void main(String... args) throws Exception { + org.openjdk.jmh.Main.main(new String[] {"com.hedera.node.app.blocks.SerializationBenchmark.serializeItem"}); + } + + public enum BlockType { + TRANSACTION, + TRANSACTION_RESULT, + TRANSACTION_OUTPUT, + KV_STATE_CHANGES, + SINGLETON_STATE_CHANGES + } + + private static final String SAMPLE_BLOCK = "sample.blk.gz"; + private static final Map SAMPLE_ITEMS = new HashMap<>(); + private static final Map SAMPLE_ITEM_SIZES = new HashMap<>(); + + @Param({"TRANSACTION_RESULT"}) + // @Param({"SINGLETON_STATE_CHANGES"}) + private BlockType blockType; + + @Param({"1000"}) + private int numItems; + + private Bytes expectedAnswer; + private ByteBuffer buffer; + + @Setup(Level.Trial) + public void setup() throws IOException, ParseException { + try (final var fin = SerializationBenchmark.class.getClassLoader().getResourceAsStream(SAMPLE_BLOCK)) { + try (final GZIPInputStream in = new GZIPInputStream(fin)) { + final var block = Block.PROTOBUF.parse(Bytes.wrap(in.readAllBytes())); + for (final var item : block.items()) { + switch (item.item().kind()) { + case EVENT_TRANSACTION -> SAMPLE_ITEMS.put(BlockType.TRANSACTION, item); + case TRANSACTION_RESULT -> SAMPLE_ITEMS.put(BlockType.TRANSACTION_RESULT, item); + case TRANSACTION_OUTPUT -> SAMPLE_ITEMS.put(BlockType.TRANSACTION_OUTPUT, item); + case STATE_CHANGES -> { + final var stateChanges = + item.stateChangesOrThrow().stateChanges().getFirst(); + if (stateChanges.hasMapUpdate()) { + SAMPLE_ITEMS.put(BlockType.KV_STATE_CHANGES, item); + } else if (stateChanges.hasSingletonUpdate()) { + SAMPLE_ITEMS.put(BlockType.SINGLETON_STATE_CHANGES, item); + } + } + } + } + SAMPLE_ITEMS.forEach((type, item) -> SAMPLE_ITEM_SIZES.put( + type, (int) BlockItem.PROTOBUF.toBytes(item).length())); + } + } + final var item = SAMPLE_ITEMS.get(blockType); + final var serializedItem = BlockItem.PROTOBUF.toBytes(item).toByteArray(); + final var itemSize = (int) BlockItem.PROTOBUF.toBytes(item).length(); + final var bytes = new byte[numItems * SAMPLE_ITEM_SIZES.get(blockType)]; + for (int i = 0; i < numItems; i++) { + System.arraycopy(serializedItem, 0, bytes, i * itemSize, itemSize); + } + expectedAnswer = Bytes.wrap(bytes); + buffer = ByteBuffer.allocate(bytes.length); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @OutputTimeUnit(TimeUnit.SECONDS) + public void serializeItem(@NonNull final Blackhole blackhole) throws IOException { + final var item = SAMPLE_ITEMS.get(blockType); + + final var serializedItems = new ArrayList(numItems); + for (int i = 0; i < numItems; i++) { + serializedItems.add(BlockItem.PROTOBUF.toBytes(item)); + } + blackhole.consume(serializedItems); + + // final var outputStream = BufferedData.wrap(buffer); + // for (int i = 0; i < numItems; i++) { + // BlockItem.PROTOBUF.write(item, outputStream); + // } + // final var bytes = buffer.array(); + // if (buffer.position() != (int) expectedAnswer.length()) { + // throw new IllegalStateException(); + // } + // buffer.rewind(); + // blackhole.consume(bytes); + } +} diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java new file mode 100644 index 000000000000..f5606da96977 --- /dev/null +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks; + +import static com.hedera.hapi.block.stream.output.SingletonUpdateChange.NewValueOneOfType.BLOCK_STREAM_INFO_VALUE; +import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_BLOCK_STREAM_INFO; +import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_PLATFORM_STATE; +import static com.hedera.node.app.blocks.BlockStreamManager.ZERO_BLOCK_HASH; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.CompletableFuture.completedFuture; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.block.stream.output.SingletonUpdateChange; +import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.SemanticVersion; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; +import com.hedera.hapi.platform.state.PlatformState; +import com.hedera.node.app.blocks.impl.BlockStreamManagerImpl; +import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; +import com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema; +import com.hedera.node.app.config.ConfigProviderImpl; +import com.hedera.node.app.fixtures.state.FakeState; +import com.hedera.node.app.services.AppContextImpl; +import com.hedera.node.app.spi.signatures.SignatureVerifier; +import com.hedera.node.app.tss.TssBaseServiceImpl; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.BlockStreamConfig; +import com.hedera.pbj.runtime.OneOf; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.pbj.runtime.io.buffer.BufferedData; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.crypto.Hash; +import com.swirlds.platform.state.service.PlatformStateService; +import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; +import com.swirlds.platform.system.Round; +import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.state.notifications.StateHashedNotification; +import com.swirlds.state.spi.Schema; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.zip.GZIPInputStream; + +public class StandaloneRoundManagement { + private static final long FIRST_ROUND_NO = 123L; + private static final Bytes FAKE_START_OF_BLOCK_STATE_HASH = Bytes.fromHex("ab".repeat(48)); + private static final Hash FAKE_STATE_HASH = new Hash(FAKE_START_OF_BLOCK_STATE_HASH.toByteArray()); + private static final String SAMPLE_BLOCK = "sample.blk.gz"; + private static final Instant FAKE_CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + private static final Timestamp FAKE_CONSENSUS_TIME = new Timestamp(1_234_567L, 890); + private static final SemanticVersion VERSION = new SemanticVersion(0, 56, 0, "", ""); + + private static final int NUM_ROUNDS = 10000; + private static final int NUM_EVENTS = 10; + private static final int NUM_TXNS_PER_EVENT = 100; + + private final Round round = new FakeRound(); + private final ConfigProvider configProvider = + new ConfigProviderImpl(false, null, Map.of("blockStream.serializationBatchSize", "32")); + private final List roundItems = new ArrayList<>(); + private final TssBaseServiceImpl tssBaseService = new TssBaseServiceImpl( + new AppContextImpl(Instant::now, fakeSignatureVerifier(), UNAVAILABLE_GOSSIP), + ForkJoinPool.commonPool(), + ForkJoinPool.commonPool()); + private final BlockStreamManagerImpl subject = new BlockStreamManagerImpl( + NoopBlockItemWriter::new, + ForkJoinPool.commonPool(), + configProvider, + tssBaseService, + new FakeBoundaryStateChangeListener(), + new InitialStateHash(completedFuture(FAKE_START_OF_BLOCK_STATE_HASH), FIRST_ROUND_NO - 1), + VERSION); + + private long roundNum = FIRST_ROUND_NO; + private FakeState state; + private BlockItem boundaryStateChanges; + private PlatformState platformState; + + public static void main(@NonNull final String[] args) throws IOException, ParseException { + final var sim = new StandaloneRoundManagement(); + sim.setup(); + for (int i = 0; i < NUM_ROUNDS; i++) { + sim.manageRound(); + } + } + + public void setup() throws IOException, ParseException { + loadSampleItems(); + state = new FakeState(); + addServiceSingleton(new V0560BlockStreamSchema(ignore -> {}), BlockStreamService.NAME, BlockStreamInfo.DEFAULT); + addServiceSingleton(new V0540PlatformStateSchema(), PlatformStateService.NAME, platformState); + subject.initLastBlockHash(ZERO_BLOCK_HASH); + tssBaseService.registerLedgerSignatureConsumer(subject); + System.out.println("serializationBatchSize = " + + configProvider + .getConfiguration() + .getConfigData(BlockStreamConfig.class) + .serializationBatchSize()); + } + + public void manageRound() { + subject.startRound(round, state); + roundItems.forEach(subject::writeItem); + subject.notify(new StateHashedNotification(roundNum, FAKE_STATE_HASH)); + subject.endRound(state, roundNum); + roundNum++; + } + + private void addServiceSingleton( + @NonNull final Schema schema, @NonNull final String serviceName, @NonNull final T singletonValue) { + final Map stateDataSources = new HashMap<>(); + schema.statesToCreate(configProvider.getConfiguration()).forEach(def -> { + if (def.singleton()) { + stateDataSources.put(def.stateKey(), new AtomicReference<>(singletonValue)); + } + }); + state.addService(serviceName, stateDataSources); + } + + private void loadSampleItems() throws IOException, ParseException { + BlockItem blockHeader = null; + BlockItem roundHeader = null; + BlockItem lastStateChanges = null; + BlockItem penultimateStateChanges = null; + BlockItem sampleEventHeader = null; + BlockItem sampleEventTxn = null; + BlockItem sampleTxnResult = null; + BlockItem sampleTxnStateChanges = null; + try (final var fin = SerializationBenchmark.class.getClassLoader().getResourceAsStream(SAMPLE_BLOCK)) { + try (final var in = new GZIPInputStream(fin)) { + final var block = Block.PROTOBUF.parse(Bytes.wrap(in.readAllBytes())); + for (final var item : block.items()) { + switch (item.item().kind()) { + case BLOCK_HEADER -> blockHeader = item; + case ROUND_HEADER -> roundHeader = item; + case EVENT_HEADER -> { + if (sampleEventHeader == null) { + sampleEventHeader = item; + } + } + case EVENT_TRANSACTION -> { + if (sampleEventTxn == null) { + sampleEventTxn = item; + } + } + case TRANSACTION_RESULT -> { + if (sampleTxnResult == null) { + sampleTxnResult = item; + } + } + case STATE_CHANGES -> { + penultimateStateChanges = lastStateChanges; + lastStateChanges = item; + if (sampleTxnStateChanges == null) { + sampleTxnStateChanges = item; + } + } + } + } + roundItems.add(requireNonNull(blockHeader)); + roundItems.add(requireNonNull(roundHeader)); + for (int i = 0; i < NUM_EVENTS; i++) { + roundItems.add(requireNonNull(sampleEventHeader)); + for (int j = 0; j < NUM_TXNS_PER_EVENT; j++) { + roundItems.add(requireNonNull(sampleEventTxn)); + roundItems.add(requireNonNull(sampleTxnResult)); + roundItems.add(requireNonNull(sampleTxnStateChanges)); + } + } + } + boundaryStateChanges = requireNonNull(penultimateStateChanges); + platformState = boundaryStateChanges.stateChangesOrThrow().stateChanges().stream() + .filter(stateChange -> stateChange.stateId() == STATE_ID_PLATFORM_STATE.protoOrdinal()) + .findFirst() + .map(StateChange::singletonUpdateOrThrow) + .map(SingletonUpdateChange::platformStateValueOrThrow) + .orElseThrow(); + } + } + + private class FakeBoundaryStateChangeListener extends BoundaryStateChangeListener { + private boolean nextChangesAreFromState = false; + + @Override + public BlockItem flushChanges() { + if (nextChangesAreFromState) { + final var blockStreamInfo = state.getReadableStates(BlockStreamService.NAME) + .getSingleton(BLOCK_STREAM_INFO_KEY) + .get(); + requireNonNull(blockStreamInfo); + final var stateChanges = new StateChanges( + FAKE_CONSENSUS_TIME, + List.of(StateChange.newBuilder() + .stateId(STATE_ID_BLOCK_STREAM_INFO.protoOrdinal()) + .singletonUpdate(new SingletonUpdateChange( + new OneOf<>(BLOCK_STREAM_INFO_VALUE, blockStreamInfo))) + .build())); + nextChangesAreFromState = false; + return BlockItem.newBuilder().stateChanges(stateChanges).build(); + } else { + nextChangesAreFromState = true; + return boundaryStateChanges; + } + } + + @NonNull + @Override + public Timestamp boundaryTimestampOrThrow() { + return FAKE_CONSENSUS_TIME; + } + } + + private static class NoopBlockItemWriter implements BlockItemWriter { + @Override + public void openBlock(final long blockNumber) { + // No-op + } + + @Override + public BlockItemWriter writeItem(@NonNull final byte[] bytes) { + return this; + } + + @Override + public BlockItemWriter writeItems(@NonNull final BufferedData data) { + return this; + } + + @Override + public void closeBlock() { + // No-op + } + } + + private class FakeRound implements Round { + @NonNull + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public long getRoundNum() { + return roundNum; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public int getEventCount() { + return 0; + } + + @NonNull + @Override + public AddressBook getConsensusRoster() { + throw new UnsupportedOperationException(); + } + + @NonNull + @Override + public Instant getConsensusTimestamp() { + return FAKE_CONSENSUS_NOW; + } + } + + private SignatureVerifier fakeSignatureVerifier() { + return new SignatureVerifier() { + @Override + public boolean verifySignature( + @NonNull Key key, + @NonNull Bytes bytes, + @NonNull MessageType messageType, + @NonNull SignatureMap signatureMap, + @Nullable Function simpleKeyVerifier) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public KeyCounts countSimpleKeys(@NonNull Key key) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + } +} diff --git a/hedera-node/hedera-app/src/jmh/resources/sample.blk.gz b/hedera-node/hedera-app/src/jmh/resources/sample.blk.gz new file mode 100644 index 000000000000..808679b245ed Binary files /dev/null and b/hedera-node/hedera-app/src/jmh/resources/sample.blk.gz differ diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index 5cf7ff07db84..1f4c6f6cbfe7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -17,24 +17,35 @@ package com.hedera.node.app; import static com.hedera.hapi.block.stream.output.StateIdentifier.STATE_ID_BLOCK_STREAM_INFO; +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.PLATFORM_NOT_ACTIVE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.UNKNOWN; +import static com.hedera.hapi.util.HapiUtils.functionOf; import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; import static com.hedera.node.app.blocks.impl.ConcurrentStreamingTreeHasher.rootHashFrom; -import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; import static com.hedera.node.app.info.UnavailableNetworkInfo.UNAVAILABLE_NETWORK_INFO; import static com.hedera.node.app.records.impl.BlockRecordInfoUtils.blockHashByBlockNumber; import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; +import static com.hedera.node.app.spi.workflows.record.StreamBuilder.nodeTransactionWith; import static com.hedera.node.app.state.merkle.VersionUtils.isSoOrdered; import static com.hedera.node.app.statedumpers.DumpCheckpoint.MOD_POST_EVENT_STREAM_REPLAY; import static com.hedera.node.app.statedumpers.DumpCheckpoint.selectedDumpCheckpoints; import static com.hedera.node.app.statedumpers.StateDumper.dumpModChildrenFrom; import static com.hedera.node.app.util.HederaAsciiArt.HEDERA; import static com.hedera.node.app.workflows.handle.metric.UnavailableMetrics.UNAVAILABLE_METRICS; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.swirlds.platform.state.service.PlatformStateService.PLATFORM_STATE_SERVICE; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; import static com.swirlds.platform.system.InitTrigger.EVENT_STREAM_RECOVERY; import static com.swirlds.platform.system.InitTrigger.GENESIS; import static com.swirlds.platform.system.InitTrigger.RECONNECT; import static com.swirlds.platform.system.address.AddressBookUtils.createRoster; +import static com.swirlds.platform.system.status.PlatformStatus.ACTIVE; +import static com.swirlds.platform.system.status.PlatformStatus.STARTING_UP; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -43,11 +54,14 @@ import com.hedera.hapi.block.stream.output.SingletonUpdateChange; import com.hedera.hapi.block.stream.output.StateChange; import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; +import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.hapi.util.HapiUtils; +import com.hedera.hapi.util.UnknownHederaFunctionality; import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.BlockStreamService; import com.hedera.node.app.blocks.InitialStateHash; @@ -79,6 +93,8 @@ import com.hedera.node.app.signature.AppSignatureVerifier; import com.hedera.node.app.signature.impl.SignatureExpanderImpl; import com.hedera.node.app.signature.impl.SignatureVerifierImpl; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.state.MerkleStateLifecyclesImpl; import com.hedera.node.app.state.recordcache.RecordCacheService; import com.hedera.node.app.statedumpers.DumpCheckpoint; @@ -86,7 +102,6 @@ import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.throttle.CongestionThrottleService; import com.hedera.node.app.tss.TssBaseService; -import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; import com.hedera.node.app.version.HederaSoftwareVersion; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.app.workflows.handle.HandleWorkflow; @@ -96,7 +111,9 @@ import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.LedgerConfig; +import com.hedera.node.config.data.NetworkAdminConfig; import com.hedera.node.config.data.VersionConfig; +import com.hedera.node.config.types.StreamMode; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistry; @@ -124,6 +141,7 @@ import com.swirlds.platform.system.SwirldMain; import com.swirlds.platform.system.events.Event; import com.swirlds.platform.system.state.notifications.StateHashedListener; +import com.swirlds.platform.system.status.PlatformStatus; import com.swirlds.platform.system.transaction.Transaction; import com.swirlds.state.State; import com.swirlds.state.StateChangeListener; @@ -142,6 +160,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -175,7 +194,7 @@ * including its state. It constructs the Dagger dependency tree, and manages the gRPC server, and in all other ways, * controls execution of the node. If you want to understand our system, this is a great place to start! */ -public final class Hedera implements SwirldMain, PlatformStatusChangeListener { +public final class Hedera implements SwirldMain, PlatformStatusChangeListener, AppContext.Gossip { private static final Logger logger = LogManager.getLogger(Hedera.class); // FUTURE: This should come from configuration, not be hardcoded. @@ -215,15 +234,16 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { private final InstantSource instantSource; /** - * The supplier for the TSS base service. + * The contract service singleton, kept as a field here to avoid constructing twice + * (once in constructor to register schemas, again inside Dagger component). */ - private final Supplier tssBaseServiceSupplier; + private final ContractServiceImpl contractServiceImpl; /** - * The contract service singleton, kept as a field here to avoid constructing twice + * The TSS base service singleton, kept as a field here to avoid constructing twice * (once in constructor to register schemas, again inside Dagger component). */ - private final ContractServiceImpl contractServiceImpl; + private final TssBaseService tssBaseService; /** * The file service singleton, kept as a field here to avoid constructing twice @@ -242,10 +262,19 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { */ private final BootstrapConfigProviderImpl bootstrapConfigProvider; + /** + * The stream mode the node is operating in. + */ + private final StreamMode streamMode; + /** * The Hashgraph Platform. This is set during state initialization. */ private Platform platform; + /** + * The current status of the platform. + */ + private PlatformStatus platformStatus = STARTING_UP; /** * The configuration for this node; non-final because its sources depend on whether * we are initializing the first consensus state from genesis or a saved state. @@ -258,6 +287,9 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { */ private HederaInjectionComponent daggerApp; + /** + * The metrics object being used for reporting. + */ private Metrics metrics; /** @@ -306,17 +338,16 @@ public final class Hedera implements SwirldMain, PlatformStatusChangeListener { * @param constructableRegistry the registry to register {@link RuntimeConstructable} factories with * @param registryFactory the factory to use for creating the services registry * @param migrator the migrator to use with the services - * @param tssBaseServiceSupplier the supplier for the TSS base service + * @param tssBaseServiceFactory the factory for the TSS base service */ public Hedera( @NonNull final ConstructableRegistry constructableRegistry, @NonNull final ServicesRegistry.Factory registryFactory, @NonNull final ServiceMigrator migrator, @NonNull final InstantSource instantSource, - @NonNull final Supplier tssBaseServiceSupplier) { + @NonNull final Function tssBaseServiceFactory) { requireNonNull(registryFactory); requireNonNull(constructableRegistry); - this.tssBaseServiceSupplier = requireNonNull(tssBaseServiceSupplier); this.serviceMigrator = requireNonNull(migrator); this.instantSource = requireNonNull(instantSource); logger.info( @@ -333,6 +364,7 @@ public Hedera( final var bootstrapConfig = bootstrapConfigProvider.getConfiguration(); hapiVersion = bootstrapConfig.getConfigData(VersionConfig.class).hapiVersion(); version = getNodeStartupVersion(bootstrapConfig); + streamMode = bootstrapConfig.getConfigData(BlockStreamConfig.class).streamMode(); servicesRegistry = registryFactory.create(constructableRegistry, bootstrapConfig); logger.info( "Creating Hedera Consensus Node {} with HAPI {}", @@ -345,15 +377,18 @@ public Hedera( new AppSignatureVerifier( bootstrapConfig.getConfigData(HederaConfig.class), new SignatureExpanderImpl(), - new SignatureVerifierImpl(CryptographyHolder.get()))); + new SignatureVerifierImpl(CryptographyHolder.get())), + this); + tssBaseService = tssBaseServiceFactory.apply(appContext); contractServiceImpl = new ContractServiceImpl(appContext); - blockStreamService = new BlockStreamService(bootstrapConfig); + blockStreamService = new BlockStreamService(); // Register all service schema RuntimeConstructable factories before platform init Set.of( new EntityIdService(), new ConsensusServiceImpl(), contractServiceImpl, fileServiceImpl, + tssBaseService, new FreezeServiceImpl(), new ScheduleServiceImpl(), new TokenServiceImpl(), @@ -369,10 +404,9 @@ public Hedera( PLATFORM_STATE_SERVICE) .forEach(servicesRegistry::register); try { - final var blockStreamsEnabled = - bootstrapConfig.getConfigData(BlockStreamConfig.class).streamBlocks(); final Supplier baseSupplier = () -> new MerkleStateRoot(new MerkleStateLifecyclesImpl(this), ServicesSoftwareVersion::new); + final var blockStreamsEnabled = isBlockStreamEnabled(); stateRootSupplier = blockStreamsEnabled ? () -> withListeners(baseSupplier.get()) : baseSupplier; onSealConsensusRound = blockStreamsEnabled ? this::manageBlockEndRound : (round, state) -> {}; // And the factory for the MerkleStateRoot class id must be our constructor @@ -419,7 +453,7 @@ public MerkleRoot newMerkleStateRoot() { @Override public void notify(@NonNull final PlatformStatusChangeNotification notification) { - final var platformStatus = notification.getNewStatus(); + this.platformStatus = notification.getNewStatus(); logger.info("HederaNode#{} is {}", platform.getSelfId(), platformStatus.name()); switch (platformStatus) { case ACTIVE -> startGrpcServer(); @@ -587,7 +621,9 @@ private List onMigrate( migrationStateChanges.addAll(migrationChanges); kvStateChangeListener.reset(); boundaryStateChangeListener.reset(); - if (isUpgrade && !trigger.equals(RECONNECT)) { + // If still using BlockRecordManager state, then for specifically a non-genesis upgrade, + // set in state that post-upgrade work is pending + if (streamMode != BLOCKS && isUpgrade && trigger != RECONNECT && trigger != GENESIS) { unmarkMigrationRecordsStreamed(state); migrationStateChanges.add( StateChanges.newBuilder().stateChanges(boundaryStateChangeListener.allStateChanges())); @@ -625,6 +661,38 @@ public void init(@NonNull final Platform platform, @NonNull final NodeId nodeId) logger.info("Locale to set to US en"); } + @Override + public void submit(@NonNull final TransactionBody body) { + requireNonNull(body); + if (platformStatus != ACTIVE) { + throw new IllegalStateException("" + PLATFORM_NOT_ACTIVE); + } + final HederaFunctionality function; + try { + function = functionOf(body); + } catch (UnknownHederaFunctionality e) { + throw new IllegalArgumentException("" + UNKNOWN); + } + try { + final var config = configProvider.getConfiguration(); + final var adminConfig = config.getConfigData(NetworkAdminConfig.class); + final var allowList = adminConfig.nodeTransactionsAllowList().functionalitySet(); + if (!allowList.contains(function)) { + throw new IllegalArgumentException("" + NOT_SUPPORTED); + } + final var payload = com.hedera.hapi.node.base.Transaction.PROTOBUF.toBytes(nodeTransactionWith(body)); + requireNonNull(daggerApp).submissionManager().submit(body, payload); + } catch (PreCheckException e) { + final var reason = e.responseCode(); + if (reason == DUPLICATE_TRANSACTION) { + // In this case the client must not retry with the same transaction, but + // could retry with a different transaction id if desired. + throw new IllegalArgumentException("" + DUPLICATE_TRANSACTION); + } + throw new IllegalStateException("" + reason); + } + } + /** * Called to perform orderly close record streams. */ @@ -793,7 +861,7 @@ public void setInitialStateHash(@NonNull final Hash stateHash) { /*================================================================================================================== * - * Workflows for use by embedded Hedera + * Exposed for use by embedded Hedera * =================================================================================================================*/ public IngestWorkflow ingestWorkflow() { @@ -812,6 +880,10 @@ public BlockStreamManager blockStreamManager() { return daggerApp.blockStreamManager(); } + public boolean isBlockStreamEnabled() { + return streamMode != RECORDS; + } + /*================================================================================================================== * * Genesis Initialization @@ -884,6 +956,7 @@ private void initializeDagger( .bootstrapConfigProviderImpl(bootstrapConfigProvider) .fileServiceImpl(fileServiceImpl) .contractServiceImpl(contractServiceImpl) + .tssBaseService(tssBaseService) .initTrigger(trigger) .softwareVersion(version.getPbjSemanticVersion()) .self(networkInfo.selfNodeInfo()) @@ -897,7 +970,6 @@ private void initializeDagger( .kvStateChangeListener(kvStateChangeListener) .boundaryStateChangeListener(boundaryStateChangeListener) .migrationStateChanges(migrationStateChanges) - .tssBaseService(tssBaseServiceSupplier.get()) .initialStateHash(initialStateHash) .networkInfo(networkInfo) .build(); @@ -918,9 +990,6 @@ private void initializeDagger( .orElseGet(() -> startBlockHashFrom(state)); }); daggerApp.tssBaseService().registerLedgerSignatureConsumer(daggerApp.blockStreamManager()); - if (daggerApp.tssBaseService() instanceof PlaceholderTssBaseService placeholderTssBaseService) { - daggerApp.inject(placeholderTssBaseService); - } } } @@ -976,14 +1045,8 @@ private Bytes startBlockHashFrom(@NonNull final State state) { // store from the pending output tree to recompute its final root hash final var penultimateOutputTreeStatus = new StreamingTreeHasher.Status( blockStreamInfo.numPrecedingOutputItems(), blockStreamInfo.rightmostPrecedingOutputTreeHashes()); - return rootHashFrom(penultimateOutputTreeStatus, BlockItem.PROTOBUF.toBytes(lastStateChanges)); - } - - private boolean isBlockStreamEnabled() { - return bootstrapConfigProvider - .getConfiguration() - .getConfigData(BlockStreamConfig.class) - .streamBlocks(); + final var lastLeafHash = noThrowSha384HashOf(BlockItem.PROTOBUF.toBytes(lastStateChanges)); + return rootHashFrom(penultimateOutputTreeStatus, lastLeafHash); } private static ServicesSoftwareVersion getNodeStartupVersion(@NonNull final Configuration config) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java index 81b260eebbce..bff0023adaea 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/HederaInjectionComponent.java @@ -49,13 +49,14 @@ import com.hedera.node.app.throttle.ThrottleServiceManager; import com.hedera.node.app.throttle.ThrottleServiceModule; import com.hedera.node.app.tss.TssBaseService; -import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; import com.hedera.node.app.workflows.FacilityInitModule; import com.hedera.node.app.workflows.WorkflowsInjectionModule; import com.hedera.node.app.workflows.handle.HandleWorkflow; import com.hedera.node.app.workflows.ingest.IngestWorkflow; +import com.hedera.node.app.workflows.ingest.SubmissionManager; import com.hedera.node.app.workflows.prehandle.PreHandleWorkflow; import com.hedera.node.app.workflows.query.QueryWorkflow; +import com.hedera.node.app.workflows.query.annotations.UserQueries; import com.swirlds.common.crypto.Cryptography; import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.listeners.ReconnectCompleteListener; @@ -117,6 +118,7 @@ public interface HederaInjectionComponent { IngestWorkflow ingestWorkflow(); + @UserQueries QueryWorkflow queryWorkflow(); BlockRecordManager blockRecordManager(); @@ -137,7 +139,7 @@ public interface HederaInjectionComponent { TssBaseService tssBaseService(); - void inject(PlaceholderTssBaseService placeholderTssBaseService); + SubmissionManager submissionManager(); @Component.Builder interface Builder { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java index 5baf9d0000b1..51c3a84bbc2a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/ServicesMain.java @@ -38,7 +38,7 @@ import com.hedera.node.app.services.OrderedServiceMigrator; import com.hedera.node.app.services.ServicesRegistryImpl; -import com.hedera.node.app.tss.impl.PlaceholderTssBaseService; +import com.hedera.node.app.tss.TssBaseServiceImpl; import com.swirlds.base.time.Time; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.RuntimeConstructable; @@ -75,6 +75,7 @@ import java.time.InstantSource; import java.util.List; import java.util.Set; +import java.util.concurrent.ForkJoinPool; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -246,6 +247,12 @@ public static void main(final String... args) throws Exception { .withPlatformContext(platformContext) .withConfiguration(configuration) .withAddressBook(addressBook) + // C.f. https://github.com/hashgraph/hedera-services/issues/14751, + // we need to choose the correct roster in the following cases: + // - At genesis, a roster loaded from disk + // - At restart, the active roster in the saved state + // - At upgrade boundary, the candidate roster in the saved state IF + // that state satisfies conditions (e.g. the roster has been keyed) .withRoster(roster) .withKeysAndCerts(keysAndCerts); @@ -363,6 +370,6 @@ private static Hedera newHedera() { ServicesRegistryImpl::new, new OrderedServiceMigrator(), InstantSource.system(), - PlaceholderTssBaseService::new); + appContext -> new TssBaseServiceImpl(appContext, ForkJoinPool.commonPool(), ForkJoinPool.commonPool())); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/AuthorizerImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/AuthorizerImpl.java index 1408c68ed8f2..811943949cdc 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/AuthorizerImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/AuthorizerImpl.java @@ -16,6 +16,7 @@ package com.hedera.node.app.authorization; +import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTHORIZATION_FAILED; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static java.util.Objects.requireNonNull; @@ -52,7 +53,6 @@ public AuthorizerImpl( this.privilegedTransactionChecker = requireNonNull(privilegedTransactionChecker); } - /** {@inheritDoc} */ @Override public boolean isAuthorized(@NonNull final AccountID id, @NonNull final HederaFunctionality function) { Objects.requireNonNull(id); @@ -85,13 +85,11 @@ public SystemPrivilege hasPrivilegedAuthorization( private ResponseCodeEnum permissibilityOf( @NonNull final AccountID givenPayer, @NonNull final HederaFunctionality function) { if (isSuperUser(givenPayer)) { - return ResponseCodeEnum.OK; + return OK; } - if (!givenPayer.hasAccountNum()) { - return ResponseCodeEnum.AUTHORIZATION_FAILED; + return AUTHORIZATION_FAILED; } - final long num = givenPayer.accountNumOrThrow(); final var permissionConfig = configProvider.getConfiguration().getConfigData(ApiPermissionConfig.class); final var permission = permissionConfig.getPermission(function); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java index 97972864aefd..7485e679d6f6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/authorization/PrivilegesVerifier.java @@ -91,7 +91,7 @@ public SystemPrivilege hasPrivileges( txBody.fileDeleteOrThrow().fileIDOrThrow().fileNum()); case CRYPTO_DELETE -> checkEntityDelete( txBody.cryptoDeleteOrThrow().deleteAccountIDOrThrow().accountNumOrThrow()); - case NODE_CREATE, NODE_DELETE -> checkNodeChange(payerId); + case NODE_CREATE, NODE_UPDATE, NODE_DELETE -> checkNodeChange(payerId); default -> SystemPrivilege.UNNECESSARY; }; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemWriter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemWriter.java index ff5985eeae87..dd4546ccec4d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemWriter.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemWriter.java @@ -16,6 +16,9 @@ package com.hedera.node.app.blocks; +import static java.util.Objects.requireNonNull; + +import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; @@ -33,9 +36,26 @@ public interface BlockItemWriter { /** * Writes a serialized item to the destination stream. * - * @param serializedItem the serialized item to write + * @param bytes the serialized item to write + */ + default BlockItemWriter writePbjItem(@NonNull final Bytes bytes) { + requireNonNull(bytes); + return writeItem(bytes.toByteArray()); + } + + /** + * Writes a serialized item to the destination stream. + * + * @param bytes the serialized item to write + */ + BlockItemWriter writeItem(@NonNull byte[] bytes); + + /** + * Writes a pre-serialized sequence of items to the destination stream. + * + * @param data the serialized item to write */ - BlockItemWriter writeItem(@NonNull Bytes serializedItem); + BlockItemWriter writeItems(@NonNull BufferedData data); /** * Closes the block. diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java new file mode 100644 index 000000000000..6d6fc387d928 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.node.contract.ContractFunctionResult; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.blocks.impl.TranslationContext; +import com.hedera.node.app.blocks.impl.contexts.AirdropOpContext; +import com.hedera.node.app.blocks.impl.contexts.ContractOpContext; +import com.hedera.node.app.blocks.impl.contexts.CryptoOpContext; +import com.hedera.node.app.blocks.impl.contexts.FileOpContext; +import com.hedera.node.app.blocks.impl.contexts.MintOpContext; +import com.hedera.node.app.blocks.impl.contexts.NodeOpContext; +import com.hedera.node.app.blocks.impl.contexts.ScheduleOpContext; +import com.hedera.node.app.blocks.impl.contexts.SubmitOpContext; +import com.hedera.node.app.blocks.impl.contexts.SupplyChangeOpContext; +import com.hedera.node.app.blocks.impl.contexts.TokenOpContext; +import com.hedera.node.app.blocks.impl.contexts.TopicOpContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Translates a {@link TransactionResult} and, optionally, one or more {@link TransactionOutput}s within a given + * {@link TranslationContext} into a {@link TransactionRecord} or {@link TransactionReceipt} appropriate for returning + * from a query. + */ +public class BlockItemsTranslator { + private static final Function CONTRACT_CALL_EXTRACTOR = + output -> output.contractCallOrThrow().contractCallResultOrThrow(); + private static final Function CONTRACT_CREATE_EXTRACTOR = + output -> output.contractCreateOrThrow().contractCreateResultOrThrow(); + + public static final BlockItemsTranslator BLOCK_ITEMS_TRANSLATOR = new BlockItemsTranslator(); + + /** + * Translate the given {@link TransactionResult} and optional {@link TransactionOutput}s into a + * {@link TransactionReceipt} appropriate for returning from a query. + * @param context the context of the transaction + * @param result the result of the transaction + * @param outputs the outputs of the transaction + * @return the translated receipt + */ + public TransactionReceipt translateReceipt( + @NonNull final TranslationContext context, + @NonNull final TransactionResult result, + @NonNull final TransactionOutput... outputs) { + requireNonNull(context); + requireNonNull(result); + requireNonNull(outputs); + final var receiptBuilder = + TransactionReceipt.newBuilder().status(result.status()).exchangeRate(result.exchangeRate()); + final var function = context.functionality(); + switch (function) { + case CONTRACT_CALL, + CONTRACT_CREATE, + CONTRACT_DELETE, + CONTRACT_UPDATE, + ETHEREUM_TRANSACTION -> receiptBuilder.contractID(((ContractOpContext) context).contractId()); + case CRYPTO_CREATE, CRYPTO_UPDATE -> receiptBuilder.accountID(((CryptoOpContext) context).accountId()); + case FILE_CREATE -> receiptBuilder.fileID(((FileOpContext) context).fileId()); + case NODE_CREATE -> receiptBuilder.nodeId(((NodeOpContext) context).nodeId()); + case SCHEDULE_CREATE -> { + final var scheduleOutput = outputValueIfPresent( + TransactionOutput::hasCreateSchedule, TransactionOutput::createScheduleOrThrow, outputs); + if (scheduleOutput != null) { + receiptBuilder + .scheduleID(scheduleOutput.scheduleId()) + .scheduledTransactionID(scheduleOutput.scheduledTransactionId()); + } + } + case SCHEDULE_DELETE -> receiptBuilder.scheduleID(((ScheduleOpContext) context).scheduleId()); + case SCHEDULE_SIGN -> { + final var signOutput = outputValueIfPresent( + TransactionOutput::hasSignSchedule, TransactionOutput::signScheduleOrThrow, outputs); + if (signOutput != null) { + receiptBuilder.scheduledTransactionID(signOutput.scheduledTransactionId()); + } + } + case CONSENSUS_SUBMIT_MESSAGE -> receiptBuilder + .topicRunningHashVersion(((SubmitOpContext) context).runningHashVersion()) + .topicSequenceNumber(((SubmitOpContext) context).sequenceNumber()) + .topicRunningHash(((SubmitOpContext) context).runningHash()); + case TOKEN_MINT -> receiptBuilder + .newTotalSupply(((MintOpContext) context).newTotalSupply()) + .serialNumbers(((MintOpContext) context).serialNumbers()); + case TOKEN_BURN, TOKEN_ACCOUNT_WIPE -> receiptBuilder.newTotalSupply( + ((SupplyChangeOpContext) context).newTotalSupply()); + case TOKEN_CREATE -> receiptBuilder.tokenID(((TokenOpContext) context).tokenId()); + case CONSENSUS_CREATE_TOPIC -> receiptBuilder.topicID(((TopicOpContext) context).topicId()); + } + return receiptBuilder.build(); + } + + /** + * Translate the given {@link TransactionResult} and optional {@link TransactionOutput}s into a + * {@link TransactionRecord} appropriate for returning from a query. + * @param context the context of the transaction + * @param result the result of the transaction + * @param outputs the outputs of the transaction + * @return the translated record + */ + public TransactionRecord translateRecord( + @NonNull final TranslationContext context, + @NonNull final TransactionResult result, + @NonNull final TransactionOutput... outputs) { + requireNonNull(context); + requireNonNull(result); + requireNonNull(outputs); + final var recordBuilder = TransactionRecord.newBuilder() + .transactionID(context.txnId()) + .memo(context.memo()) + .transactionHash(context.transactionHash()) + .consensusTimestamp(result.consensusTimestamp()) + .parentConsensusTimestamp(result.parentConsensusTimestamp()) + .scheduleRef(result.scheduleRef()) + .transactionFee(result.transactionFeeCharged()) + .transferList(result.transferList()) + .tokenTransferLists(result.tokenTransferLists()) + .automaticTokenAssociations(result.automaticTokenAssociations()) + .paidStakingRewards(result.paidStakingRewards()); + final var function = context.functionality(); + switch (function) { + case CONTRACT_CALL, CONTRACT_CREATE, CONTRACT_DELETE, CONTRACT_UPDATE, ETHEREUM_TRANSACTION -> { + if (function == CONTRACT_CALL) { + recordBuilder.contractCallResult( + outputValueIfPresent(TransactionOutput::hasContractCall, CONTRACT_CALL_EXTRACTOR, outputs)); + } else if (function == CONTRACT_CREATE) { + recordBuilder.contractCreateResult(outputValueIfPresent( + TransactionOutput::hasContractCreate, CONTRACT_CREATE_EXTRACTOR, outputs)); + } else if (function == ETHEREUM_TRANSACTION) { + final var ethOutput = outputValueIfPresent( + TransactionOutput::hasEthereumCall, TransactionOutput::ethereumCallOrThrow, outputs); + if (ethOutput != null) { + recordBuilder.ethereumHash(ethOutput.ethereumHash()); + switch (ethOutput.ethResult().kind()) { + case ETHEREUM_CALL_RESULT -> recordBuilder.contractCallResult( + ethOutput.ethereumCallResultOrThrow()); + case ETHEREUM_CREATE_RESULT -> recordBuilder.contractCreateResult( + ethOutput.ethereumCreateResultOrThrow()); + } + } + } + } + default -> { + final var synthResult = + outputValueIfPresent(TransactionOutput::hasContractCall, CONTRACT_CALL_EXTRACTOR, outputs); + if (synthResult != null) { + recordBuilder.contractCallResult(synthResult); + } + switch (function) { + case CRYPTO_TRANSFER -> { + final var cryptoOutput = outputValueIfPresent( + TransactionOutput::hasCryptoTransfer, + TransactionOutput::cryptoTransferOrThrow, + outputs); + if (cryptoOutput != null) { + recordBuilder.assessedCustomFees(cryptoOutput.assessedCustomFees()); + } + } + case CRYPTO_CREATE, CRYPTO_UPDATE -> recordBuilder.evmAddress( + ((CryptoOpContext) context).evmAddress()); + case TOKEN_AIRDROP -> recordBuilder.newPendingAirdrops( + ((AirdropOpContext) context).pendingAirdropRecords()); + case UTIL_PRNG -> { + final var prngOutput = outputValueIfPresent( + TransactionOutput::hasUtilPrng, TransactionOutput::utilPrngOrThrow, outputs); + if (prngOutput != null) { + switch (prngOutput.entropy().kind()) { + case PRNG_BYTES -> recordBuilder.prngBytes(prngOutput.prngBytesOrThrow()); + case PRNG_NUMBER -> recordBuilder.prngNumber(prngOutput.prngNumberOrThrow()); + } + } + } + } + } + } + return recordBuilder.receipt(translateReceipt(context, result, outputs)).build(); + } + + private static @Nullable T outputValueIfPresent( + @NonNull final Predicate filter, + @NonNull final Function extractor, + @NonNull final TransactionOutput... outputs) { + for (final var output : outputs) { + if (filter.test(output)) { + return extractor.apply(output); + } + } + return null; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java index 1d1fff66e7c8..08c754d1e8be 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockStreamManager.java @@ -23,6 +23,7 @@ import com.swirlds.platform.system.state.notifications.StateHashedListener; import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; import java.util.function.BiConsumer; /** @@ -39,6 +40,24 @@ public interface BlockStreamManager extends BlockRecordInfo, BiConsumer, StateHashedListener { Bytes ZERO_BLOCK_HASH = Bytes.wrap(new byte[48]); + /** + * The types of work that may be identified as pending within a block. + */ + enum PendingWork { + /** + * No work is pending. + */ + NONE, + /** + * Genesis work is pending. + */ + GENESIS_WORK, + /** + * Post-upgrade work is pending. + */ + POST_UPGRADE_WORK + } + /** * Initializes the block stream manager after a restart or during reconnect with the hash of the last block * incorporated in the state used in the restart or reconnect. (At genesis, this hash should be the @@ -49,12 +68,39 @@ public interface BlockStreamManager extends BlockRecordInfo, BiConsumer TOKEN_ASSOCIATION_COMPARATOR = Comparator.comparingLong(a -> a.tokenIdOrThrow().tokenNum()) .thenComparingLong(a -> a.accountIdOrThrow().accountNumOrThrow()); + private static final Comparator PENDING_AIRDROP_RECORD_COMPARATOR = + Comparator.comparing(PendingAirdropRecord::pendingAirdropIdOrThrow, PENDING_AIRDROP_ID_COMPARATOR); - // base transaction data + // --- Fields representing the input transaction --- + /** + * The transaction "owning" the stream items we are building. + */ private Transaction transaction; - + /** + * If set, the serialized form of the transaction; if not set, it will be serialized from the transaction. + * (We already have this pre-serialized when the transaction came from an event.) + */ @Nullable private Bytes serializedTransaction; - // fields needed for TransactionRecord - // Mutable because the provisional consensus timestamp assigned on dispatch could - // change when removable records appear "between" this record and the parent record + // --- Fields used to build the TranslationContext --- + /** + * The functionality of the transaction, set explicitly to avoid parsing the transaction again. + */ + @Nullable + private HederaFunctionality functionality; + /** + * The memo from the transaction, set explicitly to avoid parsing the transaction again. + */ + private String memo; + /** + * The id of the transaction, set explicitly to avoid parsing the transaction again. + */ + private TransactionID transactionId; + /** + * The serial numbers minted by the transaction. + */ + private List serialNumbers = new LinkedList<>(); + /** + * The new total supply of a token affected by the transaction. + */ + private long newTotalSupply = 0L; + /** + * The id of a node created by the transaction. + */ + private long nodeId; + /** + * The id of a file created by the transaction. + */ + private FileID fileId; + /** + * The id of a topic created by the transaction. + */ + private TopicID topicId; + /** + * The id of a token created by the transaction. + */ + private TokenID tokenId; + /** + * The id of an account created or updated by the transaction. + */ + private AccountID accountId; + /** + * The id of a contract called, created, updated, or deleted by the transaction. + */ + private ContractID contractId; + /** + * The sequence number of a topic receiving a message in the transaction. + */ + private long sequenceNumber = 0L; + /** + * The running hash of a topic receiving a message in the transaction. + */ + private Bytes runningHash = Bytes.EMPTY; + /** + * The version of the running hash of a topic receiving a message in the transaction. + */ + private long runningHashVersion = 0L; + /** + * Any new pending airdrops created by the transaction. + */ + private List pendingAirdropRecords = emptyList(); + /** + * The EVM address of an account created by the transaction. + */ + private Bytes evmAddress = Bytes.EMPTY; + + // --- Fields used to build the TransactionResult --- + /** + * The builder for the transaction result. + */ + private final TransactionResult.Builder transactionResultBuilder = TransactionResult.newBuilder(); + /** + * The final status of handling the transaction. + */ + private ResponseCodeEnum status = OK; + /** + * The consensus time of the transaction. + */ private Instant consensusNow; - private TransactionID transactionID; + /** + * The HBAR transfers resulting from the transaction. + */ + private TransferList transferList = TransferList.DEFAULT; + /** + * The token transfer lists resulting from the transaction. + */ private List tokenTransferLists = new LinkedList<>(); + /** + * Whether the transaction assessed custom fees. + */ private boolean hasAssessedCustomFees = false; + /** + * The assessed custom fees resulting from the transaction. + */ private List assessedCustomFees = new LinkedList<>(); - + /** + * The staking rewards paid as a result of the transaction. + */ private List paidStakingRewards = new LinkedList<>(); - private TransferList transferList = TransferList.DEFAULT; - - // fields needed for TransactionReceipt - private ResponseCodeEnum status = ResponseCodeEnum.OK; - private List serialNumbers = new LinkedList<>(); - private long newTotalSupply = 0L; - // A set of ids that should be explicitly considered as in a "reward situation", - // despite the canonical definition of a reward situation; needed for mono-service - // fidelity only - @Nullable - private Set explicitRewardReceiverIds; - // While the fee is sent to the underlying builder all the time, it is also cached here because, as of today, - // there is no way to get the transaction fee from the PBJ object. - private long transactionFee; - // If non-null, a contract function result - private ContractFunctionResult contractFunctionResult; - // If non-null, the output of a UTIL_PRNG - private BlockItem utilPrngOutputItem; + /** + * The automatic token associations resulting from the transaction. + */ + private final List automaticTokenAssociations = new LinkedList<>(); + // --- Fields used to build the TransactionOutput(s) --- + /** + * Enumerates the types of contract operations that may have a result. + */ private enum ContractOpType { + /** + * A contract creation operation. + */ CREATE, + /** + * A contract call operation. + */ CALL, - ETH_TBD, + /** + * An Ethereum transaction that was throttled by gas. + */ + ETH_THROTTLED, + /** + * An Ethereum transaction that created a contract. + */ ETH_CREATE, + /** + * An Ethereum transaction that called a contract. + */ ETH_CALL, } - // The type of contract operation that was performed + /** + * The type of contract operation that was performed. + */ private ContractOpType contractOpType = null; - private Bytes ethereumHash = Bytes.EMPTY; - - private final List automaticTokenAssociations = new LinkedList<>(); - private final TransactionResult.Builder transactionResultBuilder = TransactionResult.newBuilder(); - // Sidecar data, booleans are the migration flag + /** + * The result of a contract function call or creation. + */ + private ContractFunctionResult contractFunctionResult; + /** + * The contract state changes resulting from the transaction. + */ private final List> contractStateChanges = new LinkedList<>(); + /** + * The contract actions resulting from the transaction. + */ private final List> contractActions = new LinkedList<>(); + /** + * The contract bytecodes resulting from the transaction. + */ private final List> contractBytecodes = new LinkedList<>(); - // Fields that are not in TransactionRecord, but are needed for computing staking rewards - // These are not persisted to the record file - private final Map deletedAccountBeneficiaries = new HashMap<>(); - - @Nullable - private HederaFunctionality functionality; - - // Used for some child records builders. - private final ReversingBehavior reversingBehavior; + /** + * The hash of the Ethereum payload if relevant to the transaction. + */ + private Bytes ethereumHash = Bytes.EMPTY; + /** + * Whether the transaction creates or deletes a schedule. + */ + private boolean createsOrDeletesSchedule; + /** + * The id of a scheduled transaction created or signed by the transaction. + */ + private TransactionID scheduledTransactionId; + /** + * The prebuild item for a UTIL_PRNG output. + */ + private BlockItem utilPrngOutputItem; - // Category of the record - private final HandleContext.TransactionCategory category; + // --- Fields used to either build the TranslationContext or a TransactionOutput --- + /** + * The id of a schedule created or deleted by the transaction. + */ + private ScheduleID scheduleId; + // --- Fields used to build the StateChanges items --- + /** + * The state changes resulting from the transaction. + */ private final List stateChanges = new ArrayList<>(); - // Used to customize the externalized form of a dispatched child transaction, right before - // its record stream item is built; lets the contract service externalize certain dispatched - // CryptoCreate transactions as ContractCreate synthetic transactions - private final ExternalizedRecordCustomizer customizer; + // --- Fields used to communicate between handler logic and the HandleWorkflow --- + /** + * The ids of accounts that should be considered as in a "reward situation" despite the canonical + * definition; needed for backward compatibility. + */ + @Nullable + private Set explicitRewardReceiverIds; + /** + * The beneficiaries of accounts deleted by the transaction. + */ + private final Map deletedAccountBeneficiaries = new HashMap<>(); + /** + * A getter for the transaction fee set on the TransactionResult builder. + */ + private long transactionFee; - private TokenID tokenID; - private ScheduleID scheduleID; - private boolean createsOrDeletesSchedule; - private TransactionID scheduledTransactionId; + // --- Fields used to guide the HandleWorkflow in finalizing this builder's items in the stream --- + /** + * The category of the transaction. + */ + private final HandleContext.TransactionCategory category; + /** + * For a child transaction, how its results should be reversed during rollback. + */ + private final ReversingBehavior reversingBehavior; + /** + * How the transaction should be customized before externalization to the stream. + */ + private final ExternalizedRecordCustomizer customizer; + /** + * Constructs a builder for a user transaction with the given characteristics. + * @param reversingBehavior the reversing behavior + * @param customizer the customizer + * @param category the category + */ public BlockStreamBuilder( @NonNull final ReversingBehavior reversingBehavior, @NonNull final ExternalizedRecordCustomizer customizer, @@ -231,48 +404,140 @@ public BlockStreamBuilder( this.category = requireNonNull(category); } - @Override - public StreamBuilder stateChanges(@NonNull List stateChanges) { - this.stateChanges.addAll(stateChanges); - return this; - } + /** + * Encapsulates the output associated to a single logical {@link Transaction}, whether user or synthetic, as + * well as the logic to translate it into a {@link TransactionRecord} or {@link TransactionReceipt} given the + * {@link BlockItemsTranslator} to use. + * @param blockItems the list of block items + * @param translationContext the translation context + */ + public record Output(@NonNull List blockItems, @NonNull TranslationContext translationContext) { + public Output { + requireNonNull(blockItems); + requireNonNull(translationContext); + } - @Override - public BlockStreamBuilder functionality(@NonNull final HederaFunctionality functionality) { - this.functionality = requireNonNull(functionality); - return this; + /** + * Exposes each {@link BlockItem} in the output to the given action. + * @param action the action to apply + */ + public void forEachItem(@NonNull final Consumer action) { + requireNonNull(action); + blockItems.forEach(action); + } + + /** + * Translates the block items into a transaction record. + * @param translator the translator to use + * @return the transaction record + */ + public TransactionRecord toRecord(@NonNull final BlockItemsTranslator translator) { + requireNonNull(translator); + return toView(translator, View.RECORD); + } + + /** + * Translates the block items into a transaction receipt. + * @param translator the translator to use + * @return the transaction record + */ + public RecordSource.IdentifiedReceipt toIdentifiedReceipt(@NonNull final BlockItemsTranslator translator) { + requireNonNull(translator); + return toView(translator, View.RECEIPT); + } + + /** + * The view to translate to. + */ + private enum View { + RECEIPT, + RECORD + } + + /** + * Uses a given translator to translate the block items into a view of the requested type. The steps are, + *

    + *
  1. Find the {@link TransactionResult} in this builder's items.
  2. + *
  3. Find the {@link TransactionOutput} items, if any.
  4. + *
  5. Translate these items into a view of the requested type.
  6. + *
+ * @param translator the translator to use + * @param view the type of view to translate to + * @return the translated view + * @param the Java type of the view + */ + @SuppressWarnings("unchecked") + private T toView(@NonNull final BlockItemsTranslator translator, @NonNull final View view) { + int i = 0; + final var n = blockItems.size(); + TransactionResult result = null; + while (i < n && (result = blockItems.get(i++).transactionResult()) == null) { + // Skip over non-result items + } + requireNonNull(result); + if (i < n && blockItems.get(i).hasTransactionOutput()) { + int j = i; + while (j < n && blockItems.get(j).hasTransactionOutput()) { + j++; + } + final var outputs = new TransactionOutput[j - i]; + for (int k = i; k < j; k++) { + outputs[k - i] = blockItems.get(k).transactionOutput(); + } + return (T) + switch (view) { + case RECEIPT -> new RecordSource.IdentifiedReceipt( + translationContext.txnId(), + translator.translateReceipt(translationContext, result, outputs)); + case RECORD -> translator.translateRecord(translationContext, result, outputs); + }; + } else { + return (T) + switch (view) { + case RECEIPT -> new RecordSource.IdentifiedReceipt( + translationContext.txnId(), + translator.translateReceipt(translationContext, result)); + case RECORD -> translator.translateRecord(translationContext, result); + }; + } + } } /** - * Builds the list of block items. + * Builds the list of block items with their translation contexts. * * @return the list of block items */ - public List build() { + public Output build() { final var blockItems = new ArrayList(); - - final var transactionBlockItem = BlockItem.newBuilder() + blockItems.add(BlockItem.newBuilder() .eventTransaction(EventTransaction.newBuilder() .applicationTransaction(getSerializedTransaction()) .build()) - .build(); - blockItems.add(transactionBlockItem); - - final var resultBlockItem = getTransactionResultBlockItem(); - blockItems.add(resultBlockItem); - - addOutputItems(blockItems); - + .build()); + blockItems.add(transactionResultBlockItem()); + addOutputItemsTo(blockItems); if (!stateChanges.isEmpty()) { - final var stateChangesBlockItem = BlockItem.newBuilder() + blockItems.add(BlockItem.newBuilder() .stateChanges(StateChanges.newBuilder() .consensusTimestamp(asTimestamp(consensusNow)) .stateChanges(stateChanges) .build()) - .build(); - blockItems.add(stateChangesBlockItem); + .build()); } - return blockItems; + return new Output(blockItems, translationContext()); + } + + @Override + public StreamBuilder stateChanges(@NonNull List stateChanges) { + this.stateChanges.addAll(stateChanges); + return this; + } + + @Override + public BlockStreamBuilder functionality(@NonNull final HederaFunctionality functionality) { + this.functionality = requireNonNull(functionality); + return this; } @Override @@ -286,9 +551,6 @@ public int getNumAutoAssociations() { return automaticTokenAssociations.size(); } - // ------------------------------------------------------------------------------------------------------------------------ - // base transaction data - @Override @NonNull public BlockStreamBuilder parentConsensus(@NonNull final Instant parentConsensus) { @@ -313,7 +575,7 @@ public BlockStreamBuilder consensusTimestamp(@NonNull final Instant now) { @Override @NonNull public BlockStreamBuilder transaction(@NonNull final Transaction transaction) { - this.transaction = requireNonNull(transaction, "transaction must not be null"); + this.transaction = requireNonNull(transaction); return this; } @@ -332,36 +594,31 @@ public BlockStreamBuilder transactionBytes(@NonNull final Bytes transactionBytes @Override @NonNull public TransactionID transactionID() { - return transactionID; + return transactionId; } @Override @NonNull - public BlockStreamBuilder transactionID(@NonNull final TransactionID transactionID) { - this.transactionID = requireNonNull(transactionID, "transactionID must not be null"); + public BlockStreamBuilder transactionID(@NonNull final TransactionID transactionId) { + this.transactionId = requireNonNull(transactionId); return this; } @NonNull @Override public BlockStreamBuilder syncBodyIdFromRecordId() { - final var newTransactionID = transactionID; - final var body = - inProgressBody().copyBuilder().transactionID(newTransactionID).build(); - this.transaction = StreamBuilder.transactionWith(body); + this.transaction = StreamBuilder.transactionWith( + inProgressBody().copyBuilder().transactionID(transactionId).build()); return this; } @Override @NonNull public BlockStreamBuilder memo(@NonNull final String memo) { - // No-op + this.memo = requireNonNull(memo); return this; } - // ------------------------------------------------------------------------------------------------------------------------ - // fields needed for TransactionRecord - @Override @NonNull public Transaction transaction() { @@ -399,7 +656,7 @@ public Set explicitRewardSituationIds() { public BlockStreamBuilder contractCallResult(@Nullable final ContractFunctionResult contractCallResult) { this.contractFunctionResult = contractCallResult; if (contractCallResult != null) { - if (contractOpType != ContractOpType.ETH_TBD) { + if (contractOpType != ContractOpType.ETH_THROTTLED) { contractOpType = ContractOpType.CALL; } else { contractOpType = ContractOpType.ETH_CALL; @@ -413,7 +670,7 @@ public BlockStreamBuilder contractCallResult(@Nullable final ContractFunctionRes public BlockStreamBuilder contractCreateResult(@Nullable ContractFunctionResult contractCreateResult) { this.contractFunctionResult = contractCreateResult; if (contractCreateResult != null) { - if (contractOpType != ContractOpType.ETH_TBD) { + if (contractOpType != ContractOpType.ETH_THROTTLED) { contractOpType = ContractOpType.CREATE; } else { contractOpType = ContractOpType.ETH_CREATE; @@ -438,8 +695,7 @@ public BlockStreamBuilder transferList(@Nullable final TransferList transferList @Override @NonNull public BlockStreamBuilder tokenTransferLists(@NonNull final List tokenTransferLists) { - requireNonNull(tokenTransferLists, "tokenTransferLists must not be null"); - this.tokenTransferLists = tokenTransferLists; + this.tokenTransferLists = requireNonNull(tokenTransferLists); transactionResultBuilder.tokenTransferLists(tokenTransferLists); return this; } @@ -458,6 +714,10 @@ public BlockStreamBuilder tokenType(final @NonNull TokenType tokenType) { @Override public BlockStreamBuilder addPendingAirdrop(@NonNull final PendingAirdropRecord pendingAirdropRecord) { requireNonNull(pendingAirdropRecord); + if (pendingAirdropRecords.isEmpty()) { + pendingAirdropRecords = new LinkedList<>(); + } + pendingAirdropRecords.add(pendingAirdropRecord); return this; } @@ -487,7 +747,7 @@ public BlockStreamBuilder addAutomaticTokenAssociation(@NonNull final TokenAssoc @Override @NonNull public BlockStreamBuilder ethereumHash(@NonNull final Bytes ethereumHash) { - contractOpType = ContractOpType.ETH_TBD; + contractOpType = ContractOpType.ETH_THROTTLED; this.ethereumHash = requireNonNull(ethereumHash); return this; } @@ -495,8 +755,7 @@ public BlockStreamBuilder ethereumHash(@NonNull final Bytes ethereumHash) { @Override @NonNull public BlockStreamBuilder paidStakingRewards(@NonNull final List paidStakingRewards) { - // These need not be externalized to block streams - requireNonNull(paidStakingRewards, "paidStakingRewards must not be null"); + requireNonNull(paidStakingRewards); this.paidStakingRewards = paidStakingRewards; transactionResultBuilder.paidStakingRewards(paidStakingRewards); return this; @@ -522,7 +781,7 @@ public BlockStreamBuilder entropyBytes(@NonNull final Bytes prngBytes) { @Override @NonNull public BlockStreamBuilder evmAddress(@NonNull final Bytes evmAddress) { - // No-op + this.evmAddress = requireNonNull(evmAddress); return this; } @@ -532,13 +791,10 @@ public List getAssessedCustomFees() { return assessedCustomFees; } - // ------------------------------------------------------------------------------------------------------------------------ - // fields needed for TransactionReceipt - @Override @NonNull public BlockStreamBuilder status(@NonNull final ResponseCodeEnum status) { - this.status = requireNonNull(status, "status must not be null"); + this.status = requireNonNull(status); transactionResultBuilder.status(status); return this; } @@ -561,22 +817,23 @@ public long getGasUsedForContractTxn() { @Override @NonNull - public BlockStreamBuilder accountID(@NonNull final AccountID accountID) { - // No-op + public BlockStreamBuilder accountID(@Nullable final AccountID accountID) { + this.accountId = accountID; return this; } @Override @NonNull public BlockStreamBuilder fileID(@NonNull final FileID fileID) { - // No-op + this.fileId = fileID; return this; } @Override @NonNull public BlockStreamBuilder contractID(@Nullable final ContractID contractID) { - // No-op + this.contractId = contractID; + this.accountId = null; return this; } @@ -589,58 +846,55 @@ public BlockStreamBuilder exchangeRate(@Nullable final ExchangeRateSet exchangeR @NonNull @Override - public BlockStreamBuilder congestionMultiplier(long congestionMultiplier) { - if (congestionMultiplier != 0) { - transactionResultBuilder.congestionPricingMultiplier(congestionMultiplier); - } + public BlockStreamBuilder congestionMultiplier(final long congestionMultiplier) { + transactionResultBuilder.congestionPricingMultiplier(congestionMultiplier); return this; } @Override @NonNull public BlockStreamBuilder topicID(@NonNull final TopicID topicID) { - // No-op + this.topicId = requireNonNull(topicID); return this; } @Override @NonNull public BlockStreamBuilder topicSequenceNumber(final long topicSequenceNumber) { - // No-op + this.sequenceNumber = topicSequenceNumber; return this; } @Override @NonNull public BlockStreamBuilder topicRunningHash(@NonNull final Bytes topicRunningHash) { - // No-op + this.runningHash = requireNonNull(topicRunningHash); return this; } @Override @NonNull public BlockStreamBuilder topicRunningHashVersion(final long topicRunningHashVersion) { - // TOD0: Need to confirm what the value should be + this.runningHashVersion = topicRunningHashVersion; return this; } @Override @NonNull - public BlockStreamBuilder tokenID(@NonNull final TokenID tokenID) { - requireNonNull(tokenID, "tokenID must not be null"); - this.tokenID = tokenID; + public BlockStreamBuilder tokenID(@NonNull final TokenID tokenId) { + this.tokenId = requireNonNull(tokenId); return this; } @Override public TokenID tokenID() { - return tokenID; + return tokenId; } @Override @NonNull - public BlockStreamBuilder nodeID(long nodeId) { - // No-op + public BlockStreamBuilder nodeID(final long nodeId) { + this.nodeId = nodeId; return this; } @@ -659,7 +913,7 @@ public long getNewTotalSupply() { @NonNull public BlockStreamBuilder scheduleID(@NonNull final ScheduleID scheduleID) { this.createsOrDeletesSchedule = true; - this.scheduleID = requireNonNull(scheduleID); + this.scheduleId = requireNonNull(scheduleID); return this; } @@ -684,9 +938,6 @@ public List serialNumbers() { return serialNumbers; } - // ------------------------------------------------------------------------------------------------------------------------ - // Sidecar data, booleans are the migration flag - @Override @NonNull public BlockStreamBuilder addContractStateChanges( @@ -714,8 +965,6 @@ public BlockStreamBuilder addContractBytecode( return this; } - // ------------- Information needed by token service for redirecting staking rewards to appropriate accounts - @Override public void addBeneficiaryForDeletedAccount( @NonNull final AccountID deletedAccountID, @NonNull final AccountID beneficiaryForDeletedAccount) { @@ -762,7 +1011,12 @@ public HandleContext.TransactionCategory category() { @Override public void nullOutSideEffectFields() { serialNumbers.clear(); - tokenTransferLists.clear(); + if (!tokenTransferLists.isEmpty()) { + tokenTransferLists.clear(); + } + if (!pendingAirdropRecords.isEmpty()) { + pendingAirdropRecords.clear(); + } automaticTokenAssociations.clear(); transferList = TransferList.DEFAULT; paidStakingRewards.clear(); @@ -770,12 +1024,28 @@ public void nullOutSideEffectFields() { newTotalSupply = 0L; transactionFee = 0L; + + accountId = null; + contractId = null; + fileId = null; + tokenId = null; + topicId = null; + nodeId = 0L; + if (status != IDENTICAL_SCHEDULE_ALREADY_CREATED) { + scheduleId = null; + scheduledTransactionId = null; + } + transactionResultBuilder.scheduleRef((ScheduleID) null); - transactionResultBuilder.automaticTokenAssociations(emptyList()); + evmAddress = Bytes.EMPTY; + ethereumHash = Bytes.EMPTY; + runningHash = Bytes.EMPTY; + sequenceNumber = 0L; + runningHashVersion = 0L; } @NonNull - private BlockItem getTransactionResultBlockItem() { + private BlockItem transactionResultBlockItem() { if (!automaticTokenAssociations.isEmpty()) { automaticTokenAssociations.sort(TOKEN_ASSOCIATION_COMPARATOR); transactionResultBuilder.automaticTokenAssociations(automaticTokenAssociations); @@ -796,7 +1066,7 @@ private TransactionBody inProgressBody() { } } - private void addOutputItems(@NonNull final List items) { + private void addOutputItemsTo(@NonNull final List items) { if (utilPrngOutputItem != null) { items.add(utilPrngOutputItem); } @@ -822,8 +1092,7 @@ private void addOutputItems(@NonNull final List items) { .ethereumHash(ethereumHash) .sidecars(sidecars) .build()); - // CONSENSUS_GAS_EXHAUSTED if there is no contract function result - case ETH_TBD -> builder.ethereumCall(EthereumOutput.newBuilder() + case ETH_THROTTLED -> builder.ethereumCall(EthereumOutput.newBuilder() .ethereumHash(ethereumHash) .sidecars(sidecars) .build()); @@ -833,7 +1102,7 @@ private void addOutputItems(@NonNull final List items) { if (createsOrDeletesSchedule && scheduledTransactionId != null) { items.add(itemWith(TransactionOutput.newBuilder() .createSchedule(CreateScheduleOutput.newBuilder() - .scheduleId(scheduleID) + .scheduleId(scheduleId) .scheduledTransactionId(scheduledTransactionId) .build()))); } else if (scheduledTransactionId != null) { @@ -889,4 +1158,40 @@ private Bytes getSerializedTransaction() { private BlockItem itemWith(@NonNull final TransactionOutput.Builder output) { return BlockItem.newBuilder().transactionOutput(output).build(); } + + /** + * Returns the {@link TranslationContext} that will be needed to easily translate this builder's items into + * a {@link TransactionRecord} or {@link TransactionReceipt} if needed to answer a query. + * @return the translation context + */ + private TranslationContext translationContext() { + return switch (requireNonNull(functionality)) { + case CONTRACT_CALL, + CONTRACT_CREATE, + CONTRACT_DELETE, + CONTRACT_UPDATE, + ETHEREUM_TRANSACTION -> new ContractOpContext( + memo, transactionId, transaction, functionality, contractId); + case CRYPTO_CREATE, CRYPTO_UPDATE -> new CryptoOpContext( + memo, transactionId, transaction, functionality, accountId, evmAddress); + case FILE_CREATE -> new FileOpContext(memo, transactionId, transaction, functionality, fileId); + case NODE_CREATE -> new NodeOpContext(memo, transactionId, transaction, functionality, nodeId); + case SCHEDULE_DELETE -> new ScheduleOpContext(memo, transactionId, transaction, functionality, scheduleId); + case CONSENSUS_SUBMIT_MESSAGE -> new SubmitOpContext( + memo, transactionId, transaction, functionality, runningHash, runningHashVersion, sequenceNumber); + case TOKEN_AIRDROP -> { + if (!pendingAirdropRecords.isEmpty()) { + pendingAirdropRecords.sort(PENDING_AIRDROP_RECORD_COMPARATOR); + } + yield new AirdropOpContext(memo, transactionId, transaction, functionality, pendingAirdropRecords); + } + case TOKEN_MINT -> new MintOpContext( + memo, transactionId, transaction, functionality, serialNumbers, newTotalSupply); + case TOKEN_BURN, TOKEN_ACCOUNT_WIPE -> new SupplyChangeOpContext( + memo, transactionId, transaction, functionality, newTotalSupply); + case TOKEN_CREATE -> new TokenOpContext(memo, transactionId, transaction, functionality, tokenId); + case CONSENSUS_CREATE_TOPIC -> new TopicOpContext(memo, transactionId, transaction, functionality, topicId); + default -> new BaseOpContext(memo, transactionId, transaction, functionality); + }; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java index 05a3b1e78190..4e2b11259858 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImpl.java @@ -16,23 +16,32 @@ package com.hedera.node.app.blocks.impl; +import static com.hedera.hapi.block.stream.BlockItem.ItemOneOfType.TRANSACTION_RESULT; import static com.hedera.hapi.node.base.BlockHashAlgorithm.SHA2_384; import static com.hedera.hapi.util.HapiUtils.asInstant; +import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.hedera.node.app.blocks.BlockStreamManager.PendingWork.GENESIS_WORK; +import static com.hedera.node.app.blocks.BlockStreamManager.PendingWork.NONE; +import static com.hedera.node.app.blocks.BlockStreamManager.PendingWork.POST_UPGRADE_WORK; import static com.hedera.node.app.blocks.impl.BlockImplUtils.appendHash; import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; -import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; -import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.hapi.utils.CommonUtils.sha384DigestOrThrow; +import static com.hedera.node.app.records.BlockRecordService.EPOCH; import static com.hedera.node.app.records.impl.BlockRecordInfoUtils.HASH_SIZE; +import static com.hedera.pbj.runtime.ProtoConstants.WIRE_TYPE_DELIMITED; +import static com.hedera.pbj.runtime.ProtoWriterTools.writeTag; import static com.swirlds.platform.state.SwirldStateManagerUtils.isInFreezePeriod; import static java.util.Objects.requireNonNull; import static java.util.concurrent.CompletableFuture.completedFuture; -import static java.util.concurrent.CompletableFuture.supplyAsync; +import com.google.common.annotations.VisibleForTesting; import com.hedera.hapi.block.stream.BlockItem; import com.hedera.hapi.block.stream.BlockProof; import com.hedera.hapi.block.stream.MerkleSiblingHash; import com.hedera.hapi.block.stream.output.BlockHeader; import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.block.stream.schema.BlockSchema; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; @@ -42,12 +51,14 @@ import com.hedera.node.app.blocks.BlockStreamService; import com.hedera.node.app.blocks.InitialStateHash; import com.hedera.node.app.blocks.StreamingTreeHasher; +import com.hedera.node.app.hapi.utils.CommonUtils; import com.hedera.node.app.records.impl.BlockRecordInfoUtils; import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockRecordStreamConfig; import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.node.config.data.VersionConfig; +import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.platform.state.service.PlatformStateService; @@ -58,8 +69,14 @@ import com.swirlds.state.spi.CommittableWritableStates; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.security.DigestException; +import java.security.MessageDigest; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Queue; @@ -77,10 +94,11 @@ public class BlockStreamManagerImpl implements BlockStreamManager { private static final Logger log = LogManager.getLogger(BlockStreamManagerImpl.class); - private static final int CHUNK_SIZE = 8; - private final int roundsPerBlock; + private final int hashCombineBatchSize; + private final int serializationBatchSize; private final TssBaseService tssBaseService; + private final SemanticVersion version; private final SemanticVersion hapiVersion; private final ExecutorService executor; private final Supplier writerSupplier; @@ -89,6 +107,10 @@ public class BlockStreamManagerImpl implements BlockStreamManager { private final BlockHashManager blockHashManager; private final RunningHashManager runningHashManager; + // The status of pending work + private PendingWork pendingWork = NONE; + // The last time at which interval-based processing was done + private Instant lastIntervalProcessTime = Instant.EPOCH; // All this state is scoped to producing the current block private long blockNumber; // Set to the round number of the last round handled before entering a freeze period @@ -140,7 +162,9 @@ public BlockStreamManagerImpl( @NonNull final ConfigProvider configProvider, @NonNull final TssBaseService tssBaseService, @NonNull final BoundaryStateChangeListener boundaryStateChangeListener, - @NonNull final InitialStateHash initialStateHash) { + @NonNull final InitialStateHash initialStateHash, + @NonNull final SemanticVersion version) { + this.version = requireNonNull(version); this.writerSupplier = requireNonNull(writerSupplier); this.executor = requireNonNull(executor); this.tssBaseService = requireNonNull(tssBaseService); @@ -148,7 +172,10 @@ public BlockStreamManagerImpl( requireNonNull(configProvider); final var config = configProvider.getConfiguration(); this.hapiVersion = hapiVersionFrom(config); - this.roundsPerBlock = config.getConfigData(BlockStreamConfig.class).roundsPerBlock(); + final var blockStreamConfig = config.getConfigData(BlockStreamConfig.class); + this.roundsPerBlock = blockStreamConfig.roundsPerBlock(); + this.hashCombineBatchSize = blockStreamConfig.hashCombineBatchSize(); + this.serializationBatchSize = blockStreamConfig.serializationBatchSize(); this.blockHashManager = new BlockHashManager(config); this.runningHashManager = new RunningHashManager(); this.lastNonEmptyRoundNumber = initialStateHash.roundNum(); @@ -187,11 +214,13 @@ public void startRound(@NonNull final Round round, @NonNull final State state) { boundaryStateChangeListener.setBoundaryTimestamp(blockTimestamp); final var blockStreamInfo = blockStreamInfoFrom(state); + pendingWork = classifyPendingWork(blockStreamInfo, version); + lastIntervalProcessTime = asInstant(blockStreamInfo.lastIntervalProcessTimeOrElse(EPOCH)); blockHashManager.startBlock(blockStreamInfo, lastBlockHash); runningHashManager.startBlock(blockStreamInfo); - inputTreeHasher = new ConcurrentStreamingTreeHasher(executor); - outputTreeHasher = new ConcurrentStreamingTreeHasher(executor); + inputTreeHasher = new ConcurrentStreamingTreeHasher(executor, hashCombineBatchSize); + outputTreeHasher = new ConcurrentStreamingTreeHasher(executor, hashCombineBatchSize); blockNumber = blockStreamInfo.blockNumber() + 1; pendingItems = new ArrayList<>(); @@ -208,6 +237,30 @@ public void startRound(@NonNull final Round round, @NonNull final State state) { } } + @Override + public void confirmPendingWorkFinished() { + if (pendingWork == NONE) { + // Should never happen but throwing IllegalStateException might make the situation even worse, so just log + log.error("HandleWorkflow confirmed finished work but none was pending"); + } + pendingWork = NONE; + } + + @Override + public @NonNull PendingWork pendingWork() { + return pendingWork; + } + + @Override + public @NonNull Instant lastIntervalProcessTime() { + return lastIntervalProcessTime; + } + + @Override + public void setLastIntervalProcessTime(@NonNull final Instant lastIntervalProcessTime) { + this.lastIntervalProcessTime = requireNonNull(lastIntervalProcessTime); + } + @Override public void endRound(@NonNull final State state, final long roundNum) { if (shouldCloseBlock(roundNum, roundsPerBlock)) { @@ -237,13 +290,18 @@ public void endRound(@NonNull final State state, final long roundNum) { blockStartStateHash, outputTreeStatus.numLeaves(), outputTreeStatus.rightmostHashes(), - boundaryStateChangeListener.boundaryTimestampOrThrow())); + boundaryStateChangeListener.boundaryTimestampOrThrow(), + pendingWork != POST_UPGRADE_WORK, + version, + asTimestamp(lastIntervalProcessTime))); ((CommittableWritableStates) writableState).commit(); - // Flush the block stream info change - pendingItems.add(boundaryStateChangeListener.flushChanges()); - schedulePendingWork(); + // Serialize and hash the final block item + final var finalWork = new ScheduledWork(List.of(boundaryStateChangeListener.flushChanges())); + final var finalOutput = finalWork.computeOutput(); + // Ensure we only write and incorporate the final hash after all preceding work is done writeFuture.join(); + combineOutput(null, finalOutput); final var outputHash = outputTreeHasher.rootHash().join(); final var leftParent = combine(lastBlockHash, inputHash); final var rightParent = combine(outputHash, blockStartStateHash); @@ -262,7 +320,6 @@ public void endRound(@NonNull final State state, final long roundNum) { // Update in-memory state to prepare for the next block lastBlockHash = blockHash; writer = null; - tssBaseService.requestLedgerSignature(blockHash.toByteArray()); } } @@ -270,7 +327,7 @@ public void endRound(@NonNull final State state, final long roundNum) { @Override public void writeItem(@NonNull final BlockItem item) { pendingItems.add(item); - if (pendingItems.size() == CHUNK_SIZE) { + if (pendingItems.size() == serializationBatchSize) { schedulePendingWork(); } } @@ -281,8 +338,8 @@ public void writeItem(@NonNull final BlockItem item) { // no two consecutive transactions ever get the same seed schedulePendingWork(); writeFuture.join(); - final var seed = runningHashManager.nMinus3HashFuture.join(); - return seed == null ? null : Bytes.wrap(seed); + final var seed = runningHashManager.nMinus3Hash; + return seed == null ? null : Bytes.wrap(runningHashManager.nMinus3Hash); } @Override @@ -336,20 +393,46 @@ public synchronized void accept(@NonNull final byte[] message, @NonNull final by final var proof = block.proofBuilder() .blockSignature(blockSignature) .siblingHashes(siblingHashes.stream().flatMap(List::stream).toList()); - block.writer() - .writeItem(BlockItem.PROTOBUF.toBytes( - BlockItem.newBuilder().blockProof(proof).build())) - .closeBlock(); + final var proofItem = BlockItem.newBuilder().blockProof(proof).build(); + block.writer().writePbjItem(BlockItem.PROTOBUF.toBytes(proofItem)).closeBlock(); if (block.number() != blockNumber) { siblingHashes.removeFirst(); } } } + /** + * Classifies the type of work pending, if any, given the block stream info from state and the current + * software version. + * + * @param blockStreamInfo the block stream info + * @param version the version + * @return the type of pending work given the block stream info and version + */ + @VisibleForTesting + static PendingWork classifyPendingWork( + @NonNull final BlockStreamInfo blockStreamInfo, @NonNull final SemanticVersion version) { + requireNonNull(version); + requireNonNull(blockStreamInfo); + if (EPOCH.equals(blockStreamInfo.lastIntervalProcessTimeOrElse(EPOCH))) { + // If we have never processed any time-based events, we must be at genesis + return GENESIS_WORK; + } else if (impliesPostUpgradeWorkPending(blockStreamInfo, version)) { + return POST_UPGRADE_WORK; + } else { + return NONE; + } + } + + private static boolean impliesPostUpgradeWorkPending( + @NonNull final BlockStreamInfo blockStreamInfo, @NonNull final SemanticVersion version) { + return !version.equals(blockStreamInfo.creationSoftwareVersion()) || !blockStreamInfo.postUpgradeWorkDone(); + } + private void schedulePendingWork() { final var scheduledWork = new ScheduledWork(pendingItems); - final var pendingSerialization = CompletableFuture.supplyAsync(scheduledWork::serializeItems, executor); - writeFuture = writeFuture.thenCombine(pendingSerialization, scheduledWork::combineSerializedItems); + final var pendingOutput = CompletableFuture.supplyAsync(scheduledWork::computeOutput, executor); + writeFuture = writeFuture.thenCombine(pendingOutput, this::combineOutput); pendingItems = new ArrayList<>(); } @@ -384,71 +467,133 @@ private boolean isFreezeRound(@NonNull final PlatformState platformState, @NonNu * * */ - private class ScheduledWork { - private final List scheduledWork; + private static class ScheduledWork { + private final List items; - public ScheduledWork(@NonNull final List scheduledWork) { - this.scheduledWork = requireNonNull(scheduledWork); - } + public record Output( + @NonNull BufferedData data, + @NonNull ByteBuffer inputHashes, + @NonNull ByteBuffer outputHashes, + @NonNull ByteBuffer resultHashes) {} - /** - * Serializes the scheduled work items to bytes using the {@link BlockItem#PROTOBUF} codec. - * - * @return the serialized items - */ - public List serializeItems() { - final List serializedItems = new ArrayList<>(scheduledWork.size()); - for (final var item : scheduledWork) { - serializedItems.add(BlockItem.PROTOBUF.toBytes(item)); - } - return serializedItems; + public ScheduledWork(@NonNull final List items) { + this.items = requireNonNull(items); } /** - * Given the serialized items, schedules the hashes of the input/output items and running hash - * for the {@link TransactionResult}s to be incorporated in the input/output trees and running hash - * respectively; and writes the serialized bytes to the {@link BlockItemWriter}. + * Serializes the scheduled work items to bytes using the {@link BlockItem#PROTOBUF} codec and + * computes the associated input/output hashes, returning the serialized items and hashes bundled + * into an {@link Output}. * - * @param ignore ignored, needed for type compatibility with {@link CompletableFuture#thenCombine} - * @param serializedItems the serialized items to be processed - * @return {@code null} + * @return the output of doing the scheduled work */ - public Void combineSerializedItems(@Nullable Void ignore, @NonNull final List serializedItems) { - for (int i = 0, n = scheduledWork.size(); i < n; i++) { - final var item = scheduledWork.get(i); - final var serializedItem = serializedItems.get(i); + public Output computeOutput() { + var size = 0; + var numInputs = 0; + var numOutputs = 0; + var numResults = 0; + final var n = items.size(); + final var sizes = new int[n]; + for (var i = 0; i < n; i++) { + final var item = items.get(i); + sizes[i] = BlockItem.PROTOBUF.measureRecord(item); + // Plus (at most) 8 bytes for the preceding tag and length + size += (sizes[i] + 8); final var kind = item.item().kind(); switch (kind) { - case EVENT_HEADER, EVENT_TRANSACTION -> inputTreeHasher.addLeaf(serializedItem); - case TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> outputTreeHasher.addLeaf( - serializedItem); - default -> { - // Other items are not part of the input/output trees + case EVENT_HEADER, EVENT_TRANSACTION -> numInputs++; + case TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> { + numOutputs++; + if (kind == TRANSACTION_RESULT) { + numResults++; + } } } - if (kind == BlockItem.ItemOneOfType.TRANSACTION_RESULT) { - runningHashManager.nextResult(serializedItem); + } + final var inputHashes = new byte[numInputs * HASH_SIZE]; + final var outputHashes = new byte[numOutputs * HASH_SIZE]; + final var resultHashes = ByteBuffer.allocate(numResults * HASH_SIZE); + final var serializedItems = ByteBuffer.allocate(size); + final var data = BufferedData.wrap(serializedItems); + final var digest = sha384DigestOrThrow(); + var j = 0; + var k = 0; + for (var i = 0; i < n; i++) { + final var item = items.get(i); + writeTag(data, BlockSchema.ITEMS, WIRE_TYPE_DELIMITED); + data.writeVarInt(sizes[i], false); + final var pre = serializedItems.position(); + writeItemToBuffer(item, data); + final var post = serializedItems.position(); + final var kind = item.item().kind(); + switch (kind) { + case EVENT_HEADER, EVENT_TRANSACTION, TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> { + digest.update(serializedItems.array(), pre, post - pre); + switch (kind) { + case EVENT_HEADER, EVENT_TRANSACTION -> finish(digest, inputHashes, j++ * HASH_SIZE); + case TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> finish( + digest, outputHashes, k++ * HASH_SIZE); + } + if (kind == TRANSACTION_RESULT) { + resultHashes.put(Arrays.copyOfRange(outputHashes, (k - 1) * HASH_SIZE, k * HASH_SIZE)); + } + } + default -> { + // Other items have no special processing to do + } } - writer.writeItem(serializedItem); } - return null; + data.flip(); + return new Output(data, ByteBuffer.wrap(inputHashes), ByteBuffer.wrap(outputHashes), resultHashes.flip()); + } + + private void finish(@NonNull final MessageDigest digest, final byte[] hashes, final int offset) { + try { + digest.digest(hashes, offset, HASH_SIZE); + } catch (DigestException e) { + throw new IllegalArgumentException(e); + } } } + /** + * Given the output of a {@link ScheduledWork} instance, writes the output's serialized items and + * incorporates its input/output hashes into the corresponding trees and running hash. + * + * @param ignore ignored, needed for type compatibility with {@link CompletableFuture#thenCombine} + * @param output the output to be combined + * @return {@code null} + */ + private Void combineOutput(@Nullable Void ignore, @NonNull final ScheduledWork.Output output) { + writer.writeItems(output.data()); + while (output.inputHashes().hasRemaining()) { + inputTreeHasher.addLeaf(output.inputHashes()); + } + while (output.outputHashes().hasRemaining()) { + outputTreeHasher.addLeaf(output.outputHashes()); + } + while (output.resultHashes().hasRemaining()) { + runningHashManager.nextResultHash(output.resultHashes()); + } + return null; + } + private SemanticVersion hapiVersionFrom(@NonNull final Configuration config) { return config.getConfigData(VersionConfig.class).hapiVersion(); } - private class RunningHashManager { - CompletableFuture nMinus3HashFuture; - CompletableFuture nMinus2HashFuture; - CompletableFuture nMinus1HashFuture; - CompletableFuture hashFuture; + private static class RunningHashManager { + private static final ThreadLocal HASHES = ThreadLocal.withInitial(() -> new byte[HASH_SIZE]); + private static final ThreadLocal DIGESTS = + ThreadLocal.withInitial(CommonUtils::sha384DigestOrThrow); + + byte[] nMinus3Hash; + byte[] nMinus2Hash; + byte[] nMinus1Hash; + byte[] hash; Bytes latestHashes() { - final var all = new byte[][] { - nMinus3HashFuture.join(), nMinus2HashFuture.join(), nMinus1HashFuture.join(), hashFuture.join() - }; + final var all = new byte[][] {nMinus3Hash, nMinus2Hash, nMinus1Hash, hash}; int numMissing = 0; while (numMissing < all.length && all[numMissing] == null) { numMissing++; @@ -468,27 +613,28 @@ Bytes latestHashes() { void startBlock(@NonNull final BlockStreamInfo blockStreamInfo) { final var hashes = blockStreamInfo.trailingOutputHashes(); final var n = (int) (hashes.length() / HASH_SIZE); - nMinus3HashFuture = completedFuture(n < 4 ? null : hashes.toByteArray(0, HASH_SIZE)); - nMinus2HashFuture = completedFuture(n < 3 ? null : hashes.toByteArray((n - 3) * HASH_SIZE, HASH_SIZE)); - nMinus1HashFuture = completedFuture(n < 2 ? null : hashes.toByteArray((n - 2) * HASH_SIZE, HASH_SIZE)); - hashFuture = - completedFuture(n < 1 ? new byte[HASH_SIZE] : hashes.toByteArray((n - 1) * HASH_SIZE, HASH_SIZE)); + nMinus3Hash = n < 4 ? null : hashes.toByteArray(0, HASH_SIZE); + nMinus2Hash = n < 3 ? null : hashes.toByteArray((n - 3) * HASH_SIZE, HASH_SIZE); + nMinus1Hash = n < 2 ? null : hashes.toByteArray((n - 2) * HASH_SIZE, HASH_SIZE); + hash = n < 1 ? new byte[HASH_SIZE] : hashes.toByteArray((n - 1) * HASH_SIZE, HASH_SIZE); } /** * Updates the running hashes for the given serialized output block item. * - * @param bytes the serialized output block item + * @param hash the serialized output block item */ - void nextResult(@NonNull final Bytes bytes) { - requireNonNull(bytes); - nMinus3HashFuture = nMinus2HashFuture; - nMinus2HashFuture = nMinus1HashFuture; - nMinus1HashFuture = hashFuture; - hashFuture = hashFuture.thenCombineAsync( - supplyAsync(() -> noThrowSha384HashOf(bytes.toByteArray()), executor), - BlockImplUtils::combine, - executor); + void nextResultHash(@NonNull final ByteBuffer hash) { + requireNonNull(hash); + nMinus3Hash = nMinus2Hash; + nMinus2Hash = nMinus1Hash; + nMinus1Hash = this.hash; + final var digest = DIGESTS.get(); + digest.update(this.hash); + final var resultHash = HASHES.get(); + hash.get(resultHash); + digest.update(resultHash); + this.hash = digest.digest(); } } @@ -543,4 +689,12 @@ public void notify(@NonNull final StateHashedNotification notification) { .get(notification.round()) .complete(notification.hash().getBytes()); } + + private static void writeItemToBuffer(@NonNull final BlockItem item, @NonNull final BufferedData bufferedData) { + try { + BlockItem.PROTOBUF.write(item, bufferedData); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasher.java index 150dbcaeff7a..357834003e12 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasher.java @@ -20,8 +20,11 @@ import static java.util.Objects.requireNonNull; import com.hedera.node.app.blocks.StreamingTreeHasher; +import com.hedera.node.app.hapi.utils.CommonUtils; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -32,69 +35,76 @@ * using a concurrent algorithm that hashes leaves in parallel and combines the resulting hashes in parallel. *

* Important: This class is not thread-safe, and client code must not make concurrent calls to - * {@link #addLeaf(Bytes)} or {@link #rootHash()}. + * {@link StreamingTreeHasher#addLeaf(ByteBuffer)} or {@link #rootHash()}. */ public class ConcurrentStreamingTreeHasher implements StreamingTreeHasher { /** - * The number of leaves to hash in parallel before combining the resulting hashes. + * The default number of leaves to batch before combining the resulting hashes. */ - private static final int HASHING_CHUNK_SIZE = 16; + private static final int DEFAULT_HASH_COMBINE_BATCH_SIZE = 8; /** - * The base {@link HashCombiner} that combines the hashes of the leaves of the tree, at depth zero. + * The base {@link HashCombiner} that combines the hashes of the leaves of the tree, at height zero. */ private final HashCombiner combiner = new HashCombiner(0); /** * The {@link ExecutorService} used to parallelize the hashing and combining of the leaves of the tree. */ private final ExecutorService executorService; + /** + * The size of the batches of hashes to schedule for combination. + *

Important: This must be an even number so we can safely assume that any odd number + * of scheduled hashes to combine can be padded with appropriately nested combination of hashes + * whose descendants are all empty leaves. + */ + private final int hashCombineBatchSize; /** * The number of leaves added to the tree. */ private int numLeaves; /** - * Set once before the root hash is requested, to the depth of the tree implied by the number of leaves. + * Set once before the root hash is requested, to the height of the tree implied by the number of leaves. */ - private int maxDepth; + private int rootHeight; /** * Whether the tree has been finalized by requesting the root hash. */ private boolean rootHashRequested = false; - /** - * Leaves added but not yet scheduled to be hashed. - */ - private List pendingLeaves = new ArrayList<>(); - /** - * A future that completes after all leaves not in the pending list have been hashed and combined. - */ - private CompletableFuture hashed = CompletableFuture.completedFuture(null); public ConcurrentStreamingTreeHasher(@NonNull final ExecutorService executorService) { + this(executorService, DEFAULT_HASH_COMBINE_BATCH_SIZE); + } + + public ConcurrentStreamingTreeHasher( + @NonNull final ExecutorService executorService, final int hashCombineBatchSize) { this.executorService = requireNonNull(executorService); + if (hashCombineBatchSize % 2 == 1) { + throw new IllegalArgumentException("Hash combine batch size must be an even number"); + } + this.hashCombineBatchSize = hashCombineBatchSize; } @Override - public void addLeaf(@NonNull final Bytes leaf) { - requireNonNull(leaf); + public void addLeaf(@NonNull final ByteBuffer hash) { + requireNonNull(hash); if (rootHashRequested) { throw new IllegalStateException("Cannot add leaves after requesting the root hash"); } - numLeaves++; - pendingLeaves.add(leaf); - if (pendingLeaves.size() == HASHING_CHUNK_SIZE) { - schedulePendingWork(); + if (hash.remaining() < HASH_LENGTH) { + throw new IllegalArgumentException("Buffer has less than " + HASH_LENGTH + " bytes remaining"); } + numLeaves++; + final var bytes = new byte[HASH_LENGTH]; + hash.get(bytes); + combiner.combine(bytes); } @Override public CompletableFuture rootHash() { rootHashRequested = true; - if (!pendingLeaves.isEmpty()) { - schedulePendingWork(); - } - maxDepth = maxDepthFor(numLeaves); - return hashed.thenCompose(ignore -> combiner.finalCombination()); + rootHeight = rootHeightFor(numLeaves); + return combiner.finalCombination(); } @Override @@ -102,30 +112,26 @@ public Status status() { if (numLeaves == 0) { return Status.EMPTY; } else { - schedulePendingWork(); - final var n = numLeaves; - return hashed.thenApply(ignore -> { - final var rightmostHashes = new ArrayList(); - combiner.flushAvailable(rightmostHashes, maxDepthFor(n + 1)); - return new Status(n, rightmostHashes); - }) - .join(); + final var rightmostHashes = new ArrayList(); + combiner.flushAvailable(rightmostHashes, rootHeightFor(numLeaves + 1)); + return new Status(numLeaves, rightmostHashes); } } /** * Computes the root hash of a perfect binary Merkle tree of {@link Bytes} leaves (padded on the right with - * empty leaves to reach a power of two), given the penultimate status of the tree and the last leaf added to - * the tree. + * empty leaves to reach a power of two), given the penultimate status of the tree and the hash of the last + * leaf added to the tree. + * * @param penultimateStatus the penultimate status of the tree - * @param lastLeaf the last leaf added to the tree + * @param lastLeafHash the last leaf hash added to the tree * @return the root hash of the tree */ - public static Bytes rootHashFrom(@NonNull final Status penultimateStatus, @NonNull final Bytes lastLeaf) { - requireNonNull(lastLeaf); - var hash = noThrowSha384HashOf(lastLeaf.toByteArray()); - final var maxDepth = maxDepthFor(penultimateStatus.numLeaves() + 1); - for (int i = 0; i < maxDepth; i++) { + public static Bytes rootHashFrom(@NonNull final Status penultimateStatus, @NonNull final Bytes lastLeafHash) { + requireNonNull(lastLeafHash); + var hash = lastLeafHash.toByteArray(); + final var rootHeight = rootHeightFor(penultimateStatus.numLeaves() + 1); + for (int i = 0; i < rootHeight; i++) { final var rightmostHash = penultimateStatus.rightmostHashes().get(i); if (rightmostHash.length() == 0) { hash = BlockImplUtils.combine(hash, HashCombiner.EMPTY_HASHES[i]); @@ -136,32 +142,11 @@ public static Bytes rootHashFrom(@NonNull final Status penultimateStatus, @NonNu return Bytes.wrap(hash); } - private void schedulePendingWork() { - final var scheduledWork = pendingLeaves; - final var pendingHashes = CompletableFuture.supplyAsync( - () -> { - final List result = new ArrayList<>(); - for (final var leaf : scheduledWork) { - result.add(noThrowSha384HashOf(leaf.toByteArray())); - } - return result; - }, - executorService); - hashed = hashed.thenCombine(pendingHashes, (ignore, hashes) -> { - hashes.forEach(combiner::combine); - return null; - }); - pendingLeaves = new ArrayList<>(); - } - private class HashCombiner { + private static final ThreadLocal DIGESTS = + ThreadLocal.withInitial(CommonUtils::sha384DigestOrThrow); private static final int MAX_DEPTH = 24; - /** - * IMPORTANT - This must be an even number so we can safely assume that any odd number - * of scheduled hashes to combine can be padded with appropriately nested combination of hashes - * whose descendants are all empty leaves. - */ - private static final int COMBINATION_CHUNK_SIZE = 32; + private static final int MIN_TO_SCHEDULE = 16; private static final byte[][] EMPTY_HASHES = new byte[MAX_DEPTH][]; @@ -172,28 +157,28 @@ private class HashCombiner { } } - private final int depth; + private final int height; private HashCombiner delegate; private List pendingHashes = new ArrayList<>(); private CompletableFuture combination = CompletableFuture.completedFuture(null); - private HashCombiner(final int depth) { - if (depth >= MAX_DEPTH) { - throw new IllegalArgumentException("Cannot combine hashes at depth " + depth); + private HashCombiner(final int height) { + if (height >= MAX_DEPTH) { + throw new IllegalArgumentException("Cannot combine hashes at height " + height); } - this.depth = depth; + this.height = height; } public void combine(@NonNull final byte[] hash) { pendingHashes.add(hash); - if (pendingHashes.size() == COMBINATION_CHUNK_SIZE) { + if (pendingHashes.size() == hashCombineBatchSize) { schedulePendingWork(); } } public CompletableFuture finalCombination() { - if (depth == maxDepth) { + if (height == rootHeight) { final var rootHash = pendingHashes.isEmpty() ? EMPTY_HASHES[0] : pendingHashes.getFirst(); return CompletableFuture.completedFuture(Bytes.wrap(rootHash)); } else { @@ -204,8 +189,8 @@ public CompletableFuture finalCombination() { } } - public void flushAvailable(@NonNull final List rightmostHashes, final int stopDepth) { - if (depth < stopDepth) { + public void flushAvailable(@NonNull final List rightmostHashes, final int stopHeight) { + if (height < stopHeight) { final var newPendingHash = pendingHashes.size() % 2 == 0 ? null : pendingHashes.removeLast(); schedulePendingWork(); combination.join(); @@ -215,35 +200,43 @@ public void flushAvailable(@NonNull final List rightmostHashes, final int } else { rightmostHashes.add(Bytes.EMPTY); } - delegate.flushAvailable(rightmostHashes, stopDepth); + delegate.flushAvailable(rightmostHashes, stopHeight); } } private void schedulePendingWork() { if (delegate == null) { - delegate = new HashCombiner(depth + 1); + delegate = new HashCombiner(height + 1); + } + final CompletableFuture> pendingCombination; + if (pendingHashes.size() < MIN_TO_SCHEDULE) { + pendingCombination = CompletableFuture.completedFuture(combine(pendingHashes)); + } else { + final var hashes = pendingHashes; + pendingCombination = CompletableFuture.supplyAsync(() -> combine(hashes), executorService); } - final var scheduledWork = pendingHashes; - final var pendingCombination = CompletableFuture.supplyAsync( - () -> { - final List result = new ArrayList<>(); - for (int i = 0, m = scheduledWork.size(); i < m; i += 2) { - final var left = scheduledWork.get(i); - final var right = i + 1 < m ? scheduledWork.get(i + 1) : EMPTY_HASHES[depth]; - result.add(BlockImplUtils.combine(left, right)); - } - return result; - }, - executorService); combination = combination.thenCombine(pendingCombination, (ignore, combined) -> { combined.forEach(delegate::combine); return null; }); pendingHashes = new ArrayList<>(); } + + private List combine(@NonNull final List hashes) { + final List result = new ArrayList<>(); + final var digest = DIGESTS.get(); + for (int i = 0, m = hashes.size(); i < m; i += 2) { + final var left = hashes.get(i); + final var right = i + 1 < m ? hashes.get(i + 1) : EMPTY_HASHES[height]; + digest.update(left); + digest.update(right); + result.add(digest.digest()); + } + return result; + } } - private static int maxDepthFor(final int numLeaves) { + private static int rootHeightFor(final int numLeaves) { final var numPerfectLeaves = containingPowerOfTwo(numLeaves); return numPerfectLeaves == 0 ? 0 : Integer.numberOfTrailingZeros(numPerfectLeaves); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/FileBlockItemWriter.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/FileBlockItemWriter.java index 789af4d5e4fa..2ab1f6fb3f98 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/FileBlockItemWriter.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/FileBlockItemWriter.java @@ -25,7 +25,7 @@ import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.pbj.runtime.ProtoConstants; import com.hedera.pbj.runtime.ProtoWriterTools; -import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.hedera.pbj.runtime.io.buffer.BufferedData; import com.hedera.pbj.runtime.io.stream.WritableStreamingData; import com.swirlds.state.spi.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; @@ -111,9 +111,6 @@ public FileBlockItemWriter( } } - /** - * {@inheritDoc} - */ @Override public void openBlock(long blockNumber) { if (state == State.OPEN) throw new IllegalStateException("Cannot initialize a FileBlockItemWriter twice"); @@ -153,13 +150,9 @@ public void openBlock(long blockNumber) { state = State.OPEN; } - /** - * {@inheritDoc} - */ @Override - public FileBlockItemWriter writeItem(@NonNull Bytes serializedItem) { - requireNonNull(serializedItem, "The supplied argument 'serializedItem' cannot be null!"); - if (serializedItem.length() <= 0) throw new IllegalArgumentException("Item must be non-empty"); + public FileBlockItemWriter writeItem(@NonNull final byte[] bytes) { + requireNonNull(bytes); if (state != State.OPEN) { throw new IllegalStateException( "Cannot write to a FileBlockItemWriter that is not open for block: " + this.blockNumber); @@ -168,15 +161,23 @@ public FileBlockItemWriter writeItem(@NonNull Bytes serializedItem) { // Write the ITEMS tag. ProtoWriterTools.writeTag(writableStreamingData, BlockSchema.ITEMS, ProtoConstants.WIRE_TYPE_DELIMITED); // Write the length of the item. - writableStreamingData.writeVarInt((int) serializedItem.length(), false); + writableStreamingData.writeVarInt(bytes.length, false); // Write the item bytes themselves. - serializedItem.writeTo(writableStreamingData); + writableStreamingData.writeBytes(bytes); + return this; + } + + @Override + public BlockItemWriter writeItems(@NonNull final BufferedData data) { + requireNonNull(data); + if (state != State.OPEN) { + throw new IllegalStateException( + "Cannot write to a FileBlockItemWriter that is not open for block: " + this.blockNumber); + } + writableStreamingData.writeBytes(data); return this; } - /** - * {@inheritDoc} - */ @Override public void closeBlock() { if (state.ordinal() < State.OPEN.ordinal()) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/NaiveStreamingTreeHasher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/NaiveStreamingTreeHasher.java index bc4166b61ca1..8df0fb1b4a14 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/NaiveStreamingTreeHasher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/NaiveStreamingTreeHasher.java @@ -22,63 +22,75 @@ import com.hedera.node.app.blocks.StreamingTreeHasher; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.concurrent.CompletableFuture; +/** + * A naive implementation of {@link StreamingTreeHasher} that computes the root hash of a perfect binary Merkle tree of + * {@link ByteBuffer} leaves. Used to test the correctness of more efficient implementations. + */ public class NaiveStreamingTreeHasher implements StreamingTreeHasher { private static final byte[] EMPTY_HASH = noThrowSha384HashOf(new byte[0]); - private final List leaves = new ArrayList<>(); + private final List leafHashes = new ArrayList<>(); private boolean rootHashRequested = false; - public static Bytes hashNaively(@NonNull final List leaves) { + /** + * Computes the root hash of a perfect binary Merkle tree of {@link ByteBuffer} leaves using a naive algorithm. + * @param leafHashes the leaf hashes of the tree + * @return the root hash of the tree + */ + public static Bytes computeRootHash(@NonNull final List leafHashes) { final var hasher = new NaiveStreamingTreeHasher(); - for (final var item : leaves) { - hasher.addLeaf(item); + for (final var hash : leafHashes) { + hasher.addLeaf(ByteBuffer.wrap(hash)); } return hasher.rootHash().join(); } @Override - public void addLeaf(@NonNull final Bytes leaf) { + public void addLeaf(@NonNull final ByteBuffer hash) { if (rootHashRequested) { throw new IllegalStateException("Root hash already requested"); } - leaves.add(leaf); + if (hash.remaining() < HASH_LENGTH) { + throw new IllegalArgumentException("Buffer has less than " + HASH_LENGTH + " bytes remaining"); + } + final var bytes = new byte[HASH_LENGTH]; + hash.get(bytes); + leafHashes.add(bytes); } @Override public CompletableFuture rootHash() { rootHashRequested = true; - if (leaves.isEmpty()) { + if (leafHashes.isEmpty()) { return CompletableFuture.completedFuture(Bytes.wrap(EMPTY_HASH)); } - Queue leafHashes = new LinkedList<>(); - for (final var leaf : leaves) { - leafHashes.add(noThrowSha384HashOf(leaf.toByteArray())); - } - final int n = leafHashes.size(); + Queue hashes = new LinkedList<>(leafHashes); + final int n = hashes.size(); if ((n & (n - 1)) != 0) { final var paddedN = Integer.highestOneBit(n) << 1; - while (leafHashes.size() < paddedN) { - leafHashes.add(EMPTY_HASH); + while (hashes.size() < paddedN) { + hashes.add(EMPTY_HASH); } } - while (leafHashes.size() > 1) { + while (hashes.size() > 1) { final Queue newLeafHashes = new LinkedList<>(); - while (!leafHashes.isEmpty()) { - final byte[] left = leafHashes.poll(); - final byte[] right = leafHashes.poll(); + while (!hashes.isEmpty()) { + final byte[] left = hashes.poll(); + final byte[] right = hashes.poll(); final byte[] combined = new byte[left.length + requireNonNull(right).length]; System.arraycopy(left, 0, combined, 0, left.length); System.arraycopy(right, 0, combined, left.length, right.length); newLeafHashes.add(noThrowSha384HashOf(combined)); } - leafHashes = newLeafHashes; + hashes = newLeafHashes; } - return CompletableFuture.completedFuture(Bytes.wrap(requireNonNull(leafHashes.poll()))); + return CompletableFuture.completedFuture(Bytes.wrap(requireNonNull(hashes.poll()))); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/TranslationContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/TranslationContext.java new file mode 100644 index 000000000000..018d49d2db50 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/TranslationContext.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; + +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.pbj.runtime.io.buffer.Bytes; + +/** + * Base interface for objects that have extra context needed to easily translate a {@link TransactionResult} and, + * optionally, a {@link TransactionOutput} into a {@link TransactionRecord} to be returned from a query. + */ +public interface TranslationContext { + /** + * Returns the memo of the transaction. + * @return the memo + */ + String memo(); + + /** + * Returns the transaction ID of the transaction. + * @return the transaction ID + */ + TransactionID txnId(); + + /** + * Returns the transaction itself. + * @return the transaction + */ + Transaction transaction(); + + /** + * Returns the functionality of the transaction. + * @return the functionality + */ + HederaFunctionality functionality(); + + /** + * Returns the hash of the transaction. + * @return the hash + */ + default Bytes transactionHash() { + final Bytes transactionBytes; + final var txn = transaction(); + if (txn.signedTransactionBytes().length() > 0) { + transactionBytes = txn.signedTransactionBytes(); + } else { + transactionBytes = Transaction.PROTOBUF.toBytes(txn); + } + return Bytes.wrap(noThrowSha384HashOf(transactionBytes.toByteArray())); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/AirdropOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/AirdropOpContext.java new file mode 100644 index 000000000000..dbb3cabb4355 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/AirdropOpContext.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.PendingAirdropRecord; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * A {@link TranslationContext} implementation with the list of new pending airdrops. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param pendingAirdropRecords The list of new pending airdrops + */ +public record AirdropOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull List pendingAirdropRecords) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/BaseOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/BaseOpContext.java new file mode 100644 index 000000000000..0945fed1d67b --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/BaseOpContext.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A minimal {@link TranslationContext} implementation appropriate for most + * transaction types. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + */ +public record BaseOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ContractOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ContractOpContext.java new file mode 100644 index 000000000000..46efafbc9384 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ContractOpContext.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A {@link TranslationContext} implementation with the id of an involved contract. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param contractId The id of the involved contract + */ +public record ContractOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @Nullable ContractID contractId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/CryptoOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/CryptoOpContext.java new file mode 100644 index 000000000000..70a1595f63f9 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/CryptoOpContext.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A {@link TranslationContext} implementation with the id of an involved account. + * + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param accountId If set, the id of the involved account + * @param evmAddress If non-empty, the EVM address of the involved account + */ +public record CryptoOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @Nullable AccountID accountId, + @NonNull Bytes evmAddress) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/FileOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/FileOpContext.java new file mode 100644 index 000000000000..28cfa953d7d8 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/FileOpContext.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.FileID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A {@link TranslationContext} implementation with the id of an involved file. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param fileId The id of the involved file + */ +public record FileOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @Nullable FileID fileId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/MintOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/MintOpContext.java new file mode 100644 index 000000000000..8c195cd06afd --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/MintOpContext.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * A {@link TranslationContext} implementation with metadata for a token mint. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param serialNumbers The minted serial numbers, if the token is non-fungible + * @param newTotalSupply The new total supply of the token + */ +public record MintOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull List serialNumbers, + long newTotalSupply) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/NodeOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/NodeOpContext.java new file mode 100644 index 000000000000..3240ca18e5c2 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/NodeOpContext.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with the id of an involved node. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param nodeId The id of the involved node + */ +public record NodeOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + long nodeId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ScheduleOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ScheduleOpContext.java new file mode 100644 index 000000000000..e70903802c7d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/ScheduleOpContext.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with the id of an involved schedule. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param scheduleId The id of the involved schedule + */ +public record ScheduleOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull ScheduleID scheduleId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SubmitOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SubmitOpContext.java new file mode 100644 index 000000000000..adf8d1be9eab --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SubmitOpContext.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with the metadata of an HCS message submission. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param runningHash The new running hash of the HCS topic + * @param runningHashVersion The running hash version of the HCS topic + * @param sequenceNumber The new sequence number of the HCS message + */ +public record SubmitOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull Bytes runningHash, + long runningHashVersion, + long sequenceNumber) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SupplyChangeOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SupplyChangeOpContext.java new file mode 100644 index 000000000000..1753ffad1aaf --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/SupplyChangeOpContext.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with a new total supply for a token. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param newTotalSupply The new total supply for a token + */ +public record SupplyChangeOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + long newTotalSupply) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TokenOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TokenOpContext.java new file mode 100644 index 000000000000..af43550ba6bf --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TokenOpContext.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with the id of an involved token. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param tokenId The id of the involved token + */ +public record TokenOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull TokenID tokenId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TopicOpContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TopicOpContext.java new file mode 100644 index 000000000000..9a653a0c29da --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/TopicOpContext.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl.contexts; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.node.app.blocks.impl.TranslationContext; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@link TranslationContext} implementation with the id of an involved topic. + * @param memo The memo for the transaction + * @param txnId The transaction ID + * @param transaction The transaction + * @param functionality The functionality of the transaction + * @param topicId The id of the involved topic + */ +public record TopicOpContext( + @NonNull String memo, + @NonNull TransactionID txnId, + @NonNull Transaction transaction, + @NonNull HederaFunctionality functionality, + @NonNull TopicID topicId) + implements TranslationContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/package-info.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/package-info.java new file mode 100644 index 000000000000..5e0730dba284 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/contexts/package-info.java @@ -0,0 +1,9 @@ +/** + * This package includes {@link com.hedera.node.app.blocks.impl.contexts.BaseOpContext} and a few other specialized + * contexts to support translating the block items for a {@link com.hedera.hapi.node.base.TransactionID} into a "legacy" + * {@link com.hedera.hapi.node.transaction.TransactionRecord}. This is necessary because the record has information + * that can only be deduced from the block items if the translator repeats deserialization work and also + * maintains some extra context about the network state which may not be available at the time we want to translate the + * items. + */ +package com.hedera.node.app.blocks.impl.contexts; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java similarity index 92% rename from hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java rename to hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java index e4b632ff545d..9ae8ddf3e242 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchema.java @@ -54,23 +54,24 @@ *

  • The trailing 256 block hashes, used to implement the EVM {@code BLOCKHASH} opcode.
  • * */ -public class V0540BlockStreamSchema extends Schema { - public static final String BLOCK_STREAM_INFO_KEY = "BLOCK_STREAM_INFO"; +public class V0560BlockStreamSchema extends Schema { private static final String SHARED_BLOCK_RECORD_INFO = "SHARED_BLOCK_RECORD_INFO"; private static final String SHARED_RUNNING_HASHES = "SHARED_RUNNING_HASHES"; + public static final String BLOCK_STREAM_INFO_KEY = "BLOCK_STREAM_INFO"; + /** * The version of the schema. */ private static final SemanticVersion VERSION = - SemanticVersion.newBuilder().major(0).minor(54).patch(0).build(); + SemanticVersion.newBuilder().major(0).minor(56).patch(0).build(); private final Consumer migratedBlockHashConsumer; /** * Schema constructor. */ - public V0540BlockStreamSchema(@NonNull final Consumer migratedBlockHashConsumer) { + public V0560BlockStreamSchema(@NonNull final Consumer migratedBlockHashConsumer) { super(VERSION); this.migratedBlockHashConsumer = requireNonNull(migratedBlockHashConsumer); } @@ -81,13 +82,14 @@ public V0540BlockStreamSchema(@NonNull final Consumer migratedBlockHashCo } @Override - public void migrate(@NonNull final MigrationContext ctx) { + public void restart(@NonNull final MigrationContext ctx) { + requireNonNull(ctx); final var state = ctx.newStates().getSingleton(BLOCK_STREAM_INFO_KEY); if (ctx.previousVersion() == null) { state.put(BlockStreamInfo.DEFAULT); } else { final var blockStreamInfo = state.get(); - // This will be null if the previous version is before 0.54.0 + // This will be null if the previous version is before 0.56.0 if (blockStreamInfo == null) { final BlockInfo blockInfo = (BlockInfo) requireNonNull(ctx.sharedValues().get(SHARED_BLOCK_RECORD_INFO)); @@ -111,6 +113,9 @@ public void migrate(@NonNull final MigrationContext ctx) { .trailingBlockHashes(trailingBlockHashes) .trailingOutputHashes(appendedHashes(runningHashes)) .blockEndTime(blockInfo.consTimeOfLastHandledTxn()) + .postUpgradeWorkDone(false) + .creationSoftwareVersion(ctx.previousVersion()) + .lastIntervalProcessTime(blockInfo.consTimeOfLastHandledTxn()) .build()); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java index 80234d445c80..b121cc350436 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/config/ConfigProviderBase.java @@ -19,9 +19,11 @@ import static java.util.Objects.requireNonNull; import com.hedera.node.config.ConfigProvider; +import com.swirlds.base.utility.FileSystemUtils; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.PropertyFileConfigSource; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; @@ -73,6 +75,10 @@ protected void addFileSource( if (path.toFile().exists()) { if (!path.toFile().isDirectory()) { try { + if (!FileSystemUtils.waitForPathPresence(path)) { + throw new FileNotFoundException("File not found: " + path); + } + builder.withSource(new PropertyFileConfigSource(path, p)); } catch (IOException e) { throw new IllegalStateException("Can not create config source for property file", e); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/ChildFeeContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/ChildFeeContextImpl.java index dd4783b6148f..560132398f49 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/ChildFeeContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/ChildFeeContextImpl.java @@ -108,7 +108,7 @@ public FeeCalculatorFactory feeCalculatorFactory() { } @Override - public @Nullable Configuration configuration() { + public @NonNull Configuration configuration() { return context.configuration(); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeManager.java index 76f2b370573c..2adfb323d44e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/fees/FeeManager.java @@ -135,9 +135,11 @@ public ResponseCodeEnum update(@NonNull final Bytes bytes) { currentSchedule = FeeSchedule.DEFAULT; } - // Populate the map of HederaFunctionality -> FeeData for the current schedule - this.currentFeeDataMap = new HashMap<>(); - populateFeeDataMap(currentFeeDataMap, currentSchedule.transactionFeeSchedule()); + // Populate the map of HederaFunctionality -> FeeData for the current schedule, but avoid mutating + // the active one in-place as other threads may be using it for ingest/query fee calculations + final var newCurrentFeeDataMap = new HashMap(); + populateFeeDataMap(newCurrentFeeDataMap, currentSchedule.transactionFeeSchedule()); + this.currentFeeDataMap = newCurrentFeeDataMap; // Get the expiration time of the current schedule if (currentSchedule.hasExpiryTime()) { @@ -160,9 +162,11 @@ public ResponseCodeEnum update(@NonNull final Bytes bytes) { logger.warn("Unable to parse next fee schedule, will default to the current fee schedule."); nextFeeDataMap = new HashMap<>(currentFeeDataMap); } else { - // Populate the map of HederaFunctionality -> FeeData for the current schedule - this.nextFeeDataMap = new HashMap<>(); - populateFeeDataMap(nextFeeDataMap, nextSchedule.transactionFeeSchedule()); + // Populate the map of HederaFunctionality -> FeeData for the next schedule, but avoid mutating + // the active one in-place as other threads may be using it for ingest/query fee calculations + final var newNextFeeDataMap = new HashMap(); + populateFeeDataMap(newNextFeeDataMap, nextSchedule.transactionFeeSchedule()); + this.nextFeeDataMap = newNextFeeDataMap; } return SUCCESS; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/GrpcServerManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/GrpcServerManager.java index fd416e09e8e4..ea8af54c6110 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/GrpcServerManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/GrpcServerManager.java @@ -60,4 +60,12 @@ public interface GrpcServerManager { * @return the port of the listening tls server, or -1 if no server listening on that port */ int tlsPort(); + + /** + * Gets the port that the node operator gRPC server is listening on. + * + * @return the port of the listening server, or -1 if no server is listening on that port. Note that this value may + * be different from the port designation in configuration. + */ + int nodeOperatorPort(); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManager.java index dfdae706ff8b..9ba4395fd11c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManager.java @@ -20,16 +20,21 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.transaction.Query; import com.hedera.node.app.grpc.GrpcServerManager; import com.hedera.node.app.services.ServicesRegistry; import com.hedera.node.app.spi.RpcService; import com.hedera.node.app.workflows.ingest.IngestWorkflow; import com.hedera.node.app.workflows.query.QueryWorkflow; +import com.hedera.node.app.workflows.query.annotations.OperatorQueries; +import com.hedera.node.app.workflows.query.annotations.UserQueries; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.GrpcConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.NettyConfig; import com.hedera.node.config.types.Profile; +import com.hedera.pbj.runtime.RpcMethodDefinition; +import com.hedera.pbj.runtime.RpcServiceDefinition; import com.swirlds.metrics.api.Metrics; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -44,10 +49,15 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Singleton; import javax.net.ssl.SSLException; @@ -81,6 +91,12 @@ public final class NettyGrpcServerManager implements GrpcServerManager { * The set of {@link ServiceDescriptor}s for services that the gRPC server will expose */ private final Set services; + + /** + * The set of {@link ServiceDescriptor}s for services that the node operator gRPC server will expose + */ + private Set nodeOperatorServices = Collections.emptySet(); + /** * The configuration provider, so we can figure out ports and other information. */ @@ -94,13 +110,19 @@ public final class NettyGrpcServerManager implements GrpcServerManager { */ private Server tlsServer; + /** + * The node operator gRPC server listening on localhost port + */ + private Server nodeOperatorServer; + /** * Create a new instance. * * @param configProvider The config provider, so we can figure out ports and other information. * @param servicesRegistry The set of all services registered with the system * @param ingestWorkflow The implementation of the {@link IngestWorkflow} to use for transaction rpc methods - * @param queryWorkflow The implementation of the {@link QueryWorkflow} to use for query rpc methods + * @param userQueryWorkflow The implementation of the {@link QueryWorkflow} to use for user query rpc methods + * @param operatorQueryWorkflow The implementation of the {@link QueryWorkflow} to use for node operator query rpc methods * @param metrics Used to get/create metrics for each transaction and query method. */ @Inject @@ -108,34 +130,40 @@ public NettyGrpcServerManager( @NonNull final ConfigProvider configProvider, @NonNull final ServicesRegistry servicesRegistry, @NonNull final IngestWorkflow ingestWorkflow, - @NonNull final QueryWorkflow queryWorkflow, + @NonNull @UserQueries final QueryWorkflow userQueryWorkflow, + @NonNull @OperatorQueries final QueryWorkflow operatorQueryWorkflow, @NonNull final Metrics metrics) { this.configProvider = requireNonNull(configProvider); requireNonNull(ingestWorkflow); - requireNonNull(queryWorkflow); + requireNonNull(userQueryWorkflow); + requireNonNull(operatorQueryWorkflow); requireNonNull(metrics); + final Supplier> rpcServiceDefinitions = + () -> servicesRegistry.registrations().stream() + .map(ServicesRegistry.Registration::service) + // Not all services are RPC services, but here we need RPC services only. The main difference + // between RPC service and a service is that the RPC service has RPC definition. + .filter(v -> v instanceof RpcService) + .map(v -> (RpcService) v) + .flatMap(s -> s.rpcDefinitions().stream()); + // Convert the various RPC service definitions into transaction or query endpoints using the // GrpcServiceBuilder. - services = servicesRegistry.registrations().stream() - .map(ServicesRegistry.Registration::service) - // Not all services are RPC services, but here we need RPC services only. The main difference - // between RPC service and a service is that the RPC service has RPC definition. - .filter(v -> v instanceof RpcService) - .map(v -> (RpcService) v) - .flatMap(s -> s.rpcDefinitions().stream()) - .map(d -> { - final var builder = new GrpcServiceBuilder(d.basePath(), ingestWorkflow, queryWorkflow); - d.methods().forEach(m -> { - if (Transaction.class.equals(m.requestType())) { - builder.transaction(m.path()); - } else { - builder.query(m.path()); - } - }); - return builder.build(metrics); - }) - .collect(Collectors.toUnmodifiableSet()); + services = + buildServiceDefinitions(rpcServiceDefinitions, m -> true, ingestWorkflow, userQueryWorkflow, metrics); + + final var grpcConfig = configProvider.getConfiguration().getConfigData(GrpcConfig.class); + if (grpcConfig.nodeOperatorPortEnabled()) { + // Convert the various RPC service definitions into query endpoints permitting unpaid queries for node + // operators + nodeOperatorServices = buildServiceDefinitions( + rpcServiceDefinitions, + m -> Query.class.equals(m.requestType()), + ingestWorkflow, + operatorQueryWorkflow, + metrics); + } } @Override @@ -148,6 +176,11 @@ public int tlsPort() { return tlsServer == null ? -1 : tlsServer.getPort(); } + @Override + public int nodeOperatorPort() { + return nodeOperatorServer == null || nodeOperatorServer.isTerminated() ? -1 : nodeOperatorServer.getPort(); + } + @Override public boolean isRunning() { return plainServer != null && !plainServer.isShutdown(); @@ -173,8 +206,8 @@ public synchronized void start() { // Start the plain-port server logger.info("Starting gRPC server on port {}", port); - var nettyBuilder = builderFor(port, nettyConfig, profile); - plainServer = startServerWithRetry(nettyBuilder, startRetries, startRetryIntervalMs); + var nettyBuilder = builderFor(port, nettyConfig, profile, false); + plainServer = startServerWithRetry(services, nettyBuilder, startRetries, startRetryIntervalMs); logger.info("gRPC server listening on port {}", plainServer.getPort()); // Try to start the server listening on the tls port. If this doesn't start, then we just keep going. We should @@ -184,14 +217,28 @@ public synchronized void start() { try { final var tlsPort = grpcConfig.tlsPort(); logger.info("Starting TLS gRPC server on port {}", tlsPort); - nettyBuilder = builderFor(tlsPort, nettyConfig, profile); + nettyBuilder = builderFor(tlsPort, nettyConfig, profile, false); configureTls(nettyBuilder, nettyConfig); - tlsServer = startServerWithRetry(nettyBuilder, startRetries, startRetryIntervalMs); + tlsServer = startServerWithRetry(services, nettyBuilder, startRetries, startRetryIntervalMs); logger.info("TLS gRPC server listening on port {}", tlsServer.getPort()); } catch (SSLException | FileNotFoundException e) { tlsServer = null; logger.warn("Could not start TLS server, will continue without it: {}", e.getMessage()); } + + if (grpcConfig.nodeOperatorPortEnabled()) { + try { + final var nodeOperatorPort = grpcConfig.nodeOperatorPort(); + logger.info("Starting node operator gRPC server on port {}", nodeOperatorPort); + nettyBuilder = builderFor(nodeOperatorPort, nettyConfig, profile, true); + nodeOperatorServer = + startServerWithRetry(nodeOperatorServices, nettyBuilder, startRetries, startRetryIntervalMs); + logger.info("Node operator gRPC server listening on port {}", nodeOperatorServer.getPort()); + } catch (Exception e) { + nodeOperatorServer = null; + logger.warn("Could not start node operator gRPC server, will continue without it: {}", e.getMessage()); + } + } } @Override @@ -210,23 +257,34 @@ public synchronized void stop() { } else { logger.info("Cannot shut down an already stopped gRPC server"); } + + if (nodeOperatorServer != null && !nodeOperatorServer.isTerminated()) { + logger.info("Shutting down node operator gRPC server on port {}", nodeOperatorServer.getPort()); + terminateServer(nodeOperatorServer); + } else { + logger.info("Cannot shut down an already stopped node operator gRPC server"); + } } /** * Attempts to start the server. It will retry {@code startRetries} times until it finally gives up with * {@code startRetryIntervalMs} between attempts. * + * @param serviceDefinitions The service definitions to register with the server * @param nettyBuilder The builder used to create the server to start * @param startRetries The number of times to retry, if needed. Non-negative (enforced by config). * @param startRetryIntervalMs The time interval between retries. Positive (enforced by config). */ Server startServerWithRetry( - @NonNull final NettyServerBuilder nettyBuilder, final int startRetries, final long startRetryIntervalMs) { - + @NonNull final Iterable serviceDefinitions, + @NonNull final NettyServerBuilder nettyBuilder, + final int startRetries, + final long startRetryIntervalMs) { + requireNonNull(serviceDefinitions); requireNonNull(nettyBuilder); // Setup the GRPC Routing, such that all grpc services are registered - services.forEach(nettyBuilder::addService); + serviceDefinitions.forEach(nettyBuilder::addService); final var server = nettyBuilder.build(); var remaining = startRetries; @@ -282,13 +340,16 @@ private void terminateServer(@Nullable final Server server) { } /** - * Utility for setting up various shared configuration settings between both servers + * Utility for setting up various shared configuration settings for all servers */ private NettyServerBuilder builderFor( - final int port, @NonNull final NettyConfig config, @NonNull final Profile activeProfile) { + final int port, + @NonNull final NettyConfig config, + @NonNull final Profile activeProfile, + boolean localHostOnly) { NettyServerBuilder builder = null; try { - builder = withConfigForActiveProfile(NettyServerBuilder.forPort(port), config, activeProfile) + builder = withConfigForActiveProfile(getInitialServerBuilder(port, localHostOnly), config, activeProfile) .channelType(EpollServerSocketChannel.class) .bossEventLoopGroup(new EpollEventLoopGroup()) .workerEventLoopGroup(new EpollEventLoopGroup()); @@ -296,13 +357,21 @@ private NettyServerBuilder builderFor( } catch (final UnsatisfiedLinkError | NoClassDefFoundError ignored) { // If we can't use Epoll, then just use NIO logger.info("Epoll not available, using NIO"); - builder = withConfigForActiveProfile(NettyServerBuilder.forPort(port), config, activeProfile); + builder = withConfigForActiveProfile(getInitialServerBuilder(port, localHostOnly), config, activeProfile); } catch (final Exception unexpected) { logger.info("Unexpected exception initializing Netty", unexpected); } return builder; } + private static @NonNull NettyServerBuilder getInitialServerBuilder(int port, boolean localHostOnly) { + if (localHostOnly) { + return NettyServerBuilder.forAddress(new InetSocketAddress("localhost", port)); + } + + return NettyServerBuilder.forPort(port); + } + private NettyServerBuilder withConfigForActiveProfile( @NonNull final NettyServerBuilder builder, @NonNull final NettyConfig config, @@ -346,4 +415,26 @@ private void configureTls(final NettyServerBuilder builder, NettyConfig config) builder.sslContext(sslContext); } + + private Set buildServiceDefinitions( + @NonNull final Supplier> rpcServiceDefinitions, + @NonNull final Predicate methodFilter, + @NonNull final IngestWorkflow ingestWorkflow, + @NonNull final QueryWorkflow queryWorkflow, + @NonNull final Metrics metrics) { + return rpcServiceDefinitions + .get() + .map(d -> { + final var builder = new GrpcServiceBuilder(d.basePath(), ingestWorkflow, queryWorkflow); + d.methods().stream().filter(methodFilter).forEach(m -> { + if (Transaction.class.equals(m.requestType())) { + builder.transaction(m.path()); + } else { + builder.query(m.path()); + } + }); + return builder.build(metrics); + }) + .collect(Collectors.toUnmodifiableSet()); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordInjectionModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordInjectionModule.java index 8a57a5cf4774..2e548ac4c549 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordInjectionModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordInjectionModule.java @@ -25,7 +25,6 @@ import com.hedera.node.app.records.impl.producers.formats.BlockRecordWriterFactoryImpl; import com.hedera.node.app.records.impl.producers.formats.v6.BlockRecordFormatV6; import com.hedera.node.app.records.impl.producers.formats.v7.BlockRecordFormatV7; -import com.hedera.node.app.state.HederaRecordCache; import com.hedera.node.app.state.WorkingStateAccessor; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockRecordStreamConfig; @@ -77,8 +76,7 @@ public static BlockRecordStreamProducer provideStreamFileProducer( public static BlockRecordManager provideBlockRecordManager( @NonNull final ConfigProvider configProvider, @NonNull final WorkingStateAccessor state, - @NonNull final BlockRecordStreamProducer streamFileProducer, - @NonNull final HederaRecordCache recordCache) { + @NonNull final BlockRecordStreamProducer streamFileProducer) { final var merkleState = state.getState(); if (merkleState == null) { throw new IllegalStateException("Merkle state is null"); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordManager.java index 929151d8ad4b..4b3482ff011b 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordManager.java @@ -108,14 +108,14 @@ public interface BlockRecordManager extends BlockRecordInfo, AutoCloseable { @Override void close(); - /** - * Notifies the block record manager that any startup migration records have been streamed. - */ - void markMigrationRecordsStreamed(); - /** * Get the consensus time of the latest handled transaction, or EPOCH if no transactions have been handled yet */ @NonNull Instant consTimeOfLastHandledTxn(); + + /** + * Notifies the block record manager that any startup migration records have been streamed. + */ + void markMigrationRecordsStreamed(); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java index 97eea1909cdf..ed0b7f43c613 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/BlockRecordService.java @@ -19,7 +19,7 @@ import com.hedera.hapi.node.base.Timestamp; import com.hedera.node.app.records.impl.BlockRecordManagerImpl; import com.hedera.node.app.records.schemas.V0490BlockRecordSchema; -import com.hedera.node.app.records.schemas.V0540BlockRecordSchema; +import com.hedera.node.app.records.schemas.V0560BlockRecordSchema; import com.swirlds.state.spi.SchemaRegistry; import com.swirlds.state.spi.Service; import edu.umd.cs.findbugs.annotations.NonNull; @@ -48,6 +48,6 @@ public String getServiceName() { @Override public void registerSchemas(@NonNull final SchemaRegistry registry) { registry.register(new V0490BlockRecordSchema()); - registry.register(new V0540BlockRecordSchema()); + registry.register(new V0560BlockRecordSchema()); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/BlockRecordManagerImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/BlockRecordManagerImpl.java index f7523a1841b9..ed7cb98cccd0 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/BlockRecordManagerImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/BlockRecordManagerImpl.java @@ -32,7 +32,6 @@ import com.hedera.node.app.state.SingleTransactionRecord; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockRecordStreamConfig; -import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.DigestType; import com.swirlds.common.crypto.Hash; @@ -89,9 +88,7 @@ public final class BlockRecordManagerImpl implements BlockRecordManager { /** * True when we have completed event recovery. This is not yet implemented properly. */ - private boolean eventRecoveryCompleted = false; - - private final ConfigProvider configProvider; + private boolean eventRecoveryCompleted; /** * Construct BlockRecordManager @@ -106,7 +103,6 @@ public BlockRecordManagerImpl( @NonNull final State state, @NonNull final BlockRecordStreamProducer streamFileProducer) { requireNonNull(state); - this.configProvider = requireNonNull(configProvider); this.streamFileProducer = requireNonNull(streamFileProducer); // FUTURE: check if we were started in event recover mode and if event recovery needs to be completed before we @@ -212,7 +208,7 @@ public boolean startUserTransaction(@NonNull final Instant consensusTime, @NonNu Starting: #{} @ {}""", justFinishedBlockNumber, lastBlockInfo.firstConsTimeOfCurrentBlock(), - new Hash(lastBlockHashBytes.toByteArray(), DigestType.SHA_384), + new Hash(lastBlockHashBytes, DigestType.SHA_384), justFinishedBlockNumber + 1, consensusTime); } @@ -271,7 +267,6 @@ public void endRound(@NonNull final State state) { // Update running hashes in state with the latest running hash and the previous 3 running hashes. final var states = state.getWritableStates(BlockRecordService.NAME); final var runningHashesState = states.getSingleton(RUNNING_HASHES_STATE_KEY); - final var blockRecordInfoState = states.getSingleton(BLOCK_INFO_STATE_KEY); final var existingRunningHashes = runningHashesState.get(); assert existingRunningHashes != null : "This cannot be null because genesis migration sets it"; final var runningHashes = new RunningHashes( @@ -282,11 +277,6 @@ public void endRound(@NonNull final State state) { runningHashesState.put(runningHashes); // Commit the changes to the merkle tree. ((WritableSingletonStateBase) runningHashesState).commit(); - - final var blockStreamConfig = configProvider.getConfiguration().getConfigData(BlockStreamConfig.class); - if (blockStreamConfig.streamBlocks()) { - // FUTURE: Add runningHash state changes block item and block info block item to the stream - } } public long lastBlockNo() { @@ -361,11 +351,6 @@ public void advanceConsensusClock(@NonNull final Instant consensusTime, @NonNull .consTimeOfLastHandledTxn(Timestamp.newBuilder() .seconds(consensusTime.getEpochSecond()) .nanos(consensusTime.getNano())); - if (!this.lastBlockInfo.migrationRecordsStreamed()) { - // Any records created during migration should have been published already. Now we shut off the flag to - // disallow further publishing - builder.migrationRecordsStreamed(true); - } final var newBlockInfo = builder.build(); // Update the latest block info in state @@ -429,7 +414,7 @@ private long getBlockPeriod(@Nullable final Timestamp consensusTimestamp) { */ private BlockInfo infoOfJustFinished( @NonNull final BlockInfo lastBlockInfo, - @NonNull final long justFinishedBlockNumber, + final long justFinishedBlockNumber, @NonNull final Bytes hashOfJustFinishedBlock, @NonNull final Instant currentBlockFirstTransactionTime) { // compute new block hashes bytes diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0490BlockRecordSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0490BlockRecordSchema.java index f923981c7d3e..8c583fd388a2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0490BlockRecordSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0490BlockRecordSchema.java @@ -67,7 +67,8 @@ public void migrate(@NonNull final MigrationContext ctx) { final var isGenesis = ctx.previousVersion() == null; if (isGenesis) { final var blocksState = ctx.newStates().getSingleton(BLOCK_INFO_STATE_KEY); - final var blocks = new BlockInfo(-1, EPOCH, Bytes.EMPTY, EPOCH, false, EPOCH); + // Note there is by convention no post-upgrade work to do if starting from genesis + final var blocks = new BlockInfo(-1, EPOCH, Bytes.EMPTY, EPOCH, true, EPOCH); blocksState.put(blocks); final var runningHashState = ctx.newStates().getSingleton(RUNNING_HASHES_STATE_KEY); final var runningHashes = diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0540BlockRecordSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0560BlockRecordSchema.java similarity index 76% rename from hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0540BlockRecordSchema.java rename to hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0560BlockRecordSchema.java index 8460ae0036b2..6cb83032ebd7 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0540BlockRecordSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/schemas/V0560BlockRecordSchema.java @@ -23,31 +23,25 @@ import com.swirlds.state.spi.MigrationContext; import com.swirlds.state.spi.Schema; import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -public class V0540BlockRecordSchema extends Schema { - private static final Logger logger = LogManager.getLogger(V0540BlockRecordSchema.class); +public class V0560BlockRecordSchema extends Schema { /** * The version of the schema. */ private static final SemanticVersion VERSION = - SemanticVersion.newBuilder().major(0).minor(54).patch(0).build(); + SemanticVersion.newBuilder().major(0).minor(56).patch(0).build(); private static final String SHARED_BLOCK_RECORD_INFO = "SHARED_BLOCK_RECORD_INFO"; private static final String SHARED_RUNNING_HASHES = "SHARED_RUNNING_HASHES"; - public V0540BlockRecordSchema() { + public V0560BlockRecordSchema() { super(VERSION); } - /** - * {@inheritDoc} - * */ @Override - public void migrate(@NonNull final MigrationContext ctx) { - final var isGenesis = ctx.previousVersion() == null; - if (!isGenesis) { + public void restart(@NonNull final MigrationContext ctx) { + if (ctx.previousVersion() != null) { + // Upcoming BlockStreamService schemas may need migration info final var blocksState = ctx.newStates().getSingleton(BLOCK_INFO_STATE_KEY); final var runningHashesState = ctx.newStates().getSingleton(RUNNING_HASHES_STATE_KEY); ctx.sharedValues().put(SHARED_BLOCK_RECORD_INFO, blocksState.get()); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java index 66eef380bf64..5db175c50144 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java @@ -26,5 +26,6 @@ * @param instantSource the source of the current instant * @param signatureVerifier the signature verifier */ -public record AppContextImpl(@NonNull InstantSource instantSource, @NonNull SignatureVerifier signatureVerifier) +public record AppContextImpl( + @NonNull InstantSource instantSource, @NonNull SignatureVerifier signatureVerifier, @NonNull Gossip gossip) implements AppContext {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationStateChanges.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationStateChanges.java index 1536a60aa0f8..0bce057fc966 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationStateChanges.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/MigrationStateChanges.java @@ -16,6 +16,7 @@ package com.hedera.node.app.services; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static java.util.Objects.requireNonNull; import com.hedera.hapi.block.stream.BlockItem; @@ -44,14 +45,12 @@ public class MigrationStateChanges { * changes to the given state. * * @param state The state to track changes on - * @param config + * @param config The configuration for the state */ public MigrationStateChanges(@NonNull final State state, final Configuration config) { - requireNonNull(state); - this.state = state; - final var blockStreamsEnabled = - config.getConfigData(BlockStreamConfig.class).streamBlocks(); - if (blockStreamsEnabled) { + requireNonNull(config); + this.state = requireNonNull(state); + if (config.getConfigData(BlockStreamConfig.class).streamMode() != RECORDS) { state.registerCommitListener(kvStateChangeListener); state.registerCommitListener(roundStateChangeListener); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/DefaultKeyVerifier.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/DefaultKeyVerifier.java index 5b5f6e8ebd53..fb7639518920 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/DefaultKeyVerifier.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/signature/DefaultKeyVerifier.java @@ -23,19 +23,25 @@ import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.KeyList; +import com.hedera.node.app.spi.key.KeyComparator; import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.config.data.HederaConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.AbstractMap; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,9 +49,10 @@ * Base implementation of {@link AppKeyVerifier} */ public class DefaultKeyVerifier implements AppKeyVerifier { - private static final Logger logger = LogManager.getLogger(DefaultKeyVerifier.class); + private static final Comparator KEY_COMPARATOR = new KeyComparator(); + private final int legacyFeeCalcNetworkVpt; private final long timeout; private final Map keyVerifications; @@ -137,6 +144,16 @@ public int numSignaturesVerified() { return legacyFeeCalcNetworkVpt; } + @Override + public SortedSet signingCryptoKeys() { + return keyVerifications.entrySet().stream() + .map(entry -> new AbstractMap.SimpleImmutableEntry<>( + entry.getKey(), resolveFuture(entry.getValue(), () -> failedVerification(entry.getKey())))) + .filter(e -> e.getValue().passed()) + .map(Map.Entry::getKey) + .collect(Collectors.toCollection(() -> new TreeSet<>(KEY_COMPARATOR))); + } + /** * Get a {@link Future} for the given key. * diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaRecordCache.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaRecordCache.java index cd2a2ff716d8..966442d4cd5c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaRecordCache.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/HederaRecordCache.java @@ -16,15 +16,14 @@ package com.hedera.node.app.state; -import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.TransactionID; import com.hedera.node.app.spi.records.RecordCache; +import com.hedera.node.app.spi.records.RecordSource; import com.hedera.node.config.data.HederaConfig; import com.hederahashgraph.api.proto.java.TransactionReceiptEntries; import com.swirlds.state.State; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Instant; -import java.util.List; /** * A time-limited cache of transaction records and receipts. @@ -46,19 +45,26 @@ */ /*@ThreadSafe*/ public interface HederaRecordCache extends RecordCache { + enum DueDiligenceFailure { + YES, + NO + } + /** - * Records the fact that the given {@link TransactionID} has been seen by the given node. If the node has already - * been seen, then this call is a no-op. This call does not perform any additional validation of the transaction ID. + * Incorporates a source of records for transactions whose consensus times were assigned relative to the given user + * transaction {@link TransactionID} into the cache, using the given payer account id as the effective payer for + * all the transactions whose id matches the user transaction. * - * @param nodeId The node ID of the node that submitted this transaction to consensus, as known in the address book - * @param payerAccountId The {@link AccountID} of the "payer" of the transaction - * @param transactionRecords The list of all related transaction records. This may be a stream of 1, if the list - * only contains the user transaction. Or it may be a list including preceding - * transactions, user transactions, and child transactions. There is no requirement as to - * the order of items in this list. + * @param nodeId the node id of the node that submitted the user transaction + * @param userTxnId the id of the user transaction + * @param dueDiligenceFailure whether the node failed due diligence + * @param recordSource the source of records for the transactions */ - /*HANDLE THREAD ONLY*/ - void add(long nodeId, @NonNull AccountID payerAccountId, @NonNull List transactionRecords); + void addRecordSource( + long nodeId, + @NonNull TransactionID userTxnId, + @NonNull DueDiligenceFailure dueDiligenceFailure, + @NonNull RecordSource recordSource); /** * Checks if the given transaction ID has been seen by this node. If it has not, the result is diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/MerkleStateLifecyclesImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/MerkleStateLifecyclesImpl.java index 9e61835c6946..7f0a2ba53d3e 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/MerkleStateLifecyclesImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/MerkleStateLifecyclesImpl.java @@ -142,7 +142,7 @@ public void onUpdateWeight( // service schema's restart() hook. Here we only update the address book weights // based on the staking info in the state. weightUpdateVisitor.accept(stakingInfoVMap, (node, info) -> { - final var nodeId = new NodeId(node.number()); + final var nodeId = NodeId.of(node.number()); // If present in the address book, remove this node id from the // set of node ids left to update and update its weight if (nodeIdsLeftToUpdate.remove(nodeId)) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/BlockRecordSource.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/BlockRecordSource.java new file mode 100644 index 000000000000..2e31248be84f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/BlockRecordSource.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.state.recordcache; + +import static com.hedera.node.app.blocks.BlockItemsTranslator.BLOCK_ITEMS_TRANSLATOR; +import static com.hedera.node.app.spi.records.RecordCache.isChild; +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.blocks.BlockItemsTranslator; +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; +import com.hedera.node.app.blocks.impl.TranslationContext; +import com.hedera.node.app.spi.records.RecordSource; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A {@link RecordSource} that lazily computes {@link TransactionRecord} and {@link TransactionReceipt} histories from + * lists of {@link BlockItem}s and corresponding {@link TranslationContext}s. + */ +public class BlockRecordSource implements RecordSource { + private final BlockItemsTranslator blockItemsTranslator; + private final List outputs; + + @Nullable + private List computedRecords; + + @Nullable + private List computedReceipts; + + /** + * Constructs a {@link BlockRecordSource} from a list of {@link BlockStreamBuilder.Output}s. + * @param outputs the outputs + */ + public BlockRecordSource(@NonNull final List outputs) { + this(BLOCK_ITEMS_TRANSLATOR, outputs); + } + + /** + * Constructs a {@link BlockRecordSource} from a list of {@link BlockStreamBuilder.Output}s, also + * specifying the {@link BlockItemsTranslator} to use for creating receipts and records. + * @param blockItemsTranslator the translator + * @param outputs the outputs + */ + @VisibleForTesting + public BlockRecordSource( + @NonNull final BlockItemsTranslator blockItemsTranslator, + @NonNull final List outputs) { + this.blockItemsTranslator = requireNonNull(blockItemsTranslator); + this.outputs = requireNonNull(outputs); + } + + /** + * For each {@link BlockItem} in the source, apply the given action. + * + * @param action the action to apply + */ + public void forEachItem(@NonNull final Consumer action) { + requireNonNull(action); + outputs.forEach(output -> output.forEachItem(action)); + } + + @Override + public List identifiedReceipts() { + return computedReceipts(); + } + + @Override + public void forEachTxnRecord(@NonNull final Consumer action) { + requireNonNull(action); + computedRecords().forEach(action); + } + + @Override + public TransactionReceipt receiptOf(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + for (final var receipt : computedReceipts()) { + if (txnId.equals(receipt.txnId())) { + return receipt.receipt(); + } + } + throw new IllegalArgumentException(); + } + + @Override + public List childReceiptsOf(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + final List receipts = new ArrayList<>(); + for (final var receipt : computedReceipts()) { + if (isChild(txnId, receipt.txnId())) { + receipts.add(receipt.receipt()); + } + } + return receipts; + } + + private List computedReceipts() { + if (computedReceipts == null) { + // Mutate the list of outputs before making it visible to another traversing thread + final List computation = new ArrayList<>(); + for (final var output : outputs) { + computation.add(output.toIdentifiedReceipt(blockItemsTranslator)); + } + computedReceipts = computation; + } + return computedReceipts; + } + + private List computedRecords() { + if (computedRecords == null) { + // Mutate the list of outputs before making it visible to another traversing thread + final List computation = new ArrayList<>(); + for (final var output : outputs) { + computation.add(output.toRecord(blockItemsTranslator)); + } + computedRecords = computation; + } + return computedRecords; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/LegacyListRecordSource.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/LegacyListRecordSource.java new file mode 100644 index 000000000000..2ea4a24eb13a --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/LegacyListRecordSource.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.state.recordcache; + +import static com.hedera.node.app.spi.records.RecordCache.isChild; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.node.app.state.SingleTransactionRecord; +import com.hedera.node.config.data.BlockStreamConfig; +import com.hedera.node.config.types.StreamMode; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.function.Consumer; + +/** + * A {@link RecordSource} that uses a list of precomputed {@link SingleTransactionRecord}s. Used exclusively when + * {@link BlockStreamConfig#streamMode()} is {@link StreamMode#RECORDS} since in this case the + * {@link SingleTransactionRecord} objects are already constructed for streaming. + */ +public record LegacyListRecordSource( + @NonNull List precomputedRecords, @NonNull List identifiedReceipts) + implements RecordSource { + + @Override + public @NonNull List precomputedRecords() { + return precomputedRecords; + } + + @Override + public void forEachTxnRecord(@NonNull final Consumer action) { + requireNonNull(action); + precomputedRecords.forEach(r -> action.accept(r.transactionRecord())); + } + + @Override + public TransactionReceipt receiptOf(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + for (final var precomputed : precomputedRecords) { + if (txnId.equals(precomputed.transactionRecord().transactionIDOrThrow())) { + return precomputed.transactionRecord().receiptOrThrow(); + } + } + throw new IllegalArgumentException(); + } + + @Override + public List childReceiptsOf(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + return precomputedRecords.stream() + .filter(r -> isChild(txnId, r.transactionRecord().transactionIDOrThrow())) + .map(r -> r.transactionRecord().receiptOrThrow()) + .toList(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/PartialRecordSource.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/PartialRecordSource.java new file mode 100644 index 000000000000..990d624fd439 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/PartialRecordSource.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.state.recordcache; + +import static com.hedera.node.app.spi.records.RecordCache.isChild; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.node.config.data.BlockStreamConfig; +import com.hedera.node.config.types.StreamMode; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A {@link RecordSource} that uses a list of precomputed {@link TransactionRecord}s. Used in some tests and when + * {@link BlockStreamConfig#streamMode()} is {@link StreamMode#BLOCKS} to support queryable partial records after + * reconnect or restart. + */ +public class PartialRecordSource implements RecordSource { + private final List precomputedRecords; + private final List identifiedReceipts; + + public PartialRecordSource() { + this.precomputedRecords = new ArrayList<>(); + this.identifiedReceipts = new ArrayList<>(); + } + + public PartialRecordSource(@NonNull final TransactionRecord precomputedRecord) { + this(List.of(precomputedRecord)); + } + + public PartialRecordSource(@NonNull final List precomputedRecords) { + requireNonNull(precomputedRecords); + this.precomputedRecords = requireNonNull(precomputedRecords); + identifiedReceipts = new ArrayList<>(); + for (final var precomputed : precomputedRecords) { + identifiedReceipts.add( + new IdentifiedReceipt(precomputed.transactionIDOrThrow(), precomputed.receiptOrThrow())); + } + } + + public void incorporate(@NonNull final TransactionRecord precomputedRecord) { + requireNonNull(precomputedRecord); + precomputedRecords.add(precomputedRecord); + } + + @Override + public List identifiedReceipts() { + return identifiedReceipts; + } + + @Override + public void forEachTxnRecord(@NonNull final Consumer action) { + requireNonNull(action); + precomputedRecords.forEach(action); + } + + @Override + public TransactionReceipt receiptOf(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + for (final var precomputed : precomputedRecords) { + if (txnId.equals(precomputed.transactionIDOrThrow())) { + return precomputed.receiptOrThrow(); + } + } + throw new IllegalArgumentException(); + } + + @Override + public List childReceiptsOf(@NonNull TransactionID txnId) { + requireNonNull(txnId); + return precomputedRecords.stream() + .filter(r -> isChild(txnId, r.transactionIDOrThrow())) + .map(TransactionRecord::receiptOrThrow) + .toList(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheImpl.java index 3dccc75ce598..3411ced6a616 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/state/recordcache/RecordCacheImpl.java @@ -16,8 +16,13 @@ package com.hedera.node.app.state.recordcache; +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; import static com.hedera.hapi.util.HapiUtils.TIMESTAMP_COMPARATOR; import static com.hedera.hapi.util.HapiUtils.isBefore; +import static com.hedera.node.app.spi.records.RecordCache.matchesExceptNonce; +import static com.hedera.node.app.state.HederaRecordCache.DuplicateCheckResult.NO_DUPLICATE; +import static com.hedera.node.app.state.HederaRecordCache.DuplicateCheckResult.OTHER_NODE; +import static com.hedera.node.app.state.HederaRecordCache.DuplicateCheckResult.SAME_NODE; import static com.hedera.node.app.state.recordcache.RecordCacheService.NAME; import static com.hedera.node.app.state.recordcache.schemas.V0540RecordCacheSchema.TXN_RECEIPT_QUEUE; import static java.util.Collections.emptyList; @@ -30,9 +35,9 @@ import com.hedera.hapi.node.state.recordcache.TransactionReceiptEntry; import com.hedera.hapi.node.transaction.TransactionReceipt; import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.spi.records.RecordSource; import com.hedera.node.app.state.DeduplicationCache; import com.hedera.node.app.state.HederaRecordCache; -import com.hedera.node.app.state.SingleTransactionRecord; import com.hedera.node.app.state.WorkingStateAccessor; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.HederaConfig; @@ -41,6 +46,7 @@ import com.swirlds.state.spi.CommittableWritableStates; import com.swirlds.state.spi.ReadableQueueState; import com.swirlds.state.spi.WritableQueueState; +import com.swirlds.state.spi.info.NetworkInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Instant; @@ -77,8 +83,7 @@ * are treated as separate transactions, in that they are individually added to the queue, in the appropriate order, and * individually added to the history. However, child transactions are always included in the history of the user * transaction that triggered them, because they need to be available to the user when querying for all records for a - * given transaction ID, or for a given payer, while preceding trnasactions are treated as their own top level - * transactions. + * given transaction ID, or for a given payer. * *

    Mutation methods must be called during startup, reconnect, or on the "handle" thread. Getters may be called from * any thread. @@ -99,7 +104,19 @@ public class RecordCacheImpl implements HederaRecordCache { */ private static final History EMPTY_HISTORY = new History(); - /** Used for looking up the max valid duration window for a transaction. This must be looked up dynamically. */ + /** + * This empty History is returned whenever a transaction is known to the deduplication cache, but not yet + * added to this cache. + */ + private static final HistorySource EMPTY_HISTORY_SOURCE = new HistorySource(); + + /** + * Used for looking up fee collection account for a node that failed due diligence. This must be looked up dynamically. + */ + private final NetworkInfo networkInfo; + /** + * Used for looking up the max valid duration window for a transaction. This must be looked up dynamically. + */ private final ConfigProvider configProvider; /** * Every record added to the cache has a unique transaction ID. Each of these must be recorded in the dedupe cache @@ -109,22 +126,116 @@ public class RecordCacheImpl implements HederaRecordCache { */ private final DeduplicationCache deduplicationCache; /** - * A map of transaction IDs to the histories of all transactions that came to - * consensus with that ID, or their child - * transactions. This data structure is rebuilt during reconnect or restart. Using a non-deterministic, map is - * perfectly acceptable, as the order of these histories is not important. + * A map of transaction IDs to the sources of records for those transaction ids and their children. */ - private final Map histories; + private final Map historySources = new ConcurrentHashMap<>(); /** * A secondary index that maps from the AccountID of the payer account to a set of transaction IDs that were * submitted by this payer. This is only needed for answering queries. Ideally such queries would exist on the * mirror node instead. The answer to this query will include child records that were created as a consequence * of the original user transaction, but not any preceding records triggered by it. */ - private final Map> payerToTransactionIndex = new ConcurrentHashMap<>(); - + private final Map> payerTxnIds = new ConcurrentHashMap<>(); + /** + * The list of transaction receipts for the current round. + */ private final List transactionReceipts = new ArrayList<>(); + /** + * Contains history of transactions submitted with the same "base" {@link TransactionID}; + * i.e., with the same payer and valid start time. + *

    + * This history has two parts: + *

      + *
    1. A {@code nodeIds} set with all the node ids that have submitted a properly + * screened transaction with the scoped base {@link TransactionID}; this is used to + * classify duplicate transactions.
    2. + *
    3. A {@code recordSources} list with all the sources of records for the relevant + * base {@link TransactionID}. This is used to construct {@link TransactionRecord} + * records for answering queries.
    4. + *
    + * + * @param nodeIds The set of node ids that have submitted a properly screened transaction + * @param recordSources The sources of records for the relevant base {@link TransactionID} + */ + private record HistorySource(@NonNull Set nodeIds, @NonNull List recordSources) + implements ReceiptSource { + public HistorySource() { + this(new HashSet<>(), new ArrayList<>()); + } + + @Override + public @NonNull TransactionReceipt priorityReceipt(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + if (recordSources.isEmpty()) { + return PENDING_RECEIPT; + } + final var firstPriorityReceipt = recordSources.getFirst().receiptOf(txnId); + if (!NODE_FAILURES.contains(firstPriorityReceipt.status())) { + return firstPriorityReceipt; + } else { + for (int i = 1, n = recordSources.size(); i < n; i++) { + final var nextPriorityReceipt = recordSources.get(i).receiptOf(txnId); + if (!NODE_FAILURES.contains(nextPriorityReceipt.status())) { + return nextPriorityReceipt; + } + } + } + return firstPriorityReceipt; + } + + @Override + public @Nullable TransactionReceipt childReceipt(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + for (final var source : recordSources) { + try { + return source.receiptOf(txnId); + } catch (IllegalArgumentException ignore) { + } + } + return null; + } + + @Override + public @NonNull List duplicateReceipts(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + final List receipts = new ArrayList<>(); + recordSources.forEach(source -> receipts.add(source.receiptOf(txnId))); + receipts.remove(priorityReceipt(txnId)); + return receipts; + } + + @Override + public @NonNull List childReceipts(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + final List receipts = new ArrayList<>(); + recordSources.forEach(source -> receipts.addAll(source.childReceiptsOf(txnId))); + return receipts; + } + + /** + * Returns a {@link History} that summarizes all duplicate and child records for a given {@link TransactionID} + * from this history source in canonical order. + * + * @param userTxnId the user {@link TransactionID} to summarize records for + * @return the canonical history + */ + History historyOf(@NonNull final TransactionID userTxnId) { + final List duplicateRecords = new ArrayList<>(); + final List childRecords = new ArrayList<>(); + for (final var recordSource : recordSources) { + recordSource.forEachTxnRecord(txnRecord -> { + final var txnId = txnRecord.transactionIDOrThrow(); + if (matchesExceptNonce(txnId, userTxnId)) { + final var source = txnId.nonce() > 0 ? childRecords : duplicateRecords; + source.add(txnRecord); + } + }); + } + return new History(nodeIds, duplicateRecords, childRecords); + } + } + /** * Called once during startup to create this singleton. Rebuilds the in-memory data structures based on the current * working state at the moment of startup. The size of these data structures is fixed based on the length of time @@ -138,46 +249,49 @@ public class RecordCacheImpl implements HederaRecordCache { * state. * * @param deduplicationCache A cache containing known {@link TransactionID}s, used for deduplication - * @param configProvider Used for looking up the max valid duration window for a transaction dynamically - * @param workingStateAccessor Gives access to the current working state, needed at startup, but also any time - * records must be saved in state or read from state. + * @param workingStateAccessor Gives access to the current working state to use in rebuilding the cache + * @param configProvider Used for looking up the max valid duration window for a transaction dynamically + * @param networkInfo the network information */ @Inject public RecordCacheImpl( @NonNull final DeduplicationCache deduplicationCache, @NonNull final WorkingStateAccessor workingStateAccessor, - @NonNull final ConfigProvider configProvider) { + @NonNull final ConfigProvider configProvider, + @NonNull final NetworkInfo networkInfo) { this.deduplicationCache = requireNonNull(deduplicationCache); this.configProvider = requireNonNull(configProvider); - this.histories = new ConcurrentHashMap<>(); - - rebuild(workingStateAccessor); - } + this.networkInfo = requireNonNull(networkInfo); - /** - * Rebuild the internal data structures based on the current working state. Called during startup and during - * reconnect. The amount of time it takes to rebuild this data structure is not dependent on the size of state, but - * rather, the number of transactions in the queue (which is capped by configuration at 3 minutes by default). - */ - public void rebuild(@NonNull final WorkingStateAccessor workingStateAccessor) { - requireNonNull(workingStateAccessor); - histories.clear(); - payerToTransactionIndex.clear(); - // FUTURE: It doesn't hurt to clear the dedupe cache here, but is also probably not the best place to do it. The - // system should clear the dedupe cache directly and not indirectly through this call. deduplicationCache.clear(); - - final var queue = getReadableQueue(workingStateAccessor); - final var itr = queue.iterator(); - while (itr.hasNext()) { - final var roundReceipts = itr.next(); + final var iter = getReadableQueue(workingStateAccessor).iterator(); + while (iter.hasNext()) { + final var roundReceipts = iter.next(); for (final var receipt : roundReceipts.entries()) { - final var partialRecord = asTxnRecord(receipt); - // Make the partial record queryable - addToInMemoryCache( - receipt.nodeId(), receipt.transactionIdOrThrow().accountIDOrThrow(), partialRecord); + final var txnId = receipt.transactionIdOrThrow(); + // We group history by the base transaction ID, which is the transaction ID with a nonce of 0 + final var baseTxnId = txnId.nonce() == 0 + ? txnId + : txnId.copyBuilder().nonce(0).build(); // Ensure this node won't submit duplicate transactions and be penalized for it - deduplicationCache.add(receipt.transactionIdOrThrow()); + deduplicationCache.add(baseTxnId); + // Now update the history of this transaction id + final var historySource = historySources.computeIfAbsent(baseTxnId, ignore -> new HistorySource()); + // Honest nodes use the set of node ids that have submitted classifiable transactions with this id to + // classify user versus node duplicates; so reconstructing the set here is critical for deterministic + // transaction handling across all nodes in the network + if (!NODE_FAILURES.contains(receipt.status())) { + historySource.nodeIds().add(receipt.nodeId()); + } + // These steps only make a partial transaction record available for answering queries, and are not + // of critical importance for the operation of the node + if (historySource.recordSources().isEmpty()) { + historySource.recordSources().add(new PartialRecordSource()); + } + ((PartialRecordSource) historySource.recordSources.getFirst()).incorporate(asTxnRecord(receipt)); + payerTxnIds + .computeIfAbsent(txnId.accountIDOrThrow(), ignored -> new HashSet<>()) + .add(txnId); } } } @@ -185,33 +299,40 @@ public void rebuild(@NonNull final WorkingStateAccessor workingStateAccessor) { // --------------------------------------------------------------------------------------------------------------- // Implementation methods of HederaRecordCache // --------------------------------------------------------------------------------------------------------------- - - /** - * {@inheritDoc} - */ @Override - public void add( + public void addRecordSource( final long nodeId, - @NonNull final AccountID payerAccountId, - @NonNull final List transactionRecords) { - requireNonNull(payerAccountId); - requireNonNull(transactionRecords); - - // This really shouldn't ever happen. If it does, we'll log a warning and bail. - if (transactionRecords.isEmpty()) { - logger.warn("Received an empty list of transaction records. This should never happen"); - return; - } - - // For each transaction, in order, add to the in-memory data structures. - for (final var singleTransactionRecord : transactionRecords) { - final var rec = singleTransactionRecord.transactionRecord(); - // Make the full record queryable, but don't include all the details in state (so a reconnected node - // will only have a partial record available) - addToInMemoryCache(nodeId, payerAccountId, rec); - // Include its receipt in the current round's entries, to be committed to state and the end of the round - transactionReceipts.add(new TransactionReceiptEntry( - nodeId, rec.transactionIDOrThrow(), rec.receiptOrThrow().status())); + @NonNull final TransactionID userTxnId, + @NonNull final DueDiligenceFailure dueDiligenceFailure, + @NonNull final RecordSource recordSource) { + requireNonNull(userTxnId); + requireNonNull(recordSource); + for (final var identifiedReceipt : recordSource.identifiedReceipts()) { + final var txnId = identifiedReceipt.txnId(); + final var status = identifiedReceipt.receipt().status(); + transactionReceipts.add(new TransactionReceiptEntry(nodeId, txnId, status)); + final var baseTxnId = + txnId.nonce() == 0 ? txnId : txnId.copyBuilder().nonce(0).build(); + final var historySource = historySources.computeIfAbsent(baseTxnId, ignore -> new HistorySource()); + // We don't let improperly submitted transactions keep properly submitted transactions from using an id + if (!NODE_FAILURES.contains(status)) { + historySource.nodeIds().add(nodeId); + } + // Only add each record source once per history; since very few record sources contain more than one + // transaction id, and few transaction ids have duplicates, this is almost always an existence check + // in an empty list + if (!historySource.recordSources().contains(recordSource)) { + historySource.recordSources.add(recordSource); + } + final AccountID effectivePayerId; + if (dueDiligenceFailure == DueDiligenceFailure.YES && matchesExceptNonce(txnId, userTxnId)) { + effectivePayerId = requireNonNull(networkInfo.nodeInfo(nodeId)).accountId(); + } else { + effectivePayerId = txnId.accountIDOrThrow(); + } + payerTxnIds + .computeIfAbsent(effectivePayerId, ignored -> new HashSet<>()) + .add(txnId); } } @@ -224,7 +345,7 @@ public void resetRoundReceipts() { public void commitRoundReceipts(@NonNull final State state, @NonNull final Instant consensusNow) { requireNonNull(state); requireNonNull(consensusNow); - final var states = state.getWritableStates(RecordCacheService.NAME); + final var states = state.getWritableStates(NAME); final var queue = states.getQueue(TXN_RECEIPT_QUEUE); purgeExpiredReceiptEntries(queue, consensusNow); if (!transactionReceipts.isEmpty()) { @@ -237,57 +358,15 @@ public void commitRoundReceipts(@NonNull final State state, @NonNull final Insta @NonNull @Override - public DuplicateCheckResult hasDuplicate(@NonNull TransactionID transactionID, long nodeId) { - final var history = histories.get(transactionID); + public DuplicateCheckResult hasDuplicate(@NonNull final TransactionID txnId, final long nodeId) { + requireNonNull(txnId); + final var historySource = historySources.get(txnId); // If there is no history for this transaction id; or all its history consists of // unclassifiable records, return that it is effectively a unique id - if (history == null || history.nodeIds().isEmpty()) { - return DuplicateCheckResult.NO_DUPLICATE; + if (historySource == null || historySource.nodeIds().isEmpty()) { + return NO_DUPLICATE; } - return history.nodeIds().contains(nodeId) ? DuplicateCheckResult.SAME_NODE : DuplicateCheckResult.OTHER_NODE; - } - - /** - * Called during when constructing the cache or {@link #add(long, AccountID, List)}, this method adds the given - * {@link TransactionRecord} to the internal lookup data structures. - * - * @param nodeId The ID of the node that submitted the transaction. - * @param payerAccountId The {@link AccountID} of the payer of the transaction, so we can look up transactions by - * payer later, if needed. - * @param transactionRecord The record to add. - */ - private void addToInMemoryCache( - final long nodeId, - @NonNull final AccountID payerAccountId, - @NonNull final TransactionRecord transactionRecord) { - final var txId = transactionRecord.transactionIDOrThrow(); - // We need the hasParentConsensusTimestamp() check to detect triggered transactions that - // children of a ScheduleSign or ScheduleCreate (nonces were introduced after scheduled - // transactions, so these children still have nonce=0) - final var isChildTx = transactionRecord.hasParentConsensusTimestamp() || txId.nonce() > 0; - final var userTxId = isChildTx ? txId.copyBuilder().nonce(0).build() : txId; - - // Get or create the history for this transaction ID. - // One interesting tidbit -- at genesis, the records will piggyback on the first transaction, so whatever node - // sent the first transaction will get "credit" for all the genesis records. But it will be deterministic, and - // doesn't actually matter. - final var history = histories.computeIfAbsent(userTxId, ignored -> new History()); - final var status = transactionRecord.receiptOrThrow().status(); - // If the status indicates a due diligence failure, we don't use the result in duplicate classification - if (!DUE_DILIGENCE_FAILURES.contains(status)) { - history.nodeIds().add(nodeId); - } - - // Either we add this tx to the main records list if it is a user/preceding transaction, or to the child - // transactions list of its parent. Note that scheduled transactions are always child transactions, but - // never produce child *records*; instead, the scheduled transaction record is treated as - // a user transaction record. The map key remains the current user transaction ID, however. - final var listToAddTo = (isChildTx && !txId.scheduled()) ? history.childRecords() : history.records(); - listToAddTo.add(transactionRecord); - - // Add to the payer-to-transaction index - final var transactionIDs = payerToTransactionIndex.computeIfAbsent(payerAccountId, ignored -> new HashSet<>()); - transactionIDs.add(txId); + return historySource.nodeIds().contains(nodeId) ? SAME_NODE : OTHER_NODE; } /** @@ -321,15 +400,28 @@ private void purgeExpiredReceiptEntries( // to this map keyed to the "user transaction" ID, so removing the entry here removes both "parent" // and "child" transaction records associated with that ID. for (final var receipt : roundReceipts.entries()) { - final var txId = receipt.transactionIdOrThrow(); - histories.remove(txId); + final var txnId = receipt.transactionIdOrThrow(); + historySources.remove( + txnId.nonce() == 0 + ? txnId + : txnId.copyBuilder().nonce(0).build()); // Remove from the payer to transaction index - final var payerAccountId = txId.accountIDOrThrow(); // NOTE: Not accurate if the payer was the node - final var transactionIDs = - payerToTransactionIndex.computeIfAbsent(payerAccountId, ignored -> new HashSet<>()); - transactionIDs.remove(txId); - if (transactionIDs.isEmpty()) { - payerToTransactionIndex.remove(payerAccountId); + var payerId = txnId.accountIDOrThrow(); + var txnIds = payerTxnIds.computeIfAbsent(payerId, ignored -> new HashSet<>()); + if (!txnIds.remove(txnId)) { + // The submitting node account must have been the payer + payerId = requireNonNull(networkInfo.nodeInfo(receipt.nodeId())) + .accountId(); + txnIds = payerTxnIds.computeIfAbsent(payerId, ignored -> new HashSet<>()); + if (!txnIds.remove(txnId) && receipt.status() != DUPLICATE_TRANSACTION) { + logger.warn( + "Non-duplicate {} not cached for either payer or submitting node {}", + txnId, + payerId); + } + } + if (txnIds.isEmpty()) { + payerTxnIds.remove(payerId); } } // Remove the round receipts from the queue @@ -345,26 +437,36 @@ private void purgeExpiredReceiptEntries( @Nullable @Override - public History getHistory(@NonNull TransactionID transactionID) { - final var history = histories.get(transactionID); - return history != null ? history : (deduplicationCache.contains(transactionID) ? EMPTY_HISTORY : null); + public History getHistory(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + final var historySource = historySources.get(txnId); + return historySource != null + ? historySource.historyOf(txnId) + : (deduplicationCache.contains(txnId) ? EMPTY_HISTORY : null); + } + + @Override + public @Nullable ReceiptSource getReceipts(@NonNull final TransactionID txnId) { + requireNonNull(txnId); + final var historySource = historySources.get(txnId); + return historySource != null + ? historySource + : (deduplicationCache.contains(txnId) ? EMPTY_HISTORY_SOURCE : null); } @NonNull @Override public List getRecords(@NonNull final AccountID accountID) { - final var transactionIDs = payerToTransactionIndex.get(accountID); - if (transactionIDs == null) { + final var txnIds = payerTxnIds.get(accountID); + if (txnIds == null) { return emptyList(); } - // Note that at **most** LedgerConfig#recordsMaxQueryableByAccount() records will be available, even if the // given account has paid for more than this number of transactions in the last 180 seconds. var maxRemaining = configProvider .getConfiguration() .getConfigData(LedgerConfig.class) .recordsMaxQueryableByAccount(); - // While we still need to gather more records, collect them from the different histories. final var records = new ArrayList(maxRemaining); // Because the set of transaction IDs could be concurrently modified by @@ -372,22 +474,26 @@ public List getRecords(@NonNull final AccountID accountID) { // and return whatever we are able to gather. (I.e. this is a best-effort // query, and not a critical path; unused in production environments) try { - for (final var transactionID : transactionIDs) { - final var history = histories.get(transactionID); - if (history != null) { - final var recs = history.orderedRecords(); - records.addAll(recs.size() > maxRemaining ? recs.subList(0, maxRemaining) : recs); - maxRemaining -= recs.size(); - if (maxRemaining <= 0) break; + for (final var txnId : txnIds) { + final var historySource = historySources.get(txnId); + if (historySource != null) { + final var history = historySource.historyOf(txnId); + final var sourcedRecords = history.orderedRecords(); + records.addAll( + sourcedRecords.size() > maxRemaining + ? sourcedRecords.subList(0, maxRemaining) + : sourcedRecords); + maxRemaining -= sourcedRecords.size(); + if (maxRemaining <= 0) { + break; + } } } } catch (ConcurrentModificationException ignore) { // Ignore the exception and return what we found; this query is unused in production environments } - records.sort((a, b) -> TIMESTAMP_COMPARATOR.compare( a.consensusTimestampOrElse(Timestamp.DEFAULT), b.consensusTimestampOrElse(Timestamp.DEFAULT))); - return records; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/statedumpers/singleton/BlockInfoDumpUtils.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/statedumpers/singleton/BlockInfoDumpUtils.java index 01a49657808c..bfc1719322a3 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/statedumpers/singleton/BlockInfoDumpUtils.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/statedumpers/singleton/BlockInfoDumpUtils.java @@ -107,18 +107,17 @@ public static BBMBlockInfoAndRunningHashes combineFromMod( blockInfo.firstConsTimeOfCurrentBlock().seconds(), blockInfo.firstConsTimeOfCurrentBlock().nanos()); - var runningHash = Bytes.EMPTY.equals(runningHashes.runningHash()) - ? null - : new Hash(runningHashes.runningHash().toByteArray()); + var runningHash = + Bytes.EMPTY.equals(runningHashes.runningHash()) ? null : new Hash(runningHashes.runningHash()); var nMinus1RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus1RunningHash()) ? null - : new Hash(runningHashes.nMinus1RunningHash().toByteArray()); + : new Hash(runningHashes.nMinus1RunningHash()); var nMinus2RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus2RunningHash()) ? null - : new Hash(runningHashes.nMinus2RunningHash().toByteArray()); + : new Hash(runningHashes.nMinus2RunningHash()); var nMinus3RunningHash = Bytes.EMPTY.equals(runningHashes.nMinus3RunningHash()) ? null - : new Hash(runningHashes.nMinus3RunningHash().toByteArray()); + : new Hash(runningHashes.nMinus3RunningHash()); return new BBMBlockInfoAndRunningHashes( blockInfo.lastBlockNumber(), diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java index b131df78c200..d0ac740d1611 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseService.java @@ -16,27 +16,79 @@ package com.hedera.node.app.tss; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.tss.handlers.TssHandlers; +import com.hedera.node.app.tss.stores.ReadableTssBaseStore; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.spi.Service; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.function.BiConsumer; +import java.util.function.Consumer; /** - * The TssBaseService will attempt to generate TSS key material for any set candidate roster, giving it a ledger id and - * the ability to generate ledger signatures that can be verified by the ledger id. Once the candidate roster has - * received its full TSS key material, it can be made available for adoption by the platform. + * Provides the network's threshold signature scheme (TSS) capability and, as a side effect, the ledger id, as this + * is the exactly the same as the TSS public key. *

    - * The TssBaseService will also attempt to generate ledger signatures by aggregating share signatures produced by - * calling {@link #requestLedgerSignature(byte[])}. + * The */ public interface TssBaseService extends Service { String NAME = "TssBaseService"; + /** + * The status of the TSS service relative to a given roster and ledger id. + */ + enum Status { + /** + * The service cannot yet recover the expected ledger id from its current key material for the roster. + */ + PENDING_LEDGER_ID, + /** + * The TSS service is ready to sign. + */ + READY, + } + @NonNull @Override default String getServiceName() { return NAME; } + /** + * Returns the status of the TSS service relative to the given roster, ledger id, and given TSS base state. + * + * @param roster the candidate roster + * @param ledgerId the expected ledger id + * @param tssBaseStore the store to read the TSS base state from + * @return the status of the TSS service + */ + Status getStatus(@NonNull Roster roster, @NonNull Bytes ledgerId, @NonNull ReadableTssBaseStore tssBaseStore); + + /** + * Adopts the given roster for TSS operations. + * @param roster the active roster + * @throws IllegalArgumentException if the expected shares cannot be aggregated + */ + void adopt(@NonNull Roster roster); + + /** + * Bootstraps the TSS service for the given roster in the given context. + * @param roster the network genesis roster + * @param context the TSS context to use for bootstrapping + * @param ledgerIdConsumer the consumer of the ledger id, to receive the ledger id as soon as it is available + */ + void bootstrapLedgerId( + @NonNull Roster roster, @NonNull HandleContext context, @NonNull Consumer ledgerIdConsumer); + + /** + * Starts the process of keying a candidate roster with TSS key material. + * + * @param roster the candidate roster to key + * @param context the TSS context + */ + void setCandidateRoster(@NonNull Roster roster, @NonNull HandleContext context); + /** * Requests a ledger signature on a message hash. The ledger signature is computed asynchronously and returned * to all consumers that have been registered through {@link #registerLedgerSignatureConsumer}. @@ -58,4 +110,10 @@ default String getServiceName() { * @param consumer the consumer of ledger signatures and message hashes to unregister. */ void unregisterLedgerSignatureConsumer(@NonNull BiConsumer consumer); + + /** + * Returns the {@link TssHandlers} for this service. + * @return the handlers + */ + TssHandlers tssHandlers(); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceComponent.java new file mode 100644 index 000000000000..7b42888bb5cd --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceComponent.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss; + +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.tss.handlers.TssMessageHandler; +import com.hedera.node.app.tss.handlers.TssSubmissions; +import com.hedera.node.app.tss.handlers.TssVoteHandler; +import dagger.BindsInstance; +import dagger.Component; +import java.util.concurrent.Executor; +import javax.inject.Singleton; + +@Singleton +@Component() +public interface TssBaseServiceComponent { + @Component.Factory + interface Factory { + TssBaseServiceComponent create( + @BindsInstance AppContext.Gossip gossip, @BindsInstance Executor submissionExecutor); + } + + TssMessageHandler tssMessageHandler(); + + TssVoteHandler tssVoteHandler(); + + TssSubmissions tssSubmissions(); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceImpl.java new file mode 100644 index 000000000000..3d9363b7090f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/TssBaseServiceImpl.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.tss.TssBaseService.Status.PENDING_LEDGER_ID; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.tss.handlers.TssHandlers; +import com.hedera.node.app.tss.handlers.TssSubmissions; +import com.hedera.node.app.tss.schemas.V0560TssBaseSchema; +import com.hedera.node.app.tss.stores.ReadableTssBaseStore; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.utility.CommonUtils; +import com.swirlds.platform.roster.RosterUtils; +import com.swirlds.state.spi.SchemaRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Default implementation of the {@link TssBaseService}. + */ +public class TssBaseServiceImpl implements TssBaseService { + private static final Logger log = LogManager.getLogger(TssBaseServiceImpl.class); + + /** + * Copy-on-write list to avoid concurrent modification exceptions if a consumer unregisters + * itself in its callback. + */ + private final List> consumers = new CopyOnWriteArrayList<>(); + + private final TssHandlers tssHandlers; + private final TssSubmissions tssSubmissions; + private final ExecutorService signingExecutor; + + /** + * The hash of the active roster being used to sign with the ledger private key. + */ + @Nullable + private Bytes activeRosterHash; + + public TssBaseServiceImpl( + @NonNull final AppContext appContext, + @NonNull final ExecutorService signingExecutor, + @NonNull final Executor submissionExecutor) { + requireNonNull(appContext); + this.signingExecutor = requireNonNull(signingExecutor); + final var component = DaggerTssBaseServiceComponent.factory().create(appContext.gossip(), submissionExecutor); + tssHandlers = new TssHandlers(component.tssMessageHandler(), component.tssVoteHandler()); + tssSubmissions = component.tssSubmissions(); + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + requireNonNull(registry); + registry.register(new V0560TssBaseSchema()); + } + + @Override + public Status getStatus( + @NonNull final Roster roster, + @NonNull final Bytes ledgerId, + @NonNull final ReadableTssBaseStore tssBaseStore) { + requireNonNull(roster); + requireNonNull(ledgerId); + requireNonNull(tssBaseStore); + // (TSS-FUTURE) Determine if the given ledger id can be recovered from the key material for the given roster + return PENDING_LEDGER_ID; + } + + @Override + public void adopt(@NonNull final Roster roster) { + requireNonNull(roster); + activeRosterHash = RosterUtils.hash(roster).getBytes(); + } + + @Override + public void bootstrapLedgerId( + @NonNull final Roster roster, + @NonNull final HandleContext context, + @NonNull final Consumer ledgerIdConsumer) { + requireNonNull(roster); + requireNonNull(context); + requireNonNull(ledgerIdConsumer); + // (TSS-FUTURE) Create a real ledger id + ledgerIdConsumer.accept(Bytes.EMPTY); + } + + @Override + public void setCandidateRoster(@NonNull final Roster roster, @NonNull final HandleContext context) { + requireNonNull(roster); + // (TSS-FUTURE) https://github.com/hashgraph/hedera-services/issues/14748 + tssSubmissions.submitTssMessage(TssMessageTransactionBody.DEFAULT, context); + } + + @Override + public void requestLedgerSignature(@NonNull final byte[] messageHash) { + requireNonNull(messageHash); + // (TSS-FUTURE) Initiate asynchronous process of creating a ledger signature + final var mockSignature = noThrowSha384HashOf(messageHash); + CompletableFuture.runAsync( + () -> consumers.forEach(consumer -> { + try { + consumer.accept(messageHash, mockSignature); + } catch (Exception e) { + log.error( + "Failed to provide signature {} on message {} to consumer {}", + CommonUtils.hex(mockSignature), + CommonUtils.hex(messageHash), + consumer, + e); + } + }), + signingExecutor); + } + + @Override + public void registerLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.add(consumer); + } + + @Override + public void unregisterLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { + requireNonNull(consumer); + consumers.remove(consumer); + } + + @Override + public TssHandlers tssHandlers() { + return tssHandlers; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssLibrary.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssLibrary.java new file mode 100644 index 000000000000..9ecd5f2164a9 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssLibrary.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import com.hedera.node.app.tss.pairings.PairingPrivateKey; +import com.hedera.node.app.tss.pairings.PairingPublicKey; +import com.hedera.node.app.tss.pairings.PairingSignature; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * A Threshold Signature Scheme Library. + * Contract of TSS: + *

      + *
    • Generate TssMessages out of PrivateShares
    • + *
    • Verify TssMessages out of a ParticipantDirectory
    • + *
    • Obtain PrivateShares out of TssMessages for each owned share
    • + *
    • Aggregate PrivateShares
    • + *
    • Obtain PublicShares out of TssMessages for each share
    • + *
    • Aggregate PublicShares
    • + *
    • Sign Messages
    • + *
    • Verify Signatures
    • + *
    • Aggregate Signatures
    • + *
    + */ +public interface TssLibrary { + + /** + * Generate a {@link TssMessage} for a {@code tssParticipantDirectory}, from a random private share. + * This method can be used to bootstrap the protocol as it does not need the existence of a previous {@link + * TssPrivateShare} + * + * @param tssParticipantDirectory the participant directory that we should generate the message for + * @return a {@link TssMessage} produced out of a random share. + */ + @NonNull + TssMessage generateTssMessage(@NonNull TssParticipantDirectory tssParticipantDirectory); + + /** + * Generate a {@link TssMessage} for a {@code tssParticipantDirectory}, for the specified {@link + * TssPrivateShare}. + * + * @param tssParticipantDirectory the participant directory that we should generate the message for + * @param privateShare the secret to use for generating new keys + * @return a TssMessage for the requested share. + */ + @NonNull + TssMessage generateTssMessage( + @NonNull TssParticipantDirectory tssParticipantDirectory, @NonNull TssPrivateShare privateShare); + + /** + * Verify that a {@link TssMessage} is valid. + * + * @param participantDirectory the participant directory used to generate the message + * @param tssMessage the {@link TssMessage} to validate + * @return true if the message is valid, false otherwise + */ + boolean verifyTssMessage(@NonNull TssParticipantDirectory participantDirectory, @NonNull TssMessage tssMessage); + + /** + * Compute all private shares that belongs to this participant from a threshold minimum number of {@link + * TssMessage}s. + * It is the responsibility of the caller to ensure that the list of validTssMessages meets the required + * threshold. + * + * @param participantDirectory the pending participant directory that we should generate the private share for + * @param validTssMessages the TSS messages to extract the private shares from. They must be previously + * validated. + * @return a sorted by sharedId list of private shares the current participant owns. + * @throws IllegalStateException if there aren't enough messages to meet the threshold + */ + @NonNull + List decryptPrivateShares( + @NonNull TssParticipantDirectory participantDirectory, @NonNull List validTssMessages); + + /** + * Aggregate a threshold number of {@link TssPrivateShare}s. + * + * @param privateShares the private shares to aggregate + * @return the aggregate private key + * @throws IllegalStateException the list of private shares does not meet the required threshold. + */ + @NonNull + PairingPrivateKey aggregatePrivateShares(@NonNull List privateShares); + + /** + * Compute all public shares for all the participants in the scheme. + * + * @param participantDirectory the participant directory that we should generate the public shares for + * @param validTssMessages the {@link TssMessage}s to extract the public shares from. They must be previously + * validated. + * @return a sorted by the sharedId list of public shares. + * @throws IllegalStateException if there aren't enough messages to meet the threshold + */ + @NonNull + List computePublicShares( + @NonNull TssParticipantDirectory participantDirectory, @NonNull List validTssMessages); + + /** + * Aggregate a threshold number of {@link TssPublicShare}s. + * It is the responsibility of the caller to ensure that the list of public shares meets the required + * threshold. + * If the threshold is not met, the public key returned by this method will be invalid. + * This method is used for two distinct purposes: + *
      + *
    • Aggregating public shares to produce the Ledger ID
    • + *
    • Aggregating public shares derived from all commitments, to produce the public key for a given + * share
    • + *
    + * + * @param publicShares the public shares to aggregate + * @return the interpolated public key + */ + @NonNull + PairingPublicKey aggregatePublicShares(@NonNull List publicShares); + + /** + * Sign a message using the private share's key. + * @param privateShare the private share to sign the message with + * @param message the message to sign + * @return the signature + */ + @NonNull + TssShareSignature sign(@NonNull TssPrivateShare privateShare, @NonNull byte[] message); + + /** + * verifies a signature using the participantDirectory and the list of public shares. + * @param participantDirectory the pending share claims the TSS message was created for + * @param publicShares the public shares to verify the signature with + * @param signature the signature to verify + * @return if the signature is valid. + */ + boolean verifySignature( + @NonNull TssParticipantDirectory participantDirectory, + @NonNull List publicShares, + @NonNull TssShareSignature signature); + + /** + * Aggregate a threshold number of {@link TssShareSignature}s. + * It is the responsibility of the caller to ensure that the list of partial signatures meets the required + * threshold. If the threshold is not met, the signature returned by this method will be invalid. + * + * @param partialSignatures the list of signatures to aggregate + * @return the interpolated signature + */ + @NonNull + PairingSignature aggregateSignatures(@NonNull List partialSignatures); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssMessage.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssMessage.java new file mode 100644 index 000000000000..8678f29e75fb --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssMessage.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A message sent as part of either genesis keying, or rekeying. + * @param bytes the byte representation of the opaque underlying structure used by the library + */ +public record TssMessage(@NonNull byte[] bytes) { + + /** + * Constructor + * @param bytes bytes the byte representation of the opaque underlying structure used by the library + */ + public TssMessage { + requireNonNull(bytes, "bytes must not be null"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssParticipantDirectory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssParticipantDirectory.java new file mode 100644 index 000000000000..907dd75edc19 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssParticipantDirectory.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.pairings.PairingPrivateKey; +import com.hedera.node.app.tss.pairings.PairingPublicKey; +import com.hedera.node.app.tss.pairings.SignatureSchema; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +/** + * Represents a directory of participants in a Threshold Signature Scheme (TSS). + *

    Each participant has an associated id (called {@code participantId}), shares count and a tss encryption public key. + * It is responsibility of the user to assign each participant with a different deterministic integer representation.

    + * + *

    The current participant is represented by a {@code self} entry, and includes {@code participantId}'s id and the tss decryption private key.

    + *

    The expected {@code participantId} is the unique {@link Integer} identification for each participant executing the scheme.

    + *
    {@code
    + * PairingPrivateKey tssDecryptionPrivateKey = ...;
    + * List tssEncryptionPublicKeys = ...;
    + * TssParticipantDirectory participantDirectory = TssParticipantDirectory.createBuilder()
    + *     //id, tss private decryption key
    + *     .self(0, persistentParticipantKey)
    + *     //id, number of shares, tss public encryption key
    + *     .withParticipant(0, 5, tssEncryptionPublicKeys.get(0))
    + *     .withParticipant(1, 2, tssEncryptionPublicKeys.get(1))
    + *     .withParticipant(2, 1, tssEncryptionPublicKeys.get(2))
    + *     .withParticipant(3, 1, tssEncryptionPublicKeys.get(3))
    + *     .withThreshold(5)
    + *     .build(signatureScheme);
    + * }
    + * + */ +public final class TssParticipantDirectory { + /** + * A sorted list of unique integer representations of each participant in the protocol. + */ + private final List sortedParticipantIds; + /** + * The list of owned {@link TssShareId} by the participant that created this directory. + */ + private final List currentParticipantOwnedShares; + /** + * Stores the owner ({@code participantId}) of each {@link TssShareId} in the protocol. + */ + private final Map shareAllocationMap; + /** + * Stores the {@link PairingPublicKey} of each {@code participantId} in the protocol. + */ + private final Map tssEncryptionPublicKeyMap; + /** + * The storage that holds the key to decrypt TssMessage parts intended for the participant that created this directory. + * It is transient to assure it does not get serialized and exposed outside. + */ + private final PrivateKeyStore tssEncryptionPrivateKey; + /** + * The minimum value that allows the recovery of Private and Public shares and that guarantees a valid signature. + */ + private final int threshold; + + /** + * Constructs a {@link TssParticipantDirectory} with the specified owned share IDs, share ownership map, public key + * map, persistent pairing private key, and threshold. + *

    + * A unique integer represents each participant in the protocol and is used as {@code participantId} + * + * @param sortedParticipantIds the sorted list of {@code participantId}s + * @param currentParticipantOwnedShares the list of owned share IDs + * @param shareAllocationMap the map of share IDs to the {@code participantId} of each participant in the + * protocol. + * @param tssEncryptionPublicKeyMap the map of participant IDs to public keys + * @param tssEncryptionPrivateKeyStore the persistent pairing private key store + * @param threshold the threshold value for the TSS + */ + private TssParticipantDirectory( + @NonNull final List sortedParticipantIds, + @NonNull final List currentParticipantOwnedShares, + @NonNull final Map shareAllocationMap, + @NonNull final Map tssEncryptionPublicKeyMap, + @NonNull final PrivateKeyStore tssEncryptionPrivateKeyStore, + final int threshold) { + this.sortedParticipantIds = + List.copyOf(Objects.requireNonNull(sortedParticipantIds, "sortedParticipantIds must not be null")); + this.currentParticipantOwnedShares = List.copyOf(Objects.requireNonNull( + currentParticipantOwnedShares, "currentParticipantOwnedShares must not be null")); + this.shareAllocationMap = + Map.copyOf(Objects.requireNonNull(shareAllocationMap, "shareAllocationMap must not be null")); + this.tssEncryptionPublicKeyMap = Map.copyOf( + Objects.requireNonNull(tssEncryptionPublicKeyMap, "tssEncryptionPublicKeyMap must not be null")); + this.tssEncryptionPrivateKey = + requireNonNull(tssEncryptionPrivateKeyStore, "tssEncryptionPrivateKeyStore must not be null"); + this.threshold = threshold; + } + + /** + * Creates a new Builder for constructing a {@link TssParticipantDirectory}. + * + * @return a new Builder instance + */ + @NonNull + public static Builder createBuilder() { + return new Builder(); + } + + /** + * Returns the threshold value. + * + * @return the threshold value + */ + public int getThreshold() { + return threshold; + } + + /** + * Returns the shares owned by the participant represented as self. + * + * @return the shares owned by the participant represented as self. + */ + @NonNull + public List getCurrentParticipantOwnedShares() { + return currentParticipantOwnedShares; + } + + /** + * Return the list of all the shareIds. + * + * @return the list of all the shareIds + */ + @NonNull + public List getShareIds() { + return shareAllocationMap.entrySet().stream() + .sorted(Entry.comparingByValue()) + .map(Entry::getKey) + .toList(); + } + + /** + * A builder for creating {@link TssParticipantDirectory} instances. + */ + public static class Builder { + private SelfEntry selfEntry; + private final Map participantEntries = new HashMap<>(); + private int threshold; + + private Builder() {} + + /** + * Sets the self entry for the builder. + * + * @param participantId the participant unique {@link Integer} representation + * @param tssEncryptionPrivateKey the pairing private key used to decrypt tss share portions + * @return the builder instance + */ + @NonNull + public Builder withSelf(final int participantId, @NonNull final PairingPrivateKey tssEncryptionPrivateKey) { + if (selfEntry != null) { + throw new IllegalArgumentException("There is already an for the current participant"); + } + selfEntry = new SelfEntry(participantId, new PrivateKeyStore(tssEncryptionPrivateKey)); + return this; + } + + /** + * Sets the threshold value for the TSS. + * + * @param threshold the threshold value + * @return the builder instance + * @throws IllegalArgumentException if threshold is less than or equals to 0 + */ + @NonNull + public Builder withThreshold(final int threshold) { + if (threshold <= 0) { + throw new IllegalArgumentException("Invalid threshold: " + threshold); + } + this.threshold = threshold; + return this; + } + + /** + * Adds a participant entry to the builder. + * + * @param participantId the participant unique {@link Integer} representation + * @param numberOfShares the number of shares + * @param tssEncryptionPublicKey the pairing public key used to encrypt tss share portions designated to the participant represented by this entry + * @return the builder instance + * @throws IllegalArgumentException if participantId was previously added. + */ + @NonNull + public Builder withParticipant( + final int participantId, + final int numberOfShares, + @NonNull final PairingPublicKey tssEncryptionPublicKey) { + if (participantEntries.containsKey(participantId)) + throw new IllegalArgumentException( + "Participant with id " + participantId + " was previously added to the directory"); + + participantEntries.put(participantId, new ParticipantEntry(numberOfShares, tssEncryptionPublicKey)); + return this; + } + + /** + * Builds and returns a {@link TssParticipantDirectory} instance based on the provided entries and schema. + * + * @param schema the signature schema + * @return the constructed ParticipantDirectory instance + * @throws NullPointerException if schema is null + * @throws IllegalStateException if there is no entry for the current participant + * @throws IllegalStateException if there are no configured participants + * @throws IllegalStateException if the threshold value is higher than the total shares + */ + @NonNull + public TssParticipantDirectory build(@NonNull final SignatureSchema schema) { + Objects.requireNonNull(schema, "Schema must not be null"); + + if (isNull(selfEntry)) { + throw new IllegalStateException("There should be an entry for the current participant"); + } + + if (participantEntries.isEmpty()) { + throw new IllegalStateException("There should be at least one participant in the protocol"); + } + + if (!participantEntries.containsKey(selfEntry.participantId())) { + throw new IllegalStateException( + "The participant list does not contain a reference to the current participant"); + } + + // Get the total number of shares of to distribute in the protocol + final int totalShares = participantEntries.values().stream() + .map(ParticipantEntry::shareCount) + .reduce(0, Integer::sum); + + if (threshold > totalShares) { + throw new IllegalStateException("Threshold exceeds the number of shares"); + } + + // Create a sorted list of ShareId's from 1 to totalShares + 1 + final List ids = IntStream.range(1, totalShares + 1) + .boxed() + // In the future, when paring api is implemented, we need to: + // .map(schema.getField()::elementFromLong) + .map(TssShareId::new) + .toList(); + + // Create a sorted list of participants to make sure we assign the shares in the right order. + final List sortedParticipantIds = + participantEntries.keySet().stream().sorted().toList(); + + final Map sharesAllocationMap = + new HashMap<>(); /*To keep track of each share id owner*/ + final List currentParticipantOwnedShareIds = + new ArrayList<>(); /*To keep track of the shares owned by the creator of this directory*/ + final Map tssEncryptionPublicKeyMap = + new HashMap<>(); /*The encryption key of each participant*/ + + final AtomicInteger assignedShares = new AtomicInteger(0); /*Counter for assigned shares*/ + + // Iteration of the sorted int representation to make sure we assign the shares deterministically. + sortedParticipantIds.forEach(participantId -> { + final ParticipantEntry entry = participantEntries.get(participantId); + + // Add the public encryption key for each participant id in the iteration. + // here the order is not important, but we reuse the iteration. + tssEncryptionPublicKeyMap.put(participantId, entry.tssEncryptionPublicKey()); + + IntStream.range(0, entry.shareCount()).forEach(i -> { + final TssShareId tssShareId = ids.get(assignedShares.getAndIncrement()); + sharesAllocationMap.put(tssShareId, participantId); + // Keep a separated collection for the current participant shares + if (participantId.equals(selfEntry.participantId())) { + currentParticipantOwnedShareIds.add(tssShareId); + } + }); + }); + + return new TssParticipantDirectory( + sortedParticipantIds, + currentParticipantOwnedShareIds, + sharesAllocationMap, + tssEncryptionPublicKeyMap, + selfEntry.tssEncryptionPrivateKeyStore, + threshold); + } + } + + /** + * A class for storing the private key as protection for unintentional serialization or exposition of the data. + */ + private static final class PrivateKeyStore { + transient PairingPrivateKey privateKey; + + public PrivateKeyStore(@NonNull final PairingPrivateKey privateKey) { + this.privateKey = privateKey; + } + + @NonNull + private PairingPrivateKey getPrivateKey() { + return privateKey; + } + + @Override + public String toString() { + return "PrivateKeyStore<>"; + } + } + + /** + * Represents an entry for the participant executing the protocol, containing the ID and private key. + * @param participantId identification of the participant + */ + private record SelfEntry(int participantId, @NonNull PrivateKeyStore tssEncryptionPrivateKeyStore) { + /** + * Constructor + * @param participantId identification of the participant + */ + public SelfEntry { + requireNonNull(tssEncryptionPrivateKeyStore, "tssEncryptionPrivateKeyStore must not be null"); + } + } + + /** + * Represents an entry for a participant, containing the ID, share count, and public key. + * @param shareCount number of shares owned by the participant represented by this record + * @param tssEncryptionPublicKey the pairing public key used to encrypt tss share portions designated to the participant represented by this record + */ + private record ParticipantEntry(int shareCount, @NonNull PairingPublicKey tssEncryptionPublicKey) { + /** + * Constructor + * + * @param shareCount number of shares owned by the participant represented by this record + * @param tssEncryptionPublicKey the pairing public key used to encrypt tss share portions designated to the participant represented by this record + */ + public ParticipantEntry { + requireNonNull(tssEncryptionPublicKey, "tssEncryptionPublicKey must not be null"); + } + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPrivateShare.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPrivateShare.java new file mode 100644 index 000000000000..9f93fcd7bc5b --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPrivateShare.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.pairings.PairingPrivateKey; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A record that contains a share ID, and the corresponding private key. + * + * @param shareId the share ID + * @param privateKey the private key + */ +public record TssPrivateShare(@NonNull TssShareId shareId, @NonNull PairingPrivateKey privateKey) { + /** + * Constructor + * + * @param shareId the share ID + * @param privateKey the private key + */ + public TssPrivateShare { + requireNonNull(shareId, "shareId must not be null"); + requireNonNull(shareId, "privateKey must not be null"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPublicShare.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPublicShare.java new file mode 100644 index 000000000000..7fff4445b906 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssPublicShare.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.pairings.PairingPublicKey; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A record that contains a share ID, and the corresponding public key. + * + * @param shareId the share ID + * @param publicKey the public key + */ +public record TssPublicShare(@NonNull TssShareId shareId, @NonNull PairingPublicKey publicKey) { + /** + * Constructor + * + * @param shareId the share ID + * @param publicKey the public key + */ + public TssPublicShare { + requireNonNull(shareId, "shareId must not be null"); + requireNonNull(shareId, "publicKey must not be null"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareId.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareId.java new file mode 100644 index 000000000000..a978d0e3189d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareId.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The ID of a TSS share. + * + * @param idElement the field element that represents the share ID + */ +public record TssShareId(@NonNull Integer /*This will be a FieldElement from Pairings-Api*/ idElement) { + /** + * Constructor. + * + * @param idElement the field element that represents the share ID + */ + public TssShareId { + requireNonNull(idElement, "idElement must not be null"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareSignature.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareSignature.java new file mode 100644 index 000000000000..d9c8dc1a43a9 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/api/TssShareSignature.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.api; + +import static java.util.Objects.requireNonNull; + +import com.hedera.node.app.tss.pairings.PairingSignature; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Represents a signature created for a {@link TssPrivateShare}. + * + * @param shareId the share ID + * @param signature the signature + */ +public record TssShareSignature(@NonNull TssShareId shareId, @NonNull PairingSignature signature) { + /** + * Constructor. + * + * @param shareId the share ID + * @param signature the signature + */ + public TssShareSignature { + requireNonNull(shareId, "shareId must not be null"); + requireNonNull(signature, "signature must not be null"); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssHandlers.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssHandlers.java new file mode 100644 index 000000000000..8c20a0418f14 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssHandlers.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import com.hedera.node.app.spi.workflows.TransactionHandler; +import edu.umd.cs.findbugs.annotations.NonNull; + +public record TssHandlers(@NonNull TransactionHandler tssMessageHandler, @NonNull TransactionHandler tssVoteHandler) {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssMessageHandler.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssMessageHandler.java new file mode 100644 index 000000000000..c5bdc358fb03 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssMessageHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import com.hedera.node.app.spi.workflows.TransactionHandler; +import edu.umd.cs.findbugs.annotations.NonNull; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Validates and potentially responds with a vote to a {@link TssMessageTransactionBody}. + * (TSS-FUTURE) Tracked here. + */ +@Singleton +public class TssMessageHandler implements TransactionHandler { + private final TssSubmissions submissionManager; + + @Inject + public TssMessageHandler(@NonNull final TssSubmissions submissionManager) { + this.submissionManager = requireNonNull(submissionManager); + } + + @Override + public void preHandle(@NonNull PreHandleContext context) throws PreCheckException { + requireNonNull(context); + } + + @Override + public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { + requireNonNull(txn); + } + + @Override + public void handle(@NonNull final HandleContext context) throws HandleException { + requireNonNull(context); + submissionManager.submitTssVote(TssVoteTransactionBody.DEFAULT, context); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssSubmissions.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssSubmissions.java new file mode 100644 index 000000000000..1daee578a8d4 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssSubmissions.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Duration; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.config.data.HederaConfig; +import com.hedera.node.config.data.TssConfig; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Singleton +public class TssSubmissions { + private static final Logger log = LogManager.getLogger(TssSubmissions.class); + + private static final String DUPLICATE_TRANSACTION_REASON = "" + DUPLICATE_TRANSACTION; + + private final AppContext.Gossip gossip; + private final Executor submissionExecutor; + + @Inject + public TssSubmissions(@NonNull final AppContext.Gossip gossip, @NonNull final Executor submissionExecutor) { + this.gossip = requireNonNull(gossip); + this.submissionExecutor = requireNonNull(submissionExecutor); + } + + /** + * Attempts to submit a TSS message to the network. + * + * @param body the TSS message to submit + * @param context the TSS context + * @return a future that completes when the message has been submitted + */ + public CompletableFuture submitTssMessage( + @NonNull final TssMessageTransactionBody body, @NonNull final HandleContext context) { + requireNonNull(body); + requireNonNull(context); + return submit(b -> b.tssMessage(body), context); + } + + /** + * Attempts to submit a TSS vote to the network. + * + * @param body the TSS vote to submit + * @param context the TSS context + * @return a future that completes when the vote has been submitted + */ + public CompletableFuture submitTssVote( + @NonNull final TssVoteTransactionBody body, @NonNull final HandleContext context) { + requireNonNull(body); + requireNonNull(context); + return submit(b -> b.tssVote(body), context); + } + + private CompletableFuture submit( + @NonNull final Consumer spec, @NonNull final HandleContext context) { + final var config = context.configuration(); + final var tssConfig = config.getConfigData(TssConfig.class); + final var hederaConfig = config.getConfigData(HederaConfig.class); + final var validDuration = new Duration(hederaConfig.transactionMaxValidDuration()); + final var validStartTime = new AtomicReference<>(context.consensusNow()); + final var attemptsLeft = new AtomicInteger(tssConfig.timesToTrySubmission()); + return CompletableFuture.runAsync( + () -> { + var fatalFailure = false; + var failureReason = ""; + TransactionBody body; + do { + int txnIdsLeft = tssConfig.distinctTxnIdsToTry(); + do { + final var builder = builderWith( + validStartTime.get(), + context.networkInfo().selfNodeInfo().accountId(), + validDuration); + spec.accept(builder); + body = builder.build(); + try { + gossip.submit(body); + return; + } catch (IllegalArgumentException iae) { + failureReason = iae.getMessage(); + if (DUPLICATE_TRANSACTION_REASON.equals(failureReason)) { + validStartTime.set(validStartTime.get().plusNanos(1)); + } else { + fatalFailure = true; + break; + } + } catch (IllegalStateException ise) { + failureReason = ise.getMessage(); + // There is no point to retry immediately except on a duplicate id + break; + } + } while (txnIdsLeft-- > 1); + log.warn("Failed to submit {} ({})", body, failureReason); + try { + MILLISECONDS.sleep(tssConfig.retryDelay().toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting to retry " + body, e); + } + } while (!fatalFailure && attemptsLeft.decrementAndGet() > 0); + throw new IllegalStateException(failureReason); + }, + submissionExecutor); + } + + private TransactionBody.Builder builderWith( + @NonNull final Instant validStartTime, + @NonNull final AccountID selfAccountId, + @NonNull final Duration validDuration) { + return TransactionBody.newBuilder() + .nodeAccountID(selfAccountId) + .transactionValidDuration(validDuration) + .transactionID(new TransactionID(asTimestamp(validStartTime), selfAccountId, false, 0)); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssVoteHandler.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssVoteHandler.java new file mode 100644 index 000000000000..d0d20c44b5ed --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/handlers/TssVoteHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import com.hedera.node.app.spi.workflows.TransactionHandler; +import edu.umd.cs.findbugs.annotations.NonNull; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Validates and responds to a {@link TssVoteTransactionBody}. + *

    Tracked here + */ +@Singleton +public class TssVoteHandler implements TransactionHandler { + @Inject + public TssVoteHandler() { + // Dagger2 + } + + @Override + public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { + requireNonNull(context); + } + + @Override + public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { + requireNonNull(txn); + } + + @Override + public void handle(@NonNull final HandleContext context) throws HandleException { + requireNonNull(context); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java deleted file mode 100644 index 66d8cc3a1901..000000000000 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseService.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.tss.impl; - -import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; -import static java.util.Objects.requireNonNull; - -import com.hedera.node.app.tss.TssBaseService; -import com.hedera.node.app.tss.schemas.V0560TSSSchema; -import com.swirlds.common.utility.CommonUtils; -import com.swirlds.state.spi.SchemaRegistry; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.function.BiConsumer; -import javax.inject.Inject; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * Placeholder for the TSS base service, added to support testing production of indirect block proofs, - * c.f. this issue. - */ -public class PlaceholderTssBaseService implements TssBaseService { - private static final Logger log = LogManager.getLogger(PlaceholderTssBaseService.class); - - /** - * Copy-on-write list to avoid concurrent modification exceptions if a consumer unregisters - * itself in its callback. - */ - private final List> consumers = new CopyOnWriteArrayList<>(); - - private ExecutorService executor; - - @Inject - public void setExecutor(@NonNull final ExecutorService executor) { - this.executor = requireNonNull(executor); - } - - @Override - public void registerSchemas(@NonNull final SchemaRegistry registry) { - requireNonNull(registry); - registry.register(new V0560TSSSchema()); - } - - @Override - public void requestLedgerSignature(@NonNull final byte[] messageHash) { - requireNonNull(messageHash); - requireNonNull(executor); - // Simulate asynchronous completion of the ledger signature - final var mockSignature = noThrowSha384HashOf(messageHash); - CompletableFuture.runAsync( - () -> consumers.forEach(consumer -> { - try { - consumer.accept(messageHash, mockSignature); - } catch (Exception e) { - log.error( - "Failed to provide signature {} on message {} to consumer {}", - CommonUtils.hex(mockSignature), - CommonUtils.hex(messageHash), - consumer, - e); - } - }), - executor); - } - - @Override - public void registerLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { - requireNonNull(consumer); - consumers.add(consumer); - } - - @Override - public void unregisterLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { - requireNonNull(consumer); - consumers.remove(consumer); - } -} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeFieldElement.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeFieldElement.java new file mode 100644 index 000000000000..df08a197dc80 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeFieldElement.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import org.jetbrains.annotations.NotNull; + +public class FakeFieldElement implements FieldElement { + + private final BigInteger value; + + public FakeFieldElement(@NonNull BigInteger value) { + this.value = value; + } + + @NotNull + @Override + public BigInteger toBigInteger() { + return value; + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeGroupElement.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeGroupElement.java new file mode 100644 index 000000000000..7e5e03ebd63f --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FakeGroupElement.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import org.jetbrains.annotations.NotNull; + +public class FakeGroupElement implements GroupElement { + public static final FakeGroupElement GENERATOR = new FakeGroupElement(BigInteger.valueOf(5L)); + private final BigInteger value; + + public FakeGroupElement(@NonNull BigInteger value) { + this.value = value; + } + + @NotNull + @Override + public GroupElement add(@NotNull GroupElement other) { + return new FakeGroupElement(value.add(new BigInteger(other.toBytes()))); + } + + @NotNull + @Override + public byte[] toBytes() { + return value.toByteArray(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FieldElement.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FieldElement.java new file mode 100644 index 000000000000..f913e4d65490 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/FieldElement.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; + +public interface FieldElement { + /** + * Returns the field element as a BigInteger + * + * @return the field element as a BigInteger + */ + @NonNull + BigInteger toBigInteger(); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/GroupElement.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/GroupElement.java new file mode 100644 index 000000000000..d237885bd399 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/GroupElement.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface GroupElement { + /** + * Adds this group element with another + * + * @param other the other group element + * @return a new group element which is the addition of this element and another + */ + @NonNull + GroupElement add(@NonNull GroupElement other); + + /** + * Returns the external byte array representation of the group element + * + * @return the external byte array representation of the group element + */ + @NonNull + byte[] toBytes(); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPrivateKey.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPrivateKey.java new file mode 100644 index 000000000000..7ac7c92baf61 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPrivateKey.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public record PairingPrivateKey(@NonNull FieldElement privateKey, @NonNull SignatureSchema signatureSchema) { + public PairingPublicKey createPublicKey() { + final var privateKeyElement = new FakeGroupElement(privateKey.toBigInteger()); + GroupElement publicKey = FakeGroupElement.GENERATOR.add(privateKeyElement); + return new PairingPublicKey(publicKey, this.signatureSchema); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPublicKey.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPublicKey.java new file mode 100644 index 000000000000..4103bd103a69 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingPublicKey.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +public record PairingPublicKey(GroupElement publicKey, SignatureSchema signatureSchema) {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingSignature.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingSignature.java new file mode 100644 index 000000000000..ccd386444fb1 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/PairingSignature.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.tss.pairings.FakeGroupElement.GENERATOR; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; + +public record PairingSignature(@NonNull GroupElement signature, @NonNull SignatureSchema signatureSchema) { + // sig = privateKey + messageHash + // publicKey = generator + privateKey + // messageHash = sig - (publicKey - generator) = sig - publicKey + generator + public boolean verify(PairingPublicKey publicKey, byte[] message) { + final var sig = new BigInteger(1, this.signature().toBytes()); + final var pk = new BigInteger(1, publicKey.publicKey().toBytes()); + final var gen = new BigInteger(1, GENERATOR.toBytes()); + final var msgHashFromSig = new BigInteger(1, sig.subtract(pk).add(gen).toByteArray()); + final var messageHash = new BigInteger(1, noThrowSha384HashOf(message)); + return messageHash.equals(msgHashFromSig); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/SignatureSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/SignatureSchema.java new file mode 100644 index 000000000000..a9fe27fb29fe --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/pairings/SignatureSchema.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.pairings; + +public record SignatureSchema() { + public static SignatureSchema create(byte[] bytes) { + return new SignatureSchema(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TSSSchema.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TssBaseSchema.java similarity index 86% rename from hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TSSSchema.java rename to hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TssBaseSchema.java index 2df3da4745e2..204ff65bcd2d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TSSSchema.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/schemas/V0560TssBaseSchema.java @@ -19,20 +19,17 @@ import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.state.tss.TssMessageMapKey; import com.hedera.hapi.node.state.tss.TssVoteMapKey; -import com.hedera.hapi.node.tss.TssMessageTransactionBody; -import com.hedera.hapi.node.tss.TssVoteTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.swirlds.state.spi.Schema; import com.swirlds.state.spi.StateDefinition; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Set; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * Schema for the TSS service. */ -public class V0560TSSSchema extends Schema { - private static final Logger log = LogManager.getLogger(V0560TSSSchema.class); +public class V0560TssBaseSchema extends Schema { public static final String TSS_MESSAGE_MAP_KEY = "TSS_MESSAGES"; public static final String TSS_VOTE_MAP_KEY = "TSS_VOTES"; /** @@ -52,7 +49,7 @@ public class V0560TSSSchema extends Schema { /** * Create a new instance */ - public V0560TSSSchema() { + public V0560TssBaseSchema() { super(VERSION); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssBaseStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssBaseStore.java new file mode 100644 index 000000000000..ef30e0d68d3d --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssBaseStore.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.stores; + +import static com.hedera.node.app.tss.schemas.V0560TssBaseSchema.TSS_MESSAGE_MAP_KEY; +import static com.hedera.node.app.tss.schemas.V0560TssBaseSchema.TSS_VOTE_MAP_KEY; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.tss.TssMessageMapKey; +import com.hedera.hapi.node.state.tss.TssVoteMapKey; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.swirlds.state.spi.ReadableKVState; +import com.swirlds.state.spi.ReadableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Provides read-only access to the TSS base store. + */ +public class ReadableTssBaseStore implements ReadableTssStore { + /** + * The underlying data storage class that holds the airdrop data. + */ + private final ReadableKVState readableTssMessageState; + + private final ReadableKVState readableTssVoteState; + + /** + * Create a new {@link ReadableTssBaseStore} instance. + * + * @param states The state to use. + */ + public ReadableTssBaseStore(@NonNull final ReadableStates states) { + requireNonNull(states); + this.readableTssMessageState = states.get(TSS_MESSAGE_MAP_KEY); + this.readableTssVoteState = states.get(TSS_VOTE_MAP_KEY); + } + + /** + * {@inheritDoc} + */ + @Override + public TssMessageTransactionBody getMessage(@NonNull final TssMessageMapKey tssMessageKey) { + return readableTssMessageState.get(tssMessageKey); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean exists(@NonNull final TssMessageMapKey tssMessageKey) { + return readableTssMessageState.contains(tssMessageKey); + } + + /** + * {@inheritDoc} + */ + @Override + public TssVoteTransactionBody getVote(@NonNull final TssVoteMapKey tssVoteKey) { + return readableTssVoteState.get(tssVoteKey); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean exists(@NonNull final TssVoteMapKey tssVoteKey) { + return readableTssVoteState.contains(tssVoteKey); + } + + @Override + public long messageStateSize() { + return readableTssMessageState.size(); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStore.java new file mode 100644 index 000000000000..7ad75dd826ff --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/ReadableTssStore.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.stores; + +import com.hedera.hapi.node.state.tss.TssMessageMapKey; +import com.hedera.hapi.node.state.tss.TssVoteMapKey; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface ReadableTssStore { + /** + * Get the TSS message for the given key. + * + * @param TssMessageMapKey The key to look up. + * @return The TSS message, or null if not found. + */ + TssMessageTransactionBody getMessage(@NonNull TssMessageMapKey TssMessageMapKey); + + /** + * Check if a TSS message exists for the given key. + * + * @param tssMessageMapKey The key to check. + * @return True if a TSS message exists for the given key, false otherwise. + */ + boolean exists(@NonNull TssMessageMapKey tssMessageMapKey); + + /** + * Get the TSS vote for the given key. + * + * @param tssVoteMapKey The key to look up. + * @return The TSS vote, or null if not found. + */ + TssVoteTransactionBody getVote(@NonNull TssVoteMapKey tssVoteMapKey); + + /** + * Check if a TSS vote exists for the given key. + * + * @param tssVoteMapKey The key to check. + * @return True if a TSS vote exists for the given key, false otherwise. + */ + boolean exists(@NonNull TssVoteMapKey tssVoteMapKey); + + /** + * Get the number of entries in the TSS message state. + * + * @return The number of entries in the tss message state. + */ + long messageStateSize(); +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssBaseStore.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssBaseStore.java new file mode 100644 index 000000000000..e320460e8715 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/tss/stores/WritableTssBaseStore.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.stores; + +import static com.hedera.node.app.tss.schemas.V0560TssBaseSchema.TSS_MESSAGE_MAP_KEY; +import static com.hedera.node.app.tss.schemas.V0560TssBaseSchema.TSS_VOTE_MAP_KEY; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.state.tss.TssMessageMapKey; +import com.hedera.hapi.node.state.tss.TssVoteMapKey; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.swirlds.state.spi.WritableKVState; +import com.swirlds.state.spi.WritableStates; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Extends the {@link ReadableTssBaseStore} with write access to the TSS base store. + */ +public class WritableTssBaseStore extends ReadableTssBaseStore { + /** + * The underlying data storage class that holds the Pending Airdrops data. + */ + private final WritableKVState tssMessageState; + + private final WritableKVState tssVoteState; + + public WritableTssBaseStore(@NonNull final WritableStates states) { + super(states); + this.tssMessageState = states.get(TSS_MESSAGE_MAP_KEY); + this.tssVoteState = states.get(TSS_VOTE_MAP_KEY); + } + + public void put(@NonNull final TssMessageMapKey tssMessageMapKey, @NonNull final TssMessageTransactionBody txBody) { + requireNonNull(tssMessageMapKey); + requireNonNull(txBody); + tssMessageState.put(tssMessageMapKey, txBody); + } + + public void put(@NonNull final TssVoteMapKey tssVoteMapKey, @NonNull final TssVoteTransactionBody txBody) { + requireNonNull(tssVoteMapKey); + requireNonNull(txBody); + tssVoteState.put(tssVoteMapKey, txBody); + } + + public void remove(@NonNull final TssMessageMapKey tssMessageMapKey) { + requireNonNull(tssMessageMapKey); + tssMessageState.remove(tssMessageMapKey); + } + + public void remove(@NonNull final TssVoteMapKey tssVoteMapKey) { + requireNonNull(tssVoteMapKey); + tssVoteState.remove(tssVoteMapKey); + } + + public void clear() { + tssVoteState.keys().forEachRemaining(tssVoteState::remove); + tssMessageState.keys().forEachRemaining(tssMessageState::remove); + } +} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/metric/HandleWorkflowMetrics.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/OpWorkflowMetrics.java similarity index 90% rename from hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/metric/HandleWorkflowMetrics.java rename to hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/OpWorkflowMetrics.java index 4aa66a649795..62ee0377066a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/metric/HandleWorkflowMetrics.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/OpWorkflowMetrics.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.workflows.handle.metric; +package com.hedera.node.app.workflows; import static java.util.Objects.requireNonNull; @@ -34,10 +34,10 @@ import javax.inject.Singleton; /** - * A class to handle the metrics for the handle-workflow + * A class to handle the metrics for all operations (transactions and queries) */ @Singleton -public class HandleWorkflowMetrics { +public class OpWorkflowMetrics { private static final BinaryOperator AVERAGE = (sum, count) -> count == 0 ? 0 : sum / count; @@ -53,12 +53,12 @@ public class HandleWorkflowMetrics { private long gasUsedThisConsensusSecond = 0L; /** - * Constructor for the HandleWorkflowMetrics + * Constructor for the OpWorkflowMetrics * * @param metrics the {@link Metrics} object where all metrics will be registered */ @Inject - public HandleWorkflowMetrics(@NonNull final Metrics metrics, @NonNull final ConfigProvider configProvider) { + public OpWorkflowMetrics(@NonNull final Metrics metrics, @NonNull final ConfigProvider configProvider) { requireNonNull(metrics, "metrics must not be null"); requireNonNull(configProvider, "configProvider must not be null"); @@ -88,9 +88,9 @@ public HandleWorkflowMetrics(@NonNull final Metrics metrics, @NonNull final Conf * Update the metrics for the given functionality * * @param functionality the {@link HederaFunctionality} for which the metrics will be updated - * @param duration the duration of the transaction in {@code ns} + * @param duration the duration of the operation in {@code ns} */ - public void updateTransactionDuration(@NonNull final HederaFunctionality functionality, final int duration) { + public void updateDuration(@NonNull final HederaFunctionality functionality, final int duration) { requireNonNull(functionality, "functionality must not be null"); if (functionality == HederaFunctionality.NONE) { return; diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java index 98fc4abe168d..d76e927191e2 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java @@ -222,6 +222,9 @@ private TransactionHandler getHandler(@NonNull final TransactionBody txBody) { default -> throw new UnsupportedOperationException(SYSTEM_UNDELETE_WITHOUT_ID_CASE); }; + case TSS_MESSAGE -> handlers.tssMessageHandler(); + case TSS_VOTE -> handlers.tssVoteHandler(); + default -> throw new UnsupportedOperationException(TYPE_NOT_SUPPORTED); }; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java index d361c5838d8c..a69b6601b854 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java @@ -70,6 +70,7 @@ import com.hedera.node.app.service.token.impl.handlers.TokenUpdateHandler; import com.hedera.node.app.service.token.impl.handlers.TokenUpdateNftsHandler; import com.hedera.node.app.service.util.impl.handlers.UtilPrngHandler; +import com.hedera.node.app.spi.workflows.TransactionHandler; import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -130,4 +131,6 @@ public record TransactionHandlers( @NonNull NodeUpdateHandler nodeUpdateHandler, @NonNull NodeDeleteHandler nodeDeleteHandler, @NonNull TokenClaimAirdropHandler tokenClaimAirdropHandler, - @NonNull UtilPrngHandler utilPrngHandler) {} + @NonNull UtilPrngHandler utilPrngHandler, + @NonNull TransactionHandler tssMessageHandler, + @NonNull TransactionHandler tssVoteHandler) {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java index 940c1f2e9328..eb6cd3288d20 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchHandleContext.java @@ -180,9 +180,8 @@ public boolean tryToChargePayer(final long amount) { return feeAccumulator.chargeNetworkFee(payerId, amount); } - @NonNull @Override - public Configuration configuration() { + public @NonNull Configuration configuration() { return config; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchProcessor.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchProcessor.java index 032c2204fead..1c3425037bfb 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchProcessor.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/DispatchProcessor.java @@ -26,6 +26,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.NODE; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.PRECEDING; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.workflows.handle.HandleWorkflow.ALERT_MESSAGE; @@ -48,6 +49,7 @@ import com.hedera.node.app.workflows.handle.steps.SystemFileUpdates; import com.hedera.node.app.workflows.handle.throttle.DispatchUsageManager; import com.hedera.node.app.workflows.handle.throttle.ThrottleException; +import com.hedera.node.config.data.NetworkAdminConfig; import com.swirlds.state.spi.info.NetworkInfo; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -122,7 +124,7 @@ public void processDispatch(@NonNull final Dispatch dispatch) { } dispatchUsageManager.finalizeAndSaveUsage(dispatch); recordFinalizer.finalizeRecord(dispatch); - if (dispatch.txnCategory() == USER) { + if (dispatch.txnCategory() == USER || dispatch.txnCategory() == NODE) { dispatch.stack().commitTransaction(dispatch.recordBuilder()); } else { dispatch.stack().commitFullStack(); @@ -146,7 +148,9 @@ private void tryHandle(@NonNull final Dispatch dispatch, @NonNull final Validati dispatcher.dispatchHandle(dispatch.handleContext()); dispatch.recordBuilder().status(SUCCESS); // Only user or preceding transactions can trigger system updates in the current system - if (dispatch.txnCategory() == USER || dispatch.txnCategory() == PRECEDING) { + if (dispatch.txnCategory() == USER + || dispatch.txnCategory() == PRECEDING + || dispatch.txnCategory() == NODE) { handleSystemUpdates(dispatch); } } catch (HandleException e) { @@ -244,7 +248,7 @@ private void chargePayer(@NonNull final Dispatch dispatch, @NonNull final Valida final var shouldWaiveServiceFee = report.serviceFeeStatus() == UNABLE_TO_PAY_SERVICE_FEE || report.duplicateStatus() == DUPLICATE; final var feesToCharge = shouldWaiveServiceFee ? fees.withoutServiceComponent() : fees; - if (dispatch.txnCategory() == USER) { + if (dispatch.txnCategory() == USER || dispatch.txnCategory() == NODE) { dispatch.feeAccumulator() .chargeFees(report.payerOrThrow().accountIdOrThrow(), report.creatorId(), feesToCharge); } else { @@ -306,8 +310,18 @@ private boolean alreadyFailed(@NonNull final Dispatch dispatch, @NonNull final V * @param dispatch the dispatch to be processed */ private @Nullable ResponseCodeEnum maybeAuthorizationFailure(@NonNull final Dispatch dispatch) { - if (!authorizer.isAuthorized(dispatch.payerId(), dispatch.txnInfo().functionality())) { - return dispatch.txnInfo().functionality() == SYSTEM_DELETE ? NOT_SUPPORTED : UNAUTHORIZED; + final var function = dispatch.txnInfo().functionality(); + if (!authorizer.isAuthorized(dispatch.payerId(), function)) { + // Node transactions are judged by a different set of rules; any node account can submit + // any node transaction as long as it is in the allow list + if (dispatch.txnCategory() == NODE) { + final var adminConfig = dispatch.config().getConfigData(NetworkAdminConfig.class); + final var allowList = adminConfig.nodeTransactionsAllowList().functionalitySet(); + if (allowList.contains(function)) { + return null; + } + } + return function == SYSTEM_DELETE ? NOT_SUPPORTED : UNAUTHORIZED; } final var failure = authorizer.hasPrivilegedAuthorization( dispatch.payerId(), diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleOutput.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleOutput.java index 68092a48ded3..3f1a1cc36e61 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleOutput.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleOutput.java @@ -18,35 +18,34 @@ import static java.util.Objects.requireNonNull; -import com.hedera.hapi.block.stream.BlockItem; -import com.hedera.node.app.state.SingleTransactionRecord; +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.node.app.state.recordcache.BlockRecordSource; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.List; /** * A temporary wrapper class as we transition from the V6 record stream to the block stream; - * includes at least one of, - *

      - *
    1. The V6 record stream items,
    2. - *
    3. The block stream output items
    4. - *
    - * @param blockItems maybe the block stream output items - * @param recordStreamItems maybe the V6 record stream items + * includes at least one of the V6 record stream and/or the block stream output from a user transaction. + * + * @param blockRecordSource maybe the block stream output items + * @param recordSource maybe record source derived from the V6 record stream items */ -public record HandleOutput( - @Nullable List blockItems, @Nullable List recordStreamItems) { +public record HandleOutput(@Nullable BlockRecordSource blockRecordSource, @Nullable RecordSource recordSource) { public HandleOutput { - if (blockItems == null) { - requireNonNull(recordStreamItems); + if (blockRecordSource == null) { + requireNonNull(recordSource); } } - public @NonNull List recordsOrThrow() { - return requireNonNull(recordStreamItems); + public @NonNull RecordSource recordSourceOrThrow() { + return requireNonNull(recordSource); + } + + public @NonNull BlockRecordSource blockRecordSourceOrThrow() { + return requireNonNull(blockRecordSource); } - public @NonNull List blocksItemsOrThrow() { - return requireNonNull(blockItems); + public @NonNull RecordSource preferringBlockRecordSource() { + return blockRecordSource != null ? blockRecordSource : requireNonNull(recordSource); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java index cd0a7009833a..1c0675c5800f 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflow.java @@ -18,6 +18,8 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.BUSY; import static com.hedera.hapi.node.base.ResponseCodeEnum.FAIL_INVALID; +import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; +import static com.hedera.node.app.service.file.impl.schemas.V0490FileSchema.BLOBS_KEY; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.NOOP_RECORD_CUSTOMIZER; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REVERSIBLE; @@ -28,7 +30,11 @@ import static com.hedera.node.app.state.logging.TransactionStateLogger.logStartUserTransactionPreHandleResultP3; import static com.hedera.node.app.state.merkle.VersionUtils.isSoOrdered; import static com.hedera.node.app.workflows.handle.TransactionType.GENESIS_TRANSACTION; +import static com.hedera.node.app.workflows.handle.TransactionType.ORDINARY_TRANSACTION; import static com.hedera.node.app.workflows.handle.TransactionType.POST_UPGRADE_TRANSACTION; +import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.NODE_DUE_DILIGENCE_FAILURE; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.swirlds.platform.system.InitTrigger.EVENT_STREAM_RECOVERY; import static com.swirlds.state.spi.HapiUtils.SEMANTIC_VERSION_COMPARATOR; import static java.util.Objects.requireNonNull; @@ -40,7 +46,9 @@ import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.SemanticVersion; import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.hapi.util.HapiUtils; import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.impl.BlockStreamBuilder; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; @@ -48,6 +56,8 @@ import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.records.BlockRecordManager; +import com.hedera.node.app.records.BlockRecordService; +import com.hedera.node.app.service.file.FileService; import com.hedera.node.app.service.schedule.ScheduleService; import com.hedera.node.app.service.schedule.WritableScheduleStore; import com.hedera.node.app.service.token.TokenService; @@ -58,16 +68,20 @@ import com.hedera.node.app.services.ServiceScopeLookup; import com.hedera.node.app.spi.authorization.Authorizer; import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.spi.records.RecordSource; import com.hedera.node.app.spi.workflows.record.StreamBuilder; import com.hedera.node.app.state.HederaRecordCache; +import com.hedera.node.app.state.HederaRecordCache.DueDiligenceFailure; +import com.hedera.node.app.state.recordcache.BlockRecordSource; +import com.hedera.node.app.state.recordcache.LegacyListRecordSource; import com.hedera.node.app.store.WritableStoreFactory; import com.hedera.node.app.throttle.NetworkUtilizationManager; import com.hedera.node.app.throttle.ThrottleServiceManager; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.TransactionInfo; import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; import com.hedera.node.app.workflows.handle.cache.CacheWarmer; import com.hedera.node.app.workflows.handle.dispatch.ChildDispatchFactory; -import com.hedera.node.app.workflows.handle.metric.HandleWorkflowMetrics; import com.hedera.node.app.workflows.handle.record.RecordStreamBuilder; import com.hedera.node.app.workflows.handle.record.SystemSetup; import com.hedera.node.app.workflows.handle.steps.HollowAccountCompletions; @@ -76,6 +90,7 @@ import com.hedera.node.app.workflows.prehandle.PreHandleWorkflow; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.BlockStreamConfig; +import com.hedera.node.config.types.StreamMode; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.platform.system.InitTrigger; import com.swirlds.platform.system.Round; @@ -103,6 +118,8 @@ public class HandleWorkflow { private static final Logger logger = LogManager.getLogger(HandleWorkflow.class); public static final String ALERT_MESSAGE = "Possibly CATASTROPHIC failure"; + + private final StreamMode streamMode; private final NetworkInfo networkInfo; private final NodeStakeUpdates nodeStakeUpdates; private final Authorizer authorizer; @@ -117,7 +134,7 @@ public class HandleWorkflow { private final BlockRecordManager blockRecordManager; private final BlockStreamManager blockStreamManager; private final CacheWarmer cacheWarmer; - private final HandleWorkflowMetrics handleWorkflowMetrics; + private final OpWorkflowMetrics opWorkflowMetrics; private final ThrottleServiceManager throttleServiceManager; private final SemanticVersion version; private final InitTrigger initTrigger; @@ -132,6 +149,9 @@ public class HandleWorkflow { private final BoundaryStateChangeListener boundaryStateChangeListener; private final List migrationStateChanges; + // The last second since the epoch at which the metrics were updated; this does not affect transaction handling + private long lastMetricUpdateSecond; + @Inject public HandleWorkflow( @NonNull final NetworkInfo networkInfo, @@ -148,7 +168,7 @@ public HandleWorkflow( @NonNull final BlockRecordManager blockRecordManager, @NonNull final BlockStreamManager blockStreamManager, @NonNull final CacheWarmer cacheWarmer, - @NonNull final HandleWorkflowMetrics handleWorkflowMetrics, + @NonNull final OpWorkflowMetrics opWorkflowMetrics, @NonNull final ThrottleServiceManager throttleServiceManager, @NonNull final SemanticVersion version, @NonNull final InitTrigger initTrigger, @@ -176,7 +196,7 @@ public HandleWorkflow( this.blockRecordManager = requireNonNull(blockRecordManager); this.blockStreamManager = requireNonNull(blockStreamManager); this.cacheWarmer = requireNonNull(cacheWarmer); - this.handleWorkflowMetrics = requireNonNull(handleWorkflowMetrics); + this.opWorkflowMetrics = requireNonNull(opWorkflowMetrics); this.throttleServiceManager = requireNonNull(throttleServiceManager); this.version = requireNonNull(version); this.initTrigger = requireNonNull(initTrigger); @@ -190,6 +210,10 @@ public HandleWorkflow( this.kvStateChangeListener = requireNonNull(kvStateChangeListener); this.boundaryStateChangeListener = requireNonNull(boundaryStateChangeListener); this.migrationStateChanges = new ArrayList<>(migrationStateChanges); + this.streamMode = configProvider + .getConfiguration() + .getConfigData(BlockStreamConfig.class) + .streamMode(); } /** @@ -199,11 +223,9 @@ public HandleWorkflow( * @param round the next {@link Round} that needs to be processed */ public void handleRound(@NonNull final State state, @NonNull final Round round) { - // We only close the round with the block record manager after user transactions logStartRound(round); cacheWarmer.warm(state, round); - final var blockStreamConfig = configProvider.getConfiguration().getConfigData(BlockStreamConfig.class); - if (blockStreamConfig.streamBlocks()) { + if (streamMode != RECORDS) { blockStreamManager.startRound(round, state); blockStreamManager.writeItem(BlockItem.newBuilder() .roundHeader(new RoundHeader(round.getRoundNum())) @@ -228,9 +250,8 @@ public void handleRound(@NonNull final State state, @NonNull final Round round) private void handleEvents(@NonNull final State state, @NonNull final Round round) { final var userTransactionsHandled = new AtomicBoolean(false); - final var blockStreamConfig = configProvider.getConfiguration().getConfigData(BlockStreamConfig.class); for (final var event : round) { - if (blockStreamConfig.streamBlocks()) { + if (streamMode != RECORDS) { streamMetadata(event); } final var creator = networkInfo.nodeInfo(event.getCreatorId().id()); @@ -244,8 +265,8 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round // address book non-deterministically. logger.warn( "Received event (version {} vs current {}) from node {} which is not in the address book", - com.hedera.hapi.util.HapiUtils.toString(event.getSoftwareVersion()), - com.hedera.hapi.util.HapiUtils.toString(version), + HapiUtils.toString(event.getSoftwareVersion()), + HapiUtils.toString(version), event.getCreatorId()); } continue; @@ -259,9 +280,7 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round // skip system transactions if (!platformTxn.isSystem()) { userTransactionsHandled.set(true); - handlePlatformTransaction(state, event, creator, platformTxn, blockStreamConfig); - } else { - // TODO - handle block and signature transactions here? + handlePlatformTransaction(state, event, creator, platformTxn); } } catch (final Exception e) { logger.fatal( @@ -276,8 +295,10 @@ private void handleEvents(@NonNull final State state, @NonNull final Round round // Inform the BlockRecordManager that the round is complete, so it can update running-hashes in state // that have been being computed in background threads. The running hash has to be included in // state, but we want to synchronize with background threads as infrequently as possible. So once per - // round is the minimum we can do. - if (userTransactionsHandled.get() && blockStreamConfig.streamRecords()) { + // round is the minimum we can do. Note the BlockStreamManager#endRound() method is called in Hedera's + // implementation of SwirldState#sealConsensusRound(), since the BlockStreamManager cannot do its + // end-of-block work until the platform has finished all its state changes. + if (userTransactionsHandled.get() && streamMode != BLOCKS) { blockRecordManager.endRound(state); } } @@ -298,33 +319,40 @@ private void streamMetadata(@NonNull final ConsensusEvent event) { * @param event the {@link ConsensusEvent} that this transaction belongs to * @param creator the {@link NodeInfo} of the creator of the transaction * @param txn the {@link ConsensusTransaction} to be handled - * @param blockStreamConfig the block stream configuration */ private void handlePlatformTransaction( @NonNull final State state, @NonNull final ConsensusEvent event, @NonNull final NodeInfo creator, - @NonNull final ConsensusTransaction txn, - @NonNull final BlockStreamConfig blockStreamConfig) { + @NonNull final ConsensusTransaction txn) { final var handleStart = System.nanoTime(); // Always use platform-assigned time for user transaction, c.f. https://hips.hedera.com/hip/hip-993 final var consensusNow = txn.getConsensusTimestamp(); + var type = ORDINARY_TRANSACTION; stakePeriodManager.setCurrentStakePeriodFor(consensusNow); - final var userTxn = newUserTxn(state, event, creator, txn, consensusNow); - - if (blockStreamConfig.streamRecords()) { - blockRecordManager.startUserTransaction(consensusNow, state); + if (streamMode != BLOCKS) { + final var isBoundary = blockRecordManager.startUserTransaction(consensusNow, state); + if (streamMode == RECORDS && isBoundary) { + type = typeOfBoundary(state); + } + } + if (streamMode != RECORDS) { + type = switch (blockStreamManager.pendingWork()) { + case GENESIS_WORK -> GENESIS_TRANSACTION; + case POST_UPGRADE_WORK -> POST_UPGRADE_TRANSACTION; + default -> ORDINARY_TRANSACTION;}; } + final var userTxn = newUserTxn(state, event, creator, txn, consensusNow, type); final var handleOutput = execute(userTxn); - if (blockStreamConfig.streamRecords()) { - blockRecordManager.endUserTransaction(handleOutput.recordsOrThrow().stream(), state); + if (streamMode != BLOCKS) { + final var records = ((LegacyListRecordSource) handleOutput.recordSourceOrThrow()).precomputedRecords(); + blockRecordManager.endUserTransaction(records.stream(), state); } - if (blockStreamConfig.streamBlocks()) { - handleOutput.blocksItemsOrThrow().forEach(blockStreamManager::writeItem); + if (streamMode != RECORDS) { + handleOutput.blockRecordSourceOrThrow().forEachItem(blockStreamManager::writeItem); } - handleWorkflowMetrics.updateTransactionDuration( - userTxn.functionality(), (int) (System.nanoTime() - handleStart)); + opWorkflowMetrics.updateDuration(userTxn.functionality(), (int) (System.nanoTime() - handleStart)); } /** @@ -343,9 +371,17 @@ private void handlePlatformTransaction( * @return the stream of records */ private HandleOutput execute(@NonNull final UserTxn userTxn) { - final var blockStreamConfig = userTxn.config().getConfigData(BlockStreamConfig.class); try { if (isOlderSoftwareEvent(userTxn)) { + if (streamMode != BLOCKS) { + final var lastRecordManagerTime = blockRecordManager.consTimeOfLastHandledTxn(); + // This updates consTimeOfLastHandledTxn as a side-effect + blockRecordManager.advanceConsensusClock(userTxn.consensusNow(), userTxn.state()); + if (streamMode == RECORDS) { + // If relying on last-handled time to trigger interval processing, do so now + processInterval(userTxn, lastRecordManagerTime); + } + } initializeBuilderInfo(userTxn.baseBuilder(), userTxn.txnInfo(), exchangeRateManager.exchangeRates()) .status(BUSY); // Flushes the BUSY builder to the stream, no other side effects @@ -363,23 +399,45 @@ private HandleOutput execute(@NonNull final UserTxn userTxn) { new WritableStakingInfoStore(userTxn.stack().getWritableStates(TokenService.NAME)), new WritableNetworkStakingRewardsStore( userTxn.stack().getWritableStates(TokenService.NAME))); - if (blockStreamConfig.streamBlocks()) { - // There is no need to externalize this synthetic transaction if not using block streams + if (streamMode != RECORDS) { + // Only externalize this if we are streaming blocks streamBuilder.exchangeRate(exchangeRateManager.exchangeRates()); userTxn.stack().commitTransaction(streamBuilder); + } else { + // Only update this if we are relying on RecordManager state for post-upgrade processing + blockRecordManager.markMigrationRecordsStreamed(); } + // C.f. https://github.com/hashgraph/hedera-services/issues/14751, + // here we may need to switch the newly adopted candidate roster + // in the RosterService state to become the active roster } - updateNodeStakes(userTxn); - if (blockStreamConfig.streamRecords()) { + final var dispatch = dispatchFor(userTxn); + updateNodeStakes(userTxn, dispatch); + var lastRecordManagerTime = Instant.EPOCH; + if (streamMode != BLOCKS) { + lastRecordManagerTime = blockRecordManager.consTimeOfLastHandledTxn(); + // This updates consTimeOfLastHandledTxn as a side-effect blockRecordManager.advanceConsensusClock(userTxn.consensusNow(), userTxn.state()); } - expireSchedules(userTxn); + if (streamMode == RECORDS) { + processInterval(userTxn, lastRecordManagerTime); + } else { + if (processInterval(userTxn, blockStreamManager.lastIntervalProcessTime())) { + blockStreamManager.setLastIntervalProcessTime(userTxn.consensusNow()); + } + } logPreDispatch(userTxn); - final var dispatch = dispatchFor(userTxn, blockStreamConfig); - if (userTxn.type() == GENESIS_TRANSACTION) { - systemSetup.doGenesisSetup(dispatch); - } else if (userTxn.type() == POST_UPGRADE_TRANSACTION) { - systemSetup.doPostUpgradeSetup(dispatch); + if (userTxn.type() != ORDINARY_TRANSACTION) { + if (userTxn.type() == GENESIS_TRANSACTION) { + logger.info("Doing genesis setup @ {}", userTxn.consensusNow()); + systemSetup.doGenesisSetup(dispatch); + } else if (userTxn.type() == POST_UPGRADE_TRANSACTION) { + logger.info("Doing post-upgrade setup @ {}", userTxn.consensusNow()); + systemSetup.doPostUpgradeSetup(dispatch); + } + if (streamMode != RECORDS) { + blockStreamManager.confirmPendingWorkFinished(); + } } hollowAccountCompletions.completeHollowAccounts(userTxn, dispatch); dispatchProcessor.processDispatch(dispatch); @@ -387,14 +445,14 @@ private HandleOutput execute(@NonNull final UserTxn userTxn) { } final var handleOutput = userTxn.stack().buildHandleOutput(userTxn.consensusNow(), exchangeRateManager.exchangeRates()); - // Note that we don't yet support producing ONLY blocks, because we haven't integrated - // translators from block items to records for answering queries - if (blockStreamConfig.streamRecords()) { - recordCache.add( - userTxn.creatorInfo().nodeId(), userTxn.txnInfo().payerID(), handleOutput.recordsOrThrow()); - } else { - throw new IllegalStateException("Records must be produced directly without block item translators"); - } + final var dueDiligenceFailure = userTxn.preHandleResult().status() == NODE_DUE_DILIGENCE_FAILURE + ? DueDiligenceFailure.YES + : DueDiligenceFailure.NO; + recordCache.addRecordSource( + userTxn.creatorInfo().nodeId(), + userTxn.txnInfo().transactionID(), + dueDiligenceFailure, + handleOutput.preferringBlockRecordSource()); return handleOutput; } catch (final Exception e) { logger.error("{} - exception thrown while handling user transaction", ALERT_MESSAGE, e); @@ -409,28 +467,44 @@ private HandleOutput execute(@NonNull final UserTxn userTxn) { * @return the failure record */ private HandleOutput failInvalidStreamItems(@NonNull final UserTxn userTxn) { - userTxn.stack().rollbackFullStack(); // The stack for the user txn should never be committed - final List blockItems = new LinkedList<>(); - final var blockStreamConfig = configProvider.getConfiguration().getConfigData(BlockStreamConfig.class); - if (blockStreamConfig.streamBlocks()) { + userTxn.stack().rollbackFullStack(); + + RecordSource cacheableRecordSource = null; + final RecordSource recordSource; + if (streamMode != BLOCKS) { + final var failInvalidBuilder = new RecordStreamBuilder(REVERSIBLE, NOOP_RECORD_CUSTOMIZER, USER); + initializeBuilderInfo(failInvalidBuilder, userTxn.txnInfo(), exchangeRateManager.exchangeRates()) + .status(FAIL_INVALID) + .consensusTimestamp(userTxn.consensusNow()); + final var failInvalidRecord = failInvalidBuilder.build(); + cacheableRecordSource = recordSource = new LegacyListRecordSource( + List.of(failInvalidRecord), + List.of(new RecordSource.IdentifiedReceipt( + failInvalidRecord.transactionRecord().transactionIDOrThrow(), + failInvalidRecord.transactionRecord().receiptOrThrow()))); + } else { + recordSource = null; + } + final BlockRecordSource blockRecordSource; + if (streamMode != RECORDS) { + final List outputs = new LinkedList<>(); final var failInvalidBuilder = new BlockStreamBuilder(REVERSIBLE, NOOP_RECORD_CUSTOMIZER, USER); initializeBuilderInfo(failInvalidBuilder, userTxn.txnInfo(), exchangeRateManager.exchangeRates()) .status(FAIL_INVALID) .consensusTimestamp(userTxn.consensusNow()); - blockItems.addAll(failInvalidBuilder.build()); + outputs.add(failInvalidBuilder.build()); + cacheableRecordSource = blockRecordSource = new BlockRecordSource(outputs); + } else { + blockRecordSource = null; } - final var failInvalidBuilder = new RecordStreamBuilder(REVERSIBLE, NOOP_RECORD_CUSTOMIZER, USER); - initializeBuilderInfo(failInvalidBuilder, userTxn.txnInfo(), exchangeRateManager.exchangeRates()) - .status(FAIL_INVALID) - .consensusTimestamp(userTxn.consensusNow()); - final var failInvalidRecord = failInvalidBuilder.build(); - recordCache.add( + recordCache.addRecordSource( userTxn.creatorInfo().nodeId(), - requireNonNull(userTxn.txnInfo().payerID()), - List.of(failInvalidRecord)); - return new HandleOutput(blockItems, List.of(failInvalidRecord)); + userTxn.txnInfo().transactionID(), + DueDiligenceFailure.NO, + requireNonNull(cacheableRecordSource)); + return new HandleOutput(blockRecordSource, recordSource); } /** @@ -447,10 +521,9 @@ private boolean isOlderSoftwareEvent(@NonNull final UserTxn userTxn) { * Updates the metrics for the handle workflow. */ private void updateWorkflowMetrics(@NonNull final UserTxn userTxn) { - if (userTxn.type() == GENESIS_TRANSACTION - || userTxn.consensusNow().getEpochSecond() - > userTxn.lastHandledConsensusTime().getEpochSecond()) { - handleWorkflowMetrics.switchConsensusSecond(); + if (userTxn.type() == GENESIS_TRANSACTION || userTxn.consensusNow().getEpochSecond() > lastMetricUpdateSecond) { + opWorkflowMetrics.switchConsensusSecond(); + lastMetricUpdateSecond = userTxn.consensusNow().getEpochSecond(); } } @@ -458,10 +531,9 @@ private void updateWorkflowMetrics(@NonNull final UserTxn userTxn) { * Returns the user dispatch for the given user transaction. * * @param userTxn the user transaction - * @param blockStreamConfig the block stream configuration * @return the user dispatch */ - private Dispatch dispatchFor(@NonNull final UserTxn userTxn, @NonNull final BlockStreamConfig blockStreamConfig) { + private Dispatch dispatchFor(@NonNull final UserTxn userTxn) { final var baseBuilder = initializeBuilderInfo(userTxn.baseBuilder(), userTxn.txnInfo(), exchangeRateManager.exchangeRates()); return userTxn.newDispatch( @@ -469,7 +541,7 @@ private Dispatch dispatchFor(@NonNull final UserTxn userTxn, @NonNull final Bloc networkInfo, feeManager, dispatchProcessor, - blockRecordManager, + streamMode != RECORDS ? blockStreamManager : blockRecordManager, serviceScopeLookup, storeMetricsService, exchangeRateManager, @@ -477,7 +549,7 @@ private Dispatch dispatchFor(@NonNull final UserTxn userTxn, @NonNull final Bloc dispatcher, networkUtilizationManager, baseBuilder, - blockStreamConfig); + streamMode); } /** @@ -513,10 +585,15 @@ public static StreamBuilder initializeBuilderInfo( .memo(txnInfo.txBody().memo()); } - private void updateNodeStakes(@NonNull final UserTxn userTxn) { + private void updateNodeStakes(@NonNull final UserTxn userTxn, final Dispatch dispatch) { try { nodeStakeUpdates.process( - userTxn.stack(), userTxn.tokenContextImpl(), userTxn.type() == GENESIS_TRANSACTION); + dispatch, + userTxn.stack(), + userTxn.tokenContextImpl(), + streamMode, + userTxn.type() == GENESIS_TRANSACTION, + blockStreamManager.lastIntervalProcessTime()); } catch (final Exception e) { // We don't propagate a failure here to avoid a catastrophic scenario // where we are "stuck" trying to process node stake updates and never @@ -537,25 +614,28 @@ private static void logPreDispatch(@NonNull final UserTxn userTxn) { } /** - * Expire schedules that are due to be executed between the last handled - * transaction time and the current consensus time. + * Process all time-based events that are due since the last processing time. * * @param userTxn the user transaction + * @param lastProcessTime an upper bound on the last time that time-based events were processed + * @return true if the interval was processed */ - private void expireSchedules(@NonNull UserTxn userTxn) { - if (userTxn.type() == GENESIS_TRANSACTION) { - return; - } - final var lastHandledTxnTime = userTxn.lastHandledConsensusTime(); - if (userTxn.consensusNow().getEpochSecond() > lastHandledTxnTime.getEpochSecond()) { - final var firstSecondToExpire = lastHandledTxnTime.getEpochSecond(); - final var lastSecondToExpire = userTxn.consensusNow().getEpochSecond() - 1; + private boolean processInterval(@NonNull final UserTxn userTxn, final Instant lastProcessTime) { + // If we have never processed an interval, treat this time as the last processed time + if (Instant.EPOCH.equals(lastProcessTime)) { + return true; + } else if (lastProcessTime.getEpochSecond() < userTxn.consensusNow().getEpochSecond()) { + // There is at least one unprocessed second since the last processing time + final var startSecond = lastProcessTime.getEpochSecond(); + final var endSecond = userTxn.consensusNow().getEpochSecond() - 1; final var scheduleStore = new WritableStoreFactory( userTxn.stack(), ScheduleService.NAME, userTxn.config(), storeMetricsService) .getStore(WritableScheduleStore.class); - scheduleStore.purgeExpiredSchedulesBetween(firstSecondToExpire, lastSecondToExpire); + scheduleStore.purgeExpiredSchedulesBetween(startSecond, endSecond); userTxn.stack().commitSystemStateChanges(); + return true; } + return false; } /** @@ -567,6 +647,7 @@ private void expireSchedules(@NonNull UserTxn userTxn) { * @param creator the creator of the transaction * @param txn the consensus transaction * @param consensusNow the consensus time + * @param type the trnasaction type * @return the new user transaction */ private UserTxn newUserTxn( @@ -574,18 +655,37 @@ private UserTxn newUserTxn( @NonNull final ConsensusEvent event, @NonNull final NodeInfo creator, @NonNull final ConsensusTransaction txn, - @NonNull final Instant consensusNow) { + @NonNull final Instant consensusNow, + @NonNull final TransactionType type) { return UserTxn.from( state, event, creator, txn, consensusNow, - blockRecordManager.consTimeOfLastHandledTxn(), + type, configProvider, storeMetricsService, kvStateChangeListener, boundaryStateChangeListener, preHandleWorkflow); } + + /** + * Returns the type of transaction encountering the given state at a block boundary. + * + * @param state the boundary state + * @return the type of the boundary transaction + */ + private TransactionType typeOfBoundary(@NonNull final State state) { + final var files = state.getReadableStates(FileService.NAME).get(BLOBS_KEY); + // The files map is empty only at genesis + if (files.size() == 0) { + return GENESIS_TRANSACTION; + } + final var blockInfo = state.getReadableStates(BlockRecordService.NAME) + .getSingleton(BLOCK_INFO_STATE_KEY) + .get(); + return !requireNonNull(blockInfo).migrationRecordsStreamed() ? POST_UPGRADE_TRANSACTION : ORDINARY_TRANSACTION; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java index a1b3cbad6c06..08f009647c98 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java @@ -27,6 +27,8 @@ import com.hedera.node.app.service.token.impl.handlers.TokenHandlers; import com.hedera.node.app.service.util.impl.handlers.UtilHandlers; import com.hedera.node.app.state.WorkingStateAccessor; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.tss.handlers.TssHandlers; import com.hedera.node.app.workflows.dispatcher.TransactionHandlers; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.data.CacheConfig; @@ -49,6 +51,12 @@ static Supplier provideContractHandlers(@NonNull final Contrac return contractService::handlers; } + @Provides + @Singleton + static Supplier provideTssHandlers(@NonNull final TssBaseService tssBaseService) { + return tssBaseService::tssHandlers; + } + @Provides @Singleton static EthereumTransactionHandler provideEthereumTransactionHandler( @@ -86,6 +94,7 @@ static TransactionHandlers provideTransactionHandlers( @NonNull final ConsensusHandlers consensusHandlers, @NonNull final FileHandlers fileHandlers, @NonNull final Supplier contractHandlers, + @NonNull final Supplier tssHandlers, @NonNull final ScheduleHandlers scheduleHandlers, @NonNull final TokenHandlers tokenHandlers, @NonNull final UtilHandlers utilHandlers, @@ -144,6 +153,8 @@ static TransactionHandlers provideTransactionHandlers( addressBookHandlers.nodeUpdateHandler(), addressBookHandlers.nodeDeleteHandler(), tokenHandlers.tokenClaimAirdropHandler(), - utilHandlers.prngHandler()); + utilHandlers.prngHandler(), + tssHandlers.get().tssMessageHandler(), + tssHandlers.get().tssVoteHandler()); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java index a4dd44ea0218..42fd91619eed 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactory.java @@ -300,7 +300,8 @@ private static Fees computeChildFees( } } case CHILD -> Fees.FREE; - case USER -> throw new IllegalStateException("Should not dispatch child with user transaction category"); + case USER, NODE -> throw new IllegalStateException( + "Should not dispatch child with user transaction category"); }; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/DispatchValidator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/DispatchValidator.java index fd135ab20b84..23d90a0ed638 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/DispatchValidator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/DispatchValidator.java @@ -19,6 +19,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_SIGNATURE; import static com.hedera.hapi.util.HapiUtils.isHollow; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.NODE; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.SCHEDULED; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.state.HederaRecordCache.DuplicateCheckResult.NO_DUPLICATE; @@ -27,11 +28,11 @@ import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.ServiceFeeStatus.CAN_PAY_SERVICE_FEE; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.ServiceFeeStatus.UNABLE_TO_PAY_SERVICE_FEE; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.WorkflowCheck.NOT_INGEST; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.creatorValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerDuplicateErrorReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerUniqueValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.successReport; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newCreatorError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerDuplicateError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerUniqueError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newSuccess; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.SO_FAR_SO_GOOD; import com.hedera.hapi.node.base.AccountID; @@ -91,28 +92,28 @@ public DispatchValidator( public ValidationResult validationReportFor(@NonNull final Dispatch dispatch) { final var creatorError = creatorErrorIfKnown(dispatch); if (creatorError != null) { - return creatorValidationReport(dispatch.creatorInfo().accountId(), creatorError); + return newCreatorError(dispatch.creatorInfo().accountId(), creatorError); } else { final var payer = getPayerAccount(dispatch.readableStoreFactory(), dispatch.payerId(), dispatch.txnCategory()); final var category = dispatch.txnCategory(); - final var requiresPayerSig = category == USER || category == SCHEDULED; + final var requiresPayerSig = category == SCHEDULED || category == USER; if (requiresPayerSig && !isHollow(payer)) { // Skip payer verification for hollow accounts because ingest only submits valid signatures // for hollow payers; and if an account is still hollow here, its alias cannot have changed final var verification = dispatch.keyVerifier().verificationFor(payer.keyOrThrow()); if (verification.failed()) { - return creatorValidationReport(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE); + return newCreatorError(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE); } } - final var duplicateCheckResult = category != USER + final var duplicateCheckResult = category != USER && category != NODE ? NO_DUPLICATE : recordCache.hasDuplicate( dispatch.txnInfo().txBody().transactionIDOrThrow(), dispatch.creatorInfo().nodeId()); return switch (duplicateCheckResult) { case NO_DUPLICATE -> finalPayerValidationReport(payer, DuplicateStatus.NO_DUPLICATE, dispatch); - case SAME_NODE -> creatorValidationReport(dispatch.creatorInfo().accountId(), DUPLICATE_TRANSACTION); + case SAME_NODE -> newCreatorError(dispatch.creatorInfo().accountId(), DUPLICATE_TRANSACTION); case OTHER_NODE -> finalPayerValidationReport(payer, DuplicateStatus.DUPLICATE, dispatch); }; } @@ -142,23 +143,24 @@ private ValidationResult finalPayerValidationReport( ? dispatch.fees() : dispatch.fees().withoutServiceComponent(), NOT_INGEST, - (dispatch.txnCategory() == USER || dispatch.txnCategory() == SCHEDULED) + (dispatch.txnCategory() == USER + || dispatch.txnCategory() == SCHEDULED + || dispatch.txnCategory() == NODE) ? CHECK_OFFERED_FEE : SKIP_OFFERED_FEE_CHECK); } catch (final InsufficientServiceFeeException e) { - return payerValidationReport( - creatorId, payer, e.responseCode(), UNABLE_TO_PAY_SERVICE_FEE, duplicateStatus); + return newPayerError(creatorId, payer, e.responseCode(), UNABLE_TO_PAY_SERVICE_FEE, duplicateStatus); } catch (final InsufficientNonFeeDebitsException e) { - return payerValidationReport(creatorId, payer, e.responseCode(), CAN_PAY_SERVICE_FEE, duplicateStatus); + return newPayerError(creatorId, payer, e.responseCode(), CAN_PAY_SERVICE_FEE, duplicateStatus); } catch (final PreCheckException e) { // Includes InsufficientNetworkFeeException - return creatorValidationReport(creatorId, e.responseCode()); + return newCreatorError(creatorId, e.responseCode()); } return switch (duplicateStatus) { - case DUPLICATE -> payerDuplicateErrorReport(creatorId, payer); + case DUPLICATE -> newPayerDuplicateError(creatorId, payer); case NO_DUPLICATE -> dispatch.preHandleResult().status() == SO_FAR_SO_GOOD - ? successReport(creatorId, payer) - : payerUniqueValidationReport( + ? newSuccess(creatorId, payer) + : newPayerUniqueError( creatorId, payer, dispatch.preHandleResult().responseCode()); }; } @@ -188,7 +190,7 @@ private ResponseCodeEnum creatorErrorIfKnown(@NonNull final Dispatch dispatch) { */ @Nullable private ResponseCodeEnum getExpiryError(final @NonNull Dispatch dispatch) { - if (dispatch.txnCategory() != USER) { + if (dispatch.txnCategory() != USER && dispatch.txnCategory() != NODE) { return null; } try { @@ -221,7 +223,7 @@ private Account getPayerAccount( final var accountStore = storeFactory.getStore(ReadableAccountStore.class); final var account = accountStore.getAccountById(accountID); return switch (category) { - case USER -> { + case USER, NODE -> { if (account == null || account.deleted() || account.smartContract()) { throw new IllegalStateException( "Category " + category + " payer account should have been rejected " + account); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java index 7d726ce2de02..75a65378cf0d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResult.java @@ -56,7 +56,7 @@ public record ValidationResult( * @return the error report */ @NonNull - public static ValidationResult creatorValidationReport( + public static ValidationResult newCreatorError( @NonNull AccountID creatorId, @NonNull ResponseCodeEnum creatorError) { return new ValidationResult(creatorId, creatorError, null, null, CAN_PAY_SERVICE_FEE, NO_DUPLICATE); } @@ -69,7 +69,7 @@ public static ValidationResult creatorValidationReport( * @return the error report */ @NonNull - public static ValidationResult payerDuplicateErrorReport( + public static ValidationResult newPayerDuplicateError( @NonNull final AccountID creatorId, @NonNull final Account payer) { requireNonNull(payer); requireNonNull(creatorId); @@ -85,7 +85,7 @@ public static ValidationResult payerDuplicateErrorReport( * @return the error report */ @NonNull - public static ValidationResult payerUniqueValidationReport( + public static ValidationResult newPayerUniqueError( @NonNull final AccountID creatorId, @NonNull final Account payer, @NonNull final ResponseCodeEnum payerError) { @@ -105,7 +105,7 @@ public static ValidationResult payerUniqueValidationReport( * @return the error report */ @NonNull - public static ValidationResult payerValidationReport( + public static ValidationResult newPayerError( @NonNull AccountID creatorId, @NonNull Account payer, @NonNull ResponseCodeEnum payerError, @@ -121,7 +121,9 @@ public static ValidationResult payerValidationReport( * @return the error report */ @NonNull - public static ValidationResult successReport(@NonNull AccountID creatorId, @NonNull Account payer) { + public static ValidationResult newSuccess(@NonNull final AccountID creatorId, @NonNull final Account payer) { + requireNonNull(creatorId); + requireNonNull(payer); return new ValidationResult(creatorId, null, payer, null, CAN_PAY_SERVICE_FEE, NO_DUPLICATE); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/RecordStreamBuilder.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/RecordStreamBuilder.java index af97823b0234..d38f65dd077a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/RecordStreamBuilder.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/RecordStreamBuilder.java @@ -666,19 +666,6 @@ public int getNumAutoAssociations() { return automaticTokenAssociations.size(); } - /** - * Sets the alias. - * - * @param alias the alias - * @return the builder - */ - @NonNull - public RecordStreamBuilder alias(@NonNull final Bytes alias) { - requireNonNull(alias, "alias must not be null"); - transactionRecordBuilder.alias(alias); - return this; - } - /** * Sets the ethereum hash. * diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java index 5e0bcbb8f82d..583d33448923 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/SystemSetup.java @@ -16,6 +16,7 @@ package com.hedera.node.app.workflows.handle.record; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_CREATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS_BUT_MISSING_EXPECTED_OPERATION; import static com.hedera.hapi.util.HapiUtils.ACCOUNT_ID_COMPARATOR; @@ -53,6 +54,7 @@ import com.hedera.node.config.data.FilesConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.data.NetworkAdminConfig; +import com.hedera.node.config.data.NodesConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import com.swirlds.platform.system.InitTrigger; @@ -133,15 +135,18 @@ public void doGenesisSetup(@NonNull final Dispatch dispatch) { */ public void doPostUpgradeSetup(@NonNull final Dispatch dispatch) { final var systemContext = systemContextFor(dispatch); + final var config = dispatch.config(); // We update the node details file from the address book that resulted from all pre-upgrade HAPI node changes - final var nodeStore = dispatch.handleContext().storeFactory().readableStore(ReadableNodeStore.class); - fileService.updateAddressBookAndNodeDetailsAfterFreeze(systemContext, nodeStore); - dispatch.stack().commitFullStack(); + final var nodesConfig = config.getConfigData(NodesConfig.class); + if (nodesConfig.enableDAB()) { + final var nodeStore = dispatch.handleContext().storeFactory().readableStore(ReadableNodeStore.class); + fileService.updateAddressBookAndNodeDetailsAfterFreeze(systemContext, nodeStore); + dispatch.stack().commitFullStack(); + } // And then we update the system files for fees schedules, throttles, override properties, and override // permissions from any upgrade files that are present in the configured directory - final var config = dispatch.config(); final var filesConfig = config.getConfigData(FilesConfig.class); final var adminConfig = config.getConfigData(NetworkAdminConfig.class); final List autoUpdates = List.of( @@ -376,7 +381,8 @@ private void createAccountRecordBuilders( for (final Account account : accts) { // Since this is only called at genesis, the active savepoint's preceding record capacity will be // Integer.MAX_VALUE and this will never fail with MAX_CHILD_RECORDS_EXCEEDED (c.f., HandleWorkflow) - final var recordBuilder = context.addPrecedingChildRecordBuilder(GenesisAccountStreamBuilder.class); + final var recordBuilder = + context.addPrecedingChildRecordBuilder(GenesisAccountStreamBuilder.class, CRYPTO_CREATE); recordBuilder.accountID(account.accountIdOrThrow()).exchangeRate(exchangeRateSet); if (recordMemo != null) { recordBuilder.memo(recordMemo); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/TokenContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/TokenContextImpl.java index c780a4c3a681..a83f0845151c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/TokenContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/record/TokenContextImpl.java @@ -20,6 +20,7 @@ import static com.hedera.node.app.workflows.handle.stack.SavepointStackImpl.castBuilder; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.node.app.service.token.ReadableStakingInfoStore; import com.hedera.node.app.service.token.TokenService; import com.hedera.node.app.service.token.records.FinalizeContext; @@ -103,8 +104,11 @@ public void forEachChildRecord(@NonNull Class recordBuilderClass, @NonNul @NonNull @Override - public T addPrecedingChildRecordBuilder(@NonNull Class recordBuilderClass) { - final var result = stack.createIrreversiblePrecedingBuilder(); + public T addPrecedingChildRecordBuilder( + @NonNull final Class recordBuilderClass, @NonNull final HederaFunctionality functionality) { + requireNonNull(recordBuilderClass); + requireNonNull(functionality); + final var result = stack.createIrreversiblePrecedingBuilder().functionality(functionality); return castBuilder(result, recordBuilderClass); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/Savepoint.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/Savepoint.java index 2c7f9b8f4f51..431541a2085c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/Savepoint.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/Savepoint.java @@ -60,16 +60,16 @@ public interface Savepoint extends BuilderSink { * in this savepoint. * * @param reversingBehavior the reversing behavior to apply to the builder on rollback - * @param txnCategory the category of transaction initiating the new builder - * @param customizer the customizer to apply when externalizing the builder - * @param isBaseBuilder whether the builder is the base builder for a stack - * @param streamMode the mode of the stream + * @param txnCategory the category of transaction initiating the new builder + * @param customizer the customizer to apply when externalizing the builder + * @param streamMode the mode of the stream + * @param isBaseBuilder whether the builder is the base builder for a stack * @return the new builder */ StreamBuilder createBuilder( @NonNull ReversingBehavior reversingBehavior, @NonNull HandleContext.TransactionCategory txnCategory, @NonNull ExternalizedRecordCustomizer customizer, - boolean isBaseBuilder, - @NonNull StreamMode streamMode); + @NonNull StreamMode streamMode, + boolean isBaseBuilder); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java index c6f572b89f10..7b2e6b61a2aa 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImpl.java @@ -24,21 +24,26 @@ import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.IRREVERSIBLE; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REMOVABLE; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REVERSIBLE; +import static com.hedera.node.config.types.StreamMode.BLOCKS; import static com.hedera.node.config.types.StreamMode.RECORDS; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; import com.hedera.node.app.blocks.impl.PairedStreamBuilder; +import com.hedera.node.app.spi.records.RecordSource; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; import com.hedera.node.app.spi.workflows.record.StreamBuilder; import com.hedera.node.app.state.ReadonlyStatesWrapper; import com.hedera.node.app.state.SingleTransactionRecord; import com.hedera.node.app.state.WrappedState; +import com.hedera.node.app.state.recordcache.BlockRecordSource; +import com.hedera.node.app.state.recordcache.LegacyListRecordSource; import com.hedera.node.app.workflows.handle.HandleOutput; import com.hedera.node.app.workflows.handle.record.RecordStreamBuilder; import com.hedera.node.app.workflows.handle.stack.savepoints.BuilderSinkImpl; @@ -90,12 +95,12 @@ public class SavepointStackImpl implements HandleContext.SavepointStack, State { /** * Constructs the root {@link SavepointStackImpl} for the given state at the start of handling a user transaction. * - * @param state the state + * @param state the state * @param maxBuildersBeforeUser the maximum number of preceding builders with available consensus times * @param maxBuildersAfterUser the maximum number of following builders with available consensus times * @param boundaryStateChangeListener the listener for the round state changes * @param kvStateChangeListener the listener for the key/value state changes - * @param streamMode the stream mode + * @param streamMode the stream mode * @return the root {@link SavepointStackImpl} */ public static SavepointStackImpl newRootStack( @@ -118,11 +123,11 @@ public static SavepointStackImpl newRootStack( * Constructs a new child {@link SavepointStackImpl} for the given state, where the child dispatch has the given * reversing behavior, transaction category, and record customizer. * - * @param root the state on which the child dispatch is based + * @param root the state on which the child dispatch is based * @param reversingBehavior the reversing behavior for the initial dispatch - * @param category the transaction category - * @param customizer the record customizer - * @param streamMode the stream mode + * @param category the transaction category + * @param customizer the record customizer + * @param streamMode the stream mode * @return the child {@link SavepointStackImpl} */ public static SavepointStackImpl newChildStack( @@ -156,7 +161,7 @@ private SavepointStackImpl( this.roundStateChangeListener = requireNonNull(roundStateChangeListener); builderSink = new BuilderSinkImpl(maxBuildersBeforeUser, maxBuildersAfterUser + 1); setupFirstSavepoint(USER); - baseBuilder = peek().createBuilder(REVERSIBLE, USER, NOOP_RECORD_CUSTOMIZER, true, streamMode); + baseBuilder = peek().createBuilder(REVERSIBLE, USER, NOOP_RECORD_CUSTOMIZER, streamMode, true); this.streamMode = requireNonNull(streamMode); } @@ -168,6 +173,7 @@ private SavepointStackImpl( * @param reversingBehavior the reversing behavior of the dispatch * @param category the category of the dispatch * @param customizer the record customizer for the dispatch + * @param streamMode the stream mode */ private SavepointStackImpl( @NonNull final SavepointStackImpl parent, @@ -184,7 +190,7 @@ private SavepointStackImpl( this.kvStateChangeListener = null; this.roundStateChangeListener = null; setupFirstSavepoint(category); - baseBuilder = peek().createBuilder(reversingBehavior, category, customizer, true, streamMode); + baseBuilder = peek().createBuilder(reversingBehavior, category, customizer, streamMode, true); } @Override @@ -320,15 +326,19 @@ public T getBaseBuilder(@NonNull Class recordBuilde @NonNull @Override - public T addChildRecordBuilder(@NonNull Class recordBuilderClass) { - final var result = createReversibleChildBuilder(); + public T addChildRecordBuilder( + @NonNull Class recordBuilderClass, @NonNull final HederaFunctionality functionality) { + requireNonNull(functionality); + final var result = createReversibleChildBuilder().functionality(functionality); return castBuilder(result, recordBuilderClass); } @NonNull @Override - public T addRemovableChildRecordBuilder(@NonNull Class recordBuilderClass) { - final var result = createRemovableChildBuilder(); + public T addRemovableChildRecordBuilder( + @NonNull Class recordBuilderClass, @NonNull final HederaFunctionality functionality) { + requireNonNull(functionality); + final var result = createRemovableChildBuilder().functionality(functionality); return castBuilder(result, recordBuilderClass); } @@ -402,7 +412,7 @@ public HandleContext.TransactionCategory txnCategory() { * @return the new stream builder */ public StreamBuilder createRemovableChildBuilder() { - return peek().createBuilder(REMOVABLE, CHILD, NOOP_RECORD_CUSTOMIZER, false, streamMode); + return peek().createBuilder(REMOVABLE, CHILD, NOOP_RECORD_CUSTOMIZER, streamMode, false); } /** @@ -411,7 +421,7 @@ public StreamBuilder createRemovableChildBuilder() { * @return the new stream builder */ public StreamBuilder createReversibleChildBuilder() { - return peek().createBuilder(REVERSIBLE, CHILD, NOOP_RECORD_CUSTOMIZER, false, streamMode); + return peek().createBuilder(REVERSIBLE, CHILD, NOOP_RECORD_CUSTOMIZER, streamMode, false); } /** @@ -420,7 +430,7 @@ public StreamBuilder createReversibleChildBuilder() { * @return the new stream builder */ public StreamBuilder createIrreversiblePrecedingBuilder() { - return peek().createBuilder(IRREVERSIBLE, PRECEDING, NOOP_RECORD_CUSTOMIZER, false, streamMode); + return peek().createBuilder(IRREVERSIBLE, PRECEDING, NOOP_RECORD_CUSTOMIZER, streamMode, false); } /** @@ -439,22 +449,19 @@ Savepoint peek() { } /** - * Builds all the records for the user transaction. + * Builds the {@link BlockRecordSource} and/or {@link RecordSource} for this user transaction. * * @param consensusTime consensus time of the transaction * @param exchangeRates the active exchange rates - * @return the stream of records + * @return the source of records and/or blocks for the transaction */ public HandleOutput buildHandleOutput( @NonNull final Instant consensusTime, @NonNull final ExchangeRateSet exchangeRates) { - final List blockItems; - Instant lastAssignedConsenusTime = consensusTime; - if (streamMode == RECORDS) { - blockItems = null; - } else { - blockItems = new LinkedList<>(); - } - final List records = new ArrayList<>(); + final List outputs = streamMode != RECORDS ? new LinkedList<>() : null; + final List records = streamMode != BLOCKS ? new ArrayList<>() : null; + final List receipts = streamMode != BLOCKS ? new ArrayList<>() : null; + + var lastAssignedConsenusTime = consensusTime; final var builders = requireNonNull(builderSink).allBuilders(); TransactionID.Builder idBuilder = null; int indexOfUserRecord = 0; @@ -470,7 +477,7 @@ public HandleOutput buildHandleOutput( final var builder = builders.get(i); final var nonce = switch (builder.category()) { - case USER, SCHEDULED -> 0; + case USER, SCHEDULED, NODE -> 0; case PRECEDING, CHILD -> nextNonce++; }; // The schedule service specifies the transaction id to use for a triggered transaction @@ -493,19 +500,29 @@ public HandleOutput buildHandleOutput( } } switch (streamMode) { - case RECORDS -> records.add(((RecordStreamBuilder) builder).build()); + case RECORDS -> { + final var nextRecord = ((RecordStreamBuilder) builder).build(); + records.add(nextRecord); + receipts.add(new RecordSource.IdentifiedReceipt( + nextRecord.transactionRecord().transactionIDOrThrow(), + nextRecord.transactionRecord().receiptOrThrow())); + } + case BLOCKS -> requireNonNull(outputs).add(((BlockStreamBuilder) builder).build()); case BOTH -> { final var pairedBuilder = (PairedStreamBuilder) builder; records.add(pairedBuilder.recordStreamBuilder().build()); - requireNonNull(blockItems) - .addAll(pairedBuilder.blockStreamBuilder().build()); + requireNonNull(outputs) + .add(pairedBuilder.blockStreamBuilder().build()); } } } + BlockRecordSource blockRecordSource = null; if (streamMode != RECORDS) { requireNonNull(roundStateChangeListener).setBoundaryTimestamp(lastAssignedConsenusTime); + blockRecordSource = new BlockRecordSource(outputs); } - return new HandleOutput(blockItems, records); + final var recordSource = streamMode != BLOCKS ? new LegacyListRecordSource(records, receipts) : null; + return new HandleOutput(blockRecordSource, recordSource); } private void setupFirstSavepoint(@NonNull final HandleContext.TransactionCategory category) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/AbstractSavepoint.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/AbstractSavepoint.java index eb620d5c9a1c..eb145d6da4ba 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/AbstractSavepoint.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/AbstractSavepoint.java @@ -27,6 +27,7 @@ import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; import com.hedera.node.app.blocks.impl.PairedStreamBuilder; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer; @@ -115,17 +116,22 @@ public StreamBuilder createBuilder( @NonNull final StreamBuilder.ReversingBehavior reversingBehavior, @NonNull final HandleContext.TransactionCategory txnCategory, @NonNull final ExternalizedRecordCustomizer customizer, - final boolean isBaseBuilder, - @NonNull final StreamMode streamMode) { + @NonNull final StreamMode streamMode, + final boolean isBaseBuilder) { requireNonNull(reversingBehavior); requireNonNull(txnCategory); requireNonNull(customizer); final var builder = switch (streamMode) { case RECORDS -> new RecordStreamBuilder(reversingBehavior, customizer, txnCategory); + case BLOCKS -> new BlockStreamBuilder(reversingBehavior, customizer, txnCategory); case BOTH -> new PairedStreamBuilder(reversingBehavior, customizer, txnCategory); }; if (!customizer.shouldSuppressRecord()) { + // Other code is a bit simpler when we always put the base builder for a stack in its + // "following" list, even if the stack is child stack for a preceding child dispatch; + // the base builder will still end up in the correct relative position in the parent + // sink because of how FirstChildSavepoint implements #commitBuilders() if (txnCategory == PRECEDING && !isBaseBuilder) { addPrecedingOrThrow(builder); } else { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepoint.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepoint.java index 1deb36216863..3fd61bac5812 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepoint.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepoint.java @@ -28,7 +28,7 @@ * parent sink although the capacity is limited in both directions. */ public class FirstRootSavepoint extends AbstractSavepoint { - public FirstRootSavepoint(@NonNull WrappedState state, BuilderSink parentSink) { + public FirstRootSavepoint(@NonNull final WrappedState state, @NonNull final BuilderSink parentSink) { super(state, parentSink, parentSink.precedingCapacity(), parentSink.followingCapacity()); } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdates.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdates.java index 8c6d6c80a7ae..086b3ed6978c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdates.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdates.java @@ -16,17 +16,23 @@ package com.hedera.node.app.workflows.handle.steps; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.swirlds.common.stream.LinkedObjectStreamUtilities.getPeriod; import static java.time.ZoneOffset.UTC; import static java.util.Objects.requireNonNull; import com.google.common.annotations.VisibleForTesting; +import com.hedera.hapi.node.state.roster.Roster; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.records.ReadableBlockRecordStore; import com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUpdater; import com.hedera.node.app.service.token.records.TokenContext; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.workflows.handle.Dispatch; import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; import com.hedera.node.config.data.StakingConfig; +import com.hedera.node.config.data.TssConfig; +import com.hedera.node.config.types.StreamMode; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Instant; import java.time.LocalDate; @@ -47,13 +53,16 @@ public class NodeStakeUpdates { private final EndOfStakingPeriodUpdater stakingCalculator; private final ExchangeRateManager exchangeRateManager; + private final TssBaseService tssBaseService; @Inject public NodeStakeUpdates( @NonNull final EndOfStakingPeriodUpdater stakingPeriodCalculator, - @NonNull final ExchangeRateManager exchangeRateManager) { + @NonNull final ExchangeRateManager exchangeRateManager, + @NonNull final TssBaseService tssBaseService) { this.stakingCalculator = requireNonNull(stakingPeriodCalculator); this.exchangeRateManager = requireNonNull(exchangeRateManager); + this.tssBaseService = requireNonNull(tssBaseService); } /** @@ -61,30 +70,41 @@ public NodeStakeUpdates( * rewards. This should only be done during handling of the first transaction of each new staking * period, which staking period usually starts at midnight UTC. * - *

    The only exception to this rule is when {@code consensusTimeOfLastHandledTxn} is null, - * which should only happen on node startup. The node should therefore run this process - * to catch up on updates and distributions when first coming online. - * + * @param dispatch the dispatch * @param stack the savepoint stack - * @param isGenesis whether the current transaction is the genesis transaction * @param tokenContext the token context + * @param streamMode the stream mode + * @param isGenesis whether the current transaction is the genesis transaction + * @param lastIntervalProcessTime if known, the last instant when time-based events were processed */ public void process( + @NonNull final Dispatch dispatch, @NonNull final SavepointStackImpl stack, @NonNull final TokenContext tokenContext, - final boolean isGenesis) { - requireNonNull(stack, "stack must not be null"); - requireNonNull(tokenContext, "tokenContext must not be null"); - final var blockStore = tokenContext.readableStore(ReadableBlockRecordStore.class); + @NonNull final StreamMode streamMode, + final boolean isGenesis, + @NonNull final Instant lastIntervalProcessTime) { + requireNonNull(stack); + requireNonNull(dispatch); + requireNonNull(tokenContext); + requireNonNull(streamMode); + requireNonNull(lastIntervalProcessTime); var shouldExport = isGenesis; if (!shouldExport) { final var consensusTime = tokenContext.consensusTime(); - final var lastHandleTime = blockStore.getLastBlockInfo().consTimeOfLastHandledTxnOrThrow(); - if (consensusTime.getEpochSecond() > lastHandleTime.seconds()) { - shouldExport = isNextStakingPeriod( - consensusTime, - Instant.ofEpochSecond(lastHandleTime.seconds(), lastHandleTime.nanos()), - tokenContext); + if (streamMode == RECORDS) { + final var blockStore = tokenContext.readableStore(ReadableBlockRecordStore.class); + final var lastHandleTime = blockStore.getLastBlockInfo().consTimeOfLastHandledTxnOrThrow(); + if (consensusTime.getEpochSecond() > lastHandleTime.seconds()) { + shouldExport = isNextStakingPeriod( + consensusTime, + Instant.ofEpochSecond(lastHandleTime.seconds(), lastHandleTime.nanos()), + tokenContext); + } + } else { + if (consensusTime.getEpochSecond() > lastIntervalProcessTime.getEpochSecond()) { + shouldExport = isNextStakingPeriod(consensusTime, lastIntervalProcessTime, tokenContext); + } } } if (shouldExport) { @@ -109,6 +129,13 @@ public void process( logger.error("CATASTROPHIC failure updating end-of-day stakes", e); stack.rollbackFullStack(); } + final var config = tokenContext.configuration(); + final var tssConfig = config.getConfigData(TssConfig.class); + if (tssConfig.keyCandidateRoster()) { + final var context = dispatch.handleContext(); + // C.f. https://github.com/hashgraph/hedera-services/issues/14748 + tssBaseService.setCandidateRoster(Roster.DEFAULT, context); + } } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/UserTxn.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/UserTxn.java index a459cdee7c68..ca5bc9e70584 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/UserTxn.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/steps/UserTxn.java @@ -16,17 +16,14 @@ package com.hedera.node.app.workflows.handle.steps; -import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; -import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.workflows.handle.TransactionType.GENESIS_TRANSACTION; -import static com.hedera.node.app.workflows.handle.TransactionType.ORDINARY_TRANSACTION; import static com.hedera.node.app.workflows.handle.TransactionType.POST_UPGRADE_TRANSACTION; +import static com.hedera.node.app.workflows.standalone.impl.StandaloneDispatchFactory.getTxnCategory; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.state.blockrecords.BlockInfo; -import com.hedera.hapi.platform.state.PlatformState; import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; import com.hedera.node.app.blocks.impl.KVStateChangeListener; import com.hedera.node.app.fees.ExchangeRateManager; @@ -36,14 +33,13 @@ import com.hedera.node.app.ids.EntityIdService; import com.hedera.node.app.ids.EntityNumGeneratorImpl; import com.hedera.node.app.ids.WritableEntityIdStore; -import com.hedera.node.app.records.BlockRecordManager; -import com.hedera.node.app.records.BlockRecordService; import com.hedera.node.app.service.token.api.FeeStreamBuilder; import com.hedera.node.app.service.token.api.TokenServiceApi; import com.hedera.node.app.services.ServiceScopeLookup; import com.hedera.node.app.signature.DefaultKeyVerifier; import com.hedera.node.app.spi.authorization.Authorizer; import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.spi.records.BlockRecordInfo; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.record.StreamBuilder; import com.hedera.node.app.store.ReadableStoreFactory; @@ -68,9 +64,8 @@ import com.hedera.node.config.data.BlockStreamConfig; import com.hedera.node.config.data.ConsensusConfig; import com.hedera.node.config.data.HederaConfig; +import com.hedera.node.config.types.StreamMode; import com.swirlds.config.api.Configuration; -import com.swirlds.platform.state.service.PlatformStateService; -import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.events.ConsensusEvent; import com.swirlds.platform.system.transaction.ConsensusTransaction; import com.swirlds.state.State; @@ -92,9 +87,23 @@ public record UserTxn( @NonNull PreHandleResult preHandleResult, @NonNull ReadableStoreFactory readableStoreFactory, @NonNull Configuration config, - @NonNull Instant lastHandledConsensusTime, @NonNull NodeInfo creatorInfo) { + /** + * Creates a new {@link UserTxn} instance from the given parameters. + * @param state the state the transaction will be applied to + * @param event the consensus event containing the transaction + * @param creatorInfo the node information of the creator + * @param platformTxn the transaction itself + * @param consensusNow the current consensus time + * @param type the type of the transaction + * @param configProvider the configuration provider + * @param storeMetricsService the store metrics service + * @param kvStateChangeListener the key-value state change listener + * @param boundaryStateChangeListener the boundary state change listener + * @param preHandleWorkflow the pre-handle workflow + * @return the new user transaction + */ public static UserTxn from( // @UserTxnScope @NonNull final State state, @@ -102,28 +111,22 @@ public static UserTxn from( @NonNull final NodeInfo creatorInfo, @NonNull final ConsensusTransaction platformTxn, @NonNull final Instant consensusNow, - @NonNull final Instant lastHandledConsensusTime, + @NonNull final TransactionType type, // @Singleton @NonNull final ConfigProvider configProvider, @NonNull final StoreMetricsService storeMetricsService, @NonNull final KVStateChangeListener kvStateChangeListener, @NonNull final BoundaryStateChangeListener boundaryStateChangeListener, @NonNull final PreHandleWorkflow preHandleWorkflow) { - - final TransactionType type; - if (lastHandledConsensusTime.equals(Instant.EPOCH)) { - type = GENESIS_TRANSACTION; - } else if (isUpgradeBoundary(state)) { - type = POST_UPGRADE_TRANSACTION; - } else { - type = ORDINARY_TRANSACTION; - } final var config = configProvider.getConfiguration(); final var consensusConfig = config.getConfigData(ConsensusConfig.class); final var blockStreamConfig = config.getConfigData(BlockStreamConfig.class); + final var maxPrecedingRecords = (type == GENESIS_TRANSACTION || type == POST_UPGRADE_TRANSACTION) + ? Integer.MAX_VALUE + : consensusConfig.handleMaxPrecedingRecords(); final var stack = SavepointStackImpl.newRootStack( state, - type != ORDINARY_TRANSACTION ? Integer.MAX_VALUE : consensusConfig.handleMaxPrecedingRecords(), + maxPrecedingRecords, consensusConfig.handleMaxFollowingRecords(), boundaryStateChangeListener, kvStateChangeListener, @@ -146,7 +149,6 @@ public static UserTxn from( preHandleResult, readableStoreFactory, config, - lastHandledConsensusTime, creatorInfo); } @@ -157,7 +159,7 @@ public static UserTxn from( * @param networkInfo the network information * @param feeManager the fee manager * @param dispatchProcessor the dispatch processor - * @param blockRecordManager the block record manager + * @param blockRecordInfo the block record manager * @param serviceScopeLookup the service scope lookup * @param storeMetricsService the store metrics service * @param exchangeRateManager the exchange rate manager @@ -165,7 +167,7 @@ public static UserTxn from( * @param dispatcher the transaction dispatcher * @param networkUtilizationManager the network utilization manager * @param baseBuilder the base record builder - * @param blockStreamConfig the block stream configuration + * @param streamMode the stream mode * @return the new dispatch instance */ public Dispatch newDispatch( @@ -174,7 +176,7 @@ public Dispatch newDispatch( @NonNull final NetworkInfo networkInfo, @NonNull final FeeManager feeManager, @NonNull final DispatchProcessor dispatchProcessor, - @NonNull final BlockRecordManager blockRecordManager, + @NonNull final BlockRecordInfo blockRecordInfo, @NonNull final ServiceScopeLookup serviceScopeLookup, @NonNull final StoreMetricsService storeMetricsService, @NonNull final ExchangeRateManager exchangeRateManager, @@ -183,7 +185,7 @@ public Dispatch newDispatch( @NonNull final NetworkUtilizationManager networkUtilizationManager, // @UserTxnScope @NonNull final StreamBuilder baseBuilder, - @NonNull final BlockStreamConfig blockStreamConfig) { + @NonNull final StreamMode streamMode) { final var keyVerifier = new DefaultKeyVerifier( txnInfo.signatureMap().sigPair().size(), config.getConfigData(HederaConfig.class), @@ -207,7 +209,7 @@ public Dispatch newDispatch( txnInfo, config, authorizer, - blockRecordManager, + blockRecordInfo, priceCalculator, feeManager, storeFactory, @@ -225,7 +227,7 @@ public Dispatch newDispatch( throttleAdvisor, feeAccumulator); final var fees = dispatcher.dispatchComputeFees(dispatchHandleContext); - if (blockStreamConfig.streamBlocks()) { + if (streamMode != RECORDS) { final var congestionMultiplier = feeManager.congestionMultiplierFor( txnInfo.txBody(), txnInfo.functionality(), storeFactory.asReadOnly()); if (congestionMultiplier > 1) { @@ -247,7 +249,7 @@ public Dispatch newDispatch( preHandleResult.getHollowAccounts(), dispatchHandleContext, stack, - USER, + getTxnCategory(preHandleResult), tokenContextImpl, preHandleResult, HandleContext.ConsensusThrottling.ON); @@ -255,32 +257,10 @@ public Dispatch newDispatch( /** * Returns the base stream builder for this user transaction. + * * @return the base stream builder */ public StreamBuilder baseBuilder() { return stack.getBaseBuilder(StreamBuilder.class); } - - /** - * Returns whether the given state indicates this transaction is the first after an upgrade. - * @param state the Hedera state - * @return whether the given state indicates this transaction is the first after an upgrade - */ - private static boolean isUpgradeBoundary(@NonNull final State state) { - final var platformState = state.getReadableStates(PlatformStateService.NAME) - .getSingleton(V0540PlatformStateSchema.PLATFORM_STATE_KEY) - .get(); - requireNonNull(platformState); - if (platformState.freezeTime() == null - || !platformState.freezeTimeOrThrow().equals(platformState.lastFrozenTime())) { - return false; - } else { - // Check the state directly here instead of going through BlockManager to allow us - // to manipulate this condition easily in embedded tests - final var blockInfo = state.getReadableStates(BlockRecordService.NAME) - .getSingleton(BLOCK_INFO_STATE_KEY) - .get(); - return !requireNonNull(blockInfo).migrationRecordsStreamed(); - } - } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManager.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManager.java index 70760b0ba452..807c3022a35d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManager.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManager.java @@ -23,6 +23,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.hapi.utils.ethereum.EthTxData.populateEthTxData; import static com.hedera.node.app.spi.workflows.HandleContext.ConsensusThrottling.ON; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.NODE; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.throttle.ThrottleAccumulator.canAutoAssociate; import static com.hedera.node.app.throttle.ThrottleAccumulator.canAutoCreate; @@ -38,8 +39,8 @@ import com.hedera.node.app.throttle.CongestionThrottleService; import com.hedera.node.app.throttle.NetworkUtilizationManager; import com.hedera.node.app.throttle.ThrottleServiceManager; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.handle.Dispatch; -import com.hedera.node.app.workflows.handle.metric.HandleWorkflowMetrics; import com.hedera.node.config.data.ContractsConfig; import com.swirlds.state.spi.info.NetworkInfo; import edu.umd.cs.findbugs.annotations.NonNull; @@ -54,18 +55,18 @@ public class DispatchUsageManager { EnumSet.of(HederaFunctionality.CONTRACT_CREATE, HederaFunctionality.CONTRACT_CALL, ETHEREUM_TRANSACTION); private final NetworkInfo networkInfo; - private final HandleWorkflowMetrics handleWorkflowMetrics; + private final OpWorkflowMetrics opWorkflowMetrics; private final ThrottleServiceManager throttleServiceManager; private final NetworkUtilizationManager networkUtilizationManager; @Inject public DispatchUsageManager( @NonNull final NetworkInfo networkInfo, - @NonNull final HandleWorkflowMetrics handleWorkflowMetrics, + @NonNull final OpWorkflowMetrics opWorkflowMetrics, @NonNull final ThrottleServiceManager throttleServiceManager, @NonNull final NetworkUtilizationManager networkUtilizationManager) { this.networkInfo = requireNonNull(networkInfo); - this.handleWorkflowMetrics = requireNonNull(handleWorkflowMetrics); + this.opWorkflowMetrics = requireNonNull(opWorkflowMetrics); this.throttleServiceManager = requireNonNull(throttleServiceManager); this.networkUtilizationManager = requireNonNull(networkUtilizationManager); } @@ -102,7 +103,8 @@ public void finalizeAndSaveUsage(@NonNull final Dispatch dispatch) { if (CONTRACT_OPERATIONS.contains(function)) { leakUnusedGas(dispatch); } - if (dispatch.txnCategory() == USER && dispatch.recordBuilder().status() != SUCCESS) { + if ((dispatch.txnCategory() == USER || dispatch.txnCategory() == NODE) + && dispatch.recordBuilder().status() != SUCCESS) { if (canAutoCreate(function)) { reclaimFailedCryptoCreateCapacity(dispatch); } @@ -134,7 +136,7 @@ private void leakUnusedGas(@NonNull final Dispatch dispatch) { // EVM action tracer to get a better estimate of the actual gas used and the gas limit. if (builder.hasContractResult()) { final var gasUsed = builder.getGasUsedForContractTxn(); - handleWorkflowMetrics.addGasUsed(gasUsed); + opWorkflowMetrics.addGasUsed(gasUsed); final var contractsConfig = dispatch.config().getConfigData(ContractsConfig.class); if (contractsConfig.throttleThrottleByGas()) { final var txnInfo = dispatch.txnInfo(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java index b0c53dfbdf5b..0496a9c59f92 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImpl.java @@ -16,6 +16,8 @@ package com.hedera.node.app.workflows.prehandle; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.UNRESOLVABLE_REQUIRED_SIGNERS; import static com.hedera.hapi.util.HapiUtils.EMPTY_KEY_LIST; import static com.hedera.hapi.util.HapiUtils.isHollow; import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.verifyNotEmptyKey; @@ -62,7 +64,7 @@ public class PreHandleContextImpl implements PreHandleContext { /** * The payer account ID. Specified in the transaction body, extracted and stored separately for convenience. */ - private final AccountID payer; + private final AccountID payerId; /** * The payer's key, as found in state */ @@ -128,32 +130,28 @@ public PreHandleContextImpl( } /** - * Create a new instance + * Create a new instance of {@link PreHandleContextImpl}. + * @throws PreCheckException if the payer account does not exist */ private PreHandleContextImpl( @NonNull final ReadableStoreFactory storeFactory, @NonNull final TransactionBody txn, - @NonNull final AccountID payer, + @NonNull final AccountID payerId, @NonNull final Configuration configuration, @NonNull final TransactionDispatcher dispatcher, final boolean isUserTx) throws PreCheckException { this.storeFactory = requireNonNull(storeFactory, "storeFactory must not be null."); this.txn = requireNonNull(txn, "txn must not be null!"); - this.payer = requireNonNull(payer, "payer msut not be null!"); + this.payerId = requireNonNull(payerId, "payer must not be null!"); this.configuration = requireNonNull(configuration, "configuration must not be null!"); this.dispatcher = requireNonNull(dispatcher, "dispatcher must not be null!"); this.isUserTx = isUserTx; - this.accountStore = storeFactory.getStore(ReadableAccountStore.class); - - // Find the account, which must exist or throw a PreCheckException with the given response code. - final var account = accountStore.getAccountById(payer); - mustExist(account, ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID); - // NOTE: While it is true that the key can be null on some special accounts like - // account 800, those accounts cannot be the payer. - payerKey = account.key(); - mustExist(payerKey, ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID); + // Find the account, which must exist or throw on construction + final var payer = mustExist(accountStore.getAccountById(payerId), INVALID_PAYER_ACCOUNT_ID); + // It would be a catastrophic invariant failure if an account in state didn't have a key + payerKey = payer.keyOrThrow(); } @Override @@ -171,7 +169,7 @@ public TransactionBody body() { @Override @NonNull public AccountID payer() { - return payer; + return payerId; } @Override @@ -310,7 +308,7 @@ public PreHandleContext requireAliasedKeyOrThrow( // If we repeated the payer requirement, we would be requiring "double authorization" from // the contract doing the dispatch; but the contract has already authorized the action by // the very execution of its bytecode. - if (accountID.equals(payer)) { + if (accountID.equals(payerId)) { return this; } final Account account; @@ -330,7 +328,7 @@ public PreHandleContext requireAliasedKeyOrThrow( } // Verify this key isn't for an immutable account verifyNotStakingAccounts(account.accountIdOrThrow(), responseCode); - final var key = account.key(); + final var key = account.keyOrThrow(); if (!isValid(key)) { // Or if it is a Contract Key? Or if it is an empty key? // Or a KeyList with no // keys? Or KeyList with Contract keys only? @@ -478,18 +476,20 @@ public PreHandleContext requireSignatureForHollowAccountCreation(@NonNull final @NonNull @Override - public TransactionKeys allKeysForTransaction( - @NonNull TransactionBody nestedTxn, @NonNull final AccountID payerForNested) throws PreCheckException { - dispatcher.dispatchPureChecks(nestedTxn); - final var nestedContext = - new PreHandleContextImpl(storeFactory, nestedTxn, payerForNested, configuration, dispatcher); + public TransactionKeys allKeysForTransaction(@NonNull TransactionBody body, @NonNull final AccountID payerId) + throws PreCheckException { + // Throws PreCheckException if the transaction body is structurally invalid + dispatcher.dispatchPureChecks(body); + // Throws PreCheckException if the payer account does not exist + final var context = new PreHandleContextImpl(storeFactory, body, payerId, configuration, dispatcher); try { - dispatcher.dispatchPreHandle(nestedContext); + // Accumulate all required keys in the context + dispatcher.dispatchPreHandle(context); } catch (final PreCheckException ignored) { - // We must ignore/translate the exception here, as this is key gathering, not transaction validation. - throw new PreCheckException(ResponseCodeEnum.UNRESOLVABLE_REQUIRED_SIGNERS); + // Translate all prehandle failures to unresolvable required signers + throw new PreCheckException(UNRESOLVABLE_REQUIRED_SIGNERS); } - return nestedContext; + return context; } @Override @@ -512,8 +512,8 @@ public PreHandleContext innerContext() { public String toString() { return "PreHandleContextImpl{" + "accountStore=" + accountStore + ", txn=" - + txn + ", payer=" - + payer + ", payerKey=" + + txn + ", payerId=" + + payerId + ", payerKey=" + payerKey + ", requiredNonPayerKeys=" + requiredNonPayerKeys + ", innerContext=" + innerContext + ", storeFactory=" diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java index b2952a7f8f4a..91661fc6af5a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowImpl.java @@ -369,11 +369,8 @@ private Map verifySignatures( return signatureVerifier.verify(txInfo.signedBytes(), expanded); } - // too many parameters private boolean wasComputedWithCurrentNodeConfiguration(@Nullable PreHandleResult previousResult) { - // (FUTURE) IMPORTANT: Given a completely dynamic address book, we will also need to check - // whether this node's account id has changed since we computed the previous result; c.f., - // https://github.com/hashgraph/hedera-services/issues/10514 + // Notice that preHandleTransaction() always re-checks the node account ID, as it is not part of configuration return previousResult == null || previousResult.configVersion() == configProvider.getConfiguration().getVersion(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowInjectionModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowInjectionModule.java index 0adc0cf8caef..4831ecbcd394 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowInjectionModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/prehandle/PreHandleWorkflowInjectionModule.java @@ -20,7 +20,6 @@ import com.hedera.node.app.signature.SignatureVerifier; import com.hedera.node.app.signature.impl.SignatureExpanderImpl; import com.hedera.node.app.signature.impl.SignatureVerifierImpl; -import com.hedera.node.app.spi.workflows.PreHandleDispatcher; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -38,13 +37,6 @@ public interface PreHandleWorkflowInjectionModule { @Binds SignatureExpander bindSignatureExpander(SignatureExpanderImpl signatureExpander); - /** - * This binding is only needed to have a PreHandleDispatcher implementation that can be provided by dagger. - */ - @Deprecated - @Binds - PreHandleDispatcher bindPreHandleDispatcher(DummyPreHandleDispatcher preHandleDispatcher); - @Provides static ExecutorService provideExecutorService() { return ForkJoinPool.commonPool(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java index ec7f7ea62367..07b6b55e7f10 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowImpl.java @@ -52,6 +52,7 @@ import com.hedera.node.app.spi.workflows.QueryHandler; import com.hedera.node.app.store.ReadableStoreFactory; import com.hedera.node.app.throttle.SynchronizedThrottleAccumulator; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.ingest.IngestChecker; import com.hedera.node.app.workflows.ingest.SubmissionManager; import com.hedera.node.config.ConfigProvider; @@ -72,12 +73,10 @@ import java.util.List; import java.util.function.Function; import javax.inject.Inject; -import javax.inject.Singleton; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** Implementation of {@link QueryWorkflow} */ -@Singleton public final class QueryWorkflowImpl implements QueryWorkflow { private static final Logger logger = LogManager.getLogger(QueryWorkflowImpl.class); @@ -101,6 +100,12 @@ public final class QueryWorkflowImpl implements QueryWorkflow { private final FeeManager feeManager; private final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator; private final InstantSource instantSource; + private final OpWorkflowMetrics workflowMetrics; + + /** + * Indicates if the QueryWorkflow should charge for handling queries. + */ + private final boolean shouldCharge; /** * Constructor of {@code QueryWorkflowImpl} @@ -119,6 +124,7 @@ public final class QueryWorkflowImpl implements QueryWorkflow { * @param feeManager the {@link FeeManager} to calculate the fees * @param synchronizedThrottleAccumulator the {@link SynchronizedThrottleAccumulator} that checks transaction should be throttled * @param instantSource the {@link InstantSource} to get the current time + * @param shouldCharge If the workflow should charge for handling queries. * @throws NullPointerException if one of the arguments is {@code null} */ @Inject @@ -135,7 +141,9 @@ public QueryWorkflowImpl( @NonNull final ExchangeRateManager exchangeRateManager, @NonNull final FeeManager feeManager, @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator, - @NonNull final InstantSource instantSource) { + @NonNull final InstantSource instantSource, + @NonNull final OpWorkflowMetrics workflowMetrics, + final boolean shouldCharge) { this.stateAccessor = requireNonNull(stateAccessor, "stateAccessor must not be null"); this.submissionManager = requireNonNull(submissionManager, "submissionManager must not be null"); this.ingestChecker = requireNonNull(ingestChecker, "ingestChecker must not be null"); @@ -150,10 +158,14 @@ public QueryWorkflowImpl( this.synchronizedThrottleAccumulator = requireNonNull(synchronizedThrottleAccumulator, "hapiThrottling must not be null"); this.instantSource = requireNonNull(instantSource); + this.workflowMetrics = requireNonNull(workflowMetrics); + this.shouldCharge = shouldCharge; } @Override public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final BufferedData responseBuffer) { + final long queryStart = System.nanoTime(); + requireNonNull(requestBuffer); requireNonNull(responseBuffer); @@ -190,7 +202,7 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe Transaction allegedPayment; TransactionBody txBody; AccountID payerID = null; - if (paymentRequired) { + if (shouldCharge && paymentRequired) { allegedPayment = queryHeader.paymentOrElse(Transaction.DEFAULT); final var configuration = configProvider.getConfiguration(); @@ -257,7 +269,7 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe handler.validate(context); // 5. Check query throttles - if (synchronizedThrottleAccumulator.shouldThrottle(function, query, payerID)) { + if (shouldCharge && synchronizedThrottleAccumulator.shouldThrottle(function, query, payerID)) { throw new PreCheckException(BUSY); } @@ -295,6 +307,8 @@ public void handleQuery(@NonNull final Bytes requestBuffer, @NonNull final Buffe logger.warn("Unexpected IO exception while writing protobuf", e); throw new StatusRuntimeException(Status.INTERNAL); } + + workflowMetrics.updateDuration(function, (int) (System.nanoTime() - queryStart)); } private Query parseQuery(Bytes requestBuffer) { diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowInjectionModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowInjectionModule.java index 2c2c769426f1..aa6b221ba4b8 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowInjectionModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/QueryWorkflowInjectionModule.java @@ -19,6 +19,8 @@ import com.hedera.hapi.node.base.ResponseType; import com.hedera.hapi.node.transaction.Query; import com.hedera.node.app.components.QueryInjectionComponent; +import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.service.addressbook.impl.handlers.AddressBookHandlers; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusHandlers; import com.hedera.node.app.service.contract.impl.handlers.ContractHandlers; @@ -26,14 +28,23 @@ import com.hedera.node.app.service.networkadmin.impl.handlers.NetworkAdminHandlers; import com.hedera.node.app.service.schedule.impl.handlers.ScheduleHandlers; import com.hedera.node.app.service.token.impl.handlers.TokenHandlers; +import com.hedera.node.app.spi.authorization.Authorizer; +import com.hedera.node.app.spi.records.RecordCache; import com.hedera.node.app.state.WorkingStateAccessor; +import com.hedera.node.app.throttle.SynchronizedThrottleAccumulator; +import com.hedera.node.app.workflows.OpWorkflowMetrics; +import com.hedera.node.app.workflows.ingest.IngestChecker; +import com.hedera.node.app.workflows.ingest.SubmissionManager; +import com.hedera.node.app.workflows.query.annotations.OperatorQueries; +import com.hedera.node.app.workflows.query.annotations.UserQueries; +import com.hedera.node.config.ConfigProvider; import com.hedera.pbj.runtime.Codec; import com.swirlds.common.utility.AutoCloseableWrapper; import com.swirlds.state.State; -import dagger.Binds; import dagger.Module; import dagger.Provides; import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.InstantSource; import java.util.function.Function; import java.util.function.Supplier; import javax.inject.Singleton; @@ -43,11 +54,79 @@ */ @Module(subcomponents = {QueryInjectionComponent.class}) public interface QueryWorkflowInjectionModule { - @Binds + Runnable NO_OP = () -> {}; + + @Provides @Singleton - QueryWorkflow bindQueryWorkflow(QueryWorkflowImpl queryWorkflow); + @UserQueries + static QueryWorkflow provideUserQueryWorkflow( + @NonNull final Function> stateAccessor, + @NonNull final SubmissionManager submissionManager, + @NonNull final QueryChecker queryChecker, + @NonNull final IngestChecker ingestChecker, + @NonNull final QueryDispatcher dispatcher, + @NonNull final Codec queryParser, + @NonNull final ConfigProvider configProvider, + @NonNull final RecordCache recordCache, + @NonNull final Authorizer authorizer, + @NonNull final ExchangeRateManager exchangeRateManager, + @NonNull final FeeManager feeManager, + @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator, + @NonNull final InstantSource instantSource, + @NonNull final OpWorkflowMetrics opWorkflowMetrics) { + return new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + true); + } - Runnable NO_OP = () -> {}; + @Provides + @Singleton + @OperatorQueries + static QueryWorkflow provideOperatorQueryWorkflow( + @NonNull final Function> stateAccessor, + @NonNull final SubmissionManager submissionManager, + @NonNull final QueryChecker queryChecker, + @NonNull final IngestChecker ingestChecker, + @NonNull final QueryDispatcher dispatcher, + @NonNull final Codec queryParser, + @NonNull final ConfigProvider configProvider, + @NonNull final RecordCache recordCache, + @NonNull final Authorizer authorizer, + @NonNull final ExchangeRateManager exchangeRateManager, + @NonNull final FeeManager feeManager, + @NonNull final SynchronizedThrottleAccumulator synchronizedThrottleAccumulator, + @NonNull final InstantSource instantSource, + @NonNull final OpWorkflowMetrics opWorkflowMetrics) { + return new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + false); + } @Provides @Singleton diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/OperatorQueries.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/OperatorQueries.java new file mode 100644 index 000000000000..300b877ba3f8 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/OperatorQueries.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.query.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Qualifies a {@link com.hedera.node.app.workflows.query.QueryWorkflow} + * as being used to process node operator queries. + */ +@Target({METHOD, PARAMETER, TYPE}) +@Retention(RUNTIME) +@Documented +public @interface OperatorQueries {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/UserQueries.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/UserQueries.java new file mode 100644 index 000000000000..1b3ce7c47768 --- /dev/null +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/query/annotations/UserQueries.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.query.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * Qualifies a {@link com.hedera.node.app.workflows.query.QueryWorkflow} + * as being used to process user queries. + */ +@Target({METHOD, PARAMETER, TYPE}) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface UserQueries {} diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java index 051b9552093c..38602cc7ff8a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/ExecutorComponent.java @@ -25,6 +25,7 @@ import com.hedera.node.app.services.ServicesInjectionModule; import com.hedera.node.app.state.HederaStateInjectionModule; import com.hedera.node.app.throttle.ThrottleServiceModule; +import com.hedera.node.app.tss.TssBaseService; import com.hedera.node.app.workflows.FacilityInitModule; import com.hedera.node.app.workflows.handle.DispatchProcessor; import com.hedera.node.app.workflows.handle.HandleWorkflowModule; @@ -58,6 +59,9 @@ public interface ExecutorComponent { @Component.Builder interface Builder { + @BindsInstance + Builder tssBaseService(TssBaseService tssBaseService); + @BindsInstance Builder fileServiceImpl(FileServiceImpl fileService); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java index 3e5e682768ba..e6bee9c7277a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java @@ -16,6 +16,7 @@ package com.hedera.node.app.workflows.standalone; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.hedera.node.app.workflows.standalone.impl.NoopVerificationStrategies.NOOP_VERIFICATION_STRATEGIES; import com.hedera.node.app.config.BootstrapConfigProviderImpl; @@ -26,6 +27,8 @@ import com.hedera.node.app.signature.AppSignatureVerifier; import com.hedera.node.app.signature.impl.SignatureExpanderImpl; import com.hedera.node.app.signature.impl.SignatureVerifierImpl; +import com.hedera.node.app.state.recordcache.LegacyListRecordSource; +import com.hedera.node.app.tss.TssBaseServiceImpl; import com.hedera.node.config.data.HederaConfig; import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.metrics.noop.NoOpMetrics; @@ -35,6 +38,7 @@ import java.time.InstantSource; import java.util.List; import java.util.Map; +import java.util.concurrent.ForkJoinPool; import java.util.function.Supplier; import org.hyperledger.besu.evm.tracing.OperationTracer; @@ -73,9 +77,10 @@ public TransactionExecutor newExecutor( final var dispatch = executor.standaloneDispatchFactory().newDispatch(state, transactionBody, consensusNow); tracerBinding.runWhere(List.of(operationTracers), () -> executor.dispatchProcessor() .processDispatch(dispatch)); - return dispatch.stack() + final var recordSource = dispatch.stack() .buildHandleOutput(consensusNow, exchangeRateManager.exchangeRates()) - .recordsOrThrow(); + .recordSourceOrThrow(); + return ((LegacyListRecordSource) recordSource).precomputedRecords(); }; } @@ -87,13 +92,17 @@ private ExecutorComponent newExecutorComponent( new AppSignatureVerifier( bootstrapConfigProvider.getConfiguration().getConfigData(HederaConfig.class), new SignatureExpanderImpl(), - new SignatureVerifierImpl(CryptographyHolder.get()))); + new SignatureVerifierImpl(CryptographyHolder.get())), + UNAVAILABLE_GOSSIP); + final var tssBaseService = + new TssBaseServiceImpl(appContext, ForkJoinPool.commonPool(), ForkJoinPool.commonPool()); final var contractService = new ContractServiceImpl(appContext, NOOP_VERIFICATION_STRATEGIES, tracerBinding); final var fileService = new FileServiceImpl(); final var configProvider = new ConfigProviderImpl(false, null, properties); return DaggerExecutorComponent.builder() .configProviderImpl(configProvider) .bootstrapConfigProviderImpl(bootstrapConfigProvider) + .tssBaseService(tssBaseService) .fileServiceImpl(fileService) .contractServiceImpl(contractService) .metrics(new NoOpMetrics()) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java index 6069da2526ef..acb07d7e0f1a 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/impl/StandaloneDispatchFactory.java @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.standalone.impl; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.NODE; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.workflows.handle.HandleWorkflow.initializeBuilderInfo; import static com.hedera.node.app.workflows.handle.dispatch.ChildDispatchFactory.NO_OP_KEY_VERIFIER; @@ -210,12 +211,16 @@ public Dispatch newDispatch( preHandleResult.getHollowAccounts(), dispatchHandleContext, stack, - USER, + getTxnCategory(preHandleResult), tokenContext, preHandleResult, HandleContext.ConsensusThrottling.ON); } + public static HandleContext.TransactionCategory getTxnCategory(final PreHandleResult preHandleResult) { + return requireNonNull(preHandleResult.txInfo()).signatureMap().sigPair().isEmpty() ? NODE : USER; + } + private ConsensusTransaction consensusTransactionFor(@NonNull final TransactionBody transactionBody) { final var signedTransaction = new SignedTransaction(TransactionBody.PROTOBUF.toBytes(transactionBody), SignatureMap.DEFAULT); diff --git a/hedera-node/hedera-app/src/main/java/module-info.java b/hedera-node/hedera-app/src/main/java/module-info.java index eeeb435abd80..6a69203afbc5 100644 --- a/hedera-node/hedera-app/src/main/java/module-info.java +++ b/hedera-node/hedera-app/src/main/java/module-info.java @@ -41,7 +41,6 @@ requires com.swirlds.merkledb; requires com.swirlds.virtualmap; requires com.google.common; - requires com.google.errorprone.annotations; requires com.google.protobuf; requires io.grpc.netty; requires io.grpc; @@ -53,6 +52,7 @@ requires static com.github.spotbugs.annotations; requires static com.google.auto.service; requires static java.compiler; + requires static org.jetbrains.annotations; // javax.annotation.processing.Generated exports com.hedera.node.app; @@ -109,7 +109,10 @@ exports com.hedera.node.app.workflows.handle.metric; exports com.hedera.node.app.roster; exports com.hedera.node.app.tss; - exports com.hedera.node.app.tss.impl; + exports com.hedera.node.app.tss.api; + exports com.hedera.node.app.tss.pairings; + exports com.hedera.node.app.tss.handlers; + exports com.hedera.node.app.tss.stores; provides ConfigurationExtension with ServicesConfigExtension; diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java index b366ba087f40..10a81ac8703d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/ServicesMainTest.java @@ -171,8 +171,8 @@ private void withBadCommandLineArgs() { .thenReturn(legacyConfigProperties); List nodeIds = new ArrayList<>(); - nodeIds.add(new NodeId(1)); - nodeIds.add(new NodeId(2)); + nodeIds.add(NodeId.of(1)); + nodeIds.add(NodeId.of(2)); bootstrapUtilsMockedStatic .when(() -> BootstrapUtils.getNodesToRun(any(), any())) diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/authorization/PrivilegesVerifierTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/authorization/PrivilegesVerifierTest.java index 83c0553fae87..bbd494c03cf7 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/authorization/PrivilegesVerifierTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/authorization/PrivilegesVerifierTest.java @@ -46,6 +46,7 @@ import com.hederahashgraph.api.proto.java.FreezeTransactionBody; import com.hederahashgraph.api.proto.java.NodeCreateTransactionBody; import com.hederahashgraph.api.proto.java.NodeDeleteTransactionBody; +import com.hederahashgraph.api.proto.java.NodeUpdateTransactionBody; import com.hederahashgraph.api.proto.java.SignedTransaction; import com.hederahashgraph.api.proto.java.SystemDeleteTransactionBody; import com.hederahashgraph.api.proto.java.SystemUndeleteTransactionBody; @@ -361,17 +362,25 @@ void freezeAdminCanFreeze() throws InvalidProtocolBufferException { } @Test - void nodeAdminCanCreate() throws InvalidProtocolBufferException { + void addressBookAdminCanCreate() throws InvalidProtocolBufferException { // given: - var txn = nodeAdminTxn().setNodeCreate(NodeCreateTransactionBody.getDefaultInstance()); + var txn = addressBookAdminTxn().setNodeCreate(NodeCreateTransactionBody.getDefaultInstance()); // expect: assertEquals(SystemOpAuthorization.AUTHORIZED, subject.authForTestCase(accessor(txn))); } @Test - void nodeAdminCanDelete() throws InvalidProtocolBufferException { + void addressBookAdminCanUpdate() throws InvalidProtocolBufferException { // given: - var txn = nodeAdminTxn().setNodeDelete(NodeDeleteTransactionBody.getDefaultInstance()); + var txn = addressBookAdminTxn().setNodeUpdate(NodeUpdateTransactionBody.getDefaultInstance()); + // expect: + assertEquals(SystemOpAuthorization.AUTHORIZED, subject.authForTestCase(accessor(txn))); + } + + @Test + void addressBookAdminCanDelete() throws InvalidProtocolBufferException { + // given: + var txn = addressBookAdminTxn().setNodeDelete(NodeDeleteTransactionBody.getDefaultInstance()); // expect: assertEquals(SystemOpAuthorization.AUTHORIZED, subject.authForTestCase(accessor(txn))); } @@ -672,7 +681,7 @@ private TransactionBody.Builder exchangeRatesAdminTxn() { return txnWithPayer(57); } - private TransactionBody.Builder nodeAdminTxn() { + private TransactionBody.Builder addressBookAdminTxn() { return txnWithPayer(55); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockItemsTranslatorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockItemsTranslatorTest.java new file mode 100644 index 000000000000..21a080c21dfa --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockItemsTranslatorTest.java @@ -0,0 +1,619 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_CREATE_TOPIC; +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; +import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; +import static com.hedera.hapi.node.base.HederaFunctionality.FILE_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.NODE_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.SCHEDULE_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.SCHEDULE_DELETE; +import static com.hedera.hapi.node.base.HederaFunctionality.SCHEDULE_SIGN; +import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_AIRDROP; +import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_MINT; +import static com.hedera.hapi.node.base.HederaFunctionality.UTIL_PRNG; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.node.app.blocks.BlockItemsTranslator.BLOCK_ITEMS_TRANSLATOR; +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.block.stream.output.CallContractOutput; +import com.hedera.hapi.block.stream.output.CreateContractOutput; +import com.hedera.hapi.block.stream.output.CreateScheduleOutput; +import com.hedera.hapi.block.stream.output.CryptoTransferOutput; +import com.hedera.hapi.block.stream.output.EthereumOutput; +import com.hedera.hapi.block.stream.output.SignScheduleOutput; +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.block.stream.output.UtilPrngOutput; +import com.hedera.hapi.node.base.AccountAmount; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.FileID; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.NftID; +import com.hedera.hapi.node.base.PendingAirdropId; +import com.hedera.hapi.node.base.PendingAirdropValue; +import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.TimestampSeconds; +import com.hedera.hapi.node.base.TokenAssociation; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.contract.ContractFunctionResult; +import com.hedera.hapi.node.transaction.AssessedCustomFee; +import com.hedera.hapi.node.transaction.ExchangeRate; +import com.hedera.hapi.node.transaction.ExchangeRateSet; +import com.hedera.hapi.node.transaction.PendingAirdropRecord; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.blocks.impl.contexts.AirdropOpContext; +import com.hedera.node.app.blocks.impl.contexts.BaseOpContext; +import com.hedera.node.app.blocks.impl.contexts.ContractOpContext; +import com.hedera.node.app.blocks.impl.contexts.CryptoOpContext; +import com.hedera.node.app.blocks.impl.contexts.FileOpContext; +import com.hedera.node.app.blocks.impl.contexts.MintOpContext; +import com.hedera.node.app.blocks.impl.contexts.NodeOpContext; +import com.hedera.node.app.blocks.impl.contexts.ScheduleOpContext; +import com.hedera.node.app.blocks.impl.contexts.SubmitOpContext; +import com.hedera.node.app.blocks.impl.contexts.SupplyChangeOpContext; +import com.hedera.node.app.blocks.impl.contexts.TokenOpContext; +import com.hedera.node.app.blocks.impl.contexts.TopicOpContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class BlockItemsTranslatorTest { + private static final long FEE = 12324567890L; + private static final String MEMO = "MEMO"; + private static final Timestamp CONSENSUS_TIME = new Timestamp(1_234_567, 890); + private static final Timestamp PARENT_CONSENSUS_TIME = new Timestamp(1_234_567, 0); + private static final ScheduleID SCHEDULE_REF = + ScheduleID.newBuilder().scheduleNum(123L).build(); + private static final ContractFunctionResult FUNCTION_RESULT = + ContractFunctionResult.newBuilder().amount(666L).build(); + private static final List ASSESSED_CUSTOM_FEES = List.of(new AssessedCustomFee( + 1L, + TokenID.newBuilder().tokenNum(123).build(), + AccountID.newBuilder().accountNum(98L).build(), + List.of(AccountID.newBuilder().accountNum(2L).build()))); + private static final TransferList TRANSFER_LIST = TransferList.newBuilder() + .accountAmounts( + AccountAmount.newBuilder() + .amount(-1) + .accountID(AccountID.newBuilder().accountNum(2L).build()) + .build(), + AccountAmount.newBuilder() + .amount(+1) + .accountID(AccountID.newBuilder().accountNum(98L).build()) + .build()) + .build(); + private static final List PAID_STAKING_REWARDS = List.of( + AccountAmount.newBuilder() + .amount(-1) + .accountID(AccountID.newBuilder().accountNum(800L).build()) + .build(), + AccountAmount.newBuilder() + .amount(+1) + .accountID(AccountID.newBuilder().accountNum(2L).build()) + .build()); + private static final List AUTO_TOKEN_ASSOCIATIONS = List.of(new TokenAssociation( + TokenID.newBuilder().tokenNum(123).build(), + AccountID.newBuilder().accountNum(98L).build())); + private static final List PENDING_AIRDROP_RECORDS = List.of(new PendingAirdropRecord( + PendingAirdropId.newBuilder() + .nonFungibleToken( + new NftID(TokenID.newBuilder().tokenNum(123).build(), 1L)) + .build(), + new PendingAirdropValue(666L))); + private static final List TOKEN_TRANSFER_LISTS = List.of(new TokenTransferList( + TokenID.newBuilder().tokenNum(123).build(), TRANSFER_LIST.accountAmounts(), List.of(), 0)); + private static final TransactionID TXN_ID = TransactionID.newBuilder() + .accountID(AccountID.newBuilder().accountNum(2L).build()) + .build(); + private static final ExchangeRateSet RATES = ExchangeRateSet.newBuilder() + .currentRate(new ExchangeRate(1, 2, TimestampSeconds.DEFAULT)) + .nextRate(new ExchangeRate(3, 4, TimestampSeconds.DEFAULT)) + .build(); + private static final TransactionResult TRANSACTION_RESULT = TransactionResult.newBuilder() + .exchangeRate(RATES) + .consensusTimestamp(CONSENSUS_TIME) + .parentConsensusTimestamp(PARENT_CONSENSUS_TIME) + .scheduleRef(SCHEDULE_REF) + .transactionFeeCharged(FEE) + .transferList(TRANSFER_LIST) + .tokenTransferLists(TOKEN_TRANSFER_LISTS) + .automaticTokenAssociations(AUTO_TOKEN_ASSOCIATIONS) + .paidStakingRewards(PAID_STAKING_REWARDS) + .status(SUCCESS) + .build(); + private static final TransactionReceipt EXPECTED_BASE_RECEIPT = + TransactionReceipt.newBuilder().exchangeRate(RATES).status(SUCCESS).build(); + private static final TransactionRecord EXPECTED_BASE_RECORD = TransactionRecord.newBuilder() + .transactionID(TXN_ID) + .memo(MEMO) + .transactionHash(Bytes.wrap(noThrowSha384HashOf( + Transaction.PROTOBUF.toBytes(Transaction.DEFAULT).toByteArray()))) + .consensusTimestamp(CONSENSUS_TIME) + .parentConsensusTimestamp(PARENT_CONSENSUS_TIME) + .scheduleRef(SCHEDULE_REF) + .transactionFee(FEE) + .transferList(TRANSFER_LIST) + .tokenTransferLists(TOKEN_TRANSFER_LISTS) + .automaticTokenAssociations(AUTO_TOKEN_ASSOCIATIONS) + .paidStakingRewards(PAID_STAKING_REWARDS) + .receipt(EXPECTED_BASE_RECEIPT) + .build(); + private static final TransactionID SCHEDULED_TXN_ID = + TransactionID.newBuilder().scheduled(true).build(); + private static final ScheduleID SCHEDULE_ID = + ScheduleID.newBuilder().scheduleNum(666L).build(); + private static final ContractID CONTRACT_ID = + ContractID.newBuilder().contractNum(666L).build(); + private static final AccountID ACCOUNT_ID = + AccountID.newBuilder().accountNum(666L).build(); + private static final TokenID TOKEN_ID = TokenID.newBuilder().tokenNum(666L).build(); + private static final TopicID TOPIC_ID = TopicID.newBuilder().topicNum(666L).build(); + private static final FileID FILE_ID = FileID.newBuilder().fileNum(666L).build(); + private static final long NODE_ID = 666L; + private static final Bytes ETH_HASH = Bytes.fromHex("01".repeat(32)); + private static final Bytes EVM_ADDRESS = Bytes.fromHex("0101010101010101010101010101010101010101"); + private static final Bytes RUNNING_HASH = Bytes.fromHex("01".repeat(48)); + private static final long RUNNING_HASH_VERSION = 7L; + private static final long TOPIC_SEQUENCE_NUMBER = 666L; + private static final long NEW_TOTAL_SUPPLY = 666L; + private static final List SERIAL_NOS = List.of(1L, 2L, 3L); + + // --- RECEIPT TRANSLATION TESTS --- + @ParameterizedTest + @EnumSource( + value = HederaFunctionality.class, + mode = EnumSource.Mode.EXCLUDE, + names = { + "CONTRACT_CALL", + "CONTRACT_CREATE", + "CONTRACT_UPDATE", + "CONTRACT_DELETE", + "ETHEREUM_TRANSACTION", + "CRYPTO_CREATE", + "CRYPTO_UPDATE", + "FILE_CREATE", + "NODE_CREATE", + "TOKEN_CREATE", + "CONSENSUS_CREATE_TOPIC", + "SCHEDULE_CREATE", + "SCHEDULE_SIGN", + "SCHEDULE_DELETE", + "CONSENSUS_SUBMIT_MESSAGE", + "TOKEN_MINT", + "TOKEN_ACCOUNT_WIPE", + "TOKEN_BURN", + }) + void mostOpsUseJustUseBaseOpContextForReceipt(@NonNull final HederaFunctionality function) { + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, function); + + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT, actualReceipt); + } + + @ParameterizedTest + @EnumSource( + value = HederaFunctionality.class, + names = { + "CONTRACT_CALL", + "CONTRACT_CREATE", + "CONTRACT_UPDATE", + "CONTRACT_DELETE", + "ETHEREUM_TRANSACTION", + }) + void contractOpsUseContractOpContext(@NonNull final HederaFunctionality function) { + final var context = new ContractOpContext(MEMO, TXN_ID, Transaction.DEFAULT, function, CONTRACT_ID); + + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().contractID(CONTRACT_ID).build(), actualReceipt); + } + + @ParameterizedTest + @EnumSource( + value = HederaFunctionality.class, + names = { + "CRYPTO_CREATE", + "CRYPTO_UPDATE", + }) + void certainCryptoOpsUseCryptoOpContext(@NonNull final HederaFunctionality function) { + final var context = new CryptoOpContext(MEMO, TXN_ID, Transaction.DEFAULT, function, ACCOUNT_ID, EVM_ADDRESS); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().accountID(ACCOUNT_ID).build(), actualReceipt); + } + + @Test + void fileCreateUsesFileOpContext() { + final var context = new FileOpContext(MEMO, TXN_ID, Transaction.DEFAULT, FILE_CREATE, FILE_ID); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().fileID(FILE_ID).build(), actualReceipt); + } + + @Test + void nodeCreateUsesNodeOpContext() { + final var context = new NodeOpContext(MEMO, TXN_ID, Transaction.DEFAULT, NODE_CREATE, NODE_ID); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().nodeId(NODE_ID).build(), actualReceipt); + } + + @Test + void tokenCreateUsesTokenOpContext() { + final var context = new TokenOpContext(MEMO, TXN_ID, Transaction.DEFAULT, TOKEN_CREATE, TOKEN_ID); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().tokenID(TOKEN_ID).build(), actualReceipt); + } + + @Test + void topicCreateUsesTopicOpContext() { + final var context = new TopicOpContext(MEMO, TXN_ID, Transaction.DEFAULT, CONSENSUS_CREATE_TOPIC, TOPIC_ID); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().topicID(TOPIC_ID).build(), actualReceipt); + } + + @Test + void scheduleCreateUsesCreateScheduleOutputOnlyIfPresent() { + final var output = TransactionOutput.newBuilder() + .createSchedule(new CreateScheduleOutput(SCHEDULE_ID, SCHEDULED_TXN_ID)) + .build(); + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, SCHEDULE_CREATE); + + final var actualReceiptNoOutput = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT, actualReceiptNoOutput); + + final var actualReceiptWithOutput = + BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECEIPT + .copyBuilder() + .scheduleID(SCHEDULE_ID) + .scheduledTransactionID(SCHEDULED_TXN_ID) + .build(), + actualReceiptWithOutput); + } + + @Test + void scheduleSignUsesSignScheduleOutputOnlyIfPresent() { + final var output = TransactionOutput.newBuilder() + .signSchedule(new SignScheduleOutput(SCHEDULED_TXN_ID)) + .build(); + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, SCHEDULE_SIGN); + + final var actualReceiptNoOutput = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT, actualReceiptNoOutput); + + final var actualReceiptWithOutput = + BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECEIPT + .copyBuilder() + .scheduledTransactionID(SCHEDULED_TXN_ID) + .build(), + actualReceiptWithOutput); + } + + @Test + void scheduleDeleteUsesScheduleOpContext() { + final var context = new ScheduleOpContext(MEMO, TXN_ID, Transaction.DEFAULT, SCHEDULE_DELETE, SCHEDULE_ID); + + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECEIPT.copyBuilder().scheduleID(SCHEDULE_ID).build(), actualReceipt); + } + + @Test + void submitMessageUsesSubmitOpContext() { + final var context = new SubmitOpContext( + MEMO, + TXN_ID, + Transaction.DEFAULT, + CONSENSUS_SUBMIT_MESSAGE, + RUNNING_HASH, + RUNNING_HASH_VERSION, + TOPIC_SEQUENCE_NUMBER); + + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECEIPT + .copyBuilder() + .topicRunningHash(RUNNING_HASH) + .topicRunningHashVersion(RUNNING_HASH_VERSION) + .topicSequenceNumber(TOPIC_SEQUENCE_NUMBER) + .build(), + actualReceipt); + } + + @Test + void tokenMintUsesMintOpContext() { + final var context = + new MintOpContext(MEMO, TXN_ID, Transaction.DEFAULT, TOKEN_MINT, SERIAL_NOS, NEW_TOTAL_SUPPLY); + + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECEIPT + .copyBuilder() + .newTotalSupply(NEW_TOTAL_SUPPLY) + .serialNumbers(SERIAL_NOS) + .build(), + actualReceipt); + } + + @ParameterizedTest + @EnumSource( + value = HederaFunctionality.class, + names = { + "TOKEN_ACCOUNT_WIPE", + "TOKEN_BURN", + }) + void supplyChangeOpsUseSupplyChangeContext(@NonNull final HederaFunctionality function) { + final var context = new SupplyChangeOpContext(MEMO, TXN_ID, Transaction.DEFAULT, function, NEW_TOTAL_SUPPLY); + final var actualReceipt = BLOCK_ITEMS_TRANSLATOR.translateReceipt(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECEIPT + .copyBuilder() + .newTotalSupply(NEW_TOTAL_SUPPLY) + .build(), + actualReceipt); + } + + // --- RECORD TRANSLATION TESTS --- + + @Test + void contractCallUsesResultOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .contractCall(new CallContractOutput(List.of(), FUNCTION_RESULT)) + .build(); + final var context = new ContractOpContext(MEMO, TXN_ID, Transaction.DEFAULT, CONTRACT_CALL, CONTRACT_ID); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .contractCallResult((ContractFunctionResult) null) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .contractCallResult(FUNCTION_RESULT) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordWithOutput); + } + + @Test + void contractCreateUsesResultOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .contractCreate(new CreateContractOutput(List.of(), FUNCTION_RESULT)) + .build(); + final var context = new ContractOpContext(MEMO, TXN_ID, Transaction.DEFAULT, CONTRACT_CREATE, CONTRACT_ID); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .contractCreateResult((ContractFunctionResult) null) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .contractCreateResult(FUNCTION_RESULT) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordWithOutput); + } + + @Test + void ethTxCallUsesResultOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .ethereumCall(EthereumOutput.newBuilder() + .ethereumHash(ETH_HASH) + .ethereumCallResult(FUNCTION_RESULT) + .build()) + .build(); + final var context = new ContractOpContext(MEMO, TXN_ID, Transaction.DEFAULT, ETHEREUM_TRANSACTION, CONTRACT_ID); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .ethereumHash(Bytes.EMPTY) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .ethereumHash(ETH_HASH) + .contractCallResult(FUNCTION_RESULT) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordWithOutput); + } + + @Test + void ethTxCreateUsesResultOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .ethereumCall(EthereumOutput.newBuilder() + .ethereumHash(ETH_HASH) + .ethereumCreateResult(FUNCTION_RESULT) + .build()) + .build(); + final var context = new ContractOpContext(MEMO, TXN_ID, Transaction.DEFAULT, ETHEREUM_TRANSACTION, CONTRACT_ID); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .ethereumHash(Bytes.EMPTY) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .ethereumHash(ETH_HASH) + .contractCreateResult(FUNCTION_RESULT) + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .contractID(CONTRACT_ID) + .build()) + .build(), + actualRecordWithOutput); + } + + @Test + void cryptoTransferUsesSynthResultOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .contractCall(new CallContractOutput(List.of(), FUNCTION_RESULT)) + .build(); + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, CRYPTO_TRANSFER); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECORD, actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .contractCallResult(FUNCTION_RESULT) + .build(), + actualRecordWithOutput); + } + + @Test + void cryptoTransferUsesCustomFeesOutputIfPresent() { + final var output = TransactionOutput.newBuilder() + .cryptoTransfer(new CryptoTransferOutput(ASSESSED_CUSTOM_FEES)) + .build(); + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, CRYPTO_TRANSFER); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECORD, actualRecordNoOutput); + + final var actualRecordWithOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, output); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .assessedCustomFees(ASSESSED_CUSTOM_FEES) + .build(), + actualRecordWithOutput); + } + + @ParameterizedTest + @EnumSource( + value = HederaFunctionality.class, + names = { + "CRYPTO_CREATE", + "CRYPTO_UPDATE", + }) + void certainCryptoOpsUseEvmAddressFromContext(@NonNull final HederaFunctionality function) { + final var context = new CryptoOpContext(MEMO, TXN_ID, Transaction.DEFAULT, function, ACCOUNT_ID, EVM_ADDRESS); + final var actualRecord = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .receipt(EXPECTED_BASE_RECEIPT + .copyBuilder() + .accountID(ACCOUNT_ID) + .build()) + .evmAddress(EVM_ADDRESS) + .build(), + actualRecord); + } + + @Test + void tokenAirdropUsesPendingFromContext() { + final var context = + new AirdropOpContext(MEMO, TXN_ID, Transaction.DEFAULT, TOKEN_AIRDROP, PENDING_AIRDROP_RECORDS); + final var actualRecord = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals( + EXPECTED_BASE_RECORD + .copyBuilder() + .newPendingAirdrops(PENDING_AIRDROP_RECORDS) + .build(), + actualRecord); + } + + @Test + void utilPrngUsesOutputIfPresent() { + final var numberOutput = TransactionOutput.newBuilder() + .utilPrng(UtilPrngOutput.newBuilder().prngNumber(123).build()) + .build(); + final var seedOutput = TransactionOutput.newBuilder() + .utilPrng(UtilPrngOutput.newBuilder().prngBytes(RUNNING_HASH).build()) + .build(); + final var context = new BaseOpContext(MEMO, TXN_ID, Transaction.DEFAULT, UTIL_PRNG); + + final var actualRecordNoOutput = BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT); + assertEquals(EXPECTED_BASE_RECORD, actualRecordNoOutput); + + final var actualRecordWithNumberOutput = + BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, numberOutput); + assertEquals(EXPECTED_BASE_RECORD.copyBuilder().prngNumber(123).build(), actualRecordWithNumberOutput); + + final var actualRecordWithSeedOutput = + BLOCK_ITEMS_TRANSLATOR.translateRecord(context, TRANSACTION_RESULT, seedOutput); + assertEquals(EXPECTED_BASE_RECORD.copyBuilder().prngBytes(RUNNING_HASH).build(), actualRecordWithSeedOutput); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamBuilderTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamBuilderTest.java index 598c63c92b09..476061949b3c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamBuilderTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamBuilderTest.java @@ -16,6 +16,8 @@ package com.hedera.node.app.blocks; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.hapi.node.base.HederaFunctionality.UTIL_PRNG; import static com.hedera.hapi.util.HapiUtils.asTimestamp; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.NOOP_RECORD_CUSTOMIZER; @@ -83,7 +85,7 @@ void testBlockItemsWithCryptoTransferOutput() { .assessedCustomFees(List.of(assessedCustomFee)) .functionality(HederaFunctionality.CRYPTO_TRANSFER); - List blockItems = itemsBuilder.build(); + List blockItems = itemsBuilder.build().blockItems(); validateTransactionBlockItems(blockItems); validateTransactionResult(blockItems); @@ -101,8 +103,9 @@ void testBlockItemsWithUtilPrngOutput(TransactionRecord.EntropyOneOfType entropy return; } if (entropyOneOfType == TransactionRecord.EntropyOneOfType.PRNG_BYTES) { - final var itemsBuilder = createBaseBuilder().entropyBytes(prngBytes); - List blockItems = itemsBuilder.build(); + final var itemsBuilder = + createBaseBuilder().functionality(UTIL_PRNG).entropyBytes(prngBytes); + List blockItems = itemsBuilder.build().blockItems(); validateTransactionBlockItems(blockItems); validateTransactionResult(blockItems); @@ -112,8 +115,9 @@ void testBlockItemsWithUtilPrngOutput(TransactionRecord.EntropyOneOfType entropy assertTrue(output.hasUtilPrng()); assertEquals(prngBytes, output.utilPrng().prngBytes()); } else { - final var itemsBuilder = createBaseBuilder().entropyNumber(ENTROPY_NUMBER); - List blockItems = itemsBuilder.build(); + final var itemsBuilder = + createBaseBuilder().functionality(UTIL_PRNG).entropyNumber(ENTROPY_NUMBER); + List blockItems = itemsBuilder.build().blockItems(); validateTransactionBlockItems(blockItems); validateTransactionResult(blockItems); @@ -128,10 +132,11 @@ void testBlockItemsWithUtilPrngOutput(TransactionRecord.EntropyOneOfType entropy @Test void testBlockItemsWithContractCallOutput() { final var itemsBuilder = createBaseBuilder() + .functionality(CONTRACT_CALL) .contractCallResult(contractCallResult) .addContractStateChanges(contractStateChanges, false); - List blockItems = itemsBuilder.build(); + List blockItems = itemsBuilder.build().blockItems(); validateTransactionBlockItems(blockItems); validateTransactionResult(blockItems); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java index d164fc526ab2..0b814a9a506b 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/BlockStreamServiceTest.java @@ -16,14 +16,11 @@ package com.hedera.node.app.blocks; -import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema; -import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema; import com.swirlds.state.spi.SchemaRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,43 +32,17 @@ final class BlockStreamServiceTest { @Mock private SchemaRegistry schemaRegistry; - private BlockStreamService subject; + private final BlockStreamService subject = new BlockStreamService(); @Test void serviceNameAsExpected() { - givenDisabledSubject(); - assertThat(subject.getServiceName()).isEqualTo("BlockStreamService"); } @Test void enabledSubjectRegistersV0540Schema() { - givenEnabledSubject(); - subject.registerSchemas(schemaRegistry); - verify(schemaRegistry).register(argThat(s -> s instanceof V0540BlockStreamSchema)); - } - - @Test - void disabledSubjectDoesNotRegisterSchema() { - givenDisabledSubject(); - - subject.registerSchemas(schemaRegistry); - - verifyNoInteractions(schemaRegistry); - - assertThat(subject.migratedLastBlockHash()).isEmpty(); - } - - private void givenEnabledSubject() { - final var testConfig = HederaTestConfigBuilder.create() - .withValue("blockStream.streamMode", "BOTH") - .getOrCreateConfig(); - subject = new BlockStreamService(testConfig); - } - - private void givenDisabledSubject() { - subject = new BlockStreamService(DEFAULT_CONFIG); + verify(schemaRegistry).register(argThat(s -> s instanceof V0560BlockStreamSchema)); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamBuilderOutputTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamBuilderOutputTest.java new file mode 100644 index 000000000000..ff1c7d583709 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamBuilderOutputTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.block.stream.output.CallContractOutput; +import com.hedera.hapi.block.stream.output.CryptoTransferOutput; +import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Timestamp; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.contract.ContractFunctionResult; +import com.hedera.hapi.node.transaction.AssessedCustomFee; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.node.app.blocks.BlockItemsTranslator; +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockStreamBuilderOutputTest { + private static final Timestamp CONSENSUS_TIME = new Timestamp(1_234_567, 890); + private static final TransactionID TXN_ID = TransactionID.newBuilder() + .accountID(AccountID.newBuilder().accountNum(2L).build()) + .build(); + private static final List ASSESSED_CUSTOM_FEES = List.of(new AssessedCustomFee( + 1L, + TokenID.newBuilder().tokenNum(123).build(), + AccountID.newBuilder().accountNum(98L).build(), + List.of(AccountID.newBuilder().accountNum(2L).build()))); + private static final ContractFunctionResult FUNCTION_RESULT = + ContractFunctionResult.newBuilder().amount(666L).build(); + private static final BlockItem EVENT_TRANSACTION = BlockItem.newBuilder() + .eventTransaction(EventTransaction.newBuilder() + .applicationTransaction(Bytes.wrap("MOCK")) + .build()) + .build(); + private static final BlockItem TRANSACTION_RESULT = BlockItem.newBuilder() + .transactionResult( + TransactionResult.newBuilder().transactionFeeCharged(123L).build()) + .build(); + private static final BlockItem FIRST_OUTPUT = BlockItem.newBuilder() + .transactionOutput(TransactionOutput.newBuilder() + .cryptoTransfer(new CryptoTransferOutput(ASSESSED_CUSTOM_FEES)) + .build()) + .build(); + private static final BlockItem SECOND_OUTPUT = BlockItem.newBuilder() + .transactionOutput(TransactionOutput.newBuilder() + .contractCall(new CallContractOutput(List.of(), FUNCTION_RESULT)) + .build()) + .build(); + private static final BlockItem STATE_CHANGES = BlockItem.newBuilder() + .stateChanges(new StateChanges(CONSENSUS_TIME, List.of())) + .build(); + private static final List ITEMS_NO_OUTPUTS = + List.of(EVENT_TRANSACTION, TRANSACTION_RESULT, STATE_CHANGES); + private static final List ITEMS_WITH_OUTPUTS = + List.of(EVENT_TRANSACTION, TRANSACTION_RESULT, FIRST_OUTPUT, SECOND_OUTPUT, STATE_CHANGES); + + @Mock + private Consumer action; + + @Mock + private TranslationContext translationContext; + + @Mock + private BlockItemsTranslator translator; + + @Test + void traversesItemsAsExpected() { + final var subject = new BlockStreamBuilder.Output(ITEMS_WITH_OUTPUTS, translationContext); + + subject.forEachItem(action); + + ITEMS_WITH_OUTPUTS.forEach(item -> verify(action).accept(item)); + } + + @Test + void translatesNoOutputsToRecordAsExpected() { + given(translator.translateRecord(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(TransactionRecord.DEFAULT); + + final var subject = new BlockStreamBuilder.Output(ITEMS_NO_OUTPUTS, translationContext); + + assertSame(TransactionRecord.DEFAULT, subject.toRecord(translator)); + } + + @Test + void translatesOutputsToRecordAsExpected() { + given(translator.translateRecord( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow(), + SECOND_OUTPUT.transactionOutputOrThrow())) + .willReturn(TransactionRecord.DEFAULT); + + final var subject = new BlockStreamBuilder.Output(ITEMS_WITH_OUTPUTS, translationContext); + + assertSame(TransactionRecord.DEFAULT, subject.toRecord(translator)); + } + + @Test + void translatesNoOutputsToReceiptAsExpected() { + given(translationContext.txnId()).willReturn(TXN_ID); + given(translator.translateReceipt(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(TransactionReceipt.DEFAULT); + + final var subject = new BlockStreamBuilder.Output(ITEMS_NO_OUTPUTS, translationContext); + + assertEquals( + new RecordSource.IdentifiedReceipt(TXN_ID, TransactionReceipt.DEFAULT), + subject.toIdentifiedReceipt(translator)); + } + + @Test + void translatesOutputsToReceiptAsExpected() { + given(translationContext.txnId()).willReturn(TXN_ID); + given(translator.translateReceipt( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow(), + SECOND_OUTPUT.transactionOutputOrThrow())) + .willReturn(TransactionReceipt.DEFAULT); + + final var subject = new BlockStreamBuilder.Output(ITEMS_WITH_OUTPUTS, translationContext); + + assertEquals( + new RecordSource.IdentifiedReceipt(TXN_ID, TransactionReceipt.DEFAULT), + subject.toIdentifiedReceipt(translator)); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java index 61bef3c1c404..4eca698e8f9e 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BlockStreamManagerImplTest.java @@ -17,18 +17,23 @@ package com.hedera.node.app.blocks.impl; import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.hedera.node.app.blocks.BlockStreamManager.PendingWork.NONE; +import static com.hedera.node.app.blocks.BlockStreamManager.PendingWork.POST_UPGRADE_WORK; import static com.hedera.node.app.blocks.BlockStreamManager.ZERO_BLOCK_HASH; import static com.hedera.node.app.blocks.BlockStreamService.FAKE_RESTART_BLOCK_HASH; import static com.hedera.node.app.blocks.impl.BlockImplUtils.appendHash; import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; -import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.blocks.impl.BlockStreamManagerImpl.classifyPendingWork; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -50,6 +55,7 @@ import com.hedera.hapi.platform.event.EventTransaction; import com.hedera.hapi.platform.state.PlatformState; import com.hedera.node.app.blocks.BlockItemWriter; +import com.hedera.node.app.blocks.BlockStreamManager; import com.hedera.node.app.blocks.BlockStreamService; import com.hedera.node.app.blocks.InitialStateHash; import com.hedera.node.app.tss.TssBaseService; @@ -89,6 +95,7 @@ class BlockStreamManagerImplTest { private static final long N_MINUS_1_BLOCK_NO = 665L; private static final long N_BLOCK_NO = 666L; private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L); + private static final Timestamp CONSENSUS_THEN = new Timestamp(890, 0); private static final Hash FAKE_START_OF_BLOCK_STATE_HASH = new Hash(new byte[48]); private static final Bytes N_MINUS_2_BLOCK_HASH = Bytes.wrap(noThrowSha384HashOf(new byte[] {(byte) 0xAA})); private static final Bytes FIRST_FAKE_SIGNATURE = Bytes.fromHex("ff".repeat(48)); @@ -147,6 +154,67 @@ void setUp() { writableStates = mock(WritableStates.class, withSettings().extraInterfaces(CommittableWritableStates.class)); } + @Test + void classifiesPendingGenesisWorkByIntervalTime() { + assertSame( + BlockStreamManager.PendingWork.GENESIS_WORK, + classifyPendingWork(BlockStreamInfo.DEFAULT, SemanticVersion.DEFAULT)); + } + + @Test + void classifiesPriorVersionHasPostUpgradeWorkWithDifferentVersionButIntervalTime() { + assertSame( + POST_UPGRADE_WORK, + classifyPendingWork( + BlockStreamInfo.newBuilder() + .creationSoftwareVersion( + SemanticVersion.newBuilder().major(1).build()) + .lastIntervalProcessTime(new Timestamp(1234567, 890)) + .build(), + CREATION_VERSION)); + } + + @Test + void classifiesNonGenesisBlockOfSameVersionWithWorkNotDoneStillHasPostUpgradeWork() { + assertEquals( + POST_UPGRADE_WORK, + classifyPendingWork( + BlockStreamInfo.newBuilder() + .creationSoftwareVersion(CREATION_VERSION) + .lastIntervalProcessTime(new Timestamp(1234567, 890)) + .build(), + CREATION_VERSION)); + } + + @Test + void classifiesNonGenesisBlockOfSameVersionWithWorkDoneAsNoWork() { + assertSame( + NONE, + classifyPendingWork( + BlockStreamInfo.newBuilder() + .postUpgradeWorkDone(true) + .creationSoftwareVersion(CREATION_VERSION) + .lastIntervalProcessTime(new Timestamp(1234567, 890)) + .build(), + CREATION_VERSION)); + } + + @Test + void canUpdateIntervalProcessTime() { + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1L)); + subject = new BlockStreamManagerImpl( + () -> aWriter, + ForkJoinPool.commonPool(), + configProvider, + tssBaseService, + boundaryStateChangeListener, + hashInfo, + SemanticVersion.DEFAULT); + assertSame(Instant.EPOCH, subject.lastIntervalProcessTime()); + subject.setLastIntervalProcessTime(CONSENSUS_NOW); + assertEquals(CONSENSUS_NOW, subject.lastIntervalProcessTime()); + } + @Test void requiresLastHashToBeInitialized() { given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1)); @@ -156,7 +224,8 @@ void requiresLastHashToBeInitialized() { configProvider, tssBaseService, boundaryStateChangeListener, - hashInfo); + hashInfo, + SemanticVersion.DEFAULT); assertThrows(IllegalStateException.class, () -> subject.startRound(round, state)); } @@ -164,7 +233,8 @@ void requiresLastHashToBeInitialized() { void startsAndEndsBlockWithSingleRoundPerBlockAsExpected() throws ParseException { givenSubjectWith( 1, - blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), + blockStreamInfoWith( + Bytes.EMPTY, CREATION_VERSION.copyBuilder().patch(0).build()), platformStateWith(null), aWriter); givenEndOfRoundSetup(); @@ -177,6 +247,11 @@ void startsAndEndsBlockWithSingleRoundPerBlockAsExpected() throws ParseException // Start the round that will be block N subject.startRound(round, state); + assertSame(POST_UPGRADE_WORK, subject.pendingWork()); + subject.confirmPendingWorkFinished(); + assertSame(NONE, subject.pendingWork()); + // We don't fail hard on duplicate calls to confirm post-upgrade work + assertDoesNotThrow(() -> subject.confirmPendingWorkFinished()); // Assert the internal state of the subject has changed as expected and the writer has been opened verify(boundaryStateChangeListener).setBoundaryTimestamp(CONSENSUS_NOW); @@ -210,7 +285,10 @@ void startsAndEndsBlockWithSingleRoundPerBlockAsExpected() throws ParseException "be03f18885e3fb5e26dae1ad95d6559b62092d2162342f376712fd00fa045aaedda06811a1548a916a26878752900473"), Bytes.fromHex( "84910d7e7710b482680de1e81865de39396de9c536ab265cf3253bf378bc50ed2f6c5a3ec19a25c51ee170347f13b28d")), - Timestamp.DEFAULT); + Timestamp.DEFAULT, + true, + SemanticVersion.DEFAULT, + CONSENSUS_THEN); final var actualBlockInfo = infoRef.get(); assertEquals(expectedBlockInfo, actualBlockInfo); verify(tssBaseService).requestLedgerSignature(blockHashCaptor.capture()); @@ -230,11 +308,7 @@ void startsAndEndsBlockWithSingleRoundPerBlockAsExpected() throws ParseException @Test void doesNotEndBlockWithMultipleRoundPerBlockIfNotModZero() { - givenSubjectWith( - 7, - blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), - platformStateWith(null), - aWriter); + givenSubjectWith(7, blockStreamInfoWith(Bytes.EMPTY, CREATION_VERSION), platformStateWith(null), aWriter); // Initialize the last (N-1) block hash subject.initLastBlockHash(FAKE_RESTART_BLOCK_HASH); @@ -266,7 +340,7 @@ void alwaysEndsBlockOnFreezeRoundPerBlockAsExpected() throws ParseException { final var resultHashes = Bytes.fromHex("aa".repeat(48) + "bb".repeat(48) + "cc".repeat(48) + "dd".repeat(48)); givenSubjectWith( 7, - blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, resultHashes), + blockStreamInfoWith(resultHashes, CREATION_VERSION), platformStateWith(CONSENSUS_NOW.minusSeconds(1)), aWriter); givenEndOfRoundSetup(); @@ -315,7 +389,10 @@ void alwaysEndsBlockOnFreezeRoundPerBlockAsExpected() throws ParseException { "be03f18885e3fb5e26dae1ad95d6559b62092d2162342f376712fd00fa045aaedda06811a1548a916a26878752900473"), Bytes.fromHex( "84910d7e7710b482680de1e81865de39396de9c536ab265cf3253bf378bc50ed2f6c5a3ec19a25c51ee170347f13b28d")), - Timestamp.DEFAULT); + Timestamp.DEFAULT, + false, + SemanticVersion.DEFAULT, + CONSENSUS_THEN); final var actualBlockInfo = infoRef.get(); assertEquals(expectedBlockInfo, actualBlockInfo); verify(tssBaseService).requestLedgerSignature(blockHashCaptor.capture()); @@ -336,18 +413,14 @@ void alwaysEndsBlockOnFreezeRoundPerBlockAsExpected() throws ParseException { @Test void supportsMultiplePendingBlocksWithIndirectProofAsExpected() throws ParseException { givenSubjectWith( - 1, - blockStreamInfoWith(N_MINUS_1_BLOCK_NO, N_MINUS_2_BLOCK_HASH, Bytes.EMPTY), - platformStateWith(null), - aWriter, - bWriter); + 1, blockStreamInfoWith(Bytes.EMPTY, CREATION_VERSION), platformStateWith(null), aWriter, bWriter); givenEndOfRoundSetup(); doAnswer(invocationOnMock -> { lastBItem.set(invocationOnMock.getArgument(0)); return bWriter; }) .when(bWriter) - .writeItem(any()); + .writePbjItem(any()); final ArgumentCaptor blockHashCaptor = ArgumentCaptor.forClass(byte[].class); given(round.getRoundNum()).willReturn(ROUND_NO); @@ -424,7 +497,8 @@ private void givenSubjectWith( configProvider, tssBaseService, boundaryStateChangeListener, - hashInfo); + hashInfo, + SemanticVersion.DEFAULT); given(state.getReadableStates(BlockStreamService.NAME)).willReturn(readableStates); given(state.getReadableStates(PlatformStateService.NAME)).willReturn(readableStates); infoRef.set(blockStreamInfo); @@ -443,7 +517,7 @@ private void givenEndOfRoundSetup() { return aWriter; }) .when(aWriter) - .writeItem(any()); + .writePbjItem(any()); given(state.getWritableStates(BlockStreamService.NAME)).willReturn(writableStates); given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) .willReturn(blockStreamInfoState); @@ -456,11 +530,13 @@ private void givenEndOfRoundSetup() { } private BlockStreamInfo blockStreamInfoWith( - final long blockNumber, @NonNull final Bytes nMinus2Hash, @NonNull final Bytes resultHashes) { + @NonNull final Bytes resultHashes, @NonNull final SemanticVersion creationVersion) { return BlockStreamInfo.newBuilder() - .blockNumber(blockNumber) - .trailingBlockHashes(appendHash(nMinus2Hash, Bytes.EMPTY, 256)) + .blockNumber(N_MINUS_1_BLOCK_NO) + .creationSoftwareVersion(creationVersion) + .trailingBlockHashes(appendHash(N_MINUS_2_BLOCK_HASH, Bytes.EMPTY, 256)) .trailingOutputHashes(resultHashes) + .lastIntervalProcessTime(CONSENSUS_THEN) .build(); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListenerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListenerTest.java index f2f52b3a93a0..77d121462675 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListenerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/BoundaryStateChangeListenerTest.java @@ -31,7 +31,7 @@ import com.hedera.hapi.node.state.primitives.ProtoBytes; import com.hedera.hapi.node.state.primitives.ProtoString; import com.hedera.node.app.blocks.BlockStreamService; -import com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema; +import com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema; import com.hedera.pbj.runtime.io.buffer.Bytes; import java.time.Instant; import java.util.List; @@ -59,7 +59,7 @@ void targetTypesAreSingletonAndQueue() { @Test void understandsStateIds() { final var service = BlockStreamService.NAME; - final var stateKey = V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; + final var stateKey = V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; assertEquals(stateIdFor(service, stateKey), listener.stateIdFor(service, stateKey)); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasherTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasherTest.java index 4ff96679b985..2bc6231d583c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasherTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/ConcurrentStreamingTreeHasherTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.blocks.impl; +import static com.hedera.node.app.blocks.StreamingTreeHasher.HASH_LENGTH; import static com.hedera.node.app.blocks.impl.ConcurrentStreamingTreeHasher.rootHashFrom; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,6 +24,7 @@ import com.hedera.node.app.blocks.StreamingTreeHasher.Status; import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.nio.ByteBuffer; import java.util.SplittableRandom; import java.util.concurrent.ForkJoinPool; import org.junit.jupiter.api.Test; @@ -30,7 +32,6 @@ import org.junit.jupiter.params.provider.ValueSource; class ConcurrentStreamingTreeHasherTest { - private static final int LEAF_SIZE = 48; private static final SplittableRandom RANDOM = new SplittableRandom(); private final NaiveStreamingTreeHasher comparison = new NaiveStreamingTreeHasher(); @@ -39,35 +40,36 @@ class ConcurrentStreamingTreeHasherTest { @ParameterizedTest @ValueSource(ints = {0, 1, 3, 5, 32, 69, 100, 123, 234}) void testAddLeafAndRootHash(final int numLeaves) { - Bytes lastLeaf = null; + ByteBuffer lastLeafHash = null; var status = Status.EMPTY; for (int i = 1; i <= numLeaves; i++) { - final var contents = new byte[LEAF_SIZE]; - RANDOM.nextBytes(contents); - final var leaf = Bytes.wrap(contents); - subject.addLeaf(leaf); - comparison.addLeaf(leaf); + final var hash = new byte[HASH_LENGTH]; + RANDOM.nextBytes(hash); + final var leafHash = ByteBuffer.wrap(hash); + subject.addLeaf(ByteBuffer.wrap(hash)); + comparison.addLeaf(ByteBuffer.wrap(hash)); if (i == numLeaves - 1) { status = subject.status(); } else if (i == numLeaves) { - lastLeaf = leaf; + lastLeafHash = leafHash; } } final var actual = subject.rootHash().join(); final var expected = comparison.rootHash().join(); assertEquals(expected, actual); - if (lastLeaf != null) { + if (lastLeafHash != null) { requireNonNull(status); - final var recalculated = rootHashFrom(status, lastLeaf); + final var recalculated = rootHashFrom(status, Bytes.wrap(lastLeafHash.array())); assertEquals(expected, recalculated); } } @Test void testAddLeafAfterRootHashRequested() { - subject.addLeaf(Bytes.wrap(new byte[48])); + final var leaf = ByteBuffer.allocate(48); + subject.addLeaf(leaf); subject.rootHash(); - assertThrows(IllegalStateException.class, () -> subject.addLeaf(Bytes.EMPTY)); + assertThrows(IllegalStateException.class, () -> subject.addLeaf(leaf)); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/FileBlockItemWriterTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/FileBlockItemWriterTest.java index 4712ea99a458..873ffd7d2452 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/FileBlockItemWriterTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/FileBlockItemWriterTest.java @@ -150,7 +150,7 @@ public void testWriteItem() throws IOException { fileBlockItemWriter.openBlock(1); // Create a Bytes object and write it - Bytes bytes = Bytes.wrap(new byte[] {1, 2, 3, 4, 5}); + final var bytes = new byte[] {1, 2, 3, 4, 5}; byte[] expectedBytes = {10, 5, 1, 2, 3, 4, 5}; fileBlockItemWriter.writeItem(bytes); @@ -181,7 +181,7 @@ public void testWriteItemBeforeOpen() { FileBlockItemWriter fileBlockItemWriter = new FileBlockItemWriter(configProvider, selfNodeInfo, fileSystem); // Create a Bytes object and write it - Bytes bytes = Bytes.wrap(new byte[] {1, 2, 3, 4, 5}); + final var bytes = new byte[] {1, 2, 3, 4, 5}; assertThatThrownBy(() -> fileBlockItemWriter.writeItem(bytes), "Cannot write item before opening a block") .isInstanceOf(IllegalStateException.class); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/TranslationContextTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/TranslationContextTest.java new file mode 100644 index 000000000000..db63a1c3ae66 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/TranslationContextTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.blocks.impl; + +import static com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler.noThrowSha384HashOf; +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.node.app.blocks.impl.contexts.BaseOpContext; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import org.junit.jupiter.api.Test; + +class TranslationContextTest { + @Test + void hashIsOfSignedTransactionBytesIfSet() { + final var signedTransactionBytes = SignedTransaction.PROTOBUF.toBytes( + SignedTransaction.newBuilder().bodyBytes(Bytes.fromHex("0123")).build()); + + final var subject = new BaseOpContext( + "", + TransactionID.DEFAULT, + Transaction.newBuilder() + .signedTransactionBytes(signedTransactionBytes) + .build(), + HederaFunctionality.NONE); + + assertEquals(Bytes.wrap(noThrowSha384HashOf(signedTransactionBytes.toByteArray())), subject.transactionHash()); + } + + @Test + void hashIsOfSerializedTransactionIfMissingSignedTransactionBytes() { + final var transactionBytes = Transaction.PROTOBUF.toBytes(Transaction.DEFAULT); + + final var subject = new BaseOpContext("", TransactionID.DEFAULT, Transaction.DEFAULT, HederaFunctionality.NONE); + + assertEquals(Bytes.wrap(noThrowSha384HashOf(transactionBytes.toByteArray())), subject.transactionHash()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java similarity index 90% rename from hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java rename to hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java index 7e88862427b7..6446e8c96cbb 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0540BlockStreamSchemaTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/schemas/V0560BlockStreamSchemaTest.java @@ -16,7 +16,7 @@ package com.hedera.node.app.blocks.schemas; -import static com.hedera.node.app.blocks.schemas.V0540BlockStreamSchema.BLOCK_STREAM_INFO_KEY; +import static com.hedera.node.app.blocks.schemas.V0560BlockStreamSchema.BLOCK_STREAM_INFO_KEY; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -43,7 +43,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class V0540BlockStreamSchemaTest { +public class V0560BlockStreamSchemaTest { @Mock private MigrationContext migrationContext; @@ -56,16 +56,16 @@ public class V0540BlockStreamSchemaTest { @Mock private WritableSingletonState state; - private V0540BlockStreamSchema subject; + private V0560BlockStreamSchema subject; @BeforeEach void setUp() { - subject = new V0540BlockStreamSchema(migratedBlockHashConsumer); + subject = new V0560BlockStreamSchema(migratedBlockHashConsumer); } @Test - void versionIsV0540() { - assertEquals(new SemanticVersion(0, 54, 0, "", ""), subject.getVersion()); + void versionIsV0560() { + assertEquals(new SemanticVersion(0, 56, 0, "", ""), subject.getVersion()); } @Test @@ -83,7 +83,7 @@ void createsDefaultInfoAtGenesis() { given(writableStates.getSingleton(BLOCK_STREAM_INFO_KEY)) .willReturn(state); - subject.migrate(migrationContext); + subject.restart(migrationContext); verify(state).put(BlockStreamInfo.DEFAULT); } @@ -112,7 +112,7 @@ void assumesMigrationIfNotGenesisAndStateIsNull() { .willReturn(state); given(migrationContext.sharedValues()).willReturn(sharedValues); - subject.migrate(migrationContext); + subject.restart(migrationContext); verify(migratedBlockHashConsumer).accept(Bytes.fromHex("abcd".repeat(24))); final var expectedInfo = new BlockStreamInfo( @@ -124,6 +124,9 @@ void assumesMigrationIfNotGenesisAndStateIsNull() { Bytes.EMPTY, 0, List.of(), + blockInfo.consTimeOfLastHandledTxn(), + false, + SemanticVersion.DEFAULT, blockInfo.consTimeOfLastHandledTxn()); verify(state).put(expectedInfo); } @@ -136,7 +139,7 @@ void migrationIsNoopIfNotGenesisAndInfoIsNonNull() { .willReturn(state); given(state.get()).willReturn(BlockStreamInfo.DEFAULT); - subject.migrate(migrationContext); + subject.restart(migrationContext); verifyNoInteractions(migratedBlockHashConsumer); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java index 04a66c12164e..34c2017462ea 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java @@ -17,9 +17,11 @@ package com.hedera.node.app.components; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.swirlds.platform.system.address.AddressBookUtils.endpointFor; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -41,8 +43,10 @@ import com.hedera.node.app.signature.AppSignatureVerifier; import com.hedera.node.app.signature.impl.SignatureExpanderImpl; import com.hedera.node.app.signature.impl.SignatureVerifierImpl; +import com.hedera.node.app.spi.workflows.TransactionHandler; import com.hedera.node.app.state.recordcache.RecordCacheService; import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.tss.handlers.TssHandlers; import com.hedera.node.config.data.HederaConfig; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -74,6 +78,9 @@ class IngestComponentTest { @Mock private TssBaseService tssBaseService; + @Mock + private TransactionHandler transactionHandler; + private HederaInjectionComponent app; @BeforeEach @@ -96,7 +103,9 @@ void setUp() { new AppSignatureVerifier( DEFAULT_CONFIG.getConfigData(HederaConfig.class), new SignatureExpanderImpl(), - new SignatureVerifierImpl(CryptographyHolder.get()))); + new SignatureVerifierImpl(CryptographyHolder.get())), + UNAVAILABLE_GOSSIP); + given(tssBaseService.tssHandlers()).willReturn(new TssHandlers(transactionHandler, transactionHandler)); app = DaggerHederaInjectionComponent.builder() .configProviderImpl(configProvider) .bootstrapConfigProviderImpl(new BootstrapConfigProviderImpl()) diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/GrpcServiceBuilderTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/GrpcServiceBuilderTest.java index 7c860cfe8986..049e6a2352cd 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/GrpcServiceBuilderTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/GrpcServiceBuilderTest.java @@ -48,13 +48,6 @@ void setUp() { builder = new GrpcServiceBuilder(SERVICE_NAME, ingestWorkflow, queryWorkflow); } - @Test - @DisplayName("The ingestWorkflow cannot be null") - void ingestWorkflowIsNull() { - //noinspection ConstantConditions - assertThrows(NullPointerException.class, () -> new GrpcServiceBuilder(SERVICE_NAME, null, queryWorkflow)); - } - @Test @DisplayName("The queryWorkflow cannot be null") void queryWorkflowIsNull() { diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManagerTest.java index ba2b3e592e35..55abad3b0f57 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/grpc/impl/netty/NettyGrpcServerManagerTest.java @@ -42,7 +42,8 @@ final class NettyGrpcServerManagerTest { private ConfigProvider configProvider; private ServicesRegistry services; private IngestWorkflow ingestWorkflow; - private QueryWorkflow queryWorkflow; + private QueryWorkflow userQueryWorkflow; + private QueryWorkflow operatorQueryWorkflow; private Metrics metrics; @BeforeEach @@ -54,32 +55,39 @@ void setUp(@Mock @NonNull final Metrics metrics) { this.services = new ServicesRegistryImpl(ConstructableRegistry.getInstance(), config); // An empty set of services this.ingestWorkflow = (req, res) -> {}; - this.queryWorkflow = (req, res) -> {}; + this.userQueryWorkflow = (req, res) -> {}; + this.operatorQueryWorkflow = (req, res) -> {}; } @Test @DisplayName("Null arguments are not allowed") @SuppressWarnings("DataFlowIssue") void nullArgsThrow() { - assertThatThrownBy(() -> new NettyGrpcServerManager(null, services, ingestWorkflow, queryWorkflow, metrics)) + assertThatThrownBy(() -> new NettyGrpcServerManager( + null, services, ingestWorkflow, userQueryWorkflow, operatorQueryWorkflow, metrics)) .isInstanceOf(NullPointerException.class); - assertThatThrownBy( - () -> new NettyGrpcServerManager(configProvider, null, ingestWorkflow, queryWorkflow, metrics)) + assertThatThrownBy(() -> new NettyGrpcServerManager( + configProvider, null, ingestWorkflow, userQueryWorkflow, operatorQueryWorkflow, metrics)) .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> new NettyGrpcServerManager(configProvider, services, null, queryWorkflow, metrics)) + assertThatThrownBy(() -> new NettyGrpcServerManager( + configProvider, services, null, userQueryWorkflow, operatorQueryWorkflow, metrics)) .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> new NettyGrpcServerManager(configProvider, services, ingestWorkflow, null, metrics)) + assertThatThrownBy(() -> new NettyGrpcServerManager( + configProvider, services, ingestWorkflow, null, operatorQueryWorkflow, metrics)) .isInstanceOf(NullPointerException.class); - assertThatThrownBy( - () -> new NettyGrpcServerManager(configProvider, services, ingestWorkflow, queryWorkflow, null)) + assertThatThrownBy(() -> new NettyGrpcServerManager( + configProvider, services, ingestWorkflow, userQueryWorkflow, null, metrics)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new NettyGrpcServerManager( + configProvider, services, ingestWorkflow, userQueryWorkflow, operatorQueryWorkflow, null)) .isInstanceOf(NullPointerException.class); } @Test @DisplayName("Ports are -1 when not started") void portsAreMinusOneWhenNotStarted() { - final var subject = - new NettyGrpcServerManager(configProvider, services, ingestWorkflow, queryWorkflow, metrics); + final var subject = new NettyGrpcServerManager( + configProvider, services, ingestWorkflow, userQueryWorkflow, operatorQueryWorkflow, metrics); assertThat(subject.port()).isEqualTo(-1); assertThat(subject.tlsPort()).isEqualTo(-1); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/DefaultKeyVerifierTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/DefaultKeyVerifierTest.java index 03be40a5c2e6..68a9f96013e6 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/DefaultKeyVerifierTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/signature/DefaultKeyVerifierTest.java @@ -42,6 +42,7 @@ import com.hedera.hapi.node.base.KeyList; import com.hedera.hapi.node.base.ThresholdKey; import com.hedera.node.app.signature.impl.SignatureVerificationImpl; +import com.hedera.node.app.spi.key.KeyComparator; import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.app.workflows.prehandle.FakeSignatureVerificationFuture; @@ -50,11 +51,13 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -73,6 +76,11 @@ @ExtendWith(MockitoExtension.class) class DefaultKeyVerifierTest { private static final int LEGACY_FEE_CALC_NETWORK_VPT = 13; + private static final Key ECDSA_X1 = FAKE_ECDSA_KEY_INFOS[1].publicKey(); + private static final Key ECDSA_X2 = FAKE_ECDSA_KEY_INFOS[2].publicKey(); + private static final Key ED25519_X1 = FAKE_ED25519_KEY_INFOS[1].publicKey(); + private static final Key ED25519_X2 = FAKE_ED25519_KEY_INFOS[2].publicKey(); + private static final Comparator KEY_COMPARATOR = new KeyComparator(); private static final HederaConfig HEDERA_CONFIG = HederaTestConfigBuilder.createConfig().getConfigData(HederaConfig.class); @@ -199,6 +207,48 @@ static Stream provideCompoundKeys() { } } + @Nested + @DisplayName("Only keys with valid signatures are returned by signingCryptoKeys()") + class SigningCryptoKeysTests { + @ParameterizedTest + @MethodSource("variousValidityScenarios") + void exactlyKeysWithValidKeysAreReturned(@NonNull final Map keysAndPassFail) { + final var subject = new DefaultKeyVerifier( + LEGACY_FEE_CALC_NETWORK_VPT, HEDERA_CONFIG, verificationResults(keysAndPassFail)); + final var expectedKeys = keysAndPassFail.entrySet().stream() + .filter(Entry::getValue) + .map(Entry::getKey) + .collect(Collectors.toCollection(() -> new TreeSet<>(KEY_COMPARATOR))); + final var actualKeys = subject.signingCryptoKeys(); + assertThat(actualKeys).isEqualTo(expectedKeys); + } + + static Stream variousValidityScenarios() { + return Stream.of( + Arguments.of(named( + "ECDSA_X1=pass, ECDSA_X2=pass, ED25519_X1=fail, ED25519_X2=fail", + Map.of( + ECDSA_X1, true, + ECDSA_X2, true, + ED25519_X1, false, + ED25519_X2, false))), + Arguments.of(named( + "ECDSA_X1=fail, ECDSA_X2=pass, ED25519_X1=pass, ED25519_X2=fail", + Map.of( + ECDSA_X1, false, + ECDSA_X2, true, + ED25519_X1, true, + ED25519_X2, false))), + Arguments.of(named( + "ECDSA_X1=fail, ECDSA_X2=fail, ED25519_X1=fail, ED25519_X2=fail", + Map.of( + ECDSA_X1, false, + ECDSA_X2, false, + ED25519_X1, false, + ED25519_X2, false)))); + } + } + /** * Tests to verify that finding a {@link SignatureVerification} for compound keys (threshold keys, key lists) that * also have duplicated keys. The point of these tests is really to verify that duplicate keys are counted multiple @@ -219,14 +269,10 @@ static Stream provideCompoundKeys() { @DisplayName("Finding SignatureVerification With Complex Keys with Duplicates") @ExtendWith(MockitoExtension.class) final class FindingSignatureVerificationWithDuplicateKeysTests { - // Used once in the key list - private static final Key ECDSA_X1 = FAKE_ECDSA_KEY_INFOS[1].publicKey(); - // Used twice in the key list - private static final Key ECDSA_X2 = FAKE_ECDSA_KEY_INFOS[2].publicKey(); - // Used once in the key list - private static final Key ED25519_X1 = FAKE_ED25519_KEY_INFOS[1].publicKey(); - // Used twice in the key list - private static final Key ED25519_X2 = FAKE_ED25519_KEY_INFOS[2].publicKey(); + // ECDSA_X1 is used once in the key list + // ECDSA_X2 is used twice in the key list + // ED25519_X1 is used once in the key list + // ED25519_X2 is used twice in the key list @BeforeEach void setup() { @@ -236,17 +282,6 @@ void setup() { }); } - private Map verificationResults(Map keysAndPassFail) { - final var results = new HashMap(); - for (final var entry : keysAndPassFail.entrySet()) { - results.put( - entry.getKey(), - new FakeSignatureVerificationFuture( - new SignatureVerificationImpl(entry.getKey(), null, entry.getValue()))); - } - return results; - } - @Test @DisplayName("All signatures are valid for the KeyList") void allValidInKeyList() { @@ -1282,4 +1317,15 @@ private static Key thresholdKey(int threshold, Key... keys) { .threshold(threshold)) .build(); } + + private static Map verificationResults(Map keysAndPassFail) { + final var results = new HashMap(); + for (final var entry : keysAndPassFail.entrySet()) { + results.put( + entry.getKey(), + new FakeSignatureVerificationFuture( + new SignatureVerificationImpl(entry.getKey(), null, entry.getValue()))); + } + return results; + } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/MerkleStateLifecyclesImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/MerkleStateLifecyclesImplTest.java index d5228384271d..fb85d3591342 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/MerkleStateLifecyclesImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/MerkleStateLifecyclesImplTest.java @@ -114,8 +114,8 @@ void delegatesOnStateInitialized() { @Test void updatesAddressBookWithZeroWeightOnGenesisStart() { - final var node0 = new NodeId(0); - final var node1 = new NodeId(1); + final var node0 = NodeId.of(0); + final var node1 = NodeId.of(1); given(platform.getSelfId()).willReturn(node0); final var pretendAddressBook = createPretendBookFrom(platform, true); @@ -133,8 +133,8 @@ void updatesAddressBookWithZeroWeightOnGenesisStart() { @Test void updatesAddressBookWithZeroWeightForNewNodes() { - final var node0 = new NodeId(0); - final var node1 = new NodeId(1); + final var node0 = NodeId.of(0); + final var node1 = NodeId.of(1); given(platform.getSelfId()).willReturn(node0); final var pretendAddressBook = createPretendBookFrom(platform, true); given(merkleStateRoot.findNodeIndex(TokenService.NAME, STAKING_INFO_KEY)) @@ -163,9 +163,9 @@ void updatesAddressBookWithZeroWeightForNewNodes() { @Test void doesntUpdateAddressBookIfNodeIdFromStateDoesntExist() { - final var node0 = new NodeId(0); - final var node1 = new NodeId(1); - final var node2 = new NodeId(2); + final var node0 = NodeId.of(0); + final var node1 = NodeId.of(1); + final var node2 = NodeId.of(2); given(platform.getSelfId()).willReturn(node0); final var pretendAddressBook = createPretendBookFrom(platform, true); @@ -214,8 +214,8 @@ void doesntUpdateAddressBookIfNodeIdFromStateDoesntExist() { @Test void updatesAddressBookWithNonZeroWeightsOnGenesisStartIfStakesExist() { - final var node0 = new NodeId(0); - final var node1 = new NodeId(1); + final var node0 = NodeId.of(0); + final var node1 = NodeId.of(1); given(platform.getSelfId()).willReturn(node0); final var pretendAddressBook = createPretendBookFrom(platform, true); @@ -278,8 +278,8 @@ void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() { .when(weightUpdateVisitor) .accept(any(), any()); - final var node0 = new NodeId(0); - final var node1 = new NodeId(1); + final var node0 = NodeId.of(0); + final var node1 = NodeId.of(1); given(platform.getSelfId()).willReturn(node0); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java index 7d99d2473d59..b5d7fce62ef5 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/merkle/SerializationTest.java @@ -17,24 +17,40 @@ package com.hedera.node.app.state.merkle; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.swirlds.platform.state.snapshot.SignedStateFileReader.readStateFileData; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.hedera.node.app.ids.WritableEntityIdStore; import com.hedera.node.app.services.MigrationStateChanges; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.config.data.HederaConfig; +import com.swirlds.base.test.fixtures.time.FakeTime; +import com.swirlds.common.config.StateCommonConfig_; import com.swirlds.common.constructable.ClassConstructorPair; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.constructable.RuntimeConstructable; +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.crypto.CryptographyFactory; +import com.swirlds.common.crypto.config.CryptoConfig; import com.swirlds.common.io.utility.LegacyTemporaryFileBuilder; +import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; +import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.sources.SimpleConfigSource; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; import com.swirlds.metrics.api.Metrics; +import com.swirlds.platform.config.StateConfig_; import com.swirlds.platform.state.MerkleStateLifecycles; import com.swirlds.platform.state.MerkleStateRoot; +import com.swirlds.platform.state.signed.SignedState; +import com.swirlds.platform.state.snapshot.SignedStateFileReader; +import com.swirlds.platform.state.snapshot.SignedStateFileUtils; +import com.swirlds.platform.system.InitTrigger; +import com.swirlds.platform.system.Platform; import com.swirlds.platform.test.fixtures.state.MerkleTestBase; +import com.swirlds.platform.test.fixtures.state.RandomSignedStateGenerator; import com.swirlds.platform.test.fixtures.state.TestSchema; import com.swirlds.state.merkle.disk.OnDiskReadableKVState; import com.swirlds.state.merkle.disk.OnDiskWritableKVState; @@ -62,6 +78,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; @@ -79,6 +96,9 @@ class SerializationTest extends MerkleTestBase { @Mock private MigrationStateChanges migrationStateChanges; + @TempDir + Path tempDir; + @BeforeEach void setUp() throws IOException { setupConstructableRegistry(); @@ -90,6 +110,7 @@ void setUp() throws IOException { .withValue(VirtualMapConfig_.COPY_FLUSH_THRESHOLD, 1 + "")) .withConfigDataType(VirtualMapConfig.class) .withConfigDataType(HederaConfig.class) + .withConfigDataType(CryptoConfig.class) .getOrCreateConfig(); this.networkInfo = mock(NetworkInfo.class); } @@ -176,7 +197,7 @@ private void forceFlush(ReadableKVState state) { @ValueSource(booleans = {true, false}) void simpleReadAndWrite(boolean forceFlush) throws IOException, ConstructableRegistryException { final var schemaV1 = createV1Schema(); - final var originalTree = createMerkleHederaState(schemaV1); + final var originalTree = (MerkleStateRoot) createMerkleHederaState(schemaV1); // When we serialize it to bytes and deserialize it back into a tree MerkleStateRoot copy = originalTree.copy(); // make a copy to make VM flushable @@ -192,11 +213,45 @@ void simpleReadAndWrite(boolean forceFlush) throws IOException, ConstructableReg serializedBytes = writeTree(originalTree, dir); } - final MerkleStateRoot loadedTree = loadeMerkleTree(schemaV1, serializedBytes); + final MerkleStateRoot loadedTree = loadedMerkleTree(schemaV1, serializedBytes); assertTree(loadedTree); } + @Test + void snapshot() throws IOException { + final var schemaV1 = createV1Schema(); + final var originalTree = createMerkleHederaState(schemaV1); + final var configBuilder = new TestConfigBuilder() + .withValue(StateConfig_.SIGNED_STATE_DISK, 1) + .withValue( + StateCommonConfig_.SAVED_STATE_DIRECTORY, + tempDir.toFile().toString()); + final var cryptography = CryptographyFactory.create(); + final var merkleCryptography = MerkleCryptographyFactory.create(config, cryptography); + final PlatformContext context = TestPlatformContextBuilder.create() + .withMerkleCryptography(merkleCryptography) + .withConfiguration(configBuilder.getOrCreateConfig()) + .withTime(new FakeTime()) + .build(); + + Platform mockPlatform = mock(Platform.class); + when(mockPlatform.getContext()).thenReturn(context); + originalTree.init(mockPlatform, InitTrigger.RESTART, new ServicesSoftwareVersion(schemaV1.getVersion())); + + // prepare the tree and create a snapshot + originalTree.copy(); + originalTree.computeHash(); + originalTree.createSnapshot(tempDir); + + final SignedStateFileReader.StateFileData deserializedSignedState = readStateFileData( + tempDir.resolve(SignedStateFileUtils.SIGNED_STATE_FILE_NAME), SignedStateFileUtils::readState); + + MerkleStateRoot state = (MerkleStateRoot) deserializedSignedState.state(); + initServices(schemaV1, state); + assertTree(state); + } + /** * This test scenario is trickier, and it's designed to reproduce #13335: OnDiskKeySerializer uses wrong classId for OnDiskKey. * This issue can be reproduced only if at first it gets flushed to disk, then it gets loaded back in, and this time it remains in cache. @@ -205,7 +260,7 @@ void simpleReadAndWrite(boolean forceFlush) throws IOException, ConstructableReg @Test void dualReadAndWrite() throws IOException, ConstructableRegistryException { final var schemaV1 = createV1Schema(); - final var originalTree = createMerkleHederaState(schemaV1); + final var originalTree = (MerkleStateRoot) createMerkleHederaState(schemaV1); MerkleStateRoot copy = originalTree.copy(); // make a copy to make VM flushable @@ -214,7 +269,7 @@ void dualReadAndWrite() throws IOException, ConstructableRegistryException { CRYPTO.digestTreeSync(copy); final byte[] serializedBytes = writeTree(copy, dir); - MerkleStateRoot loadedTree = loadeMerkleTree(schemaV1, serializedBytes); + MerkleStateRoot loadedTree = loadedMerkleTree(schemaV1, serializedBytes); ((OnDiskReadableKVState) originalTree.getReadableStates(FIRST_SERVICE).get(ANIMAL_STATE_KEY)).reset(); populateVmCache(loadedTree); @@ -226,7 +281,7 @@ void dualReadAndWrite() throws IOException, ConstructableRegistryException { final byte[] serializedBytesWithCache = writeTree(loadedTree, dir); // let's load it again and see if it works - MerkleStateRoot loadedTreeWithCache = loadeMerkleTree(schemaV1, serializedBytesWithCache); + MerkleStateRoot loadedTreeWithCache = loadedMerkleTree(schemaV1, serializedBytesWithCache); ((OnDiskReadableKVState) loadedTreeWithCache.getReadableStates(FIRST_SERVICE).get(ANIMAL_STATE_KEY)) .reset(); @@ -234,11 +289,8 @@ void dualReadAndWrite() throws IOException, ConstructableRegistryException { assertTree(loadedTreeWithCache); } - private MerkleStateRoot loadeMerkleTree(Schema schemaV1, byte[] serializedBytes) + private MerkleStateRoot loadedMerkleTree(Schema schemaV1, byte[] serializedBytes) throws ConstructableRegistryException, IOException { - final var newRegistry = - new MerkleSchemaRegistry(registry, FIRST_SERVICE, DEFAULT_CONFIG, new SchemaApplications()); - newRegistry.register(schemaV1); // Register the MerkleStateRoot so, when found in serialized bytes, it will register with // our migration callback, etc. (normally done by the Hedera main method) @@ -248,6 +300,15 @@ private MerkleStateRoot loadeMerkleTree(Schema schemaV1, byte[] serializedBytes) registry.registerConstructable(pair); final MerkleStateRoot loadedTree = parseTree(serializedBytes, dir); + initServices(schemaV1, loadedTree); + + return loadedTree; + } + + private void initServices(Schema schemaV1, MerkleStateRoot loadedTree) { + final var newRegistry = + new MerkleSchemaRegistry(registry, FIRST_SERVICE, DEFAULT_CONFIG, new SchemaApplications()); + newRegistry.register(schemaV1); newRegistry.migrate( loadedTree, schemaV1.getVersion(), @@ -259,12 +320,13 @@ private MerkleStateRoot loadeMerkleTree(Schema schemaV1, byte[] serializedBytes) new HashMap<>(), migrationStateChanges); loadedTree.migrate(MerkleStateRoot.CURRENT_VERSION); - - return loadedTree; } private MerkleStateRoot createMerkleHederaState(Schema schemaV1) { - final var originalTree = new MerkleStateRoot(lifecycles, version -> new ServicesSoftwareVersion(version, 0)); + final SignedState randomState = + new RandomSignedStateGenerator().setRound(1).build(); + + final var originalTree = (MerkleStateRoot) randomState.getState(); final var originalRegistry = new MerkleSchemaRegistry(registry, FIRST_SERVICE, DEFAULT_CONFIG, new SchemaApplications()); originalRegistry.register(schemaV1); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/BlockRecordSourceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/BlockRecordSourceTest.java new file mode 100644 index 000000000000..e070dcfb11e3 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/BlockRecordSourceTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.state.recordcache; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.hapi.block.stream.output.CryptoTransferOutput; +import com.hedera.hapi.block.stream.output.TransactionOutput; +import com.hedera.hapi.block.stream.output.TransactionResult; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.blocks.BlockItemsTranslator; +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; +import com.hedera.node.app.blocks.impl.TranslationContext; +import com.hedera.node.app.spi.records.RecordSource; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockRecordSourceTest { + private static final TransactionRecord FIRST_RECORD = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(SCHEDULE_ALREADY_DELETED) + .build()) + .transactionID(TransactionID.newBuilder().nonce(1).build()) + .memo("FIRST") + .build(); + private static final TransactionRecord SECOND_RECORD = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder().status(SUCCESS).build()) + .transactionID(TransactionID.newBuilder().nonce(2).build()) + .memo("SECOND") + .build(); + private static final BlockItem FIRST_OUTPUT = BlockItem.newBuilder() + .transactionOutput(TransactionOutput.newBuilder() + .cryptoTransfer(new CryptoTransferOutput(List.of())) + .build()) + .build(); + private static final BlockItem TRANSACTION_RESULT = BlockItem.newBuilder() + .transactionResult( + TransactionResult.newBuilder().transactionFeeCharged(123L).build()) + .build(); + + @Mock + private BlockItemsTranslator recordTranslator; + + @Mock + private Consumer itemAction; + + @Mock + private Consumer recordAction; + + @Mock + private TranslationContext translationContext; + + private BlockRecordSource subject; + + @Test + void actionSeesAllItems() { + subjectWith(List.of( + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT, FIRST_OUTPUT), translationContext), + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT), translationContext))); + + subject.forEachItem(itemAction); + + verify(itemAction, times(3)).accept(any(BlockItem.class)); + } + + @Test + void hasDefaultBlockItemTranslator() { + assertDoesNotThrow(() -> new BlockRecordSource(List.of())); + } + + @Test + void forEachTxnRecordIncludesRecordsFromAllOutputs() { + given(recordTranslator.translateRecord( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow())) + .willReturn(FIRST_RECORD); + given(recordTranslator.translateRecord(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(SECOND_RECORD); + subjectWith(List.of( + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT, FIRST_OUTPUT), translationContext), + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT), translationContext))); + + subject.forEachTxnRecord(recordAction); + + verify(recordAction).accept(FIRST_RECORD); + verify(recordAction).accept(SECOND_RECORD); + + assertDoesNotThrow(() -> subject.forEachTxnRecord(recordAction)); + } + + @Test + void identifiedReceiptsIncludeReceiptsFromAllOutputs() { + given(translationContext.txnId()) + .willReturn(FIRST_RECORD.transactionIDOrThrow()) + .willReturn(SECOND_RECORD.transactionIDOrThrow()); + given(recordTranslator.translateReceipt( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow())) + .willReturn(FIRST_RECORD.receiptOrThrow()); + given(recordTranslator.translateReceipt(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(SECOND_RECORD.receiptOrThrow()); + subjectWith(List.of( + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT, FIRST_OUTPUT), translationContext), + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT), translationContext))); + + assertThat(subject.identifiedReceipts()) + .containsExactly( + new RecordSource.IdentifiedReceipt( + FIRST_RECORD.transactionIDOrThrow(), FIRST_RECORD.receiptOrThrow()), + new RecordSource.IdentifiedReceipt( + SECOND_RECORD.transactionIDOrThrow(), SECOND_RECORD.receiptOrThrow())); + + assertDoesNotThrow(() -> subject.identifiedReceipts()); + } + + @Test + void findsReceiptForPresentTxnIdAndThrowsOtherwise() { + given(translationContext.txnId()) + .willReturn(FIRST_RECORD.transactionIDOrThrow()) + .willReturn(SECOND_RECORD.transactionIDOrThrow()); + given(recordTranslator.translateReceipt( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow())) + .willReturn(FIRST_RECORD.receiptOrThrow()); + given(recordTranslator.translateReceipt(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(SECOND_RECORD.receiptOrThrow()); + subjectWith(List.of( + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT, FIRST_OUTPUT), translationContext), + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT), translationContext))); + + assertThat(subject.receiptOf(FIRST_RECORD.transactionIDOrThrow())).isEqualTo(FIRST_RECORD.receiptOrThrow()); + assertThat(subject.receiptOf(SECOND_RECORD.transactionIDOrThrow())).isEqualTo(SECOND_RECORD.receiptOrThrow()); + assertThrows( + IllegalArgumentException.class, + () -> subject.receiptOf(TransactionID.newBuilder().nonce(3).build())); + } + + @Test + void findsChildReceiptsForTxnIdAndEmptyListOtherwise() { + given(translationContext.txnId()) + .willReturn(FIRST_RECORD.transactionIDOrThrow()) + .willReturn(SECOND_RECORD.transactionIDOrThrow()); + given(recordTranslator.translateReceipt( + translationContext, + TRANSACTION_RESULT.transactionResultOrThrow(), + FIRST_OUTPUT.transactionOutputOrThrow())) + .willReturn(FIRST_RECORD.receiptOrThrow()); + given(recordTranslator.translateReceipt(translationContext, TRANSACTION_RESULT.transactionResultOrThrow())) + .willReturn(SECOND_RECORD.receiptOrThrow()); + subjectWith(List.of( + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT, FIRST_OUTPUT), translationContext), + new BlockStreamBuilder.Output(List.of(TRANSACTION_RESULT), translationContext))); + + assertThat(subject.childReceiptsOf(TransactionID.DEFAULT)) + .containsExactly(FIRST_RECORD.receiptOrThrow(), SECOND_RECORD.receiptOrThrow()); + assertThat(subject.childReceiptsOf(TransactionID.newBuilder() + .accountID(AccountID.newBuilder().accountNum(2L).build()) + .build())) + .isEmpty(); + } + + private void subjectWith(@NonNull final List outputs) { + subject = new BlockRecordSource(recordTranslator, outputs); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/LegacyListRecordSourceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/LegacyListRecordSourceTest.java new file mode 100644 index 000000000000..0dfc8967e319 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/LegacyListRecordSourceTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.state.recordcache; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionReceipt; +import com.hedera.hapi.node.transaction.TransactionRecord; +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.node.app.state.SingleTransactionRecord; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LegacyListRecordSourceTest { + private static final TransactionRecord FIRST_RECORD = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder() + .status(SCHEDULE_ALREADY_DELETED) + .build()) + .transactionID(TransactionID.newBuilder().nonce(1).build()) + .memo("FIRST") + .build(); + private static final TransactionRecord SECOND_RECORD = TransactionRecord.newBuilder() + .receipt(TransactionReceipt.newBuilder().status(SUCCESS).build()) + .transactionID(TransactionID.newBuilder().nonce(2).build()) + .memo("SECOND") + .build(); + private static final SingleTransactionRecord FIRST_ITEM = new SingleTransactionRecord( + Transaction.DEFAULT, FIRST_RECORD, List.of(), new SingleTransactionRecord.TransactionOutputs(null)); + private static final SingleTransactionRecord SECOND_ITEM = new SingleTransactionRecord( + Transaction.DEFAULT, SECOND_RECORD, List.of(), new SingleTransactionRecord.TransactionOutputs(null)); + private static final List ITEMS = List.of(FIRST_ITEM, SECOND_ITEM); + private static final List RECEIPTS = List.of( + new RecordSource.IdentifiedReceipt(FIRST_RECORD.transactionIDOrThrow(), FIRST_RECORD.receiptOrThrow()), + new RecordSource.IdentifiedReceipt(SECOND_RECORD.transactionIDOrThrow(), SECOND_RECORD.receiptOrThrow())); + + private final LegacyListRecordSource subject = new LegacyListRecordSource(ITEMS, RECEIPTS); + + @Mock + private Consumer recordAction; + + @Test + void consumerGetsAllRecords() { + subject.forEachTxnRecord(recordAction); + + verify(recordAction).accept(FIRST_RECORD); + verify(recordAction).accept(SECOND_RECORD); + } + + @Test + void getsPresentReceiptsAndThrowsOtherwise() { + assertEquals(FIRST_RECORD.receiptOrThrow(), subject.receiptOf(FIRST_RECORD.transactionIDOrThrow())); + assertEquals(SECOND_RECORD.receiptOrThrow(), subject.receiptOf(SECOND_RECORD.transactionIDOrThrow())); + assertThrows( + IllegalArgumentException.class, + () -> subject.receiptOf(TransactionID.newBuilder().nonce(3).build())); + } + + @Test + void getsChildReceipts() { + assertEquals( + List.of(FIRST_RECORD.receiptOrThrow(), SECOND_RECORD.receiptOrThrow()), + subject.childReceiptsOf(TransactionID.DEFAULT)); + assertEquals( + List.of(), + subject.childReceiptsOf(TransactionID.newBuilder().nonce(3).build())); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/RecordCacheImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/RecordCacheImplTest.java index 09e1d7e2e2f6..a237dce6a8e4 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/RecordCacheImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/state/recordcache/RecordCacheImplTest.java @@ -28,6 +28,7 @@ import static com.hedera.node.app.state.recordcache.schemas.V0540RecordCacheSchema.TXN_RECEIPT_QUEUE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.lenient; import com.hedera.hapi.node.base.AccountID; @@ -44,7 +45,7 @@ import com.hedera.node.app.fixtures.state.FakeState; import com.hedera.node.app.spi.records.RecordCache; import com.hedera.node.app.state.DeduplicationCache; -import com.hedera.node.app.state.SingleTransactionRecord; +import com.hedera.node.app.state.HederaRecordCache.DueDiligenceFailure; import com.hedera.node.app.state.SingleTransactionRecord.TransactionOutputs; import com.hedera.node.app.state.WorkingStateAccessor; import com.hedera.node.config.ConfigProvider; @@ -53,6 +54,7 @@ import com.hedera.node.config.data.LedgerConfig; import com.swirlds.state.spi.WritableQueueState; import com.swirlds.state.spi.info.NetworkInfo; +import com.swirlds.state.spi.info.NodeInfo; import com.swirlds.state.test.fixtures.ListWritableQueueState; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Instant; @@ -81,6 +83,8 @@ final class RecordCacheImplTest extends AppTestBase { private static final int RESPONSE_CODES_TO_TEST = 32; private static final TransactionReceipt UNHANDLED_RECEIPT = TransactionReceipt.newBuilder().status(UNKNOWN).build(); + private static final AccountID NODE_ACCOUNT_ID = + AccountID.newBuilder().accountNum(3).build(); private static final AccountID PAYER_ACCOUNT_ID = AccountID.newBuilder().accountNum(1001).build(); private static final TransactionOutputs SIMPLE_OUTPUT = new TransactionOutputs(TokenType.FUNGIBLE_COMMON); @@ -95,6 +99,12 @@ final class RecordCacheImplTest extends AppTestBase { @Mock private ConfigProvider props; + @Mock + private NetworkInfo networkInfo; + + @Mock + private NodeInfo nodeInfo; + @BeforeEach void setUp( @Mock final VersionedConfiguration versionedConfig, @@ -132,10 +142,12 @@ private TransactionID transactionID(int nanos) { @DisplayName("Null args to constructor throw NPE") @SuppressWarnings("DataFlowIssue") void nullArgsToConstructorThrowNPE() { - assertThatThrownBy(() -> new RecordCacheImpl(null, wsa, props)).isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> new RecordCacheImpl(dedupeCache, null, props)) + assertThatThrownBy(() -> new RecordCacheImpl(null, wsa, props, networkInfo)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new RecordCacheImpl(dedupeCache, null, props, networkInfo)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new RecordCacheImpl(dedupeCache, wsa, null, networkInfo)) .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> new RecordCacheImpl(dedupeCache, wsa, null)).isInstanceOf(NullPointerException.class); } private TransactionRecord getRecord(RecordCache cache, TransactionID txId) { @@ -145,7 +157,7 @@ private TransactionRecord getRecord(RecordCache cache, TransactionID txId) { private TransactionReceipt getReceipt(RecordCache cache, TransactionID txId) { final var history = cache.getHistory(txId); - return history == null ? null : history.userTransactionReceipt(); + return history == null ? null : history.priorityReceipt(); } private List getRecords(RecordCache cache, TransactionID txId) { @@ -204,7 +216,7 @@ void reloadsIntoCacheOnConstruction() { ((ListWritableQueueState) queue).commit(); // When we create the cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var entry0Record = asRecord(entries.get(0)); final var entry1Record = asRecord(entries.get(1)); @@ -244,21 +256,10 @@ void reloadsIntoCacheOnConstruction() { @Test @DisplayName("Rebuild replaces all entries in the in-memory data structures") void reloadsIntoCache() { - // Given a state with some entries and a cache created with that state - final var oldPayer = accountId(1003); - final var oldTxId = - transactionID().copyBuilder().accountID(oldPayer).build(); - final var oldEntry = TransactionReceiptEntries.newBuilder() - .entries(new TransactionReceiptEntry(0, oldTxId, SUCCESS)) - .build(); - final var state = wsa.getState(); assertThat(state).isNotNull(); final var services = state.getWritableStates(RecordCacheService.NAME); final WritableQueueState queue = services.getQueue(TXN_RECEIPT_QUEUE); - assertThat(queue).isNotNull(); - queue.add(oldEntry); - ((ListWritableQueueState) queue).commit(); // When we replace the data "behind the scenes" (emulating a reconnect) and call rebuild final var payer1 = accountId(1001); @@ -273,12 +274,11 @@ void reloadsIntoCache() { new TransactionReceiptEntry(2, txId2, DUPLICATE_TRANSACTION), new TransactionReceiptEntry(3, txId1, DUPLICATE_TRANSACTION)); - assertThat(queue.poll()).isEqualTo(oldEntry); final var entry = new TransactionReceiptEntries(entries); queue.add(entry); ((ListWritableQueueState) queue).commit(); - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var entry0Record = asRecord(entries.get(0)); final var entry1Record = asRecord(entries.get(1)); @@ -299,13 +299,6 @@ void reloadsIntoCache() { assertThat(cache.getRecords(payer2)).containsExactly(entry1Record, entry2Record); assertThat(getReceipts(cache, txId2)).containsExactly(entry1Record.receipt(), entry2Record.receipt()); assertThat(getReceipts(cache, payer2)).containsExactly(entry1Record.receipt(), entry2Record.receipt()); - // And the old state is not in the cache - assertThat(getRecord(cache, oldTxId)).isNull(); - assertThat(getReceipt(cache, oldTxId)).isNull(); - assertThat(getRecords(cache, oldTxId)).isEmpty(); - assertThat(getReceipts(cache, oldTxId)).isEmpty(); - assertThat(cache.getRecords(oldPayer)).isEmpty(); - assertThat(getReceipts(cache, oldPayer)).isEmpty(); } private AccountID accountId(final int num) { @@ -327,7 +320,7 @@ final class ReceiptQueryTests { @DisplayName("Query for receipt for no such txn returns null") void queryForReceiptForNoSuchTxnReturnsNull() { // Given a transaction unknown to the record cache and de-duplication cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var missingTxId = transactionID(); // When we look up the receipt, then we get null @@ -338,7 +331,7 @@ void queryForReceiptForNoSuchTxnReturnsNull() { @DisplayName("Query for receipts for no such txn returns EMPTY LIST") void queryForReceiptsForNoSuchTxnReturnsNull() { // Given a transaction unknown to the record cache and de-duplication cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var missingTxId = transactionID(); // When we look up the receipts, then we get an empty list @@ -349,7 +342,7 @@ void queryForReceiptsForNoSuchTxnReturnsNull() { @DisplayName("Query for receipts for an account ID with no receipts returns EMPTY LIST") void queryForReceiptsForAccountWithNoRecords() { // Given a transaction unknown to the record cache and de-duplication cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); // When we look up the receipts, then we get an empty list assertThat(getReceipts(cache, PAYER_ACCOUNT_ID)).isEmpty(); @@ -359,7 +352,7 @@ void queryForReceiptsForAccountWithNoRecords() { @DisplayName("Query for receipt for txn in UNKNOWN state returns UNKNOWN") void queryForReceiptForUnhandledTxnReturnsNull() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var unhandledTxId = transactionID(); dedupeCache.add(unhandledTxId); @@ -371,7 +364,7 @@ void queryForReceiptForUnhandledTxnReturnsNull() { @DisplayName("Query for receipts by account ID for txn in UNKNOWN state returns EMPTY LIST") void queryForReceiptsForUnhandledTxnByAccountID() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var unhandledTxId = transactionID(); dedupeCache.add(unhandledTxId); @@ -387,9 +380,8 @@ void queryForReceiptsForUnhandledTxnByAccountID() { @DisplayName("Query for receipt for a txn with a proper record") void queryForReceiptForTxnWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -397,7 +389,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(getReceipt(cache, txId)).isEqualTo(receipt); @@ -408,9 +400,8 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Query for receipts for a txn with a proper record") void queryForReceiptsForTxnWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -418,7 +409,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(getReceipts(cache, txId)).containsExactly(receipt); @@ -429,9 +420,8 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Query for receipts for an account ID with a proper record") void queryForReceiptsForAccountIdWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -439,7 +429,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(getReceipts(cache, PAYER_ACCOUNT_ID)).containsExactly(receipt); @@ -451,12 +441,11 @@ final var record = TransactionRecord.newBuilder() "Only up to recordsMaxQueryableByAccount receipts are returned for an account ID with multiple records") void queryForManyReceiptsForAccountID(final int numRecords) { // Given a number of transactions with several records each, all for the same payer - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); // Normally consensus time is AFTER the transaction ID time by a couple of seconds var consensusTime = Instant.now().plusSeconds(2); for (int i = 0; i < numRecords; i++) { final var txId = transactionID(i); - final var tx = simpleCryptoTransfer(txId); for (int j = 0; j < 3; j++) { consensusTime = consensusTime.plus(1, ChronoUnit.NANOS); final var status = j == 0 ? OK : DUPLICATE_TRANSACTION; @@ -466,10 +455,7 @@ final var record = TransactionRecord.newBuilder() .transactionID(txId) .receipt(receipt) .build(); - cache.add( - 0, - PAYER_ACCOUNT_ID, - List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); } } @@ -494,7 +480,7 @@ final class RecordQueryTests { @Test @DisplayName("Query for record for unknown txn returns null") void queryForRecordForUnknownTxnReturnsNull() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var missingTxId = transactionID(); assertThat(getRecord(cache, missingTxId)).isNull(); @@ -503,7 +489,7 @@ void queryForRecordForUnknownTxnReturnsNull() { @Test @DisplayName("Query for records for unknown txn returns EMPTY LIST") void queryForRecordsForUnknownTxnReturnsNull() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var missingTxId = transactionID(); assertThat(getRecords(cache, missingTxId)).isEmpty(); @@ -512,7 +498,7 @@ void queryForRecordsForUnknownTxnReturnsNull() { @Test @DisplayName("Query for record for account ID with no receipts returns EMPTY LIST") void queryForRecordByAccountForUnknownTxn() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); assertThat(cache.getRecords(PAYER_ACCOUNT_ID)).isEmpty(); } @@ -520,7 +506,7 @@ void queryForRecordByAccountForUnknownTxn() { @Test @DisplayName("Query for record for tx with receipt in UNKNOWN state returns null") void queryForRecordForUnknownTxn() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); dedupeCache.add(txId); @@ -530,7 +516,7 @@ void queryForRecordForUnknownTxn() { @Test @DisplayName("Query for records for tx with receipt in UNKNOWN state returns EMPTY LIST") void queryForRecordsForUnknownTxn() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); dedupeCache.add(txId); @@ -540,7 +526,7 @@ void queryForRecordsForUnknownTxn() { @Test @DisplayName("Query for records for tx by account ID with receipt in UNKNOWN state returns EMPTY LIST") void queryForRecordsByAccountForUnknownTxn() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); dedupeCache.add(txId); @@ -552,9 +538,8 @@ void queryForRecordsByAccountForUnknownTxn() { @DisplayName("Query for record for a txn with a proper record") void queryForRecordForTxnWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -562,7 +547,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(getRecord(cache, txId)).isEqualTo(record); @@ -573,7 +558,7 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Query for records for a txn with a proper record") void queryForRecordsForTxnWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); @@ -583,7 +568,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(getRecords(cache, txId)).containsExactly(record); @@ -592,9 +577,8 @@ final var record = TransactionRecord.newBuilder() @Test void unclassifiableStatusIsNotPriority() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var unclassifiableReceipt = TransactionReceipt.newBuilder().status(INVALID_NODE_ACCOUNT).build(); final var unclassifiableRecord = TransactionRecord.newBuilder() @@ -607,18 +591,14 @@ void unclassifiableStatusIsNotPriority() { .transactionID(txId) .receipt(classifiableReceipt) .build(); + given(nodeInfo.accountId()).willReturn(NODE_ACCOUNT_ID); + given(networkInfo.nodeInfo(0)).willReturn(nodeInfo); // When the unclassifiable record is added to the cache - cache.add( - 0, - PAYER_ACCOUNT_ID, - List.of(new SingleTransactionRecord(tx, unclassifiableRecord, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.YES, new PartialRecordSource(unclassifiableRecord)); // It does not prevent a "good" record from using this transaction id assertThat(cache.hasDuplicate(txId, 0L)).isEqualTo(NO_DUPLICATE); - cache.add( - 0, - PAYER_ACCOUNT_ID, - List.of(new SingleTransactionRecord(tx, classifiableRecord, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(classifiableRecord)); // And we get the success record from userTransactionRecord() assertThat(cache.getHistory(txId)).isNotNull(); @@ -632,7 +612,7 @@ void unclassifiableStatusIsNotPriority() { @DisplayName("Query for records for an account ID with a proper record") void queryForRecordsForAccountIdWithRecord(@NonNull final ResponseCodeEnum status) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(status).build(); @@ -642,7 +622,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(0, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(0, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can query for the receipt by transaction ID assertThat(cache.getRecords(PAYER_ACCOUNT_ID)).containsExactly(record); @@ -664,14 +644,14 @@ final class DuplicateCheckTests { @DisplayName("Null args to hasDuplicate throw NPE") @SuppressWarnings("DataFlowIssue") void duplicateCheckWithIllegalParameters() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); assertThatThrownBy(() -> cache.hasDuplicate(null, 1L)).isInstanceOf(NullPointerException.class); } @Test @DisplayName("Check duplicate for unknown txn returns NO_DUPLICATE") void duplicateCheckForUnknownTxn() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var missingTxId = transactionID(); assertThat(cache.hasDuplicate(missingTxId, 1L)).isEqualTo(NO_DUPLICATE); @@ -680,7 +660,7 @@ void duplicateCheckForUnknownTxn() { @Test @DisplayName("Check duplicate for tx with receipt in UNKNOWN state returns NO_DUPLICATE") void duplicateCheckForUnknownState() { - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); dedupeCache.add(txId); @@ -691,7 +671,7 @@ void duplicateCheckForUnknownState() { @DisplayName("Check duplicate for txn with a proper record from other node") void duplicateCheckForTxnFromOtherNode() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(OK).build(); @@ -701,7 +681,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(1L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(1L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can check for a duplicate by transaction ID assertThat(cache.hasDuplicate(txId, 2L)).isEqualTo(OTHER_NODE); @@ -711,7 +691,7 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Check duplicate for txn with a proper record from same node") void duplicateCheckForTxnFromSameNode() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(OK).build(); @@ -721,7 +701,7 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(1L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(1L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can check for a duplicate by transaction ID assertThat(cache.hasDuplicate(txId, 1L)).isEqualTo(SAME_NODE); @@ -731,9 +711,8 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Check duplicate for txn with a proper record from several other nodes") void duplicateCheckForTxnFromMultipleOtherNodes() { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(OK).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -741,9 +720,9 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(1L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); - cache.add(2L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); - cache.add(3L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(1L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); + cache.addRecordSource(2L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); + cache.addRecordSource(3L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can check for a duplicate by transaction ID assertThat(cache.hasDuplicate(txId, 11L)).isEqualTo(OTHER_NODE); @@ -754,9 +733,8 @@ final var record = TransactionRecord.newBuilder() @DisplayName("Check duplicate for txn with a proper record from several nodes including the current") void duplicateCheckForTxnFromMultipleNodesIncludingCurrent(final long currentNodeId) { // Given a transaction known to the de-duplication cache but not the record cache - final var cache = new RecordCacheImpl(dedupeCache, wsa, props); + final var cache = new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); final var txId = transactionID(); - final var tx = simpleCryptoTransfer(txId); final var receipt = TransactionReceipt.newBuilder().status(OK).build(); final var record = TransactionRecord.newBuilder() .transactionID(txId) @@ -764,9 +742,9 @@ final var record = TransactionRecord.newBuilder() .build(); // When the record is added to the cache - cache.add(1L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); - cache.add(2L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); - cache.add(3L, PAYER_ACCOUNT_ID, List.of(new SingleTransactionRecord(tx, record, List.of(), SIMPLE_OUTPUT))); + cache.addRecordSource(1L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); + cache.addRecordSource(2L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); + cache.addRecordSource(3L, txId, DueDiligenceFailure.NO, new PartialRecordSource(record)); // Then we can check for a duplicate by transaction ID assertThat(cache.hasDuplicate(txId, currentNodeId)).isEqualTo(SAME_NODE); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcOperatorQueryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcOperatorQueryTest.java new file mode 100644 index 000000000000..c534d1361981 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcOperatorQueryTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.test.grpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import com.hedera.node.app.workflows.ingest.IngestWorkflow; +import com.hedera.node.app.workflows.query.QueryWorkflow; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * This test verifies gRPC calls made by operators over the network. Since our gRPC code deals with bytes, and + * all serialization and deserialization of protobuf objects happens in the workflows, these tests + * can work on simple primitives such as Strings, making the tests easier to understand without + * missing any possible cases. + */ +class GrpcOperatorQueryTest extends GrpcTestBase { + private static final String SERVICE = "proto.TestService"; + private static final String METHOD = "testQuery"; + private static final String GOOD_RESPONSE = "All Good"; + private static final byte[] GOOD_RESPONSE_BYTES = GOOD_RESPONSE.getBytes(StandardCharsets.UTF_8); + + private static final QueryWorkflow GOOD_QUERY = (req, res) -> res.writeBytes(GOOD_RESPONSE_BYTES); + private static final IngestWorkflow UNIMPLEMENTED_INGEST = (r, r2) -> fail("The Ingest should not be called"); + private static final QueryWorkflow UNIMPLEMENTED_QUERY = + (r, r2) -> fail("The user query workflow should not be called"); + + private void setUp(@NonNull final QueryWorkflow query) { + registerQuery(METHOD, UNIMPLEMENTED_INGEST, UNIMPLEMENTED_QUERY, query); + startServer(true); + } + + @Test + @DisplayName("Call a query on a gRPC service endpoint that succeeds") + void sendGoodQuery() { + // Given a server with a service endpoint and a QueryWorkflow that returns a good response + setUp(GOOD_QUERY); + + // When we call the service + final var response = sendAsNodeOperator(SERVICE, METHOD, "A Query"); + + // Then the response is good, and no exception is thrown. + assertEquals(GOOD_RESPONSE, response); + } + + @Test + @DisplayName("A query throwing a RuntimeException returns the UNKNOWN status code") + void queryThrowingRuntimeExceptionReturnsUNKNOWNError() { + // Given a server where the service will throw a RuntimeException + setUp((req, res) -> { + throw new RuntimeException("Failing with RuntimeException"); + }); + + // When we invoke that service + final var e = assertThrows(StatusRuntimeException.class, () -> sendAsNodeOperator(SERVICE, METHOD, "A Query")); + + // Then the Status code will be UNKNOWN. + assertEquals(Status.UNKNOWN, e.getStatus()); + } + + public static Stream badStatusCodes() { + return Arrays.stream(Code.values()).filter(c -> c != Code.OK).map(Arguments::of); + } + + @ParameterizedTest(name = "{0} Should Fail") + @MethodSource("badStatusCodes") + @DisplayName("Explicitly thrown StatusRuntimeException passes the code through to the response") + void explicitlyThrowStatusRuntimeException(@NonNull final Code code) { + // Given a server where the service will throw a specific StatusRuntimeException + setUp((req, res) -> { + throw new StatusRuntimeException(code.toStatus()); + }); + + // When we invoke that service + final var e = assertThrows(StatusRuntimeException.class, () -> sendAsNodeOperator(SERVICE, METHOD, "A Query")); + + // Then the Status code will match the exception + assertEquals(code.toStatus(), e.getStatus()); + } + + @Test + @DisplayName("Send a valid query to an unknown endpoint and get back UNIMPLEMENTED") + void sendQueryToUnknownEndpoint() { + // Given a client that knows about a method that DOES NOT EXIST on the server + setUp(GOOD_QUERY); + + // When I call the service but with an unknown method + final var e = assertThrows(StatusRuntimeException.class, () -> sendAsNodeOperator(SERVICE, "unknown", "query")); + + // Then the resulting status code is UNIMPLEMENTED + assertEquals(Status.UNIMPLEMENTED.getCode(), e.getStatus().getCode()); + } + + @Test + @DisplayName("Send a valid query to an unknown service") + void sendQueryToUnknownService() { + // Given a client that knows about a service that DOES NOT exist on the server + setUp(GOOD_QUERY); + + // When I call the unknown service + final var e = + assertThrows(StatusRuntimeException.class, () -> sendAsNodeOperator("UnknownService", METHOD, "query")); + + // Then the resulting status code is UNIMPLEMENTED + assertEquals(Status.UNIMPLEMENTED.getCode(), e.getStatus().getCode()); + } + + // Interestingly, I thought it should return INVALID_ARGUMENT, and I attempted to update the + // NoopMarshaller to return INVALID_ARGUMENT by throwing a StatusRuntimeException. But the gRPC library we are + // using DOES NOT special case for StatusRuntimeException thrown in the marshaller, and always returns + // UNKNOWN to the client. So there is really no other response code possible for this case. + @Test + @DisplayName("Sending way too many bytes leads to UNKNOWN") + void sendTooMuchData() { + // Given a service + setUp(GOOD_QUERY); + + // When I call a method on the service and pass too many bytes + final var payload = randomString(1024 * 10); + final var e = assertThrows(StatusRuntimeException.class, () -> sendAsNodeOperator(SERVICE, METHOD, payload)); + + // Then the resulting status code is UNKNOWN + assertEquals(Status.UNKNOWN.getCode(), e.getStatus().getCode()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTestBase.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTestBase.java index 92b90b9cc341..287454541188 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTestBase.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTestBase.java @@ -87,7 +87,7 @@ abstract class GrpcTestBase extends TestBase { /** * Represents "this node" in our tests. */ - private final NodeId nodeSelfId = new NodeId(7); + private final NodeId nodeSelfId = NodeId.of(7); /** * This {@link NettyGrpcServerManager} is used to handle the wire protocol tasks and delegate to our gRPC handlers @@ -115,32 +115,40 @@ abstract class GrpcTestBase extends TestBase { /** The ingest workflow to use. */ private IngestWorkflow ingestWorkflow = NOOP_INGEST_WORKFLOW; /** The query workflow to use. */ - private QueryWorkflow queryWorkflow = NOOP_QUERY_WORKFLOW; + private QueryWorkflow userQueryWorkflow = NOOP_QUERY_WORKFLOW; + + private QueryWorkflow operatorQueryWorkflow = NOOP_QUERY_WORKFLOW; /** The channel on the client to connect to the grpc server */ private Channel channel; + /** The channel on the client to connect to the node operator grpc server */ + private Channel nodeOperatorChannel; /** */ protected void registerQuery( @NonNull final String methodName, @NonNull final IngestWorkflow ingestWorkflow, - @NonNull final QueryWorkflow queryWorkflow) { + @NonNull final QueryWorkflow userQueryWorkflow, + @NonNull final QueryWorkflow operatorQueryWorkflow) { this.queryMethodName = methodName; - this.queryWorkflow = queryWorkflow; + this.userQueryWorkflow = userQueryWorkflow; + this.operatorQueryWorkflow = operatorQueryWorkflow; this.ingestWorkflow = ingestWorkflow; } protected void registerIngest( @NonNull final String methodName, @NonNull final IngestWorkflow ingestWorkflow, - @NonNull final QueryWorkflow queryWorkflow) { + @NonNull final QueryWorkflow userQueryWorkflow, + @NonNull final QueryWorkflow operatorQueryWorkflow) { this.ingestMethodName = methodName; this.ingestWorkflow = ingestWorkflow; - this.queryWorkflow = queryWorkflow; + this.userQueryWorkflow = userQueryWorkflow; + this.operatorQueryWorkflow = operatorQueryWorkflow; } /** Starts the grpcServer and sets up the clients. */ - protected void startServer() { + protected void startServer(boolean withNodeOperatorPort) { final var testService = new RpcService() { @NonNull @Override @@ -182,15 +190,24 @@ public void registerSchemas(@NonNull SchemaRegistry registry) { final var servicesRegistry = new ServicesRegistryImpl(ConstructableRegistry.getInstance(), configuration); servicesRegistry.register(testService); - final var config = createConfig(new TestSource()); + final var config = createConfig(new TestSource().withNodeOperatorPortEnabled(withNodeOperatorPort)); this.grpcServer = new NettyGrpcServerManager( - () -> new VersionedConfigImpl(config, 1), servicesRegistry, ingestWorkflow, queryWorkflow, metrics); + () -> new VersionedConfigImpl(config, 1), + servicesRegistry, + ingestWorkflow, + userQueryWorkflow, + operatorQueryWorkflow, + metrics); grpcServer.start(); this.channel = NettyChannelBuilder.forAddress("localhost", grpcServer.port()) .usePlaintext() .build(); + + this.nodeOperatorChannel = NettyChannelBuilder.forAddress("localhost", grpcServer.nodeOperatorPort()) + .usePlaintext() + .build(); } @AfterEach @@ -198,7 +215,8 @@ void tearDown() { if (this.grpcServer != null) this.grpcServer.stop(); grpcServer = null; ingestWorkflow = NOOP_INGEST_WORKFLOW; - queryWorkflow = NOOP_QUERY_WORKFLOW; + userQueryWorkflow = NOOP_QUERY_WORKFLOW; + operatorQueryWorkflow = NOOP_QUERY_WORKFLOW; queryMethodName = null; ingestMethodName = null; } @@ -226,6 +244,19 @@ protected String send(final String service, final String function, final String payload); } + protected String sendAsNodeOperator(final String service, final String function, final String payload) { + return ClientCalls.blockingUnaryCall( + nodeOperatorChannel, + MethodDescriptor.newBuilder() + .setFullMethodName(service + "/" + function) + .setRequestMarshaller(new StringMarshaller()) + .setResponseMarshaller(new StringMarshaller()) + .setType(MethodType.UNARY) + .build(), + CallOptions.DEFAULT, + payload); + } + protected Configuration createConfig(@NonNull final TestSource testConfig) { return ConfigurationBuilder.create() .withConfigDataType(MetricsConfig.class) @@ -260,6 +291,7 @@ protected static final class TestSource implements ConfigSource { private int tlsPort = 0; private int startRetries = 3; private int startRetryIntervalMs = 100; + private boolean nodeOperatorPortEnabled = false; @Override public int getOrdinal() { @@ -269,7 +301,12 @@ public int getOrdinal() { @NonNull @Override public Set getPropertyNames() { - return Set.of("grpc.port", "grpc.tlsPort", "netty.startRetryIntervalMs", "netty.startRetries"); + return Set.of( + "grpc.port", + "grpc.tlsPort", + "grpc.nodeOperatorPortEnabled", + "netty.startRetryIntervalMs", + "netty.startRetries"); } @Nullable @@ -277,6 +314,7 @@ public Set getPropertyNames() { public String getValue(@NonNull String s) throws NoSuchElementException { return switch (s) { case "grpc.port" -> String.valueOf(port); + case "grpc.nodeOperatorPortEnabled" -> String.valueOf(nodeOperatorPortEnabled); case "grpc.tlsPort" -> String.valueOf(tlsPort); case "netty.startRetryIntervalMs" -> String.valueOf(startRetryIntervalMs); case "netty.startRetries" -> String.valueOf(startRetries); @@ -293,6 +331,11 @@ public TestSource withPort(final int value) { return this; } + public TestSource withNodeOperatorPortEnabled(boolean value) { + this.nodeOperatorPortEnabled = value; + return this; + } + // Locates a free port on its own public TestSource withFreePort() { this.port = findFreePort(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTransactionTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTransactionTest.java index 722fcef65f21..21e0a58dead6 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTransactionTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcTransactionTest.java @@ -52,8 +52,8 @@ class GrpcTransactionTest extends GrpcTestBase { private static final QueryWorkflow UNIMPLEMENTED_QUERY = (r, r2) -> fail("The Query should not be called"); private void setUp(@NonNull final IngestWorkflow ingest) { - registerIngest(METHOD, ingest, UNIMPLEMENTED_QUERY); - startServer(); + registerIngest(METHOD, ingest, UNIMPLEMENTED_QUERY, UNIMPLEMENTED_QUERY); + startServer(false); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcQueryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcUserQueryTest.java similarity index 94% rename from hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcQueryTest.java rename to hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcUserQueryTest.java index 95178301ee1c..50595f76392b 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcQueryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/GrpcUserQueryTest.java @@ -37,12 +37,12 @@ import org.junit.jupiter.params.provider.MethodSource; /** - * This test verifies gRPC calls made over the network. Since our gRPC code deals with bytes, and + * This test verifies gRPC calls made by users over the network. Since our gRPC code deals with bytes, and * all serialization and deserialization of protobuf objects happens in the workflows, these tests * can work on simple primitives such as Strings, making the tests easier to understand without * missing any possible cases. */ -class GrpcQueryTest extends GrpcTestBase { +class GrpcUserQueryTest extends GrpcTestBase { private static final String SERVICE = "proto.TestService"; private static final String METHOD = "testQuery"; private static final String GOOD_RESPONSE = "All Good"; @@ -50,10 +50,12 @@ class GrpcQueryTest extends GrpcTestBase { private static final QueryWorkflow GOOD_QUERY = (req, res) -> res.writeBytes(GOOD_RESPONSE_BYTES); private static final IngestWorkflow UNIMPLEMENTED_INGEST = (r, r2) -> fail("The Ingest should not be called"); + private static final QueryWorkflow UNIMPLEMENTED_QUERY = + (r, r2) -> fail("The operator query workflow should not be called"); private void setUp(@NonNull final QueryWorkflow query) { - registerQuery(METHOD, UNIMPLEMENTED_INGEST, query); - startServer(); + registerQuery(METHOD, UNIMPLEMENTED_INGEST, query, UNIMPLEMENTED_QUERY); + startServer(false); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/NettyManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/NettyManagerTest.java index 42c9ebf40a10..546ad4fb8158 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/NettyManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/test/grpc/NettyManagerTest.java @@ -45,6 +45,7 @@ private NettyGrpcServerManager createServerManager(@NonNull final TestSource tes new ServicesRegistryImpl(ConstructableRegistry.getInstance(), config), (req, res) -> {}, (req, res) -> {}, + (req, res) -> {}, metrics); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java similarity index 85% rename from hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java rename to hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java index cd314373f5b6..ea7631f248f5 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/impl/PlaceholderTssBaseServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/TssBaseServiceImplTest.java @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.hedera.node.app.tss.impl; +package com.hedera.node.app.tss; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; -import com.hedera.node.app.tss.schemas.V0560TSSSchema; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.tss.schemas.V0560TssBaseSchema; import com.swirlds.state.spi.SchemaRegistry; import java.util.ArrayList; import java.util.List; @@ -36,7 +38,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class PlaceholderTssBaseServiceTest { +class TssBaseServiceImplTest { private CountDownLatch latch; private final List receivedMessageHashes = new ArrayList<>(); private final List receivedSignatures = new ArrayList<>(); @@ -51,11 +53,18 @@ class PlaceholderTssBaseServiceTest { @Mock private SchemaRegistry registry; - private final PlaceholderTssBaseService subject = new PlaceholderTssBaseService(); + @Mock + private AppContext.Gossip gossip; + + @Mock + private AppContext appContext; + + private TssBaseServiceImpl subject; @BeforeEach void setUp() { - subject.setExecutor(ForkJoinPool.commonPool()); + given(appContext.gossip()).willReturn(gossip); + subject = new TssBaseServiceImpl(appContext, ForkJoinPool.commonPool(), ForkJoinPool.commonPool()); } @Test @@ -84,6 +93,6 @@ void onlyRegisteredConsumerReceiveCallbacks() throws InterruptedException { @Test void placeholderRegistersSchemas() { subject.registerSchemas(registry); - verify(registry).register(argThat(s -> s instanceof V0560TSSSchema)); + verify(registry).register(argThat(s -> s instanceof V0560TssBaseSchema)); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssMessageHandlerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssMessageHandlerTest.java new file mode 100644 index 000000000000..be02eafc8a79 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssMessageHandlerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import com.swirlds.state.spi.info.NetworkInfo; +import com.swirlds.state.spi.info.NodeInfo; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TssMessageHandlerTest { + private static final AccountID NODE_ACCOUNT_ID = + AccountID.newBuilder().accountNum(666L).build(); + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + + @Mock + private TssSubmissions submissionManager; + + @Mock + private PreHandleContext preHandleContext; + + @Mock(strictness = Mock.Strictness.LENIENT) + private HandleContext handleContext; + + @Mock(strictness = Mock.Strictness.LENIENT) + private NodeInfo nodeInfo; + + @Mock(strictness = Mock.Strictness.LENIENT) + private NetworkInfo networkInfo; + + private TssMessageHandler subject; + + @BeforeEach + void setUp() { + subject = new TssMessageHandler(submissionManager); + } + + @Test + void nothingImplementedYet() { + assertDoesNotThrow(() -> subject.preHandle(preHandleContext)); + assertDoesNotThrow(() -> subject.pureChecks(tssMessage())); + } + + @Test + void submitsToyVoteOnHandlingMessage() { + given(handleContext.networkInfo()).willReturn(networkInfo); + given(handleContext.consensusNow()).willReturn(CONSENSUS_NOW); + given(handleContext.configuration()).willReturn(DEFAULT_CONFIG); + given(networkInfo.selfNodeInfo()).willReturn(nodeInfo); + given(nodeInfo.accountId()).willReturn(NODE_ACCOUNT_ID); + + subject.handle(handleContext); + } + + private TransactionBody tssMessage() { + return TransactionBody.DEFAULT; + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssSubmissionsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssSubmissionsTest.java new file mode 100644 index 000000000000..727197dc7fee --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssSubmissionsTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ACCOUNT; +import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.swirlds.platform.system.status.PlatformStatus.BEHIND; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Duration; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.config.data.HederaConfig; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.swirlds.config.api.Configuration; +import com.swirlds.state.spi.info.NetworkInfo; +import com.swirlds.state.spi.info.NodeInfo; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TssSubmissionsTest { + private static final int TIMES_TO_TRY_SUBMISSION = 3; + private static final int DISTINCT_TXN_IDS_TO_TRY = 2; + private static final String RETRY_DELAY = "1ms"; + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + private static final AccountID NODE_ACCOUNT_ID = + AccountID.newBuilder().accountNum(666L).build(); + private static final Configuration TEST_CONFIG = HederaTestConfigBuilder.create() + .withValue("tss.timesToTrySubmission", TIMES_TO_TRY_SUBMISSION) + .withValue("tss.distinctTxnIdsToTry", DISTINCT_TXN_IDS_TO_TRY) + .withValue("tss.retryDelay", RETRY_DELAY) + .getOrCreateConfig(); + private static final Duration DURATION = + new Duration(TEST_CONFIG.getConfigData(HederaConfig.class).transactionMaxValidDuration()); + + @Mock + private HandleContext context; + + @Mock + private NodeInfo nodeInfo; + + @Mock + private NetworkInfo networkInfo; + + @Mock + private AppContext.Gossip gossip; + + private TssSubmissions subject; + + @BeforeEach + void setUp() { + subject = new TssSubmissions(gossip, ForkJoinPool.commonPool()); + given(context.consensusNow()).willReturn(CONSENSUS_NOW); + given(context.networkInfo()).willReturn(networkInfo); + given(networkInfo.selfNodeInfo()).willReturn(nodeInfo); + given(nodeInfo.accountId()).willReturn(NODE_ACCOUNT_ID); + given(context.configuration()).willReturn(TEST_CONFIG); + } + + @Test + void futureResolvesOnSuccessfulSubmission() throws ExecutionException, InterruptedException, TimeoutException { + final var future = subject.submitTssMessage(TssMessageTransactionBody.DEFAULT, context); + + future.get(1, TimeUnit.SECONDS); + + verify(gossip).submit(messageSubmission(0)); + } + + @Test + void futureCompletesExceptionallyAfterRetriesExhausted() + throws ExecutionException, InterruptedException, TimeoutException { + doThrow(new IllegalStateException("" + BEHIND)).when(gossip).submit(any()); + + final var future = subject.submitTssVote(TssVoteTransactionBody.DEFAULT, context); + + future.exceptionally(t -> { + verify(gossip, times(TIMES_TO_TRY_SUBMISSION)).submit(voteSubmission(0)); + return null; + }) + .get(1, TimeUnit.SECONDS); + assertTrue(future.isCompletedExceptionally()); + } + + @Test + void immediatelyRetriesOnDuplicateIae() throws ExecutionException, InterruptedException, TimeoutException { + doThrow(new IllegalArgumentException("" + DUPLICATE_TRANSACTION)) + .when(gossip) + .submit(voteSubmission(0)); + + final var future = subject.submitTssVote(TssVoteTransactionBody.DEFAULT, context); + + future.get(1, TimeUnit.SECONDS); + + verify(gossip).submit(voteSubmission(1)); + } + + @Test + void failsImmediatelyOnHittingNonDuplicateIae() throws ExecutionException, InterruptedException, TimeoutException { + doThrow(new IllegalArgumentException("" + DUPLICATE_TRANSACTION)) + .when(gossip) + .submit(messageSubmission(0)); + doThrow(new IllegalArgumentException("" + INVALID_NODE_ACCOUNT)) + .when(gossip) + .submit(messageSubmission(1)); + + final var future = subject.submitTssMessage(TssMessageTransactionBody.DEFAULT, context); + + future.exceptionally(t -> { + for (int i = 0; i < DISTINCT_TXN_IDS_TO_TRY; i++) { + verify(gossip).submit(messageSubmission(i)); + } + return null; + }) + .get(1, TimeUnit.SECONDS); + assertTrue(future.isCompletedExceptionally()); + } + + private TransactionBody voteSubmission(final int nanoOffset) { + return builderFor(nanoOffset).tssVote(TssVoteTransactionBody.DEFAULT).build(); + } + + private TransactionBody messageSubmission(final int nanoOffset) { + return builderFor(nanoOffset) + .tssMessage(TssMessageTransactionBody.DEFAULT) + .build(); + } + + private TransactionBody.Builder builderFor(final int nanoOffset) { + return TransactionBody.newBuilder() + .nodeAccountID(NODE_ACCOUNT_ID) + .transactionValidDuration(DURATION) + .transactionID(TransactionID.newBuilder() + .accountID(NODE_ACCOUNT_ID) + .transactionValidStart(asTimestamp(CONSENSUS_NOW.plusNanos(nanoOffset))) + .build()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssVoteHandlerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssVoteHandlerTest.java new file mode 100644 index 000000000000..cac2aea3bd97 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/handlers/TssVoteHandlerTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.tss.handlers; + +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TssVoteHandlerTest { + @Mock + private TssSubmissions submissionManager; + + @Mock + private PreHandleContext preHandleContext; + + @Mock + private HandleContext handleContext; + + private TssVoteHandler subject; + + @BeforeEach + void setUp() { + subject = new TssVoteHandler(); + } + + @Test + void nothingImplementedYet() { + assertDoesNotThrow(() -> subject.preHandle(preHandleContext)); + assertDoesNotThrow(() -> subject.pureChecks(tssVote())); + assertDoesNotThrow(() -> subject.handle(handleContext)); + } + + private TransactionBody tssVote() { + return TransactionBody.DEFAULT; + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/V0560TSSSchemaTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/V0560TSSSchemaTest.java index 91985cda9c84..72e9a7811887 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/V0560TSSSchemaTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/tss/schemas/V0560TSSSchemaTest.java @@ -24,11 +24,11 @@ import org.junit.jupiter.api.Test; public class V0560TSSSchemaTest { - private V0560TSSSchema subject; + private V0560TssBaseSchema subject; @BeforeEach void setUp() { - subject = new V0560TSSSchema(); + subject = new V0560TssBaseSchema(); } @Test @@ -37,7 +37,7 @@ void registersExpectedSchema() { assertThat(statesToCreate.size()).isEqualTo(2); final var iter = statesToCreate.stream().map(StateDefinition::stateKey).sorted().iterator(); - assertEquals(V0560TSSSchema.TSS_MESSAGE_MAP_KEY, iter.next()); - assertEquals(V0560TSSSchema.TSS_VOTE_MAP_KEY, iter.next()); + assertEquals(V0560TssBaseSchema.TSS_MESSAGE_MAP_KEY, iter.next()); + assertEquals(V0560TssBaseSchema.TSS_VOTE_MAP_KEY, iter.next()); } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/utils/TestUtils.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/utils/TestUtils.java index 7ea3f1958ae7..de314e218abb 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/utils/TestUtils.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/utils/TestUtils.java @@ -49,7 +49,7 @@ public static Metrics metrics() { HederaTestConfigBuilder.createConfig().getConfigData(MetricsConfig.class); return new DefaultPlatformMetrics( - new NodeId(DEFAULT_NODE_ID), + NodeId.of(DEFAULT_NODE_ID), new MetricKeyRegistry(), Executors.newSingleThreadScheduledExecutor(), new PlatformMetricsFactoryImpl(metricsConfig), diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchProcessorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchProcessorTest.java index 03bd9e46dea4..aa56f2daf61d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchProcessorTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/DispatchProcessorTest.java @@ -38,10 +38,10 @@ import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.USER; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.DuplicateStatus.NO_DUPLICATE; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.ServiceFeeStatus.UNABLE_TO_PAY_SERVICE_FEE; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.creatorValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerDuplicateErrorReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.successReport; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newCreatorError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerDuplicateError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newSuccess; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doThrow; @@ -192,7 +192,7 @@ void creatorErrorAsExpected() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); given(dispatchValidator.validationReportFor(dispatch)) - .willReturn(creatorValidationReport(CREATOR_ACCOUNT_ID, INVALID_PAYER_SIGNATURE)); + .willReturn(newCreatorError(CREATOR_ACCOUNT_ID, INVALID_PAYER_SIGNATURE)); subject.processDispatch(dispatch); @@ -204,7 +204,7 @@ void creatorErrorAsExpected() { @Test void waivedFeesDoesNotCharge() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.fees()).willReturn(FEES); @@ -226,7 +226,7 @@ void waivedFeesDoesNotCharge() { @Test void unauthorizedSystemDeleteIsNotSupported() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(SYS_DEL_TXN_INFO); given(authorizer.isAuthorized(PAYER_ACCOUNT_ID, SYS_DEL_TXN_INFO.functionality())) @@ -245,7 +245,7 @@ void unauthorizedSystemDeleteIsNotSupported() { @Test void unauthorizedOtherIsUnauthorized() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(SYS_UNDEL_TXN_INFO); given(authorizer.isAuthorized(PAYER_ACCOUNT_ID, SYS_UNDEL_TXN_INFO.functionality())) @@ -264,7 +264,7 @@ void unauthorizedOtherIsUnauthorized() { @Test void unprivilegedSystemUndeleteIsAuthorizationFailed() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(SYS_DEL_TXN_INFO); given(authorizer.isAuthorized(PAYER_ACCOUNT_ID, SYS_DEL_TXN_INFO.functionality())) @@ -285,7 +285,7 @@ void unprivilegedSystemUndeleteIsAuthorizationFailed() { @Test void unprivilegedSystemDeleteIsImpermissible() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(SYS_DEL_TXN_INFO); given(authorizer.isAuthorized(PAYER_ACCOUNT_ID, SYS_DEL_TXN_INFO.functionality())) @@ -306,7 +306,7 @@ void unprivilegedSystemDeleteIsImpermissible() { @Test void invalidSignatureCryptoTransferFails() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.txnCategory()).willReturn(USER); @@ -327,7 +327,7 @@ void invalidSignatureCryptoTransferFails() { @Test void invalidHollowAccountCryptoTransferFails() { - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); givenAuthorization(); @@ -352,7 +352,7 @@ void invalidHollowAccountCryptoTransferFails() { void thrownHandleExceptionRollsBackIfRequested() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.handleContext()).willReturn(context); @@ -375,7 +375,7 @@ void thrownHandleExceptionRollsBackIfRequested() { void thrownHandleExceptionDoesNotRollBackIfNotRequested() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CONTRACT_TXN_INFO); given(dispatch.handleContext()).willReturn(context); @@ -398,7 +398,7 @@ void thrownHandleExceptionDoesNotRollBackIfNotRequested() { void consGasExhaustedWaivesServiceFee() throws ThrottleException { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CONTRACT_TXN_INFO); givenAuthorization(CONTRACT_TXN_INFO); @@ -422,7 +422,7 @@ void consGasExhaustedForEthTxnDoesExtraWork() throws ThrottleException { given(dispatch.fees()).willReturn(FEES); given(dispatch.handleContext()).willReturn(context); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(ETH_TXN_INFO); givenAuthorization(ETH_TXN_INFO); @@ -446,7 +446,7 @@ void consGasExhaustedForEthTxnDoesExtraWork() throws ThrottleException { void failInvalidWaivesServiceFee() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.handleContext()).willReturn(context); @@ -467,7 +467,7 @@ void failInvalidWaivesServiceFee() { void happyPathContractCallAsExpected() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CONTRACT_TXN_INFO); given(dispatch.handleContext()).willReturn(context); @@ -493,7 +493,7 @@ void happyPathContractCallAsExpected() { void happyPathChildCryptoTransferAsExpected() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.txnCategory()).willReturn(HandleContext.TransactionCategory.CHILD); @@ -511,7 +511,7 @@ void happyPathChildCryptoTransferAsExpected() { @Test void happyPathFreeChildCryptoTransferAsExpected() { given(dispatch.fees()).willReturn(Fees.FREE); - given(dispatchValidator.validationReportFor(dispatch)).willReturn(successReport(CREATOR_ACCOUNT_ID, PAYER)); + given(dispatchValidator.validationReportFor(dispatch)).willReturn(newSuccess(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.txnCategory()).willReturn(HandleContext.TransactionCategory.CHILD); @@ -530,7 +530,7 @@ void unableToAffordServiceFeesChargesAccordingly() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); given(dispatchValidator.validationReportFor(dispatch)) - .willReturn(payerValidationReport( + .willReturn(newPayerError( CREATOR_ACCOUNT_ID, PAYER, INSUFFICIENT_ACCOUNT_BALANCE, @@ -553,7 +553,7 @@ void duplicateChargesAccordingly() { given(dispatch.fees()).willReturn(FEES); given(dispatch.feeAccumulator()).willReturn(feeAccumulator); given(dispatchValidator.validationReportFor(dispatch)) - .willReturn(payerDuplicateErrorReport(CREATOR_ACCOUNT_ID, PAYER)); + .willReturn(newPayerDuplicateError(CREATOR_ACCOUNT_ID, PAYER)); given(dispatch.payerId()).willReturn(PAYER_ACCOUNT_ID); given(dispatch.txnInfo()).willReturn(CRYPTO_TRANSFER_TXN_INFO); given(dispatch.txnCategory()).willReturn(USER); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleOutputTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleOutputTest.java new file mode 100644 index 000000000000..edc43d4bc5d4 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleOutputTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.handle; + +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.node.app.spi.records.RecordSource; +import com.hedera.node.app.state.recordcache.BlockRecordSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HandleOutputTest { + @Mock + private BlockRecordSource blockRecordSource; + + @Mock + private RecordSource recordSource; + + @Test + void throwsIfMissingRecordSourceWhenRequired() { + final var subject = new HandleOutput(blockRecordSource, null); + assertThrows(NullPointerException.class, subject::recordSourceOrThrow); + } + + @Test + void throwsIfMissingBlockRecordSourceWhenRequired() { + final var subject = new HandleOutput(null, recordSource); + assertThrows(NullPointerException.class, subject::blockRecordSourceOrThrow); + } + + @Test + void returnsRecordSourceWhenPresent() { + final var subject = new HandleOutput(null, recordSource); + assertEquals(recordSource, subject.recordSourceOrThrow()); + } + + @Test + void returnsBlockRecordSourceWhenPresent() { + final var subject = new HandleOutput(blockRecordSource, null); + assertEquals(blockRecordSource, subject.blockRecordSourceOrThrow()); + } + + @Test + void returnsBlockRecordSourceWhenPresentOtherwiseRecordSource() { + final var withBlockSource = new HandleOutput(blockRecordSource, recordSource); + assertEquals(blockRecordSource, withBlockSource.preferringBlockRecordSource()); + + final var withoutBlockSource = new HandleOutput(null, recordSource); + assertEquals(recordSource, withoutBlockSource.preferringBlockRecordSource()); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java index 1b2191338f68..e5524cfbe383 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java @@ -76,6 +76,9 @@ import com.hedera.node.app.service.token.impl.handlers.TokenUpdateHandler; import com.hedera.node.app.service.util.impl.handlers.UtilHandlers; import com.hedera.node.app.service.util.impl.handlers.UtilPrngHandler; +import com.hedera.node.app.tss.handlers.TssHandlers; +import com.hedera.node.app.tss.handlers.TssMessageHandler; +import com.hedera.node.app.tss.handlers.TssVoteHandler; import com.hedera.node.app.workflows.dispatcher.TransactionHandlers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -255,6 +258,12 @@ class HandleWorkflowModuleTest { @Mock private UtilPrngHandler utilPrngHandler; + @Mock + private TssMessageHandler tssMessageHandler; + + @Mock + private TssVoteHandler tssVoteHandler; + @Test void usesComponentsToGetHandlers() { given(consensusHandlers.consensusCreateTopicHandler()).willReturn(consensusCreateTopicHandler); @@ -312,6 +321,7 @@ void usesComponentsToGetHandlers() { consensusHandlers, fileHandlers, () -> contractHandlers, + () -> new TssHandlers(tssMessageHandler, tssVoteHandler), scheduleHandlers, tokenHandlers, utilHandlers, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java index df0b83117024..62521d3e6035 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowTest.java @@ -37,10 +37,10 @@ import com.hedera.node.app.state.HederaRecordCache; import com.hedera.node.app.throttle.NetworkUtilizationManager; import com.hedera.node.app.throttle.ThrottleServiceManager; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; import com.hedera.node.app.workflows.handle.cache.CacheWarmer; import com.hedera.node.app.workflows.handle.dispatch.ChildDispatchFactory; -import com.hedera.node.app.workflows.handle.metric.HandleWorkflowMetrics; import com.hedera.node.app.workflows.handle.record.SystemSetup; import com.hedera.node.app.workflows.handle.steps.HollowAccountCompletions; import com.hedera.node.app.workflows.handle.steps.NodeStakeUpdates; @@ -112,7 +112,7 @@ class HandleWorkflowTest { private CacheWarmer cacheWarmer; @Mock - private HandleWorkflowMetrics handleWorkflowMetrics; + private OpWorkflowMetrics opWorkflowMetrics; @Mock private ThrottleServiceManager throttleServiceManager; @@ -173,7 +173,7 @@ void setUp() { blockRecordManager, blockStreamManager, cacheWarmer, - handleWorkflowMetrics, + opWorkflowMetrics, throttleServiceManager, version, initTrigger, @@ -191,8 +191,8 @@ void setUp() { @Test void onlySkipsEventWithMissingCreator() { - final var presentCreatorId = new NodeId(1L); - final var missingCreatorId = new NodeId(2L); + final var presentCreatorId = NodeId.of(1L); + final var missingCreatorId = NodeId.of(2L); final var eventFromPresentCreator = mock(ConsensusEvent.class); final var eventFromMissingCreator = mock(ConsensusEvent.class); given(round.iterator()) @@ -204,7 +204,6 @@ void onlySkipsEventWithMissingCreator() { given(networkInfo.nodeInfo(missingCreatorId.id())).willReturn(null); given(eventFromPresentCreator.consensusTransactionIterator()).willReturn(Collections.emptyIterator()); given(round.getConsensusTimestamp()).willReturn(Instant.ofEpochSecond(12345L)); - given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1)); subject.handleRound(state, round); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java index ab15f5809709..8b682da82c08 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ChildDispatchFactoryTest.java @@ -39,6 +39,7 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.hapi.util.UnknownHederaFunctionality; +import com.hedera.node.app.blocks.BlockItemsTranslator; import com.hedera.node.app.fees.ExchangeRateManager; import com.hedera.node.app.fees.FeeManager; import com.hedera.node.app.service.token.ReadableAccountStore; @@ -137,6 +138,9 @@ class ChildDispatchFactoryTest { @Mock private ExchangeRateManager exchangeRateManager; + @Mock + private BlockItemsTranslator recordTranslator; + private ChildDispatchFactory subject; private static final AccountID payerId = diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationReporterTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationReporterTest.java index bb033460010c..46f9b3b7016d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationReporterTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationReporterTest.java @@ -31,9 +31,9 @@ import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.ServiceFeeStatus.CAN_PAY_SERVICE_FEE; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.ServiceFeeStatus.UNABLE_TO_PAY_SERVICE_FEE; import static com.hedera.node.app.workflows.handle.dispatch.DispatchValidator.WorkflowCheck.NOT_INGEST; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.creatorValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.payerValidationReport; -import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.successReport; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newCreatorError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newPayerError; +import static com.hedera.node.app.workflows.handle.dispatch.ValidationResult.newSuccess; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.NODE_DUE_DILIGENCE_FAILURE; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.PRE_HANDLE_FAILURE; import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.SO_FAR_SO_GOOD; @@ -87,7 +87,7 @@ class ValidationReporterTest { AccountID.newBuilder().accountNum(1_234).build(); private static final AccountID CREATOR_ACCOUNT_ID = AccountID.newBuilder().accountNum(3).build(); - private static final NodeId CREATOR_NODE_ID = new NodeId(0L); + private static final NodeId CREATOR_NODE_ID = NodeId.of(0L); @Mock private NodeInfo creatorInfo; @@ -127,7 +127,7 @@ void dueDiligencePreHandleIsCreatorError() { final var report = subject.validationReportFor(dispatch); - assertEquals(creatorValidationReport(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE), report); + assertEquals(newCreatorError(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE), report); } @Test @@ -143,7 +143,7 @@ void invalidTransactionDurationIsCreatorError() throws PreCheckException { final var report = subject.validationReportFor(dispatch); - assertEquals(creatorValidationReport(dispatch.creatorInfo().accountId(), INVALID_TRANSACTION_DURATION), report); + assertEquals(newCreatorError(dispatch.creatorInfo().accountId(), INVALID_TRANSACTION_DURATION), report); } @Test @@ -157,7 +157,7 @@ void invalidPayerSigIsCreatorError() throws PreCheckException { final var report = subject.validationReportFor(dispatch); - assertEquals(creatorValidationReport(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE), report); + assertEquals(newCreatorError(dispatch.creatorInfo().accountId(), INVALID_PAYER_SIGNATURE), report); } @Test @@ -179,7 +179,7 @@ void solvencyCheckDoesNotLookAtOfferedFeesForPrecedingDispatch() throws PreCheck FEES, NOT_INGEST, SKIP_OFFERED_FEE_CHECK); - assertEquals(successReport(dispatch.creatorInfo().accountId(), payerAccount), report); + assertEquals(newSuccess(dispatch.creatorInfo().accountId(), payerAccount), report); } @Test @@ -204,7 +204,7 @@ void hollowUserDoesNotRequireSig() throws PreCheckException { FEES, NOT_INGEST, CHECK_OFFERED_FEE); - assertEquals(successReport(dispatch.creatorInfo().accountId(), payerAccount), report); + assertEquals(newSuccess(dispatch.creatorInfo().accountId(), payerAccount), report); } @Test @@ -229,7 +229,7 @@ void otherNodeDuplicateUserIsPayerError() throws PreCheckException { NOT_INGEST, CHECK_OFFERED_FEE); assertEquals( - payerValidationReport( + newPayerError( dispatch.creatorInfo().accountId(), payerAccount, DUPLICATE_TRANSACTION, @@ -250,7 +250,7 @@ void sameNodeDuplicateUserIsCreatorError() { final var report = subject.validationReportFor(dispatch); - assertEquals(creatorValidationReport(dispatch.creatorInfo().accountId(), DUPLICATE_TRANSACTION), report); + assertEquals(newCreatorError(dispatch.creatorInfo().accountId(), DUPLICATE_TRANSACTION), report); } @Test @@ -275,7 +275,7 @@ void userPreHandleFailureIsPayerError() throws PreCheckException { NOT_INGEST, CHECK_OFFERED_FEE); assertEquals( - payerValidationReport( + newPayerError( dispatch.creatorInfo().accountId(), payerAccount, UNSUCCESSFUL_PREHANDLE.responseCode(), @@ -305,7 +305,7 @@ void precedingInsufficientServiceFeeIsPayerError() throws PreCheckException { final var report = subject.validationReportFor(dispatch); assertEquals( - payerValidationReport( + newPayerError( dispatch.creatorInfo().accountId(), payerAccount, INSUFFICIENT_ACCOUNT_BALANCE, @@ -336,7 +336,7 @@ void scheduledNonFeeDebitsFeeIsPayerError() throws PreCheckException { final var report = subject.validationReportFor(dispatch); assertEquals( - payerValidationReport( + newPayerError( dispatch.creatorInfo().accountId(), payerAccount, INSUFFICIENT_ACCOUNT_BALANCE, @@ -367,7 +367,7 @@ void insufficientNetworkFeeIsCreatorError() throws PreCheckException { final var report = subject.validationReportFor(dispatch); - assertEquals(creatorValidationReport(dispatch.creatorInfo().accountId(), INSUFFICIENT_PAYER_BALANCE), report); + assertEquals(newCreatorError(dispatch.creatorInfo().accountId(), INSUFFICIENT_PAYER_BALANCE), report); } @Test diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java index 02ce17cbfe84..079d7eeb768a 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/dispatch/ValidationResultTest.java @@ -41,8 +41,7 @@ public class ValidationResultTest { @Test public void testCreatorErrorReport() { - ValidationResult report = - ValidationResult.creatorValidationReport(CREATOR_ACCOUNT_ID, INVALID_TRANSACTION_DURATION); + ValidationResult report = ValidationResult.newCreatorError(CREATOR_ACCOUNT_ID, INVALID_TRANSACTION_DURATION); assertEquals(CREATOR_ACCOUNT_ID, report.creatorId()); assertEquals(INVALID_TRANSACTION_DURATION, report.creatorError()); @@ -56,7 +55,7 @@ public void testCreatorErrorReport() { @Test public void testPayerDuplicateErrorReport() { - ValidationResult report = ValidationResult.payerDuplicateErrorReport(CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID); + ValidationResult report = ValidationResult.newPayerDuplicateError(CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID); assertEquals(CREATOR_ACCOUNT_ID, report.creatorId()); assertNull(report.creatorError()); @@ -70,8 +69,8 @@ public void testPayerDuplicateErrorReport() { @Test public void testPayerUniqueErrorReport() { - ValidationResult report = ValidationResult.payerUniqueValidationReport( - CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID, INVALID_PAYER_SIGNATURE); + ValidationResult report = + ValidationResult.newPayerUniqueError(CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID, INVALID_PAYER_SIGNATURE); assertEquals(CREATOR_ACCOUNT_ID, report.creatorId()); assertNull(report.creatorError()); @@ -85,7 +84,7 @@ public void testPayerUniqueErrorReport() { @Test public void testPayerErrorReport() { - ValidationResult report = ValidationResult.payerValidationReport( + ValidationResult report = ValidationResult.newPayerError( CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID, INVALID_PAYER_SIGNATURE, UNABLE_TO_PAY_SERVICE_FEE, DUPLICATE); assertEquals(CREATOR_ACCOUNT_ID, report.creatorId()); @@ -100,7 +99,7 @@ public void testPayerErrorReport() { @Test public void testErrorFreeReport() { - ValidationResult report = ValidationResult.successReport(CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID); + ValidationResult report = ValidationResult.newSuccess(CREATOR_ACCOUNT_ID, PAYER_ACCOUNT_ID); assertEquals(CREATOR_ACCOUNT_ID, report.creatorId()); assertNull(report.creatorError()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/HandleWorkflowMetricsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/OpWorkflowMetricsTest.java similarity index 68% rename from hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/HandleWorkflowMetricsTest.java rename to hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/OpWorkflowMetricsTest.java index 0334210da05d..6ceadc9e6a31 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/HandleWorkflowMetricsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/metrics/OpWorkflowMetricsTest.java @@ -23,7 +23,7 @@ import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.node.app.utils.TestUtils; -import com.hedera.node.app.workflows.handle.metric.HandleWorkflowMetrics; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.config.ConfigProvider; import com.hedera.node.config.VersionedConfigImpl; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; @@ -31,7 +31,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class HandleWorkflowMetricsTest { +class OpWorkflowMetricsTest { private final Metrics metrics = TestUtils.metrics(); private ConfigProvider configProvider; @@ -44,15 +44,14 @@ void setUp() { @SuppressWarnings("DataFlowIssue") @Test void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> new HandleWorkflowMetrics(null, configProvider)) - .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> new HandleWorkflowMetrics(metrics, null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new OpWorkflowMetrics(null, configProvider)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new OpWorkflowMetrics(metrics, null)).isInstanceOf(NullPointerException.class); } @Test void testConstructorInitializesMetrics() { // when - new HandleWorkflowMetrics(metrics, configProvider); + new OpWorkflowMetrics(metrics, configProvider); // then final int transactionMetricsCount = (HederaFunctionality.values().length - 1) * 2; @@ -62,7 +61,7 @@ void testConstructorInitializesMetrics() { @Test void testInitialValue() { // given - new HandleWorkflowMetrics(metrics, configProvider); + new OpWorkflowMetrics(metrics, configProvider); // then assertThat(metrics.getMetric("app", "cryptoCreateDurationMax").get(VALUE)) @@ -73,22 +72,22 @@ void testInitialValue() { @SuppressWarnings("DataFlowIssue") @Test - void testUpdateTransactionDurationWithInvalidArguments() { + void testUpdateDurationWithInvalidArguments() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when - assertThatThrownBy(() -> handleWorkflowMetrics.updateTransactionDuration(null, 0)) + assertThatThrownBy(() -> handleWorkflowMetrics.updateDuration(null, 0)) .isInstanceOf(NullPointerException.class); } @Test void testUpdateTransactionDurationSingleUpdate() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 42); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 42); // then assertThat(metrics.getMetric("app", "cryptoCreateDurationMax").get(VALUE)) @@ -98,13 +97,13 @@ void testUpdateTransactionDurationSingleUpdate() { } @Test - void testUpdateTransactionDurationTwoUpdates() { + void testUpdateDurationTwoUpdates() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 11); - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 22); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 11); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 22); // then assertThat(metrics.getMetric("app", "cryptoCreateDurationMax").get(VALUE)) @@ -114,14 +113,14 @@ void testUpdateTransactionDurationTwoUpdates() { } @Test - void testUpdateTransactionDurationThreeUpdates() { + void testUpdateDurationThreeUpdates() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 13); - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 5); - handleWorkflowMetrics.updateTransactionDuration(HederaFunctionality.CRYPTO_CREATE, 3); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 13); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 5); + handleWorkflowMetrics.updateDuration(HederaFunctionality.CRYPTO_CREATE, 3); // then assertThat(metrics.getMetric("app", "cryptoCreateDurationMax").get(VALUE)) @@ -133,7 +132,7 @@ void testUpdateTransactionDurationThreeUpdates() { @Test void testInitialStartConsensusRound() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when handleWorkflowMetrics.switchConsensusSecond(); @@ -146,7 +145,7 @@ void testInitialStartConsensusRound() { @Test void testUpdateGasZero() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when handleWorkflowMetrics.addGasUsed(0L); @@ -160,7 +159,7 @@ void testUpdateGasZero() { @Test void testUpdateGas() { // given - final var handleWorkflowMetrics = new HandleWorkflowMetrics(metrics, configProvider); + final var handleWorkflowMetrics = new OpWorkflowMetrics(metrics, configProvider); // when handleWorkflowMetrics.addGasUsed(1_000_000L); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java index 0a3951496de0..93fada44deff 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordManagerTest.java @@ -126,6 +126,7 @@ void setUpEach() throws Exception { .withConfigValue("hedera.recordStream.signatureFileVersion", 6) .withConfigValue("hedera.recordStream.compressFilesOnCreation", true) .withConfigValue("hedera.recordStream.sidecarMaxSizeMb", 256) + .withConfigValue("blockStream.streamMode", "BOTH") .withService(new BlockRecordService()) .withService(PLATFORM_STATE_SERVICE) .build(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java index 1742ee7985c1..2c1fd7df386a 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/BlockRecordServiceTest.java @@ -30,7 +30,7 @@ import com.hedera.hapi.node.state.blockrecords.RunningHashes; import com.hedera.node.app.records.BlockRecordService; import com.hedera.node.app.records.schemas.V0490BlockRecordSchema; -import com.hedera.node.app.records.schemas.V0540BlockRecordSchema; +import com.hedera.node.app.records.schemas.V0560BlockRecordSchema; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.state.spi.MigrationContext; import com.swirlds.state.spi.Schema; @@ -87,9 +87,9 @@ void testRegisterSchemas() { assertEquals( new RunningHashes(GENESIS_HASH, Bytes.EMPTY, Bytes.EMPTY, Bytes.EMPTY), runningHashesCapture.getValue()); - assertEquals(new BlockInfo(-1, EPOCH, Bytes.EMPTY, EPOCH, false, EPOCH), blockInfoCapture.getValue()); + assertEquals(new BlockInfo(-1, EPOCH, Bytes.EMPTY, EPOCH, true, EPOCH), blockInfoCapture.getValue()); } else { - assertThat(schema).isInstanceOf(V0540BlockRecordSchema.class); + assertThat(schema).isInstanceOf(V0560BlockRecordSchema.class); } return null; }); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/StreamBuilderTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/StreamBuilderTest.java index 61a965572cbf..d500e1128c90 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/StreamBuilderTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/StreamBuilderTest.java @@ -134,7 +134,6 @@ void testBuilder(TransactionRecord.EntropyOneOfType entropyOneOfType) { .scheduleRef(scheduleRef) .assessedCustomFees(assessedCustomFees) .automaticTokenAssociations(automaticTokenAssociations) - .alias(alias) .ethereumHash(ethereumHash) .paidStakingRewards(paidStakingRewards) .evmAddress(evmAddress) @@ -207,7 +206,6 @@ void testBuilder(TransactionRecord.EntropyOneOfType entropyOneOfType) { assertEquals( HapiUtils.asTimestamp(PARENT_CONSENSUS_TIME), singleTransactionRecord.transactionRecord().parentConsensusTimestamp()); - assertEquals(alias, singleTransactionRecord.transactionRecord().alias()); assertEquals(ethereumHash, singleTransactionRecord.transactionRecord().ethereumHash()); assertEquals( paidStakingRewards, singleTransactionRecord.transactionRecord().paidStakingRewards()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/v6/BlockRecordWriterV6Test.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/v6/BlockRecordWriterV6Test.java index 5fa0ca9f968e..11af013d79b0 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/v6/BlockRecordWriterV6Test.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/record/impl/producers/formats/v6/BlockRecordWriterV6Test.java @@ -87,7 +87,6 @@ void setUp() { appBuilder = appBuilder() .withHapiVersion(VERSION) - .withSoftwareVersion(VERSION) .withConfigValue("hedera.recordStream.enabled", true) .withConfigValue("hedera.recordStream.logDir", tempDir.toString()) .withConfigValue("hedera.recordStream.sidecarDir", "sidecar") diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImplBlocksStreamModeTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImplBlocksStreamModeTest.java new file mode 100644 index 000000000000..d8481aeb545e --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/SavepointStackImplBlocksStreamModeTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.handle.stack; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; +import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; +import com.hedera.node.app.blocks.impl.KVStateChangeListener; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; +import com.swirlds.state.State; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SavepointStackImplBlocksStreamModeTest { + @Mock + private State state; + + @Mock + private StreamBuilder streamBuilder; + + @Mock + private BoundaryStateChangeListener boundaryStateChangeListener; + + @Mock + private KVStateChangeListener kvStateChangeListener; + + private SavepointStackImpl subject; + + @BeforeEach + void setUp() { + subject = SavepointStackImpl.newRootStack( + state, 3, 50, boundaryStateChangeListener, kvStateChangeListener, BLOCKS); + } + + @Test + void commitsFullStackAsExpected() { + final var mockChanges = List.of(StateChange.DEFAULT); + given(kvStateChangeListener.getStateChanges()).willReturn(mockChanges); + + subject.commitTransaction(streamBuilder); + + verify(kvStateChangeListener).reset(); + verify(streamBuilder).stateChanges(mockChanges); + } + + @Test + void usesBlockStreamBuilderForChild() { + final var childBuilder = subject.addChildRecordBuilder(StreamBuilder.class, CONTRACT_CALL); + assertThat(childBuilder).isInstanceOf(BlockStreamBuilder.class); + } + + @Test + void usesBlockStreamBuilderForRemovableChild() { + final var childBuilder = subject.addRemovableChildRecordBuilder(StreamBuilder.class, CONTRACT_CALL); + assertThat(childBuilder).isInstanceOf(BlockStreamBuilder.class); + } + + @Test + void allCreatedBuildersAreBlockStreamBuilders() { + assertThat(subject.createIrreversiblePrecedingBuilder()).isInstanceOf(BlockStreamBuilder.class); + assertThat(subject.createRemovableChildBuilder()).isInstanceOf(BlockStreamBuilder.class); + assertThat(subject.createReversibleChildBuilder()).isInstanceOf(BlockStreamBuilder.class); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepointTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepointTest.java new file mode 100644 index 000000000000..a5d42c98b4e1 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/stack/savepoints/FirstRootSavepointTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.handle.stack.savepoints; + +import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; +import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.NOOP_RECORD_CUSTOMIZER; +import static com.hedera.node.app.spi.workflows.record.StreamBuilder.ReversingBehavior.REVERSIBLE; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.BDDMockito.given; + +import com.hedera.node.app.blocks.impl.BlockStreamBuilder; +import com.hedera.node.app.state.WrappedState; +import com.hedera.node.app.workflows.handle.stack.BuilderSink; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FirstRootSavepointTest { + @Mock + private WrappedState state; + + @Mock + private BuilderSink parentSink; + + private FirstRootSavepoint subject; + + @Test + void usesBlockStreamBuilderForBlocksStreamMode() { + givenSubjectWithCapacities(1, 2); + + final var builder = subject.createBuilder(REVERSIBLE, CHILD, NOOP_RECORD_CUSTOMIZER, BLOCKS, false); + + assertThat(builder).isInstanceOf(BlockStreamBuilder.class); + } + + private void givenSubjectWithCapacities(final int maxPreceding, final int maxFollowing) { + given(parentSink.precedingCapacity()).willReturn(maxPreceding); + given(parentSink.followingCapacity()).willReturn(maxFollowing); + subject = new FirstRootSavepoint(state, parentSink); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdatesTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdatesTest.java index 9511a22cba87..248f411d009f 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdatesTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/NodeStakeUpdatesTest.java @@ -18,6 +18,8 @@ import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static com.hedera.node.app.service.token.impl.handlers.staking.StakePeriodManager.DEFAULT_STAKING_PERIOD_MINS; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -33,6 +35,8 @@ import com.hedera.node.app.records.ReadableBlockRecordStore; import com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUpdater; import com.hedera.node.app.service.token.records.TokenContext; +import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.workflows.handle.Dispatch; import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; import com.hedera.node.config.data.StakingConfig; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; @@ -62,24 +66,30 @@ class NodeStakeUpdatesTest { @Mock private ReadableBlockRecordStore blockStore; + @Mock + private TssBaseService tssBaseService; + @Mock private SavepointStackImpl stack; + @Mock + private Dispatch dispatch; + private NodeStakeUpdates subject; @BeforeEach void setUp() { given(context.readableStore(ReadableBlockRecordStore.class)).willReturn(blockStore); - subject = new NodeStakeUpdates(stakingPeriodCalculator, exchangeRateManager); + subject = new NodeStakeUpdates(stakingPeriodCalculator, exchangeRateManager, tssBaseService); } @SuppressWarnings("DataFlowIssue") @Test void nullArgConstructor() { - Assertions.assertThatThrownBy(() -> new NodeStakeUpdates(null, exchangeRateManager)) + Assertions.assertThatThrownBy(() -> new NodeStakeUpdates(null, exchangeRateManager, tssBaseService)) .isInstanceOf(NullPointerException.class); - Assertions.assertThatThrownBy(() -> new NodeStakeUpdates(stakingPeriodCalculator, null)) + Assertions.assertThatThrownBy(() -> new NodeStakeUpdates(stakingPeriodCalculator, null, tssBaseService)) .isInstanceOf(NullPointerException.class); } @@ -92,8 +102,9 @@ void processUpdateSkippedForPreviousPeriod() { @Test void processUpdateCalledForGenesisTxn() { given(exchangeRateManager.exchangeRates()).willReturn(ExchangeRateSet.DEFAULT); + given(context.configuration()).willReturn(DEFAULT_CONFIG); - subject.process(stack, context, true); + subject.process(dispatch, stack, context, RECORDS, true, Instant.EPOCH); verify(stakingPeriodCalculator).updateNodes(context, ExchangeRateSet.DEFAULT); verify(exchangeRateManager).updateMidnightRates(stack); @@ -110,14 +121,14 @@ void processUpdateSkippedForPreviousConsensusTime() { .nanos(CONSENSUS_TIME_1234567.getNano())) .build()); - subject.process(stack, context, false); + subject.process(dispatch, stack, context, RECORDS, false, Instant.EPOCH); verifyNoInteractions(stakingPeriodCalculator); verifyNoInteractions(exchangeRateManager); } @Test - void processUpdateCalledForNextPeriod() { + void processUpdateCalledForNextPeriodWithRecordsStreamMode() { given(context.configuration()).willReturn(newPeriodMinsConfig()); // Use any number of seconds that gets isNextPeriod(...) to return true final var currentConsensusTime = CONSENSUS_TIME_1234567.plusSeconds(500_000); @@ -135,7 +146,29 @@ void processUpdateCalledForNextPeriod() { .isTrue(); given(exchangeRateManager.exchangeRates()).willReturn(ExchangeRateSet.DEFAULT); - subject.process(stack, context, false); + subject.process(dispatch, stack, context, RECORDS, false, Instant.EPOCH); + + verify(stakingPeriodCalculator) + .updateNodes( + argThat(stakingContext -> currentConsensusTime.equals(stakingContext.consensusTime())), + eq(ExchangeRateSet.DEFAULT)); + verify(exchangeRateManager).updateMidnightRates(stack); + } + + @Test + void processUpdateCalledForNextPeriodWithBlocksStreamMode() { + given(context.configuration()).willReturn(newPeriodMinsConfig()); + // Use any number of seconds that gets isNextPeriod(...) to return true + final var currentConsensusTime = CONSENSUS_TIME_1234567.plusSeconds(500_000); + given(context.consensusTime()).willReturn(currentConsensusTime); + + // Pre-condition check + Assertions.assertThat( + NodeStakeUpdates.isNextStakingPeriod(currentConsensusTime, CONSENSUS_TIME_1234567, context)) + .isTrue(); + given(exchangeRateManager.exchangeRates()).willReturn(ExchangeRateSet.DEFAULT); + + subject.process(dispatch, stack, context, BLOCKS, false, CONSENSUS_TIME_1234567); verify(stakingPeriodCalculator) .updateNodes( @@ -157,7 +190,8 @@ void processUpdateExceptionIsCaught() { given(context.consensusTime()).willReturn(CONSENSUS_TIME_1234567.plus(Duration.ofDays(2))); given(context.configuration()).willReturn(DEFAULT_CONFIG); - Assertions.assertThatNoException().isThrownBy(() -> subject.process(stack, context, false)); + Assertions.assertThatNoException() + .isThrownBy(() -> subject.process(dispatch, stack, context, RECORDS, false, Instant.EPOCH)); verify(stakingPeriodCalculator).updateNodes(context, ExchangeRateSet.DEFAULT); verify(exchangeRateManager).updateMidnightRates(stack); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java index daac35601aa8..cc86223d888c 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/SystemSetupTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.workflows.handle.steps; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_CREATE; import static com.hedera.node.app.service.file.impl.schemas.V0490FileSchema.parseFeeSchedules; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -39,7 +40,6 @@ import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.ExchangeRateSet; -import com.hedera.node.app.records.ReadableBlockRecordStore; import com.hedera.node.app.service.addressbook.ReadableNodeStore; import com.hedera.node.app.service.file.impl.FileServiceImpl; import com.hedera.node.app.service.file.impl.schemas.V0490FileSchema; @@ -103,9 +103,6 @@ class SystemSetupTest { @Mock(strictness = Mock.Strictness.LENIENT) private TokenContext context; - @Mock(strictness = Mock.Strictness.LENIENT) - private ReadableBlockRecordStore blockStore; - @Mock private SyntheticAccountCreator syntheticAccountCreator; @@ -144,13 +141,9 @@ class SystemSetupTest { @BeforeEach void setup() { - given(context.readableStore(ReadableBlockRecordStore.class)).willReturn(blockStore); given(context.consensusTime()).willReturn(CONSENSUS_NOW); - given(context.addPrecedingChildRecordBuilder(GenesisAccountStreamBuilder.class)) + given(context.addPrecedingChildRecordBuilder(GenesisAccountStreamBuilder.class, CRYPTO_CREATE)) .willReturn(genesisAccountRecordBuilder); - given(context.readableStore(ReadableBlockRecordStore.class)).willReturn(blockStore); - - given(blockStore.getLastBlockInfo()).willReturn(defaultStartupBlockInfo()); subject = new SystemSetup(fileService, syntheticAccountCreator); } @@ -159,6 +152,7 @@ void setup() { void successfulAutoUpdatesAreDispatchedWithFilesAvailable() throws IOException { final var config = HederaTestConfigBuilder.create() .withValue("networkAdmin.upgradeSysFilesLoc", tempDir.toString()) + .withValue("nodes.enableDAB", true) .getOrCreateConfig(); final var adminConfig = config.getConfigData(NetworkAdminConfig.class); Files.writeString(tempDir.resolve(adminConfig.upgradePropertyOverridesFile()), validPropertyOverrides()); @@ -169,10 +163,10 @@ void successfulAutoUpdatesAreDispatchedWithFilesAvailable() throws IOException { given(dispatch.config()).willReturn(config); given(dispatch.consensusNow()).willReturn(CONSENSUS_NOW); given(dispatch.handleContext()).willReturn(handleContext); - given(handleContext.storeFactory()).willReturn(storeFactory); - given(storeFactory.readableStore(ReadableNodeStore.class)).willReturn(readableNodeStore); given(handleContext.dispatchPrecedingTransaction(any(), any(), any(), any())) .willReturn(streamBuilder); + given(handleContext.storeFactory()).willReturn(storeFactory); + given(storeFactory.readableStore(ReadableNodeStore.class)).willReturn(readableNodeStore); subject.doPostUpgradeSetup(dispatch); @@ -189,6 +183,7 @@ void successfulAutoUpdatesAreDispatchedWithFilesAvailable() throws IOException { void onlyAddressBookAndNodeDetailsAutoUpdateIsDispatchedWithNoFilesAvailable() { final var config = HederaTestConfigBuilder.create() .withValue("networkAdmin.upgradeSysFilesLoc", tempDir.toString()) + .withValue("nodes.enableDAB", true) .getOrCreateConfig(); given(dispatch.stack()).willReturn(stack); given(dispatch.config()).willReturn(config); @@ -213,6 +208,7 @@ void onlyAddressBookAndNodeDetailsAutoUpdateIsDispatchedWithNoFilesAvailable() { void onlyAddressBookAndNodeDetailsAutoUpdateIsDispatchedWithInvalidFilesAvailable() throws IOException { final var config = HederaTestConfigBuilder.create() .withValue("networkAdmin.upgradeSysFilesLoc", tempDir.toString()) + .withValue("nodes.enableDAB", true) .getOrCreateConfig(); final var adminConfig = config.getConfigData(NetworkAdminConfig.class); Files.writeString(tempDir.resolve(adminConfig.upgradePropertyOverridesFile()), invalidPropertyOverrides()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/UserTxnTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/UserTxnTest.java new file mode 100644 index 000000000000..6209d98524e4 --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/steps/UserTxnTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.workflows.handle.steps; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_CREATE_TOPIC; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.ACCOUNTS_KEY; +import static com.hedera.node.app.workflows.handle.TransactionType.GENESIS_TRANSACTION; +import static com.hedera.node.config.types.StreamMode.BLOCKS; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.platform.event.EventTransaction; +import com.hedera.node.app.blocks.impl.BoundaryStateChangeListener; +import com.hedera.node.app.blocks.impl.KVStateChangeListener; +import com.hedera.node.app.fees.ExchangeRateManager; +import com.hedera.node.app.fees.FeeManager; +import com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl; +import com.hedera.node.app.service.token.api.FeeStreamBuilder; +import com.hedera.node.app.services.ServiceScopeLookup; +import com.hedera.node.app.spi.authorization.Authorizer; +import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.spi.records.BlockRecordInfo; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; +import com.hedera.node.app.store.ReadableStoreFactory; +import com.hedera.node.app.throttle.NetworkUtilizationManager; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.app.workflows.dispatcher.TransactionDispatcher; +import com.hedera.node.app.workflows.handle.DispatchProcessor; +import com.hedera.node.app.workflows.handle.dispatch.ChildDispatchFactory; +import com.hedera.node.app.workflows.handle.record.RecordStreamBuilder; +import com.hedera.node.app.workflows.prehandle.PreHandleResult; +import com.hedera.node.app.workflows.prehandle.PreHandleWorkflow; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfigImpl; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.config.api.Configuration; +import com.swirlds.platform.system.events.ConsensusEvent; +import com.swirlds.platform.system.transaction.ConsensusTransaction; +import com.swirlds.platform.system.transaction.TransactionWrapper; +import com.swirlds.state.State; +import com.swirlds.state.spi.WritableKVState; +import com.swirlds.state.spi.WritableStates; +import com.swirlds.state.spi.info.NetworkInfo; +import com.swirlds.state.spi.info.NodeInfo; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserTxnTest { + private static final long CONGESTION_MULTIPLIER = 2L; + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); + private static final ConsensusTransaction PLATFORM_TXN = new TransactionWrapper(EventTransaction.DEFAULT); + private static final AccountID PAYER_ID = + AccountID.newBuilder().accountNum(1234).build(); + private static final Key AN_ED25519_KEY = Key.newBuilder() + .ed25519(Bytes.fromHex("0101010101010101010101010101010101010101010101010101010101010101")) + .build(); + private static final Configuration BLOCKS_CONFIG = HederaTestConfigBuilder.create() + .withValue("blockStream.streamMode", "BLOCKS") + .getOrCreateConfig(); + + @Mock + private State state; + + @Mock + private ConsensusEvent event; + + @Mock + private NodeInfo creatorInfo; + + @Mock + private PreHandleResult preHandleResult; + + @Mock + private PreHandleWorkflow preHandleWorkflow; + + @Mock + private TransactionInfo txnInfo; + + @Mock + private ConfigProvider configProvider; + + @Mock + private StoreMetricsService storeMetricsService; + + @Mock + private KVStateChangeListener kvStateChangeListener; + + @Mock + private BoundaryStateChangeListener boundaryStateChangeListener; + + @Mock + private Authorizer authorizer; + + @Mock + private NetworkInfo networkInfo; + + @Mock + private FeeManager feeManager; + + @Mock + private DispatchProcessor dispatchProcessor; + + @Mock + private BlockRecordInfo blockRecordInfo; + + @Mock + private ServiceScopeLookup serviceScopeLookup; + + @Mock + private ExchangeRateManager exchangeRateManager; + + @Mock + private ChildDispatchFactory childDispatchFactory; + + @Mock + private TransactionDispatcher dispatcher; + + @Mock + private NetworkUtilizationManager networkUtilizationManager; + + @Mock + private WritableStates writableStates; + + @Mock + private WritableKVState accountState; + + private StreamBuilder baseBuilder; + + @BeforeEach + void setUp() { + given(preHandleWorkflow.getCurrentPreHandleResult( + eq(creatorInfo), eq(PLATFORM_TXN), any(ReadableStoreFactory.class))) + .willReturn(preHandleResult); + given(preHandleResult.txInfo()).willReturn(txnInfo); + given(txnInfo.functionality()).willReturn(CONSENSUS_CREATE_TOPIC); + } + + @Test + void usesRecordStreamBuilderWithDefaultConfig() { + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1)); + + final var subject = UserTxn.from( + state, + event, + creatorInfo, + PLATFORM_TXN, + CONSENSUS_NOW, + GENESIS_TRANSACTION, + configProvider, + storeMetricsService, + kvStateChangeListener, + boundaryStateChangeListener, + preHandleWorkflow); + + assertSame(GENESIS_TRANSACTION, subject.type()); + assertSame(CONSENSUS_CREATE_TOPIC, subject.functionality()); + assertSame(CONSENSUS_NOW, subject.consensusNow()); + assertSame(state, subject.state()); + assertSame(event, subject.event()); + assertSame(PLATFORM_TXN, subject.platformTxn()); + assertSame(txnInfo, subject.txnInfo()); + assertSame(preHandleResult, subject.preHandleResult()); + assertSame(creatorInfo, subject.creatorInfo()); + assertNotNull(subject.tokenContextImpl()); + assertNotNull(subject.stack()); + assertNotNull(subject.readableStoreFactory()); + assertNotNull(subject.config()); + + assertThat(subject.baseBuilder()).isInstanceOf(RecordStreamBuilder.class); + } + + @Test + void constructsDispatchAsExpectedWithCongestionMultiplierGreaterThanOne() { + baseBuilder = Mockito.mock(StreamBuilder.class, withSettings().extraInterfaces(FeeStreamBuilder.class)); + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(BLOCKS_CONFIG, 1)); + given(txnInfo.payerID()).willReturn(PAYER_ID); + given(txnInfo.txBody()).willReturn(TransactionBody.DEFAULT); + given(txnInfo.signatureMap()).willReturn(SignatureMap.DEFAULT); + given(preHandleResult.payerKey()).willReturn(AN_ED25519_KEY); + given(preHandleResult.getVerificationResults()).willReturn(emptyMap()); + given(feeManager.congestionMultiplierFor( + eq(TransactionBody.DEFAULT), eq(CONSENSUS_CREATE_TOPIC), any(ReadableStoreFactory.class))) + .willReturn(CONGESTION_MULTIPLIER); + given(serviceScopeLookup.getServiceName(TransactionBody.DEFAULT)).willReturn(ConsensusServiceImpl.NAME); + given(state.getWritableStates(any())).willReturn(writableStates); + given(writableStates.get(ACCOUNTS_KEY)).willReturn(accountState); + given(accountState.getStateKey()).willReturn(ACCOUNTS_KEY); + given(dispatcher.dispatchComputeFees(any())).willReturn(Fees.FREE); + + final var subject = UserTxn.from( + state, + event, + creatorInfo, + PLATFORM_TXN, + CONSENSUS_NOW, + GENESIS_TRANSACTION, + configProvider, + storeMetricsService, + kvStateChangeListener, + boundaryStateChangeListener, + preHandleWorkflow); + + final var dispatch = subject.newDispatch( + authorizer, + networkInfo, + feeManager, + dispatchProcessor, + blockRecordInfo, + serviceScopeLookup, + storeMetricsService, + exchangeRateManager, + childDispatchFactory, + dispatcher, + networkUtilizationManager, + baseBuilder, + BLOCKS); + + assertSame(PAYER_ID, dispatch.payerId()); + verify(baseBuilder).congestionMultiplier(CONGESTION_MULTIPLIER); + } + + @Test + void constructsDispatchAsExpectedWithCongestionMultiplierEqualToOne() { + baseBuilder = Mockito.mock(StreamBuilder.class, withSettings().extraInterfaces(FeeStreamBuilder.class)); + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(BLOCKS_CONFIG, 1)); + given(txnInfo.payerID()).willReturn(PAYER_ID); + given(txnInfo.txBody()).willReturn(TransactionBody.DEFAULT); + given(txnInfo.signatureMap()).willReturn(SignatureMap.DEFAULT); + given(preHandleResult.getVerificationResults()).willReturn(emptyMap()); + given(feeManager.congestionMultiplierFor( + eq(TransactionBody.DEFAULT), eq(CONSENSUS_CREATE_TOPIC), any(ReadableStoreFactory.class))) + .willReturn(1L); + given(serviceScopeLookup.getServiceName(TransactionBody.DEFAULT)).willReturn(ConsensusServiceImpl.NAME); + given(state.getWritableStates(any())).willReturn(writableStates); + given(writableStates.get(ACCOUNTS_KEY)).willReturn(accountState); + given(accountState.getStateKey()).willReturn(ACCOUNTS_KEY); + given(dispatcher.dispatchComputeFees(any())).willReturn(Fees.FREE); + + final var subject = UserTxn.from( + state, + event, + creatorInfo, + PLATFORM_TXN, + CONSENSUS_NOW, + GENESIS_TRANSACTION, + configProvider, + storeMetricsService, + kvStateChangeListener, + boundaryStateChangeListener, + preHandleWorkflow); + + final var dispatch = subject.newDispatch( + authorizer, + networkInfo, + feeManager, + dispatchProcessor, + blockRecordInfo, + serviceScopeLookup, + storeMetricsService, + exchangeRateManager, + childDispatchFactory, + dispatcher, + networkUtilizationManager, + baseBuilder, + BLOCKS); + + assertSame(PAYER_ID, dispatch.payerId()); + verify(baseBuilder, never()).congestionMultiplier(1); + } +} diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManagerTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManagerTest.java index 2472fd3ae76b..a961d88d28cb 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManagerTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/throttle/DispatchUsageManagerTest.java @@ -53,9 +53,9 @@ import com.hedera.node.app.throttle.CongestionThrottleService; import com.hedera.node.app.throttle.NetworkUtilizationManager; import com.hedera.node.app.throttle.ThrottleServiceManager; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.TransactionInfo; import com.hedera.node.app.workflows.handle.Dispatch; -import com.hedera.node.app.workflows.handle.metric.HandleWorkflowMetrics; import com.hedera.node.app.workflows.handle.record.RecordStreamBuilder; import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; @@ -147,7 +147,7 @@ class DispatchUsageManagerTest { private RecordStreamBuilder recordBuilder; @Mock - private HandleWorkflowMetrics handleWorkflowMetrics; + private OpWorkflowMetrics opWorkflowMetrics; @Mock private ThrottleServiceManager throttleServiceManager; @@ -166,7 +166,7 @@ class DispatchUsageManagerTest { @BeforeEach void setUp() { subject = new DispatchUsageManager( - networkInfo, handleWorkflowMetrics, throttleServiceManager, networkUtilizationManager); + networkInfo, opWorkflowMetrics, throttleServiceManager, networkUtilizationManager); } @Test @@ -245,7 +245,7 @@ void onlySnapshotsForNonAutoCreatingOperations() { subject.finalizeAndSaveUsage(dispatch); verify(throttleServiceManager).saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); - verifyNoInteractions(handleWorkflowMetrics); + verifyNoInteractions(opWorkflowMetrics); } @Test @@ -260,7 +260,7 @@ void leaksUnusedGasForContractCall() { subject.finalizeAndSaveUsage(dispatch); - verify(handleWorkflowMetrics).addGasUsed(GAS_USED); + verify(opWorkflowMetrics).addGasUsed(GAS_USED); verify(networkUtilizationManager).leakUnusedGasPreviouslyReserved(CONTRACT_CALL_TXN_INFO, GAS_LIMIT - GAS_USED); verify(throttleServiceManager).saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); } @@ -277,7 +277,7 @@ void leaksUnusedGasForContractCreate() { subject.finalizeAndSaveUsage(dispatch); - verify(handleWorkflowMetrics).addGasUsed(GAS_USED); + verify(opWorkflowMetrics).addGasUsed(GAS_USED); verify(networkUtilizationManager) .leakUnusedGasPreviouslyReserved(CONTRACT_CREATE_TXN_INFO, GAS_LIMIT - GAS_USED); verify(throttleServiceManager).saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); @@ -297,7 +297,7 @@ void leaksUnusedGasForEthTx() { subject.finalizeAndSaveUsage(dispatch); - verify(handleWorkflowMetrics).addGasUsed(GAS_USED); + verify(opWorkflowMetrics).addGasUsed(GAS_USED); verify(networkUtilizationManager) .leakUnusedGasPreviouslyReserved(ETH_TXN_INFO, ETH_DATA_WITH_TO_ADDRESS.gasLimit() - GAS_USED); verify(throttleServiceManager).saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); @@ -312,7 +312,7 @@ void doesNotLeakUnusedGasForContractOperationWithoutResult() { subject.finalizeAndSaveUsage(dispatch); - verify(handleWorkflowMetrics, never()).addGasUsed(GAS_USED); + verify(opWorkflowMetrics, never()).addGasUsed(GAS_USED); verify(networkUtilizationManager, never()).leakUnusedGasPreviouslyReserved(any(), anyLong()); verify(throttleServiceManager).saveThrottleSnapshotsAndCongestionLevelStartsTo(stack); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImplTest.java index f381cba8bd05..eba0292e00b3 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextImplTest.java @@ -95,14 +95,14 @@ class PreHandleContextImplTest implements Scenarios { void setup() throws PreCheckException { given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); given(accountStore.getAccountById(PAYER)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); final var txn = createAccountTransaction(); subject = new PreHandleContextImpl(storeFactory, txn, configuration, dispatcher); } @Test - void gettersWork() throws PreCheckException { + void gettersWork() { subject.requireKey(otherKey); assertThat(subject.body()).isEqualTo(createAccountTransaction()); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextListUpdatesTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextListUpdatesTest.java index 3c5c840bfc40..015ae6cfed6e 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextListUpdatesTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/prehandle/PreHandleContextListUpdatesTest.java @@ -53,7 +53,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -@SuppressWarnings("removal") @ExtendWith(MockitoExtension.class) class PreHandleContextListUpdatesTest { @@ -121,7 +120,7 @@ class PreHandleContextListUpdatesTest { void gettersWorkAsExpectedWhenOnlyPayerKeyExist() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); final var txn = createAccountTransaction(); @@ -139,7 +138,7 @@ void gettersWorkAsExpectedWhenOnlyPayerKeyExist() throws PreCheckException { void nullInputToBuilderArgumentsThrows() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); // When we create a PreHandleContext by passing null as either argument @@ -173,7 +172,7 @@ void nullInputToBuilderArgumentsThrows() throws PreCheckException { void requireSomeOtherKey() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer, and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -188,7 +187,7 @@ void requireSomeOtherKey() throws PreCheckException { void requireSomeOtherKeyTwice() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer, and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -204,7 +203,7 @@ void requireSomeOtherKeyTwice() throws PreCheckException { void payerIsIgnoredWhenRequired() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer, and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -231,7 +230,7 @@ void failsWhenPayerKeyDoesntExist() throws PreCheckException { void returnsIfGivenKeyIsPayer() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -251,7 +250,7 @@ void returnsIfGivenKeyIsPayer() throws PreCheckException { void returnsIfGivenKeyIsInvalidAccountId() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -265,7 +264,7 @@ void addsContractIdKey() throws PreCheckException { // Given an account with a key, and a transaction using that account as the payer, // and a contract account with a key, and a PreHandleContext given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(accountStore.getContractById(otherContractId)).willReturn(contractAccount); given(contractAccount.key()).willReturn(contractIdKey); given(contractAccount.keyOrElse(EMPTY_KEY_LIST)).willReturn(contractIdKey); @@ -286,7 +285,7 @@ void doesntFailForAliasedAccount() throws PreCheckException { final var alias = AccountID.newBuilder().alias(Bytes.wrap("test")).build(); given(accountStore.getAccountById(alias)).willReturn(account); given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); given(account.accountIdOrThrow()).willReturn(payer); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); @@ -307,7 +306,7 @@ void doesntFailForAliasedContract() throws PreCheckException { given(contractAccount.keyOrElse(EMPTY_KEY_LIST)).willReturn(otherKey); given(contractAccount.accountIdOrThrow()).willReturn(asAccount(otherContractId.contractNum())); given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher) @@ -322,7 +321,7 @@ void failsForInvalidAlias() throws PreCheckException { final var alias = AccountID.newBuilder().alias(Bytes.wrap("test")).build(); given(accountStore.getAccountById(alias)).willReturn(null); given(accountStore.getAccountById(payer)).willReturn(account); - given(account.key()).willReturn(payerKey); + given(account.keyOrThrow()).willReturn(payerKey); given(storeFactory.getStore(ReadableAccountStore.class)).willReturn(accountStore); subject = new PreHandleContextImpl(storeFactory, createAccountTransaction(), CONFIG, dispatcher); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/query/QueryWorkflowImplTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/query/QueryWorkflowImplTest.java index 9693407501a2..78bc6e27eb7d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/query/QueryWorkflowImplTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/query/QueryWorkflowImplTest.java @@ -18,6 +18,7 @@ import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; import static com.hedera.hapi.node.base.HederaFunctionality.FILE_GET_INFO; +import static com.hedera.hapi.node.base.HederaFunctionality.NETWORK_GET_EXECUTION_TIME; import static com.hedera.hapi.node.base.ResponseCodeEnum.BUSY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NODE_ACCOUNT; @@ -32,6 +33,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.BDDMockito.given; @@ -70,6 +72,7 @@ import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.QueryContext; import com.hedera.node.app.throttle.SynchronizedThrottleAccumulator; +import com.hedera.node.app.workflows.OpWorkflowMetrics; import com.hedera.node.app.workflows.TransactionInfo; import com.hedera.node.app.workflows.ingest.IngestChecker; import com.hedera.node.app.workflows.ingest.SubmissionManager; @@ -92,6 +95,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -144,6 +149,9 @@ class QueryWorkflowImplTest extends AppTestBase { @Mock(strictness = LENIENT) private SynchronizedThrottleAccumulator synchronizedThrottleAccumulator; + @Mock + private OpWorkflowMetrics opWorkflowMetrics; + private VersionedConfiguration configuration; private Transaction payment; private TransactionBody txBody; @@ -211,7 +219,9 @@ void setup(@Mock FeeCalculator feeCalculator) throws ParseException, PreCheckExc exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource); + instantSource, + opWorkflowMetrics, + true); } @SuppressWarnings("ConstantConditions") @@ -230,7 +240,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -245,7 +257,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -260,7 +274,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -275,7 +291,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -290,7 +308,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -305,7 +325,9 @@ void testConstructorWithIllegalParameters() { exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -313,14 +335,16 @@ void testConstructorWithIllegalParameters() { queryChecker, ingestChecker, dispatcher, + queryParser, null, - configProvider, recordCache, authorizer, exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -329,13 +353,15 @@ void testConstructorWithIllegalParameters() { ingestChecker, dispatcher, queryParser, + configProvider, null, - recordCache, authorizer, exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -345,12 +371,14 @@ void testConstructorWithIllegalParameters() { dispatcher, queryParser, configProvider, + recordCache, null, - authorizer, exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -361,11 +389,13 @@ void testConstructorWithIllegalParameters() { queryParser, configProvider, recordCache, + authorizer, null, - exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + instantSource, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -377,10 +407,46 @@ void testConstructorWithIllegalParameters() { configProvider, recordCache, authorizer, + exchangeRateManager, + null, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + true)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, null, + instantSource, + opWorkflowMetrics, + true)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, feeManager, synchronizedThrottleAccumulator, - instantSource)) + null, + opWorkflowMetrics, + true)) .isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> new QueryWorkflowImpl( stateAccessor, @@ -394,8 +460,10 @@ void testConstructorWithIllegalParameters() { authorizer, exchangeRateManager, feeManager, + synchronizedThrottleAccumulator, + instantSource, null, - instantSource)) + true)) .isInstanceOf(NullPointerException.class); } @@ -411,9 +479,26 @@ void testHandleQueryWithIllegalParameters() { assertThatThrownBy(() -> workflow.handleQuery(requestBuffer, null)).isInstanceOf(NullPointerException.class); } - @Test - void testSuccessIfPaymentNotRequired() throws ParseException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSuccessIfPaymentNotRequired(boolean shouldCharge) throws ParseException { // given + workflow = new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + shouldCharge); final var responseBuffer = newEmptyBuffer(); // when workflow.handleQuery(requestBuffer, responseBuffer); @@ -424,11 +509,29 @@ void testSuccessIfPaymentNotRequired() throws ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(OK); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } - @Test - void testSuccessIfPaymentRequired() throws ParseException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSuccessIfPaymentRequired(boolean shouldCharge) throws ParseException { // given + workflow = new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + shouldCharge); given(handler.computeFees(any(QueryContext.class))).willReturn(new Fees(100L, 0L, 100L)); given(handler.requiresNodePayment(any())).willReturn(true); when(handler.findResponse(any(), any())) @@ -448,6 +551,7 @@ void testSuccessIfPaymentRequired() throws ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(OK); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -480,6 +584,7 @@ void testSuccessIfPaymentRequiredAndNotProvided() throws ParseException, PreChec assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(INSUFFICIENT_TX_FEE); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -516,6 +621,7 @@ void testSuccessIfCostOnly() throws ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(OK); assertThat(header.responseType()).isEqualTo(COST_ANSWER); assertThat(header.cost()).isEqualTo(fees.totalFee()); + verifyMetricsSent(); } @Test @@ -570,6 +676,7 @@ void testInvalidNodeFails() throws PreCheckException, ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(INVALID_NODE_ACCOUNT); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -597,6 +704,7 @@ void testUnsupportedResponseTypeFails() throws ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(NOT_SUPPORTED); assertThat(header.responseType()).isEqualTo(ANSWER_STATE_PROOF); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -615,6 +723,41 @@ void testThrottleFails() throws ParseException { assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(BUSY); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); + } + + @Test + void testThrottleDoesNotFailWhenWorkflowShouldNotCharge() throws ParseException { + // given + workflow = new QueryWorkflowImpl( + stateAccessor, + submissionManager, + queryChecker, + ingestChecker, + dispatcher, + queryParser, + configProvider, + recordCache, + authorizer, + exchangeRateManager, + feeManager, + synchronizedThrottleAccumulator, + instantSource, + opWorkflowMetrics, + false); + when(synchronizedThrottleAccumulator.shouldThrottle(eq(HederaFunctionality.FILE_GET_INFO), any(), any())) + .thenReturn(true); + final var responseBuffer = newEmptyBuffer(); + + // when + workflow.handleQuery(requestBuffer, responseBuffer); + + // then + final var response = parseResponse(responseBuffer); + final var header = response.fileGetInfoOrThrow().headerOrThrow(); + assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(OK); + assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); + assertThat(header.cost()).isZero(); } @Test @@ -635,6 +778,7 @@ void testPaidQueryWithInvalidTransactionFails() throws PreCheckException, ParseE assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(INVALID_TRANSACTION_BODY); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -653,6 +797,7 @@ void testPaidQueryWithInvalidCryptoTransferFails() throws PreCheckException, Par assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(INSUFFICIENT_TX_FEE); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -680,6 +825,7 @@ void testPaidQueryForSuperUserDoesNotSubmitCryptoTransfer() throws PreCheckExcep assertThat(header.cost()).isZero(); verify(submissionManager, never()).submit(any(), any()); + verifyMetricsSent(); } @Test @@ -700,6 +846,7 @@ void testPaidQueryWithInsufficientPermissionFails() throws PreCheckException, Pa assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(NOT_SUPPORTED); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); } @Test @@ -724,6 +871,7 @@ void testPaidQueryWithInsufficientBalanceFails() throws PreCheckException, Parse assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(INSUFFICIENT_TX_FEE); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isEqualTo(12345L); + verifyMetricsSent(); } @Test @@ -763,6 +911,7 @@ void testUnpaidQueryWithRestrictedFunctionalityFails(@Mock NetworkGetExecutionTi assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(NOT_SUPPORTED); assertThat(header.responseType()).isEqualTo(COST_ANSWER); assertThat(header.cost()).isZero(); + verify(opWorkflowMetrics).updateDuration(eq(NETWORK_GET_EXECUTION_TIME), anyInt()); } @Test @@ -785,6 +934,7 @@ void testQuerySpecificValidationFails() throws PreCheckException, ParseException assertThat(header.cost()).isZero(); final var queryContext = captor.getValue(); assertThat(queryContext.payer()).isNull(); + verifyMetricsSent(); } @Test @@ -806,6 +956,11 @@ void testPaidQueryWithFailingSubmissionFails() throws PreCheckException, ParseEx assertThat(header.nodeTransactionPrecheckCode()).isEqualTo(PLATFORM_TRANSACTION_NOT_CREATED); assertThat(header.responseType()).isEqualTo(ANSWER_ONLY); assertThat(header.cost()).isZero(); + verifyMetricsSent(); + } + + private void verifyMetricsSent() { + verify(opWorkflowMetrics).updateDuration(eq(FILE_GET_INFO), anyInt()); } private static Response parseResponse(BufferedData responseBuffer) throws ParseException { diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java index 48549ab406e1..6b8dda2cadf8 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.standalone; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; import static com.hedera.node.app.util.FileUtilities.createFileID; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.TRANSACTION_EXECUTORS; @@ -272,7 +273,8 @@ private void registerServices(@NonNull final ServicesRegistry servicesRegistry) Set.of( new EntityIdService(), new ConsensusServiceImpl(), - new ContractServiceImpl(new AppContextImpl(InstantSource.system(), signatureVerifier)), + new ContractServiceImpl( + new AppContextImpl(InstantSource.system(), signatureVerifier, UNAVAILABLE_GOSSIP)), new FileServiceImpl(), new FreezeServiceImpl(), new ScheduleServiceImpl(), @@ -328,7 +330,7 @@ public NodeInfo nodeInfo(final long nodeId) { @Override public boolean containsNode(final long nodeId) { - return addressBook.contains(new NodeId(nodeId)); + return addressBook.contains(NodeId.of(nodeId)); } @Override diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java index 714108232234..4c0f66ccaf77 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/AppTestBase.java @@ -136,7 +136,7 @@ public WritableStates getWritableStates(@NonNull String serviceName) { private final SemanticVersion hapiVersion = SemanticVersion.newBuilder().major(1).minor(2).patch(3).build(); /** Represents "this node" in our tests. */ - protected final NodeId nodeSelfId = new NodeId(7); + protected final NodeId nodeSelfId = NodeId.of(7); /** The AccountID of "this node" in our tests. */ protected final AccountID nodeSelfAccountId = AccountID.newBuilder().shardNum(0).realmNum(0).accountNum(8).build(); @@ -232,7 +232,6 @@ public void commit() { } public static final class TestAppBuilder { - private SemanticVersion softwareVersion = CURRENT_VERSION; private SemanticVersion hapiVersion = CURRENT_VERSION; private Set services = new LinkedHashSet<>(); private TestConfigBuilder configBuilder = HederaTestConfigBuilder.create(); @@ -259,11 +258,6 @@ public TestAppBuilder withHapiVersion(@NonNull final SemanticVersion version) { return this; } - public TestAppBuilder withSoftwareVersion(@NonNull final SemanticVersion version) { - this.softwareVersion = version; - return this; - } - public TestAppBuilder withConfigSource(@NonNull final ConfigSource source) { configBuilder.withSource(source); return this; @@ -337,7 +331,7 @@ public App build() { final ConfigProvider configProvider = () -> new VersionedConfigImpl(configBuilder.getOrCreateConfig(), 1); final var addresses = nodes.stream() .map(nodeInfo -> new Address() - .copySetNodeId(new NodeId(nodeInfo.nodeId())) + .copySetNodeId(NodeId.of(nodeInfo.nodeId())) .copySetWeight(nodeInfo.zeroStake() ? 0 : 10)) .toList(); final var addressBook = new AddressBook(addresses); diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java index 6a889ae002eb..578131b40c01 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakePlatform.java @@ -57,7 +57,7 @@ public final class FakePlatform implements Platform { * Constructor for Embedded Hedera that uses a single node network */ public FakePlatform() { - this.selfNodeId = new NodeId(0L); + this.selfNodeId = NodeId.of(0L); final var addressBuilder = RandomAddressBuilder.create(random); final var address = addressBuilder.withNodeId(selfNodeId).withWeight(500L).build(); @@ -73,7 +73,7 @@ public FakePlatform() { * @param addresses the address book */ public FakePlatform(final long nodeId, final AddressBook addresses) { - this.selfNodeId = new NodeId(nodeId); + this.selfNodeId = NodeId.of(nodeId); this.addressBook = addresses; this.context = createPlatformContext(); this.notificationEngine = NotificationEngine.buildEngine(getStaticThreadManager()); diff --git a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java index 90073caddc54..b4712d57f604 100644 --- a/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java +++ b/hedera-node/hedera-app/src/testFixtures/java/com/hedera/node/app/fixtures/state/FakeSchemaRegistry.java @@ -16,6 +16,7 @@ package com.hedera.node.app.fixtures.state; +import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; import static com.hedera.node.app.state.merkle.SchemaApplicationType.MIGRATION; import static com.hedera.node.app.state.merkle.SchemaApplicationType.RESTART; import static com.hedera.node.app.state.merkle.SchemaApplicationType.STATE_DEFINITIONS; @@ -29,7 +30,6 @@ import com.hedera.node.app.spi.state.FilteredWritableStates; import com.hedera.node.app.state.merkle.SchemaApplications; import com.swirlds.config.api.Configuration; -import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.state.spi.MigrationContext; import com.swirlds.state.spi.ReadableStates; import com.swirlds.state.spi.Schema; @@ -69,14 +69,7 @@ public SchemaRegistry register(@NonNull final Schema schema) { @SuppressWarnings("rawtypes") public void migrate( @NonNull final String serviceName, @NonNull final FakeState state, @NonNull final NetworkInfo networkInfo) { - migrate( - serviceName, - state, - CURRENT_VERSION, - networkInfo, - ConfigurationBuilder.create().build(), - new HashMap<>(), - new AtomicLong()); + migrate(serviceName, state, CURRENT_VERSION, networkInfo, DEFAULT_CONFIG, new HashMap<>(), new AtomicLong()); } public void migrate( diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java index 35e6c29ba2d5..3d00f6119f12 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java @@ -260,7 +260,7 @@ public record ApiPermissionConfig( @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenCancelAirdrop, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange tokenClaimAirdrop, @ConfigProperty(defaultValue = "2-55") PermissionedAccountsRange createNode, - @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange updateNode, + @ConfigProperty(defaultValue = "2-55") PermissionedAccountsRange updateNode, @ConfigProperty(defaultValue = "2-55") PermissionedAccountsRange deleteNode, @ConfigProperty(defaultValue = "0-0") PermissionedAccountsRange tssMessage, @ConfigProperty(defaultValue = "0-0") PermissionedAccountsRange tssVote) { diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockStreamConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockStreamConfig.java index c10a23ef0ecc..b536e39b6749 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockStreamConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/BlockStreamConfig.java @@ -16,8 +16,6 @@ package com.hedera.node.config.data; -import static com.hedera.node.config.types.StreamMode.BOTH; - import com.hedera.node.config.NetworkProperty; import com.hedera.node.config.NodeProperty; import com.hedera.node.config.types.BlockStreamWriterMode; @@ -38,12 +36,6 @@ public record BlockStreamConfig( @ConfigProperty(defaultValue = "FILE") @NodeProperty BlockStreamWriterMode writerMode, @ConfigProperty(defaultValue = "data/block-streams") @NodeProperty String blockFileDir, @ConfigProperty(defaultValue = "true") @NetworkProperty boolean compressFilesOnCreation, - @ConfigProperty(defaultValue = "1") @NetworkProperty int roundsPerBlock) { - public boolean streamBlocks() { - return streamMode == BOTH; - } - - public boolean streamRecords() { - return true; - } -} + @ConfigProperty(defaultValue = "32") @NetworkProperty int serializationBatchSize, + @ConfigProperty(defaultValue = "32") @NetworkProperty int hashCombineBatchSize, + @ConfigProperty(defaultValue = "1") @NetworkProperty int roundsPerBlock) {} diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/GrpcConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/GrpcConfig.java index 8a10f238a16b..2160f2d21fe7 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/GrpcConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/GrpcConfig.java @@ -29,6 +29,11 @@ * @param tlsPort The port for tls-encrypted grpc traffic. Must be non-negative. A value of 0 indicates an ephemeral * port should be automatically selected by the computer. Must not be the same value as {@link #port()} * unless both are 0. Must be a value between 0 and 65535, inclusive. + * @param nodeOperatorPortEnabled Whether the node operator port is enabled. If true, the node operator port will be + * enabled and must be a non-negative value between 1 and 65535, inclusive. If false, the + * node operator port will be disabled and the value of {@link #nodeOperatorPort()} will be + * ignored. + * @param nodeOperatorPort The port for the node operator. Must be a non-negative value between 1 and 65535, inclusive. * @param workflowsPort Deprecated * @param workflowsTlsPort Deprecated * @param maxMessageSize The maximum message size in bytes that the server can receive. Must be non-negative. @@ -41,6 +46,8 @@ public record GrpcConfig( @ConfigProperty(defaultValue = "50211") @Min(0) @Max(65535) @NodeProperty int port, @ConfigProperty(defaultValue = "50212") @Min(0) @Max(65535) @NodeProperty int tlsPort, + @ConfigProperty(defaultValue = "false") @NodeProperty boolean nodeOperatorPortEnabled, + @ConfigProperty(defaultValue = "50213") @Min(1) @Max(65535) @NodeProperty int nodeOperatorPort, @ConfigProperty(defaultValue = "60211") @Min(0) @Max(65535) @NodeProperty int workflowsPort, @ConfigProperty(defaultValue = "60212") @Min(0) @Max(65535) @NodeProperty int workflowsTlsPort, @ConfigProperty(defaultValue = "4194304") @Max(4194304) @Min(0) int maxMessageSize, @@ -76,5 +83,10 @@ private void validateUniqueWorkflowsPorts(int port1, int port2) { if (port1 == port2 && port1 != 0) { throw new IllegalArgumentException("grpc.workflowsPort and grpc.workflowsTlsPort must be different"); } + + if (nodeOperatorPortEnabled && (nodeOperatorPort == port || nodeOperatorPort == tlsPort)) { + throw new IllegalArgumentException( + "grpc.nodeOperatorPort must be different from grpc.port and grpc.tlsPort"); + } } } diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/NetworkAdminConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/NetworkAdminConfig.java index e4858845d63e..849b0ec32136 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/NetworkAdminConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/NetworkAdminConfig.java @@ -16,7 +16,9 @@ package com.hedera.node.config.data; +import com.hedera.node.config.NetworkProperty; import com.hedera.node.config.NodeProperty; +import com.hedera.node.config.types.HederaFunctionalitySet; import com.swirlds.config.api.ConfigData; import com.swirlds.config.api.ConfigProperty; @@ -40,4 +42,6 @@ public record NetworkAdminConfig( @ConfigProperty(defaultValue = "feeSchedules.json") String upgradeFeeSchedulesFile, @ConfigProperty(defaultValue = "throttles.json") String upgradeThrottlesFile, @ConfigProperty(defaultValue = "application-override.properties") String upgradePropertyOverridesFile, - @ConfigProperty(defaultValue = "api-permission-override.properties") String upgradePermissionOverridesFile) {} + @ConfigProperty(defaultValue = "api-permission-override.properties") String upgradePermissionOverridesFile, + @ConfigProperty(defaultValue = "TssMessage,TssVote") @NetworkProperty + HederaFunctionalitySet nodeTransactionsAllowList) {} diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/StakingConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/StakingConfig.java index c885e2fecffd..b2b907d988cf 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/StakingConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/StakingConfig.java @@ -34,6 +34,8 @@ public record StakingConfig( // @ConfigProperty(defaultValue = "") Map nodeMaxToMinStakeRatios, @ConfigProperty(defaultValue = "true") @NetworkProperty boolean isEnabled, @ConfigProperty(defaultValue = "false") @NetworkProperty boolean requireMinStakeToReward, + // Assume there should have been no skipped staking periods + @ConfigProperty(defaultValue = "true") @NetworkProperty boolean assumeContiguousPeriods, // Can be renamed to just "rewardRate" when the "staking.rewardRate" property is removed // from all production 0.0.121 system files @ConfigProperty(defaultValue = "6849") @NetworkProperty long perHbarRewardRate, diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java index c2d050add823..4dee3429b2df 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TssConfig.java @@ -19,14 +19,21 @@ import com.hedera.node.config.NetworkProperty; import com.swirlds.config.api.ConfigData; import com.swirlds.config.api.ConfigProperty; +import java.time.Duration; /** * Configuration for the TSS service. * @param maxSharesPerNode The maximum number of shares that can be assigned to a node. + * @param timesToTrySubmission The number of times to retry a submission on getting an {@link IllegalStateException} + * @param distinctTxnIdsToTry The number of distinct transaction IDs to try in the event of a duplicate id * @param keyActiveRoster A test-only configuration; set this to true to enable the process that will key the candidate roster with TSS key material. */ @ConfigData("tss") public record TssConfig( @ConfigProperty(defaultValue = "3") @NetworkProperty long maxSharesPerNode, + @ConfigProperty(defaultValue = "5") @NetworkProperty int timesToTrySubmission, + @ConfigProperty(defaultValue = "5s") @NetworkProperty Duration retryDelay, + @ConfigProperty(defaultValue = "10") @NetworkProperty int distinctTxnIdsToTry, + @ConfigProperty(defaultValue = "false") @NetworkProperty boolean keyCandidateRoster, @ConfigProperty(defaultValue = "false") @NetworkProperty boolean keyActiveRoster, @ConfigProperty(defaultValue = "false") @NetworkProperty boolean enableLedgerId) {} diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/types/StreamMode.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/types/StreamMode.java index 5356246b8497..1227019e5609 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/types/StreamMode.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/types/StreamMode.java @@ -25,6 +25,10 @@ public enum StreamMode { * Stream records only. */ RECORDS, + /** + * Stream blocks only. + */ + BLOCKS, /** * Stream both blocks and records. */ diff --git a/hedera-node/hedera-config/src/test/java/com/hedera/node/config/data/GrpcConfigTest.java b/hedera-node/hedera-config/src/test/java/com/hedera/node/config/data/GrpcConfigTest.java index 6cfb3180b354..3dcd0c42aed7 100644 --- a/hedera-node/hedera-config/src/test/java/com/hedera/node/config/data/GrpcConfigTest.java +++ b/hedera-node/hedera-config/src/test/java/com/hedera/node/config/data/GrpcConfigTest.java @@ -26,11 +26,13 @@ class GrpcConfigTest { @Test void testValidConfiguration() { // Test valid configuration - GrpcConfig config = new GrpcConfig(50211, 50212, 60211, 60212, 4194304, 4194304, 4194304); + GrpcConfig config = new GrpcConfig(50211, 50212, true, 50213, 60211, 60212, 4194304, 4194304, 4194304); assertThat(config).isNotNull(); assertThat(config.port()).isEqualTo(50211); assertThat(config.tlsPort()).isEqualTo(50212); + assertThat(config.nodeOperatorPortEnabled()).isTrue(); + assertThat(config.nodeOperatorPort()).isEqualTo(50213); assertThat(config.workflowsPort()).isEqualTo(60211); assertThat(config.workflowsTlsPort()).isEqualTo(60212); assertThat(config.maxMessageSize()).isEqualTo(4194304); @@ -41,7 +43,7 @@ void testValidConfiguration() { @Test void testInvalidPortAndTlsPort() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(50212, 50212, 50212, 60212, 4194304, 4194304, 4194304); + new GrpcConfig(50212, 50212, true, 50213, 50212, 60212, 4194304, 4194304, 4194304); }); assertThat(throwable) .isInstanceOf(IllegalArgumentException.class) @@ -51,7 +53,7 @@ void testInvalidPortAndTlsPort() { @Test void testInvalidWorkflowsPortAndTlsPort() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(50211, 50212, 60212, 60212, 4194304, 4194304, 4194304); + new GrpcConfig(50211, 50212, true, 50213, 60212, 60212, 4194304, 4194304, 4194304); }); assertThat(throwable) .isInstanceOf(IllegalArgumentException.class) @@ -60,7 +62,7 @@ void testInvalidWorkflowsPortAndTlsPort() { @Test void testValidZeroPorts() { - GrpcConfig config = new GrpcConfig(0, 0, 60211, 60212, 4194304, 4194304, 4194304); + GrpcConfig config = new GrpcConfig(0, 0, true, 50213, 60211, 60212, 4194304, 4194304, 4194304); assertThat(config).isNotNull(); assertThat(config.port()).isEqualTo(0); @@ -75,7 +77,7 @@ void testValidZeroPorts() { @Test void testInvalidMinValue() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(-1, 50212, 60211, 60212, 4194304, 5194304, 7194304); + new GrpcConfig(-1, 50212, true, 50213, 60211, 60212, 4194304, 5194304, 7194304); }); assertThat(throwable) @@ -86,7 +88,7 @@ void testInvalidMinValue() { @Test void testInvalidMaxValue() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(65536, 50212, 60211, 60212, 4194304, 5194304, 7194304); + new GrpcConfig(65536, 50212, true, 50213, 60211, 60212, 4194304, 5194304, 7194304); }); assertThat(throwable) @@ -97,7 +99,7 @@ void testInvalidMaxValue() { @Test void testMaxMessageSizeMaxValue() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(50211, 50212, 60211, 60212, 4194305, 4194304, 4194304); + new GrpcConfig(50211, 50212, true, 50213, 60211, 60212, 4194305, 4194304, 4194304); }); assertThat(throwable) @@ -109,7 +111,7 @@ void testMaxMessageSizeMaxValue() { @Test void testMaxResponseSizeMaxValue() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(50211, 50212, 60211, 60212, 4194304, 4194305, 4194304); + new GrpcConfig(50211, 50212, true, 50213, 60211, 60212, 4194304, 4194305, 4194304); }); assertThat(throwable) @@ -121,7 +123,7 @@ void testMaxResponseSizeMaxValue() { @Test void testNoopMarshallerMaxMessageSizeMaxValue() { Throwable throwable = catchThrowable(() -> { - new GrpcConfig(50211, 50212, 60211, 60212, 4194304, 4194304, 4194305); + new GrpcConfig(50211, 50212, true, 50213, 60211, 60212, 4194304, 4194304, 4194305); }); assertThat(throwable) diff --git a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/NetworkTransactionGetReceiptHandler.java b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/NetworkTransactionGetReceiptHandler.java index c1e1a2331b02..c0a827571837 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/NetworkTransactionGetReceiptHandler.java +++ b/hedera-node/hedera-network-admin-service-impl/src/main/java/com/hedera/node/app/service/networkadmin/impl/handlers/NetworkTransactionGetReceiptHandler.java @@ -28,7 +28,6 @@ import com.hedera.hapi.node.transaction.Query; import com.hedera.hapi.node.transaction.Response; import com.hedera.hapi.node.transaction.TransactionGetReceiptResponse; -import com.hedera.hapi.node.transaction.TransactionRecord; import com.hedera.node.app.spi.workflows.FreeQueryHandler; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.QueryContext; @@ -92,8 +91,8 @@ public Response findResponse(@NonNull final QueryContext context, @NonNull final final var topLevelTxnId = transactionId.nonce() > 0 ? transactionId.copyBuilder().nonce(0).build() : transactionId; - final var history = recordCache.getHistory(topLevelTxnId); - if (history == null) { + final var receipts = recordCache.getReceipts(topLevelTxnId); + if (receipts == null) { // We only return RECEIPT_NOT_FOUND if we have never heard of this transaction. responseBuilder.header(header.copyBuilder() .nodeTransactionPrecheckCode(RECEIPT_NOT_FOUND) @@ -101,27 +100,21 @@ public Response findResponse(@NonNull final QueryContext context, @NonNull final } else { // Only top-level transactions can have children and duplicates if (transactionId == topLevelTxnId) { - responseBuilder.receipt(history.userTransactionReceipt()); + responseBuilder.receipt(receipts.priorityReceipt(topLevelTxnId)); if (op.includeDuplicates()) { - responseBuilder.duplicateTransactionReceipts(history.duplicateRecords().stream() - .map(TransactionRecord::receiptOrThrow) - .toList()); + responseBuilder.duplicateTransactionReceipts(receipts.duplicateReceipts(topLevelTxnId)); } if (op.includeChildReceipts()) { - responseBuilder.childTransactionReceipts(history.childRecords().stream() - .map(TransactionRecord::receiptOrThrow) - .toList()); + responseBuilder.childTransactionReceipts(receipts.childReceipts(topLevelTxnId)); } } else { - final var maybeRecord = history.childRecords().stream() - .filter(record -> transactionId.equals(record.transactionID())) - .findFirst(); - if (maybeRecord.isEmpty()) { + final var maybeReceipt = receipts.childReceipt(transactionId); + if (maybeReceipt != null) { + responseBuilder.receipt(maybeReceipt); + } else { responseBuilder.header(header.copyBuilder() .nodeTransactionPrecheckCode(RECEIPT_NOT_FOUND) .build()); - } else { - responseBuilder.receipt(maybeRecord.get().receipt()); } } } diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkAdminHandlerTestBase.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkAdminHandlerTestBase.java index 0c7685b6d3c9..ca935dad2d05 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkAdminHandlerTestBase.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkAdminHandlerTestBase.java @@ -31,7 +31,6 @@ import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenSupplyType; import com.hedera.hapi.node.base.TokenType; -import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.state.common.EntityIDPair; import com.hedera.hapi.node.state.token.Account; @@ -52,10 +51,10 @@ import com.hedera.node.app.service.token.impl.ReadableTokenStoreImpl; import com.hedera.node.app.spi.fees.FeeCalculator; import com.hedera.node.app.state.DeduplicationCache; -import com.hedera.node.app.state.SingleTransactionRecord; -import com.hedera.node.app.state.SingleTransactionRecord.TransactionOutputs; +import com.hedera.node.app.state.HederaRecordCache; import com.hedera.node.app.state.WorkingStateAccessor; import com.hedera.node.app.state.recordcache.DeduplicationCacheImpl; +import com.hedera.node.app.state.recordcache.PartialRecordSource; import com.hedera.node.app.state.recordcache.RecordCacheImpl; import com.hedera.node.app.state.recordcache.RecordCacheService; import com.hedera.node.app.workflows.handle.stack.SavepointStackImpl; @@ -151,6 +150,7 @@ public class NetworkAdminHandlerTestBase { protected TransactionRecord duplicate1; protected TransactionRecord duplicate2; protected TransactionRecord duplicate3; + protected TransactionRecord otherRecord; protected TransactionRecord recordOne; protected TransactionRecord recordTwo; protected TransactionRecord recordThree; @@ -202,8 +202,8 @@ void commonSetUp() { final var validStartTime = Instant.ofEpochMilli(123456789L); // aligned to millisecond boundary for convenience. transactionID = transactionID(validStartTime, 0); otherNonceOneTransactionID = transactionID(validStartTime.plusNanos(1), 1); - otherNonceTwoTransactionID = transactionID(validStartTime.plusNanos(2), 2); - otherNonceThreeTransactionID = transactionID(validStartTime.plusNanos(3), 3); + otherNonceTwoTransactionID = transactionID(validStartTime.plusNanos(1), 2); + otherNonceThreeTransactionID = transactionID(validStartTime.plusNanos(1), 3); transactionIDNotInCache = transactionID(validStartTime.plusNanos(5), 5); givenValidAccount(false, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); @@ -281,36 +281,55 @@ private void givenRecordCacheState() { .receipt(receipt) .consensusTimestamp(asTimestamp(consensusTimestamp.plusMillis(3))) .build(); + final var otherTxnId = otherNonceOneTransactionID.copyBuilder().nonce(0).build(); + otherRecord = TransactionRecord.newBuilder() + .transactionID(otherTxnId) + .receipt(receipt) + .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(5))) + .build(); recordOne = TransactionRecord.newBuilder() .transactionID(otherNonceOneTransactionID) .receipt(receipt) - .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(1))) + .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(6))) .parentConsensusTimestamp(asTimestamp(consensusTimestamp)) .build(); recordTwo = TransactionRecord.newBuilder() .transactionID(otherNonceTwoTransactionID) .receipt(receipt) - .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(2))) + .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(7))) .parentConsensusTimestamp(asTimestamp(consensusTimestamp)) .build(); recordThree = TransactionRecord.newBuilder() .transactionID(otherNonceThreeTransactionID) .receipt(receipt) - .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(3))) + .consensusTimestamp(asTimestamp(consensusTimestamp.plusNanos(8))) .parentConsensusTimestamp(asTimestamp(consensusTimestamp)) .build(); - cache.add(0, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(primaryRecord))); - cache.add(1, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(duplicate1))); - cache.add(2, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(duplicate2))); - cache.add(3, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(duplicate3))); - cache.add(0, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(recordOne))); - cache.add(0, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(recordTwo))); - cache.add(0, PAYER_ACCOUNT_ID, List.of(singleTransactionRecord(recordThree))); - } - - private SingleTransactionRecord singleTransactionRecord(TransactionRecord record) { - return new SingleTransactionRecord( - Transaction.DEFAULT, record, List.of(), new TransactionOutputs(TokenType.FUNGIBLE_COMMON)); + cache.addRecordSource( + 0, + primaryRecord.transactionIDOrThrow(), + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of(primaryRecord))); + cache.addRecordSource( + 1, + duplicate1.transactionIDOrThrow(), + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of(duplicate1))); + cache.addRecordSource( + 2, + duplicate2.transactionIDOrThrow(), + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of(duplicate2))); + cache.addRecordSource( + 3, + duplicate3.transactionIDOrThrow(), + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of(duplicate3))); + cache.addRecordSource( + 0, + otherTxnId, + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of(otherRecord, recordOne, recordTwo, recordThree))); } protected MapReadableKVState readableAccountState() { @@ -334,7 +353,7 @@ protected MapReadableKVState.Builder emptyReadableT @NonNull protected RecordCacheImpl emptyRecordCacheBuilder() { dedupeCache = new DeduplicationCacheImpl(props, instantSource); - return new RecordCacheImpl(dedupeCache, wsa, props); + return new RecordCacheImpl(dedupeCache, wsa, props, networkInfo); } @NonNull @@ -471,14 +490,10 @@ protected void givenNonFungibleTokenRelation() { } private TransactionID transactionID(Instant validStartTime, int nonce) { - return transactionID(validStartTime, 0, nonce); - } - - private TransactionID transactionID(Instant validStartTime, int nanos, int nonce) { return TransactionID.newBuilder() .transactionValidStart(Timestamp.newBuilder() .seconds(validStartTime.getEpochSecond()) - .nanos(nanos)) + .nanos(validStartTime.getNano())) .accountID(PAYER_ACCOUNT_ID) .nonce(nonce) .build(); diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetReceiptHandlerTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetReceiptHandlerTest.java index 1e7cba97a6d8..ce7fde6dfcb7 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetReceiptHandlerTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetReceiptHandlerTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.networkadmin.impl.test.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_NFT_SERIAL_NUMBER; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; @@ -45,7 +46,9 @@ import com.hedera.hapi.node.transaction.TransactionRecord; import com.hedera.node.app.service.networkadmin.impl.handlers.NetworkTransactionGetReceiptHandler; import com.hedera.node.app.spi.workflows.QueryContext; +import com.hedera.node.app.state.HederaRecordCache; import com.hedera.node.app.state.SingleTransactionRecord; +import com.hedera.node.app.state.recordcache.PartialRecordSource; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -136,17 +139,24 @@ void usesParentTxnIdWhenGivenNonZeroNonce() { final var query = createGetTransactionReceiptQuery(targetChildTxnId, false, false); when(context.query()).thenReturn(query); when(context.recordCache()).thenReturn(cache); - cache.add( + cache.addRecordSource( 0L, - AccountID.newBuilder().accountNum(2L).build(), - List.of( - singleRecordWith(TransactionRecord.newBuilder() + topLevelId, + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of( + TransactionRecord.newBuilder() + .transactionID(topLevelId) + .receipt(TransactionReceipt.newBuilder() + .status(CONTRACT_REVERT_EXECUTED) + .build()) + .build(), + TransactionRecord.newBuilder() .transactionID(otherChildTxnId) .receipt(TransactionReceipt.newBuilder() .status(REVERTED_SUCCESS) .build()) - .build()), - singleRecordWith(TransactionRecord.newBuilder() + .build(), + TransactionRecord.newBuilder() .transactionID(targetChildTxnId) .receipt(TransactionReceipt.newBuilder() .status(INVALID_TOKEN_NFT_SERIAL_NUMBER) @@ -177,23 +187,29 @@ void stillDetectsMissingChildRecord() { final var query = createGetTransactionReceiptQuery(missingChildTxnId, false, false); when(context.query()).thenReturn(query); when(context.recordCache()).thenReturn(cache); - cache.add( + cache.addRecordSource( 0L, - AccountID.newBuilder().accountNum(2L).build(), - List.of( - singleRecordWith(TransactionRecord.newBuilder() + topLevelId, + HederaRecordCache.DueDiligenceFailure.NO, + new PartialRecordSource(List.of( + TransactionRecord.newBuilder() + .transactionID(topLevelId) + .receipt(TransactionReceipt.newBuilder() + .status(CONTRACT_REVERT_EXECUTED) + .build()) + .build(), + TransactionRecord.newBuilder() .transactionID(otherChildTxnId) .receipt(TransactionReceipt.newBuilder() .status(REVERTED_SUCCESS) .build()) - .build()), - singleRecordWith(TransactionRecord.newBuilder() + .build(), + TransactionRecord.newBuilder() .transactionID(targetChildTxnId) .receipt(TransactionReceipt.newBuilder() .status(INVALID_TOKEN_NFT_SERIAL_NUMBER) .build()) .build()))); - final var response = networkTransactionGetReceiptHandler.findResponse(context, responseHeader); final var answer = response.transactionGetReceiptOrThrow(); assertEquals(RECEIPT_NOT_FOUND, answer.headerOrThrow().nodeTransactionPrecheckCode()); @@ -263,17 +279,19 @@ void getsResponseIfOkResponseWithDuplicates() { void getsResponseIfOkResponseWithChildrenReceipt() { final var responseHeader = ResponseHeader.newBuilder().nodeTransactionPrecheckCode(OK).build(); - final var expectedReceipt = getExpectedReceipt(); - final List expectedChildReceiptList = getExpectedChildReceiptList(); + final List expectedChildReceiptList = + List.of(recordOne.receiptOrThrow(), recordTwo.receiptOrThrow(), recordThree.receiptOrThrow()); - final var query = createGetTransactionReceiptQuery(transactionID, false, true); + final var txnId = + recordThree.transactionIDOrThrow().copyBuilder().nonce(0).build(); + final var query = createGetTransactionReceiptQuery(txnId, false, true); when(context.query()).thenReturn(query); when(context.recordCache()).thenReturn(cache); final var response = networkTransactionGetReceiptHandler.findResponse(context, responseHeader); final var op = response.transactionGetReceiptOrThrow(); assertEquals(OK, op.header().nodeTransactionPrecheckCode()); - assertEquals(expectedReceipt, op.receipt()); + assertEquals(otherRecord.receiptOrThrow(), op.receipt()); assertEquals(expectedChildReceiptList, op.childTransactionReceipts()); assertEquals( expectedChildReceiptList.size(), op.childTransactionReceipts().size()); @@ -287,10 +305,6 @@ private List getExpectedDuplicateList() { return List.of(duplicate1.receipt(), duplicate2.receipt(), duplicate3.receipt()); } - private List getExpectedChildReceiptList() { - return List.of(recordOne.receipt(), recordTwo.receipt(), recordThree.receipt()); - } - private Query createGetTransactionReceiptQuery( final TransactionID transactionID, final boolean includeDuplicates, final boolean includeChildReceipts) { final var data = TransactionGetReceiptQuery.newBuilder() diff --git a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetRecordHandlerTest.java b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetRecordHandlerTest.java index 561408f25430..715177d78919 100644 --- a/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetRecordHandlerTest.java +++ b/hedera-node/hedera-network-admin-service-impl/src/test/java/com/hedera/node/app/service/networkadmin/impl/test/handlers/NetworkTransactionGetRecordHandlerTest.java @@ -105,7 +105,7 @@ void needsAnswerOnlyCostForCostAnswer() { } @Test - void validatesQueryWhenValidRecord() throws Throwable { + void validatesQueryWhenValidRecord() { final var query = createGetTransactionRecordQuery(transactionID, false, false); given(context.query()).willReturn(query); @@ -115,7 +115,7 @@ void validatesQueryWhenValidRecord() throws Throwable { } @Test - void validatesQueryWhenNoTransactionId() throws Throwable { + void validatesQueryWhenNoTransactionId() { final var query = createEmptysQuery(); given(context.query()).willReturn(query); @@ -124,8 +124,7 @@ void validatesQueryWhenNoTransactionId() throws Throwable { } @Test - void validatesQueryWhenNoAccountId() throws Throwable { - + void validatesQueryWhenNoAccountId() { final var query = createGetTransactionRecordQuery(transactionIDWithoutAccount(0, 0), false, false); given(context.query()).willReturn(query); @@ -170,7 +169,7 @@ void getsResponseIfOkResponse() { final var responseHeader = ResponseHeader.newBuilder() .nodeTransactionPrecheckCode(ResponseCodeEnum.OK) .build(); - final var expectedRecord = getExpectedRecord(transactionID); + final var expectedRecord = getExpectedRecord(); final var query = createGetTransactionRecordQuery(transactionID, false, false); when(context.query()).thenReturn(query); @@ -188,7 +187,7 @@ void getsResponseIfOkResponseWithDuplicates() { final var responseHeader = ResponseHeader.newBuilder() .nodeTransactionPrecheckCode(ResponseCodeEnum.OK) .build(); - final var expectedRecord = getExpectedRecord(transactionID); + final var expectedRecord = getExpectedRecord(); final List expectedDuplicateRecords = getExpectedDuplicateList(); final var query = createGetTransactionRecordQuery(transactionID, true, false); @@ -209,10 +208,10 @@ void getsResponseIfOkResponseWithChildrenRecord() { final var responseHeader = ResponseHeader.newBuilder() .nodeTransactionPrecheckCode(ResponseCodeEnum.OK) .build(); - final var expectedRecord = getExpectedRecord(transactionID); - final List expectedChildRecordList = getExpectedChildRecordList(); + final List expectedChildRecordList = List.of(recordOne, recordTwo, recordThree); - final var query = createGetTransactionRecordQuery(transactionID, false, true); + final var txnId = otherRecord.transactionIDOrThrow(); + final var query = createGetTransactionRecordQuery(txnId, false, true); when(context.query()).thenReturn(query); when(context.recordCache()).thenReturn(cache); @@ -220,14 +219,14 @@ void getsResponseIfOkResponseWithChildrenRecord() { final var op = response.transactionGetRecordOrThrow(); assertThat(op.header()).isNotNull(); assertThat(op.header().nodeTransactionPrecheckCode()).isEqualTo(ResponseCodeEnum.OK); - assertThat(op.transactionRecord()).isEqualTo(expectedRecord); + assertThat(op.transactionRecord()).isEqualTo(otherRecord); assertThat(op.childTransactionRecords()).isEqualTo(expectedChildRecordList); assertThat(op.childTransactionRecords().size()).isEqualTo(expectedChildRecordList.size()); } @Test @DisplayName("test computeFees When Free") - void testComputeFees() throws Throwable { + void testComputeFees() { final var query = createGetTransactionRecordQuery(transactionID, false, false); given(context.query()).willReturn(query); given(context.recordCache()).willReturn(cache); @@ -240,7 +239,7 @@ void testComputeFees() throws Throwable { @Test @DisplayName("test computeFees with duplicates and children") - void testComputeFeesWithDuplicatesAndChildRecords() throws Throwable { + void testComputeFeesWithDuplicatesAndChildRecords() { final var query = createGetTransactionRecordQuery(transactionID, true, true); given(context.query()).willReturn(query); given(context.recordCache()).willReturn(cache); @@ -254,7 +253,7 @@ void testComputeFeesWithDuplicatesAndChildRecords() throws Throwable { assertThat(networkFee).isZero(); } - private TransactionRecord getExpectedRecord(TransactionID transactionID) { + private TransactionRecord getExpectedRecord() { return primaryRecord; } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandler.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandler.java index 8f9ee0799f12..0be8e9453fbc 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandler.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandler.java @@ -16,337 +16,251 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SCHEDULE_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.hapi.node.base.ResponseCodeEnum.UNRESOLVABLE_REQUIRED_SIGNERS; +import static com.hedera.hapi.util.HapiUtils.asTimestamp; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.childAsOrdinary; import static com.hedera.node.app.spi.workflows.HandleContext.ConsensusThrottling.ON; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.ScheduleID; -import com.hedera.hapi.node.base.Timestamp; -import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.state.schedule.Schedule; -import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.schedule.ReadableScheduleStore; import com.hedera.node.app.service.schedule.ScheduleStreamBuilder; -import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.spi.key.KeyComparator; -import com.hedera.node.app.spi.signatures.SignatureVerification; +import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; -import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.app.spi.workflows.TransactionKeys; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ConcurrentSkipListSet; -import java.util.function.Predicate; /** * Provides some implementation support needed for both the {@link ScheduleCreateHandler} and {@link * ScheduleSignHandler}. */ abstract class AbstractScheduleHandler { - protected static final String NULL_CONTEXT_MESSAGE = - "Dispatcher called the schedule handler with a null context; probable internal data corruption."; + private static final Comparator KEY_COMPARATOR = new KeyComparator(); - /** - * A simple record to return both "deemed valid" signatories and remaining primitive keys that must sign. - * - * @param updatedSignatories a Set of "deemed valid" signatories, possibly updated with new entries - * @param remainingRequiredKeys A Set of Key entries that have not yet signed the scheduled transaction, but - * must sign that transaction before it can be executed. - */ - protected record ScheduleKeysResult(Set updatedSignatories, Set remainingRequiredKeys) {} + @FunctionalInterface + protected interface TransactionKeysFn { + TransactionKeys apply(@NonNull TransactionBody body, @NonNull AccountID payerId) throws PreCheckException; + } /** - * Gets the set of all the keys required to sign a transaction. - * - * @param scheduleInState the schedule in state - * @param context the Prehandle context - * @return the set of keys required to sign the transaction - * @throws PreCheckException if the transaction cannot be handled successfully due to a validation failure of the - * dispatcher related to signer requirements or other pre-validation criteria. + * Gets the {@link TransactionKeys} summarizing a schedule's signing requirements. + * @param schedule the schedule + * @param fn the function to get required keys by category + * @return the schedule's signing requirements + * @throws HandleException if the signing requirements cannot be determined */ - @NonNull - protected Set allKeysForTransaction( - @NonNull final Schedule scheduleInState, @NonNull final PreHandleContext context) throws PreCheckException { - final TransactionBody scheduledAsOrdinary = HandlerUtility.childAsOrdinary(scheduleInState); - final AccountID originalCreatePayer = - scheduleInState.originalCreateTransaction().transactionID().accountID(); - // note, payerAccount will never be null, but we're dealing with Sonar here. - final AccountID payerForNested = scheduleInState.payerAccountIdOrElse(originalCreatePayer); - final TransactionKeys keyStructure = context.allKeysForTransaction(scheduledAsOrdinary, payerForNested); - return getKeySetFromTransactionKeys(keyStructure); + protected @NonNull TransactionKeys getTransactionKeysOrThrow( + @NonNull final Schedule schedule, @NonNull final TransactionKeysFn fn) throws HandleException { + requireNonNull(schedule); + requireNonNull(fn); + try { + return getRequiredKeys(schedule, fn); + } catch (final PreCheckException e) { + throw new HandleException(e.responseCode()); + } } /** - * Get the schedule keys result to sign the transaction. - * - * @param scheduleInState the schedule in state - * @param context the Prehandle context - * @return the schedule keys result containing the updated signatories and the remaining required keys - * @throws HandleException if any validation check fails when getting the keys for the transaction + * Gets the {@link TransactionKeys} summarizing a schedule's signing requirements. + * @param schedule the schedule + * @param fn the function to get required keys by category + * @return the schedule's signing requirements + * @throws PreCheckException if the signing requirements cannot be determined */ @NonNull - protected ScheduleKeysResult allKeysForTransaction( - @NonNull final Schedule scheduleInState, @NonNull final HandleContext context) throws HandleException { - final AccountID originalCreatePayer = - scheduleInState.originalCreateTransaction().transactionID().accountID(); - // note, payerAccount should never be null, but we're playing it safe here. - final AccountID payer = scheduleInState.payerAccountIdOrElse(originalCreatePayer); - final TransactionBody scheduledAsOrdinary = HandlerUtility.childAsOrdinary(scheduleInState); - final TransactionKeys keyStructure; - try { - keyStructure = context.allKeysForTransaction(scheduledAsOrdinary, payer); - // @todo('9447') We have an issue here. Currently, allKeysForTransaction fails in many cases where a - // key is currently unavailable, but could be in the future. We need the keys, even - // if the transaction is currently invalid, because we may create and sign schedules for - // invalid transactions, then only fail when the transaction is executed. This would allow - // (e.g.) scheduling the transfer of a dApp service fee from a newly created account to be - // set up before the account (or key) is created; then the new account, once funded, signs - // the scheduled transaction and the funds are immediately transferred. Currently that - // would fail on create. Long-term we should fix that. - } catch (final PreCheckException translated) { - throw new HandleException(translated.responseCode()); + protected TransactionKeys getRequiredKeys(@NonNull final Schedule schedule, @NonNull final TransactionKeysFn fn) + throws PreCheckException { + requireNonNull(schedule); + requireNonNull(fn); + final var body = childAsOrdinary(schedule); + final var creatorId = schedule.originalCreateTransactionOrThrow() + .transactionIDOrThrow() + .accountIDOrThrow(); + final var payerId = schedule.payerAccountIdOrElse(creatorId); + final var transactionKeys = fn.apply(body, payerId); + // We do not currently support scheduling transactions that would need to complete hollow accounts + if (!transactionKeys.requiredHollowAccounts().isEmpty()) { + throw new PreCheckException(UNRESOLVABLE_REQUIRED_SIGNERS); } - final Set scheduledRequiredKeys = getKeySetFromTransactionKeys(keyStructure); - // Ensure the *custom* payer is required; some rare corner cases may not require it otherwise. - final Key payerKey = getKeyForAccount(context, payer); - if (hasCustomPayer(scheduleInState) && payerKey != null) scheduledRequiredKeys.add(payerKey); - final Set currentSignatories = setOfKeys(scheduleInState.signatories()); - final Set remainingRequiredKeys = - filterRemainingRequiredKeys(context, scheduledRequiredKeys, currentSignatories, originalCreatePayer); - // Mono doesn't store extra signatures, so for now we mustn't either. - // This is structurally wrong for long term schedules, so we must remove this later. - // @todo('9447') Stop removing currently unused signatures, just store all the verified signatures until - // there are enough to execute, so we don't discard a signature now that would be required later. - HandlerUtility.filterSignatoriesToRequired(currentSignatories, scheduledRequiredKeys); - return new ScheduleKeysResult(currentSignatories, remainingRequiredKeys); - } - - private boolean hasCustomPayer(final Schedule scheduleToCheck) { - final AccountID originalCreatePayer = - scheduleToCheck.originalCreateTransaction().transactionID().accountID(); - final AccountID assignedPayer = scheduleToCheck.payerAccountId(); - // Will never be null, but Sonar doesn't know that. - return assignedPayer != null && !assignedPayer.equals(originalCreatePayer); + return transactionKeys; } /** - * Verify that at least one "new" required key signed the transaction. - *

    - * If there exists a {@link Key} nKey, a member of newSignatories, such that nKey is not - * in existingSignatories, then a new key signed. Otherwise an {@link HandleException} is - * thrown with status {@link ResponseCodeEnum#NO_NEW_VALID_SIGNATURES}. - * - * @param existingSignatories a List of signatories representing all prior signatures before the current - * ScheduleSign transaction. - * @param newSignatories a Set of signatories representing all signatures following the current ScheduleSign - * transaction. - * @throws HandleException if there are no new signatures compared to the prior state. + * Gets all required keys for a transaction, including the payer key and all non-payer keys. + * @param keys the transaction keys + * @return the required keys */ - protected void verifyHasNewSignatures( - @NonNull final List existingSignatories, @NonNull final Set newSignatories) - throws HandleException { - SortedSet preExisting = setOfKeys(existingSignatories); - if (preExisting.containsAll(newSignatories)) { - throw new HandleException(ResponseCodeEnum.NO_NEW_VALID_SIGNATURES); - } + protected @NonNull List allRequiredKeys(@NonNull final TransactionKeys keys) { + final var all = new ArrayList(); + all.add(keys.payerKey()); + all.addAll(keys.requiredNonPayerKeys()); + return all; } /** - * Gets key for account. - * - * @param context the handle context - * @param accountToQuery the account to query - * @return the key for account + * Given a set of signing crypto keys, a list of signatories, and a list of required keys, returns a new list of + * signatories that includes all the original signatories and any crypto keys that are both constituents of the + * required keys and in the signing crypto keys set. + * @param signingCryptoKeys the signing crypto keys + * @param signatories the original signatories + * @param requiredKeys the required keys + * @return the new signatories */ - @Nullable - protected Key getKeyForAccount(@NonNull final HandleContext context, @NonNull final AccountID accountToQuery) { - final ReadableAccountStore accountStore = context.storeFactory().readableStore(ReadableAccountStore.class); - final Account accountData = accountStore.getAccountById(accountToQuery); - return (accountData != null && accountData.key() != null) ? accountData.key() : null; + protected @NonNull List newSignatories( + @NonNull final SortedSet signingCryptoKeys, + @NonNull final List signatories, + @NonNull final List requiredKeys) { + requireNonNull(signingCryptoKeys); + requireNonNull(signatories); + requireNonNull(requiredKeys); + final var newSignatories = new ConcurrentSkipListSet<>(KEY_COMPARATOR); + newSignatories.addAll(signatories); + requiredKeys.forEach(k -> accumulateNewSignatories(newSignatories, signingCryptoKeys, k)); + return new ArrayList<>(newSignatories); } /** - * Given a transaction body and schedule store, validate the transaction meets minimum requirements to - * be completed. - *

    This method checks that the Schedule ID is not null, references a schedule in readable state, - * and the referenced schedule has a child transaction. - * The full set of checks in the {@link #validate(Schedule, Instant, boolean)} method must also - * pass. - * If all validation checks pass, the schedule metadata is returned. - * If any checks fail, then a {@link PreCheckException} is thrown.

    - * @param idToValidate the ID of the schedule to validate - * @param scheduleStore data from readable state which contains, at least, a metadata entry for the schedule - * that the current transaction will sign. - * @throws PreCheckException if the ScheduleSign transaction provided fails any of the required validation - * checks. + * Either returns a schedule from the given store with the given id, ready to be modified, or throws a + * {@link PreCheckException} if the schedule is not found or is not in a valid state. + * + * @param scheduleId the schedule to get and validate + * @param scheduleStore the schedule store + * @throws PreCheckException if the schedule is not found or is not in a valid state */ @NonNull - protected Schedule preValidate( + protected Schedule getValidated( + @NonNull final ScheduleID scheduleId, @NonNull final ReadableScheduleStore scheduleStore, - final boolean isLongTermEnabled, - @Nullable final ScheduleID idToValidate) + final boolean isLongTermEnabled) throws PreCheckException { - if (idToValidate != null) { - final Schedule scheduleData = scheduleStore.get(idToValidate); - if (scheduleData != null) { - if (scheduleData.scheduledTransaction() != null) { - final ResponseCodeEnum validationResult = validate(scheduleData, null, isLongTermEnabled); - if (validationResult == ResponseCodeEnum.OK) { - return scheduleData; - } else { - throw new PreCheckException(validationResult); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } + requireNonNull(scheduleId); + requireNonNull(scheduleStore); + final var schedule = scheduleStore.get(scheduleId); + final var validationResult = validate(schedule, null, isLongTermEnabled); + if (validationResult == OK) { + return requireNonNull(schedule); } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_ID); + throw new PreCheckException(validationResult); } } /** * Given a schedule, consensus time, and long term scheduling enabled flag, validate the transaction - * meets minimum requirements to be handled. + * meets minimum requirements to be handled. Returns {@link ResponseCodeEnum#OK} if the schedule is valid. *

    - * This method checks that, as of the current consensus time, the schedule is + * This method checks that, as of the current consensus time, the schedule, *

      - *
    • not null
    • - *
    • has a scheduled transaction
    • - *
    • has not been executed
    • - *
    • is not deleted
    • - *
    • has not expired
    • + *
    • Is not null.
    • + *
    • Has a scheduled transaction.
    • + *
    • Has not been executed.
    • + *
    • Is not deleted.
    • + *
    • Has not expired.
    • *
    - * - * @param scheduleToValidate the {@link Schedule} to validate. If this is null then - * {@link ResponseCodeEnum#INVALID_SCHEDULE_ID} is returned. - * @param consensusTime the consensus time {@link Instant} applicable to this transaction. - * If this is null then we assume this is a pre-check and do not validate expiration. - * @param isLongTermEnabled a flag indicating if long term scheduling is currently enabled. This modifies - * which response code is sent when a schedule is expired. - * @return a response code representing the result of the validation. This is {@link ResponseCodeEnum#OK} - * if all checks pass, or an appropriate failure code if any checks fail. + * @param schedule the schedule to validate + * @param consensusNow the current consensus time + * @param isLongTermEnabled whether long term scheduling is enabled + * @return the validation result */ @NonNull protected ResponseCodeEnum validate( - @Nullable final Schedule scheduleToValidate, - @Nullable final Instant consensusTime, - final boolean isLongTermEnabled) { - final ResponseCodeEnum result; - final Instant effectiveConsensusTime = Objects.requireNonNullElse(consensusTime, Instant.MIN); - if (scheduleToValidate != null) { - if (scheduleToValidate.hasScheduledTransaction()) { - if (!scheduleToValidate.executed()) { - if (!scheduleToValidate.deleted()) { - final long expiration = scheduleToValidate.calculatedExpirationSecond(); - final Instant calculatedExpiration = - (expiration != Schedule.DEFAULT.calculatedExpirationSecond() - ? Instant.ofEpochSecond(expiration) - : Instant.MAX); - if (calculatedExpiration.getEpochSecond() >= effectiveConsensusTime.getEpochSecond()) { - result = ResponseCodeEnum.OK; - } else { - // We are past expiration time - if (!isLongTermEnabled) { - result = ResponseCodeEnum.INVALID_SCHEDULE_ID; - } else { - // This is not failure, it indicates the schedule should execute if it can, - // or be removed if it is not executable (i.e. it lacks required signatures) - result = ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION; - } - } - } else { - result = ResponseCodeEnum.SCHEDULE_ALREADY_DELETED; - } - } else { - result = ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED; - } - } else { - result = ResponseCodeEnum.INVALID_TRANSACTION; - } + @Nullable final Schedule schedule, @Nullable final Instant consensusNow, final boolean isLongTermEnabled) { + if (schedule == null) { + return INVALID_SCHEDULE_ID; + } + if (!schedule.hasScheduledTransaction()) { + return INVALID_TRANSACTION; + } + if (schedule.executed()) { + return SCHEDULE_ALREADY_EXECUTED; + } + if (schedule.deleted()) { + return SCHEDULE_ALREADY_DELETED; + } + final long expiration = schedule.calculatedExpirationSecond(); + final var calculatedExpiration = (expiration != Schedule.DEFAULT.calculatedExpirationSecond() + ? Instant.ofEpochSecond(expiration) + : Instant.MAX); + final var effectiveNow = Objects.requireNonNullElse(consensusNow, Instant.MIN); + if (calculatedExpiration.getEpochSecond() >= effectiveNow.getEpochSecond()) { + return OK; } else { - result = ResponseCodeEnum.INVALID_SCHEDULE_ID; + return isLongTermEnabled ? SCHEDULE_PENDING_EXPIRATION : INVALID_SCHEDULE_ID; } - return result; } /** - * Very basic transaction ID validation. - * This just checks that the transaction is not scheduled (you cannot schedule a schedule), - * that the account ID is not null (so we can fill in scheduler account), - * and that the start timestamp is not null (so we can fill in schedule valid start time) - * @param currentId a TransactionID to validate - * @throws PreCheckException if the transaction is scheduled, the account ID is null, or the start time is null. + * Indicates if the given validation result is one that may allow a validated schedule to be executed. + * @param validationResult the validation result + * @return if the schedule might be executable */ - protected void checkValidTransactionId(@Nullable final TransactionID currentId) throws PreCheckException { - if (currentId == null) throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_ID); - final AccountID payer = currentId.accountID(); - final Timestamp validStart = currentId.transactionValidStart(); - final boolean isScheduled = currentId.scheduled(); - if (isScheduled) throw new PreCheckException(ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); - if (payer == null) throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_PAYER_ID); - if (validStart == null) throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_START); + protected boolean isMaybeExecutable(@NonNull final ResponseCodeEnum validationResult) { + return validationResult == OK || validationResult == SUCCESS || validationResult == SCHEDULE_PENDING_EXPIRATION; } /** - * Try to execute a schedule. Will attempt to execute a schedule if the remaining signatories are empty - * and the schedule is not waiting for expiration. - * - * @param context the context - * @param scheduleToExecute the schedule to execute - * @param remainingSignatories the remaining signatories - * @param validSignatories the valid signatories - * @param validationResult the validation result - * @param isLongTermEnabled the is long term enabled - * @return boolean indicating if the schedule was executed + * Tries to execute a schedule, if all conditions are met. Returns true if the schedule was executed. + * @param context the context + * @param schedule the schedule to execute + * @param validationResult the validation result + * @param isLongTermEnabled the is long term enabled + * @return if the schedule was executed */ protected boolean tryToExecuteSchedule( @NonNull final HandleContext context, - @NonNull final Schedule scheduleToExecute, - @NonNull final Set remainingSignatories, - @NonNull final Set validSignatories, + @NonNull final Schedule schedule, + @NonNull final List requiredKeys, @NonNull final ResponseCodeEnum validationResult, final boolean isLongTermEnabled) { - if (canExecute(remainingSignatories, isLongTermEnabled, validationResult, scheduleToExecute)) { - final AccountID originalPayer = scheduleToExecute - .originalCreateTransaction() - .transactionID() - .accountID(); - final Set acceptedSignatories = new HashSet<>(); - acceptedSignatories.addAll(validSignatories); - acceptedSignatories.add(getKeyForAccount(context, originalPayer)); - final Predicate assistant = new DispatchPredicate(acceptedSignatories); - // This sets the child transaction ID to scheduled. - final TransactionBody childTransaction = HandlerUtility.childAsOrdinary(scheduleToExecute); - final ScheduleStreamBuilder recordBuilder = context.dispatchChildTransaction( - childTransaction, - ScheduleStreamBuilder.class, - assistant, - scheduleToExecute.payerAccountId(), - TransactionCategory.SCHEDULED, - ON); - // If the child failed, we would prefer to fail with the same result. - // We do not fail, however, at least mono service code does not. - // We succeed and the record of the child transaction is failed. - // set the schedule ref for the child transaction to the schedule that we're executing - recordBuilder.scheduleRef(scheduleToExecute.scheduleId()); - // also set the child transaction ID as scheduled transaction ID in the parent record. - final ScheduleStreamBuilder parentRecordBuilder = - context.savepointStack().getBaseBuilder(ScheduleStreamBuilder.class); - parentRecordBuilder.scheduledTransactionID(childTransaction.transactionID()); + requireNonNull(context); + requireNonNull(schedule); + requireNonNull(requiredKeys); + requireNonNull(validationResult); + + final var signatories = new HashSet<>(schedule.signatories()); + final VerificationAssistant callback = (k, ignore) -> signatories.contains(k); + final var remainingKeys = new HashSet<>(requiredKeys); + remainingKeys.removeIf( + k -> context.keyVerifier().verificationFor(k, callback).passed()); + final boolean isExpired = validationResult == SCHEDULE_PENDING_EXPIRATION; + if (canExecute(schedule, remainingKeys, isExpired, isLongTermEnabled)) { + final var body = childAsOrdinary(schedule); + context.dispatchChildTransaction( + body, + ScheduleStreamBuilder.class, + signatories::contains, + schedule.payerAccountIdOrThrow(), + TransactionCategory.SCHEDULED, + ON) + .scheduleRef(schedule.scheduleId()); + context.savepointStack() + .getBaseBuilder(ScheduleStreamBuilder.class) + .scheduledTransactionID(body.transactionID()); return true; } else { return false; @@ -354,84 +268,69 @@ protected boolean tryToExecuteSchedule( } /** - * Checks if the validation is OK, SUCCESS, or SCHEDULE_PENDING_EXPIRATION. - * - * @param validationResult the validation result - * @return boolean indicating status of the validation + * Returns a version of the given schedule marked as executed at the given time. + * @param schedule the schedule to mark as executed + * @param consensusNow the time to mark the schedule as executed + * @return the marked schedule */ - protected boolean validationOk(final ResponseCodeEnum validationResult) { - return validationResult == ResponseCodeEnum.OK - || validationResult == ResponseCodeEnum.SUCCESS - || validationResult == ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION; - } - - @NonNull - private SortedSet getKeySetFromTransactionKeys(final TransactionKeys requiredKeys) { - final SortedSet scheduledRequiredKeys = new ConcurrentSkipListSet<>(new KeyComparator()); - scheduledRequiredKeys.addAll(requiredKeys.requiredNonPayerKeys()); - scheduledRequiredKeys.addAll(requiredKeys.optionalNonPayerKeys()); - return scheduledRequiredKeys; + protected static @NonNull Schedule markedExecuted( + @NonNull final Schedule schedule, @NonNull final Instant consensusNow) { + return schedule.copyBuilder() + .executed(true) + .resolutionTime(asTimestamp(consensusNow)) + .build(); } - private SortedSet filterRemainingRequiredKeys( - final HandleContext context, - final Set scheduledRequiredKeys, - final Set currentSignatories, - final AccountID originalCreatePayer) { - // the final output must be a sorted/ordered set. - final KeyComparator keyMatcher = new KeyComparator(); - final SortedSet remainingKeys = new ConcurrentSkipListSet<>(keyMatcher); - final Set currentUnverifiedKeys = new HashSet<>(1); - final Key originalPayerKey = getKeyForAccount(context, originalCreatePayer); - final var assistant = new ScheduleVerificationAssistant(currentSignatories, currentUnverifiedKeys); - for (final Key next : scheduledRequiredKeys) { - // The schedule verification assistant observes each primitive key in the tree - final SignatureVerification isVerified = context.keyVerifier().verificationFor(next, assistant); - // unverified primitive keys only count if the top-level key failed verification. - // @todo('9447') The comparison to originalPayerKey here is to match monoservice - // "hidden default payer" behavior. We intend to remove that behavior after v1 - // release as it is not considered fully "correct", particularly for long term schedules. - if (!isVerified.passed() && keyMatcher.compare(next, originalPayerKey) != 0) { - remainingKeys.addAll(currentUnverifiedKeys); - } - currentUnverifiedKeys.clear(); + /** + * Evaluates whether a schedule with given remaining signatories, validation result, and can be executed + * in the context of long-term scheduling on or off. + * + * @param schedule the schedule to execute + * @param remainingKeys the remaining keys that must sign + * @param isExpired whether the schedule is expired + * @param isLongTermEnabled the long term scheduling flag + * @return boolean indicating if the schedule can be executed + */ + private boolean canExecute( + @NonNull final Schedule schedule, + @NonNull final Set remainingKeys, + final boolean isExpired, + final boolean isLongTermEnabled) { + // We can only execute if there are no remaining keys required to sign + if (!remainingKeys.isEmpty()) { + return false; + } + // If long-term transactions are disabled, everything executes immediately + if (!isLongTermEnabled) { + return true; } - return remainingKeys; + // Otherwise we can only execute in two cases, + // (1) The schedule is allowed to execute immediately, and is not expired. + // (2) The schedule is waiting for its expiry to execute, and is expired. + return schedule.waitForExpiry() == isExpired; } /** - * Given an arbitrary {@code Iterable}, return a modifiable {@code SortedSet} containing - * the same objects as the input. - * This set must be sorted to ensure a deterministic order of values in state. - * If there are any duplicates in the input, only one of each will be in the result. - * If there are any null values in the input, those values will be excluded from the result. - * @param keyCollection an Iterable of Key values. - * @return a modifiable {@code SortedSet} containing the same contents as the input with duplicates - * and null values excluded + * Accumulates the valid signatories from a key structure into a set of signatories. + * @param signatories the set of signatories to accumulate into + * @param signingCryptoKeys the signing crypto keys + * @param key the key structure to accumulate signatories from */ - @NonNull - private SortedSet setOfKeys(@Nullable final Iterable keyCollection) { - if (keyCollection != null) { - final SortedSet results = new ConcurrentSkipListSet<>(new KeyComparator()); - for (final Key next : keyCollection) { - if (next != null) results.add(next); + private void accumulateNewSignatories( + @NonNull final Set signatories, @NonNull final Set signingCryptoKeys, @NonNull final Key key) { + switch (key.key().kind()) { + case ED25519, ECDSA_SECP256K1 -> { + if (signingCryptoKeys.contains(key)) { + signatories.add(key); + } } - return results; - } else { - // cannot use Set.of() or Collections.emptySet() here because those are unmodifiable and unsorted. - return new ConcurrentSkipListSet<>(new KeyComparator()); + case KEY_LIST -> key.keyListOrThrow() + .keys() + .forEach(k -> accumulateNewSignatories(signatories, signingCryptoKeys, k)); + case THRESHOLD_KEY -> key.thresholdKeyOrThrow() + .keysOrThrow() + .keys() + .forEach(k -> accumulateNewSignatories(signatories, signingCryptoKeys, k)); } } - - private boolean canExecute( - final Set remainingSignatories, - final boolean isLongTermEnabled, - final ResponseCodeEnum validationResult, - final Schedule scheduleToExecute) { - // either we're waiting and pending, or not waiting and not pending - final boolean longTermReady = - scheduleToExecute.waitForExpiry() == (validationResult == ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION); - final boolean allSignturesGathered = remainingSignatories == null || remainingSignatories.isEmpty(); - return allSignturesGathered && (!isLongTermEnabled || longTermReady); - } } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicate.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicate.java deleted file mode 100644 index 9768b3e7fc2e..000000000000 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicate.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.schedule.impl.handlers; - -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.Key; -import com.hedera.node.app.spi.signatures.VerificationAssistant; -import com.hedera.node.app.spi.workflows.HandleContext; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.Set; -import java.util.function.Predicate; - -/** - * Predicate for child dispatch key validation required because {@link HandleContext} no longer - * allows a {@link VerificationAssistant} to be used for dispatch. - */ -public class DispatchPredicate implements Predicate { - private final Set preValidatedKeys; - - /** - * Create a new DispatchPredicate using the given set of keys as deemed-valid. - * - * @param preValidatedKeys an unmodifiable {@code Set} of primitive keys - * previously verified. - */ - public DispatchPredicate(@NonNull final Set preValidatedKeys) { - this.preValidatedKeys = requireNonNull(preValidatedKeys); - } - - @Override - public boolean test(@NonNull final Key key) { - return preValidatedKeys.contains(requireNonNull(key)); - } -} diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java index 59a12eb82eb6..f8df4d1b16f0 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java @@ -16,26 +16,19 @@ package com.hedera.node.app.service.schedule.impl.handlers; -import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.ScheduleID; -import com.hedera.hapi.node.base.ScheduleID.Builder; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; import com.hedera.hapi.node.scheduled.SchedulableTransactionBody.DataOneOfType; -import com.hedera.hapi.node.scheduled.ScheduleCreateTransactionBody; import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.spi.workflows.HandleException; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Instant; -import java.util.Collection; -import java.util.List; -import java.util.Set; /** * A package-private utility class for Schedule Handlers. @@ -176,216 +169,66 @@ static HederaFunctionality functionalityForType(final DataOneOfType transactionT }; } - /** - * Given a Schedule, return a copy of that schedule with the executed flag and resolution time set. - * @param schedule a {@link Schedule} to mark executed. - * @param consensusTime the current consensus time, used to set {@link Schedule#resolutionTime()}. - * @return a new Schedule which matches the input, except that the execute flag is set and the resolution time - * is set to the consensusTime provided. - */ - @NonNull - static Schedule markExecuted(@NonNull final Schedule schedule, @NonNull final Instant consensusTime) { - final Timestamp consensusTimestamp = new Timestamp(consensusTime.getEpochSecond(), consensusTime.getNano()); - return schedule.copyBuilder() - .executed(true) - .resolutionTime(consensusTimestamp) - .build(); - } - - /** - * Replace the signatories of a schedule with a new set of signatories. - * The schedule is not modified in place. - * - * @param schedule the schedule - * @param newSignatories the new signatories - * @return the schedule - */ - @NonNull - static Schedule replaceSignatories(@NonNull final Schedule schedule, @NonNull final Set newSignatories) { - return schedule.copyBuilder().signatories(List.copyOf(newSignatories)).build(); - } - - /** - * Replace signatories and mark executed schedule. - * - * @param schedule the schedule - * @param newSignatories the new signatories - * @param consensusTime the consensus time - * @return the schedule - */ - @NonNull - static Schedule replaceSignatoriesAndMarkExecuted( - @NonNull final Schedule schedule, - @NonNull final Set newSignatories, - @NonNull final Instant consensusTime) { - final Timestamp consensusTimestamp = new Timestamp(consensusTime.getEpochSecond(), consensusTime.getNano()); - final Schedule.Builder builder = schedule.copyBuilder().executed(true).resolutionTime(consensusTimestamp); - return builder.signatories(List.copyOf(newSignatories)).build(); - } - /** * Create a new Schedule, but without an ID or signatories. * This method is used to create a schedule object for processing during a ScheduleCreate, but without the * schedule ID, as we still need to complete validation and other processing. Once all processing is complete, * a new ID is allocated and signatories are added immediately prior to storing the new object in state. - * @param currentTransaction The transaction body of the current Schedule Create transaction. We assume that + * @param body The transaction body of the current Schedule Create transaction. We assume that * the transaction is a ScheduleCreate, but require the less specific object so that we have access to * the transaction ID via {@link TransactionBody#transactionID()} from the TransactionBody stored in * the {@link Schedule#originalCreateTransaction()} attribute of the Schedule. - * @param currentConsensusTime The current consensus time for the network. - * @param maxLifeSeconds The maximum number of seconds a schedule is permitted to exist on the ledger + * @param consensusNow The current consensus time for the network. + * @param maxLifetime The maximum number of seconds a schedule is permitted to exist on the ledger * before it expires. * @return a newly created Schedule with a null schedule ID * @throws HandleException if the */ @NonNull static Schedule createProvisionalSchedule( - @NonNull final TransactionBody currentTransaction, - @NonNull final Instant currentConsensusTime, - final long maxLifeSeconds) - throws HandleException { - // The next three items will never be null, but Sonar is persnickety, so we force NPE if any are null. - final TransactionID parentTransactionId = currentTransaction.transactionIDOrThrow(); - final ScheduleCreateTransactionBody createTransaction = currentTransaction.scheduleCreateOrThrow(); - final AccountID schedulerAccount = parentTransactionId.accountIDOrThrow(); - final long calculatedExpirationTime = - calculateExpiration(createTransaction.expirationTime(), currentConsensusTime, maxLifeSeconds); - final ScheduleID nullId = null; - - Schedule.Builder builder = Schedule.newBuilder(); - builder.scheduleId(nullId).deleted(false).executed(false); - builder.waitForExpiry(createTransaction.waitForExpiry()); - builder.adminKey(createTransaction.adminKey()).schedulerAccountId(parentTransactionId.accountID()); - builder.payerAccountId(createTransaction.payerAccountIDOrElse(schedulerAccount)); - builder.schedulerAccountId(schedulerAccount); - builder.scheduleValidStart(parentTransactionId.transactionValidStart()); - builder.calculatedExpirationSecond(calculatedExpirationTime); - builder.providedExpirationSecond( - createTransaction.expirationTimeOrElse(Timestamp.DEFAULT).seconds()); - builder.originalCreateTransaction(currentTransaction); - builder.memo(createTransaction.memo()); - builder.scheduledTransaction(createTransaction.scheduledTransactionBody()); - return builder.build(); - } - - /** - * Complete the processing of a provisional schedule, which was created during a ScheduleCreate transaction. - * The schedule is completed by adding a schedule ID and signatories. - * - * @param provisionalSchedule the provisional schedule - * @param newEntityNumber the new entity number - * @param finalSignatories the final signatories for the schedule - * @return the schedule - * @throws HandleException if the transaction is not handled successfully. - */ - @NonNull - static Schedule completeProvisionalSchedule( - @NonNull final Schedule provisionalSchedule, - final long newEntityNumber, - @NonNull final Set finalSignatories) - throws HandleException { - final TransactionBody originalTransaction = provisionalSchedule.originalCreateTransactionOrThrow(); - final TransactionID parentTransactionId = originalTransaction.transactionIDOrThrow(); - final ScheduleID finalId = getNextScheduleID(parentTransactionId, newEntityNumber); - - Schedule.Builder build = provisionalSchedule.copyBuilder(); - build.scheduleId(finalId).deleted(false).executed(false); - build.schedulerAccountId(parentTransactionId.accountID()); - build.signatories(List.copyOf(finalSignatories)); - return build.build(); + @NonNull final TransactionBody body, @NonNull final Instant consensusNow, final long maxLifetime) { + final var txnId = body.transactionIDOrThrow(); + final var op = body.scheduleCreateOrThrow(); + final var payerId = txnId.accountIDOrThrow(); + final long expiry = calculateExpiration(op.expirationTime(), consensusNow, maxLifetime); + return Schedule.newBuilder() + .scheduleId((ScheduleID) null) + .deleted(false) + .executed(false) + .waitForExpiry(op.waitForExpiry()) + .adminKey(op.adminKey()) + .schedulerAccountId(payerId) + .payerAccountId(op.payerAccountIDOrElse(payerId)) + .schedulerAccountId(payerId) + .scheduleValidStart(txnId.transactionValidStart()) + .calculatedExpirationSecond(expiry) + .providedExpirationSecond( + op.expirationTimeOrElse(Timestamp.DEFAULT).seconds()) + .originalCreateTransaction(body) + .memo(op.memo()) + .scheduledTransaction(op.scheduledTransactionBody()) + .build(); } /** - * Gets next schedule id for a given parent transaction id and new schedule number. - * The schedule ID is created using the shard and realm numbers from the parent transaction ID, - * and the new schedule number. - * - * @param parentTransactionId the parent transaction id - * @param newScheduleNumber the new schedule number - * @return the next schedule id + * Builds the transaction id for a scheduled transaction from its schedule. + * @param schedule the schedule + * @return its transaction id */ @NonNull - static ScheduleID getNextScheduleID( - @NonNull final TransactionID parentTransactionId, final long newScheduleNumber) { - final AccountID schedulingAccount = parentTransactionId.accountIDOrThrow(); - final long shardNumber = schedulingAccount.shardNum(); - final long realmNumber = schedulingAccount.realmNum(); - final Builder builder = ScheduleID.newBuilder().shardNum(shardNumber).realmNum(realmNumber); - return builder.scheduleNum(newScheduleNumber).build(); - } - - /** - * Transaction id for scheduled transaction id. - * - * @param valueInState the value in state - * @return the transaction id - */ - @NonNull - static TransactionID transactionIdForScheduled(@NonNull Schedule valueInState) { - // original create transaction and its transaction ID will never be null, but Sonar... - final TransactionBody originalTransaction = valueInState.originalCreateTransactionOrThrow(); - final TransactionID parentTransactionId = originalTransaction.transactionIDOrThrow(); - final TransactionID.Builder builder = parentTransactionId.copyBuilder(); - // This is tricky. - // The scheduled child transaction that is executed must have a transaction ID that exactly matches - // the original CREATE transaction, not the parent transaction that triggers execution. So the child - // record is a child of "trigger" with an ID matching "create". This is what mono service does, but it - // is not ideal. Future work should change this (if at all possible) to have ID and parent match - // better, not rely on exact ID match, and only use the scheduleRef and scheduledId values in the transaction - // records (scheduleRef on the child pointing to the schedule ID, and scheduled ID on the parent pointing - // to the child transaction) for connecting things. - builder.scheduled(true); - return builder.build(); + static TransactionID transactionIdForScheduled(@NonNull final Schedule schedule) { + final var op = schedule.originalCreateTransactionOrThrow(); + final var parentTxnId = op.transactionIDOrThrow(); + return parentTxnId.copyBuilder().scheduled(true).build(); } private static long calculateExpiration( - @Nullable final Timestamp givenExpiration, - @NonNull final Instant currentConsensusTime, - final long maxLifeSeconds) { + @Nullable final Timestamp givenExpiration, @NonNull final Instant consensusNow, final long maxLifetime) { if (givenExpiration != null) { return givenExpiration.seconds(); } else { - final Instant currentPlusMaxLife = currentConsensusTime.plusSeconds(maxLifeSeconds); + final var currentPlusMaxLife = consensusNow.plusSeconds(maxLifetime); return currentPlusMaxLife.getEpochSecond(); } } - - /** - * Filters the signatories to only those that are required. - * The required signatories are those that are present in the incoming signatories set. - * - * @param signatories the signatories - * @param required the required - */ - static void filterSignatoriesToRequired(Set signatories, Set required) { - final Set incomingSignatories = Set.copyOf(signatories); - signatories.clear(); - filterSignatoriesToRequired(signatories, required, incomingSignatories); - } - - private static void filterSignatoriesToRequired( - final Set signatories, final Collection required, final Set incomingSignatories) { - for (final Key next : required) { - switch (next.key().kind()) { - case ED25519, ECDSA_SECP256K1, CONTRACT_ID, DELEGATABLE_CONTRACT_ID: - // Handle "primitive" keys, which are what the signatories set stores. - if (incomingSignatories.contains(next)) { - signatories.add(next); - } - break; - case KEY_LIST: - // Dive down into the elements of the key list - filterSignatoriesToRequired(signatories, next.keyList().keys(), incomingSignatories); - break; - case THRESHOLD_KEY: - // Dive down into the elements of the threshold key candidates list - filterSignatoriesToRequired( - signatories, next.thresholdKey().keys().keys(), incomingSignatories); - break; - case ECDSA_384, RSA_3072, UNSET: - // These types are unsupported - break; - } - } - } } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandler.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandler.java index 5eb6530944b0..8cdd5420e7b2 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandler.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandler.java @@ -16,20 +16,31 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST; +import static com.hedera.hapi.node.base.ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ADMIN_KEY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MEMO_TOO_LONG; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST; +import static com.hedera.hapi.node.base.SubType.DEFAULT; +import static com.hedera.hapi.node.base.SubType.SCHEDULE_CREATE_CONTRACT_CALL; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.createProvisionalSchedule; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.functionalityForType; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.transactionIdForScheduled; +import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; -import com.hedera.hapi.node.base.SubType; -import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; -import com.hedera.hapi.node.scheduled.SchedulableTransactionBody.DataOneOfType; import com.hedera.hapi.node.scheduled.ScheduleCreateTransactionBody; import com.hedera.hapi.node.state.schedule.Schedule; -import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.fees.usage.SigUsage; import com.hedera.node.app.hapi.fees.usage.schedule.ScheduleOpsUsage; @@ -50,11 +61,10 @@ import com.hederahashgraph.api.proto.java.FeeData; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Instant; import java.time.InstantSource; +import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -72,316 +82,162 @@ public ScheduleCreateHandler(@NonNull final InstantSource instantSource) { } @Override - public void pureChecks(@Nullable final TransactionBody currentTransaction) throws PreCheckException { - if (currentTransaction != null) { - checkValidTransactionId(currentTransaction.transactionID()); - checkLongTermSchedulable(getValidScheduleCreateBody(currentTransaction)); - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } + public void pureChecks(@NonNull final TransactionBody body) throws PreCheckException { + requireNonNull(body); + validateTruePreCheck(body.hasScheduleCreate(), INVALID_TRANSACTION_BODY); + final var op = body.scheduleCreateOrThrow(); + validateTruePreCheck(op.hasScheduledTransactionBody(), INVALID_TRANSACTION); + // (FUTURE) Add a dedicated response code for an op waiting for an unspecified expiration time + validateFalsePreCheck(op.waitForExpiry() && !op.hasExpirationTime(), INVALID_TRANSACTION); } - /** - * Pre-handles a {@link HederaFunctionality#SCHEDULE_CREATE} transaction, returning the metadata - * required to, at minimum, validate the signatures of all required and optional signing keys. - * - * @param context the {@link PreHandleContext} which collects all information - * @throws PreCheckException if the transaction cannot be handled successfully. - * The response code appropriate to the failure reason will be provided via this exception. - */ @Override public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final TransactionBody currentTransaction = context.body(); - final LedgerConfig ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); - final HederaConfig hederaConfig = context.configuration().getConfigData(HederaConfig.class); - final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); - final long maxExpireConfig = schedulingConfig.longTermEnabled() - ? schedulingConfig.maxExpirationFutureSeconds() - : ledgerConfig.scheduleTxExpiryTimeSecs(); - final ScheduleCreateTransactionBody scheduleBody = getValidScheduleCreateBody(currentTransaction); - if (scheduleBody.memo() != null && scheduleBody.memo().length() > hederaConfig.transactionMaxMemoUtf8Bytes()) { - throw new PreCheckException(ResponseCodeEnum.MEMO_TOO_LONG); + requireNonNull(context); + final var body = context.body(); + // We ensure this exists in pureChecks() + final var op = body.scheduleCreateOrThrow(); + final var config = context.configuration(); + final var hederaConfig = config.getConfigData(HederaConfig.class); + validateTruePreCheck(op.memo().length() <= hederaConfig.transactionMaxMemoUtf8Bytes(), MEMO_TOO_LONG); + // For backward compatibility, use ACCOUNT_ID_DOES_NOT_EXIST for a nonexistent designated payer + if (op.hasPayerAccountID()) { + final var accountStore = context.createStore(ReadableAccountStore.class); + final var payer = accountStore.getAccountById(op.payerAccountIDOrThrow()); + mustExist(payer, ACCOUNT_ID_DOES_NOT_EXIST); } - // @todo('future') add whitelist check here; mono checks very late, so we cannot check that here yet. - // validate the schedulable transaction - getSchedulableTransaction(currentTransaction); - // @todo('future') This key/account validation should move to handle once we finish and validate - // modularization; mono does this check too early, and may reject transactions - // that should succeed. - validatePayerAndScheduler(context, scheduleBody); - // If we have an explicit payer account for the scheduled child transaction, - // add it to optional keys (it might not have signed yet). - final Key payerKey = getKeyForPayerAccount(scheduleBody, context); - if (payerKey != null) context.optionalKey(payerKey); - if (scheduleBody.hasAdminKey()) { - // If an admin key is present, it must sign the create transaction. - context.requireKey(scheduleBody.adminKeyOrThrow()); + final var schedulingConfig = config.getConfigData(SchedulingConfig.class); + validateTruePreCheck( + isAllowedFunction(op.scheduledTransactionBodyOrThrow(), schedulingConfig), + SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); + // If an admin key is present, it must sign + if (op.hasAdminKey()) { + context.requireKey(op.adminKeyOrThrow()); } - checkSchedulableWhitelist(scheduleBody, schedulingConfig); - final TransactionID transactionId = currentTransaction.transactionID(); - if (transactionId != null) { - final Schedule provisionalSchedule = HandlerUtility.createProvisionalSchedule( - currentTransaction, instantSource.instant(), maxExpireConfig); - final Set allRequiredKeys = allKeysForTransaction(provisionalSchedule, context); - context.optionalKeys(allRequiredKeys); - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); + final var ledgerConfig = config.getConfigData(LedgerConfig.class); + final long maxLifetime = schedulingConfig.longTermEnabled() + ? schedulingConfig.maxExpirationFutureSeconds() + : ledgerConfig.scheduleTxExpiryTimeSecs(); + final var schedule = createProvisionalSchedule(body, instantSource.instant(), maxLifetime); + final var transactionKeys = getRequiredKeys(schedule, context::allKeysForTransaction); + // If the schedule payer inherits from the ScheduleCreate, it is already in the required keys + if (op.hasPayerAccountID()) { + context.optionalKey(transactionKeys.payerKey()); } + // Any required non-payer key may optionally provide its signature with the ScheduleCreate + context.optionalKeys(transactionKeys.requiredNonPayerKeys()); } - /** - * This method is called during the handle workflow. It executes the actual transaction. - * - * @throws HandleException if the transaction is not handled successfully. - * The response code appropriate to the failure reason will be provided via this exception. - */ @Override public void handle(@NonNull final HandleContext context) throws HandleException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final Instant currentConsensusTime = context.consensusNow(); - final WritableScheduleStore scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); - final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); - final LedgerConfig ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); - final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); - // Note: We must store the original ScheduleCreate transaction body in the Schedule so that we can compare - // those bytes to any new ScheduleCreate transaction for detecting duplicate ScheduleCreate - // transactions. SchedulesByEquality is the virtual map for that task - final TransactionBody currentTransaction = context.body(); - if (currentTransaction.hasScheduleCreate()) { - final var expirationSeconds = isLongTermEnabled - ? schedulingConfig.maxExpirationFutureSeconds() - : ledgerConfig.scheduleTxExpiryTimeSecs(); - final Schedule provisionalSchedule = HandlerUtility.createProvisionalSchedule( - currentTransaction, currentConsensusTime, expirationSeconds); - checkSchedulableWhitelistHandle(provisionalSchedule, schedulingConfig); - context.attributeValidator().validateMemo(provisionalSchedule.memo()); - context.attributeValidator() - .validateMemo(provisionalSchedule.scheduledTransaction().memo()); - if (provisionalSchedule.hasAdminKey()) { - try { - context.attributeValidator().validateKey(provisionalSchedule.adminKeyOrThrow()); - } catch (HandleException e) { - throw new HandleException(ResponseCodeEnum.INVALID_ADMIN_KEY); - } - } - final ResponseCodeEnum validationResult = - validate(provisionalSchedule, currentConsensusTime, isLongTermEnabled); - if (validationOk(validationResult)) { - final List possibleDuplicates = scheduleStore.getByEquality(provisionalSchedule); - if (isPresentIn(context, possibleDuplicates, provisionalSchedule)) { - throw new HandleException(ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED); - } - if (scheduleStore.numSchedulesInState() + 1 > schedulingConfig.maxNumber()) { - throw new HandleException(ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - } - // Need to process the child transaction again, to get the *primitive* keys possibly required - final ScheduleKeysResult requiredKeysResult = allKeysForTransaction(provisionalSchedule, context); - final Set allRequiredKeys = requiredKeysResult.remainingRequiredKeys(); - final Set updatedSignatories = requiredKeysResult.updatedSignatories(); - final long nextId = context.entityNumGenerator().newEntityNum(); - Schedule finalSchedule = - HandlerUtility.completeProvisionalSchedule(provisionalSchedule, nextId, updatedSignatories); - if (tryToExecuteSchedule( - context, - finalSchedule, - allRequiredKeys, - updatedSignatories, - validationResult, - isLongTermEnabled)) { - finalSchedule = HandlerUtility.markExecuted(finalSchedule, currentConsensusTime); - } - scheduleStore.put(finalSchedule); - final ScheduleStreamBuilder scheduleRecords = - context.savepointStack().getBaseBuilder(ScheduleStreamBuilder.class); - scheduleRecords - .scheduleID(finalSchedule.scheduleId()) - .scheduledTransactionID(HandlerUtility.transactionIdForScheduled(finalSchedule)); - } else { - throw new HandleException(validationResult); - } - } else { - throw new HandleException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } - - private boolean isPresentIn( - @NonNull final HandleContext context, - @Nullable final List possibleDuplicates, - @NonNull final Schedule provisionalSchedule) { - if (possibleDuplicates != null) { - for (final Schedule candidate : possibleDuplicates) { - if (compareForDuplicates(candidate, provisionalSchedule)) { - // Do not forget to set the ID of the existing duplicate in the receipt... - TransactionID scheduledTransactionID = candidate - .originalCreateTransaction() - .transactionID() - .copyBuilder() - .scheduled(true) - .build(); - context.savepointStack() - .getBaseBuilder(ScheduleStreamBuilder.class) - .scheduleID(candidate.scheduleId()) - .scheduledTransactionID(scheduledTransactionID); - return true; - } - } - } - return false; - } - - private boolean compareForDuplicates(@NonNull final Schedule candidate, @NonNull final Schedule requested) { - return candidate.waitForExpiry() == requested.waitForExpiry() - // @todo('9447') This should be modified to use calculated expiration once - // differential testing completes - && candidate.providedExpirationSecond() == requested.providedExpirationSecond() - && Objects.equals(candidate.memo(), requested.memo()) - && Objects.equals(candidate.adminKey(), requested.adminKey()) - // @note We should check scheduler here, but mono doesn't, so we cannot either, yet. - && Objects.equals(candidate.scheduledTransaction(), requested.scheduledTransaction()); - } - - @NonNull - private ScheduleCreateTransactionBody getValidScheduleCreateBody(@Nullable final TransactionBody currentTransaction) - throws PreCheckException { - if (currentTransaction != null) { - final ScheduleCreateTransactionBody scheduleCreateTransaction = currentTransaction.scheduleCreate(); - if (scheduleCreateTransaction != null) { - if (scheduleCreateTransaction.hasScheduledTransactionBody()) { - // this validates the schedulable transaction. - getSchedulableTransaction(currentTransaction); - return scheduleCreateTransaction; - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } - - @Nullable - private Key getKeyForPayerAccount( - @NonNull final ScheduleCreateTransactionBody scheduleBody, @NonNull final PreHandleContext context) - throws PreCheckException { - if (scheduleBody.hasPayerAccountID()) { - final AccountID payerForSchedule = scheduleBody.payerAccountIDOrThrow(); - return getKeyForAccount(context, payerForSchedule); - } else { - return null; - } - } + requireNonNull(context); - @NonNull - private static Key getKeyForAccount(@NonNull final PreHandleContext context, final AccountID accountToQuery) - throws PreCheckException { - final ReadableAccountStore accountStore = context.createStore(ReadableAccountStore.class); - final Account accountData = accountStore.getAccountById(accountToQuery); - if (accountData != null && accountData.key() != null) return accountData.key(); - else throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_PAYER_ID); - } - - @SuppressWarnings("DataFlowIssue") - private void checkSchedulableWhitelistHandle(final Schedule provisionalSchedule, final SchedulingConfig config) - throws HandleException { - final Set whitelist = config.whitelist().functionalitySet(); - final SchedulableTransactionBody scheduled = - provisionalSchedule.originalCreateTransaction().scheduleCreate().scheduledTransactionBody(); - final DataOneOfType transactionType = scheduled.data().kind(); - final HederaFunctionality functionType = HandlerUtility.functionalityForType(transactionType); - if (!whitelist.contains(functionType)) { - throw new HandleException(ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); - } - } - - private void validatePayerAndScheduler( - final PreHandleContext context, final ScheduleCreateTransactionBody scheduleBody) throws PreCheckException { - final ReadableAccountStore accountStore = context.createStore(ReadableAccountStore.class); - final AccountID payerForSchedule = scheduleBody.payerAccountID(); - if (payerForSchedule != null) { - final Account payer = accountStore.getAccountById(payerForSchedule); - if (payer == null) { - throw new PreCheckException(ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST); - } - } - final AccountID schedulerId = context.payer(); - if (schedulerId != null) { - final Account scheduler = accountStore.getAccountById(schedulerId); - if (scheduler == null) { - throw new PreCheckException(ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST); + final var schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); + final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); + final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); + final var expirationSeconds = isLongTermEnabled + ? schedulingConfig.maxExpirationFutureSeconds() + : ledgerConfig.scheduleTxExpiryTimeSecs(); + final var consensusNow = context.consensusNow(); + final var provisionalSchedule = createProvisionalSchedule(context.body(), consensusNow, expirationSeconds); + validateTrue( + isAllowedFunction(provisionalSchedule.scheduledTransactionOrThrow(), schedulingConfig), + SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); + context.attributeValidator().validateMemo(provisionalSchedule.memo()); + context.attributeValidator() + .validateMemo(provisionalSchedule.scheduledTransactionOrThrow().memo()); + if (provisionalSchedule.hasAdminKey()) { + try { + context.attributeValidator().validateKey(provisionalSchedule.adminKeyOrThrow()); + } catch (HandleException e) { + throw new HandleException(INVALID_ADMIN_KEY); } } - } + final var validationResult = validate(provisionalSchedule, consensusNow, isLongTermEnabled); + validateTrue(isMaybeExecutable(validationResult), validationResult); - private void checkSchedulableWhitelist( - @NonNull final ScheduleCreateTransactionBody scheduleCreate, @NonNull final SchedulingConfig config) - throws PreCheckException { - final Set whitelist = config.whitelist().functionalitySet(); - final DataOneOfType transactionType = - scheduleCreate.scheduledTransactionBody().data().kind(); - final HederaFunctionality functionType = HandlerUtility.functionalityForType(transactionType); - if (!whitelist.contains(functionType)) { - throw new PreCheckException(ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); + // Note that we must store the original ScheduleCreate transaction body in the Schedule so + // we can compare those bytes to any new ScheduleCreate transaction for detecting duplicate + // ScheduleCreate transactions. SchedulesByEquality is the virtual map for that task. + final var scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); + final var possibleDuplicates = scheduleStore.getByEquality(provisionalSchedule); + final var duplicate = maybeDuplicate(provisionalSchedule, possibleDuplicates); + if (duplicate != null) { + final var scheduledTxnId = duplicate + .originalCreateTransactionOrThrow() + .transactionIDOrThrow() + .copyBuilder() + .scheduled(true) + .build(); + context.savepointStack() + .getBaseBuilder(ScheduleStreamBuilder.class) + .scheduleID(duplicate.scheduleId()) + .scheduledTransactionID(scheduledTxnId); + throw new HandleException(IDENTICAL_SCHEDULE_ALREADY_CREATED); } - } + validateTrue( + scheduleStore.numSchedulesInState() + 1 <= schedulingConfig.maxNumber(), + MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - private void checkLongTermSchedulable(final ScheduleCreateTransactionBody scheduleCreate) throws PreCheckException { - // @todo('long term schedule') HIP needed?, before enabling long term schedules, add a response code for - // INVALID_LONG_TERM_SCHEDULE and fix this exception. - if (scheduleCreate.waitForExpiry() && !scheduleCreate.hasExpirationTime()) { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION /*INVALID_LONG_TERM_SCHEDULE*/); - } - } - - @NonNull - private SchedulableTransactionBody getSchedulableTransaction(@NonNull final TransactionBody currentTransaction) - throws PreCheckException { - final ScheduleCreateTransactionBody scheduleBody = currentTransaction.scheduleCreate(); - if (scheduleBody != null) { - final SchedulableTransactionBody scheduledTransaction = scheduleBody.scheduledTransactionBody(); - if (scheduledTransaction != null) { - return scheduledTransaction; - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); + // With all validations done, we check if the new schedule is already executable + final var transactionKeys = getTransactionKeysOrThrow(provisionalSchedule, context::allKeysForTransaction); + final var requiredKeys = allRequiredKeys(transactionKeys); + final var signatories = + newSignatories(context.keyVerifier().signingCryptoKeys(), Collections.emptyList(), requiredKeys); + final var schedulingTxnId = + provisionalSchedule.originalCreateTransactionOrThrow().transactionIDOrThrow(); + final var schedulerId = schedulingTxnId.accountIDOrThrow(); + final var scheduleId = ScheduleID.newBuilder() + .shardNum(schedulerId.shardNum()) + .realmNum(schedulerId.realmNum()) + .scheduleNum(context.entityNumGenerator().newEntityNum()) + .build(); + var schedule = provisionalSchedule + .copyBuilder() + .scheduleId(scheduleId) + .schedulerAccountId(schedulerId) + .signatories(signatories) + .build(); + if (tryToExecuteSchedule(context, schedule, requiredKeys, validationResult, isLongTermEnabled)) { + schedule = markedExecuted(schedule, consensusNow); } + scheduleStore.put(schedule); + context.savepointStack() + .getBaseBuilder(ScheduleStreamBuilder.class) + .scheduleID(schedule.scheduleId()) + .scheduledTransactionID(transactionIdForScheduled(schedule)); } @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { requireNonNull(feeContext); - final var op = feeContext.body(); + final var body = feeContext.body(); final var config = feeContext.configuration(); final var ledgerConfig = config.getConfigData(LedgerConfig.class); final var schedulingConfig = config.getConfigData(SchedulingConfig.class); - final var subType = (op.scheduleCreateOrThrow().hasScheduledTransactionBody() - && op.scheduleCreateOrThrow().scheduledTransactionBody().hasContractCall()) - ? SubType.SCHEDULE_CREATE_CONTRACT_CALL - : SubType.DEFAULT; - + final var subType = body.scheduleCreateOrElse(ScheduleCreateTransactionBody.DEFAULT) + .scheduledTransactionBodyOrElse(SchedulableTransactionBody.DEFAULT) + .hasContractCall() + ? SCHEDULE_CREATE_CONTRACT_CALL + : DEFAULT; return feeContext .feeCalculatorFactory() .feeCalculator(subType) .legacyCalculate(sigValueObj -> usageGiven( - fromPbj(op), + fromPbj(body), sigValueObj, schedulingConfig.longTermEnabled(), ledgerConfig.scheduleTxExpiryTimeSecs())); } - public FeeData usageGiven( - final com.hederahashgraph.api.proto.java.TransactionBody txn, - final SigValueObj svo, + private @NonNull FeeData usageGiven( + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txn, + @NonNull final SigValueObj svo, final boolean longTermEnabled, final long scheduledTxExpiryTimeSecs) { final var op = txn.getScheduleCreate(); final var sigUsage = new SigUsage(svo.getTotalSigCount(), svo.getSignatureSize(), svo.getPayerAcctSigCount()); - final long lifetimeSecs; if (op.hasExpirationTime() && longTermEnabled) { lifetimeSecs = Math.max( @@ -393,4 +249,32 @@ public FeeData usageGiven( } return scheduleOpsUsage.scheduleCreateUsage(txn, sigUsage, lifetimeSecs); } + + private @Nullable Schedule maybeDuplicate( + @NonNull final Schedule schedule, @Nullable final List duplicates) { + if (duplicates == null) { + return null; + } + for (final var duplicate : duplicates) { + if (areIdentical(duplicate, schedule)) { + return duplicate; + } + } + return null; + } + + private boolean areIdentical(@NonNull final Schedule candidate, @NonNull final Schedule requested) { + return candidate.waitForExpiry() == requested.waitForExpiry() + && candidate.providedExpirationSecond() == requested.providedExpirationSecond() + && Objects.equals(candidate.memo(), requested.memo()) + && Objects.equals(candidate.adminKey(), requested.adminKey()) + // @note We should check scheduler here, but mono doesn't, so we cannot either, yet. + && Objects.equals(candidate.scheduledTransaction(), requested.scheduledTransaction()); + } + + private boolean isAllowedFunction( + @NonNull final SchedulableTransactionBody body, @NonNull final SchedulingConfig config) { + final var scheduledFunctionality = functionalityForType(body.data().kind()); + return config.whitelist().functionalitySet().contains(scheduledFunctionality); + } } diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandler.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandler.java index dc3d612b6b5e..ea4dfa3e504e 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandler.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandler.java @@ -16,12 +16,19 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SCHEDULE_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_IS_IMMUTABLE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.scheduled.ScheduleDeleteTransactionBody; @@ -35,7 +42,6 @@ import com.hedera.node.app.service.schedule.WritableScheduleStore; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; -import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -46,7 +52,6 @@ import com.hederahashgraph.api.proto.java.FeeData; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.Objects; import javax.inject.Inject; import javax.inject.Singleton; @@ -58,113 +63,74 @@ public class ScheduleDeleteHandler extends AbstractScheduleHandler implements Tr private final ScheduleOpsUsage scheduleOpsUsage = new ScheduleOpsUsage(); @Inject - public ScheduleDeleteHandler() {} - - @Override - public void pureChecks(@Nullable final TransactionBody currentTransaction) throws PreCheckException { - getValidScheduleDeleteBody(currentTransaction); + public ScheduleDeleteHandler() { + // Dagger2 } - @NonNull - private ScheduleDeleteTransactionBody getValidScheduleDeleteBody(@Nullable final TransactionBody currentTransaction) - throws PreCheckException { - if (currentTransaction != null) { - final ScheduleDeleteTransactionBody scheduleDeleteTransaction = currentTransaction.scheduleDelete(); - if (scheduleDeleteTransaction != null) { - if (scheduleDeleteTransaction.scheduleID() != null) { - return scheduleDeleteTransaction; - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); - } + @Override + public void pureChecks(@NonNull final TransactionBody body) throws PreCheckException { + requireNonNull(body); + validateTruePreCheck(body.hasScheduleDelete(), INVALID_TRANSACTION_BODY); + final var op = body.scheduleDeleteOrThrow(); + validateTruePreCheck(op.hasScheduleID(), INVALID_SCHEDULE_ID); } @Override public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final ReadableScheduleStore scheduleStore = context.createStore(ReadableScheduleStore.class); + requireNonNull(context); + final var scheduleStore = context.createStore(ReadableScheduleStore.class); final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); - final TransactionBody currentTransaction = context.body(); - final ScheduleDeleteTransactionBody scheduleDeleteTransaction = getValidScheduleDeleteBody(currentTransaction); - if (scheduleDeleteTransaction.scheduleID() != null) { - final Schedule scheduleData = - preValidate(scheduleStore, isLongTermEnabled, scheduleDeleteTransaction.scheduleID()); - final Key adminKey = scheduleData.adminKey(); - if (adminKey != null) context.requireKey(adminKey); - else throw new PreCheckException(ResponseCodeEnum.SCHEDULE_IS_IMMUTABLE); - // Once deleted or executed, no later transaction will change that status. - if (scheduleData.deleted()) throw new PreCheckException(ResponseCodeEnum.SCHEDULE_ALREADY_DELETED); - if (scheduleData.executed()) throw new PreCheckException(ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED); - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } + final var op = context.body().scheduleDeleteOrThrow(); + final var schedule = getValidated(op.scheduleIDOrThrow(), scheduleStore, isLongTermEnabled); + validateFalse(schedule.deleted(), SCHEDULE_ALREADY_DELETED); + validateFalse(schedule.executed(), SCHEDULE_ALREADY_EXECUTED); + validateTruePreCheck(schedule.hasAdminKey(), SCHEDULE_IS_IMMUTABLE); + context.requireKey(schedule.adminKeyOrThrow()); } @Override public void handle(@NonNull final HandleContext context) throws HandleException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final WritableScheduleStore scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); - final TransactionBody currentTransaction = context.body(); - final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); - try { - final ScheduleDeleteTransactionBody scheduleToDelete = getValidScheduleDeleteBody(currentTransaction); - final ScheduleID idToDelete = scheduleToDelete.scheduleID(); - if (idToDelete != null) { - final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); - final Schedule scheduleData = reValidate(scheduleStore, isLongTermEnabled, idToDelete); - if (scheduleData.hasAdminKey()) { - final SignatureVerification verificationResult = - context.keyVerifier().verificationFor(scheduleData.adminKeyOrThrow()); - if (verificationResult.passed()) { - scheduleStore.delete(idToDelete, context.consensusNow()); - final ScheduleStreamBuilder scheduleRecords = - context.savepointStack().getBaseBuilder(ScheduleStreamBuilder.class); - scheduleRecords.scheduleID(idToDelete); - } else { - throw new HandleException(ResponseCodeEnum.UNAUTHORIZED); - } - } else { - throw new HandleException(ResponseCodeEnum.SCHEDULE_IS_IMMUTABLE); - } - } else { - throw new HandleException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } - } catch (final IllegalStateException ignored) { - throw new HandleException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } catch (final PreCheckException translate) { - throw new HandleException(translate.responseCode()); - } + requireNonNull(context); + final var scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); + final var body = context.body(); + final var op = body.scheduleDeleteOrThrow(); + final var scheduleId = op.scheduleIDOrThrow(); + final var schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); + final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); + final var schedule = revalidateOrThrow(scheduleId, scheduleStore, isLongTermEnabled); + validateTrue(schedule.hasAdminKey(), SCHEDULE_IS_IMMUTABLE); + final var verificationResult = context.keyVerifier().verificationFor(schedule.adminKeyOrThrow()); + validateTrue(verificationResult.passed(), UNAUTHORIZED); + scheduleStore.delete(scheduleId, context.consensusNow()); + context.savepointStack().getBaseBuilder(ScheduleStreamBuilder.class).scheduleID(scheduleId); } /** * Verify that the transaction and schedule still meet the validation criteria expressed in the - * {@link AbstractScheduleHandler#preValidate(ReadableScheduleStore, boolean, ScheduleID)} method. + * {@link AbstractScheduleHandler#getValidated(ScheduleID, ReadableScheduleStore, boolean)} method. + * + * @param scheduleId the Schedule ID of the item to mark as deleted. * @param scheduleStore a Readable source of Schedule data from state * @param isLongTermEnabled a flag indicating if long term scheduling is enabled in configuration. - * @param idToDelete the Schedule ID of the item to mark as deleted. * @return a schedule metadata read from state for the ID given, if all validation checks pass * @throws HandleException if any validation check fails. */ @NonNull - protected Schedule reValidate( + protected Schedule revalidateOrThrow( + @NonNull final ScheduleID scheduleId, @NonNull final ReadableScheduleStore scheduleStore, - final boolean isLongTermEnabled, - @Nullable final ScheduleID idToDelete) + final boolean isLongTermEnabled) throws HandleException { + requireNonNull(scheduleId); + requireNonNull(scheduleStore); try { - final Schedule validSchedule = preValidate(scheduleStore, isLongTermEnabled, idToDelete); - // Once deleted or executed, no later transaction will change that status. - if (validSchedule.deleted()) throw new HandleException(ResponseCodeEnum.SCHEDULE_ALREADY_DELETED); - if (validSchedule.executed()) throw new HandleException(ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED); - return validSchedule; - } catch (final PreCheckException translated) { - throw new HandleException(translated.responseCode()); + final var schedule = getValidated(scheduleId, scheduleStore, isLongTermEnabled); + validateFalse(schedule.deleted(), SCHEDULE_ALREADY_DELETED); + validateFalse(schedule.executed(), SCHEDULE_ALREADY_EXECUTED); + return schedule; + } catch (final PreCheckException e) { + throw new HandleException(e.responseCode()); } } @@ -172,11 +138,10 @@ protected Schedule reValidate( @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { requireNonNull(feeContext); - final var op = feeContext.body(); - final var scheduleStore = feeContext.readableStore(ReadableScheduleStore.class); - final var schedule = scheduleStore.get(op.scheduleDeleteOrThrow().scheduleIDOrThrow()); - + final var op = feeContext.body(); + final var schedule = scheduleStore.get( + op.scheduleDeleteOrElse(ScheduleDeleteTransactionBody.DEFAULT).scheduleIDOrElse(ScheduleID.DEFAULT)); return feeContext .feeCalculatorFactory() .feeCalculator(SubType.DEFAULT) @@ -191,12 +156,11 @@ public Fees calculateFees(@NonNull final FeeContext feeContext) { } private FeeData usageGiven( - final com.hederahashgraph.api.proto.java.TransactionBody txn, - final SigValueObj svo, - final Schedule schedule, + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txn, + @NonNull final SigValueObj svo, + @Nullable final Schedule schedule, final long scheduledTxExpiryTimeSecs) { final var sigUsage = new SigUsage(svo.getTotalSigCount(), svo.getSignatureSize(), svo.getPayerAcctSigCount()); - if (schedule != null) { return scheduleOpsUsage.scheduleDeleteUsage(txn, sigUsage, schedule.calculatedExpirationSecond()); } else { diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandler.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandler.java index 6d2b6e64f669..66b782034cea 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandler.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandler.java @@ -16,19 +16,20 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SCHEDULE_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NO_NEW_VALID_SIGNATURES; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.transactionIdForScheduled; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.SubType; -import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; import com.hedera.hapi.node.scheduled.ScheduleSignTransactionBody; import com.hedera.hapi.node.state.schedule.Schedule; -import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.fees.usage.SigUsage; import com.hedera.node.app.hapi.fees.usage.schedule.ScheduleOpsUsage; @@ -36,7 +37,6 @@ import com.hedera.node.app.service.schedule.ReadableScheduleStore; import com.hedera.node.app.service.schedule.ScheduleStreamBuilder; import com.hedera.node.app.service.schedule.WritableScheduleStore; -import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -49,9 +49,6 @@ import com.hederahashgraph.api.proto.java.FeeData; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Instant; -import java.util.Objects; -import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -59,160 +56,79 @@ * This class contains all workflow-related functionality regarding {@link HederaFunctionality#SCHEDULE_SIGN}. */ @Singleton -@SuppressWarnings("OverlyCoupledClass") public class ScheduleSignHandler extends AbstractScheduleHandler implements TransactionHandler { private final ScheduleOpsUsage scheduleOpsUsage = new ScheduleOpsUsage(); @Inject - public ScheduleSignHandler() {} + public ScheduleSignHandler() { + // Dagger2 + } @Override - public void pureChecks(@Nullable final TransactionBody currentTransaction) throws PreCheckException { - if (currentTransaction != null) { - checkValidTransactionId(currentTransaction.transactionID()); - getValidScheduleSignBody(currentTransaction); - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } + public void pureChecks(@NonNull final TransactionBody body) throws PreCheckException { + requireNonNull(body); + validateTruePreCheck(body.hasScheduleSign(), INVALID_TRANSACTION_BODY); + final var op = body.scheduleSignOrThrow(); + validateTruePreCheck(op.hasScheduleID(), INVALID_SCHEDULE_ID); } - /** - * Pre-handles a {@link HederaFunctionality#SCHEDULE_SIGN} transaction, returning the metadata - * required to, at minimum, validate the signatures of all required signing keys. - * - * @param context the {@link PreHandleContext} which collects all information - * @throws PreCheckException if the transaction cannot be handled successfully. - * The response code appropriate to the failure reason will be provided via this exception. - */ @Override public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final ReadableScheduleStore scheduleStore = context.createStore(ReadableScheduleStore.class); - final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); + requireNonNull(context); + final var op = context.body().scheduleSignOrThrow(); + final var scheduleStore = context.createStore(ReadableScheduleStore.class); + final var schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); - final TransactionBody currentTransaction = context.body(); - final ScheduleSignTransactionBody scheduleSignTransaction = getValidScheduleSignBody(currentTransaction); - if (scheduleSignTransaction.scheduleID() != null) { - final Schedule scheduleData = - preValidate(scheduleStore, isLongTermEnabled, scheduleSignTransaction.scheduleID()); - final AccountID payerAccount = scheduleData.payerAccountId(); - // Note, payer should never be null, but we have to check anyway, because Sonar doesn't know better. - if (payerAccount != null) { - final ReadableAccountStore accountStore = context.createStore(ReadableAccountStore.class); - final Account payer = accountStore.getAccountById(payerAccount); - if (payer != null) { - final Key payerKey = payer.key(); - if (payerKey != null) context.optionalKey(payerKey); - } - } - try { - final Set allKeysNeeded = allKeysForTransaction(scheduleData, context); - context.optionalKeys(allKeysNeeded); - } catch (HandleException translated) { - throw new PreCheckException(translated.getStatus()); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } - // context now has all of the keys required by the scheduled transaction in optional keys + final var schedule = getValidated(op.scheduleIDOrThrow(), scheduleStore, isLongTermEnabled); + final var requiredKeys = getRequiredKeys(schedule, context::allKeysForTransaction); + context.optionalKey(requiredKeys.payerKey()); + context.optionalKeys(requiredKeys.requiredNonPayerKeys()); } - /** - * This method is called during the handle workflow. It executes the actual transaction. - * - * @throws HandleException if the transaction is not handled successfully. - * The response code appropriate to the failure reason will be provided via this exception. - */ - @SuppressWarnings({"FeatureEnvy", "OverlyCoupledMethod"}) @Override public void handle(@NonNull final HandleContext context) throws HandleException { - Objects.requireNonNull(context, NULL_CONTEXT_MESSAGE); - final Instant currentConsensusTime = context.consensusNow(); - final WritableScheduleStore scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); - final SchedulingConfig schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); + requireNonNull(context); + final var op = context.body().scheduleSignOrThrow(); + final var scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class); + // Non-final because we may update signatories and/or mark it as executed before putting it back + var schedule = scheduleStore.getForModify(op.scheduleIDOrThrow()); + + final var consensusNow = context.consensusNow(); + final var schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class); final boolean isLongTermEnabled = schedulingConfig.longTermEnabled(); - final TransactionBody currentTransaction = context.body(); - if (currentTransaction.hasScheduleSign()) { - final ScheduleSignTransactionBody signTransaction = currentTransaction.scheduleSignOrThrow(); - final ScheduleID idToSign = signTransaction.scheduleID(); - final Schedule scheduleData = scheduleStore.get(idToSign); - final ResponseCodeEnum validationResult = validate(scheduleData, currentConsensusTime, isLongTermEnabled); - if (validationOk(validationResult)) { - final Schedule scheduleToSign = scheduleStore.getForModify(idToSign); - // ID to sign will never be null here, but sonar needs this check... - if (scheduleToSign != null && idToSign != null) { - final SchedulableTransactionBody schedulableTransaction = scheduleToSign.scheduledTransaction(); - if (schedulableTransaction != null) { - final ScheduleKeysResult requiredKeysResult = allKeysForTransaction(scheduleToSign, context); - final Set allRequiredKeys = requiredKeysResult.remainingRequiredKeys(); - final Set updatedSignatories = requiredKeysResult.updatedSignatories(); - if (tryToExecuteSchedule( - context, - scheduleToSign, - allRequiredKeys, - updatedSignatories, - validationResult, - isLongTermEnabled)) { - scheduleStore.put(HandlerUtility.replaceSignatoriesAndMarkExecuted( - scheduleToSign, updatedSignatories, currentConsensusTime)); - } else { - verifyHasNewSignatures(scheduleToSign.signatories(), updatedSignatories); - scheduleStore.put(HandlerUtility.replaceSignatories(scheduleToSign, updatedSignatories)); - } - final ScheduleStreamBuilder scheduleRecords = - context.savepointStack().getBaseBuilder(ScheduleStreamBuilder.class); - scheduleRecords.scheduledTransactionID( - HandlerUtility.transactionIdForScheduled(scheduleToSign)); - // Based on fuzzy-record matching this field may not be set in mono-service records - // scheduleRecords.scheduleID(idToSign); - } else { - // Note, this will never happen, but Sonar static analysis can't figure that out. - throw new HandleException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } - } else { - throw new HandleException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } - } else { - throw new HandleException(validationResult); - } - } else { - throw new HandleException(ResponseCodeEnum.INVALID_TRANSACTION); - } - } + final var validationResult = validate(schedule, consensusNow, isLongTermEnabled); + validateTrue(isMaybeExecutable(validationResult), validationResult); - @NonNull - private ScheduleSignTransactionBody getValidScheduleSignBody(@Nullable final TransactionBody currentTransaction) - throws PreCheckException { - if (currentTransaction != null) { - final ScheduleSignTransactionBody scheduleSignTransaction = currentTransaction.scheduleSign(); - if (scheduleSignTransaction != null) { - if (scheduleSignTransaction.scheduleID() != null) { - return scheduleSignTransaction; - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_SCHEDULE_ID); - } - } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } + // With all validations done, we update the signatories on the schedule + final var transactionKeys = getTransactionKeysOrThrow(schedule, context::allKeysForTransaction); + final var requiredKeys = allRequiredKeys(transactionKeys); + final var signatories = schedule.signatories(); + final var newSignatories = newSignatories(context.keyVerifier().signingCryptoKeys(), signatories, requiredKeys); + schedule = schedule.copyBuilder().signatories(newSignatories).build(); + if (tryToExecuteSchedule(context, schedule, requiredKeys, validationResult, isLongTermEnabled)) { + scheduleStore.put(markedExecuted(schedule, consensusNow)); } else { - throw new PreCheckException(ResponseCodeEnum.INVALID_TRANSACTION); + validateTrue(!newSignatories.equals(signatories), NO_NEW_VALID_SIGNATURES); + scheduleStore.put(schedule); } + context.savepointStack() + .getBaseBuilder(ScheduleStreamBuilder.class) + .scheduledTransactionID(transactionIdForScheduled(schedule)); } @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { requireNonNull(feeContext); - final var op = feeContext.body(); - + final var body = feeContext.body(); final var scheduleStore = feeContext.readableStore(ReadableScheduleStore.class); - final var schedule = scheduleStore.get(op.scheduleSignOrThrow().scheduleIDOrThrow()); - + final var schedule = scheduleStore.get( + body.scheduleSignOrElse(ScheduleSignTransactionBody.DEFAULT).scheduleIDOrElse(ScheduleID.DEFAULT)); return feeContext .feeCalculatorFactory() .feeCalculator(SubType.DEFAULT) .legacyCalculate(sigValueObj -> usageGiven( - fromPbj(op), + fromPbj(body), sigValueObj, schedule, feeContext @@ -222,12 +138,11 @@ public Fees calculateFees(@NonNull final FeeContext feeContext) { } private FeeData usageGiven( - final com.hederahashgraph.api.proto.java.TransactionBody txn, - final SigValueObj svo, - final Schedule schedule, + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txn, + @NonNull final SigValueObj svo, + @Nullable final Schedule schedule, final long scheduledTxExpiryTimeSecs) { final var sigUsage = new SigUsage(svo.getTotalSigCount(), svo.getSignatureSize(), svo.getPayerAcctSigCount()); - if (schedule != null) { return scheduleOpsUsage.scheduleSignUsage(txn, sigUsage, schedule.calculatedExpirationSecond()); } else { diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleVerificationAssistant.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleVerificationAssistant.java deleted file mode 100644 index ff30a9031980..000000000000 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleVerificationAssistant.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.schedule.impl.handlers; - -import com.hedera.hapi.node.base.Key; -import com.hedera.node.app.spi.signatures.SignatureVerification; -import com.hedera.node.app.spi.signatures.VerificationAssistant; -import java.util.Set; - -/** - * Verification Assistant that "verifies" keys previously verified via Schedule create or sign transactions. - * This class also observes all primitive keys that are still unverified, and potentially passes those - * on via the side effect of adding them to the sets provided in the constructor, which must be modifiable. - */ -public class ScheduleVerificationAssistant implements VerificationAssistant { - private final Set preValidatedKeys; - private final Set failedPrimitiveKeys; - - /** - * Create a new schedule verification assistant. - * - * @param preValidatedKeys a modifiable {@code Set} of primitive keys previously verified. - * @param failedPrimitiveKeys an empty and modifiable {@code Set} to receive a list of - * primitive keys that are still unverified. - */ - public ScheduleVerificationAssistant(final Set preValidatedKeys, Set failedPrimitiveKeys) { - this.preValidatedKeys = preValidatedKeys; - this.failedPrimitiveKeys = failedPrimitiveKeys; - } - - @Override - public boolean test(final Key key, final SignatureVerification priorVerify) { - if (key.hasKeyList() || key.hasThresholdKey() || key.hasContractID() || key.hasDelegatableContractId()) { - return priorVerify.passed(); - } else { - final boolean isValid = priorVerify.passed() || preValidatedKeys.contains(key); - if (!isValid) { - failedPrimitiveKeys.add(key); - } else if (priorVerify.passed()) { - preValidatedKeys.add(key); - } - return isValid; - } - } -} diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/module-info.java b/hedera-node/hedera-schedule-service-impl/src/main/java/module-info.java index 5c707dc1d6a0..ae3d52e1be9f 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/module-info.java @@ -1,6 +1,5 @@ module com.hedera.node.app.service.schedule.impl { requires transitive com.hedera.node.app.hapi.fees; - requires transitive com.hedera.node.app.hapi.utils; requires transitive com.hedera.node.app.service.schedule; requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.hapi; @@ -10,6 +9,7 @@ requires transitive dagger; requires transitive static java.compiler; // javax.annotation.processing.Generated requires transitive javax.inject; + requires com.hedera.node.app.hapi.utils; requires com.hedera.node.app.service.token; // ReadableAccountStore: payer account details on create, sign, query requires com.hedera.node.config; requires com.google.common; diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandlerTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandlerTest.java deleted file mode 100644 index 22762832afaf..000000000000 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/AbstractScheduleHandlerTest.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.schedule.impl.handlers; - -import static org.assertj.core.api.BDDAssertions.assertThat; -import static org.assertj.core.api.BDDAssertions.assertThatNoException; -import static org.assertj.core.api.BDDAssertions.assertThatThrownBy; -import static org.mockito.Mockito.any; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; -import com.hedera.hapi.node.base.ScheduleID; -import com.hedera.hapi.node.base.TransactionID; -import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; -import com.hedera.hapi.node.state.schedule.Schedule; -import com.hedera.hapi.node.transaction.TransactionBody; -import com.hedera.node.app.service.schedule.impl.handlers.AbstractScheduleHandler.ScheduleKeysResult; -import com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory; -import com.hedera.node.app.spi.workflows.HandleException; -import com.hedera.node.app.spi.workflows.PreCheckException; -import com.hedera.node.app.spi.workflows.PreHandleContext; -import com.hedera.node.app.spi.workflows.TransactionKeys; -import com.hedera.node.app.workflows.handle.record.RecordStreamBuilder; -import com.hedera.node.app.workflows.prehandle.PreHandleContextImpl; -import java.security.InvalidKeyException; -import java.time.Instant; -import java.util.Set; -import java.util.function.Predicate; -import org.assertj.core.api.Condition; -import org.assertj.core.api.ThrowableAssert.ThrowingCallable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.BDDMockito; -import org.mockito.Mockito; - -class AbstractScheduleHandlerTest extends ScheduleHandlerTestBase { - private static final SchedulableTransactionBody NULL_SCHEDULABLE_BODY = null; - private AbstractScheduleHandler testHandler; - private PreHandleContext realPreContext; - - @BeforeEach - void setUp() throws PreCheckException, InvalidKeyException { - setUpBase(); - testHandler = new TestScheduleHandler(); - } - - @Test - void validationOkReturnsSuccessForOKAndPending() { - for (final ResponseCodeEnum testValue : ResponseCodeEnum.values()) { - switch (testValue) { - case SUCCESS, OK, SCHEDULE_PENDING_EXPIRATION -> assertThat(testHandler.validationOk(testValue)) - .isTrue(); - default -> assertThat(testHandler.validationOk(testValue)).isFalse(); - } - } - } - - @Test - void preValidateVerifiesSchedulableAndID() throws PreCheckException { - // missing schedule or schedule ID should throw invalid ID - assertThatThrownBy(() -> testHandler.preValidate(scheduleStore, false, null)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_SCHEDULE_ID)); - reset(writableById); - scheduleMapById.put(testScheduleID, null); - assertThatThrownBy(() -> testHandler.preValidate(scheduleStore, false, testScheduleID)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_SCHEDULE_ID)); - for (final Schedule next : listOfScheduledOptions) { - final ScheduleID testId = next.scheduleId(); - reset(writableById); - // valid schedules should not throw - scheduleMapById.put(testId, next); - assertThatNoException().isThrownBy(() -> testHandler.preValidate(scheduleStore, false, testId)); - // scheduled without scheduled transaction should throw invalid transaction - final Schedule missingScheduled = next.copyBuilder() - .scheduledTransaction(NULL_SCHEDULABLE_BODY) - .build(); - reset(writableById); - scheduleMapById.put(testId, missingScheduled); - assertThatThrownBy(() -> testHandler.preValidate(scheduleStore, false, testId)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_TRANSACTION)); - // Non-success codes returned by validate should become exceptions - reset(writableById); - scheduleMapById.put(testId, next.copyBuilder().executed(true).build()); - assertThatThrownBy(() -> testHandler.preValidate(scheduleStore, false, testId)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED)); - - reset(writableById); - scheduleMapById.put(testId, next.copyBuilder().deleted(true).build()); - assertThatThrownBy(() -> testHandler.preValidate(scheduleStore, false, testId)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.SCHEDULE_ALREADY_DELETED)); - } - } - - @Test - void validateVerifiesExecutionDeletionAndExpiration() { - assertThat(testHandler.validate(null, testConsensusTime, false)) - .isEqualTo(ResponseCodeEnum.INVALID_SCHEDULE_ID); - for (final Schedule next : listOfScheduledOptions) { - assertThat(testHandler.validate(next, testConsensusTime, false)).isEqualTo(ResponseCodeEnum.OK); - assertThat(testHandler.validate(next, testConsensusTime, true)).isEqualTo(ResponseCodeEnum.OK); - Schedule failures = next.copyBuilder() - .scheduledTransaction(NULL_SCHEDULABLE_BODY) - .build(); - assertThat(testHandler.validate(failures, testConsensusTime, false)) - .isEqualTo(ResponseCodeEnum.INVALID_TRANSACTION); - failures = next.copyBuilder().executed(true).build(); - assertThat(testHandler.validate(failures, testConsensusTime, false)) - .isEqualTo(ResponseCodeEnum.SCHEDULE_ALREADY_EXECUTED); - failures = next.copyBuilder().deleted(true).build(); - assertThat(testHandler.validate(failures, testConsensusTime, false)) - .isEqualTo(ResponseCodeEnum.SCHEDULE_ALREADY_DELETED); - final Instant consensusAfterExpiration = Instant.ofEpochSecond(next.calculatedExpirationSecond() + 5); - assertThat(testHandler.validate(next, consensusAfterExpiration, false)) - .isEqualTo(ResponseCodeEnum.INVALID_SCHEDULE_ID); - assertThat(testHandler.validate(next, consensusAfterExpiration, true)) - .isEqualTo(ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION); - } - } - - @Test - void verifyCheckTxnId() { - assertThatThrownBy(new CallCheckValid(null, testHandler)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_TRANSACTION_ID)); - for (final Schedule next : listOfScheduledOptions) { - final TransactionID idToTest = next.originalCreateTransaction().transactionID(); - assertThatNoException().isThrownBy(new CallCheckValid(idToTest, testHandler)); - TransactionID brokenId = idToTest.copyBuilder().scheduled(true).build(); - assertThatThrownBy(new CallCheckValid(brokenId, testHandler)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST)); - brokenId = idToTest.copyBuilder().accountID((AccountID) null).build(); - assertThatThrownBy(new CallCheckValid(brokenId, testHandler)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_SCHEDULE_PAYER_ID)); - brokenId = idToTest.copyBuilder().transactionValidStart(nullTime).build(); - assertThatThrownBy(new CallCheckValid(brokenId, testHandler)) - .is(new PreCheckExceptionMatch(ResponseCodeEnum.INVALID_TRANSACTION_START)); - } - } - - @Test - void verifyKeysForPreHandle() throws PreCheckException { - // Run through the "standard" schedules to ensure we handle the common cases - for (final Schedule next : listOfScheduledOptions) { - realPreContext = new PreHandleContextImpl( - mockStoreFactory, next.originalCreateTransaction(), testConfig, mockDispatcher); - Set keysObtained = testHandler.allKeysForTransaction(next, realPreContext); - // Should have no keys, because the mock dispatcher returns no keys - assertThat(keysObtained).isEmpty(); - } - // One check with a complex set of key returns, to ensure we process required and optional correctly. - final TransactionKeys testKeys = - new TestTransactionKeys(schedulerKey, Set.of(payerKey, adminKey), Set.of(optionKey, otherKey)); - // Must spy the context for this, the real dispatch would require calling other service handlers - PreHandleContext spyableContext = new PreHandleContextImpl( - mockStoreFactory, scheduleInState.originalCreateTransaction(), testConfig, mockDispatcher); - PreHandleContext spiedContext = BDDMockito.spy(spyableContext); - // given...return fails because it calls the real method before it can be replaced. - BDDMockito.doReturn(testKeys).when(spiedContext).allKeysForTransaction(any(), any()); - final Set keysObtained = testHandler.allKeysForTransaction(scheduleInState, spiedContext); - assertThat(keysObtained).isNotEmpty(); - assertThat(keysObtained).containsExactly(adminKey, optionKey, otherKey, payerKey); - } - - @Test - void verifyKeysForHandle() throws PreCheckException { - final TransactionKeys testKeys = - new TestTransactionKeys(schedulerKey, Set.of(payerKey, adminKey), Set.of(optionKey, schedulerKey)); - BDDMockito.given(mockContext.allKeysForTransaction(any(), any())).willReturn(testKeys); - final AccountID payerAccountId = schedulerAccount.accountId(); - BDDMockito.given(mockContext.payer()).willReturn(payerAccountId); - // This is how you get side-effects replicated, by having the "Answer" called in place of the real method. - BDDMockito.given(keyVerifier.verificationFor(any(), any())).will(new VerificationForAnswer(testKeys)); - // For this test, Context must mock `payer()`, `allKeysForTransaction()`, and `verificationFor` - // `verificationFor` is needed because we check verification in allKeysForTransaction to reduce - // the required keys set (potentially to empty) during handle. We must use an "Answer" for verification - // because verificationFor relies on side-effects for important results. - // Run through the "standard" schedules to ensure we handle the common cases - for (final Schedule next : listOfScheduledOptions) { - final ScheduleKeysResult verificationResult = testHandler.allKeysForTransaction(next, mockContext); - final Set keysRequired = verificationResult.remainingRequiredKeys(); - final Set keysObtained = verificationResult.updatedSignatories(); - // we *mock* verificationFor side effects, which is what fills in/clears the sets, - // so results should all be the same, despite empty signatories and mocked HandleContext. - // We do so based on verifier calls, so it still exercises the code to be tested, however. - // @todo('9447') add the schedulerKey back in. - // Note, however, we exclude the schedulerKey because it paid for the original create, so it - // is "deemed valid" and not included. - assertThat(keysRequired).isNotEmpty().hasSize(1).containsExactly(optionKey); - assertThat(keysObtained).isNotEmpty().hasSize(2).containsExactly(adminKey, payerKey); - } - } - - @SuppressWarnings("unchecked") - @Test - void verifyTryExecute() { - final var mockRecordBuilder = Mockito.mock(RecordStreamBuilder.class); - BDDMockito.given(mockContext.dispatchChildTransaction( - any(TransactionBody.class), - any(), - any(Predicate.class), - any(AccountID.class), - any(TransactionCategory.class), - any())) - .willReturn(mockRecordBuilder); - for (final Schedule testItem : listOfScheduledOptions) { - Set testRemaining = Set.of(); - final Set testSignatories = Set.of(adminKey, payerKey); - BDDMockito.given(mockRecordBuilder.status()).willReturn(ResponseCodeEnum.OK); - ResponseCodeEnum priorResponse = ResponseCodeEnum.SUCCESS; - assertThat(testHandler.tryToExecuteSchedule( - mockContext, testItem, testRemaining, testSignatories, priorResponse, false)) - .isTrue(); - priorResponse = ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION; - assertThat(testHandler.tryToExecuteSchedule( - mockContext, testItem, testRemaining, testSignatories, priorResponse, false)) - .isTrue(); - priorResponse = ResponseCodeEnum.SCHEDULE_PENDING_EXPIRATION; - assertThat(testHandler.tryToExecuteSchedule( - mockContext, testItem, testRemaining, testSignatories, priorResponse, true)) - .isFalse(); - BDDMockito.given(mockRecordBuilder.status()).willReturn(ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE); - assertThatNoException() - .isThrownBy(() -> testHandler.tryToExecuteSchedule( - mockContext, testItem, testRemaining, testSignatories, ResponseCodeEnum.OK, false)); - } - } - - // Callable required by AssertJ throw assertions; unavoidable due to limitations on lambda syntax. - private static final class CallCheckValid implements ThrowingCallable { - private final TransactionID idToTest; - private final AbstractScheduleHandler testHandler; - - CallCheckValid(final TransactionID idToTest, final AbstractScheduleHandler testHandler) { - this.idToTest = idToTest; - this.testHandler = testHandler; - } - - @Override - public void call() throws PreCheckException { - testHandler.checkValidTransactionId(idToTest); - } - } - - private static final class TestScheduleHandler extends AbstractScheduleHandler {} - - private static final class PreCheckExceptionMatch extends Condition { - private final ResponseCodeEnum codeToMatch; - - PreCheckExceptionMatch(final ResponseCodeEnum codeToMatch) { - this.codeToMatch = codeToMatch; - } - - @Override - public boolean matches(final Throwable thrown) { - return thrown instanceof PreCheckException e && e.responseCode() == codeToMatch; - } - } - - private static final class HandleExceptionMatch extends Condition { - private final ResponseCodeEnum codeToMatch; - - HandleExceptionMatch(final ResponseCodeEnum codeToMatch) { - this.codeToMatch = codeToMatch; - } - - @Override - public boolean matches(final Throwable thrown) { - return thrown instanceof HandleException e && e.getStatus() == codeToMatch; - } - } -} diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicateTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicateTest.java deleted file mode 100644 index 5d6126607bb4..000000000000 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/DispatchPredicateTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.schedule.impl.handlers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.KeyList; -import com.hedera.hapi.node.base.ThresholdKey; -import com.hedera.pbj.runtime.io.buffer.Bytes; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Function; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class DispatchPredicateTest { - private static final String A_NAME = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - private static final String B_NAME = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; - private static final String C_NAME = "cccccccccccccccccccccccccccccccc"; - private static final Function KEY_BUILDER = - value -> Key.newBuilder().ed25519(Bytes.wrap(value.getBytes())); - public static final Key A_THRESHOLD_KEY = Key.newBuilder() - .thresholdKey(ThresholdKey.newBuilder() - .threshold(2) - .keys(KeyList.newBuilder() - .keys( - KEY_BUILDER.apply(A_NAME).build(), - KEY_BUILDER.apply(B_NAME).build(), - KEY_BUILDER.apply(C_NAME).build()) - .build())) - .build(); - public static final Key A_COMPLEX_KEY = Key.newBuilder() - .thresholdKey(ThresholdKey.newBuilder() - .threshold(2) - .keys(KeyList.newBuilder() - .keys( - KEY_BUILDER.apply(A_NAME).build(), - KEY_BUILDER.apply(B_NAME).build(), - A_THRESHOLD_KEY))) - .build(); - public static final Key B_COMPLEX_KEY = Key.newBuilder() - .thresholdKey(ThresholdKey.newBuilder() - .threshold(2) - .keys(KeyList.newBuilder() - .keys( - KEY_BUILDER.apply(A_NAME).build(), - KEY_BUILDER.apply(B_NAME).build(), - A_COMPLEX_KEY))) - .build(); - - private Set validKeys; - private DispatchPredicate predicate; - - @BeforeEach - void setUp() { - validKeys = new HashSet<>(); - validKeys.add(A_COMPLEX_KEY); - validKeys.add(A_THRESHOLD_KEY); - predicate = new DispatchPredicate(validKeys); - } - - @Test - @DisplayName("Testing Constructor") - void testConstructor() { - assertThat(predicate).isNotNull(); - DispatchPredicate dispatchPredicate = new DispatchPredicate(validKeys); - assertThat(predicate).isNotEqualTo(dispatchPredicate); - assertThatThrownBy(() -> new DispatchPredicate(null)).isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("Test for when predicate contains keys") - void testContainsKey() { - assertThat(predicate.test(A_COMPLEX_KEY)).isTrue(); - assertThat(predicate.test(B_COMPLEX_KEY)).isFalse(); - } - - @Test - @DisplayName("Test for when predicate is missing keys") - void testContainsKeyIsNotNull() { - assertThatThrownBy(() -> predicate.test(null)).isInstanceOf(NullPointerException.class); - } -} diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtilityTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtilityTest.java index 48036cbd4b35..b2811dea1f46 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtilityTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtilityTest.java @@ -19,8 +19,6 @@ import static org.assertj.core.api.BDDAssertions.assertThat; import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.scheduled.SchedulableTransactionBody.DataOneOfType; import com.hedera.hapi.node.state.schedule.Schedule; @@ -31,8 +29,6 @@ import java.time.Instant; import java.util.Collection; import java.util.Collections; -import java.util.Set; -import org.assertj.core.api.BDDAssertions; import org.assertj.core.api.Condition; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -66,77 +62,6 @@ void functionalityForTypeHandlesAllTypes() { } } - @Test - void markExecutedModifiesSchedule() { - // The utility method call should modify the return only by marking it executed, and - // setting resolution time. - // No other value should change, and the original Schedule should not change. - for (final Schedule expected : listOfScheduledOptions) { - final Schedule marked = HandlerUtility.markExecuted(expected, testConsensusTime); - assertThat(expected.executed()).isFalse(); - assertThat(marked.executed()).isTrue(); - assertThat(expected.hasResolutionTime()).isFalse(); - assertThat(marked.hasResolutionTime()).isTrue(); - assertThat(marked.resolutionTime()).isEqualTo(timestampFromInstant(testConsensusTime)); - - assertThat(marked.deleted()).isEqualTo(expected.deleted()); - assertThat(marked.signatories()).containsExactlyElementsOf(expected.signatories()); - - verifyPartialEquality(marked, expected); - assertThat(marked.scheduleId()).isEqualTo(expected.scheduleId()); - } - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @Test - void replaceSignatoriesModifiesSchedule() { - // The utility method call should modify the return only by replacing signatories. - // No other value should change, and the original Schedule should not change. - final Set fakeSignatories = Set.of(schedulerKey, adminKey); - for (final Schedule expected : listOfScheduledOptions) { - final Schedule modified = HandlerUtility.replaceSignatories(expected, fakeSignatories); - // AssertJ is terrible at inverse conditions, and the condition definitions are REALLY bad - // Too much effort and confusing syntax for something that should be - // "assertThat(modified.signatories()).not().containsExactlyInAnyOrderElementsOf(expected.signatories())" - final var signatoryCondition = new ContainsAllElements(expected.signatories()); - assertThat(modified.signatories()).is(BDDAssertions.not(signatoryCondition)); - assertThat(modified.signatories()).containsExactlyInAnyOrderElementsOf(fakeSignatories); - - assertThat(modified.executed()).isEqualTo(expected.executed()); - assertThat(modified.resolutionTime()).isEqualTo(expected.resolutionTime()); - assertThat(modified.deleted()).isEqualTo(expected.deleted()); - - verifyPartialEquality(modified, expected); - assertThat(modified.scheduleId()).isEqualTo(expected.scheduleId()); - } - } - - @Test - void replaceSignatoriesAndMarkExecutedMakesBothModifications() { - // The utility method call should modify the return only by replacing signatories and setting executed to true. - // No other value should change, and the original Schedule should not change. - final Set fakeSignatories = Set.of(payerKey, adminKey); - for (final Schedule expected : listOfScheduledOptions) { - final Schedule modified = - HandlerUtility.replaceSignatoriesAndMarkExecuted(expected, fakeSignatories, testConsensusTime); - - // AssertJ is terrible at inverse conditions, and the condition definitions are REALLY bad - // Too much effort and confusing syntax for something that should be as simple as - // "assertThat(modified.signatories()).not().containsExactlyInAnyOrderElementsOf(expected.signatories())" - final ContainsAllElements signatoryCondition = new ContainsAllElements<>(expected.signatories()); - assertThat(modified.signatories()).is(BDDAssertions.not(signatoryCondition)); - assertThat(modified.signatories()).containsExactlyInAnyOrderElementsOf(fakeSignatories); - - assertThat(modified.executed()).isTrue(); - assertThat(modified.resolutionTime()).isNotEqualTo(expected.resolutionTime()); - assertThat(modified.resolutionTime()).isEqualTo(timestampFromInstant(testConsensusTime)); - assertThat(modified.deleted()).isEqualTo(expected.deleted()); - - verifyPartialEquality(modified, expected); - assertThat(modified.scheduleId()).isEqualTo(expected.scheduleId()); - } - } - @Test void createProvisionalScheduleCreatesCorrectSchedule() { // Creating a provisional schedule should produce the expected Schedule except for Schedule ID. @@ -160,32 +85,6 @@ void createProvisionalScheduleCreatesCorrectSchedule() { } } - @Test - void completeProvisionalScheduleModifiesWithNewId() { - final Set fakeSignatories = Set.of(payerKey, adminKey, schedulerKey); - final long testEntityNumber = 1791L; - // Completing a provisional schedule should produce the exact same Schedule except for Schedule ID. - for (final Schedule expected : listOfScheduledOptions) { - final TransactionBody createTransaction = expected.originalCreateTransaction(); - final AccountID baseId = createTransaction.transactionID().accountID(); - final ScheduleID expectedId = new ScheduleID(baseId.shardNum(), baseId.realmNum(), testEntityNumber); - final long maxLifeSeconds = scheduleConfig.maxExpirationFutureSeconds(); - final Schedule provisional = - HandlerUtility.createProvisionalSchedule(createTransaction, testConsensusTime, maxLifeSeconds); - final Schedule completed = - HandlerUtility.completeProvisionalSchedule(provisional, testEntityNumber, fakeSignatories); - - assertThat(completed.scheduleId()).isNotEqualTo(provisional.scheduleId()); - assertThat(completed.scheduleId()).isEqualTo(expectedId); - assertThat(completed.executed()).isEqualTo(provisional.executed()); - assertThat(completed.deleted()).isEqualTo(provisional.deleted()); - assertThat(completed.resolutionTime()).isEqualTo(provisional.resolutionTime()); - assertThat(completed.signatories()).containsExactlyElementsOf(fakeSignatories); - - verifyPartialEquality(completed, provisional); - } - } - /** * Verify that "actual" is equal to "expected" with respect to almost all values. *

    The following attributes are not verified here: diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandlerTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandlerTest.java index ce6331b35c03..81d0f3cf48a3 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandlerTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleCreateHandlerTest.java @@ -16,7 +16,10 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST; +import static com.hedera.hapi.node.base.ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST; import static org.assertj.core.api.BDDAssertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -24,7 +27,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TransactionID; @@ -37,6 +39,7 @@ import com.hedera.node.app.signature.impl.SignatureVerificationImpl; import com.hedera.node.app.spi.fixtures.Assertions; import com.hedera.node.app.spi.ids.EntityNumGenerator; +import com.hedera.node.app.spi.key.KeyComparator; import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -45,6 +48,7 @@ import java.security.InvalidKeyException; import java.time.InstantSource; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; @@ -113,8 +117,7 @@ void preHandleMissingPayerThrowsInvalidPayer() throws PreCheckException { final TransactionBody createBody = scheduleCreateTransaction(payer); realPreContext = new PreHandleContextImpl(mockStoreFactory, createBody, testConfig, mockDispatcher); - Assertions.assertThrowsPreCheck( - () -> subject.preHandle(realPreContext), ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST); + Assertions.assertThrowsPreCheck(() -> subject.preHandle(realPreContext), ACCOUNT_ID_DOES_NOT_EXIST); } @Test @@ -132,8 +135,7 @@ void preHandleRejectsNonWhitelist() throws PreCheckException { assertThat(realPreContext.payerKey()).isNotNull().isEqualTo(schedulerKey); } else { Assertions.assertThrowsPreCheck( - () -> subject.preHandle(realPreContext), - ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); + () -> subject.preHandle(realPreContext), SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); } } } @@ -142,46 +144,14 @@ void preHandleRejectsNonWhitelist() throws PreCheckException { void handleRejectsDuplicateTransaction() throws PreCheckException { final TransactionBody createTransaction = otherScheduleInState.originalCreateTransaction(); prepareContext(createTransaction, otherScheduleInState.scheduleId().scheduleNum() + 1); - throwsHandleException(() -> subject.handle(mockContext), ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED); - } - - @Test - void verifyPureChecks() throws PreCheckException { - final TransactionBody.Builder failures = alternateCreateTransaction.copyBuilder(); - final TransactionID originalId = alternateCreateTransaction.transactionID(); - Assertions.assertThrowsPreCheck(() -> subject.pureChecks(null), ResponseCodeEnum.INVALID_TRANSACTION_BODY); - failures.transactionID(nullTransactionId); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_TRANSACTION_ID); - TransactionID.Builder idErrors = originalId.copyBuilder().scheduled(true); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); - idErrors = originalId.copyBuilder().transactionValidStart(nullTime); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_TRANSACTION_START); - idErrors = originalId.copyBuilder().accountID(nullAccount); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_SCHEDULE_PAYER_ID); - failures.transactionID(originalId); - setLongTermError(failures, alternateCreateTransaction); - // The code here should be INVALID_LONG_TERM_SCHEDULE when/if that is added to response codes. - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_TRANSACTION); - } - - private void setLongTermError(final TransactionBody.Builder failures, final TransactionBody original) { - final var createBuilder = original.scheduleCreate().copyBuilder(); - createBuilder.waitForExpiry(true).expirationTime(nullTime); - failures.scheduleCreate(createBuilder); + throwsHandleException(() -> subject.handle(mockContext), IDENTICAL_SCHEDULE_ALREADY_CREATED); } @Test void handleRejectsNonWhitelist() throws HandleException, PreCheckException { final Set configuredWhitelist = scheduleConfig.whitelist().functionalitySet(); + given(keyVerifier.signingCryptoKeys()).willReturn(new ConcurrentSkipListSet<>(new KeyComparator())); for (final Schedule next : listOfScheduledOptions) { final TransactionBody createTransaction = next.originalCreateTransaction(); final TransactionID createId = createTransaction.transactionID(); @@ -194,8 +164,7 @@ void handleRejectsNonWhitelist() throws HandleException, PreCheckException { subject.handle(mockContext); verifyHandleSucceededForWhitelist(next, createId, startCount); } else { - throwsHandleException( - () -> subject.handle(mockContext), ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); + throwsHandleException(() -> subject.handle(mockContext), SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); } } } @@ -244,6 +213,7 @@ void handleExecutesImmediateIfPossible() throws HandleException, PreCheckExcepti // all keys are "valid" with this mock setup given(keyVerifier.verificationFor(BDDMockito.any(Key.class), BDDMockito.any(VerificationAssistant.class))) .willReturn(new SignatureVerificationImpl(nullKey, null, true)); + given(keyVerifier.signingCryptoKeys()).willReturn(new ConcurrentSkipListSet<>(new KeyComparator())); final int startCount = scheduleMapById.size(); if (configuredWhitelist.contains(functionType)) { subject.handle(mockContext); diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandlerTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandlerTest.java index 96d4e153b91f..9c8c899fd811 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandlerTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleDeleteHandlerTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.BDDAssertions.assertThat; import static org.mockito.BDDMockito.given; @@ -88,19 +89,6 @@ void failsIfScheduleIsImmutable() throws PreCheckException { () -> subject.preHandle(realPreContext), ResponseCodeEnum.SCHEDULE_IS_IMMUTABLE); } - @Test - void verifyPureChecks() throws PreCheckException { - final TransactionBody originalDelete = scheduleDeleteTransaction(testScheduleID); - final TransactionBody.Builder failures = originalDelete.copyBuilder(); - Assertions.assertThrowsPreCheck(() -> subject.pureChecks(null), ResponseCodeEnum.INVALID_TRANSACTION); - final var deleteBuilder = originalDelete.scheduleDelete().copyBuilder().scheduleID(nullScheduleId); - failures.scheduleDelete(deleteBuilder); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_SCHEDULE_ID); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(originalCreateTransaction), ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } - @Test void verifySimpleDelete() throws PreCheckException { final Schedule beforeDelete = scheduleStore.get(testScheduleID); @@ -132,7 +120,7 @@ void verifyHandleExceptionsForDelete() throws PreCheckException { final TransactionBody.Builder nextFailure = baseDelete.copyBuilder(); failures.scheduleID(nullScheduleId); prepareContext(nextFailure.scheduleDelete(failures).build()); - throwsHandleException(() -> subject.handle(mockContext), ResponseCodeEnum.INVALID_SCHEDULE_ID); + assertThatThrownBy(() -> subject.handle(mockContext)).isInstanceOf(NullPointerException.class); final Schedule failBase = listOfScheduledOptions.get(3); final Schedule noAdmin = failBase.copyBuilder().adminKey(nullKey).build(); diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandlerTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandlerTest.java index bd7fabb863b3..cd78bb53fd7c 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandlerTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/ScheduleSignHandlerTest.java @@ -27,6 +27,7 @@ import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.spi.fixtures.Assertions; +import com.hedera.node.app.spi.key.KeyComparator; import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -38,6 +39,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; @@ -83,36 +85,6 @@ void vanillaWithOptionalPayerSet() throws PreCheckException { assertThat(realPreContext.optionalNonPayerKeys()).isNotEqualTo(Collections.emptySet()); } - @Test - void verifyPureChecks() throws PreCheckException { - final TransactionBody originalSign = scheduleSignTransaction(null); - final TransactionBody.Builder failures = originalSign.copyBuilder(); - final TransactionID originalId = originalSign.transactionID(); - Assertions.assertThrowsPreCheck(() -> subject.pureChecks(null), ResponseCodeEnum.INVALID_TRANSACTION_BODY); - failures.transactionID(nullTransactionId); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_TRANSACTION_ID); - TransactionID.Builder idErrors = originalId.copyBuilder().scheduled(true); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST); - idErrors = originalId.copyBuilder().transactionValidStart(nullTime); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_TRANSACTION_START); - idErrors = originalId.copyBuilder().accountID(nullAccount); - failures.transactionID(idErrors); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_SCHEDULE_PAYER_ID); - failures.transactionID(originalId); - final var signBuilder = originalSign.scheduleSign().copyBuilder().scheduleID(nullScheduleId); - failures.scheduleSign(signBuilder); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(failures.build()), ResponseCodeEnum.INVALID_SCHEDULE_ID); - Assertions.assertThrowsPreCheck( - () -> subject.pureChecks(originalCreateTransaction), ResponseCodeEnum.INVALID_TRANSACTION_BODY); - } - @Test void verifySignatoriesAreUpdatedWithoutExecution() throws PreCheckException { int successCount = 0; @@ -139,10 +111,6 @@ void verifyErrorConditions() throws PreCheckException { prepareContext(signTransaction); throwsHandleException(() -> subject.handle(mockContext), ResponseCodeEnum.INVALID_SCHEDULE_ID); - // verify we fail when the wrong transaction type is sent - prepareContext(alternateCreateTransaction); - throwsHandleException(() -> subject.handle(mockContext), ResponseCodeEnum.INVALID_TRANSACTION); - // verify we fail a sign for a deleted transaction. // Use an arbitrary schedule from the big list for this. Schedule deleteTest = listOfScheduledOptions.get(3); @@ -175,7 +143,7 @@ void handleExecutesImmediateIfPossible() throws HandleException, PreCheckExcepti private void verifyAllSignatories(final Schedule original, final TransactionKeys expectedKeys) { final Set combinedSet = new LinkedHashSet<>(5); combinedSet.addAll(expectedKeys.requiredNonPayerKeys()); - combinedSet.addAll(expectedKeys.optionalNonPayerKeys()); + combinedSet.add(expectedKeys.payerKey()); verifySignatorySet(original, combinedSet); } @@ -202,10 +170,13 @@ private Set prepareContext(final TransactionBody signTransaction) throws Pr // We leave out "other" key from the "valid" keys for that reason. final Set acceptedKeys = Set.of(payerKey, optionKey); final TestTransactionKeys accepted = new TestTransactionKeys(payerKey, acceptedKeys, Collections.emptySet()); + final var signingSet = new ConcurrentSkipListSet<>(new KeyComparator()); + signingSet.addAll(acceptedKeys); + given(keyVerifier.signingCryptoKeys()).willReturn(signingSet); // This is how you get side-effects replicated, by having the "Answer" called in place of the real method. given(keyVerifier.verificationFor(BDDMockito.any(Key.class), BDDMockito.any(VerificationAssistant.class))) .will(new VerificationForAnswer(accepted)); - return acceptedKeys; // return the expected set of signatories after the transaction is handled. + return Set.of(payerKey); // return the expected set of signatories after the transaction is handled. } private void prepareContextAllPass(final TransactionBody signTransaction) throws PreCheckException { @@ -213,6 +184,9 @@ private void prepareContextAllPass(final TransactionBody signTransaction) throws given(mockContext.allKeysForTransaction(Mockito.any(), Mockito.any())).willReturn(testChildKeys); // for signature verification to succeed, the "Answer" needs to be "valid" for all keys final Set allKeys = Set.of(payerKey, adminKey, schedulerKey, optionKey, otherKey); + final var signingSet = new ConcurrentSkipListSet<>(new KeyComparator()); + signingSet.addAll(allKeys); + given(keyVerifier.signingCryptoKeys()).willReturn(signingSet); final TestTransactionKeys allRequired = new TestTransactionKeys(payerKey, allKeys, Collections.emptySet()); // This is how you get side-effects replicated, by having the "Answer" called in place of the real method. given(keyVerifier.verificationFor(BDDMockito.any(Key.class), BDDMockito.any(VerificationAssistant.class))) diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/TestTransactionKeys.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/TestTransactionKeys.java index 723c377b374f..a2f85231a3c7 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/TestTransactionKeys.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/handlers/TestTransactionKeys.java @@ -16,6 +16,8 @@ package com.hedera.node.app.service.schedule.impl.handlers; +import static java.util.Collections.emptySet; + import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.state.token.Account; import com.hedera.node.app.spi.workflows.TransactionKeys; @@ -47,7 +49,7 @@ public Set requiredNonPayerKeys() { @Override public Set requiredHollowAccounts() { - return null; + return emptySet(); } @Override diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java index bcd75ced8f8d..8ed4849de690 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java @@ -38,7 +38,6 @@ */ public class ContractServiceImpl implements ContractService { public static final long INTRINSIC_GAS_LOWER_BOUND = 21_000L; - public static final String LAZY_MEMO = "lazy-created account"; private final ContractServiceComponent component; diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java index a370ffb16e4d..e61ea26610f3 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaNativeOperations.java @@ -19,7 +19,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.selfDestructBeneficiariesFor; -import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.LAZY_CREATION_MEMO; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthHollowAccountCreation; import static java.util.Objects.requireNonNull; @@ -108,7 +107,6 @@ public void setNonce(final long contractNumber, final long nonce) { public @NonNull ResponseCodeEnum createHollowAccount(@NonNull final Bytes evmAddress) { final var synthTxn = TransactionBody.newBuilder() .cryptoCreateAccount(synthHollowAccountCreation(evmAddress)) - .memo(LAZY_CREATION_MEMO) .build(); // Note the use of the null "verification assistant" callback; we don't want any @@ -120,8 +118,6 @@ public void setNonce(final long contractNumber, final long nonce) { null, context.payer(), HandleContext.ConsensusThrottling.ON); - childRecordBuilder.memo(LAZY_CREATION_MEMO); - return childRecordBuilder.status(); } catch (final HandleException e) { // It is critically important we don't let HandleExceptions propagate to the workflow because diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaOperations.java index d1ca12bf6dde..8bb0bfb45f3c 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleHederaOperations.java @@ -16,17 +16,24 @@ package com.hedera.node.app.service.contract.impl.exec.scope; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; -import static com.hedera.node.app.service.contract.impl.ContractServiceImpl.LAZY_MEMO; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.FEE_SCHEDULE_UNITS_PER_TINYCENT; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; -import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.*; +import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.THREE_MONTHS_IN_SECONDS; +import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthAccountCreationFromHapi; +import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthContractCreationForExternalization; +import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthContractCreationFromParent; import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; import static com.hedera.node.app.spi.workflows.record.ExternalizedRecordCustomizer.SUPPRESSING_EXTERNALIZED_RECORD_CUSTOMIZER; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.transactionWith; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.base.*; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.Duration; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; import com.hedera.hapi.node.contract.ContractFunctionResult; import com.hedera.hapi.node.token.CryptoCreateTransactionBody; @@ -74,8 +81,7 @@ public class HandleHederaOperations implements HederaOperations { .initialBalance(0) .maxAutomaticTokenAssociations(0) .autoRenewPeriod(Duration.newBuilder().seconds(THREE_MONTHS_IN_SECONDS)) - .key(IMMUTABILITY_SENTINEL_KEY) - .memo(LAZY_MEMO); + .key(IMMUTABILITY_SENTINEL_KEY); private final TinybarValues tinybarValues; private final LedgerConfig ledgerConfig; @@ -343,7 +349,7 @@ public long getOriginalSlotsUsed(final ContractID contractID) { @Override public void externalizeHollowAccountMerge(@NonNull ContractID contractId, @Nullable Bytes evmAddress) { final var recordBuilder = context.savepointStack() - .addRemovableChildRecordBuilder(ContractCreateStreamBuilder.class) + .addRemovableChildRecordBuilder(ContractCreateStreamBuilder.class, CONTRACT_CREATE) .contractID(contractId) .status(SUCCESS) .transaction(transactionWith(TransactionBody.newBuilder() @@ -393,6 +399,7 @@ private void dispatchAndMarkCreation( // have been pre-validated in ProxyWorldUpdater.createAccount() so this is an invariant failure throw new IllegalStateException("Unexpected failure creating new contract - " + recordBuilder.status()); } + recordBuilder.functionality(CONTRACT_CREATE); // If this creation runs to a successful completion, its ContractBytecode sidecar // goes in the top-level record or the just-created child record depending on whether // we are doing this on behalf of a HAPI ContractCreate call; we only include the diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java index 7c4a863612f3..0928a72ce33c 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.exec.scope; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; import static com.hedera.node.app.spi.workflows.record.StreamBuilder.transactionWith; @@ -23,6 +24,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; @@ -34,6 +36,7 @@ import com.hedera.node.app.service.contract.impl.annotations.TransactionScope; import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder; import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.function.Predicate; @@ -72,7 +75,7 @@ public HandleSystemContractOperations(@NonNull final HandleContext context, @Nul * {@inheritDoc} */ @Override - public @NonNull T dispatch( + public @NonNull T dispatch( @NonNull final TransactionBody syntheticBody, @NonNull final VerificationStrategy strategy, @NonNull final AccountID syntheticPayerId, @@ -92,45 +95,37 @@ public HandleSystemContractOperations(@NonNull final HandleContext context, @Nul @Override public ContractCallStreamBuilder externalizePreemptedDispatch( - @NonNull final TransactionBody syntheticBody, @NonNull final ResponseCodeEnum preemptingStatus) { + @NonNull final TransactionBody syntheticBody, + @NonNull final ResponseCodeEnum preemptingStatus, + @NonNull final HederaFunctionality functionality) { requireNonNull(syntheticBody); requireNonNull(preemptingStatus); + requireNonNull(functionality); return context.savepointStack() - .addChildRecordBuilder(ContractCallStreamBuilder.class) + .addChildRecordBuilder(ContractCallStreamBuilder.class, functionality) .transaction(transactionWith(syntheticBody)) .status(preemptingStatus); } - /** - * {@inheritDoc} - */ - @Override - public void externalizeResult( - @NonNull final ContractFunctionResult result, @NonNull final ResponseCodeEnum responseStatus) { - final var childRecordBuilder = context.savepointStack().addChildRecordBuilder(ContractCallStreamBuilder.class); - childRecordBuilder - .transaction(Transaction.DEFAULT) - .contractID(result.contractID()) - .status(responseStatus) - .contractCallResult(result); - } - @Override public void externalizeResult( @NonNull final ContractFunctionResult result, @NonNull final ResponseCodeEnum responseStatus, - @NonNull Transaction transaction) { + @NonNull final Transaction transaction) { requireNonNull(transaction); context.savepointStack() - .addChildRecordBuilder(ContractCallStreamBuilder.class) + .addChildRecordBuilder(ContractCallStreamBuilder.class, CONTRACT_CALL) .transaction(transaction) .status(responseStatus) .contractCallResult(result); } @Override - public Transaction syntheticTransactionForNativeCall(Bytes input, ContractID contractID, boolean isViewCall) { + public Transaction syntheticTransactionForNativeCall( + @NonNull final Bytes input, @NonNull final ContractID contractID, boolean isViewCall) { + requireNonNull(input); + requireNonNull(contractID); var functionParameters = tuweniToPbjBytes(input); var contractCallBodyBuilder = ContractCallTransactionBody.newBuilder().contractID(contractID).functionParameters(functionParameters); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/QuerySystemContractOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/QuerySystemContractOperations.java index d32cacacf22f..085bd06677cd 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/QuerySystemContractOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/QuerySystemContractOperations.java @@ -20,6 +20,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; @@ -29,6 +30,7 @@ import com.hedera.node.app.service.contract.impl.annotations.QueryScope; import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder; import com.hedera.node.app.spi.workflows.QueryContext; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.InstantSource; @@ -55,58 +57,43 @@ public QuerySystemContractOperations( this.instantSource = requireNonNull(instantSource); } - /** - * {@inheritDoc} - */ @Override - public @NonNull T dispatch( + public @NonNull T dispatch( @NonNull final TransactionBody syntheticTransaction, @NonNull final VerificationStrategy strategy, - @NonNull AccountID syntheticPayerId, - @NonNull Class recordBuilderClass) { + @NonNull final AccountID syntheticPayerId, + @NonNull final Class recordBuilderClass) { throw new UnsupportedOperationException("Cannot dispatch synthetic transaction"); } @Override public ContractCallStreamBuilder externalizePreemptedDispatch( - @NonNull final TransactionBody syntheticBody, @NonNull final ResponseCodeEnum preemptingStatus) { + @NonNull final TransactionBody syntheticBody, + @NonNull final ResponseCodeEnum preemptingStatus, + @NonNull final HederaFunctionality functionality) { throw new UnsupportedOperationException("Cannot externalize preempted dispatch"); } - /** - * {@inheritDoc} - */ @Override public @NonNull Predicate activeSignatureTestWith(@NonNull final VerificationStrategy strategy) { throw new UnsupportedOperationException("Cannot compute a signature test"); } - @Override - public void externalizeResult(@NonNull ContractFunctionResult result, @NonNull ResponseCodeEnum responseStatus) {} - - /** - * {@inheritDoc} - */ @Override public void externalizeResult( @NonNull final ContractFunctionResult result, @NonNull final ResponseCodeEnum responseStatus, @Nullable Transaction transaction) { - // no-op + // No-op } - /** - * {@inheritDoc} - */ @Override - public Transaction syntheticTransactionForNativeCall(Bytes input, ContractID contractID, boolean isViewCall) { - // no-op - return null; + public Transaction syntheticTransactionForNativeCall( + @NonNull final Bytes input, @NonNull final ContractID contractID, final boolean isViewCall) { + // Ignored since externalizeResult() is a no-op + return Transaction.DEFAULT; } - /** - * {@inheritDoc} - */ @Override @NonNull public ExchangeRate currentExchangeRate() { diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/SystemContractOperations.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/SystemContractOperations.java index d3a88ed3cc3a..d52e0ab80196 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/SystemContractOperations.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/SystemContractOperations.java @@ -18,6 +18,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.Transaction; @@ -25,6 +26,7 @@ import com.hedera.hapi.node.transaction.ExchangeRate; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.function.Predicate; import org.apache.tuweni.bytes.Bytes; @@ -45,7 +47,7 @@ public interface SystemContractOperations { * @return the result of the dispatch */ @NonNull - T dispatch( + T dispatch( @NonNull TransactionBody syntheticBody, @NonNull VerificationStrategy strategy, @NonNull AccountID syntheticPayerId, @@ -57,10 +59,13 @@ T dispatch( * * @param syntheticBody the preempted dispatch * @param preemptingStatus the status code causing the preemption + * @param functionality the functionality of the preemption * @return the record of the preemption */ ContractCallStreamBuilder externalizePreemptedDispatch( - @NonNull TransactionBody syntheticBody, @NonNull ResponseCodeEnum preemptingStatus); + @NonNull TransactionBody syntheticBody, + @NonNull ResponseCodeEnum preemptingStatus, + @NonNull HederaFunctionality functionality); /** * Returns a {@link Predicate} that tests whether the given {@link Key} is active based on the @@ -73,17 +78,10 @@ ContractCallStreamBuilder externalizePreemptedDispatch( Predicate activeSignatureTestWith(@NonNull VerificationStrategy strategy); /** - * Attempts to create a child record of the current record, with the given {@code result} - * - * @param result contract function result - */ - void externalizeResult( - @NonNull final ContractFunctionResult result, @NonNull final ResponseCodeEnum responseStatus); - - /** - * Attempts to create a child record of the current record, with the given {@code result} - * - * @param result contract function result + * Attempts to create a child record of the current record, with the given {@code result}. + * @param result contract function result + * @param responseStatus response status + * @param transaction transaction */ void externalizeResult( @NonNull ContractFunctionResult result, @@ -93,12 +91,13 @@ void externalizeResult( /** * Generate synthetic transaction for child hts call * - * @param input - * @param contractID - * @param isViewCall - * @return + * @param input the input data + * @param contractID the contract id + * @param isViewCall if the call is a view call + * @return the synthetic transaction */ - Transaction syntheticTransactionForNativeCall(Bytes input, ContractID contractID, boolean isViewCall); + Transaction syntheticTransactionForNativeCall( + @NonNull Bytes input, @NonNull ContractID contractID, boolean isViewCall); /** * Returns the {@link ExchangeRate} for the current consensus time. This will enable the translation from hbars diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/PrngSystemContract.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/PrngSystemContract.java index eae91e967b0a..b6421a1af759 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/PrngSystemContract.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/PrngSystemContract.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts; +import static com.hedera.hapi.node.base.HederaFunctionality.UTIL_PRNG; import static com.hedera.node.app.hapi.utils.CommonPbjConverters.toPbj; import static com.hedera.node.app.hapi.utils.ValidationUtils.validateTrue; import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.systemContractGasCalculatorOf; @@ -151,7 +152,7 @@ void createFailedRecord( updater.enhancement() .systemOperations() - .externalizePreemptedDispatch(synthBody(), toPbj(responseCode)) + .externalizePreemptedDispatch(synthBody(), toPbj(responseCode), UTIL_PRNG) .contractCallResult(contractResult); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/common/AbstractNativeSystemContract.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/common/AbstractNativeSystemContract.java index dff91f05f52f..0fabe2b5f544 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/common/AbstractNativeSystemContract.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/common/AbstractNativeSystemContract.java @@ -80,7 +80,7 @@ public FullResult computeFully(@NonNull final Bytes input, @NonNull final Messag return haltResult(PRECOMPILE_ERROR, frame.getRemainingGas()); } final Call call; - final AbstractCallAttempt attempt; + final AbstractCallAttempt attempt; try { validateTrue(input.size() >= FUNCTION_SELECTOR_LENGTH, INVALID_TRANSACTION_BODY); attempt = callFactory.createCallAttemptFrom(input, callType, frame); @@ -100,7 +100,7 @@ public FullResult computeFully(@NonNull final Bytes input, @NonNull final Messag @SuppressWarnings({"java:S2637", "java:S2259"}) // this function is going to be refactored soon. private static FullResult resultOfExecuting( - @NonNull final AbstractCallAttempt attempt, + @NonNull final AbstractCallAttempt attempt, @NonNull final Call call, @NonNull final Bytes input, @NonNull final MessageFrame frame, @@ -164,7 +164,7 @@ private static void externalizeFailure( final long gasRequirement, @NonNull final Bytes input, @NonNull final Bytes output, - @NonNull final AbstractCallAttempt attempt, + @NonNull final AbstractCallAttempt attempt, @NonNull final ResponseCodeEnum status, @NonNull final HederaWorldUpdater.Enhancement enhancement, @NonNull final ContractID contractID) { diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java index 07d60d1cb8ad..161c601cd289 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.create; +import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_CREATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; @@ -119,7 +120,8 @@ private record LegacyActivation(long contractNum, Bytes pbjAddress, Address besu if (frame.getValue().lessThan(Wei.of(nonGasCost))) { return completionWith( FIXED_GAS_COST, - systemContractOperations().externalizePreemptedDispatch(syntheticCreate, INSUFFICIENT_TX_FEE), + systemContractOperations() + .externalizePreemptedDispatch(syntheticCreate, INSUFFICIENT_TX_FEE, TOKEN_CREATE), RC_AND_ADDRESS_ENCODER.encodeElements((long) INSUFFICIENT_TX_FEE.protoOrdinal(), ZERO_ADDRESS)); } else { operations().collectFee(spenderId, nonGasCost); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCall.java index c4a12bd0dc3e..9d789de0cf69 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCall.java @@ -50,10 +50,8 @@ public DefaultKycStatusCall( @Override protected @NonNull PricedResult resultOfViewingToken(@Nullable final Token token) { requireNonNull(token); - return gasOnly( - fullResultsFor(SUCCESS, gasCalculator.viewGasRequirement(), token.accountsKycGrantedByDefault()), - SUCCESS, - true); + final boolean kycStatus = !token.hasKycKey() || token.accountsKycGrantedByDefault(); + return gasOnly(fullResultsFor(SUCCESS, gasCalculator.viewGasRequirement(), kycStatus), SUCCESS, true); } @Override diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java index 8d2ee2e02ec3..316ad50d23e0 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/transfer/ClassicTransfersCall.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RECEIVING_NODE_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED; @@ -134,12 +135,14 @@ public ClassicTransfersCall( return reversionWith( gasRequirement, systemContractOperations() - .externalizePreemptedDispatch(syntheticTransfer, INVALID_RECEIVING_NODE_ACCOUNT)); + .externalizePreemptedDispatch( + syntheticTransfer, INVALID_RECEIVING_NODE_ACCOUNT, CRYPTO_TRANSFER)); } if (executionIsNotSupported()) { return haltWith( gasRequirement, - systemContractOperations().externalizePreemptedDispatch(syntheticTransfer, NOT_SUPPORTED)); + systemContractOperations() + .externalizePreemptedDispatch(syntheticTransfer, NOT_SUPPORTED, CRYPTO_TRANSFER)); } final var transferToDispatch = shouldRetryWithApprovals() ? syntheticTransfer diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaWorldUpdater.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaWorldUpdater.java index f6942487f9f5..ce481added56 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaWorldUpdater.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/hevm/HederaWorldUpdater.java @@ -20,9 +20,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; -import com.hedera.hapi.node.contract.ContractFunctionResult; import com.hedera.hapi.node.transaction.ExchangeRate; import com.hedera.node.app.service.contract.impl.exec.scope.HederaNativeOperations; import com.hedera.node.app.service.contract.impl.exec.scope.HederaOperations; @@ -273,13 +271,6 @@ Optional tryTrackingSelfDestructBeneficiary( @NonNull List pendingStorageUpdates(); - /** - * Externalizes the results of a system contract call into a record - * @param result The result of the system contract call - */ - void externalizeSystemContractResults( - @NonNull final ContractFunctionResult result, @NonNull ResponseCodeEnum responseStatus); - /** * Returns the {@link ExchangeRate} for the current consensus timestamp * Delegates to {@link SystemContractOperations#currentExchangeRate()} ()} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyWorldUpdater.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyWorldUpdater.java index 0a3ae70631f4..d41582d1fc2a 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyWorldUpdater.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/state/ProxyWorldUpdater.java @@ -31,9 +31,7 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; -import com.hedera.hapi.node.contract.ContractFunctionResult; import com.hedera.hapi.node.transaction.ExchangeRate; import com.hedera.node.app.service.contract.impl.exec.scope.HandleHederaOperations; import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater; @@ -469,15 +467,6 @@ public void commit() { throw new UnsupportedOperationException(); } - /** - * {@inheritDoc} - */ - @Override - public void externalizeSystemContractResults( - @NonNull final ContractFunctionResult result, @NonNull ResponseCodeEnum responseStatus) { - enhancement.systemOperations().externalizeResult(result, responseStatus); - } - /** * {@inheritDoc} */ diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SynthTxnUtils.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SynthTxnUtils.java index ed3c4ab26073..914a07e02649 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SynthTxnUtils.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/utils/SynthTxnUtils.java @@ -41,7 +41,6 @@ public class SynthTxnUtils { public static final long THREE_MONTHS_IN_SECONDS = 7776000L; public static final Duration DEFAULT_AUTO_RENEW_PERIOD = Duration.newBuilder().seconds(THREE_MONTHS_IN_SECONDS).build(); - public static final String LAZY_CREATION_MEMO = "lazy-created account"; private SynthTxnUtils() { throw new UnsupportedOperationException("Utility Class"); @@ -159,7 +158,6 @@ public static CryptoCreateTransactionBody synthHollowAccountCreation(@NonNull fi .initialBalance(0L) .alias(evmAddress) .key(IMMUTABILITY_SENTINEL_KEY) - .memo(LAZY_CREATION_MEMO) .autoRenewPeriod(DEFAULT_AUTO_RENEW_PERIOD) .build(); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaNativeOperationsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaNativeOperationsTest.java index a94fa34440a4..9db96859f9cc 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaNativeOperationsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaNativeOperationsTest.java @@ -36,7 +36,6 @@ import static com.hedera.node.app.service.contract.impl.test.TestHelpers.PARANOID_SOMEBODY; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.SOMEBODY; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; -import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.LAZY_CREATION_MEMO; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthHollowAccountCreation; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -179,7 +178,6 @@ void getTokenUsesStore() { void createsHollowAccountByDispatching() { final var synthLazyCreate = TransactionBody.newBuilder() .cryptoCreateAccount(synthHollowAccountCreation(CANONICAL_ALIAS)) - .memo(LAZY_CREATION_MEMO) .build(); given(context.payer()).willReturn(A_NEW_ACCOUNT_ID); @@ -192,19 +190,17 @@ void createsHollowAccountByDispatching() { .thenReturn(cryptoCreateRecordBuilder); given(cryptoCreateRecordBuilder.status()).willReturn(OK); - given(cryptoCreateRecordBuilder.memo(LAZY_CREATION_MEMO)).willReturn(cryptoCreateRecordBuilder); final var status = subject.createHollowAccount(CANONICAL_ALIAS); assertEquals(OK, status); - verify(cryptoCreateRecordBuilder).memo(LAZY_CREATION_MEMO); + verify(cryptoCreateRecordBuilder, never()).memo(any()); } @Test void createsHollowAccountByDispatchingDoesNotThrowErrors() { final var synthLazyCreate = TransactionBody.newBuilder() .cryptoCreateAccount(synthHollowAccountCreation(CANONICAL_ALIAS)) - .memo(LAZY_CREATION_MEMO) .build(); given(context.payer()).willReturn(A_NEW_ACCOUNT_ID); given(context.dispatchRemovablePrecedingTransaction( @@ -219,7 +215,7 @@ void createsHollowAccountByDispatchingDoesNotThrowErrors() { final var status = assertDoesNotThrow(() -> subject.createHollowAccount(CANONICAL_ALIAS)); assertThat(status).isEqualTo(MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - verify(cryptoCreateRecordBuilder).memo(LAZY_CREATION_MEMO); + verify(cryptoCreateRecordBuilder, never()).memo(any()); } @Test diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaOperationsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaOperationsTest.java index 1435f317dfe9..83196b6050ed 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaOperationsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleHederaOperationsTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.test.exec.scope; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; @@ -648,7 +649,7 @@ void externalizeHollowAccountMerge() { // given var contractId = ContractID.newBuilder().contractNum(1001).build(); given(context.savepointStack()).willReturn(stack); - given(stack.addRemovableChildRecordBuilder(ContractCreateStreamBuilder.class)) + given(stack.addRemovableChildRecordBuilder(ContractCreateStreamBuilder.class, CONTRACT_CREATE)) .willReturn(contractCreateRecordBuilder); given(contractCreateRecordBuilder.contractID(contractId)).willReturn(contractCreateRecordBuilder); given(contractCreateRecordBuilder.status(any())).willReturn(contractCreateRecordBuilder); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java index efd91301d324..e3f3fcbc23a2 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/HandleSystemContractOperationsTest.java @@ -16,12 +16,16 @@ package com.hedera.node.app.service.contract.impl.test.exec.scope; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.AN_ED25519_KEY; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.A_NEW_ACCOUNT_ID; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.A_SECP256K1_KEY; import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -122,30 +126,18 @@ void dispatchesRespectingGivenStrategy() { } @Test - void externalizeSuccessfulResultTest() { - var contractFunctionResult = SystemContractUtils.successResultOfZeroValueTraceable( - 0, - org.apache.tuweni.bytes.Bytes.EMPTY, - 100L, - org.apache.tuweni.bytes.Bytes.EMPTY, - AccountID.newBuilder().build()); - - // given + void externalizesPreemptedAsExpected() { given(context.savepointStack()).willReturn(savepointStack); - given(savepointStack.addChildRecordBuilder(ContractCallStreamBuilder.class)) + given(savepointStack.addChildRecordBuilder(ContractCallStreamBuilder.class, CRYPTO_TRANSFER)) .willReturn(recordBuilder); - given(recordBuilder.transaction(Transaction.DEFAULT)).willReturn(recordBuilder); - given(recordBuilder.status(ResponseCodeEnum.SUCCESS)).willReturn(recordBuilder); - given(recordBuilder.contractID(any())).willReturn(recordBuilder); + given(recordBuilder.transaction(any())).willReturn(recordBuilder); + given(recordBuilder.status(any())).willReturn(recordBuilder); - // when - subject.externalizeResult(contractFunctionResult, ResponseCodeEnum.SUCCESS); + final var preemptedBuilder = + subject.externalizePreemptedDispatch(TransactionBody.DEFAULT, ACCOUNT_DELETED, CRYPTO_TRANSFER); - // then - verify(recordBuilder).contractID(any()); - verify(recordBuilder).transaction(Transaction.DEFAULT); - verify(recordBuilder).status(ResponseCodeEnum.SUCCESS); - verify(recordBuilder).contractCallResult(contractFunctionResult); + assertSame(recordBuilder, preemptedBuilder); + verify(recordBuilder).status(ACCOUNT_DELETED); } @Test @@ -164,7 +156,7 @@ void externalizeSuccessfulResultWithTransactionBodyTest() { // given given(context.savepointStack()).willReturn(savepointStack); - given(savepointStack.addChildRecordBuilder(ContractCallStreamBuilder.class)) + given(savepointStack.addChildRecordBuilder(ContractCallStreamBuilder.class, CONTRACT_CALL)) .willReturn(recordBuilder); given(recordBuilder.transaction(transaction)).willReturn(recordBuilder); given(recordBuilder.status(ResponseCodeEnum.SUCCESS)).willReturn(recordBuilder); @@ -177,33 +169,6 @@ void externalizeSuccessfulResultWithTransactionBodyTest() { verify(recordBuilder).contractCallResult(contractFunctionResult); } - @Test - void externalizeFailedResultTest() { - var contractFunctionResult = SystemContractUtils.successResultOfZeroValueTraceable( - 0, - org.apache.tuweni.bytes.Bytes.EMPTY, - 100L, - org.apache.tuweni.bytes.Bytes.EMPTY, - AccountID.newBuilder().build()); - - // given - given(context.savepointStack()).willReturn(savepointStack); - given(savepointStack.addChildRecordBuilder(ContractCallStreamBuilder.class)) - .willReturn(recordBuilder); - given(recordBuilder.transaction(Transaction.DEFAULT)).willReturn(recordBuilder); - given(recordBuilder.status(ResponseCodeEnum.FAIL_INVALID)).willReturn(recordBuilder); - given(recordBuilder.contractID(any())).willReturn(recordBuilder); - - // when - subject.externalizeResult(contractFunctionResult, ResponseCodeEnum.FAIL_INVALID); - - // then - verify(recordBuilder).contractID(any()); - verify(recordBuilder).transaction(Transaction.DEFAULT); - verify(recordBuilder).status(ResponseCodeEnum.FAIL_INVALID); - verify(recordBuilder).contractCallResult(contractFunctionResult); - } - @Test void syntheticTransactionForHtsCallTest() { assertNotNull(subject.syntheticTransactionForNativeCall(Bytes.EMPTY, ContractID.DEFAULT, true)); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/QuerySystemContractOperationsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/QuerySystemContractOperationsTest.java index 2b5f7ff77952..41ddf79a80eb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/QuerySystemContractOperationsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/scope/QuerySystemContractOperationsTest.java @@ -16,31 +16,30 @@ package com.hedera.node.app.service.contract.impl.test.exec.scope; -import static com.hedera.node.app.service.contract.impl.test.TestHelpers.MOCK_VERIFICATION_STRATEGY; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.NftID; -import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.ContractID; import com.hedera.hapi.node.base.TimestampSeconds; -import com.hedera.hapi.node.base.TokenRelationship; +import com.hedera.hapi.node.base.Transaction; import com.hedera.hapi.node.contract.ContractFunctionResult; -import com.hedera.hapi.node.state.token.Account; -import com.hedera.hapi.node.state.token.Nft; -import com.hedera.hapi.node.state.token.Token; import com.hedera.hapi.node.transaction.ExchangeRate; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.contract.impl.exec.scope.QuerySystemContractOperations; -import com.hedera.node.app.service.contract.impl.exec.scope.ResultTranslator; +import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy; import com.hedera.node.app.spi.fees.ExchangeRateInfo; import com.hedera.node.app.spi.workflows.QueryContext; -import com.hedera.node.config.data.ContractsConfig; -import com.swirlds.config.api.Configuration; +import com.hedera.node.app.spi.workflows.record.StreamBuilder; import java.time.InstantSource; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,28 +52,10 @@ class QuerySystemContractOperationsTest { private QueryContext context; @Mock - private NftID nftID; - - @Mock - private ResultTranslator nftResultTranslator; - - @Mock - private ResultTranslator tokenResultTranslator; - - @Mock - private ResultTranslator accountResultTranslator; - - @Mock - private ResultTranslator tokenRelResultTranslator; - - @Mock - private Configuration configuration; - - @Mock - private ContractsConfig contractsConfig; + private ExchangeRateInfo exchangeRateInfo; @Mock - private ExchangeRateInfo exchangeRateInfo; + private VerificationStrategy verificationStrategy; private final InstantSource instantSource = InstantSource.system(); @@ -86,23 +67,35 @@ void setUp() { } @Test - void doesNotSupportAnyMutations() { + void exchangeRateTest() { + final ExchangeRate exchangeRate = new ExchangeRate(1, 2, TimestampSeconds.DEFAULT); + given(context.exchangeRateInfo()).willReturn(exchangeRateInfo); + given(exchangeRateInfo.activeRate(any())).willReturn(exchangeRate); + var result = subject.currentExchangeRate(); + assertThat(result).isEqualTo(exchangeRate); + } + + @Test + void dispatchingNotSupported() { assertThrows( UnsupportedOperationException.class, () -> subject.dispatch( - TransactionBody.DEFAULT, MOCK_VERIFICATION_STRATEGY, AccountID.DEFAULT, Object.class)); + TransactionBody.DEFAULT, verificationStrategy, AccountID.DEFAULT, StreamBuilder.class)); assertThrows( - UnsupportedOperationException.class, () -> subject.activeSignatureTestWith(MOCK_VERIFICATION_STRATEGY)); + UnsupportedOperationException.class, + () -> subject.externalizePreemptedDispatch(TransactionBody.DEFAULT, ACCOUNT_DELETED, CRYPTO_TRANSFER)); + } - assertDoesNotThrow(() -> subject.externalizeResult(ContractFunctionResult.DEFAULT, ResponseCodeEnum.SUCCESS)); + @Test + void sigTestNotSupported() { + assertThrows(UnsupportedOperationException.class, () -> subject.activeSignatureTestWith(verificationStrategy)); } @Test - void exchangeRateTest() { - final ExchangeRate exchangeRate = new ExchangeRate(1, 2, TimestampSeconds.DEFAULT); - given(context.exchangeRateInfo()).willReturn(exchangeRateInfo); - given(exchangeRateInfo.activeRate(any())).willReturn(exchangeRate); - var result = subject.currentExchangeRate(); - assertThat(result).isEqualTo(exchangeRate); + void externalizingResultsAreNoop() { + assertDoesNotThrow( + () -> subject.externalizeResult(ContractFunctionResult.DEFAULT, SUCCESS, Transaction.DEFAULT)); + assertSame( + Transaction.DEFAULT, subject.syntheticTransactionForNativeCall(Bytes.EMPTY, ContractID.DEFAULT, true)); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/PrngSystemContractTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/PrngSystemContractTest.java index d9ccf1cfbc31..2af79ad806eb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/PrngSystemContractTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/PrngSystemContractTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.test.exec.systemcontracts; +import static com.hedera.hapi.node.base.HederaFunctionality.UTIL_PRNG; import static com.hedera.node.app.service.contract.impl.exec.scope.HandleHederaOperations.ZERO_ENTROPY; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.EXPECTED_RANDOM_NUMBER; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.PRECOMPILE_CONTRACT_FAILED_RESULT; @@ -24,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -158,7 +160,7 @@ void computePrecompileFailedTest() { given(messageFrame.isStatic()).willReturn(false); given(messageFrame.getWorldUpdater()).willReturn(proxyWorldUpdater); given(proxyWorldUpdater.entropy()).willReturn(Bytes.wrap(ZERO_ENTROPY.toByteArray())); - when(systemContractOperations.externalizePreemptedDispatch(any(), any())) + when(systemContractOperations.externalizePreemptedDispatch(any(), any(), eq(UTIL_PRNG))) .thenReturn(mock(ContractCallStreamBuilder.class)); // when: @@ -176,7 +178,7 @@ void computePrecompileInvalidFeeSubmittedFailedTest() { given(systemContractGasCalculator.canonicalGasRequirement(any())).willReturn(GAS_REQUIRED); given(messageFrame.isStatic()).willReturn(false); given(messageFrame.getWorldUpdater()).willReturn(proxyWorldUpdater); - when(systemContractOperations.externalizePreemptedDispatch(any(), any())) + when(systemContractOperations.externalizePreemptedDispatch(any(), any(), eq(UTIL_PRNG))) .thenReturn(mock(ContractCallStreamBuilder.class)); // when: @@ -192,7 +194,7 @@ void wrongFunctionSelectorFailedTest() { givenCommon(); given(systemContractGasCalculator.canonicalGasRequirement(any())).willReturn(GAS_REQUIRED); - when(systemContractOperations.externalizePreemptedDispatch(any(), any())) + when(systemContractOperations.externalizePreemptedDispatch(any(), any(), eq(UTIL_PRNG))) .thenReturn(mock(ContractCallStreamBuilder.class)); // when: diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/create/ClassicCreatesCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/create/ClassicCreatesCallTest.java index 55f4fc941dd3..fe4ac20ad676 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/create/ClassicCreatesCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/create/ClassicCreatesCallTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.create; +import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_CREATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_HAS_NO_SUPPLY_KEY; @@ -281,7 +282,7 @@ void createNonFungibleTokenWithCustomFeesHappyPathV3() { void requiresNonGasCostToBeProvidedAsValue() { commonGivens(200_000L, 99_999L, true); given(recordBuilder.status()).willReturn(SUCCESS); - given(systemContractOperations.externalizePreemptedDispatch(any(), eq(INSUFFICIENT_TX_FEE))) + given(systemContractOperations.externalizePreemptedDispatch(any(), eq(INSUFFICIENT_TX_FEE), eq(TOKEN_CREATE))) .willReturn(recordBuilder); final var result = subject.execute(frame).fullResult().result(); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCallTest.java index 1ee4f0a24a0b..f807d620dbaa 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/defaultkycstatus/DefaultKycStatusCallTest.java @@ -18,6 +18,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_EVERYTHING_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN; import static com.hedera.node.app.service.contract.impl.test.TestHelpers.revertOutputFor; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -39,6 +40,25 @@ void returnsDefaultKycStatusForPresentToken() { final var result = subject.execute().fullResult().result(); + assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); + assertEquals( + Bytes.wrap(DefaultKycStatusTranslator.DEFAULT_KYC_STATUS + .getOutputs() + .encodeElements(SUCCESS.protoOrdinal(), true) + .array()), + result.getOutput()); + } + + @Test + void returnsDefaultKycStatusForPresentTokenWithKey() { + final var token = FUNGIBLE_EVERYTHING_TOKEN + .copyBuilder() + .accountsKycGrantedByDefault(false) + .build(); + final var subject = new DefaultKycStatusCall(gasCalculator, mockEnhancement(), false, token); + + final var result = subject.execute().fullResult().result(); + assertEquals(MessageFrame.State.COMPLETED_SUCCESS, result.getState()); assertEquals( Bytes.wrap(DefaultKycStatusTranslator.DEFAULT_KYC_STATUS diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java index 581da854a350..f993c993418b 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/transfer/ClassicTransfersCallTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.transfer; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RECEIVING_NODE_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SIGNATURE; @@ -176,7 +177,8 @@ void retryingTransferInvalidSignatureCompletesWithStandardizedResponseCode() { @Test void unsupportedV2transferHaltsWithNotSupportedReason() { givenV2SubjectWithV2Disabled(); - given(systemContractOperations.externalizePreemptedDispatch(any(TransactionBody.class), eq(NOT_SUPPORTED))) + given(systemContractOperations.externalizePreemptedDispatch( + any(TransactionBody.class), eq(NOT_SUPPORTED), eq(CRYPTO_TRANSFER))) .willReturn(recordBuilder); given(recordBuilder.status()).willReturn(NOT_SUPPORTED); @@ -192,7 +194,7 @@ void systemAccountCreditReverts() { given(systemAccountCreditScreen.creditsToSystemAccount(CryptoTransferTransactionBody.DEFAULT)) .willReturn(true); given(systemContractOperations.externalizePreemptedDispatch( - any(TransactionBody.class), eq(INVALID_RECEIVING_NODE_ACCOUNT))) + any(TransactionBody.class), eq(INVALID_RECEIVING_NODE_ACCOUNT), eq(CRYPTO_TRANSFER))) .willReturn(recordBuilder); given(recordBuilder.status()).willReturn(INVALID_RECEIVING_NODE_ACCOUNT); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/ProxyWorldUpdaterTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/ProxyWorldUpdaterTest.java index 04ca80c14be0..d8d79c2abfe6 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/ProxyWorldUpdaterTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/state/ProxyWorldUpdaterTest.java @@ -43,7 +43,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; import com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason; import com.hedera.node.app.service.contract.impl.exec.scope.HederaNativeOperations; @@ -57,7 +56,6 @@ import com.hedera.node.app.service.contract.impl.state.ProxyWorldUpdater; import com.hedera.node.app.service.contract.impl.state.StorageAccess; import com.hedera.node.app.service.contract.impl.state.StorageAccesses; -import com.hedera.node.app.service.contract.impl.utils.SystemContractUtils; import com.hedera.node.app.spi.workflows.ResourceExhaustedException; import java.util.List; import java.util.Optional; @@ -509,19 +507,6 @@ void delegatesEntropy() { assertEquals(pbjToTuweniBytes(OUTPUT_DATA), subject.entropy()); } - @Test - void externalizeSystemContractResultTest() { - var contractFunctionResult = SystemContractUtils.successResultOfZeroValueTraceable( - 0, - org.apache.tuweni.bytes.Bytes.EMPTY, - 100L, - org.apache.tuweni.bytes.Bytes.EMPTY, - AccountID.newBuilder().build()); - - subject.externalizeSystemContractResults(contractFunctionResult, ResponseCodeEnum.SUCCESS); - verify(systemContractOperations).externalizeResult(contractFunctionResult, ResponseCodeEnum.SUCCESS); - } - @Test void currentExchangeRateTest() { subject.currentExchangeRate(); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SynthTxnUtilsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SynthTxnUtilsTest.java index f3482433e92f..4b0a87c0555d 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SynthTxnUtilsTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/utils/SynthTxnUtilsTest.java @@ -27,12 +27,11 @@ import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.tuweniToPbjBytes; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.DEFAULT_AUTO_RENEW_PERIOD; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.IMMUTABILITY_SENTINEL_KEY; -import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.LAZY_CREATION_MEMO; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthAccountCreationFromHapi; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthContractCreationForExternalization; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthContractCreationFromParent; import static com.hedera.node.app.service.contract.impl.utils.SynthTxnUtils.synthHollowAccountCreation; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; @@ -44,6 +43,9 @@ import org.junit.jupiter.api.Test; class SynthTxnUtilsTest { + + private static final String LAZY_CREATION_MEMO = ""; + @Test void createsExpectedHollowSynthBody() { final var expected = CryptoCreateTransactionBody.newBuilder() diff --git a/hedera-node/hedera-token-service-impl/build.gradle.kts b/hedera-node/hedera-token-service-impl/build.gradle.kts index 037f96c17053..f082d79f5467 100644 --- a/hedera-node/hedera-token-service-impl/build.gradle.kts +++ b/hedera-node/hedera-token-service-impl/build.gradle.kts @@ -21,12 +21,6 @@ plugins { description = "Default Hedera Token Service Implementation" -// Remove the following line to enable all 'javac' lint checks that we have turned on by default -// and then fix the reported issues. -tasks.withType().configureEach { - options.compilerArgs.add("-Xlint:-exports,-lossy-conversions,-static") -} - mainModuleInfo { annotationProcessor("dagger.compiler") } testModuleInfo { diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/TokenServiceImpl.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/TokenServiceImpl.java index e68ce57f56b9..adae38fcccda 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/TokenServiceImpl.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/TokenServiceImpl.java @@ -32,8 +32,6 @@ public class TokenServiceImpl implements TokenService { public static final long THREE_MONTHS_IN_SECONDS = 7776000L; public static final long MAX_SERIAL_NO_ALLOWED = 0xFFFFFFFFL; public static final long HBARS_TO_TINYBARS = 100_000_000L; - public static final String AUTO_MEMO = "auto-created account"; - public static final String LAZY_MEMO = "lazy-created account"; public static final ZoneId ZONE_UTC = ZoneId.of("UTC"); public TokenServiceImpl() { diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenDissociateFromAccountHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenDissociateFromAccountHandler.java index ea87b9885e09..f1954d52c143 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenDissociateFromAccountHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/TokenDissociateFromAccountHandler.java @@ -132,7 +132,7 @@ public void handle(@NonNull final HandleContext context) { // positive balances to remove from the account separate from the account object itself. Confusing, but we'll // _add_ the number to subtract to each aggregating variable. The total subtraction for each variable will be // done outside the dissociation loop - var numNftsToSubtract = 0; + var numNftsToSubtract = 0L; var numAutoAssociationsToSubtract = 0; var numAssociationsToSubtract = 0; var numPositiveBalancesToSubtract = 0; diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java index 4fc3a82a34d3..6eb580ed5bcb 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/EndOfStakingPeriodUpdater.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.handlers.staking; +import static com.hedera.hapi.node.base.HederaFunctionality.NODE_STAKE_UPDATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.service.token.impl.TokenServiceImpl.HBARS_TO_TINYBARS; import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; @@ -262,7 +263,7 @@ public EndOfStakingPeriodUpdater( // We don't want to fail adding the preceding child record for the node stake update that happens every // midnight. So, we add the preceding child record builder as unchecked, that doesn't fail with // MAX_CHILD_RECORDS_EXCEEDED - return context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class) + return context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE) .transaction(transactionWith(syntheticNodeStakeUpdateTxn.build())) .memo(END_OF_PERIOD_MEMO) .exchangeRate(exchangeRates) diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakeInfoHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakeInfoHelper.java index b463fe9444e8..eada583bd683 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakeInfoHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakeInfoHelper.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.handlers.staking; +import static com.hedera.hapi.node.base.HederaFunctionality.NODE_STAKE_UPDATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUtils.copyBuilderFrom; import static com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUtils.fromStakingInfo; @@ -247,7 +248,7 @@ public StreamBuilder adjustPostUpgradeStakes( stakingConfig.maxStakeRewarded(), POST_UPGRADE_MEMO); log.info("Exporting:\n{}", nodeStakes); - return context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class) + return context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE) .transaction(transactionWith(syntheticNodeStakeUpdateTxn.build())) .memo(POST_UPGRADE_MEMO) .status(SUCCESS); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java index 2aacdc67b720..70604e77649f 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/staking/StakingRewardsHelper.java @@ -29,6 +29,8 @@ import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableNetworkStakingRewardsStore; import com.hedera.node.app.service.token.impl.WritableStakingInfoStore; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.StakingConfig; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.ArrayList; @@ -53,12 +55,18 @@ public class StakingRewardsHelper { */ public static final long MAX_PENDING_REWARDS = 50_000_000_000L * HBARS_TO_TINYBARS; + private final boolean assumeContiguousPeriods; + /** * Default constructor for injection. */ @Inject - public StakingRewardsHelper() { - // Exists for Dagger injection + public StakingRewardsHelper(@NonNull final ConfigProvider configProvider) { + requireNonNull(configProvider); + this.assumeContiguousPeriods = configProvider + .getConfiguration() + .getConfigData(StakingConfig.class) + .assumeContiguousPeriods(); } /** @@ -171,11 +179,14 @@ public void decreasePendingRewardsBy( final var currentPendingRewards = stakingRewardsStore.pendingRewards(); var newPendingRewards = currentPendingRewards - amount; if (newPendingRewards < 0) { - log.error( - "Pending rewards decreased by {} to a meaningless {}, fixing to zero hbar", - amount, - newPendingRewards, - nodeId); + // If staking periods have been skipped in an environment, it is no longer + // guaranteed that pending rewards are maintained accurately + if (assumeContiguousPeriods) { + log.error( + "Pending rewards decreased by {} to a meaningless {}, fixing to zero hbar", + amount, + newPendingRewards); + } newPendingRewards = 0; } final var stakingRewards = stakingRewardsStore.get(); @@ -187,11 +198,15 @@ public void decreasePendingRewardsBy( final var currentNodePendingRewards = stakingInfo.pendingRewards(); var newNodePendingRewards = currentNodePendingRewards - amount; if (newNodePendingRewards < 0) { - log.error( - "Pending rewards decreased by {} to a meaningless {} for node {}, fixing to zero hbar", - amount, - newNodePendingRewards, - nodeId); + // If staking periods have been skipped in an environment, it is no longer + // guaranteed that pending rewards are maintained accurately + if (assumeContiguousPeriods) { + log.error( + "Pending rewards decreased by {} to a meaningless {} for node {}, fixing to zero hbar", + amount, + newNodePendingRewards, + nodeId); + } newNodePendingRewards = 0; } final var stakingInfoCopy = diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/transfer/AutoAccountCreator.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/transfer/AutoAccountCreator.java index 70455fa8e52f..a67afd19bd3b 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/transfer/AutoAccountCreator.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/transfer/AutoAccountCreator.java @@ -19,8 +19,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.FAIL_INVALID; import static com.hedera.node.app.service.token.AliasUtils.asKeyFromAlias; import static com.hedera.node.app.service.token.AliasUtils.isOfEvmAddressSize; -import static com.hedera.node.app.service.token.impl.TokenServiceImpl.AUTO_MEMO; -import static com.hedera.node.app.service.token.impl.TokenServiceImpl.LAZY_MEMO; import static com.hedera.node.app.service.token.impl.TokenServiceImpl.THREE_MONTHS_IN_SECONDS; import static com.hedera.node.app.service.token.impl.handlers.BaseTokenHandler.UNLIMITED_AUTOMATIC_ASSOCIATIONS; import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; @@ -75,7 +73,6 @@ public AccountID create(@NonNull final Bytes alias, int requiredAutoAssociations ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); final TransactionBody.Builder syntheticCreation; - String memo; final var isAliasEVMAddress = isOfEvmAddressSize(alias); final var entitiesConfig = handleContext.configuration().getConfigData(EntitiesConfig.class); @@ -83,12 +80,10 @@ public AccountID create(@NonNull final Bytes alias, int requiredAutoAssociations ? UNLIMITED_AUTOMATIC_ASSOCIATIONS : requiredAutoAssociations; if (isAliasEVMAddress) { - syntheticCreation = createHollowAccount(alias, 0L, autoAssociations, LAZY_MEMO); - memo = LAZY_MEMO; + syntheticCreation = createHollowAccount(alias, 0L, autoAssociations); } else { final var key = asKeyFromAlias(alias); syntheticCreation = createZeroBalanceAccount(alias, key, autoAssociations); - memo = AUTO_MEMO; } // Dispatch the auto-creation record as a preceding record; note we pass null for the @@ -99,7 +94,6 @@ public AccountID create(@NonNull final Bytes alias, int requiredAutoAssociations null, handleContext.payer(), HandleContext.ConsensusThrottling.ON); - childRecord.memo(memo); // If the child transaction failed, we should fail the parent transaction as well and propagate the failure. validateTrue(childRecord.status() == ResponseCodeEnum.SUCCESS, childRecord.status()); @@ -118,16 +112,14 @@ public AccountID create(@NonNull final Bytes alias, int requiredAutoAssociations * @param alias alias of the account * @param balance initial balance of the account * @param maxAutoAssociations maxAutoAssociations of the account - * @param memo the memo to set on the transaction body * @return transaction body for new hollow-account */ public TransactionBody.Builder createHollowAccount( - @NonNull final Bytes alias, final long balance, final int maxAutoAssociations, @NonNull final String memo) { + @NonNull final Bytes alias, final long balance, final int maxAutoAssociations) { requireNonNull(alias); - requireNonNull(memo); final var baseBuilder = createAccountBase(balance, maxAutoAssociations); - baseBuilder.key(IMMUTABILITY_SENTINEL_KEY).alias(alias).memo(LAZY_MEMO); - return TransactionBody.newBuilder().memo(memo).cryptoCreateAccount(baseBuilder.build()); + baseBuilder.key(IMMUTABILITY_SENTINEL_KEY).alias(alias); + return TransactionBody.newBuilder().cryptoCreateAccount(baseBuilder.build()); } /** @@ -154,7 +146,7 @@ private CryptoCreateTransactionBody.Builder createAccountBase(final long balance private TransactionBody.Builder createZeroBalanceAccount( @NonNull final Bytes alias, @NonNull final Key key, final int maxAutoAssociations) { final var baseBuilder = createAccountBase(0L, maxAutoAssociations); - baseBuilder.key(key).alias(alias).memo(AUTO_MEMO).receiverSigRequired(false); - return TransactionBody.newBuilder().memo(AUTO_MEMO).cryptoCreateAccount(baseBuilder.build()); + baseBuilder.key(key).alias(alias).receiverSigRequired(false); + return TransactionBody.newBuilder().cryptoCreateAccount(baseBuilder.build()); } } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/TokenAirdropValidator.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/TokenAirdropValidator.java index d6540ad84dd4..e788eb5431b3 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/TokenAirdropValidator.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/validators/TokenAirdropValidator.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.validators; +import static com.hedera.hapi.node.base.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; @@ -24,7 +25,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable; import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsableForAliasedId; import static com.hedera.node.app.service.token.impl.validators.CryptoTransferValidator.validateTokenTransfers; @@ -48,7 +49,7 @@ import com.hedera.node.app.service.token.impl.handlers.transfer.customfees.CustomFeeExemptions; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; -import com.hedera.node.config.data.TokensConfig; +import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import javax.inject.Inject; @@ -97,11 +98,9 @@ public void validateSemantics( @NonNull final ReadableTokenStore tokenStore, @NonNull final ReadableTokenRelationStore tokenRelStore, @NonNull final ReadableNftStore nftStore) { - var tokensConfig = context.configuration().getConfigData(TokensConfig.class); - validateTrue( - op.tokenTransfers().size() <= tokensConfig.maxAllowedAirdropTransfersPerTx(), - TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED); - + var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); + var totalFungibleTransfers = 0; + var totalNftTransfers = 0; for (final var xfers : op.tokenTransfers()) { final var tokenId = xfers.tokenOrThrow(); final var token = getIfUsable(tokenId, tokenStore); @@ -118,6 +117,12 @@ public void validateSemantics( // 1. Validate token associations validateFungibleTransfers( context.payer(), senderAccount, tokenId, senderAccountAmount.get(), tokenRelStore); + totalFungibleTransfers += xfers.transfers().size(); + + // Verify that the current total number of (counted) fungible transfers does not exceed the limit + validateTrue( + totalFungibleTransfers <= ledgerConfig.tokenTransfersMaxLen(), + TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED); } // process non-fungible tokens transfers if any @@ -137,14 +142,11 @@ public void validateSemantics( final var senderId = nftTransfer.orElseThrow().senderAccountIDOrThrow(); final var senderAccount = accountStore.getAliasedAccountById(senderId); validateTrue(senderAccount != null, INVALID_ACCOUNT_ID); - validateNftTransfers( - context.payer(), - senderAccount, - tokenId, - xfers.nftTransfers(), - tokenRelStore, - tokenStore, - nftStore); + validateNftTransfers(senderAccount, tokenId, xfers.nftTransfers(), tokenRelStore, tokenStore, nftStore); + + totalNftTransfers += xfers.nftTransfers().size(); + // Verify that the current total number of (counted) nft transfers does not exceed the limit + validateTrue(totalNftTransfers <= ledgerConfig.nftTransfersMaxLen(), BATCH_SIZE_LIMIT_EXCEEDED); } } } @@ -184,7 +186,6 @@ private static void validateFungibleTransfers( } private void validateNftTransfers( - @NonNull final AccountID payer, @NonNull final Account senderAccount, @NonNull final TokenID tokenId, @NonNull final List nftTransfers, diff --git a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java index e5ba7bd85c1e..a9236706c640 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java @@ -26,26 +26,31 @@ provides com.hedera.node.app.service.token.TokenService with com.hedera.node.app.service.token.impl.TokenServiceImpl; + exports com.hedera.node.app.service.token.impl to + com.hedera.node.app, + com.hedera.node.app.service.contract.impl, + com.hedera.node.test.clients; + exports com.hedera.node.app.service.token.impl.schemas to + com.hedera.node.app, + com.hedera.node.test.clients; + exports com.hedera.node.app.service.token.impl.handlers.staking to + com.hedera.node.app, + com.hedera.node.test.clients; exports com.hedera.node.app.service.token.impl.handlers to com.hedera.node.app, - com.hedera.node.app.service.token.impl.test, com.hedera.node.test.clients; - exports com.hedera.node.app.service.token.impl; + exports com.hedera.node.app.service.token.impl.handlers.transfer to + com.hedera.node.app, + com.hedera.node.test.clients; exports com.hedera.node.app.service.token.impl.api to com.hedera.node.app, - com.hedera.node.app.service.token.impl.api.test; - exports com.hedera.node.app.service.token.impl.validators; + com.hedera.node.app.service.contract.impl, + com.hedera.node.test.clients; exports com.hedera.node.app.service.token.impl.util to com.hedera.node.app, - com.hedera.node.app.service.token.impl.test; - exports com.hedera.node.app.service.token.impl.handlers.staking to - com.hedera.node.app, - com.hedera.node.app.service.token.impl.test; - exports com.hedera.node.app.service.token.impl.handlers.transfer to - com.hedera.node.app; - exports com.hedera.node.app.service.token.impl.schemas to + com.hedera.node.test.clients; + exports com.hedera.node.app.service.token.impl.validators to com.hedera.node.app, - com.hedera.node.app.service.token.impl.api.test, com.hedera.node.test.clients; exports com.hedera.node.app.service.token.impl.comparator; } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAirdropHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAirdropHandlerTest.java index b9ff3d46b11d..568146da84d0 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAirdropHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenAirdropHandlerTest.java @@ -16,12 +16,12 @@ package com.hedera.node.app.service.token.impl.test.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_ACCOUNT_AMOUNTS; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_AMOUNTS; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED; import static com.hedera.node.app.service.token.impl.handlers.BaseTokenHandler.asToken; import static com.hedera.node.app.service.token.impl.test.handlers.transfer.AirDropTransferType.NFT_AIRDROP; import static com.hedera.node.app.service.token.impl.test.handlers.transfer.AirDropTransferType.TOKEN_AIRDROP; @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.NftTransfer; import com.hedera.hapi.node.base.PendingAirdropId; import com.hedera.hapi.node.base.PendingAirdropValue; import com.hedera.hapi.node.base.ResponseCodeEnum; @@ -379,13 +380,10 @@ void tokenTransfersAboveMax() { tokenAirdropHandler = new TokenAirdropHandler(tokenAirdropValidator, validator); given(handleContext.savepointStack()).willReturn(stack); given(stack.getBaseBuilder(TokenAirdropStreamBuilder.class)).willReturn(tokenAirdropRecordBuilder); - var tokenWithNoCustomFees = - fungibleToken.copyBuilder().customFees(Collections.emptyList()).build(); var nftWithNoCustomFees = nonFungibleToken .copyBuilder() .customFees(Collections.emptyList()) .build(); - writableTokenStore.put(tokenWithNoCustomFees); writableTokenStore.put(nftWithNoCustomFees); given(storeFactory.writableStore(WritableTokenStore.class)).willReturn(writableTokenStore); given(storeFactory.readableStore(ReadableTokenStore.class)).willReturn(writableTokenStore); @@ -396,26 +394,13 @@ void tokenTransfersAboveMax() { .build(); givenAirdropTxn(txn, payerId); - given(handleContext.dispatchRemovablePrecedingTransaction( - any(), eq(TokenAirdropStreamBuilder.class), eq(null), eq(payerId), any())) - .will((invocation) -> { - var pendingAirdropId = PendingAirdropId.newBuilder().build(); - var pendingAirdropValue = PendingAirdropValue.newBuilder().build(); - var pendingAirdropRecord = PendingAirdropRecord.newBuilder() - .pendingAirdropId(pendingAirdropId) - .pendingAirdropValue(pendingAirdropValue) - .build(); - - return tokenAirdropRecordBuilder.addPendingAirdrop(pendingAirdropRecord); - }); - given(handleContext.expiryValidator()).willReturn(expiryValidator); given(handleContext.feeCalculatorFactory()).willReturn(feeCalculatorFactory); given(handleContext.tryToChargePayer(anyLong())).willReturn(true); Assertions.assertThatThrownBy(() -> tokenAirdropHandler.handle(handleContext)) .isInstanceOf(HandleException.class) - .has(responseCode(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED)); + .has(responseCode(BATCH_SIZE_LIMIT_EXCEEDED)); } @Test @@ -575,9 +560,12 @@ private List transactionBodyAboveMaxTransferLimit() { for (int i = 0; i <= MAX_TOKEN_TRANSFERS; i++) { result.add(TokenTransferList.newBuilder() - .token(TOKEN_2468) - .transfers(ACCT_4444_PLUS_10) - .nftTransfers(SERIAL_1_FROM_3333_TO_4444, SERIAL_1_FROM_3333_TO_4444) + .token(nonFungibleTokenId) + .nftTransfers(NftTransfer.newBuilder() + .serialNumber(1) + .senderAccountID(ownerId) + .receiverAccountID(tokenReceiverId) + .build()) .build()); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/EndOfStakingPeriodUpdaterTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/EndOfStakingPeriodUpdaterTest.java index bfa41d3d2f4b..1f59bc968800 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/EndOfStakingPeriodUpdaterTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/EndOfStakingPeriodUpdaterTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.test.handlers.staking; +import static com.hedera.hapi.node.base.HederaFunctionality.NODE_STAKE_UPDATE; import static com.hedera.node.app.service.token.Units.HBARS_TO_TINYBARS; import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; import static com.hedera.node.app.service.token.impl.handlers.staking.EndOfStakingPeriodUpdater.calculateWeightFromStake; @@ -99,7 +100,8 @@ void setup() { .accountId(asAccount(800)) .tinybarBalance(100_000_000_000L) .build()); - subject = new EndOfStakingPeriodUpdater(new StakingRewardsHelper(), DEFAULT_CONFIG_PROVIDER); + subject = new EndOfStakingPeriodUpdater( + new StakingRewardsHelper(DEFAULT_CONFIG_PROVIDER), DEFAULT_CONFIG_PROVIDER); } @Test @@ -433,7 +435,7 @@ private void commonSetup( .willReturn((WritableSingletonState) stakingRewardsState); stakingRewardsStore = new WritableNetworkStakingRewardsStore(states); given(context.writableStore(WritableNetworkStakingRewardsStore.class)).willReturn(stakingRewardsStore); - given(context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class)) + given(context.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE)) .willReturn(nodeStakeUpdateRecordBuilder); given(context.knownNodeIds()).willReturn(Set.of(NODE_NUM_1.number(), NODE_NUM_2.number(), NODE_NUM_3.number())); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeInfoHelperTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeInfoHelperTest.java index aa2fa8a03e63..e2fc1aef8a4f 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeInfoHelperTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeInfoHelperTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.impl.test.handlers.staking; +import static com.hedera.hapi.node.base.HederaFunctionality.NODE_STAKE_UPDATE; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.STAKING_INFO_KEY; import static com.hedera.node.app.service.token.impl.schemas.V0490TokenSchema.STAKING_NETWORK_REWARDS_KEY; import static com.hedera.node.app.service.token.impl.test.WritableStakingInfoStoreImplTest.NODE_ID_1; @@ -57,7 +58,7 @@ @ExtendWith(MockitoExtension.class) class StakeInfoHelperTest { - private static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); + public static final Configuration DEFAULT_CONFIG = HederaTestConfigBuilder.createConfig(); private WritableStakingInfoStore infoStore; @@ -115,7 +116,7 @@ void marksNonExistingNodesToDeletedInStateAndAddsNewNodesToState() { // Platform address book has node Ids 2, 4, 8 final var networkInfo = new FakeNetworkInfo(); given(tokenContext.consensusTime()).willReturn(Instant.EPOCH); - given(tokenContext.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class)) + given(tokenContext.addPrecedingChildRecordBuilder(NodeStakeUpdateStreamBuilder.class, NODE_STAKE_UPDATE)) .willReturn(streamBuilder); given(streamBuilder.transaction(any())).willReturn(streamBuilder); given(streamBuilder.memo(any())).willReturn(streamBuilder); diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeRewardCalculatorImplTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeRewardCalculatorImplTest.java index aa551001e8a5..d8c9640849b7 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeRewardCalculatorImplTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakeRewardCalculatorImplTest.java @@ -79,14 +79,16 @@ void setUp() { @Test void zeroRewardsForMissingNodeStakeInfo() { - final var reward = subject.computeRewardFromDetails(Account.newBuilder().build(), null, 321, 123); + final var reward = StakeRewardCalculatorImpl.computeRewardFromDetails( + Account.newBuilder().build(), null, 321, 123); assertEquals(0, reward); } @Test void zeroRewardsForDeletedNodeStakeInfo() { final var stakingInfo = StakingNodeInfo.newBuilder().deleted(true).build(); - final var reward = subject.computeRewardFromDetails(Account.newBuilder().build(), stakingInfo, 321, 123); + final var reward = StakeRewardCalculatorImpl.computeRewardFromDetails( + Account.newBuilder().build(), stakingInfo, 321, 123); assertEquals(0, reward); } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java index 24d1ff39c8fe..20a5c0c5edf8 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHandlerImplTest.java @@ -91,7 +91,7 @@ public void setUp() { given(context.consensusTime()).willReturn(consensusInstant); givenStoresAndConfig(context); - stakingRewardHelper = new StakingRewardsHelper(); + stakingRewardHelper = new StakingRewardsHelper(configProvider); stakePeriodManager = new StakePeriodManager(configProvider, instantSource); stakeRewardCalculator = new StakeRewardCalculatorImpl(stakePeriodManager); rewardsPayer = new StakingRewardsDistributor(stakingRewardHelper, stakeRewardCalculator); diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHelperTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHelperTest.java index 96d6a26e3af1..82986bc136e8 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHelperTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/staking/StakingRewardsHelperTest.java @@ -18,7 +18,9 @@ import static com.hedera.node.app.service.token.impl.handlers.staking.StakingRewardsHelper.MAX_PENDING_REWARDS; import static com.hedera.node.app.service.token.impl.handlers.staking.StakingRewardsHelper.requiresExternalization; +import static com.hedera.node.app.service.token.impl.test.handlers.staking.StakeInfoHelperTest.DEFAULT_CONFIG; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.BDDMockito.given; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; @@ -28,24 +30,34 @@ import com.hedera.node.app.spi.fixtures.util.LogCaptureExtension; import com.hedera.node.app.spi.fixtures.util.LoggingSubject; import com.hedera.node.app.spi.fixtures.util.LoggingTarget; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfigImpl; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(LogCaptureExtension.class) +@ExtendWith({MockitoExtension.class, LogCaptureExtension.class}) class StakingRewardsHelperTest extends CryptoTokenHandlerTestBase { - @LoggingSubject - private StakingRewardsHelper subject = new StakingRewardsHelper(); @LoggingTarget private LogCaptor logCaptor; + @Mock + private ConfigProvider configProvider; + + @LoggingSubject + private StakingRewardsHelper subject; + @BeforeEach public void setUp() { super.setUp(); refreshWritableStores(); + given(configProvider.getConfiguration()).willReturn(new VersionedConfigImpl(DEFAULT_CONFIG, 1)); + subject = new StakingRewardsHelper(configProvider); } @Test @@ -175,7 +187,6 @@ void decreasesPendingRewardsToZeroInStakingInfoMapIfNegative() { @Test void increasesPendingRewardsAccurately() { - final var subject = new StakingRewardsHelper(); assertThat(writableRewardsStore.get().pendingRewards()).isEqualTo(1000L); final var copyStakingInfo = subject.increasePendingRewardsBy(writableRewardsStore, 100L, writableStakingInfoStore.get(0L)); @@ -184,7 +195,6 @@ void increasesPendingRewardsAccurately() { @Test void increasesPendingRewardsByZeroIfStkingInfoShowsDeleted() { - final var subject = new StakingRewardsHelper(); writableStakingInfoStore.put( node0Id.number(), node0Info.copyBuilder().deleted(true).build()); assertThat(writableStakingInfoStore.get(0).pendingRewards()).isEqualTo(1000000L); @@ -197,7 +207,6 @@ void increasesPendingRewardsByZeroIfStkingInfoShowsDeleted() { @Test void increasesPendingRewardsByMaxValueIfVeryLargeNumber() { - final var subject = new StakingRewardsHelper(); assertThat(writableStakingInfoStore.get(0).pendingRewards()).isEqualTo(1000000L); assertThat(writableRewardsStore.get().pendingRewards()).isEqualTo(1000L); diff --git a/hedera-node/hedera-token-service/build.gradle.kts b/hedera-node/hedera-token-service/build.gradle.kts index d6c299811c32..f8e09e6d5f3f 100644 --- a/hedera-node/hedera-token-service/build.gradle.kts +++ b/hedera-node/hedera-token-service/build.gradle.kts @@ -22,10 +22,6 @@ plugins { description = "Hedera Token Service API" -// Remove the following line to enable all 'javac' lint checks that we have turned on by default -// and then fix the reported issues. -tasks.withType().configureEach { options.compilerArgs.add("-Xlint:-exports") } - testModuleInfo { requires("org.assertj.core") requires("org.junit.jupiter.api") diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/api/AccountSummariesApi.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/api/AccountSummariesApi.java index 8ef25ed01b38..33907ff9f8be 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/api/AccountSummariesApi.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/api/AccountSummariesApi.java @@ -23,14 +23,13 @@ import static com.hedera.hapi.node.base.TokenKycStatus.KYC_NOT_APPLICABLE; import static com.hedera.hapi.node.base.TokenKycStatus.REVOKED; import static com.hedera.node.app.hapi.utils.CommonUtils.asEvmAddress; +import static com.hedera.node.app.service.token.AliasUtils.extractEvmAddress; import static com.hedera.node.app.service.token.api.StakingRewardsApi.epochSecondAtStartOfPeriod; import static com.hedera.node.app.service.token.api.StakingRewardsApi.estimatePendingReward; -import static com.hedera.node.app.spi.key.KeyUtils.ECDSA_SECP256K1_COMPRESSED_KEY_LENGTH; import static com.swirlds.common.utility.CommonUtils.hex; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.StakingInfo; import com.hedera.hapi.node.base.Timestamp; import com.hedera.hapi.node.base.TokenFreezeStatus; @@ -40,17 +39,13 @@ import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.state.token.Token; import com.hedera.hapi.node.state.token.TokenRelation; -import com.hedera.node.app.hapi.utils.EthSigsUtils; import com.hedera.node.app.service.token.ReadableStakingInfoStore; import com.hedera.node.app.service.token.ReadableTokenRelationStore; import com.hedera.node.app.service.token.ReadableTokenStore; -import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.function.UnaryOperator; /** * Methods for summarizing an account's relationships and attributes to HAPI clients. @@ -78,17 +73,11 @@ public interface AccountSummariesApi { */ static String hexedEvmAddressOf(@NonNull final Account account) { requireNonNull(account); - final var alias = account.alias().toByteArray(); - if (alias.length == EVM_ADDRESS_SIZE) { - return hex(alias); - } - // If we can recover an Ethereum EOA address from the account key, we should return that - final var evmAddress = tryAddressRecovery(account.key(), EthSigsUtils::recoverAddressFromPubKey); - if (evmAddress.length == EVM_ADDRESS_SIZE) { - return Bytes.wrap(evmAddress).toHex(); - } else { - return hex(asEvmAddress(account.accountIdOrThrow().accountNumOrThrow())); - } + final var arbitraryEvmAddress = extractEvmAddress(account.alias()); + final var evmAddress = arbitraryEvmAddress != null + ? arbitraryEvmAddress.toByteArray() + : asEvmAddress(account.accountIdOrThrow().accountNumOrThrow()); + return hex(evmAddress); } /** @@ -157,32 +146,6 @@ private static void addTokenRelation( ret.add(tokenRelationship); } - /** - * Tries to recover EVM address from an account key with a given recovery function. - * - * @param key the key of the account - * @param addressRecovery the function to recover EVM address - * @return the explicit EVM address bytes, if recovered; empty array otherwise - */ - private static byte[] tryAddressRecovery(@Nullable final Key key, final UnaryOperator addressRecovery) { - if (key != null && key.hasEcdsaSecp256k1()) { - // Only compressed keys are stored at the moment - final var keyBytes = key.ecdsaSecp256k1().toByteArray(); - if (keyBytes.length == ECDSA_SECP256K1_COMPRESSED_KEY_LENGTH) { - final var evmAddress = addressRecovery.apply(keyBytes); - if (evmAddress != null && evmAddress.length == EVM_ADDRESS_SIZE) { - return evmAddress; - } else { - // Not ever expected, since above checks should imply a valid input to the - // LibSecp256k1 library - throw new IllegalArgumentException( - "Unable to recover EVM address from structurally valid " + hex(keyBytes)); - } - } - } - return new byte[0]; - } - /** * Returns the summary of an account's staking info, given the account, the staking info store, and some * information about the network. diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/TokenContext.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/TokenContext.java index 8f1cf1b3c72f..2e98ff62850d 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/TokenContext.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/TokenContext.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.token.records; +import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.node.app.service.token.TokenService; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.record.StreamBuilder; @@ -74,14 +75,16 @@ public interface TokenContext { * Adds a preceding child record builder to the list of record builders. If the current {@link HandleContext} (or * any parent context) is rolled back, all child record builders will be reverted. * - * @param recordBuilderClass the record type * @param the record type + * @param recordBuilderClass the record type + * @param functionality the functionality of the record * @return the new child record builder * @throws NullPointerException if {@code recordBuilderClass} is {@code null} * @throws IllegalArgumentException if the record builder type is unknown to the app */ @NonNull - T addPrecedingChildRecordBuilder(@NonNull Class recordBuilderClass); + T addPrecedingChildRecordBuilder( + @NonNull Class recordBuilderClass, @NonNull HederaFunctionality functionality); /** * Returns the set of all known node ids, including ids that may no longer be active. diff --git a/hedera-node/hedera-token-service/src/main/java/module-info.java b/hedera-node/hedera-token-service/src/main/java/module-info.java index 89e0be69c10e..1b3b0d05b252 100644 --- a/hedera-node/hedera-token-service/src/main/java/module-info.java +++ b/hedera-node/hedera-token-service/src/main/java/module-info.java @@ -3,11 +3,7 @@ */ module com.hedera.node.app.service.token { exports com.hedera.node.app.service.token; - exports com.hedera.node.app.service.token.api to - com.hedera.node.app.service.contract.impl, - com.hedera.node.app, - com.hedera.node.app.service.token.impl, - com.hedera.node.app.service.token.test.fixtures; + exports com.hedera.node.app.service.token.api; exports com.hedera.node.app.service.token.records to com.hedera.node.app.service.contract.impl, com.hedera.node.app, diff --git a/hedera-node/infrastructure/docker/containers/production-next/consensus-node/entrypoint.sh b/hedera-node/infrastructure/docker/containers/production-next/consensus-node/entrypoint.sh index 6f92d8794d10..37724ddcdf00 100644 --- a/hedera-node/infrastructure/docker/containers/production-next/consensus-node/entrypoint.sh +++ b/hedera-node/infrastructure/docker/containers/production-next/consensus-node/entrypoint.sh @@ -31,7 +31,7 @@ function waitForFile() { [[ -z "${fileName}" ]] && return 1 for (( attempts = 0; attempts < 20; attempts++ )); do if [[ -f "${fileName}" ]]; then - size="$(stat -f '+%z' "${fileName}")" + size="$(stat --format='%s' "${fileName}")" [[ -n "${size}" && "${size}" -gt 0 ]] && return 0 fi sleep 6 @@ -84,7 +84,8 @@ if [[ -n "${JAVA_HEAP_MAX}" ]]; then JAVA_HEAP_OPTS="${JAVA_HEAP_OPTS} -Xmx${JAVA_HEAP_MAX}" fi -# Setup Main Class - Updated to default to the new ServicesMain entrypoint class +# Setup Main Class - Updated to default to the ServicesMain entrypoint class introduced in release v0.35.0 and production +# ready as of the v0.52.0 release. [[ -z "${JAVA_MAIN_CLASS}" ]] && JAVA_MAIN_CLASS="com.hedera.node.app.ServicesMain" # Setup Classpath diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/LeakyRepeatableHapiTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/LeakyRepeatableHapiTest.java new file mode 100644 index 000000000000..4c1fc360ab6e --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/LeakyRepeatableHapiTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.junit; + +import static com.hedera.services.bdd.junit.TestTags.ONLY_REPEATABLE; +import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ; + +import com.hedera.services.bdd.junit.extensions.NetworkTargetingExtension; +import com.hedera.services.bdd.junit.extensions.SpecNamingExtension; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.ResourceLock; + +/** + * Annotation for a {@link HapiTest} that can only be run in repeatable mode, and also changes network state + * in a way that would "leak" into other tests if not cleaned up. The {@link RepeatableReason} annotation + * enumerates common reasons a test has to run in repeatable mode. + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@TestFactory +@ExtendWith({NetworkTargetingExtension.class, SpecNamingExtension.class}) +@ResourceLock(value = "NETWORK", mode = READ) +@Tag(ONLY_REPEATABLE) +public @interface LeakyRepeatableHapiTest { + /** + * The reasons the test has to run in repeatable mode. + * @return the reasons the test has to run in repeatable mode + */ + RepeatableReason[] value(); + + /** + * If set, the names of properties this test overrides and needs automatically + * restored to their original values after the test completes. + * @return the names of properties this test overrides + */ + String[] overrides() default {}; + + /** + * If not blank, the path of a JSON file containing the throttles to apply to the test. The + * original contents of the throttles system file will be restored after the test completes. + * @return the name of a resource to load throttles from + */ + String throttles() default ""; + + /** + * If not blank, the path of a JSON file containing the fee schedules to apply to the test. The + * original contents of the fee schedules system file will be restored after the test completes. + * @return the name of a resource to load fee schedules from + */ + String fees() default ""; +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableHapiTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableHapiTest.java index 4d8894fd9e0f..1a8073cc8c7e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableHapiTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/RepeatableHapiTest.java @@ -31,8 +31,8 @@ import org.junit.jupiter.api.parallel.ResourceLock; /** - * Annotation for a {@link HapiTest} that can only be run in repeatable mode. The {@link EmbeddedReason} annotation - * enumerates common reasons a test has to run in embedded mode. + * Annotation for a {@link HapiTest} that can only be run in repeatable mode. The {@link RepeatableReason} annotation + * enumerates common reasons a test has to run in repeatable mode. */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/ExtensionUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/ExtensionUtils.java index ba2c12f7f36f..249f6fbb65fd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/ExtensionUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/ExtensionUtils.java @@ -23,6 +23,7 @@ import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.LeakyEmbeddedHapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; +import com.hedera.services.bdd.junit.LeakyRepeatableHapiTest; import com.hedera.services.bdd.junit.RepeatableHapiTest; import edu.umd.cs.findbugs.annotations.NonNull; import java.lang.reflect.Method; @@ -40,6 +41,7 @@ private static boolean isHapiTest(@NonNull final Method method) { || isAnnotated(method, GenesisHapiTest.class) || isAnnotated(method, EmbeddedHapiTest.class) || isAnnotated(method, RepeatableHapiTest.class) - || isAnnotated(method, LeakyEmbeddedHapiTest.class); + || isAnnotated(method, LeakyEmbeddedHapiTest.class) + || isAnnotated(method, LeakyRepeatableHapiTest.class); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/NetworkTargetingExtension.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/NetworkTargetingExtension.java index d4bce03187db..ef910fb125dc 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/NetworkTargetingExtension.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/extensions/NetworkTargetingExtension.java @@ -26,6 +26,7 @@ import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.LeakyEmbeddedHapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; +import com.hedera.services.bdd.junit.LeakyRepeatableHapiTest; import com.hedera.services.bdd.junit.hedera.HederaNetwork; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedNetwork; import com.hedera.services.bdd.spec.HapiSpec; @@ -67,6 +68,9 @@ public void beforeEach(@NonNull final ExtensionContext extensionContext) { } else if (isAnnotated(method, LeakyEmbeddedHapiTest.class)) { final var a = method.getAnnotation(LeakyEmbeddedHapiTest.class); bindThreadTargets(a.requirement(), a.overrides(), a.throttles(), a.fees()); + } else if (isAnnotated(method, LeakyRepeatableHapiTest.class)) { + final var a = method.getAnnotation(LeakyRepeatableHapiTest.class); + bindThreadTargets(new ContextRequirement[] {}, a.overrides(), a.throttles(), a.fees()); } } }); @@ -96,7 +100,7 @@ private void bindThreadTargets( * @param contextRequirements the context requirements of the test * @param relevantRequirement the relevant context requirement * @param resource the path to the resource - * @return the effective throttle resource + * @return the effective resource */ private @Nullable String effectiveResource( @NonNull final ContextRequirement[] contextRequirements, diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractNode.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractNode.java index 3ee9f3c43f95..0e8712de4205 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractNode.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/AbstractNode.java @@ -55,6 +55,11 @@ public int getGrpcPort() { return metadata.grpcPort(); } + @Override + public int getGrpcNodeOperatorPort() { + return metadata.grpcNodeOperatorPort(); + } + @Override public long getNodeId() { return metadata.nodeId(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNode.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNode.java index 0bde5a328647..c70f0e77b29a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNode.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/HederaNode.java @@ -41,6 +41,13 @@ public interface HederaNode { */ int getGrpcPort(); + /** + * Gets the port number of the node operator gRPC service. + * + * @return the port number of the node operator gRPC service + */ + int getGrpcNodeOperatorPort(); + /** * Gets the node ID, such as 0, 1, 2, or 3. * @return the node ID diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/NodeMetadata.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/NodeMetadata.java index 5bdba5ecf5b7..a2f4af7ba8a5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/NodeMetadata.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/NodeMetadata.java @@ -29,6 +29,7 @@ public record NodeMetadata( AccountID accountId, String host, int grpcPort, + int grpcNodeOperatorPort, int gossipPort, int gossipTlsPort, int prometheusPort, @@ -39,15 +40,29 @@ public record NodeMetadata( * Create a new instance with the same values as this instance, but different ports. * * @param grpcPort the new grpc port + * @param grpcNodeOperatorPort the new grpc node operator port * @param gossipPort the new gossip port * @param tlsGossipPort the new tls gossip port * @param prometheusPort the new prometheus port * @return a new instance with the same values as this instance, but different ports */ public NodeMetadata withNewPorts( - final int grpcPort, final int gossipPort, final int tlsGossipPort, final int prometheusPort) { + final int grpcPort, + final int grpcNodeOperatorPort, + final int gossipPort, + final int tlsGossipPort, + final int prometheusPort) { return new NodeMetadata( - nodeId, name, accountId, host, grpcPort, gossipPort, tlsGossipPort, prometheusPort, workingDir); + nodeId, + name, + accountId, + host, + grpcPort, + grpcNodeOperatorPort, + gossipPort, + tlsGossipPort, + prometheusPort, + workingDir); } /** @@ -58,7 +73,16 @@ public NodeMetadata withNewPorts( public NodeMetadata withNewAccountId(@NonNull final AccountID accountId) { requireNonNull(accountId); return new NodeMetadata( - nodeId, name, accountId, host, grpcPort, gossipPort, gossipTlsPort, prometheusPort, workingDir); + nodeId, + name, + accountId, + host, + grpcPort, + grpcNodeOperatorPort, + gossipPort, + gossipTlsPort, + prometheusPort, + workingDir); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java index b939ce5ff3eb..631d5015ebae 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/AbstractEmbeddedHedera.java @@ -55,6 +55,7 @@ import com.swirlds.platform.system.SoftwareVersion; import com.swirlds.platform.system.address.Address; import com.swirlds.platform.system.address.AddressBook; +import com.swirlds.platform.system.state.notifications.StateHashedNotification; import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookBuilder; import com.swirlds.state.spi.CommittableWritableStates; import com.swirlds.state.spi.WritableSingletonState; @@ -77,13 +78,12 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { private static final Logger log = LogManager.getLogger(AbstractEmbeddedHedera.class); private static final int NANOS_IN_A_SECOND = 1_000_000_000; - private static final long VALID_START_TIME_OFFSET_SECS = 42; private static final SemanticVersion EARLIER_SEMVER = SemanticVersion.newBuilder().patch(1).build(); private static final SemanticVersion LATER_SEMVER = SemanticVersion.newBuilder().major(999).build(); - protected static final NodeId MISSING_NODE_ID = new NodeId(666L); + protected static final NodeId MISSING_NODE_ID = NodeId.of(666L); protected static final int MAX_PLATFORM_TXN_SIZE = 1024 * 6; protected static final int MAX_QUERY_RESPONSE_SIZE = 1024 * 1024 * 2; protected static final Hash FAKE_START_OF_STATE_HASH = new Hash(new byte[48]); @@ -93,6 +93,8 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected static final PlatformStatusChangeNotification FREEZE_COMPLETE_NOTIFICATION = new PlatformStatusChangeNotification(FREEZE_COMPLETE); + private final boolean blockStreamEnabled; + protected final Map nodeIds; protected final Map accountIds; protected final FakeState state = new FakeState(); @@ -102,9 +104,10 @@ public abstract class AbstractEmbeddedHedera implements EmbeddedHedera { protected final AtomicInteger nextNano = new AtomicInteger(0); protected final Hedera hedera; protected final ServicesSoftwareVersion version; - protected final FakeTssBaseService tssBaseService; protected final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + protected FakeTssBaseService tssBaseService; + protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { requireNonNull(node); addressBook = loadAddressBook(node.getExternalPath(ADDRESS_BOOK)); @@ -114,14 +117,17 @@ protected AbstractEmbeddedHedera(@NonNull final EmbeddedNode node) { .collect(toMap(Address::getNodeId, address -> parseAccount(address.getMemo()))); defaultNodeId = addressBook.getNodeId(0); defaultNodeAccountId = fromPbj(accountIds.get(defaultNodeId)); - tssBaseService = new FakeTssBaseService(); hedera = new Hedera( ConstructableRegistry.getInstance(), FakeServicesRegistry.FACTORY, new FakeServiceMigrator(), this::now, - () -> tssBaseService); + appContext -> { + this.tssBaseService = new FakeTssBaseService(appContext); + return tssBaseService; + }); version = (ServicesSoftwareVersion) hedera.getSoftwareVersion(); + blockStreamEnabled = hedera.isBlockStreamEnabled(); Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow)); } @@ -173,7 +179,7 @@ public Timestamp nextValidStart() { nextNano.set(1); } return Timestamp.newBuilder() - .setSeconds(now().getEpochSecond() - VALID_START_TIME_OFFSET_SECS) + .setSeconds(now().getEpochSecond() - validStartOffsetSecs()) .setNanos(candidateNano) .build(); } @@ -209,9 +215,25 @@ public TransactionResponse submit( }); } + /** + * If block stream is enabled, notify the block stream manager of the state hash at the end of the round. + * @param roundNumber the round number + */ + protected void notifyBlockStreamManagerIfEnabled(final long roundNumber) { + if (blockStreamEnabled) { + hedera.blockStreamManager().notify(new StateHashedNotification(roundNumber, FAKE_START_OF_STATE_HASH)); + } + } + protected abstract TransactionResponse submit( @NonNull Transaction transaction, @NonNull AccountID nodeAccountId, @NonNull SemanticVersion version); + /** + * Returns the number of seconds to offset the next valid start time. + * @return the number of seconds to offset the next valid start time + */ + protected abstract long validStartOffsetSecs(); + /** * Returns the fake platform to start and stop. * diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java index 9ebbb63abd61..0ba160333025 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/ConcurrentEmbeddedHedera.java @@ -32,7 +32,6 @@ import com.hederahashgraph.api.proto.java.TransactionResponse; import com.swirlds.platform.system.Platform; import com.swirlds.platform.system.events.ConsensusEvent; -import com.swirlds.platform.system.state.notifications.StateHashedNotification; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Duration; import java.time.Instant; @@ -50,6 +49,7 @@ */ class ConcurrentEmbeddedHedera extends AbstractEmbeddedHedera implements EmbeddedHedera { private static final Logger log = LogManager.getLogger(ConcurrentEmbeddedHedera.class); + private static final long VALID_START_TIME_OFFSET_SECS = 42; private static final Duration SIMULATED_ROUND_DURATION = Duration.ofMillis(1); private final ConcurrentFakePlatform platform; @@ -96,6 +96,11 @@ public TransactionResponse submit( } } + @Override + protected long validStartOffsetSecs() { + return VALID_START_TIME_OFFSET_SECS; + } + @Override protected AbstractFakePlatform fakePlatform() { return platform; @@ -157,9 +162,7 @@ private void handleTransactions() { final var round = new FakeRound(roundNo.getAndIncrement(), addressBook, consensusEvents); hedera.handleWorkflow().handleRound(state, round); hedera.onSealConsensusRound(round, state); - // Immediately notify the block stream manager of the "hash" at the end of this round - hedera.blockStreamManager() - .notify(new StateHashedNotification(round.getRoundNum(), FAKE_START_OF_STATE_HASH)); + notifyBlockStreamManagerIfEnabled(round.getRoundNum()); prehandledEvents.clear(); } // Now drain all events that will go in the next round and pre-handle them @@ -167,8 +170,8 @@ private void handleTransactions() { queue.drainTo(newEvents); newEvents.forEach(event -> hedera.onPreHandle(event, state)); prehandledEvents.addAll(newEvents); - } catch (Exception e) { - log.warn("Error handling transactions", e); + } catch (Throwable t) { + log.error("Error handling transactions", t); } } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedNetwork.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedNetwork.java index 154a3b6c6066..7df523bbe565 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedNetwork.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/EmbeddedNetwork.java @@ -72,7 +72,7 @@ public static HederaNetwork newSharedNetwork() { } public EmbeddedNetwork(@NonNull final String name, @NonNull final String workingDir) { - super(name, List.of(new EmbeddedNode(classicMetadataFor(0, name, FAKE_HOST, workingDir, 0, 0, 0, 0)))); + super(name, List.of(new EmbeddedNode(classicMetadataFor(0, name, FAKE_HOST, workingDir, 0, 0, 0, 0, 0)))); this.embeddedNode = (EmbeddedNode) nodes().getFirst(); // Even though we are only embedding node0, we generate an address book // for a "classic" HapiTest network with 4 nodes so that tests can still @@ -81,8 +81,8 @@ public EmbeddedNetwork(@NonNull final String name, @NonNull final String working this.configTxt = configTxtForLocal( name(), IntStream.range(0, CLASSIC_HAPI_TEST_NETWORK_SIZE) - .mapToObj(nodeId -> - new EmbeddedNode(classicMetadataFor(nodeId, name, FAKE_HOST, workingDir, 0, 0, 0, 0))) + .mapToObj(nodeId -> new EmbeddedNode( + classicMetadataFor(nodeId, name, FAKE_HOST, workingDir, 0, 0, 0, 0, 0))) .toList(), 0, 0); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java index 573d16b4ab52..9794b3ce565c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/RepeatableEmbeddedHedera.java @@ -36,7 +36,6 @@ import com.swirlds.platform.system.Round; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.events.ConsensusEvent; -import com.swirlds.platform.system.state.notifications.StateHashedNotification; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Duration; import java.time.Instant; @@ -94,18 +93,35 @@ public TransactionResponse submit( new FakeEvent(nodeId, time.now(), semanticVersion, createAppPayloadWrapper(payload)); } if (response.getNodeTransactionPrecheckCode() == OK) { - hedera.onPreHandle(platform.lastCreatedEvent, state); - final var round = platform.nextConsensusRound(); - // Handle each transaction in own round - hedera.handleWorkflow().handleRound(state, round); - hedera.onSealConsensusRound(round, state); - // Immediately notify the block stream manager of the "hash" at the end of this round - hedera.blockStreamManager() - .notify(new StateHashedNotification(round.getRoundNum(), FAKE_START_OF_STATE_HASH)); + handleNextRound(); + // If handling this transaction scheduled TSS work, do it synchronously as well + while (tssBaseService.hasTssSubmission()) { + platform.lastCreatedEvent = null; + tssBaseService.executeNextTssSubmission(); + if (platform.lastCreatedEvent != null) { + handleNextRound(); + } + } } return response; } + @Override + protected long validStartOffsetSecs() { + // We handle each transaction in a round starting in the next second of fake consensus time, so + // we don't need any offset here; this simplifies tests that validate purging expired receipts + return 0L; + } + + private void handleNextRound() { + hedera.onPreHandle(platform.lastCreatedEvent, state); + final var round = platform.nextConsensusRound(); + // Handle each transaction in own round + hedera.handleWorkflow().handleRound(state, round); + hedera.onSealConsensusRound(round, state); + notifyBlockStreamManagerIfEnabled(round.getRoundNum()); + } + private class SynchronousFakePlatform extends AbstractFakePlatform implements Platform { private FakeEvent lastCreatedEvent; @@ -117,7 +133,7 @@ public SynchronousFakePlatform( } @Override - public boolean createTransaction(@NonNull byte[] transaction) { + public boolean createTransaction(@NonNull final byte[] transaction) { lastCreatedEvent = new FakeEvent( defaultNodeId, time.now(), version.getPbjSemanticVersion(), createAppPayloadWrapper(transaction)); return true; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java index 0ce0657779e5..04c29d6ac293 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/FakeTssBaseService.java @@ -19,15 +19,26 @@ import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.state.roster.Roster; +import com.hedera.node.app.spi.AppContext; +import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.tss.TssBaseService; +import com.hedera.node.app.tss.TssBaseServiceImpl; +import com.hedera.node.app.tss.handlers.TssHandlers; +import com.hedera.node.app.tss.stores.ReadableTssBaseStore; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.junit.HapiTest; import com.swirlds.common.utility.CommonUtils; import com.swirlds.state.spi.SchemaRegistry; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayDeque; import java.util.List; +import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ForkJoinPool; import java.util.function.BiConsumer; +import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,6 +54,24 @@ public class FakeTssBaseService implements TssBaseService { private static final Logger log = LogManager.getLogger(FakeTssBaseService.class); + private final TssBaseServiceImpl delegate; + + /** + * The type of signing to perform. + */ + public enum Signing { + /** + * If not ignoring requests, provide a fake signature by hashing the message. + */ + FAKE, + /** + * Delegate to the real TSS base service. + */ + DELEGATE + } + + private Signing signing = Signing.FAKE; + /** * Copy-on-write list to avoid concurrent modification exceptions if a consumer unregisters * itself in its callback. @@ -51,6 +80,28 @@ public class FakeTssBaseService implements TssBaseService { private boolean ignoreRequests = false; + private final Queue pendingTssSubmission = new ArrayDeque<>(); + + public FakeTssBaseService(@NonNull final AppContext appContext) { + delegate = new TssBaseServiceImpl(appContext, ForkJoinPool.commonPool(), pendingTssSubmission::offer); + } + + /** + * Returns whether there is a pending TSS submission. + * + * @return if the fake TSS base service has a pending TSS submission + */ + public boolean hasTssSubmission() { + return !pendingTssSubmission.isEmpty(); + } + + /** + * Executes the pending TSS submission and clears it. + */ + public void executeNextTssSubmission() { + requireNonNull(pendingTssSubmission.poll()).run(); + } + /** * When called, will start ignoring any requests for ledger signatures. */ @@ -65,42 +116,103 @@ public void stopIgnoringRequests() { ignoreRequests = false; } + /** + * Switches to using fake signatures. + */ + public void useFakeSignatures() { + signing = Signing.FAKE; + } + + /** + * Switches to using real signatures. + */ + public void useRealSignatures() { + signing = Signing.DELEGATE; + } + @Override - public void registerSchemas(@NonNull final SchemaRegistry registry) { - // No-op for now + public Status getStatus( + @NonNull final Roster roster, + @NonNull final Bytes ledgerId, + @NonNull final ReadableTssBaseStore tssBaseStore) { + requireNonNull(roster); + requireNonNull(ledgerId); + requireNonNull(tssBaseStore); + return delegate.getStatus(roster, ledgerId, tssBaseStore); + } + + @Override + public void adopt(@NonNull final Roster roster) { + requireNonNull(roster); + delegate.adopt(roster); + } + + @Override + public void bootstrapLedgerId( + @NonNull final Roster roster, + @NonNull final HandleContext context, + @NonNull final Consumer ledgerIdConsumer) { + requireNonNull(roster); + requireNonNull(context); + requireNonNull(ledgerIdConsumer); + delegate.bootstrapLedgerId(roster, context, ledgerIdConsumer); } @Override public void requestLedgerSignature(@NonNull final byte[] messageHash) { requireNonNull(messageHash); - if (ignoreRequests) { - return; - } - final var mockSignature = noThrowSha384HashOf(messageHash); - // Simulate asynchronous completion of the ledger signature - CompletableFuture.runAsync(() -> consumers.forEach(consumer -> { - try { - consumer.accept(messageHash, mockSignature); - } catch (Exception e) { - log.error( - "Failed to provide signature {} on message {} to consumer {}", - CommonUtils.hex(mockSignature), - CommonUtils.hex(messageHash), - consumer, - e); + switch (signing) { + case FAKE -> { + if (ignoreRequests) { + return; + } + final var mockSignature = noThrowSha384HashOf(messageHash); + // Simulate asynchronous completion of the ledger signature + CompletableFuture.runAsync(() -> consumers.forEach(consumer -> { + try { + consumer.accept(messageHash, mockSignature); + } catch (Exception e) { + log.error( + "Failed to provide signature {} on message {} to consumer {}", + CommonUtils.hex(mockSignature), + CommonUtils.hex(messageHash), + consumer, + e); + } + })); } - })); + case DELEGATE -> delegate.requestLedgerSignature(messageHash); + } + } + + @Override + public void setCandidateRoster(@NonNull final Roster roster, @NonNull final HandleContext context) { + requireNonNull(roster); + requireNonNull(context); + delegate.setCandidateRoster(roster, context); + } + + @Override + public void registerSchemas(@NonNull final SchemaRegistry registry) { + delegate.registerSchemas(registry); } @Override public void registerLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { requireNonNull(consumer); consumers.add(consumer); + delegate.registerLedgerSignatureConsumer(consumer); } @Override public void unregisterLedgerSignatureConsumer(@NonNull final BiConsumer consumer) { requireNonNull(consumer); consumers.remove(consumer); + delegate.unregisterLedgerSignatureConsumer(consumer); + } + + @Override + public TssHandlers tssHandlers() { + return delegate.tssHandlers(); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibrary.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibrary.java new file mode 100644 index 000000000000..99e66d1e03ce --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibrary.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.junit.hedera.embedded.fakes.tss; + +import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; + +import com.hedera.node.app.tss.api.TssLibrary; +import com.hedera.node.app.tss.api.TssMessage; +import com.hedera.node.app.tss.api.TssParticipantDirectory; +import com.hedera.node.app.tss.api.TssPrivateShare; +import com.hedera.node.app.tss.api.TssPublicShare; +import com.hedera.node.app.tss.api.TssShareSignature; +import com.hedera.node.app.tss.pairings.FakeFieldElement; +import com.hedera.node.app.tss.pairings.FakeGroupElement; +import com.hedera.node.app.tss.pairings.PairingPrivateKey; +import com.hedera.node.app.tss.pairings.PairingPublicKey; +import com.hedera.node.app.tss.pairings.PairingSignature; +import com.hedera.node.app.tss.pairings.SignatureSchema; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +public class FakeTssLibrary implements TssLibrary { + public static final SignatureSchema SIGNATURE_SCHEMA = SignatureSchema.create(new byte[] {1}); + private static final PairingPrivateKey AGGREGATED_PRIVATE_KEY = + new PairingPrivateKey(new FakeFieldElement(BigInteger.valueOf(42L)), SIGNATURE_SCHEMA); + private static final PairingPublicKey LEDGER_ID = AGGREGATED_PRIVATE_KEY.createPublicKey(); + + private static final SecureRandom RANDOM = new SecureRandom(); + private final int threshold; + private byte[] message = new byte[0]; + + public FakeTssLibrary(int threshold) { + if (threshold <= 0) { + throw new IllegalArgumentException("Invalid threshold: " + threshold); + } + + this.threshold = threshold; + } + + @NotNull + @Override + public TssMessage generateTssMessage(@NotNull TssParticipantDirectory tssParticipantDirectory) { + return null; + } + + @NotNull + @Override + public TssMessage generateTssMessage( + @NotNull TssParticipantDirectory tssParticipantDirectory, @NotNull TssPrivateShare privateShare) { + return null; + } + + @Override + public boolean verifyTssMessage( + @NotNull TssParticipantDirectory participantDirectory, @NotNull TssMessage tssMessage) { + return false; + } + + @NotNull + @Override + public List decryptPrivateShares( + @NotNull TssParticipantDirectory participantDirectory, @NotNull List validTssMessages) { + return List.of(); + } + + @NotNull + @Override + public PairingPrivateKey aggregatePrivateShares(@NotNull List privateShares) { + if (privateShares.size() < threshold) { + throw new IllegalStateException("Not enough shares to aggregate"); + } + return AGGREGATED_PRIVATE_KEY; + } + + @NotNull + @Override + public List computePublicShares( + @NotNull TssParticipantDirectory participantDirectory, @NotNull List validTssMessages) { + return List.of(); + } + + @NotNull + @Override + public PairingPublicKey aggregatePublicShares(@NotNull List publicShares) { + if (publicShares.size() < threshold) { + return new PairingPublicKey(new FakeGroupElement(BigInteger.valueOf(RANDOM.nextLong())), SIGNATURE_SCHEMA); + } + return LEDGER_ID; + } + + @NotNull + @Override + public PairingSignature aggregateSignatures(@NotNull List partialSignatures) { + if (partialSignatures.size() < threshold) { + return new PairingSignature(new FakeGroupElement(BigInteger.valueOf(RANDOM.nextLong())), SIGNATURE_SCHEMA); + } + + final var messageHash = noThrowSha384HashOf(message); + final var messageGroupElement = new BigInteger(1, messageHash); + final var privateKeyFieldElement = AGGREGATED_PRIVATE_KEY.privateKey().toBigInteger(); + final var signatureGroupElement = new FakeGroupElement(messageGroupElement.add(privateKeyFieldElement)); + return new PairingSignature(signatureGroupElement, SIGNATURE_SCHEMA); + } + + @NotNull + @Override + public TssShareSignature sign(@NotNull TssPrivateShare privateShare, @NotNull byte[] message) { + final var messageHash = noThrowSha384HashOf(message); + final var messageGroupElement = new BigInteger(1, messageHash); + final var privateKeyFieldElement = + privateShare.privateKey().privateKey().toBigInteger(); + final var signatureGroupElement = new FakeGroupElement(messageGroupElement.add(privateKeyFieldElement)); + final var signature = new PairingSignature(signatureGroupElement, SIGNATURE_SCHEMA); + return new TssShareSignature(privateShare.shareId(), signature); + } + + @Override + public boolean verifySignature( + @NotNull TssParticipantDirectory participantDirectory, + @NotNull List publicShares, + @NotNull TssShareSignature signature) { + if (participantDirectory.getThreshold() != this.threshold) { + throw new IllegalArgumentException("Invalid threshold"); + } + + if (publicShares.size() < threshold) { + return false; + } + + final PairingPublicKey ledgerId = aggregatePublicShares(publicShares); + final var fakePairingSignature = signature.signature(); + return fakePairingSignature.verify(ledgerId, message); + } + + // This method is not part of the TssLibrary interface, used for testing purposes + void setTestMessage(byte[] message) { + this.message = message; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNetwork.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNetwork.java index 00a8c13daf0d..09bc5d814d8c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNetwork.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/remote/RemoteNetwork.java @@ -97,6 +97,7 @@ private static NodeMetadata metadataFor(final int nodeId, @NonNull final NodeCon UNKNOWN_PORT, UNKNOWN_PORT, UNKNOWN_PORT, + UNKNOWN_PORT, null); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/ProcessUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/ProcessUtils.java index 8855dce526f2..9a2a48610908 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/ProcessUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/ProcessUtils.java @@ -131,6 +131,7 @@ public static ProcessHandle startSubProcessNodeFrom(@NonNull final NodeMetadata environment.put("LC_ALL", "en.UTF-8"); environment.put("LANG", "en_US.UTF-8"); environment.put("grpc.port", Integer.toString(metadata.grpcPort())); + environment.put("grpc.nodeOperatorPort", Integer.toString(metadata.grpcNodeOperatorPort())); environment.put("hedera.config.version", Integer.toString(configVersion)); try { final var redirectFile = guaranteedExtantFile( @@ -190,7 +191,7 @@ public static CompletableFuture conditionFuture(@NonNull final BooleanSupp * * @param condition the condition to wait for * @param checkBackoffMs the supplier of the number of milliseconds to back off between checks - * @return + * @return a future that resolves when the condition is true or the timeout is reached */ public static CompletableFuture conditionFuture( @NonNull final BooleanSupplier condition, @NonNull final LongSupplier checkBackoffMs) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNetwork.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNetwork.java index fbf059b11cac..4a41db2a4bc8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNetwork.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNetwork.java @@ -71,8 +71,8 @@ public class SubProcessNetwork extends AbstractGrpcNetwork implements HederaNetwork { private static final Logger log = LogManager.getLogger(SubProcessNetwork.class); - // We need 5 ports for each node in the network (gRPC, gRPC, gossip, gossip TLS, prometheus) - private static final int PORTS_PER_NODE = 5; + // We need 6 ports for each node in the network (gRPC, gRPC TLS, gRPC node operator, gossip, gossip TLS, prometheus) + private static final int PORTS_PER_NODE = 6; private static final int MAX_PORT_REASSIGNMENTS = 3; private static final SplittableRandom RANDOM = new SplittableRandom(); private static final int FIRST_CANDIDATE_PORT = 30000; @@ -86,6 +86,7 @@ public class SubProcessNetwork extends AbstractGrpcNetwork implements HederaNetw // We initialize these randomly to reduce risk of port binding conflicts in CI runners private static int nextGrpcPort; + private static int nextNodeOperatorPort; private static int nextGossipPort; private static int nextGossipTlsPort; private static int nextPrometheusPort; @@ -219,6 +220,7 @@ public void assignNewMetadata(@NonNull final ReassignPorts reassignPorts) { ((SubProcessNode) node) .reassignPorts( nextGrpcPort + nodeId * 2, + nextNodeOperatorPort + nodeId * 2, nextGossipPort + nodeId * 2, nextGossipTlsPort + nodeId * 2, nextPrometheusPort + nodeId); @@ -274,6 +276,7 @@ public void addNode(final long nodeId, @NonNull final UpgradeConfigTxt upgradeCo SUBPROCESS_HOST, SHARED_NETWORK_NAME.equals(name()) ? null : name(), nextGrpcPort + (int) nodeId * 2, + nextNodeOperatorPort + (int) nodeId * 2, nextGossipPort + (int) nodeId * 2, nextGossipTlsPort + (int) nodeId * 2, nextPrometheusPort + (int) nodeId), @@ -337,6 +340,7 @@ private static synchronized HederaNetwork liveNetwork(@NonNull final String name SUBPROCESS_HOST, SHARED_NETWORK_NAME.equals(name) ? null : name, nextGrpcPort, + nextNodeOperatorPort, nextGossipPort, nextGossipTlsPort, nextPrometheusPort), @@ -411,16 +415,19 @@ private static void initializeNextPortsForNetwork(final int size) { public static void initializeNextPortsForNetwork(final int size, final int firstGrpcPort) { // Suppose firstGrpcPort is 10000 with 4 nodes in the network, then: // - nextGrpcPort = 10000 - // - nextGossipPort = 10008 - // - nextGossipTlsPort = 10009 + // - nextNodeOperatorPort = 10008 + // - nextGossipPort = 10009 + // - nextGossipTlsPort = 10010 // - nextPrometheusPort = 10016 // So for a nodeId of 2, the assigned ports are: // - grpcPort = nextGrpcPort + nodeId * 2 = 10004 - // - gossipPort = nextGossipPort + nodeId * 2 = 10012 - // - gossipTlsPort = nextGossipTlsPort + nodeId * 2 = 10013 + // - grpcNodeOperatorPort = nextNodeOperatorPort + nodeId * 2 = 10012 + // - gossipPort = nextGossipPort + nodeId * 2 = 10013 + // - gossipTlsPort = nextGossipTlsPort + nodeId * 2 = 10014 // - prometheusPort = nextPrometheusPort + nodeId = 10018 nextGrpcPort = firstGrpcPort; - nextGossipPort = nextGrpcPort + 2 * size; + nextNodeOperatorPort = nextGrpcPort + 2 * size; + nextGossipPort = nextNodeOperatorPort + 1; nextGossipTlsPort = nextGossipPort + 1; nextPrometheusPort = nextGossipPort + 2 * size; nextPortsInitialized = true; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNode.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNode.java index 78ef9c136bc1..9ca0dcf7d800 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNode.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/subprocess/SubProcessNode.java @@ -193,13 +193,18 @@ public SubProcessNode startWithConfigVersion(final int configVersion) { * Reassigns the ports used by this node. * * @param grpcPort the new gRPC port + * @param grpcNodeOperatorPort the new gRPC node operator port * @param gossipPort the new gossip port * @param tlsGossipPort the new TLS gossip port * @param prometheusPort the new Prometheus port */ public void reassignPorts( - final int grpcPort, final int gossipPort, final int tlsGossipPort, final int prometheusPort) { - metadata = metadata.withNewPorts(grpcPort, gossipPort, tlsGossipPort, prometheusPort); + final int grpcPort, + final int grpcNodeOperatorPort, + final int gossipPort, + final int tlsGossipPort, + final int prometheusPort) { + metadata = metadata.withNewPorts(grpcPort, grpcNodeOperatorPort, gossipPort, tlsGossipPort, prometheusPort); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/AddressBookUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/AddressBookUtils.java index e58ec04eca13..5f33d39e111d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/AddressBookUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/AddressBookUtils.java @@ -109,6 +109,7 @@ public static NodeMetadata classicMetadataFor( @NonNull final String host, @Nullable String scope, final int nextGrpcPort, + final int nextNodeOperatorPort, final int nextGossipPort, final int nextGossipTlsPort, final int nextPrometheusPort) { @@ -122,6 +123,7 @@ public static NodeMetadata classicMetadataFor( .build(), host, nextGrpcPort + nodeId * 2, + nextNodeOperatorPort + nodeId * 2, nextGossipPort + nodeId * 2, nextGossipTlsPort + nodeId * 2, nextPrometheusPort + nodeId, @@ -170,6 +172,6 @@ public static ServiceEndpoint endpointFor(@NonNull final String host, final int */ public static Address nodeAddressFrom(@NonNull final AddressBook addressBook, final long nodeId) { requireNonNull(addressBook); - return addressBook.getAddress(new NodeId(nodeId)); + return addressBook.getAddress(NodeId.of(nodeId)); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/HgcaaLogValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/HgcaaLogValidator.java index d856435f11e4..2e91dfae1be2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/HgcaaLogValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/HgcaaLogValidator.java @@ -59,6 +59,8 @@ private static class ProblemTracker { List.of("Interrupted while waiting for signature verification"), List.of("Could not start TLS server, will continue without it"), List.of("Properties file", "does not exist and won't be used as configuration source"), + // Using a 1-minute staking period in CI can lead to periods with no transactions, breaking invariants + List.of("StakingRewardsHelper", "Pending rewards decreased"), List.of("Throttle multiplier for CryptoTransfer throughput congestion has no throttle buckets")); private int numProblems = 0; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockContentsValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockContentsValidator.java new file mode 100644 index 000000000000..446aa27f1f46 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/BlockContentsValidator.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.junit.support.validators.block; + +import static com.hedera.services.bdd.junit.hedera.utils.WorkingDirUtils.workingDirFor; + +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.block.stream.BlockItem; +import com.hedera.services.bdd.junit.support.BlockStreamAccess; +import com.hedera.services.bdd.junit.support.BlockStreamValidator; +import com.hedera.services.bdd.spec.HapiSpec; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Paths; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Assertions; + +public class BlockContentsValidator implements BlockStreamValidator { + private static final Logger logger = LogManager.getLogger(BlockContentsValidator.class); + + public static void main(String[] args) { + final var node0Dir = Paths.get("hedera-node/test-clients") + .resolve(workingDirFor(0, "hapi")) + .toAbsolutePath() + .normalize(); + final var validator = new BlockContentsValidator(); + final var blocks = + BlockStreamAccess.BLOCK_STREAM_ACCESS.readBlocks(node0Dir.resolve("data/block-streams/block-0.0.3")); + validator.validateBlocks(blocks); + } + + public static final Factory FACTORY = new Factory() { + @NonNull + @Override + public BlockStreamValidator create(@NonNull final HapiSpec spec) { + return new BlockContentsValidator(); + } + }; + + @Override + public void validateBlocks(@NonNull final List blocks) { + for (int i = 0; i < blocks.size(); i++) { + try { + validate(blocks.get(i)); + } catch (AssertionError err) { + logger.error("Error validating block {}", blocks.get(i)); + throw err; + } + } + } + + private static void validate(Block block) { + final var blockItems = block.items(); + + // A block SHALL start with a `header`. + if (!blockItems.getFirst().hasBlockHeader()) { + Assertions.fail("Block does not start with a block header"); + } + + // A block SHALL end with a `state_proof`. + if (!blockItems.getLast().hasBlockProof()) { + Assertions.fail("Block does not end with a block proof"); + } + + // In general, a `block_header` SHALL be followed by an `round_header` + if (!blockItems.get(1).hasRoundHeader()) { + Assertions.fail("Block header not followed by an round header"); + } + + // In general, a `round_header` SHALL be followed by an `event_header`, but for hapiTestRestart we get + // state change singleton update for BLOCK_INFO_VALUE because the post-restart State initialization changes + // state before any event has reached consensus + if (!blockItems.get(2).hasEventHeader() && !blockItems.get(2).hasStateChanges()) { + Assertions.fail("Round header not followed by an event header or state changes"); + } + + if (blockItems.stream().noneMatch(BlockItem::hasEventTransaction)) { // block without a user transaction + // A block with no user transactions contains a `block_header`, `event_headers`, `state_changes` and + // `state_proof`. + if (blockItems.stream() + .skip(2) // skip block_header and round_header + .limit(blockItems.size() - 3L) // skip state_proof + .anyMatch(item -> !item.hasEventHeader() && !item.hasStateChanges())) { + Assertions.fail( + "Block with no user transactions should contain items of type `block_header`, `event_headers`, `state_changes` or `state_proof`"); + } + + return; + } + + for (int i = 0; i < blockItems.size(); i++) { + // An `event_transaction` SHALL be followed by a `transaction_result`. + if (blockItems.get(i).hasEventTransaction() + && !blockItems.get(i + 1).hasTransactionResult()) { + Assertions.fail("Event transaction not followed by a transaction result"); + } + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java index 1c3649117de4..c597803d96fc 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/validators/block/StateChangesValidator.java @@ -18,7 +18,9 @@ import static com.hedera.node.app.blocks.impl.BlockImplUtils.combine; import static com.hedera.node.app.hapi.utils.CommonUtils.noThrowSha384HashOf; +import static com.hedera.node.app.hapi.utils.CommonUtils.sha384DigestOrThrow; import static com.hedera.node.app.info.UnavailableNetworkInfo.UNAVAILABLE_NETWORK_INFO; +import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.hedera.node.app.workflows.handle.metric.UnavailableMetrics.UNAVAILABLE_METRICS; import static com.hedera.services.bdd.junit.hedera.ExternalPath.APPLICATION_PROPERTIES; import static com.hedera.services.bdd.junit.hedera.ExternalPath.SAVED_STATES_DIR; @@ -79,6 +81,7 @@ import com.hedera.node.app.spi.signatures.SignatureVerifier; import com.hedera.node.app.state.recordcache.RecordCacheService; import com.hedera.node.app.throttle.CongestionThrottleService; +import com.hedera.node.app.tss.TssBaseServiceImpl; import com.hedera.node.app.version.ServicesSoftwareVersion; import com.hedera.node.config.VersionedConfiguration; import com.hedera.node.config.converter.BytesConverter; @@ -115,6 +118,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -125,6 +129,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; import java.util.function.Function; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; @@ -249,7 +254,7 @@ public StateChangesValidator( final var lifecycles = newPlatformInitLifecycle(bootstrapConfig, currentVersion, migrator, servicesRegistry); this.state = new MerkleStateRoot(lifecycles, version -> new ServicesSoftwareVersion(version, configVersion)); initGenesisPlatformState( - new FakePlatformContext(new NodeId(0), Executors.newSingleThreadScheduledExecutor()), + new FakePlatformContext(NodeId.of(0), Executors.newSingleThreadScheduledExecutor()), this.state.getWritablePlatformState(), addressBook, currentVersion); @@ -336,9 +341,12 @@ private void hashInputOutputTree( final StreamingTreeHasher inputTreeHasher, final StreamingTreeHasher outputTreeHasher) { final var itemSerialized = BlockItem.PROTOBUF.toBytes(item); + final var digest = sha384DigestOrThrow(); switch (item.item().kind()) { - case EVENT_HEADER, EVENT_TRANSACTION -> inputTreeHasher.addLeaf(itemSerialized); - case TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> outputTreeHasher.addLeaf(itemSerialized); + case EVENT_HEADER, EVENT_TRANSACTION -> inputTreeHasher.addLeaf( + ByteBuffer.wrap(digest.digest(itemSerialized.toByteArray()))); + case TRANSACTION_RESULT, TRANSACTION_OUTPUT, STATE_CHANGES -> outputTreeHasher.addLeaf( + ByteBuffer.wrap(digest.digest(itemSerialized.toByteArray()))); default -> { // Other items are not part of the input/output trees } @@ -517,19 +525,21 @@ private void registerServices( final InstantSource instantSource, final ServicesRegistry servicesRegistry, final VersionedConfiguration bootstrapConfig) { + final var appContext = new AppContextImpl(instantSource, fakeSignatureVerifier(), UNAVAILABLE_GOSSIP); // Register all service schema RuntimeConstructable factories before platform init Set.of( new EntityIdService(), new ConsensusServiceImpl(), - new ContractServiceImpl(new AppContextImpl(instantSource, fakeSignatureVerifier())), + new ContractServiceImpl(appContext), new FileServiceImpl(), + new TssBaseServiceImpl(appContext, ForkJoinPool.commonPool(), ForkJoinPool.commonPool()), new FreezeServiceImpl(), new ScheduleServiceImpl(), new TokenServiceImpl(), new UtilServiceImpl(), new RecordCacheService(), new BlockRecordService(), - new BlockStreamService(bootstrapConfig), + new BlockStreamService(), new FeeService(), new CongestionThrottleService(), new NetworkServiceImpl(), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java index 9dd08370541e..395383ff14a2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiPropertySource.java @@ -29,6 +29,7 @@ import com.hedera.hapi.node.base.ServiceEndpoint; import com.hedera.node.config.converter.LongPairConverter; import com.hedera.node.config.types.LongPair; +import com.hedera.node.config.types.StreamMode; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.hedera.services.bdd.spec.keys.KeyFactory; import com.hedera.services.bdd.spec.keys.SigControl; @@ -111,6 +112,16 @@ default AccountID getAccount(String property) { return AccountID.getDefaultInstance(); } + /** + * Returns an {@link StreamMode} parsed from the given property. + * @param property the property to get the value from + * @return the {@link StreamMode} value + */ + default StreamMode getStreamMode(@NonNull final String property) { + requireNonNull(property); + return StreamMode.valueOf(get(property)); + } + default ServiceEndpoint getServiceEndpoint(String property) { try { return asServiceEndpoint(get(property)); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/AccountInfoAsserts.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/AccountInfoAsserts.java index 57d7d69dbc74..3b9bfbe55abd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/AccountInfoAsserts.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/AccountInfoAsserts.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.spec.assertions; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.explicitFromHeadlong; import static com.hedera.services.bdd.suites.HapiSuite.EMPTY_KEY; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hederahashgraph.api.proto.java.CryptoGetInfoResponse.AccountInfo; @@ -25,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.esaulpaugh.headlong.abi.Address; import com.google.protobuf.ByteString; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.HapiSpec; @@ -229,6 +231,16 @@ public AccountInfoAsserts evmAddress(ByteString evmAddress) { return this; } + /** + * Asserts that the account has the given EVM address. + * @param evmAddress the EVM address + * @return this + */ + public AccountInfoAsserts evmAddress(@NonNull final Address evmAddress) { + requireNonNull(evmAddress); + return evmAddress(ByteString.copyFrom(explicitFromHeadlong(evmAddress))); + } + public AccountInfoAsserts noAlias() { registerProvider((spec, o) -> assertTrue(((AccountInfo) o).getAlias().isEmpty(), BAD_ALIAS)); return this; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/infrastructure/HapiSpecRegistry.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/infrastructure/HapiSpecRegistry.java index 98937e8410b6..5b1717763665 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/infrastructure/HapiSpecRegistry.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/infrastructure/HapiSpecRegistry.java @@ -387,7 +387,7 @@ public Key getWipeKey(String name) { } public Key getKycKey(String name) { - return get(name + "Kyc", Key.class); + return getOrElse(name + "Kyc", Key.class, null); } public Long getExpiry(String name) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/props/NodeConnectInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/props/NodeConnectInfo.java index 1e7c8246afbf..274aeee673de 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/props/NodeConnectInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/props/NodeConnectInfo.java @@ -20,11 +20,17 @@ import static com.hedera.services.bdd.spec.transactions.TxnUtils.isNumericLiteral; import com.google.common.base.MoreObjects; +import com.hedera.services.bdd.junit.hedera.HederaNode; import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hederahashgraph.api.proto.java.AccountID; import java.util.stream.Stream; +/** + * Node connection information. + * @deprecated get node connection info directly from a {@link HederaNode} object instead + */ +@Deprecated(forRemoval = true) public class NodeConnectInfo { public static int NEXT_DEFAULT_ACCOUNT_NUM = 3; private static final int DEFAULT_PORT = 50211; @@ -57,6 +63,7 @@ public NodeConnectInfo(String inString) { } else { tlsPort = DEFAULT_TLS_PORT; } + account = Stream.of(aspects) .filter(TxnUtils::isIdLiteral) .map(HapiPropertySource::asAccount) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java index c9d1e6fc9edc..eaf8af5f0555 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java @@ -915,8 +915,10 @@ protected void processAnswerOnlyResponse(@NonNull final HapiSpec spec) { final List creationDetails = (creationDetailsObserver != null) ? new ArrayList<>() : null; final List tokenCreations = (createdTokenIdsObserver != null) ? new ArrayList<>() : null; + final var firstUserNum = spec.startupProperties().getLong("hedera.firstUserEntity"); for (final var rec : childRecords) { - if (rec.getReceipt().hasAccountID()) { + if (rec.getReceipt().hasAccountID() + && rec.getReceipt().getAccountID().getAccountNum() >= firstUserNum) { if (creations != null) { creations.add( HapiPropertySource.asAccountString(rec.getReceipt().getAccountID())); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/schedule/HapiGetScheduleInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/schedule/HapiGetScheduleInfo.java index 202b2bdb9fb8..a5de85c47e3a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/schedule/HapiGetScheduleInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/schedule/HapiGetScheduleInfo.java @@ -28,6 +28,7 @@ import com.hedera.services.bdd.spec.queries.HapiQueryOp; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hederahashgraph.api.proto.java.HederaFunctionality; +import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.KeyList; import com.hederahashgraph.api.proto.java.Query; import com.hederahashgraph.api.proto.java.ResponseType; @@ -192,11 +193,10 @@ protected void assertExpectationsGiven(HapiSpec spec) { var registry = spec.registry(); - expectedSignatories.ifPresent(s -> { - var expect = KeyList.newBuilder(); - for (String signatory : s) { - var key = registry.getKey(signatory); - expect.addKeys(key); + expectedSignatories.ifPresent(signatories -> { + final var expect = KeyList.newBuilder(); + for (final var signatory : signatories) { + accumulateSimple(registry.getKey(signatory), expect); } Assertions.assertArrayEquals( expect.build().getKeysList().toArray(), @@ -222,6 +222,16 @@ protected void assertExpectationsGiven(HapiSpec spec) { expectedLedgerId.ifPresent(id -> Assertions.assertEquals(id, actualInfo.getLedgerId())); } + private static void accumulateSimple(@NonNull final Key key, @NonNull final KeyList.Builder builder) { + if (key.hasEd25519() || key.hasECDSASecp256K1()) { + builder.addKeys(key); + } else if (key.hasKeyList()) { + key.getKeyList().getKeysList().forEach(k -> accumulateSimple(k, builder)); + } else if (key.hasThresholdKey()) { + key.getThresholdKey().getKeys().getKeysList().forEach(k -> accumulateSimple(k, builder)); + } + } + private void assertTimestampMatches(String txn, int nanoOffset, Timestamp actual, String errMsg, HapiSpec spec) { var subOp = getTxnRecord(txn); allRunFor(spec, subOp); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java index 8efdfdf68d49..d4c7f3313430 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java @@ -98,6 +98,7 @@ public abstract class HapiTxnOp> extends HapiSpecOperatio private boolean ensureResolvedStatusIsntFromDuplicate = false; private final TupleType LONG_TUPLE = TupleType.parse("(int64)"); + protected boolean fireAndForget = false; protected boolean deferStatusResolution = false; protected boolean unavailableStatusIsOk = false; protected boolean acceptAnyStatus = false; @@ -285,7 +286,7 @@ && isWithInRetryLimit(retryCount)) { } spec.adhocIncrement(); - if (!deferStatusResolution) { + if (!deferStatusResolution && !fireAndForget) { resolveStatus(spec); } if (requiresFinalization(spec)) { @@ -406,7 +407,7 @@ private void assertRecordHasExpectedMemo(HapiSpec spec) throws Throwable { @Override public boolean requiresFinalization(HapiSpec spec) { - return actualPrecheck == OK && deferStatusResolution; + return !fireAndForget && actualPrecheck == OK && deferStatusResolution; } @Override @@ -703,6 +704,14 @@ public T hasAnyStatusAtAll() { return self(); } + public T fireAndForget() { + hasAnyPrecheck(); + hasAnyStatusAtAll(); + orUnavailableStatus(); + fireAndForget = true; + return self(); + } + public T hasAnyPrecheck() { acceptAnyPrecheck = true; return self(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoCreate.java index 744983af6f20..aaa74f57ba29 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoCreate.java @@ -16,13 +16,13 @@ package com.hedera.services.bdd.spec.transactions.crypto; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.explicitFromHeadlong; import static com.hedera.services.bdd.spec.HapiPropertySource.idAsHeadlongAddress; import static com.hedera.services.bdd.spec.keys.KeyFactory.KeyType; import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; import static com.hedera.services.bdd.spec.transactions.TxnUtils.bannerWith; import static com.hedera.services.bdd.spec.transactions.TxnUtils.netOf; import static com.hedera.services.bdd.spec.transactions.TxnUtils.suFrom; -import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.evmAddressFromSecp256k1Key; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import com.esaulpaugh.headlong.abi.Address; @@ -247,6 +247,10 @@ public HapiCryptoCreate evmAddress(final ByteString evmAddress) { return this; } + public HapiCryptoCreate evmAddress(final Address evmAddress) { + return evmAddress(ByteString.copyFrom(explicitFromHeadlong(evmAddress))); + } + @Override protected HapiCryptoCreate self() { return this; @@ -341,10 +345,7 @@ protected void updateStateOf(final HapiSpec spec) { Optional.ofNullable(addressObserver) .ifPresent(obs -> evmAddress.ifPresentOrElse( address -> obs.accept(HapiParserUtil.asHeadlongAddress(address.toByteArray())), - () -> obs.accept( - key.hasECDSASecp256K1() - ? evmAddressFromSecp256k1Key(key) - : idAsHeadlongAddress(lastReceipt.getAccountID())))); + () -> obs.accept(idAsHeadlongAddress(lastReceipt.getAccountID())))); if (advertiseCreation) { final String banner = "\n\n" diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/EmbeddedVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/EmbeddedVerbs.java index 88253588422d..888bc14f1e18 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/EmbeddedVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/EmbeddedVerbs.java @@ -16,16 +16,25 @@ package com.hedera.services.bdd.spec.utilops; +import static com.hedera.node.config.types.StreamMode.RECORDS; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static java.util.Objects.requireNonNull; + import com.hedera.hapi.node.state.addressbook.Node; +import com.hedera.hapi.node.state.blockrecords.BlockInfo; +import com.hedera.hapi.node.state.blockstream.BlockStreamInfo; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.state.token.AccountPendingAirdrop; import com.hedera.services.bdd.junit.hedera.embedded.EmbeddedNetwork; +import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.utilops.embedded.MutateAccountOp; import com.hedera.services.bdd.spec.utilops.embedded.MutateNodeOp; import com.hedera.services.bdd.spec.utilops.embedded.ViewAccountOp; import com.hedera.services.bdd.spec.utilops.embedded.ViewNodeOp; import com.hedera.services.bdd.spec.utilops.embedded.ViewPendingAirdropOp; +import com.swirlds.state.spi.CommittableWritableStates; import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; import java.util.function.Consumer; /** @@ -96,4 +105,47 @@ public static ViewPendingAirdropOp viewAccountPendingAirdrop( @NonNull final Consumer observer) { return new ViewPendingAirdropOp(tokenName, senderName, receiverName, observer); } + + /** + * Returns an operation that changes the state of an embedded network to appear to be handling + * the first transaction after an upgrade. + * + * @return the operation that simulates the first transaction after an upgrade + */ + public static SpecOperation simulatePostUpgradeTransaction() { + return withOpContext((spec, opLog) -> { + if (spec.targetNetworkOrThrow() instanceof EmbeddedNetwork embeddedNetwork) { + // Ensure there are no in-flight transactions that will overwrite our state changes + spec.sleepConsensusTime(Duration.ofSeconds(1)); + final var embeddedHedera = embeddedNetwork.embeddedHederaOrThrow(); + final var fakeState = embeddedHedera.state(); + final var streamMode = spec.startupProperties().getStreamMode("blockStream.streamMode"); + if (streamMode == RECORDS) { + // Mark the migration records as not streamed + final var writableStates = fakeState.getWritableStates("BlockRecordService"); + final var blockInfo = writableStates.getSingleton("BLOCKS"); + blockInfo.put(requireNonNull(blockInfo.get()) + .copyBuilder() + .migrationRecordsStreamed(false) + .build()); + ((CommittableWritableStates) writableStates).commit(); + // Ensure the next transaction is in a new round with ConcurrentEmbeddedHedera + spec.sleepConsensusTime(Duration.ofMillis(10L)); + } else { + final var writableStates = fakeState.getWritableStates("BlockStreamService"); + final var state = writableStates.getSingleton("BLOCK_STREAM_INFO"); + final var blockStreamInfo = requireNonNull(state.get()); + state.put(blockStreamInfo + .copyBuilder() + .postUpgradeWorkDone(false) + .build()); + ((CommittableWritableStates) writableStates).commit(); + } + // Ensure the next transaction is in a new block period + spec.sleepConsensusTime(Duration.ofSeconds(2L)); + } else { + throw new IllegalStateException("Cannot simulate post-upgrade transaction on non-embedded network"); + } + }); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java index 48899b170524..b5ecf71f1126 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/UtilVerbs.java @@ -46,6 +46,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.updateInitCodeWithConstructorArgs; import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromAccountToAlias; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; import static com.hedera.services.bdd.spec.transactions.file.HapiFileUpdate.getUpdated121; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; @@ -94,7 +95,6 @@ import com.esaulpaugh.headlong.abi.Tuple; import com.google.protobuf.ByteString; import com.hedera.hapi.node.state.addressbook.Node; -import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.state.token.Account; import com.hedera.services.bdd.junit.hedera.MarkerFile; import com.hedera.services.bdd.junit.hedera.NodeSelector; @@ -105,6 +105,7 @@ import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; import com.hedera.services.bdd.spec.infrastructure.OpProvider; +import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.queries.HapiQueryOp; import com.hedera.services.bdd.spec.queries.meta.HapiGetTxnRecord; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; @@ -192,9 +193,7 @@ import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionRecord; import com.swirlds.common.utility.CommonUtils; -import com.swirlds.platform.state.service.WritablePlatformStateStore; import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.state.spi.CommittableWritableStates; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -217,7 +216,9 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -529,42 +530,40 @@ public static HapiTxnOp.SubmissionStrategy usingVersion(@NonNull final Synthetic }; } + public static WaitForStatusOp waitForFrozenNetwork(@NonNull final Duration timeout) { + return new WaitForStatusOp(NodeSelector.allNodes(), FREEZE_COMPLETE, timeout); + } + /** - * Returns an operation that changes the state of an embedded network to appear to be handling - * the first transaction after an upgrade. - * - * @return the operation that simulates the first transaction after an upgrade + * Returns an operation that initiates background traffic running until the target network's + * first node has reached {@link com.swirlds.platform.system.status.PlatformStatus#FREEZE_COMPLETE}. + * @return the operation */ - public static SpecOperation simulatePostUpgradeTransaction() { + public static SpecOperation runBackgroundTrafficUntilFreezeComplete() { return withOpContext((spec, opLog) -> { - if (spec.targetNetworkOrThrow() instanceof EmbeddedNetwork embeddedNetwork) { - final var embeddedHedera = embeddedNetwork.embeddedHederaOrThrow(); - final var fakeState = embeddedHedera.state(); - // First make the freeze and last freeze times non-null and identical - final var aTime = spec.consensusTime(); - // This store immediately commits mutations, hence no cast and call to commit - final var writablePlatformStateStore = - new WritablePlatformStateStore(fakeState.getWritableStates("PlatformStateService")); - writablePlatformStateStore.setLastFrozenTime(aTime); - writablePlatformStateStore.setFreezeTime(aTime); - // Next mark the migration records as not streamed - final var writableBlockStates = fakeState.getWritableStates("BlockRecordService"); - final var blockInfo = writableBlockStates.getSingleton("BLOCKS"); - blockInfo.put(requireNonNull(blockInfo.get()) - .copyBuilder() - .migrationRecordsStreamed(false) - .build()); - ((CommittableWritableStates) writableBlockStates).commit(); - } else { - throw new IllegalStateException("Cannot simulate post-upgrade transaction on non-embedded network"); - } + opLog.info("Starting background traffic until freeze complete"); + final var stopTraffic = new AtomicBoolean(); + CompletableFuture.runAsync(() -> { + while (!stopTraffic.get()) { + allRunFor( + spec, + cryptoTransfer(tinyBarsFromTo(GENESIS, STAKING_REWARD, 1)) + .fireAndForget() + .noLogging()); + spec.sleepConsensusTime(Duration.ofMillis(1L)); + } + }); + spec.targetNetworkOrThrow() + .nodes() + .getFirst() + .statusFuture(FREEZE_COMPLETE, (status) -> {}) + .thenRun(() -> { + stopTraffic.set(true); + opLog.info("Stopping background traffic after freeze complete"); + }); }); } - public static WaitForStatusOp waitForFrozenNetwork(@NonNull final Duration timeout) { - return new WaitForStatusOp(NodeSelector.allNodes(), FREEZE_COMPLETE, timeout); - } - public static HapiSpecSleep sleepFor(long timeMs) { return new HapiSpecSleep(timeMs); } @@ -909,6 +908,21 @@ public static SpecOperation restoreDefault(@NonNull final String property) { return doWithStartupConfig(property, value -> overriding(property, value)); } + /** + * Returns an operation that runs a given callback with the EVM address implied by the given key. + * + * @param obs the callback to run with the address + * @return the operation that runs the callback using the address + */ + public static SpecOperation useAddressOfKey(@NonNull final String key, @NonNull final Consumer

    obs) { + return withOpContext((spec, opLog) -> { + final var publicKey = fromByteString(spec.registry().getKey(key).getECDSASecp256K1()); + final var address = + asHeadlongAddress(recoverAddressFromPubKey(publicKey).toByteArray()); + obs.accept(address); + }); + } + /** * Returns an operation that computes and executes a {@link SpecOperation} returned by a function whose * input is the EVM address implied by the given key. @@ -1015,6 +1029,73 @@ public static SpecOperation createHollow( }); } + /** + * Returns an operation that creates the requested number of HIP-32 auto-created accounts using a key alias + * of the given type, with names given by the given name function and default {@link HapiCryptoTransfer} using + * the standard transfer of tinybar to a key alias. + * @param n the number of HIP-32 accounts to create + * @param keyShape the type of key alias to use + * @param nameFn the function that computes the spec registry names for the accounts + * @return the operation + */ + public static SpecOperation createHip32Auto( + final int n, @NonNull final KeyShape keyShape, @NonNull final IntFunction nameFn) { + return createHip32Auto( + n, + keyShape, + nameFn, + keyName -> cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, keyName, ONE_HUNDRED_HBARS))); + } + + /** + * The function that computes the spec registry names of the keys that + * {@link #createHollow(int, IntFunction, Function)} uses to create the hollow accounts. + */ + public static final IntFunction AUTO_CREATION_KEY_NAME_FN = i -> "forAutoCreated" + i; + + /** + * Returns an operation that creates the requested number of HIP-32 auto-created accounts using a key alias + * of the given type, with names given by the given name function and {@link HapiCryptoTransfer} derived + * from the given factory. + * @param n the number of HIP-32 accounts to create + * @param keyShape the type of key alias to use + * @param nameFn the function that computes the spec registry names for the accounts + * @param creationFn the function that computes the creation operation for each account + * @return the operation + */ + public static SpecOperation createHip32Auto( + final int n, + @NonNull final KeyShape keyShape, + @NonNull final IntFunction nameFn, + @NonNull final Function creationFn) { + requireNonNull(nameFn); + requireNonNull(keyShape); + requireNonNull(creationFn); + return withOpContext((spec, opLog) -> { + final List createdIds = new ArrayList<>(); + final List keyNames = new ArrayList<>(); + for (int i = 0; i < n; i++) { + final var keyName = AUTO_CREATION_KEY_NAME_FN.apply(i); + keyNames.add(keyName); + allRunFor(spec, newKeyNamed(keyName).shape(keyShape)); + } + allRunFor( + spec, + blockingOrder(keyNames.stream() + .map(keyName -> blockingOrder( + creationFn.apply(keyName).via("hip32" + keyName), + getTxnRecord("hip32" + keyName) + .exposingCreationsTo( + creations -> createdIds.add(asAccount(creations.getFirst()))))) + .toArray(SpecOperation[]::new))); + for (int i = 0; i < n; i++) { + final var name = nameFn.apply(i); + spec.registry().saveKey(name, spec.registry().getKey(keyNames.get(i))); + spec.registry().saveAccountId(name, createdIds.get(i)); + } + }); + } + public static HapiSpecOperation overridingTwo( final String aProperty, final String aValue, final String bProperty, final String bValue) { return overridingAllOf(Map.of( @@ -1074,6 +1155,20 @@ public static EventualRecordStreamAssertion recordStreamMustIncludePassFrom( return EventualRecordStreamAssertion.eventuallyAssertingExplicitPass(assertion); } + /** + * Returns an operation that asserts that the record stream must include a pass from the given assertion + * before its timeout elapses. + * @param assertion the assertion to apply to the record stream + * @param timeout the timeout for the assertion + * @return the operation that asserts a passing record stream + */ + public static EventualRecordStreamAssertion recordStreamMustIncludePassFrom( + @NonNull final Function assertion, @NonNull final Duration timeout) { + requireNonNull(assertion); + requireNonNull(timeout); + return EventualRecordStreamAssertion.eventuallyAssertingExplicitPass(assertion, timeout); + } + /** * Returns an operation that asserts that the block stream must include no failures from the given assertion * before its timeout elapses. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java index 6609c698cc76..837147f140c9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/EventualAssertionResult.java @@ -24,40 +24,72 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +/** + * Represents the possibly pending result of an assertion. The result will be pending for at most the given timeout + * duration, after which the result will be completed as either a success or a failure based on the value of the + * {@code passAfterTimeout} flag. + */ public class EventualAssertionResult { - private final boolean hasPassedIfNothingFailed; + /** + * The maximum time the result can be pending before timing out. + */ private final Duration timeout; - + /** + * Whether the result on timing out should be taken as success. + */ + private final boolean passAfterTimeout; + /** + * The latch that will be counted down when the result is ready. + */ private final CountDownLatch ready = new CountDownLatch(1); + /** + * The result of the assertion. + */ private AssertionResult result; - public EventualAssertionResult(@NonNull final Duration timeout) { - this(false, timeout); - } - - public EventualAssertionResult(final boolean hasPassedIfNothingFailed, @NonNull final Duration timeout) { - this.hasPassedIfNothingFailed = hasPassedIfNothingFailed; + /** + * Creates a new {@link EventualAssertionResult} with the given timeout and the given flag. + * @param passAfterTimeout whether the result on timing out should be taken as success + * @param timeout the maximum time the result can be pending before timing out + */ + public EventualAssertionResult(final boolean passAfterTimeout, @NonNull final Duration timeout) { + this.passAfterTimeout = passAfterTimeout; this.timeout = requireNonNull(timeout); } + /** + * Blocks until the result is ready, or until the timeout has elapsed. If the result is not ready by the time the + * timeout has elapsed, the result will be completed as either a success or a timeout based on the value of the + * {@code passAfterTimeout} flag. + * @return the result of the assertion + * @throws InterruptedException if the thread is interrupted while waiting + */ public AssertionResult get() throws InterruptedException { if (!ready.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { - if (hasPassedIfNothingFailed && result == null) { - return AssertionResult.success(); + if (passAfterTimeout && result == null) { + return AssertionResult.newSuccess(); } else { - return AssertionResult.timeout(timeout); + return AssertionResult.newTimeout(timeout); } } return result; } + /** + * Completes the result as a success. + */ public void pass() { - this.result = AssertionResult.success(); + this.result = AssertionResult.newSuccess(); ready.countDown(); } - public void fail(final String reason) { + /** + * Completes the result as a failure with the given reason. + * @param reason the reason for the failure + */ + public void fail(@NonNull final String reason) { + requireNonNull(reason); this.result = AssertionResult.failure(reason); ready.countDown(); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java index b3ebd6cea1ce..5e420cc8bbe1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/StreamValidationOp.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.spec.utilops.streams; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.hedera.services.bdd.junit.hedera.ExternalPath.BLOCK_STREAMS_DIR; import static com.hedera.services.bdd.junit.hedera.ExternalPath.RECORD_STREAMS_DIR; import static com.hedera.services.bdd.junit.support.BlockStreamAccess.BLOCK_STREAM_ACCESS; @@ -37,6 +38,7 @@ import com.hedera.services.bdd.junit.support.validators.ExpiryRecordsValidator; import com.hedera.services.bdd.junit.support.validators.TokenReconciliationValidator; import com.hedera.services.bdd.junit.support.validators.TransactionBodyValidator; +import com.hedera.services.bdd.junit.support.validators.block.BlockContentsValidator; import com.hedera.services.bdd.junit.support.validators.block.StateChangesValidator; import com.hedera.services.bdd.junit.support.validators.block.TransactionRecordParityValidator; import com.hedera.services.bdd.spec.HapiSpec; @@ -71,8 +73,8 @@ public class StreamValidationOp extends UtilOp { new BalanceReconciliationValidator(), new TokenReconciliationValidator()); - private static final List BLOCK_STREAM_VALIDATOR_FACTORIES = - List.of(TransactionRecordParityValidator.FACTORY, StateChangesValidator.FACTORY); + private static final List BLOCK_STREAM_VALIDATOR_FACTORIES = List.of( + TransactionRecordParityValidator.FACTORY, StateChangesValidator.FACTORY, BlockContentsValidator.FACTORY); public static void main(String[] args) {} @@ -104,12 +106,16 @@ protected boolean submitOp(@NonNull final HapiSpec spec) throws Throwable { dataRef.set(data); }, () -> Assertions.fail("No record stream data found")); + // If there are no block streams to validate, we are done + if (spec.startupProperties().getStreamMode("blockStream.streamMode") == RECORDS) { + return false; + } // Freeze the network allRunFor( spec, freezeOnly().payingWith(GENESIS).startingIn(2).seconds(), // Wait for the final stream files to be created - sleepFor(8 * BUFFER_MS)); + sleepFor(10 * BUFFER_MS)); readMaybeBlockStreamsFor(spec) .ifPresentOrElse( blocks -> { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java index abdd80dfee49..a70476febcf2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AbstractEventualStreamAssertion.java @@ -18,6 +18,7 @@ import com.hedera.services.bdd.spec.utilops.UtilOp; import com.hedera.services.bdd.spec.utilops.streams.EventualAssertionResult; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; import org.junit.jupiter.api.Assertions; @@ -51,6 +52,10 @@ protected AbstractEventualStreamAssertion(final boolean hasPassedIfNothingFailed result = new EventualAssertionResult(hasPassedIfNothingFailed, DEFAULT_TIMEOUT); } + protected AbstractEventualStreamAssertion(final boolean hasPassedIfNothingFailed, @NonNull final Duration timeout) { + result = new EventualAssertionResult(hasPassedIfNothingFailed, timeout); + } + /** * Returns true if this assertion needs background traffic to be running in order to pass. * @return true if this assertion needs background traffic @@ -75,13 +80,20 @@ public void unsubscribe() { public void assertHasPassed() { try { final var eventualResult = result.get(); - unsubscribe(); if (!eventualResult.passed()) { - Assertions.fail(eventualResult.getErrorDetails()); + Assertions.fail(assertionDescription() + " ended with result: " + eventualResult.getErrorDetails()); } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); Assertions.fail("Interrupted while waiting for " + this + " to pass"); + } finally { + unsubscribe(); } } + + /** + * Returns a description of the assertion. + * @return a description of the assertion + */ + protected abstract String assertionDescription(); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AssertionResult.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AssertionResult.java index 30ba75832239..07ca90a4a716 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AssertionResult.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/AssertionResult.java @@ -16,41 +16,88 @@ package com.hedera.services.bdd.spec.utilops.streams.assertions; -import com.hedera.services.bdd.spec.utilops.streams.AssertionOutcome; +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.Duration; +/** + * Represents the result of an assertion. The result can be either a success, a failure, or a timeout. + */ public class AssertionResult { - private static final AssertionResult SUCCESS = new AssertionResult(null, AssertionOutcome.SUCCESS); + private static final AssertionResult SUCCESS = new AssertionResult(null, Outcome.SUCCESS); + + /** + * The possible outcomes of an assertion. + */ + public enum Outcome { + SUCCESS, + FAILURE, + TIMEOUT, + } + /** + * The details of the error that caused the assertion to fail, or null if the assertion passed. + */ @Nullable private final String errorDetails; + /** + * The outcome of the assertion. + */ + private final Outcome outcome; - private final AssertionOutcome outcome; - - public AssertionResult(final @Nullable String errorDetails, final AssertionOutcome outcome) { + /** + * Creates a new {@link AssertionResult} with the given error details and outcome. + * @param errorDetails the details of the error that caused the assertion to fail + * @param outcome the outcome of the assertion + */ + public AssertionResult(@Nullable final String errorDetails, @NonNull final Outcome outcome) { + this.outcome = requireNonNull(outcome); this.errorDetails = errorDetails; - this.outcome = outcome; } - public static AssertionResult success() { + /** + * Returns an {@link AssertionResult} that represents a successful assertion. + * @return the successful assertion result + */ + public static AssertionResult newSuccess() { return SUCCESS; } - public static AssertionResult timeout(final Duration timeout) { - return new AssertionResult("Timed out in " + timeout, AssertionOutcome.TIMEOUT); + /** + * Returns an {@link AssertionResult} that represents a timed out assertion. + * @param timeout the duration of the timeout + * @return the timed out assertion result + */ + public static AssertionResult newTimeout(@NonNull final Duration timeout) { + requireNonNull(timeout); + return new AssertionResult("Timed out in " + timeout, Outcome.TIMEOUT); } - public static AssertionResult failure(final String failureDetails) { - return new AssertionResult(failureDetails, AssertionOutcome.FAILURE); + /** + * Returns an {@link AssertionResult} that represents a failed assertion. + * @param failureDetails the details of the error that caused the assertion to fail + * @return the failed assertion result + */ + public static AssertionResult failure(@NonNull final String failureDetails) { + requireNonNull(failureDetails); + return new AssertionResult(failureDetails, Outcome.FAILURE); } + /** + * Returns true if the assertion passed. + * @return true if the assertion passed + */ public boolean passed() { - return outcome == AssertionOutcome.SUCCESS; + return outcome == Outcome.SUCCESS; } - @Nullable - public String getErrorDetails() { + /** + * Returns the details of the error that caused the assertion to fail, or null if the assertion passed. + * @return the details of the error that caused the assertion to fail + */ + public @Nullable String getErrorDetails() { return errorDetails; } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java index 54b7e2d9bb04..5bd321ba49a8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/BlockStreamAssertion.java @@ -26,6 +26,7 @@ * *

    Typical implementations will be stateful, and will be constructed with their "parent" {@link HapiSpec}. */ +@FunctionalInterface public interface BlockStreamAssertion { /** * Updates the assertion's state based on a relevant {@link Block}, throwing an {@link AssertionError} if a @@ -35,15 +36,5 @@ public interface BlockStreamAssertion { * @throws AssertionError if the assertion has failed * @return true if the assertion has succeeded */ - default boolean test(@NonNull final Block block) throws AssertionError { - return true; - } - - /** - * Hint to implementers to return a string that describes the assertion. - * - * @return a string that describes the assertion - */ - @Override - String toString(); + boolean test(@NonNull Block block) throws AssertionError; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java index 1434dd376e91..ddeac898dc39 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualBlockStreamAssertion.java @@ -98,6 +98,11 @@ public String name() { return false; } + @Override + protected String assertionDescription() { + return assertion == null ? "" : assertion.toString(); + } + /** * Returns the block stream location for the first listed node in the network targeted * by the given spec. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java index 5e63b339eba8..d7083ce51e71 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/EventualRecordStreamAssertion.java @@ -28,6 +28,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.nio.file.Path; +import java.time.Duration; import java.util.function.Function; /** @@ -69,6 +70,20 @@ public static EventualRecordStreamAssertion eventuallyAssertingExplicitPass( return new EventualRecordStreamAssertion(assertionFactory, false); } + /** + * Returns an {@link EventualRecordStreamAssertion} that will pass only if the given assertion explicitly + * passes within the default timeout. + * @param assertionFactory the assertion factory + * @return the eventual record stream assertion that must pass + */ + public static EventualRecordStreamAssertion eventuallyAssertingExplicitPass( + @NonNull final Function assertionFactory, + @NonNull final Duration timeout) { + requireNonNull(assertionFactory); + requireNonNull(timeout); + return new EventualRecordStreamAssertion(assertionFactory, false, timeout); + } + private EventualRecordStreamAssertion( @NonNull final Function assertionFactory, final boolean hasPassedIfNothingFailed) { @@ -76,12 +91,21 @@ private EventualRecordStreamAssertion( this.assertionFactory = requireNonNull(assertionFactory); } + private EventualRecordStreamAssertion( + @NonNull final Function assertionFactory, + final boolean hasPassedIfNothingFailed, + @NonNull final Duration timeout) { + super(hasPassedIfNothingFailed, timeout); + this.assertionFactory = requireNonNull(assertionFactory); + } + @Override protected boolean submitOp(final HapiSpec spec) throws Throwable { assertion = requireNonNull(assertionFactory.apply(spec)); unsubscribe = STREAM_FILE_ACCESS.subscribe(recordStreamLocFor(spec), new StreamDataListener() { @Override - public void onNewItem(RecordStreamItem item) { + public void onNewItem(@NonNull final RecordStreamItem item) { + requireNonNull(item); if (assertion.isApplicableTo(item)) { try { if (assertion.test(item)) { @@ -114,9 +138,14 @@ public String name() { return false; } + @Override + protected String assertionDescription() { + return assertion == null ? "" : assertion.toString(); + } + @Override public String toString() { - return "EventuallyRecordStream{" + assertion + "}"; + return "EventuallyRecordStream{" + assertionDescription() + "}"; } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java deleted file mode 100644 index cf75149ccf04..000000000000 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/assertions/IndirectProofsAssertion.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.services.bdd.spec.utilops.streams.assertions; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hedera.hapi.block.stream.Block; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * A {@link BlockStreamAssertion} used to verify the presence of some number {@code n} of expected indirect proofs - * in the block stream. When constructed, it assumes proof construction is paused, and fails if any block - * is written in this stage. - *

    - * After {@link #startExpectingBlocks()} is called, the assertion will verify that the next {@code n} proofs are - * indirect proofs with the correct number of sibling hashes; and are followed by a direct proof, at which point - * it passes. - */ -public class IndirectProofsAssertion implements BlockStreamAssertion { - private boolean proofsArePaused; - private int remainingIndirectProofs; - - public IndirectProofsAssertion(final int remainingIndirectProofs) { - this.proofsArePaused = true; - this.remainingIndirectProofs = remainingIndirectProofs; - } - - /** - * Signals that the assertion should now expect proofs to be created, hence blocks to be written. - */ - public void startExpectingBlocks() { - proofsArePaused = false; - } - - @Override - public boolean test(@NonNull final Block block) throws AssertionError { - if (proofsArePaused) { - throw new AssertionError("No blocks should be written when proofs are unavailable"); - } else { - final var items = block.items(); - final var proofItem = items.getLast(); - assertTrue(proofItem.hasBlockProof(), "Block proof is expected as the last item"); - final var proof = proofItem.blockProofOrThrow(); - if (remainingIndirectProofs == 0) { - assertTrue(proof.siblingHashes().isEmpty(), "No sibling hashes should be present on a direct proof"); - return true; - } else { - assertEquals( - // Two sibling hashes per indirection level - 2 * remainingIndirectProofs, - proof.siblingHashes().size(), - "Wrong number of sibling hashes for indirect proof"); - } - remainingIndirectProofs--; - return false; - } - } -} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java index f393906a57ca..70f9e530d19f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/DefaultTokenStatusSuite.java @@ -24,6 +24,8 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.grantTokenKyc; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; @@ -37,6 +39,7 @@ import static com.hedera.services.bdd.suites.token.TokenAssociationSpecs.VANILLA_TOKEN; import static com.hedera.services.bdd.suites.utils.contracts.precompile.HTSPrecompileResult.htsPrecompileResult; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_HAS_NO_KYC_KEY; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import com.hedera.node.app.hapi.utils.contracts.ParsingConstants.FunctionType; @@ -103,6 +106,7 @@ final Stream getTokenDefaultFreezeStatus() { @HapiTest final Stream getTokenDefaultKycStatus() { final AtomicReference vanillaTokenID = new AtomicReference<>(); + final AtomicReference noKycTokenId = new AtomicReference<>(); final var notAnAddress = new byte[20]; return hapiTest( @@ -115,6 +119,15 @@ final Stream getTokenDefaultKycStatus() { .kycKey(KYC_KEY) .initialSupply(1_000) .exposingCreatedIdTo(id -> vanillaTokenID.set(asToken(id))), + tokenAssociate(ACCOUNT, VANILLA_TOKEN), + grantTokenKyc(VANILLA_TOKEN, ACCOUNT), + tokenCreate("noKycToken") + .tokenType(FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(1_000) + .exposingCreatedIdTo(id -> noKycTokenId.set(asToken(id))), + tokenAssociate(ACCOUNT, "noKycToken"), + grantTokenKyc("noKycToken", ACCOUNT).hasKnownStatus(TOKEN_HAS_NO_KYC_KEY), uploadInitCode(TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT), contractCreate(TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT), withOpContext((spec, opLog) -> allRunFor( @@ -126,6 +139,13 @@ final Stream getTokenDefaultKycStatus() { .payingWith(ACCOUNT) .via("GetTokenDefaultKycStatusTx") .gas(GAS_TO_OFFER), + contractCall( + TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, + GET_TOKEN_DEFAULT_KYC, + HapiParserUtil.asHeadlongAddress(asAddress(noKycTokenId.get()))) + .payingWith(ACCOUNT) + .via("defaultKycStatus") + .gas(GAS_TO_OFFER), contractCallLocal( TOKEN_DEFAULT_KYC_FREEZE_STATUS_CONTRACT, GET_TOKEN_DEFAULT_KYC, @@ -139,6 +159,16 @@ final Stream getTokenDefaultKycStatus() { .contractCallResult(htsPrecompileResult() .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) .withStatus(SUCCESS) - .withTokenDefaultKycStatus(false))))); + .withTokenDefaultKycStatus(false)))), + childRecordsCheck( + "defaultKycStatus", + SUCCESS, + recordWith() + .status(SUCCESS) + .contractCallResult(resultWith() + .contractCallResult(htsPrecompileResult() + .forFunction(FunctionType.GET_TOKEN_DEFAULT_KYC_STATUS) + .withStatus(SUCCESS) + .withTokenDefaultKycStatus(true))))); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java index 67c0b2887d90..476355a79f63 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/TokenInfoHTSSuite.java @@ -87,6 +87,7 @@ import com.hederahashgraph.api.proto.java.RoyaltyFee; import com.hederahashgraph.api.proto.java.Timestamp; import com.hederahashgraph.api.proto.java.TokenInfo; +import com.hederahashgraph.api.proto.java.TokenKycStatus; import com.hederahashgraph.api.proto.java.TokenNftInfo; import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; @@ -260,7 +261,8 @@ final Stream happyPathGetTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get()))))), + targetLedgerId.get(), + TokenKycStatus.Revoked))))), childRecordsCheck( "TOKEN_INFO_TXN_V2", SUCCESS, @@ -281,7 +283,8 @@ final Stream happyPathGetTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get())))))); + targetLedgerId.get(), + TokenKycStatus.Revoked)))))); })); } @@ -390,7 +393,8 @@ final Stream happyPathGetFungibleTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get()))))), + targetLedgerId.get(), + TokenKycStatus.Revoked))))), childRecordsCheck( "FUNGIBLE_TOKEN_INFO_TXN_V2", SUCCESS, @@ -412,7 +416,8 @@ final Stream happyPathGetFungibleTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get())))))); + targetLedgerId.get(), + TokenKycStatus.Revoked)))))); })); } @@ -539,7 +544,8 @@ final Stream happyPathGetNonFungibleTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get())) + targetLedgerId.get(), + TokenKycStatus.Revoked)) .withNftTokenInfo(nftTokenInfo)))), childRecordsCheck( "NON_FUNGIBLE_TOKEN_INFO_TXN_V2", @@ -559,7 +565,8 @@ final Stream happyPathGetNonFungibleTokenInfo() { getTokenKeyFromSpec( spec, TokenKeyType.ADMIN_KEY), expirySecond, - targetLedgerId.get())) + targetLedgerId.get(), + TokenKycStatus.Revoked)) .withNftTokenInfo(nftTokenInfo))))); })); } @@ -1211,7 +1218,8 @@ final Stream happyPathUpdateTokenInfoAndGetLatestInfo() { spec.registry() .getKey(CONTRACT_KEY), expirySecond, - targetLedgerId.get())))))); + targetLedgerId.get(), + TokenKycStatus.Revoked)))))); })); } @@ -1321,7 +1329,8 @@ final Stream happyPathUpdateFungibleTokenInfoAndGetLatestInfo() { spec.registry() .getKey(CONTRACT_KEY), expirySecond, - targetLedgerId.get())))))); + targetLedgerId.get(), + TokenKycStatus.Revoked)))))); })); } @@ -1443,7 +1452,8 @@ final Stream happyPathUpdateNonFungibleTokenInfoAndGetLatestInfo() spec.registry() .getKey(CONTRACT_KEY), expirySecond, - targetLedgerId.get())) + targetLedgerId.get(), + TokenKycStatus.Revoked)) .withNftTokenInfo(nftTokenInfo))))); })); } @@ -1599,9 +1609,10 @@ private TokenInfo getTokenInfoStructForFungibleToken( final AccountID treasury, final Key adminKey, final long expirySecond, - ByteString ledgerId) { + ByteString ledgerId, + final TokenKycStatus kycDefault) { - return buildBaseTokenInfo(spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId) + return buildBaseTokenInfo(spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId, kycDefault) .build(); } @@ -1613,11 +1624,12 @@ private TokenInfo getTokenInfoStructForFungibleTokenV2( final AccountID treasury, final Key adminKey, final long expirySecond, - ByteString ledgerId) { + ByteString ledgerId, + final TokenKycStatus kycDefault) { final ByteString meta = ByteString.copyFrom("metadata".getBytes(StandardCharsets.UTF_8)); - return buildBaseTokenInfo(spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId) + return buildBaseTokenInfo(spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId, kycDefault) .setMetadata(meta) .setMetadataKey(getTokenKeyFromSpec(spec, TokenKeyType.METADATA_KEY)) .build(); @@ -1631,7 +1643,8 @@ private TokenInfo.Builder buildBaseTokenInfo( final AccountID treasury, final Key adminKey, final long expirySecond, - ByteString ledgerId) { + ByteString ledgerId, + final TokenKycStatus kycDefault) { final var autoRenewAccount = spec.registry().getAccountID(AUTO_RENEW_ACCOUNT); final var customFees = getExpectedCustomFees(spec); @@ -1657,7 +1670,8 @@ private TokenInfo.Builder buildBaseTokenInfo( .setWipeKey(getTokenKeyFromSpec(spec, TokenKeyType.WIPE_KEY)) .setSupplyKey(getTokenKeyFromSpec(spec, TokenKeyType.SUPPLY_KEY)) .setFeeScheduleKey(getTokenKeyFromSpec(spec, TokenKeyType.FEE_SCHEDULE_KEY)) - .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)); + .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)) + .setDefaultKycStatus(kycDefault); } @NonNull @@ -1708,8 +1722,10 @@ private TokenInfo getTokenInfoStructForNonFungibleToken( final AccountID treasury, final Key adminKey, final long expirySecond, - final ByteString ledgerId) { - return buildTokenInfo(spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId, null, false); + final ByteString ledgerId, + final TokenKycStatus kycDefault) { + return buildTokenInfo( + spec, tokenName, symbol, memo, treasury, adminKey, expirySecond, ledgerId, null, false, kycDefault); } private TokenInfo getTokenInfoStructForNonFungibleTokenV2( @@ -1717,7 +1733,8 @@ private TokenInfo getTokenInfoStructForNonFungibleTokenV2( final AccountID treasury, final Key adminKey, final long expirySecond, - final ByteString ledgerId) { + final ByteString ledgerId, + final TokenKycStatus kycDefault) { final ByteString meta = ByteString.copyFrom("metadata".getBytes(StandardCharsets.UTF_8)); return buildTokenInfo( spec, @@ -1729,7 +1746,8 @@ private TokenInfo getTokenInfoStructForNonFungibleTokenV2( expirySecond, ledgerId, meta, - true); + true, + kycDefault); } private TokenInfo buildTokenInfo( @@ -1742,7 +1760,8 @@ private TokenInfo buildTokenInfo( final long expirySecond, final ByteString ledgerId, final ByteString metadata, - final boolean includeMetadataKey) { + final boolean includeMetadataKey, + final TokenKycStatus kycDefault) { final var autoRenewAccount = spec.registry().getAccountID(AUTO_RENEW_ACCOUNT); TokenInfo.Builder builder = TokenInfo.newBuilder() @@ -1766,7 +1785,8 @@ private TokenInfo buildTokenInfo( .setWipeKey(getTokenKeyFromSpec(spec, TokenKeyType.WIPE_KEY)) .setSupplyKey(getTokenKeyFromSpec(spec, TokenKeyType.SUPPLY_KEY)) .setFeeScheduleKey(getTokenKeyFromSpec(spec, TokenKeyType.FEE_SCHEDULE_KEY)) - .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)); + .setPauseKey(getTokenKeyFromSpec(spec, TokenKeyType.PAUSE_KEY)) + .setDefaultKycStatus(kycDefault); if (metadata != null) { builder.setMetadata(metadata); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/records/RecordsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/records/RecordsSuite.java index 0d0f72a19d78..c143c7728563 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/records/RecordsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/records/RecordsSuite.java @@ -16,6 +16,7 @@ package com.hedera.services.bdd.suites.contract.records; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION; import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; @@ -294,7 +295,10 @@ final Stream blck001And002And003And004ReturnsCorrectBlockProperties final var secondBlockNumber = Longs.fromByteArray(Arrays.copyOfRange(secondBlockHashLogData, 24, 32)); - assertEquals(firstBlockNumber + 1, secondBlockNumber, "Wrong previous block number"); + if (spec.startupProperties().getStreamMode("blockStream.streamMode") == RECORDS) { + // This relationship is only guaranteed if block boundaries are based on time periods + assertEquals(firstBlockNumber + 1, secondBlockNumber, "Wrong previous block number"); + } final var secondBlockHash = Bytes32.wrap(Arrays.copyOfRange(secondBlockHashLogData, 32, 64)); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationSuite.java index c9df3e049a5c..9f36cae2e0c0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationSuite.java @@ -133,8 +133,8 @@ public class AutoAccountCreationSuite { private static final Key VALID_ED_25519_KEY = Key.newBuilder().setEd25519(ALIAS_CONTENT).build(); private static final ByteString VALID_25519_ALIAS = VALID_ED_25519_KEY.toByteString(); - private static final String AUTO_MEMO = "auto-created account"; - public static final String LAZY_MEMO = "lazy-created account"; + private static final String AUTO_MEMO = ""; + public static final String LAZY_MEMO = ""; public static final String VALID_ALIAS = "validAlias"; private static final String PAYER = "payer"; private static final String TRANSFER_TXN = "transferTxn"; @@ -160,9 +160,9 @@ public class AutoAccountCreationSuite { private static final String SPONSOR = "autoCreateSponsor"; public static final String LAZY_CREATE_SPONSOR = "lazyCreateSponsor"; - private static final long EXPECTED_HBAR_TRANSFER_AUTO_CREATION_FEE = 39418863L; - private static final long EXPECTED_MULTI_TOKEN_TRANSFER_AUTO_CREATION_FEE = 39418863L; - private static final long EXPECTED_SINGLE_TOKEN_TRANSFER_AUTO_CREATE_FEE = 39418863L; + private static final long EXPECTED_HBAR_TRANSFER_AUTO_CREATION_FEE = 39_376_619L; + private static final long EXPECTED_MULTI_TOKEN_TRANSFER_AUTO_CREATION_FEE = 39_376_619L; + private static final long EXPECTED_SINGLE_TOKEN_TRANSFER_AUTO_CREATE_FEE = 39_376_619L; private static final long EXPECTED_ASSOCIATION_FEE = 41666666L; public static final String CRYPTO_TRANSFER_RECEIVER = "cryptoTransferReceiver"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationUnlimitedAssociationsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationUnlimitedAssociationsSuite.java index bbfcb8b6048e..115abde70b27 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationUnlimitedAssociationsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/AutoAccountCreationUnlimitedAssociationsSuite.java @@ -82,7 +82,7 @@ public class AutoAccountCreationUnlimitedAssociationsSuite { public static final String TRUE = "true"; public static final String FALSE = "false"; - public static final String LAZY_MEMO = "lazy-created account"; + public static final String LAZY_MEMO = ""; public static final String VALID_ALIAS = "validAlias"; public static final String A_TOKEN = "tokenA"; public static final String PARTY = "party"; @@ -90,10 +90,10 @@ public class AutoAccountCreationUnlimitedAssociationsSuite { private static final String TRANSFER_TXN = "transferTxn"; private static final String MULTI_KEY = "multi"; private static final long INITIAL_BALANCE = 1000L; - private static final String AUTO_MEMO = "auto-created account"; + private static final String AUTO_MEMO = ""; private static final String CIVILIAN = "somebody"; private static final String SPONSOR = "autoCreateSponsor"; - private static final long EXPECTED_HBAR_TRANSFER_AUTO_CREATION_FEE = 39418863L; + private static final long EXPECTED_HBAR_TRANSFER_AUTO_CREATION_FEE = 39_376_619L; private static final String HBAR_XFER = "hbarXfer"; private static final String FT_XFER = "ftXfer"; private static final String NFT_XFER = "nftXfer"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoCreateSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoCreateSuite.java index 0e511f0daf5d..904aa6bfe66c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoCreateSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoCreateSuite.java @@ -74,6 +74,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.KEY_REQUIRED; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; +import com.esaulpaugh.headlong.abi.Address; import com.google.protobuf.ByteString; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; @@ -617,6 +618,7 @@ final Stream createAnAccountWithED25519Alias() { @HapiTest final Stream createAnAccountWithECKeyAndNoAlias() { + final var mirrorAddress = new AtomicReference

    (); return defaultHapiSpec("CreateAnAccountWithECKeyAndNoAlias") .given(newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE)) .when(withOpContext((spec, opLog) -> { @@ -625,12 +627,13 @@ final Stream createAnAccountWithECKeyAndNoAlias() { final var addressBytes = recoverAddressFromPubKey(tmp); assert addressBytes.length > 0; final var evmAddressBytes = ByteString.copyFrom(addressBytes); - final var createWithECDSAKey = cryptoCreate(ACCOUNT).key(SECP_256K1_SOURCE_KEY); - final var getAccountInfo = getAccountInfo(ACCOUNT) + final var createWithECDSAKey = + cryptoCreate(ACCOUNT).key(SECP_256K1_SOURCE_KEY).exposingEvmAddressTo(mirrorAddress::set); + final var getAccountInfo = sourcing(() -> getAccountInfo(ACCOUNT) .has(accountWith() .key(SECP_256K1_SOURCE_KEY) .noAlias() - .evmAddress(evmAddressBytes)); + .evmAddress(mirrorAddress.get()))); final var getECDSAAliasAccountInfo = getAliasedAccountInfo(ecdsaKey.toByteString()).hasCostAnswerPrecheck(INVALID_ACCOUNT_ID); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java index b4f2be4a2d5a..5b364509371f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/CryptoUpdateSuite.java @@ -16,10 +16,14 @@ package com.hedera.services.bdd.suites.crypto; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.asHeadlongAddress; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.explicitFromHeadlong; +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.headlongAddressOf; import static com.hedera.services.bdd.junit.TestTags.CRYPTO; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.assertions.AccountDetailsAsserts.accountDetailsWith; +import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.accountWith; import static com.hedera.services.bdd.spec.assertions.ContractInfoAsserts.contractWith; import static com.hedera.services.bdd.spec.keys.ControlForKey.forKey; import static com.hedera.services.bdd.spec.keys.KeyLabels.complex; @@ -28,6 +32,8 @@ import static com.hedera.services.bdd.spec.keys.SigControl.ANY; import static com.hedera.services.bdd.spec.keys.SigControl.OFF; import static com.hedera.services.bdd.spec.keys.SigControl.ON; +import static com.hedera.services.bdd.spec.keys.SigControl.SECP256K1_ON; +import static com.hedera.services.bdd.spec.keys.TrieSigMapGenerator.uniqueWithFullPrefixesFor; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountDetails; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; @@ -41,21 +47,34 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.AUTO_CREATION_KEY_NAME_FN; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.blockingOrder; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHip32Auto; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doWithStartupConfigNow; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doingContextual; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overridingTwo; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.submitModified; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.validateChargedUsd; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.visibleNonSyntheticItems; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withAddressOfKey; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.spec.utilops.mod.ModificationUtils.withSuccessivelyVariedBodyIds; import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; +import static com.hedera.services.bdd.suites.HapiSuite.FUNDING; import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.THREE_MONTHS_IN_SECONDS; import static com.hedera.services.bdd.suites.HapiSuite.ZERO_BYTE_MEMO; import static com.hedera.services.bdd.suites.contract.hapi.ContractUpdateSuite.ADMIN_KEY; +import static com.hederahashgraph.api.proto.java.HederaFunctionality.CryptoCreate; import static com.hederahashgraph.api.proto.java.HederaFunctionality.CryptoUpdate; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT; @@ -67,19 +86,34 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ZERO_BYTE_IN_STRING; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.esaulpaugh.headlong.abi.Address; +import com.google.protobuf.ByteString; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.LeakyHapiTest; -import com.hedera.services.bdd.spec.assertions.AccountInfoAsserts; +import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; import com.hedera.services.bdd.spec.keys.KeyLabels; import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.keys.SigControl; +import com.hedera.services.bdd.spec.queries.crypto.HapiGetAccountInfo; +import com.hedera.services.bdd.spec.transactions.TxnUtils; +import com.hedera.services.bdd.spec.utilops.streams.assertions.VisibleItemsValidator; import com.hederahashgraph.api.proto.java.ContractID; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.TokenType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Tag; @@ -132,6 +166,125 @@ final Stream idVariantsTreatedAsExpected() { .newStakedAccountId("0.0.21"))); } + private static final String[] ACCOUNTS_TO_HAVE_KEYS_ROTATED = { + "longZero", "autoCreated", "hollowAccount", "explicitAlias" + }; + private static final UnaryOperator ROTATION_TXN = account -> account + "KeyRotation"; + + /** + * Creates four accounts with ECDSA keys, the first having a long-zero EVM address and the other three having + * arbitrary EVM addresses created via, + *
      + *
    1. Legacy HIP-32 auto-account creation via transfer to a key alias.
    2. + *
    3. Hollow account creation via transfer to an EVM address.
    4. + *
    5. Explicit HIP-583 specification of the EVM address on creation.
    6. + *
    + * Then asserts that each of them have the expected EVM addresses in the {@link HapiGetAccountInfo} query both + * before and after key rotation; and that the record stream does not imply anything different. + */ + @HapiTest + final Stream keyRotationDoesNotChangeEvmAddress() { + final Map evmAddresses = new HashMap<>(); + final var allTxnIds = Stream.concat( + Arrays.stream(ACCOUNTS_TO_HAVE_KEYS_ROTATED), + Arrays.stream(ACCOUNTS_TO_HAVE_KEYS_ROTATED).map(ROTATION_TXN)) + .toArray(String[]::new); + return hapiTest( + recordStreamMustIncludePassFrom( + visibleNonSyntheticItems(keyRotationsValidator(evmAddresses), allTxnIds), + Duration.ofSeconds(10)), + // If the FileAlterationObserver just started the monitor, there's a chance we could miss the + // first couple of creations, so wait for a new record file boundary + doingContextual(TxnUtils::triggerAndCloseAtLeastOneFileIfNotInterrupted), + // --- CREATE ACCOUNTS --- + // The account with a long-zero EVM address + cryptoCreate("longZero") + .via("longZero") + .keyShape(SECP256K1_ON) + .exposingEvmAddressTo(address -> evmAddresses.put("longZero", address)), + // The auto-created account with an ECDSA key alias + createHip32Auto(1, KeyShape.SECP256K1, i -> "autoCreated"), + withAddressOfKey("autoCreated", evmAddress -> { + evmAddresses.put("autoCreated", evmAddress); + return withOpContext((spec, opLog) -> spec.registry() + .saveTxnId( + "autoCreated", + spec.registry().getTxnId("hip32" + AUTO_CREATION_KEY_NAME_FN.apply(0)))); + }), + // The hollow account - create and complete it for convenience + createHollow( + 1, + i -> "hollowAccount", + evmAddress -> cryptoTransfer(tinyBarsFromTo(GENESIS, evmAddress, ONE_HUNDRED_HBARS))), + withAddressOfKey("hollowAccount", evmAddress -> { + evmAddresses.put("hollowAccount", evmAddress); + return withOpContext((spec, opLog) -> spec.registry() + .saveTxnId("hollowAccount", spec.registry().getTxnId("autoCreate" + evmAddress))); + }), + cryptoTransfer(tinyBarsFromTo("hollowAccount", FUNDING, 1)) + .payingWith("hollowAccount") + .sigMapPrefixes(uniqueWithFullPrefixesFor("hollowAccount")), + // The account with an explicit EVM address + newKeyNamed("bEcdsaKey").shape(KeyShape.SECP256K1), + withAddressOfKey("bEcdsaKey", evmAddress -> { + evmAddresses.put("explicitAlias", evmAddress); + return cryptoCreate("explicitAlias") + .key("bEcdsaKey") + .evmAddress(evmAddress) + .via("explicitAlias"); + }), + // --- ROTATE KEYS --- + blockingOrder(IntStream.range(0, ACCOUNTS_TO_HAVE_KEYS_ROTATED.length) + .mapToObj(i -> { + final var newKey = "replKey" + i; + final var targetAccount = ACCOUNTS_TO_HAVE_KEYS_ROTATED[i]; + return blockingOrder( + newKeyNamed(newKey).shape(KeyShape.SECP256K1), + cryptoUpdate(targetAccount).key(newKey).via(ROTATION_TXN.apply(targetAccount))); + }) + .toArray(SpecOperation[]::new))); + } + + private static VisibleItemsValidator keyRotationsValidator(@NonNull final Map evmAddresses) { + return (spec, records) -> { + for (final var txnId : ACCOUNTS_TO_HAVE_KEYS_ROTATED) { + final var successItems = requireNonNull(records.get(txnId), txnId + " not found"); + final var creationEntry = successItems.entries().stream() + .filter(entry -> entry.function() == CryptoCreate) + .findFirst() + .orElseThrow(); + final var recordEvmAddress = creationEntry.transactionRecord().getEvmAddress(); + final var bodyEvmAddress = + creationEntry.body().getCryptoCreateAccount().getAlias(); + final var numEvmAddresses = + ((recordEvmAddress.size() == 20) ? 1 : 0) + ((bodyEvmAddress.size() == 20) ? 1 : 0); + assertTrue(numEvmAddresses <= 1); + final var evmAddress = numEvmAddresses == 0 + ? headlongAddressOf(creationEntry.createdAccountId()) + : asHeadlongAddress( + (recordEvmAddress.size() == 20) + ? recordEvmAddress.toByteArray() + : bodyEvmAddress.toByteArray()); + assertEquals(evmAddresses.get(txnId), evmAddress); + allRunFor( + spec, + getAccountInfo("0.0." + creationEntry.createdAccountId().accountNumOrThrow()) + .has(accountWith().evmAddress(ByteString.copyFrom(explicitFromHeadlong(evmAddress))))); + } + final var rotationTxnIds = Arrays.stream(ACCOUNTS_TO_HAVE_KEYS_ROTATED) + .map(ROTATION_TXN) + .toArray(String[]::new); + for (final var txnId : rotationTxnIds) { + final var successItems = requireNonNull(records.get(txnId), txnId + " not found"); + final var updateEntry = successItems.entries().stream() + .filter(entry -> entry.function() == CryptoUpdate) + .findFirst() + .orElseThrow(); + assertEquals(0, updateEntry.txnRecord().getEvmAddress().size()); + } + }; + } + @HapiTest final Stream updateForMaxAutoAssociationsForAccountsWorks() { return defaultHapiSpec("updateForMaxAutoAssociationsForAccountsWorks") @@ -179,41 +332,31 @@ final Stream updateStakingFieldsWorks() { newKeyNamed(ADMIN_KEY), cryptoCreate("user").key(ADMIN_KEY).stakedAccountId("0.0.20").declinedReward(true), getAccountInfo("user") - .has(AccountInfoAsserts.accountWith() + .has(accountWith() .stakedAccountId("0.0.20") .noStakingNodeId() .isDeclinedReward(true)), cryptoUpdate("user").newStakedNodeId(0L).newDeclinedReward(false), getAccountInfo("user") - .has(AccountInfoAsserts.accountWith() - .noStakedAccountId() - .stakedNodeId(0L) - .isDeclinedReward(false)), + .has(accountWith().noStakedAccountId().stakedNodeId(0L).isDeclinedReward(false)), cryptoUpdate("user").newStakedNodeId(-1L), cryptoUpdate("user").newStakedNodeId(-25L).hasKnownStatus(INVALID_STAKING_ID), getAccountInfo("user") - .has(AccountInfoAsserts.accountWith() - .noStakedAccountId() - .noStakingNodeId() - .isDeclinedReward(false)), + .has(accountWith().noStakedAccountId().noStakingNodeId().isDeclinedReward(false)), cryptoUpdate("user").key(ADMIN_KEY).newStakedAccountId("0.0.20").newDeclinedReward(true), getAccountInfo("user") - .has(AccountInfoAsserts.accountWith() + .has(accountWith() .stakedAccountId("0.0.20") .noStakingNodeId() .isDeclinedReward(true)) .logged(), cryptoUpdate("user").key(ADMIN_KEY).newStakedAccountId("0.0.0"), getAccountInfo("user") - .has(AccountInfoAsserts.accountWith() - .noStakedAccountId() - .noStakingNodeId() - .isDeclinedReward(true)) + .has(accountWith().noStakedAccountId().noStakingNodeId().isDeclinedReward(true)) .logged(), // For completeness stake back to a node cryptoUpdate("user").key(ADMIN_KEY).newStakedNodeId(1), - getAccountInfo("user") - .has(AccountInfoAsserts.accountWith().stakedNodeId(1L).isDeclinedReward(true))); + getAccountInfo("user").has(accountWith().stakedNodeId(1L).isDeclinedReward(true))); } @LeakyHapiTest(overrides = {"entities.maxLifetime", "ledger.maxAutoAssociations"}) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/HollowAccountFinalizationSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/HollowAccountFinalizationSuite.java index 19ddd8c1217b..3af715993831 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/HollowAccountFinalizationSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/HollowAccountFinalizationSuite.java @@ -103,7 +103,7 @@ public class HollowAccountFinalizationSuite { private static final String ANOTHER_SECP_256K1_SOURCE_KEY = "anotherSecp256k1Alias"; private static final String PAY_RECEIVABLE = "PayReceivable"; private static final long INITIAL_BALANCE = 1000L; - private static final String LAZY_MEMO = "lazy-created account"; + private static final String LAZY_MEMO = ""; private static final String TRANSFER_TXN = "transferTxn"; private static final String TRANSFER_TXN_2 = "transferTxn2"; private static final String PARTY = "party"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/LeakyCryptoTestsSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/LeakyCryptoTestsSuite.java index 6556b5c6c553..2b3fc2bc8281 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/LeakyCryptoTestsSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/LeakyCryptoTestsSuite.java @@ -772,7 +772,7 @@ final Stream lazyCreateViaEthereumCryptoTransfer() { .setCallingAccount(senderAccountIdReference.get()) .setRecipientAccount(lazyAccountIdReference.get()) .setGas(629_000L) - .setGasUsed(555_112L) + .setGasUsed(554_517L) .setValue(FIVE_HBARS) .setOutput(EMPTY) .build()))); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TxnRecordRegression.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TxnRecordRegression.java index 6f2e32c8619e..9d82a7529ea2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TxnRecordRegression.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/crypto/TxnRecordRegression.java @@ -120,9 +120,14 @@ final Stream getReceiptReturnsInvalidForUnspecifiedTxnId() { @RepeatableHapiTest(NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION) final Stream receiptUnavailableAfterCacheTtl() { return hapiTest( + // Every transaction in a repeatable spec reaches consensus one second apart, + // and it uses a valid start offset of one second; hence this will reach + // consensus at some time T with a valid start of T-1, and be purged after + // any transaction that reaches consensus at T+180 or later cryptoCreate("misc").via("success").balance(1_000L), - sleepFor(181_000L), - // Run a transaction to give receipt expiration a chance to occur + // Sleep until T+179 + sleepFor(179_000L), + // Run a transaction that will reach consensus at T+180 to purge receipts cryptoTransfer(tinyBarsFromTo(GENESIS, FUNDING, 1L)), getReceipt("success").hasAnswerOnlyPrecheck(RECEIPT_NOT_FOUND)); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java index 61e5eeb655db..f325cfca3433 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java @@ -148,7 +148,7 @@ public class EthereumSuite { public static final String EMIT_SENDER_ORIGIN_CONTRACT = "EmitSenderOrigin"; private static final long DEPOSIT_AMOUNT = 20_000L; private static final String PARTY = "party"; - private static final String LAZY_MEMO = "lazy-created account"; + private static final String LAZY_MEMO = ""; private static final String PAY_RECEIVABLE_CONTRACT = "PayReceivable"; private static final String TOKEN_CREATE_CONTRACT = "NewTokenCreateContract"; private static final String HELLO_WORLD_MINT_CONTRACT = "HelloWorldMint"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeUpdateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeUpdateTest.java index 9dda2e1d4a33..7c7ef3178114 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeUpdateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/NodeUpdateTest.java @@ -24,6 +24,7 @@ import static com.hedera.services.bdd.spec.HapiPropertySource.invalidServiceEndpoint; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnUtils.WRONG_LENGTH_EDDSA_KEY; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.nodeCreate; @@ -32,6 +33,7 @@ import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.viewNode; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.suites.HapiSuite.ADDRESS_BOOK_CONTROL; import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; import static com.hedera.services.bdd.suites.HapiSuite.GENESIS; import static com.hedera.services.bdd.suites.HapiSuite.NONSENSE_KEY; @@ -45,6 +47,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_IPV4_ADDRESS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NODE_DESCRIPTION; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NODE_ID; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_SIGNATURE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.KEY_REQUIRED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.NOT_SUPPORTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SERVICE_ENDPOINTS_EXCEEDED_LIMIT; @@ -360,4 +363,28 @@ final Stream checkDABEnable() throws CertificateEncodingException { .serviceEndpoint(List.of(asServiceEndpoint("127.0.0.2:60"), invalidServiceEndpoint())) .hasPrecheck(NOT_SUPPORTED)); } + + @HapiTest + final Stream signedByCouncilNotAdminKeyFail() throws CertificateEncodingException { + return hapiTest( + newKeyNamed("adminKey"), + nodeCreate("testNode") + .adminKey("adminKey") + .gossipCaCertificate(gossipCertificates.getFirst().getEncoded()), + nodeUpdate("testNode").signedBy(ADDRESS_BOOK_CONTROL).hasPrecheck(INVALID_SIGNATURE)); + } + + @HapiTest + final Stream signedByCouncilAndAdminKeySuccess() throws CertificateEncodingException { + return hapiTest( + newKeyNamed("adminKey"), + nodeCreate("testNode") + .adminKey("adminKey") + .gossipCaCertificate(gossipCertificates.getFirst().getEncoded()), + nodeUpdate("testNode") + .signedBy(ADDRESS_BOOK_CONTROL, "adminKey") + .description("updated description") + .via("successUpdate"), + getTxnRecord("successUpdate").logged()); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/UpdateAccountEnabledTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/UpdateAccountEnabledTest.java index 61b15adfc89e..87bdec4c864e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/UpdateAccountEnabledTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip869/UpdateAccountEnabledTest.java @@ -27,6 +27,7 @@ import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.viewNode; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.validateChargedUsdWithin; +import static com.hedera.services.bdd.suites.HapiSuite.DEFAULT_PAYER; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.hip869.NodeCreateTest.generateX509Certificates; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_NODE_ACCOUNT_ID; @@ -100,16 +101,17 @@ final Stream validateFees() throws CertificateEncodingException { // Submit to a different node so ingest check is skipped nodeUpdate("node100") .setNode("0.0.5") - .payingWith("payer") + .signedBy(DEFAULT_PAYER) .accountId("0.0.1000") .fee(ONE_HBAR) .hasKnownStatus(INVALID_SIGNATURE) .via("failedUpdate"), getTxnRecord("failedUpdate").logged(), - // The fee is charged here because the payer is not privileged - validateChargedUsdWithin("failedUpdate", 0.001, 3.0), + // The fee is not charged here because the payer is privileged + validateChargedUsdWithin("failedUpdate", 0.0, 3.0), nodeUpdate("node100") .adminKey("testKey") + .signedBy(DEFAULT_PAYER, "testKey") .accountId("0.0.1000") .fee(ONE_HBAR) .via("updateNode"), @@ -120,12 +122,12 @@ final Stream validateFees() throws CertificateEncodingException { // Submit with several signatures and the price should increase nodeUpdate("node100") .setNode("0.0.5") - .payingWith("payer") - .signedBy("payer", "payer", "randomAccount", "testKey") + .signedBy(DEFAULT_PAYER, "payer", "payer", "randomAccount", "testKey") .accountId("0.0.1000") .fee(ONE_HBAR) .via("failedUpdateMultipleSigs"), - validateChargedUsdWithin("failedUpdateMultipleSigs", 0.0011276316, 3.0)); + // The fee is not charged here because the payer is privileged + validateChargedUsdWithin("failedUpdateMultipleSigs", 0.0, 3.0)); } @EmbeddedHapiTest(NEEDS_STATE_ACCESS) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenAirdropTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenAirdropTest.java index e41bc8fdd390..3f6df430a8c3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenAirdropTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenAirdropTest.java @@ -83,6 +83,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_HAS_PENDING_AIRDROPS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.BATCH_SIZE_LIMIT_EXCEEDED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.EMPTY_TOKEN_TRANSFER_BODY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; @@ -101,7 +102,7 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_IS_PAUSED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import static com.hederahashgraph.api.proto.java.TokenType.FUNGIBLE_COMMON; import static com.hederahashgraph.api.proto.java.TokenType.NON_FUNGIBLE_UNIQUE; @@ -113,6 +114,7 @@ import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.LeakyHapiTest; import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.keys.SigControl; import com.hedera.services.bdd.spec.transactions.token.HapiTokenCreate; import com.hedera.services.bdd.spec.transactions.token.TokenMovement; @@ -122,11 +124,13 @@ import com.swirlds.common.utility.CommonUtils; import edu.umd.cs.findbugs.annotations.NonNull; import java.math.BigInteger; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.IntStream; +import java.util.stream.LongStream; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -479,23 +483,13 @@ final Stream tokenAirdropMultipleTokens() { createTokenWithName("FT2"), createTokenWithName("FT3"), createTokenWithName("FT4"), - createTokenWithName("FT5"), - createTokenWithName("FT6"), - createTokenWithName("FT7"), - createTokenWithName("FT8"), - createTokenWithName("FT9"), - createTokenWithName("FT10")) + createTokenWithName("FT5")) .when(tokenAirdrop( defaultMovementOfToken("FT1"), defaultMovementOfToken("FT2"), defaultMovementOfToken("FT3"), defaultMovementOfToken("FT4"), - defaultMovementOfToken("FT5"), - defaultMovementOfToken("FT6"), - defaultMovementOfToken("FT7"), - defaultMovementOfToken("FT8"), - defaultMovementOfToken("FT9"), - defaultMovementOfToken("FT10")) + defaultMovementOfToken("FT5")) .payingWith(OWNER) .via("fungible airdrop")) .then( @@ -509,17 +503,7 @@ final Stream tokenAirdropMultipleTokens() { getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) .hasTokenBalance("FT4", 10), getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT5", 10), - getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT6", 10), - getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT7", 10), - getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT8", 10), - getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT9", 10), - getAccountBalance(RECEIVER_WITH_UNLIMITED_AUTO_ASSOCIATIONS) - .hasTokenBalance("FT10", 10)); + .hasTokenBalance("FT5", 10)); } } @@ -1604,7 +1588,7 @@ final Stream aboveMaxTransfersFails() { defaultMovementOfToken("FUNGIBLE10"), defaultMovementOfToken("FUNGIBLE11")) .payingWith(OWNER) - .hasKnownStatus(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED)); + .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED)); } @HapiTest @@ -2001,7 +1985,7 @@ final Stream moreThanTenTokensToMultipleAccounts() { moving(10L, FUNGIBLE_TOKEN_J).between(ALICE, STEVE), moving(10L, FUNGIBLE_TOKEN_K).between(ALICE, STEVE)) .signedByPayerAnd(ALICE) - .hasKnownStatus(TOKEN_REFERENCE_LIST_SIZE_LIMIT_EXCEEDED)); + .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED)); } @HapiTest @@ -2183,6 +2167,70 @@ final Stream airdropTo0x0Address() { .payingWith(OWNER) .hasKnownStatus(INVALID_ACCOUNT_ID)); } + + @HapiTest + @DisplayName("airdrop 1 fungible token to 10 accounts") + final Stream pendingAirdropOneTokenToMoreThan10Accounts() { + final var accountNames = generateAccountNames(10); + return hapiTest(flattened( + // create 10 accounts with 0 auto associations + createAccounts(accountNames, 0), + tokenAirdrop(distributeTokens(FUNGIBLE_TOKEN, OWNER, accountNames)) + .payingWith(OWNER) + .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED))); + } + + @HapiTest + @DisplayName("airdrop more than 10 nft") + final Stream airdropMoreThan10Nft() { + final var nft = "nft"; + var nftSupplyKey = "nftSupplyKey"; + return hapiTest(flattened( + newKeyNamed(nftSupplyKey), + tokenCreate(nft) + .supplyKey(nftSupplyKey) + .tokenType(NON_FUNGIBLE_UNIQUE) + .initialSupply(0) + .treasury(OWNER), + // mint from 1 to 10 serials + mintToken( + nft, + IntStream.range(0, 10) + .mapToObj(a -> ByteString.copyFromUtf8(String.valueOf(a))) + .toList()), + // mint 11th serial + mintToken(nft, List.of(ByteString.copyFromUtf8(String.valueOf(11)))), + // try to airdrop 11 NFT + tokenAirdrop(distributeNFT(nft, OWNER, RECEIVER_WITH_0_AUTO_ASSOCIATIONS)) + .payingWith(OWNER) + .hasKnownStatus(BATCH_SIZE_LIMIT_EXCEEDED))); + } + + private static ArrayList generateAccountNames(int count) { + final var accountNames = new ArrayList(count); + for (int i = 0; i < count; i++) { + accountNames.add(String.format("account%d", i)); + } + return accountNames; + } + + private static ArrayList createAccounts( + ArrayList accountNames, int numberOfAutoAssociations) { + final var specOps = new ArrayList(accountNames.size()); + for (String accountName : accountNames) { + specOps.add(cryptoCreate(accountName).maxAutomaticTokenAssociations(numberOfAutoAssociations)); + } + return specOps; + } + + private static TokenMovement distributeTokens(String token, String sender, ArrayList accountNames) { + return moving(accountNames.size(), token).distributing(sender, accountNames.toArray(new String[0])); + } + + private static TokenMovement distributeNFT(String token, String sender, String receiver) { + final long[] serials = LongStream.rangeClosed(1, 11).toArray(); + return TokenMovement.movingUnique(token, serials).between(sender, receiver); + } } @EmbeddedHapiTest(EmbeddedReason.NEEDS_STATE_ACCESS) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenClaimAirdropTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenClaimAirdropTest.java index c3e46de15587..1c2436aecbc5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenClaimAirdropTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/TokenClaimAirdropTest.java @@ -374,7 +374,9 @@ final Stream tokenClaimOfTenFT() { inParallel( mapNTokens(token -> createFT(token, DEFAULT_PAYER, 1000L), SpecOperation.class, "ft", 1, 10)), tokenAirdrop(mapNTokens( - token -> moving(1, token).between(DEFAULT_PAYER, recipient), TokenMovement.class, "ft", 1, 10)), + token -> moving(1, token).between(DEFAULT_PAYER, recipient), TokenMovement.class, "ft", 1, 5)), + tokenAirdrop(mapNTokens( + token -> moving(1, token).between(DEFAULT_PAYER, recipient), TokenMovement.class, "ft", 6, 10)), claimAndFailToReclaim(() -> tokenClaimAirdrop(mapNTokens( token -> pendingAirdrop(DEFAULT_PAYER, recipient, token), Function.class, "ft", 1, 10)) .payingWith(recipient)), @@ -407,7 +409,8 @@ final Stream tokenClaimOfTenFTAndNFTToTwoReceivers() { moving(1, FUNGIBLE_TOKEN_2).between(DEFAULT_PAYER, BOB), moving(1, FUNGIBLE_TOKEN_3).between(DEFAULT_PAYER, BOB), moving(1, FUNGIBLE_TOKEN_4).between(DEFAULT_PAYER, BOB), - movingUnique(NON_FUNGIBLE_TOKEN, 1).between(DEFAULT_PAYER, BOB), + movingUnique(NON_FUNGIBLE_TOKEN, 1).between(DEFAULT_PAYER, BOB)), + tokenAirdrop( moving(1, FUNGIBLE_TOKEN_6).between(DEFAULT_PAYER, CAROL), moving(1, FUNGIBLE_TOKEN_7).between(DEFAULT_PAYER, CAROL), moving(1, FUNGIBLE_TOKEN_8).between(DEFAULT_PAYER, CAROL), @@ -476,7 +479,9 @@ final Stream tokenClaimByFiveDifferentReceivers() { moving(1, FUNGIBLE_TOKEN_2).between(ALICE, CAROL), moving(1, FUNGIBLE_TOKEN_3).between(ALICE, YULIA), moving(1, FUNGIBLE_TOKEN_4).between(ALICE, TOM), - moving(1, FUNGIBLE_TOKEN_5).between(ALICE, STEVE), + moving(1, FUNGIBLE_TOKEN_5).between(ALICE, STEVE)) + .payingWith(ALICE), + tokenAirdrop( moving(1, FUNGIBLE_TOKEN_6).between(ALICE, BOB), moving(1, FUNGIBLE_TOKEN_7).between(ALICE, CAROL), moving(1, FUNGIBLE_TOKEN_8).between(ALICE, YULIA), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/UnlimitedAutoAssociationSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/UnlimitedAutoAssociationSuite.java index 98983e319fd2..9e7da6e43936 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/UnlimitedAutoAssociationSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip904/UnlimitedAutoAssociationSuite.java @@ -184,7 +184,7 @@ final Stream autoAssociateTokensHappyPath() { .via(transferFungible), getTxnRecord(transferFungible) .andAllChildRecords() - .hasChildRecordCount(0) + .hasNonStakingChildRecordCount(0) .hasNewTokenAssociation(tokenA, secondUser) .logged(), // Transfer NFT @@ -193,7 +193,7 @@ final Stream autoAssociateTokensHappyPath() { .via(transferNonFungible), getTxnRecord(transferNonFungible) .andAllChildRecords() - .hasChildRecordCount(0) + .hasNonStakingChildRecordCount(0) .hasNewTokenAssociation(tokenB, secondUser) .logged(), getAccountInfo(secondUser) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java index a0daee134ba6..6d6ae491f4d5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip993/SystemFileExportsTest.java @@ -35,6 +35,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.scheduleCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; +import static com.hedera.services.bdd.spec.utilops.EmbeddedVerbs.simulatePostUpgradeTransaction; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.blockingOrder; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doWithStartupConfig; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.given; @@ -44,7 +45,6 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludeNoFailuresFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.recordStreamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.selectedItems; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.simulatePostUpgradeTransaction; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcingContextual; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.validateChargedUsdWithin; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/issues/IssueRegressionTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/issues/IssueRegressionTests.java index 06d9fdc5c5d4..e4ba6cf46d29 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/issues/IssueRegressionTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/issues/IssueRegressionTests.java @@ -216,20 +216,19 @@ final Stream duplicatedTxnsDifferentTypesDetected() { @HapiTest final Stream duplicatedTxnsSameTypeDifferentNodesDetected() { - return defaultHapiSpec("duplicatedTxnsSameTypeDifferentNodesDetected") - .given( - cryptoCreate("acct3").setNode("0.0.3").via("txnId1"), - sleepFor(2000), - cryptoCreate("acctWithDuplicateTxnId") + return hapiTest( + cryptoCreate("acct3").setNode("0.0.3").via("txnId1"), + sleepFor(2000), + cryptoCreate("acctWithDuplicateTxnId") + .setNode("0.0.5") + .txnId("txnId1") + .hasPrecheck(DUPLICATE_TRANSACTION), + uncheckedSubmit(cryptoCreate("acctWithDuplicateTxnId") .setNode("0.0.5") - .txnId("txnId1") - .hasPrecheck(DUPLICATE_TRANSACTION), - uncheckedSubmit(cryptoCreate("acctWithDuplicateTxnId") - .setNode("0.0.5") - .txnId("txnId1")) - .setNode("0.0.5")) - .when(sleepFor(2000)) - .then(getTxnRecord("txnId1") + .txnId("txnId1")) + .setNode("0.0.5"), + sleepFor(2000), + getTxnRecord("txnId1") .andAnyDuplicates() .assertingNothingAboutHashes() .hasPriority(recordWith().status(SUCCESS)) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/DabEnabledUpgradeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/DabEnabledUpgradeTest.java index 11649b283c10..bb78aa5094ff 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/DabEnabledUpgradeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/DabEnabledUpgradeTest.java @@ -28,7 +28,6 @@ import static com.hedera.services.bdd.spec.dsl.operations.transactions.TouchBalancesOperation.touchBalanceOf; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getVersionInfo; import static com.hedera.services.bdd.spec.transactions.TxnUtils.sysFileUpdateTo; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.nodeCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.nodeDelete; @@ -145,9 +144,7 @@ final Stream sameNodesTest() { prepareFakeUpgrade(), validateUpgradeAddressBooks(DabEnabledUpgradeTest::hasClassicAddressMetadata), upgradeToNextConfigVersion(), - assertExpectedConfigVersion(startVersion::get), - // Ensure we have a post-upgrade transaction to trigger system file exports - cryptoCreate("somebodyNew")); + assertExpectedConfigVersion(startVersion::get)); } } @@ -241,9 +238,7 @@ final Stream exportedAddressBookIncludesNodeId4() { // node4 was not active before this the upgrade, so it could not have written a config.txt validateUpgradeAddressBooks(exceptNodeIds(4L), addressBook -> assertThat(nodeIdsFrom(addressBook)) .contains(4L)), - upgradeToNextConfigVersion(FakeNmt.addNode(4L, DAB_GENERATED)), - // Ensure we have a post-upgrade transaction to trigger system file exports - cryptoCreate("somebodyNew")); + upgradeToNextConfigVersion(FakeNmt.addNode(4L, DAB_GENERATED))); } } @@ -326,11 +321,11 @@ final Stream exportedAddressBookReflectsOnlyEditsBeforePrepareUpgra private static void validateMultipartEdits(@NonNull final AddressBook addressBook) { assertThat(nodeIdsFrom(addressBook)).containsExactlyInAnyOrder(0L, 2L, 5L); - final var node0 = addressBook.getAddress(new NodeId(0L)); + final var node0 = addressBook.getAddress(NodeId.of(0L)); assertEquals(classicFeeCollectorIdLiteralFor(0), node0.getMemo()); - final var node2 = addressBook.getAddress(new NodeId(2L)); + final var node2 = addressBook.getAddress(NodeId.of(2L)); assertEquals(classicFeeCollectorIdLiteralFor(902), node2.getMemo()); - final var node5 = addressBook.getAddress(new NodeId(5L)); + final var node5 = addressBook.getAddress(NodeId.of(5L)); assertEquals(classicFeeCollectorIdLiteralFor(905), node5.getMemo()); assertEquals("127.0.0.1", node5.getHostnameInternal()); assertEquals(33000, node5.getPortInternal()); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/LifecycleTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/LifecycleTest.java index 8014b60cd794..1e5c62c83357 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/LifecycleTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/regression/system/LifecycleTest.java @@ -18,16 +18,19 @@ import static com.hedera.services.bdd.junit.hedera.MarkerFile.EXEC_IMMEDIATE_MF; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getVersionInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.blockingOrder; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.buildUpgradeZipFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doAdhoc; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doingContextual; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeOnly; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.freezeUpgrade; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.noOp; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.prepareUpgrade; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.purgeUpgradeArtifacts; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.runBackgroundTrafficUntilFreezeComplete; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.updateSpecialFile; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.waitForActive; @@ -176,6 +179,7 @@ default SpecOperation upgradeToNextConfigVersion(@NonNull final SpecOperation... default HapiSpecOperation upgradeToConfigVersion(final int version, @NonNull final SpecOperation... preRestartOps) { requireNonNull(preRestartOps); return blockingOrder( + runBackgroundTrafficUntilFreezeComplete(), sourcing(() -> freezeUpgrade() .startingIn(2) .seconds() @@ -185,7 +189,11 @@ default HapiSpecOperation upgradeToConfigVersion(final int version, @NonNull fin blockingOrder(preRestartOps), FakeNmt.restartNetwork(version), doAdhoc(() -> CURRENT_CONFIG_VERSION.set(version)), - waitForActiveNetwork(RESTART_TIMEOUT)); + waitForActiveNetwork(RESTART_TIMEOUT), + cryptoCreate("postUpgradeAccount"), + // Ensure we have a post-upgrade transaction in a new period to trigger + // system file exports while still streaming records + doingContextual(TxnUtils::triggerAndCloseAtLeastOneFileIfNotInterrupted)); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateTest.java similarity index 99% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateSpecs.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateTest.java index 727b9bde29c1..06daf64a39b3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleCreateTest.java @@ -92,7 +92,7 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Tag; -public class ScheduleCreateSpecs { +public class ScheduleCreateTest { @HapiTest final Stream aliasNotAllowedAsPayer() { return defaultHapiSpec("BodyAndPayerCreation") @@ -164,7 +164,7 @@ final Stream validateSignersInInfo() { .then(getScheduleInfo(VALID_SCHEDULE) .hasScheduleId(VALID_SCHEDULE) .hasRecordedScheduledTxn() - .hasSignatories(SENDER)); + .hasSignatories(DEFAULT_PAYER, SENDER)); } @HapiTest diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteTest.java similarity index 99% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteSpecs.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteTest.java index 560bdaabbc4f..cb02f1f9510f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleDeleteTest.java @@ -47,7 +47,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.DynamicTest; -public class ScheduleDeleteSpecs { +public class ScheduleDeleteTest { @HapiTest final Stream deleteWithNoAdminKeyFails() { return defaultHapiSpec("DeleteWithNoAdminKeyFails") diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionTest.java similarity index 99% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecs.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionTest.java index e2c78a862e87..1b00b36a885e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionTest.java @@ -143,7 +143,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; -public class ScheduleExecutionSpecs { +public class ScheduleExecutionTest { private final long normalTriggeredTxnTimestampOffset = 1; @SuppressWarnings("java:S2245") // using java.util.Random in tests is fine diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordTest.java similarity index 99% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordTest.java index 4ba7a187c88d..c4428cf5ca04 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordTest.java @@ -68,7 +68,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.DynamicTest; -public class ScheduleRecordSpecs { +public class ScheduleRecordTest { @HapiTest final Stream noFeesChargedIfTriggeredPayerIsUnwilling() { return defaultHapiSpec("NoFeesChargedIfTriggeredPayerIsUnwilling") diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignTest.java similarity index 96% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignSpecs.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignTest.java index 73befb86eaf7..b431f7f5ab4e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleSignTest.java @@ -82,10 +82,7 @@ import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Tag; -public class ScheduleSignSpecs { - private static final int SCHEDULE_EXPIRY_TIME_SECS = 10; - private static final int SCHEDULE_EXPIRY_TIME_MS = SCHEDULE_EXPIRY_TIME_SECS * 1000; - +public class ScheduleSignTest { @HapiTest final Stream idVariantsTreatedAsExpected() { return defaultHapiSpec("idVariantsTreatedAsExpected") @@ -106,19 +103,17 @@ final Stream signingDeletedSchedulesHasNoEffect() { String schedule = "Z"; String adminKey = ADMIN; - return defaultHapiSpec("SigningDeletedSchedulesHasNoEffect") - .given( - newKeyNamed(adminKey), - cryptoCreate(sender), - cryptoCreate(receiver).balance(0L), - scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) - .adminKey(adminKey) - .payingWith(DEFAULT_PAYER), - getAccountBalance(receiver).hasTinyBars(0L)) - .when( - scheduleDelete(schedule).signedBy(DEFAULT_PAYER, adminKey), - scheduleSign(schedule).alsoSigningWith(sender).hasKnownStatus(SCHEDULE_ALREADY_DELETED)) - .then(getAccountBalance(receiver).hasTinyBars(0L)); + return hapiTest( + newKeyNamed(adminKey), + cryptoCreate(sender), + cryptoCreate(receiver).balance(0L), + scheduleCreate(schedule, cryptoTransfer(tinyBarsFromTo(sender, receiver, 1))) + .adminKey(adminKey) + .payingWith(DEFAULT_PAYER), + getAccountBalance(receiver).hasTinyBars(0L), + scheduleDelete(schedule).signedBy(DEFAULT_PAYER, adminKey), + scheduleSign(schedule).alsoSigningWith(sender).hasKnownStatus(SCHEDULE_ALREADY_DELETED), + getAccountBalance(receiver).hasTinyBars(0L)); } @HapiTest @@ -458,13 +453,12 @@ final Stream scheduleAlreadyExecutedDoesntRepeatTransaction() { final Stream basicSignatureCollectionWorks() { var txnBody = cryptoTransfer(tinyBarsFromTo(SENDER, RECEIVER, 1)); - return defaultHapiSpec("BasicSignatureCollectionWorks") - .given( - cryptoCreate(SENDER), - cryptoCreate(RECEIVER).receiverSigRequired(true), - scheduleCreate(BASIC_XFER, txnBody)) - .when(scheduleSign(BASIC_XFER).alsoSigningWith(RECEIVER)) - .then(getScheduleInfo(BASIC_XFER).hasSignatories(RECEIVER)); + return hapiTest( + cryptoCreate(SENDER), + cryptoCreate(RECEIVER).receiverSigRequired(true), + scheduleCreate(BASIC_XFER, txnBody), + scheduleSign(BASIC_XFER).alsoSigningWith(RECEIVER), + getScheduleInfo(BASIC_XFER).hasSignatories(DEFAULT_PAYER, RECEIVER)); } @HapiTest diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecStateful.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/StatefulScheduleExecutionTest.java similarity index 99% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecStateful.java rename to hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/StatefulScheduleExecutionTest.java index b7b46510ffa8..9fa4faf39f81 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleExecutionSpecStateful.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/StatefulScheduleExecutionTest.java @@ -67,7 +67,7 @@ import org.junit.jupiter.api.TestMethodOrder; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class ScheduleExecutionSpecStateful { +public class StatefulScheduleExecutionTest { @HapiTest @Order(4) final Stream scheduledBurnWithInvalidTokenThrowsUnresolvableSigners() { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java index cec1d2c1e7f4..8171d81826a8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/tss/RepeatableTssTests.java @@ -16,19 +16,42 @@ package com.hedera.services.bdd.suites.tss; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; +import static com.hedera.node.config.types.StreamMode.RECORDS; import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_TSS_CONTROL; +import static com.hedera.services.bdd.junit.RepeatableReason.NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.TssVerbs.startIgnoringTssSignatureRequests; import static com.hedera.services.bdd.spec.utilops.TssVerbs.stopIgnoringTssSignatureRequests; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.blockStreamMustIncludePassFrom; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doAdhoc; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.doWithStartupConfig; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.overriding; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.waitUntilStartOfNextStakingPeriod; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static java.lang.Long.parseLong; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.hedera.hapi.block.stream.Block; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.transaction.SignedTransaction; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssMessageTransactionBody; +import com.hedera.hapi.services.auxiliary.tss.TssVoteTransactionBody; import com.hedera.node.app.blocks.BlockStreamManager; +import com.hedera.pbj.runtime.ParseException; +import com.hedera.services.bdd.junit.LeakyRepeatableHapiTest; import com.hedera.services.bdd.junit.RepeatableHapiTest; import com.hedera.services.bdd.junit.hedera.embedded.fakes.FakeTssBaseService; -import com.hedera.services.bdd.spec.utilops.streams.assertions.IndirectProofsAssertion; +import com.hedera.services.bdd.spec.utilops.streams.assertions.BlockStreamAssertion; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntConsumer; import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; /** @@ -36,8 +59,8 @@ */ public class RepeatableTssTests { /** - * A test that simulates the behavior of the {@link FakeTssBaseService} under specific conditions - * related to signature requests and block creation. + * Validates behavior of the {@link BlockStreamManager} under specific conditions related to signature requests + * and block creation. * *

    This test follows three main steps:

    *
      @@ -57,14 +80,146 @@ public class RepeatableTssTests { @RepeatableHapiTest(NEEDS_TSS_CONTROL) Stream blockStreamManagerCatchesUpWithIndirectProofs() { final var indirectProofsAssertion = new IndirectProofsAssertion(2); + return hapiTest(withOpContext((spec, opLog) -> { + if (spec.startupProperties().getStreamMode("blockStream.streamMode") != RECORDS) { + allRunFor( + spec, + startIgnoringTssSignatureRequests(), + blockStreamMustIncludePassFrom(ignore -> indirectProofsAssertion), + // Each transaction is placed into its own round and hence block with default config + cryptoCreate("firstIndirectProof"), + cryptoCreate("secondIndirectProof"), + stopIgnoringTssSignatureRequests(), + doAdhoc(indirectProofsAssertion::startExpectingBlocks), + cryptoCreate("directProof")); + } + })); + } + + /** + * Validates that after the first transaction in a staking period, the embedded node generates a + * {@link TssMessageTransactionBody} which then triggers a successful {@link TssVoteTransactionBody}. + * This is a trivial placeholder for he real TSS rekeying process that begins on the first transaction + * in a staking period. + *

      + * TODO: Continue the rekeying happy path after the successful TSS message. + *

        + *
      1. (TSS-FUTURE) Initialize the roster such that the embedded node has more than one share; + * verify it creates a successful {@link TssMessageTransactionBody} for each of its shares.
      2. + *
      3. (TSS-FUTURE) Submit valid TSS messages from other nodes in the embedded "network".
      4. + *
      5. (TSS-FUTURE) Confirm the embedded node votes yes for the the first {@code t} successful + * messages, where {@code t} suffices to meet the recovery threshold.
      6. + *
      7. (TSS-FUTURE) Confirm the embedded node's recovered ledger id in its + * {@link TssVoteTransactionBody} matches the id returned by the fake TSS library.
      8. + *
      + */ + @LeakyRepeatableHapiTest( + value = {NEEDS_TSS_CONTROL, NEEDS_VIRTUAL_TIME_FOR_FAST_EXECUTION}, + overrides = {"tss.keyCandidateRoster"}) + Stream tssMessageSubmittedForRekeyingIsSuccessful() { return hapiTest( - startIgnoringTssSignatureRequests(), - blockStreamMustIncludePassFrom(spec -> indirectProofsAssertion), - // Each transaction is placed into its own round and hence block with default config - cryptoCreate("firstIndirectProof"), - cryptoCreate("secondIndirectProof"), - stopIgnoringTssSignatureRequests(), - doAdhoc(indirectProofsAssertion::startExpectingBlocks), - cryptoCreate("directProof")); + blockStreamMustIncludePassFrom(spec -> successfulTssMessageThenVote()), + // Current TSS default is not to try to key the candidate + overriding("tss.keyCandidateRoster", "true"), + doWithStartupConfig( + "staking.periodMins", + stakePeriodMins -> waitUntilStartOfNextStakingPeriod(parseLong(stakePeriodMins))), + // This transaction is now first in a new staking period and should trigger the TSS rekeying process, + // in particular a successful TssMessage from the embedded node (and then a TssVote since this is our + // placeholder implementation of TssMessageHandler) + cryptoCreate("rekeyingTransaction")); + } + + /** + * Returns an assertion that only passes when it has seen a successful TSS message follows by a successful + * TSS vote in the block stream. + * + * @return the assertion + */ + private static BlockStreamAssertion successfulTssMessageThenVote() { + final var sawTssMessage = new AtomicBoolean(false); + return block -> { + final var items = block.items(); + final IntConsumer assertSuccessResultAt = i -> { + assertTrue(i < items.size(), "Missing transaction result"); + final var resultItem = items.get(i); + assertTrue(resultItem.hasTransactionResult(), "Misplaced transaction result"); + final var result = resultItem.transactionResultOrThrow(); + assertEquals(SUCCESS, result.status()); + }; + for (int i = 0, n = items.size(); i < n; i++) { + final var item = items.get(i); + if (item.hasEventTransaction()) { + try { + final var wrapper = Transaction.PROTOBUF.parse( + item.eventTransactionOrThrow().applicationTransactionOrThrow()); + final var signedTxn = SignedTransaction.PROTOBUF.parse(wrapper.signedTransactionBytes()); + final var txn = TransactionBody.PROTOBUF.parse(signedTxn.bodyBytes()); + if (txn.hasTssMessage()) { + assertSuccessResultAt.accept(i + 1); + sawTssMessage.set(true); + } else if (txn.hasTssVote()) { + assertTrue(sawTssMessage.get(), "Vote seen before message"); + assertSuccessResultAt.accept(i + 1); + return true; + } + } catch (ParseException e) { + Assertions.fail(e.getMessage()); + } + } + } + return false; + }; + } + + /** + * A {@link BlockStreamAssertion} used to verify the presence of some number {@code n} of expected indirect proofs + * in the block stream. When constructed, it assumes proof construction is paused, and fails if any block + * is written in this stage. + *

      + * After {@link #startExpectingBlocks()} is called, the assertion will verify that the next {@code n} proofs are + * indirect proofs with the correct number of sibling hashes; and are followed by a direct proof, at which point + * it passes. + */ + private static class IndirectProofsAssertion implements BlockStreamAssertion { + private boolean proofsArePaused; + private int remainingIndirectProofs; + + public IndirectProofsAssertion(final int remainingIndirectProofs) { + this.proofsArePaused = true; + this.remainingIndirectProofs = remainingIndirectProofs; + } + + /** + * Signals that the assertion should now expect proofs to be created, hence blocks to be written. + */ + public void startExpectingBlocks() { + proofsArePaused = false; + } + + @Override + public boolean test(@NonNull final Block block) throws AssertionError { + if (proofsArePaused) { + throw new AssertionError("No blocks should be written when proofs are unavailable"); + } else { + final var items = block.items(); + final var proofItem = items.getLast(); + assertTrue(proofItem.hasBlockProof(), "Block proof is expected as the last item"); + final var proof = proofItem.blockProofOrThrow(); + if (remainingIndirectProofs == 0) { + assertTrue( + proof.siblingHashes().isEmpty(), "No sibling hashes should be present on a direct proof"); + return true; + } else { + assertEquals( + // Two sibling hashes per indirection level + 2 * remainingIndirectProofs, + proof.siblingHashes().size(), + "Wrong number of sibling hashes for indirect proof"); + } + remainingIndirectProofs--; + return false; + } + } } } diff --git a/hedera-node/test-clients/src/main/java/module-info.java b/hedera-node/test-clients/src/main/java/module-info.java index 472a6aa69106..9867edbefc82 100644 --- a/hedera-node/test-clients/src/main/java/module-info.java +++ b/hedera-node/test-clients/src/main/java/module-info.java @@ -61,9 +61,11 @@ exports com.hedera.services.bdd.junit.support.validators.utils; exports com.hedera.services.bdd.junit.support.validators.block; exports com.hedera.services.bdd.utils; + exports com.hedera.services.bdd.junit.hedera.embedded.fakes.tss; requires transitive com.hedera.node.app.hapi.fees; requires transitive com.hedera.node.app.hapi.utils; + requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.app.test.fixtures; requires transitive com.hedera.node.app; requires transitive com.hedera.node.config; @@ -96,13 +98,12 @@ requires com.hedera.node.app.service.token.impl; requires com.hedera.node.app.service.token; requires com.hedera.node.app.service.util.impl; - requires com.hedera.node.app.spi; requires com.swirlds.base.test.fixtures; requires com.swirlds.config.extensions.test.fixtures; requires com.swirlds.platform.core.test.fixtures; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; - requires com.github.docker.java.api; + requires com.github.dockerjava.api; requires com.sun.jna; requires io.grpc.netty; requires io.grpc.stub; diff --git a/hedera-node/test-clients/src/test/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibraryTest.java b/hedera-node/test-clients/src/test/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibraryTest.java new file mode 100644 index 000000000000..2f8cb16bc791 --- /dev/null +++ b/hedera-node/test-clients/src/test/java/com/hedera/services/bdd/junit/hedera/embedded/fakes/tss/FakeTssLibraryTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.junit.hedera.embedded.fakes.tss; + +import static com.hedera.services.bdd.junit.hedera.embedded.fakes.tss.FakeTssLibrary.SIGNATURE_SCHEMA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hedera.node.app.tss.api.TssParticipantDirectory; +import com.hedera.node.app.tss.api.TssPrivateShare; +import com.hedera.node.app.tss.api.TssPublicShare; +import com.hedera.node.app.tss.api.TssShareId; +import com.hedera.node.app.tss.api.TssShareSignature; +import com.hedera.node.app.tss.pairings.FakeFieldElement; +import com.hedera.node.app.tss.pairings.FakeGroupElement; +import com.hedera.node.app.tss.pairings.PairingPrivateKey; +import com.hedera.node.app.tss.pairings.PairingPublicKey; +import com.hedera.node.app.tss.pairings.PairingSignature; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class FakeTssLibraryTest { + + @Test + void sign() { + final var fakeTssLibrary = new FakeTssLibrary(1); + final var privateKeyElement = new FakeFieldElement(BigInteger.valueOf(2L)); + final var pairingPrivateKey = new PairingPrivateKey(privateKeyElement, SIGNATURE_SCHEMA); + final var privateShare = new TssPrivateShare(new TssShareId(1), pairingPrivateKey); + + final var tssShareSignature = fakeTssLibrary.sign(privateShare, "Hello, World!".getBytes()); + + assertNotNull(tssShareSignature); + assertEquals(privateShare.shareId(), tssShareSignature.shareId()); + assertNotNull(tssShareSignature.signature()); + } + + @Test + void aggregatePrivateShares() { + final var fakeTssLibrary = new FakeTssLibrary(2); + final var privateShares = new ArrayList(); + final var privateKeyShares = new long[] {1, 2, 3}; + for (int i = 0; i < privateKeyShares.length; i++) { + final var privateKeyElement = new FakeFieldElement(BigInteger.valueOf(privateKeyShares[i])); + privateShares.add( + new TssPrivateShare(new TssShareId(i), new PairingPrivateKey(privateKeyElement, SIGNATURE_SCHEMA))); + } + + final var aggregatedPrivateKey = fakeTssLibrary.aggregatePrivateShares(privateShares); + + assertNotNull(aggregatedPrivateKey); + assertEquals("42", aggregatedPrivateKey.privateKey().toBigInteger().toString()); + } + + @Test + void aggregatePrivateSharesWithNotEnoughShares() { + final var fakeTssLibrary = new FakeTssLibrary(3); + final var privateShares = new ArrayList(); + final var privateKeyShares = new long[] {1, 2}; + for (int i = 0; i < privateKeyShares.length; i++) { + final var privateKeyElement = new FakeFieldElement(BigInteger.valueOf(privateKeyShares[i])); + privateShares.add( + new TssPrivateShare(new TssShareId(i), new PairingPrivateKey(privateKeyElement, SIGNATURE_SCHEMA))); + } + + assertTrue(assertThrows(IllegalStateException.class, () -> fakeTssLibrary.aggregatePrivateShares(privateShares)) + .getMessage() + .contains("Not enough shares to aggregate")); + } + + @Test + void aggregatePublicShares() { + final var fakeTssLibrary = new FakeTssLibrary(2); + final var publicShares = new ArrayList(); + final var publicKeyShares = new long[] {1, 2, 3}; + for (int i = 0; i < publicKeyShares.length; i++) { + final var publicKeyElement = new FakeGroupElement(BigInteger.valueOf(publicKeyShares[i])); + publicShares.add( + new TssPublicShare(new TssShareId(i), new PairingPublicKey(publicKeyElement, SIGNATURE_SCHEMA))); + } + + final var aggregatedPublicKey = fakeTssLibrary.aggregatePublicShares(publicShares); + + assertNotNull(aggregatedPublicKey); + assertEquals("47", new BigInteger(1, aggregatedPublicKey.publicKey().toBytes()).toString()); + } + + @Test + void aggregateSignatures() { + final var fakeTssLibrary = new FakeTssLibrary(2); + final var partialSignatures = new ArrayList(); + final var signatureShares = new long[] {1, 2, 3}; + for (int i = 0; i < signatureShares.length; i++) { + final var signatureElement = new FakeGroupElement(BigInteger.valueOf(signatureShares[i])); + partialSignatures.add( + new TssShareSignature(new TssShareId(i), new PairingSignature(signatureElement, SIGNATURE_SCHEMA))); + } + + final var aggregatedSignature = fakeTssLibrary.aggregateSignatures(partialSignatures); + + assertNotNull(aggregatedSignature); + final var expectedSignature = + "8725231785142640510958974801449281668044511174527971820957835005137448197712608590715499503138764434364488379578757"; + assertEquals( + expectedSignature, + new BigInteger(1, aggregatedSignature.signature().toBytes()).toString()); + } + + @Test + void verifySignature() { + final var privateKeyElement = new FakeFieldElement(BigInteger.valueOf(42L)); + final var pairingPrivateKey = new PairingPrivateKey(privateKeyElement, SIGNATURE_SCHEMA); + final var pairingPublicKey = pairingPrivateKey.createPublicKey(); + final var p0PrivateShare = new TssPrivateShare(new TssShareId(0), pairingPrivateKey); + + final var tssDirectoryBuilder = TssParticipantDirectory.createBuilder() + .withSelf(0, pairingPrivateKey) + .withParticipant(0, 1, pairingPublicKey); + + final var publicShares = new ArrayList(); + publicShares.add(new TssPublicShare(new TssShareId(0), pairingPublicKey)); + + final var publicKeyShares = new long[] {37L, 73L}; + for (int i = 0; i < publicKeyShares.length; i++) { + final var publicKeyElement = new FakeGroupElement(BigInteger.valueOf(publicKeyShares[i])); + final var publicKey = new PairingPublicKey(publicKeyElement, SIGNATURE_SCHEMA); + publicShares.add(new TssPublicShare(new TssShareId(i + 1), publicKey)); + tssDirectoryBuilder.withParticipant(i + 1, 1, publicKey); + } + + final var threshold = 2; + final var fakeTssLibrary = new FakeTssLibrary(threshold); + final PairingPublicKey ledgerID = fakeTssLibrary.aggregatePublicShares(publicShares); + + final TssParticipantDirectory p0sDirectory = + tssDirectoryBuilder.withThreshold(threshold).build(SIGNATURE_SCHEMA); + + // then + // After genesis, and assuming the same participantDirectory p0 will have a list of 1 private share + + final SecureRandom random = new SecureRandom(); + final byte[] messageToSign = new byte[20]; + random.nextBytes(messageToSign); + + final List privateShares = List.of(p0PrivateShare); + final List signatures = new ArrayList<>(); + for (TssPrivateShare privateShare : privateShares) { + signatures.add(fakeTssLibrary.sign(privateShare, messageToSign)); + } + + // After signing, it will collect all other participant signatures + final List p1Signatures = List.of(new TssShareSignature( + new TssShareId(1), signatures.getFirst().signature())); // pretend we get another valid signature + final byte[] invalidSignature = new byte[20]; + random.nextBytes(invalidSignature); + final List p2Signatures = List.of(new TssShareSignature( + new TssShareId(2), + new PairingSignature(new FakeGroupElement(new BigInteger(1, invalidSignature)), SIGNATURE_SCHEMA))); + + final List collectedSignatures = new ArrayList<>(); + collectedSignatures.addAll(signatures); + collectedSignatures.addAll(p1Signatures); + collectedSignatures.addAll(p2Signatures); + + fakeTssLibrary.setTestMessage(messageToSign); + final List validSignatures = collectedSignatures.stream() + .filter(sign -> fakeTssLibrary.verifySignature(p0sDirectory, publicShares, sign)) + .toList(); + + final PairingSignature aggregatedSignature = fakeTssLibrary.aggregateSignatures(validSignatures); + assertTrue(aggregatedSignature.verify(ledgerID, messageToSign)); + } +} diff --git a/platform-sdk/consensus-gossip-impl/build.gradle.kts b/platform-sdk/consensus-gossip-impl/build.gradle.kts new file mode 100644 index 000000000000..3310d8eeef45 --- /dev/null +++ b/platform-sdk/consensus-gossip-impl/build.gradle.kts @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("com.hedera.gradle.services") + id("com.hedera.gradle.services-publish") +} + +description = "Default Consensus Gossip Implementation" diff --git a/platform-sdk/consensus-gossip-impl/src/main/java/com/hedera/service/gossip/impl/GossipServiceImpl.java b/platform-sdk/consensus-gossip-impl/src/main/java/com/hedera/service/gossip/impl/GossipServiceImpl.java new file mode 100644 index 000000000000..5b21a18e6756 --- /dev/null +++ b/platform-sdk/consensus-gossip-impl/src/main/java/com/hedera/service/gossip/impl/GossipServiceImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.service.gossip.impl; + +import com.hedera.service.gossip.GossipService; + +/** + * Implementation for the mock gossip service. + */ +public final class GossipServiceImpl implements GossipService {} diff --git a/platform-sdk/consensus-gossip-impl/src/main/java/module-info.java b/platform-sdk/consensus-gossip-impl/src/main/java/module-info.java new file mode 100644 index 000000000000..3b7a196f6c32 --- /dev/null +++ b/platform-sdk/consensus-gossip-impl/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module com.hedera.consensus.gossip.impl { + requires transitive com.hedera.service.gossip; + + provides com.hedera.service.gossip.GossipService with + com.hedera.service.gossip.impl.GossipServiceImpl; +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/AssertionOutcome.java b/platform-sdk/consensus-gossip/build.gradle.kts similarity index 76% rename from hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/AssertionOutcome.java rename to platform-sdk/consensus-gossip/build.gradle.kts index aa0ac490d423..d00919743eeb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/utilops/streams/AssertionOutcome.java +++ b/platform-sdk/consensus-gossip/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2024 Hedera Hashgraph, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.hedera.services.bdd.spec.utilops.streams; - -public enum AssertionOutcome { - SUCCESS, - FAILURE, - TIMEOUT +plugins { + id("com.hedera.gradle.services") + id("com.hedera.gradle.services-publish") } + +description = "Consensus Gossip API" diff --git a/platform-sdk/consensus-gossip/src/main/java/com/hedera/service/gossip/GossipService.java b/platform-sdk/consensus-gossip/src/main/java/com/hedera/service/gossip/GossipService.java new file mode 100644 index 000000000000..8957b7a72f69 --- /dev/null +++ b/platform-sdk/consensus-gossip/src/main/java/com/hedera/service/gossip/GossipService.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.service.gossip; + +/** + * Mock gossip service. + */ +public interface GossipService {} diff --git a/platform-sdk/consensus-gossip/src/main/java/module-info.java b/platform-sdk/consensus-gossip/src/main/java/module-info.java new file mode 100644 index 000000000000..148062dd027d --- /dev/null +++ b/platform-sdk/consensus-gossip/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module com.hedera.service.gossip { + exports com.hedera.service.gossip; +} diff --git a/platform-sdk/description.txt b/platform-sdk/description.txt new file mode 100644 index 000000000000..90ec8bd24256 --- /dev/null +++ b/platform-sdk/description.txt @@ -0,0 +1,3 @@ +Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud +without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at +a fraction of the cost of traditional server-based platforms. diff --git a/platform-sdk/developers.properties b/platform-sdk/developers.properties new file mode 100644 index 000000000000..6e8ae7c80af6 --- /dev/null +++ b/platform-sdk/developers.properties @@ -0,0 +1,4 @@ +platform-base@hashgraph.com=Platform Base Team +platform-hashgraph@hashgraph.com=Platform Hashgraph Team +platform-data@hashgraph.com=Platform Data Team +release-engineering@hashgraph.com=Release Engineering Team diff --git a/platform-sdk/docs/proposals/consensus-layer/Consensus-Layer.md b/platform-sdk/docs/proposals/consensus-layer/Consensus-Layer.md new file mode 100644 index 000000000000..2581921deb18 --- /dev/null +++ b/platform-sdk/docs/proposals/consensus-layer/Consensus-Layer.md @@ -0,0 +1,684 @@ +# Consensus Layer of the Consensus Node + +--- + +## Summary + +Update the architecture for the consensus node to reduce complexity, improve performance, and improve stability. + +| Metadata | Entities | +|--------------------|----------------------------------------------------------| +| Designers | Richard Bair, Jasper Potts, Oleg Mazurov, Austin Littley | +| Functional Impacts | Consensus Node | + +--- + +## Assumptions + +1. The proposed design assumes that the work to + [use a birth-round based definition of ancient](https://github.com/hashgraph/hedera-services/issues/13817) has been + completed + +## Purpose and Context + +Much of the motivation for this design can come down to paying down technical debt and simplifying the overall design. +While the current design is full of amazing high quality solutions to various problems, it is also more complex than +necessary, leading to hard-to-find or predict bugs, performance problems, or liveness (stability) issues while under +load. This work is also necessary to prepare for autonomous node operation, and community nodes. + +1. This design defines several high-level modules providing strong encapsulation and isolation with strict contracts + between modules, leading to an overall simpler and more correct system. +2. Assumptions and requirements that led to tight coupling between modules have been revisited, and where possible, + eliminated. +3. The two fundamental modules are "consensus" and "execution". The Consensus module takes transactions and produces + rounds. Everything required to make that happen (gossip, event validation, hashgraph, event creation, etc.) is part + of the Consensus module. It is a library, and instances of the classes and interfaces within this library are created + and managed by the Execution module. The Consensus module does not persist state in the merkle tree, has no main + method, and has minimal dependencies. +4. The Execution module is a mixture of what we have called "services" and some parts of "platform". The responsibility + for reconnect, state saving, lifecycle, etc. will be merged with modules making up the base of the application, + dramatically simplifying the interaction between "consensus" and "execution". +5. Maintaining high availability under unpredictable conditions ("liveness under stress"), will be designed based on a + combination of network and individual (per-node) actions. Each node individually will do its best to deal + gracefully when under stress, and the network as a whole will cooperate to reduce load when enough nodes in the + network are under stress. This network-wide throttling is based on "dynamic network throttles". + +The purpose of this document is not to describe the implementation details of each of the different modules. Nor does +it go into great detail about the design of the Execution module (which is primarily documented elsewhere). Instead, +it provides an overview of the whole system, with an emphasis on the Consensus module, and how the Consensus module +interacts with the Execution module. + +This document supports existing features implemented in new ways, and it provides for new features (such as the dynamic +address book) which have not been implemented. After acceptance, a long series of changes will be required to modify +the existing codebase to meet this new design. This will not happen overnight, nor will it block progress on all other +initiatives. Instead, this plan provides the blueprint for our new node architecture, which we will work towards +implementing with every change we make going forward. This blueprint will also provide the framework within which we +will evaluate all other feature designs and implementations. + +## Design + +![Network](network.png) + +A group of consensus nodes together form a _consensus network_. The network, as a whole, takes as input _transactions_ +and each node in the network produces as output a _blockchain_ represented as a _block stream_. Each node in the network +maintains _state_. The job of a node is to (a) work with other nodes to come to _consensus_ on which transactions to +include, (b) order those transactions and assign a _consensus timestamp_ to each transaction, (c) handle each +transaction, transitioning state from _S_ to _S'_, (d) produce blocks containing a history of inputs and state +transitions, (e) work with other nodes to sign each block, and (f) export the block. + +As a decentralized network, each node can make no assumptions about the other nodes in the network. Other nodes may be +faster, or slower. They may have superior or inferior network connections. They may be far away from each other or +colocated. Each of those parameters may change over time. They may be running modified software or the official builds. +They may be honest, or dishonest. Each node must assume that just more than 2/3 of the other nodes are honest, but must +also assume that any particular node, other than itself, may be dishonest. + +The network must also _as a whole_ remain resilient and operational regardless of the transaction load supplied to the +network. Nodes that are unable to keep up with the transaction load must be able to fail gracefully and rejoin the +network if conditions improve. If a sufficiently large number of nodes are unable to keep up with the transaction load, +then the network as a whole must collaborate to _throttle_ transaction ingestion to a level that will permit the network +to remain stable. + +The design of the consensus node _does not require_ careful tuning for particular execution environments in order +to remain live and responsive. Indeed, it is a hard requirement that tuning _cannot be required_. It is infeasible to +test the exact configuration of a decentralized network, by definition, and therefore cannot be required for stable +operation. + +![Design](consensus-module-arch.png) + +The consensus node is made up of two parts, a Consensus layer, and an Execution layer. The Consensus layer takes as +input transactions and produces as output an ordered list of rounds. Each round contains the ordered and timestamped +list of transactions produced by the hashgraph consensus algorithm. The Execution layer is responsible for executing +transactions, transitioning state, producing blocks, signing blocks, and exporting blocks. + +Each layer is represented by JPMS modules. The Consensus layer will actually be made up of two different modules -- an +API module and an implementation module, though unless the distinction is important, this document will usually refer +to just "the Consensus Module". The API module will define an `Interface` corresponding to the dotted-line box in the +Consensus layer blue box. The Execution implementation module will have a compile-time dependency on the Consensus +layer's API module, and a runtime dependency on the Consensus layer's implementation module. + +Each submodule will likewise be defined by a pair of JPMS modules -- an API module and an implementation module. By +separating the API and implementation modules, we make it possible to supply multiple implementation modules (which is +useful for testing or future maintenance tasks), and we also support circular dependencies between modules. + +### Foundational Concepts + +This design relies on several foundational concepts based on the hashgraph consensus algorithm. The term "hashgraph" +refers to a data structure, while the "hashgraph consensus algorithm" refers to the particular consensus algorithm +making use of a hashgraph for the purposes of consensus. The algorithm itself is only superficially described here, +sufficient only to understand the overall system architecture. + +A hashgraph is a directed acyclic graph (or DAG), made up of _events_. Each event maintains references to "parent" +events. When the hashgraph consensus algorithm runs, it collects events into _consensus rounds_. One or more rounds is +grouped together by the Execution layer, executed, and used to form a block in the blockchain. + +Each event contains an ordered list of _transactions_. + +Nodes create events. Each event in the hashgraph has a _creator_. The creator is the node that created the event. Each +event also has a _birth round_. This is the most recent round number known by the creator at the time it created the +event. When a node creates an event, it fills that event with some, or all, of the transactions it knows about. Each +creator creates a single event at a time, with some interval between event creations (say, every 50ms), and some maximum +network-wide configuration for the number of events per second per creator. Each event will have as a "self-parent" the +previous event created by that creator, assuming the creator remembers its previous event, and that event isn't ancient. +Each event will additionally have one or more "other parent" events created by other creators, apart from the edge cases +of network genesis and single node networks, where there may be zero other parents. + +Any given node has a system clock, and this clock provides the node with the current _wall clock time_. This is the +current "real" time, as the node understands it. Since we cannot trust the clock of any particular node, we cannot trust +the wall clock time of the creator to be accurate. The creator may lie. During the execution of the hashgraph consensus +algorithm, each node will deterministically assign a _consensus time_ to each event (and thus by extension to each of +the transactions within the event). + +Each node has a _roster_ listing all other nodes, their public cryptographic keys, their consensus weights (since the +network is a proof-of-stake network, different nodes may have different "weights" when voting for consensus), etc. The +cryptographic keys are used to verify that an event created by a creator was truly created by that creator. The roster +can change over time, so it is vital that the correct roster be used for verifying each event. The correct roster to use +for verifying an event is the roster that was active during that event's birth round. + +![Hashgraph](hashgraph.png) + +The hashgraph can be organized visually in a simple series of "swim lanes", running vertically, one per creator. Each +"other parent" is a line from an event to a swimlane for another creator. Newer events are on the top of the hashgraph. + +For example, in the above diagram, each event has exactly 2 parents: the self-parent, and one other-parent. Event `B3` +has as a self-parent `B2`, and an other-parent of `A2`. Events `A1`, `A2`, `A3`, and `A4` are all events created by node +`A`, while `B1`, `B2`, `B3`, and `B4` are created by node `B`, and so on. + +Each node has its own copy of the hashgraph. Since events are being gossiped asynchronously throughout the network, +newer events (those at the top of the graph) may be known to some nodes, and not to others. Broken or dishonest nodes +may work to prevent some events from being known to all nodes, and thus there may be some differences in the hashgraph +of each node. But the hashgraph algorithm will, with probability 1, come to consensus given just over 2/3 of the nodes +are honest. + +#### The Road to Finality + +A transaction is submitted to a node in the network. This node, upon verifying the integrity of the transaction, will +include this transaction in a future event it creates. This new event is assigned a _birth round_ matching the most +recent round number of the hashgraph on the node that created the event. This birth round is used to determine which +roster should be used to verify the event, and lets other nodes in the network know how far along in processing the +hashgraph this node was at the time the event was created. + +The event is then gossiped, or distributed throughout the network. Each node that receives this event validates it and +inserts it into their own copy of the hashgraph. Eventually, the hashgraph algorithm runs on each node (which may +happen at different wall clock times!) and the event will either become stale, or be included in a round. Every honest +node will always come to the same conclusion, and either determine that the event is stale, or include the event in the +same round. + +Each round is then passed to the Execution layer, where the transactions in the round are executed, and the state is +transitioned accordingly. For example, hbars may be transferred from one account to another. At the end of some +deterministic number of rounds, the Execution layer will create a block. The block hash will be signed and all the +nodes together will work to sign the block (using an algorithm known as TSS). The block is then exported from the +node. In cases where a block signature can't be created in time, then validity will propagate backwards from a signature +on a future block. + +Once the block is exported from the node, the transaction execution is truly final. Since the network together signed +the block, users have an iron-clad guarantee that the contents of the block represent the consensus result of executing +those transactions. Since they are included in a blockchain, there is an immutable, cryptographic proof of execution. + +### Liveness Under Stress + +A node is under stress when it is unable to process events fast enough to keep up with the other nodes in the network. +The network is under stress when a sufficient number of nodes are under stress. When the network is under stress, all +nodes together must work to reduce the number of transactions allowed into the network, to give the network a chance +to recover. The method by which this is done will be covered in another design document related to "dynamic throttling". + +The Consensus Layer performs work on events. The more events, the more work. If events are unbounded, then a node +under stress will eventually run out of memory and crash. If there are no events, then there is virtually no CPU or +memory used by Consensus. It is therefore critical that the number of events be bounded within any given node, +even if the node is running slower than other nodes in the network. Each node must maintain _at least_ all non-ancient +events, and should maintain additional non-expired events (though these could be stored on disk to remove them from +the memory requirement of the node). In addition, _birth round filtering_ prevents a node from accepting very old or far +future events (see [Birth-Round Filtering](#birth-round-filtering)). + +#### CPU Pressure + +From a high level, either Execution or Consensus can be the primary bottleneck in handling events. + +##### Consensus Bottlenecks + +Let us suppose that we have a node, Alice. Perhaps initially Alice is able to receive and process events at the same +speed as other nodes in the network. Perhaps the network load increases past some point that Alice can handle. At this +point, other nodes are receiving, creating, gossiping, and coming to consensus on rounds faster than Alice. Remember: + +1. Birth-round filtering limits the number of events received by Alice to coincide with the pace at which Alice is + handling consensus. +2. The Tipset algorithm only creates events when doing so will advance consensus. It relies on events that have already + passed through Birth-round filtering. + +As the other nodes progress farther than Alice, they begin to send events with newer and newer birth rounds. At some +point, they get too far ahead and begin delivering events that are too far in the future and fail to pass Alice's +birth-round filter. This prevents Alice from being overwhelmed by events. + +Further, since events are coming more slowly to Alice, she will also create her own events more slowly. + +Further, since events are coming more slowly to Alice, her Event Creator will update more slowly. When the tipset +algorithm is consulted for an "other parent", it will find after a short time that there are no other parents it can +select that will advance consensus. This will cause it to stop creating events until enough events from other creators +have been processed. This natural slowing of event creation provides a virtuous cycle: as each stressed node slows down +the event creation rate, it starts to create events with more and more transactions within each event and at a slower +rate. This will lead to fewer overall events, allowing it to do less work. If the node is still not able to keep up, +eventually it will refuse to accept any additional transactions from users. If enough nodes in the network are stressed, +then the overall transaction ingestion rate of the network will be reduced, further reducing the amount of work each +node has to do. Eventually, an equilibrium is reached. + +If the rate at which the network is creating events slows, Alice will be able to catch up by retrieving all required +events through gossip, and will be able to process them and catch up. Or, in the last extremity where Alice has fallen +too far behind, Alice will wait for some time and reconnect. + +##### Slow Execution + +Under normal circumstances, the Execution layer is always the actual bottleneck. The cost of processing a few hundreds +of events pales in comparison to the cost of processing tens of thousands of transactions. Execution must therefore +provide some backpressure on Consensus. In this design, we propose that the Hashgraph module **never** runs the +consensus algorithm until it is told to do so from the Execution module. + +The TCP Sliding Window is a classic technique for controlling backpressure in networking. The receiver controls the rate +of transmission by signalling to the sender how many bytes can be sent before receiving a new update from the receiver. +The same technique is used for backpressure in the HTTP2 protocol. We will use the same concept here. + +Execution will instruct Consensus each time it needs additional rounds processed. It could indicate any number of +additional rounds. For each round, it will produce the appropriate roster, even if the roster doesn't change between +rounds. (And if we have any other dynamic configuration, it is also provided in like manner). Execution is therefore +responsible for dictating the rate at which rounds can be produced, providing natural backpressure. In addition, by +sending the roster information for each round, a quick and efficient mechanism is provided for deterministically +changing the roster for any given round. This is very useful for punishing malicious nodes. + +By keeping Consensus tied to Execution in this way, if one node's Execution runs slowly compared to other nodes in the +network, that node will naturally handle consensus slower than the others, and will therefore eventually fall behind +and need to reconnect. Indeed, from the perspective of the rest of the consensus module, or from the other nodes in +the network, the behavior of the node in stress is **exactly the same** whether Consensus or Execution are the reason +for falling behind. + +##### A Silly Example + +Suppose we have a 4 node network, where 1 node is a Raspberry PI and the other 3 nodes are 64-core 256GB machines. In +this network, at 10 TPS, all 4 nodes may be able to process events and handle transactions without any problems. If the +transaction load were to increase to 10,000 TPS, then the Raspberry PI may not be able to keep up with this workload, +while the other 3 machines might. The healthy machines will continue to accept transactions and create new events, while +the machine under stress is unable to create new consensus rounds fast enough, since consensus is stalled waiting for +the Execution layer to finish processing previous rounds. As time goes on, the slow machine cannot receive all events +from other nodes, since the events are too far in the future. If this occurs, the slow machine will begin to fall +behind. + +It may be that the load decreases back to a manageable level, and the Raspberry PI is able to successfully "catch up" +with the other nodes. Or, it may be that the load continues long enough that the Raspberry PI falls fully behind. At +this point, the only recourse is for the node to reconnect. This it will do, using an exponential backoff algorithm, so +that if the PI is continually falling behind, it will wait a longer and longer time before it attempts to reconnect +again. + +Eventually, the PI may encounter a quieter network, and successfully reconnect and rejoin the network. Or the node +operator may decide to upgrade to a more capable machine so it can rejoin the network and participate. In either case, +the node was able to gracefully handle slow execution without having to take any direct or extraordinary action. + +### Lifecycle of the Consensus Module + +When Execution starts, it will (at the appropriate time in its startup routine) create an instance of Consensus, and +`initialize` it with appropriate arguments, which will be defined in detail in further documents. Critically, +Consensus **does not persist state in the merkle tree**. Execution is wholly responsible for the management of the +state. To start Consensus from a particular moment in time, Execution will need to initialize it with some information +such as the judges of the round it wants to start from. It is by using this `initialize` method that Execution is able +to create a Consensus instance that starts from genesis, or from a particular round. + +Likewise, if a node needs to reconnect, Execution will `destroy` the existing Consensus instance, and create a new one, +and `initialize` it appropriately with information from the starting round, after having downloaded necessary data and +initializing itself with the correct round. Reconnect therefore is the responsibility of Execution. Consensus does not +have to consider reconnect at all. + +## Modules + +### Gossip + +The Gossip module is responsible for gossiping messages between neighbors. The actual gossip implementation is not +described here, except to say that it will be possible to define and implement both event-aware and event-agnostic +gossip implementations either to a fully connected network or where the set of neighbors is a subset of the whole. This +document does not dictate whether raw TCP, UDP, HTTP2, gRPC, or other network protocols are used. This will be left to +the design documents for Gossip. + +![Gossip](gossip-module.png) + +Gossip is the only part of Consensus that communicates over the network with gossip neighbors. When Gossip is +initialized, it is supplied a roster. This roster contains the full set of nodes participating in gossip, along with +their metadata such as RSA signing keys, IP addresses, and so forth. The Gossip module decides which neighbors to gossip +with (using whatever algorithm it chooses). + +#### Events + +Gossip is event-oriented, meaning that it is given events to gossip, and emits events it receives through gossip. An +implementation of Gossip could be based on a lower-level implementation based on bytes, but at the module level, it +works in terms of events. + +When the Gossip module receives events through gossip, it *may* choose to perform some deduplication before sending +them to Event Intake, but it is not required to do so. + +Some gossip algorithms send events in *topological order*. A neighbor may still receive events out of order, because +different events may arrive from different neighbors at different times. Events received by Gossip are not immediately +retransmitted to its neighbors. **An honest node will only send valid events through gossip**. If invalid events are +ever received, you may know the node that sent them to you is dishonest. Validating events increases latency at each +"hop", but allows us to identify dishonest gossip neighbors and discipline them accordingly. For this reason, events +received by Gossip are sent to Event Intake, which eventually send valid, ordered events _back_ to Gossip for +redistribution. + +During execution, for all nodes that are online and able to keep up, events are received "live" and processed +immediately and re-gossiped. However, if a node is offline and then comes back online, or is starting back up after +reconnect, it may be missing events. In this case, the node will need to ask its neighbors for any events it is missing. + +For this reason, every honest node needs to buffer some events, so when its neighbors ask it for events, it is able to +send them. The Gossip module **may** cache all non-expired events, but **must** cache all non-ancient events. +Non-expired events are crucial, because such events allow a node that is moderately far behind its neighbors to catch +back up without incurring the high cost of a reconnect. This may occur during normal operation, if a node is +experiencing stress, but is particularly likely just after having performed a reconnect. A recently reconnected node may +be several minutes behind its neighbors, but still be able to catch the rest of the way up by receiving those older +events through gossip. + +#### Neighbor Discipline + +If a neighbor misbehaves, the Gossip module will notify the Sheriff module that one of its neighbors is misbehaving. For +example, if a neighbor is not responding to requests, even after repeated attempts to make a TCP connection with it, it +may be "bad". Or if the neighbor is sending events that exceed an acceptable rate, or exceed an acceptable size, then it +is "bad". Or if the events it sends cannot be parsed, or are signed incorrectly, or in other ways fail validation, then +it is "bad". There may be additional rules by the Gossip module or others (such as Event Intake detecting branching) +that could lead to a neighbor being marked as "bad". A "bad" node may be dishonest, or it may be broken. The two cases +may be indistinguishable, so punishment must be adjusted based on the severity of the behavior. + +If the Sheriff decides that the neighbor should be penalized, then it will instruct the Gossip module to "shun" that +neighbor. "Shunning" is a unilateral behavior that one node can take towards another, where it terminates the connection +and refuses to work further with that neighbor. If the Sheriff decides to welcome a neighbor back into the fold, it can +instruct the Gossip module to "welcome" the neighbor back. + +#### Falling Behind + +When a node is operating, it receives events from its neighbors through gossip. If the node for some reason is unable to +receive and process events fast enough, it may start to "fall behind" the other nodes. Perhaps Bob is processing round +200 while Alice is still on round 100. If Alice continues to fall farther and farther behind, the time may come when she +can no longer get old events from her neighbors. From the perspective of the neighbor, the events Alice says she needs +may have expired, and Bob may no longer be holding those events. + +If this happens, then Alice has "fallen behind" and must reconnect. There is no longer any chance that she can get the +events she needs through gossip alone. Gossip will detect this situation and make a call through the Consensus module +interface to notify Execution that the node is behind. Execution will then initiate reconnect. + +#### Roster Changes + +At runtime, it is possible that the roster will change dynamically (as happens with the dynamic address book feature). +Roster changes at the gossip level may influence which neighbors the module will work with. As with all other modules +using rosters, Gossip may need a deterministic understanding of which roster applies to which round. It will receive +this information from Hashgraph in the form of round metadata. + +### Event Intake + +The Event Intake System is responsible for receiving events, validating them, and emitting them in *topological order*. +It also makes sure they are durably persisted before emission, which prevents branching during upgrades, and adds +resilience to the node in case of certain catastrophic failure scenarios. + +![Event Intake](event-intake-module.png) + +#### Validation + +One of the core responsibilities of Event Intake is to validate the events it has received. While this document does +not specify the validation pipeline, it will define some of the primary steps involved in validation, so as to motivate +the purpose of this module. That is, the following description is non-normative, but important for understanding the +context within which this module operates. + +Event Intake receives events from gossip, or from the Event Creator module (i.e. "self-events"). Event Intake is +responsible for computing some pieces of metadata pertaining to the event, such as the event hash. Event Intake also +deduplicates events, and checks for "syntactic" correctness. For example, it verifies that all required fields are +populated. While the Gossip system has already checked to ensure the payload of the event (its transactions) are +limited in size and count, Event Intake will also check this as an additional safety measure. + +If an event is valid, then we finally check the signature. Since validation and deduplication and hashing are +significantly less expensive than signature verification, we wait on signature verification until the other steps are +completed. The operating principle is that we want to fail fast and limit work for further stages in the pipeline. + +If an event has a very old birth-round that is ancient, it is dropped. If a node sends a large number of ancient events, +it may end up being disciplined (the exact rules around this will be defined in subsequent design docs for the +Event Intake module). + +If an event builds upon a parent with a newer birth-round than itself, then it is invalid and discarded. + +#### Topological Ordering + +Events are buffered if necessary to ensure that each parent event has been emitted from Event Intake before any child +events. A simple map (the same used for deduplication) can be used here. Given some event, for each parent, look up the +parent by its hash. If each parent is found in the map, then emit the event. Otherwise, remember the event so when the +missing parent is received, the child may be emitted. The current implementation uses what is known as the "orphan +buffer" for this purpose. + +Since Event Intake will also maintain some buffers, it needs to know about the progression of the hashgraph, +so it can evict old events. In this case, the "orphan buffer" holds events until either the parent events have +arrived, or the events have become ancient due to the advancement of the "non-ancient event window" and the event is +dropped from the buffer. This document does not prescribe the existence of the orphan buffer or the method by which +events are sorted and emitted in topological order, but it does describe a method by which old events can be dropped. + +#### Birth-Round Filtering + +When an event is created, it is assigned a "birth round". This is the most recent consensus round on the node at the +time the event is created. Two different nodes in the same network might be working on very different moments in the +hashgraph timeline. One node, Alice, may be working on round 100 while a better-connected or faster node, Bob is working +on round 200. When Alice creates an event, it will be for birth-round 100, while an event created by Bob at the same +instant would be for birth-round 200. + +It is not possible for any one node to get much farther ahead of all other nodes, since the only way to have a newer +birth-round is to advance the hashgraph, and that requires >2/3 of the network by stake weight. Therefore, in this +example, Alice is not just 100 rounds behind Bob, she must be 100 rounds behind at least 2/3 of the network by +stake weight, or, Bob is lying. He may create events at round 200, but not actually have a hashgraph that has advanced +to that round. + +Each event must be validated using the roster associated with its birth-round. If Alice is far behind Bob, and she +receives an event for a birth-round she doesn't have the roster for, then she cannot validate the event. If the Event +Intake module receives a far-future event which cannot be validated, then the event will be dropped. + +#### Self Events + +Events are not only given to the Event Intake system through gossip. Self events (those events created by the node +itself) are also fed to Event Intake. These events **may** bypass some steps in the pipeline. For example, self-events +do not need validation. Likewise, when replaying events from the pre-consensus event buffer, those checks are not needed +(since they have already been proved valid and are in topological order). + +#### Neighbor Discipline + +During the validation process, invalid events are rejected, and this information is passed to the Sheriff module so the +offending node may be disciplined. Note that the node to be disciplined will be the node that sent this bad event, not +the creator. This information (which node sent the event) must be captured by Gossip and passed to Event Intake as part +of the event metadata. + +#### Branch Detection + +The Event Intake module inspects events to determine whether any given event creator is "branching" the hashgraph. A +"branch" happens when two or more different events from the same creator have the same "self-event" parent. Any node +that branches (known affectionately as a "Dirty Rotten Brancher") will be reported to the Sheriff. Branching is a sign +of either a dishonest node, or a seriously broken node. In either case, it may be subject to "shunning", and will be +reported to the Execution layer for further observation and, if required, action (such as canceling rewards for +stakers to that node). + +Pre-consensus branch detection and remediation can happen quickly, but to _prove_ a node is a DRB, the check will +have to happen in the Hashgraph module. When determining to take system-wide action, only the actually proven bad +behavior post-consensus should be used. + +#### Persistence + +Event Intake is also responsible to durably persist pre-consensus events **before** they are emitted, but after they +have been ordered. This system is known as the "Pre-Consensus Event Stream", or PCES. The current implementation +requires coordination between the PCES and the Hashgraph component to know when to flush, and the PCES needs to know +when rounds are signed so it knows when to prune files from the PCES. + +It is essential for events to be durably persisted before being sent to the Hashgraph, and self-events must be +persisted before being gossiped. While it may not be necessary for all code paths to have durable pre-consensus events +before they can handle them, to simplify the understanding of the system, we simply make all events durable before +distributing them. This leads to a nice, clean, simple understanding that, during replay, the entire system will +behave predictably. + +#### Emitting Events + +When the Event Intake module emits valid, topologically sorted events, it sends them to: +- The Gossip module, to be sent to gossip neighbors +- The Event Creator module, for "other parent" selection +- The Execution layer as a "pre-handle" event +- The Hashgraph module for consensus + +The call to each of these systems is "fire and forget". Specifically, there is no guarantee to Execution that it will +definitely see an event via `pre-handle` prior to seeing it in `handle`. Technically, Consensus always calls +`pre-handle` first, but that thread may be parked arbitrarily long by the system and the `handle` thread may actually +execute first. This is extremely unlikely, but must be defended against in the Execution layer. + +### Hashgraph Module + +The Hashgraph module orders events into rounds, and assigns timestamps to events. It is given ordered, persisted +events from Event Intake. Sometimes when an event is added, it turns out to be the last event that was needed to cause +one or more entire "rounds" of events to come to consensus. When this happens, the Hashgraph module emits a `round`. +The round includes metadata about the round (the list of judge hashes, the round number, the roster, etc.) along with +the events that were included in the round, in order, with their consensus-assigned timestamps. + +![Hashgraph](hashgraph-module.png) + +Rounds are immutable. They are sent "fire and forget" style from the Hashgraph module to other modules that require +them. Some modules only really need the metadata, or a part of the metadata. Others require the actual round data. +We will pass the full round info (metadata + events) to all listeners, and they can pull from it what they need. + +#### Roster and Configuration Changes + +When the roster (or any other network-wide configuration impacting Consensus) changes, Consensus must be updated in a +deterministic manner. This section discusses specifically how rosters are updated, but configuration in general would +use a similar path. + +![Roster Changes](roster-changes.png) + +Execution may hold many rounds. It will have a round that is currently being signed, one that is being hashed (which +happens before signing), one that is being handled, and any number of rounds it is holding awaiting execution. It is +very likely this "awaiting execution" buffer will be a very small number, perhaps as small as 1. This number of rounds +held in buffer is completely under the control of Execution. Consensus does not concern itself with rounds once they +are handed to Execution. + +Consensus **only produces rounds on demand**. Execution will ask Consensus to produce the next round. Only then does +Consensus begin inserting events into the hashgraph and executing the hashgraph consensus algorithm. When any one event +is added to the hashgraph, it may produce 0 or more consensus rounds. Events are added until at least one consensus +round is produced. + +The Execution module is responsible for defining the roster. When it asks Consensus for a new round, it also supplies +the new roster. The Hashgraph module will decide in what round the new roster becomes active. Let us define `N` as the +number of rounds from the latest consensus round at which the candidate roster will become active. This number will be +configured with the same value for all nodes in the network. + +For example, suppose Execution has just finished handling round 10, and has buffered up round 11. The most recent round +that the Hashgraph module produced was round 11. Suppose `N` is 10. If the call to `nextRound` from Execution at the +end of round 10 were to supply a new Roster (`R`). The Hashgraph module will assign `R` to become active as of round 22 +(`latest_consensus_round + N + 1`). It will then work on producing consensus round 12. Once produced, metadata +indicating that `R` will be used for rounds starting at 22 is passed to Gossip, Event Intake, Event Creator, and +Execution. The roster used for round 22 may also be included in this metadata. + +There is a critical relationship between the value of N, the latency for adopting a new roster, and the number of future +events held in memory on the node. Remember that each event has a birth-round. Let us define another variable called +`Z`, such that `Z <= N`. Any event with `birth_round > latest_consensus_round + z` is considered a "far future" event, +and will be either dropped by the birth-round filtering logic in Event Intake, or simply never sent in the first place. +Any event with `birth_round > latest_consensus_round` is just a "future" event. + +The smaller the value of `Z`, the fewer events are held in memory on the node. Larger values of `Z` means more events in +memory, but it also means more "smoothing" in gossip and in handling any performance spikes. On the other hand, the +larger the value of `N`, the larger the latency between the round we know about a new roster, and the round at which it +can be actually used. Ideally, the value of `N` would be small, like 3, but we may find that 3 is too small a number for +`Z`. + +Each node may select its own value of `Z`, so long as it is less than or equal to `N`. But all nodes must use the same +value for `N`, or they will ISS since they will assign different rosters to different rounds. The value of `N` is not +defined here, but will need some investigation. Unfortunately, this number appears to require "just being chosen" and +cannot be deterministically dynamically computed based on network conditions. + +#### State + +The Hashgraph module also includes a `state` section in the metadata of the round. This is used by the Execution layer +to persist the Consensus state, for reconnect and for restart. In the state are the judges for the round, and possibly +other information. When `initialize` is called, this same state is made available again to the Consensus node. + +### Event Creator Module + +Every node in the network participating in consensus is permitted to create events to gossip to neighbors. These events +are used both for transmitting user transactions, and as the basis of the hashgraph algorithm for "gossiping about +gossip". Therefore, the Event Creator has two main responsibilities: + +1. Create events with "other parent(s)" so as to help the hashgraph progress consensus +2. Fill events with transactions to be sent to the network through Gossip + +![Event Creator](event-creator-module.png) + +#### Creating Events + +The Event Creator is configured with a `maximum_event_creation_frequency`, measured in events/sec. This is a network +wide setting. If any node creates events more rapidly than this setting, then the node will be reported to the Sheriff. +An event is not necessarily created at this frequency, but will be created at no more than this frequency. + +When it is time to potentially create an event, the Event Creator will determine whether it *should* create the event. +It may consider whether there are any transactions to send, or whether creating an event will help advance the +hashgraph. It may decide that creating the event would be bad for the network, and veto such creation. Or it may decide +that creating the event should be permitted. + +If the event is to be created, the Event Creator will decide which nodes to select as "other parents". Today, we have +exactly one "other parent" per event, but multiple "other parents" is shown to effectively reduce latency and network +traffic. While the implementation of Event Creator may choose to support only a single "other parent", the module is +designed and intended to support multiple "other parents". + +#### Filling Events + +Events form a large amount of the network traffic between nodes. Each event has some overhead in terms of metadata, +such as the hashes of the parent events and cryptographic signatures. Thus, for bandwidth and scalability reasons, it is +more desirable to have fewer, large events rather than many small events. On the other hand, events should be created +frequently enough to reduce the overall latency experienced by a transaction. The Event Creator is designed so as to +find the optimal balance between event creation frequency and size. The particular algorithm that does so (the Tipset +algorithm, or "Enhanced Other Parent Selection" algorithm) is not defined here, but can be found in the design +documentation for Event Creator. + +When it is time to create a new event, a call is made to Execution to fill the event with user transactions. Newly +created events are sent to Event Intake, which then validates them, assigns generations, durably persists them, etc., +before sending them out through Gossip and so forth. + +#### Stale Self-Events + +The Event Creator needs to know about the state of the hashgraph for several reasons. If it uses the Tipset algorithm, +then it needs a way to evict events from its internal caches that are ancient. And it needs to report "stale" +self-events to the Execution layer. A stale self-event is a self-event that became ancient without ever coming to +consensus. If the Event Creator determines that a self-event has become stale, then it will notify the Execution layer. +Execution may look at each transaction within the self-event, and decide that some transactions (such as those that have +expired or will soon expire) should be dropped while others (such as those not close to expiration) should be +resubmitted in the next event. + +### Sheriff Module + +When misbehavior is found for a node, it is reported to the Sheriff. This module keeps track of the different types of +misbehavior each node is accused of, and uses this information to determine whether to "shun" or "welcome" a node. It +also sends this information to the Execution layer, so it may record misbehavior in state, if it so chooses, or publish +misbehavior to other nodes in the network, allowing the network as a whole to observe and report dishonest or broken +nodes. + +![Sheriff](sheriff-module.png) + +A node may "shun" another node, by refusing to talk with it via Gossip. If a node were to be shunned by all its +gossip neighbors, then it has been effectively removed from the network, as it can no longer submit events that will be +spread through the network, and will therefore not contribute to consensus. Should a malicious node attempt to attack +its neighbors, if those neighbors discover this attack and simply shun the node, then taking no other action, the +malicious node is prevented from causing further harm to the network. + +It may be that a node is misbehaving due to a bug, or environmental issue, rather than due to malicious intent. For +example, a broken node with network trouble may attempt to create many connections, as each prior connection having +failed for some reason. But it could also be malicious intent. Unable to tell the difference, the Sheriff may decide to +shun the node for some time period, and then "welcome" it back by allowing it to form connections again. It is up to +the Sheriff's algorithms to decide on the correct response to different behaviors. These algorithms are not defined +here, but will be defined within the Sheriff's design documentation. + +### Public API + +![Consensus API](consensus-api.png) + +The public API of the Consensus module forms the interface boundary between Consensus and Execution. + +#### initialize + +Called by the Execution layer when it creates a new Consensus instance. This call will include any required arguments +to fully initialized Consensus. For example, it will include any initial state corresponding to the current round +at which Consensus should begin work (such as the correct set of judges for that particular round). Each module within +Consensus supports some kind of initialization API that will be called. For example, each module must be initialized +with the initial roster for the starting round. + +#### destroy + +Called by the Execution layer when it destroys an existing Consensus instance. This is particularly needed to shut down +network connections held by Gossip, but could be used to stop executors, background tasks, etc. as necessary. + +#### onBehind + +Called by Consensus to notify Execution that the Consensus system is very far behind in processing relative to its +neighbors (most likely because it cannot find a neighbor that contains any of the events needed for advancing consensus). +Execution will use this call to initiate a reconnect procedure. + +#### onBadNode + +Called by Consensus to notify Execution of information about bad nodes. This can include both when a node goes "bad", +or when it is back in the good graces of the Sheriff. + +#### badNode + +Called by Execution to notify Consensus of information about bad nodes. The information to be passed here pertains to +coordinated enforcement decisions that the network has come to consensus on. + +#### getTransactionsForEvent + +Called by the Event Creator module of Consensus to give the Execution layer a chance to provide transactions for an +event. Note that, in the current system, the control is inverted -- transactions are submitted by Execution to +Consensus, which buffers them and includes them in events. As per this new design, Consensus will reach out to Execution +to ask it for all transactions that should be included in the next event, allowing the Execution layer (the application) +to decide which transactions to include. + +#### nextRound + +Called by the Execution layer when it is ready to receive another round from the Consensus layer. This call may contain +a new roster, if one is prepared and ready for use. + +#### onStaleEvent + +Called by Consensus to notify Execution when an event has become stale, giving Execution a chance to inspect the +transactions of that stale event, and resubmit those transactions in the next `onNewEvent` if they are still valid. + +#### onPreHandleEvent + +Called by Consensus once for each event emitted in topological order from Event Intake, giving Execution a chance to +perform some work before the event even comes to consensus. + +#### onRound + +Called by Consensus once for each round that comes to consensus. diff --git a/platform-sdk/docs/proposals/consensus-layer/consensus-api.png b/platform-sdk/docs/proposals/consensus-layer/consensus-api.png new file mode 100644 index 000000000000..afcdcb1b71b0 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/consensus-api.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/consensus-module-arch.png b/platform-sdk/docs/proposals/consensus-layer/consensus-module-arch.png new file mode 100644 index 000000000000..c2c4a4420100 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/consensus-module-arch.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/event-creator-module.png b/platform-sdk/docs/proposals/consensus-layer/event-creator-module.png new file mode 100644 index 000000000000..4a3524f4d7b1 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/event-creator-module.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/event-intake-module.png b/platform-sdk/docs/proposals/consensus-layer/event-intake-module.png new file mode 100644 index 000000000000..fb08bb6c0053 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/event-intake-module.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/gossip-module.png b/platform-sdk/docs/proposals/consensus-layer/gossip-module.png new file mode 100644 index 000000000000..f6b7392a3958 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/gossip-module.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/hashgraph-module.png b/platform-sdk/docs/proposals/consensus-layer/hashgraph-module.png new file mode 100644 index 000000000000..b01515baf992 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/hashgraph-module.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/hashgraph.png b/platform-sdk/docs/proposals/consensus-layer/hashgraph.png new file mode 100644 index 000000000000..0874275a38d4 Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/hashgraph.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/network.png b/platform-sdk/docs/proposals/consensus-layer/network.png new file mode 100644 index 000000000000..9539d3207aeb Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/network.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/roster-changes.png b/platform-sdk/docs/proposals/consensus-layer/roster-changes.png new file mode 100644 index 000000000000..3f5a75e8c75a Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/roster-changes.png differ diff --git a/platform-sdk/docs/proposals/consensus-layer/sheriff-module.png b/platform-sdk/docs/proposals/consensus-layer/sheriff-module.png new file mode 100644 index 000000000000..d8b04e5d6aaa Binary files /dev/null and b/platform-sdk/docs/proposals/consensus-layer/sheriff-module.png differ diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedIss.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedIss.java index b5476fd49f3c..80aed9219a37 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedIss.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedIss.java @@ -218,7 +218,7 @@ public static PlannedIss fromString(@NonNull final String plannedIssString) { final List nodes = new ArrayList<>(); for (final String nodeString : nodeStrings) { - final NodeId nodeId = new NodeId(Long.parseLong(nodeString)); + final NodeId nodeId = NodeId.of(Long.parseLong(nodeString)); nodes.add(nodeId); if (!uniqueNodeIds.add(nodeId)) { logger.error(EXCEPTION.getMarker(), "Node {} appears more than once in ISS description!", nodeId); diff --git a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedLogError.java b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedLogError.java index 232e1121907f..65625cbe7fe3 100644 --- a/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedLogError.java +++ b/platform-sdk/platform-apps/tests/ISSTestingTool/src/main/java/com/swirlds/demo/iss/PlannedLogError.java @@ -119,7 +119,7 @@ public static PlannedLogError fromString(@NonNull final String plannedLogErrorSt final List nodeIds = new ArrayList<>(); for (final String nodeIdString : nodeIdsStrings) { - nodeIds.add(new NodeId(Integer.parseInt(nodeIdString))); + nodeIds.add(NodeId.of(Integer.parseInt(nodeIdString))); } return new PlannedLogError(Duration.ofSeconds(elapsedSeconds), nodeIds); diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java index 8d69ef62c793..e1847a320e8a 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PlatformTestingToolMain.java @@ -710,7 +710,7 @@ private void initBasedOnPayloadCfgSimple(final PayloadCfgSimple pConfig) { } private void initializeAppClient(final String[] pars, final ObjectMapper objectMapper) throws IOException { - if ((pars == null) || (pars.length < 2) || !selfId.equals(new NodeId(0L))) { + if ((pars == null) || (pars.length < 2) || !selfId.equals(NodeId.of(0L))) { return; } @@ -785,7 +785,7 @@ public void run() { // if single mode only node 0 can submit transactions // if not single mode anyone can submit transactions - if (!submitConfig.isSingleNodeSubmit() || selfId.equals(new NodeId(0L))) { + if (!submitConfig.isSingleNodeSubmit() || selfId.equals(NodeId.of(0L))) { if (submitConfig.isSubmitInTurn()) { // Delay the start of transactions by interval multiply by node id diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PttTransactionPool.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PttTransactionPool.java index a41add61b21a..6301eb07fb25 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PttTransactionPool.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/main/java/com/swirlds/demo/platform/PttTransactionPool.java @@ -172,7 +172,7 @@ public PttTransactionPool( /** If the startFreezeAfterMin is 0, we don't send freeze transaction */ if (freezeConfig != null - && Objects.equals(platform.getSelfId(), new NodeId(0L)) + && Objects.equals(platform.getSelfId(), NodeId.of(0L)) && freezeConfig.getStartFreezeAfterMin() > 0) { this.freezeConfig = freezeConfig; this.needToSubmitFreezeTx = true; diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/test/java/com/swirlds/demo/merkle/map/internal/ExpectedFCMFamilyTest.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/test/java/com/swirlds/demo/merkle/map/internal/ExpectedFCMFamilyTest.java index d938b4d0aced..09ace01003b5 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/test/java/com/swirlds/demo/merkle/map/internal/ExpectedFCMFamilyTest.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/test/java/com/swirlds/demo/merkle/map/internal/ExpectedFCMFamilyTest.java @@ -150,7 +150,7 @@ class ExpectedFCMFamilyTest { static { platform = Mockito.mock(Platform.class); - Mockito.when(platform.getSelfId()).thenReturn(new NodeId(0L)); + Mockito.when(platform.getSelfId()).thenReturn(NodeId.of(0L)); } @BeforeAll diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java index 2ad781aa9f7b..43737a0082da 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/merkle/map/MapValueFCQTests.java @@ -79,7 +79,7 @@ public static void setUp() throws ConstructableRegistryException { mapKey = new MapKey(0, 0, random.nextLong()); state = Mockito.spy(PlatformTestingToolState.class); final Platform platform = Mockito.mock(Platform.class); - when(platform.getSelfId()).thenReturn(new NodeId(0L)); + when(platform.getSelfId()).thenReturn(NodeId.of(0L)); AddressBook addressBook = Mockito.spy(AddressBook.class); when(addressBook.getNumberWithWeight()).thenReturn(4); when(platform.getAddressBook()).thenReturn(addressBook); diff --git a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java index 0b09d53c6ae2..1ff76d7aeb62 100644 --- a/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java +++ b/platform-sdk/platform-apps/tests/PlatformTestingTool/src/timingSensitive/java/com/swirlds/demo/platform/PttTransactionPoolTest.java @@ -69,7 +69,7 @@ public class PttTransactionPoolTest { static { platform = Mockito.mock(Platform.class); - Mockito.when(platform.getSelfId()).thenReturn(new NodeId(myID)); + Mockito.when(platform.getSelfId()).thenReturn(NodeId.of(myID)); System.arraycopy(payload, 0, payloadWithSig, 0, payload.length); expectedFCMFamily = new DummyExpectedFCMFamily(myID); fCMFamily = new FCMFamily(true); diff --git a/platform-sdk/swirlds-base/build.gradle.kts b/platform-sdk/swirlds-base/build.gradle.kts index 91078842e575..0393e4a78e6b 100644 --- a/platform-sdk/swirlds-base/build.gradle.kts +++ b/platform-sdk/swirlds-base/build.gradle.kts @@ -28,7 +28,13 @@ tasks.withType().configureEach { options.compilerArgs.add("-Xlint:-exports,-varargs,-static") } -testModuleInfo { requires("org.junit.jupiter.api") } +testModuleInfo { + requires("org.junit.jupiter.api") + requires("org.assertj.core") + requires("org.mockito") + requires("org.mockito.junit.jupiter") + requires("awaitility") +} timingSensitiveModuleInfo { requires("com.swirlds.base.test.fixtures") diff --git a/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/FileSystemUtils.java b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/FileSystemUtils.java new file mode 100644 index 000000000000..5fd84c76466f --- /dev/null +++ b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/FileSystemUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import static com.swirlds.base.utility.Retry.DEFAULT_RETRY_DELAY; +import static com.swirlds.base.utility.Retry.DEFAULT_WAIT_TIME; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Objects; + +/** + * Provides file system-related utility methods. + */ +public final class FileSystemUtils { + + /** + * Private constructor to prevent utility class instantiation. + */ + private FileSystemUtils() {} + + /** + * Evaluates a given {@code path} to ensure it exists. + * + *

      + * This method includes retry logic to handle files which may not yet exist but will become available within a + * given threshold. Examples of these cases are Kubernetes projected volumes and persistent volumes. + * + *

      + * This overloaded method uses a default wait time of {@link Retry#DEFAULT_WAIT_TIME} and a default retry delay of + * {@link Retry#DEFAULT_RETRY_DELAY}. + * + * @param path the path to check for existence. + * @return true if the file exists before the retries have been exhausted; otherwise false. + * @throws NullPointerException if the {@code path} argument is a {@code null} value. + */ + public static boolean waitForPathPresence(@NonNull final Path path) { + return waitForPathPresence(path, DEFAULT_WAIT_TIME); + } + + /** + * Evaluates a given {@code path} to ensure it exists. + * + *

      + * This method includes retry logic to handle files which may not yet exist but will become available within a + * given threshold. Examples of these cases are Kubernetes projected volumes and persistent volumes. + * + *

      + * This overloaded method uses a default retry delay of {@link Retry#DEFAULT_RETRY_DELAY}. + * + * @param path the path to check for existence. + * @param waitTime the maximum amount of time to wait for the file, directory, block device, or symlink to become available. + * @return true if the file exists before the retries have been exhausted; otherwise false. + * @throws NullPointerException if the {@code path} argument is a {@code null} value. + */ + public static boolean waitForPathPresence(@NonNull final Path path, @NonNull final Duration waitTime) { + return waitForPathPresence(path, waitTime, DEFAULT_RETRY_DELAY); + } + + /** + * Evaluates a given {@code path} to ensure it exists. + * + *

      + * This method includes retry logic to handle files which may not yet exist but will become available within a + * given threshold. Examples of these cases are Kubernetes projected volumes and persistent volumes. + * + * @param path the path to check for existence. + * @param waitTime the maximum amount of time to wait for the file, directory, block device, or symlink to become available. + * @param retryDelay the delay between retry attempts. + * @return true if the file exists before the retries have been exhausted; otherwise false. + * @throws IllegalArgumentException if the {@code waitTime} argument is less than or equal to zero (0) or the + * {@code retryDelay} argument is less than or equal to zero (0). + * @throws NullPointerException if the {@code path} argument is a {@code null} value. + */ + public static boolean waitForPathPresence( + @NonNull final Path path, @NonNull final Duration waitTime, @NonNull final Duration retryDelay) { + Objects.requireNonNull(path, "path must not be null"); + try { + return Retry.check(FileSystemUtils::checkForPathPresenceInternal, path, waitTime, retryDelay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * Internal method to evaluate a given {@code path} to ensure it is present. + * + * @param path the path to check for existence. + * @return true if the file, directory, block device, or symlink exists and is not empty if the path references a + * file; otherwise false. + */ + private static boolean checkForPathPresenceInternal(@NonNull final Path path) { + // If the path does not exist, then we should consider it not present + if (!Files.exists(path)) { + return false; + } + + if (Files.isRegularFile(path)) { + try { + // If the path exists and is a file, then we must check the size to ensure it is not empty + return Files.size(path) > 0; + } catch (IOException ignored) { + // If an exception occurs while checking the file size, then we should consider the file not present + return false; + } + } + + // If the path exists and is a directory, block device or symlink, then we can consider it present + return true; + } +} diff --git a/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/NetworkUtils.java b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/NetworkUtils.java new file mode 100644 index 000000000000..9667ac2b9feb --- /dev/null +++ b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/NetworkUtils.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import static com.swirlds.base.utility.Retry.DEFAULT_RETRY_DELAY; +import static com.swirlds.base.utility.Retry.DEFAULT_WAIT_TIME; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Objects; + +/** + * Provides network-related utility methods. + */ +public final class NetworkUtils { + + /** + * Private constructor to prevent utility class instantiation. + */ + private NetworkUtils() {} + + /** + * Evaluates a given {@code hostname} to ensure it is resolvable to one or more valid IPv4 or IPv6 addresses. + * Supports domain name fragments, fully qualified domain names (FQDN), and IP addresses. + * + *

      + * This method includes retry logic to handle slow DNS queries due to network conditions, slow DNS record + * updates/propagation, and/or intermittent network connections. + * + *

      + * This overloaded method uses a default wait time of {@link Retry#DEFAULT_WAIT_TIME} and a default retry delay of + * {@link Retry#DEFAULT_RETRY_DELAY}. + * + * @param name the domain name fragment, fully qualified domain name (FQDN), or IP address to be resolved. + * @return true if the name resolves to one or more valid IP addresses; otherwise false. + * @see #isNameResolvable(String, Duration, Duration) + */ + public static boolean isNameResolvable(@NonNull final String name) { + return isNameResolvable(name, DEFAULT_WAIT_TIME, DEFAULT_RETRY_DELAY); + } + + /** + * Evaluates a given {@code hostname} to ensure it is resolvable to one or more valid IPv4 or IPv6 addresses. + * Supports domain name fragments, fully qualified domain names (FQDN), and IP addresses. + * + *

      + * This method includes retry logic to handle slow DNS queries due to network conditions, slow DNS record + * updates/propagation, and/or intermittent network connections. + * + *

      + * This overloaded method uses a default retry delay of {@link Retry#DEFAULT_RETRY_DELAY}. + * + * @param name the domain name fragment, fully qualified domain name (FQDN), or IP address to be resolved. + * @param waitTime the maximum amount of time to wait for the DNS hostname to become resolvable. + * @return true if the name resolves to one or more valid IP addresses; otherwise false. + * @see #isNameResolvable(String, Duration, Duration) + */ + public static boolean isNameResolvable(@NonNull final String name, @NonNull final Duration waitTime) { + return isNameResolvable(name, waitTime, DEFAULT_RETRY_DELAY); + } + + /** + * Evaluates a given {@code hostname} to ensure it is resolvable to one or more valid IPv4 or IPv6 addresses. + * Supports domain name fragments, fully qualified domain names (FQDN), and IP addresses. + * + *

      + * This method includes retry logic to handle slow DNS queries due to network conditions, slow DNS record + * updates/propagation, and/or intermittent network connections. + * + * @param name the domain name fragment, fully qualified domain name (FQDN), or IP address to be resolved. + * @param waitTime the maximum amount of time to wait for the DNS hostname to become resolvable. + * @param retryDelay the delay between retry attempts. + * @return true if the name resolves to one or more valid IP addresses; otherwise false. + */ + public static boolean isNameResolvable( + @NonNull final String name, @NonNull final Duration waitTime, @NonNull final Duration retryDelay) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(waitTime, "waitTime must not be null"); + Objects.requireNonNull(retryDelay, "retryDelay must not be null"); + try { + return Retry.check(NetworkUtils::isNameResolvableInternal, name, waitTime, retryDelay); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * Internal implementation used by the {@link #isNameResolvable(String, Duration, Duration)} method. + * + * @param name the domain name fragment, fully qualified domain name (FQDN), or IP address to be resolved. + * @return true if the name resolves to one or more valid IP addresses; otherwise false. + */ + private static boolean isNameResolvableInternal(@NonNull final String name) { + Objects.requireNonNull(name, "name must not be null"); + try { + final InetAddress[] addresses = InetAddress.getAllByName(name); + // If addresses is not null and has a length greater than zero (0), then we should consider this + // address resolvable. + if (addresses != null && addresses.length > 0) { + return true; + } + } catch (final UnknownHostException ignored) { + // Intentionally suppressed + } + + return false; + } +} diff --git a/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/Retry.java b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/Retry.java new file mode 100644 index 000000000000..43c452fe4327 --- /dev/null +++ b/platform-sdk/swirlds-base/src/main/java/com/swirlds/base/utility/Retry.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * Utility class that provides a method to retry a given operation until a specified condition is met or the maximum + * number of attempts have been exhausted. Methods may support an optional delay between each retry attempt. + */ +public final class Retry { + /** + * The default delay between retry attempts. + */ + static final Duration DEFAULT_RETRY_DELAY = Duration.ofMillis(500); + /** + * The default amount of time to wait for the file, directory, block device, or symlink to be avaliable. + */ + static final Duration DEFAULT_WAIT_TIME = Duration.ofSeconds(10); + + /** + * Private constructor to prevent utility class instantiation. + **/ + private Retry() {} + + /** + * Evaluates a given {@code value} using the provided {@code checkFn} method until the return value is {@code true} + * or the {@code maxWaitTime} is exceeded. A delay of {@code retryDelay} will be applied between each call to the + * {@code checkFn} method. + * + * @param checkFn the function to be called to check the user value. + * @param value the user value to be passed to the {@code checkFn} method. + * @param maxWaitTime the maximum duration to wait for the {@code checkFn} method to return true. + * @param retryDelay the delay between retry attempts. must be greater than zero and less than or equal to the + * {@code maxWaitTime}. + * @param the type of the value argument. + * @return true if the {@code checkFn} method returns true before all attempts have been exhausted; false + * if the retries are exhausted. + * @throws NullPointerException if the {@code checkFn}, the {@code value}, the {@code maxWaitTime}, or the + * {@code retryDelay} arguments are a {@code null} value. + * @throws IllegalArgumentException if the {@code maxWaitTime} argument is less than or equal to zero (0), the + * {@code retryDelay} argument is less than or equal to zero (0), or the {@code retryDelay} argument + * is greater than the {@code maxWaitTime}. + * @throws InterruptedException if the thread is interrupted while waiting. + */ + public static boolean check( + @NonNull final Function checkFn, + @NonNull final T value, + @NonNull final Duration maxWaitTime, + @NonNull final Duration retryDelay) + throws InterruptedException { + Objects.requireNonNull(checkFn, "checkFn must not be null"); + Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(maxWaitTime, "maxWaitTime must not be null"); + Objects.requireNonNull(retryDelay, "retryDelay must not be null"); + + if (maxWaitTime.isNegative() || maxWaitTime.isZero()) { + throw new IllegalArgumentException("The maximum wait time must be greater than zero (0)"); + } + + if (retryDelay.isNegative() || retryDelay.isZero()) { + throw new IllegalArgumentException("The retry delay must be greater than zero (0)"); + } + + if (retryDelay.compareTo(maxWaitTime) > 0) { + throw new IllegalArgumentException("The retry delay must be less than or equal to the maximum wait time"); + } + + final int maxAttempts = Math.round(maxWaitTime.dividedBy(retryDelay)); + return check(checkFn, value, maxAttempts, retryDelay.toMillis()); + } + + /** + * Evaluates a given {@code value} using the provided {@code checkFn} method until the return value is {@code true} + * or the {@code maxAttempts} are exhausted. A delay of {@code delayMs} will be applied between each call to the + * {@code checkFn} method. Setting the {@code delayMs} parameter to zero (0) will disable the delay mechanism and + * the {@code checkFn} method will be called as fast as possible. + * + * @param checkFn the function to be called to check the user value. + * @param value the user value to be passed to the {@code checkFn} method. + * @param maxAttempts the maximum number of retry attempts. + * @param delayMs the delay between retry attempts. must greater than or equal to zero (0). if a zero value is + * specified then no delay will be applied. + * @param the type of the value argument. + * @return true if the {@code checkFn} method returns true before all attempts have been exhausted; false + * if the retries are exhausted. + * @throws NullPointerException if the {@code checkFn} or the {@code value} arguments are a {@code null} values. + * @throws IllegalArgumentException if the {@code maxAttempts} argument is less than or equal to zero (0) or the + * {@code delayMs} argument is less than zero (0). + * @throws InterruptedException if the thread is interrupted while waiting. + */ + public static boolean check( + @NonNull final Function checkFn, + @NonNull final T value, + final int maxAttempts, + final long delayMs) + throws InterruptedException { + Objects.requireNonNull(checkFn, "checkFn must not be null"); + Objects.requireNonNull(value, "value must not be null"); + + if (maxAttempts <= 0) { + throw new IllegalArgumentException("The maximum number of attempts must be greater than zero (0)"); + } + + if (delayMs < 0) { + throw new IllegalArgumentException("The delay must be greater than or equal to zero (0)"); + } + + for (int i = 0; i < maxAttempts; i++) { + if (checkFn.apply(value)) { + return true; + } + + if (delayMs > 0) { + TimeUnit.MILLISECONDS.sleep(delayMs); + } + } + + // If the checkFn failed to resolve to a true value then we should fall + // through to here and fail the operation. + return false; + } +} diff --git a/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/FileSystemUtilsTest.java b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/FileSystemUtilsTest.java new file mode 100644 index 000000000000..74bb6216cec0 --- /dev/null +++ b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/FileSystemUtilsTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import static com.swirlds.base.utility.FileSystemUtils.waitForPathPresence; +import static com.swirlds.base.utility.Retry.DEFAULT_WAIT_TIME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; + +/** + * Validates the behavior of the {@link FileSystemUtils} utility class. + */ +class FileSystemUtilsTest { + + private static final Duration SHORT_WAIT_TIME = Duration.of(50, ChronoUnit.MILLIS); + private static final Duration SHORT_RETRY_DELAY = Duration.of(25, ChronoUnit.MILLIS); + + @TempDir(cleanup = CleanupMode.ALWAYS) + Path tempDir; + + private static ExecutorService executorService; + + @BeforeAll + static void setup() { + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + static void teardown() { + if (!executorService.isShutdown()) { + executorService.shutdownNow(); + } + } + + @Test + void delayedFileCreationResolves() { + final CountDownLatch latch = new CountDownLatch(1); + final Path targetFile = tempDir.resolve("test.txt"); + + final Future filePresence = executorService.submit(() -> { + await().atMost(DEFAULT_WAIT_TIME.plus(Duration.of(2, ChronoUnit.SECONDS))) + .until(() -> waitForPathPresence(targetFile)); + }); + + final Future fileCreation = executorService.submit(() -> { + await().until(() -> { + latch.await(); + TimeUnit.SECONDS.sleep(1); + Files.write(targetFile, new byte[] {0x01, 0x02, 0x03}); + return Files.exists(targetFile); + }); + }); + + latch.countDown(); + await().atMost(5, TimeUnit.SECONDS).until(() -> fileCreation.isDone() && filePresence.isDone()); + } + + @Test + void missingFileTimeout() { + final Path targetFile = tempDir.resolve("test.txt"); + assertThat(waitForPathPresence( + targetFile, Duration.of(50, ChronoUnit.MILLIS), Duration.of(25, ChronoUnit.MILLIS))) + .isFalse(); + } + + @Test + void zeroLengthFileTimeout() throws IOException { + final Path targetFile = tempDir.resolve("test.txt"); + Files.createFile(targetFile); + + assertThat(waitForPathPresence(targetFile, SHORT_WAIT_TIME, SHORT_RETRY_DELAY)) + .isFalse(); + } + + @Test + void nullPathShouldThrow() { + assertThatThrownBy(() -> waitForPathPresence(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("path must not be null"); + } + + @Test + void zeroDelayShouldThrow() { + assertThatThrownBy(() -> waitForPathPresence(tempDir, SHORT_WAIT_TIME, Duration.ZERO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The retry delay must be greater than zero (0)"); + } + + @Test + void negativeDelayShouldThrow() { + assertThatThrownBy(() -> waitForPathPresence(tempDir, SHORT_WAIT_TIME, SHORT_RETRY_DELAY.negated())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The retry delay must be greater than zero (0)"); + } + + @Test + void zeroWaitTimeShouldThrow() { + assertThatThrownBy(() -> waitForPathPresence(tempDir, Duration.ZERO, SHORT_RETRY_DELAY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The maximum wait time must be greater than zero (0)"); + } + + @Test + void negativeWaitTimeShouldThrow() { + assertThatThrownBy(() -> waitForPathPresence(tempDir, SHORT_WAIT_TIME.negated(), SHORT_RETRY_DELAY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The maximum wait time must be greater than zero (0)"); + } + + @Test + void delayedDirectoryCreationResolves() { + final CountDownLatch latch = new CountDownLatch(1); + final Path targetDir = tempDir.resolve("test-dir"); + + final Future filePresence = executorService.submit(() -> { + await().atMost(DEFAULT_WAIT_TIME.plus(Duration.of(2, ChronoUnit.SECONDS))) + .until(() -> waitForPathPresence(targetDir)); + }); + + final Future fileCreation = executorService.submit(() -> { + await().until(() -> { + latch.await(); + TimeUnit.SECONDS.sleep(1); + Files.createDirectory(targetDir); + return Files.exists(targetDir); + }); + }); + + latch.countDown(); + await().atMost(5, TimeUnit.SECONDS).until(() -> fileCreation.isDone() && filePresence.isDone()); + } +} diff --git a/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/NetworkUtilsTest.java b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/NetworkUtilsTest.java new file mode 100644 index 000000000000..0d092ab10af4 --- /dev/null +++ b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/NetworkUtilsTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import static com.swirlds.base.utility.NetworkUtils.isNameResolvable; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.Test; + +/** + * Validates the behavior of the {@link NetworkUtils} utility class. + */ +public class NetworkUtilsTest { + + private static final Duration SHORT_WAIT_TIME = Duration.of(25, ChronoUnit.SECONDS); + private static final Duration SHORT_RETRY_DELAY = Duration.of(25, ChronoUnit.MILLIS); + + @Test + void ipAddressShouldResolve() { + final String ipv4 = "127.0.0.1"; + assertThat(isNameResolvable(ipv4)).isTrue(); + } + + @Test + void validHostnameShouldResolve() { + final String hostname = "localhost"; + assertThat(isNameResolvable(hostname)).isTrue(); + } + + @Test + void invalidHostnameShouldNotResolve() { + final String hostname = "invalid-hostname"; + assertThat(isNameResolvable(hostname, SHORT_WAIT_TIME, SHORT_RETRY_DELAY)) + .isFalse(); + } + + @Test + void publicHostnameShouldResolve() { + final String hostname = "google.com"; + assertThat(isNameResolvable(hostname)).isTrue(); + } + + @Test + void nullHostnameShouldThrow() { + //noinspection DataFlowIssue + assertThatThrownBy(() -> isNameResolvable(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("name must not be null"); + } + + @Test + void zeroDelayShouldThrow() { + assertThatThrownBy(() -> isNameResolvable("localhost", SHORT_WAIT_TIME, Duration.ZERO)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The retry delay must be greater than zero (0)"); + } + + @Test + void negativeDelayShouldThrow() { + assertThatThrownBy(() -> isNameResolvable("localhost", SHORT_WAIT_TIME, SHORT_RETRY_DELAY.negated())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The retry delay must be greater than zero (0)"); + } + + @Test + void zeroWaitTimeShouldThrow() { + assertThatThrownBy(() -> isNameResolvable("localhost", Duration.ZERO, SHORT_RETRY_DELAY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The maximum wait time must be greater than zero (0)"); + } + + @Test + void negativeWaitTimeShouldThrow() { + assertThatThrownBy(() -> isNameResolvable("localhost", SHORT_WAIT_TIME.negated(), SHORT_RETRY_DELAY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The maximum wait time must be greater than zero (0)"); + } +} diff --git a/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/RetryTest.java b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/RetryTest.java new file mode 100644 index 000000000000..c34974974099 --- /dev/null +++ b/platform-sdk/swirlds-base/src/test/java/com/swirlds/base/utility/RetryTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.base.utility; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Validates the behavior of the {@link Retry} utility class. + */ +@ExtendWith(MockitoExtension.class) +class RetryTest { + + @Test + void nullValueShouldThrow() { + //noinspection DataFlowIssue + assertThatThrownBy(() -> Retry.check(value -> true, null, 1, 0)) + .isInstanceOf(NullPointerException.class) + .hasMessage("value must not be null"); + } + + @Test + void nullCheckFnShouldThrow() { + //noinspection DataFlowIssue + assertThatThrownBy(() -> Retry.check(null, 1, 1, 0)) + .isInstanceOf(NullPointerException.class) + .hasMessage("checkFn must not be null"); + } + + @Test + void checkPropagatesException() { + assertThatThrownBy(() -> Retry.check( + value -> { + throw new RuntimeException("Test exception"); + }, + 1, + 1, + 0)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Test exception"); + } + + @Test + void checkResolvesWithinThreshold() throws ExecutionException, InterruptedException { + resolvesAtAttempt(1, 5, true); + resolvesAtAttempt(2, 5, true); + resolvesAtAttempt(3, 5, true); + resolvesAtAttempt(4, 5, true); + resolvesAtAttempt(5, 5, true); + resolvesAtAttempt(6, 5, false); + } + + @Test + void checkResolvesWithinThresholdWithDelay() throws ExecutionException, InterruptedException { + final AtomicInteger counter = new AtomicInteger(0); + + final Function checkFn = spyLambda(value -> { + final int val = counter.incrementAndGet(); + return val == value; + }); + + await().atMost(125, TimeUnit.MILLISECONDS).until(() -> Retry.check(checkFn, 2, 5, 100)); + verify(checkFn, Mockito.times(2)).apply(2); + + //noinspection unchecked + reset(checkFn); + counter.set(0); + Awaitility.with().untilAsserted(() -> { + assertThat(Retry.check(checkFn, 2, 5, 100)).isTrue(); + verify(checkFn, Mockito.times(2)).apply(2); + }); + + //noinspection unchecked + reset(checkFn); + counter.set(0); + await().pollDelay(10, TimeUnit.MILLISECONDS) + .atMost(25, TimeUnit.MILLISECONDS) + .until(() -> Retry.check(checkFn, 1, 5, 100)); + verify(checkFn, Mockito.times(1)).apply(1); + } + + @Test + void invalidDelayShouldThrow() { + assertThatThrownBy(() -> Retry.check(value -> true, 1, 5, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The delay must be greater than or equal to zero (0)"); + } + + @Test + void invalidMaxAttemptsShouldThrow() { + assertThatThrownBy(() -> Retry.check(value -> true, 1, 0, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The maximum number of attempts must be greater than zero (0)"); + } + + private static void resolvesAtAttempt(final int targetAttempt, final int maxAttempts, final boolean shouldResolve) + throws ExecutionException, InterruptedException { + final AtomicInteger counter = new AtomicInteger(0); + + final Function checkFn = spyLambda(value -> { + final int val = counter.incrementAndGet(); + return val == value; + }); + + boolean status = Retry.check(checkFn, targetAttempt, maxAttempts, 0); + + assertThat(status).isEqualTo(shouldResolve); + verify(checkFn, Mockito.times(Math.min(targetAttempt, maxAttempts))).apply(targetAttempt); + } + + /** + * This method overcomes the issue with the original Mockito.spy when passing a lambda which fails with an error + * saying that the passed class is final. + */ + @SuppressWarnings("unchecked") + static P spyLambda(final P lambda) { + return (P) mock((Class) Function.class, delegatesTo(lambda)); + } +} diff --git a/platform-sdk/swirlds-cli/src/main/java/com/swirlds/cli/logging/LogProcessor.java b/platform-sdk/swirlds-cli/src/main/java/com/swirlds/cli/logging/LogProcessor.java index 2640db1997db..7e5bd2e19582 100644 --- a/platform-sdk/swirlds-cli/src/main/java/com/swirlds/cli/logging/LogProcessor.java +++ b/platform-sdk/swirlds-cli/src/main/java/com/swirlds/cli/logging/LogProcessor.java @@ -81,7 +81,7 @@ private static Map findLogFiles(@NonNull final Path inputDirectory final Path logFile = subdirectory.resolve("swirlds.log"); if (Files.exists(logFile)) { - final NodeId nodeId = new NodeId(Integer.parseInt(subdirectoryName.substring(4))); + final NodeId nodeId = NodeId.of(Integer.parseInt(subdirectoryName.substring(4))); logFilesByNode.put(nodeId, logFile); logger.info(LogMarker.CLI.getMarker(), "Found log file: {}", logFile); diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/crypto/Hash.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/crypto/Hash.java index 53190125905f..ddfc694e3da6 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/crypto/Hash.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/crypto/Hash.java @@ -48,19 +48,33 @@ public Hash() { } /** - * Same as {@link #Hash(byte[], DigestType)} but with an empty byte array. + * Same as {@link #Hash(Bytes, DigestType)} but with an empty byte array. */ public Hash(@NonNull final DigestType digestType) { - this(new byte[digestType.digestLength()], digestType); + this(Bytes.wrap(new byte[digestType.digestLength()]), digestType); } /** - * Same as {@link #Hash(byte[], DigestType)} but with a digest type ({@link DigestType#SHA_384}) + * Same as {@link #Hash(Bytes, DigestType)} but with a digest type ({@link DigestType#SHA_384}) and wrapping the byte array. */ public Hash(@NonNull final byte[] value) { + this(Bytes.wrap(value), DigestType.SHA_384); + } + + /** + * Same as {@link #Hash(Bytes, DigestType)} but with a digest type ({@link DigestType#SHA_384}) + */ + public Hash(@NonNull final Bytes value) { this(value, DigestType.SHA_384); } + /** + * Same as {@link #Hash(Bytes, DigestType)} but with wrapping the byte array. + */ + public Hash(@NonNull final byte[] value, @NonNull final DigestType digestType) { + this(Bytes.wrap(value), digestType); + } + /** * Instantiate a hash with data from a byte array with a specific digest type. This constructor assumes that the * array provided will not be modified after this call. @@ -70,16 +84,16 @@ public Hash(@NonNull final byte[] value) { * @param digestType * the digest type */ - public Hash(@NonNull final byte[] value, @NonNull final DigestType digestType) { + public Hash(@NonNull final Bytes value, @NonNull final DigestType digestType) { Objects.requireNonNull(value, "value"); Objects.requireNonNull(digestType, "digestType"); - if (value.length != digestType.digestLength()) { - throw new IllegalArgumentException("value: " + value.length); + if ((int) value.length() != digestType.digestLength()) { + throw new IllegalArgumentException("value: " + value.length()); } this.digestType = digestType; - this.bytes = Bytes.wrap(value); + this.bytes = value; } /** diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java index 219bd12d1a15..5f9822f6244b 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/io/streams/SerializableDataInputStream.java @@ -54,7 +54,7 @@ */ public class SerializableDataInputStream extends AugmentedDataInputStream { - private static final int PROTOCOL_VERSION = SERIALIZATION_PROTOCOL_VERSION; + private static final Set SUPPORTED_PROTOCOL_VERSIONS = Set.of(SERIALIZATION_PROTOCOL_VERSION); /** A stream used to read PBJ objects */ private final ReadableSequentialData readableSequentialData; @@ -70,16 +70,17 @@ public SerializableDataInputStream(final InputStream in) { } /** - * Reads the protocol version written by {@link SerializableDataOutputStream#writeProtocolVersion()} and saves it - * internally. From this point on, it will use this version number to deserialize. + * Reads the protocol version written by {@link SerializableDataOutputStream#writeProtocolVersion()} + * From this point on, it will use this version number to deserialize. * * @throws IOException thrown if any IO problems occur */ - public void readProtocolVersion() throws IOException { + public int readProtocolVersion() throws IOException { final int protocolVersion = readInt(); - if (protocolVersion != PROTOCOL_VERSION) { - throw new IOException("invalid protocol version " + protocolVersion); + if (!SUPPORTED_PROTOCOL_VERSIONS.contains(protocolVersion)) { + throw new IOException("Unsupported protocol version " + protocolVersion); } + return protocolVersion; } /** diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/noop/NoOpMetrics.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/noop/NoOpMetrics.java index bc46eb90980d..fd76deb4bd3e 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/noop/NoOpMetrics.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/metrics/noop/NoOpMetrics.java @@ -46,7 +46,7 @@ public class NoOpMetrics implements PlatformMetrics { @Override public NodeId getNodeId() { - return new NodeId(42L); + return NodeId.of(42L); } @Override diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeId.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeId.java index 85a835015771..9f067769d852 100644 --- a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeId.java +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeId.java @@ -48,11 +48,23 @@ public static final class ClassVersion { public static final long LOWEST_NODE_NUMBER = 0L; /** The first NodeId. */ - public static final NodeId FIRST_NODE_ID = new NodeId(LOWEST_NODE_NUMBER); + public static final NodeId FIRST_NODE_ID = NodeId.of(LOWEST_NODE_NUMBER); /** The ID number. */ private long id; + /** + * Return a potentially cached NodeId instance for a given node id value. + * The caller MUST NOT mutate the returned object even though the NodeId class is technically mutable. + * If the caller needs to mutate the instance, then it must use the regular NodeId(long) constructor instead. + * + * @param id a node id value + * @return a NodeId instance + */ + public static NodeId of(final long id) { + return NodeIdCache.getOrCreate(id); + } + /** * Constructs an empty NodeId objects, used in deserialization only. */ @@ -64,7 +76,7 @@ public NodeId() {} * @param id the ID number * @throws IllegalArgumentException if the ID number is negative */ - public NodeId(final long id) { + protected NodeId(final long id) { if (id < LOWEST_NODE_NUMBER) { throw new IllegalArgumentException("id must be non-negative"); } @@ -116,7 +128,7 @@ public NodeId getOffset(final long offset) { throw new IllegalArgumentException("the new NodeId, %d, must not be below the minimum value of %d." .formatted(newValue, LOWEST_NODE_NUMBER)); } - return new NodeId(newValue); + return NodeId.of(newValue); } /** @@ -168,7 +180,7 @@ public static NodeId deserializeLong(SerializableDataInputStream in, boolean all } throw new IOException("id must be non-negative"); } - return new NodeId(longValue); + return NodeId.of(longValue); } /** diff --git a/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeIdCache.java b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeIdCache.java new file mode 100644 index 000000000000..4c35b0de0276 --- /dev/null +++ b/platform-sdk/swirlds-common/src/main/java/com/swirlds/common/platform/NodeIdCache.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.common.platform; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A cache for NodeId objects. + * + * It's useful to support code that uses long values to identify nodes (e.g. the Roster) + * and needs to interact with code that uses NodeId objects for the same purpose + * as it helps prevent creating many duplicate NodeId instances for the same long id value. + */ +final class NodeIdCache { + private NodeIdCache() {} + + /** Minimum node id to cache. MUST BE non-negative. */ + private static final int MIN = 0; + /** Maximum node id to cache. MUST BE non-negative, >= MIN, and reasonably small. */ + private static final int MAX = 63; + + private static final NodeId[] CACHE = new NodeId[MAX - MIN + 1]; + + static { + for (int id = MIN; id <= MAX; id++) { + CACHE[id - MIN] = new NodeId(id); + } + } + + /** + * Fetch a NodeId value from the cache, or create a new NodeId object. + * The caller MUST NOT mutate the returned object even though the NodeId class is technically mutable. + * Note that whilst this class allows creation of NodeId with IDs beyond the boundary defined in this class, + * it does not cache such NodeId instances as these are expected to be used for testing purposes only. + * + * @param id a node id value + * @return a NodeId object + */ + @NonNull + static NodeId getOrCreate(final long id) { + if (id >= MIN && id <= MAX) { + return CACHE[(int) id - MIN]; + } + return new NodeId(id); + } +} diff --git a/platform-sdk/swirlds-common/src/main/java/module-info.java b/platform-sdk/swirlds-common/src/main/java/module-info.java index 73986c8a5141..83ab83db9ad0 100644 --- a/platform-sdk/swirlds-common/src/main/java/module-info.java +++ b/platform-sdk/swirlds-common/src/main/java/module-info.java @@ -77,7 +77,8 @@ exports com.swirlds.common.crypto.internal to com.swirlds.platform.core, com.swirlds.common.test.fixtures, - com.swirlds.common.testing; + com.swirlds.common.testing, + com.swirlds.platform.core.test.fixtures; exports com.swirlds.common.notification.internal to com.swirlds.common.testing; exports com.swirlds.common.crypto.engine to diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/context/internal/DefaultPlatformContextTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/context/internal/DefaultPlatformContextTest.java index bced50ec1cc2..ae8a2442111c 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/context/internal/DefaultPlatformContextTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/context/internal/DefaultPlatformContextTest.java @@ -41,7 +41,7 @@ class DefaultPlatformContextTest { void testNoNullServices() { // given final Configuration configuration = new TestConfigBuilder().getOrCreateConfig(); - final NodeId nodeId = new NodeId(3256733545L); + final NodeId nodeId = NodeId.of(3256733545L); final PlatformMetricsProvider metricsProvider = new DefaultMetricsProvider(configuration); metricsProvider.createGlobalMetrics(); final MerkleCryptography merkleCryptography = mock(MerkleCryptography.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/crypto/HashTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/crypto/HashTests.java index f1e8c38a8d4b..f1f81a31701f 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/crypto/HashTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/crypto/HashTests.java @@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.test.fixtures.RandomUtils; @@ -58,6 +59,7 @@ public void exceptionTests() { assertThrows(NullPointerException.class, () -> new Hash((DigestType) null)); assertThrows(NullPointerException.class, () -> new Hash((byte[]) null)); + assertThrows(NullPointerException.class, () -> new Hash((Bytes) null)); assertThrows(IllegalArgumentException.class, () -> new Hash((Hash) null)); assertThrows(NullPointerException.class, () -> new Hash(nonZeroHashValue, null)); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/LegacyCsvWriterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/LegacyCsvWriterTest.java index 6e94c9ce696d..14e881484967 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/LegacyCsvWriterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/LegacyCsvWriterTest.java @@ -55,7 +55,7 @@ class LegacyCsvWriterTest { - private static final NodeId NODE_ID = new NodeId(42L); + private static final NodeId NODE_ID = NodeId.of(42L); private Metrics metrics; private MetricsConfig metricsConfig; private Configuration configuration; diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/MetricKeyRegistrationTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/MetricKeyRegistrationTest.java index cf2b18ea2205..593429d1188a 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/MetricKeyRegistrationTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/MetricKeyRegistrationTest.java @@ -26,7 +26,7 @@ class MetricKeyRegistrationTest { - private static final NodeId NODE_ID = new NodeId(1L); + private static final NodeId NODE_ID = NodeId.of(1L); private static final String METRIC_KEY = calculateMetricKey("CaTeGoRy", "NaMe"); @Test @@ -98,7 +98,7 @@ void testAddingExistingPlatformMetricForOtherPlatform() { registry.register(NODE_ID, METRIC_KEY, Counter.class); // when - final boolean result = registry.register(new NodeId(111L), METRIC_KEY, Counter.class); + final boolean result = registry.register(NodeId.of(111L), METRIC_KEY, Counter.class); // then assertThat(result).isTrue(); @@ -187,7 +187,7 @@ void testAddingPlatformMetricWhenGlobalMetricWasDeleted() { @Test void testAddingGlobalMetricWhenOnlyOnePlatformMetricWasDeleted() { // given - final NodeId nodeId2 = new NodeId(111L); + final NodeId nodeId2 = NodeId.of(111L); final MetricKeyRegistry registry = new MetricKeyRegistry(); registry.register(NODE_ID, METRIC_KEY, Counter.class); registry.register(nodeId2, METRIC_KEY, Counter.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/SnapshotServiceTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/SnapshotServiceTest.java index 1e8c582b06e1..8312faa6ec00 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/SnapshotServiceTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/SnapshotServiceTest.java @@ -57,8 +57,8 @@ @ExtendWith(MockitoExtension.class) class SnapshotServiceTest { - private static final NodeId NODE_ID_1 = new NodeId(1L); - private static final NodeId NODE_ID_2 = new NodeId(2L); + private static final NodeId NODE_ID_1 = NodeId.of(1L); + private static final NodeId NODE_ID_2 = NodeId.of(2L); @Mock private SnapshotableMetric globalMetric; diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/BooleanAdapterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/BooleanAdapterTest.java index 0d279d64e03e..5ccc596ba1e2 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/BooleanAdapterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/BooleanAdapterTest.java @@ -141,7 +141,7 @@ void testUpdatePlatformMetric() { final BooleanAdapter adapter = new BooleanAdapter(registry, metric, PLATFORM); // when - adapter.update(Snapshot.of(metric), new NodeId(1L)); + adapter.update(Snapshot.of(metric), NodeId.of(1L)); // then assertThat(registry.getSampleValue(MAPPING_NAME, NODE_LABEL, NODE_VALUE)) @@ -155,7 +155,7 @@ void testUpdateWithNullParameters() { final PlatformFunctionGauge metric = new PlatformFunctionGauge<>(new FunctionGauge.Config<>(CATEGORY, NAME, Boolean.class, () -> true)); final BooleanAdapter adapter = new BooleanAdapter(registry, metric, PLATFORM); - final NodeId nodeId = new NodeId(1L); + final NodeId nodeId = NodeId.of(1L); // then assertThatThrownBy(() -> adapter.update(null, null)).isInstanceOf(NullPointerException.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/CounterAdapterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/CounterAdapterTest.java index a5e8ff39dbed..0b7ff130d155 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/CounterAdapterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/CounterAdapterTest.java @@ -138,7 +138,7 @@ void testUpdatePlatformMetric() { final CounterAdapter adapter = new CounterAdapter(registry, metric, PLATFORM); // when - adapter.update(Snapshot.of(metric), new NodeId(1L)); + adapter.update(Snapshot.of(metric), NodeId.of(1L)); // then assertThat(registry.getSampleValue(MAPPING_NAME + "_total", NODE_LABEL, NODE_VALUE)) @@ -151,7 +151,7 @@ void testUpdateWithNullParameters() { final CollectorRegistry registry = new CollectorRegistry(); final DefaultCounter metric = new DefaultCounter(new Counter.Config(CATEGORY, NAME)); final CounterAdapter adapter = new CounterAdapter(registry, metric, PLATFORM); - final NodeId nodeId = new NodeId(1L); + final NodeId nodeId = NodeId.of(1L); // then assertThatThrownBy(() -> adapter.update(null, null)).isInstanceOf(NullPointerException.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/DistributionAdapterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/DistributionAdapterTest.java index f4df277afeff..07bba2cb5878 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/DistributionAdapterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/DistributionAdapterTest.java @@ -155,7 +155,7 @@ void testUpdatePlatformMetric() { final DistributionAdapter adapter = new DistributionAdapter(registry, metric, PLATFORM); // when - adapter.update(Snapshot.of(metric), new NodeId(1L)); + adapter.update(Snapshot.of(metric), NodeId.of(1L)); // then assertThat(registry.getSampleValue(MAPPING_NAME, NODE_LABEL, new String[] {"1", "mean"})) @@ -175,7 +175,7 @@ void testUpdateWithNullParameters() { final PlatformRunningAverageMetric metric = new PlatformRunningAverageMetric(new RunningAverageMetric.Config(CATEGORY, NAME)); final DistributionAdapter adapter = new DistributionAdapter(registry, metric, PLATFORM); - final NodeId nodeId = new NodeId(1L); + final NodeId nodeId = NodeId.of(1L); // then assertThatThrownBy(() -> adapter.update(null, null)).isInstanceOf(NullPointerException.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/NumberAdapterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/NumberAdapterTest.java index b638bbccaeb5..43deebe5e410 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/NumberAdapterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/NumberAdapterTest.java @@ -141,7 +141,7 @@ void testUpdatePlatformMetric() { final NumberAdapter adapter = new NumberAdapter(registry, metric, PLATFORM); // when - adapter.update(Snapshot.of(metric), new NodeId(1L)); + adapter.update(Snapshot.of(metric), NodeId.of(1L)); // then assertThat(registry.getSampleValue(MAPPING_NAME, NODE_LABEL, NODE_VALUE)) @@ -154,7 +154,7 @@ void testUpdateWithNullParameters() { final CollectorRegistry registry = new CollectorRegistry(); final DefaultIntegerGauge metric = new DefaultIntegerGauge(new IntegerGauge.Config(CATEGORY, NAME)); final NumberAdapter adapter = new NumberAdapter(registry, metric, PLATFORM); - final NodeId nodeId = new NodeId(1L); + final NodeId nodeId = NodeId.of(1L); // then assertThatThrownBy(() -> adapter.update(null, null)).isInstanceOf(NullPointerException.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/PrometheusEndpointTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/PrometheusEndpointTest.java index 2bbfdd3c43fe..b9716792e504 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/PrometheusEndpointTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/PrometheusEndpointTest.java @@ -81,9 +81,9 @@ class PrometheusEndpointTest { private static final String CATEGORY = "CaTeGoRy"; private static final String NAME = "NaMe"; - private static final NodeId NODE_ID_1 = new NodeId(1L); + private static final NodeId NODE_ID_1 = NodeId.of(1L); private static final String LABEL_1 = NODE_ID_1.toString(); - private static final NodeId NODE_ID_2 = new NodeId(2L); + private static final NodeId NODE_ID_2 = NodeId.of(2L); private static final String LABEL_2 = NODE_ID_2.toString(); private static final InetSocketAddress ADDRESS = new InetSocketAddress(0); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/StringAdapterTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/StringAdapterTest.java index d719aaefc321..0c8f7d85b16f 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/StringAdapterTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/metrics/platform/prometheus/StringAdapterTest.java @@ -144,7 +144,7 @@ void testUpdatePlatformMetric() { final StringAdapter adapter = new StringAdapter(registry, metric, PLATFORM); // when - adapter.update(Snapshot.of(metric), new NodeId(1L)); + adapter.update(Snapshot.of(metric), NodeId.of(1L)); // then assertThat(registry.getSampleValue(MAPPING_NAME + "_info", NODE_LABEL, new String[] {"1", "Hello World"})) @@ -158,7 +158,7 @@ void testUpdateWithNullParameters() { final PlatformFunctionGauge metric = new PlatformFunctionGauge<>( new FunctionGauge.Config<>(CATEGORY, NAME, String.class, () -> "Hello World")); final StringAdapter adapter = new StringAdapter(registry, metric, PLATFORM); - final NodeId nodeId = new NodeId(1L); + final NodeId nodeId = NodeId.of(1L); // then assertThatThrownBy(() -> adapter.update(null, null)).isInstanceOf(NullPointerException.class); diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/ThreadTests.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/ThreadTests.java index 569a531aaf37..b5fcea854644 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/ThreadTests.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/ThreadTests.java @@ -284,7 +284,7 @@ void factoryTest() { Thread.currentThread().getContextClassLoader().getParent(); final ThreadFactory factory = new ThreadConfiguration(getStaticThreadManager()) - .setNodeId(new NodeId(1234L)) + .setNodeId(NodeId.of(1234L)) .setComponent("pool1") .setThreadName("thread1") .setDaemon(false) @@ -333,7 +333,7 @@ void namingTests() { .setRunnable(() -> {}) .setComponent("foo") .setThreadName("bar") - .setNodeId(new NodeId(1234L)) + .setNodeId(NodeId.of(1234L)) .build(); assertEquals("", thread3.getName(), "unexpected thread name"); @@ -341,8 +341,8 @@ void namingTests() { .setRunnable(() -> {}) .setComponent("foo") .setThreadName("bar") - .setNodeId(new NodeId(1234L)) - .setOtherNodeId(new NodeId(4321L)) + .setNodeId(NodeId.of(1234L)) + .setOtherNodeId(NodeId.of(4321L)) .build(); assertEquals("", thread4.getName(), "unexpected thread name"); @@ -350,8 +350,8 @@ void namingTests() { .setRunnable(() -> {}) .setComponent("foo") .setThreadName("bar") - .setNodeId(new NodeId(1234L)) - .setOtherNodeId(new NodeId(4321L)) + .setNodeId(NodeId.of(1234L)) + .setOtherNodeId(NodeId.of(4321L)) .buildFactory(); assertEquals("", factory.newThread(null).getName(), "unexpected thread name"); @@ -460,7 +460,7 @@ void configurationMutabilityTest() { assertThrows( MutabilityException.class, - () -> configuration0.setNodeId(new NodeId(0L)), + () -> configuration0.setNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -476,7 +476,7 @@ void configurationMutabilityTest() { "configuration should be immutable"); assertThrows( MutabilityException.class, - () -> configuration0.setOtherNodeId(new NodeId(0L)), + () -> configuration0.setOtherNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -510,7 +510,7 @@ void configurationMutabilityTest() { assertThrows( MutabilityException.class, - () -> configuration1.setNodeId(new NodeId(0L)), + () -> configuration1.setNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -526,7 +526,7 @@ void configurationMutabilityTest() { "configuration should be immutable"); assertThrows( MutabilityException.class, - () -> configuration1.setOtherNodeId(new NodeId(0L)), + () -> configuration1.setOtherNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -560,7 +560,7 @@ void configurationMutabilityTest() { assertThrows( MutabilityException.class, - () -> configuration2.setNodeId(new NodeId(0L)), + () -> configuration2.setNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -576,7 +576,7 @@ void configurationMutabilityTest() { "configuration should be immutable"); assertThrows( MutabilityException.class, - () -> configuration2.setOtherNodeId(new NodeId(0L)), + () -> configuration2.setOtherNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -648,7 +648,7 @@ void copyTest() { final Runnable runnable = () -> {}; final ThreadConfiguration configuration = new ThreadConfiguration(getStaticThreadManager()) - .setNodeId(new NodeId(1234L)) + .setNodeId(NodeId.of(1234L)) .setComponent("component") .setThreadName("name") .setThreadGroup(group) diff --git a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/framework/internal/AbstractQueueThreadConfigurationTest.java b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/framework/internal/AbstractQueueThreadConfigurationTest.java index db67a436c1f5..0ff4d8b2cca8 100644 --- a/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/framework/internal/AbstractQueueThreadConfigurationTest.java +++ b/platform-sdk/swirlds-common/src/test/java/com/swirlds/common/threading/framework/internal/AbstractQueueThreadConfigurationTest.java @@ -72,7 +72,7 @@ public DummyQueueThreadConfiguration copy() { } } - static final NodeId NODE_ID = new NodeId(1L); + static final NodeId NODE_ID = NodeId.of(1L); static final String THREAD_POOL_NAME = "myThreadPool"; static final String THREAD_NAME = "myThread"; static final int MAX_BUFFER_SIZE = 50; diff --git a/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/metrics/platform/DefaultMetricsTest.java b/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/metrics/platform/DefaultMetricsTest.java index 8f59d7a42187..0d51dcf08af3 100644 --- a/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/metrics/platform/DefaultMetricsTest.java +++ b/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/metrics/platform/DefaultMetricsTest.java @@ -55,7 +55,7 @@ @ExtendWith(MockitoExtension.class) class DefaultMetricsTest { - private static final NodeId NODE_ID = new NodeId(42L); + private static final NodeId NODE_ID = NodeId.of(42L); private static final String CATEGORY_1 = "CaTeGoRy1"; private static final String CATEGORY_1a = "CaTeGoRy1.a"; private static final String CATEGORY_1b = "CaTeGoRy1.b"; diff --git a/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/threading/StoppableThreadTests.java b/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/threading/StoppableThreadTests.java index de08276829f9..0a7d1186a004 100644 --- a/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/threading/StoppableThreadTests.java +++ b/platform-sdk/swirlds-common/src/timingSensitive/java/com/swirlds/common/threading/StoppableThreadTests.java @@ -555,7 +555,7 @@ void configurationMutabilityTest() { assertThrows( MutabilityException.class, - () -> configuration.setNodeId(new NodeId(0L)), + () -> configuration.setNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, @@ -571,7 +571,7 @@ void configurationMutabilityTest() { "configuration should be immutable"); assertThrows( MutabilityException.class, - () -> configuration.setOtherNodeId(new NodeId(0L)), + () -> configuration.setOtherNodeId(NodeId.of(0L)), "configuration should be immutable"); assertThrows( MutabilityException.class, diff --git a/platform-sdk/swirlds-config-extensions/src/main/java/com/swirlds/config/extensions/sources/AbstractFileConfigSource.java b/platform-sdk/swirlds-config-extensions/src/main/java/com/swirlds/config/extensions/sources/AbstractFileConfigSource.java index 79253dfdfb51..7a086ade5b69 100644 --- a/platform-sdk/swirlds-config-extensions/src/main/java/com/swirlds/config/extensions/sources/AbstractFileConfigSource.java +++ b/platform-sdk/swirlds-config-extensions/src/main/java/com/swirlds/config/extensions/sources/AbstractFileConfigSource.java @@ -57,6 +57,7 @@ protected AbstractFileConfigSource(@NonNull final Path filePath, final int ordin this.internalProperties = new HashMap<>(); this.filePath = Objects.requireNonNull(filePath, "filePath can not be null"); this.ordinal = ordinal; + try (BufferedReader reader = getReader()) { final Properties loadedProperties = new Properties(); loadedProperties.load(reader); diff --git a/platform-sdk/swirlds-config-impl/build.gradle.kts b/platform-sdk/swirlds-config-impl/build.gradle.kts index f296d4524572..8b88116a6706 100644 --- a/platform-sdk/swirlds-config-impl/build.gradle.kts +++ b/platform-sdk/swirlds-config-impl/build.gradle.kts @@ -30,6 +30,7 @@ testModuleInfo { } jmhModuleInfo { + requires("com.swirlds.base") requires("com.swirlds.config.api") requires("com.swirlds.config.extensions") requires("jmh.core") diff --git a/platform-sdk/swirlds-config-impl/src/jmh/java/com/swirlds/config/benchmark/ConfigBenchmark.java b/platform-sdk/swirlds-config-impl/src/jmh/java/com/swirlds/config/benchmark/ConfigBenchmark.java index ca02aef458c5..c47b081b7c0d 100644 --- a/platform-sdk/swirlds-config-impl/src/jmh/java/com/swirlds/config/benchmark/ConfigBenchmark.java +++ b/platform-sdk/swirlds-config-impl/src/jmh/java/com/swirlds/config/benchmark/ConfigBenchmark.java @@ -16,10 +16,12 @@ package com.swirlds.config.benchmark; +import com.swirlds.base.utility.FileSystemUtils; import com.swirlds.config.api.ConfigData; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.extensions.sources.PropertyFileConfigSource; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -66,6 +68,11 @@ public void setup() throws IOException, URISyntaxException { ConfigBenchmark.class.getResource("app.properties").toURI(); fileSystem = FileSystems.newFileSystem(configUri, Collections.emptyMap()); final Path configFile = Paths.get(configUri); + + if (!FileSystemUtils.waitForPathPresence(configFile)) { + throw new FileNotFoundException("File not found: " + configFile); + } + configuration = ConfigurationBuilder.create() .withConfigDataType(AppConfig.class) .withSource(new PropertyFileConfigSource(configFile)) diff --git a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/DefaultLoggingSystem.java b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/DefaultLoggingSystem.java index 31ddb8200e31..d1ae237d4a6a 100644 --- a/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/DefaultLoggingSystem.java +++ b/platform-sdk/swirlds-logging/src/main/java/com/swirlds/logging/api/internal/DefaultLoggingSystem.java @@ -17,6 +17,7 @@ package com.swirlds.logging.api.internal; import com.swirlds.base.internal.BaseExecutorFactory; +import com.swirlds.base.utility.FileSystemUtils; import com.swirlds.config.api.Configuration; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.config.api.source.ConfigSource; @@ -31,6 +32,7 @@ import com.swirlds.logging.api.internal.configuration.MarkerStateConverter; import com.swirlds.logging.api.internal.emergency.EmergencyLoggerImpl; import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; import java.time.Duration; @@ -121,6 +123,10 @@ private static Configuration createConfiguration() { final Path configFilePath = Optional.ofNullable(logConfigPath).map(Path::of).orElseGet(() -> Path.of("log.properties")); try { + if (!FileSystemUtils.waitForPathPresence(configFilePath)) { + throw new FileNotFoundException("File not found: " + configFilePath); + } + final ConfigSource configSource = new PropertyFileConfigSource(configFilePath); return ConfigurationBuilder.create() .withSource(configSource) diff --git a/platform-sdk/swirlds-merkle/src/testFixtures/java/com/swirlds/merkle/test/fixtures/map/lifecycle/SaveExpectedMapHandler.java b/platform-sdk/swirlds-merkle/src/testFixtures/java/com/swirlds/merkle/test/fixtures/map/lifecycle/SaveExpectedMapHandler.java index 8c9495e71294..9fc1cd5ae51d 100644 --- a/platform-sdk/swirlds-merkle/src/testFixtures/java/com/swirlds/merkle/test/fixtures/map/lifecycle/SaveExpectedMapHandler.java +++ b/platform-sdk/swirlds-merkle/src/testFixtures/java/com/swirlds/merkle/test/fixtures/map/lifecycle/SaveExpectedMapHandler.java @@ -236,7 +236,7 @@ public Hash deserialize(final JsonParser p, final DeserializationContext ctxt) t final String digestString = node.get("digestType").asText(); final DigestType digestType = DigestType.valueOf(digestString); final String hex = node.get("bytes").asText(); - return new Hash(Bytes.fromHex(hex).toByteArray(), digestType); + return new Hash(Bytes.fromHex(hex), digestType); } } } diff --git a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileReader.java b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileReader.java index ba8a29c504a1..6ed15c0908af 100644 --- a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileReader.java +++ b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileReader.java @@ -423,13 +423,20 @@ private BufferedData read(final long byteOffsetInFile) throws IOException { readBB.clear(); readBB.limit(PRE_READ_BUF_SIZE); // No need to read more than that for a header int bytesRead = MerkleDbFileUtils.completelyRead(fileChannel, readBB, byteOffsetInFile); - assert !isFileCompleted() || (bytesRead == Math.min(PRE_READ_BUF_SIZE, getSize() - byteOffsetInFile)); + if (isFileCompleted() && (bytesRead != Math.min(PRE_READ_BUF_SIZE, getSize() - byteOffsetInFile))) { + throw new IOException("Failed to read all bytes: toread=" + + Math.min(PRE_READ_BUF_SIZE, getSize() - byteOffsetInFile) + " read=" + bytesRead + + " file=" + getIndex() + " off=" + byteOffsetInFile); + } // Then read the tag and size from the read buffer, since it's wrapped over the byte buffer readBuf.reset(); final int tag = readBuf.getVarInt(0, false); // tag - assert tag - == ((FIELD_DATAFILE_ITEMS.number() << TAG_FIELD_OFFSET) - | ProtoConstants.WIRE_TYPE_DELIMITED.ordinal()); + if (tag + != ((FIELD_DATAFILE_ITEMS.number() << TAG_FIELD_OFFSET) + | ProtoConstants.WIRE_TYPE_DELIMITED.ordinal())) { + throw new IOException( + "Unknown data item tag: tag=" + tag + " file=" + getIndex() + " off=" + byteOffsetInFile); + } final int sizeOfTag = ProtoWriterTools.sizeOfUnsignedVarInt32(tag); final int size = readBuf.getVarInt(sizeOfTag, false); final int sizeOfSize = ProtoWriterTools.sizeOfUnsignedVarInt32(size); @@ -451,7 +458,10 @@ private BufferedData read(final long byteOffsetInFile) throws IOException { } bytesRead = MerkleDbFileUtils.completelyRead( fileChannel, readBB, byteOffsetInFile + sizeOfTag + sizeOfSize); - assert bytesRead == size : "Failed to read all data item bytes"; + if (bytesRead != size) { + throw new IOException("Failed to read all bytes: toread=" + size + " read=" + bytesRead + " file=" + + getIndex() + " off=" + byteOffsetInFile); + } readBuf.position(0); readBuf.limit(bytesRead); return readBuf; diff --git a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileWriter.java b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileWriter.java index 1cdc35e0cda7..3990d6ec74d4 100644 --- a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileWriter.java +++ b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/DataFileWriter.java @@ -164,15 +164,21 @@ public DataFileMetadata getMetadata() { public synchronized long storeDataItem(final BufferedData dataItem) throws IOException { // find offset for the start of this new data item, we assume we always write data in a // whole number of blocks - final long currentWritingMmapPos = writingPbjData.position(); + long currentWritingMmapPos = writingPbjData.position(); final long byteOffset = mmapPositionInFile + currentWritingMmapPos; // write serialized data final int size = Math.toIntExact(dataItem.remaining()); - if (writingPbjData.remaining() < ProtoWriterTools.sizeOfDelimited(FIELD_DATAFILE_ITEMS, size)) { + final int sizeToWrite = ProtoWriterTools.sizeOfDelimited(FIELD_DATAFILE_ITEMS, size); + if (writingPbjData.remaining() < sizeToWrite) { moveWritingBuffer(byteOffset); + currentWritingMmapPos = 0; } try { ProtoWriterTools.writeDelimited(writingPbjData, FIELD_DATAFILE_ITEMS, size, o -> o.writeBytes(dataItem)); + if (writingPbjData.position() != currentWritingMmapPos + sizeToWrite) { + throw new IOException("Estimated size / written bytes mismatch: expected=" + sizeToWrite + " written=" + + (writingPbjData.position() - currentWritingMmapPos)); + } } catch (final BufferOverflowException e) { // Buffer overflow here means the mapped buffer is smaller than even a single data item throw new IOException(DataFileCommon.ERROR_DATAITEM_TOO_LARGE, e); diff --git a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/Bucket.java b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/Bucket.java index 7205de4e11d2..46ccaff9bc9d 100644 --- a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/Bucket.java +++ b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/Bucket.java @@ -320,8 +320,9 @@ public void readFrom(final ReadableSequentialData in) { } else { logger.error( MERKLE_DB.getMarker(), - "Cannot read bucket: in={} off={} bd.pos={} bd.lim={} bd.data={}", + "Cannot read bucket: in={} in.pos={} off={} bd.pos={} bd.lim={} bd.data={}", in, + in.position(), fieldOffset, bucketData.position(), bucketData.limit(), diff --git a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/HalfDiskHashMap.java b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/HalfDiskHashMap.java index 7e9b507ec2ea..b9869b45562b 100644 --- a/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/HalfDiskHashMap.java +++ b/platform-sdk/swirlds-merkledb/src/main/java/com/swirlds/merkledb/files/hashmap/HalfDiskHashMap.java @@ -457,7 +457,6 @@ public DataFileReader endWriting() throws IOException { if (Thread.currentThread() != writingThread) { throw new IllegalStateException("Tried calling endWriting with different thread to startWriting()"); } - writingThread = null; final int size = oneTransactionsData.size(); logger.info( MERKLE_DB.getMarker(), @@ -466,8 +465,8 @@ public DataFileReader endWriting() throws IOException { size, oneTransactionsData.stream().mapToLong(BucketMutation::size).sum()); final DataFileReader dataFileReader; - if (size > 0) { - try { + try { + if (size > 0) { final Iterator> it = oneTransactionsData.keyValuesView().iterator(); fileCollection.startWriting(); @@ -490,14 +489,15 @@ public DataFileReader endWriting() throws IOException { dataFileReader = fileCollection.endWriting(0, numOfBuckets); // we have updated all indexes so the data file can now be included in merges dataFileReader.setFileCompleted(); - } catch (final Exception z) { - throw new RuntimeException("Exception in HDHM.endWriting()", z); + } else { + dataFileReader = null; } - } else { - dataFileReader = null; + } catch (final Exception z) { + throw new RuntimeException("Exception in HDHM.endWriting()", z); + } finally { + writingThread = null; + oneTransactionsData = null; } - // clear put cache - oneTransactionsData = null; return dataFileReader; } @@ -615,6 +615,7 @@ protected boolean exec() { createAndScheduleStoreTask(bucket); return true; } catch (final IOException z) { + logger.error(MERKLE_DB.getMarker(), "Failed to read / update bucket " + bucketIndex, z); exceptionOccurred.set(z); completeExceptionally(z); // Make sure the writing thread is resumed diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/CommandLineArgs.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/CommandLineArgs.java index 7e73e744271d..b673ad7f9fac 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/CommandLineArgs.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/CommandLineArgs.java @@ -49,7 +49,7 @@ public static CommandLineArgs parse(@NonNull final String[] args) { currentOption = arg; } else if (currentOption != null) { try { - localNodesToStart.add(new NodeId(Integer.parseInt(arg))); + localNodesToStart.add(NodeId.of(Integer.parseInt(arg))); } catch (final NumberFormatException ex) { // Intentionally suppress the NumberFormatException } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java index e72df9a6ad31..a0ab593d21bb 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/SwirldsPlatform.java @@ -53,7 +53,6 @@ import com.swirlds.platform.event.preconsensus.PcesFileTracker; import com.swirlds.platform.event.preconsensus.PcesReplayer; import com.swirlds.platform.eventhandling.EventConfig; -import com.swirlds.platform.gossip.SyncGossip; import com.swirlds.platform.metrics.RuntimeMetrics; import com.swirlds.platform.pool.TransactionPoolNexus; import com.swirlds.platform.publisher.DefaultPlatformPublisher; @@ -89,7 +88,6 @@ import java.time.Duration; import java.util.List; import java.util.Objects; -import java.util.function.LongSupplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -256,20 +254,6 @@ public SwirldsPlatform(@NonNull final PlatformComponentBuilder builder) { final EventWindowManager eventWindowManager = new DefaultEventWindowManager(); - final boolean useOldStyleIntakeQueue = platformContext - .getConfiguration() - .getConfigData(EventConfig.class) - .useOldStyleIntakeQueue(); - - final LongSupplier intakeQueueSizeSupplier; - if (useOldStyleIntakeQueue) { - final SyncGossip gossip = (SyncGossip) builder.buildGossip(); - intakeQueueSizeSupplier = () -> gossip.getOldStyleIntakeQueueSize(); - } else { - intakeQueueSizeSupplier = platformWiring.getIntakeQueueSizeSupplier(); - } - - blocks.intakeQueueSizeSupplierSupplier().set(intakeQueueSizeSupplier); blocks.isInFreezePeriodReference().set(swirldStateManager::isInFreezePeriod); final BirthRoundMigrationShim birthRoundMigrationShim = buildBirthRoundMigrationShim(initialState, ancientMode); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java index 14929e4471e8..d96265f11f47 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuilder.java @@ -461,7 +461,6 @@ public PlatformComponentBuilder buildComponentBuilder() { new TransactionPoolNexus(platformContext), new AtomicReference<>(), new AtomicReference<>(), - new AtomicReference<>(), initialPcesFiles, issScratchpad, NotificationEngine.buildEngine(getStaticThreadManager()), diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java index bc74e1d6c31f..916e4086b13e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformBuildingBlocks.java @@ -43,7 +43,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.LongSupplier; import java.util.function.Predicate; import java.util.function.Supplier; @@ -69,8 +68,6 @@ * into gossip event storage, per peer * @param randomBuilder a builder for creating random number generators * @param transactionPoolNexus provides transactions to be added to new events - * @param intakeQueueSizeSupplierSupplier supplies a method which supplies the size of the intake queue. This - * hack is required due to the lack of a platform health monitor. * @param isInFreezePeriodReference a reference to a predicate that determines if a timestamp is in the * freeze period, this can be deleted as soon as the CES is retired. * @param latestImmutableStateProviderReference a reference to a method that supplies the latest immutable state. Input @@ -111,7 +108,6 @@ public record PlatformBuildingBlocks( @NonNull IntakeEventCounter intakeEventCounter, @NonNull RandomBuilder randomBuilder, @NonNull TransactionPoolNexus transactionPoolNexus, - @NonNull AtomicReference intakeQueueSizeSupplierSupplier, @NonNull AtomicReference> isInFreezePeriodReference, @NonNull AtomicReference> latestImmutableStateProviderReference, @NonNull PcesFileTracker initialPcesFiles, @@ -137,7 +133,6 @@ public record PlatformBuildingBlocks( requireNonNull(intakeEventCounter); requireNonNull(randomBuilder); requireNonNull(transactionPoolNexus); - requireNonNull(intakeQueueSizeSupplierSupplier); requireNonNull(isInFreezePeriodReference); requireNonNull(latestImmutableStateProviderReference); requireNonNull(initialPcesFiles); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java index 88e9f3cd898e..fd1264a20ee0 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/builder/PlatformComponentBuilder.java @@ -505,10 +505,7 @@ public EventCreationManager buildEventCreationManager() { blocks.transactionPoolNexus()); eventCreationManager = new DefaultEventCreationManager( - blocks.platformContext(), - blocks.transactionPoolNexus(), - blocks.intakeQueueSizeSupplierSupplier().get(), - eventCreator); + blocks.platformContext(), blocks.transactionPoolNexus(), eventCreator); } return eventCreationManager; } @@ -1043,7 +1040,6 @@ public Gossip buildGossip() { blocks.initialAddressBook(), blocks.selfId(), blocks.appVersion(), - () -> blocks.intakeQueueSizeSupplierSupplier().get().getAsLong(), blocks.swirldStateManager(), () -> blocks.getLatestCompleteStateReference().get().get(), x -> blocks.statusActionSubmitterReference().get().submitStatusAction(x), diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/BrowseCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/BrowseCommand.java index e23f0023961f..5cd861c4798c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/BrowseCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/BrowseCommand.java @@ -57,7 +57,7 @@ public class BrowseCommand extends AbstractCommand { + "specified by repeating the parameter `-l #1 -l #2 -l #3`.") private void setLocalNodes(@NonNull final Long... localNodes) { for (final Long nodeId : localNodes) { - this.localNodes.add(new NodeId(nodeId)); + this.localNodes.add(NodeId.of(nodeId)); } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/EventStreamRecoverCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/EventStreamRecoverCommand.java index 83de30215324..880c9de2b6e1 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/EventStreamRecoverCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/EventStreamRecoverCommand.java @@ -93,7 +93,7 @@ private void setBootstrapSignedState(final Path bootstrapSignedState) { description = "The ID of the node that is being used to recover the state. " + "This node's keys should be available locally.") private void setSelfId(final long selfId) { - this.selfId = new NodeId(selfId); + this.selfId = NodeId.of(selfId); } @CommandLine.Option( diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/PcesRecoveryCommand.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/PcesRecoveryCommand.java index 72da2705b2b2..1b88b3602009 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/PcesRecoveryCommand.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/cli/PcesRecoveryCommand.java @@ -37,7 +37,7 @@ public class PcesRecoveryCommand extends AbstractCommand { @Override public Integer call() throws IOException, InterruptedException { - Browser.launch(new CommandLineArgs(Set.of(new NodeId(nodeId))), true); + Browser.launch(new CommandLineArgs(Set.of(NodeId.of(nodeId))), true); return null; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoader.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoader.java index b5e82aba0765..2ccebe1a11ed 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoader.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoader.java @@ -16,6 +16,7 @@ package com.swirlds.platform.config.legacy; +import static com.swirlds.base.utility.FileSystemUtils.waitForPathPresence; import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; import com.swirlds.common.utility.CommonUtils; @@ -27,7 +28,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.text.ParseException; import java.util.Arrays; @@ -41,7 +41,7 @@ * Loader that load all properties form the config.txt file * * @deprecated will be replaced by the {@link com.swirlds.config.api.Configuration} API in near future once the - * onfig.txt has been migrated to the regular config API. If you need to use this class please try to do as less static + * config.txt has been migrated to the regular config API. If you need to use this class please try to do as less static * access as possible. */ @Deprecated(forRemoval = true) @@ -66,14 +66,14 @@ public final class LegacyConfigPropertiesLoader { private LegacyConfigPropertiesLoader() {} /** - * @throws NullPointerException in case {@code configPath} parameter is {@code null} + * @throws NullPointerException in case {@code configPath} parameter is {@code null} * @throws ConfigurationException in case {@code configPath} cannot be found in the system */ public static LegacyConfigProperties loadConfigFile(@NonNull final Path configPath) throws ConfigurationException { Objects.requireNonNull(configPath, "configPath must not be null"); // Load config.txt file, parse application jar file name, main class name, address book, and parameters - if (!Files.exists(configPath)) { + if (!waitForPathPresence(configPath)) { throw new ConfigurationException( "ERROR: Configuration file not found: %s".formatted(configPath.toString())); } @@ -125,6 +125,11 @@ public static LegacyConfigProperties loadConfigFile(@NonNull final Path configPa onError(ERROR_ADDRESS_NOT_ENOUGH_PARAMETERS); } } + case "nextnodeid" -> { + // As of release 0.56, nextNodeId is not used and ignored. + // CI/CD pipelines need to be updated to remove this field from files. + // Future Work: remove this case when nextNodeId is no longer present in CI/CD pipelines. + } default -> onError(ERROR_PROPERTY_NOT_KNOWN.formatted(pars[0])); } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/DefaultEventCreationManager.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/DefaultEventCreationManager.java index 803ad04678b6..c71143fc1b31 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/DefaultEventCreationManager.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/DefaultEventCreationManager.java @@ -27,7 +27,6 @@ import com.swirlds.platform.consensus.EventWindow; import com.swirlds.platform.event.PlatformEvent; import com.swirlds.platform.event.creation.rules.AggregateEventCreationRules; -import com.swirlds.platform.event.creation.rules.BackpressureRule; import com.swirlds.platform.event.creation.rules.EventCreationRule; import com.swirlds.platform.event.creation.rules.MaximumRateRule; import com.swirlds.platform.event.creation.rules.PlatformHealthRule; @@ -41,7 +40,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.LongSupplier; /** * Default implementation of the {@link EventCreationManager}. @@ -78,28 +76,21 @@ public class DefaultEventCreationManager implements EventCreationManager { * * @param platformContext the platform context * @param transactionPoolNexus provides transactions to be added to new events - * @param eventIntakeQueueSize supplies the size of the event intake queue * @param creator creates events */ public DefaultEventCreationManager( @NonNull final PlatformContext platformContext, @NonNull final TransactionPoolNexus transactionPoolNexus, - @NonNull final LongSupplier eventIntakeQueueSize, @NonNull final EventCreator creator) { this.creator = Objects.requireNonNull(creator); final EventCreationConfig config = platformContext.getConfiguration().getConfigData(EventCreationConfig.class); - final boolean useLegacyBackpressure = config.useLegacyBackpressure(); final List rules = new ArrayList<>(); rules.add(new MaximumRateRule(platformContext)); rules.add(new PlatformStatusRule(this::getPlatformStatus, transactionPoolNexus)); - if (useLegacyBackpressure) { - rules.add(new BackpressureRule(platformContext, eventIntakeQueueSize)); - } else { - rules.add(new PlatformHealthRule(config.maximumPermissibleUnhealthyDuration(), this::getUnhealthyDuration)); - } + rules.add(new PlatformHealthRule(config.maximumPermissibleUnhealthyDuration(), this::getUnhealthyDuration)); this.eventCreationRules = AggregateEventCreationRules.of(rules); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/EventCreationConfig.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/EventCreationConfig.java index 5457c70203ac..3c79dbac85b6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/EventCreationConfig.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/event/creation/EventCreationConfig.java @@ -42,8 +42,6 @@ * used to compute selfishness scores. * @param eventIntakeThrottle when the size of the event intake queue equals or exceeds this value, do * not permit the creation of new self events. - * @param useLegacyBackpressure whether to use the legacy backpressure (i.e. where we look at the size of - * the first queue in intake) * @param maximumPermissibleUnhealthyDuration the maximum amount of time that the system can be unhealthy before event * creation stops */ @@ -54,5 +52,4 @@ public record EventCreationConfig( @ConfigProperty(defaultValue = "10") double antiSelfishnessFactor, @ConfigProperty(defaultValue = "10") int tipsetSnapshotHistorySize, @ConfigProperty(defaultValue = "1024") int eventIntakeThrottle, - @ConfigProperty(defaultValue = "false") boolean useLegacyBackpressure, @ConfigProperty(defaultValue = "1s") Duration maximumPermissibleUnhealthyDuration) {} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/EventConfig.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/EventConfig.java index 29fef47f9322..8ee1c7899300 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/EventConfig.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/eventhandling/EventConfig.java @@ -24,7 +24,6 @@ /** * Configuration for event handling inside the platform. * - * @param eventIntakeQueueThrottleSize The value for the event intake queue at which the node should stop syncing * @param eventStreamQueueCapacity capacity of the blockingQueue from which we take events and write to * EventStream files * @param eventsLogPeriod period of generating eventStream file @@ -33,17 +32,14 @@ * @param useBirthRoundAncientThreshold if true, use birth rounds instead of generations for deciding if an event is * ancient or not. Once this setting has been enabled on a network, it can * never be disabled again (migration pathway is one-way). - * @param useOldStyleIntakeQueue if true then use an old style queue between gossip and the intake queue */ @ConfigData("event") public record EventConfig( - @ConfigProperty(defaultValue = "1000") int eventIntakeQueueThrottleSize, @ConfigProperty(defaultValue = "5000") int eventStreamQueueCapacity, @ConfigProperty(defaultValue = "5") long eventsLogPeriod, @ConfigProperty(defaultValue = "/opt/hgcapp/eventsStreams") String eventsLogDir, @ConfigProperty(defaultValue = "true") boolean enableEventStreaming, - @ConfigProperty(defaultValue = "false") boolean useBirthRoundAncientThreshold, - @ConfigProperty(defaultValue = "false") boolean useOldStyleIntakeQueue) { + @ConfigProperty(defaultValue = "false") boolean useBirthRoundAncientThreshold) { /** * @return the {@link AncientMode} based on useBirthRoundAncientThreshold diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java index c4f169ed0e65..1b2e64aeda0b 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/SyncGossip.java @@ -22,10 +22,7 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.merkle.synchronization.config.ReconnectConfig; import com.swirlds.common.platform.NodeId; -import com.swirlds.common.threading.framework.QueueThread; import com.swirlds.common.threading.framework.StoppableThread; -import com.swirlds.common.threading.framework.config.QueueThreadConfiguration; -import com.swirlds.common.threading.framework.config.QueueThreadMetricsConfiguration; import com.swirlds.common.threading.framework.config.StoppableThreadConfiguration; import com.swirlds.common.threading.manager.ThreadManager; import com.swirlds.common.threading.pool.CachedPoolParallelExecutor; @@ -40,7 +37,6 @@ import com.swirlds.platform.consensus.EventWindow; import com.swirlds.platform.crypto.KeysAndCerts; import com.swirlds.platform.event.PlatformEvent; -import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.gossip.permits.SyncPermitProvider; import com.swirlds.platform.gossip.shadowgraph.Shadowgraph; import com.swirlds.platform.gossip.shadowgraph.ShadowgraphSynchronizer; @@ -104,7 +100,6 @@ public class SyncGossip implements ConnectionTracker, Gossip { private boolean started = false; - private final PlatformContext platformContext; private final ReconnectController reconnectController; private final AtomicBoolean gossipHalted = new AtomicBoolean(false); @@ -140,13 +135,6 @@ public class SyncGossip implements ConnectionTracker, Gossip { private Consumer receivedEventHandler; - /** - * The old style intake queue (if enabled), null if not enabled. - */ - private QueueThread oldStyleIntakeQueue; - - private final ThreadManager threadManager; - /** * Builds the gossip engine, depending on which flavor is requested in the configuration. * @@ -156,7 +144,6 @@ public class SyncGossip implements ConnectionTracker, Gossip { * @param addressBook the current address book * @param selfId this node's ID * @param appVersion the version of the app - * @param intakeQueueSizeSupplier a supplier for the size of the event intake queue * @param swirldStateManager manages the mutable state * @param latestCompleteState holds the latest signed state that has enough signatures to be verifiable * @param statusActionSubmitter for submitting updates to the platform status manager @@ -171,7 +158,6 @@ public SyncGossip( @NonNull final AddressBook addressBook, @NonNull final NodeId selfId, @NonNull final SoftwareVersion appVersion, - @NonNull final LongSupplier intakeQueueSizeSupplier, @NonNull final SwirldStateManager swirldStateManager, @NonNull final Supplier latestCompleteState, @NonNull final StatusActionSubmitter statusActionSubmitter, @@ -179,10 +165,6 @@ public SyncGossip( @NonNull final Runnable clearAllPipelinesForReconnect, @NonNull final IntakeEventCounter intakeEventCounter) { - this.platformContext = Objects.requireNonNull(platformContext); - - this.threadManager = Objects.requireNonNull(threadManager); - shadowgraph = new Shadowgraph(platformContext, addressBook, intakeEventCounter); this.statusActionSubmitter = Objects.requireNonNull(statusActionSubmitter); @@ -227,11 +209,7 @@ public SyncGossip( () -> getReconnectController().start(), platformContext.getConfiguration().getConfigData(ReconnectConfig.class)); - syncManager = new SyncManagerImpl( - platformContext, - intakeQueueSizeSupplier, - fallenBehindManager, - platformContext.getConfiguration().getConfigData(EventConfig.class)); + syncManager = new SyncManagerImpl(platformContext, fallenBehindManager); final ReconnectConfig reconnectConfig = platformContext.getConfiguration().getConfigData(ReconnectConfig.class); @@ -274,8 +252,6 @@ public SyncGossip( stateConfig); this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); - final EventConfig eventConfig = platformContext.getConfiguration().getConfigData(EventConfig.class); - syncConfig = platformContext.getConfiguration().getConfigData(SyncConfig.class); final ParallelExecutor shadowgraphExecutor = new CachedPoolParallelExecutor(threadManager, "node-sync"); @@ -311,14 +287,12 @@ public SyncGossip( threadManager, selfId, appVersion, - intakeQueueSizeSupplier, latestCompleteState, syncMetrics, currentPlatformStatus::get, hangingThreadDuration, protocolConfig, - reconnectConfig, - eventConfig); + reconnectConfig); thingsToStart.add(() -> syncProtocolThreads.forEach(StoppableThread::start)); } @@ -328,14 +302,12 @@ private void buildSyncProtocolThreads( final ThreadManager threadManager, final NodeId selfId, final SoftwareVersion appVersion, - final LongSupplier intakeQueueSizeSupplier, final Supplier getLatestCompleteState, final SyncMetrics syncMetrics, final Supplier platformStatusSupplier, final Duration hangingThreadDuration, final ProtocolConfig protocolConfig, - final ReconnectConfig reconnectConfig, - final EventConfig eventConfig) { + final ReconnectConfig reconnectConfig) { final ProtocolFactory syncProtocolFactory = new SyncProtocolFactory( platformContext, @@ -344,7 +316,6 @@ private void buildSyncProtocolThreads( syncPermitProvider, intakeEventCounter, gossipHalted::get, - () -> intakeQueueSizeSupplier.getAsLong() >= eventConfig.eventIntakeQueueThrottleSize(), Duration.ZERO, syncMetrics, platformStatusSupplier); @@ -501,42 +472,6 @@ public void bind( systemHealthInput.bindConsumer(syncPermitProvider::reportUnhealthyDuration); platformStatusInput.bindConsumer(currentPlatformStatus::set); - final boolean useOldStyleIntakeQueue = platformContext - .getConfiguration() - .getConfigData(EventConfig.class) - .useOldStyleIntakeQueue(); - - if (useOldStyleIntakeQueue) { - oldStyleIntakeQueue = new QueueThreadConfiguration(threadManager) - .setCapacity(10_000) - .setThreadName("old_style_intake_queue") - .setComponent("platform") - .setHandler(eventOutput::forward) - .setMetricsConfiguration( - new QueueThreadMetricsConfiguration(platformContext.getMetrics()).enableMaxSizeMetric()) - .build(); - thingsToStart.add(oldStyleIntakeQueue); - - receivedEventHandler = event -> { - try { - oldStyleIntakeQueue.put(event); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("interrupted while attempting to enqueue event from gossip", e); - } - }; - - } else { - receivedEventHandler = eventOutput::forward; - } - } - - /** - * Get the size of the old style intake queue. - * - * @return the size of the old style intake queue - */ - public int getOldStyleIntakeQueueSize() { - return oldStyleIntakeQueue.size(); + receivedEventHandler = eventOutput::forward; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncManagerImpl.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncManagerImpl.java index e598007b3312..1bfecf952871 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncManagerImpl.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/SyncManagerImpl.java @@ -22,13 +22,10 @@ import com.swirlds.common.context.PlatformContext; import com.swirlds.common.metrics.FunctionGauge; import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.eventhandling.EventConfig; import com.swirlds.platform.gossip.FallenBehindManager; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.LongSupplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,39 +36,19 @@ public class SyncManagerImpl implements FallenBehindManager { private static final Logger logger = LogManager.getLogger(SyncManagerImpl.class); - private final EventConfig eventConfig; - - /** - * Supplies the event intake queue size. - */ - private final LongSupplier intakeQueueSizeSupplier; - /** This object holds data on how nodes are connected to each other. */ private final FallenBehindManager fallenBehindManager; - /** - * True if gossip has been artificially halted. - */ - private final AtomicBoolean gossipHalted = new AtomicBoolean(false); - /** * Creates a new SyncManager * * @param platformContext the platform context - * @param intakeQueueSizeSupplier a supplier for the size of the event intake queue * @param fallenBehindManager the fallen behind manager - * @param eventConfig the event config */ public SyncManagerImpl( - @NonNull final PlatformContext platformContext, - @NonNull final LongSupplier intakeQueueSizeSupplier, - @NonNull final FallenBehindManager fallenBehindManager, - @NonNull final EventConfig eventConfig) { - - this.intakeQueueSizeSupplier = Objects.requireNonNull(intakeQueueSizeSupplier); + @NonNull final PlatformContext platformContext, @NonNull final FallenBehindManager fallenBehindManager) { this.fallenBehindManager = Objects.requireNonNull(fallenBehindManager); - this.eventConfig = Objects.requireNonNull(eventConfig); platformContext .getMetrics() @@ -89,47 +66,12 @@ public SyncManagerImpl( .withUnit("count")); } - /** - * A method called by the sync listener to determine whether a sync should be accepted or not - * - * @return true if the sync should be accepted, false otherwise - */ - public boolean shouldAcceptSync() { - // don't gossip if halted - if (gossipHalted.get()) { - return false; - } - - // we shouldn't sync if the event intake queue is too big - final long intakeQueueSize = intakeQueueSizeSupplier.getAsLong(); - if (intakeQueueSize > eventConfig.eventIntakeQueueThrottleSize()) { - return false; - } - return true; - } - - /** - * A method called by the sync caller to determine whether a sync should be initiated or not - * - * @return true if the sync should be initiated, false otherwise - */ - public boolean shouldInitiateSync() { - // don't gossip if halted - if (gossipHalted.get()) { - return false; - } - - // we shouldn't sync if the event intake queue is too big - return intakeQueueSizeSupplier.getAsLong() <= eventConfig.eventIntakeQueueThrottleSize(); - } - /** * Observers halt requested dispatches. Causes gossip to permanently stop (until node reboot). * * @param reason the reason why gossip is being stopped */ public void haltRequestedObserver(final String reason) { - gossipHalted.set(true); logger.info(FREEZE.getMarker(), "Gossip frozen, reason: {}", reason); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/protocol/SyncProtocol.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/protocol/SyncProtocol.java index 7c0c8037b1b0..fe8385320159 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/protocol/SyncProtocol.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/gossip/sync/protocol/SyncProtocol.java @@ -83,11 +83,6 @@ public class SyncProtocol implements Protocol { */ private final BooleanSupplier gossipHalted; - /** - * Returns true if the intake queue is too full, false otherwise - */ - private final BooleanSupplier intakeIsTooFull; - /** * The last time this protocol executed */ @@ -113,7 +108,6 @@ public class SyncProtocol implements Protocol { * @param intakeEventCounter keeps track of how many events have been received from each peer, but haven't yet * made it through the intake pipeline * @param gossipHalted returns true if gossip is halted, false otherwise - * @param intakeIsTooFull returns true if the intake queue is too full to continue syncing, false otherwise * @param sleepAfterSync the amount of time to sleep after a sync * @param syncMetrics metrics tracking syncing * @param platformStatusSupplier provides the current platform status @@ -126,7 +120,6 @@ public SyncProtocol( @NonNull final SyncPermitProvider permitProvider, @NonNull final IntakeEventCounter intakeEventCounter, @NonNull final BooleanSupplier gossipHalted, - @NonNull final BooleanSupplier intakeIsTooFull, @NonNull final Duration sleepAfterSync, @NonNull final SyncMetrics syncMetrics, @NonNull final Supplier platformStatusSupplier) { @@ -138,7 +131,6 @@ public SyncProtocol( this.permitProvider = Objects.requireNonNull(permitProvider); this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); this.gossipHalted = Objects.requireNonNull(gossipHalted); - this.intakeIsTooFull = Objects.requireNonNull(intakeIsTooFull); this.sleepAfterSync = Objects.requireNonNull(sleepAfterSync); this.syncMetrics = Objects.requireNonNull(syncMetrics); this.platformStatusSupplier = Objects.requireNonNull(platformStatusSupplier); @@ -175,11 +167,6 @@ private boolean shouldSync() { return false; } - if (intakeIsTooFull.getAsBoolean()) { - syncMetrics.doNotSyncIntakeQueue(); - return false; - } - if (fallenBehindManager.hasFallenBehind()) { syncMetrics.doNotSyncFallenBehind(); return false; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SyncMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SyncMetrics.java index 79dde39daf38..9ab4128233e6 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SyncMetrics.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/metrics/SyncMetrics.java @@ -113,12 +113,6 @@ public class SyncMetrics { .withDescription("Number of times per second we do not sync because gossip is halted"); private final CountPerSecond doNotSyncHalted; - private static final CountPerSecond.Config DO_NOT_SYNC_INTAKE_QUEUE_CONFIG = new CountPerSecond.Config( - PLATFORM_CATEGORY, "doNotSyncIntakeQueue") - .withUnit("hz") - .withDescription("Number of times per second we do not sync because the intake queue is too full"); - private final CountPerSecond doNotSyncIntakeQueue; - private static final CountPerSecond.Config DO_NOT_SYNC_FALLEN_BEHIND_CONFIG = new CountPerSecond.Config( PLATFORM_CATEGORY, "doNotSyncFallenBehind") .withUnit("hz") @@ -175,7 +169,6 @@ public SyncMetrics(final Metrics metrics) { doNoSyncPlatformStatus = new CountPerSecond(metrics, DO_NOT_SYNC_PLATFORM_STATUS); doNotSyncCooldown = new CountPerSecond(metrics, DO_NOT_SYNC_COOLDOWN_CONFIG); doNotSyncHalted = new CountPerSecond(metrics, DO_NOT_SYNC_HALTED_CONFIG); - doNotSyncIntakeQueue = new CountPerSecond(metrics, DO_NOT_SYNC_INTAKE_QUEUE_CONFIG); doNotSyncFallenBehind = new CountPerSecond(metrics, DO_NOT_SYNC_FALLEN_BEHIND_CONFIG); doNotSyncNoPermits = new CountPerSecond(metrics, DO_NOT_SYNC_NO_PERMITS_CONFIG); doNotSyncIntakeCounter = new CountPerSecond(metrics, DO_NOT_SYNC_INTAKE_COUNTER_CONFIG); @@ -412,13 +405,6 @@ public void doNotSyncHalted() { doNotSyncHalted.count(); } - /** - * Signal that we chose not to sync because the intake queue is too full. - */ - public void doNotSyncIntakeQueue() { - doNotSyncIntakeQueue.count(); - } - /** * Signal that we chose not to sync because we have fallen behind. */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/protocol/SyncProtocolFactory.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/protocol/SyncProtocolFactory.java index b6c117c0a38f..52ef7eb26d75 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/protocol/SyncProtocolFactory.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/network/protocol/SyncProtocolFactory.java @@ -42,7 +42,6 @@ public class SyncProtocolFactory implements ProtocolFactory { private final SyncPermitProvider permitProvider; private final IntakeEventCounter intakeEventCounter; private final BooleanSupplier gossipHalted; - private final BooleanSupplier intakeIsTooFull; private final Duration sleepAfterSync; private final SyncMetrics syncMetrics; private final Supplier platformStatusSupplier; @@ -56,7 +55,6 @@ public class SyncProtocolFactory implements ProtocolFactory { * @param permitProvider provides permits to sync * @param intakeEventCounter keeps track of how many events have been received from each peer * @param gossipHalted returns true if gossip is halted, false otherwise - * @param intakeIsTooFull returns true if the intake queue is too full to continue syncing, false otherwise * @param sleepAfterSync the amount of time to sleep after a sync * @param syncMetrics metrics tracking syncing * @param platformStatusSupplier provides the current platform status @@ -68,7 +66,6 @@ public SyncProtocolFactory( @NonNull final SyncPermitProvider permitProvider, @NonNull final IntakeEventCounter intakeEventCounter, @NonNull final BooleanSupplier gossipHalted, - @NonNull final BooleanSupplier intakeIsTooFull, @NonNull final Duration sleepAfterSync, @NonNull final SyncMetrics syncMetrics, @NonNull final Supplier platformStatusSupplier) { @@ -79,7 +76,6 @@ public SyncProtocolFactory( this.permitProvider = Objects.requireNonNull(permitProvider); this.intakeEventCounter = Objects.requireNonNull(intakeEventCounter); this.gossipHalted = Objects.requireNonNull(gossipHalted); - this.intakeIsTooFull = Objects.requireNonNull(intakeIsTooFull); this.sleepAfterSync = Objects.requireNonNull(sleepAfterSync); this.syncMetrics = Objects.requireNonNull(syncMetrics); this.platformStatusSupplier = Objects.requireNonNull(platformStatusSupplier); @@ -99,7 +95,6 @@ public SyncProtocol build(@NonNull final NodeId peerId) { permitProvider, intakeEventCounter, gossipHalted, - intakeIsTooFull, sleepAfterSync, syncMetrics, platformStatusSupplier); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java index 3375518f26fb..67ccce8a2826 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRoot.java @@ -19,6 +19,7 @@ import com.swirlds.common.merkle.MerkleInternal; import com.swirlds.platform.system.SwirldState; import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; /** * This interface represents the root node of the Merkle tree. @@ -74,4 +75,9 @@ public interface MerkleRoot extends MerkleInternal { /** {@inheritDoc} */ @NonNull MerkleRoot copy(); + + /** Creates a snapshots for the state. The state has to be hashed and immutable before calling this method. + * @param targetPath The path to save the snapshot. + */ + void createSnapshot(final @NonNull Path targetPath); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRootSnapshotMetrics.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRootSnapshotMetrics.java new file mode 100644 index 000000000000..3c7c71ba8857 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleRootSnapshotMetrics.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.state; + +import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.metrics.RunningAverageMetric; +import com.swirlds.metrics.api.Metrics; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * This class encapsulates metrics for the Merkle root snapshot. + */ +public class MerkleRootSnapshotMetrics { + private static final RunningAverageMetric.Config WRITE_MERKLE_ROOT_TO_DISK_TIME_CONFIG = + new RunningAverageMetric.Config("platform", "writeMerkleRootToDisk") + .withDescription("average time it takes to write a Merkle tree to disk (in milliseconds)") + .withUnit("ms"); + + private final RunningAverageMetric writeMerkleRootToDiskTime; + /** + * Constructor. + * + * @param platformContext the platform context + */ + public MerkleRootSnapshotMetrics(@NonNull final PlatformContext platformContext) { + final Metrics metrics = platformContext.getMetrics(); + writeMerkleRootToDiskTime = metrics.getOrCreate(WRITE_MERKLE_ROOT_TO_DISK_TIME_CONFIG); + } + + /** + * No-arg constructor constructs an object that does not track metrics. + */ + public MerkleRootSnapshotMetrics() { + writeMerkleRootToDiskTime = null; + } + + /** + * Update the metric tracking the average time required to write a Merkle tree to disk. + * @param timeTakenMs the time taken to write the state to disk + */ + public void updateWriteStateToDiskTimeMetric(final long timeTakenMs) { + if (writeMerkleRootToDiskTime != null) { + writeMerkleRootToDiskTime.update(timeTakenMs); + } + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java index 948d441ddd88..3291e3ceb1a9 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleStateRoot.java @@ -29,6 +29,7 @@ import com.hedera.hapi.block.stream.output.StateChanges; import com.hedera.hapi.node.base.SemanticVersion; +import com.swirlds.base.time.Time; import com.swirlds.common.constructable.ConstructableIgnored; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.merkle.MerkleInternal; @@ -84,6 +85,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -144,6 +146,7 @@ public class MerkleStateRoot extends PartialNaryMerkleInternal private final Function versionFactory; private MerkleCryptography merkleCryptography; + private Time time; public Map>> getServices() { return services; @@ -151,6 +154,11 @@ public class MerkleStateRoot extends PartialNaryMerkleInternal private Metrics metrics; + /** + * Metrics for the snapshot creation process + */ + private MerkleRootSnapshotMetrics snapshotMetrics = new MerkleRootSnapshotMetrics(); + /** * Maintains information about each service, and each state of each service, known by this * instance. The key is the "service-name.state-key". @@ -235,8 +243,11 @@ public void init( @NonNull final Platform platform, @NonNull final InitTrigger trigger, @Nullable final SoftwareVersion deserializedVersion) { - metrics = platform.getContext().getMetrics(); - merkleCryptography = platform.getContext().getMerkleCryptography(); + final PlatformContext platformContext = platform.getContext(); + time = platformContext.getTime(); + metrics = platformContext.getMetrics(); + merkleCryptography = platformContext.getMerkleCryptography(); + snapshotMetrics = new MerkleRootSnapshotMetrics(platformContext); // If we are initialized for event stream recovery, we have to register an // extra listener to make sure we call all the required Hedera lifecycles @@ -1009,6 +1020,15 @@ public PlatformStateAccessor getReadablePlatformState() { return readablePlatformStateStore(); } + /** + * Sets the time for this state. + * + * @param time the time to set + */ + public void setTime(final Time time) { + this.time = time; + } + /** * {@inheritDoc} */ @@ -1092,4 +1112,18 @@ public void computeHash() { Thread.currentThread().interrupt(); } } + + /** + * {@inheritDoc} + */ + @Override + public void createSnapshot(@NonNull final Path targetPath) { + requireNonNull(time); + requireNonNull(snapshotMetrics); + throwIfMutable(); + throwIfDestroyed(); + final long startTime = time.currentTimeMillis(); + MerkleTreeSnapshotWriter.createSnapshot(this, targetPath); + snapshotMetrics.updateWriteStateToDiskTimeMetric(time.currentTimeMillis() - startTime); + } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleTreeSnapshotWriter.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleTreeSnapshotWriter.java new file mode 100644 index 000000000000..09ffb1492080 --- /dev/null +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/MerkleTreeSnapshotWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.swirlds.platform.state; + +import static com.swirlds.common.io.utility.FileUtils.writeAndFlush; +import static com.swirlds.logging.legacy.LogMarker.EXCEPTION; +import static com.swirlds.logging.legacy.LogMarker.STATE_TO_DISK; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNED_STATE_FILE_NAME; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIG_SET_SEPARATE_STATE_FILE_VERSION; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.VERSIONED_FILE_BYTE; + +import com.swirlds.common.io.streams.MerkleDataOutputStream; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.nio.file.Path; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Utility class for writing a snapshot of a {@link MerkleRoot} to disk. + */ +public final class MerkleTreeSnapshotWriter { + + private static final Logger logger = LogManager.getLogger(MerkleTreeSnapshotWriter.class); + + private MerkleTreeSnapshotWriter() { + // prevent instantiation + } + + /** + * Writes a snapshot of the given {@link MerkleRoot} to the given {@link Path}. + * @param merkleRoot the {@link MerkleRoot} to write + * @param targetPath the {@link Path} to write the snapshot to + */ + static void createSnapshot(@NonNull final MerkleRoot merkleRoot, @NonNull final Path targetPath) { + final long round = merkleRoot.getReadablePlatformState().getRound(); + logger.info(STATE_TO_DISK.getMarker(), "Creating a snapshot on demand in {} for round {}", targetPath, round); + try { + writeMerkleRootToFile(targetPath, merkleRoot); + logger.info( + STATE_TO_DISK.getMarker(), + "Successfully created a snapshot on demand in {} for round {}", + targetPath, + round); + } catch (final Throwable e) { + logger.error( + EXCEPTION.getMarker(), + "Unable to write a snapshot on demand for round {} to {}.", + round, + targetPath, + e); + } + } + + private static void writeMerkleRootToFile(@NonNull final Path directory, @NonNull final MerkleRoot merkleRoot) + throws IOException { + writeAndFlush( + directory.resolve(SIGNED_STATE_FILE_NAME), out -> writeMerkleRootToStream(out, directory, merkleRoot)); + } + + private static void writeMerkleRootToStream( + @NonNull final MerkleDataOutputStream out, + @NonNull final Path directory, + @NonNull final MerkleRoot merkleRoot) + throws IOException { + out.write(VERSIONED_FILE_BYTE); + out.writeInt(SIG_SET_SEPARATE_STATE_FILE_VERSION); + out.writeProtocolVersion(); + out.writeMerkleTree(directory, merkleRoot); + out.writeSerializable(merkleRoot.getHash(), true); + } +} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java index 3b436ee1cb03..373cd7db8101 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/State.java @@ -26,6 +26,7 @@ import com.swirlds.common.utility.RuntimeObjectRegistry; import com.swirlds.platform.system.SwirldState; import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.file.Path; import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -202,6 +203,7 @@ public int getVersion() { public MerkleRoot copy() { throwIfImmutable(); throwIfDestroyed(); + setImmutable(true); return new State(this); } @@ -249,6 +251,16 @@ public String getInfoString(final int hashDepth) { return createInfoString(hashDepth, platformState, getHash(), this); } + /** + * {@inheritDoc} + */ + @Override + public void createSnapshot(@NonNull final Path targetPath) { + throwIfMutable(); + throwIfDestroyed(); + MerkleTreeSnapshotWriter.createSnapshot(this, targetPath); + } + /** * {@inheritDoc} */ diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/DefaultIssDetector.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/DefaultIssDetector.java index 1c14782e3042..d5a357f18cf7 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/DefaultIssDetector.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/iss/DefaultIssDetector.java @@ -364,8 +364,8 @@ private IssNotification handlePostconsensusSignature( return null; } - final boolean decided = roundValidator.reportHashFromNetwork( - signerId, nodeWeight, new Hash(signaturePayload.hash().toByteArray())); + final boolean decided = + roundValidator.reportHashFromNetwork(signerId, nodeWeight, new Hash(signaturePayload.hash())); if (decided) { return checkValidity(roundValidator); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PbjConverter.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PbjConverter.java index c2e8c1c7956c..431dc182bf80 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PbjConverter.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/PbjConverter.java @@ -207,7 +207,7 @@ public static com.hedera.hapi.platform.state.AddressBook toPbjAddressBook(@Nulla .collect(toList())); result.setRound(addressBook.round()); if (addressBook.nextNodeId() != null) { - result.setNextNodeId(new NodeId(addressBook.nextNodeId().id())); + result.setNextNodeId(NodeId.of(addressBook.nextNodeId().id())); } return result; } @@ -239,9 +239,7 @@ public static ConsensusSnapshot fromPbjConsensusSnapshot( return new ConsensusSnapshot( consensusSnapshot.round(), - consensusSnapshot.judgeHashes().stream() - .map(v -> new Hash(v.toByteArray())) - .collect(toList()), + consensusSnapshot.judgeHashes().stream().map(Hash::new).collect(toList()), consensusSnapshot.minimumJudgeInfoList().stream() .map(PbjConverter::fromPbjMinimumJudgeInfo) .collect(toList()), @@ -304,7 +302,7 @@ private static com.hedera.hapi.platform.state.Address toPbjAddress(@NonNull fina private static Address fromPbjAddress(@NonNull final com.hedera.hapi.platform.state.Address address) { requireNonNull(address.id()); return new Address( - new NodeId(address.id().id()), + NodeId.of(address.id().id()), address.nickname(), address.selfName(), address.weight(), diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadablePlatformStateStore.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadablePlatformStateStore.java index 4df3c28e8908..30dbb492cc33 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadablePlatformStateStore.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/service/ReadablePlatformStateStore.java @@ -121,7 +121,7 @@ public long getRound() { @Nullable public Hash getLegacyRunningEventHash() { final var hash = stateOrThrow().legacyRunningEventHash(); - return hash.length() == 0 ? null : new Hash(hash.toByteArray()); + return hash.length() == 0 ? null : new Hash(hash); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SigSet.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SigSet.java index b5fd75aa106f..1ddc733990ce 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SigSet.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/signed/SigSet.java @@ -200,7 +200,7 @@ public void deserialize(final SerializableDataInputStream in, final int version) for (int index = 0; index < signatureCount; index++) { final NodeId nodeId; if (version < ClassVersion.SELF_SERIALIZABLE_NODE_ID) { - nodeId = new NodeId(in.readLong()); + nodeId = NodeId.of(in.readLong()); } else { nodeId = in.readSerializable(false, NodeId::new); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java index ddc2560fe64c..08adc60b523a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SavedStateMetadata.java @@ -124,7 +124,7 @@ public record SavedStateMetadata( /** * Use this constant for the node ID if the thing writing the state is not a node. */ - public static final NodeId NO_NODE_ID = new NodeId(Long.MAX_VALUE); + public static final NodeId NO_NODE_ID = NodeId.of(Long.MAX_VALUE); private static final Logger logger = LogManager.getLogger(SavedStateMetadata.class); @@ -147,7 +147,7 @@ public static SavedStateMetadata parse(final Path metadataFile) throws IOExcepti parsePrimitiveLong(data, MINIMUM_GENERATION_NON_ANCIENT), parseNonNullString(data, SOFTWARE_VERSION), parseNonNullInstant(data, WALL_CLOCK_TIME), - new NodeId(parsePrimitiveLong(data, NODE_ID)), + NodeId.of(parsePrimitiveLong(data, NODE_ID)), parseNodeIdList(data, SIGNING_NODES), parsePrimitiveLong(data, SIGNING_WEIGHT_SUM), parsePrimitiveLong(data, TOTAL_WEIGHT)); @@ -485,7 +485,7 @@ private static List parseNodeIdList( for (final String part : parts) { try { - list.add(new NodeId(Long.parseLong(part.strip()))); + list.add(NodeId.of(Long.parseLong(part.strip()))); } catch (final NumberFormatException e) { throwInvalidRequiredField(field, value, e); return null; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java index b17a7d923640..d9452361fbf2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileReader.java @@ -17,6 +17,11 @@ package com.swirlds.platform.state.snapshot; import static com.swirlds.common.io.streams.StreamDebugUtils.deserializeAndDebugOnFailure; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.INIT_STATE_FILE_VERSION; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNATURE_SET_FILE_NAME; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIG_SET_SEPARATE_STATE_FILE_VERSION; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SUPPORTED_SIGSET_VERSIONS; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SUPPORTED_STATE_FILE_VERSIONS; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.VERSIONED_FILE_BYTE; import static java.nio.file.Files.exists; @@ -32,7 +37,9 @@ import com.swirlds.platform.state.signed.SigSet; import com.swirlds.platform.state.signed.SignedState; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.io.BufferedInputStream; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Files; @@ -65,6 +72,16 @@ public static List getSavedStateFiles( .getSavedStateFiles(mainClassName, platformId, swirldName); } + /** + * This is a helper class to hold the data read from a state file. + * @param state the Merkle tree state + * @param hash the hash of the state + * @param sigSet the signature set + * @param fileVersion the version of the file + */ + public record StateFileData( + @NonNull MerkleRoot state, @NonNull Hash hash, @Nullable SigSet sigSet, int fileVersion) {} + /** * Reads a SignedState from disk using the provided snapshot reader function. If the reader throws * an exception, it is propagated by this method to the caller. @@ -87,43 +104,111 @@ public static List getSavedStateFiles( checkSignedStatePath(stateFile); final DeserializedSignedState returnState; + final StateFileData data = readStateFileData(stateFile, snapshotStateReader); - record StateFileData(MerkleRoot state, Hash hash, SigSet sigSet) {} - - final StateFileData data = deserializeAndDebugOnFailure( - () -> new BufferedInputStream(new FileInputStream(stateFile.toFile())), - (final MerkleDataInputStream in) -> { - readAndCheckVersion(in); - - final Path directory = stateFile.getParent(); - - try { - final MerkleRoot state = snapshotStateReader.apply(in, directory); - final Hash hash = in.readSerializable(); + final StateFileData normalizedData; + if (data.sigSet == null) { + final File sigSetFile = + stateFile.getParent().resolve(SIGNATURE_SET_FILE_NAME).toFile(); + normalizedData = deserializeAndDebugOnFailure( + () -> new BufferedInputStream(new FileInputStream(sigSetFile)), + (final MerkleDataInputStream in) -> { + final int fileVersion = readAndCheckSigSetFileVersion(in); final SigSet sigSet = in.readSerializable(); - return new StateFileData(state, hash, sigSet); - } catch (final IOException e) { - throw new IOException("Failed to read snapshot file " + stateFile.toFile(), e); - } - }); + return new StateFileData(data.state, data.hash, sigSet, fileVersion); + }); + } else { + normalizedData = data; + } final SignedState newSignedState = new SignedState( platformContext, CryptoStatic::verifySignature, - data.state(), + normalizedData.state, "SignedStateFileReader.readStateFile()", false, false, false); - newSignedState.setSigSet(data.sigSet()); + newSignedState.setSigSet(normalizedData.sigSet); returnState = new DeserializedSignedState( - newSignedState.reserve("SignedStateFileReader.readStateFile()"), data.hash()); + newSignedState.reserve("SignedStateFileReader.readStateFile()"), normalizedData.hash); return returnState; } + /** + * Reads a SignedState from disk using the provided snapshot reader function. + * @param stateFile the file to read from + * @param snapshotStateReader state snapshot reading function + * @return a signed state with it's associated hash (as computed when the state was serialized) + * @throws IOException if there is any problems with reading from a file + */ + @NonNull + public static StateFileData readStateFileData( + @NonNull final Path stateFile, + @NonNull final CheckedBiFunction snapshotStateReader) + throws IOException { + return deserializeAndDebugOnFailure( + () -> new BufferedInputStream(new FileInputStream(stateFile.toFile())), + (final MerkleDataInputStream in) -> { + final int fileVersion = readAndCheckStateFileVersion(in); + + final Path directory = stateFile.getParent(); + if (fileVersion == INIT_STATE_FILE_VERSION) { + return readStateFileDataV1(stateFile, snapshotStateReader, in, directory); + } else if (fileVersion == SIG_SET_SEPARATE_STATE_FILE_VERSION) { + return readStateFileDataV2(stateFile, snapshotStateReader, in, directory); + } else { + throw new IOException("Unsupported state file version: " + fileVersion); + } + }); + } + + /** + * This method reads the state file data from a version 1 state file. This version of the state file contains + * signature set data. + */ + @NonNull + private static StateFileData readStateFileDataV1( + @NonNull final Path stateFile, + @NonNull final CheckedBiFunction snapshotStateReader, + @NonNull final MerkleDataInputStream in, + @NonNull final Path directory) + throws IOException { + try { + final MerkleRoot state = snapshotStateReader.apply(in, directory); + final Hash hash = in.readSerializable(); + final SigSet sigSet = in.readSerializable(); + return new StateFileData(state, hash, sigSet, INIT_STATE_FILE_VERSION); + } catch (final IOException e) { + throw new IOException("Failed to read snapshot file " + stateFile.toFile(), e); + } + } + + /** + * This method reads the state file data from a version 2 state file. This version of the state file + * doesn't contain signature set data. Instead, the signature set data is stored in a separate file, + * and the resulting object doesn't have {@link SigSet} field initialized. + */ + @NonNull + private static StateFileData readStateFileDataV2( + @NonNull final Path stateFile, + @NonNull final CheckedBiFunction snapshotStateReader, + @NonNull final MerkleDataInputStream in, + @NonNull final Path directory) + throws IOException { + try { + final MerkleRoot state = snapshotStateReader.apply(in, directory); + final Hash hash = in.readSerializable(); + return new StateFileData(state, hash, null, SIG_SET_SEPARATE_STATE_FILE_VERSION); + + } catch (final IOException e) { + throw new IOException("Failed to read snapshot file " + stateFile.toFile(), e); + } + } + /** * Check the path of a signed state file * @@ -144,14 +229,34 @@ private static void checkSignedStatePath(@NonNull final Path stateFile) throws I * * @param in the stream to read from * @throws IOException if the version is invalid + * @return the protocol version */ - private static void readAndCheckVersion(@NonNull final MerkleDataInputStream in) throws IOException { + private static int readAndCheckStateFileVersion(@NonNull final MerkleDataInputStream in) throws IOException { final byte versionByte = in.readByte(); if (versionByte != VERSIONED_FILE_BYTE) { throw new IOException("File is not versioned -- data corrupted or is an unsupported legacy state"); } - in.readInt(); // file version + final int fileVersion = in.readInt(); + if (!SUPPORTED_STATE_FILE_VERSIONS.contains(fileVersion)) { + throw new IOException("Unsupported file version: " + fileVersion); + } + in.readProtocolVersion(); + return fileVersion; + } + /** + * Read the version from a signature set file and check it + * + * @param in the stream to read from + * @throws IOException if the version is invalid + * @return the protocol version + */ + private static int readAndCheckSigSetFileVersion(@NonNull final MerkleDataInputStream in) throws IOException { + final int fileVersion = in.readInt(); + if (!SUPPORTED_SIGSET_VERSIONS.contains(fileVersion)) { + throw new IOException("Unsupported file version: " + fileVersion); + } in.readProtocolVersion(); + return fileVersion; } } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileUtils.java index d5dea247230b..a550503e54f4 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileUtils.java @@ -21,6 +21,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.nio.file.Path; +import java.util.Set; /** * Utility methods for dealing with signed states on disk. @@ -32,6 +33,8 @@ public final class SignedStateFileUtils { */ public static final String SIGNED_STATE_FILE_NAME = "SignedState.swh"; + public static final String SIGNATURE_SET_FILE_NAME = "signatureSet.bin"; + public static final String HASH_INFO_FILE_NAME = "hashInfo.txt"; /** @@ -45,9 +48,31 @@ public final class SignedStateFileUtils { public static final byte VERSIONED_FILE_BYTE = Byte.MAX_VALUE; /** - * The current version of the signed state file + * The previous version of the signed state file + */ + public static final int INIT_STATE_FILE_VERSION = 1; + + /** + * The current version of the signed state file. A file of this version no longer contains the signature set, + * instead the signature set is stored in a separate file. + */ + public static final int SIG_SET_SEPARATE_STATE_FILE_VERSION = 2; + + /** + * The initial version of the signature set file + */ + public static final int INIT_SIG_SET_FILE_VERSION = 1; + + /** + * The supported versions of the signed state file + */ + public static final Set SUPPORTED_STATE_FILE_VERSIONS = + Set.of(INIT_STATE_FILE_VERSION, SIG_SET_SEPARATE_STATE_FILE_VERSION); + + /** + * The supported versions of the signature set file */ - public static final int FILE_VERSION = 1; + public static final Set SUPPORTED_SIGSET_VERSIONS = Set.of(INIT_SIG_SET_FILE_VERSION); public static final int MAX_MERKLE_NODES_IN_STATE = Integer.MAX_VALUE; diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java index b79c5087a73a..ee0db341f50c 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/state/snapshot/SignedStateFileWriter.java @@ -23,10 +23,9 @@ import static com.swirlds.platform.config.internal.PlatformConfigUtils.writeSettingsUsed; import static com.swirlds.platform.event.preconsensus.BestEffortPcesFileCopy.copyPcesFilesRetryOnFailure; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.CURRENT_ADDRESS_BOOK_FILE_NAME; -import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.FILE_VERSION; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.HASH_INFO_FILE_NAME; -import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNED_STATE_FILE_NAME; -import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.VERSIONED_FILE_BYTE; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.INIT_STATE_FILE_VERSION; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNATURE_SET_FILE_NAME; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.io.streams.MerkleDataOutputStream; @@ -36,6 +35,7 @@ import com.swirlds.platform.config.StateConfig; import com.swirlds.platform.recovery.emergencyfile.EmergencyRecoveryFile; import com.swirlds.platform.state.MerkleRoot; +import com.swirlds.platform.state.MerkleStateRoot; import com.swirlds.platform.state.signed.SignedState; import com.swirlds.platform.system.address.AddressBook; import edu.umd.cs.findbugs.annotations.NonNull; @@ -109,31 +109,26 @@ public static void writeMetadataFile( } /** - * Write a {@link SignedState} to a stream. + * Write a {@link com.swirlds.platform.state.signed.SigSet} to a stream. * * @param out the stream to write to - * @param directory the directory to write to * @param signedState the signed state to write */ - private static void writeStateFileToStream( - final MerkleDataOutputStream out, final Path directory, final SignedState signedState) throws IOException { - out.write(VERSIONED_FILE_BYTE); - out.writeInt(FILE_VERSION); + private static void writeSignatureSetToStream(final MerkleDataOutputStream out, final SignedState signedState) + throws IOException { + out.writeInt(INIT_STATE_FILE_VERSION); out.writeProtocolVersion(); - out.writeMerkleTree(directory, signedState.getState()); - out.writeSerializable(signedState.getState().getHash(), true); out.writeSerializable(signedState.getSigSet(), true); } /** - * Write the signed state file. - * - * @param directory the directory to write to - * @param signedState the signed state to write + * Write the signature set file. + * @param directory the directory to write to + * @param signedState the signature set file */ - public static void writeStateFile(final Path directory, final SignedState signedState) throws IOException { - writeAndFlush( - directory.resolve(SIGNED_STATE_FILE_NAME), out -> writeStateFileToStream(out, directory, signedState)); + public static void writeSignatureSetFile(final @NonNull Path directory, final @NonNull SignedState signedState) + throws IOException { + writeAndFlush(directory.resolve(SIGNATURE_SET_FILE_NAME), out -> writeSignatureSetToStream(out, signedState)); } /** @@ -154,7 +149,12 @@ public static void writeSignedStateFilesToDirectory( Objects.requireNonNull(directory); Objects.requireNonNull(signedState); - writeStateFile(directory, signedState); + final MerkleRoot state = signedState.getState(); + if (state instanceof MerkleStateRoot merkleStateRoot) { + merkleStateRoot.setTime(platformContext.getTime()); + } + state.createSnapshot(directory); + writeSignatureSetFile(directory, signedState); writeHashInfoFile(platformContext, directory, signedState.getState()); writeMetadataFile(selfId, directory, signedState); writeEmergencyRecoveryFile(directory, signedState); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBook.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBook.java index a9957c0ce756..24dd8bd3fe86 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBook.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBook.java @@ -563,7 +563,7 @@ public void deserialize(@NonNull final SerializableDataInputStream in, final int round = in.readLong(); if (version < ClassVersion.SELF_SERIALIZABLE_NODE_ID) { - nextNodeId = new NodeId(in.readLong()); + nextNodeId = NodeId.of(in.readLong()); } else { nextNodeId = in.readSerializable(false, NodeId::new); } diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java index 7aa5559ec402..d16edb628d53 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/address/AddressBookUtils.java @@ -16,6 +16,7 @@ package com.swirlds.platform.system.address; +import static com.swirlds.base.utility.NetworkUtils.isNameResolvable; import static com.swirlds.platform.util.BootstrapUtils.detectSoftwareUpgrade; import com.hedera.hapi.node.base.ServiceEndpoint; @@ -32,8 +33,6 @@ import com.swirlds.platform.system.SoftwareVersion; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.cert.CertificateEncodingException; import java.text.ParseException; import java.util.ArrayList; @@ -121,6 +120,10 @@ public static AddressBook parseAddressBookText(@NonNull final String addressBook if (address != null) { addressBook.add(address); } + } else if (trimmedLine.startsWith("nextNodeId")) { + // As of release 0.56, nextNodeId is not used and ignored. + // CI/CD pipelines need to be updated to remove this field from files. + // Future Work: remove this case and hard fail when nextNodeId is no longer present in CI/CD pipelines. } else { throw new ParseException( "The line [%s] does not start with `%s`." @@ -164,7 +167,7 @@ public static Address parseAddressText(@NonNull final String addressText) throws } final NodeId nodeId; try { - nodeId = new NodeId(Long.parseLong(parts[1])); + nodeId = NodeId.of(Long.parseLong(parts[1])); } catch (final Exception e) { throw new ParseException("Cannot parse node id from '" + parts[1] + "'", 1); } @@ -178,11 +181,8 @@ public static Address parseAddressText(@NonNull final String addressText) throws } // FQDN Support: The original string value is preserved, whether it is an IP Address or a FQDN. final String internalHostname = parts[5]; - try { - // validate that an InetAddress can be created from the internal hostname. - InetAddress.getByName(internalHostname); - } catch (UnknownHostException e) { - throw new ParseException("Cannot parse ip address from '" + parts[5] + "'", 5); + if (!isNameResolvable(internalHostname)) { + throw new ParseException("Cannot parse ip address from '" + internalHostname + "'", 5); } final int internalPort; try { @@ -192,11 +192,8 @@ public static Address parseAddressText(@NonNull final String addressText) throws } // FQDN Support: The original string value is preserved, whether it is an IP Address or a FQDN. final String externalHostname = parts[7]; - try { - // validate that an InetAddress can be created from the external hostname. - InetAddress.getByName(externalHostname); - } catch (UnknownHostException e) { - throw new ParseException("Cannot parse ip address from '" + parts[7] + "'", 7); + if (!isNameResolvable(externalHostname)) { + throw new ParseException("Cannot parse ip address from '" + externalHostname + "'", 7); } final int externalPort; try { @@ -327,13 +324,15 @@ private static RosterEntry toRosterEntry(@NonNull final Address address, @NonNul .gossipEndpoint(serviceEndpoints) .build(); } + /** * Initializes the address book from the configuration and platform saved state. - * @param selfId the node ID of the current node - * @param version the software version of the current node - * @param initialState the initial state of the platform + * + * @param selfId the node ID of the current node + * @param version the software version of the current node + * @param initialState the initial state of the platform * @param bootstrapAddressBook the bootstrap address book - * @param platformContext the platform context + * @param platformContext the platform context * @return the initialized address book */ public static @NonNull AddressBook initializeAddressBook( diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java index bf15e1240cb8..b6a44b85f6f3 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventDescriptorWrapper.java @@ -62,10 +62,7 @@ public static boolean valid(final int version) { } public EventDescriptorWrapper(@NonNull EventDescriptor eventDescriptor) { - this( - eventDescriptor, - new Hash(eventDescriptor.hash().toByteArray()), - new NodeId(eventDescriptor.creatorNodeId())); + this(eventDescriptor, new Hash(eventDescriptor.hash()), NodeId.of(eventDescriptor.creatorNodeId())); } /** diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventMetadata.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventMetadata.java index 280ac3487f7c..2579d3cc906e 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventMetadata.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/system/events/EventMetadata.java @@ -106,7 +106,7 @@ public EventMetadata( */ public EventMetadata(@NonNull final GossipEvent gossipEvent) { Objects.requireNonNull(gossipEvent.eventCore(), "The eventCore must not be null"); - this.creatorId = new NodeId(gossipEvent.eventCore().creatorNodeId()); + this.creatorId = NodeId.of(gossipEvent.eventCore().creatorNodeId()); this.allParents = gossipEvent.eventCore().parents().stream() .map(EventDescriptorWrapper::new) .toList(); diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java index eff8af1a9dc7..f72cba1310a2 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformSchedulersConfig.java @@ -24,7 +24,6 @@ /** * Contains configuration values for the platform schedulers. * - * @param eventHasherUnhandledCapacity number of unhandled tasks allowed in the event hasher scheduler * @param internalEventValidator configuration for the internal event validator scheduler * @param eventDeduplicator configuration for the event deduplicator scheduler * @param eventSignatureValidator configuration for the event signature validator scheduler @@ -58,13 +57,11 @@ * @param transactionPool configuration for the transaction pool scheduler * @param gossip configuration for the gossip scheduler * @param eventHasher configuration for the event hasher scheduler - * @param postHashCollector configuration for the post hash collector scheduler * @param branchDetector configuration for the branch detector scheduler * @param branchReporter configuration for the branch reporter scheduler */ @ConfigData("platformSchedulers") public record PlatformSchedulersConfig( - @ConfigProperty(defaultValue = "500") int eventHasherUnhandledCapacity, @ConfigProperty(defaultValue = "CONCURRENT CAPACITY(500) FLUSHABLE UNHANDLED_TASK_METRIC") TaskSchedulerConfiguration internalEventValidator, @ConfigProperty(defaultValue = "SEQUENTIAL CAPACITY(5000) FLUSHABLE UNHANDLED_TASK_METRIC") @@ -128,12 +125,9 @@ public record PlatformSchedulersConfig( @ConfigProperty(defaultValue = "DIRECT_THREADSAFE") TaskSchedulerConfiguration transactionPool, @ConfigProperty(defaultValue = "SEQUENTIAL CAPACITY(500) FLUSHABLE UNHANDLED_TASK_METRIC") TaskSchedulerConfiguration gossip, - @ConfigProperty(defaultValue = "CONCURRENT CAPACITY(5000) UNHANDLED_TASK_METRIC") + @ConfigProperty(defaultValue = "CONCURRENT CAPACITY(500) FLUSHABLE UNHANDLED_TASK_METRIC") TaskSchedulerConfiguration eventHasher, - @ConfigProperty(defaultValue = "SEQUENTIAL CAPACITY(-1) UNHANDLED_TASK_METRIC") - TaskSchedulerConfiguration postHashCollector, @ConfigProperty(defaultValue = "SEQUENTIAL CAPACITY(500) FLUSHABLE UNHANDLED_TASK_METRIC") TaskSchedulerConfiguration branchDetector, @ConfigProperty(defaultValue = "SEQUENTIAL CAPACITY(500) FLUSHABLE UNHANDLED_TASK_METRIC") - TaskSchedulerConfiguration branchReporter, - @ConfigProperty(defaultValue = "false") boolean hashCollectorEnabled) {} + TaskSchedulerConfiguration branchReporter) {} diff --git a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java index 08824f484b13..b38dd21bfd9a 100644 --- a/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java +++ b/platform-sdk/swirlds-platform-core/src/main/java/com/swirlds/platform/wiring/PlatformWiring.java @@ -16,7 +16,6 @@ package com.swirlds.platform.wiring; -import static com.swirlds.common.wiring.model.diagram.HyperlinkBuilder.platformCoreHyperlink; import static com.swirlds.common.wiring.schedulers.builders.TaskSchedulerConfiguration.DIRECT_THREADSAFE_CONFIGURATION; import static com.swirlds.common.wiring.schedulers.builders.TaskSchedulerConfiguration.NO_OP_CONFIGURATION; import static com.swirlds.common.wiring.wires.SolderType.INJECT; @@ -29,11 +28,7 @@ import com.swirlds.common.io.IOIterator; import com.swirlds.common.stream.RunningEventHashOverride; import com.swirlds.common.wiring.component.ComponentWiring; -import com.swirlds.common.wiring.counters.BackpressureObjectCounter; -import com.swirlds.common.wiring.counters.ObjectCounter; import com.swirlds.common.wiring.model.WiringModel; -import com.swirlds.common.wiring.schedulers.TaskScheduler; -import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerBuilder; import com.swirlds.common.wiring.schedulers.builders.TaskSchedulerConfiguration; import com.swirlds.common.wiring.transformers.RoutableData; import com.swirlds.common.wiring.transformers.WireFilter; @@ -108,16 +103,13 @@ import com.swirlds.platform.system.status.StatusStateMachine; import com.swirlds.platform.system.transaction.TransactionWrapper; import com.swirlds.platform.wiring.components.GossipWiring; -import com.swirlds.platform.wiring.components.PassThroughWiring; import com.swirlds.platform.wiring.components.PcesReplayerWiring; import com.swirlds.platform.wiring.components.RunningEventHashOverrideWiring; import com.swirlds.platform.wiring.components.StateAndRound; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.time.Duration; import java.util.List; import java.util.Objects; -import java.util.function.LongSupplier; /** * Encapsulates wiring for {@link com.swirlds.platform.SwirldsPlatform}. @@ -131,7 +123,6 @@ public class PlatformWiring { private final boolean inlinePces; private final ComponentWiring eventHasherWiring; - private final PassThroughWiring postHashCollectorWiring; private final ComponentWiring internalEventValidatorWiring; private final ComponentWiring eventDeduplicatorWiring; private final ComponentWiring eventSignatureValidatorWiring; @@ -179,8 +170,6 @@ public class PlatformWiring { private final ComponentWiring branchDetectorWiring; private final ComponentWiring branchReporterWiring; - private final boolean hashCollectorEnabled; - /** * Constructor. * @@ -202,7 +191,6 @@ public PlatformWiring( .getConfiguration() .getConfigData(ComponentWiringConfig.class) .inlinePces(); - hashCollectorEnabled = config.hashCollectorEnabled(); final AncientMode ancientMode = platformContext .getConfiguration() @@ -215,29 +203,7 @@ public PlatformWiring( birthRoundMigrationShimWiring = null; } - // Provides back pressure across both the event hasher and the post hash collector - final ObjectCounter hashingObjectCounter; - if (hashCollectorEnabled) { - hashingObjectCounter = new BackpressureObjectCounter( - "hashingObjectCounter", - platformContext - .getConfiguration() - .getConfigData(PlatformSchedulersConfig.class) - .eventHasherUnhandledCapacity(), - Duration.ofNanos(100)); - } else { - hashingObjectCounter = null; - } - - eventHasherWiring = - new ComponentWiring<>(model, EventHasher.class, buildEventHasherScheduler(hashingObjectCounter)); - - if (hashCollectorEnabled) { - postHashCollectorWiring = new PassThroughWiring<>( - model, "PlatformEvent", buildPostHashCollectorScheduler(hashingObjectCounter)); - } else { - postHashCollectorWiring = null; - } + eventHasherWiring = new ComponentWiring<>(model, EventHasher.class, config.eventHasher()); internalEventValidatorWiring = new ComponentWiring<>(model, InternalEventValidator.class, config.internalEventValidator()); @@ -328,13 +294,7 @@ public PlatformWiring( branchReporterWiring = new ComponentWiring<>(model, BranchReporter.class, config.branchReporter()); platformCoordinator = new PlatformCoordinator( - () -> { - if (hashCollectorEnabled) { - hashingObjectCounter.waitUntilEmpty(); - } else { - eventHasherWiring.flush(); - } - }, + eventHasherWiring::flush, internalEventValidatorWiring, eventDeduplicatorWiring, eventSignatureValidatorWiring, @@ -357,53 +317,6 @@ public PlatformWiring( wire(); } - /** - * Build the event hasher scheduler. Normally we don't build schedulers in this class, but a special exception is - * made here because for back pressure reasons. Will be removed from this class when we implement a platform health - * monitor. - * - * @param hashingObjectCounter the object counter to use for back pressure - * @return the event hasher scheduler - */ - @NonNull - private TaskScheduler buildEventHasherScheduler(@NonNull final ObjectCounter hashingObjectCounter) { - final TaskSchedulerBuilder builder = model.schedulerBuilder("EventHasher") - .configure(config.eventHasher()) - .withUnhandledTaskMetricEnabled(true) - .withHyperlink(platformCoreHyperlink(EventHasher.class)); - - if (hashCollectorEnabled) { - builder.withOnRamp(hashingObjectCounter).withExternalBackPressure(true); - } else { - builder.withUnhandledTaskCapacity(platformContext - .getConfiguration() - .getConfigData(PlatformSchedulersConfig.class) - .eventHasherUnhandledCapacity()) - .withFlushingEnabled(true); - } - - return builder.build().cast(); - } - - /** - * Build the post hash collector scheduler. Normally we don't build schedulers in this class, but a special - * exception is made here because for back pressure reasons. Will be removed from this class when we implement a - * platform health monitor. - * - * @param hashingObjectCounter the object counter to use for back pressure - * @return the post hash collector scheduler - */ - @NonNull - private TaskScheduler buildPostHashCollectorScheduler( - @NonNull final ObjectCounter hashingObjectCounter) { - return model.schedulerBuilder("PostHashCollector") - .configure(config.postHashCollector()) - .withOffRamp(hashingObjectCounter) - .withExternalBackPressure(true) - .build() - .cast(); - } - /** * Get the wiring model. * @@ -475,16 +388,9 @@ private void wire() { } gossipWiring.getEventOutput().solderTo(pipelineInputWire); - if (hashCollectorEnabled) { - eventHasherWiring.getOutputWire().solderTo(postHashCollectorWiring.getInputWire()); - postHashCollectorWiring - .getOutputWire() - .solderTo(internalEventValidatorWiring.getInputWire(InternalEventValidator::validateEvent)); - } else { - eventHasherWiring - .getOutputWire() - .solderTo(internalEventValidatorWiring.getInputWire(InternalEventValidator::validateEvent)); - } + eventHasherWiring + .getOutputWire() + .solderTo(internalEventValidatorWiring.getInputWire(InternalEventValidator::validateEvent)); internalEventValidatorWiring .getOutputWire() @@ -1037,24 +943,6 @@ public ComponentWiring getNotifierWiring() { return notifierWiring; } - /** - * Get a supplier for the number of unprocessed tasks at the front of the intake pipeline. This is for the purpose - * of applying backpressure to the event creator and gossip when the intake pipeline is overloaded. - *

      - * Technically, the first component of the intake pipeline is the hasher, but tasks to be passed along actually - * accumulate in the post hash collector. This is due to how the concurrent hasher handles backpressure. - * - * @return a supplier for the number of unprocessed tasks in the PostHashCollector - */ - @NonNull - public LongSupplier getIntakeQueueSizeSupplier() { - if (hashCollectorEnabled) { - return () -> postHashCollectorWiring.getScheduler().getUnprocessedTaskCount(); - } else { - return () -> 0; - } - } - /** * Update the running hash for all components that need it. * @@ -1069,7 +957,6 @@ public void updateRunningHash(@NonNull final RunningEventHashOverride runningHas * * @param state the overriding state */ - @NonNull public void overrideIssDetectorState(@NonNull final ReservedSignedState state) { issDetectorWiring.getInputWire(IssDetector::overridingState).put(state); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java index c7eb9c0b0396..613376d544b9 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/AddressBookInitializerTest.java @@ -76,7 +76,7 @@ void forceUseOfConfigAddressBook() throws IOException { final AddressBook configAddressBook = getRandomAddressBook(randotron); final SignedState signedState = getMockSignedState7WeightRandomAddressBook(randotron); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(2), // no software upgrade false, @@ -106,7 +106,7 @@ void noStateLoadedFromDisk() throws IOException { // initial state has no address books set. final SignedState signedState = getMockSignedState(10, null, null, true); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(2), // no software upgrade false, @@ -133,7 +133,7 @@ void noStateLoadedFromDiskGenesisStateSetZeroWeight() throws IOException { // genesis state address book is set to null to test the code path where it may be null. final SignedState signedState = getMockSignedState(10, null, null, true); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(2), // no software upgrade false, @@ -159,7 +159,7 @@ void noStateLoadedFromDiskGenesisStateChangedAddressBook() throws IOException { final AddressBook configAddressBook = getRandomAddressBook(randotron); final SignedState signedState = getMockSignedState(7, configAddressBook, null, true); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(2), // no software upgrade false, @@ -189,7 +189,7 @@ void currentVersionEqualsStateVersion() throws IOException { getMockSignedState(2, getRandomAddressBook(randotron), getRandomAddressBook(randotron), false); final AddressBook configAddressBook = copyWithWeightChanges(signedState.getAddressBook(), 10); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(2), // no software upgrade false, @@ -220,7 +220,7 @@ void versionUpgradeSwirldStateZeroWeight() throws IOException { getMockSignedState(0, getRandomAddressBook(randotron), getRandomAddressBook(randotron), false); final AddressBook configAddressBook = copyWithWeightChanges(signedState.getAddressBook(), 10); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(3), // software upgrade true, @@ -250,7 +250,7 @@ void versionUpgradeSwirldStateModifiedAddressBook() throws IOException { getMockSignedState(2, getRandomAddressBook(randotron), getRandomAddressBook(randotron), false); final AddressBook configAddressBook = copyWithWeightChanges(signedState.getAddressBook(), 3); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(3), // software upgrade true, @@ -279,7 +279,7 @@ void versionUpgradeSwirldStateWeightUpdateWorks() throws IOException { final SignedState signedState = getMockSignedState7WeightRandomAddressBook(randotron); final AddressBook configAddressBook = copyWithWeightChanges(signedState.getAddressBook(), 5); final AddressBookInitializer initializer = new AddressBookInitializer( - new NodeId(0), + NodeId.of(0), getMockSoftwareVersion(3), // software upgrade true, diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DummyHashgraph.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DummyHashgraph.java deleted file mode 100644 index f3378fbf01c1..000000000000 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/DummyHashgraph.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2016-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.swirlds.platform; - -import com.swirlds.common.platform.NodeId; -import com.swirlds.platform.system.address.AddressBook; -import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookBuilder; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.HashMap; -import java.util.Random; - -public class DummyHashgraph { - - public int eventIntakeQueueSize; - public HashMap isInCriticalQuorum; - public NodeId selfId; - public long numUserTransEvents; - public long lastRoundReceivedAllTransCons; - public AddressBook addressBook; - - DummyHashgraph(@NonNull final Random random, final int selfIndex) { - eventIntakeQueueSize = 0; - isInCriticalQuorum = new HashMap<>(); - numUserTransEvents = 0; - lastRoundReceivedAllTransCons = 0; - addressBook = RandomAddressBookBuilder.create(random).withSize(41).build(); - this.selfId = addressBook.getNodeId(selfIndex); - } - - int getEventIntakeQueueSize() { - return eventIntakeQueueSize; - } - - AddressBook getAddressBook() { - return addressBook; - } -} diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/ReconnectThrottleTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/ReconnectThrottleTest.java index 4bd7c9afa4cf..8d0e0889210c 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/ReconnectThrottleTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/ReconnectThrottleTest.java @@ -52,11 +52,11 @@ private ReconnectConfig buildSettings(final String minimumTimeBetweenReconnects) void simultaneousReconnectTest() { final ReconnectThrottle reconnectThrottle = new ReconnectThrottle(buildSettings("10m"), Time.getCurrent()); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(0)), "reconnect should be allowed"); - assertFalse(reconnectThrottle.initiateReconnect(new NodeId(1)), "reconnect should be blocked"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(0)), "reconnect should be allowed"); + assertFalse(reconnectThrottle.initiateReconnect(NodeId.of(1)), "reconnect should be blocked"); reconnectThrottle.reconnectAttemptFinished(); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(1)), "reconnect should be allowed"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(1)), "reconnect should be allowed"); } @Test @@ -66,19 +66,19 @@ void repeatedReconnectTest() { final ReconnectThrottle reconnectThrottle = new ReconnectThrottle(buildSettings("1s"), Time.getCurrent()); reconnectThrottle.setCurrentTime(() -> Instant.ofEpochMilli(0)); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(0)), "reconnect should be allowed"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(0)), "reconnect should be allowed"); reconnectThrottle.reconnectAttemptFinished(); - assertFalse(reconnectThrottle.initiateReconnect(new NodeId(0)), "reconnect should be blocked"); + assertFalse(reconnectThrottle.initiateReconnect(NodeId.of(0)), "reconnect should be blocked"); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(1)), "reconnect should be allowed"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(1)), "reconnect should be allowed"); reconnectThrottle.reconnectAttemptFinished(); - assertFalse(reconnectThrottle.initiateReconnect(new NodeId(1)), "reconnect should be blocked"); + assertFalse(reconnectThrottle.initiateReconnect(NodeId.of(1)), "reconnect should be blocked"); reconnectThrottle.setCurrentTime(() -> Instant.ofEpochMilli(2000)); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(0)), "reconnect should be allowed"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(0)), "reconnect should be allowed"); reconnectThrottle.reconnectAttemptFinished(); - assertTrue(reconnectThrottle.initiateReconnect(new NodeId(1)), "reconnect should be allowed"); + assertTrue(reconnectThrottle.initiateReconnect(NodeId.of(1)), "reconnect should be allowed"); reconnectThrottle.reconnectAttemptFinished(); } @@ -99,7 +99,7 @@ void manyNodeTest() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 100; j++) { // Each request is for a unique node - reconnectThrottle.initiateReconnect(new NodeId((i + 1000) * (j + 1))); + reconnectThrottle.initiateReconnect(NodeId.of((i + 1000) * (j + 1))); reconnectThrottle.reconnectAttemptFinished(); assertTrue( diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java index 6e2cae0a7713..3ca6cb49546b 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SavedStateMetadataTests.java @@ -90,7 +90,7 @@ private SavedStateMetadata serializeDeserialize(final SavedStateMetadata metadat /** generates a random non-negative node id. */ private NodeId generateRandomNodeId(@NonNull final Random random) { Objects.requireNonNull(random, "random must not be null"); - return new NodeId(random.nextLong(Long.MAX_VALUE)); + return NodeId.of(random.nextLong(Long.MAX_VALUE)); } @Test @@ -217,11 +217,11 @@ void signingNodesSortedTest() { when(platformState.getAddressBook()).thenReturn(addressBook); when(signedState.getSigSet()).thenReturn(sigSet); when(sigSet.getSigningNodes()) - .thenReturn(new ArrayList<>(List.of(new NodeId(3L), new NodeId(1L), new NodeId(2L)))); + .thenReturn(new ArrayList<>(List.of(NodeId.of(3L), NodeId.of(1L), NodeId.of(2L)))); - final SavedStateMetadata metadata = SavedStateMetadata.create(signedState, new NodeId(1234), Instant.now()); + final SavedStateMetadata metadata = SavedStateMetadata.create(signedState, NodeId.of(1234), Instant.now()); - assertEquals(List.of(new NodeId(1L), new NodeId(2L), new NodeId(3L)), metadata.signingNodes()); + assertEquals(List.of(NodeId.of(1L), NodeId.of(2L), NodeId.of(3L)), metadata.signingNodes()); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java index ff6b501a241f..b3f598340ba5 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SignedStateFileReadWriteTest.java @@ -20,10 +20,11 @@ import static com.swirlds.platform.state.snapshot.SignedStateFileReader.readStateFile; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.CURRENT_ADDRESS_BOOK_FILE_NAME; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.HASH_INFO_FILE_NAME; +import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNATURE_SET_FILE_NAME; import static com.swirlds.platform.state.snapshot.SignedStateFileUtils.SIGNED_STATE_FILE_NAME; import static com.swirlds.platform.state.snapshot.SignedStateFileWriter.writeHashInfoFile; +import static com.swirlds.platform.state.snapshot.SignedStateFileWriter.writeSignatureSetFile; import static com.swirlds.platform.state.snapshot.SignedStateFileWriter.writeSignedStateToDisk; -import static com.swirlds.platform.state.snapshot.SignedStateFileWriter.writeStateFile; import static java.nio.file.Files.exists; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -36,6 +37,7 @@ import com.swirlds.common.constructable.ConstructableRegistry; import com.swirlds.common.constructable.ConstructableRegistryException; import com.swirlds.common.context.PlatformContext; +import com.swirlds.common.io.streams.MerkleDataOutputStream; import com.swirlds.common.io.utility.LegacyTemporaryFileBuilder; import com.swirlds.common.merkle.crypto.MerkleCryptoFactory; import com.swirlds.common.merkle.utility.MerkleTreeVisualizer; @@ -53,10 +55,15 @@ import com.swirlds.platform.system.BasicSoftwareVersion; import com.swirlds.platform.test.fixtures.state.FakeMerkleStateLifecycles; import com.swirlds.platform.test.fixtures.state.RandomSignedStateGenerator; +import com.swirlds.state.State; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -124,9 +131,64 @@ void writeHashInfoFileTest() throws IOException { void writeThenReadStateFileTest() throws IOException { final SignedState signedState = new RandomSignedStateGenerator().build(); final Path stateFile = testDirectory.resolve(SIGNED_STATE_FILE_NAME); + final Path signatureSetFile = testDirectory.resolve(SIGNATURE_SET_FILE_NAME); assertFalse(exists(stateFile), "signed state file should not yet exist"); - writeStateFile(testDirectory, signedState); + assertFalse(exists(signatureSetFile), "signature set file should not yet exist"); + + State state = (State) signedState.getState(); + state.copy(); + state.createSnapshot(testDirectory); + writeSignatureSetFile(testDirectory, signedState); + + assertTrue(exists(stateFile), "signed state file should be present"); + assertTrue(exists(signatureSetFile), "signature set file should be present"); + + final DeserializedSignedState deserializedSignedState = + readStateFile(TestPlatformContextBuilder.create().build(), stateFile, SignedStateFileUtils::readState); + MerkleCryptoFactory.getInstance() + .digestTreeSync( + deserializedSignedState.reservedSignedState().get().getState()); + + assertNotNull(deserializedSignedState.originalHash(), "hash should not be null"); + assertEquals(signedState.getState().getHash(), deserializedSignedState.originalHash(), "hash should match"); + assertEquals( + signedState.getState().getHash(), + deserializedSignedState.reservedSignedState().get().getState().getHash(), + "hash should match"); + assertNotSame(signedState, deserializedSignedState.reservedSignedState(), "state should be a different object"); + } + + @Test + @DisplayName("Write Then Read State File (protocol v1) Test") + void writeThenReadStateFileTest_v1() throws IOException { + final SignedState signedState = new RandomSignedStateGenerator().build(); + final Path stateFile = testDirectory.resolve(SIGNED_STATE_FILE_NAME); + final Path signatureSetFile = testDirectory.resolve(SIGNATURE_SET_FILE_NAME); + + assertFalse(exists(stateFile), "signed state file should not yet exist"); + assertFalse(exists(signatureSetFile), "signature set file should not yet exist"); + + State state = (State) signedState.getState(); + state.copy(); + state.createSnapshot(testDirectory); + + // now we need to emulate v1 by modifying the protocol version and appending signatures to the state file + final byte[] fileContent = Files.readAllBytes(stateFile); + final int fileVersionOffset = 1; + ByteBuffer buffer = ByteBuffer.wrap(fileContent); + buffer.position(fileVersionOffset); + // set the protocol version to v1 + buffer.putInt(1); + try (OutputStream out = Files.newOutputStream( + stateFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + MerkleDataOutputStream merkleOut = new MerkleDataOutputStream(out)) { + // Write the modified content back to the file + out.write(fileContent); + // And append the signature set + merkleOut.writeSerializable(signedState.getSigSet(), true); + } + assertTrue(exists(stateFile), "signed state file should be present"); final DeserializedSignedState deserializedSignedState = @@ -162,8 +224,11 @@ void writeSavedStateToDiskTest() throws IOException { .withConfiguration(configuration) .build(); + // make immutable + signedState.getSwirldState().copy(); + writeSignedStateToDisk( - platformContext, new NodeId(0), directory, signedState, StateToDiskReason.PERIODIC_SNAPSHOT); + platformContext, NodeId.of(0), directory, signedState, StateToDiskReason.PERIODIC_SNAPSHOT); assertTrue(exists(stateFile), "state file should exist"); assertTrue(exists(hashInfoFile), "hash info file should exist"); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java index c81b87111226..bb8ae2ec9fd5 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/StateFileManagerTests.java @@ -84,7 +84,7 @@ class StateFileManagerTests { - private static final NodeId SELF_ID = new NodeId(1234); + private static final NodeId SELF_ID = NodeId.of(1234); private static final String MAIN_CLASS_NAME = "com.swirlds.foobar"; private static final String SWIRLD_NAME = "mySwirld"; @@ -170,6 +170,7 @@ private void validateSavingOfState(final SignedState originalState, final Path s @DisplayName("Standard Operation Test") void standardOperationTest(final boolean successExpected) throws IOException { final SignedState signedState = new RandomSignedStateGenerator().build(); + makeImmutable(signedState); if (!successExpected) { // To make the save fail, create a file with the name of the directory the state will try to be saved to @@ -202,6 +203,7 @@ void saveFatalSignedState() throws InterruptedException, IOException { final StateSnapshotManager manager = new DefaultStateSnapshotManager(context, MAIN_CLASS_NAME, SELF_ID, SWIRLD_NAME); signedState.markAsStateToSave(FATAL_ERROR); + makeImmutable(signedState); final Thread thread = new ThreadConfiguration(getStaticThreadManager()) .setInterruptableRunnable( @@ -228,6 +230,7 @@ void saveISSignedState() throws IOException { final StateSnapshotManager manager = new DefaultStateSnapshotManager(context, MAIN_CLASS_NAME, SELF_ID, SWIRLD_NAME); signedState.markAsStateToSave(ISS); + makeImmutable(signedState); manager.dumpStateTask(StateDumpRequest.create(signedState.reserve("test"))); final Path stateDirectory = testDirectory.resolve("iss").resolve("node1234_round" + signedState.getRound()); @@ -304,6 +307,7 @@ void sequenceOfStatesTest(final boolean startAtGenesis) throws IOException { final ReservedSignedState reservedSignedState = signedState.reserve("initialTestReservation"); controller.markSavedState(new StateAndRound(reservedSignedState, mock(ConsensusRound.class))); + makeImmutable(reservedSignedState.get()); if (signedState.isStateToSave()) { assertTrue( @@ -396,6 +400,7 @@ void stateDeletionTest() throws IOException { .resolve("node" + SELF_ID + "_round" + issRound); final SignedState issState = new RandomSignedStateGenerator(random).setRound(issRound).build(); + makeImmutable(issState); issState.markAsStateToSave(ISS); manager.dumpStateTask(StateDumpRequest.create(issState.reserve("test"))); validateSavingOfState(issState, issDirectory); @@ -408,6 +413,7 @@ void stateDeletionTest() throws IOException { .resolve("node" + SELF_ID + "_round" + fatalRound); final SignedState fatalState = new RandomSignedStateGenerator(random).setRound(fatalRound).build(); + makeImmutable(fatalState); fatalState.markAsStateToSave(FATAL_ERROR); manager.dumpStateTask(StateDumpRequest.create(fatalState.reserve("test"))); validateSavingOfState(fatalState, fatalDirectory); @@ -419,6 +425,7 @@ void stateDeletionTest() throws IOException { new RandomSignedStateGenerator(random).setRound(round).build(); issState.markAsStateToSave(PERIODIC_SNAPSHOT); states.add(signedState); + makeImmutable(signedState); manager.saveStateTask(signedState.reserve("test")); // Verify that the states we want to be on disk are still on disk @@ -441,4 +448,8 @@ void stateDeletionTest() throws IOException { validateSavingOfState(fatalState, fatalDirectory); } } + + private static void makeImmutable(SignedState signedState) { + signedState.getState().copy(); + } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SyncManagerTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SyncManagerTest.java index 9732e787cd8a..eb5f37e25f0c 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SyncManagerTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/SyncManagerTest.java @@ -31,8 +31,6 @@ import com.swirlds.common.test.fixtures.platform.TestPlatformContextBuilder; import com.swirlds.config.api.Configuration; import com.swirlds.config.extensions.test.fixtures.TestConfigBuilder; -import com.swirlds.platform.eventhandling.EventConfig; -import com.swirlds.platform.eventhandling.EventConfig_; import com.swirlds.platform.gossip.FallenBehindManagerImpl; import com.swirlds.platform.gossip.sync.SyncManagerImpl; import com.swirlds.platform.network.PeerInfo; @@ -41,6 +39,7 @@ import com.swirlds.platform.pool.TransactionPoolNexus; import com.swirlds.platform.system.address.AddressBook; import com.swirlds.platform.system.status.StatusActionSubmitter; +import com.swirlds.platform.test.fixtures.addressbook.RandomAddressBookBuilder; import java.util.List; import java.util.Random; import org.junit.jupiter.api.MethodOrderer; @@ -56,7 +55,6 @@ class SyncManagerTest { * A helper class that contains dummy data to feed into SyncManager lambdas. */ private static class SyncManagerTestData { - public DummyHashgraph hashgraph; public AddressBook addressBook; public NodeId selfId; public TransactionPoolNexus transactionPoolNexus; @@ -65,36 +63,32 @@ private static class SyncManagerTestData { public SyncManagerTestData() { final Random random = getRandomPrintSeed(); - hashgraph = new DummyHashgraph(random, 0); final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); transactionPoolNexus = spy(new TransactionPoolNexus(platformContext)); - this.addressBook = hashgraph.getAddressBook(); + this.addressBook = + RandomAddressBookBuilder.create(random).withSize(41).build(); this.selfId = addressBook.getNodeId(0); configuration = new TestConfigBuilder() .withValue(ReconnectConfig_.FALLEN_BEHIND_THRESHOLD, "0.25") - .withValue(EventConfig_.EVENT_INTAKE_QUEUE_THROTTLE_SIZE, "100") .getOrCreateConfig(); final ReconnectConfig reconnectConfig = configuration.getConfigData(ReconnectConfig.class); - final EventConfig eventConfig = configuration.getConfigData(EventConfig.class); final List peers = Utilities.createPeerInfoList(addressBook, selfId); final NetworkTopology topology = new StaticTopology(peers, selfId); syncManager = new SyncManagerImpl( platformContext, - hashgraph::getEventIntakeQueueSize, new FallenBehindManagerImpl( addressBook, selfId, topology, mock(StatusActionSubmitter.class), () -> {}, - reconnectConfig), - eventConfig); + reconnectConfig)); } } @@ -150,42 +144,4 @@ void basicTest() { // we should now be back where we started assertFalse(test.syncManager.hasFallenBehind()); } - - /** - * Test when the SyncManager should accept an incoming sync - */ - @Test - @Order(1) - void shouldAcceptSyncTest() { - final SyncManagerTestData test = new SyncManagerTestData(); - - // We should accept a sync if the event queue is empty and we aren't exceeding the maximum number of syncs - test.hashgraph.eventIntakeQueueSize = 0; - assertTrue(test.syncManager.shouldAcceptSync()); - - // We should not accept a sync if the event queue fills up - test.hashgraph.eventIntakeQueueSize = 101; - assertFalse(test.syncManager.shouldAcceptSync()); - test.hashgraph.eventIntakeQueueSize = 0; - - // Once the queue and concurrent syncs decrease we should be able to sync again. - assertTrue(test.syncManager.shouldAcceptSync()); - } - - /** - * Test when the sync manager should initiate a sync of its own. - */ - @Test - @Order(2) - void shouldInitiateSyncTest() { - final SyncManagerTestData test = new SyncManagerTestData(); - - // It is ok to initiate a sync if the intake queue is not full. - test.hashgraph.eventIntakeQueueSize = 0; - assertTrue(test.syncManager.shouldInitiateSync()); - - // It is not ok to initiate a sync if the intake queue is full. - test.hashgraph.eventIntakeQueueSize = 101; - assertFalse(test.syncManager.shouldInitiateSync()); - } } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java index 46b50281f948..2f80b21c8bad 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/config/legacy/LegacyConfigPropertiesLoaderTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.swirlds.common.platform.NodeId; import com.swirlds.platform.system.address.Address; @@ -47,13 +48,11 @@ void testEmptyConfig() { final Path path = Paths.get( LegacyConfigPropertiesLoaderTest.class.getResource("empty.txt").getPath()); - // when - final LegacyConfigProperties properties = LegacyConfigPropertiesLoader.loadConfigFile(path); - - // then - assertNotNull(properties, "The properties should never be null"); - Assertions.assertFalse(properties.appConfig().isPresent(), "Value must not be set for an empty file"); - Assertions.assertFalse(properties.swirldName().isPresent(), "Value must not be set for an empty file"); + // when & then + // NK(2024-10-10): An empty file should be considered an invalid configuration file. The fact that this was + // previously considered a valid configuration file is a bug. + // The correct expected behavior is to throw a ConfigurationException. + assertThrows(ConfigurationException.class, () -> LegacyConfigPropertiesLoader.loadConfigFile(path)); } @Test diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java index 3d5805480198..bd8f9cb7abf8 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/EventDeduplicatorTests.java @@ -159,7 +159,7 @@ void standardOperation(final boolean useBirthRoundForAncientThreshold) { for (int i = 0; i < TEST_EVENT_COUNT; i++) { if (submittedEvents.isEmpty() || random.nextBoolean()) { // submit a brand new event half the time - final NodeId creatorId = new NodeId(random.nextInt(NODE_ID_COUNT)); + final NodeId creatorId = NodeId.of(random.nextInt(NODE_ID_COUNT)); final long eventGeneration = Math.max(0, minimumGenerationNonAncient + random.nextInt(-1, 10)); final long eventBirthRound = Math.max(ConsensusConstants.ROUND_FIRST, minimumRoundNonAncient + random.nextLong(-1, 4)); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java index 8091b1c2a187..5b6be0250828 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/FutureEventBufferTests.java @@ -76,7 +76,7 @@ void futureEventsBufferedTest() { for (int i = 0; i < count; i++) { final PlatformEvent event = new TestingEventBuilder(random) .setBirthRound(random.nextLong(1, maxFutureRound)) - .setCreatorId(new NodeId(random.nextInt(100))) + .setCreatorId(NodeId.of(random.nextInt(100))) .setTimeCreated(randomInstant(random)) .build(); events.add(event); @@ -161,7 +161,7 @@ void eventsGoAncientWhileBufferedTest() { for (int i = 0; i < count; i++) { final PlatformEvent event = new TestingEventBuilder(random) .setBirthRound(random.nextLong(1, maxFutureRound)) - .setCreatorId(new NodeId(random.nextInt(100))) + .setCreatorId(NodeId.of(random.nextInt(100))) .setTimeCreated(randomInstant(random)) .build(); events.add(event); @@ -220,7 +220,7 @@ void eventInBufferIsReleasedOnTimeTest() { final long eventBirthRound = pendingConsensusRound + roundsUntilRelease; final PlatformEvent event = new TestingEventBuilder(random) .setBirthRound(eventBirthRound) - .setCreatorId(new NodeId(random.nextInt(100))) + .setCreatorId(NodeId.of(random.nextInt(100))) .setTimeCreated(randomInstant(random)) .build(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/ConsensusEventLinkerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/ConsensusEventLinkerTests.java index b3eba2c37460..cb2cd2623557 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/ConsensusEventLinkerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/ConsensusEventLinkerTests.java @@ -73,7 +73,7 @@ void eventsAreUnlinkedTest(final boolean birthRoundAncientMode) { new StandardEventSource()); final List linkedEvents = new LinkedList<>(); - final InOrderLinker linker = new ConsensusLinker(platformContext, new NodeId(0)); + final InOrderLinker linker = new ConsensusLinker(platformContext, NodeId.of(0)); EventWindow eventWindow = EventWindow.getGenesisEventWindow(ancientMode); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/InOrderLinkerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/InOrderLinkerTests.java index dc20b54829bb..f03302c3ad43 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/InOrderLinkerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/linking/InOrderLinkerTests.java @@ -54,8 +54,8 @@ class InOrderLinkerTests { private FakeTime time; - private NodeId selfId = new NodeId(0); - private NodeId otherId = new NodeId(1); + private NodeId selfId = NodeId.of(0); + private NodeId otherId = NodeId.of(1); /** * Set up the in order linker for testing diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/orphan/OrphanBufferTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/orphan/OrphanBufferTests.java index c308d2aef32f..27ba540474e1 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/orphan/OrphanBufferTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/orphan/OrphanBufferTests.java @@ -129,7 +129,7 @@ private PlatformEvent createBootstrapEvent( private PlatformEvent createRandomEvent( @NonNull final List parentCandidates, @NonNull final Map tips) { - final NodeId eventCreator = new NodeId(random.nextInt(NODE_ID_COUNT)); + final NodeId eventCreator = NodeId.of(random.nextInt(NODE_ID_COUNT)); final PlatformEvent selfParent = tips.computeIfAbsent(eventCreator, creator -> createBootstrapEvent(creator, parentCandidates)); @@ -283,13 +283,13 @@ void testParentIterator() { final Random random = Randotron.create(); final PlatformEvent selfParent = - new TestingEventBuilder(random).setCreatorId(new NodeId(0)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(0)).build(); final PlatformEvent otherParent1 = - new TestingEventBuilder(random).setCreatorId(new NodeId(1)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(1)).build(); final PlatformEvent otherParent2 = - new TestingEventBuilder(random).setCreatorId(new NodeId(2)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(2)).build(); final PlatformEvent otherParent3 = - new TestingEventBuilder(random).setCreatorId(new NodeId(3)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(3)).build(); final PlatformEvent event = new TestingEventBuilder(random) .setSelfParent(selfParent) diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/stale/StaleEventDetectorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/stale/StaleEventDetectorTests.java index bbd6e482cf65..6f612865ea1f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/stale/StaleEventDetectorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/event/stale/StaleEventDetectorTests.java @@ -104,7 +104,7 @@ private List getStaleSelfEvents(@NonNull final List payloads = leafNodes.subList(0, payloadCount); final Map signatures = new HashMap<>(); for (int i = 0; i < signatureCount; i++) { - final NodeId nodeId = new NodeId(random.nextLong(1, 1000)); + final NodeId nodeId = NodeId.of(random.nextLong(1, 1000)); final Signature signature = randomSignature(random); signatures.put(nodeId, signature); } @@ -426,7 +426,7 @@ void roundTripSerializationTest() throws IOException { final List payloads = leafNodes.subList(0, payloadCount); final Map signatures = new HashMap<>(); for (int i = 0; i < signatureCount; i++) { - final NodeId nodeId = new NodeId(random.nextLong(1, 1000)); + final NodeId nodeId = NodeId.of(random.nextLong(1, 1000)); final Signature signature = randomSignature(random); signatures.put(nodeId, signature); } @@ -549,7 +549,7 @@ void signatureNotInAddressBookTest() { assertFalse(stateProofA.isValid(cryptography, addressBook, SUPER_MAJORITY, signatureBuilder)); // Adding a signature for a node not in the address book should not change the result. - final NodeId nodeId = new NodeId(10000000); + final NodeId nodeId = NodeId.of(10000000); assertFalse(addressBook.contains(nodeId)); final Signature signature = randomSignature(random); signatures.put(nodeId, signature); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java index e0fd1ded3c6b..a7ecfdcc17c0 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/DefaultSignedStateValidatorTests.java @@ -143,7 +143,7 @@ private static List initRandomizedNodes(final RandomGenerator r) { // Allow zero-weight final int weight = r.nextInt(MAX_WEIGHT_PER_NODE); final boolean hasValidSig = r.nextBoolean(); - nodes.add(new Node(new NodeId(i), weight, hasValidSig)); + nodes.add(new Node(NodeId.of(i), weight, hasValidSig)); } return nodes; } @@ -213,13 +213,13 @@ private static List initNodes(final List isValidSigList) { } final List nodes = new ArrayList<>(NUM_NODES_IN_STATIC_TESTS); - nodes.add(new Node(new NodeId(0L), 5L, isValidSigList.get(0))); - nodes.add(new Node(new NodeId(1L), 5L, isValidSigList.get(1))); - nodes.add(new Node(new NodeId(2L), 8L, isValidSigList.get(2))); - nodes.add(new Node(new NodeId(3L), 15L, isValidSigList.get(3))); - nodes.add(new Node(new NodeId(4L), 17L, isValidSigList.get(4))); - nodes.add(new Node(new NodeId(5L), 10L, isValidSigList.get(5))); - nodes.add(new Node(new NodeId(6L), 30L, isValidSigList.get(6))); + nodes.add(new Node(NodeId.of(0L), 5L, isValidSigList.get(0))); + nodes.add(new Node(NodeId.of(1L), 5L, isValidSigList.get(1))); + nodes.add(new Node(NodeId.of(2L), 8L, isValidSigList.get(2))); + nodes.add(new Node(NodeId.of(3L), 15L, isValidSigList.get(3))); + nodes.add(new Node(NodeId.of(4L), 17L, isValidSigList.get(4))); + nodes.add(new Node(NodeId.of(5L), 10L, isValidSigList.get(5))); + nodes.add(new Node(NodeId.of(6L), 30L, isValidSigList.get(6))); return nodes; } @@ -322,7 +322,7 @@ private SignedState stateSignedByNodes(final List signingNodes) { if (signature.getByte(0) == 0) { return false; } - final Hash hash = new Hash(data.toByteArray(), stateHash.getDigestType()); + final Hash hash = new Hash(data, stateHash.getDigestType()); return hash.equals(stateHash); }; diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectProtocolTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectProtocolTests.java index 11bbf0f87805..014e5df044ea 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectProtocolTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectProtocolTests.java @@ -71,7 +71,7 @@ */ class ReconnectProtocolTests { private final Configuration configuration = new TestConfigBuilder().getOrCreateConfig(); - private static final NodeId PEER_ID = new NodeId(1L); + private static final NodeId PEER_ID = NodeId.of(1L); private ReconnectController reconnectController; private ReconnectThrottle teacherThrottle; @@ -145,7 +145,7 @@ void shouldInitiateTest(final InitiateParams params) { final List neighborsForReconnect = LongStream.range(0L, 10L) .filter(id -> id != PEER_ID.id() || params.isReconnectNeighbor) - .mapToObj(NodeId::new) + .mapToObj(NodeId::of) .toList(); final FallenBehindManager fallenBehindManager = mock(FallenBehindManager.class); @@ -281,8 +281,8 @@ void testTeacherThrottleReleased() { final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final NodeId node1 = new NodeId(1L); - final NodeId node2 = new NodeId(2L); + final NodeId node1 = NodeId.of(1L); + final NodeId node2 = NodeId.of(2L); final ReconnectProtocol peer1 = new ReconnectProtocol( platformContext, getStaticThreadManager(), @@ -362,7 +362,7 @@ void abortedLearner() { fallenBehindManager, () -> ACTIVE, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertTrue(protocol.shouldInitiate()); protocol.initiateFailed(); @@ -407,7 +407,7 @@ void abortedTeacher() { () -> ACTIVE, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertTrue(protocol.shouldAccept()); protocol.acceptFailed(); @@ -444,7 +444,7 @@ void teacherHasNoSignedState() { fallenBehindManager, () -> ACTIVE, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertFalse(protocol.shouldAccept()); } @@ -474,7 +474,7 @@ void teacherNotActive() { fallenBehindManager, () -> PlatformStatus.CHECKING, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertFalse(protocol.shouldAccept()); } @@ -500,7 +500,7 @@ void teacherHoldsLearnerPermit() { mock(FallenBehindManager.class), () -> ACTIVE, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertTrue(protocol.shouldAccept()); verify(reconnectController, times(1)).blockLearnerPermit(); @@ -546,7 +546,7 @@ void teacherCantAcquireLearnerPermit() { mock(FallenBehindManager.class), () -> ACTIVE, configuration); - final Protocol protocol = reconnectProtocolFactory.build(new NodeId(0)); + final Protocol protocol = reconnectProtocolFactory.build(NodeId.of(0)); assertFalse(protocol.shouldAccept()); verify(reconnectController, times(1)).blockLearnerPermit(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectTest.java index e9505946bff8..46c34c211e6d 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/reconnect/ReconnectTest.java @@ -109,7 +109,7 @@ private void executeReconnect(final ReconnectMetrics reconnectMetrics) throws In final long weightPerNode = 100L; final int numNodes = 4; final List nodeIds = - IntStream.range(0, numNodes).mapToObj(NodeId::new).toList(); + IntStream.range(0, numNodes).mapToObj(NodeId::of).toList(); final Random random = RandomUtils.getRandomPrintSeed(); final AddressBook addressBook = RandomAddressBookBuilder.create(random) @@ -158,7 +158,7 @@ private AddressBook buildAddressBook(final int numAddresses) { for (int i = 0; i < numAddresses; i++) { final Address address = mock(Address.class); when(address.getSigPublicKey()).thenReturn(publicKey); - when(address.getNodeId()).thenReturn(new NodeId(i)); + when(address.getNodeId()).thenReturn(NodeId.of(i)); addresses.add(address); } return new AddressBook(addresses); @@ -173,8 +173,8 @@ private ReconnectTeacher buildSender( final PlatformContext platformContext = TestPlatformContextBuilder.create().build(); - final NodeId selfId = new NodeId(0); - final NodeId otherId = new NodeId(3); + final NodeId selfId = NodeId.of(0); + final NodeId otherId = NodeId.of(3); final long lastRoundReceived = 100; return new ReconnectTeacher( platformContext, diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/RecoveryTestUtils.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/RecoveryTestUtils.java index 58bddcdd417d..2c8baae63a4f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/RecoveryTestUtils.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/recovery/RecoveryTestUtils.java @@ -80,10 +80,10 @@ public static CesEvent generateRandomEvent( .setTransactionSize(random.nextInt(10) + 1) .setSystemTransactionCount(0) .setSelfParent(new TestingEventBuilder(random) - .setCreatorId(new NodeId(random.nextLong(0, Long.MAX_VALUE))) + .setCreatorId(NodeId.of(random.nextLong(0, Long.MAX_VALUE))) .build()) .setOtherParent(new TestingEventBuilder(random) - .setCreatorId(new NodeId(random.nextLong(0, Long.MAX_VALUE))) + .setCreatorId(NodeId.of(random.nextLong(0, Long.MAX_VALUE))) .build()) .setTimeCreated(now) .setConsensusTimestamp(now) @@ -163,7 +163,7 @@ public static void writeRandomEventStream( .build(); final DefaultConsensusEventStream eventStreamManager = new DefaultConsensusEventStream( - platformContext, new NodeId(0L), x -> randomSignature(random), "test", x -> false); + platformContext, NodeId.of(0L), x -> randomSignature(random), "test", x -> false); // The event stream writer has flaky asynchronous behavior, // so we need to be extra careful when waiting for it to finish. diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/scratchpad/ScratchpadTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/scratchpad/ScratchpadTests.java index 960c4e0c9db4..10fce8b24d66 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/scratchpad/ScratchpadTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/scratchpad/ScratchpadTests.java @@ -58,7 +58,7 @@ class ScratchpadTests { private PlatformContext platformContext; - private final NodeId selfId = new NodeId(0); + private final NodeId selfId = NodeId.of(0); @BeforeEach void beforeEach() throws IOException { @@ -120,7 +120,7 @@ void basicBehaviorTest() { assertEquals(long1, scratchpad.get(TestScratchpadType.BAR)); assertNull(scratchpad.get(TestScratchpadType.BAZ)); - final NodeId nodeId1 = new NodeId(random.nextInt(0, 1000)); + final NodeId nodeId1 = NodeId.of(random.nextInt(0, 1000)); assertNull(scratchpad.set(TestScratchpadType.BAZ, nodeId1)); assertEquals(1, scratchpadDirectory.toFile().listFiles().length); assertEquals(hash1, scratchpad.get(TestScratchpadType.FOO)); @@ -143,7 +143,7 @@ void basicBehaviorTest() { assertEquals(long2, scratchpad.get(TestScratchpadType.BAR)); assertEquals(nodeId1, scratchpad.get(TestScratchpadType.BAZ)); - final NodeId nodeId2 = new NodeId(random.nextInt(1001, 2000)); + final NodeId nodeId2 = NodeId.of(random.nextInt(1001, 2000)); assertEquals(nodeId1, scratchpad.set(TestScratchpadType.BAZ, nodeId2)); assertEquals(1, scratchpadDirectory.toFile().listFiles().length); assertEquals(hash2, scratchpad.get(TestScratchpadType.FOO)); @@ -174,7 +174,7 @@ void basicBehaviorTest() { assertEquals(long3, scratchpad.get(TestScratchpadType.BAR)); assertNull(scratchpad.get(TestScratchpadType.BAZ)); - final NodeId nodeId3 = new NodeId(random.nextInt(2001, 3000)); + final NodeId nodeId3 = NodeId.of(random.nextInt(2001, 3000)); assertNull(scratchpad.set(TestScratchpadType.BAZ, nodeId3)); assertEquals(1, scratchpadDirectory.toFile().listFiles().length); assertEquals(hash3, scratchpad.get(TestScratchpadType.FOO)); @@ -269,7 +269,7 @@ void atomicOperationTest() { final Hash hash1 = randomHash(random); final SerializableLong long1 = new SerializableLong(random.nextLong()); - final NodeId nodeId1 = new NodeId(random.nextInt(0, 1000)); + final NodeId nodeId1 = NodeId.of(random.nextInt(0, 1000)); scratchpad.atomicOperation(map -> { assertNull(map.put(TestScratchpadType.FOO, hash1)); @@ -287,7 +287,7 @@ void atomicOperationTest() { final Hash hash2 = randomHash(random); final SerializableLong long2 = new SerializableLong(random.nextLong()); - final NodeId nodeId2 = new NodeId(random.nextInt(1001, 2000)); + final NodeId nodeId2 = NodeId.of(random.nextInt(1001, 2000)); scratchpad.atomicOperation(map -> { assertEquals(hash1, map.put(TestScratchpadType.FOO, hash2)); @@ -320,7 +320,7 @@ void atomicOperationTest() { final Hash hash3 = randomHash(random); final SerializableLong long3 = new SerializableLong(random.nextLong()); - final NodeId nodeId3 = new NodeId(random.nextInt(2001, 3000)); + final NodeId nodeId3 = NodeId.of(random.nextInt(2001, 3000)); scratchpad.atomicOperation(map -> { assertNull(map.put(TestScratchpadType.FOO, hash3)); @@ -375,7 +375,7 @@ void optionalAtomicOperationTest() { final Hash hash1 = randomHash(random); final SerializableLong long1 = new SerializableLong(random.nextLong()); - final NodeId nodeId1 = new NodeId(random.nextInt(0, 1000)); + final NodeId nodeId1 = NodeId.of(random.nextInt(0, 1000)); scratchpad.atomicOperation(map -> { assertNull(map.put(TestScratchpadType.FOO, hash1)); @@ -398,7 +398,7 @@ void optionalAtomicOperationTest() { final Hash hash2 = randomHash(random); final SerializableLong long2 = new SerializableLong(random.nextLong()); - final NodeId nodeId2 = new NodeId(random.nextInt(1001, 2000)); + final NodeId nodeId2 = NodeId.of(random.nextInt(1001, 2000)); scratchpad.atomicOperation(map -> { assertEquals(hash1, map.put(TestScratchpadType.FOO, hash2)); @@ -441,7 +441,7 @@ void optionalAtomicOperationTest() { final Hash hash3 = randomHash(random); final SerializableLong long3 = new SerializableLong(random.nextLong()); - final NodeId nodeId3 = new NodeId(random.nextInt(2001, 3000)); + final NodeId nodeId3 = NodeId.of(random.nextInt(2001, 3000)); scratchpad.atomicOperation(map -> { assertNull(map.put(TestScratchpadType.FOO, hash3)); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java index 2378e3c7ed94..d8314a0aaff9 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/MerkleStateRootTest.java @@ -50,6 +50,7 @@ import com.swirlds.common.merkle.MerkleNode; import com.swirlds.common.merkle.crypto.MerkleCryptography; import com.swirlds.common.merkle.crypto.MerkleCryptographyFactory; +import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.config.api.ConfigurationBuilder; import com.swirlds.merkle.map.MerkleMap; import com.swirlds.platform.state.service.PlatformStateService; @@ -1114,6 +1115,7 @@ void setUp() { final PlatformContext platformContext = mock(PlatformContext.class); when(platform.getContext()).thenReturn(platformContext); when(platformContext.getMerkleCryptography()).thenReturn(merkleCryptography); + when(platformContext.getMetrics()).thenReturn(new NoOpMetrics()); stateRoot.init(platform, InitTrigger.GENESIS, mock(SoftwareVersion.class)); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SigSetTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SigSetTests.java index ce4c357f8c87..a04728aafc34 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SigSetTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SigSetTests.java @@ -56,7 +56,7 @@ private static Map generateSignatureMap(final Random random) for (int i = 0; i < 1_000; i++) { // There will be a few duplicates, but that doesn't really matter - final NodeId nodeId = new NodeId(random.nextLong(0, 10_000)); + final NodeId nodeId = NodeId.of(random.nextLong(0, 10_000)); final Signature signature = randomSignature(random); signatures.put(nodeId, signature); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java index 2a001ba1020f..d12ba13d50f9 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/SwirldStateManagerTests.java @@ -57,7 +57,7 @@ void setup() { swirldStateManager = new SwirldStateManager( platformContext, addressBook, - new NodeId(0L), + NodeId.of(0L), mock(StatusActionSubmitter.class), new BasicSoftwareVersion(1)); swirldStateManager.setInitialState(initialState); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PbjConverterTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PbjConverterTest.java index 3bd4386a33fc..5fed5741fdc4 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PbjConverterTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/PbjConverterTest.java @@ -57,8 +57,8 @@ @SuppressWarnings("removal") class PbjConverterTest { - public static final NodeId NODE_ID_1 = new NodeId(1); - public static final NodeId NODE_ID_2 = new NodeId(2); + public static final NodeId NODE_ID_1 = NodeId.of(1); + public static final NodeId NODE_ID_2 = NodeId.of(2); private Randotron randotron; @BeforeEach diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/WritablePlatformStateStoreTest.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/WritablePlatformStateStoreTest.java index 39abf5543050..4a28f6f09e97 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/WritablePlatformStateStoreTest.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/service/WritablePlatformStateStoreTest.java @@ -19,6 +19,7 @@ import static com.swirlds.common.test.fixtures.RandomUtils.nextInt; import static com.swirlds.common.test.fixtures.RandomUtils.randomHash; import static com.swirlds.platform.state.service.PbjConverter.toPbjPlatformState; +import static com.swirlds.platform.state.service.PbjConverterTest.randomAddressBook; import static com.swirlds.platform.state.service.PbjConverterTest.randomPlatformState; import static com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema.PLATFORM_STATE_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -94,14 +95,14 @@ void verifyCreationSoftwareVersion() { @Test void verifyAddressBook() { - final var addressBook = PbjConverterTest.randomAddressBook(randotron); + final var addressBook = randomAddressBook(randotron); store.setAddressBook(addressBook); assertEquals(addressBook, store.getAddressBook()); } @Test void verifyPreviousAddressBook() { - final var addressBook = PbjConverterTest.randomAddressBook(randotron); + final var addressBook = randomAddressBook(randotron); store.setPreviousAddressBook(addressBook); assertEquals(addressBook, store.getPreviousAddressBook()); } diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java index 929c1be4af1b..74e03b151b26 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/state/signed/StartupStateUtilsTests.java @@ -76,7 +76,7 @@ public class StartupStateUtilsTests { private SignedStateFilePath signedStateFilePath; - private final NodeId selfId = new NodeId(0); + private final NodeId selfId = NodeId.of(0); private final String mainClassName = "mainClassName"; private final String swirldName = "swirldName"; @@ -136,6 +136,9 @@ private SignedState writeState( .setEpoch(epoch) .build(); + // make the state immutable + signedState.getState().copy(); + final Path savedStateDirectory = signedStateFilePath.getSignedStateDirectory(mainClassName, selfId, swirldName, round); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/sync/protocol/SyncProtocolFactoryTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/sync/protocol/SyncProtocolFactoryTests.java index ab9cbb5929a0..6d03cd1caa7f 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/sync/protocol/SyncProtocolFactoryTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/sync/protocol/SyncProtocolFactoryTests.java @@ -85,7 +85,7 @@ private static int countAvailablePermits(@NonNull final SyncPermitProvider permi @BeforeEach void setup() { - peerId = new NodeId(1); + peerId = NodeId.of(1); shadowGraphSynchronizer = mock(ShadowgraphSynchronizer.class); fallenBehindManager = mock(FallenBehindManager.class); @@ -101,7 +101,7 @@ void setup() { // node is not fallen behind Mockito.when(fallenBehindManager.hasFallenBehind()).thenReturn(false); // only peer with ID 1 is needed for fallen behind - Mockito.when(fallenBehindManager.getNeededForFallenBehind()).thenReturn(List.of(new NodeId(1L))); + Mockito.when(fallenBehindManager.getNeededForFallenBehind()).thenReturn(List.of(NodeId.of(1L))); } @Test @@ -114,7 +114,6 @@ void shouldInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -136,7 +135,6 @@ void initiateCooldown() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, Duration.ofMillis(100), syncMetrics, () -> ACTIVE); @@ -174,7 +172,6 @@ void incorrectStatusToInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> BEHIND); @@ -195,7 +192,6 @@ void noPermitAvailableToInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -221,7 +217,6 @@ void peerAgnosticChecksFailAtInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> true, - () -> true, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -245,7 +240,6 @@ void fallenBehindAtInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -268,7 +262,6 @@ void initiateForFallenBehind() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -290,11 +283,10 @@ void initiateForCriticalQuorum() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); - final Protocol protocol = syncProtocolFactory.build(new NodeId(6)); + final Protocol protocol = syncProtocolFactory.build(NodeId.of(6)); assertEquals(2, countAvailablePermits(permitProvider)); assertTrue(protocol.shouldInitiate()); @@ -316,7 +308,6 @@ void shouldAccept() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -338,7 +329,6 @@ void acceptCooldown() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, Duration.ofMillis(100), syncMetrics, () -> ACTIVE); @@ -377,7 +367,6 @@ void incorrectStatusToAccept() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> BEHIND); @@ -406,7 +395,6 @@ void noPermitAvailableToAccept() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -426,7 +414,6 @@ void peerAgnosticChecksFailAtAccept() { permitProvider, mock(IntakeEventCounter.class), () -> true, - () -> true, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -450,7 +437,6 @@ void fallenBehindAtAccept() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -471,7 +457,6 @@ void permitClosesAfterFailedAccept() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -494,7 +479,6 @@ void permitClosesAfterFailedInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -517,7 +501,6 @@ void successfulInitiatedProtocol() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -540,7 +523,6 @@ void successfulAcceptedProtocol() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -564,7 +546,6 @@ void rethrowParallelExecutionException() permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -594,7 +575,6 @@ void rethrowRootCauseIOException() permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -623,7 +603,6 @@ void rethrowSyncException() throws ParallelExecutionException, IOException, Sync permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); @@ -651,7 +630,6 @@ void acceptOnSimultaneousInitiate() { permitProvider, mock(IntakeEventCounter.class), () -> false, - () -> false, sleepAfterSync, syncMetrics, () -> ACTIVE); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/address/AddressBookTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/address/AddressBookTests.java index f997574c9dcc..ed85e0fb104e 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/address/AddressBookTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/address/AddressBookTests.java @@ -162,7 +162,7 @@ void validateAddressBookUpdateWeightTest() { @NonNull private static Address buildNextAddress(@NonNull final Random random, @NonNull final AddressBook addressBook) { return RandomAddressBuilder.create(random) - .withNodeId(new NodeId(addressBook.getNextAvailableNodeId().id() + random.nextInt(0, 3))) + .withNodeId(NodeId.of(addressBook.getNextAvailableNodeId().id() + random.nextInt(0, 3))) .build(); } @@ -283,8 +283,8 @@ void serializationTest() throws IOException, ConstructableRegistryException { "this.is.a.really.long.host.name.that.should.be.able.to.fit.in.the.address.book")); // make sure that certs are part of the round trip test. - assertNotNull(original.getAddress(new NodeId(0)).getSigCert()); - assertNotNull(original.getAddress(new NodeId(0)).getAgreeCert()); + assertNotNull(original.getAddress(NodeId.of(0)).getSigCert()); + assertNotNull(original.getAddress(NodeId.of(0)).getAgreeCert()); validateAddressBookConsistency(original); @@ -356,9 +356,9 @@ void outOfOrderAddTest() { // The address book has gaps. Make sure we can't insert anything into those gaps. for (int i = 0; i < addressBook.getNextAvailableNodeId().id(); i++) { - final Address address = buildNextAddress(randotron, addressBook).copySetNodeId(new NodeId(i)); + final Address address = buildNextAddress(randotron, addressBook).copySetNodeId(NodeId.of(i)); - if (addressBook.contains(new NodeId(i))) { + if (addressBook.contains(NodeId.of(i))) { // It's ok to update an existing address addressBook.add(address); } else { diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java index 4e7e87e3247d..d65ebcb253ac 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/system/events/BirthRoundMigrationShimTests.java @@ -43,7 +43,7 @@ private PlatformEvent buildEvent( final long generation, final long birthRound) { - final NodeId creatorId = new NodeId(random.nextLong(1, 10)); + final NodeId creatorId = NodeId.of(random.nextLong(1, 10)); final PlatformEvent selfParent = new TestingEventBuilder(random) .setCreatorId(creatorId) .setBirthRound(random.nextLong(birthRound - 2, birthRound + 1)) /* realistic range */ diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java index 070eefa88cc9..2f44d980b110 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/turtle/runner/TurtleNode.java @@ -32,7 +32,6 @@ import com.swirlds.platform.builder.PlatformComponentBuilder; import com.swirlds.platform.config.BasicConfig_; import com.swirlds.platform.crypto.KeysAndCerts; -import com.swirlds.platform.eventhandling.EventConfig_; import com.swirlds.platform.state.MerkleRoot; import com.swirlds.platform.state.snapshot.SignedStateFileUtils; import com.swirlds.platform.system.BasicSoftwareVersion; @@ -84,7 +83,6 @@ public class TurtleNode { @NonNull final SimulatedNetwork network) { final Configuration configuration = new TestConfigBuilder() - .withValue(EventConfig_.USE_OLD_STYLE_INTAKE_QUEUE, false) .withValue(PlatformSchedulersConfig_.CONSENSUS_EVENT_STREAM, "NO_OP") .withValue(BasicConfig_.JVM_PAUSE_DETECTOR_SLEEP_MS, "0") .getOrCreateConfig(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/uptime/UptimeTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/uptime/UptimeTests.java index 05d52276f011..2aa7b79bc496 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/uptime/UptimeTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/uptime/UptimeTests.java @@ -285,7 +285,7 @@ void roundScanChangingAddressBookTest() { final NodeId nodeToRemove = addressBook.getNodeId(0); newAddressBook.remove(nodeToRemove); final Address newAddress = - addressBook.getAddress(addressBook.getNodeId(0)).copySetNodeId(new NodeId(12345)); + addressBook.getAddress(addressBook.getNodeId(0)).copySetNodeId(NodeId.of(12345)); newAddressBook.add(newAddress); final Set noSecondRoundEvents = Set.of(); final Set noSecondRoundJudges = Set.of(); diff --git a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java index 5d5e6ae72e5a..1afc9a21996d 100644 --- a/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java +++ b/platform-sdk/swirlds-platform-core/src/test/java/com/swirlds/platform/util/AddressBookNetworkUtilsTests.java @@ -99,10 +99,10 @@ void testCreateRosterFromNonEmptyAddressBook() { "swirldName", new BasicSoftwareVersion(1), ReservedSignedState.createNullReservation(), - new NodeId(0)); + NodeId.of(0)); - final Address address1 = new Address(new NodeId(1), "", "", 10, null, 77, null, 88, null, null, ""); - final Address address2 = new Address(new NodeId(2), "", "", 10, null, 77, null, 88, null, null, ""); + final Address address1 = new Address(NodeId.of(1), "", "", 10, null, 77, null, 88, null, null, ""); + final Address address2 = new Address(NodeId.of(2), "", "", 10, null, 77, null, 88, null, null, ""); final AddressBook addressBook = new AddressBook(); addressBook.add(address1); addressBook.add(address2); @@ -130,7 +130,7 @@ void testCreateRosterFromEmptyAddressBook() { "swirldName", new BasicSoftwareVersion(1), ReservedSignedState.createNullReservation(), - new NodeId(0)); + NodeId.of(0)); final AddressBook addressBook = new AddressBook(); platformBuilder.withAddressBook(addressBook); final Roster roster = AddressBookUtils.createRoster(addressBook); diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/AddresBookUtils.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/AddresBookUtils.java index e0674891bad3..b85f0b16dada 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/AddresBookUtils.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/AddresBookUtils.java @@ -46,7 +46,7 @@ public static AddressBook createPretendBookFrom(final Platform platform, final b new SerializableX509Certificate(cert), ""); final var address2 = new Address( - new NodeId(1), + NodeId.of(1), "", "", 10L, diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/RandomAddressBuilder.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/RandomAddressBuilder.java index 8b3d0ccdc18b..eba550453381 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/RandomAddressBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/addressbook/RandomAddressBuilder.java @@ -74,7 +74,7 @@ public Address build() { // Future work: use randotron utility methods once randotron changes merge if (nodeId == null) { - nodeId = new NodeId(random.nextLong(0, Long.MAX_VALUE)); + nodeId = NodeId.of(random.nextLong(0, Long.MAX_VALUE)); } if (weight == null) { diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/crypto/PreGeneratedX509Certs.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/crypto/PreGeneratedX509Certs.java index 9bc5e8a20580..4d2903ad5e77 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/crypto/PreGeneratedX509Certs.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/crypto/PreGeneratedX509Certs.java @@ -147,7 +147,7 @@ public static SerializableX509Certificate getSigCert(final long nodeId) { } } long index = nodeId % sigCerts.size(); - return sigCerts.get(new NodeId(index)); + return sigCerts.get(NodeId.of(index)); } /** @@ -164,7 +164,7 @@ public static SerializableX509Certificate getAgreeCert(final long nodeId) { } } long index = nodeId % agreeCerts.size(); - return agreeCerts.get(new NodeId(index)); + return agreeCerts.get(NodeId.of(index)); } /** @@ -196,7 +196,7 @@ private static void loadCerts() { for (int i = 0; i < numSigCerts; i++) { SerializableX509Certificate sigCert = sigCertDis.readSerializable(false, SerializableX509Certificate::new); - sigCerts.put(new NodeId(i), sigCert); + sigCerts.put(NodeId.of(i), sigCert); } // load agreement certs @@ -204,7 +204,7 @@ private static void loadCerts() { for (int i = 0; i < numAgreeCerts; i++) { SerializableX509Certificate agreeCert = agreeCertDis.readSerializable(false, SerializableX509Certificate::new); - agreeCerts.put(new NodeId(i), agreeCert); + agreeCerts.put(NodeId.of(i), agreeCert); } } catch (final IOException e) { throw new IllegalStateException("critical failure in loading certificates", e); diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java index ff3959263955..4a643864c872 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/TestingEventBuilder.java @@ -53,7 +53,7 @@ public class TestingEventBuilder { private static final Instant DEFAULT_TIMESTAMP = Instant.ofEpochMilli(1588771316678L); private static final SoftwareVersion DEFAULT_SOFTWARE_VERSION = new BasicSoftwareVersion(1); - private static final NodeId DEFAULT_CREATOR_ID = new NodeId(0); + private static final NodeId DEFAULT_CREATOR_ID = NodeId.of(0); private static final int DEFAULT_APP_TRANSACTION_COUNT = 2; private static final int DEFAULT_SYSTEM_TRANSACTION_COUNT = 0; private static final int DEFAULT_TRANSACTION_SIZE = 4; diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/generator/StandardGraphGenerator.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/generator/StandardGraphGenerator.java index 94be7f4b1a26..c413717956c8 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/generator/StandardGraphGenerator.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/event/generator/StandardGraphGenerator.java @@ -197,7 +197,7 @@ private StandardGraphGenerator(final StandardGraphGenerator that, final long see private void initializeInternalConsensus() { consensus = new ConsensusImpl(platformContext, new NoOpConsensusMetrics(), addressBook); - inOrderLinker = new ConsensusLinker(platformContext, new NodeId(0)); + inOrderLinker = new ConsensusLinker(platformContext, NodeId.of(0)); } /** diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java index b02b3496446f..fc74ae3d315a 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/BlockingSwirldState.java @@ -31,6 +31,7 @@ import com.swirlds.platform.system.Round; import com.swirlds.platform.system.SwirldState; import com.swirlds.state.merkle.singleton.StringLeaf; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -84,9 +85,11 @@ public void handleConsensusRound(final Round round, final PlatformStateModifier /** * {@inheritDoc} */ + @NonNull @Override public BlockingSwirldState copy() { throwIfImmutable(); + setImmutable(true); return new BlockingSwirldState(this); } diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java index 4e0a63edf70c..24583d91829c 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/com/swirlds/platform/test/fixtures/state/RandomSignedStateGenerator.java @@ -22,6 +22,7 @@ import static com.swirlds.platform.test.fixtures.state.FakeMerkleStateLifecycles.FAKE_MERKLE_STATE_LIFECYCLES; import static com.swirlds.platform.test.fixtures.state.FakeMerkleStateLifecycles.registerMerkleStateRootClassIds; +import com.swirlds.base.time.Time; import com.swirlds.common.context.PlatformContext; import com.swirlds.common.crypto.Hash; import com.swirlds.common.crypto.Signature; @@ -133,6 +134,7 @@ public SignedState build() { stateInstance = new MerkleStateRoot( FAKE_MERKLE_STATE_LIFECYCLES, version -> new BasicSoftwareVersion(version.major())); } + ((MerkleStateRoot) stateInstance).setTime(Time.getCurrent()); } else { stateInstance = state; } diff --git a/platform-sdk/swirlds-platform-core/src/testFixtures/java/module-info.java b/platform-sdk/swirlds-platform-core/src/testFixtures/java/module-info.java index 23f5c89d7e14..360dfe6e78da 100644 --- a/platform-sdk/swirlds-platform-core/src/testFixtures/java/module-info.java +++ b/platform-sdk/swirlds-platform-core/src/testFixtures/java/module-info.java @@ -7,6 +7,7 @@ requires transitive com.swirlds.state.api.test.fixtures; requires transitive com.swirlds.state.api; requires transitive com.swirlds.virtualmap; + requires com.swirlds.base; requires com.swirlds.config.api; requires com.swirlds.config.extensions.test.fixtures; requires com.swirlds.logging; diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/State.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/State.java index 6b09ceef2b31..a26de1e45731 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/State.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/State.java @@ -25,6 +25,7 @@ import com.swirlds.state.spi.WritableStates; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.file.Path; /** * The full state used of the app. The primary implementation is based on a merkle tree, and the data @@ -77,6 +78,7 @@ default void unregisterCommitListener(@NonNull final StateChangeListener listene /** * {@inheritDoc} */ + @NonNull @Override default State copy() { throw new UnsupportedOperationException(); @@ -96,4 +98,12 @@ default Hash getHash() { default void computeHash() { throw new UnsupportedOperationException(); } + + /** + * Creates a snapshots for the state. The state has to be hashed and immutable before calling this method. + * @param targetPath The path to save the snapshot. + */ + default void createSnapshot(final @NonNull Path targetPath) { + throw new UnsupportedOperationException(); + } } diff --git a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/spi/ReadableKVState.java b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/spi/ReadableKVState.java index 06b155ec25cc..dfb596216235 100644 --- a/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/spi/ReadableKVState.java +++ b/platform-sdk/swirlds-state-api/src/main/java/com/swirlds/state/spi/ReadableKVState.java @@ -16,7 +16,6 @@ package com.swirlds.state.spi; -import com.hedera.pbj.runtime.Schema; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.util.Iterator; diff --git a/platform-sdk/swirlds-state-api/src/testFixtures/java/com/swirlds/state/test/fixtures/merkle/MerkleTestBase.java b/platform-sdk/swirlds-state-api/src/testFixtures/java/com/swirlds/state/test/fixtures/merkle/MerkleTestBase.java index 3b17773369cd..5704eb4e1cea 100644 --- a/platform-sdk/swirlds-state-api/src/testFixtures/java/com/swirlds/state/test/fixtures/merkle/MerkleTestBase.java +++ b/platform-sdk/swirlds-state-api/src/testFixtures/java/com/swirlds/state/test/fixtures/merkle/MerkleTestBase.java @@ -274,6 +274,7 @@ protected void setupConstructableRegistry() { registry.registerConstructables("com.swirlds.common"); registry.registerConstructables("com.swirlds.merkle"); registry.registerConstructables("com.swirlds.merkle.tree"); + registry.registerConstructables("com.swirlds.platform"); } catch (ConstructableRegistryException ex) { throw new AssertionError(ex); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java index eeaef37bbbb7..2b006d301a2d 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/consensus/TestIntake.java @@ -70,7 +70,7 @@ public class TestIntake { * @param addressBook the address book used by this intake */ public TestIntake(@NonNull final PlatformContext platformContext, @NonNull final AddressBook addressBook) { - final NodeId selfId = new NodeId(0); + final NodeId selfId = NodeId.of(0); roundsNonAncient = platformContext .getConfiguration() .getConfigData(ConsensusConfig.class) diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/simulated/config/MapBuilder.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/simulated/config/MapBuilder.java index 07801278742b..70ee82ee9847 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/simulated/config/MapBuilder.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/main/java/com/swirlds/platform/test/simulated/config/MapBuilder.java @@ -63,7 +63,7 @@ private MapBuilder() {} */ public @NonNull MapBuilder times(final int num) { for (int i = 0; i < num; i++) { - map.put(new NodeId(lastIndex++), lastElement); + map.put(NodeId.of(lastIndex++), lastElement); } return this; } @@ -75,7 +75,7 @@ private MapBuilder() {} */ public @NonNull Map build() { if (map.isEmpty()) { - map.put(new NodeId(lastIndex++), lastElement); + map.put(NodeId.of(lastIndex++), lastElement); } return map; } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SocketConnectionTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SocketConnectionTests.java index 4d880f07202d..a46df30a5358 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SocketConnectionTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/SocketConnectionTests.java @@ -51,8 +51,8 @@ class SocketConnectionTests { private final Configuration configuration = new TestConfigBuilder().getOrCreateConfig(); - private NodeId selfId = new NodeId(0L); - private NodeId otherId = new NodeId(1L); + private NodeId selfId = NodeId.of(0L); + private NodeId otherId = NodeId.of(1L); private Socket socket; private SyncInputStream dis; private SyncOutputStream dos; diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/GraphGeneratorTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/GraphGeneratorTests.java index 106c2520acf5..b7a10c23cf81 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/GraphGeneratorTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/consensus/GraphGeneratorTests.java @@ -673,16 +673,16 @@ public void otherParentAgeTests(final boolean birthRoundAsAncientThreshold) { events = generator.generateEvents(numberOfEvents); final HashSet excludedNodes = new HashSet<>(); - excludedNodes.add(new NodeId(3L)); + excludedNodes.add(NodeId.of(3L)); eventAges = gatherOtherParentAges(events, excludedNodes); assertAgeRatio(eventAges, 0, 0.95, 0.05); assertAgeRatio(eventAges, 1, 0.05 * 0.95, 0.05); assertAgeRatio(eventAges, 2, 0.05 * 0.05 * 0.95, 0.2); excludedNodes.clear(); - excludedNodes.add(new NodeId(0L)); - excludedNodes.add(new NodeId(1L)); - excludedNodes.add(new NodeId(2L)); + excludedNodes.add(NodeId.of(0L)); + excludedNodes.add(NodeId.of(1L)); + excludedNodes.add(NodeId.of(2L)); eventAges = gatherOtherParentAges(events, excludedNodes); assertAgeRatio(eventAges, 0, 0.5, 0.05); assertAgeRatio(eventAges, 1, 0.5 * 0.5, 0.05); @@ -704,7 +704,7 @@ public void otherParentAgeTests(final boolean birthRoundAsAncientThreshold) { events = generator.generateEvents(numberOfEvents); excludedNodes.clear(); - excludedNodes.add(new NodeId(3L)); + excludedNodes.add(NodeId.of(3L)); eventAges = gatherOtherParentAges(events, excludedNodes); assertAgeRatio(eventAges, 0, 0.666666, 0.05); assertAgeRatio(eventAges, 1, 0.0, 0.0); @@ -712,9 +712,9 @@ public void otherParentAgeTests(final boolean birthRoundAsAncientThreshold) { assertAgeRatio(eventAges, 3, 0.333333, 0.05); excludedNodes.clear(); - excludedNodes.add(new NodeId(0L)); - excludedNodes.add(new NodeId(1L)); - excludedNodes.add(new NodeId(2L)); + excludedNodes.add(NodeId.of(0L)); + excludedNodes.add(NodeId.of(1L)); + excludedNodes.add(NodeId.of(2L)); eventAges = gatherOtherParentAges(events, excludedNodes); assertAgeRatio(eventAges, 0, 1.0, 0.0); assertAgeRatio(eventAges, 1, 0.0, 0.0); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesBirthRoundMigrationTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesBirthRoundMigrationTests.java index 91ea6b3224e7..8b5da4982172 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesBirthRoundMigrationTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesBirthRoundMigrationTests.java @@ -261,7 +261,7 @@ void standardMigrationTest(final int discontinuity) throws IOException { final long migrationRound = random.nextLong(100, 1000); PcesBirthRoundMigration.migratePcesToBirthRoundMode( - platformContext, new NodeId(0), migrationRound, middleGeneration); + platformContext, NodeId.of(0), migrationRound, middleGeneration); // We should not find any generation based PCES files in the database directory. assertTrue(findPcesFiles(pcesPath, GENERATION_THRESHOLD).isEmpty()); @@ -313,7 +313,7 @@ void standardMigrationTest(final int discontinuity) throws IOException { } PcesBirthRoundMigration.migratePcesToBirthRoundMode( - platformContext, new NodeId(0), migrationRound, middleGeneration); + platformContext, NodeId.of(0), migrationRound, middleGeneration); final Set allFilesAfterSecondMigration = new HashSet<>(); try (final Stream stream = Files.walk(testDirectory)) { @@ -346,7 +346,7 @@ void genesisWithBirthRoundsTest() throws IOException { .build(); // should not throw - PcesBirthRoundMigration.migratePcesToBirthRoundMode(platformContext, new NodeId(0), ROUND_FIRST, -1); + PcesBirthRoundMigration.migratePcesToBirthRoundMode(platformContext, NodeId.of(0), ROUND_FIRST, -1); } @Test @@ -382,7 +382,7 @@ void botchedMigrationRecoveryTest() throws IOException { final long migrationRound = random.nextLong(1, 1000); PcesBirthRoundMigration.migratePcesToBirthRoundMode( - platformContext, new NodeId(0), migrationRound, middleGeneration); + platformContext, NodeId.of(0), migrationRound, middleGeneration); // Some funny business: copy the original files back into the PCES database directory. // This simulates a crash in the middle of the migration process after we have created @@ -402,7 +402,7 @@ void botchedMigrationRecoveryTest() throws IOException { // Run migration again. PcesBirthRoundMigration.migratePcesToBirthRoundMode( - platformContext, new NodeId(0), migrationRound, middleGeneration); + platformContext, NodeId.of(0), migrationRound, middleGeneration); // We should not find any generation based PCES files in the database directory. assertTrue(findPcesFiles(pcesPath, GENERATION_THRESHOLD).isEmpty()); @@ -455,7 +455,7 @@ void botchedMigrationRecoveryTest() throws IOException { } PcesBirthRoundMigration.migratePcesToBirthRoundMode( - platformContext, new NodeId(0), migrationRound, middleGeneration); + platformContext, NodeId.of(0), migrationRound, middleGeneration); final Set allFilesAfterSecondMigration = new HashSet<>(); try (final Stream stream = Files.walk(testDirectory)) { diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileManagerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileManagerTests.java index 396cb8636c2f..730c8d1f0932 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileManagerTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesFileManagerTests.java @@ -117,7 +117,7 @@ void generateDescriptorsWithManagerTest(@NonNull final AncientMode ancientMode) Instant timestamp = Instant.now(); final PcesFileManager generatingManager = - new PcesFileManager(platformContext, new PcesFileTracker(ancientMode), new NodeId(0), 0); + new PcesFileManager(platformContext, new PcesFileTracker(ancientMode), NodeId.of(0), 0); for (int i = 0; i < fileCount; i++) { final PcesFile file = generatingManager.getNextFileDescriptor(lowerBound, upperBound); @@ -183,7 +183,7 @@ void incrementalPruningByAncientBoundaryTest(@NonNull final AncientMode ancientM final PcesFileTracker fileTracker = PcesFileReader.readFilesFromDisk(platformContext, fileDirectory, 0, false, ancientMode); - final PcesFileManager manager = new PcesFileManager(platformContext, fileTracker, new NodeId(0), 0); + final PcesFileManager manager = new PcesFileManager(platformContext, fileTracker, NodeId.of(0), 0); assertIteratorEquality(files.iterator(), fileTracker.getFileIterator(NO_LOWER_BOUND, 0)); @@ -296,7 +296,7 @@ void incrementalPruningByTimestampTest(@NonNull final AncientMode ancientMode) t final PcesFileTracker fileTracker = PcesFileReader.readFilesFromDisk(platformContext, fileDirectory, 0, false, ancientMode); - final PcesFileManager manager = new PcesFileManager(platformContext, fileTracker, new NodeId(0), 0); + final PcesFileManager manager = new PcesFileManager(platformContext, fileTracker, NodeId.of(0), 0); assertIteratorEquality(files.iterator(), fileTracker.getFileIterator(NO_LOWER_BOUND, 0)); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java index 73b98f512277..ff2d65c03959 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/preconsensus/PcesWriterTests.java @@ -102,7 +102,7 @@ class PcesWriterTests { @TempDir Path testDirectory; - private final NodeId selfId = new NodeId(0); + private final NodeId selfId = NodeId.of(0); private final int numEvents = 1_000; protected static Stream buildArguments() { diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/ChildlessEventTrackerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/ChildlessEventTrackerTests.java index fe810b9b7503..1d0aeab47ff4 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/ChildlessEventTrackerTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/ChildlessEventTrackerTests.java @@ -72,7 +72,7 @@ void basicBehaviorTest() { // Adding some event with no parents final List batch1 = new ArrayList<>(); for (int i = 0; i < 10; i++) { - final EventDescriptorWrapper descriptor = newEventDescriptor(randomHash(random), new NodeId(i), 0); + final EventDescriptorWrapper descriptor = newEventDescriptor(randomHash(random), NodeId.of(i), 0); tracker.addEvent(descriptor, List.of()); batch1.add(descriptor); } @@ -85,13 +85,13 @@ void basicBehaviorTest() { final List batch2 = new ArrayList<>(); for (int i = 0; i < 10; i++) { - final NodeId nonExistentParentId = new NodeId(i + 100); + final NodeId nonExistentParentId = NodeId.of(i + 100); final EventDescriptorWrapper nonExistentParent = newEventDescriptor(randomHash(random), nonExistentParentId, 0); final int oddParentId = (i * 2 + 1) % 10; final EventDescriptorWrapper oddParent = batch1.get(oddParentId); - final EventDescriptorWrapper descriptor = newEventDescriptor(randomHash(), new NodeId(i), 1); + final EventDescriptorWrapper descriptor = newEventDescriptor(randomHash(), NodeId.of(i), 1); tracker.addEvent(descriptor, List.of(nonExistentParent, oddParent)); batch2.add(descriptor); } @@ -120,9 +120,9 @@ void branchingTest() { final ChildlessEventTracker tracker = new ChildlessEventTracker(); - final EventDescriptorWrapper e0 = newEventDescriptor(randomHash(random), new NodeId(0), 0); - final EventDescriptorWrapper e1 = newEventDescriptor(randomHash(random), new NodeId(0), 1); - final EventDescriptorWrapper e2 = newEventDescriptor(randomHash(random), new NodeId(0), 2); + final EventDescriptorWrapper e0 = newEventDescriptor(randomHash(random), NodeId.of(0), 0); + final EventDescriptorWrapper e1 = newEventDescriptor(randomHash(random), NodeId.of(0), 1); + final EventDescriptorWrapper e2 = newEventDescriptor(randomHash(random), NodeId.of(0), 2); tracker.addEvent(e0, List.of()); tracker.addEvent(e1, List.of(e0)); @@ -132,8 +132,8 @@ void branchingTest() { assertEquals(1, batch1.size()); assertEquals(e2, batch1.get(0)); - final EventDescriptorWrapper e3 = newEventDescriptor(randomHash(random), new NodeId(0), 3); - final EventDescriptorWrapper e3Branch = newEventDescriptor(randomHash(random), new NodeId(0), 3); + final EventDescriptorWrapper e3 = newEventDescriptor(randomHash(random), NodeId.of(0), 3); + final EventDescriptorWrapper e3Branch = newEventDescriptor(randomHash(random), NodeId.of(0), 3); // Branch with the same generation, existing event should not be discarded. tracker.addEvent(e3, List.of(e2)); @@ -144,14 +144,14 @@ void branchingTest() { assertEquals(e3, batch2.get(0)); // Branch with a lower generation, existing event should not be discarded. - final EventDescriptorWrapper e2Branch = newEventDescriptor(randomHash(random), new NodeId(0), 2); + final EventDescriptorWrapper e2Branch = newEventDescriptor(randomHash(random), NodeId.of(0), 2); tracker.addEvent(e2Branch, List.of(e1)); assertEquals(1, batch2.size()); assertEquals(e3, batch2.get(0)); // Branch with a higher generation, existing event should be discarded. - final EventDescriptorWrapper e99Branch = newEventDescriptor(randomHash(random), new NodeId(0), 99); + final EventDescriptorWrapper e99Branch = newEventDescriptor(randomHash(random), NodeId.of(0), 99); tracker.addEvent(e99Branch, List.of()); final List batch3 = tracker.getChildlessEvents(); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/EventCreationManagerTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/EventCreationManagerTests.java index 9460c147d544..bed2a7a0b492 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/EventCreationManagerTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/EventCreationManagerTests.java @@ -36,13 +36,10 @@ import com.swirlds.platform.system.status.PlatformStatus; import java.time.Duration; import java.util.List; -import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class EventCreationManagerTests { - private AtomicLong intakeQueueSize; private EventCreator creator; private List eventsToCreate; private FakeTime time; @@ -64,10 +61,7 @@ void setUp() { .withTime(time) .build(); - intakeQueueSize = new AtomicLong(0); - - manager = new DefaultEventCreationManager( - platformContext, mock(TransactionPoolNexus.class), intakeQueueSize::get, creator); + manager = new DefaultEventCreationManager(platformContext, mock(TransactionPoolNexus.class), creator); manager.updatePlatformStatus(PlatformStatus.ACTIVE); } @@ -116,32 +110,6 @@ void statusPreventsCreation() { assertSame(eventsToCreate.get(1), e1); } - /** - * This form of backpressure is not currently enabled. - */ - @Disabled - @Test - void backpressurePreventsCreation() { - final UnsignedEvent e0 = manager.maybeCreateEvent(); - verify(creator, times(1)).maybeCreateEvent(); - assertNotNull(e0); - assertSame(eventsToCreate.get(0), e0); - - time.tick(Duration.ofSeconds(1)); - intakeQueueSize.set(11); - - assertNull(manager.maybeCreateEvent()); - verify(creator, times(1)).maybeCreateEvent(); - - time.tick(Duration.ofSeconds(1)); - intakeQueueSize.set(9); - - final UnsignedEvent e1 = manager.maybeCreateEvent(); - assertNotNull(e1); - verify(creator, times(2)).maybeCreateEvent(); - assertSame(eventsToCreate.get(1), e1); - } - @Test void ratePreventsCreation() { final UnsignedEvent e0 = manager.maybeCreateEvent(); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/TipsetEventCreatorTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/TipsetEventCreatorTests.java index e453ebf4b742..9f76f261a691 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/TipsetEventCreatorTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/event/tipset/TipsetEventCreatorTests.java @@ -253,7 +253,7 @@ private EventImpl linkEvent( @NonNull final UnsignedEvent event) { eventCreators - .get(new NodeId(event.getEventCore().creatorNodeId())) + .get(NodeId.of(event.getEventCore().creatorNodeId())) .tipsetTracker .addEvent(event.getDescriptor(), event.getMetadata().getAllParents()); @@ -952,7 +952,7 @@ void notRegisteringEventsFromNodesNotInAddressBook() { final NodeId nodeC = addressBook.getNodeId(2); final NodeId nodeD = addressBook.getNodeId(3); // Node 4 (E) is not in the address book. - final NodeId nodeE = new NodeId(nodeD.id() + 1); + final NodeId nodeE = NodeId.of(nodeD.id() + 1); // All nodes except for node 0 are fully mocked. This test is testing how node 0 behaves. final EventCreator eventCreator = buildEventCreator(random, time, addressBook, nodeA, Collections::emptyList); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/graph/SimpleGraphs.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/graph/SimpleGraphs.java index 1f02e4c0c0c6..271e2a52a994 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/graph/SimpleGraphs.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/graph/SimpleGraphs.java @@ -40,20 +40,20 @@ public class SimpleGraphs { */ public static List graph5e2n(final Random random) { final PlatformEvent e0 = - new TestingEventBuilder(random).setCreatorId(new NodeId(1)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(1)).build(); final PlatformEvent e1 = - new TestingEventBuilder(random).setCreatorId(new NodeId(2)).build(); + new TestingEventBuilder(random).setCreatorId(NodeId.of(2)).build(); final PlatformEvent e2 = new TestingEventBuilder(random) - .setCreatorId(new NodeId(1)) + .setCreatorId(NodeId.of(1)) .setSelfParent(e0) .setOtherParent(e1) .build(); final PlatformEvent e3 = new TestingEventBuilder(random) - .setCreatorId(new NodeId(1)) + .setCreatorId(NodeId.of(1)) .setSelfParent(e2) .build(); final PlatformEvent e4 = new TestingEventBuilder(random) - .setCreatorId(new NodeId(2)) + .setCreatorId(NodeId.of(2)) .setSelfParent(e1) .setOtherParent(e2) .build(); @@ -85,7 +85,7 @@ public static List graph9e3n(final Random random) { // generation 0 final EventImpl e0 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(1)) + .setCreatorId(NodeId.of(1)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.680Z")), null, null); @@ -93,7 +93,7 @@ public static List graph9e3n(final Random random) { final EventImpl e1 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(2)) + .setCreatorId(NodeId.of(2)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.681Z")), null, null); @@ -101,7 +101,7 @@ public static List graph9e3n(final Random random) { final EventImpl e2 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(3)) + .setCreatorId(NodeId.of(3)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.682Z")), null, null); @@ -109,14 +109,14 @@ public static List graph9e3n(final Random random) { // generation 1 final EventImpl e3 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(1)) + .setCreatorId(NodeId.of(1)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.683Z")), e0, e1); final EventImpl e4 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(3)) + .setCreatorId(NodeId.of(3)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.686Z")), e2, null); @@ -124,21 +124,21 @@ public static List graph9e3n(final Random random) { // generation 2 final EventImpl e5 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(1)) + .setCreatorId(NodeId.of(1)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.685Z")), e3, null); final EventImpl e6 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(2)) + .setCreatorId(NodeId.of(2)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.686Z")), e1, e3); final EventImpl e7 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(3)) + .setCreatorId(NodeId.of(3)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.690Z")), e4, e1); @@ -146,7 +146,7 @@ public static List graph9e3n(final Random random) { // generation 3 final EventImpl e8 = createEventImpl( new TestingEventBuilder(random) - .setCreatorId(new NodeId(3)) + .setCreatorId(NodeId.of(3)) .setTimeCreated(Instant.parse("2020-05-06T13:21:56.694Z")), e7, e6); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/FakeConnection.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/FakeConnection.java index 0ce014d7cd68..14e75d583398 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/FakeConnection.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/FakeConnection.java @@ -29,7 +29,7 @@ public class FakeConnection implements Connection { private final NodeId peerId; public FakeConnection() { - this(new NodeId(0L), new NodeId(1)); + this(NodeId.of(0L), NodeId.of(1)); } public FakeConnection(final NodeId selfId, final NodeId peerId) { diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/OutboundConnectionManagerTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/OutboundConnectionManagerTest.java index 7110a9e05e36..f5c64b056faf 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/OutboundConnectionManagerTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/OutboundConnectionManagerTest.java @@ -40,7 +40,7 @@ class OutboundConnectionManagerTest { @Test void createConnectionTest() { - final NodeId nodeId = new NodeId(0L); + final NodeId nodeId = NodeId.of(0L); ; final Connection connection1 = new FakeConnection(); final Connection connection2 = new FakeConnection(); @@ -74,7 +74,7 @@ void createConnectionTest() { @Test void concurrencyTest() throws InterruptedException { final int numThreads = 10; - final NodeId nodeId = new NodeId(0L); + final NodeId nodeId = NodeId.of(0L); final OutboundConnectionCreator creator = mock(OutboundConnectionCreator.class); final Connection connection = new FakeConnection(); final CountDownLatch waitingForConnection = new CountDownLatch(1); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/HashHandshakeTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/HashHandshakeTests.java index b9955085b954..c8b3683dd50c 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/HashHandshakeTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/HashHandshakeTests.java @@ -63,7 +63,7 @@ void setup() throws ConstructableRegistryException, IOException { registry.registerConstructable(new ClassConstructorPair(SerializableLong.class, SerializableLong::new)); final Pair connections = - ConnectionFactory.createLocalConnections(new NodeId(0L), new NodeId(1)); + ConnectionFactory.createLocalConnections(NodeId.of(0L), NodeId.of(1)); myConnection = connections.left(); theirConnection = connections.right(); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java index 3118aa4b517f..153ffe534013 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/handshake/VersionHandshakeTests.java @@ -66,7 +66,7 @@ void setup() throws ConstructableRegistryException, IOException { registry.registerConstructable(new ClassConstructorPair(SerializableLong.class, SerializableLong::new)); final Pair connections = - ConnectionFactory.createLocalConnections(new NodeId(0L), new NodeId(1)); + ConnectionFactory.createLocalConnections(NodeId.of(0L), NodeId.of(1)); myConnection = connections.left(); theirConnection = connections.right(); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorMultithreadedTest.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorMultithreadedTest.java index 472636ccb0d3..88a20ce55767 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorMultithreadedTest.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorMultithreadedTest.java @@ -47,7 +47,7 @@ void runProtocol() throws Exception { @Test void keepalive() throws Exception { final Pair connections = - ConnectionFactory.createLocalConnections(new NodeId(0L), new NodeId(1)); + ConnectionFactory.createLocalConnections(NodeId.of(0L), NodeId.of(1)); final NegotiatorPair pair = new NegotiatorPair( new TestProtocol().setShouldInitiate(false), Pair.of(new ExpiringConnection(connections.left(), 1), new ExpiringConnection(connections.right(), 1))); diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorPair.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorPair.java index 8db06772b236..1431a82e10cf 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorPair.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/network/communication/multithreaded/NegotiatorPair.java @@ -39,7 +39,7 @@ public NegotiatorPair(final TestProtocol protocol, final Pair getPartitionNodes( (averageWeight + random.nextGaussian() * standardDeviationWeight))); nodes.add(new NodeToAdd(nextNodeId, nextNodeWeight, partition.hash)); - nextNodeId = new NodeId(nextNodeId.id() + 1); + nextNodeId = NodeId.of(nextNodeId.id() + 1); remainingWeight -= nextNodeWeight; } } @@ -111,11 +111,11 @@ private List getNodes( final List partitions) { final List nodes = new ArrayList<>(); - NodeId nextNodeId = new NodeId(0); + NodeId nextNodeId = NodeId.of(0); for (final PartitionDescription partition : partitions) { final List partitionNodes = getPartitionNodes(random, averageWeight, standardDeviationWeight, nextNodeId, partition); - nextNodeId = new NodeId(nextNodeId.id() + partitionNodes.size()); + nextNodeId = NodeId.of(nextNodeId.id() + partitionNodes.size()); nodes.addAll(partitionNodes); } return nodes; @@ -137,7 +137,7 @@ void singlePartitionTest(final long totalWeight) { assertEquals(0, hashFinder.getPartitionMap().size(), "there shouldn't be any partitions yet"); // Add weight up until >1/2, but as soon as we meet or exceed 1/2 exit the loop - NodeId nextNodeId = new NodeId(0L); + NodeId nextNodeId = NodeId.of(0L); while (!MAJORITY.isSatisfiedBy(hashFinder.getHashReportedWeight(), totalWeight)) { assertEquals(UNDECIDED, hashFinder.getStatus(), "status should not yet be decided"); @@ -154,7 +154,7 @@ void singlePartitionTest(final long totalWeight) { hashFinder.addHash(nextNodeId, nextNodeWeight, randomHash(random)); assertEquals(currentAccumulatedWeight, hashFinder.getHashReportedWeight(), "duplicates should be no-ops"); - nextNodeId = new NodeId(nextNodeId.id() + 1L); + nextNodeId = NodeId.of(nextNodeId.id() + 1L); assertEquals(1, hashFinder.getPartitionMap().size(), "there should only be 1 partition"); assertTrue(hashFinder.getPartitionMap().containsKey(hash), "invalid partition map"); @@ -163,7 +163,7 @@ void singlePartitionTest(final long totalWeight) { hashFinder.getPartitionMap().get(hash).getNodes().size(), "incorrect partition size"); assertTrue( - hashFinder.getPartitionMap().get(hash).getNodes().contains(new NodeId(nextNodeId.id() - 1)), + hashFinder.getPartitionMap().get(hash).getNodes().contains(NodeId.of(nextNodeId.id() - 1)), "could not find node that was just added"); } @@ -322,14 +322,14 @@ void earlyIssDetectionTest(final long totalWeight) { smallPartitions.add(new PartitionDescription(randomHash(random), partitionWeight)); } - NodeId nextNodeId = new NodeId(0); + NodeId nextNodeId = NodeId.of(0); final List nodes = new ArrayList<>(); // Add the nodes from the small partitions for (final PartitionDescription partition : smallPartitions) { final List partitionNodes = getPartitionNodes(random, averageWeight, standardDeviationWeight, nextNodeId, partition); - nextNodeId = new NodeId(nextNodeId.id() + partitionNodes.size()); + nextNodeId = NodeId.of(nextNodeId.id() + partitionNodes.size()); nodes.addAll(partitionNodes); } @@ -373,14 +373,14 @@ void completePartitionIsLastTest(final long totalWeight) { smallPartitions.add(new PartitionDescription(randomHash(random), partitionWeight)); } - NodeId nextNodeId = new NodeId(0); + NodeId nextNodeId = NodeId.of(0); final List nodes = new ArrayList<>(); // Add the nodes from the small partitions for (final PartitionDescription partition : smallPartitions) { final List partitionNodes = getPartitionNodes(random, averageWeight, standardDeviationWeight, nextNodeId, partition); - nextNodeId = new NodeId(nextNodeId.id() + partitionNodes.size()); + nextNodeId = NodeId.of(nextNodeId.id() + partitionNodes.size()); nodes.addAll(partitionNodes); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java index 51c1ee6d9058..e39406d869ff 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/state/IssMetricsTests.java @@ -49,7 +49,7 @@ void updateNonExistentNode() { assertThrows( IllegalArgumentException.class, () -> issMetrics.stateHashValidityObserver( - 0L, new NodeId(Integer.MAX_VALUE), randomHash(), randomHash()), + 0L, NodeId.of(Integer.MAX_VALUE), randomHash(), randomHash()), "should not be able to update stats for non-existent node"); } diff --git a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncNode.java b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncNode.java index 70fbda56e0c6..a1affa4756bd 100644 --- a/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncNode.java +++ b/platform-sdk/swirlds-unit-tests/core/swirlds-platform-test/src/test/java/com/swirlds/platform/test/sync/SyncNode.java @@ -120,7 +120,7 @@ public SyncNode( this.ancientMode = Objects.requireNonNull(ancientMode); this.numNodes = numNodes; - this.nodeId = new NodeId(nodeId); + this.nodeId = NodeId.of(nodeId); this.eventEmitter = eventEmitter; syncManager = new TestingSyncManager(); diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/AbstractHashListener.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/AbstractHashListener.java index 0d465996b845..5233fc0656aa 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/AbstractHashListener.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/AbstractHashListener.java @@ -16,6 +16,8 @@ package com.swirlds.virtualmap.internal.merkle; +import static com.swirlds.logging.legacy.LogMarker.VIRTUAL_MERKLE_STATS; + import com.swirlds.common.config.singleton.ConfigurationHolder; import com.swirlds.common.crypto.Hash; import com.swirlds.virtualmap.VirtualKey; @@ -37,6 +39,8 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * The hashing algorithm in the {@link com.swirlds.virtualmap.internal.hash.VirtualHasher} is setup to @@ -67,6 +71,8 @@ public abstract class AbstractHashListener implements VirtualHashListener { + private static final Logger logger = LogManager.getLogger(AbstractHashListener.class); + private final KeySerializer keySerializer; private final ValueSerializer valueSerializer; private final VirtualDataSource dataSource; @@ -83,6 +89,8 @@ public abstract class AbstractHashListener keySerializer, final ValueSerializer valueSerializer, - final VirtualDataSource dataSource) { + @NonNull final VirtualDataSource dataSource, + @NonNull final VirtualMapStatistics statistics) { if (firstLeafPath != Path.INVALID_PATH && !(firstLeafPath > 0 && firstLeafPath <= lastLeafPath)) { throw new IllegalArgumentException("The first leaf path is invalid. firstLeafPath=" + firstLeafPath @@ -119,6 +130,7 @@ protected AbstractHashListener( this.keySerializer = Objects.requireNonNull(keySerializer); this.valueSerializer = Objects.requireNonNull(valueSerializer); this.dataSource = Objects.requireNonNull(dataSource); + this.statistics = Objects.requireNonNull(statistics); } @Override @@ -186,7 +198,13 @@ private void flush( @NonNull final List> leavesToFlush) { assert flushInProgress.get() : "Flush in progress flag must be set"; try { + logger.debug( + VIRTUAL_MERKLE_STATS.getMarker(), + "Flushing {} hashes and {} leaves", + hashesToFlush.size(), + leavesToFlush.size()); // flush it down + final long start = System.currentTimeMillis(); try { dataSource.saveRecords( firstLeafPath, @@ -195,6 +213,9 @@ private void flush( leavesToFlush.stream().map(r -> r.toBytes(keySerializer, valueSerializer)), findLeavesToRemove().map(r -> r.toBytes(keySerializer, valueSerializer)), true); + final long end = System.currentTimeMillis(); + statistics.recordFlush(end - start); + logger.debug(VIRTUAL_MERKLE_STATS.getMarker(), "Flushed in {} ms", end - start); } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/FullLeafRehashHashListener.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/FullLeafRehashHashListener.java index c76063e8686d..6c814be4a4e8 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/FullLeafRehashHashListener.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/FullLeafRehashHashListener.java @@ -23,6 +23,7 @@ import com.swirlds.virtualmap.internal.hash.VirtualHashListener; import com.swirlds.virtualmap.serialize.KeySerializer; import com.swirlds.virtualmap.serialize.ValueSerializer; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.stream.Stream; /** @@ -64,14 +65,17 @@ public class FullLeafRehashHashListener keySerializer, final ValueSerializer valueSerializer, - final VirtualDataSource dataSource) { - super(firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource); + @NonNull final VirtualDataSource dataSource, + @NonNull final VirtualMapStatistics statistics) { + super(firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource, statistics); } /** diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/VirtualRootNode.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/VirtualRootNode.java index 358c725af0d4..95b84d9e15f0 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/VirtualRootNode.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/merkle/VirtualRootNode.java @@ -500,7 +500,7 @@ public void fullLeafRehashIfNecessary() { lastLeafPath, getRoute()); final FullLeafRehashHashListener hashListener = new FullLeafRehashHashListener<>( - firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource); + firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource, statistics); // This background thread will be responsible for hashing the tree and sending the // data to the hash listener to flush. @@ -1602,6 +1602,7 @@ public void prepareReconnectHashing(final long firstLeafPath, final long lastLea keySerializer, valueSerializer, reconnectRecords.getDataSource(), + statistics, nodeRemover); // This background thread will be responsible for hashing the tree and sending the diff --git a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListener.java b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListener.java index 34f527137f90..9af88e9b217e 100644 --- a/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListener.java +++ b/platform-sdk/swirlds-virtualmap/src/main/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListener.java @@ -22,8 +22,11 @@ import com.swirlds.virtualmap.datasource.VirtualLeafRecord; import com.swirlds.virtualmap.internal.hash.VirtualHashListener; import com.swirlds.virtualmap.internal.merkle.AbstractHashListener; +import com.swirlds.virtualmap.internal.merkle.VirtualMapStatistics; import com.swirlds.virtualmap.serialize.KeySerializer; import com.swirlds.virtualmap.serialize.ValueSerializer; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; import java.util.stream.Stream; /** @@ -65,16 +68,19 @@ public class ReconnectHashListener * The last leaf path. Must be a valid path. * @param dataSource * The data source. Cannot be null. + * @param statistics + * Virtual map stats. Cannot be null. */ public ReconnectHashListener( final long firstLeafPath, final long lastLeafPath, final KeySerializer keySerializer, final ValueSerializer valueSerializer, - final VirtualDataSource dataSource, - final ReconnectNodeRemover nodeRemover) { - super(firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource); - this.nodeRemover = nodeRemover; + @NonNull final VirtualDataSource dataSource, + @NonNull final VirtualMapStatistics statistics, + @NonNull final ReconnectNodeRemover nodeRemover) { + super(firstLeafPath, lastLeafPath, keySerializer, valueSerializer, dataSource, statistics); + this.nodeRemover = Objects.requireNonNull(nodeRemover); } /** diff --git a/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListenerTest.java b/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListenerTest.java index 9a6e978dbf9c..550926bb1fad 100644 --- a/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListenerTest.java +++ b/platform-sdk/swirlds-virtualmap/src/test/java/com/swirlds/virtualmap/internal/reconnect/ReconnectHashListenerTest.java @@ -31,6 +31,7 @@ import com.swirlds.virtualmap.datasource.VirtualLeafBytes; import com.swirlds.virtualmap.datasource.VirtualLeafRecord; import com.swirlds.virtualmap.internal.hash.VirtualHasher; +import com.swirlds.virtualmap.internal.merkle.VirtualMapStatistics; import com.swirlds.virtualmap.serialize.KeySerializer; import com.swirlds.virtualmap.serialize.ValueSerializer; import com.swirlds.virtualmap.test.fixtures.InMemoryBuilder; @@ -61,13 +62,39 @@ class ReconnectHashListenerTest { @Test @DisplayName("Null datasource throws") void nullDataSourceThrows() { + final VirtualMapStatistics statistics = mock(VirtualMapStatistics.class); + final ReconnectNodeRemover nodeRemover = mock(ReconnectNodeRemover.class); assertThrows( NullPointerException.class, - () -> new ReconnectHashListener( - 1, 1, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, null, null), + () -> new ReconnectHashListener<>( + 1, 1, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, null, statistics, nodeRemover), "A null data source should produce an NPE"); } + @Test + @DisplayName("Null statistics throws") + void nullStatisticsThrows() { + final VirtualDataSource ds = new InMemoryBuilder().build("nullStatisticsThrows", false); + final ReconnectNodeRemover nodeRemover = mock(ReconnectNodeRemover.class); + assertThrows( + NullPointerException.class, + () -> new ReconnectHashListener<>( + 1, 1, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, null, nodeRemover), + "A null statistics should produce an NPE"); + } + + @Test + @DisplayName("Null node remover throws") + void nullNodeRemoverThrows() { + final VirtualDataSource ds = new InMemoryBuilder().build("nullStatisticsThrows", false); + final VirtualMapStatistics statistics = mock(VirtualMapStatistics.class); + assertThrows( + NullPointerException.class, + () -> new ReconnectHashListener<>( + 1, 1, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, statistics, null), + "A null node remover should produce an NPE"); + } + // Future: We should also check for first/last leaf path being equal when not 1. That really should never happen. // That check should be laced through everything, including VirtualMapState. @ParameterizedTest @@ -81,7 +108,9 @@ void nullDataSourceThrows() { }) // Invalid (both should be equal only if == 1 @DisplayName("Illegal first and last leaf path combinations throw") void badLeafPaths(long firstLeafPath, long lastLeafPath) { - final VirtualDataSource ds = new InMemoryBuilder().build("badLeafPaths", true); + final VirtualDataSource ds = new InMemoryBuilder().build("badLeafPaths", false); + final VirtualMapStatistics statistics = mock(VirtualMapStatistics.class); + final ReconnectNodeRemover nodeRemover = mock(ReconnectNodeRemover.class); assertThrows( IllegalArgumentException.class, () -> new ReconnectHashListener<>( @@ -90,7 +119,8 @@ void badLeafPaths(long firstLeafPath, long lastLeafPath) { TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, - null), + statistics, + nodeRemover), "Should have thrown IllegalArgumentException"); } @@ -99,9 +129,17 @@ void badLeafPaths(long firstLeafPath, long lastLeafPath) { @DisplayName("Valid configurations create an instance") void goodLeafPaths(long firstLeafPath, long lastLeafPath) { final VirtualDataSource ds = new InMemoryBuilder().build("goodLeafPaths", true); + final VirtualMapStatistics statistics = mock(VirtualMapStatistics.class); + final ReconnectNodeRemover nodeRemover = mock(ReconnectNodeRemover.class); try { new ReconnectHashListener<>( - firstLeafPath, lastLeafPath, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, null); + firstLeafPath, + lastLeafPath, + TestKeySerializer.INSTANCE, + TestValueSerializer.INSTANCE, + ds, + statistics, + nodeRemover); } catch (Exception e) { fail("Should have been able to create the instance", e); } @@ -114,12 +152,13 @@ void goodLeafPaths(long firstLeafPath, long lastLeafPath) { void flushOrder(int size) { final VirtualDataSourceSpy ds = new VirtualDataSourceSpy(new InMemoryBuilder().build("flushOrder", true)); - final ReconnectNodeRemover remover = mock(ReconnectNodeRemover.class); + final VirtualMapStatistics statistics = mock(VirtualMapStatistics.class); + final ReconnectNodeRemover nodeRemover = mock(ReconnectNodeRemover.class); // 100 leaves would have firstLeafPath = 99, lastLeafPath = 198 final long last = size + size; final ReconnectHashListener listener = new ReconnectHashListener<>( - size, last, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, remover); + size, last, TestKeySerializer.INSTANCE, TestValueSerializer.INSTANCE, ds, statistics, nodeRemover); final VirtualHasher hasher = new VirtualHasher<>(); hasher.hash( this::hash, LongStream.range(size, last).mapToObj(this::leaf).iterator(), size, last, listener); diff --git a/settings.gradle.kts b/settings.gradle.kts index 83f80f05bf1e..38b28cd7d8ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,8 @@ javaModules { module("swirlds") // not actually a Module as it has no module-info.java module("swirlds-benchmarks") // not actually a Module as it has no module-info.java module("swirlds-unit-tests/core/swirlds-platform-test") // nested module is not found automatically + module("consensus-gossip") { artifact = "consensus-gossip" } + module("consensus-gossip-impl") { artifact = "consensus-gossip-impl" } } // The Hedera services modules